docs: 效能/增量 API 文檔+I1/I6 收緊(作者空格規範化不還原)

README:新增「動態頁面:增量恢復」(rerender/invalidate/observe/disconnect 用法+元素變化正確性
告示);新增「revert 之範圍」注(revert 淨移除 jz-* 結構、回邏輯等價態,但不還原 spacing 依契約
規範化之贅餘作者空格;需保留原始源者勿以 revert 當「還原到未處理源碼」)。

ARCHITECTURE:§3.1 增量 API;§4.1 記憶化(category+祖先游走)與快取生命週期/失效;§6.1a S1
對齊門。**I1/I6 收緊**(消除與固有 strip 行為之措辭張力,方案 A):
- I1 明示 spacing 規範化贅餘作者空格為刻意之常態例外(語義不變、但確刪空格且不經 revert 還原),
  除此之外預設「只包裝、不改寫文字」。
- I6 明示 revert 淨移除全部 jz-* 結構、回「邏輯等價」態;「邏輯等價≠逐字節」之唯一差異即被規範化
  之作者空格(穩定、不隨反覆 render/revert 累積;revert 後無 [data-jz] 殘留)。

依據:4 份真人撰寫真實文檔驗收——非空白字元多重集守恆、無字元改/增/漏/亂序,唯贅餘作者空格依
契約規範化(doc-A 55、B 31、C 0、D 85 個);真瀏覽器整條管線(含 lineEdge/gapTrim)零報錯。
This commit is contained in:
2026-07-12 02:21:07 +08:00
parent c5a503580c
commit 2f04ea4085
2 changed files with 87 additions and 10 deletions
+49 -10
View File
@@ -29,9 +29,14 @@
這些不變量是「避免返工」的核心。任何新功能點都不得違反。 這些不變量是「避免返工」的核心。任何新功能點都不得違反。
- **I1 複製乾淨(copy-clean)**:注入之間隙與標記一律 `user-select: none` - **I1 複製乾淨(copy-clean)**:注入之間隙與標記一律 `user-select: none`,不改變邏輯
改變邏輯 `textContent`語義;若某 pass 必須改寫文字(如標點替換),則須為 `textContent`**語義**內容。**一項刻意之常態正規化例外**:`spacing` pass 剝除**贅餘之作者
顯式 opt-in 且可 `revert` 還原。預設「只包裝、不改寫文字」。 空格**——CJK↔西文之作者空格吸收為受管 `margin` 間隙、CJK↔CJK(含跨 inline 標籤)之作者
空格剝除不留隙。此為 CJK 排版正解(該空格屬裝飾性、間隙改由 margin 承載,語義內容不變),
但**確實刪除空格字元、且此刪除不經 `revert` 還原**(見 I6)。除此常態正規化外,預設「只包裝、
不改寫文字」;其餘真正會改寫文字之 pass(如標點替換 `biaodian`)須為顯式 opt-in 且可 `revert`
還原。(實測 4 份真人撰寫之真實文檔:非空白字元多重集守恆、無任何字元改/增/漏/亂序,唯
贅餘作者空格依此契約規範化。)
- **I2 JSCSS 職責分離**JS 只注入**結構**(`jz-*` 元素、class、`data-jz-*` - **I2 JSCSS 職責分離**JS 只注入**結構**(`jz-*` 元素、class、`data-jz-*`
屬性);**所有視覺尺寸(marginletter-spacing/寬度/懸掛位移)一律寫在 屬性);**所有視覺尺寸(marginletter-spacing/寬度/懸掛位移)一律寫在
`juzhen.css`**。同一段 DOM 在 justify 與否、主題與否下都成立。此分離是擴充性 `juzhen.css`**。同一段 DOM 在 justify 與否、主題與否下都成立。此分離是擴充性
@@ -46,8 +51,12 @@
`text-autospace`),使三引擎行為收斂於同一路徑(見 §9.1)。 `text-autospace`),使三引擎行為收斂於同一路徑(見 §9.1)。
- **I5 justify 單份間距**:間隙與擠壓的結構設計,須保證每個標點在 - **I5 justify 單份間距**:間隙與擠壓的結構設計,須保證每個標點在
`text-align: justify` 下只貢獻**一份**對齊伸縮量(見 §8)。 `text-align: justify` 下只貢獻**一份**對齊伸縮量(見 §8)。
- **I6 冪等與可還原**:每個 pass 對其產物打標記,重跑跳過;提供 `revert` 乾淨 - **I6 冪等與可還原**:每個 pass 對其產物打標記,重跑跳過;提供 `revert` **乾淨移除聚珍注入之
移除,DOM 回到 pass 前狀態(至少邏輯等價)。 全部結構**`jz-*` 元素、class、`data-jz-*` 屬性;wrap→解包還原原內容、marker→移除、
splitoff→併回原元素),DOM 回到 pass 前狀態之**邏輯等價**態。「邏輯等價」而非逐字節相同之**唯一
差異**`spacing` 規範化之贅餘作者空格不還原(見 I1)——故 `render→revert` 對含 CJK↔西文作者
空格之源,還原後語義一致、結構全清,唯該空格已正規化(此差異穩定、不隨 `render``revert` 反覆
累積)。`revert` 後 DOM 內無任何 `[data-jz]` 殘留(實測驗收成立)。
- **I7 邊界感知**:跨樣式行內元素的鄰接(如 `<strong>是</strong>A`)須能識別並 - **I7 邊界感知**:跨樣式行內元素的鄰接(如 `<strong>是</strong>A`)須能識別並
正確處理——由核心 finder 統一提供,各 pass 不自行重複造輪。 正確處理——由核心 finder 統一提供,各 pass 不自行重複造輪。
- **I8 向下相容 v1(已退役)**:相容層曾保障 v1 消費端(build.sh 單檔串接、Nvim - **I8 向下相容 v1(已退役)**:相容層曾保障 v1 消費端(build.sh 單檔串接、Nvim
@@ -69,9 +78,20 @@
const jz = createJuzhen(options); const jz = createJuzhen(options);
jz.render(root?); // 對 root(預設 document.body)套用啟用之 pass jz.render(root?); // 對 root(預設 document.body)套用啟用之 pass
jz.revert(root?); // 還原 jz.revert(root?); // 還原
// 增量恢復(動態頁面;root 皆可為局部 Element,成本 O(該塊) 而非 O(全文))
jz.rerender(root?); // revert(root) + render(root)(便利)
jz.invalidate(root?); // 清該子樹之內部快取(改屬性但不 revert 時用;revert 已自動失效)
jz.observe(root?); // 以 MutationObserver 自動增量恢復(opt-in;自身注入不自觸發)
jz.disconnect(); // 停止觀察
// 全域: window.Juzhen.createJuzhen(...) // 全域: window.Juzhen.createJuzhen(...)
``` ```
**增量恢復(第三層)**`render``revert` 皆收局部 Element,配合**持久化 Finder 快取**(§4.1
末)與快取失效,頁面局部變化只重處理受影響子樹。`observe``MutationObserver`:內容/分類屬性
變化 → 自動 `revert`+`render` 最近塊、新增之塊直接 `render`(免全容器重掃);renderrevert 期間
**暫停觀察**,故 juzhen 自身之 jz-* 注入絕不自觸發。實測編輯 400 段中之一段,局部恢復較全樹快數
百倍。**元素變化正確性**見 §4.1 末「快取生命週期與失效」。
`options` 形如: `options` 形如:
``` ```
@@ -257,11 +277,21 @@ Nvim peekClaude 模板等 v1 消費端之過渡期。**最後一批 v1 消費
`jiyaAdjacency``isTransparent`)皆 `switch(category)` 分派,`canSplit` `SPLITTABLE ∩ transparent` `jiyaAdjacency``isTransparent`)皆 `switch(category)` 分派,`canSplit` `SPLITTABLE ∩ transparent`
——分類集中一處、優先序不再散落各原語各自短路(階段二後之清晰化)。 ——分類集中一處、優先序不再散落各原語各自短路(階段二後之清晰化)。
**效能(S3 記憶化)**分類含至多三次 `el.matches()`pillisolateavoid 選擇器),且被上列各遍歷 **效能(記憶化)**三處逐元素/逐節點之重複計算以 `WeakMap` 記憶化——
原語逐元素、跨 pass 重算——`category``categoryCache``WeakMap<Element, InlineCategory>`)記憶化, - **S3 category**`category` 含至多三次 `el.matches()`pillisolateavoid),且被上列各遍歷原語跨
砍除跨 pass 重複(實測 pillisolate 密集之頁面 `el.matches()` 約降三成)。安全性:單次 render 內一元素 pass 重算 → `categoryCache` 砍除跨 pass 重複。
之分類恆定(classskip 屬性/nodeName 皆不因 render 變更;jz-* 包裹按 nodeName 分類亦恆定),且 Finder - **祖先游走(`insidePill``isAvoided`**:舊版逐文本節點沿祖先鏈調 `pillMatches``avoidsSelf`
`makeCtx` 每次 render 重建 → 快取僅存活單次 render、無跨 render 失效問題(持久化屬另議)。 `eachTextNode` 每文本節點一趟、`collectRuns` 再一趟),O(文本節點 × depth)、深 DOM 下二次 →
`ancestorOrSelfPill``…Avoided` 以「沿鏈迭代回填」記憶化(每元素恰算一次、兄弟共用祖先鏈快取),
收斂為 O(元素數)。`insidePill``el.matches()` 之最大來源;記憶化後掃描層 `el.matches()` 累計約降
五成、深 DOM render 時間顯著下降。
**快取生命週期與失效(元素變化正確性)**Finder 隨實例**持久化**(跨 renderrevert 復用上列快取,
非每次 render 重建),使頁面局部變化時未變子樹之分類/級別/功能免重算。元素鍵 `WeakMap`:移除之元素
自動回收、新增者重新計算。**屬性變化**classdata-jz-*lang 等改變既有元素之分類/級別/功能)會令其
快取變脏——且因級別/功能/祖先游走依賴祖先鏈,某祖先變則其**整棵子樹**之快取變脏,故 `invalidate(el)`
清 el 子樹全部元素鍵快取。`revert(el)` **自動** `invalidate` 其子樹(安全預設),故 `revert``rerender`
`observe` 路徑天然正確;`invalidate(el)` 另供「改屬性但不 revert」之顯式入口(見 §3.1 增量 API)。
判定**以元素為準、非標籤名**`isTransparent(el) category(el)==="transparent"`,前置排除 pill 判定**以元素為準、非標籤名**`isTransparent(el) category(el)==="transparent"`,前置排除 pill
isolateavoid isolateavoid
@@ -445,6 +475,15 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
`lastRect`,margin 實際落點)為準,兼顧跨行 pill(行尾 `code` 折行亦曾致右側間隙誤消)。 `lastRect`,margin 實際落點)為準,兼顧跨行 pill(行尾 `code` 折行亦曾致右側間隙誤消)。
實 Chromium 多寬度(822em、raggedjustify)掃描確認:偽 trim 由「某些寬度存在」歸零; 實 Chromium 多寬度(822em、raggedjustify)掃描確認:偽 trim 由「某些寬度存在」歸零;
真行末仍 trimjustify 齊右不變)。 真行末仍 trimjustify 齊右不變)。
- **效能(S1 對齊門)**:逐一定案為「左→右斷行因果 + 讀 `getClientRects` 觸發同步重排」之固有
代價——每間隙一次重排,gap 密集之頁面達 O(gaps) 次重排(實測 Chromium 上 ~19k gap 之頁面
render 達數秒、且 `gapTrim` 佔約 99%)。此代價**唯 justifyrightcenter 等「右緣 flush 或含
margin 置中」之對齊才需付**:尾隨 `margin-right` 之可見性依對齊而定——`left``start`(ltr) 之右緣
ragged、行末 margin 落於不可見側,去隙與否視覺無別。故 `trimGaps` 按最近容器之 computed
`text-align``direction` 過濾:**`left``start`(ltr) 之間隙整批跳過**(省其全部重排),其餘對齊
justifyrightcenterendRTL)保留完整逐一定案、行為**不變**。實測非兩端對齊之常見內容
`gapTrim` 由數千毫秒降至數十毫秒(getClientRects 歸零);justify 內容之成本不變(固有)。門以
per-parentElement memo`text-align``direction` 皆繼承)攤薄。
### 6.1b 標點稽核:CJK 標點不生誤隙(下游回饋) ### 6.1b 標點稽核:CJK 標點不生誤隙(下游回饋)
+38
View File
@@ -59,6 +59,44 @@ jz.render(document.querySelector("main"));
**JS 與 CSS 缺一不可**JS 只注入 `jz-*` 結構,所有視覺尺寸(間隙寬度、擠壓量、 **JS 與 CSS 缺一不可**JS 只注入 `jz-*` 結構,所有視覺尺寸(間隙寬度、擠壓量、
`justify` 原子化、字體堆疊)都在 `juzhen.css` `justify` 原子化、字體堆疊)都在 `juzhen.css`
### 動態頁面:增量恢復(局部變化免全樹重掃)
`render(root)``revert(root)` **皆可傳入局部 Element**——頁面局部變化時,只重處理受影響
之子樹即可,成本 O(該塊) 而非 O(全文)(實測編輯 400 段中之一段,局部恢復較全樹快數百倍)。
```ts
const jz = createJuzhen();
jz.render(document.body); // 初次全樹
// 局部變化後(手動):
jz.rerender(changedBlock); // revert(changedBlock) + render(changedBlock)
jz.render(newlyAppendedBlock); // 新增之塊:直接 render(無須 revert
// 自動(opt-in):觀察 DOM,內容/分類屬性變化時自動只重處理受影響之最近塊。
jz.observe(document.body); // 之後 DOM 變動即自動增量恢復
// jz.disconnect(); // 停止觀察
```
- **`rerender(root?)`**`revert``render` 同一(子)樹(便利方法)。
- **`observe(root?)``disconnect()`**:以 `MutationObserver` 自動增量。juzhen 自身之注入
於處理期間暫停觀察、**絕不自觸發**;新增之塊直接 render(免全容器重掃)。需 `MutationObserver`
(瀏覽器/jsdom 有;無則 no-op)。
- **`invalidate(root?)`**:清該子樹之內部快取。**`revert` 已自動失效其子樹**,故僅在「改了既有
元素之分類相關屬性(`class``data-jz-*``lang` 等)**但不走 `revert`**」時才需顯式調用,以
免下次 render 沿用陳舊分類。內容(子節點/文本)變化不改分類、無需 `invalidate`(但須
`revert`+`render` 該塊方能重處理)。
> **元素變化之正確性**:實例內部對「元素分類(透明/pill/隔離/avoid)/功能級別/功能開關」
> 快取以加速跨 render 復用(快取隨實例持久、非每次 render 重建)。**改變既有元素之上列屬性後,
> 須經 `revert``rerender``invalidate``observe` 任一使該子樹快取失效**,否則沿用舊分類。
> 新增/移除元素與純內容編輯無此顧慮(新元素重新計算、移除者自動回收)。
> **`revert` 之範圍**`revert` 乾淨移除聚珍注入之全部結構(`jz-*``data-jz-*`),DOM 回到**邏輯
> 等價**態;但**不還原** `spacing` 依契約規範化之贅餘作者空格(CJK↔西文吸收為 margin 間隙、
> CJK↔CJK 剝除;見上表「中西間隙」)。即 `render→revert` 對含 CJK↔西文作者空格之源,還原後語義
> 一致、無 `jz-*` 殘留,唯該空格已正規化(此差異穩定、不隨反覆 render/revert 累積)。若消費端需
> 保留原始作者空格以供編輯,應自存原文、勿以 `revert` 當「還原到未處理源碼」。
--- ---
## 切片 1 已實作功能 ## 切片 1 已實作功能