feat: 字符分類規則集抽象(charClass,可配置;向下兼容)
把散落於各 pass 之硬編碼字符分類(中文 CJK/西文 ANS/全寬標點 BD_* 子類)統一抽象為
規則集 Ruleset:內置默認即現狀值,消費端經 options.charClass 擴充,以覆寫 spacing/
jiya/jinze 對特定字符之分類判定。
動機:英文全角句號 .(U+FF0E,異於中文句號 。U+3002)實證為**完全未分類**——不被
charify、不挤壓、與 CJK 間不補隙。與其每次硬編碼補字符(如先前 € 之西文集),不如給
消費端可配置之分類鉤子。
API(追加語義):
charClass?: {
cjk?: string; // 追加為中文
western?: string; // 追加為西文(半寬標點亦歸此)
biaodian?: { stop?, pause?, open?, close?, middle?, liga? }; // 追加為全寬標點,按子類
}
實作:
- core/unicode.ts:新增 Ruleset/CharClassOverride/BdSubclass 型別、DEFAULT_RULESET、
resolveRuleset(追加合併);classifyBiaodian(ch, biaodian) 改吃規則集(預設參數向下兼容)。
- types.ts:JuzhenOptions.charClass;ResolvedOptions.ruleset;index 再匯出公開型別。
- index.ts:resolveRuleset(opts.charClass) → ResolvedOptions.ruleset。
- typeset/spacing.ts:所有 CJK/ANS 依賴正則移入 makeRules(ruleset),per-instance 編譯
(非每 block);ALNUM/ALPHA/運算符等結構性 ASCII 仍為固定常數。
- typeset/jiya.ts:classifyBiaodian(ch, ruleset.biaodian)。
- typeset/jinze.ts:詞元邊界之 anyCjk 由 ruleset.cjk 建。
向下兼容:不傳 charClass 則 ruleset === DEFAULT_RULESET、行為與重構前完全一致。
v1 為追加語義,不支援重分類/移除內置字符(範圍式集合難挖洞、需求罕見,列後續)。
驗證:實 Chromium end-to-end——charClass.biaodian.stop="." → . charify 為 bd-stop
(开明式句末維持全角,比照 。);western="★"/cjk="〇" → 與對側正確補/不補中西間隙。
測試 66 通過(含 9 條 charClass 新測——默認未分類/各子類 charify/實例隔離不可變
(resolveRuleset 不污染 DEFAULT)/追加不破壞內置/多子類組合;57 既有不變)。
- README「自定義字符分類」節+ARCHITECTURE §4.2 規則集抽象。dist 重建。
This commit is contained in:
@@ -508,3 +508,88 @@ test("CSS:聚珍不內建 font-family(字型交消費端),半形顯式
|
||||
assert.equal(/font-family/.test(css), false, "聚珍 CSS 不應內建任何 font-family");
|
||||
assert.match(css, /font-feature-settings:\s*"locl"\s+1,\s*"halt"\s+1/, "半形 feature 應顯式保留 locl");
|
||||
});
|
||||
|
||||
// ----- charClass:自定義字符分類規則集(向下兼容+擴充)-----
|
||||
|
||||
test("charClass:預設不傳時,英文全角句號 . 未分類(不 charify、不挤、不補隙)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">中.文</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" } }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.equal(countTag(p, "jz-char"), 0, ". 預設未分類為標點,不 charify");
|
||||
assert.equal(countTag(p, "jz-hws"), 0, ". 預設非西文,無中西間隙");
|
||||
});
|
||||
|
||||
test("charClass.biaodian.stop:把 . 歸為全寬句末標點 → 被 jiya charify 為 bd-stop", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">中.文.</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { stop: "." } } }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.ok(countTag(p, "jz-char.bd-stop") >= 1, ". 應 charify 為 bd-stop");
|
||||
});
|
||||
|
||||
test("charClass.western:把未分類符號 ★ 歸為西文 → 與 CJK 邊界補 jz-hws", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">中★文</p>');
|
||||
// 先確認預設不補隙
|
||||
createJuzhen({}).render(document.body);
|
||||
assert.equal(countTag(document.querySelector("p"), "jz-hws"), 0, "預設 ★ 非西文、不補隙");
|
||||
const dom2 = setupDom('<p lang="zh-Hans">中★文</p>');
|
||||
createJuzhen({ charClass: { western: "★" } }).render(dom2.document.body);
|
||||
assert.ok(countTag(dom2.document.querySelector("p"), "jz-hws") >= 1, "★ 歸西文後,中★/★文 邊界補隙");
|
||||
});
|
||||
|
||||
test("charClass.cjk:把符號歸為中文 → 與西文邊界補 jz-hws", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">〇A</p>');
|
||||
createJuzhen({ charClass: { cjk: "〇" } }).render(document.body);
|
||||
assert.ok(countTag(document.querySelector("p"), "jz-hws") >= 1, "〇 歸中文後,〇↔A 為中西邊界、補隙");
|
||||
});
|
||||
|
||||
test("charClass:實例隔離——覆寫不污染內置默認(resolveRuleset 不 mutate DEFAULT_RULESET)", () =>
|
||||
{
|
||||
// 先建帶覆寫之實例(★ 視為西文)。
|
||||
const a = setupDom('<p lang="zh-Hans">中★文</p>');
|
||||
createJuzhen({ charClass: { western: "★" } }).render(a.document.body);
|
||||
assert.ok(countTag(a.document.querySelector("p"), "jz-hws") >= 1, "覆寫實例:★ 補隙");
|
||||
// 再建無覆寫之實例:★ 應仍未分類、不補隙(前實例之覆寫未洩漏至默認)。
|
||||
const b = setupDom('<p lang="zh-Hans">中★文</p>');
|
||||
createJuzhen({}).render(b.document.body);
|
||||
assert.equal(countTag(b.document.querySelector("p"), "jz-hws"), 0,
|
||||
"無覆寫實例:★ 仍未分類、不補隙(默認規則集未被前實例污染)");
|
||||
});
|
||||
|
||||
test("charClass.biaodian.pause:未分類符號歸句內點號 → 開明式挤半形(jz-half)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">中•文</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { pause: "•" } } }).render(document.body);
|
||||
const c = document.querySelector("jz-char.bd-pause");
|
||||
assert.ok(c, "• charify 為 bd-pause");
|
||||
assert.ok(c.classList.contains("jz-half"), "開明式句內點號挤半形(jz-half)");
|
||||
});
|
||||
|
||||
test("charClass.biaodian.open:歸開括號 → 禁則避尾,與後字綁為 jz-jinze", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hant">甲•乙</p>');
|
||||
createJuzhen({ charClass: { biaodian: { open: "•" } } }).render(document.body);
|
||||
assert.ok(countTag(document.querySelector("p"), "jz-jinze") >= 1, "開類 • 與後字綁定(避尾)為 jz-jinze");
|
||||
});
|
||||
|
||||
test("charClass.western:追加不破壞內置——€(內置)與 ★(追加)皆補隙", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">價€元★好</p>');
|
||||
createJuzhen({ jiya: false, longWord: false, jinze: false, charClass: { western: "★" } }).render(document.body);
|
||||
const n = countTag(document.querySelector("p"), "jz-hws");
|
||||
// 價↔€、€↔元、元↔★、★↔好 四處中西邊界皆補隙(內置 € 不因追加而失)。
|
||||
assert.ok(n >= 4, "內置 € 與追加 ★ 之四邊界皆補隙,實得 " + n);
|
||||
});
|
||||
|
||||
test("charClass.biaodian:多子類同傳,各自生效(stop/pause)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">甲.乙•丙</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { stop: ".", pause: "•" } } }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.ok(countTag(p, "jz-char.bd-stop") >= 1, ". → bd-stop");
|
||||
const pause = p.querySelector("jz-char.bd-pause");
|
||||
assert.ok(pause && pause.classList.contains("jz-half"), "• → bd-pause 且開明式挤半形");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user