fix: 行內元素三分類(透明/隔離/整體)——jinze 下潛肢解 KaTeX 修復(下游問題六);v1 相容層移除

下游問題六:行內公式緊鄰禁則標點時,jinze 為綁鄰字透明下潛 .katex(全樹皆 SPAN,
僅憑標籤名判透明),把公式內側末字連同完整殼層逐層克隆切出(splitoff),上標漂移
錯位;pillSelector/skipAttribute/scope.avoid 三層防護全被刺穿。復現另揭兩向量:
相鄰 pill 時 descend 刺穿致 jz-hws 插入公式內部;pill 內部標點被 jiya charify。

確立三分類(§4.1,所有遍歷原語共用同一判定):
  透明(styleInlines+jz-*)— 穿越/run 延續/可克隆切分;
  隔離(isolateSelector,預設 "mark",新增選項)— 邊界斷 run/斷相鄰/不可切分、
    兩端補隙(同 pill),內部照常處理(自成脈絡);
  整體(pillSelector)— 內部對一切 pass 不可見(insidePill 過濾),禁則時作為
    原子鄰字單位整體綁入 jz-jinze。

機制修復:
  finder — transparentInline 改以元素判定(先排除 pill/isolate/avoid 再查標籤集);
    avoidsSelf 自身判定(isAvoided 僅查祖先,自身命中 avoid 曾被穿越);descend 三態
    (邊界字/無內容續找兄弟/blocked 終止——不可越界跳找更遠之字);eachTextNode
    過濾 pill 內部;collectRuns 隔離元素內部自成 run。
  core/split — canSplit 諮詢 finder(pill/isolate/avoid 不可切 → 退化整綁);
    MARK 移出 SPLITTABLE(背景盒切分有可見接縫)。
  jiyaAdjacency — pill/avoid(自身判定)斷相鄰不下探;isolate 斷而內部自成脈絡。
  spacing Pass B — 邊界選擇器擴為 pill ∪ isolate;巢於 pill 內者跳過。
  orphan — avoidsSelf 邊界;avoid 塊不綁(舊漏判:avoid 子樹內裸文本曾被計數包綁)。

v1 相容層移除(消費端已全數遷移 v2 API):src/compat/v1.ts、IIFE 全域別名
createCjkAutospace、死選項 autospaceClass(經查從未被任何 pass 讀取)一併刪;
I8 退役。dist/compat 殘留聲明清除、dist/core/split.d.ts 補入庫(前次遺漏)。

