fix(split): 攀升對稱守衛——切分前驗整條祖先鏈可切,opaque 中間祖先不再被克隆肢解
下游 .selection-bar-message 病例:段末實義字落於 opaque <button>(巢於可切 <span>), splitElementAt 僅驗頂層 span 可切;splitTreeAfter 逐層克隆 textNode→top 祖先鏈時,中間 opaque button 從未複查即被克隆肢解(複製出 splitoff button、破壞互動元素)。firstText/ lastText 守下潛(遇非可切回 null),但攀升無對稱守衛——此即真正病根,A7 之 per-instance .selection-bar-message 迴避僅治標。 修復:splitTreeAfter 切分前先驗「textNode.parentNode…top(含 top)」整條祖先鏈皆 canSplit, 任一層 opaque/pill/isolate/帶 id 即回 null → 呼叫端綁整個 top(退化但安全,與下潛對稱)。 純新增守衛、無新克隆邏輯;jinze 之 isolateBoundaryToken 因下潛已守,行為不變。附帶亦杜絕 中間祖先帶 id 之克隆重複 id。 測試(129 全綠,+2):末實義字在 opaque button(巢於可切 span)→ 綁整 span、無 splitoff、 button 完整無缺;退化路徑 render→revert 還原一致。亲驗:暫禁守衛則該測試於「無 splitoff」 斷言失敗(證明非空通過、病根屬實)。 文檔:split.ts 檔頭+splitTreeAfter JSDoc+ARCHITECTURE §6.5.1 補「canSplit 須對整條祖先鏈 成立、下潛+攀升雙向守衛」之不變量。dist 重建。
This commit is contained in:
@@ -1042,6 +1042,41 @@ test("垂懸字 元素最小綁定 revert:兩個 <strong> 併回單一、文
|
||||
assert.equal(p.querySelector("strong").textContent, "一段很長的加粗文字", "strong 還原");
|
||||
});
|
||||
|
||||
// 攀升對稱守衛(下游 .selection-bar-message 病例):段末實義字落於 opaque <button>,
|
||||
// 而 button 巢於可切 <span>(塊級直接子)內。splitElementAt 僅驗頂層 span 可切,若無攀升
|
||||
// 守衛,splitTreeAfter 逐層克隆 textNode→span 時會把中間 opaque button 一併克隆肢解
|
||||
// (複製出 splitoff button)。守衛須沿整條祖先鏈驗可切,遇 button 即放棄、退化為綁整 span。
|
||||
test("垂懸字 攀升守衛:末實義字在 opaque <button>(巢於可切 span)→ 綁整 span、絕不肢解 button", () =>
|
||||
{
|
||||
const { document } = setupDom(
|
||||
'<p lang="zh-Hant">開頭較長的一段文字內容之後<span>接續<button>結尾文字</button></span></p>',
|
||||
);
|
||||
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(
|
||||
'<p lang="zh-Hant">開頭較長的一段文字內容之後<span>接續<button>結尾文字</button></span></p>',
|
||||
);
|
||||
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 原子互斥。
|
||||
|
||||
Reference in New Issue
Block a user