"`)、`revertPass(name, root)`(移除該 pass 之產物、
-解包 `jz-*`、合併回文本節點並 `normalize()`)。
+`createJz(ownedElements, tag, pass, kind, opts)` 建立元素、登記私有所有權,並寫入
+`data-jz`/`data-jz-kind` 供定址。`setOwnedAttribute` 與 `revertOwnedAttribute`/
+`revertOwnedAttributes` 以共享寫者堆疊管理受控屬性;外部修改或其他實例寫入不被無條件覆蓋。
-`data-jz-kind` 區分產物還原方式:`marker`(純注入,移除)、`wrap`(包裝原內容,解包)、
-**`splitoff`**(行內元素克隆切分之邊界詞元,見 `core/split.ts`/§6.4)。`revertPass` 分
-**兩階段**處理:主迴圈逆序解包 marker/wrap,第二階段再把 splitoff 併回前兄弟(同標籤
-原元素)——順序必要,否則 splitoff 之前兄弟可能仍是 wrap 殼(如 `jz-jinze`)而非原元素。
+`revertPass(pass, root, ownedElements)` 先驗證本實例所有權與合法 pass/kind/tag 組合。
+目前接受 `wrap`(包裝原內容)與 **`splitoff`**(行內元素克隆切分之邊界詞元);不提供
+泛用 `marker` 還原。第一階段逆序解包 wrap,第二階段把 splitoff 併回同標籤前兄弟,
+最後 normalize 受影響父節點。順序必要,否則 splitoff 前兄弟可能仍是 wrap 殼。
+
+`recordUnwrappedParent`/`resolveUnwrappedParent` 記錄及解析合法產物的受控解包鏈,
+供跨節點來源回退使用;重用空殼會使舊位置失效,重定向環不作為文字恢復依據。
+包裝請求由 `core/pass.ts` 執行,安全行內切分位於 `core/split.ts`;本模組沒有
+`createEl`/`wrap`/`splitAt`/`insertMarker` 等泛用變更函數。
---
@@ -584,7 +600,8 @@ class 約定以現有 CSS/pass 為準:標點採 `bd-open`/`bd-close`/`bd
`halt` 收的是字身寬度、由字型按字形正確定位(簡左/繁中/括號偏側皆對),故
不重疊。`font-feature-settings:"halt"` 各引擎一致(當初疑慮之不一致者為**自動**
的 `text-spacing-trim`/`chws`,非顯式 `halt`);缺 `halt` 之字型優雅降級為全形。
- 鄰接/行端之「收哪一字」之**決策**仍由 `blankSides` 邏輯定,渲染統一為 `halt`。
+ 鄰接/行端之「收哪一字」之**決策**仍由 `blankSides` 邏輯定;預設以 `halt` 渲染,
+ 缺半形特性的字型可採下述逐字 `margin` 後備。
- **句末標點集含全角句點 `.`(下游回饋)**:`BD_STOP` 為 `。.!?`——除表意句號 `。`
(U+3002)外,納入**全角句點 `.`**(U+FF0E)。實測 Noto CJK SC/TC 之 `halt`(GPOS
SinglePos 調 advance)皆覆蓋 `.`、挤壓收 0.5em,與 `。` 一致;`inkBias` 同步補 `.`
@@ -617,7 +634,9 @@ class 約定以現有 CSS/pass 為準:標點採 `bd-open`/`bd-close`/`bd
依**字身墨色中心**給出 `left|center|right|full`(簡體點號偏左、繁體居中;彎引號
`“”`/角括號 `「」`/尖括號 `〈〉` 墨強偏一側→開 right、閉 left;圓方書名括號
`()【】《》…` 近中→center),charify 時標為 `jz-ink-{left,center,right}`。消費端可
- 經 `jiya.bias` 逐 lang 逐字覆寫;配字工作流與量測工具 `tools/measure-bias.py` 見 README。
+ 經 `jiya.bias` 逐語言分類、逐字覆寫;`full` 不套 margin 的 0.5em 盒寬,保留全形。
+ **此逐字字型相容方案仍是當前支援路徑**;配字工作流、JSON 範例與量測工具
+ `tools/measure-bias.py` 見 README,不屬已移除的負 letter-spacing/標點懸掛機制。
- **激活與粒度(I3/I10)**:最近有效 `data-jz-halfwidth="halt|margin"` 屬性透過
可繼承 CSS 變數決定模式;`render()` 在全域 margin 時自動掛於 root。可在任意祖先
逐區塊/逐語言指定並交替巢狀,切回 halt 會撤銷 margin 盒寬、內盒及偏移。bias
@@ -704,20 +723,24 @@ class 約定以現有 CSS/pass 為準:標點採 `bd-open`/`bd-close`/`bd
直覺:文本級=「字符級排版」(間隙、標點形態);段落級=「行/段級排版」(換行、行端、段末)。
**標記 API(下游)**:
-- 預設推斷:塊級元素(`blockTags`:p/div/li/td…)→ **段落級(全功能)**。
+
+- 級別由自身向上查最近有效信號,不依標籤名推斷;沒有任何信號時預設 **paragraph**。
- 顯式降級:`data-jz-level="text"` 於元素/祖先 → 該子樹**僅文本級**(段落級功能自動關);
- `data-jz-level="paragraph"`(或不標)→ 全功能。
+ 子元素可用 `data-jz-level="paragraph"` 重新允許段落級功能,但仍受全域功能開關、
+ 逐元素功能閘與作用域限制。不標記的子元素繼承祖先信號,不會自動重新啟用全功能。
- 選項 `level?: { text?: string }`:選擇器一鍵標記(如 `level:{ text: "h1,h2,h3,button,.ui" }`)。
- 典型:標題/按鈕/UI 文字標 **text-level**(要間隙與標點半形、不要避孤字/避頭尾);
正文 `` 預設全功能。
**實現**:
+
- 每個 pass 加元數據 `level: "text" | "paragraph"`。
-- `finder.levelFor(el)`:查 `data-jz-level` 祖先 + 選項 `level.text` 選擇器 + 預設(塊→段落)。
+- `finder.levelFor(el)`:從自身向上查;每層先取合法 `data-jz-level`,再查 `level.text`
+ 選擇器,第一個有效信號勝出;皆無才回退 paragraph。非法屬性值繼續查找。
- gate:段落級 pass 對 text-level 元素跳過;文本級 pass 不受限。與現有 `blocks`/
`featureEnabledFor` 疊加(level=粗粒度語義組先判,blocks=細粒度逐功能後調)。
-- **文本級功能須支持非塊/單元素**:`spacing` 現用 `eachBlock`,須確認 scope root 為 inline/
- 單元素時亦能處理(或放寬走訪),使單行標題等之中西間隙、標點半形生效。
+- **文本級功能支持非塊/單元素**:`spacing` 在逐塊處理後,補做未被塊覆蓋的行內 root
+ 及 `scope.include` 命中的行內子樹,避免重複處理;標點 charify 也可走訪行內 root。
**與本批關係**:分級已落地(pass 標 level、`levelFor`、`data-jz-level`/選項);`orphan`
等段落級 pass 只作用於段落級元素、跳過 text-level 者。`hanging` 未註冊為 pass。
@@ -739,7 +762,7 @@ class 約定以現有 CSS/pass 為準:標點採 `bd-open`/`bd-close`/`bd
| 機制 | 作用層 | 唯一職責 | 手段 | 狀態 |
|---|---|---|---|---|
| 禁則(§6.4 jinze) | **斷行層** | 把標點趕到正確的邊(點號/閉類→行末、開類→行首;連續標點不拆) | nowrap `jz-jinze` | 已實作 |
-| 行端半形(lineedge 現有) | **渲染層·紧排** | 行首開類/行末閉類**收半形** | `jz-half-le`(halt) | 已實作(將併入行端模式) |
+| 行端半形(lineedge 現有) | **渲染層·紧排** | 行首開類/行末閉類**收半形** | `jz-half-le`(halt/margin) | 已實作 |
| 標點懸掛 | **渲染層·凸出** | 行末點號/閉類**右凸**、行首開類**左凸**出版心 | ~~負 `margin`~~ | ❌ 已移除(機制級死路,見 6.5.6) |
| 垂懸字避免 | **斷行層** | 段末行 **CJK 實義字數 ≥ N** | nowrap `jz-orphan` | 已實作 |
@@ -761,7 +784,7 @@ class 約定以現有 CSS/pass 為準:標點採 `bd-open`/`bd-close`/`bd
- **保證(成功建立尾部綁定時,免測 layout)**:該整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型/
瀏覽器成立。平時無害(末行本多字時不改變),僅末行將塌為 ` 直跑——保留
build.sh/Nvim peek 之單檔串接消費)
- `dist/juzhen.css`(合併之樣式)
- - `tsc --emitDeclarationOnly` → `dist/index.d.ts`
+- **完整構建**:`npm run build` 先執行 `node build.mjs`,再執行
+ `tsc --emitDeclarationOnly` → `dist/index.d.ts` 及各模組型別宣告。
- **發佈物**:`dist/` 提交入庫(消費端可直接取單檔/CSS)。
---
@@ -1056,10 +1082,15 @@ build.mjs esbuild:ESM + IIFE + CSS;tsc:d.ts
- **jsdom(自動)**:驗證 DOM 變換——包裝結構、`bd-*` class、邊界處理、冪等、
`revert` 還原、copy-clean(`textContent` 不被注入污染)、lang 政策分支。
-- **瀏覽器(目視,demo 頁)**:layout 相關**無法**於 jsdom 驗證——justify 單份
- 間距、擠壓視覺量、行端折疊、inline-block 基線。demo 頁載 Noto CJK,簡繁
- 並列、可切 justify、附選取/複製測試區,於 Chrome/Safari/FF 目視核對。
-- 文件明確標注哪些結論來自自動測試、哪些待瀏覽器目視。
+- **原生 WKWebView(自動)**:`npm run test:wkwebview` 需 macOS、Xcode 與 GUI 會話;
+ runner 編譯 Swift 宿主並載入 `demo/e2e.html`,驗證真實行盒、幾何、計算樣式、
+ Selection 及 observer/layout 生命週期。此命令使用現有 `dist`,不自動構建;
+ 修改源碼後應先 `npm run build`。jsdom 本身不提供真實版面量測。
+- **跨引擎目視(demo 頁)**:檢查擠壓墨色、基線、字型適配、justify 和寬度變化等。
+ demo 頁簡繁並列、可切 justify、附選取/複製測試區;Chrome/Safari/Firefox 是
+ 待逐環境執行的驗證路線,WKWebView 通過不代表三引擎目視或真實下游頁面已通過。
+- 驗證記錄須綁定實際快照與環境,分別標示 DOM、原生幾何與目視證據;已知交付記錄
+ 見 `UPDATES.md`,不能由測試入口存在推定當前驗收完成。
---
diff --git a/README.md b/README.md
index 1108e62..21dd3d0 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,8 @@ import { createJuzhen } from "cjk-autospace";
import "cjk-autospace/css";
const jz = createJuzhen({ lang: { default: "zh-Hant" } });
-jz.render(document.querySelector("main"));
+const main = document.querySelector("main");
+if (main) { jz.render(main); }
```
**JS 與 CSS 缺一不可**:JS 只注入 `jz-*` 結構,所有視覺尺寸(間隙寬度、擠壓量、
@@ -145,11 +146,12 @@ jz.observe(document.body); // 之後 DOM 變動即自動增量恢復
> 器之固有限制(任何外科式文本 patch 之框架皆會與注入後之 DOM 失步)、非缺陷——聚珍對當前
> DOM 之處理始終自洽。欲改文字,以塊為單位替換內容即可。
-> **`revert` 之範圍**:`revert` 乾淨移除聚珍注入之全部結構(`jz-*`/`data-jz-*`),DOM 回到**邏輯
-> 等價**態;但**不還原** `spacing` 依契約規範化之贅餘作者空格(CJK↔西文吸收為 margin 間隙、
-> CJK↔CJK 剝除;見上表「中西間隙」)。即 `render→revert` 對含 CJK↔西文作者空格之源,還原後語義
-> 一致、無 `jz-*` 殘留,唯該空格已正規化(此差異穩定、不隨反覆 render/revert 累積)。若消費端需
-> 保留原始作者空格以供編輯,應自存原文、勿以 `revert` 當「還原到未處理源碼」。
+> **`revert` 之範圍**:撤回本實例仍持有所有權、且符合產物與來源完整性檢查的結構及屬性,
+> 保留作者標記與其他實例的產物。沒有外部修改時,DOM 回到處理前的**邏輯等價**態;但
+> **不還原** `spacing` 依契約規範化之贅餘作者空格(CJK↔西文吸收為 margin 間隙、
+> CJK↔CJK 剝除;見後面的「中西間隙」功能表)。此差異穩定、不隨反覆 render/revert 累積。
+> 外部克隆、替換或刪除後,以作者當前內容為準,不保證恢復歷史快照或清空所有 `jz-*`。
+> 若消費端需保留原始作者空格以供編輯,應自存原文、勿以 `revert` 當「還原到未處理源碼」。
外部克隆/替換/刪除跨節點載體後,`revert` 保留作者當前內容,不補回已刪文字;
克隆不繼承本實例所有權。欲修改原文,先 `revert` 再修改,或直接替換整塊內容。
@@ -217,7 +219,7 @@ createJuzhen({
|---|---|---|---|
| `lang.default` | string | `"zh-Hant"` | 無 `lang` 祖先時之預設語言標籤 |
| `lang.style` | `quanjiao|kaiming|banjiao` | 逐語言內建(繁→全角、簡→開明) | **全域政策一鍵覆寫** |
-| `lang.policy` | `Record` | `{}` | 逐 lang 政策 |
+| `lang.policy` | `Partial>` | `{}` | 按語言分類覆寫:鍵僅為 `zh-Hans`、`zh-Hant`、`ja`、`other`;如 `zh-TW` 先歸入 `zh-Hant`,不直接作為鍵 |
| `lang.stylePolicy` | `{selector,style}[]` | `[]` | **逐元素樣式覆寫**(選擇器命中之子樹用指定樣式;如 `[{selector:"h1,button,.ui",style:"banjiao"}]`);配合 `data-jz-style` 屬性,優先於 lang |
**作用域**
@@ -248,8 +250,8 @@ createJuzhen({
|---|---|---|---|
| `pillSelector` | CSS 選擇器 | `"code, kbd"` | 整體元素(內部不可見、兩端補隙) |
| `isolateSelector` | CSS 選擇器 | `"mark"`(`""` 停用) | 隔離元素(內部照常、兩端補隙) |
-| `styleInlines` | 標籤名[] | 27 個標準行內標籤 | **透明穿越白名單**(未列之行內標籤=邊界) |
-| `blockTags` | 標籤名[] | 36 個塊標籤 | 塊級邊界集 |
+| `styleInlines` | 標籤名[] | 28 個標準行內標籤 | **透明穿越白名單**(未列之行內標籤=邊界) |
+| `blockTags` | 標籤名[] | 43 個塊標籤 | 塊級邊界集 |
| `skipTags` | 標籤名[] | `PRE,SAMP,TT,VAR,SCRIPT,STYLE,TEXTAREA,INPUT` | avoid 標籤集(**不含 CODE/KBD**,由 pill 涵蓋) |
| `skipAttribute` | string | `null` | 自訂「跳過」屬性名(命中即 avoid) |
| `isSkipped` | `(node)=>bool` | `null` | 自訂跳過謂詞(函數級排除鉤子) |
@@ -291,8 +293,9 @@ createJuzhen({
```
優先序:**逐元素覆寫**(`data-jz-style` 屬性/`stylePolicy` 選擇器,**近祖先勝、同層屬性優先於
-選擇器**)> `lang.policy` > `lang.style` > 內建逐語言預設。屬性值非法(拼錯)則忽略、回退
-lang。半角式渲染沿用既有 `halt`/`margin` 機制(見下節),本功能只做「按元素選樣式」、無新增
+選擇器**)> `lang.policy` > `lang.style` > 內建逐語言預設。屬性值非法(拼錯)只忽略該信號,
+仍查同層 `stylePolicy` 選擇器與祖先;沒有任何逐元素信號時才回退語言政策。
+半角式渲染沿用既有 `halt`/`margin` 機制(見下節),本功能只做「按元素選樣式」、無新增
渲染邏輯。
### 行內元素三分類:透明/隔離/整體
@@ -487,6 +490,10 @@ CSS 皆刪)。`hanging` 選項仍被接受(API 相容)但**無任何效果
## 標點半形:`halt` 預設與 `margin` 後備
+**缺半形特性字型的逐字相容方案仍保留**:`margin` 後備搭配 `jiya.bias`,可按語言分類、
+逐標點指定 `left`/`center`/`right`/`full`。它與字型自帶的 `halt` 是兩條渲染路徑;
+偏側覆寫、量測工具與目視微調流程見本節,不因移除舊負 letter-spacing 方案而取消。
+
開明/半角式把標點收成「半形」,有兩種渲染機制:
| 機制 | 設定 | 原理 | 適用 |
@@ -605,11 +612,11 @@ JS 只注入 `jz-*` 結構,**所有視覺尺寸在 `juzhen.css`**——故 CSS
關閉原生 `text-autospace`(避免與受管間隙重複作用);不設定 `text-spacing-trim`,
間隙不注入空白字元;僅跨節點 IVS gap 之 `U+200B ::after`
設 `user-select:none`,避免該斷行提示進入選取/複製。
-8. **段落屬性照常設、原子透明**:聚珍把標點拆成 inline-block 原子,但已確保這些原子對
- 消費端段落樣式透明——可在段落正常設 `text-indent`(首行縮排)、`hanging-punctuation`、
- `text-decoration`(下劃線)等;庫內已歸零會被原子重複套用之繼承屬性、並令原子承接段落
- 裝飾,故首行縮排正常、下劃線連續不斷,無需任何額外配置(機制見 ARCHITECTURE「原子透明
- 原則」)。
+8. **段落縮排與裝飾**:聚珍把標點拆成 inline-block 原子,於原子上歸零 `text-indent`,
+ 避免段落首行縮排被每個原子重複套用;原子也承接 `text-decoration` 裝飾。
+ 可在段落設定縮排與下劃線(機制見 ARCHITECTURE「原子透明原則」)。
+ `hanging-punctuation` 在原子上設為 `none`;當前管線不提供標點懸掛,不能由此推定
+ 段落設定該屬性便會使受管標點出版心。
---
diff --git a/UPDATES.md b/UPDATES.md
index c92e5da..0c0a075 100644
--- a/UPDATES.md
+++ b/UPDATES.md
@@ -16,6 +16,9 @@
切回 halt 也會殘留 inline-block 內盒。現在由最近有效標記決定模式,可交替巢狀,
切回 halt 同時復原內盒與偏移。margin 的偏側位移僅作用於已壓縮標點;全形標點不再偏移。
+既有缺半形特性字型的逐字相容方案仍保留:`margin` 後備搭配 `jiya.bias`,可按語言分類、
+逐標點選 `left`/`center`/`right`/`full`;字型量測與目視微調流程見 README。
+
**受影響者**:混用含 halt 與缺 halt 字型、巢狀半形模式,或 margin 區域含全形標點的消費端。
標記與呼叫方式無需更換。若曾加 CSS workaround 抵銷舊版行為,更新後須確認其是否仍需要。
@@ -114,6 +117,7 @@ N 字,或不可切容器還含排除內容時不綁。pill 仍可作完整單
353 項 Node 測試、發佈構建及原生 WKWebView 53 項驗收;layout/數值回歸見
`test/layout-lifecycle.test.mjs` 與 `test/numeric-options.test.mjs`;觀察根及跨節點標點
回歸見 `test/observer-boundaries.test.mjs` 與 `test/punctuation-atoms.test.mjs`。
- 系統交互新增 42 項回歸見 `test/systematic-interactions.test.mjs`;另有 32 組功能
- 開關×10 類結構共 320 個 DOM 組合及 splitoff 多實例 36 個組合通過。
+ 系統交互新增 42 項回歸見 `test/systematic-interactions.test.mjs`。修復過程中的中間快照
+ 另通過 32 組功能開關×10 類結構共 320 個 DOM 組合及 splitoff 多實例 36 個組合;
+ 這兩組為調查階段證據,不列為最終提交的獨立復驗。最終快照驗收為上述 353/53 項。
真實下游頁面仍須自行驗證其字型與版面。