From 96cc182a8c0734fb5c82067f11971e7eac8c1712 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 14 Oct 2025 19:09:23 +0800 Subject: [PATCH] feat: support overriding html language code with language code in metadata, closes #2222 (#2227) --- .../app/reader/components/FoliateViewer.tsx | 9 +++++--- .../src/services/transformers/index.ts | 2 ++ .../src/services/transformers/language.ts | 8 ++++--- .../src/services/transformers/types.ts | 1 + .../src/services/transformers/whitespace.ts | 21 +++++++++++++++++++ 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 apps/readest-app/src/services/transformers/whitespace.ts diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 59989549..bc19a631 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -43,6 +43,7 @@ import { getMaxInlineSize } from '@/utils/config'; import { getDirFromUILanguage } from '@/utils/rtl'; import { isCJKLang } from '@/utils/lang'; import { isTauriAppPlatform } from '@/services/environment'; +import { TransformContext } from '@/services/transformers/types'; import { transformContent } from '@/services/transformService'; import { lockScreenOrientation } from '@/utils/bridge'; import { useTextTranslation } from '../hooks/useTextTranslation'; @@ -118,15 +119,17 @@ const FoliateViewer: React.FC<{ detail.data = Promise.resolve(detail.data) .then((data) => { const viewSettings = getViewSettings(bookKey); + const bookData = getBookData(bookKey); if (viewSettings && detail.type === 'text/css') return transformStylesheet(width, height, data); - if (viewSettings && detail.type === 'application/xhtml+xml') { + if (viewSettings && bookData && detail.type === 'application/xhtml+xml') { const ctx = { bookKey, viewSettings, + primaryLanguage: bookData.book?.primaryLanguage, content: data, - transformers: ['punctuation', 'footnote', 'language'], - }; + transformers: ['punctuation', 'footnote', 'whitespace', 'language'], + } as TransformContext; return Promise.resolve(transformContent(ctx)); } return data; diff --git a/apps/readest-app/src/services/transformers/index.ts b/apps/readest-app/src/services/transformers/index.ts index 71eba61d..a4a6bf91 100644 --- a/apps/readest-app/src/services/transformers/index.ts +++ b/apps/readest-app/src/services/transformers/index.ts @@ -2,10 +2,12 @@ import type { Transformer } from './types'; import { footnoteTransformer } from './footnote'; import { languageTransformer } from './language'; import { punctuationTransformer } from './punctuation'; +import { whitespaceTransformer } from './whitespace'; export const availableTransformers: Transformer[] = [ punctuationTransformer, footnoteTransformer, languageTransformer, + whitespaceTransformer, // Add more transformers here ]; diff --git a/apps/readest-app/src/services/transformers/language.ts b/apps/readest-app/src/services/transformers/language.ts index dc0c1ee7..f3b578da 100644 --- a/apps/readest-app/src/services/transformers/language.ts +++ b/apps/readest-app/src/services/transformers/language.ts @@ -1,10 +1,11 @@ -import { detectLanguage, isValidLang } from '@/utils/lang'; +import { detectLanguage, isSameLang, isValidLang } from '@/utils/lang'; import type { Transformer } from './types'; export const languageTransformer: Transformer = { name: 'language', transform: async (ctx) => { + const primaryLanguage = ctx.primaryLanguage; let result = ctx.content; const attrsMatch = result.match(/]*)>/i); if (attrsMatch) { @@ -13,9 +14,10 @@ export const languageTransformer: Transformer = { const xmlLangRegex = / xml:lang="([^"]*)"/i; const xmlLangMatch = attrs.match(xmlLangRegex); const langMatch = attrs.match(langRegex); - if (!isValidLang(langMatch?.[1] || xmlLangMatch?.[1])) { + const docLang = langMatch?.[1] || xmlLangMatch?.[1]; + if (!isValidLang(docLang) || !isSameLang(docLang, primaryLanguage)) { const mainContent = result.replace(/<[^>]+>/g, ' '); - const lang = detectLanguage(mainContent); + const lang = isValidLang(primaryLanguage) ? primaryLanguage : detectLanguage(mainContent); const newLangAttr = ` lang="${lang}"`; const newXmlLangAttr = ` xml:lang="${lang}"`; attrs = langMatch ? attrs.replace(langRegex, newLangAttr) : attrs + newLangAttr; diff --git a/apps/readest-app/src/services/transformers/types.ts b/apps/readest-app/src/services/transformers/types.ts index 16473e08..c48109ef 100644 --- a/apps/readest-app/src/services/transformers/types.ts +++ b/apps/readest-app/src/services/transformers/types.ts @@ -3,6 +3,7 @@ import { ViewSettings } from '@/types/book'; export type TransformContext = { bookKey: string; viewSettings: ViewSettings; + primaryLanguage?: string; content: string; transformers: string[]; reversePunctuationTransform?: boolean; diff --git a/apps/readest-app/src/services/transformers/whitespace.ts b/apps/readest-app/src/services/transformers/whitespace.ts new file mode 100644 index 00000000..62eb3789 --- /dev/null +++ b/apps/readest-app/src/services/transformers/whitespace.ts @@ -0,0 +1,21 @@ +import type { Transformer } from './types'; + +export const whitespaceTransformer: Transformer = { + name: 'whitespace', + + transform: async (ctx) => { + const viewSettings = ctx.viewSettings; + if (viewSettings.overrideLayout) { + const cleaned = ctx.content + // Replace   but skip literal "&nbsp;" + .replace(/(?