commit 3e5e45fb893922c9706fb7f5e8abb803b0a5f182 Author: commilitia Date: Fri May 22 22:44:49 2026 +0800 docs: 詳細 HTML 文檔(API、雙 pass、配置、整合範例) diff --git a/index.html b/index.html new file mode 100644 index 0000000..5e63007 --- /dev/null +++ b/index.html @@ -0,0 +1,4531 @@ + + + + + + +cjk-autospace — 詳細文檔 + + + + + + +
+
+

cjk-autospace

+

CJK ↔ Latin 邊界自動間距 shim — 工廠模式、跨樣式透明、行內塊邊界、Pangu.js v7.2.1 規則 port

+

· docs branch

+
+
+ + + + + +
+
+ +
+ +

Pangu.js v7.2.1 之規則 port,加上「跨樣式標籤透明 + 行內塊邊界」雙 pass 架構。工廠函式設計,由消費端注入 pill 選擇器、樣式 / 段落 / skip 標籤集等配置。輸出 <span class="cjk-autospace" aria-hidden="true">(空白)</span>,靠 CSS user-select: none 讓選取與剪貼板跳過。textContent 不變 → Copy 與序列化都乾淨。

+ +

Quick start

+ +
// 1) 載入 shim(以下任一)
+import { createCjkAutospace } from "./cjk-autospace.js";   // ESM / TS
+// 或 inline <script> / require / globalThis.createCjkAutospace
+
+// 2) 配置(pill 選擇器是唯一需要消費端自訂的;其餘有合理預設)
+const autospace = createCjkAutospace({
+    pillSelector: "code, kbd, .your-pill-class",
+});
+
+// 3) 套用
+autospace.apply(document.body);
+
+ +

套用後 DOM 內每個 Pangu 認定之邊界都被加上 .cjk-autospace marker。在已套用之 DOM 上重跑為冪等。

+ +

必備 CSS

+ +
.cjk-autospace {
+    -webkit-user-select: none;
+    user-select: none;
+    /* 視覺寬度由消費端決定,例如 0.25em */
+}
+ +

標籤三分類

+ + + + + + + + + + + + + + + + + + + + + + +
類別典型成員shim 對待
STYLE_INLINESstrong / em / a / b / i / u / s / mark / ins / del / sub / sup / small / abbr / cite / dfn / q / span / time / ruby / wbr / …透明。shim 在 Pass A 走 DFS 時穿過去,把內外文本當成連續文本判斷 Pangu 邊界。
PILLpillSelector 指定,預設 code, kbdopaque 行內塊。Pass A 視為 run 邊界;Pass B 處理其與鄰字之介面。
BLOCKp / div / h1-h6 / li / pre / table / tr / td / br / hr / …段落級。Pass A 走到此即中斷 run;子 block 各自一次 processBlockText 呼叫。
+ +
+ 關鍵設計 樣式標籤透明 → shim 看穿 <strong>是</strong> Neovim 這類跨元素邊界,把「是」(CJK)+ 空格 + 「N」(ANS)視為 Pangu 觸發位置;而非僅在單一 text-node 內找匹配。 +
+ +

Pass A — per-block Pangu on tag-stripped text

+ +

對 root 之每個 BLOCK 後代執行一次。流程:

+ +
    +
  1. DFS 走 block 內部子節點: +
      +
    • 遇 text node:把每個 char 累積到 curText,同步把 { textNode, offset } 推入 curMap
    • +
    • 遇 STYLE_INLINES:透明遞迴,curText 繼續累積
    • +
    • 遇 PILL / 子 BLOCK / 既有 .cjk-autospaceflushRun(),把累積之 (text, map) 作為一個 run 推入 runs,重置
    • +
    +
  2. +
  3. 對每個 run,套 panguSpace(text),得到 spaced 字串(含 MARK PUA 字符 U+E007)
  4. +
  5. 平行掃描 spacedtext,把 MARK 對應到原文之插入位置: +
      +
    • text[origIdx] 是 ASCII space → MARK 替換了該空格。動作:strip 該空格 + 在前一字後插 marker
    • +
    • 否則 → MARK 純插入。動作:在前一字後插 marker
    • +
    +
  6. +
  7. actions 倒序執行:保證前面動作之 textNode offset 不被後面動作(splitText / data slicing)影響
  8. +
+ +

+ +

<p>本資料夾<strong>不是</strong> Neovim 使用者目錄。</p>

+ +

