聚珍(Juzhen)是現代簡/繁中文網頁排版套件,Han.css 之現代化重寫,沿其功能版圖,並針對四點作調整:複製乾淨(中西間隙不入剪貼板)、依 lang 全角/開明自動切換、justify 下句末標點只佔一份間距、跨瀏覽器一致。倉庫名仍為 cjk-autospace;公開 API 為 createJuzhen /全域 Juzhen;注入之自訂元素前綴 jz-。
本版(2.x)為從零重寫。v1 之 createCjkAutospace 相容層已移除(最後一批 v1 消費端已遷移 v2 API);v1 原始碼保存於 v1-autospace 分支。設計契約見 ARCHITECTURE.md。
安裝與構建
npm install
npm run build # esbuild → dist/juzhen.{mjs,iife.js,css};tsc → dist/index.d.ts
npm test # jsdom DOM 正確性測試
產物(dist/,已入庫):juzhen.mjs(ESM,供打包器/TypeScript)、juzhen.iife.js(IIFE,全域 Juzhen,原生 <script> 直跑)、juzhen.css(合併樣式,必須引入)、index.d.ts(型別宣告)。
用法
原生 <script>
<link rel="stylesheet" href="dist/juzhen.css">
<script src="dist/juzhen.iife.js"></script>
<script>
const jz = Juzhen.createJuzhen({ scope: { include: ".prose" } });
jz.render(); // 對 document.body 套用(僅命中 .prose 子樹)
// jz.revert(); // 還原
</script>
ESM/TypeScript
import { createJuzhen } from "cjk-autospace";
import "cjk-autospace/css";
createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("main"));
jz-* 結構,所有視覺尺寸(間隙寬度、擠壓量、justify 原子化、字體堆疊)都在 juzhen.css。缺 CSS 則全部失效。已實作功能
| 功能 | 說明 |
|---|---|
中西間隙 spacing | CJK↔西文邊界,margin 模型:包裹左側字、以 margin-right 留隙,不注入空白字元 → textContent 完全不變、複製緊湊、justify 不暴增、行首乾淨;行末由 gapTrim layout pass 去隙以齊右。含跨樣式邊界與 pill/隔離元素邊界(code/kbd/.katex 與 mark,兩端補隙口徑一致;pill 緊鄰標點時穿越 jz-jinze 仍補間隙)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 |
長英文詞 longWord | [A-Za-z]{6,} 包入 <span lang="en">,配合消費端 CSS hyphens:auto 原生斷詞(DOM 無連字符 → 複製不污染)。 |
標點擠壓 jiya | 標點 charify 為 jz-char + bd-* class。三機制:baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠)+行內連續擠壓+行端擠壓。半形預設以字型 OpenType halt 渲染;缺 halt 字型可切 margin 後備模式(見下)。 |
禁則 jinze | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末);只隔離邊界詞元,長引文中段仍可斷。邊界字落於透明行內樣式元素(strong/em 等)內時克隆切分只綁邊界詞元(避免整串入 nowrap 致行末整體掉行),克隆標 data-jz-kind="splitoff"、revert 時併回;pill/隔離/avoid 不可切、整元素綁定——pill(如行內公式 .katex)作為原子鄰字單位整體綁入 jz-jinze,絕不下潛肢解(見「行內元素三分類」)。亦為 justify 綁定載體。 |
垂懸字避免 orphan | 段末行 CJK 實義字 ≥ N(預設 2;標點不計)。方案 C(斷行層無測量):段末末 N 實義字+尾隨標點包入 jz-orphan(white-space:nowrap),整體不可斷 → 末行恒含之 ⇒ 末行 ≥ N。邊界字落於行內元素內時同樣克隆切分(避免長元素溢出容器)。預設開;段落級。 |
hanging | 已移除:純 CSS 機制為機制級死路(寬度守恆矛盾——墨色出去多少版心內就空多少,CSS 接觸不到斷行器)。原生 hanging-punctuation 為唯一正解但僅 Safari 支援、且與 inline-block 原子互斥。hanging 選項保留為 no-op(無效果)。詳見 ARCHITECTURE §6.5.6。 |
功能分級 level | 文本級(間隙、擠壓——任意文本適用)vs 段落級(禁則、垂懸字、長詞——需行/段布局)。塊級預設全功能;data-jz-level="text" 子樹或 level:{text:"選擇器"}(標題/按鈕)僅跑文本級。 |
| justify 單份間距 | 每個 jz-char 為 inline-block 原子,句末標點只貢獻一份對齊伸縮量。 |
後續(架構已預留,不返工):標點修正(簡↔繁)、著重號、注音/Ruby。
選項
createJuzhen({
lang: {
default: "zh-Hant", // 無 lang 祖先時之預設語言標籤
style: "kaiming", // 全域擠壓預設:quanjiao|kaiming|banjiao
policy: { "zh-Hant": "quanjiao" }, // 逐 lang 覆寫
stylePolicy: [ { selector: "h1,button,.ui", style: "banjiao" } ], // 逐元素樣式(優先於 lang)
},
scope: { root: document.body, include: ".prose", avoid: "blockquote.raw" },
spacing: true,
longWord: { minLength: 6, lang: "en" },
jiya: true, // 或物件:{ halfWidth: "halt"|"margin", bias }
jinze: true,
orphan: true, // 垂懸字避免(預設開);或 { chars: 2 };false 關
// hanging:已移除、無效果(純 CSS 機制級死路,詳見 §6.5.6)
level: { text: "h1,h2,h3,button,.ui" }, // 此選擇器子樹僅跑文本級功能
justifyAtoms: true, // 分頁器下設 false 改 inline(見下)
blocks: [ { selector: ".code-comment", features: [ "spacing" ] } ],
});
擠壓政策優先序:逐元素 data-jz-style/stylePolicy > 逐 lang policy > 全域 style > 內建預設(繁→全角、簡→開明)。逐元素覆寫供「非段落/UI 元素用半角式、段落用全角/開明」等:屬性 data-jz-style="banjiao|kaiming|quanjiao"(標於元素或祖先)或選擇器 stylePolicy,近祖先勝、屬性優先於選擇器、非法值回退 lang。
分塊功能選擇(HTML 屬性,就近繼承)
<p data-juzhen="spacing jiya">…</p> <!-- 白名單:僅此二功能 -->
<p data-juzhen-off="jiya">…</p> <!-- 黑名單:停用 jiya,其餘繼承 -->
<div lang="zh-Hans">…</div> <!-- lang 驅動全角/開明 -->
標點半形: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+FF0E,異於表意句號 。 U+3002,Noto CJK 有 halt);全角式與開明式句末皆保持全角 1em,banjiao 挤半。連續標點對每一標點獨立判斷——若其有空白之側緊鄰另一標點即收半形:故 ,「 全角式與開明式皆 1em、。」 全角式 1.5em/開明式 1em;並透明穿越 strong/em 等行內邊界。句界例外:句末點號接開類(。「/?「)為句界,保留句末點號右側空白、僅收開類左側 → 全角式與開明式皆 1.5em(對稱於 。」);例外僅限句末點號+開類,句內點號 ,「 與雙括號 」「/》《 仍兩側皆收 1em。半角式句末點號由 baseline 已半形,故仍 1em(不回歸全寬)。激活與粒度
模式由 data-jz-halfwidth 屬性所在子樹決定(CSS 後代選擇器),故粒度極靈活:
- 全域:
jiya: { halfWidth: "margin" }→render()於處理 root 自動掛屬性。 - 逐區塊/逐語言:在 HTML 任一祖先手寫
data-jz-halfwidth="margin"(或"halt"),即令該子樹用對應機制,無需另建實例。常見如「全頁halt,某用無halt字型之區塊掛margin」,或反之(margin頁中嵌一塊halt島)。
<!-- 全頁 halt(預設),此塊改用無 halt 字型 → 局部 margin 後備 -->
<section data-jz-halfwidth="margin" style="font-family: '方正书宋_GBK'">…</section>
機制與字形偏側表(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。可逐 lang 逐字覆寫:
createJuzhen({
jiya: {
halfWidth: "margin",
bias: {
"zh-Hans": { ",": "center", "(": "right" }, // 覆寫參考預設
"zh-Hant": { "。": "center" },
},
},
});
// 偏側值(決定字身於 0.5em 盒內之左移):
// left 墨偏左 → 不移(字身左對齊)
// center 墨居中 → 左移 0.25em
// right 墨偏右 → 左移 0.5em(右半入盒)
// full 無可壓縮空白 → 不收、維持全形
字型適配工作流(margin 模式配字)
換用新字型時,依下列步驟確認半形渲染:
- 驗
halt:python3 tools/measure-bias.py <font.ttf>(需pip install fonttools)。首行報該字型是否含 OpenTypehalt。有則直接用預設(halt 模式),到此為止;無則用margin後備,續下。 - 取偏側建議:同一工具量各標點 glyph 之墨色中心、印出建議偏側表與可貼入
jiya.bias之 JSON。 - 套用:
createJuzhen({ jiya: { halfWidth: "margin", bias: <貼入 JSON> } });多數宋體系 GBK 字型與內建參考表相符,未必需覆寫。 - 目視微調:與原文對照逐字檢查——標點與鄰字重疊則偏側往「不移」方向調(
right→center→left);標點與內容仍有空隙則往「多移」方向調(left→center→right);某標點無可壓空白則設full。 - 記錄:把該字型之
bias覆寫存入專案設定,作為該字型之適配結果。
偏側是「字身於 0.5em 盒內左移多少」的三檔量化(0/0.25/0.5em);量測值近門檻時以目視為準。halt 字型由字型精確定位,無此配字需求——margin 配字僅為缺 halt 字型之務實近似。
CSS 基礎設定(消費端須知)
JS 只注入 jz-* 結構,所有視覺尺寸在 juzhen.css。消費端須備妥:
- 引入樣式(必須):
<link rel="stylesheet" href="dist/juzhen.css">(或 ESMimport "cjk-autospace/css")。 - 設定
lang(強烈建議):內容容器標lang="zh-Hans"/"zh-Hant"/"ja",驅動全角/開明政策與字形locl。 - 作用域類:若設
scope.include: ".juzhen",須給內容容器加對應 class;juzhen.css之 normalize 亦收斂於此域。 - 長英文詞斷字:須由消費端 CSS 提供
hyphens: auto(含-webkit-hyphens)。 justify(選用):欲兩端對齊則自設text-align: justify;句末標點單份間距與行末去隙由聚珍處理。- 字型由消費端負責:聚珍不內建任何
font-family/字型堆疊——逐語言字型完全由消費端 CSS 決定(推薦範式:逐lang設於.juzhen或帶lang之容器,勿直接設jz-inner)。聚珍僅以jz-* { font: inherit }使原子繼承消費端字型、不強加,並顯式保留locl(不改寫lang、不關閉 locl,故正確標lang+ 用支援 locl 之字型即正常)。半形擠壓依字型 OpenTypehalt;缺halt之字型須用jiya.halfWidth:"margin"+按字型以jiya.bias微調。 - 自動處理:
juzhen.css已於.juzhen範圍內以@supports關閉 Blink 原生text-spacing-trim/text-autospace,並令注入間隙user-select: none(複製乾淨)。 - 段落屬性照常設、原子透明:聚珍把標點拆成
inline-block原子,但已確保原子對消費端段落樣式透明——段落可正常設text-indent(首行縮排)、hanging-punctuation、text-decoration(下劃線)等。庫內已歸零會被原子重複套用之繼承屬性、並令原子承接段落裝飾,故首行縮排正常、下劃線連續不斷,無需任何額外配置。
行內元素三分類:透明/隔離/整體
行內元素對排版有且僅有三種口徑。邏輯文本流之遍歷原語共用單一判定 isTransparent(中西間隙、擠壓相鄰、禁則綁定、垂懸字);未在 styleInlines 之未知行內標籤(自訂元素等)採嚴格白名單、一律當邊界,透明須顯式登記。多選擇器同命中時依單一分類器 finder.category 之優先序 avoid > pill > isolate > block > transparent > opaque 定案(如 data-jz-skip 之 pill → avoid;pill+isolate → pill)。判定以元素為準、非標籤名——KaTeX 全樹皆 <span>,但 .katex 命中 pillSelector 即為整體邊界。結構克隆切分 canSplit、行端物理版面 lineedge 為正當之獨立層(邊界各異,見架構)。
| 分類 | 選擇器 | 邊界 | 內部 | 典型 |
|---|---|---|---|---|
| 透明 | styleInlines(未列之行內標籤=邊界) | 穿越——間隙/相鄰/run 皆延續 | 照常處理;禁則可克隆切分(最小綁定) | strong/em/a/span |
| 隔離 | isolateSelector(預設 "mark";"" 停用) | 斷 run/斷相鄰/不可切分;兩端補 jz-hws 間隙 | 照常處理(自成脈絡) | 帶背景色之 <mark>、badge 類 |
| 整體 | pillSelector(預設 "code, kbd") | 同隔離 | 完全不可見——不 charify、不插間隙、不切分、不下潛 | code/kbd、行內公式 .katex、引用標 .cite-ref |
禁則遇 pill(…$公式$,):整個 pill 作為原子鄰字單位整體綁入 jz-jinze(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。avoid 七源(isSkipped/skipTags/SVG/contentEditable/data-jz-skip/skipAttribute/scope.avoid)以自身判定、命中即邊界;一次性檢查用組合入口 isAvoided(自身或祖先)。skipTags 預設不含 code/kbd——二者由 pillSelector 以 pill 涵蓋(三分類互斥)。
pillSelector: "code, kbd, .katex, .cite-ref"——兩側補隙、吸收作者空格、整體綁定,統一處理。分頁器(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 之字型。自定義字符分類:charClass
每個字符之分類(中文/西文、全寬標點各子類)抽象為規則集:內置默認即聚珍分類表,消費端經 options.charClass 擴充,以覆寫 spacing/jiya/jinze 對特定字符之判定。語義為追加——把當前未分類字符或新符號歸入對應類。
createJuzhen({
charClass: {
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(字母數字/運算符)為固定集、不開放。margin 模式下新增開/閉括號之墨色偏側可一併以 jiya.bias 指定。v1 向下相容 — 已移除
v1 相容層(createCjkAutospace 導出、IIFE 之全域別名、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 跨行右隙)。demo/showcase.html 以定寬文本框並列,逐場景演示功能分級與垂懸字。版面相關結論(justify 單份間距、擠壓視覺量、行端、inline-block 基線、間隙隨寬度之穩定性)須於 Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。