docs: 詳細 HTML 文檔(API、雙 pass、配置、整合範例)
This commit is contained in:
+4531
File diff suppressed because it is too large
Load Diff
+319
@@ -0,0 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>cjk-autospace — 詳細文檔</title>
|
||||
<meta name="generated-at" content="2026-05-22">
|
||||
<meta name="source" content="cjk-autospace docs branch">
|
||||
<!-- RUNTIME_CSS -->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="page">
|
||||
<div class="meta">
|
||||
<h1>cjk-autospace</h1>
|
||||
<p class="subtitle">CJK ↔ Latin 邊界自動間距 shim — 工廠模式、跨樣式透明、行內塊邊界、Pangu.js v7.2.1 規則 port</p>
|
||||
<p class="timestamp"><time>2026-05-22</time> · <span>docs branch</span></p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<!-- ACTIONS -->
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<p class="lede">Pangu.js v7.2.1 之規則 port,加上「跨樣式標籤透明 + 行內塊邊界」雙 pass 架構。工廠函式設計,由消費端注入 pill 選擇器、樣式 / 段落 / skip 標籤集等配置。輸出 <code><span class="cjk-autospace" aria-hidden="true">(空白)</span></code>,靠 CSS <code>user-select: none</code> 讓選取與剪貼板跳過。textContent 不變 → Copy 與序列化都乾淨。</p>
|
||||
|
||||
<h2>Quick start</h2>
|
||||
|
||||
<pre><code>// 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);
|
||||
</code></pre>
|
||||
|
||||
<p>套用後 DOM 內每個 Pangu 認定之邊界都被加上 <code>.cjk-autospace</code> marker。在已套用之 DOM 上重跑為冪等。</p>
|
||||
|
||||
<h3>必備 CSS</h3>
|
||||
|
||||
<pre><code>.cjk-autospace {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
/* 視覺寬度由消費端決定,例如 0.25em */
|
||||
}</code></pre>
|
||||
|
||||
<h2>標籤三分類</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>類別</th><th>典型成員</th><th>shim 對待</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>STYLE_INLINES</strong></td>
|
||||
<td><code>strong</code> / <code>em</code> / <code>a</code> / <code>b</code> / <code>i</code> / <code>u</code> / <code>s</code> / <code>mark</code> / <code>ins</code> / <code>del</code> / <code>sub</code> / <code>sup</code> / <code>small</code> / <code>abbr</code> / <code>cite</code> / <code>dfn</code> / <code>q</code> / <code>span</code> / <code>time</code> / <code>ruby</code> / <code>wbr</code> / …</td>
|
||||
<td><strong>透明</strong>。shim 在 Pass A 走 DFS 時穿過去,把內外文本當成連續文本判斷 Pangu 邊界。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>PILL</strong></td>
|
||||
<td>由 <code>pillSelector</code> 指定,預設 <code>code, kbd</code></td>
|
||||
<td><strong>opaque 行內塊</strong>。Pass A 視為 run 邊界;Pass B 處理其與鄰字之介面。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>BLOCK</strong></td>
|
||||
<td><code>p</code> / <code>div</code> / <code>h1</code>-<code>h6</code> / <code>li</code> / <code>pre</code> / <code>table</code> / <code>tr</code> / <code>td</code> / <code>br</code> / <code>hr</code> / …</td>
|
||||
<td><strong>段落級</strong>。Pass A 走到此即中斷 run;子 block 各自一次 <code>processBlockText</code> 呼叫。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout info">
|
||||
<span class="label">關鍵設計</span> 樣式標籤透明 → shim 看穿 <code><strong>是</strong> Neovim</code> 這類跨元素邊界,把「是」(CJK)+ 空格 + 「N」(ANS)視為 Pangu 觸發位置;而非僅在單一 text-node 內找匹配。
|
||||
</div>
|
||||
|
||||
<h2>Pass A — per-block Pangu on tag-stripped text</h2>
|
||||
|
||||
<p>對 root 之每個 BLOCK 後代執行一次。流程:</p>
|
||||
|
||||
<ol>
|
||||
<li>DFS 走 block 內部子節點:
|
||||
<ul>
|
||||
<li>遇 text node:把每個 char 累積到 <code>curText</code>,同步把 <code>{ textNode, offset }</code> 推入 <code>curMap</code></li>
|
||||
<li>遇 STYLE_INLINES:透明遞迴,curText 繼續累積</li>
|
||||
<li>遇 PILL / 子 BLOCK / 既有 <code>.cjk-autospace</code>:<code>flushRun()</code>,把累積之 (text, map) 作為一個 run 推入 runs,重置</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>對每個 run,套 <code>panguSpace(text)</code>,得到 <code>spaced</code> 字串(含 MARK PUA 字符 U+E007)</li>
|
||||
<li>平行掃描 <code>spaced</code> 與 <code>text</code>,把 MARK 對應到原文之插入位置:
|
||||
<ul>
|
||||
<li>若 <code>text[origIdx]</code> 是 ASCII space → MARK 替換了該空格。動作:strip 該空格 + 在前一字後插 marker</li>
|
||||
<li>否則 → MARK 純插入。動作:在前一字後插 marker</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>actions 倒序執行:保證前面動作之 textNode offset 不被後面動作(splitText / data slicing)影響</li>
|
||||
</ol>
|
||||
|
||||
<h3>例</h3>
|
||||
|
||||
<p><code><p>本資料夾<strong>不是</strong> Neovim 使用者目錄。</p></code></p>
|
||||
|
||||
<p>DFS 收集到的 <code>curText</code>(樣式標籤剝離後):<code>"本資料夾不是 Neovim 使用者目錄。"</code></p>
|
||||
|
||||
<p><code>panguSpace</code> 應用:<code>是 N</code> 命中 <code>STRIP_CJK_SPACE_ANS</code>(CJK + 空格 + ANS)→ 「<code>是<sub>MARK</sub>N</code>」(空格被 MARK 取代);<code>m 使</code> 命中 <code>STRIP_ANS_SPACE_CJK</code> → 同理。</p>
|
||||
|
||||
<p>反映射:</p>
|
||||
|
||||
<ul>
|
||||
<li>位置 1:原文「是」與「 」之間,「 」被 strip → marker 插在「是」後(DOM 上是 <code><strong></code> 之 nextSibling 處)</li>
|
||||
<li>位置 2:原文「m」與「 」之間,「 」被 strip → marker 插在「m」後(同一 text node 內,splitText 後插入)</li>
|
||||
</ul>
|
||||
|
||||
<p>最終 DOM(簡化):<code>本資料夾<strong>不是</strong><span class="cjk-autospace"> </span>Neovim<span class="cjk-autospace"> </span>使用者目錄。</code></p>
|
||||
|
||||
<h2>Pass B — pill boundary, style-inline aware</h2>
|
||||
|
||||
<p>對 root 內每個匹配 <code>pillSelector</code> 之元素執行:</p>
|
||||
|
||||
<ol>
|
||||
<li><code>adjacentLogicalChar(pill, -1)</code>:走 prev sibling 鏈,遇 text 取末字元;遇 STYLE_INLINES 遞迴進入找其最末 text;遇 PILL / BLOCK / 既有 marker / skip ancestor → null</li>
|
||||
<li>若回傳邏輯鄰字是 CJK / 字母數字 → 在 pill 前插 marker</li>
|
||||
<li>若邏輯鄰字是 1 個 ASCII space 且該 space 另一側(同 text node 內)是內容字 → strip 該 space + 插 marker</li>
|
||||
<li>對 <code>+1</code> 方向(pill 後)做對稱處理</li>
|
||||
</ol>
|
||||
|
||||
<h3>例</h3>
|
||||
|
||||
<p><code>命令 <code>git</code> 更新</code></p>
|
||||
|
||||
<ul>
|
||||
<li>Pass A run 因 pill 中斷:「命令 」(trailing space 為 run 末尾,不觸發 panguSpace 之 STRIP rule,因 rule 要求兩側都是內容字而 run 末尾沒有右側字)</li>
|
||||
<li>Pass B 處理 pill 之 prev:text 末字元是 <code>' '</code>,前一字元是 <code>'令'</code>(CJK),命中 strip 規則 → 移除空格 + 在 pill 前插 marker</li>
|
||||
<li>Pass B 處理 pill 之 next:text 首字元是 <code>' '</code>,後一字元是 <code>'更'</code>(CJK),同理 strip + 插 marker</li>
|
||||
</ul>
|
||||
|
||||
<p>結果:<code>命令<span class="cjk-autospace"> </span><code>git</code><span class="cjk-autospace"> </span>更新</code></p>
|
||||
|
||||
<h2>空白規則</h2>
|
||||
|
||||
<p>所有觸發點(Pangu 命中或 PILL 邊界),對「插入位置之原有空白」的處理一致:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>原有空白</th><th>動作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>0 個</td><td>純插 marker</td></tr>
|
||||
<tr><td>1 個 ASCII space</td><td>strip 該 space + 插 marker(取代)</td></tr>
|
||||
<tr><td>2 個或更多 space</td><td>跳過(視為刻意 structural whitespace)</td></tr>
|
||||
<tr><td>tab / newline / 其他空白</td><td>跳過(panguSpace 之 regex 只匹配 single ASCII space)</td></tr>
|
||||
<tr><td>標點隔開</td><td>跳過(panguSpace 命中要求兩端是內容字)</td></tr>
|
||||
<tr><td>既有 <code>.cjk-autospace</code> marker</td><td>跳過(避免雙重插入;冪等性)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>配置選項</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>選項</th><th>預設</th><th>說明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>pillSelector</code></td><td><code>"code, kbd"</code></td><td>CSS 選擇器;行內塊範圍。常見擴充:<code>code, kbd, .cjk-pill</code>(按 class opt-in)或 <code>code, kbd, mark, samp</code>(更廣)</td></tr>
|
||||
<tr><td><code>styleInlines</code></td><td>樣式標籤預設集</td><td>tagName 大寫之陣列;自訂可改寫</td></tr>
|
||||
<tr><td><code>blockTags</code></td><td>段落標籤預設集</td><td>tagName 大寫之陣列</td></tr>
|
||||
<tr><td><code>skipTags</code></td><td><code>["CODE", "KBD", "PRE", "SAMP", "TT", "VAR", "SCRIPT", "STYLE", "TEXTAREA", "INPUT"]</code></td><td>整 subtree skip,連祖先 chain 一併判斷</td></tr>
|
||||
<tr><td><code>autospaceClass</code></td><td><code>"cjk-autospace"</code></td><td>marker span 之 class 名</td></tr>
|
||||
<tr><td><code>skipAttribute</code></td><td><code>"data-md-key"</code></td><td>任一祖先帶此屬性 → 整 subtree skip</td></tr>
|
||||
<tr><td><code>isSkipped</code></td><td>(無)</td><td>自訂 callback;先於內建判定執行;回傳 true 即 skip</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Marker span 解剖</h2>
|
||||
|
||||
<pre><code><span class="cjk-autospace" aria-hidden="true"> </span></code></pre>
|
||||
|
||||
<dl>
|
||||
<dt><code>class="cjk-autospace"</code></dt>
|
||||
<dd>由消費端之 CSS 添加 <code>user-select: none</code>(選取時跳過);視覺寬度也由消費端決定(典型 0.25em)。</dd>
|
||||
<dt><code>aria-hidden="true"</code></dt>
|
||||
<dd>讓 screen reader 忽略,避免「空格、空格、空格」朗讀干擾。</dd>
|
||||
<dt>內含真實 ASCII space</dt>
|
||||
<dd>不是 0 寬 span — 真實字符。layout 視為一般 whitespace:行末可 collapse、容許在此換行、justify 可拉伸該空白。<br>對比方案(inline-block + fixed em width)破壞 line-end 行為與 justify 之分配;不採用。</dd>
|
||||
<dt>textContent 不變</dt>
|
||||
<dd>從 DOM serialization 或選取與複製角度看,marker 是「不可見」之 span;user-select: none 讓 selection range 跳過該 span;故剪貼板 / Copy MD 之輸出無空白污染。</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Pangu 規則簡表</h2>
|
||||
|
||||
<p>panguSpace 內部依序套 30+ 條 regex。簡列各觸發類別:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>類別</th><th>例</th><th>動作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Pre-pass strip</td><td><code>2023 年</code> → <code>2023年</code></td><td>strip CJK ↔ ANS 之間單 ASCII space,後續 pass 把它當 flush 寫法處理</td></tr>
|
||||
<tr><td>CJK + Latin / 數字</td><td><code>使用 GPT</code></td><td>插 MARK</td></tr>
|
||||
<tr><td>CJK + ASCII 標點</td><td><code>是,沒錯</code></td><td>標點後插 MARK</td></tr>
|
||||
<tr><td>CJK + 引號 / 括號</td><td><code>是(實驗)</code></td><td>插 MARK</td></tr>
|
||||
<tr><td>CJK + 運算子 + ANS</td><td><code>是+1</code></td><td>運算子兩側各插 MARK</td></tr>
|
||||
<tr><td>compound word 保護</td><td><code>GPT-4</code> / <code>state-of-the-art</code></td><td>以 PUA 占位替換,避免運算子 pass wedge 入空白;bracket / CJK_ANS pass 前還原</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>整合範例</h2>
|
||||
|
||||
<h3>HTML build script 串接</h3>
|
||||
|
||||
<pre><code># 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'
|
||||
}</code></pre>
|
||||
|
||||
<p>由 runtime.js 之 init 呼叫:<code>createCjkAutospace({}).apply();</code>。</p>
|
||||
|
||||
<h3>Lua 內嵌 JS 字串</h3>
|
||||
|
||||
<pre><code>-- 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)</code></pre>
|
||||
|
||||
<h3>TypeScript / Vite</h3>
|
||||
|
||||
<pre><code>// 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 });</code></pre>
|
||||
|
||||
<h2>邊界情境與限制</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>SVG namespace</strong>:jdgement <code>p.namespaceURI === 'http://www.w3.org/2000/svg'</code> 整 subtree skip。SVG <code><text></code> 不會被處理。</li>
|
||||
<li><strong>contentEditable</strong>:用戶輸入區整 subtree skip,避免影響游標 / 選取。</li>
|
||||
<li><strong>已存在 marker</strong>:Pass A 走 DFS 時,<code>.cjk-autospace</code> 中斷 run;Pass B <code>adjacentLogicalChar</code> 遇 marker 即停。故重複呼叫 <code>apply()</code> 不會雙倍插入。</li>
|
||||
<li><strong>動態 DOM</strong>:shim 自身不訂閱 mutation;增量更新由消費端用 <code>MutationObserver</code> 包裝(範例見上)。建議處理時暫斷 observer 避免自反饋。</li>
|
||||
<li><strong>跨 BLOCK 邊界</strong>:Pangu 規則不跨段落觸發。<code><p>是</p><p>Neovim</p></code> 不會插 marker(兩段獨立)。</li>
|
||||
<li><strong>Compound word 保護限制</strong>:僅匹配 <code>[A-Za-z0-9]*[a-z][A-Za-z0-9]*-[A-Za-z0-9]+</code> 等模式;無 dash 之純大寫縮寫(<code>HTML</code>、<code>API</code>)不被特殊保護,但本來就沒問題(不會被運算子規則 wedge)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>授權與來源</h2>
|
||||
|
||||
<p>規則表 port 自 <a href="https://github.com/vinta/pangu.js">Pangu.js v7.2.1</a>(MIT 授權)。本 lib 之雙 pass 架構(樣式透明 + 行內塊邊界)為原創設計,解決 Pangu.js 原版「text-node walker 看不到跨元素邊界」之問題。</p>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="page">
|
||||
<p>cjk-autospace · docs branch · 2026-05-22</p>
|
||||
</footer>
|
||||
|
||||
<!-- RUNTIME_SPRITE -->
|
||||
|
||||
<script type="text/markdown" id="md-source">
|
||||
# cjk-autospace
|
||||
|
||||
CJK ↔ Latin 邊界自動間距 shim。工廠模式,由消費端注入配置。
|
||||
|
||||
## Quick start
|
||||
|
||||
```js
|
||||
import { createCjkAutospace } from "./cjk-autospace.js";
|
||||
const autospace = createCjkAutospace({
|
||||
pillSelector: "code, kbd, .cjk-pill",
|
||||
});
|
||||
autospace.apply(document.body);
|
||||
```
|
||||
|
||||
## 標籤三分類
|
||||
|
||||
- STYLE_INLINES — 樣式標籤,shim 透明走過
|
||||
- PILL — 行內塊,邊界由 Pass B 處理
|
||||
- BLOCK — 段落級,Pass A 把它們當作 run 邊界
|
||||
|
||||
## Pass A
|
||||
|
||||
對每個 BLOCK 走 DFS 收集邏輯文本(樣式透明、PILL / 子 BLOCK 中斷 run),整段套 panguSpace,反映射 MARK 回 textNode / offset。
|
||||
|
||||
## Pass B
|
||||
|
||||
對每個 PILL 透過樣式鏈遞迴找邏輯鄰字,按 0/1/多+特殊規則處理(0 個或 1 個 ASCII space 插 marker;其餘跳過)。
|
||||
</script>
|
||||
|
||||
<!-- RUNTIME_JS -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user