Files
cjk-autospace/src/core/locale.ts
T

133 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 語言與全角/開明政策(架構 §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<Record<LangClass, PunctStyle>>;
/** 按選擇器**逐元素**覆寫標點樣式(優先於 lang;與 `data-jz-style` 屬性同一機制)。
* 近祖先勝、同層屬性優先於選擇器。空/未設則不覆寫、純由 lang 決定。 */
stylePolicy?: StyleRule[];
}
const DEFAULT_POLICY: Record<LangClass, PunctStyle> = {
"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);
}