feat: 聚珍切片 1 — 中西間隙/標點擠壓/禁則 + v1 相容層
從零重寫之第一垂直切片(依 ARCHITECTURE.md 契約),涵蓋並超越 v1 全部功能。 核心基座(src/core):unicode 字元表+標點子類+字形側位;locale 語言與 全角/開明政策(全域 lang.style +逐 lang policy);dom(jz-* 工廠+可還原 標記);finder 遍歷引擎(avoid/scope.include/邏輯 run/pill 鄰字/分塊閘); pass 聚合管線。 功能 pass(src/typeset): - spacing:中西間隙採 margin 模型——包裹邊界左側字、以 margin-right 留隙, 不注入空白字元(textContent 完全不變、複製緊湊、justify 不暴增、不撐框、 行首乾淨)。含跨樣式邊界、pill、原檔空格移除。 - gapTrim(lineedge):layout pass,行末去隙,修正 justify 右緣內縮。 - longword:長英文詞 <span lang> + hyphens。 - jiya:標點 charify + 全角/開明/半角(全域 + 逐 lang);半形以字型 OpenType halt 渲染(按字形正確定位、不重疊,居中字形 !? 與繁體 locl 句逗點號皆正確); 行內連續擠壓。 - jinze:行首行末避頭尾(閉類/點號綁前字、開類綁後字,以詞元為邊界、長引文 中段可斷);亦為 justify 綁定載體。 - lineedge:行端擠壓 layout pass(括號引號;ResizeObserver 重算,瀏覽器專用)。 §8 justify:jz-char inline-block 原子,句末標點單份間距。 v1 相容層(compat/v1):createCjkAutospace().apply 映射,IIFE 全域別名。 構建:esbuild → ESM/IIFE/CSS;tsc → d.ts。測試:39 個 jsdom 案例。 demo:簡繁並列+justify+強制繁體開明+標點寬度量測,載 Noto webfont 作參考 (擠壓依賴字型 halt)。間隔號 · 不觸發中西間隙;200% 等詞元不被禁則切斷。
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// 語言與全角/開明政策(架構 §4.3)。
|
||||
//
|
||||
// 解析節點之有效 lang,歸類,並依政策決定標點寬度(全角式/開明式/半角式)。
|
||||
// CSS 端以 :lang() 落實同一政策(I2/I3);本模組供 JS pass 在需要時查詢。
|
||||
|
||||
/** 標點寬度政策。 */
|
||||
export type PunctStyle = "quanjiao" | "kaiming" | "banjiao";
|
||||
|
||||
/** 語言分類。 */
|
||||
export type LangClass = "zh-Hans" | "zh-Hant" | "ja" | "other";
|
||||
|
||||
export interface LocaleConfig
|
||||
{
|
||||
/** 無 lang 祖先時之預設語言。 */
|
||||
default: string;
|
||||
/** 全域標點擠壓預設(全角/開明/半角)。設則覆寫內建之逐語言預設,
|
||||
* 但仍可被 policy 之逐 lang 設定覆寫。未設則回退內建預設。 */
|
||||
style?: PunctStyle;
|
||||
/** 語言分類 → 標點寬度政策之逐 lang 覆寫(最高優先)。 */
|
||||
policy: Partial<Record<LangClass, PunctStyle>>;
|
||||
}
|
||||
|
||||
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 || 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];
|
||||
}
|
||||
|
||||
/** 解析節點之有效政策(lang → 分類 → 政策)。 */
|
||||
export function resolveStyle(node: Node, config: LocaleConfig): PunctStyle
|
||||
{
|
||||
const lang = effectiveLang(node, config.default);
|
||||
return styleFor(classify(lang), config);
|
||||
}
|
||||
Reference in New Issue
Block a user