測試 112 全綠(問題六 ×7、isolate ×6、compat 移除 ×1);headless Chrome 實證
公式完整(splitoff=0、內部無 jz-hws/jz-char)、mark 與公式整體入 jz-jinze。
文檔:README 三分類節+選項示例+功能表(hanging 行補「已移除」);ARCHITECTURE
§4.1 三分類/§3.5 相容層移除/§6.1/§6.3/§6.4/I8 退役。
This commit is contained in:
2026-06-12 17:15:00 +08:00
parent a3c94ddee5
commit c4e8c6cfd5
21 changed files with 732 additions and 546 deletions
+43 -28
View File
@@ -50,9 +50,9 @@
移除,DOM 回到 pass 前狀態(至少邏輯等價)。
- **I7 邊界感知**:跨樣式行內元素的鄰接(如 `<strong>是</strong>A`)須能識別並
正確處理——由核心 finder 統一提供,各 pass 不自行重複造輪。
- **I8 向下相容 v1**:既有 `createCjkAutospace(options).apply(root)` 消費端
build.sh 單檔串接、Nvim peek、Claude 模板)**不得 break、無功能倒退**。行為
**不要求與 v1 完全一致**,但須等價可用;以相容層達成(見 §3.5)
- **I8 向下相容 v1(已退役)**:相容層曾保障 v1 消費端(build.sh 單檔串接、Nvim
peek、Claude 模板)過渡期不 break;消費端遷移 v2 API 完成後相容層已移除(見
§3.5),此不變量隨之退役、編號保留不復用
- **I9 遍歷聚合**:邏輯上「一功能一 pass」,但執行上**相容之 pass 共享單次遍歷**
(見 §3.4),以減少全樹走訪次數;聚合不得犧牲 I6(冪等可還原)與逐功能開關。
- **I10 作用域與分塊**:作用範圍可由 `scope.include` 選擇器限定(opt-in);功能集
@@ -175,27 +175,13 @@ for phase in phases:
> 只是 P1 諸 pass 的 `collect` 在同一走訪內完成。若某 pass 不相容(需獨立走訪),
> 退化為自跑一次,不影響正確性。
### 3.5 v1 向下相容層I8`src/compat/v1.ts`
### 3.5 v1 向下相容層 — 已移除(I8 退役
導出 `createCjkAutospace(options)`,把 v1 API 映射到 `createJuzhen`**保證不
break、無功能倒退**,行為等價但不要求完全一致:
| v1 選項/行為 | 映射 |
|---|---|
| `.apply(root)` | → `.render(root)` |
| `pillSelector` | → pill 鄰接處理 + avoid |
| `styleInlines``blockTags``skipTags` | → finder 設定 |
| `autospaceClass`(預設 `cjk-autospace` | → 套用於 `jz-hws` 之 class,使既有
`.cjk-autospace{…}` CSS 仍命中間隙元素 |
| `skipAttribute``isSkipped` | → avoid 判定 |
| `longWordWrap` | → `longWord` |
| `punctuationSqueeze`v1 halt 句內半角) | → `jiya`(margin 開明/全角;行為近似,
非逐位等同——符合「不要求完全一致」) |
IIFE 構建額外掛 `globalThis.createCjkAutospace` 別名,使
`globalThis.createCjkAutospace` 之消費端(Nvim peek/模板)零改動可跑。相容層之
差異(間隙元素由 `span.cjk-autospace` 變為 `jz-hws[class]`、擠壓由 halt 變 margin
於 README 明列。
相容層(`src/compat/v1.ts``createCjkAutospace` 導出、IIFE 之全域別名、
`autospaceClass` 選項)曾把 v1 API 映射到 `createJuzhen`,服務 build.sh 單檔串接/
Nvim peekClaude 模板等 v1 消費端之過渡期。**最後一批 v1 消費端遷移 v2 API 完成後
已整體移除**`autospaceClass` 經查從未被任何 pass 讀取,屬死選項,一併刪)。v1
原始碼保存於 `v1-autospace` 分支。
### 3.6 作用域與分塊功能選擇(I10)
@@ -248,9 +234,31 @@ IIFE 構建額外掛 `globalThis.createCjkAutospace` 別名,使
設計要點:finder 不知道任何具體排版規則;它只提供「在何處、跨何種邊界、對哪些
字元」的遍歷與安全變更原語。規則由各 pass 以 unicode 表 + 回呼注入。
**行內元素三分類(下游問題六後確立)**:行內元素對排版有且僅有三種口徑,所有遍歷
原語(`collectRuns``adjacentLogicalChar``eachTextNode`、`jiyaAdjacency` 之相鄰
掃描、`core/split` 之可切判定、orphan 之實義字收集)共用同一判定:
- **透明**`styleInlines` wrap 類 `jz-*`)——穿越、run 延續、禁則可克隆切分。
- **隔離**`isolateSelector`,預設 `mark`)——邊界斷 run/斷相鄰/不可切分、兩端
補隙(同 pill),**內部照常處理**(自成脈絡)。典型:帶背景色之 `<mark>`(切分
背景盒有可見接縫,故亦自 `SPLITTABLE` 白名單移除)。
- **整體**`pillSelector`)——原子單位:內部對一切 pass **不可見**(`eachTextNode`
過濾 `insidePill`,不 charify/不插間隙/不切分/不下潛),禁則時作為原子鄰字
單位**整體綁入** `jz-jinze`。典型:`code``kbd`、行內公式 `.katex`。
判定**以元素為準、非標籤名**`transparentInline(el)`,先排除 pillisolate
avoid 再查標籤集):KaTeX 全樹皆 `<span>`,舊版按標籤名判透明曾令 `canSplit`
`descend` 整棵下潛——禁則把公式內側末字連同完整殼層逐層克隆切出(`splitoff`),
上標漂移錯位(下游問題六);`pillSelector``skipAttribute``scope.avoid` 三層
防護全被刺穿。同根修復:**avoid 以自身判定**(`avoidsSelf(el)`——`isAvoided` 僅查
祖先鏈,元素自身命中 avoid 時曾被穿越);`adjacentLogicalChar` 之 `descend` 改三態
(邊界字/無內容續找兄弟/**blocked** 終止——遇 pillisolateavoid 不可跳過續找
更遠之字,否則重蹈「越過 `jz-char` 取更前字 → 誤隙」之同型錯誤);`core/split` 之
`canSplit` 諮詢 finderpillisolateavoid 一律不可切、退化整綁)。
**「行內透明」原則之一致性(下游 Bug 修復)**:「邏輯文本應透明穿越之行內元素」一語
須在所有遍歷原語中**口徑一致**,否則同一段文本在不同原語下被切成不同邏輯串、生成相
互矛盾之結果。`isTransparentInline` 為單一真相源,列入:樣式行內標籤、`jz-hws`
互矛盾之結果。`transparentInline` 為單一真相源,列入:樣式行內標籤、`jz-hws`
`jz-jinze`wrap 群組)、**`jz-char``jz-inner`(標點原子)**。後者必須透明,否則
`adjacentLogicalChar` 之 `descend` 遇標點原子既不進入亦不終止、而是**跳過**它續找更前
之兄弟——`漢字。<code>` 經禁則綁定後,pill 之左鄰字應為「。」(標點,非 pill 鄰字、不
@@ -360,11 +368,13 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
- **copy-cleanI1,較 Han 更純)**:間隙為純視覺 margin,`textContent` 不增不減
語義字元,剪貼板得緊湊原文。
- **切片**:涵蓋 v1 Pass A、Pass Bpill `code/kbd` 邊界亦走同一機制——左字或
pill 元素包入 `jz-hws`pill 由 avoid 與鄰接判定處理)。
pill 元素包入 `jz-hws`pill 由 avoid 與鄰接判定處理)。Pass B 之邊界選擇器為
**pill isolate**(§4.1 三分類:隔離元素兩端補隙口徑與 pill 一致,差異僅在
內部處理;巢於 pill 內者跳過)。
- **邊界正規化一致性(下游回饋 1a/1b 修)**:
- **1a pill 穿越 jz-jinze**jinzeorder 40)先於 spacingorder 50),會把尾隨
標點之 pill 納入 `jz-jinze`。`adjacentLogicalChar` 須**透明穿越 `jz-jinze`**
(與 `jz-hws`、樣式行內同列,見 `finder.isTransparentInline`),否則 pill 左鄰字
(與 `jz-hws`、樣式行內同列,見 `finder.transparentInline`),否則 pill 左鄰字
不可達、左緣間隙與作者空格剝除雙失。
- **1b CJK↔CJK 贅餘空格**:中文不用詞間空格,故 CJK 表意字之間的單一 ASCII 作者
空格(含跨樣式行內標籤,`collectRuns` 已透明合併)一律**剝除、不留隙**(以
@@ -458,7 +468,8 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
pause 右空白 + open 左空白)兩側皆收 → 全角式與開明式皆合占 1em;`。」`stop 右空白;
」左為墨)只收 ``→ 全角式 1.5em/開明式 1em(」baseline 已半);`·—`(皆無指向對方之
空白)皆不收 → 兩式皆 2em(不宜挤之例外)。多標點鏈逐對處理。`jiyaAdjacencyPass` 以**從 root 之透明遍歷**(穿越 strong
em… 等樣式行內;block 邊界與 avoid 子樹斷相鄰;jz-char 為原子不下探)取代原「同 parent
em… 等樣式行內;block/pill/avoid(自身判定)斷相鄰且不下探,isolate 斷相鄰但內部
自成脈絡照常配對——§4.1 三分類;jz-char 為原子不下探)取代原「同 parent
直接兄弟」掃描,使**跨 inline 邊界**之連續標點亦正確配對(原漏致跨 strong/em 之標點對未挤、
達 2em)。
- **`margin` 後備模式(.feedback 增補)**:消費端字型未必含 `halt`(多數系統字型、
@@ -497,7 +508,11 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
把邊界詞元剝入一個淺克隆(保留標籤/class/style/lang,去 id),只綁該克隆(最小綁定),
原元素其餘部分留原位、渲染視覺無縫。克隆標 `data-jz-kind="splitoff"``revertPass` 之**延後
階段**(先解包 wrap、後併 splitoff,確保前兄弟為原元素而非 wrap 殼)併回原元素、還原一致
I6)。僅純樣式標籤白名單可切`a``abbr``ruby`/帶 id 者退化為整元素綁定(安全)。
I6)。僅純樣式標籤白名單可切,且 `canSplit` **諮詢 finder 三分類**(§4.1):`a``abbr`
`ruby`/帶 id 者,以及 **pill(整體)/isolate(隔離)/avoid 自身命中者**,一律退化為
整元素綁定(安全)——pill 即「作為原子鄰字單位整體綁入 `jz-jinze`」之期望行為(下游問題六:
KaTeX 全樹皆 SPAN,舊版僅憑標籤白名單曾整棵下潛肢解公式);isolate(如背景色 `mark`)切分
有可見接縫故整綁;`MARK` 並自白名單移除。
- **切片**:切片 1 完成行首行末避頭尾(閉類/點號避頭、開類避尾);行首縮排聯動、
更細之禁則對象列後續,沿用同一 `jz-jinze` 結構。
- **已知交互(待瀏覽器精修)**:`lineedge` 之行端偵測需跨 `jz-jinze` 群組邊界向上