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:
+19
-2
@@ -17,6 +17,11 @@
|
||||
// finder 三分類(§4.1):pill(整體,如 .katex——KaTeX 全樹皆 SPAN,僅憑標籤名會
|
||||
// 整棵下潛肢解;下游問題六)、isolate(隔離,帶盒樣式切分有可見接縫)、avoid 自身
|
||||
// 命中者,皆不可切——回退整元素綁定。
|
||||
//
|
||||
// 可切性須對「切點 textNode 至 top 之**整條**祖先鏈」成立,非僅 top:下潛由 firstText/
|
||||
// lastText 守(遇非可切回 null),攀升由 splitTreeAfter 之對稱守衛守(切分前驗祖先鏈全可
|
||||
// 切)。二者缺一則反例(splitElementAt 之切點取自 orphan 之 collectSubst,可穿透 opaque
|
||||
// <button>)會令中間 opaque 祖先被逐層克隆肢解——下游 A7 之 .selection-bar-message 病例。
|
||||
|
||||
import type { Finder } from "./finder.js";
|
||||
|
||||
@@ -107,8 +112,9 @@ function trailingTokenStart(d: string, anyCjk: RegExp): number
|
||||
|
||||
/**
|
||||
* 把 top 自切點(textNode, offset)起至末尾之內容剝入一個淺克隆鏈,克隆作為 top 之
|
||||
* 後兄弟插入並標記 splitoff。回傳該頂層克隆;若切點位於 top 內容之最起點(左側無內容、
|
||||
* 即整個 top 都在切點之後)則回 null(呼叫端據此綁整個 top)。
|
||||
* 後兄弟插入並標記 splitoff。回傳該頂層克隆;下列任一情形回 null(呼叫端據此綁整個
|
||||
* top):切點位於 top 內容之最起點(左側無內容、即整個 top 都在切點之後);或 textNode
|
||||
* 至 top 之祖先鏈含不可切層(opaque/pill/isolate/帶 id——見攀升守衛)。
|
||||
*
|
||||
* 沿祖先鏈逐層剝離:每層把「切點側之首節點及其後兄弟」搬入該層元素之淺克隆,空克隆
|
||||
* 跳過(避免殘留空元素破壞 revert 還原)。
|
||||
@@ -121,6 +127,17 @@ function splitTreeAfter(
|
||||
finder: Finder,
|
||||
): Element | null
|
||||
{
|
||||
// 攀升對稱守衛:firstText/lastText 守下潛(遇非可切回 null),但 splitElementAt
|
||||
// (orphan)之切點取自 collectSubst,其 textNode→top 祖先鏈未必全可切——中間祖先若
|
||||
// 為 opaque(如 <button>)/pill/isolate/帶 id,逐層淺克隆會肢解該邊界元素(複製
|
||||
// <button>、重複 id、破壞 pill/isolate 整體)。故切分前先驗整條祖先鏈(textNode.parentNode
|
||||
// …top,含 top)皆可切;任一層不可切即放棄(回 null → 綁整個 top,退化但安全,與下潛對稱)。
|
||||
for (let a: Node | null = textNode.parentNode; a && a.nodeType === 1; a = a.parentNode)
|
||||
{
|
||||
if (!canSplit(a as Element, finder)) { return null; }
|
||||
if (a === top) { break; }
|
||||
}
|
||||
|
||||
// 切點位於 top 最起點(首文本節點之 offset 0)→ 左側無內容,不切。
|
||||
if (offset <= 0 && firstText(top, finder) === textNode) { return null; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user