# 聚珍(Juzhen) > 現代簡/繁中文網頁排版套件。Han.css 之現代化重寫,沿其功能版圖,並針對 > 數種情況作調整:**複製乾淨**(中西間隙不入剪貼板)、**依 `lang` 全角/開明 > 自動切換**、**`justify` 下句末標點只佔一份間距**、**跨瀏覽器一致** > (Chrome/Edge/Safari/WebKit/Firefox)。 倉庫名仍為 `cjk-autospace`;公開 API 為 `createJuzhen` / 全域 `Juzhen`;注入之 自訂元素前綴 `jz-`。完整設計見 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。 > 本版(2.x)為從零重寫。v1 之 `createCjkAutospace` 相容層**已移除**(最後一批 > v1 消費端已遷移 v2 API);v1 原始碼保存於 `v1-autospace` 分支。 --- ## 安裝與構建 ```sh npm install npm run build # esbuild → dist/juzhen.{mjs,iife.js,css};tsc → dist/index.d.ts npm test # jsdom DOM 正確性測試 ``` 產物(`dist/`,已入庫): | 檔 | 用途 | |---|---| | `dist/juzhen.mjs` | ESM,供打包器/TypeScript | | `dist/juzhen.iife.js` | IIFE,全域 `Juzhen`,原生 ` ``` ### ESM/TypeScript ```ts import { createJuzhen } from "cjk-autospace"; import "cjk-autospace/css"; const jz = createJuzhen({ lang: { default: "zh-Hant" } }); jz.render(document.querySelector("main")); ``` **JS 與 CSS 缺一不可**:JS 只注入 `jz-*` 結構,所有視覺尺寸(間隙寬度、擠壓量、 `justify` 原子化、字體堆疊)都在 `juzhen.css`。 ### 動態頁面:增量恢復(局部變化免全樹重掃) `render(root)`/`revert(root)` **皆可傳入局部 Element**——頁面局部變化時,只重處理受影響 之子樹即可,成本 O(該塊) 而非 O(全文)(實測編輯 400 段中之一段,局部恢復較全樹快數百倍)。 ```ts const jz = createJuzhen(); jz.render(document.body); // 初次全樹 // 局部變化後(手動): jz.rerender(changedBlock); // = revert(changedBlock) + render(changedBlock) jz.render(newlyAppendedBlock); // 新增之塊:直接 render(無須 revert) // 自動(opt-in):觀察 DOM,內容/分類屬性變化時自動只重處理受影響之最近塊。 jz.observe(document.body); // 之後 DOM 變動即自動增量恢復 // jz.disconnect(); // 停止觀察 ``` - **`rerender(root?)`**:`revert` 後 `render` 同一(子)樹(便利方法)。 - **`observe(root?)`/`disconnect()`**:以 `MutationObserver` 自動增量。juzhen 自身之注入 於處理期間暫停觀察、**絕不自觸發**;新增之塊直接 render(免全容器重掃)。需 `MutationObserver` (瀏覽器/jsdom 有;無則 no-op)。 - **`invalidate(root?)`**:清該子樹之內部快取。**`revert` 已自動失效其子樹**,故僅在「改了既有 元素之分類相關屬性(`class`/`data-jz-*`/`lang` 等)**但不走 `revert`**」時才需顯式調用,以 免下次 render 沿用陳舊分類。內容(子節點/文本)變化不改分類、無需 `invalidate`(但須 `revert`+`render` 該塊方能重處理)。 > **元素變化之正確性**:實例內部對「元素分類(透明/pill/隔離/avoid)/功能級別/功能開關」 > 快取以加速跨 render 復用(快取隨實例持久、非每次 render 重建)。**改變既有元素之上列屬性後, > 須經 `revert`/`rerender`/`invalidate`/`observe` 任一使該子樹快取失效**,否則沿用舊分類。 > 新增/移除元素與純內容編輯無此顧慮(新元素重新計算、移除者自動回收)。 > **`revert` 之範圍**:`revert` 乾淨移除聚珍注入之全部結構(`jz-*`/`data-jz-*`),DOM 回到**邏輯 > 等價**態;但**不還原** `spacing` 依契約規範化之贅餘作者空格(CJK↔西文吸收為 margin 間隙、 > CJK↔CJK 剝除;見上表「中西間隙」)。即 `render→revert` 對含 CJK↔西文作者空格之源,還原後語義 > 一致、無 `jz-*` 殘留,唯該空格已正規化(此差異穩定、不隨反覆 render/revert 累積)。若消費端需 > 保留原始作者空格以供編輯,應自存原文、勿以 `revert` 當「還原到未處理源碼」。 --- ## 切片 1 已實作功能 | 功能 | 說明 | |---|---| | **中西間隙** `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;**唯句末點號接開類為句界**——`。「`/`?「` 保留句末點號右空白、僅收開類,全角式與開明式皆 1.5em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 | | **禁則** `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 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器),原生 `hanging-punctuation` 為唯一正解但僅 Safari 支援、且與本庫 inline-block 原子互斥。`hanging` 選項仍被接受(API 相容)但**無任何效果**;相關程式碼/CSS 已全數刪除,原生路徑屬遠期「原生管線」新功能。 | | **功能分級** `level` | 功能分文本級(中西間隙、標點擠壓——任意文本片段適用)與段落級(禁則、垂懸字、懸掛、長詞——需行/段布局)。塊級元素預設全功能;`data-jz-level="text"` 子樹或選項 `level:{text:"選擇器"}`(如標題/按鈕)僅跑文本級。 | | **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 | 後續(架構已預留,不返工):標點修正(簡↔繁)、著重號、注音/Ruby。 --- ## 選項 ```ts createJuzhen({ lang: { default: "zh-Hant", // 無 lang 祖先時之預設 style: "kaiming", // 全域政策一鍵覆寫(quanjiao|kaiming|banjiao) policy: { "zh-Hant": "quanjiao", "zh-Hans": "kaiming" }, // 逐 lang(最高優先) }, scope: { root: document.body, // 處理根;render(root) 可逐次覆寫 include: ".prose", // 啟用選擇器(opt-in):僅處理命中子樹 avoid: "blockquote.raw", // 追加排除選擇器 }, spacing: true, longWord: { minLength: 6, lang: "en" }, jiya: true, // 或物件:{ halfWidth: "halt" | "margin", bias } jinze: true, orphan: true, // 垂懸字避免(預設開);或 { chars: 2 };false 關 // 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: [ // 分塊功能選擇 { selector: ".code-comment", features: [ "spacing" ] }, ], }); ``` ### 全部選項速查(含少見但強力者) 完整選項一覽。**「標籤名」型別只吃大寫標籤名陣列**(精確比對 `nodeName`),與 `pillSelector`/`isolateSelector`/`scope.*`(**CSS 選擇器**,可用 class/屬性)**不同機制**, 勿混。斜體「詳見」指向本文檔展開該項之小節。 **語言與擠壓政策**(優先序:**逐元素** `data-jz-style`/`stylePolicy` > `policy` > `style` > 逐語言內建) | 選項 | 型別 | 預設 | 作用 | |---|---|---|---| | `lang.default` | string | `"zh-Hant"` | 無 `lang` 祖先時之預設語言標籤 | | `lang.style` | `quanjiao|kaiming|banjiao` | 逐語言內建(繁→全角、簡→開明) | **全域政策一鍵覆寫** | | `lang.policy` | `Record` | `{}` | 逐 lang 政策 | | `lang.stylePolicy` | `{selector,style}[]` | `[]` | **逐元素樣式覆寫**(選擇器命中之子樹用指定樣式;如 `[{selector:"h1,button,.ui",style:"banjiao"}]`);配合 `data-jz-style` 屬性,優先於 lang | **作用域** | 選項 | 型別 | 預設 | 作用 | |---|---|---|---| | `scope.root` | Element|選擇器 | `document.body` | 處理根(`render(root)` 可逐次覆寫) | | `scope.include` | 選擇器 | `null` | opt-in:僅處理命中此選擇器之子樹 | | `scope.avoid` | 選擇器 | `null` | 追加排除子樹 | **功能開關 / 對齊 / 分級 / 分塊**(詳見「已實作功能」表及各節) | 選項 | 型別 | 預設 | 作用 | |---|---|---|---| | `spacing` | bool | `true` | 中西間隙 | | `longWord` | bool|`{minLength,lang}` | `true`(`{6,"en"}`) | 長英文詞斷字 | | `jiya` | bool|`{halfWidth,bias}` | `true` | 標點擠壓(*詳見「標點半形」*) | | `jinze` | bool | `true` | 禁則避頭尾 | | `orphan` | bool|`{chars}` | `true`(`{chars:2}`) | 垂懸字避免 | | `justifyAtoms` | bool | `true` | inline-block 對齊原子(分頁器設 `false`) | | `level.text` | 選擇器 | `null` | 命中子樹僅跑文本級(*詳見「功能分級」*) | | `blocks` | `{selector,features}[]` | `[]` | 分塊功能選擇 | | `charClass` | `{cjk,western,biaodian}` | — | 追加字符分類(*詳見「自定義字符分類」*) | **行內三分類與排除**(*詳見「行內元素三分類」*) | 選項 | 型別 | 預設 | 作用 | |---|---|---|---| | `pillSelector` | CSS 選擇器 | `"code, kbd"` | 整體元素(內部不可見、兩端補隙) | | `isolateSelector` | CSS 選擇器 | `"mark"`(`""` 停用) | 隔離元素(內部照常、兩端補隙) | | `styleInlines` | 標籤名[] | 27 個標準行內標籤 | **透明穿越白名單**(未列之行內標籤=邊界) | | `blockTags` | 標籤名[] | 36 個塊標籤 | 塊級邊界集 | | `skipTags` | 標籤名[] | `PRE,SAMP,TT,VAR,SCRIPT,STYLE,TEXTAREA,INPUT` | avoid 標籤集(**不含 CODE/KBD**,由 pill 涵蓋) | | `skipAttribute` | string | `null` | 自訂「跳過」屬性名(命中即 avoid) | | `isSkipped` | `(node)=>bool` | `null` | 自訂跳過謂詞(函數級排除鉤子) | **HTML 屬性**(就近繼承,最近祖先勝) | 屬性 | 值 | 作用 | |---|---|---| | `data-jz-level` | `text|paragraph` | 逐子樹功能分級 | | `data-jz-style` | `quanjiao|kaiming|banjiao` | 逐子樹標點樣式覆寫(近祖先勝、優先於選擇器與 lang) | | `data-juzhen` | 功能名(空格分隔) | 白名單:僅列出之功能生效 | | `data-juzhen-off` | 功能名 | 黑名單:停用列出之功能 | | `data-jz-skip` | (存在即可) | 標記元素為 avoid(內外皆不處理) | | `data-jz-halfwidth` | `halt|margin` | 逐子樹半形渲染模式 | **預留、當前無效果**:`hanging`(已移除,見下);`biaodian`/`emphasis`/`ruby`(架構預留切片 #7/#8)。注意頂層 `biaodian`(功能開關)與 `charClass.biaodian`(標點子類擴充表)**同名異義**。 ### 逐元素標點樣式(`data-jz-style`/`stylePolicy`) 標點樣式(`quanjiao` 全角式/`kaiming` 開明式/`banjiao` 半角式)預設由 `lang` 決定,但可 **逐元素/逐範圍覆寫**——典型:**非段落/UI 元素(標題、按鈕)用半角式,段落用全角/開明**。 ```ts // 選擇器批量:h1/h2/button 及 .ui 子樹用半角式,其餘(段落)走 lang createJuzhen({ lang: { default: "zh-Hant", // 段落 → 全角式(繁) stylePolicy: [ { selector: "h1, h2, button, .ui", style: "banjiao" } ], }, }); ``` ```html

