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
+17 -11
View File
@@ -53,15 +53,21 @@ export declare class Finder {
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
private transparentInline;
/** 該元素**自身**是否為 avoid 邊界(內建 skip 名單 / SVG / contentEditable /
* data-jz-skip / skipAttribute / avoid 選擇器 / isSkipped)。isAvoided 沿祖先
* 鏈逐層呼叫本判定;遍歷原語另須對「正要進入」之元素直接判自身——舊版僅查
* 祖先,元素自身命中 avoid 時被當透明穿越(下游問題六:scope.avoid 對 .katex
* 形同虛設)。 */
isTransparent(el: Element): boolean;
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRESAMPSCRIPT…,可覆寫)
* ③ SVG 命名空間 ④ contentEditable
* ⑤ data-jz-skip 屬性(內建) ⑥ skipAttribute(消費端指定屬性名)
* ⑦ scope.avoid 選擇器
* **此為自身判定**,供遍歷原語(collectRunsjiyaAdjacencycanSplitisTransparent)於
* 下探時逐元素自判(祖先由遞迴涵蓋)。**一次性**問「某節點是否被 avoid(自身或祖先)」
* 請改用 isAvoided(單一組合入口),勿只用本方法而漏判祖先、或只用 isAvoided 而漏判自身
* (下游問題六/階段一 #2:漏一半致 avoid 對自身形同虛設)。 */
avoidsSelf(el: Element): boolean;
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
/** 節點是否 avoid——**自身即邊界**元素 avoidsSelf 之七源;非元素節點僅判
* isSkipped**或**祖先鏈上任一為 avoid 邊界。此為「是否 avoid」之**單一組合入口(安全
* 預設)**:一次性檢查用此,免除「avoidsSelf ‖ isAvoided」易漏一半之誤用。遍歷原語逐
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
isAvoided(node: Node): boolean;
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
inScope(node: Node): boolean;
@@ -70,9 +76,9 @@ export declare class Finder {
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
eachBlock(root: Element, cb: (block: Element) => void): void;
/**
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
* jz-* 透明穿越;blockpillavoid/間隙 markerjz-hws)中斷 run。
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)
* 收集 block 之邏輯文本 run(架構 §4.1,I7):透明行內(isTransparentstyleInlines
* wrap 類 jz-*)穿越、run 延續;blockpillavoid 中斷 runisolate 與**未知行內
* 標籤**(嚴格白名單)中斷 run 且內部自成獨立 run。回傳 text.length >= 2 之 run。
*/
collectRuns(block: Element): LogicalRun[];
/**