From 33ef781e21960177d8f0c326639adad8e5954b61 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 1 Dec 2025 16:27:22 +0800 Subject: [PATCH] feat: also convert quotation marks between Chinese variants (#2578) --- .../reader/components/annotator/Annotator.tsx | 9 ++- .../src/services/transformers/punctuation.ts | 65 ++++++++++++++----- .../src/services/transformers/simplecc.ts | 4 +- apps/readest-app/src/utils/simplecc.ts | 19 +++++- apps/readest-app/src/utils/toc.ts | 4 +- 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index bfdc08bc..af04f258 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -27,6 +27,7 @@ import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/ import { eventDispatcher } from '@/utils/event'; import { findTocItemBS } from '@/utils/toc'; import { throttle } from '@/utils/throttle'; +import { runSimpleCC } from '@/utils/simplecc'; import { HIGHLIGHT_COLOR_HEX } from '@/services/constants'; import AnnotationPopup from './AnnotationPopup'; import WiktionaryPopup from './WiktionaryPopup'; @@ -485,7 +486,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const handleSearch = () => { if (!selection || !selection.text) return; handleDismissPopupAndSelection(); - eventDispatcher.dispatch('search', { term: selection.text }); + + let term = selection.text; + const convertChineseVariant = viewSettings.convertChineseVariant; + if (convertChineseVariant && convertChineseVariant !== 'none') { + term = runSimpleCC(term, convertChineseVariant, true); + } + eventDispatcher.dispatch('search', { term }); }; const handleDictionary = () => { diff --git a/apps/readest-app/src/services/transformers/punctuation.ts b/apps/readest-app/src/services/transformers/punctuation.ts index b23cc8d4..c643adfa 100644 --- a/apps/readest-app/src/services/transformers/punctuation.ts +++ b/apps/readest-app/src/services/transformers/punctuation.ts @@ -1,40 +1,73 @@ import type { Transformer } from './types'; -const punctuationMapHans: Record = { +const verticalQuotationsMapHans: Record = { '“': '﹃', '”': '﹄', '‘': '﹁', '’': '﹂', + '「': '﹁', + '」': '﹂', + '『': '﹃', + '』': '﹄', }; -const punctuationMapHant: Record = { +const verticalQuotationsMapHant: Record = { '“': '﹁', '”': '﹂', '‘': '﹃', '’': '﹄', + '「': '﹁', + '」': '﹂', + '『': '﹃', + '』': '﹄', +}; + +const quotationsMapHans2Hant = { + '“': '「', + '”': '」', + '‘': '『', + '’': '』', + '﹃': '﹁', + '﹄': '﹂', + '﹁': '﹃', + '﹂': '﹄', }; export const punctuationTransformer: Transformer = { name: 'punctuation', transform: async (ctx) => { - const shouldTransform = ctx.viewSettings.vertical && ctx.viewSettings.replaceQuotationMarks; - if (!shouldTransform) return ctx.content; + if (!ctx.viewSettings.replaceQuotationMarks) return ctx.content; let result = ctx.content; - const traditionalChineseLocales = ['zh-Hant', 'zh-TW', 'zh_TW']; - let punctuationMap: Record = punctuationMapHans; - if ( - traditionalChineseLocales.includes(ctx.primaryLanguage || '') || - traditionalChineseLocales.includes(ctx.userLocale) - ) { - punctuationMap = punctuationMapHant; + + const convertChineseVariant = ctx.viewSettings.convertChineseVariant; + const reversePunctuationTransform = ctx.reversePunctuationTransform || false; + const shouldConvertChineseVariants = convertChineseVariant && convertChineseVariant !== 'none'; + if (shouldConvertChineseVariants) { + for (const [s, t] of Object.entries(quotationsMapHans2Hant)) { + const shouldReverse = reversePunctuationTransform !== convertChineseVariant.includes('2s'); + const [from, to] = shouldReverse ? [t, s] : [s, t]; + result = result.replace(new RegExp(from, 'g'), to); + } } - for (const [original, vertical] of Object.entries(punctuationMap)) { - if (ctx.reversePunctuationTransform) { - result = result.replace(new RegExp(vertical, 'g'), original); - } else { - result = result.replace(new RegExp(original, 'g'), vertical); + + const shouldTransformVertical = ctx.viewSettings.vertical; + if (shouldTransformVertical) { + const traditionalChineseLocales = ['zh-Hant', 'zh-TW', 'zh_TW']; + let punctuationMap: Record = verticalQuotationsMapHans; + if ( + traditionalChineseLocales.includes(ctx.primaryLanguage || '') || + traditionalChineseLocales.includes(ctx.userLocale) + ) { + punctuationMap = verticalQuotationsMapHant; + } + for (const [original, vertical] of Object.entries(punctuationMap)) { + if (ctx.reversePunctuationTransform) { + result = result.replace(new RegExp(vertical, 'g'), original); + } else { + result = result.replace(new RegExp(original, 'g'), vertical); + } } } diff --git a/apps/readest-app/src/services/transformers/simplecc.ts b/apps/readest-app/src/services/transformers/simplecc.ts index 761f5b71..5645174c 100644 --- a/apps/readest-app/src/services/transformers/simplecc.ts +++ b/apps/readest-app/src/services/transformers/simplecc.ts @@ -1,5 +1,5 @@ import type { Transformer } from './types'; -import { simplecc, initSimpleCC } from '@/utils/simplecc'; +import { initSimpleCC, runSimpleCC } from '@/utils/simplecc'; export const simpleccTransformer: Transformer = { name: 'simplecc', @@ -36,7 +36,7 @@ export const simpleccTransformer: Transformer = { for (const textNode of textNodes) { if (textNode.textContent) { - textNode.textContent = simplecc(textNode.textContent, convertChineseVariant); + textNode.textContent = runSimpleCC(textNode.textContent, convertChineseVariant); } } diff --git a/apps/readest-app/src/utils/simplecc.ts b/apps/readest-app/src/utils/simplecc.ts index d6f722ff..f0608732 100644 --- a/apps/readest-app/src/utils/simplecc.ts +++ b/apps/readest-app/src/utils/simplecc.ts @@ -1,4 +1,5 @@ import init, { simplecc } from '@simplecc/simplecc_wasm'; +import { ConvertChineseVariant } from '@/types/book'; let initialized = false; @@ -9,4 +10,20 @@ const initSimpleCC = async () => { initialized = true; }; -export { simplecc, initSimpleCC }; +const convertReverseMap: Record = { + none: 'none', + s2t: 't2s', + t2s: 's2t', + s2tw: 'tw2s', + s2hk: 'hk2s', + s2twp: 'tw2sp', + tw2s: 's2tw', + hk2s: 's2hk', + tw2sp: 's2twp', +}; + +const runSimpleCC = (text: string, variant: ConvertChineseVariant, reverse = false): string => { + return reverse ? simplecc(text, convertReverseMap[variant]) : simplecc(text, variant); +}; + +export { initSimpleCC, runSimpleCC }; diff --git a/apps/readest-app/src/utils/toc.ts b/apps/readest-app/src/utils/toc.ts index 22e0e164..a0f3bce7 100644 --- a/apps/readest-app/src/utils/toc.ts +++ b/apps/readest-app/src/utils/toc.ts @@ -1,6 +1,6 @@ import { ConvertChineseVariant } from '@/types/book'; import { SectionItem, TOCItem, CFI, BookDoc } from '@/libs/document'; -import { simplecc, initSimpleCC } from '@/utils/simplecc'; +import { initSimpleCC, runSimpleCC } from '@/utils/simplecc'; export const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => { for (const item of toc) { @@ -87,7 +87,7 @@ export const updateToc = async ( const convertTocLabels = (items: TOCItem[], convertChineseVariant: ConvertChineseVariant) => { items.forEach((item) => { if (item.label) { - item.label = simplecc(item.label, convertChineseVariant); + item.label = runSimpleCC(item.label, convertChineseVariant); } if (item.subitems) { convertTocLabels(item.subitems, convertChineseVariant);