標題:半角標點更緊湊

段落沿用 lang 之全角/開明式。

``` 優先序:**逐元素覆寫**(`data-jz-style` 屬性/`stylePolicy` 選擇器,**近祖先勝、同層屬性優先於 選擇器**)> `lang.policy` > `lang.style` > 內建逐語言預設。屬性值非法(拼錯)則忽略、回退 lang。半角式渲染沿用既有 `halt`/`margin` 機制(見下節),本功能只做「按元素選樣式」、無新增 渲染邏輯。 ### 行內元素三分類:透明/隔離/整體 行內元素對排版有且僅有三種口徑。**邏輯文本流之遍歷原語**(中西間隙、擠壓相鄰、禁則綁定、 垂懸字)**共用同一判定** `isTransparent`;未在 `styleInlines` 之**未知行內標籤**(自訂元素/ web component 等)採**嚴格白名單**、一律當**邊界**(語義同隔離、內部自成脈絡),透明須顯式 登記 `styleInlines`。(另兩層各有其正當之邊界、刻意不併入:結構克隆切分之安全白名單 `canSplit`、行端物理版面測量 `lineedge`,詳見 `ARCHITECTURE.md`。) | 分類 | 選擇器 | 邊界 | 內部 | 典型 | |---|---|---|---|---| | **透明** | `styleInlines`(預設 27 個常見樣式標籤;**未列之行內標籤=邊界**) | 穿越——間隙/相鄰/run 皆延續 | 照常處理;禁則可**克隆切分**(最小綁定) | `strong`/`em`/`b`/`i`/`a`/`span`(自訂元素須自行登記) | | **隔離** | `isolateSelector`(預設 `"mark"`) | 斷 run/斷相鄰/不可切分;**兩端**與 CJK/西文相鄰時補 `jz-hws` 間隙 | **照常處理**(自成脈絡:內部標點照擠、內部中西照補隙) | 帶背景色之 ``、badge 類盒樣式行內 | | **整體** | `pillSelector`(預設 `"code, kbd"`) | 同隔離(邊界斷、兩端補隙) | **完全不可見**——不 charify、不插間隙、不切分、不下潛 | `code`/`kbd`、行內公式 `.katex`、引用標 `.cite-ref` | 要點: - **分類優先序(單一判定 `finder.category`)**:一元素恰屬其一,多選擇器同命中時依序短路定案—— `avoid > pill > isolate > block > transparent > opaque`(`opaque`=未登記之未知行內標籤, 當隔離式邊界)。故帶 `data-jz-skip` 之 pill/isolate 判為 avoid;同命中 pill 與 isolate 判為 pill。 邏輯文本流各原語(`collectRuns`/`jiyaAdjacency`/`adjacentLogicalChar`)皆據此**同一分類**分派。 - **判定以元素為準、非標籤名**:KaTeX 全樹皆 ``,但 `.katex` 命中 `pillSelector` 即為整體邊界——舊版按標籤名判透明曾整棵下潛、把公式內側末字連同殼層克隆切出(肢解)。 - **pill 行為覆蓋 isolate**:兩者邊界口徑完全相同(兩端補隙、斷 run/斷相鄰、不可切分, 同一段邊界處理代碼),差異僅在內部;同一元素同時命中兩選擇器時 **pill 勝出**(內部不可見)。 - **禁則之整體綁定**:pill 緊鄰禁則標點時(`…$公式$,`),整個 pill 作為原子鄰字單位 綁入 `jz-jinze`(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。 - **avoid(排除)之七種來源**,任一命中即邊界、內外皆不處理:`isSkipped` 謂詞/`skipTags` 標籤集/SVG/`contentEditable`/`data-jz-skip` 屬性/`skipAttribute`(自訂屬性名)/ `scope.avoid` 選擇器。**以自身判定**(元素自身命中即是,非僅查祖先);一次性檢查用 `isAvoided`(自身或祖先之組合判定)。**`skipTags` 預設不含 `CODE`/`KBD`**——二者已由 `pillSelector` 以 pill 涵蓋(三分類互斥;若把 `code` 移出 `pillSelector`,須自行加入 `skipTags`/`scope.avoid`,否則其內部將被當普通文本處理)。 - **行內公式/夾注引用宜列為 pill**:`pillSelector: "code, kbd, .katex, .cite-ref"`—— 獨立行內單位以 pill 邊界邏輯統一處理(兩側補隙、吸收作者空格、整體綁定)。 ### 分頁器(Paged.js 等)相容:`justifyAtoms` 預設 `jz-char`/`jz-jinze` 為 `inline-block`,作 `justify` 之單一對齊原子(句末標點 單份間距)。但**分頁器**(如 Paged.js)之對齊引擎會把每個 inline-block 邊界當伸縮點、 與 CJK inter-ideograph 疊加,於窄欄混排時字距失控、整段散架。此時設 `justifyAtoms: false`——`jz-char`/`jz-jinze` 改 `display:inline`(半形 `halt` 與禁則 `nowrap` 綁定均保留)。常規流式版面無需設定。 > 注意:`margin` 後備半形依賴 `inline-block`(`width:0.5em`),故 > `justifyAtoms:false` 與 `margin` 模式不可兼得——分頁器場景請用含 `halt` 之字型。 ### 分塊功能選擇(HTML 屬性,就近繼承) ```html

