17 Commits

Author SHA1 Message Date
admin c4f0ee02d4 docs: changelog 2026-07-12 效能第二/三層——掃描/佈局提速 + 增量恢復(Tier3)
新增 changelog 條目+md-twin。掃描/佈局提速(中間/S4 祖先游走記憶化、S1 gapTrim 對齊門)+增量
恢復(C1 持久化 Finder+失效、C3 rerender、C2 observe,append 修復);獨立複驗(5 法)+4 份真
人真實文檔驗收;I1/I6 收緊(作者空格規範化不還原)。changelog HTML 經 build.sh 重建。
2026-07-12 02:31:30 +08:00
admin fdad106980 docs: changelog 2026-07-11 效能第一層——掃描提速(category 記憶化 + orphan 葉塊預計算)
新增 changelog 條目+md-twin 同步。下游效能訴求一之第一層:S3 category 以 WeakMap 記憶化砍除跨
pass 重複 el.matches()(實測約降三成)、S2 orphan 以上攀預計算「非葉塊」取代逐塊 querySelector 掃
子樹之 O(Σ塊子樹)。皆無狀態、保行為、無公開 API 變更;測試 129→130 全綠。並記亲驗數據與 insidePill
之下一機會。changelog HTML 經 build.sh 重建。
2026-07-11 19:58:41 +08:00
admin cda0903089 docs: changelog 2026-07-11 splitTreeAfter 攀升對稱守衛(opaque 中間祖先克隆肢解修復)
新增 changelog 條目+md-twin 同步。下游 .selection-bar-message 病例:垂懸字段末實義字
落於 opaque `<button>`(巢於可切 `<span>`),splitTreeAfter 攀升克隆「文本節點→top」祖先
鏈時中間 opaque button 從未複查即被克隆肢解。修復:切分前先驗整條祖先鏈皆 canSplit(下潛
+攀升雙向守衛),任一層 opaque/pill/isolate/帶 id 即退化整綁。changelog HTML 經
build.sh 重建。
2026-07-11 13:35:46 +08:00
admin 738c917a5e docs: changelog 2026-07-11 逐元素標點樣式覆寫(data-jz-style/stylePolicy)+ index 選項/優先序
新增 changelog 2026-07-11 條目(下游需求:非段落/特定範圍用半角式;設置格式——data-jz-style
屬性+lang.stylePolicy 選擇器+優先序)+md-twin 同步。index 選項示例補 stylePolicy、擠壓政策
優先序補逐元素覆寫(data-jz-style/stylePolicy > policy > style > 內建)。changelog/index HTML
經 build.sh 重建。
2026-07-11 01:49:06 +08:00
admin 5e74818153 docs: 同步 4f84d31——changelog 補元素分類收斂+ResizeObserver 確認;index 補分類優先序
docs 分支補齊至 main 最新(4f84d31)。「專案整理」changelog 條目補:
・階段二收尾——元素分類單一入口 finder.category(優先序 avoid>pill>isolate>block>
  transparent>opaque;collectRuns/jiyaAdjacency switch(category)、canSplit=SPLITTABLE∩
  transparent、isTransparent=category==="transparent",行為等價、121 全綠)。
・復驗後定性——lineEdge/gapTrim 雙 ResizeObserver 派發序確認為非缺陷(Chrome headless 實測
  回調按 observer 創建序派發,lineEdge 先於 gapTrim 建、resize 恆先派發、不用陳舊半形態;
  註解鎖為不變量、不需改碼)。

index 三分類補分類優先序(finder.category)+md-twin 同步。changelog/index HTML 經 build.sh 重建。
2026-07-07 22:01:00 +08:00
admin 648605ac53 docs: changelog 2026-07-07 專案整理(階段一/二)+ index 三分類分層/avoid 七源/demo/19.7em
新增 changelog 2026-07-07「專案整理」條目(架構一致性:透明判定單一真相源+跨透明邊界避頭尾
+avoid 排除歸一;確認缺陷修復+清理;復驗後 lineedge 非缺陷/五處幂等不做之定性;文檔重整)
+md-twin 同步。

index 同步:三分類「共用同一判定」改分層表述(isTransparent 為邏輯流單一真相源+未知行內採
嚴格白名單邊界;canSplit/lineedge 為獨立層)、透明列標「未列=邊界」、avoid 改七源+isAvoided
組合入口+skipTags 不含 code/kbd;修 19.5em→19.7em(與 main 一致);目視驗證補 demo/resize-check
與 demo/showcase 引用。changelog/index HTML 經 build.sh 重建。
2026-07-07 21:21:52 +08:00
admin f75382c9b5 docs: changelog 2026-06-13 標點擠壓句界例外 + index 連續擠壓行為說明補齊
新增 changelog 2026-06-13 條目(句末點號接開類保留右空白;。「/?「 全角式與開明式
皆 1.5em、對稱於 。」;例外僅限句末點號+開類,,「 與 」「/》《 仍 1em;半角式不
回歸;唯一改動點 jiyaAdjacencyPass.squeeze() 之 stopBeforeOpen 守衛)+md-twin 同步。

index 連續擠壓 callout 與 md-twin 補句界例外,使行為說明最新且完整。changelog/index
HTML 經 build.sh 重建。
2026-06-13 15:41:42 +08:00
admin 4b4505654f docs: 行內元素三分類(KaTeX 肢解修復)+ v1 相容層移除(changelog 2026-06-12、index)
changelog 新增 2026-06-12 條目:下游問題六(jinze 透明下潛肢解 KaTeX)三根因修復
(元素判透明/avoidsSelf 自身判定/descend 三態 blocked);三分類確立(透明/隔離
isolateSelector 新增/整體);v1 相容層移除(I8 退役)。md 孿生塊補回填 06-09/06-11
兩條(前兩輪漏更)。
index:頭注 v1 改已移除;功能表 spacing/jinze 行補三分類口徑;新增「行內元素三分類」
節(對照表+pill 建議);v1 節改移除記錄;md 孿生塊同步。
重建 changelog.html/index.html。
2026-06-12 17:21:01 +08:00
admin 445716e34e docs: 行內透明性修復 + 標點懸掛移除(changelog 2026-06-11、index 功能表)
changelog 新增 2026-06-11 條目:兩個下游 Bug(pill 前標點誤隙、行內元素整體掉行)
+審計 T4-T6;標點懸掛由「停用」正式移除(寬度守恆論證、已移除清單、原生管線未來路徑)。
index 功能表:禁則/垂懸字行補克隆切分(splitoff)說明;標點懸掛行改「已移除」。
重建 changelog.html/index.html。
2026-06-11 16:19:15 +08:00
admin bbda4cb3d5 docs: 標點懸掛改記為「實作後停用」(同步 main 停用決定)
changelog 該條改為「功能分級 + 垂懸字(標點懸掛停用)」:功能分級/垂懸字落地、行端
模式重構+行歸屬 bug 修正;標點懸掛全面停用(四項不可行原因+三重防逃逸+未來原生
hanging-punctuation 路徑)。index 功能表懸掛列改刪除線+停用說明、選項示例與 md twin 同步。
重建 index.html/changelog.html。
2026-06-09 23:34:49 +08:00
admin 38a1cbaace docs: 功能分級 + 垂懸字 + 標點懸掛(同步 main §6.5 落地)
changelog 增「功能分級 + 垂懸字 + 標點懸掛」條目(2026-06-09):功能分級
(文本/段落級)、垂懸字 orphan(方案 C,預設開)、標點懸掛 hanging(按類凸出量、
headless 校準、需 justifyAtoms:false)、行歸屬 bug 修正、兩階段防自我應驗、demo 增區。
index 功能表增 orphan/hanging/level 三列、選項示例與 md twin 同步、各功能標級別。
重建 index.html/changelog.html。
2026-06-09 22:19:29 +08:00
admin 43ac591d6a docs: 標點挤压一致性(同步 main fdf071c)
changelog 增「標點挤压一致性(2026-06-04)」:全角句點 . 納入句末標點、連續標點雙側
獨立挤压+透明穿越;並把前批 charClass 條目之 . 例改為 ‥(. 今已內建)。使用文檔
標點半形節增句末標點與連續挤压規則(涉長度處標明全角式/開明式)。HTML 與 Markdown
孿生同步、manifest 重建。
2026-06-09 15:23:05 +08:00
admin 309bc358a8 docs: 字符分類規則集 charClass(同步 main b821cd1)
使用文檔增「自定義字符分類:charClass」節(規則集擴充、英文全角句號 . 用例、向下兼容
與追加語義要點);changelog 增「字符分類規則集(2026-06-04)」。HTML 與 Markdown 孿生
同步、manifest 重建。
2026-06-04 16:24:40 +08:00
admin 0f49ba8ecc docs: 字型交消費端(同步 main 之 _lang.css 移除)
changelog 增「變更:字型移出聚珍管理」(不內建 font-family、半形顯式保留 locl、
render 不改 lang;halt/margin 兩模式驗證正常)。使用文檔 CSS 基礎設定「字型」項
改寫為「字型由消費端負責」+推薦逐 lang 範式。HTML 與 Markdown 孿生同步、manifest 重建。
2026-06-03 02:02:26 +08:00
admin e4d1dac315 docs: changelog 增「下游回饋修復批次(2026-06-03)」
同步 main 之原子透明+中西間隙回饋修復:gapTrim 自我應驗、西文字集完整化
(貨幣/拼音/西里爾/希臘/數學)、全角省略號與收尾彎引號之標點誤隙、標點稽核,
及 demo/resize-check.html 可拖曳檢查頁。HTML 與 Markdown 孿生同步,manifest 重建。
2026-06-03 00:59:57 +08:00
admin e9b956ca4e docs: 使用文檔增補「段落屬性照常設、原子透明」一項
同步 main 之原子透明修復:CSS 基礎設定新增第 8 項,說明段落可正常設
text-indent/hanging-punctuation/text-decoration,庫內已統一處理(歸零過
套用之繼承屬性、令原子承接段落裝飾),首行縮排正常、下劃線連續,無需額外配置。
HTML 列表與 Markdown 孿生同步,經 manifest 重建為自含單檔。
2026-06-02 17:20:40 +08:00
admin 92c37cf63e docs: 聚珍 HTML 文檔(使用文檔+變更紀錄)
docs 分支僅含文檔,與程式碼歷史脫鉤(orphan,無 src/dist 等舊實現之提交)。
以 HTML manifest 模板(report 模式)構建為自含單檔,無外部依賴。

- docs/index.html  使用文檔:安裝/用法/功能/選項/halt 與 margin 後備/
  字型偏側與配字工作流/CSS 基礎設定/分頁器相容/v1 相容。
- docs/changelog.html  變更紀錄。
- *.src.html 為可編輯之文檔源。

