refactor: 專案整理階段二——透明判定單一真相源+跨透明邊界避頭尾+avoid 排除歸一

承階段一之下游審計。核心貫穿問題:「透明穿越」判定實有兩套口徑散落各處,抵觸 finder
檔頭「所有遍歷共用同一判定」之聲明。用戶抉擇:**嚴格白名單**(未知行內標籤=邊界,
透明須顯式登記 styleInlines)。

透明判定單一真相源(2a):
・transparentInline 提升為公開 isTransparent,作邏輯文本流之單一透明判定。
・collectRuns 改由 isTransparent 判定——未在 styleInlines 之未知行內標籤(如 <button>/自訂
  元素)當隔離式邊界(斷 run、內部自成脈絡);isolate 與未知行內併為同一分支。
・jiyaAdjacency 同改用 finder.isTransparent;adjacentLogicalChar 沿用(僅隨改名)。
・分層澄清(承 #4 教訓,不強行一統):邏輯文本流統一為 isTransparent;另兩層各有正當之
  不同邊界、刻意不併入——結構切分安全(split canSplit:更窄白名單,<a>/ABBR 不可切)與
  物理版面(lineedge:getClientRects 物理相鄰,須跨 isolate 找真實鄰接)。檔頭改分層表述。

跨透明元素邊界避頭尾綁定(2b,修下游審計 #3):
・避頭類標點若落於透明元素邊緣(「你好<strong>。世界</strong>」之 。位於 strong 首位、與
  「好」不同 DOM parent),舊版 forbiddenBreak 僅認直接 jz-char、render 僅處理 jz-char 之直接
  父(strong),外層 p 之「好↔strong」綁定從不被評估 → 。漏綁、可誤置行首。
・偵測:edgeCharEl 經 finder.isTransparent 穿越透明元素定位邊緣 jz-char。
・父級處理:render 沿透明元素邊緣上攀納入外層 parent,父集深者先處理。綁定端沿用
  isolateBoundaryToken(可切則最小綁定;邊界字為已 charify 之 jz-char 無法越過時退化整綁,
  安全、與 <a> 同型)。

avoid 排除機制歸一(2c):
・isAvoided 改單一組合入口(自身即邊界 或 祖先鏈任一邊界),免除「avoidsSelf ‖ isAvoided」
  易漏一半之誤用(階段一 #2 即此類);avoidsSelf 保留供遍歷原語逐元素下探自判(免 O(n²))。
・processPills/orphan/spacing root 收斂為 isAvoided(x);doc 補齊七種 avoid 來源與優先序。

一致性小項(2e):scope.include 型別去多餘 `| null`;PASSES 陣列補註(執行序由 pass.order
分群排序決定);gapTrim 補 featureEnabledFor("spacing"),與 lineEdge relayout 逐節點閘對稱。

評估後不做(2d):五處幂等判斷(jiya 祖先/longword 父/spacing 父/orphan 子/jinze 父)檢查
不同位置關係、無單一 isJz 形狀可全覆蓋;真正缺陷(jinze 二次 render 巢狀)已於階段一以 isJz
修復,其餘各自正確,強行歸一徒增風險(承 #4 教訓:正當差異不硬併)。

新增契約/回歸測試(button 嚴格白名單、跨透明邊界避頭);121 測試全綠、構建乾淨。
This commit is contained in:
2026-07-07 21:05:25 +08:00
parent 30a11b5d6e
commit e3296104a6
13 changed files with 435 additions and 186 deletions
+35
View File
@@ -509,6 +509,24 @@ test("jinze 不可切元素退化整綁:<a> 連結整體綁定(不克隆出
assert.ok(group && group.querySelector("a"), "整個 <a> 入綁(退化但安全)");
});
test("jinze 跨透明邊界避頭:<strong> 首位句末點號綁至前字(你好<strong>。世界</strong>", () =>
{
// 下游審計 #3:。位於 <strong> 首位、與「好」不同 DOM parent,舊版僅處理 jz-char 之
// 直接父(strong),外層 p 之「好↔strong」綁定不被評估致 。可誤置行首。修後於外層
// 偵測(edgeCharEl 穿越透明元素)並綁定。
const { document } = setupDom('<p lang="zh-Hant">你好<strong>。世界</strong>結束</p>');
const jz = createJuzhen({ spacing: false, longWord: false });
jz.render(document.body);
const p = document.querySelector("p");
const group = p.querySelector("jz-jinze");
assert.ok(group, "產生跨邊界 jz-jinze 綁定組");
assert.ok(group.textContent.includes("好") && group.textContent.includes("。"),
"。(strong 首位、避頭)與前字「好」綁入同一 nowrap 群組、不置行首");
jz.revert(document.body);
assert.equal(p.textContent, "你好。世界結束", "revert 還原");
assert.equal(countTag(p, "jz-jinze"), 0);
});
test("jinzerevert 還原(無 jz-jinzejz-char,文字回原樣)", () =>
{
const { document } = setupDom('<p lang="zh-Hant">他說好。」嗎</p>');
@@ -1073,6 +1091,23 @@ test("三分類互斥:pill 自身 avoiddata-jz-skip)→ 兩端不補間
"自身 avoid 之 pill 兩端不補間隙");
});
test("嚴格白名單:未知行內標籤(<button>)為邊界,登記 styleInlines 後轉透明(單一真相源)", () =>
{
// 邏輯文本流之三原語(collectRunsadjacentLogicalCharjiyaAdjacency)共用 isTransparent
// 未在 styleInlines 之行內標籤一律當隔離式邊界(斷 run/相鄰、內部自成脈絡)。
const a = setupDom("<p>你好<button>Neo</button>寫字</p>");
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(a.document.body);
assert.equal(countTag(a.document.querySelector("p"), "jz-hws"), 0,
"預設 button 為邊界:內外不相鄰、無跨界中西隙");
// 顯式登記 styleInlines → 透明穿越:好↔Neo、Neo↔寫 兩處中西邊界補隙。
const b = setupDom("<p>你好<button>Neo</button>寫字</p>");
createJuzhen({ jiya: false, longWord: false, jinze: false, styleInlines: [ "BUTTON" ] })
.render(b.document.body);
assert.equal(countTag(b.document.querySelector("p"), "jz-hws"), 2,
"登記後透明穿越,跨界補中西隙");
});
test("問題六:相鄰兩 pill——descend 不刺穿,jz-hws 絕不插入公式內部", () =>
{
const { document } = setupDom("<p>公式" + KATEX + KATEX + "文字</p>");