``` ### 功能分級:文本級 vs 段落級(`level`/`data-jz-level`) 功能分兩級: - **文本級**(中西間隙 `spacing`、標點擠壓 `jiya`):僅依字符鄰接,**任意文本片段**(單行 標題、按鈕、單一行內元素)皆適用且想要。 - **段落級**(禁則 `jinze`、垂懸字 `orphan`、標點懸掛 `hanging`、長詞 `longWord`):依賴 行/段布局(換行、行端、段末),對單行/非段落文本無意義甚至有害。 塊級元素(`p`/`div`/`li`/`h1`…)**預設段落級(全功能)**。把標題/按鈕/UI 文字標為 **文本級**(要間隙與標點半形、不要懸掛/避孤字/避頭尾): ```html

標題:只跑中西間隙與標點擠壓

``` ```ts createJuzhen({ level: { text: "h1,h2,h3,button,.ui" } }); // 選擇器一鍵標記 ``` ### 標點懸掛(`hanging`)— 已移除 原計劃之標點懸掛(行端標點凸出版心)經實測為**機制級死路、已移除**(負 margin 程式碼與 CSS 皆刪)。`hanging` 選項仍被接受(API 相容)但**無任何效果**——`hanging:true`/ `data-juzhen`/`blocks` 任一要求皆被靜默忽略,行端僅做半形擠壓。 **為何機制級不可行**(純 CSS 之寬度守恆矛盾):行端標點盒若保持非零寬,則「墨色出去多少、 版心內就空出多少」(行末實義字無法齊版心右緣);若收為零寬,則釋放一整字寬、斷行器把下一字 拉上本行與標點重疊(且 `justifyAtoms` 預設之 inline-block 下負 margin 被 justify 吸收)。 **墨色出去多少版心內就空多少;想不空就得讓斷行器重排這塊寬度,而 CSS hack 接觸不到斷行器**。 補償墊片、兩階段試錯都改變不了此恒等式。 唯一正解為原生 CSS `hanging-punctuation`,惟 **Chrome/Firefox 至今不支援**(僅 Safari),且 本庫之 `jz-char` 為 inline-block 原子、非文本,原生懸掛不作用——未來若做,須是繞開 `jz-char` 的「原生管線」新功能(`text-spacing-trim` + `hanging-punctuation`,按能力門控),與已移除之 程式碼無共享。詳見 `ARCHITECTURE.md §6.5.6`。 --- ## 標點半形:`halt` 預設與 `margin` 後備 開明/半角式把標點收成「半形」,有兩種渲染機制: | 機制 | 設定 | 原理 | 適用 | |---|---|---|---| | **`halt`**(預設) | `jiya: true` | 字型 OpenType `halt`,由字型按字形正確定位、不重疊 | 字型含 `halt`(Noto CJK/思源系列) | | **`margin`**(後備) | `jiya: { halfWidth: "margin" }` | `width:0.5em`+字身依墨色偏側溢出定位(以 CSS 重現 halt) | 字型**缺** `halt`(多數系統字型、GBK 老字型如方正書宋 GBK、PingFang) | > 為何需要後備:`halt` 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測 > **方正書宋 GBK 僅含 `vert`、無 `halt`** → halt 模式不擠;切 `margin` 後備後, > (開明式 zh-Hans)同句行寬 `24.0em → 19.7em`、單一句內點號 `1.0em → 0.5em`,半形生效。 > **句末標點含全角句點 `.`**:句末點號為 `。.!?`——除表意句號 `。`(U+3002)外納入 > 全角句點 `.`(U+FF0E,實測 Noto CJK 有 `halt`)。政策行為與 `。!?` 同:全角式與開明式 > 句末**保持全角 1em**;唯 banjiao(或與鄰標點連續被收)時 `halt` 收為半形 advance 0.5em。 > **`halt` 即字型之「挤壓意願」**:字型不提供某標點之 `halt`(其字形占滿字框、不宜挤半, > 如某些字型之全角 `?`、`〈〉`)時,聚珍**信任字型、保持全角不強挤**(優雅降級); > `margin` 後備僅用於**整體無 `halt`** 之字型,逐字型以 `jiya.bias` 調教。 ### 激活與粒度 模式由 **`data-jz-halfwidth` 屬性所在子樹**決定(CSS 後代選擇器),故粒度極靈活: - **全域**:`jiya: { halfWidth: "margin" }` → `render()` 於處理 root 自動掛屬性。 - **逐區塊/逐語言**:在 HTML 任一祖先手寫 `data-jz-halfwidth="margin"`(或 `"halt"`),即令該子樹用對應機制——無需另建實例。常見如「全頁 `halt`,某用 無 `halt` 字型之區塊掛 `margin`」,或反之(`margin` 頁中嵌一塊 `halt` 島)。 ```html
``` ### 機制與字形偏側表(`margin` 模式專用,可覆寫) `margin` 模式把標點 `jz-char` 之 inline-block 寬收為 **0.5em(真半形 advance)**,字身 (`jz-inner`)以 `overflow` 溢出該盒,再依**墨色偏側**左移、使墨色落於 0.5em 盒內、 空白溢向外側——即以 CSS 盒模型重現 `halt` 之「收字身寬+按字形定位」。相鄰兩標點各 0.5em、合佔 1em,無鄰接疊加或單側重疊問題。 偏側須知每字之墨色中心。聚珍內建**參考表**(依方正書宋 GBK 實測之 glyph ink 位): 簡體點號 `,、。;:!?` 偏左;繁體點號居中;彎引號 `“”‘’`、角括號 `「」`、尖括號 `〈〉` 墨色強偏一側(開→`right`、閉→`left`);圓/方/書名等括號 `()【】《》…` 墨色 近中→`center`。於 charify 標為 `jz-ink-{left,center,right}`。因字型字身分布各異,可逐 `lang` 逐字覆寫: ```ts createJuzhen({ jiya: { halfWidth: "margin", bias: { "zh-Hans": { ",": "center", "(": "right" }, // 覆寫參考預設 "zh-Hant": { "。": "center" }, }, }, }); // 偏側值(決定字身於 0.5em 盒內之左移): // left 墨偏左 → 不移(字身左對齊) // center 墨居中 → 左移 0.25em // right 墨偏右 → 左移 0.5em(右半入盒) // full 無可壓縮空白 → 不收、維持全形 ``` > `halt` 模式不讀偏側表(由字型負責定位)。偏側表僅在 `margin` 後備模式生效。 > `demo/fallback.html` 用方正書宋 GBK 對照 halt 失效 vs margin 生效。配字工作流見下節。 ### 字型適配工作流(margin 模式配字) 換用新字型時,依下列步驟確認半形渲染: 1. **驗 `halt`**:`python3 tools/measure-bias.py `(需 `pip install fonttools`)。 首行報該字型是否含 OpenType `halt`。 - **有 `halt`** → 直接用預設(`jiya: true`,halt 模式),無需配字,到此為止。 - **無 `halt`**(多數系統字型、GBK 老字型)→ 用 `margin` 後備,續下。 2. **取偏側建議**:同一工具同時量各標點 glyph 之墨色中心、印出建議偏側表 (`left|center|right|full`)與可貼入 `jiya.bias` 之 JSON。 3. **套用**:`createJuzhen({ jiya: { halfWidth: "margin", bias: <貼入 JSON> } })`; 或全用內建參考表(多數宋體系 GBK 字型與參考表相符,未必需覆寫)。 4. **目視微調**:以 `demo/fallback.html`(或自有頁面)對照原文,逐字檢查—— - 標點與相鄰字**重疊** → 該字墨色比量測更偏該側,偏側往「不移」方向調 (`right`→`center`→`left`)。 - 標點與內容**仍有空隙、不夠半形** → 往「多移」方向調(`left`→`center`→`right`)。 - 某標點根本無可壓空白(字面已滿全形)→ 設 `full`,不收。 5. **記錄**:把該字型之 `bias` 覆寫存入專案設定,作為該字型之適配結果。 > 偏側是「字身於 0.5em 盒內左移多少」的三檔量化(0/0.25/0.5em),量測值近門檻 > (如 0.6 介於 center/right)時,以目視為準。`halt` 字型走 OpenType 由字型精確 > 定位,無此配字需求——`margin` 配字僅為缺 `halt` 字型之務實近似。 --- ## CSS 基礎設定(消費端須知) JS 只注入 `jz-*` 結構,**所有視覺尺寸在 `juzhen.css`**——故 CSS 不可或缺。消費端 須備妥下列幾項: 1. **引入樣式(必須)**:``(或 ESM `import "cjk-autospace/css"`)。缺此則間隙/擠壓/justify 原子化全部失效。 2. **設定 `lang`(強烈建議)**:在內容容器標 `lang="zh-Hans"`/`"zh-Hant"`/`"ja"`, 驅動全角/開明政策與字形 `locl`。無 `lang` 時回退 `options.lang.default`。 3. **作用域類(用 `scope.include` 時)**:若設 `scope.include: ".juzhen"`,須給內容 容器加對應 class(如 `class="juzhen"`);`juzhen.css` 之 normalize 亦收斂於此域, 不外溢。 4. **長英文詞斷字**:`longWord` 包出 `` 後,須由消費端 CSS 提供 `hyphens: auto`(含 `-webkit-hyphens`)才會原生斷詞顯示連字符。 5. **`justify`(選用)**:欲兩端對齊則自行設 `text-align: justify`;句末標點單份間距 與行末去隙由聚珍處理,無需額外設定。 6. **字型由消費端負責(聚珍不管字型)**:聚珍**不**內建任何 `font-family`/字型堆疊 ——逐語言字型完全由消費端 CSS 決定。聚珍只確保原子(`jz-*`)以 `font: inherit` 繼承消費端字型、絕不強加,並顯式保留 `locl`(見下「字型與 locl」一節)。半形擠壓 依字型 OpenType `halt`:含 `halt` 之字型(Noto CJK/思源)預設即可;**缺 `halt` 之字型**(如方正書宋 GBK)須改用 `jiya.halfWidth:"margin"` 後備,並按字型以 `jiya.bias` 逐字微調墨色偏側(見「字型適配工作流」)。 7. **自動處理、無需消費端介入**:`juzhen.css` 已在 `.juzhen` 範圍內以 `@supports` 關閉 Blink 原生 `text-spacing-trim`/`text-autospace`(避免與託管擠壓重複作用), 並令注入間隙 `user-select: none`(複製乾淨)。 8. **段落屬性照常設、原子透明**:聚珍把標點拆成 inline-block 原子,但已確保這些原子對 消費端段落樣式透明——可在段落正常設 `text-indent`(首行縮排)、`hanging-punctuation`、 `text-decoration`(下劃線)等;庫內已歸零會被原子重複套用之繼承屬性、並令原子承接段落 裝飾,故首行縮排正常、下劃線連續不斷,無需任何額外配置(機制見 ARCHITECTURE「原子透明 原則」)。 --- ## 字型與 locl(消費端設定) 聚珍**不管字型**;逐語言字型由你以 CSS 設定。推薦範式——逐 `lang` 設 `font-family`, 掛在 `.juzhen` 或帶 `lang` 之容器(**勿**直接設 `jz-inner`/`jz-char`,它們靠繼承取字型): ```css .juzhen:lang(zh-Hans), .juzhen [lang]:lang(zh-Hans) { font-family: "Noto Serif SC", "Source Han Serif SC", serif; } .juzhen:lang(zh-Hant), .juzhen [lang]:lang(zh-Hant) { font-family: "Noto Serif TC", "Source Han Serif TC", serif; } .juzhen:lang(ja), .juzhen [lang]:lang(ja) { font-family: "Noto Serif JP", serif; } ``` 要點: - **字型須含 OpenType `halt`** 才能半形擠壓;否則用 `jiya.halfWidth:"margin"` + 按字型 以 `jiya.bias` 逐字微調墨色偏側(見「字型適配工作流」)。 - **locl(地域字形)**:泛 CJK 字型(思源/Noto CJK)以 `locl` 依 `lang` 選地域字形 (如「直/海/骨」之簡繁日差異)。聚珍**不改寫 `lang`、不關閉 locl**,故只要你 ① 正確標 `lang`、② 用支援 locl 之字型,locl 即正常。body 文本之 locl 全由你的字型+ `lang` 決定;標點(`jz-inner`)之 `halt` 已顯式併入 `locl`,地域標點字形不致失。 - 若只想全語言同一字型,直接 `.juzhen { font-family: … }` 即可(無需逐 `lang`)。 --- ## 自定義字符分類(`charClass`) 每個字符的分類(中文/西文、全寬標點各子類)抽象為**規則集**:內置默認規則集即聚珍 的分類表,消費端可經 `options.charClass` **擴充**,以覆寫 `spacing`/`jiya`/`jinze` 等規則對特定字符之判定。語義為**追加**——把當前未分類之字符、或新符號歸入對應類。 ```js createJuzhen({ charClass: { cjk: "〇", // 追加為中文(CJK):與西文邊界補中西間隙、禁則/justify 視為單字 western: "★☆", // 追加為西文(半寬標點亦歸此):與 CJK 邊界補間隙 biaodian: { // 追加為全寬標點,按子類(jiya charify/擠壓、禁則避頭尾) stop: ".", // 句末(。!?類) pause: "", // 句內(,、;:類) open: "", close: "", // 開/閉括號引號 middle: "", liga: "", // 間隔號/省略破折 }, }, }); ``` **典型用例**:英文全角句號 `.`(U+FF0E,有別於中文句號 `。` U+3002)預設**未分類** ——不被 charify、不挤壓、與 CJK 間不補隙。若內容用 `.` 作句末標點,以 `charClass: { biaodian: { stop: "." } }` 歸為全寬句末標點,即比照 `。` 處理(開明式挤半形、 全角式佔全形、行首避頭尾)。 要點: - **向下兼容**:不傳 `charClass` 時,分類與行為與內置默認**完全一致**。 - **追加語義(v1)**:擴充內置類;**不**支援把內置字符「重分類/移除」(屬罕見需求,後續再議)。 勿把同一字符同時加入相衝之兩類。 - **結構性 ASCII**(字母數字 `A-Za-z0-9`、運算符 `+-*/` 等)為固定集、不開放覆寫——它們 服務於運算符/複合詞等內部規則,非「字符類型」。 - `margin` 後備模式下若新增開/閉括號,其墨色偏側未必命中內建參考表,可一併以 `jiya.bias` 指定(見「字型適配工作流」)。 --- ## v1 向下相容 — 已移除 v1 相容層(`createCjkAutospace` 導出、IIFE 之 `globalThis.createCjkAutospace` 別名、`autospaceClass` 選項)**已隨最後一批 v1 消費端遷移完成而移除**。既有消費端 請改用 `createJuzhen(options).render(root)` v2 API;v1 原始碼保存於 `v1-autospace` 分支。 --- ## 目視驗證 `demo/index.html` 載入 `dist/` 產物,簡繁並列、可切 `justify`、附 copy-clean 測試區。 `demo/fallback.html` 以**方正書宋 GBK**(無 `halt`)對照 `halt` 模式失效 vs `margin` 後備生效。`demo/resize-check.html` 為**可拖曳容器**檢查頁:拖動方框寬度,驗證中西間隙在 各寬度下恆穩(含行末去隙之自我應驗、pill 跨行右隙、貨幣/拼音/西里爾外文補隙、全角省略號 與各式中文標點無誤隙),可切 `justify` 與「顯示間隙」描邊。版面相關結論(justify 單份間距、 擠壓視覺量、行端、`inline-block` 基線、間隙隨寬度之穩定性)須於 Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。 --- ## 授權 MIT。借鑑 Han.css/Heti 之思路與分類表(MIT),成品自含、不依賴其執行期程式碼。