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
+2 -2
View File
@@ -7,9 +7,9 @@ export declare const ALNUM = "A-Za-z0-9";
export declare const ALPHA = "A-Za-z";
/** 任一 CJK 字元測試。 */
export declare const ANY_CJK: RegExp;
/** CJK 之後可接間隙之西文集(含希臘字母、運算符、拉丁補充、箭號、裝飾符)。 */
/** CJK 之後可接間隙之西文集(含 @)。 */
export declare const ANS_AFTER_CJK: string;
/** CJK 之前可接間隙之西文集(同上,去 @)。 */
/** CJK 之前可接間隙之西文集(去 @)。 */
export declare const ANS_BEFORE_CJK: string;
/** 句末點號:。!?(開明式與全角式皆保持全形,不擠壓)。 */
export declare const BD_STOP = "\u3002\uFF01\uFF1F";
+17 -13
View File
@@ -512,8 +512,9 @@ var Juzhen = (() => {
var ALNUM = "A-Za-z0-9";
var ALPHA = "A-Za-z";
var ANY_CJK = new RegExp("[" + CJK + "]");
var ANS_AFTER_CJK = ALPHA + "\u0370-\u03FF0-9@\\$%\\^&\\*\\-\\+\\\\=\xA1-\xB6\xB8-\xFF\u2150-\u218F\u2700-\u27BF";
var ANS_BEFORE_CJK = ALPHA + "\u0370-\u03FF0-9\\$%\\^&\\*\\-\\+\\\\=\xA1-\xB6\xB8-\xFF\u2150-\u218F\u2700-\u27BF";
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
var BD_STOP = "\u3002\uFF01\uFF1F";
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
@@ -936,6 +937,10 @@ var Juzhen = (() => {
const rects = el.getClientRects();
return rects.length ? rects[0] : null;
}
function lastRect(el) {
const rects = el.getClientRects();
return rects.length ? rects[rects.length - 1] : null;
}
function relayout(root, finder) {
root.querySelectorAll("jz-char." + LE).forEach((e) => e.classList.remove(LE));
const chars = Array.from(root.querySelectorAll("jz-char")).filter(
@@ -984,18 +989,19 @@ var Juzhen = (() => {
};
var TRIM = "jz-hws-trim";
function trimGaps(root, finder) {
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;
}
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);
}
}
}
@@ -1086,7 +1092,7 @@ var Juzhen = (() => {
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
var DOTS_CJK = new RegExp("([\\.]{2,})([" + CJK + "])", "g");
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
var CJK_TILDE = new RegExp("([" + CJK + "])(~+)(?!=)(?=[" + CJK + ALNUM + "])", "g");
@@ -1096,7 +1102,6 @@ var Juzhen = (() => {
var AN_COLON_CJK = new RegExp("([" + ALNUM + "])(:)([" + CJK + "])", "g");
var CJK_QUOTE = new RegExp("([" + CJK + "])([" + QUOTES_CLS + "])", "g");
var QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + CJK + "])", "g");
var QUOTE_AN = new RegExp("([\u201D])([" + ALNUM + "])", "g");
var CJK_QUOTE_AN = new RegExp("([" + CJK + '])(")([' + ALNUM + "])", "g");
var CJK_HASH = new RegExp("([" + CJK + "])(#([^ ]))", "g");
var HASH_CJK = new RegExp("(([^ ])#)([" + CJK + "])", "g");
@@ -1134,7 +1139,6 @@ var Juzhen = (() => {
s = s.replace(AN_COLON_CJK, "$1$2" + MARK + "$3");
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");
s = s.replace(HASH_CJK, "$1" + MARK + "$3");
+17 -13
View File
@@ -484,8 +484,9 @@ var CJK = "\u2E80-\u2EFF\u2F00-\u2FDF\u3040-\u309F\u30A0-\u30FA\u30FC-\u30FF\u31
var ALNUM = "A-Za-z0-9";
var ALPHA = "A-Za-z";
var ANY_CJK = new RegExp("[" + CJK + "]");
var ANS_AFTER_CJK = ALPHA + "\u0370-\u03FF0-9@\\$%\\^&\\*\\-\\+\\\\=\xA1-\xB6\xB8-\xFF\u2150-\u218F\u2700-\u27BF";
var ANS_BEFORE_CJK = ALPHA + "\u0370-\u03FF0-9\\$%\\^&\\*\\-\\+\\\\=\xA1-\xB6\xB8-\xFF\u2150-\u218F\u2700-\u27BF";
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
var BD_STOP = "\u3002\uFF01\uFF1F";
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
@@ -908,6 +909,10 @@ function firstRect(el) {
const rects = el.getClientRects();
return rects.length ? rects[0] : null;
}
function lastRect(el) {
const rects = el.getClientRects();
return rects.length ? rects[rects.length - 1] : null;
}
function relayout(root, finder) {
root.querySelectorAll("jz-char." + LE).forEach((e) => e.classList.remove(LE));
const chars = Array.from(root.querySelectorAll("jz-char")).filter(
@@ -956,18 +961,19 @@ var lineEdgePass = {
};
var TRIM = "jz-hws-trim";
function trimGaps(root, finder) {
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;
}
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);
}
}
}
@@ -1058,7 +1064,7 @@ var DEL = String.fromCharCode(57347);
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
var DOTS_CJK = new RegExp("([\\.]{2,})([" + CJK + "])", "g");
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
var CJK_TILDE = new RegExp("([" + CJK + "])(~+)(?!=)(?=[" + CJK + ALNUM + "])", "g");
@@ -1068,7 +1074,6 @@ var AN_PERIOD_CJK = new RegExp("([" + ALNUM + "])(\\.)([" + CJK + "])", "g");
var AN_COLON_CJK = new RegExp("([" + ALNUM + "])(:)([" + CJK + "])", "g");
var CJK_QUOTE = new RegExp("([" + CJK + "])([" + QUOTES_CLS + "])", "g");
var QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + CJK + "])", "g");
var QUOTE_AN = new RegExp("([\u201D])([" + ALNUM + "])", "g");
var CJK_QUOTE_AN = new RegExp("([" + CJK + '])(")([' + ALNUM + "])", "g");
var CJK_HASH = new RegExp("([" + CJK + "])(#([^ ]))", "g");
var HASH_CJK = new RegExp("(([^ ])#)([" + CJK + "])", "g");
@@ -1106,7 +1111,6 @@ function panguSpace(text) {
s = s.replace(AN_COLON_CJK, "$1$2" + MARK + "$3");
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");
s = s.replace(HASH_CJK, "$1" + MARK + "$3");