Files
cjk-autospace/README.md
admin 82c2d22535 feat: Pass D 開明式標點擠壓(句內點號半形)
句內點號(,、;:)逐字包進 <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 測試。
2026-06-01 18:53:50 +08:00

7.8 KiB
Raw Permalink Blame History

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 讓選取與剪貼板跳過 markermarker 內含真實 ASCII spacelayout 視為 whitespace:行末 collapse、容許換行、justify 可拉伸)。

開明式標點擠壓(Pass D)另需:

/* 句內點號半形——CSS 表達不了「只擠句內、不擠句末」,故由 lib 逐字包裝,
   此處只負責把被包裝者擠成半形。需字型具 haltNoto 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 / TypeScriptimport { createCjkAutospace } from "./cjk-autospace.js"(檔尾 export { createCjkAutospace },配 cjk-autospace.d.ts

  • CommonJS / Noderequire("./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)。