fix: 中西間隙下游回饋批次(B 省略號/C+E gapTrim 自我應驗/D 西文集/標點稽核)

實 Chromium 定位並修復下游回饋(Peek.nvim,commit 96f5ac3 基線)之全部間隙問題,
另完成現代漢語標點之誤隙稽核。jsdom 不可信(TreeWalker/offset 失真),結論均以
真實 Chromium 量測。

B 全角省略號誤隙:DOTS_CJK 之 alternation `|…` 誤把全角省略號 U+2026 納入,致
  `……` 後接 CJK 冒出中西隙。移除 `|…`,僅留 ASCII `...`。

C+E gapTrim 自我應驗誤判(容器寬相關「最後一個間隙消失」、pill 跨行右隙消失):
  間隙之 margin-right 參與斷行;依「後字是否在更低行」判行末,而後字落點又被
  margin 推擠 → 間隙自身 margin 把後字擠到次行 → 判行末 → trim 去 margin → 後字
  回同行,自相矛盾、卡死於「已 trim、後字同行、間隙消失」。污染雙向:下游間隙之
  margin 亦令夾在中間之內容放不下其行。修法:trimGaps 改「起始全部 trim、由左至右
  逐一定案」——量測時前方已定案、自身與其後皆 trim,後字落點不受任何 margin 污染。
  以 lastRect 為準兼顧跨行 pill。實測 8–22em(ragged/justify)偽 trim 歸零、真行末
  仍 trim(justify 齊右不變)。

D 西文字集完整化(列舉法,2016/Windows 10 相容、\uXXXX 無需 flag):補入貨幣
  U+20A0–20BF(€ 等)、拉丁擴充 A/B(拼音聲調 ā ǎ ē)、西里爾、數學運算符
  U+2200–22FF;並把希臘由原 U+0370–037D 補全至整塊(原漏 α-ω 本身)。€ 鄰接 CJK
  之手工空格遂正規化、邊界補隙。

標點稽核:實 Chromium 全部常用中文標點 × 簡繁共 104 例,CJK 間皆 0 誤隙。修正
  唯一偏例——收尾彎引號 `”`(pangu QUOTE_AN 令其後接拉丁/數字補隙,為唯一會補隙
  之收尾標點,與 」』)》】〕 不一致;clreq 下全角收尾標點字身已含右側留白)。移除
  QUOTE_AN。

- src/core/unicode.ts:WEST_SHARED 列舉補全(\uXXXX、區塊註記)。
- src/typeset/spacing.ts:DOTS_CJK 去 `|…`;移除 QUOTE_AN。
- src/typeset/lineedge.ts:trimGaps 起始全 trim、左→右逐一定案;新增 lastRect。
- test/juzhen.test.mjs:B/D/標點稽核/彎引號 回歸(56 測試通過)。
- demo/resize-check.html:可拖曳容器檢查頁(各寬度間隙穩定性、標點無誤隙)。
- README.md 目視驗證、ARCHITECTURE.md §6.1/§6.1a 自我應驗/§6.1b 標點稽核。
- dist/ 重建(juzhen.css 不變,僅 JS 邏輯)。
This commit is contained in:
2026-06-03 00:57:40 +08:00
parent 96f5ac32e1
commit 79fee8481f
10 changed files with 277 additions and 49 deletions
+55
View File
@@ -48,6 +48,61 @@ test("spacingrevert 還原至原文(無 jz-hwstextContent 回到原樣
assert.equal(document.querySelector("p").textContent, "我用Neovim寫程式");
});
test("spacing:全角省略號 …… 後接 CJK 不插中西間隙(問題 B:原 DOTS_CJK `|…` 誤匹配)", () =>
{
const { document } = setupDom('<p lang="zh-Hant">他竟然走了……留下一地</p>');
createJuzhen({ lang: { style: "quanjiao" } }).render(document.body);
const p = document.querySelector("p");
// …… 屬 CJK 標點(已 charify 為 bd-liga),後接 CJK 不應有西文間隙。
assert.equal(countTag(p, "jz-hws"), 0, "…… 與後接 CJK 之間不應有 jz-hws");
});
test("spacing:貨幣符號 € 視為西文——與 CJK 邊界補隙、手工空格正規化(問題 D)", () =>
{
const { document } = setupDom('<p lang="zh-Hant">售價 €1099 都行</p>');
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body);
const p = document.querySelector("p");
assert.equal(/售價\s€/.test(p.textContent), false, "售價↔€ 之手工空格應正規化剝除");
assert.ok(countTag(p, "jz-hws") >= 2, "價↔€ 與 9↔都 兩處中西邊界各補 jz-hws");
});
test("spacing:擴充西文集——拼音聲調 ā/希臘 α/西里爾 Я 皆視為西文補隙(問題 D 完整化)", () =>
{
const { document } = setupDom('<p lang="zh-Hant">中ā文α字Я尾</p>');
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body);
const p = document.querySelector("p");
// ā(U+0101 拉丁擴充)、α(U+03B1 希臘,驗證希臘集補全)、Я(U+042F 西里爾) 皆為西文。
assert.ok(countTag(p, "jz-hws") >= 5, "ā/α/Я 與相鄰 CJK 各成邊界、補 jz-hws");
});
test("spacingCJK 標點介於 CJK 間一律不補中西隙(標點稽核:。!?「」()《》……—— 等)", () =>
{
const samples = [
"前。後", "前!後", "前?後", "前,後", "前、後", "前;後", "前:後",
"前「後", "前」後", "前(後", "前)後", "前《後", "前》後", "前【後", "前】後",
"前〔後", "前〕後", "前……後", "前——後", "前·後", "前~後", "前“後", "前”後",
];
for (const s of samples)
{
const { document } = setupDom('<p lang="zh-Hant">' + s + "</p>");
createJuzhen({ lang: { style: "quanjiao" } }).render(document.body);
assert.equal(countTag(document.querySelector("p"), "jz-hws"), 0, s + " 不應有 jz-hws");
}
});
test("spacing:收尾彎引號 ” 後接拉丁/數字不補隙(標點稽核移除 QUOTE_AN ” 偏例,與 」)》 一致)", () =>
{
// 原 pangu QUOTE_AN 令 ”+字母數字補隙,使 ” 成唯一會補隙之收尾標點(其他收尾標點皆不)。
const { document } = setupDom('<p lang="zh-Hans">“中文”ABC 與 ”123</p>');
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body);
const p = document.querySelector("p");
// “”ABC、”123 中之彎引號鄰拉丁/數字皆不補隙;惟「與」兩側作者空格為 CJK↔Latin
// 邊界(與|ABC 之 ABC 結尾、123|與 之類)仍會處理,故只斷言不含「彎引號旁之隙」——
// 以 textContent 不變 + 彎引號未被 jz-hws 包裹驗證。
const hwsTexts = Array.from(p.querySelectorAll("jz-hws")).map((h) => h.textContent);
assert.equal(hwsTexts.includes("”"), false, "收尾彎引號 ” 不應被包入 jz-hws(不補隙)");
});
test("spacing:跨樣式行內邊界(<strong>是</strong>Neovim)亦識別(I7", () =>
{
const { document } = setupDom("<p>這<strong>是</strong>Neovim</p>");