133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
// 語言與全角/開明政策(架構 §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);
|
||
}
|