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:
2026-06-01 21:31:07 +08:00
parent 8098991b6b
commit 2d42c7ecf2
46 changed files with 7737 additions and 27 deletions
+120
View File
@@ -0,0 +1,120 @@
// 字元分類與規則字集(架構 §4.2)。
//
// 集中所有 Unicode 範圍與標點子類,供各 pass 共用,杜絕分類散落不一致。
// CJK/西文字集沿用 v1cjk-autospace)已驗證之 Pangu 範圍;標點子類為新增,
// 對應 CSS classjz-char 之 bd-* 標記)。
//
// 這些常數以「regex 字元類片段」形式給出(已就 RegExp 字元類做必要轉義),
// 故可直接 `new RegExp("[" + X + "]")`。
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
export const CJK = (
"⺀-⻿⼀-⿟" // CJK 部首補充 + 康熙部首
+ "぀-ゟ゠-ヺー-ヿ" // 平假名 + 片假名
+ "㄀-ㄯ㈀-㋿" // 注音符號 + 圈號 CJK
+ "㐀-䶿一-鿿豈-﫿" // 擴展 A + 統一表意 + 相容
);
/** 西文字母與數字。 */
export const ALNUM = "A-Za-z0-9";
/** 西文字母。 */
export const ALPHA = "A-Za-z";
/** 任一 CJK 字元測試。 */
export const ANY_CJK = new RegExp("[" + CJK + "]");
// ----- Pangu 中西邊界用之擴充西文字集(沿用 v1)-----
// 拉丁補充以 ¡-¶¸-ÿ 表達(U+00A100B6, 00B800FF),刻意跳過 U+00B7 中間點
// `·`——它是 CJK 間隔號(姓名分隔,如 卡尔·马克思),屬 CJK 標點而非西文,
// 不應觸發中西間隙(否則圓點側會多一個空白)。
/** CJK 之後可接間隙之西文集(含希臘字母、運算符、拉丁補充、箭號、裝飾符)。 */
export const ANS_AFTER_CJK = (
ALPHA + "Ͱ-Ͽ0-9@\\$%\\^&\\*\\-\\+\\\\=¡-¶¸-ÿ⅐-↏✀-➿"
);
/** CJK 之前可接間隙之西文集(同上,去 @)。 */
export const ANS_BEFORE_CJK = (
ALPHA + "Ͱ-Ͽ0-9\\$%\\^&\\*\\-\\+\\\\=¡-¶¸-ÿ⅐-↏✀-➿"
);
// ----- 標點(biaodian)子類。對應 jz-char 之 bd-* class -----
//
// 注意:分類依「字形角色」,而開明式/全角式之別依「句法角色」。slice 1 之
// 擠壓集(句內點號)由 BD_PAUSE 表達——此即 v1 Pass D 所擠之 ,、;:。
/** 句末點號:。!?(開明式與全角式皆保持全形,不擠壓)。 */
export const BD_STOP = "。!?";
/** 句內點號:,、;:(開明式擠成半形;全角式保持全形)。 */
export const BD_PAUSE = ",、;:";
/** 開始括號/引號:「『(《〈【〖〔[{“‘。 */
export const BD_OPEN = "「『(《〈【〖〔[{“‘";
/** 結束括號/引號:」』)》〉】〗〕]}”’。 */
export const BD_CLOSE = "」』)》〉】〗〕]}”’";
/** 間隔/分隔號:·・‧。 */
export const BD_MIDDLE = "·・‧";
/** 連續省略/破折:…—。 */
export const BD_LIGA = "…—";
/** 全體標點字元(供 charify 走訪偵測)。 */
export const BD_ALL = (
BD_STOP + BD_PAUSE + BD_OPEN + BD_CLOSE + BD_MIDDLE + BD_LIGA
);
const BD_SETS: ReadonlyArray<readonly [ string, string ]> = [
[ "bd-stop", BD_STOP ],
[ "bd-pause", BD_PAUSE ],
[ "bd-open", BD_OPEN ],
[ "bd-close", BD_CLOSE ],
[ "bd-middle", BD_MIDDLE ],
[ "bd-liga", BD_LIGA ],
];
/**
* 將單一標點字元歸入其 bd-* 子類;非標點回傳 null。
* 供 jiya 之 charify 為 jz-char 標記 class。
*/
export function classifyBiaodian(ch: string): string | null
{
for (const [ name, set ] of BD_SETS)
{
if (set.indexOf(ch) >= 0) { return name; }
}
return null;
}
/** 全體 bd-* 子類名(DOM 端讀取 jz-char 之 bd class 用)。 */
export const BD_CLASS_NAMES = [
"bd-stop", "bd-pause", "bd-open", "bd-close", "bd-middle", "bd-liga",
] as const;
/**
* 標點之「可壓縮空白」側(橫排)。字面偏左者空白在右(句內/句末/閉類),
* 開類字面偏右、空白在左。間隔號/連接號不壓縮。擠壓即收掉對應側之半形空白。
*/
export function blankSides(bdClass: string | null): { left: boolean; right: boolean }
{
switch (bdClass)
{
case "bd-open":
{
return { left: true, right: false };
}
case "bd-close":
case "bd-pause":
case "bd-stop":
{
return { left: false, right: true };
}
default:
{
return { left: false, right: false };
}
}
}