# 聚珍(Juzhen)架構文檔 > 現代簡/繁中文網頁排版套件。Han.css 之現代化重寫,沿其功能版圖,並針對 > 我們關注的數種情況(複製乾淨、依 `lang` 全角/開明切換、justify 句末標點 > 單份間距、跨瀏覽器一致)作調整。 > > 倉庫名 `cjk-autospace` 不變;公開 API 為 `createJuzhen` / 全域 `Juzhen`; > 自訂元素前綴 `jz-`。本文件先於任何程式碼,作為實作契約——後續每個功能點 > 皆按此落地,以避免不一致與重大返工。 --- ## 1. 目標與範圍 1. **第一垂直切片 = v1(cjk-autospace)全部功能**,在新引擎上重寫:中西間隙 (Pass A/B)、長英文詞斷詞包裝(Pass C)、開明式標點擠壓(Pass D)。 2. **架構須預留完整復刻 Han.css 的能力**:標點修正、擠壓、懸掛、禁則、著重號、 注音/ruby、樣式正常化、字體偵測、計數器等,皆能在不改動切片 1 既有程式碼 的前提下,以「新增一個 pass」的方式加入。 3. **跨瀏覽器一致**(Chrome/Edge/Safari/WebKit/Firefox):一致性關鍵的版面 行為,只用普遍支援的 CSS 原語;原生新特性僅作非必要增強。 非目標:不依賴 Han.css/Heti 等既有庫之執行期程式碼(MIT 條款下可借鑑其思路與 分類表,但成品自含)。 --- ## 2. 設計不變量(所有 pass 必須遵守) 這些不變量是「避免返工」的核心。任何新功能點都不得違反。 - **I1 複製乾淨(copy-clean)**:注入之間隙與標記一律 `user-select: none`,且不 改變邏輯 `textContent` 的語義;若某 pass 必須改寫文字(如標點替換),則須為 顯式 opt-in 且可 `revert` 還原。預設「只包裝、不改寫文字」。 - **I2 JS/CSS 職責分離**:JS 只注入**結構**(`jz-*` 元素、class、`data-jz-*` 屬性);**所有視覺尺寸(margin/letter-spacing/寬度/懸掛位移)一律寫在 `juzhen.css`**。同一段 DOM 在 justify 與否、主題與否下都成立。此分離是擴充性 與跨瀏覽器調校的關鍵。 - **I3 依 `lang` 行為**:每個 pass 解析節點的有效 `lang`(最近祖先),行為與 CSS 皆以 `:lang()` 與 locale 政策驅動。全角式/開明式之別即由此決定。 - **I4 跨瀏覽器一致**:間隙、擠壓等一致性關鍵層只用 `margin`/`letter-spacing`/ `inline-block`/`user-select` 等普遍原語。`text-spacing-trim`/ `hanging-punctuation`/`text-emphasis` 等僅作漸進增強,絕不作為唯一機制。**且當 某原生原語在各引擎支援不一致、會與我們託管之版面衝突或重複作用時,須在 `.juzhen` 範圍內以 `@supports` 顯式禁用之**(如關閉 Blink 的 `text-spacing-trim`/ `text-autospace`),使三引擎行為收斂於同一路徑(見 §9.1)。 - **I5 justify 單份間距**:間隙與擠壓的結構設計,須保證每個標點在 `text-align: justify` 下只貢獻**一份**對齊伸縮量(見 §8)。 - **I6 冪等與可還原**:每個 pass 對其產物打標記,重跑跳過;提供 `revert` 乾淨 移除,DOM 回到 pass 前狀態(至少邏輯等價)。 - **I7 邊界感知**:跨樣式行內元素的鄰接(如 `A`)須能識別並 正確處理——由核心 finder 統一提供,各 pass 不自行重複造輪。 - **I8 向下相容 v1**:既有 `createCjkAutospace(options).apply(root)` 消費端 (build.sh 單檔串接、Nvim peek、Claude 模板)**不得 break、無功能倒退**。行為 **不要求與 v1 完全一致**,但須等價可用;以相容層達成(見 §3.5)。 - **I9 遍歷聚合**:邏輯上「一功能一 pass」,但執行上**相容之 pass 共享單次遍歷** (見 §3.4),以減少全樹走訪次數;聚合不得犧牲 I6(冪等可還原)與逐功能開關。 - **I10 作用域與分塊**:作用範圍可由 `scope.include` 選擇器限定(opt-in);功能集 可逐塊覆寫(屬性或 `blocks` 映射),由 pass 在 `collect` 內以 per-node 閘解析 (見 §3.6)。CSS 視覺規則須與 JS 作用域同域收斂,不外溢。 --- ## 3. 整體架構 ### 3.1 對外 API ``` const jz = createJuzhen(options); jz.render(root?); // 對 root(預設 document.body)套用啟用之 pass jz.revert(root?); // 還原 // 全域: window.Juzhen.createJuzhen(...) ``` `options` 形如: ``` { // 全域。style = 全域擠壓預設(quanjiao|kaiming|banjiao);policy = 逐 lang // 覆寫。優先序:policy[lang] > style > 內建預設(繁→全角、簡→開明)。 lang: { default: "zh-Hant", style: "kaiming", policy: { "zh-Hant": "quanjiao" } }, classPrefix: "jz", // 作用域限定(§3.6) scope: { root: document.body, // 預設處理根;render(root) 可逐次覆寫 include: null, // 啟用選擇器;設則僅處理命中子樹(opt-in) // 如 ".juzhen, [data-juzhen]" avoid: "code, kbd, pre, samp, textarea, [contenteditable]", // 追加於內建 avoid }, // 全域逐功能開關(true/false 或細部物件),預設見各 pass spacing: true, // 中西間隙 longWord: { minLength: 6, lang: "en" }, jiya: true, // 標點擠壓 jinze: true, // 禁則(含 justify 綁定) hanging: false, // 標點懸掛(預設關,繁體橫排不宜) biaodian: false, // 標點修正(opt-in,可能改寫文字) emphasis: false, ruby: false, // 分塊功能選擇(§3.6):選擇器 → 該塊內生效之功能集(覆寫全域) blocks: [ { selector: ".prose", features: ["spacing", "jiya", "jinze", "emphasis"] }, { selector: ".code-comment", features: ["spacing"] }, // 此塊只間隙、不擠壓 ], } ``` ### 3.2 Pass 管線 每個功能點是一個 `Pass`: ``` interface Pass { name: string; phase: "P0" | "P1" | "P2"; // 遍歷階段,供聚合(§3.4) order: number; // phase 內 apply 先後(charify→擠壓→間隙→…) enabled(options): boolean; // 聚合模式:collect 純判定(同階段共享單次走訪),apply 批次變更 collect(node: Text, ctx: RenderContext): Target[]; apply(targets: Target[], ctx: RenderContext): void; revert(ctx: RenderContext): void; } ``` 需獨立走訪/layout 之 pass(如 hanging)可改採 `render(ctx)` 整段式而不參與 collect 聚合;執行器兩種皆容納。 `createJuzhen` 內建一個**有序、可開關**的 pass 註冊表。`render` 依 `order` 執行 已啟用者。順序契約(避免互相破壞): ``` 10 biaodian 標點修正(若開,最先;可能改寫文字/包裝) 20 charify 標點字元包裝(jz-char/jz-inner)——擠壓、禁則、著重號之共同前置 30 jiya 標點擠壓(依 class + :lang 之 CSS) 40 jinze 禁則 + justify 綁定 50 spacing 中西間隙(jz-hws) 60 longWord 長英文詞 lang 包裝 70 emphasis 著重號 80 ruby 注音/ruby 90 hanging 標點懸掛(需 layout,最後;掛 ResizeObserver) ``` > 順序理由:charify 先把標點變成可定址的 `jz-char`,擠壓/禁則/著重號才有對象; > 間隙在標點結構穩定後插入;懸掛依賴最終行盒,必須最後且 layout 後再算。 ### 3.3 RenderContext `render`/`revert` 收到統一上下文:`{ root, options, finder, locale }`。各 pass 共用同一 `finder` 實例與 locale 解析,確保 avoid 名單、邊界規則、語言判定一致。 ### 3.4 遍歷聚合(減少 pass 遍歷次數,I9) 樸素做法是每個 pass 各跑一次 `TreeWalker`,9 個 pass 即 9 次全樹走訪。為此引入 **遍歷階段(phase)**:相容之 pass 共享單次走訪。Pass 介面拆為 `collect(node, ctx) → targets[]`(純判定、不變更 DOM)與 `apply(targets, ctx)` (批次變更),執行器: ``` for phase in phases: walk(root) 一次:對每個文本節點,依序呼叫該 phase 內各 pass 的 collect() 依 order 對收集到的 targets 做 apply()(兩階段:先收集後變更,避免遍歷中改 DOM) ``` 階段劃分(把 ~9 次走訪壓到 3 次): | phase | 內含 pass | 走訪 | 備註 | |---|---|---|---| | P0 文字修正 | biaodian(opt-in) | 1 次 | 唯一可能改寫文字者,須最先 | | P1 行內結構 | charify/jiya 標記/jinze/spacing/longWord/emphasis | 1 次 | 共享走訪,`apply` 依 order | | P2 版面後處理 | hanging | 0 次走訪+layout | 需最終行盒,掛 ResizeObserver | > 聚合僅是**執行優化**:邏輯上仍「一功能一 pass」、可獨立開關與 `revert`(I6); > 只是 P1 諸 pass 的 `collect` 在同一走訪內完成。若某 pass 不相容(需獨立走訪), > 退化為自跑一次,不影響正確性。 ### 3.5 v1 向下相容層(I8,`src/compat/v1.ts`) 導出 `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 明列。 ### 3.6 作用域與分塊功能選擇(I10) **作用域限定**——控制「在頁面何處生效」,避免污染無關內容: - `scope.root`:處理根(節點或選擇器),預設 `document.body`;`render(root)` 可逐次 覆寫;允許多實例/多根並存。 - `scope.include`:**啟用選擇器**(opt-in 模式)。設定後,**僅處理命中該選擇器之 子樹**(如 `".juzhen, [data-juzhen]"`),其餘一概不動——此即「限定作用範圍」。 - `scope.avoid`:排除選擇器,與內建 avoid 合併。 - **CSS 同域**:`juzhen.css` 之視覺規則一律收斂於同一啟用選擇器(如 `.juzhen` 下), 與 JS 作用域一致,杜絕樣式外溢(呼應 I2、§9.1 之禁用層亦限於此域)。 **分塊功能選擇**——不同塊啟用不同功能集: - **宣告式(屬性,就近繼承)**: - `data-juzhen="spacing jiya"`——白名單:此塊內僅啟用列出之功能。 - `data-juzhen-off="jiya hanging"`——黑名單:此塊停用列出者,其餘繼承。 - `lang`——驅動全角/開明(正交於功能開關)。 - **程式式**:`options.blocks: [{ selector, features }]`,選擇器 → 功能集。 - **解析(per-node 閘)**:對每個節點,向上找最近帶功能指令之祖先(屬性或命中 `blocks` 選擇器者)決定其**有效功能集**,無則用全域 `options`;render 期間以 元素為鍵快取。每個 pass 之 `collect(node)` 先問 `featureEnabledFor(node, name)`, 否則跳過——**故分塊閘只是單次走訪內的 per-node 判定,不增遍歷、不破壞聚合 (§3.4)**。 - **與 revert 之關係**:`revert` 依標記移除某功能產物,**不受當前閘狀態影響**,故 「關閉某功能 + revert」恆能乾淨還原(I6)。 --- ## 4. 核心基座(`src/core/`) ### 4.1 `finder.ts`——文本遍歷引擎(取代 Fibre.js) 職責(所有 pass 的共同地基): - **遍歷**:`TreeWalker(SHOW_TEXT)` 走 `root` 下文本節點。 - **avoid**:跳過 avoid 名單之子樹(內建:`code/kbd/pre/samp/textarea/script/ style/SVG/[contenteditable]/[data-jz-skip]`,可由 `scope.avoid` 追加)。 - **scope.include**:若設啟用選擇器,僅走訪命中之子樹(opt-in,§3.6)。 - **featureEnabledFor(node, name)**:解析該節點之有效功能集(§3.6),供各 pass 之 `collect` per-node 閘用;結果以元素為鍵快取。 - **charify**:把符合條件之字元/字串包進指定 `jz-*` 元素(兩階段:先收集目標、 後變更,避免遍歷中改 DOM 失效)。 - **邊界感知 replace(I7)**:對「跨相鄰文本節點、可跨樣式行內元素」的邏輯字串 做正則 find,回呼決定如何包裝/插入標記。樣式行內元素(`strong/em/a/span/…`) 透明穿越;block/pill/avoid 中斷邏輯串。此即 v1 Pass A「剝離樣式收集邏輯文本 再反映射」之一般化,提升為可重用引擎。 設計要點:finder 不知道任何具體排版規則;它只提供「在何處、跨何種邊界、對哪些 字元」的遍歷與安全變更原語。規則由各 pass 以 unicode 表 + 回呼注入。 **「行內透明」原則之一致性(下游 Bug 修復)**:「邏輯文本應透明穿越之行內元素」一語 須在所有遍歷原語中**口徑一致**,否則同一段文本在不同原語下被切成不同邏輯串、生成相 互矛盾之結果。`isTransparentInline` 為單一真相源,列入:樣式行內標籤、`jz-hws`/ `jz-jinze`(wrap 群組)、**`jz-char`/`jz-inner`(標點原子)**。後者必須透明,否則 `adjacentLogicalChar` 之 `descend` 遇標點原子既不進入亦不終止、而是**跳過**它續找更前 之兄弟——`漢字。` 經禁則綁定後,pill 之左鄰字應為「。」(標點,非 pill 鄰字、不 補間隙),舊邏輯卻越過 `jz-char` 取到「字」誤補 `jz-hws`。修復後與 `collectRuns` 之既 有透明遞迴對齊。同理 `lineedge` 之 `edgeRect` 攀爬集由 `options.finder.styleInlines` **派生**(非硬編碼子集),免遺漏 `ins`/`del`/`sub` 等致行端誤判(T4)。 ### 4.2 `unicode.ts`——字元分類與規則表 集中所有 Unicode 範圍與標點子類,供各 pass 共用,杜絕分類散落不一致: - **CJK**:表意文字、假名、注音符號等範圍(沿用並擴充 v1 之 `PANGU_CJK`)。 - **西文**:拉丁字母、數字、常見符號(v1 之 `AN_CHARS` 等)。 - **標點(biaodian)子類**(對應 CSS class): - `bd-open` 開始括號/引號:`「『(《〈【〖〔[{` 等 - `bd-close` 結束括號/引號:`」』)》〉】〗〕]}` 等 - `bd-cop` 句逗號(comma/period,字面偏移因簡繁而異):`,。.` - `bd-stop` 句末其餘:`!?;:⁇‼` 等 - `bd-sep` 分隔/間隔號:`·・‧` - `bd-liga` 連續省略/破折:`…—` 等 - `quote` 彎引號:`“”‘’` - **規則正則**:中西間隙邊界、擠壓相鄰組合、禁則對象等,皆以此表組裝。 **規則集抽象(`Ruleset`/`charClass`,可配置)**:字符分類不再各 pass 硬編碼,而統一為 一個**規則集**——`{ cjk, ansAfter, ansBefore, biaodian{6 子類} }`,各為 regex 字元類片段。 `DEFAULT_RULESET` 即內置默認(上述 CJK/西文/BD_* 值)。消費端經 `options.charClass` (`{ cjk?, western?, biaodian? }`)**擴充**,`resolveRuleset(override)` 以**追加**語義合併 為最終規則集,置於 `ResolvedOptions.ruleset`。各 pass 由此取分類: - `spacing`:`makeRules(ruleset)` 由 `cjk`/`ansAfter`/`ansBefore` **per-instance 編譯** 全部間隙正則(createJuzhen 各一次,非每 block);`ALNUM`/`ALPHA`/運算符/括號等結構性 ASCII 集為固定常數、不隨規則集變。 - `jiya`:`classifyBiaodian(ch, ruleset.biaodian)` 決定 charify 與 bd-* 子類。 - `jinze`:`anyCjk = RegExp(ruleset.cjk)` 決定詞元邊界之 CJK 判定。 **設計取捨**:① **向下兼容**——不傳 `charClass` 則 `ruleset === DEFAULT_RULESET`,行為與 重構前完全一致(現有測試全綠)。② **追加語義(v1)**——擴充內置類(把未分類字符如英文全角 句號 `.`U+FF0E 歸入、或納新符號);不支援重分類/移除內置字符(範圍式集合難以挖洞、且需 求罕見,列為後續)。③ 結構性 ASCII 不開放(非「字符類型」)。 > 借鑑 Han 之 `regex/typeset.js`、`regex/unicode.js` 與 Heti 之分類字集,重寫為 > 現代 TS 常數表+可配置規則集。 ### 4.3 `locale.ts`——語言與全角/開明政策 - `effectiveLang(node)`:最近祖先 `lang`(小寫),無則回退 `options.lang.default`。 - `classify(lang)`:歸為 `zh-Hans` / `zh-Hant`(含 `zh-TW`/`zh-HK`)/ `ja` / 其他; BCP-47 前綴邊界比對(`zh` 命中 `zh-Hant`,不命中 `zhx`)。 - `style(lang)`:回傳該語言之標點寬度政策——`quanjiao`(全角式)|`kaiming` (開明式)|`banjiao`(半角式)。預設:`zh-Hant`/`zh-HK` → 全角式;`zh-Hans` → 開明式;可由 `options.lang.policy` 覆寫(即你要的「依 lang 自由選擇」)。 CSS 端對應以 `:lang()` 選擇器落實同一政策(I2/I3)。 ### 4.4 `dom.ts`——`jz-*` 元素工廠與還原 `createEl('jz-char', {class, attrs})`、`wrap(node, el)`、`splitAt`、`insertMarker`、 冪等標記(`data-jz=""`)、`revertPass(name, root)`(移除該 pass 之產物、 解包 `jz-*`、合併回文本節點並 `normalize()`)。 `data-jz-kind` 區分產物還原方式:`marker`(純注入,移除)、`wrap`(包裝原內容,解包)、 **`splitoff`**(行內元素克隆切分之邊界詞元,見 `core/split.ts`/§6.4)。`revertPass` 分 **兩階段**處理:主迴圈逆序解包 marker/wrap,第二階段再把 splitoff 併回前兄弟(同標籤 原元素)——順序必要,否則 splitoff 之前兄弟可能仍是 wrap 殼(如 `jz-jinze`)而非原元素。 --- ## 5. 渲染元素詞彙(`jz-*`) 自訂元素皆含連字號(合法 custom element 名),預設 `display: inline`(未知元素 天然 inline),語義中性,可被 `revert` 移除: | 元素 | 用途 | |---|---| | `jz-hws` | 中西間隙。`user-select:none`,copy-clean,justify 安全(§8) | | `jz-char` | 包裝單一標點/漢字,承載 `bd-*` class 與 `data-jz` 屬性 | | `jz-inner` | `jz-char` 內層字面容器,供 margin/letter-spacing 擠壓而不動外盒 | | `jz-cs` | 字間補償空白(連續/行端擠壓用,於行邊界折疊) | | `jz-jinze` | 禁則群組:綁定不可斷單元(亦為 justify 綁定載體) | | `jz-orphan` | 垂懸字避免群組:段末末 N 實義字 `nowrap`(§6.5.1) | | `jz-em` | 著重號載體 | | `jz-ruby`/`jz-rb`/`jz-rt` | ruby/注音 | class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`jiya`、 `jiya-consecutive`、`hangable`、`jinze-{tou,wei,touwei}` 等。 --- ## 6. 功能點逐項實作方案(對照 Han.css) 每項列出:Han 做法 → 我們做法 → 跨瀏覽器 → lang → 我們的調整 → 切片歸屬。 ### 6.1 中西間隙(HWS,漢字↔西文)— 切片 1 - **Han**:插入 `