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