DFS 收集到的 curText(樣式標籤剝離後):"本資料夾不是 Neovim 使用者目錄。"

+ +

panguSpace 應用:是 N 命中 STRIP_CJK_SPACE_ANS(CJK + 空格 + ANS)→ 「MARKN」(空格被 MARK 取代);m 使 命中 STRIP_ANS_SPACE_CJK → 同理。

+ +

反映射:

+ + + +

最終 DOM(簡化):本資料夾<strong>不是</strong><span class="cjk-autospace"> </span>Neovim<span class="cjk-autospace"> </span>使用者目錄。

+ +

Pass B — pill boundary, style-inline aware

+ +

對 root 內每個匹配 pillSelector 之元素執行:

+ +
    +
  1. adjacentLogicalChar(pill, -1):走 prev sibling 鏈,遇 text 取末字元;遇 STYLE_INLINES 遞迴進入找其最末 text;遇 PILL / BLOCK / 既有 marker / skip ancestor → null
  2. +
  3. 若回傳邏輯鄰字是 CJK / 字母數字 → 在 pill 前插 marker
  4. +
  5. 若邏輯鄰字是 1 個 ASCII space 且該 space 另一側(同 text node 內)是內容字 → strip 該 space + 插 marker
  6. +
  7. +1 方向(pill 後)做對稱處理
  8. +
+ +

+ +

命令 <code>git</code> 更新

+ + + +

結果:命令<span class="cjk-autospace"> </span><code>git</code><span class="cjk-autospace"> </span>更新

+ +

空白規則

+ +

所有觸發點(Pangu 命中或 PILL 邊界),對「插入位置之原有空白」的處理一致:

+ + + + + + + + + + + + + +
原有空白動作
0 個純插 marker
1 個 ASCII spacestrip 該 space + 插 marker(取代)
2 個或更多 space跳過(視為刻意 structural whitespace)
tab / newline / 其他空白跳過(panguSpace 之 regex 只匹配 single ASCII space)
標點隔開跳過(panguSpace 命中要求兩端是內容字)
既有 .cjk-autospace marker跳過(避免雙重插入;冪等性)
+ +

配置選項

+ + + + + + + + + + + + + + +
選項預設說明
pillSelector"code, kbd"CSS 選擇器;行內塊範圍。常見擴充:code, kbd, .cjk-pill(按 class opt-in)或 code, kbd, mark, samp(更廣)
styleInlines樣式標籤預設集tagName 大寫之陣列;自訂可改寫
blockTags段落標籤預設集tagName 大寫之陣列
skipTags["CODE", "KBD", "PRE", "SAMP", "TT", "VAR", "SCRIPT", "STYLE", "TEXTAREA", "INPUT"]整 subtree skip,連祖先 chain 一併判斷
autospaceClass"cjk-autospace"marker span 之 class 名
skipAttribute"data-md-key"任一祖先帶此屬性 → 整 subtree skip
isSkipped(無)自訂 callback;先於內建判定執行;回傳 true 即 skip
+ +

Marker span 解剖

+ +
<span class="cjk-autospace" aria-hidden="true"> </span>
+ +
+
class="cjk-autospace"
+
由消費端之 CSS 添加 user-select: none(選取時跳過);視覺寬度也由消費端決定(典型 0.25em)。
+
aria-hidden="true"
+
讓 screen reader 忽略,避免「空格、空格、空格」朗讀干擾。
+
內含真實 ASCII space
+
不是 0 寬 span — 真實字符。layout 視為一般 whitespace:行末可 collapse、容許在此換行、justify 可拉伸該空白。
對比方案(inline-block + fixed em width)破壞 line-end 行為與 justify 之分配;不採用。
+
textContent 不變
+
從 DOM serialization 或選取與複製角度看,marker 是「不可見」之 span;user-select: none 讓 selection range 跳過該 span;故剪貼板 / Copy MD 之輸出無空白污染。
+
+ +

Pangu 規則簡表

+ +

panguSpace 內部依序套 30+ 條 regex。簡列各觸發類別:

+ + + + + + + + + + + + + +
類別動作
Pre-pass strip2023 年2023年strip CJK ↔ ANS 之間單 ASCII space,後續 pass 把它當 flush 寫法處理
CJK + Latin / 數字使用 GPT插 MARK
CJK + ASCII 標點是,沒錯標點後插 MARK
CJK + 引號 / 括號是(實驗)插 MARK
CJK + 運算子 + ANS是+1運算子兩側各插 MARK
compound word 保護GPT-4 / state-of-the-art以 PUA 占位替換,避免運算子 pass wedge 入空白;bracket / CJK_ANS pass 前還原
+ +

