diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 009aec9..12f1ed3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -539,6 +539,11 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j 整元素綁定(安全)——pill 即「作為原子鄰字單位整體綁入 `jz-jinze`」之期望行為(下游問題六: KaTeX 全樹皆 SPAN,舊版僅憑標籤白名單曾整棵下潛肢解公式);isolate(如背景色 `mark`)切分 有可見接縫故整綁;`MARK` 並自白名單移除。 + `canSplit` 須對「切點 `textNode` 至 `top` 之**整條**祖先鏈」成立、非僅 `top`:下潛由 + `firstText`/`lastText` 守(遇非可切回 `null`),**攀升由 `splitTreeAfter` 之對稱守衛守** + (切分前先驗祖先鏈全可切,任一層 opaque/pill/isolate/帶 id 即放棄、退化整綁)。二者 + 缺一則 `splitElementAt`(orphan:切點取自 `collectSubst`,可穿透 opaque `button`)會令 + 中間 opaque 祖先被逐層克隆肢解——下游 `.selection-bar-message` 病例。 - **切片**:切片 1 完成行首行末避頭尾(閉類/點號避頭、開類避尾);行首縮排聯動、 更細之禁則對象列後續,沿用同一 `jz-jinze` 結構。 - **已知交互(待瀏覽器精修)**:`lineedge` 之行端偵測需跨 `jz-jinze` 群組邊界向上 diff --git a/dist/juzhen.iife.js b/dist/juzhen.iife.js index 0948602..3fe6fde 100644 --- a/dist/juzhen.iife.js +++ b/dist/juzhen.iife.js @@ -1049,6 +1049,14 @@ var Juzhen = (() => { return s; } function splitTreeAfter(top, textNode, offset, pass, finder) { + for (let a = textNode.parentNode; a && a.nodeType === 1; a = a.parentNode) { + if (!canSplit(a, finder)) { + return null; + } + if (a === top) { + break; + } + } if (offset <= 0 && firstText(top, finder) === textNode) { return null; } diff --git a/dist/juzhen.mjs b/dist/juzhen.mjs index ba6fcc3..1fd2428 100644 --- a/dist/juzhen.mjs +++ b/dist/juzhen.mjs @@ -1022,6 +1022,14 @@ function trailingTokenStart(d, anyCjk) { return s; } function splitTreeAfter(top, textNode, offset, pass, finder) { + for (let a = textNode.parentNode; a && a.nodeType === 1; a = a.parentNode) { + if (!canSplit(a, finder)) { + return null; + } + if (a === top) { + break; + } + } if (offset <= 0 && firstText(top, finder) === textNode) { return null; } diff --git a/src/core/split.ts b/src/core/split.ts index 55f4a46..60a05d3 100644 --- a/src/core/split.ts +++ b/src/core/split.ts @@ -17,6 +17,11 @@ // finder 三分類(§4.1):pill(整體,如 .katex——KaTeX 全樹皆 SPAN,僅憑標籤名會 // 整棵下潛肢解;下游問題六)、isolate(隔離,帶盒樣式切分有可見接縫)、avoid 自身 // 命中者,皆不可切——回退整元素綁定。 +// +// 可切性須對「切點 textNode 至 top 之**整條**祖先鏈」成立,非僅 top:下潛由 firstText/ +// lastText 守(遇非可切回 null),攀升由 splitTreeAfter 之對稱守衛守(切分前驗祖先鏈全可 +// 切)。二者缺一則反例(splitElementAt 之切點取自 orphan 之 collectSubst,可穿透 opaque +//

', + ); + createJuzhen().render(document.body); + const p = document.querySelector("p"); + const orphan = p.querySelector("jz-orphan"); + assert.ok(orphan, "應產生 jz-orphan"); + assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0, "無克隆切分殼層(button 未被肢解)"); + assert.equal(p.querySelectorAll("button").length, 1, "button 未被複製(仍單一)"); + assert.equal(p.querySelector("button").textContent, "結尾文字", "button 內容完整無缺"); + assert.equal(orphan.textContent, "接續結尾文字", "退化綁定整個 span(含 button)"); + assert.equal(p.textContent, "開頭較長的一段文字內容之後接續結尾文字", "textContent 不變"); +}); + +test("垂懸字 攀升守衛 revert:綁整 span 之退化路徑 render→revert 還原一致", () => +{ + const { document } = setupDom( + '

開頭較長的一段文字內容之後接續

', + ); + const jz = createJuzhen(); + jz.render(document.body); + jz.revert(document.body); + const p = document.querySelector("p"); + assert.equal(countTag(p, "jz-orphan"), 0, "無殘留 jz-orphan"); + assert.equal(p.querySelectorAll("span").length, 1, "span 單一"); + assert.equal(p.querySelectorAll("button").length, 1, "button 單一"); + assert.equal(p.textContent, "開頭較長的一段文字內容之後接續結尾文字", "textContent 還原"); +}); + // ----- 標點懸掛已移除(§6.5.6):hanging 選項保留為 no-op,永不產生 jz-hang-* ----- // 純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);原生 // hanging-punctuation 為唯一正解但僅 Safari 支援、且與 inline-block 原子互斥。