句內點號(,、;:)逐字包進 <span class="cjk-squeeze">,配合消費端 CSS `font-feature-settings: "halt"` 擠成半形;句末點號(。?!)保持全形—— 此即開明式。span 內含真標點字元,複製不污染原文。 clreq 之「連續標點擠壓」與「行端壓縮」屬 layout-aware 規則,交由原生 `text-spacing-trim` 處理(Chrome 123+ 預設生效,要求字型有 halt/chws), JS 不涉入以免重複擠壓。真·行尾懸掛(hanging-punctuation)僅 Safari 支援 且繁體橫排不宜,故不納入配方。 預設啟用,支援 `langWhitelist` 依 lang 前綴限定擠壓範圍、`marks` 與 `squeezeClass` 自訂。含 16 項 jsdom DOM 測試。
cjk-autospace
CJK ↔ Latin / 數字 / 標點 / 引號 / 括號 / 運算子等邊界之自動間距 shim。Pangu.js v7.2.1 規則之 ES5 端口,加上「跨樣式標籤透明 + 行內塊邊界」兩 pass 架構,再加 Pass C 長英文段 <span lang> 包裝(讓瀏覽器原生連字斷詞)、Pass D 開明式標點擠壓(句內點號半形)。工廠模式,由消費端注入配置。
詳細文檔 + Changelog 位於
docs分支:架構解析、雙 pass 流程、整合範例見index.html;變更記錄見CHANGELOG.html。本機並列檢出:git worktree add ../cjk-autospace-docs docs。
API
const autospace = createCjkAutospace({
pillSelector: "code, kbd, .your-pill-class",
});
autospace.apply(document.body);
回傳 { apply(root?) }。冪等:在已注入 marker 之 DOM 上重跑會跳過既有 .cjk-autospace。完整選項見 cjk-autospace.d.ts。
CSS 需求
.cjk-autospace {
-webkit-user-select: none;
user-select: none;
/* 視覺寬度由消費端決定,例如 0.25em via inline-block + width */
}
user-select: none 讓選取與剪貼板跳過 marker;marker 內含真實 ASCII space(layout 視為 whitespace:行末 collapse、容許換行、justify 可拉伸)。
開明式標點擠壓(Pass D)另需:
/* 句內點號半形——CSS 表達不了「只擠句內、不擠句末」,故由 lib 逐字包裝,
此處只負責把被包裝者擠成半形。需字型具 halt(Noto Sans/Serif CJK 皆有)。 */
.cjk-squeeze {
font-feature-settings: "halt" 1;
}
/* 連續標點擠壓 + 行端壓縮(clreq)——原生 layout-aware,套在正文容器上。
Chrome 123+ 預設即 normal,明確標注以防被 reset;同樣要求字型具 halt/chws。 */
.article {
text-spacing-trim: normal;
}
不加 .cjk-squeeze 規則則 span 惰性(DOM 變更但無視覺效果)。text-spacing-trim 於 Chromium 不支援時優雅降級(句內半角仍由 halt 生效)。
標籤三分類
- STYLE_INLINES(
<strong>/<em>/<a>/<b>/<i>/<u>/<s>/<mark>/<ins>/<del>/<sub>/<sup>/<small>/<abbr>/<cite>/<dfn>/<q>/<span>/<var>/<samp>/<time>/<bdo>/<bdi>/<ruby>/<rb>/<rt>/<rp>/<wbr>)— shim 透明走過。跨樣式之 Pangu 邊界(如<strong>是</strong> Neovim)由 Pass A 在剝離後識別。 - PILL(由
pillSelector指定,預設code, kbd)— 行內塊。邊界由 Pass B 處理,與內容鄰接(CJK / 字母數字)即插 marker。 - BLOCK(
<p>/<div>/<h1>–<h6>/<li>/<pre>/<table>/<br>/<hr>/ …)— 段落級。Pass A 把它們當作 run 邊界,子 block 各自一次。
四 pass
-
Pass A — per-block Pangu on tag-stripped text:對每個 BLOCK 走 DFS 收集邏輯文本(STYLE 透明、PILL 與子 BLOCK 中斷 run、既有 marker 也中斷),整段套
panguSpace,把 MARK 反映射回 textNode/offset。MARK 對應原文若是單 ASCII space 則 strip 後插 marker,否則純插。actions 倒序執行確保前面偏移不被後面動作影響。 -
Pass B — pill boundary:對每個 PILL 透過 STYLE 鏈遞迴找邏輯鄰字(
adjacentLogicalChar),鄰字若是 CJK / 字母數字直接插 marker;若是 1 個 ASCII space 且另一側為內容字則 strip 後插 marker;其餘(標點 / 多空格 / 換行 / 既有 marker)跳過。 -
Pass C — long English word wrap(預設啟用,
longWordWrap: false顯式關閉):將文本節點內所有[A-Za-z]{N,}(預設N=6)之長英文段包進<span lang="en">。冪等:parent 已為<span lang>直接跳過。連字符不在 DOM,由瀏覽器hyphens: auto在斷行時純視覺渲染 → 選取 / 複製返回原文無污染。需消費端加 CSS:[lang|="en"] { hyphens: auto; -webkit-hyphens: auto; hyphenate-limit-chars: 6 3 3; /* 詞長 / 斷點前 / 斷點後最少字符 */ }不加 CSS 則 span 為惰性(DOM 變更但無視覺效果)。
-
Pass D — 開明式標點擠壓(預設啟用,
punctuationSqueeze: false顯式關閉):把句內點號(,、;:)逐字包進<span class="cjk-squeeze">,配合上節 CSS 之font-feature-settings: "halt"擠成半形;句末點號(。?!)保持全形——此即「開明式」。span 內含真標點字元本身(非注入空白),故 Copy / Paste / 序列化返回原文不變,無需user-select特殊處理。冪等:parent 已為.cjk-squeeze直接跳過。可傳{ squeezeClass, marks, langWhitelist }自訂。職責分工(不重複擠壓):開明式「只擠句內、不擠句末」CSS 無法表達(
halt套整個元素會把句末也擠成半形 → 變半角式),故由 lib 逐字包裝。clreq 之「連續標點擠壓」「行端壓縮」屬 layout-aware(相鄰對之側別、斷行位置只有 layout 時才知),交由原生text-spacing-trim(其 collapse 規則與 clreq 逐條對應)。兩層字符集/職責不相交。langWhitelist:BCP-47 前綴陣列。非空時僅擠壓最近祖先lang前綴命中之文本("zh"命中"zh-Hant";無lang祖先視為不命中);空則不限。開明式於簡體字型下字面位於左下角、擠壓最自然,繁體點號居中視覺略異——欲限定簡體可傳{ langWhitelist: ["zh-Hans"] }。hanging-punctuation(真·行尾懸掛)不納入配方:Chromium 至今未實作(無 flag 可開),僅 Safari 支援;且 clreq 言繁體中文橫排不宜行尾懸掛。clreq 的「行尾結束括號縮減末側半形」屬壓縮,已由text-spacing-trim涵蓋。
配置選項
| 選項 | 預設 | 說明 |
|---|---|---|
pillSelector |
"code, kbd" |
行內塊 CSS 選擇器 |
styleInlines |
預設樣式標籤集 | tagName 大寫之陣列 |
blockTags |
預設段落標籤集 | tagName 大寫之陣列 |
skipTags |
mono / form / script / style | tagName 大寫之陣列;整 subtree skip |
autospaceClass |
"cjk-autospace" |
marker span class 名 |
skipAttribute |
"data-md-key" |
任一祖先帶此屬性即整 subtree skip |
isSkipped |
(無) | 自訂 skip callback;先於內建判定檢查 |
longWordWrap |
true(預設啟用) |
Pass C 開關。傳 false 顯式關閉;{minLength, lang} 自訂;true / 省略等同 {minLength: 6, lang: "en"} |
punctuationSqueeze |
true(預設啟用) |
Pass D 開明式擠壓開關。傳 false 顯式關閉;{squeezeClass, marks, langWhitelist} 自訂;true / 省略等同句內點號 [",","、",";",":"] / class "cjk-squeeze" / 不限語言 |
模組形式
四種消費場景並存:
-
ES module / TypeScript:
import { createCjkAutospace } from "./cjk-autospace.js"(檔尾export { createCjkAutospace },配cjk-autospace.d.ts) -
CommonJS / Node:
require("./cjk-autospace.js").createCjkAutospace(檔尾module.exports) -
Browser global:載入後可
globalThis.createCjkAutospace -
IIFE 串接:把本檔內容
cat進單一閉包(如 Claude 模板build.sh/ Nvim peek build hook)—export {…}為 ESM module-level 語法,串入函式體內會語法錯誤,串接端須剝掉那行:sed '/^export[[:space:]]*{/d' cjk-autospace.js其餘
module.exports/globalThis兩塊在閉包內安全(typeof檢查不會 throw、globalThis 賦值無副作用)。
授權
基於 Pangu.js v7.2.1 的規則表 port(原始項目 MIT)。