diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c6443bf..79a86d8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -50,9 +50,9 @@ 移除,DOM 回到 pass 前狀態(至少邏輯等價)。 - **I7 邊界感知**:跨樣式行內元素的鄰接(如 `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 peek/Claude 模板等 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),**內部照常處理**(自成脈絡)。典型:帶背景色之 ``(切分 + 背景盒有可見接縫,故亦自 `SPLITTABLE` 白名單移除)。 +- **整體**(`pillSelector`)——原子單位:內部對一切 pass **不可見**(`eachTextNode` + 過濾 `insidePill`,不 charify/不插間隙/不切分/不下潛),禁則時作為原子鄰字 + 單位**整體綁入** `jz-jinze`。典型:`code`/`kbd`、行內公式 `.katex`。 + +判定**以元素為準、非標籤名**(`transparentInline(el)`,先排除 pill/isolate/ +avoid 再查標籤集):KaTeX 全樹皆 ``,舊版按標籤名判透明曾令 `canSplit`/ +`descend` 整棵下潛——禁則把公式內側末字連同完整殼層逐層克隆切出(`splitoff`), +上標漂移錯位(下游問題六);`pillSelector`/`skipAttribute`/`scope.avoid` 三層 +防護全被刺穿。同根修復:**avoid 以自身判定**(`avoidsSelf(el)`——`isAvoided` 僅查 +祖先鏈,元素自身命中 avoid 時曾被穿越);`adjacentLogicalChar` 之 `descend` 改三態 +(邊界字/無內容續找兄弟/**blocked** 終止——遇 pill/isolate/avoid 不可跳過續找 +更遠之字,否則重蹈「越過 `jz-char` 取更前字 → 誤隙」之同型錯誤);`core/split` 之 +`canSplit` 諮詢 finder(pill/isolate/avoid 一律不可切、退化整綁)。 + **「行內透明」原則之一致性(下游 Bug 修復)**:「邏輯文本應透明穿越之行內元素」一語 須在所有遍歷原語中**口徑一致**,否則同一段文本在不同原語下被切成不同邏輯串、生成相 -互矛盾之結果。`isTransparentInline` 為單一真相源,列入:樣式行內標籤、`jz-hws`/ +互矛盾之結果。`transparentInline` 為單一真相源,列入:樣式行內標籤、`jz-hws`/ `jz-jinze`(wrap 群組)、**`jz-char`/`jz-inner`(標點原子)**。後者必須透明,否則 `adjacentLogicalChar` 之 `descend` 遇標點原子既不進入亦不終止、而是**跳過**它續找更前 之兄弟——`漢字。` 經禁則綁定後,pill 之左鄰字應為「。」(標點,非 pill 鄰字、不 @@ -360,11 +368,13 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j - **copy-clean(I1,較 Han 更純)**:間隙為純視覺 margin,`textContent` 不增不減 語義字元,剪貼板得緊湊原文。 - **切片**:涵蓋 v1 Pass A、Pass B(pill `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**:jinze(order 40)先於 spacing(order 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` 群組邊界向上 diff --git a/README.md b/README.md index 5bfff1d..687f14e 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ 倉庫名仍為 `cjk-autospace`;公開 API 為 `createJuzhen` / 全域 `Juzhen`;注入之 自訂元素前綴 `jz-`。完整設計見 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。 -> 本版(2.x)為從零重寫。v1 之 `createCjkAutospace` API 由相容層保留(見下), -> 既有消費端不需改碼即可運行;v1 原始碼保存於 `v1-autospace` 分支。 +> 本版(2.x)為從零重寫。v1 之 `createCjkAutospace` 相容層**已移除**(最後一批 +> v1 消費端已遷移 v2 API);v1 原始碼保存於 `v1-autospace` 分支。 --- @@ -65,12 +65,12 @@ jz.render(document.querySelector("main")); | 功能 | 說明 | |---|---| -| **中西間隙** `spacing` | CJK↔西文邊界插入 copy-clean 之 `jz-hws`(`user-select:none`,剪貼板跳過)。「西文」涵蓋拉丁(含擴充 A/B,拼音聲調 `ā ǎ ē`)、希臘、西里爾、數字、貨幣(`€ ₩ ₿`)、數學運算符等;CJK 標點(`。,「」()……` 等)不誤判為西文、不生隙。含跨樣式行內邊界與 `code`/`kbd` pill 邊界(pill 緊鄰標點時穿越 `jz-jinze` 仍補間隙;pill 跨行右隙、間隙落行末之自我應驗皆已修)。**pill 之邏輯鄰字為標點時(如 `漢字。`)不誤加間隙**(標點非 pill 鄰字;`jz-char`/`jz-inner` 透明穿越,與 `collectRuns` 一致)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 | +| **中西間隙** `spacing` | CJK↔西文邊界插入 copy-clean 之 `jz-hws`(`user-select:none`,剪貼板跳過)。「西文」涵蓋拉丁(含擴充 A/B,拼音聲調 `ā ǎ ē`)、希臘、西里爾、數字、貨幣(`€ ₩ ₿`)、數學運算符等;CJK 標點(`。,「」()……` 等)不誤判為西文、不生隙。含跨樣式行內邊界與 pill/隔離元素邊界(`code`/`kbd`/`.katex` 與 `mark`,兩端補隙口徑一致;pill 緊鄰標點時穿越 `jz-jinze` 仍補間隙;pill 跨行右隙、間隙落行末之自我應驗皆已修)。**pill 之邏輯鄰字為標點時(如 `漢字。`)不誤加間隙**(標點非 pill 鄰字;`jz-char`/`jz-inner` 透明穿越,與 `collectRuns` 一致)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 | | **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 ``,配合 CSS `hyphens:auto` 原生斷詞(DOM 無連字符 → 複製不污染)。 | | **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點**雙側獨立**收——各標點若其有空白之側緊鄰另一標點即收半,故 `,「` 全角式與開明式皆 1em、`。」` 全角式 1.5em/開明式 1em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 | -| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。邊界字落於行內樣式元素(`strong`/`em`/`span` 等)內時**克隆切分**只綁邊界詞元(避免整個元素入 `nowrap` 致行末整體掉行、上行留巨大空隙),切出之克隆標 `data-jz-kind="splitoff"`、`revert` 時自動併回原元素;不可切元素(`a`/`abbr`/`ruby`/帶 `id`)退化為整元素綁定。 | +| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。邊界字落於**透明**行內樣式元素(`strong`/`em` 等)內時**克隆切分**只綁邊界詞元(避免整個元素入 `nowrap` 致行末整體掉行、上行留巨大空隙),切出之克隆標 `data-jz-kind="splitoff"`、`revert` 時自動併回原元素;不可切元素(`a`/`abbr`/`ruby`/帶 `id`,以及 **pill/隔離/avoid**——見「行內元素三分類」)退化為**整元素綁定**——pill(如行內公式 `.katex`)即作為原子鄰字單位整體綁入 `jz-jinze`,絕不下潛肢解其內部。 | | **垂懸字避免** `orphan` | 段末行 CJK 實義字 ≥ N(預設 2;標點/符號不計)。方案 C(斷行層、無測量):把段末「第 N 個實義字起至段末」包入 `jz-orphan`(`white-space:nowrap`),整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型成立。單行段無害。邊界字落於行內元素內時同樣**克隆切分**(只綁末 N 字、避免整個長元素入 `nowrap` 溢出容器;機制同禁則)。**預設開**(`orphan:false` 關,`{chars:n}` 調)。段落級功能。 | -| ~~**標點懸掛** `hanging`~~ | **已評估停用**(§6.5.6):純負 margin 機制不可行(墨色不出版心/滿凸拉升後字重疊/resize 重排錯亂),原生 `hanging-punctuation` 為正解但 Chrome 不支援。`hanging` 選項仍被接受但**無任何效果**;程式碼休眠待原生支援成熟。 | +| ~~**標點懸掛** `hanging`~~ | **已移除**(§6.5.6):純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器),原生 `hanging-punctuation` 為唯一正解但僅 Safari 支援、且與本庫 inline-block 原子互斥。`hanging` 選項仍被接受(API 相容)但**無任何效果**;相關程式碼/CSS 已全數刪除,原生路徑屬遠期「原生管線」新功能。 | | **功能分級** `level` | 功能分文本級(中西間隙、標點擠壓——任意文本片段適用)與段落級(禁則、垂懸字、懸掛、長詞——需行/段布局)。塊級元素預設全功能;`data-jz-level="text"` 子樹或選項 `level:{text:"選擇器"}`(如標題/按鈕)僅跑文本級。 | | **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 | @@ -96,8 +96,10 @@ createJuzhen({ jiya: true, // 或物件:{ halfWidth: "halt" | "margin", bias } jinze: true, orphan: true, // 垂懸字避免(預設開);或 { chars: 2 };false 關 - // hanging:已停用、無效果(見「標點懸掛」一節) + // hanging:已移除、無效果(見「標點懸掛」一節) level: { text: "h1,h2,h3,button,.ui" }, // 此選擇器子樹僅跑文本級功能(見下) + pillSelector: "code, kbd, .katex", // 整體(pill)元素(見「行內元素三分類」) + isolateSelector: "mark", // 內外隔離元素(預設 mark;"" 停用) justifyAtoms: true, // jz-char/jz-jinze 以 inline-block 作對齊原子; // 分頁器(Paged.js)下設 false 改 inline(見下) blocks: [ // 分塊功能選擇 @@ -106,6 +108,28 @@ createJuzhen({ }); ``` +### 行內元素三分類:透明/隔離/整體 + +行內元素對排版有且僅有三種口徑,**所有遍歷原語共用同一判定**(中西間隙、擠壓相鄰、 +禁則綁定、垂懸字、克隆切分皆一致): + +| 分類 | 選擇器 | 邊界 | 內部 | 典型 | +|---|---|---|---|---| +| **透明** | `styleInlines`(預設常見樣式標籤) | 穿越——間隙/相鄰/run 皆延續 | 照常處理;禁則可**克隆切分**(最小綁定) | `strong`/`em`/`b`/`i`/`a`/`span` | +| **隔離** | `isolateSelector`(預設 `"mark"`) | 斷 run/斷相鄰/不可切分;**兩端**與 CJK/西文相鄰時補 `jz-hws` 間隙 | **照常處理**(自成脈絡:內部標點照擠、內部中西照補隙) | 帶背景色之 ``、badge 類盒樣式行內 | +| **整體** | `pillSelector`(預設 `"code, kbd"`) | 同隔離(邊界斷、兩端補隙) | **完全不可見**——不 charify、不插間隙、不切分、不下潛 | `code`/`kbd`、行內公式 `.katex`、引用標 `.cite-ref` | + +要點: + +- **判定以元素為準、非標籤名**:KaTeX 全樹皆 ``,但 `.katex` 命中 `pillSelector` + 即為整體邊界——舊版按標籤名判透明曾整棵下潛、把公式內側末字連同殼層克隆切出(肢解)。 +- **禁則之整體綁定**:pill 緊鄰禁則標點時(`…$公式$,`),整個 pill 作為原子鄰字單位 + 綁入 `jz-jinze`(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。 +- **`scope.avoid`/`skipAttribute`/`data-jz-skip` 以自身判定**:命中元素自身即邊界、 + 內外皆不處理(舊版僅查祖先,元素自身命中時仍被穿越)。 +- **行內公式/夾注引用宜列為 pill**:`pillSelector: "code, kbd, .katex, .cite-ref"`—— + 獨立行內單位以 pill 邊界邏輯統一處理(兩側補隙、吸收作者空格、整體綁定)。 + ### 分頁器(Paged.js 等)相容:`justifyAtoms` 預設 `jz-char`/`jz-jinze` 為 `inline-block`,作 `justify` 之單一對齊原子(句末標點 @@ -351,21 +375,12 @@ createJuzhen({ --- -## v1 向下相容 +## v1 向下相容 — 已移除 -```js -// 既有消費端零改動: -const cjk = createCjkAutospace({ pillSelector: "code, kbd" }); -cjk.apply(document.body); -``` - -IIFE 構建另掛 `globalThis.createCjkAutospace` 別名。**與 v1 之差異**(行為等價、 -不要求逐位一致): - -- 間隙標記由 `` 變為 `` - (仍帶原 class,既有 `.cjk-autospace{user-select:none}` CSS 仍命中)。 -- 標點擠壓改由 `juzhen.css` 託管(須引入),預設仍以字型 `halt` 渲染半形——與 v1 - 消費端 `font-feature-settings:"halt"` 同機制;缺 `halt` 之字型可切 `margin` 後備。 +v1 相容層(`createCjkAutospace` 導出、IIFE 之 `globalThis.createCjkAutospace` +別名、`autospaceClass` 選項)**已隨最後一批 v1 消費端遷移完成而移除**。既有消費端 +請改用 `createJuzhen(options).render(root)` v2 API;v1 原始碼保存於 `v1-autospace` +分支。 --- diff --git a/build.mjs b/build.mjs index f366042..8515813 100644 --- a/build.mjs +++ b/build.mjs @@ -2,8 +2,7 @@ // 另行產出(見 package.json scripts)。 // // dist/juzhen.mjs ESM,供打包器/TypeScript/jsdom 測試。 -// dist/juzhen.iife.js IIFE,全域 `Juzhen`,原生