整合範例

+ +

HTML build script 串接

+ +
# build.sh 把 lib 串到 runtime.js 之前
+{
+    printf '<script>\n'
+    cat "$LIB/cjk-autospace.js"   # 工廠函式
+    printf '\n'
+    sed '$d' "$RUNTIME/runtime.js"  # 應用程式 runtime
+    cat "$MODE_BEHAVIOR_JS"
+    printf '\n})();\n'
+    printf '</script>\n'
+}
+ +

由 runtime.js 之 init 呼叫:createCjkAutospace({}).apply();

+ +

Lua 內嵌 JS 字串

+ +
-- local.lua build hook 讀 lib 內容,sprintf 注入 selector 配置
+local lib = read_file("lua/user/lib/cjk-autospace/cjk-autospace.js")
+local entry = string.format(
+    "createCjkAutospace({ pillSelector: %q }).apply(document.getElementById('peek-markdown-body'));",
+    "code, kbd, mark, samp"
+)
+local script = "<script>" .. lib .. "\n" .. entry .. "</script>"
+inject_into_html(script)
+ +

TypeScript / Vite

+ +
// cjkAutospace.ts
+import { createCjkAutospace } from "./lib/cjk-autospace/cjk-autospace.js";
+
+const autospace = createCjkAutospace({
+    pillSelector: "code, kbd, .cjk-pill",
+});
+
+let pending = false;
+const obs = new MutationObserver(() => {
+    if (pending) return;
+    pending = true;
+    queueMicrotask(() => {
+        pending = false;
+        obs.disconnect();
+        try { autospace.apply(); }
+        finally { obs.observe(document.body, { childList: true, subtree: true, characterData: true }); }
+    });
+});
+autospace.apply();
+obs.observe(document.body, { childList: true, subtree: true, characterData: true });
+ +

邊界情境與限制

+ + + +

授權與來源

+ +

規則表 port 自 Pangu.js v7.2.1(MIT 授權)。本 lib 之雙 pass 架構(樣式透明 + 行內塊邊界)為原創設計,解決 Pangu.js 原版「text-node walker 看不到跨元素邊界」之問題。

+ +
+ + + + + + + + + + + diff --git a/index.src.html b/index.src.html new file mode 100644 index 0000000..228d904 --- /dev/null +++ b/index.src.html @@ -0,0 +1,319 @@ + + + + + +cjk-autospace — 詳細文檔 + + + + + + +
+
+

cjk-autospace

+

CJK ↔ Latin 邊界自動間距 shim — 工廠模式、跨樣式透明、行內塊邊界、Pangu.js v7.2.1 規則 port

+

· docs branch

+
+
+ +
+
+ +
+ +

Pangu.js v7.2.1 之規則 port,加上「跨樣式標籤透明 + 行內塊邊界」雙 pass 架構。工廠函式設計,由消費端注入 pill 選擇器、樣式 / 段落 / skip 標籤集等配置。輸出 <span class="cjk-autospace" aria-hidden="true">(空白)</span>,靠 CSS user-select: none 讓選取與剪貼板跳過。textContent 不變 → Copy 與序列化都乾淨。

+ +

Quick start

+ +
// 1) 載入 shim(以下任一)
+import { createCjkAutospace } from "./cjk-autospace.js";   // ESM / TS
+// 或 inline <script> / require / globalThis.createCjkAutospace
+
+// 2) 配置(pill 選擇器是唯一需要消費端自訂的;其餘有合理預設)
+const autospace = createCjkAutospace({
+    pillSelector: "code, kbd, .your-pill-class",
+});
+
+// 3) 套用
+autospace.apply(document.body);
+
+ +

套用後 DOM 內每個 Pangu 認定之邊界都被加上 .cjk-autospace marker。在已套用之 DOM 上重跑為冪等。

+ +

必備 CSS

+ +
.cjk-autospace {
+    -webkit-user-select: none;
+    user-select: none;
+    /* 視覺寬度由消費端決定,例如 0.25em */
+}
+ +

標籤三分類

