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

110 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cjk-autospace
CJK ↔ Latin / 數字 / 標點 / 引號 / 括號 / 運算子等邊界之自動間距 shim。Pangu.js v7.2.1 規則之 ES5 端口,加上「跨樣式標籤透明 + 行內塊邊界」兩 pass 架構,再加 Pass C 長英文段 `<span lang>` 包裝(讓瀏覽器原生連字斷詞)、Pass D 開明式標點擠壓(句內點號半形)。工廠模式,由消費端注入配置。
> **詳細文檔 + Changelog 位於 `docs` 分支**:架構解析、雙 pass 流程、整合範例見 [`index.html`](https://git.commilitia.net/admin/cjk-autospace/src/branch/docs/index.html);變更記錄見 [`CHANGELOG.html`](https://git.commilitia.net/admin/cjk-autospace/src/branch/docs/CHANGELOG.html)。本機並列檢出:`git worktree add ../cjk-autospace-docs docs`。
## API
```js
const autospace = createCjkAutospace({
pillSelector: "code, kbd, .your-pill-class",
});
autospace.apply(document.body);
```
回傳 `{ apply(root?) }`。冪等:在已注入 marker 之 DOM 上重跑會跳過既有 `.cjk-autospace`。完整選項見 `cjk-autospace.d.ts`
## CSS 需求
```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
/* 句內點號半形——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:
```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 語法,串入函式體內會語法錯誤,串接端須剝掉那行:
```sh
sed '/^export[[:space:]]*{/d' cjk-autospace.js
```
其餘 `module.exports` / `globalThis` 兩塊在閉包內安全(`typeof` 檢查不會 throw、globalThis 賦值無副作用)。
## 授權
基於 Pangu.js v7.2.1 的規則表 port[原始項目](https://github.com/vinta/pangu.js) MIT)。