// 語言與全角/開明政策(架構 §4.3)。 // // 解析節點之有效 lang,歸類,並依政策決定標點寬度(全角式/開明式/半角式)。 // CSS 端以 :lang() 落實同一政策(I2/I3);本模組供 JS pass 在需要時查詢。 /** 標點寬度政策。 */ export type PunctStyle = "quanjiao" | "kaiming" | "banjiao"; /** 語言分類。 */ export type LangClass = "zh-Hans" | "zh-Hant" | "ja" | "other"; /** 按選擇器逐元素覆寫標點樣式之規則(§4.3;如非段落/UI 元素用半角式)。 */ export interface StyleRule { selector: string; style: PunctStyle; } export interface LocaleConfig { /** 無 lang 祖先時之預設語言。 */ default: string; /** 全域標點擠壓預設(全角/開明/半角)。設則覆寫內建之逐語言預設, * 但仍可被 policy 之逐 lang 設定覆寫。未設則回退內建預設。 */ style?: PunctStyle; /** 語言分類 → 標點寬度政策之逐 lang 覆寫。 */ policy: Partial>; /** 按選擇器**逐元素**覆寫標點樣式(優先於 lang;與 `data-jz-style` 屬性同一機制)。 * 近祖先勝、同層屬性優先於選擇器。空/未設則不覆寫、純由 lang 決定。 */ stylePolicy?: StyleRule[]; } const DEFAULT_POLICY: Record = { "zh-Hant": "quanjiao", "zh-Hans": "kaiming", "ja": "quanjiao", "other": "quanjiao", }; /** * 由節點向上找最近帶 lang 屬性之祖先,回傳小寫值;無則回傳 fallback。 */ export function effectiveLang(node: Node, fallback: string): string { let p: Node | null = node.parentNode; while (p && p.nodeType === 1) { const el = p as Element; const v = el.getAttribute("lang"); if (v) { return v.toLowerCase(); } p = p.parentNode; } return (fallback || "").toLowerCase(); } /** * 將 BCP-47 語言標籤歸類。前綴以子標籤邊界比對('zh-tw' 命中 zh-Hant, * 'zhx' 不命中 zh)。香港/台灣繁體歸 zh-Hant;含 Hant/Hans 子標籤者優先。 */ export function classify(lang: string): LangClass { const l = (lang || "").toLowerCase(); if (l === "ja" || l.indexOf("ja-") === 0) { return "ja"; } if (l === "zh" || l.indexOf("zh-") === 0) { // 顯式子標籤優先。 if (l.indexOf("hant") >= 0) { return "zh-Hant"; } if (l.indexOf("hans") >= 0) { return "zh-Hans"; } // 地區推斷:台灣/香港/澳門 → 繁體;其餘(含 cn/sg)→ 簡體。 if (/-(tw|hk|mo)\b/.test(l)) { return "zh-Hant"; } if (l === "zh") { return "zh-Hant"; } return "zh-Hans"; } return "other"; } /** * 回傳該語言分類在給定 config 下之標點寬度政策。優先序: * 逐 lang policy > 全域 style > 內建逐語言預設 DEFAULT_POLICY。 */ export function styleFor(langClass: LangClass, config: LocaleConfig): PunctStyle { const perLang = config.policy ? config.policy[langClass] : undefined; if (perLang) { return perLang; } if (config.style) { return config.style; } return DEFAULT_POLICY[langClass]; } /** 該字串是否為合法標點樣式。 */ function isPunctStyle(v: string | null): v is PunctStyle { return v === "quanjiao" || v === "kaiming" || v === "banjiao"; } /** * 逐元素之標點樣式覆寫(§4.3):自 node 向上找**最近**祖先(含自身)之樣式信號—— * `data-jz-style` 屬性(值須為合法樣式,優先),或 `stylePolicy` 選擇器命中——回傳其樣式; * 皆無則 null(回退 lang 決定)。近祖先勝,同層屬性優先於選擇器。 */ function resolveStyleOverride(node: Node, stylePolicy?: StyleRule[]): PunctStyle | null { let el: Element | null = node.nodeType === 1 ? (node as Element) : node.parentElement; while (el) { const attr = el.getAttribute ? el.getAttribute("data-jz-style") : null; if (isPunctStyle(attr)) { return attr; } if (stylePolicy) { for (const rule of stylePolicy) { if (rule.selector && el.matches && el.matches(rule.selector)) { return rule.style; } } } el = el.parentElement; } return null; } /** * 解析節點之有效標點樣式。優先序:**逐元素覆寫**(`data-jz-style` 屬性/`stylePolicy` * 選擇器,近祖先勝)> 逐 lang `policy` > 全域 `style` > 內建逐語言預設。 */ export function resolveStyle(node: Node, config: LocaleConfig): PunctStyle { const override = resolveStyleOverride(node, config.stylePolicy); if (override) { return override; } const lang = effectiveLang(node, config.default); return styleFor(classify(lang), config); }