+ + + + + + + + + + + + + + + + + + + + + + +
類別典型成員shim 對待
STYLE_INLINESstrong / em / a / b / i / u / s / mark / ins / del / sub / sup / small / abbr / cite / dfn / q / span / time / ruby / wbr / …透明。shim 在 Pass A 走 DFS 時穿過去,把內外文本當成連續文本判斷 Pangu 邊界。
PILLpillSelector 指定,預設 code, kbdopaque 行內塊。Pass A 視為 run 邊界;Pass B 處理其與鄰字之介面。
BLOCKp / div / h1-h6 / li / pre / table / tr / td / br / hr / …段落級。Pass A 走到此即中斷 run;子 block 各自一次 processBlockText 呼叫。
+ +
+ 關鍵設計 樣式標籤透明 → shim 看穿 <strong>是</strong> Neovim 這類跨元素邊界,把「是」(CJK)+ 空格 + 「N」(ANS)視為 Pangu 觸發位置;而非僅在單一 text-node 內找匹配。 +
+ +

Pass A — per-block Pangu on tag-stripped text

+ +

對 root 之每個 BLOCK 後代執行一次。流程:

+ +
    +
  1. DFS 走 block 內部子節點: +
      +
    • 遇 text node:把每個 char 累積到 curText,同步把 { textNode, offset } 推入 curMap
    • +
    • 遇 STYLE_INLINES:透明遞迴,curText 繼續累積
    • +
    • 遇 PILL / 子 BLOCK / 既有 .cjk-autospaceflushRun(),把累積之 (text, map) 作為一個 run 推入 runs,重置
    • +
    +
  2. +
  3. 對每個 run,套 panguSpace(text),得到 spaced 字串(含 MARK PUA 字符 U+E007)
  4. +
  5. 平行掃描 spacedtext,把 MARK 對應到原文之插入位置: +
      +
    • text[origIdx] 是 ASCII space → MARK 替換了該空格。動作:strip 該空格 + 在前一字後插 marker
    • +
    • 否則 → MARK 純插入。動作:在前一字後插 marker
    • +
    +
  6. +
  7. actions 倒序執行:保證前面動作之 textNode offset 不被後面動作(splitText / data slicing)影響
  8. +
+ +

+ +

<p>本資料夾<strong>不是</strong> Neovim 使用者目錄。</p>

+ +

DFS 收集到的 curText(樣式標籤剝離後):"本資料夾不是 Neovim 使用者目錄。"

+ +

panguSpace 應用:是 N 命中 STRIP_CJK_SPACE_ANS(CJK + 空格 + ANS)→ 「MARKN」(空格被 MARK 取代);m 使 命中 STRIP_ANS_SPACE_CJK → 同理。

+ +

反映射:

+ + + +

最終 DOM(簡化):本資料夾<strong>不是</strong><span class="cjk-autospace"> </span>Neovim<span class="cjk-autospace"> </span>使用者目錄。

+ +

Pass B — pill boundary, style-inline aware

+ +

對 root 內每個匹配 pillSelector 之元素執行:

+ +
    +
  1. adjacentLogicalChar(pill, -1):走 prev sibling 鏈,遇 text 取末字元;遇 STYLE_INLINES 遞迴進入找其最末 text;遇 PILL / BLOCK / 既有 marker / skip ancestor → null
  2. +
  3. 若回傳邏輯鄰字是 CJK / 字母數字 → 在 pill 前插 marker
  4. +
  5. 若邏輯鄰字是 1 個 ASCII space 且該 space 另一側(同 text node 內)是內容字 → strip 該 space + 插 marker
  6. +
  7. +1 方向(pill 後)做對稱處理
  8. +
+ +

+ +

命令 <code>git</code> 更新

+ + + +

結果:命令<span class="cjk-autospace"> </span><code>git</code><span class="cjk-autospace"> </span>更新

+ +

空白規則

+ +

所有觸發點(Pangu 命中或 PILL 邊界),對「插入位置之原有空白」的處理一致:

+ + + + + + + + + + + + + +
原有空白動作
0 個純插 marker
1 個 ASCII spacestrip 該 space + 插 marker(取代)
2 個或更多 space跳過(視為刻意 structural whitespace)
tab / newline / 其他空白跳過(panguSpace 之 regex 只匹配 single ASCII space)
標點隔開跳過(panguSpace 命中要求兩端是內容字)
既有 .cjk-autospace marker跳過(避免雙重插入;冪等性)
+ +

配置選項

