diff --git a/apps/readest-app/src/__tests__/services/transformers/transformers.test.ts b/apps/readest-app/src/__tests__/services/transformers/transformers.test.ts index 3961506e..4fa4567f 100644 --- a/apps/readest-app/src/__tests__/services/transformers/transformers.test.ts +++ b/apps/readest-app/src/__tests__/services/transformers/transformers.test.ts @@ -20,6 +20,7 @@ vi.mock('@/utils/lang', () => ({ getLanguageInfo: vi.fn(() => ({ direction: 'ltr' })), isSameLang: vi.fn(() => true), isValidLang: vi.fn(() => true), + normalizedLangCode: (lang?: string | null) => (lang ? lang.split('-')[0]!.toLowerCase() : ''), })); vi.mock('@/store/settingsStore', () => ({ @@ -769,6 +770,140 @@ describe('simpleccTransformer', () => { }); }); +// ============================================================================= +// nbspTransformer +// ============================================================================= + +describe('nbspTransformer', () => { + let nbspTransformer: typeof import('@/services/transformers/nbsp').nbspTransformer; + + beforeEach(async () => { + ({ nbspTransformer } = await import('@/services/transformers/nbsp')); + }); + + test('has the correct name', () => { + expect(nbspTransformer.name).toBe('nbsp'); + }); + + test('returns content unchanged for unsupported languages', async () => { + const html = '
в доме
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'en' }), + ); + expect(result).toBe(html); + }); + + describe('for Russian books', () => { + const NBSP = '\u00A0'; + + test('glues a single-letter preposition to the next word', async () => { + const html = 'книга в доме
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toContain(`в${NBSP}доме`); + expect(result).not.toContain('в доме'); + }); + + test('glues a two-letter conjunction', async () => { + const html = 'тепло но ветрено
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toContain(`но${NBSP}ветрено`); + }); + + test('glues multi-letter function words from the list', async () => { + const html = 'только если хотя дом
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru-RU' }), + ); + expect(result).toContain(`только${NBSP}если${NBSP}хотя${NBSP}дом`); + }); + + test('handles capitalized words at the start of a sentence', async () => { + const html = 'Но что делать
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toContain(`Но${NBSP}что${NBSP}делать`); + }); + + test('glues consecutive short words', async () => { + const html = 'и в доме
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toContain(`и${NBSP}в${NBSP}доме`); + }); + + test('does not split a longer word that begins with a short word', async () => { + const html = 'в наказание
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toContain(`в${NBSP}наказание`); + // "на" inside "наказание" must stay intact (no NBSP injected mid-word) + expect(result).not.toContain(`на${NBSP}`); + }); + + test('leaves text without short function words unchanged', async () => { + const html = 'наш дом большой
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toBe(html); + }); + + test('does not modify attribute values, only text nodes', async () => { + const html = 'в доме
'; + const result = await nbspTransformer.transform( + makeCtx({ content: html, primaryLanguage: 'ru' }), + ); + expect(result).toContain('title="это и то"'); + expect(result).toContain(`в${NBSP}доме`); + }); + + test('skipsв доме