margin 後備(供缺 OpenType halt 之字型): - jiya.halfWidth: "halt"(預設)|"margin"。margin 以 width:0.5em+字身依墨色 偏側溢出定位,於 CSS 盒模型重現 halt 之半形 advance(真 0.5em;相鄰標點各 0.5em、合佔 1em,無負 margin 之鄰接疊加/單側重疊)。模式由 data-jz-halfwidth 子樹屬性決定(可逐區塊/逐 lang,含 halt-island 反置)。 - 偏側 inkBias 依字身墨色中心給 left/center/right/full;參考表據方正書宋 GBK 實測,consumer 可經 jiya.bias 逐 lang 逐字覆寫。 - _lang.css 字型堆疊僅作後備(全包 :where 零特異度、只命中帶 lang 屬性之元素、 不直接覆寫 jz-inner)→ 消費端 font-family 恆優先,標點隨之以消費端字型渲染。 - 新增 tools/measure-bias.py(量 glyph ink 中心、產偏側建議表)+ README 「字型適配工作流」。demo/fallback.html:方正書宋 GBK 對照 halt 失效 vs margin。 下游回饋修復: - 1a:adjacentLogicalChar 透明穿越 jz-jinze(同 jz-hws/樣式行內)→ pill 緊鄰 CJK 標點時,左緣間隙與作者空格剝除復原(jinze 先於 spacing 之交互)。 - 1b:CJK↔CJK 贅餘作者空格(含跨 inline 標籤)剝除、不留隙,與 CJK↔Latin 之 邊界正規化一致(DEL 哨符,全形空格 U+3000 不動)。 - 問題二:新增 justifyAtoms 選項;false → jz-char/jz-jinze 改 display:inline, 相容分頁器(Paged.js)對齊引擎(halt 半形與禁則 nowrap 保留)。 測試 43→49 全通過;dist 重建;README/ARCHITECTURE 同步更新契約。
聚珍(Juzhen)
現代簡/繁中文網頁排版套件。Han.css 之現代化重寫,沿其功能版圖,並針對 數種情況作調整:複製乾淨(中西間隙不入剪貼板)、依
lang全角/開明 自動切換、justify下句末標點只佔一份間距、跨瀏覽器一致 (Chrome/Edge/Safari/WebKit/Firefox)。
倉庫名仍為 cjk-autospace;公開 API 為 createJuzhen / 全域 Juzhen;注入之
自訂元素前綴 jz-。完整設計見 ARCHITECTURE.md。
本版(2.x)為從零重寫。v1 之
createCjkAutospaceAPI 由相容層保留(見下), 既有消費端不需改碼即可運行;v1 原始碼保存於v1-autospace分支。
安裝與構建
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,原生 <script> 直跑 |
dist/juzhen.css |
合併樣式(必須引入,視覺尺寸全在此) |
dist/index.d.ts |
TypeScript 型別宣告 |
用法
原生 <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";
const jz = createJuzhen({ lang: { default: "zh-Hant" } });
jz.render(document.querySelector("main"));
JS 與 CSS 缺一不可:JS 只注入 jz-* 結構,所有視覺尺寸(間隙寬度、擠壓量、
justify 原子化、字體堆疊)都在 juzhen.css。
切片 1 已實作功能
| 功能 | 說明 |
|---|---|
中西間隙 spacing |
CJK↔西文邊界插入 copy-clean 之 jz-hws(user-select:none,剪貼板跳過)。含跨樣式行內邊界與 code/kbd pill 邊界(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 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點對向側收);③行端擠壓(lineedge layout pass)。半形預設以字型 halt 渲染(按字形正確定位,居中字形如 !?/繁體 。, 不重疊;不依賴 text-spacing-trim)。字型缺 halt 時可切 margin 後備模式(見下節)。 |
禁則 jinze |
行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 justify 綁定載體。 |
| justify 單份間距 | 每個 jz-char 為 inline-block 原子項,句末標點只貢獻一份對齊伸縮量。 |
後續(架構已預留,不返工):標點懸掛、標點修正(簡↔繁)、著重號、注音/Ruby。
選項
createJuzhen({
lang: {
default: "zh-Hant", // 無 lang 祖先時之預設
policy: { "zh-Hant": "quanjiao", "zh-Hans": "kaiming" }, // 全角/開明/半角
},
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,
justifyAtoms: true, // jz-char/jz-jinze 以 inline-block 作對齊原子;
// 分頁器(Paged.js)下設 false 改 inline(見下)
blocks: [ // 分塊功能選擇
{ selector: ".code-comment", features: [ "spacing" ] },
],
});
分頁器(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 屬性,就近繼承)
<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後備後, 同句行寬24.0em → 19.7em、單一標點1.0em → 0.5em,半形生效。
激活與粒度
模式由 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。於 charify 標為 jz-ink-{left,center,right}。因字型字身分布各異,可逐
lang 逐字覆寫:
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 模式配字)
換用新字型時,依下列步驟確認半形渲染:
- 驗
halt:python3 tools/measure-bias.py <font.ttf>(需pip install fonttools)。 首行報該字型是否含 OpenTypehalt。- 有
halt→ 直接用預設(jiya: true,halt 模式),無需配字,到此為止。 - 無
halt(多數系統字型、GBK 老字型)→ 用margin後備,續下。
- 有
- 取偏側建議:同一工具同時量各標點 glyph 之墨色中心、印出建議偏側表
(
left|center|right|full)與可貼入jiya.bias之 JSON。 - 套用:
createJuzhen({ jiya: { halfWidth: "margin", bias: <貼入 JSON> } }); 或全用內建參考表(多數宋體系 GBK 字型與參考表相符,未必需覆寫)。 - 目視微調:以
demo/fallback.html(或自有頁面)對照原文,逐字檢查——- 標點與相鄰字重疊 → 該字墨色比量測更偏該側,偏側往「不移」方向調
(
right→center→left)。 - 標點與內容仍有空隙、不夠半形 → 往「多移」方向調(
left→center→right)。 - 某標點根本無可壓空白(字面已滿全形)→ 設
full,不收。
- 標點與相鄰字重疊 → 該字墨色比量測更偏該側,偏側往「不移」方向調
(
- 記錄:把該字型之
bias覆寫存入專案設定,作為該字型之適配結果。
偏側是「字身於 0.5em 盒內左移多少」的三檔量化(0/0.25/0.5em),量測值近門檻 (如 0.6 介於 center/right)時,以目視為準。
halt字型走 OpenType 由字型精確 定位,無此配字需求——margin配字僅為缺halt字型之務實近似。
CSS 基礎設定(消費端須知)
JS 只注入 jz-* 結構,所有視覺尺寸在 juzhen.css——故 CSS 不可或缺。消費端
須備妥下列幾項:
- 引入樣式(必須):
<link rel="stylesheet" href="dist/juzhen.css">(或 ESMimport "cjk-autospace/css")。缺此則間隙/擠壓/justify 原子化全部失效。 - 設定
lang(強烈建議):在內容容器標lang="zh-Hans"/"zh-Hant"/"ja", 驅動全角/開明政策與字形locl。無lang時回退options.lang.default。 - 作用域類(用
scope.include時):若設scope.include: ".juzhen",須給內容 容器加對應 class(如class="juzhen");juzhen.css之 normalize/字型堆疊亦 收斂於此域,不外溢。 - 長英文詞斷字:
longWord包出<span lang="en">後,須由消費端 CSS 提供hyphens: auto(含-webkit-hyphens)才會原生斷詞顯示連字符。 justify(選用):欲兩端對齊則自行設text-align: justify;句末標點單份間距 與行末去隙由聚珍處理,無需額外設定。- 字型(影響擠壓):開明/半角式半形預設依字型
halt。若用含halt之字型 (Noto CJK/思源),預設即可;若用缺halt之字型,改用margin後備模式(見 上節)。juzhen.css內建依:lang之 Noto Sans/Serif CJK 字型堆疊,但僅作 後備(全包於:where()、零特異度,且只命中帶lang屬性之元素、不直接覆寫標點 字面容器jz-inner)——消費端在.juzhen或任一祖先設font-family即恆優先, 標點亦隨之以消費端字型渲染(故缺halt字型務必配margin後備,否則標點不擠)。 - 自動處理、無需消費端介入:
juzhen.css已在.juzhen範圍內以@supports關閉 Blink 原生text-spacing-trim/text-autospace(避免與託管擠壓重複作用), 並令注入間隙user-select: none(複製乾淨)。
v1 向下相容
// 既有消費端零改動:
const cjk = createCjkAutospace({ pillSelector: "code, kbd" });
cjk.apply(document.body);
IIFE 構建另掛 globalThis.createCjkAutospace 別名。與 v1 之差異(行為等價、
不要求逐位一致):
- 間隙標記由
<span class="cjk-autospace">變為<jz-hws class="cjk-autospace">(仍帶原 class,既有.cjk-autospace{user-select:none}CSS 仍命中)。 - 標點擠壓改由
juzhen.css託管(須引入),預設仍以字型halt渲染半形——與 v1 消費端font-feature-settings:"halt"同機制;缺halt之字型可切margin後備。
目視驗證
demo/index.html 載入 dist/ 產物,簡繁並列、可切 justify、附 copy-clean 測試區。
demo/fallback.html 以方正書宋 GBK(無 halt)對照 halt 模式失效 vs margin
後備生效。版面相關結論(justify 單份間距、擠壓視覺量、行端、inline-block 基線)須於
Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。
授權
MIT。借鑑 Han.css/Heti 之思路與分類表(MIT),成品自含、不依賴其執行期程式碼。