+ + + + + + + + + + + + + + +
選項預設說明
pillSelector"code, kbd"CSS 選擇器;行內塊範圍。常見擴充:code, kbd, .cjk-pill(按 class opt-in)或 code, kbd, mark, samp(更廣)
styleInlines樣式標籤預設集tagName 大寫之陣列;自訂可改寫
blockTags段落標籤預設集tagName 大寫之陣列
skipTags["CODE", "KBD", "PRE", "SAMP", "TT", "VAR", "SCRIPT", "STYLE", "TEXTAREA", "INPUT"]整 subtree skip,連祖先 chain 一併判斷
autospaceClass"cjk-autospace"marker span 之 class 名
skipAttribute"data-md-key"任一祖先帶此屬性 → 整 subtree skip
isSkipped(無)自訂 callback;先於內建判定執行;回傳 true 即 skip
+ +

Marker span 解剖

+ +
<span class="cjk-autospace" aria-hidden="true"> </span>
+ +
+
class="cjk-autospace"
+
由消費端之 CSS 添加 user-select: none(選取時跳過);視覺寬度也由消費端決定(典型 0.25em)。
+
aria-hidden="true"
+
讓 screen reader 忽略,避免「空格、空格、空格」朗讀干擾。
+
內含真實 ASCII space
+
不是 0 寬 span — 真實字符。layout 視為一般 whitespace:行末可 collapse、容許在此換行、justify 可拉伸該空白。
對比方案(inline-block + fixed em width)破壞 line-end 行為與 justify 之分配;不採用。
+
textContent 不變
+
從 DOM serialization 或選取與複製角度看,marker 是「不可見」之 span;user-select: none 讓 selection range 跳過該 span;故剪貼板 / Copy MD 之輸出無空白污染。
+
+ +

Pangu 規則簡表

+ +

panguSpace 內部依序套 30+ 條 regex。簡列各觸發類別:

+ + + + + + + + + + + + + +
類別動作
Pre-pass strip2023 年2023年strip CJK ↔ ANS 之間單 ASCII space,後續 pass 把它當 flush 寫法處理
CJK + Latin / 數字使用 GPT插 MARK
CJK + ASCII 標點是,沒錯標點後插 MARK
CJK + 引號 / 括號是(實驗)插 MARK
CJK + 運算子 + ANS是+1運算子兩側各插 MARK
compound word 保護GPT-4 / state-of-the-art以 PUA 占位替換,避免運算子 pass wedge 入空白;bracket / CJK_ANS pass 前還原
+ +

整合範例

+ +

HTML build script 串接

+ +
# build.sh 把 lib 串到 runtime.js 之前
+{
+    printf '<script>\n'
+    cat "$LIB/cjk-autospace.js"   # 工廠函式
+    printf '\n'
+    sed '$d' "$RUNTIME/runtime.js"  # 應用程式 runtime
+    cat "$MODE_BEHAVIOR_JS"
+    printf '\n})();\n'
+    printf '</script>\n'
+}
+ +

由 runtime.js 之 init 呼叫:createCjkAutospace({}).apply();

+ +

Lua 內嵌 JS 字串

+ +
-- local.lua build hook 讀 lib 內容,sprintf 注入 selector 配置
+local lib = read_file("lua/user/lib/cjk-autospace/cjk-autospace.js")
+local entry = string.format(
+    "createCjkAutospace({ pillSelector: %q }).apply(document.getElementById('peek-markdown-body'));",
+    "code, kbd, mark, samp"
+)
+local script = "<script>" .. lib .. "\n" .. entry .. "</script>"
+inject_into_html(script)
+ +

TypeScript / Vite

+ +
// cjkAutospace.ts
+import { createCjkAutospace } from "./lib/cjk-autospace/cjk-autospace.js";
+
+const autospace = createCjkAutospace({
+    pillSelector: "code, kbd, .cjk-pill",
+});
+
+let pending = false;
+const obs = new MutationObserver(() => {
+    if (pending) return;
+    pending = true;
+    queueMicrotask(() => {
+        pending = false;
+        obs.disconnect();
+        try { autospace.apply(); }
+        finally { obs.observe(document.body, { childList: true, subtree: true, characterData: true }); }
+    });
+});
+autospace.apply();
+obs.observe(document.body, { childList: true, subtree: true, characterData: true });
+ +

邊界情境與限制

+ + + +

授權與來源

+ +

規則表 port 自 Pangu.js v7.2.1(MIT 授權)。本 lib 之雙 pass 架構(樣式透明 + 行內塊邊界)為原創設計,解決 Pangu.js 原版「text-node walker 看不到跨元素邊界」之問題。

+ +
+ + + + + + + + + + +