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:
+31
-6
@@ -102,6 +102,15 @@ function firstRect(el: Element): DOMRect | null
|
||||
return rects.length ? rects[0]! : null;
|
||||
}
|
||||
|
||||
// 元素之**最後**一個行盒矩形。jz-hws 之 margin-right 落在其內容末尾之 fragment,
|
||||
// 故行末判定須以此 rect(而非 firstRect)為準——否則包裹會跨行之 pill 時,firstRect
|
||||
// 取到首行矩形、與末行之後續內容比較會誤判(見 trimGaps)。
|
||||
function lastRect(el: Element): DOMRect | null
|
||||
{
|
||||
const rects = el.getClientRects();
|
||||
return rects.length ? rects[rects.length - 1]! : null;
|
||||
}
|
||||
|
||||
function relayout(root: Element, finder: Finder): void
|
||||
{
|
||||
// 清除上次行端 class。
|
||||
@@ -164,17 +173,33 @@ const TRIM = "jz-hws-trim";
|
||||
|
||||
function trimGaps(root: Element, finder: Finder): void
|
||||
{
|
||||
root.querySelectorAll("jz-hws." + TRIM).forEach((e) => e.classList.remove(TRIM));
|
||||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g),
|
||||
);
|
||||
if (!gaps.length) { return; }
|
||||
|
||||
// 自我應驗陷阱(下游回饋 C/容器寬相關之「最後一個間隙消失」):間隙以 margin-right
|
||||
// 承載、會參與斷行。若依「後字是否在更低行」判行末,而後字落點又被 margin 推擠,便
|
||||
// 自相矛盾——間隙 margin 把後字擠到次行 → 判行末 → trim 去 margin → 後字回同行,振盪
|
||||
// 停在「已 trim、後字同行、間隙消失」之錯誤終態(ResizeObserver 只觀 root 尺寸、不因
|
||||
// 內部 margin 變化重觸發,故卡死)。**污染是雙向的**:不僅當前間隙自身 margin,**下游
|
||||
// 間隙之 margin** 亦會令夾在中間之內容(如 `或<gap>$1299<gap>`)放不下該行、致後字
|
||||
// 假性換行。
|
||||
//
|
||||
// 解法:**起始全部 trim**(margin 全 0),再由左至右逐一定案。量測某間隙時,其前皆已
|
||||
// 定案(最終 margin 狀態)、其自身與**其後全部**仍 trim(margin 0);故後字落點既不受
|
||||
// 自身、亦不受任何下游 margin 污染,純粹反映「無本間隙 margin 時後字是否仍被擠到次
|
||||
// 行」。後字仍在更低行(或無後字)→ 真行末、維持 trim;後字在同行 → 行內容得下、非真
|
||||
// 行末 → 還原間隙。逐一(非一次量全部)必要:斷行為左→右因果,當前間隙之行起點取決
|
||||
// 於其前已定案內容;讀取 getClientRects 觸發同步重排,故每次量測皆為最新佈局。以末行
|
||||
// rect(lastRect,margin 實際落點)為準,兼顧跨行 pill(行尾 code 折行亦曾致隙誤消)。
|
||||
gaps.forEach((g) => g.classList.add(TRIM));
|
||||
for (const g of gaps)
|
||||
{
|
||||
const r = firstRect(g);
|
||||
if (!r) { continue; }
|
||||
const next = edgeRect(g.nextSibling, "next", g);
|
||||
// 行末:後無內容,或後一字在更低行。
|
||||
if (!next || next.top > r.top + EPS) { g.classList.add(TRIM); }
|
||||
const r = lastRect(g);
|
||||
const next = r ? edgeRect(g.nextSibling, "next", g) : null;
|
||||
const atLineEnd = !r || !next || next.top > r.top + EPS;
|
||||
if (!atLineEnd) { g.classList.remove(TRIM); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ const STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "
|
||||
// 用 lookahead 使連續 CJK 空格逐一命中;只匹配 ASCII 空格,全形空格 U+3000 不動。
|
||||
const STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||
|
||||
const DOTS_CJK = new RegExp("([\\.]{2,}|…)([" + CJK + "])", "g");
|
||||
// 僅 ASCII 省略號 `...中文` 補隙;**不含**全角省略號 U+2026——全角 …/…… 屬 CJK
|
||||
// 標點(已由 jiya charify 為 bd-liga),後接 CJK 不應有中西間隙(下游回報問題 B:
|
||||
// 原 alternation `|…` 誤把全角納入,致 `……留` 間冒出 jz-hws)。
|
||||
const DOTS_CJK = new RegExp("([\\.]{2,})([" + CJK + "])", "g");
|
||||
const CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||
const AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||||
const CJK_TILDE = new RegExp("([" + CJK + "])(~+)(?!=)(?=[" + CJK + ALNUM + "])", "g");
|
||||
@@ -49,7 +52,10 @@ const AN_PERIOD_CJK = new RegExp("([" + ALNUM + "])(\\.)([" + CJK + "])", "g");
|
||||
const AN_COLON_CJK = new RegExp("([" + ALNUM + "])(:)([" + CJK + "])", "g");
|
||||
const CJK_QUOTE = new RegExp("([" + CJK + "])([" + QUOTES_CLS + "])", "g");
|
||||
const QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + CJK + "])", "g");
|
||||
const QUOTE_AN = new RegExp("([”])([" + ALNUM + "])", "g");
|
||||
// 註:原 pangu 之 QUOTE_AN(`([”])([ALNUM])` → 收尾彎引號後接字母數字補隙)已移除。
|
||||
// 標點稽核發現它令收尾彎引號 ” 成為唯一「後接拉丁/數字會補隙」之收尾標點,與所有其他
|
||||
// 收尾標點(」』)》】〕…)行為不一致(後者皆不補);clreq 下全角收尾標點字身已內含
|
||||
// 右側留白,後接拉丁不需額外隙。故移除以求一致、消除誤隙(下游回饋之標點稽核)。
|
||||
const CJK_QUOTE_AN = new RegExp("([" + CJK + "])(\")([" + ALNUM + "])", "g");
|
||||
const CJK_HASH = new RegExp("([" + CJK + "])(#([^ ]))", "g");
|
||||
const HASH_CJK = new RegExp("(([^ ])#)([" + CJK + "])", "g");
|
||||
@@ -93,7 +99,6 @@ function panguSpace(text: string): string
|
||||
|
||||
s = s.replace(CJK_QUOTE, "$1" + MARK + "$2");
|
||||
s = s.replace(QUOTE_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(QUOTE_AN, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||||
|
||||
s = s.replace(CJK_HASH, "$1" + MARK + "$2");
|
||||
|
||||
Reference in New Issue
Block a user