程式碼正本與其歷史在 main;本分支不記錄任何實現。
2026-06-02 01:41:35 +08:00
9 changed files with 14559 additions and 9647 deletions
+3
View File
@@ -0,0 +1,3 @@
.DS_Store
node_modules/
*.log
-4475
View File
File diff suppressed because it is too large Load Diff
-123
View File
@@ -1,123 +0,0 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cjk-autospace — Changelog</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 — Changelog</h1>
<p class="subtitle">時間倒序。架構細節見同分支 <code>index.html</code></p>
<p class="timestamp"><time>2026-05-22</time> · <span>docs branch</span></p>
</div>
<div class="actions">
<!-- ACTIONS -->
</div>
</header>
<main>
<h2>2026-05-26 — 補 ESM <code>export</code>(四種消費形式並存)</h2>
<p>下游 Vite / Rollup / TypeScript 之靜態 ESM 解析需要真正之 <code>export</code> 關鍵字;原 UMD-ish<code>module.exports</code> + <code>globalThis</code>)對 ESM 消費端仍須走「副作用導入 + 從 global 取 factory」之 workaround。檔尾在既有 CJS / globalThis 兩塊後加 <code>export { createCjkAutospace }</code>,順序刻意如此 —— 讓 IIFE 串接端(Claude 模板 <code>build.sh</code> / Nvim peek build hook)只需以 <code>sed '/^export {/d'</code> 剝最後一行。</p>
<p>四種消費形式並存:ESM <code>import</code> / CommonJS <code>require</code> / browser <code>globalThis</code> / IIFE 串接(後者需剝 <code>export</code> 行)。</p>
<h2>2026-05-26 — Pass C:長英文段 lang 包裝(預設啟用)</h2>
<p>解決 CJK 兩端對齊正文中行末長英文詞之斷行問題。原模板於 runtime 端用 <code>&lt;wbr&gt;</code> 每 6 字元亂插斷詞機會,產生兩個觀感問題:(1)位置與音節邊界無關,斷在怪位;(2)<code>&lt;wbr&gt;</code> 為 zero-width,斷處不顯連字符,讀者無法辨認「上行末是被斷開的詞」。</p>
<p>Pass C 走另一條路:把 <code>[A-Za-z]{N,}</code>(預設 <code>N=6</code>)之長英文段包進 <code>&lt;span lang="en"&gt;</code>,配合消費端 CSS</p>
<pre><code>[lang|="en"] {
hyphens: auto;
-webkit-hyphens: auto;
hyphenate-limit-chars: 6 3 3;
hyphenate-character: "-"; /* 強制 U+002D 而非預設 U+2010 */
-webkit-hyphenate-character: "-";
}</code></pre>
<ul>
<li>瀏覽器原生 <code>hyphens: auto</code> 用 en 字典做斷詞 + 渲染連字符;連字符純由 CSS 渲染,<strong>DOM 內不存在</strong> → 選取 / 複製返回原文無污染(符合「斷字符僅供渲染」之核心約束)</li>
<li>冪等:parent 為 <code>&lt;span lang&gt;</code> 直接跳過,重跑或 MutationObserver 觸發不會重複包裝</li>
<li>skip 鏈與 Pass A / B 共用 <code>isSkipped</code>CODE / KBD / PRE / SAMP / TT / VAR / SCRIPT / STYLE / TEXTAREA / INPUT / SVG / contentEditable / <code>data-md-key</code> 皆跳過</li>
<li><strong>預設啟用</strong><code>longWordWrap: false</code> 顯式關閉;<code>{ minLength, lang }</code> 自訂粒度與標語</li>
</ul>
<h3>字型陷阱:U+2010 之全形 CJK 設計</h3>
<p>瀏覽器 <code>hyphens: auto</code> 預設在斷處插 U+2010 HYPHEN。Noto Serif CJK SC 等 CJK serif 字型把 U+2010 設計為 <strong>全形 1 em</strong>(與一個漢字同寬),以服務 CJK 範圍表達(<code>1月—3月</code>);而 U+002D HYPHEN-MINUS 在同字型內為窄拉丁(約 0.35 em)。直接吃預設會看到「全形連字符」突兀地接在英文詞末。</p>
<p>解法是消費端 CSS 加 <code>hyphenate-character: "-"</code>,強制使用 U+002D;仍為純視覺渲染,DOM 與剪貼簿不受影響。lib 不涉入字型決策,僅在文檔中提示。</p>
<h2>2026-05-22 — 初始發布</h2>
<ul>
<li>工廠函式 <code>createCjkAutospace(options)</code>,回傳 <code>{ apply(root?) }</code>。Port 自 Pangu.js v7.2.1 規則表(MIT 授權),覆寫架構為雙 pass:
<ul>
<li><strong>Pass A — per-block Pangu on tag-stripped text</strong>:對每個 BLOCK 走 DFS 收集邏輯文本(樣式標籤透明、PILL 與子 BLOCK 中斷 run),整段套 <code>panguSpace</code>,把 MARK 反映射回 textNode/offset 並插入 <code>.cjk-autospace</code> marker span。</li>
<li><strong>Pass B — pill boundary, style-inline aware</strong>:對每個 PILL 透過樣式鏈遞迴找邏輯鄰字(<code>adjacentLogicalChar</code>),按 0/1/多+特殊規則處理空白。</li>
</ul>
</li>
<li>標籤三分類:STYLE_INLINES / PILL / BLOCK,皆可由消費端配置。</li>
<li>UMD-ish export:同時支援 ESM <code>import</code>、CommonJS <code>require</code>、browser global <code>globalThis.createCjkAutospace</code> 與 inline <code>&lt;script&gt;</code> 函式宣告形式。</li>
<li>TS 宣告:<code>cjk-autospace.d.ts</code> 提供完整 <code>CjkAutospaceOptions</code><code>CjkAutospaceInstance</code> 型別。</li>
<li>初始消費者:
<ul>
<li>Claude HTML 模板(<code>Base/templates/html/runtime/lib/cjk-autospace/</code>pillSelector <code>"code, kbd"</code></li>
<li>Peek.nvim Markdown 預覽 shim<code>Nvim/lua/user/lib/cjk-autospace/</code>pillSelector <code>"code, kbd, mark, samp"</code></li>
<li>cdrop<code>cdrop/web/src/lib/cjk-autospace/</code>pillSelector <code>"code, kbd, .cjk-pill"</code></li>
</ul>
</li>
<li>docs 分支:<code>index.html</code>(架構詳細文檔)+ <code>CHANGELOG.html</code>(本檔)。</li>
</ul>
<div class="callout info">
<span class="label">設計來源</span> 雙 pass 架構是原創設計,解決 Pangu.js 原版「text-node walker 看不到跨元素邊界」之問題(典型癥狀:<code>&lt;strong&gt;&lt;/strong&gt; Neovim</code> 此類邊界無法被識別)。
</div>
</main>
<footer class="page">
<p>cjk-autospace · docs branch · CHANGELOG</p>
</footer>
<!-- RUNTIME_SPRITE -->
<script type="text/markdown" id="md-source">
# cjk-autospace Changelog
## 2026-05-26 ESM `export`四種消費形式並存
- 檔尾加 `export { createCjkAutospace }`順序在 CJS / globalThis 兩塊之後 IIFE 串接端只需 `sed '/^export {/d'` 剝最後一行
- 四種消費形式並存ESM `import` / CommonJS `require` / browser `globalThis` / IIFE 串接
## 2026-05-26 Pass C長英文段 lang 包裝預設啟用
- [A-Za-z]{N,} 之長英文段包進 `<span lang="en">`配消費端 CSS `[lang|="en"] { hyphens: auto; ... }` 用瀏覽器原生 en 字典斷詞 + 渲染連字符
- 連字符純由 CSS 渲染**DOM 內不存在**選取 / 複製返回原文無污染
- 預設啟用`longWordWrap: false` 顯式關閉
- 字型陷阱CJK serif 字型把 U+2010 設計為全形1 em須加 `hyphenate-character: "-"` 強制 U+002D 才不會看到全形連字符
## 2026-05-22 初始發布
- 工廠函式 createCjkAutospace(options)Port Pangu.js v7.2.1 規則表
- pass 架構
- Pass A per-block Pangu on tag-stripped text
- Pass B pill boundary, style-inline aware
- 標籤三分類STYLE_INLINES / PILL / BLOCK皆可配置
- UMD-ish export + TS 宣告
- 初始消費者Claude HTML 模板Peek.nvimcdrop
- docs 分支index.html架構+ CHANGELOG.html本檔
</script>
<!-- RUNTIME_JS -->
</body>
</html>
+7086
View File
File diff suppressed because one or more lines are too long
+463
View File
@@ -0,0 +1,463 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>變更紀錄(Changelog</title>
<meta name="generated-at" content="2026-07-11">
<meta name="source" content="cjk-autospace · 變更紀錄">
<!-- RUNTIME_CSS -->
</head>
<body>
<header class="page">
<div class="meta">
<h1>變更紀錄(Changelog</h1>
<p class="subtitle">聚珍(Juzhen)/cjk-autospace 之版本變更紀錄。</p>
<p class="timestamp"><time>2026-07-11</time> · <span>cjk-autospace · 變更紀錄</span></p>
</div>
<div class="actions">
<!-- ACTIONS -->
</div>
</header>
<main>
<!-- BODY -->
<p class="lede">聚珍(Juzhen)/cjk-autospace 變更紀錄。<a href="./index.html">← 回文檔</a></p>
<h2>效能第二/三層:掃描/佈局提速 + 增量恢復(Tier3) <span class="badge accent">2026-07-12</span></h2>
<p>承第一層(S2+S3)。本批含掃描/佈局提速與**增量恢復**(動態頁面局部變化免全樹重掃),全程獨立複驗+4 份真人撰寫真實文檔驗收。單元測試 130→140 全綠。</p>
<h3>掃描 / 佈局提速</h3>
<ul>
<li><strong>祖先游走記憶化(中間發現+S4</strong><code>insidePill</code><code>isAvoided</code> 之「逐文本節點沿祖先鏈」以迭代回填記憶化(每元素恰算一次、兄弟共用),O(文本×depth)→O(元素)。掃描層 <code>el.matches()</code> 累計約降五成;深嵌套 render 進程隔離墻鐘 pre-Tier1 707ms→254ms<strong>2.8×</strong>)。</li>
<li><strong>S1 gapTrim 對齊門</strong>:尾隨 margin 僅在右緣 flush 之對齊可見,唯 <code>left</code><code>start</code>(ltr) 之 ragged 右緣可整批跳過,省其 O(gaps) 次同步重排。非兩端對齊內容 gapTrim 由數千 ms 降至數十 msjustifycenterrightRTL 保留完整算法、行為不變(真瀏覽器差分:trim 集指紋逐位相同)。</li>
</ul>
<h3>增量恢復(第三層,向下兼容 API</h3>
<ul>
<li><strong>C1 持久化 Finder</strong>:跨 render 復用元素鍵快取。正確性靠失效——新增 <code>invalidate(el)</code> 清整棵子樹快取(祖先屬性變則後代快取變脏),<code>revert</code> 自動失效其子樹。</li>
<li><strong>C3 <code>rerender(root)</code></strong>revert+render(局部恢復,實測編輯 400 段中一段 <strong>766×</strong>、matches 1441×)。</li>
<li><strong>C2 <code>observe</code><code>disconnect</code></strong>MutationObserver 自動增量——內容/分類屬性變化重處理最近塊、新增之塊直接 render;render/revert 期間暫停觀察,自身注入絕不自觸發。</li>
<li><strong>append 修復</strong>(獨立差分複驗 IV-3 發現、單測漏掉):向葉塊(帶 <code>jz-orphan</code>)追加子塊使其轉非葉,須撤容器陳舊 orphan。</li>
</ul>
<h3>獨立複驗 真實文檔驗收</h3>
<ul>
<li>5 種獨立方法(差分/往返/增量≡全量/進程隔離測速/真瀏覽器 S1),皆與既有單測/基準不同源——<strong>抓到並修復 append 真 bug</strong></li>
<li>4 份真人撰寫真實文檔(繁體技術/簡體評論/文學對話/混排):非空白字元多重集守恆、無字元改/增/漏/亂序,<code>revert</code> 淨移除結構、真瀏覽器整條管線零報錯。</li>
</ul>
<h3>文檔(I1/I6 收緊)</h3>
<ul>
<li>驗收浮現:<code>render→revert</code> 不還原 <code>spacing</code> 依契約規範化之贅餘作者空格(<code>在 Neovim 中</code><code>在Neovim中</code>)——固有、非本批引入。<strong>I1</strong> 明示此為刻意常態例外(語義不變、確刪空格且不還原);<strong>I6</strong> 明示「邏輯等價≠逐字節」之唯一差異即被規範化之空格。README 補「<code>revert</code> 之範圍」注。</li>
</ul>
<h2>效能第一層:掃描提速(category 記憶化 + orphan 葉塊預計算) <span class="badge accent">2026-07-11</span></h2>
<p>下游效能訴求(掃描疑 O(n²))之第一層——無狀態、保行為、無公開 API 變更之確定收益。單元測試 129→130 全綠、輸出不變。</p>
<h3>優化</h3>
<ul>
<li><strong>S3 category 記憶化</strong><code>finder.category</code><code>WeakMap&lt;Element, InlineCategory&gt;</code> 記憶化,砍除 <code>collectRuns</code><code>jiyaAdjacency</code><code>jinze</code><code>split</code> 跨 pass 對同一元素之重複 <code>el.matches()</code>pillisolateavoid 選擇器)。實測 pillisolate 密集頁面 <code>el.matches()</code> 8204→5842(約降三成,命中率 55%)。安全:單次 render 內一元素之分類恆定、且 Finder 隨 <code>makeCtx</code> 每次 render 重建 → 快取僅存活單次 render、無跨 render 失效(持久化屬另議)。</li>
<li><strong>S2 orphan 葉塊預計算</strong>:以一趟上攀預計算「非葉塊」Set 取代逐塊 <code>block.querySelector(sel)</code> 掃整棵子樹之 O(Σ塊子樹)(深塊嵌套下 O(n²));某塊為另一塊之最近塊祖先 ⟺ 含塊級後代,與原 querySelector 同口徑、行為等價。</li>
</ul>
<h3>亲驗與待議</h3>
<ul>
<li>亲驗:暫禁 category 快取則 <code>computeCategory</code> 由 1041 增至 2322、<code>el.matches()</code> 由 5842 增至 8204——證快取真減負而非空操作(初次插桩誤打 <code>HTMLBodyElement.prototype</code> 而漏計,改補 <code>Element.prototype.matches</code> 後為準)。</li>
<li>新發現(下一機會):剩餘 <code>el.matches()</code> 之最大來源為 <code>insidePill</code> 之「逐文本節點沿祖先鏈 <code>pillMatches</code>」(<code>eachTextNode</code> 每文本節點一趟、未記憶化)。</li>
</ul>
<h2>splitTreeAfter 攀升對稱守衛(opaque 中間祖先不再被克隆肢解) <span class="badge accent">2026-07-11</span></h2>
<p>下游 <code>.selection-bar-message</code> 病例:垂懸字(orphan)之段末實義字落於 opaque <code>&lt;button&gt;</code>(巢於可切 <code>&lt;span&gt;</code>)時,<code>splitElementAt</code> 僅驗頂層 span 可切;<code>splitTreeAfter</code> 逐層克隆「文本節點→top」祖先鏈時,中間 opaque button 從未複查即被克隆肢解(複製出 <code>splitoff</code> button、破壞互動元素)。<code>firstText</code><code>lastText</code> 守下潛(遇非可切回 <code>null</code>),攀升卻無對稱守衛——此即真正病根,下游 per-instance 迴避僅治標。</p>
<h3>修復</h3>
<ul>
<li><strong>攀升守衛</strong><code>splitTreeAfter</code> 切分前先驗「<code>textNode.parentNode</code>…top(含 top)」整條祖先鏈皆 <code>canSplit</code>,任一層 opaquepillisolate/帶 id 即放棄、回退整綁(退化但安全,與下潛守衛對稱)。純新增守衛、無新克隆邏輯;jinze 之 <code>isolateBoundaryToken</code> 因下潛已守,行為不變。附帶亦杜絕中間祖先帶 <code>id</code> 之克隆重複 id。</li>
<li><strong>不變量</strong><code>canSplit</code> 須對「切點 textNode 至 top 之<strong>整條</strong>祖先鏈」成立、非僅 top——下潛(<code>firstText</code><code>lastText</code>)+攀升(<code>splitTreeAfter</code> 守衛)雙向守。</li>
<li>單元測試 127→129 全綠(+2):末實義字在 opaque button(巢於可切 span)→ 綁整 span、無 <code>splitoff</code>、button 完整;退化路徑 render→revert 還原一致。亲驗——暫禁守衛則該測試於「無 splitoff」斷言失敗(證明非空通過、病根屬實)。</li>
</ul>
<h2>逐元素標點樣式覆寫(data-jz-stylestylePolicy <span class="badge accent">2026-07-11</span></h2>
<p>下游需求:非段落/特定範圍元素用半角式(banjiao),段落用全角/開明。原標點樣式僅由 <code>lang</code> 決定(<code>lang.policy</code><code>lang.style</code>),無逐元素覆寫。新增與 <code>data-jz-level</code><code>level.text</code> 同構之逐元素樣式機制(<code>banjiao</code> 渲染早已就緒、此為純路由,無新壓縮邏輯/CSS)。單元測試 121→127 全綠。</p>
<h3>設置格式</h3>
<ul>
<li><strong>HTML 屬性</strong><code>data-jz-style="banjiao|kaiming|quanjiao"</code>——標於元素或任一祖先,該子樹用此樣式。</li>
<li><strong>選項</strong><code>lang: { stylePolicy: [{ selector, style }] }</code>——如 <code>[{ selector: "h1, h2, button, .ui", style: "banjiao" }]</code></li>
<li><strong>優先序</strong>:逐元素覆寫(近祖先勝、同層屬性優先於選擇器)> <code>lang.policy</code> <code>lang.style</code> > 內建逐語言預設。屬性值非法(拼錯)則忽略、回退 lang。</li>
</ul>
<h2>專案整理:架構一致性 + 確認缺陷修復 + 文檔重整 <span class="badge accent">2026-07-07</span></h2>
<p>四路並行審計(coretypeset+CSS/設置 API/文檔)後之兩階段整理。核心:消除「透明穿越」判定之兩套口徑分裂,並修四個審計缺陷中之確認項(另一證偽、一延後)。單元測試 116→121 全綠。</p>
<h3>階段一 · 確認缺陷修復 清理</h3>
<ul>
<li><strong>jinze 冪等</strong>:二次 render(未 revert)曾產生巢狀 <code>jz-jinze</code>(違反 I6,實測 3→6)——守衛改判 <code>isJz(parent, PASS)</code></li>
<li><strong>pill 自身 avoid 漏檢</strong><code>processPills</code><code>avoidsSelf</code>——自身命中 avoid(如 <code>&lt;code data-jz-skip&gt;</code>)之 pill 曾仍被補間隙。連帶:<strong><code>CODE</code><code>KBD</code><code>skipTags</code> 預設移除</strong>(已由 <code>pillSelector</code> 以 pill 涵蓋;再列 skipTags 違反三分類互斥、且會抹掉普通 <code>code</code> 之 pill 兩端間隙)。若把 <code>code</code> 移出 <code>pillSelector</code>,須自行加入 <code>skipTags</code><code>scope.avoid</code></li>
<li>刪零引用死代碼(<code>createMarker</code><code>ANY_CJK</code><code>BD_ALL</code><code>BD_CLASS_NAMES</code><code>CreateOptions.attrs</code>);死旗標 <code>biaodian</code><code>emphasis</code><code>ruby</code><code>@reserved</code> 註記。</li>
</ul>
<h3>階段二 · 架構一致性</h3>
<ul>
<li><strong>透明判定單一真相源</strong><code>transparentInline</code> 提升為公開 <code>isTransparent</code>,邏輯文本流之遍歷原語(<code>collectRuns</code><code>adjacentLogicalChar</code><code>jiyaAdjacency</code>)共用之。<strong>未在 <code>styleInlines</code> 之未知行內標籤(如 <code>&lt;button&gt;</code>)採嚴格白名單、一律當邊界</strong>(用戶抉擇;透明須顯式登記)。分層澄清:結構切分安全 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 各有正當之不同邊界、刻意不併入。</li>
<li><strong>跨透明元素邊界避頭尾</strong>(修 #3):避頭類標點落於 <code>&lt;strong&gt;</code> 邊緣(<code>你好&lt;strong&gt;。世界&lt;/strong&gt;</code> 之句末點號位於 strong 首位)時,<code>forbiddenBreak</code><code>edgeCharEl</code> 穿越透明元素下探定位、render 沿透明邊緣上攀處理外層 parent,不再漏綁、誤置行首。</li>
<li><strong>avoid 排除歸一</strong><code>isAvoided</code> 改單一組合入口(自身或祖先),免除「<code>avoidsSelf ‖ isAvoided</code>」易漏一半之誤用(階段一 pill 漏檢即此類)。</li>
<li>小項:<code>scope.include</code> 型別去多餘 <code>| null</code><code>gapTrim</code> 補逐節點功能閘;<code>PASSES</code> 陣列補註執行序。</li>
<li><strong>元素分類單一入口 <code>finder.category</code></strong>(收尾):散落各遍歷原語各自短路之分類優先序,收斂為單一分類器——一元素恰屬 avoidpillisolateblocktransparentopaque 之一,優先序 <code>avoid pill isolate block transparent opaque</code>(帶 <code>data-jz-skip</code> 之 pill → avoid;同命中 pill 與 isolate → pill)。<code>isTransparent</code> <code>category==="transparent"</code><code>collectRuns</code><code>jiyaAdjacency</code><code>switch(category)</code><code>canSplit</code> <code>SPLITTABLE ∩ transparent</code>。行為等價(121 全綠)。</li>
</ul>
<h3>復驗後重新定性(不照單全收)</h3>
<ul>
<li><strong>lineedge 攀穿 isolate——非缺陷</strong><code>edgeRect</code> 為物理版面測量(<code>getClientRects</code>),isolate 斷的是邏輯 run/相鄰,物理行首行尾判定須跨 <code>&lt;mark&gt;</code> 找真實相鄰內容;初評誤判、已還原。</li>
<li><strong>lineEdgegapTrim 雙 ResizeObserver 派發序——確認非缺陷</strong>Chrome headless 實測回調按 observer <strong>創建順序</strong>派發(ResizeObserver 規範遍歷 observers 插入序,非 observe 序;實測 seq=12);<code>lineEdge</code>order 90)之 observer 先於 <code>gapTrim</code>order 92)建、故 resize 時恆先派發,<code>gapTrim</code> 之行末去隙恆基於已套用之半形版面(非陳舊態)。以註解鎖為不變量、不需改碼。</li>
<li><strong>五處幂等收斂——評估後不做</strong>:各檢查不同位置關係(祖先/父/子),無單一 <code>isJz</code> 形狀可全覆蓋,強行歸一徒增風險。</li>
</ul>
<h3>文檔(本次)</h3>
<ul>
<li>README 新增「全部選項速查」表——突出被埋沒之 <code>lang.style</code><code>styleInlines</code><code>blockTags</code><code>skipTags</code>(標籤名陣列)/<code>skipAttribute</code><code>isSkipped</code>;三分類改分層表述、avoid 改七源+組合入口。</li>
<li>ARCHITECTURE §3.1 <code>scope.avoid</code> 過時示例、§4.1 透明分層與 avoid 七源、§4.2 標點六子類(舊列 <code>bd-cop</code><code>bd-sep</code><code>quote</code> 與實作不符)修正。</li>
</ul>
<h2>標點擠壓句界例外(句末點號接開類保留右空白) <span class="badge accent">2026-06-13</span></h2>
<p>行內連續擠壓(機制 A)新增<strong>句界例外</strong>:句末點號(<code>。.!?</code>)後接開類引號/括號(<code>。「</code><code>?「</code>)時保留句末點號右側空白、僅收開類左側,全角式與開明式皆合占 <strong>1.5em</strong>(對稱於 <code>。」</code>)——下游回饋之語感修正。單元測試 113→116 全綠。</p>
<h3>變更</h3>
<ul>
<li><strong>句界例外</strong>:原機制 A 對相鄰標點對 <code>(L, R)</code> 兩側獨立收——<code>L</code> 有右側空白即收、<strong>不看 R 為何類</strong>,致句末點號接開類兩側皆收、合占 1em,丟失句界視覺(句末點號收尾後緊接開類起新引文,本應保留句界間距)。修為:<code>L</code> 為句末點號(<code>bd-stop</code>)且 <code>R</code> 為開類(<code>bd-open</code>)時不收 <code>L</code>、僅收 <code>R</code> 左側 → 1.5em。</li>
<li><strong>例外僅限句末點號+開類</strong>:句內點號 <code>,「</code>(屬同句)與雙括號 <code>」「</code><code>》《</code>(貼合相連)<strong></strong>屬句界,仍兩側皆收 1em<code>。」</code>(句末接閉類,閉類左為墨無左空白)本就只收句末點號、合占 1.5em,未變。</li>
<li><strong>半角式不回歸</strong>:半角式句末點號由 baseline(機制 C)已收半形,句界守衛僅跳過 adjacency 之重複追加、<strong>不移除</strong> baseline 之 <code>jz-half</code>;32 個可壓縮標點於半角式仍全半形(實測 0 回歸)。非可壓縮 <code>·・‧</code><code>…—</code> 各式皆全寬,屬設計。</li>
</ul>
<h3>範圍</h3>
<ul>
<li>唯一改動點 <code>jiyaAdjacencyPass.squeeze()</code><code>src/typeset/jiya.ts</code>)加 <code>stopBeforeOpen</code> 守衛;行端擠壓 <code>lineedge</code>、baseline 政策、<code>jinze</code><code>orphan</code><code>spacing</code> 皆不波及(純視覺寬度、無結構變更)。</li>
</ul>
<h2>行內元素三分類(KaTeX 肢解修復)+ v1 相容層移除 <span class="badge accent">2026-06-12</span></h2>
<p>下游問題六(KaTeX 公式被禁則綁定肢解)修復:確立行內元素<strong>三分類</strong>口徑(透明/隔離/整體),所有遍歷原語共用同一判定。v1 相容層隨最後一批消費端遷移完成而移除。單元測試 101→112 全綠。</p>
<h3>修復(下游問題六)</h3>
<ul>
<li><strong>Bugjinze 透明下潛肢解 pill</strong>。行內公式緊鄰禁則標點(<code>…$x$</code>)時,jinze 為綁鄰字下潛 <code>.katex</code>——KaTeX 全樹皆 <code>&lt;span&gt;</code>,舊版<strong>僅憑標籤名判透明</strong>,把公式內側末字連同完整殼層逐層克隆切出(<code>splitoff</code>),上標漂移錯位;<code>pillSelector</code><code>skipAttribute</code><code>scope.avoid</code> 三層防護全被刺穿。復現另揭兩向量:相鄰兩 pill 時 <code>descend</code> 刺穿致 <code>jz-hws</code> 插入公式內部;pill 內部標點被 jiya charify。</li>
<li><strong>根因一:判透明以標籤名而非元素</strong><code>transparentInline(el)</code> 改以元素判定——先排除 pill/isolateavoid 再查標籤集;<code>core/split</code><code>canSplit</code> 諮詢 finderpillisolateavoid 一律不可切 → 退化<strong>整元素綁定</strong>——pill 即「作為原子鄰字單位整體綁入 <code>jz-jinze</code>」之期望行為)。</li>
<li><strong>根因二:avoid 僅查祖先</strong>。新增 <code>avoidsSelf(el)</code> 自身判定——元素自身命中 <code>scope.avoid</code><code>skipAttribute</code><code>data-jz-skip</code> 時即為邊界(舊版被當透明穿越,防護形同虛設)。</li>
<li><strong>根因三:descend 越界跳找</strong><code>adjacentLogicalChar</code><code>descend</code> 改三態(邊界字/無內容續找兄弟/<strong>blocked</strong> 終止)——遇 pillisolate/avoid 不可跳過續找更遠之字。</li>
<li><strong>pill 內部全面不可見</strong><code>eachTextNode</code> 過濾 <code>insidePill</code>——pill 內部不 charify、不插間隙、不做長詞包裝(舊版消費端自加之 pill 如 <code>.katex</code> 內部曾被直接變更)。</li>
</ul>
<h3>新增:行內元素三分類(§4.1</h3>
<ul>
<li><strong>透明</strong><code>styleInlines</code> <code>jz-*</code>):穿越、run 延續、禁則可克隆切分——<code>strong</code><code>em</code> 等純樣式。</li>
<li><strong>隔離</strong><code>isolateSelector</code><strong>新增選項</strong>,預設 <code>"mark"</code><code>""</code> 停用):邊界斷 run/斷相鄰/不可切分、<strong>兩端補隙</strong>(同 pill),<strong>內部照常處理</strong>(自成脈絡)——帶背景色之 <code>&lt;mark&gt;</code> 等盒樣式行內(切分背景盒有可見接縫,<code>MARK</code> 並自可切白名單移除)。</li>
<li><strong>整體</strong><code>pillSelector</code>):內部對一切 pass 不可見,禁則時整體綁入——<code>code</code><code>kbd</code>、行內公式 <code>.katex</code>、引用標 <code>.cite-ref</code> 等宜納入。</li>
<li>spacing Pass B 邊界選擇器擴為 <strong>pill isolate</strong>jiyaAdjacency 對 isolate 斷相鄰而內部自成脈絡;orphan 補 avoid 塊不綁(舊漏判)。</li>
</ul>
<h3>移除:v1 相容層</h3>
<ul>
<li>最後一批 v1 消費端遷移 v2 API 完成,<code>src/compat/v1.ts</code><code>createCjkAutospace</code>)、IIFE 全域別名、死選項 <code>autospaceClass</code>(經查從未被任何 pass 讀取)一併移除;不變量 I8 退役。v1 原始碼保存於 <code>v1-autospace</code> 分支。</li>
</ul>
<h2>行內排版透明性修復 標點懸掛移除 <span class="badge accent">2026-06-11</span></h2>
<p>修復兩個下游 Bug(同源於「行內元素對排版透明」原則在各遍歷原語口徑不一)+三項同根隱患審計;並把上一輪「停用」之標點懸掛<strong>正式移除</strong>(負 margin 程式碼/CSS/型別一併刪)。單元測試 90→101 全綠。</p>
<h3>修復</h3>
<ul>
<li><strong>Bugpill 前標點誤加間隙</strong>(如 <code>漢字。&lt;code&gt;</code>)。根因:<code>finder.adjacentLogicalChar</code><code>descend</code> 遇不透明之 <code>jz-char</code> 既不進入亦不終止、而<strong>跳過</strong>它越過標點取到更前之漢字,誤補 <code>jz-hws</code>。修復=<code>isTransparentInline</code><code>JZ-CHAR</code><code>JZ-INNER</code><code>descend</code> 遂回傳標點(非 pill 鄰字、不補隙),與 <code>collectRuns</code> 之既有透明遞迴一致。</li>
<li><strong>Bug:行內樣式元素整體掉行</strong><code>&lt;strong&gt;很長一串&lt;/strong&gt;</code> 把整串粗體塞進 <code>nowrap</code> inline-block,行末放不下即整體掉行、上行留巨大空隙)。新增 <code>core/split.ts</code> 之克隆切分:沿祖先鏈把邊界詞元剝入淺克隆(保留標籤/class/stylelang、去 id;標 <code>data-jz-kind="splitoff"</code>),禁則/垂懸字只綁該克隆(最小綁定),原元素餘部留原位、渲染無縫。<code>revertPass</code> 增延後階段把 splitoff 併回原元素(先解包 wrap、後併 splitoff),<code>render→revert</code> 還原一致。純樣式標籤白名單可切,<code>a</code><code>abbr</code><code>ruby</code>/帶 id 退化整綁。</li>
<li><strong>審計(同根隱患)</strong>T4 <code>lineedge</code> 之行端攀爬集改由 <code>options.finder.styleInlines</code> <strong>派生</strong>(棄硬編碼子集,免遺漏 <code>ins</code><code>del</code><code>sub</code> 等致行端誤判);T5 <code>jiyaAdjacency</code> 遇 pill 斷相鄰、不穿越;T6 <code>spacing</code> processPills 補 per-node 功能閘(<code>data-juzhen-off="spacing"</code> 區域內不補隙)。</li>
</ul>
<h3>標點懸掛:實作後停用 → 正式移除</h3>
<p>純 CSS 手段(負 margin/相對位移/零寬盒)經實測為<strong>機制級死路</strong>,已移除(負 margin 之 <code>lineedge</code> 程式碼/<code>jz-hang-*</code> CSS<code>LineEndMode</code><code>lineEnd</code><code>features.hanging</code> 一併刪)。行端僅餘半形擠壓(<code>jz-half-le</code>)。</p>
<ul>
<li><strong>寬度守恆論證</strong>:行端標點盒若保持非零寬,則墨色出去多少、版心內就空出多少(行末實義字無法齊版心右緣);若收為零寬,則釋放一整字寬、斷行器把下一字拉上本行與標點重疊(且 <code>justifyAtoms</code> 預設之 inline-block 下負 margin 被 justify 吸收)。<strong>CSS hack 接觸不到斷行器,此恒等式無解。</strong></li>
<li><strong>唯一正解=原生</strong> <code>hanging-punctuation</code>,惟 ChromeFirefox 至今不支援(僅 Safari),且 <code>jz-char</code> 為 inline-block 原子非文本、原生懸掛不作用之。未來若做須是繞開 jz-char 的「原生管線」新功能(<code>text-spacing-trim</code> <code>hanging-punctuation</code>,按能力門控),與已移除碼無共享,故不保留休眠碼。</li>
<li><code>hanging</code> 公開選項保留為 <strong>@deprecated no-op</strong>(API 相容;測試覆蓋 option/<code>data-juzhen</code><code>blocks</code> 各路徑皆不產生 <code>jz-hang-*</code> 且輸出與不傳一致)。詳見 <code>ARCHITECTURE §6.5.6</code></li>
</ul>
<h2>功能分級 垂懸字(標點懸掛停用) <span class="badge accent">2026-06-09</span></h2>
<p>ARCHITECTURE §6.5:功能分級、垂懸字(方案 C)落地工作良好;原計劃之標點懸掛經實測<strong>確認不可行、全面停用</strong>。共 90 單元測試全綠(含懸掛防逃逸覆蓋)。</p>
<h3>新增</h3>
<ul>
<li><strong>功能分級(文本級 vs 段落級)</strong>:把已實作功能切為兩級——<strong>文本級</strong>(中西間隙 <code>spacing</code>、標點擠壓 <code>jiya</code>,任意文本片段適用)與<strong>段落級</strong>(禁則 <code>jinze</code>、垂懸字 <code>orphan</code>、長詞 <code>longWord</code>,需行/段布局)。塊級元素預設全功能;<code>data-jz-level="text"</code> 子樹(最近祖先勝)或選項 <code>level:{text:"選擇器"}</code> 標記之元素(標題/按鈕/UI)僅跑文本級。實作為 <code>Finder.levelFor</code><code>levelAllows</code> 各 pass <code>level</code> 元數據;段落級 pass(含 charify 之 longWord)逐元素受閘。<code>spacing</code> 補單元素 root 支援(行內/非塊 root 亦處理)。</li>
<li><strong>垂懸字避免 <code>orphan</code>(方案 C,斷行層無測量,order 70)</strong>:段末行 CJK 實義字 ≥ N(預設 2;標點/符號不計)。把段末「第 N 個實義字起至段末」(含尾隨標點)包入 <code>jz-orphan</code><code>white-space:nowrap</code>),整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型成立、無需量測。邊界字落於行內元素內時綁定整個塊級直接子(跨 <code>jz-jinze</code> 攀爬、不切割)。<strong>預設開 chars=2</strong><code>orphan:false</code> 關);段落級,<code>data-jz-level="text"</code> 跳過。</li>
<li><strong>行端模式重構 + 行歸屬判定 bug 修正</strong>:lineedge 行端處理抽象為可配模式(<code>squeeze</code>hangnone),現有行端半形=<code>squeeze</code>、保留並改善。修「行歸屬判定」bug<code>jz-char</code> 為 inline-block,其行盒 <code>top</code> 與相鄰純文本字形 <code>top</code> 因基線差約 5px、遠小於行高卻超舊容差(EPS=4px),致同行誤判為下一行;改判「垂直區間」(下一行=<code>b.top ≥ a.bottom</code>、上一行=<code>b.bottom ≤ a.top</code>,容 2px)。</li>
</ul>
<h3>標點懸掛:實作後全面停用</h3>
<p>原計劃之標點懸掛(行端標點凸出版心)經 headless + 真實瀏覽器實測<strong>確認純負 <code>margin</code> 機制不可行</strong>,已全面停用(<code>hanging</code> 選項仍被接受但無任何效果)。</p>
<ul>
<li><strong>不可行原因</strong>:① 句號墨色偏字框左下角,安全凸出量(&lt; 1 個 CJK 字寬 1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);② 凸出滿 1em 釋放一整字寬、把下一字拉上本行致<strong>重疊</strong>;③ 與 <code>inline-block</code> 對齊原子互斥(<code>justifyAtoms:true</code> 下負 margin 被 justify 吸收);④ <code>ResizeObserver</code> 重排時兩階段標記錯亂。</li>
<li><strong>三重防逃逸</strong>:① <code>index</code><code>lineEnd:"squeeze"</code>(忽略 <code>hanging</code> 選項);② <code>modeForChar</code> 無條件降 squeeze;③ <code>applyEdge</code><code>HANG_ENABLED=false</code> 總開關。縱使下游以 <code>hanging</code><code>data-juzhen</code><code>blocks</code> 任何方式要求亦不啟用(單元測試覆蓋各路徑皆不產生 <code>jz-hang-*</code>)。</li>
<li><strong>未來路徑</strong>:原生 CSS <code>hanging-punctuation</code> 為正解,待 Chrome 支援(現僅 Safari)並解決與 inline-block 原子衝突。hang 之程式碼、CSS、型別保留休眠。詳見 <code>ARCHITECTURE §6.5.6</code></li>
</ul>
<h3>工具</h3>
<ul>
<li><code>demo/showcase.html</code>(新增):定寬文本框並排對照,逐場景演示功能分級與垂懸字(關閉/開啟孤字、chars=3、標點不計入 N)。</li>
<li><code>demo/resize-check.html</code>:增垂懸字(<code>jz-orphan</code> 描框 + 關閉對照)拖曳檢查區。兩頁均不含懸掛(已停用)。</li>
</ul>
<h2>標點挤压一致性 <span class="badge accent">2026-06-04</span></h2>
<h3>新增/修復</h3>
<ul>
<li><strong>全角句點 <code></code> 納入句末標點</strong><code>BD_STOP</code><code>。!?</code> 擴為 <code>。.!?</code>——納入全角句點 <code></code>U+FF0E,異於表意句號 <code></code> U+3002)。實測 Noto CJK SCTC 之 <code>halt</code> 皆覆蓋、挤压收半形 advance 0.5em,與 <code></code> 一致;<code>inkBias</code> 同步補(簡 left/繁 center)。政策行為同 <code>。!?</code><strong>全角式與開明式句末保持全角 1em</strong>,banjiao(或鄰接被收)挤半。設計原則:<code>halt</code> 即字型之「挤压意願」,字型不提供某標點 <code>halt</code>(字形占滿、不宜挤)則信任字型、保持全角不強挤。</li>
<li><strong>連續標點挤压規則:雙側獨立 + 透明穿越</strong>(修正既有不一致)。規則:相鄰兩標點,<strong>對每一標點</strong>獨立判斷——若其「有空白之側」緊鄰另一標點即收半。原為「二選一只收一側」,致 <code>,「</code> 全角式誤為 1.5em;修後 <code>,「</code> <strong>全角式與開明式皆 1em</strong><code>。」</code> <strong>全角式 1.5em/開明式 1em</strong><code>·—</code> 兩式皆 2em(不宜挤)。並改「同 parent 直接兄弟」掃描為<strong>從 root 之透明遍歷</strong>(穿越 <code>strong</code><code>em</code> 等行內邊界、blockavoid 斷相鄰),使跨 inline 邊界之連續標點亦正確配對(原漏致達 2em)。</li>
</ul>
<h2>字符分類規則集 <span class="badge accent">2026-06-04</span></h2>
<h3>新增</h3>
<ul>
<li><strong>可配置字符分類(<code>charClass</code></strong>:把散落各 pass 之硬編碼分類(中文 CJK/西文 ANS/全寬標點 BD_* 子類)統一抽象為<strong>規則集</strong>,內置默認即現狀值,消費端經 <code>options.charClass</code><code>{ cjk?, western?, biaodian?:{stop,pause,open,close,middle,liga} }</code><strong>追加擴充</strong>,覆寫 <code>spacing</code><code>jiya</code><code>jinze</code> 之分類判定。動機:未分類符號(如二點省略 <code></code> U+2025)——不 charify/不挤;以 <code>charClass:{biaodian:{stop:"‥"}}</code> 即可歸入對應類。<strong>向下兼容</strong>:不傳則行為與內置完全一致、各實例隔離(覆寫不污染默認)。v1 為追加語義(不重分類/移除內置);結構性 ASCII 不開放。<code>spacing</code> 之間隙正則改由規則集 per-instance 編譯。測試 66 通過(含 9 條 charClass)。</li>
</ul>
<h2>下游回饋修復批次 <span class="badge accent">2026-06-03</span></h2>
<p>聚焦 Peek.nvim 等下游回饋之原子透明與中西間隙正確性,皆以真實 Chromium 量測驗證(jsdom 之 TreeWalkeroffset 失真,不可信)。</p>
<h3>修復</h3>
<ul>
<li><strong>原子透明(段落繼承屬性漏溢)</strong><code>jz-*</code> inline-block 原子對消費端段落樣式透明——歸零會被原子重複套用之 <code>text-indent</code><code>hanging-punctuation</code>(過套用),並令原子 <code>text-decoration: inherit</code> 以接續段落下劃線(欠套用)。段落首行縮排正常、下劃線連續不斷,消費端無需配置。</li>
<li><strong>中西間隙 · <code>gapTrim</code> 自我應驗</strong>:行末去隙原依「後字是否在更低行」判定,而後字落點受間隙自身(及下游間隙)之 <code>margin</code> 推擠,致行中間隙被誤 trim——表現為容器寬相關之「最後一個間隙消失」、行內 <code>code</code> pill 跨行時右側間隙消失。改為「起始全部 trim、由左至右逐一定案」,量測不受任何 <code>margin</code> 污染。</li>
<li><strong>中西間隙 · 西文字集完整化</strong>:補入貨幣符號(<code></code> 等 U+20A0–20BF)、拉丁擴充 A/B(拼音聲調 ā ǎ ē)、西里爾、數學運算符,並把希臘補全至含 α-ω。鄰接 CJK 之手工空格遂正規化、邊界補隙。列舉法以 <code>\uXXXX</code> 表示、無需 regex flag,相容 2016Windows 10 時代瀏覽器。</li>
<li><strong>標點誤隙</strong>:全角省略號 …… 後接 CJK 不再誤補隙(<code>DOTS_CJK</code> 移除 <code>|…</code>);收尾彎引號 ” 後接拉丁/數字不再補隙(移除 pangu <code>QUOTE_AN</code> 偏例,與 」』)》】〕 等所有收尾標點一致)。全部常用中文標點 × 簡繁共 104 例稽核,CJK 間皆 0 誤隙。</li>
</ul>
<h3>變更</h3>
<ul>
<li><strong>字型移出聚珍管理</strong>:字型屬消費端領域,聚珍不再內建任何 <code>font-family</code>/字型堆疊(移除 <code>_lang.css</code><code>dist/juzhen.css</code> 零字型規則)。逐語言字型完全由消費端 CSS 設定(範式見文檔「字型與 locl」)。聚珍僅守三項非管理性本分:<code>jz-* { font: inherit }</code> 使原子繼承消費端字型、不強加;半形 <code>font-feature-settings</code> 顯式併入 <code>"locl" 1</code>(連同 <code>halt</code>)確保泛 CJK 字型之地域標點字形不因設 halt 而失;render 只讀不改 <code>lang</code>。halt/margin 兩模式皆已驗證字型繼承與 locl 正常。</li>
</ul>
<h3>工具</h3>
<ul>
<li><strong><code>demo/resize-check.html</code></strong>:可拖曳容器檢查頁——拖動方框寬度,驗證中西間隙在各寬度下恆穩、標點無誤隙;可切 <code>justify</code> 與「顯示間隙」描邊。</li>
</ul>
<h2>v2 切片 1 · 增補 <span class="badge accent">2026-06</span></h2>
<p>切片 1 之上的修補與強化,聚焦標點半形對缺 <code>halt</code> 字型之支援,及下游回饋之邊界/分頁器修復。</p>
<h3>新增</h3>
<ul>
<li><strong>標點半形 <code>margin</code> 後備模式</strong>(缺 OpenType <code>halt</code> 之字型):<code>jiya.halfWidth: "halt"</code>(預設)|<code>"margin"</code><code>margin</code><code>width:0.5em</code> +字身依墨色偏側溢出定位,於 CSS 盒模型重現 <code>halt</code> 之半形 advance(真 0.5em;相鄰標點各 0.5em、合佔 1em,無負 margin 之鄰接疊加/單側重疊)。模式由 <code>data-jz-halfwidth</code> 子樹屬性決定,可逐區塊/逐 <code>lang</code>(含 halt-island 反置)。</li>
<li><strong>字形偏側表 <code>inkBias</code></strong>:依字身墨色中心給 <code>leftcenterrightfull</code>;彎引號/角括號/尖括號偏一側、圓方括號近中。參考表據方正書宋 GBK 實測,可經 <code>jiya.bias</code><code>lang</code> 逐字覆寫。</li>
<li><strong>字型適配工具與工作流</strong><code>tools/measure-bias.py</code>(量 glyph ink 中心、產偏側建議表)+文檔之「字型適配工作流」。<code>demo/fallback.html</code>:方正書宋 GBK 對照 halt 失效 vs margin 生效。</li>
<li><strong><code>justifyAtoms</code> 選項</strong>:預設 <code>true</code>;設 <code>false</code><code>jz-char</code><code>jz-jinze</code><code>display:inline</code>,相容分頁器(Paged.js)之對齊引擎(<code>halt</code> 半形與禁則 <code>nowrap</code> 保留)。</li>
</ul>
<h3>修復</h3>
<ul>
<li><strong>字型不覆寫消費端</strong><code>_lang.css</code> 之 Noto 字型堆疊改為純後備——全包 <code>:where()</code> 零特異度、只命中帶 <code>lang</code> 屬性之元素、不直接覆寫標點字面容器 <code>jz-inner</code>。消費端 <code>font-family</code> 恆優先,標點隨之以消費端字型渲染(修正先前標點被強制換成 Noto 之缺陷)。</li>
<li><strong>pill 緊鄰標點之左緣間隙</strong><code>adjacentLogicalChar</code> 透明穿越 <code>jz-jinze</code>(同 <code>jz-hws</code>/樣式行內)→ pill 後接 CJK 標點時,左緣 <code>jz-hws</code> 與作者空格剝除復原。</li>
<li><strong>CJK↔CJK 贅餘作者空格</strong>:含跨 inline 標籤者一律剝除、不留隙,與 CJK↔西文之邊界正規化一致(全形空格 U+3000 不動)。</li>
</ul>
<h3>已知限制</h3>
<ul>
<li><code>margin</code> 後備半形依賴 <code>inline-block</code><code>width:0.5em</code>),與 <code>justifyAtoms:false</code> 不可兼得——分頁器場景請用含 <code>halt</code> 之字型。</li>
<li><code>margin</code> 偏側為三檔量化(0/0.25/0.5em)之近似;字形極端者須以 <code>jiya.bias</code> 逐字微調。</li>
</ul>
<h2>v2.0.0 <span class="badge accent">切片 1</span> · 從零重寫</h2>
<p><code>ARCHITECTURE.md</code> 契約,從零重寫為「現代簡/繁中文網頁排版套件」。倉庫名 <code>cjk-autospace</code> 不變;公開 API 為 <code>createJuzhen</code> /全域 <code>Juzhen</code>;自訂元素前綴 <code>jz-</code>。v1 完整保留於 <code>v1-autospace</code> 分支。</p>
<h3>新增</h3>
<ul>
<li><strong>核心引擎</strong><code>finder</code>(遍歷/avoid<code>scope.include</code>/邏輯 run/pill 鄰字/分塊閘 <code>featureEnabledFor</code>)、<code>unicode</code>(字元表+標點子類+字形側位)、<code>locale</code>(語言分類+全角/開明政策)、<code>dom</code><code>jz-*</code> 工廠+可還原標記)、<code>pass</code> 聚合管線。</li>
<li><strong>中西間隙</strong>(margin 模型):包裹邊界左側字、以 <code>margin-right</code> 留隙,<strong>不注入空白字元</strong><code>textContent</code> 完全不變、複製緊湊、<code>justify</code> 不暴增、不撐框、行首乾淨。<code>gapTrim</code> layout pass 行末去隙以齊右。</li>
<li><strong>標點擠壓</strong>:全角式/開明式/半角式,三機制(baseline 政策+行內連續擠壓+行端擠壓)。半形以字型 OpenType <code>halt</code> 渲染。全域 <code>lang.style</code> +逐 <code>lang</code> <code>policy</code></li>
<li><strong>禁則(避頭尾)</strong>:閉類/點號綁前一字、開類綁後一字,以詞元為邊界、長引文中段可斷;兼作 <code>justify</code> 綁定。</li>
<li><strong>長英文詞</strong><code>&lt;span lang&gt;</code> <code>hyphens</code> 原生斷詞。</li>
<li><strong>justify 句末單份間距</strong><code>jz-char</code> inline-block 原子化。</li>
<li><strong>v1 相容層</strong><code>createCjkAutospace().apply()</code> 映射;IIFE 全域別名。</li>
<li><strong>構建/測試</strong>esbuildESMIIFECSS)+ tscd.ts);jsdom 測試;<code>demo/</code> 簡繁並列+justify+標點寬度量測。</li>
</ul>
<h3>相對 v1 之改進</h3>
<ul>
<li>中西間隙複製乾淨(<code>textContent</code> 不變;v1 之 <code>&lt;span&gt;</code> 真實空格會被選取)。</li>
<li>擠壓由「<code>halt</code> 套整段=半角式」進化為依 <code>lang</code> 之全角/開明/半角,且加入行內連續與行端擠壓。</li>
<li>新增禁則避頭尾、justify 句末單份間距、跨瀏覽器一致策略與顯式禁用層。</li>
</ul>
<h2>v1cjk-autospace · v1-autospace 分支</h2>
<p>Pangu.js v7.2.1 風格之 CJK↔Latin 邊界自動間距 shim,工廠 <code>createCjkAutospace(options).apply(root)</code></p>
<ul>
<li>Pass A:跨樣式邊界中西間隙(marker span 含真實空格 <code>user-select:none</code>)。</li>
<li>Pass Bpill<code>code</code><code>kbd</code>)邊界間隙。</li>
<li>Pass C:長英文詞 <code>&lt;span lang&gt;</code> <code>hyphens</code></li>
<li>Pass D:開明式標點擠壓(句內點號 <code>font-feature-settings:"halt"</code> 半形)。</li>
</ul>
</main>
<footer class="page">
<p>本頁由聚珍排版工程記錄。所有文檔操作均於 docs 分支進行;main 僅存一份未追蹤之本地鏡像。</p>
</footer>
<!-- RUNTIME_SPRITE -->
<script type="text/markdown" id="md-source">
# 變更紀錄Changelog 聚珍Juzhencjk-autospace
## 效能第二三層掃描/佈局提速 增量恢復Tier32026-07-12
承第一層S2+S3含掃描/佈局提速與增量恢復動態頁面局部變化免全樹重掃全程獨立複驗4 份真人撰寫真實文檔驗收測試 130140 全綠
### 掃描 / 佈局提速
- 祖先游走記憶化中間發現S4insidePillisAvoided 逐文本節點沿祖先鏈迭代回填記憶化O(文本×depth)O(元素)el.matches() 約降五成深嵌套 render 進程隔離墻鐘 707ms254ms2.8×
- S1 gapTrim 對齊門尾隨 margin 僅右緣 flush 之對齊可見 leftstart(ltr) 可整批跳過 O(gaps) 次重排非兩端對齊 gapTrim 數千 ms數十 msjustify/center/right/RTL 行為不變真瀏覽器差分 trim 集逐位相同
### 增量恢復第三層向下兼容 API
- C1 持久化 Finder render 復用元素鍵快取正確性靠失效invalidate(el) 清整棵子樹快取revert 自動失效其子樹
- C3 rerender(root)revert+render編輯 400 段中一段 766×matches 1441×
- C2 observe/disconnectMutationObserver 自動增量內容/分類屬性變化重處理最近塊新增之塊直接 renderrender/revert 期間暫停觀察不自觸發
- append 修復IV-3 差分複驗發現單測漏掉向葉塊 jz-orphan追加子塊使其轉非葉須撤容器陳舊 orphan
### 獨立複驗 真實文檔驗收
- 5 種獨立方法差分/往返/增量全量/進程隔離測速/真瀏覽器 S1與既有單測/基準不同源抓到並修復 append bug
- 4 份真人撰寫真實文檔多重集守恆無字元改///亂序revert 淨移除結構真瀏覽器整條管線零報錯
### 文檔I1/I6 收緊
- 驗收浮現 renderrevert 不還原 spacing 規範化之贅餘作者空格 Neovim 在Neovim中固有非本批引入I1 明示此為刻意常態例外I6 明示邏輯等價逐字節之唯一差異即被規範化之空格README revert 之範圍
## 效能第一層掃描提速category 記憶化 orphan 葉塊預計算2026-07-11
下游效能訴求掃描疑 O()之第一層無狀態保行為無公開 API 變更測試 129130 全綠輸出不變
### 優化
- S3 category 記憶化finder.category WeakMap<Element,InlineCategory> 記憶化砍除各遍歷原語跨 pass
對同一元素之重複 el.matches()實測 pill/isolate 密集頁 el.matches() 82045842約降三成命中率 55%
安全單次 render 內分類恆定Finder 每次 render 重建 快取僅存活單次 render無跨 render 失效
- S2 orphan 葉塊預計算以一趟上攀預計算非葉塊Set 取代逐塊 block.querySelector(sel) 掃子樹之
O(Σ塊子樹)深塊嵌套 O()某塊為另一塊之最近塊祖先 含塊級後代與原 querySelector 同口徑等價
### 亲驗與待議
- 亲驗暫禁 category 快取則 computeCategory 10412322el.matches() 58428204初次插桩誤打
HTMLBodyElement.prototype 漏計改補 Element.prototype.matches 後為準
- 新發現下一機會剩餘 el.matches() 最大來源為 insidePill 之逐文本節點沿祖先鏈 pillMatches未記憶化
## splitTreeAfter 攀升對稱守衛opaque 中間祖先不再被克隆肢解2026-07-11
下游 .selection-bar-message 病例垂懸字段末實義字落於 opaque `<button>`巢於可切 `<span>`splitElementAt 僅驗頂層 spansplitTreeAfter 逐層克隆文本節點top祖先鏈時中間 opaque button 從未複查即被克隆肢解複製出 splitoff buttonfirstTextlastText 守下潛攀升卻無對稱守衛真正病根下游 per-instance 迴避僅治標
### 修復
- 攀升守衛splitTreeAfter 切分前先驗textNode.parentNodetop top整條祖先鏈皆 canSplit任一層 opaquepillisolate id 即放棄回退整綁與下潛守衛對稱純新增守衛無新克隆邏輯jinze isolateBoundaryToken 因下潛已守行為不變附帶杜絕中間祖先帶 id 之克隆重複 id
- 不變量canSplit 須對切點 textNode top 之整條祖先鏈成立非僅 top下潛firstTextlastText攀升splitTreeAfter 守衛雙向守
- 測試 127129 全綠2末實義字在 opaque button巢於可切 span 綁整 span splitoffbutton 完整退化路徑 renderrevert 還原一致亲驗暫禁守衛則該測試於 splitoff斷言失敗
## 逐元素標點樣式覆寫data-jz-stylestylePolicy2026-07-11
下游需求非段落特定範圍元素用半角式段落用全角開明原樣式僅由 lang 決定新增逐元素覆寫 data-jz-level 同構純路由banjiao 渲染早已就緒測試 121127 全綠
### 設置格式
- HTML 屬性 data-jz-style="banjiao|kaiming|quanjiao"標於元素或任一祖先該子樹生效
- 選項 lang.stylePolicy: [{ selector, style }] [{selector:"h1,h2,button,.ui", style:"banjiao"}]
- 優先序逐元素覆寫近祖先勝屬性優先於選擇器 lang.policy lang.style 內建非法屬性值回退 lang
## 專案整理架構一致性 確認缺陷修復 文檔重整2026-07-07
四路並行審計後之兩階段整理核心消除透明穿越判定之兩套口徑分裂測試 116121 全綠
### 階段一 · 確認缺陷 清理
- jinze 冪等二次 render 曾巢狀 jz-jinze違反 I636守衛改判 isJz(parent, PASS)
- pill 自身 avoid 漏檢processPills avoidsSelf連帶 CODE/KBD skipTags 預設移除已由 pillSelector pill 涵蓋再列 skipTags 違反三分類互斥會抹掉普通 code pill 間隙若把 code 移出 pillSelector 須自行加入 skipTags/scope.avoid
- 刪死代碼 createMarker/ANY_CJK/BD_ALL/BD_CLASS_NAMES/CreateOptions.attrs死旗標 biaodian/emphasis/ruby @reserved
### 階段二 · 架構一致性
- 透明判定單一真相源transparentInline 提升為公開 isTransparent邏輯文本流三原語collectRuns/adjacentLogicalChar/jiyaAdjacency共用之未在 styleInlines 之未知行內標籤採嚴格白名單一律當邊界用戶抉擇分層canSplit切分安全lineedge物理版面各有正當之不同邊界不併入
- 跨透明元素邊界避頭尾 #3句末點號落於 <strong> 首位你好<strong>世界</strong> forbiddenBreak edgeCharEl render 沿 parent
- avoid 排除歸一isAvoided 改組合入口自身或祖先avoidsSelf isAvoided漏一半
- 小項scope.include 去多餘 |nullgapTrim 補功能閘PASSES 補註執行序
- 元素分類單一入口 finder.category收尾分類優先序收斂為單一分類器avoidpillisolateblocktransparentopaqueisTransparent=category==="transparent"collectRuns/jiyaAdjacency switch(category)canSplit=SPLITTABLEtransparent行為等價
### 復驗後重新定性不照單全收
- lineedge 攀穿 isolate非缺陷edgeRect 為物理測量須跨 mark 找真實相鄰初評誤判已還原
- lineEdge/gapTrim ResizeObserver 派發序確認非缺陷Chrome 實測按 observer 創建序派發規範插入序 observe lineEdgeorder 90先於 gapTrimorder 92resize 恆先派發不會用陳舊半形態註解鎖不變量不改碼
- 五處幂等收斂不做各檢查不同位置關係無單一 isJz 形狀可全覆蓋強行歸一徒增風險
### 文檔
- README 新增全部選項速查突出 lang.style/styleInlines/skipTags/skipAttribute/isSkipped三分類改分層avoid 改七源
- ARCHITECTURE §3.1 scope.avoid 過時示例§4.1 透明分層§4.2 標點六子類修正
## 標點擠壓句界例外句末點號接開類保留右空白2026-06-13
### 變更
- 句界例外機制 A 原對相鄰標點對 (L, R) 兩側獨立收L 有右空白即收不看 R 為何類致句末點號接開類兩側皆收合占 1em丟失句界視覺修為L 為句末點號bd-stop R 為開類bd-open時不收 L僅收 R 左側 全角式與開明式皆 1.5em對稱於
- 例外僅限句末點號開類句內點號 與雙括號 仍兩側皆收 1em句末接閉類本就只收句末點號合占 1.5em未變
- 半角式不回歸句末點號於半角式由 baseline 已收半形守衛僅跳過 adjacency 之重複追加不移除 baseline jz-half實測 32 可壓縮標點全半形0 回歸· 各式皆全寬屬設計
### 範圍
- 唯一改動點 jiyaAdjacencyPass.squeeze() stopBeforeOpen 守衛lineedgebaselinejinzeorphanspacing 不波及測試 113116 全綠
## 行內元素三分類KaTeX 肢解修復 v1 相容層移除2026-06-12
### 修復下游問題六
- jinze 透明下潛肢解 pill行內公式緊鄰禁則標點時KaTeX 全樹皆 span舊版僅憑標籤名判透明公式內側末字連殼層被克隆切出splitoff上標漂移pillSelectorskipAttributescope.avoid 三層防護全被刺穿另修相鄰 pill descend 刺穿致 jz-hws 插入公式內部pill 內部標點被 jiya charify
- 根因修復transparentInline 以元素判定先排除 pillisolateavoidcanSplit 諮詢 finder不可切整元素綁定pill 即整體綁入 jz-jinzeavoidsSelf 自身判定舊僅查祖先descend 三態blocked 終止不越界跳找eachTextNode 過濾 pill 內部
### 新增行內元素三分類§4.1
- 透明styleInlinesjz-*穿越run 延續可克隆切分
- 隔離isolateSelector 新增選項預設 "mark""" 停用邊界斷 run斷相鄰不可切分兩端補隙內部照常處理MARK 自可切白名單移除背景盒切分有可見接縫
- 整體pillSelector內部對一切 pass 不可見禁則時整體綁入spacing Pass B 邊界選擇器擴為 pill isolate
### 移除v1 相容層
- createCjkAutospaceIIFE 全域別名死選項 autospaceClass 一併刪消費端已全數遷移 v2 APII8 退役測試 101112 全綠
## 行內排版透明性修復 標點懸掛移除2026-06-11
### 修復
- pill 前標點誤加間隙漢字\<code\>descend 越過 jz-char 取更前字誤補 jz-hwsisTransparentInline JZ-CHARJZ-INNER
- 行內樣式元素整體掉行core/split 克隆切分splitoff禁則垂懸字只綁邊界詞元revert 延後階段併回
- 審計 T4 lineedge 攀爬集由 styleInlines 派生T5 jiyaAdjacency pill 斷相鄰T6 spacing processPills per-node 功能閘
### 移除
- 標點懸掛由停用轉正式移除寬度守恆矛盾CSS 接觸不到斷行器 margin 程式碼CSS型別一併刪hanging 選項保留為 no-op
## 功能分級 垂懸字2026-06-09
- 功能分級文本級spacingjiyavs 段落級jinzeorphanlongWorddata-jz-levellevel:{text} 標記
- 垂懸字避免 orphan方案 C斷行層無測量預設開 chars=2
- lineedge 行端半形squeeze行歸屬判定改垂直區間比較標點懸掛實作後確認不可行全面停用
## 標點挤压一致性2026-06-04
### 新增修復
- 全角句點 納入句末標點BD_STOP 擴為 U+FF0E 異於 U+3002Noto CJK halt挤压收 0.5em 一致inkBias left center政策同 全角式與開明式句末保持全角 1embanjiao 挤半原則halt 即字型挤压意願字型不給某標點 halt不宜挤則信任字型不強挤
- 連續標點挤压雙側獨立 透明穿越規則相鄰兩標點對每一標點獨立判斷若其有空白之側緊鄰另一標點即收半修後 全角式與開明式皆 1em 全角式 1.5em開明式 1em· 兩式皆 2em並改同 parent 掃描為從 root 透明遍歷 strong/em 邊界之連續標點亦正確挤原漏致 2em
## 字符分類規則集2026-06-04
### 新增
- 可配置字符分類 charClass硬編碼分類中文西文全寬標點子類統一抽象為規則集內置默認消費端經 options.charClasscjkwesternbiaodian{stop,pause,open,close,middle,liga}追加擴充覆寫 spacingjiyajinze 判定未分類符號 U+2025可經 biaodian.stop 歸全寬句末標點向下兼容不傳則行為不變實例隔離v1 追加語義不重分類內置結構性 ASCII 不開放spacing 間隙正則改由規則集 per-instance 編譯測試 66 通過
## 下游回饋修復批次2026-06-03
皆以真實 Chromium 量測驗證jsdom 不可信
### 修復
- 原子透明段落繼承屬性漏溢jz-* inline-block 原子對段落樣式透明歸零 text-indenthanging-punctuation過套用令原子 text-decoration:inherit 接續下劃線欠套用
- 中西間隙 gapTrim 自我應驗行末去隙原依後字是否在更低行判定後字落點受間隙自身及下游間隙 margin 推擠致行中間隙誤 trim容器寬相關最後一個間隙消失pill 跨行右隙消失起始全 trim右逐一定案
- 中西間隙西文集完整化補貨幣 拉丁擴充 A/B拼音 ā ǎ ē西里爾數學運算符希臘補全至 α-ω列舉法 \uXXXX無需 flag相容 2016
- 標點誤隙 後接 CJK 不誤補DOTS_CJK |收尾彎引號 後接拉丁數字不補移除 pangu QUOTE_AN 偏例中文標點 × 簡繁 104 例稽核皆 0 誤隙
### 變更
- 字型移出聚珍管理不再內建 font-family字型堆疊移除 _lang.css逐語言字型交消費端 CSS範式見文檔字型與 locl聚珍僅守jz-* font:inherit 繼承消費端字型半形 font-feature 顯式併 locl連同 haltrender 不改 langhaltmargin 兩模式字型繼承與 locl 皆驗證正常
### 工具
- demo/resize-check.html可拖曳容器檢查頁各寬度間隙穩定性標點無誤隙
## v2 切片 1 · 增補2026-06
### 新增
- 標點半形 margin 後備模式 halt 字型jiya.halfWidth "halt"|"margin"margin width:0.5em字身偏側溢出定位重現 halt模式由 data-jz-halfwidth 子樹屬性決定
- 字形偏側表 inkBiasleft/center/right/fulljiya.bias lang 逐字覆寫
- tools/measure-bias.py 字型偏側量測字型適配工作流demo/fallback.html
- justifyAtoms 選項false jz-char/jz-jinze inline相容 Paged.js
### 修復
- _lang.css 字型堆疊改純後備:where 零特異度只命中 [lang] 元素不覆寫 jz-inner 消費端字型恆優先
- adjacentLogicalChar 透明穿越 jz-jinze pill 緊鄰標點之左緣間隙與作者空格剝除復原
- CJKCJK 贅餘作者空格含跨 inline 標籤剝除不留隙
### 已知限制
- margin 後備與 justifyAtoms:false 不可兼得分頁器場景用 halt 字型
## v2.0.0 切片 1 · 從零重寫
ARCHITECTURE.md 從零重寫公開 API createJuzhen全域 Juzhen前綴 jz-
- 核心引擎 finderunicodelocaledompass
- 中西間隙margin 模型copy-cleangapTrim 行末去隙
- 標點擠壓 全角開明半角三機制半形 halt
- 禁則避頭尾justify 綁定長英文詞justify 句末單份間距v1 相容層
## v1cjk-autospacev1-autospace 分支
Pangu 風格 CJKLatin 間距 shimPass A 中西間隙B pillC 長詞D 開明擠壓
</script>
<!-- RUNTIME_JS -->
</body>
</html>
+6736
View File
File diff suppressed because one or more lines are too long
+271
View File
@@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>聚珍(Juzhen</title>
<meta name="generated-at" content="2026-06-02">
<meta name="source" content="cjk-autospace · 使用文檔">
<!-- RUNTIME_CSS -->
</head>
<body data-toc="1">
<header class="page">
<div class="meta">
<h1>聚珍(Juzhen</h1>
<p class="subtitle">現代簡/繁中文網頁排版套件——Han.css 之現代化重寫,複製乾淨、依 lang 全角/開明切換、justify 句末標點單份間距、跨瀏覽器一致。</p>
<p class="timestamp"><time>2026-06-02</time> · <span>cjk-autospace · 使用文檔</span></p>
</div>
<div class="actions">
<!-- ACTIONS -->
</div>
</header>
<main>
<!-- BODY -->
<p class="lede">聚珍(Juzhen)是現代簡/繁中文網頁排版套件,Han.css 之現代化重寫,沿其功能版圖,並針對四點作調整:複製乾淨(中西間隙不入剪貼板)、依 <code>lang</code> 全角/開明自動切換、<code>justify</code> 下句末標點只佔一份間距、跨瀏覽器一致。倉庫名仍為 <code>cjk-autospace</code>;公開 API 為 <code>createJuzhen</code> /全域 <code>Juzhen</code>;注入之自訂元素前綴 <code>jz-</code></p>
<p>本版(2.x)為從零重寫。v1 之 <code>createCjkAutospace</code> 相容層<strong>已移除</strong>(最後一批 v1 消費端已遷移 v2 API);v1 原始碼保存於 <code>v1-autospace</code> 分支。設計契約見 <code>ARCHITECTURE.md</code></p>
<h2>安裝與構建</h2>
<pre><code>npm install
npm run build # esbuild → dist/juzhen.{mjs,iife.js,css}tsc → dist/index.d.ts
npm test # jsdom DOM 正確性測試</code></pre>
<p>產物(<code>dist/</code>,已入庫):<code>juzhen.mjs</code>ESM,供打包器/TypeScript)、<code>juzhen.iife.js</code>IIFE,全域 <code>Juzhen</code>,原生 <code>&lt;script&gt;</code> 直跑)、<code>juzhen.css</code>(合併樣式,<strong>必須引入</strong>)、<code>index.d.ts</code>(型別宣告)。</p>
<h2>用法</h2>
<h3>原生 <code>&lt;script&gt;</code></h3>
<pre><code>&lt;link rel="stylesheet" href="dist/juzhen.css"&gt;
&lt;script src="dist/juzhen.iife.js"&gt;&lt;/script&gt;
&lt;script&gt;
const jz = Juzhen.createJuzhen({ scope: { include: ".prose" } });
jz.render(); // 對 document.body 套用(僅命中 .prose 子樹)
// jz.revert(); // 還原
&lt;/script&gt;</code></pre>
<h3>ESMTypeScript</h3>
<pre><code>import { createJuzhen } from "cjk-autospace";
import "cjk-autospace/css";
createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("main"));</code></pre>
<div class="callout info"><span class="label">JS 與 CSS 缺一不可</span> JS 只注入 <code>jz-*</code> 結構,所有視覺尺寸(間隙寬度、擠壓量、<code>justify</code> 原子化、字體堆疊)都在 <code>juzhen.css</code>。缺 CSS 則全部失效。</div>
<h2>已實作功能</h2>
<table>
<thead><tr><th>功能</th><th>說明</th></tr></thead>
<tbody>
<tr><td>中西間隙 <code>spacing</code></td><td>CJK↔西文邊界,<strong>margin 模型</strong>:包裹左側字、以 <code>margin-right</code> 留隙,不注入空白字元 → <code>textContent</code> 完全不變、複製緊湊、<code>justify</code> 不暴增、行首乾淨;行末由 <code>gapTrim</code> layout pass 去隙以齊右。含跨樣式邊界與 pill/隔離元素邊界(<code>code</code><code>kbd</code><code>.katex</code><code>mark</code>,兩端補隙口徑一致;pill 緊鄰標點時穿越 <code>jz-jinze</code> 仍補間隙)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。</td></tr>
<tr><td>長英文詞 <code>longWord</code></td><td><code>[A-Za-z]{6,}</code> 包入 <code>&lt;span lang="en"&gt;</code>,配合消費端 CSS <code>hyphens:auto</code> 原生斷詞(DOM 無連字符 → 複製不污染)。</td></tr>
<tr><td>標點擠壓 <code>jiya</code></td><td>標點 charify 為 <code>jz-char</code> <code>bd-*</code> class。三機制:baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠)+行內連續擠壓+行端擠壓。半形預設以字型 OpenType <code>halt</code> 渲染;缺 <code>halt</code> 字型可切 <code>margin</code> 後備模式(見下)。</td></tr>
<tr><td>禁則 <code>jinze</code></td><td>行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末);只隔離邊界詞元,長引文中段仍可斷。邊界字落於<strong>透明</strong>行內樣式元素(<code>strong</code><code>em</code> 等)內時<strong>克隆切分</strong>只綁邊界詞元(避免整串入 <code>nowrap</code> 致行末整體掉行),克隆標 <code>data-jz-kind="splitoff"</code><code>revert</code> 時併回;pill/隔離/avoid 不可切、<strong>整元素綁定</strong>——pill(如行內公式 <code>.katex</code>)作為原子鄰字單位整體綁入 <code>jz-jinze</code>,絕不下潛肢解(見「行內元素三分類」)。亦為 <code>justify</code> 綁定載體。</td></tr>
<tr><td>垂懸字避免 <code>orphan</code></td><td>段末行 CJK 實義字 ≥ N(預設 2;標點不計)。方案 C(斷行層無測量):段末末 N 實義字+尾隨標點包入 <code>jz-orphan</code><code>white-space:nowrap</code>),整體不可斷 → 末行恒含之 ⇒ 末行 ≥ N。邊界字落於行內元素內時同樣<strong>克隆切分</strong>(避免長元素溢出容器)。<strong>預設開</strong>;段落級。</td></tr>
<tr><td><s>標點懸掛 <code>hanging</code></s></td><td><strong>已移除</strong>:純 CSS 機制為機制級死路(寬度守恆矛盾——墨色出去多少版心內就空多少,CSS 接觸不到斷行器)。原生 <code>hanging-punctuation</code> 為唯一正解但僅 Safari 支援、且與 inline-block 原子互斥。<code>hanging</code> 選項保留為 no-op(無效果)。詳見 ARCHITECTURE §6.5.6。</td></tr>
<tr><td>功能分級 <code>level</code></td><td>文本級(間隙、擠壓——任意文本適用)vs 段落級(禁則、垂懸字、長詞——需行/段布局)。塊級預設全功能;<code>data-jz-level="text"</code> 子樹或 <code>level:{text:"選擇器"}</code>(標題/按鈕)僅跑文本級。</td></tr>
<tr><td>justify 單份間距</td><td>每個 <code>jz-char</code><code>inline-block</code> 原子,句末標點只貢獻一份對齊伸縮量。</td></tr>
</tbody>
</table>
<p>後續(架構已預留,不返工):標點修正(簡↔繁)、著重號、注音/Ruby。</p>
<h2>選項</h2>
<pre><code>createJuzhen({
lang: {
default: "zh-Hant", // 無 lang 祖先時之預設語言標籤
style: "kaiming", // 全域擠壓預設:quanjiaokaimingbanjiao
policy: { "zh-Hant": "quanjiao" }, // 逐 lang 覆寫
stylePolicy: [ { selector: "h1,button,.ui", style: "banjiao" } ], // 逐元素樣式(優先於 lang)
},
scope: { root: document.body, include: ".prose", avoid: "blockquote.raw" },
spacing: true,
longWord: { minLength: 6, lang: "en" },
jiya: true, // 或物件:{ halfWidth: "halt"|"margin", bias }
jinze: true,
orphan: true, // 垂懸字避免(預設開);或 { chars: 2 }false 關
// hanging:已移除、無效果(純 CSS 機制級死路,詳見 §6.5.6)
level: { text: "h1,h2,h3,button,.ui" }, // 此選擇器子樹僅跑文本級功能
justifyAtoms: true, // 分頁器下設 false 改 inline(見下)
blocks: [ { selector: ".code-comment", features: [ "spacing" ] } ],
});</code></pre>
<p>擠壓政策優先序:<strong>逐元素 <code>data-jz-style</code><code>stylePolicy</code> 逐 lang <code>policy</code> 全域 <code>style</code> 內建預設</strong>(繁→全角、簡→開明)。<strong>逐元素覆寫</strong>供「非段落/UI 元素用半角式、段落用全角/開明」等:屬性 <code>data-jz-style="banjiao|kaiming|quanjiao"</code>(標於元素或祖先)或選擇器 <code>stylePolicy</code>,近祖先勝、屬性優先於選擇器、非法值回退 lang。</p>
<h3>分塊功能選擇(HTML 屬性,就近繼承)</h3>
<pre><code>&lt;p data-juzhen="spacing jiya"&gt;&lt;/p&gt; &lt;!-- 白名單:僅此二功能 --&gt;
&lt;p data-juzhen-off="jiya"&gt;&lt;/p&gt; &lt;!-- 黑名單:停用 jiya,其餘繼承 --&gt;
&lt;div lang="zh-Hans"&gt;&lt;/div&gt; &lt;!-- lang 驅動全角/開明 --&gt;</code></pre>
<h2>標點半形:<code>halt</code> 預設與 <code>margin</code> 後備</h2>
<p>開明/半角式把標點收成「半形」,有兩種渲染機制:</p>
<table>
<thead><tr><th>機制</th><th>設定</th><th>原理</th><th>適用</th></tr></thead>
<tbody>
<tr><td><code>halt</code>(預設)</td><td><code>jiya: true</code></td><td>字型 OpenType <code>halt</code>,由字型按字形正確定位、不重疊</td><td>字型含 <code>halt</code>Noto CJK/思源系列)</td></tr>
<tr><td><code>margin</code>(後備)</td><td><code>jiya: { halfWidth: "margin" }</code></td><td><code>width:0.5em</code> +字身依墨色偏側溢出定位(以 CSS 重現 halt)</td><td>字型<strong></strong> <code>halt</code>(多數系統字型、GBK 老字型如方正書宋 GBK、PingFang</td></tr>
</tbody>
</table>
<div class="callout warn"><span class="label">為何需要後備</span> <code>halt</code> 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測<strong>方正書宋 GBK 僅含 <code>vert</code>、無 <code>halt</code></strong> → halt 模式不擠;切 <code>margin</code> 後備後(開明式 zh-Hans),同句行寬 <code>24.0em → 19.7em</code>、單一句內點號 <code>1.0em → 0.5em</code>,半形生效。</div>
<div class="callout"><span class="label">句末標點與連續挤压</span> 句末點號為 <code>。.!?</code>——含<strong>全角句點 <code></code></strong>U+FF0E,異於表意句號 <code></code> U+3002Noto CJK 有 <code>halt</code>);全角式與開明式句末皆<strong>保持全角 1em</strong>banjiao 挤半。<strong>連續標點</strong>對每一標點獨立判斷——若其有空白之側緊鄰另一標點即收半形:故 <code>,「</code> <strong>全角式與開明式皆 1em</strong><code>。」</code> <strong>全角式 1.5em/開明式 1em</strong>;並透明穿越 <code>strong</code><code>em</code> 等行內邊界。<strong>句界例外</strong>:句末點號接開類(<code>。「</code><code>?「</code>)為句界,保留句末點號右側空白、僅收開類左側 → 全角式與開明式皆 <strong>1.5em</strong>(對稱於 <code>。」</code>);例外僅限句末點號+開類,句內點號 <code>,「</code> 與雙括號 <code>」「</code><code>》《</code> 仍兩側皆收 1em。半角式句末點號由 baseline 已半形,故仍 1em(不回歸全寬)。</div>
<h3>激活與粒度</h3>
<p>模式由 <strong><code>data-jz-halfwidth</code> 屬性所在子樹</strong>決定(CSS 後代選擇器),故粒度極靈活:</p>
<ul>
<li><strong>全域</strong><code>jiya: { halfWidth: "margin" }</code><code>render()</code> 於處理 root 自動掛屬性。</li>
<li><strong>逐區塊/逐語言</strong>:在 HTML 任一祖先手寫 <code>data-jz-halfwidth="margin"</code>(或 <code>"halt"</code>),即令該子樹用對應機制,無需另建實例。常見如「全頁 <code>halt</code>,某用無 <code>halt</code> 字型之區塊掛 <code>margin</code>」,或反之(<code>margin</code> 頁中嵌一塊 <code>halt</code> 島)。</li>
</ul>
<pre><code>&lt;!-- 全頁 halt(預設),此塊改用無 halt 字型 → 局部 margin 後備 --&gt;
&lt;section data-jz-halfwidth="margin" style="font-family: '方正书宋_GBK'"&gt;&lt;/section&gt;</code></pre>
<h3>機制與字形偏側表(<code>margin</code> 模式專用,可覆寫)</h3>
<p><code>margin</code> 模式把標點 <code>jz-char</code> 之 inline-block 寬收為 <strong>0.5em(真半形 advance</strong>,字身(<code>jz-inner</code>)以 <code>overflow</code> 溢出該盒,再依<strong>墨色偏側</strong>左移、使墨色落於 0.5em 盒內、空白溢向外側——即以 CSS 盒模型重現 <code>halt</code> 之「收字身寬+按字形定位」。相鄰兩標點各 0.5em、合佔 1em,無鄰接疊加或單側重疊問題。</p>
<p>偏側依每字墨色中心。聚珍內建<strong>參考表</strong>(依方正書宋 GBK 實測之 glyph ink 位):簡體點號 <code>,、。;:!?</code> 偏左;繁體點號居中;彎引號 <code>“”‘’</code>、角括號 <code>「」</code>、尖括號 <code>〈〉</code> 墨色強偏一側(開→<code>right</code>、閉→<code>left</code>);圓/方/書名等括號 <code>()【】《》…</code> 墨色近中→<code>center</code>。可逐 <code>lang</code> 逐字覆寫:</p>
<pre><code>createJuzhen({
jiya: {
halfWidth: "margin",
bias: {
"zh-Hans": { "": "center", "": "right" }, // 覆寫參考預設
"zh-Hant": { "。": "center" },
},
},
});
// 偏側值(決定字身於 0.5em 盒內之左移):
// left 墨偏左 → 不移(字身左對齊)
// center 墨居中 → 左移 0.25em
// right 墨偏右 → 左移 0.5em(右半入盒)
// full 無可壓縮空白 → 不收、維持全形</code></pre>
<h3>字型適配工作流(margin 模式配字)</h3>
<p>換用新字型時,依下列步驟確認半形渲染:</p>
<ol>
<li><strong><code>halt</code></strong><code>python3 tools/measure-bias.py &lt;font.ttf&gt;</code>(需 <code>pip install fonttools</code>)。首行報該字型是否含 OpenType <code>halt</code>。有則直接用預設(halt 模式),到此為止;無則用 <code>margin</code> 後備,續下。</li>
<li><strong>取偏側建議</strong>:同一工具量各標點 glyph 之墨色中心、印出建議偏側表與可貼入 <code>jiya.bias</code> 之 JSON。</li>
<li><strong>套用</strong><code>createJuzhen({ jiya: { halfWidth: "margin", bias: &lt;貼入 JSON&gt; } })</code>;多數宋體系 GBK 字型與內建參考表相符,未必需覆寫。</li>
<li><strong>目視微調</strong>:與原文對照逐字檢查——標點與鄰字<strong>重疊</strong>則偏側往「不移」方向調(<code>right→center→left</code>);標點與內容<strong>仍有空隙</strong>則往「多移」方向調(<code>left→center→right</code>);某標點無可壓空白則設 <code>full</code></li>
<li><strong>記錄</strong>:把該字型之 <code>bias</code> 覆寫存入專案設定,作為該字型之適配結果。</li>
</ol>
<p>偏側是「字身於 0.5em 盒內左移多少」的三檔量化(0/0.25/0.5em);量測值近門檻時以目視為準。<code>halt</code> 字型由字型精確定位,無此配字需求——<code>margin</code> 配字僅為缺 <code>halt</code> 字型之務實近似。</p>
<h2>CSS 基礎設定(消費端須知)</h2>
<p>JS 只注入 <code>jz-*</code> 結構,<strong>所有視覺尺寸在 <code>juzhen.css</code></strong>。消費端須備妥:</p>
<ol>
<li><strong>引入樣式(必須)</strong><code>&lt;link rel="stylesheet" href="dist/juzhen.css"&gt;</code>(或 ESM <code>import "cjk-autospace/css"</code>)。</li>
<li><strong>設定 <code>lang</code>(強烈建議)</strong>:內容容器標 <code>lang="zh-Hans"</code><code>"zh-Hant"</code><code>"ja"</code>,驅動全角/開明政策與字形 <code>locl</code></li>
<li><strong>作用域類</strong>:若設 <code>scope.include: ".juzhen"</code>,須給內容容器加對應 class<code>juzhen.css</code> 之 normalize 亦收斂於此域。</li>
<li><strong>長英文詞斷字</strong>:須由消費端 CSS 提供 <code>hyphens: auto</code>(含 <code>-webkit-hyphens</code>)。</li>
<li><strong><code>justify</code>(選用)</strong>:欲兩端對齊則自設 <code>text-align: justify</code>;句末標點單份間距與行末去隙由聚珍處理。</li>
<li><strong>字型由消費端負責</strong>:聚珍<strong></strong>內建任何 <code>font-family</code>/字型堆疊——逐語言字型完全由消費端 CSS 決定(推薦範式:逐 <code>lang</code> 設於 <code>.juzhen</code> 或帶 <code>lang</code> 之容器,<strong></strong>直接設 <code>jz-inner</code>)。聚珍僅以 <code>jz-* { font: inherit }</code> 使原子繼承消費端字型、不強加,並顯式保留 <code>locl</code>(不改寫 <code>lang</code>、不關閉 locl,故正確標 <code>lang</code> + 用支援 locl 之字型即正常)。半形擠壓依字型 OpenType <code>halt</code>;缺 <code>halt</code> 之字型須用 <code>jiya.halfWidth:"margin"</code> +按字型以 <code>jiya.bias</code> 微調。</li>
<li><strong>自動處理</strong><code>juzhen.css</code> 已於 <code>.juzhen</code> 範圍內以 <code>@supports</code> 關閉 Blink 原生 <code>text-spacing-trim</code><code>text-autospace</code>,並令注入間隙 <code>user-select: none</code>(複製乾淨)。</li>
<li><strong>段落屬性照常設、原子透明</strong>:聚珍把標點拆成 <code>inline-block</code> 原子,但已確保原子對消費端段落樣式透明——段落可正常設 <code>text-indent</code>(首行縮排)、<code>hanging-punctuation</code><code>text-decoration</code>(下劃線)等。庫內已歸零會被原子重複套用之繼承屬性、並令原子承接段落裝飾,故首行縮排正常、下劃線連續不斷,<strong>無需任何額外配置</strong></li>
</ol>
<h2>行內元素三分類:透明/隔離/整體</h2>
<p>行內元素對排版有且僅有三種口徑。<strong>邏輯文本流之遍歷原語共用單一判定</strong> <code>isTransparent</code>(中西間隙、擠壓相鄰、禁則綁定、垂懸字);<strong>未在 <code>styleInlines</code> 之未知行內標籤(自訂元素等)採嚴格白名單、一律當邊界</strong>,透明須顯式登記。多選擇器同命中時依單一分類器 <code>finder.category</code><strong>優先序</strong> <code>avoid pill isolate block transparent opaque</code> 定案(如 <code>data-jz-skip</code> 之 pill → avoidpillisolate → pill)。判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code>&lt;span&gt;</code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。結構克隆切分 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 為正當之獨立層(邊界各異,見架構)。</p>
<table>
<thead><tr><th>分類</th><th>選擇器</th><th>邊界</th><th>內部</th><th>典型</th></tr></thead>
<tbody>
<tr><td><strong>透明</strong></td><td><code>styleInlines</code><strong>未列之行內標籤=邊界</strong></td><td>穿越——間隙/相鄰/run 皆延續</td><td>照常處理;禁則可<strong>克隆切分</strong>(最小綁定)</td><td><code>strong</code><code>em</code><code>a</code><code>span</code></td></tr>
<tr><td><strong>隔離</strong></td><td><code>isolateSelector</code>(預設 <code>"mark"</code><code>""</code> 停用)</td><td>斷 run/斷相鄰/不可切分;<strong>兩端</strong><code>jz-hws</code> 間隙</td><td><strong>照常處理</strong>(自成脈絡)</td><td>帶背景色之 <code>&lt;mark&gt;</code>、badge 類</td></tr>
<tr><td><strong>整體</strong></td><td><code>pillSelector</code>(預設 <code>"code, kbd"</code></td><td>同隔離</td><td><strong>完全不可見</strong>——不 charify、不插間隙、不切分、不下潛</td><td><code>code</code><code>kbd</code>、行內公式 <code>.katex</code>、引用標 <code>.cite-ref</code></td></tr>
</tbody>
</table>
<p>禁則遇 pill<code>…$公式$</code>):整個 pill 作為原子鄰字單位<strong>整體綁入</strong> <code>jz-jinze</code>(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。<strong>avoid 七源</strong><code>isSkipped</code><code>skipTags</code>SVG<code>contentEditable</code><code>data-jz-skip</code><code>skipAttribute</code><code>scope.avoid</code>)以<strong>自身</strong>判定、命中即邊界;一次性檢查用組合入口 <code>isAvoided</code>(自身或祖先)。<strong><code>skipTags</code> 預設不含 <code>code</code><code>kbd</code></strong>——二者由 <code>pillSelector</code> 以 pill 涵蓋(三分類互斥)。</p>
<div class="callout"><span class="label">建議</span> 行內公式/夾注引用等獨立行內單位宜列為 pill:<code>pillSelector: "code, kbd, .katex, .cite-ref"</code>——兩側補隙、吸收作者空格、整體綁定,統一處理。</div>
<h2>分頁器(Paged.js 等)相容:<code>justifyAtoms</code></h2>
<p>預設 <code>jz-char</code><code>jz-jinze</code><code>inline-block</code>,作 <code>justify</code> 之單一對齊原子。但<strong>分頁器</strong>(如 Paged.js)之對齊引擎會把每個 inline-block 邊界當伸縮點、與 CJK inter-ideograph 疊加,於窄欄混排時字距失控、整段散架。此時設 <code>justifyAtoms: false</code>——<code>jz-char</code><code>jz-jinze</code><code>display:inline</code>(半形 <code>halt</code> 與禁則 <code>nowrap</code> 綁定均保留)。常規流式版面無需設定。</p>
<div class="callout warn"><span class="label">注意</span> <code>margin</code> 後備半形依賴 <code>inline-block</code><code>width:0.5em</code>),故 <code>justifyAtoms:false</code><code>margin</code> 模式不可兼得——分頁器場景請用含 <code>halt</code> 之字型。</div>
<h2>自定義字符分類:<code>charClass</code></h2>
<p>每個字符之分類(中文/西文、全寬標點各子類)抽象為<strong>規則集</strong>:內置默認即聚珍分類表,消費端經 <code>options.charClass</code> <strong>擴充</strong>,以覆寫 <code>spacing</code><code>jiya</code><code>jinze</code> 對特定字符之判定。語義為<strong>追加</strong>——把當前未分類字符或新符號歸入對應類。</p>
<pre><code>createJuzhen({
charClass: {
cjk: "", // 追加為中文:與西文邊界補間隙、禁則/justify 視為單字
western: "★☆", // 追加為西文(半寬標點亦歸此):與 CJK 邊界補間隙
biaodian: { // 追加為全寬標點,按子類:jiya charify/擠壓、禁則避頭尾
stop: "", // 句末(。!?類) pause 句內(,、;:類)
open: "", close: "", middle: "", liga: "",
},
},
});</code></pre>
<p><strong>典型用例</strong>:英文全角句號 <code></code>U+FF0E,異於中文句號 <code></code> U+3002)預設<strong>未分類</strong>——不 charify、不挤壓、與 CJK 間不補隙。以 <code>charClass:{ biaodian:{ stop:"" } }</code> 歸為全寬句末標點,即比照 <code></code> 處理(開明式挤半形、全角式佔全形、行首避頭尾)。</p>
<div class="callout"><span class="label">要點</span> <strong>向下兼容</strong>:不傳 <code>charClass</code> 時分類與行為與內置完全一致、且各實例隔離(覆寫不污染默認)。<strong>追加語義(v1</strong>:擴充內置類,不支援重分類/移除內置字符。結構性 ASCII(字母數字/運算符)為固定集、不開放。<code>margin</code> 模式下新增開/閉括號之墨色偏側可一併以 <code>jiya.bias</code> 指定。</div>
<h2>v1 向下相容 — 已移除</h2>
<p>v1 相容層(<code>createCjkAutospace</code> 導出、IIFE 之全域別名、<code>autospaceClass</code> 選項)<strong>已隨最後一批 v1 消費端遷移完成而移除</strong>。請改用 <code>createJuzhen(options).render(root)</code> v2 APIv1 原始碼保存於 <code>v1-autospace</code> 分支。</p>
<h2>目視驗證</h2>
<p><code>demo/index.html</code> 載入 <code>dist/</code> 產物,簡繁並列、可切 <code>justify</code>、附 copy-clean 測試區。<code>demo/fallback.html</code> 以方正書宋 GBK(無 <code>halt</code>)對照 <code>halt</code> 失效 vs <code>margin</code> 後備生效。<code>demo/resize-check.html</code> 為可拖曳容器檢查頁(驗中西間隙在各寬度恆穩、行末去隙、pill 跨行右隙)。<code>demo/showcase.html</code> 以定寬文本框並列,逐場景演示功能分級與垂懸字。版面相關結論(justify 單份間距、擠壓視覺量、行端、<code>inline-block</code> 基線、間隙隨寬度之穩定性)須於 Chrome/SafariFirefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
</main>
<footer class="page">
<p>本頁為聚珍(Juzhen)使用文檔。所有文檔操作均於 docs 分支進行;main 僅存一份未追蹤之本地鏡像。MIT 授權;借鑑 Han.css/Heti 之思路與分類表,成品自含。</p>
</footer>
<!-- RUNTIME_SPRITE -->
<script type="text/markdown" id="md-source">
# 聚珍Juzhen
現代簡繁中文網頁排版套件Han.css 之現代化重寫複製乾淨 `lang` 全角開明切換`justify` 句末標點單份間距跨瀏覽器一致公開 API `createJuzhen` 全域 `Juzhen`自訂元素前綴 `jz-`
## 用法
```html
<link rel="stylesheet" href="dist/juzhen.css">
<script src="dist/juzhen.iife.js"></script>
<script>
Juzhen.createJuzhen({ scope: { include: ".prose" } }).render();
</script>
```
JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
## 已實作功能
- spacing 中西間隙(margin 模型,copy-clean;含 pill/隔離元素邊界與作者空格正規化)【文本級】
- longWord 長英文詞包裝(`<span lang>` + hyphens:auto)【段落級】
- jiya 標點擠壓(全角/開明/半角;半形 halt 預設、margin 後備)【文本級】
- jinze 禁則(避頭尾、justify 綁定;透明元素克隆切分最小綁定,pill/隔離/avoid 整元素綁定)【段落級】
- orphan 垂懸字避免(方案 C,末行 CJK 實義字 ≥ N;預設開 chars=2)【段落級】
- hanging 標點懸掛:已移除(純 CSS 機制級死路:寬度守恆矛盾,CSS 接觸不到斷行器;原生 hanging-punctuation 為唯一正解但僅 Safari 支援、且與 inline-block 原子互斥;選項保留為 no-op;詳見 §6.5.6
- level 功能分級(文本級 vs 段落級;data-jz-level="text"level:{text:選擇器} 標記標題等僅跑文本級)
- justify 句末標點單份間距(jz-char inline-block 原子)
## 標點半形:halt 預設與 margin 後備
- halt(預設,`jiya: true`):字型 OpenType halt 定位,需字型支援。
- margin`jiya: { halfWidth: "margin" }`):width:0.5em+字身依墨色偏側溢出定位,以 CSS 重現 halt;供缺 halt 字型(如方正書宋 GBK,僅含 vert)。
- 模式由 `data-jz-halfwidth` 子樹屬性決定,可逐區塊/lang。
- 偏側 inkBias 給 left/center/right/full,可 `jiya.bias` 逐 lang 逐字覆寫。
- 句末點號 。.!?(含全角句點 . U+FF0E,異於 。 U+3002):全角式與開明式句末皆保持全角 1em,banjiao 挤半。
- 連續標點:對每一標點獨立判斷,若其有空白之側緊鄰另一標點即收半。,「 全角式與開明式皆 1em;。」 全角式 1.5em/開明式 1em;透明穿越 strong/em 等行內邊界。句界例外:句末點號接開類(。「/?「)保留句末點號右空白、僅收開類 → 全角式與開明式皆 1.5em(對稱於 。」);僅限句末點號+開類,,「 與 」「/》《 仍 1em。半角式句末點號 baseline 已半、仍 1em(不回歸)。
### 字型適配工作流
1. `python3 tools/measure-bias.py <font.ttf>` 驗 halt、量偏側。
2. 有 halt → 用預設;無 halt → margin 後備+偏側表。
3. 套用 jiya.bias,瀏覽器目視微調(重疊→往不移;有隙→往多移)。
## CSS 基礎設定
引入 juzhen.css(必須);設 langscope classhyphens:autotext-align:justify(選用)。字型由消費端負責:聚珍不內建 font-family,逐語言字型自設於 .juzhen 或帶 lang 之容器(含 halt 字型,缺則 margin 後備+bias);聚珍以 font:inherit 繼承消費端字型、顯式保留 locl(正確標 lang + 支援 locl 之字型即正常)。段落屬性照常設、原子透明:text-indenthanging-punctuationtext-decoration 等已由庫內統一處理(歸零過套用、令原子承接段落裝飾),首行縮排正常、下劃線連續,無需額外配置。
## 行內元素三分類
行內元素對排版三種口徑,邏輯文本流之遍歷原語共用單一判定 isTransparent(以元素為準、非標籤名);未在 styleInlines 之未知行內標籤採嚴格白名單、一律當邊界(透明須顯式登記):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。結構克隆切分 canSplit、行端物理版面 lineedge 為正當之獨立層。多命中依單一分類器 finder.category 之優先序 avoidpillisolateblocktransparentopaque 定案(data-jz-skip 之 pill→avoidpillisolate→pill)。avoid 七源(isSkippedskipTagsSVGcontentEditabledata-jz-skipskipAttributescope.avoid)以自身判定、命中即邊界(一次性用組合 isAvoided);skipTags 預設不含 code/kbd(由 pill 涵蓋、三分類互斥)。行內公式/夾注引用宜列為 pill`pillSelector: "code, kbd, .katex, .cite-ref"`。
## 分頁器相容
`justifyAtoms: false` → jz-char/jz-jinze 改 display:inline,相容 Paged.js。margin 後備與 justifyAtoms:false 不可兼得。
## 自定義字符分類 charClass
字符分類(中/西文、全寬標點各子類)抽象為規則集,內置默認,消費端經 options.charClass 追加擴充,覆寫 spacingjiyajinze 之判定。`charClass: { cjk?, western?, biaodian?:{stop,pause,open,close,middle,liga} }`。例:英文全角句號 .(U+FF0E)預設未分類,以 biaodian.stop:"" 歸為全寬句末標點即比照 。處理。向下兼容(不傳則行為不變、實例隔離);v1 為追加語義,不重分類內置;結構性 ASCII 不開放。
## v1 相容 — 已移除
v1 相容層(createCjkAutospaceIIFE 全域別名/autospaceClass)已隨最後一批 v1 消費端遷移完成而移除;請改用 createJuzhen(options).render(root)。v1 原始碼保存於 v1-autospace 分支。
MIT。借鑑 Han.css/Heti 思路與分類表,成品自含。
</script>
<!-- RUNTIME_JS -->
</body>
</html>
-4693
View File
File diff suppressed because it is too large Load Diff
-356
View File
@@ -1,356 +0,0 @@
<!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 架構,再加 Pass C 之「長英文段 <code>&lt;span lang&gt;</code> 包裝」(給瀏覽器原生 <code>hyphens: auto</code> 用)。工廠函式設計,由消費端注入 pill 選擇器、樣式 / 段落 / skip 標籤集等配置。輸出 <code>&lt;span class="cjk-autospace" aria-hidden="true"&gt;(空白)&lt;/span&gt;</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 &lt;script&gt; / 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>&lt;strong&gt;&lt;/strong&gt; 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>&lt;p&gt;本資料夾&lt;strong&gt;不是&lt;/strong&gt; Neovim 使用者目錄。&lt;/p&gt;</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>&lt;strong&gt;</code> 之 nextSibling 處)</li>
<li>位置 2:原文「m」與「 」之間,「 」被 strip → marker 插在「m」後(同一 text node 內,splitText 後插入)</li>
</ul>
<p>最終 DOM(簡化):<code>本資料夾&lt;strong&gt;不是&lt;/strong&gt;&lt;span class="cjk-autospace"&gt; &lt;/span&gt;Neovim&lt;span class="cjk-autospace"&gt; &lt;/span&gt;使用者目錄。</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>命令 &lt;code&gt;git&lt;/code&gt; 更新</code></p>
<ul>
<li>Pass A run 因 pill 中斷:「命令 」(trailing space 為 run 末尾,不觸發 panguSpace 之 STRIP rule,因 rule 要求兩側都是內容字而 run 末尾沒有右側字)</li>
<li>Pass B 處理 pill 之 prevtext 末字元是 <code>' '</code>,前一字元是 <code>'令'</code>(CJK),命中 strip 規則 → 移除空格 + 在 pill 前插 marker</li>
<li>Pass B 處理 pill 之 nexttext 首字元是 <code>' '</code>,後一字元是 <code>'更'</code>CJK),同理 strip + 插 marker</li>
</ul>
<p>結果:<code>命令&lt;span class="cjk-autospace"&gt; &lt;/span&gt;&lt;code&gt;git&lt;/code&gt;&lt;span class="cjk-autospace"&gt; &lt;/span&gt;更新</code></p>
<h2>Pass C — long English word wrap(預設啟用)</h2>
<p>解 CJK 兩端對齊正文中「行末長英文詞」之斷行問題:原靠 <code>&lt;wbr&gt;</code> 每 N 字元亂插之路徑會產生「斷點位置與音節無關 + 斷處不顯連字符」兩個觀感問題。Pass C 改採 <code>&lt;span lang="en"&gt;</code> 包裝,把斷字決定權交給瀏覽器原生 <code>hyphens: auto</code> 用內建英文字典處理。</p>
<ol>
<li>對 root 內所有 text node 走 TreeWalkerSHOW_TEXT</li>
<li>對每個 node:先 <code>isSkipped</code>(共用 Pass A / B 之 skip 鏈:CODE / KBD / PRE / SAMP / TT / VAR / SCRIPT / STYLE / TEXTAREA / INPUT / SVG / contentEditable / <code>data-md-key</code>+ 冪等檢查(parent 為 <code>&lt;span lang&gt;</code> → skip</li>
<li>對通過篩選之 node:用 <code>[A-Za-z]{minLength,}</code> 收所有匹配,把每個匹配位置之子串裹進 <code>&lt;span lang="en"&gt;</code>(其餘原文保留為純 text node),整個用 fragment 一次性 replaceChild</li>
</ol>
<h3>消費端 CSS(與 lib 解耦但必須配套)</h3>
<pre><code>[lang|="en"] {
hyphens: auto;
-webkit-hyphens: auto;
hyphenate-limit-chars: 6 3 3; /* 詞長 / 前 / 後最少字符 */
hyphenate-character: "-"; /* 強制 U+002D,避免 CJK 字型把 U+2010 渲成全形 */
-webkit-hyphenate-character: "-";
}</code></pre>
<div class="callout warn">
<span class="label">字型陷阱</span> Noto Serif CJK SC 等 CJK serif 字型把 U+2010 HYPHEN 設計為全形 1 em(與漢字同寬),以服務 CJK 範圍表達;而 <code>hyphens: auto</code> 預設正是插 U+2010。直接用會看到全形連字符突兀地接在英文詞末。<code>hyphenate-character: "-"</code> 強制改用 U+002D(同字型內為窄拉丁,與作者鍵入的 <code>-</code> 一致),仍為純視覺渲染、DOM 內不存在。
</div>
<h3>選取 / 複製潔淨性</h3>
<p>連字符純由 CSS 渲染(不存在於 DOM),<code>Selection.toString()</code><code>Cmd+C</code> 返回原文無污染。包裝 span 本身是 inline<code>textContent</code> 視為原樣,序列化 / 複製不受影響。</p>
<h3>不在範圍</h3>
<ul>
<li><strong>CamelCase / 標識符</strong><code>getUserByEmailAddress</code><code>OpenAIWhisperAPI</code>):不在英文字典 → 瀏覽器不斷詞;本實作未開 <code>overflow-wrap: anywhere</code> fallback(會破壞「連字符必須存在」之原則),故識別符 overflow 時整塊保留</li>
<li><strong>URL / 純數字串</strong>:同上,<code>https://example.com/very/long/path</code> 此類超長 token 不被處理</li>
<li><strong>非英文之 ASCII 拉丁變體</strong>café、naïve):正則 <code>[A-Za-z]</code> 不匹配重音字符;如需擴充可自訂 <code>longWordWrap.minLength</code> 之外再 patch lib</li>
</ul>
<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>longWordWrap</code></td><td><code>true</code>(預設啟用)</td><td>Pass C 開關。傳 <code>false</code> 顯式關閉;<code>{ minLength, lang }</code> 自訂粒度與標語;<code>true</code> / 省略等同 <code>{ minLength: 6, lang: "en" }</code></td></tr>
<tr><td><code>isSkipped</code></td><td>(無)</td><td>自訂 callback;先於內建判定執行;回傳 true 即 skip</td></tr>
</tbody>
</table>
<h2>Marker span 解剖</h2>
<pre><code>&lt;span class="cjk-autospace" aria-hidden="true"&gt; &lt;/span&gt;</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 是「不可見」之 spanuser-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 '&lt;script&gt;\n'
cat "$LIB/cjk-autospace.js" # 工廠函式
printf '\n'
sed '$d' "$RUNTIME/runtime.js" # 應用程式 runtime
cat "$MODE_BEHAVIOR_JS"
printf '\n})();\n'
printf '&lt;/script&gt;\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 = "&lt;script&gt;" .. lib .. "\n" .. entry .. "&lt;/script&gt;"
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>&lt;text&gt;</code> 不會被處理。</li>
<li><strong>contentEditable</strong>:用戶輸入區整 subtree skip,避免影響游標 / 選取。</li>
<li><strong>已存在 marker</strong>Pass A 走 DFS 時,<code>.cjk-autospace</code> 中斷 runPass 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>&lt;p&gt;&lt;/p&gt;&lt;p&gt;Neovim&lt;/p&gt;</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 架構(樣式透明 + 行內塊邊界 + 長英文 lang 包裝)為原創設計,前兩 pass 解決 Pangu.js 原版「text-node walker 看不到跨元素邊界」之問題;Pass C 把 CJK 兩端對齊正文中之斷英文詞責任交給瀏覽器原生 <code>hyphens: auto</code></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>