docs: 同步現行契約並保留逐字半形後備

This commit is contained in:
2026-10-02 19:29:33 +08:00
parent 0bef51973a
commit 024ad5da93
3 changed files with 113 additions and 71 deletions
+84 -53
View File
@@ -51,8 +51,9 @@ CSS box、observer、瀏覽器量測,以及 TeX node/glue/penalty/macro
屬性);**所有視覺尺寸(margin/letter-spacing/寬度/懸掛位移)一律寫在
`juzhen.css`**。同一段 DOM 在 justify 與否、主題與否下都成立。此分離是擴充性
與跨瀏覽器調校的關鍵。
- **I3 依 `lang` 行為**:每個 pass 解析節點的有效 `lang`(最近祖先),行為與
CSS 皆以 `:lang()` 與 locale 政策驅動。全角式/開明式之別即由此決定。
- **I3 依 `lang` 行為**:JS 解析節點的有效 `lang` 與逐元素樣式政策,決定標點是否半形,
再以 `jz-half`/`jz-half-le` class 交由 CSS 渲染;CSS 不以 `:lang()` 另判寬度政策。
字型地域字形由呼叫端的 `lang` 與字型決定,`options.lang.default` 不會改寫 DOM 的 `lang`。
- **I4 跨瀏覽器一致**:間隙、擠壓等一致性關鍵層只用 `margin`/`letter-spacing`/
`inline-block`/`user-select` 等普遍原語。`text-spacing-trim`/
`hanging-punctuation`/`text-emphasis` 等僅作漸進增強,絕不作為唯一機制。**且當
@@ -61,12 +62,12 @@ CSS box、observer、瀏覽器量測,以及 TeX node/glue/penalty/macro
`text-spacing-trim` 不設定),使三引擎行為收斂於同一路徑(見 §9.1)。
- **I5 justify 單份間距**:間隙與擠壓的結構設計,須保證每個標點在
`text-align: justify` 下只貢獻**一份**對齊伸縮量(見 §8)。
- **I6 冪等與可還原**:每個 pass 對其產物打標記,重跑跳過;提供 `revert` **乾淨移除聚珍注入之
全部結構**(`jz-*` 元素、class、`data-jz-*` 屬性;wrap→解包還原原內容、marker→移除、
splitoff→併回原元素),DOM 回到 pass 前狀態之**邏輯等價**態。「邏輯等價」而非逐字節相同之**唯一
差異**:`spacing` 規範化之贅餘作者空格不還原(見 I1)——故 `render→revert` 對含 CJK↔西文作者
空格之源,還原後語義一致、結構全清,唯該空格已正規化(此差異穩定、不隨 `render`/`revert` 反覆
累積)。`revert` 後 DOM 內無任何 `[data-jz]` 殘留(實測驗收成立)。
- **I6 冪等與可還原**:每個 pass 對產物打標記供定址,私有所有權憑證才決定還原權限。
`revert` 撤回本實例仍有效的產物(wrap→解包、splitoff→併回)及受控屬性,保留作者與
其他實例內容。沒有外部修改時,DOM 回到處理前的**邏輯等價**態;`spacing` 正規化的
贅餘作者空格不還原(見 I1),此差異不隨反覆 render/revert 累積。
外部克隆、替換、刪除後,按產物及來源完整性檢查安全回退,不復活過時文字;
不保證恢復歷史快照或清空所有 `[data-jz]`。DOM 標記本身不授予所有權。
- **I7 邊界感知**:跨樣式行內元素的鄰接(如 `<strong>是</strong>A`)須能識別並
正確處理——由核心 finder 統一提供,各 pass 不自行重複造輪。
- **I8 向下相容 v1(已退役)**:相容層曾保障 v1 消費端(build.sh 單檔串接、Nvim
@@ -76,7 +77,8 @@ CSS box、observer、瀏覽器量測,以及 TeX node/glue/penalty/macro
(見 §3.4),以減少全樹走訪次數;聚合不得犧牲 I6(冪等可還原)與逐功能開關。
- **I10 作用域與分塊**:作用範圍可由 `scope.include` 選擇器限定(opt-in);功能集
可逐塊覆寫(屬性或 `blocks` 映射),由 pass 在 `collect` 內以 per-node 閘解析
(見 §3.6)。CSS 視覺規則須與 JS 作用域同域收斂,不外溢。
(見 §3.6)。`scope.include` 只限定 JS 處理;結構樣式直接作用於 `jz-*`,原生間距
禁用層固定於 `.juzhen`。呼叫端負責讓容器標記與 JS 作用域對應。
---
@@ -105,6 +107,10 @@ jz.disconnect(); // 停止觀察
同批移出根的節點與已離根的舊記錄則跳過,不以 `isConnected` 代替作用域判斷。
搬入既有塊時,以同批移除來源、私有產物或已知 render 根辨識;先 revert 再 render
撤回舊祖先的語言/級別/功能效果。純新增塊直接 render,但插入子樹的快取先失效。
手動 `render`/`revert`/`rerender` 在最外層暫停 observer 前,先同步結算 `takeRecords()`
中的待交付變化,可能更新其他區塊;隨後才執行手動操作,局部回退仍最後生效。
結算舊記錄的錯誤由宿主微任務非同步報告,不成為手動 API 的同步例外;手動操作自己的
錯誤仍照常同步拋出。
**增量粒度契約(下游說明八)**:增量更新須以**塊**為粒度——換塊內容(`textContent`/
`innerHTML`)、增刪塊、改分類屬性,皆與全量重繪一致(差分複驗逐位元相同)。**不支持**對聚珍
@@ -165,6 +171,7 @@ interface BasePass {
interface CharifyPass extends BasePass {
kind: "charify";
prepare?: (ctx: RenderContext) => void;
collect(node: Text, ctx: RenderContext): WrapRequest[];
}
@@ -176,7 +183,8 @@ interface StandalonePass extends BasePass {
type Pass = CharifyPass | StandalonePass;
```
`CharifyPass` 只收集文本包裝請求;`StandalonePass` 自行執行功能所需的掃描或 layout。
`CharifyPass` 可先以 `prepare` 處理跨節點完整原子,再收集普通 Text 的包裝請求;
`StandalonePass` 自行執行功能所需的掃描或 layout。
兩類皆由同一註冊表管理,且均按 `level` 與功能閘判定是否生效。
`createJuzhen` 內建一個**有序、可開關**的 pass 註冊表。`render` 依 `order` 執行
@@ -247,8 +255,9 @@ Nvim peek/Claude 模板等 v1 消費端之過渡期。**最後一批 v1 消費
- `scope.include`:**啟用選擇器**(opt-in 模式)。設定後,**僅處理命中該選擇器之
子樹**(如 `".juzhen, [data-juzhen]"`),其餘一概不動——此即「限定作用範圍」。
- `scope.avoid`:排除選擇器,與內建 avoid 合併。
- **CSS 同域**:`juzhen.css` 之視覺規則一律收斂於同一啟用選擇器(如 `.juzhen` 下),
與 JS 作用域一致,杜絕樣式外溢(呼應 I2、§9.1 之禁用層亦限於此域)。
- **CSS 作用域**:`scope.include` 不改寫 CSS。`jz-*` 結構樣式直接按元素/class 生效;
§9.1 的原生間距禁用層固定於 `.juzhen`。若 JS 用 `.prose` 啟用,呼叫端可用
`class="prose juzhen"` 對齊兩者;作者自行建立的 `jz-*` 也會匹配結構樣式。
**分塊功能選擇**——不同塊啟用不同功能集:
@@ -281,12 +290,11 @@ Nvim peek/Claude 模板等 v1 消費端之過渡期。**最後一批 v1 消費
- **scope.include**:若設啟用選擇器,僅走訪命中之子樹(opt-in,§3.6)。
- **featureEnabledFor(node, name)**:解析該節點之有效功能集(§3.6),供各 pass 之
`collect` per-node 閘用;結果以元素為鍵快取。
- **charify**:把符合條件之字元/字串包進指定 `jz-*` 元素(兩階段:先收集目標、
後變更,避免遍歷中改 DOM 失效)。
- **邊界感知 replace(I7)**:對「跨相鄰文本節點、可跨樣式行內元素」的邏輯字串
做正則 find,回呼決定如何包裝/插入標記。樣式行內元素(`strong/em/a/span/…`)
透明穿越;block/pill/avoid 中斷邏輯串。此即 v1 Pass A「剝離樣式收集邏輯文本
再反映射」之一般化,提升為可重用引擎。
- **文字走訪**:`eachTextNode` 提供受過濾的 Text,charify pass 收集 `WrapRequest`,
由 `core/pass.ts` 批次包裝;Finder 本身不提供 charify mutation API。
- **邊界與來源映射(I7)**:`collectRuns` 收集跨透明 Text 的邏輯字串及完整來源跨度,
`adjacentLogicalChar` 查詢完整鄰字。block/pill/avoid/isolate 等按分類中斷脈絡;
正則匹配與間隙回放由功能模組實作,不是 Finder 的泛用 replace/回呼插入 API。
- **完整 Unicode 原子**:`collectRuns` 將跨透明 Text 的 surrogate pair 先合成完整碼點,
再將 base 與至多一個 variation selector 合為原子;`map.parts` 保留所有來源跨度。
間隙、禁則與避孤字共用此映射,不在原子中間建間隙/斷點、不把 selector 另算實義字。
@@ -385,8 +393,9 @@ avoid」用組合入口 `isAvoided`(=自身或祖先,階段二歸一,免
(`{ cjk?, western?, biaodian? }`)**擴充**,`resolveRuleset(override)` 以**追加**語義合併
為最終規則集,置於 `ResolvedOptions.ruleset`。各 pass 由此取分類:
- `spacing`:`makeRules(ruleset)` 由 `cjk`/`ansAfter`/`ansBefore` **per-instance 編譯**
全部間隙正則(createJuzhen 各一次,非每 block);`ALNUM`/`ALPHA`/運算符/括號等結構性
- `spacing`:每次 spacing render 開始時,`makeRules(ruleset)` 按該實例的
`cjk`/`ansAfter`/`ansBefore` 編譯全部間隙正則,復用於本次各處理塊;
`ALNUM`/`ALPHA`/運算符/括號等結構性
ASCII 集為固定常數、不隨規則集變。
- `jiya`:`classifyBiaodian(ch, ruleset.biaodian)` 決定 charify 與 bd-* 子類。
- `jinze`:`anyCjk = RegExp(ruleset.cjk)` 決定詞元邊界之 CJK 判定。
@@ -409,21 +418,28 @@ avoid」用組合入口 `isAvoided`(=自身或祖先,階段二歸一,免
祖先鏈掃 `data-jz-style` 屬性/`stylePolicy` 選擇器命中,近祖先勝、同層屬性優先於選擇器)
> 逐 lang `policy` > 全域 `style` > 內建逐語言預設(`zh-Hant`/`zh-HK` → 全角、`zh-Hans` →
開明)。逐元素覆寫供「非段落/UI 元素用半角、段落用全角/開明」等需求(`data-jz-style` 屬性
值非法則忽略、回退 lang)。此純為「按元素選樣式」之路由,`banjiao` 半形渲染沿用 `halt`/
值非法時只忽略該信號,仍查同層選擇器與祖先;逐元素信號皆缺失才回退語言政策)。
此純為「按元素選樣式」之路由,`banjiao` 半形渲染沿用 `halt`/
`margin`(§6.3),無新增壓縮邏輯。
CSS 端對應以 `:lang()` 選擇器落實 lang 層政策(I2/I3);逐元素覆寫屬 JS 端 baseline 判定。
語言與逐元素政策均由 JS 判定並轉為半形 class;CSS 按 class 使用 `halt`/`margin` 渲染。
`lang.policy` 的鍵是 `zh-Hans`/`zh-Hant`/`ja`/`other` 四個分類,非任意完整語言標籤。
### 4.4 `dom.ts`——`jz-*` 元素工廠與還原
`createEl('jz-char', {class, attrs})`、`wrap(node, el)`、`splitAt`、`insertMarker`、
冪等標記(`data-jz="<pass>"`)、`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**(要間隙與標點半形、不要避孤字/避頭尾);
正文 `<p>` 預設全功能。
**實現**:
- 每個 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,任意寬度/字型/
瀏覽器成立。平時無害(末行本多字時不改變),僅末行將塌為 <N 字時「拉字下來」。
- **選項**:`orphan?: boolean | { chars?: number }`(feature gate;**預設開 chars=2**;per-block
經 `blocks`/`data-jz`;段落級,`data-jz-level="text"` 子樹跳過)。lang 無關。
經 `blocks`/`data-juzhen`/`data-juzhen-off`;段落級,`data-jz-level="text"` 子樹跳過)。lang 無關。
- **邊界**:avoid/text/`orphan` 關閉/`isSkipped` 區域清空先前計數,只綁其後的合法
段末區域;不足 N 字則不綁。完整碼點及 base+VS 只算一個字。pill 不計內部字,仍可
作完整外部單元參與綁定;不可切且含排除內容的容器不退化整綁。極窄容器(<N 字寬)
@@ -920,8 +943,9 @@ order `90 jiyaLineEdge`,`revert` 清 `jz-half-le`。純版面、jsdom 跳過
2. `jz-char` 設 `display: inline-block`:使其成為 justify 的**單一原子項**,內部
不開伸縮 gap,標點與其字面留白同屬一項,對齊伸縮只落在**項與項之間** → 標點
只貢獻一份。
3. **開明式**下,`jz-inner` 以負 letter-spacing 擠去半形留白,該項本身即半形,
justify 一份 gap 即足。
3. **開明式**下,JS 為可壓縮標點加半形 class;預設由 `jz-inner` 的 OpenType `halt`
收 advance。缺半形特性的字型可用 `margin` 後備:0.5em 外盒搭配逐字 `jiya.bias`
偏側定位(§6.3)。原子內部不另開 justify 伸縮量;舊負 letter-spacing 方案不再使用。
4. **全角式**下,留白保留於原子項內,仍只算一份;以 `jz-jinze` 綁定確保不在留白
側額外開機會點。
@@ -949,8 +973,8 @@ order `90 jiyaLineEdge`,`revert` 清 `jz-half-le`。純版面、jsdom 跳過
- `text-indent: 0`——段落首行縮排傳入後,每原子內部首行各縮排一次,shrink-to-fit 寬膨脹為
`縮排量+字形`,標點附近冒出巨大間距。實測:歸零後段落首行縮排仍保持 2em,原子寬由 ~3em
回到 0.5em/1em。
- `hanging-punctuation: none`——同屬區塊容器首/末行機制,每原子各自懸掛自身標點。Chrome
未實作(測不出)、Safari 會觸發,且與規劃中之「標點懸掛」(§任務 6)相衝,故按機制一併歸零。
- `hanging-punctuation: none`——避免區塊首/末行機制在每個標點原子內重複套用。
當前管線沒有標點懸掛 pass;段落保留此屬性不代表 inline-block 內的受管標點可以懸掛。
**不歸零者**(實測不洩漏或本應逐字穿透):`letter-spacing`/`word-spacing`(原子邊界處理
一致、無翻倍,實測 delta 與基線同);`text-align`/`text-align-last`/`text-justify`
@@ -1013,10 +1037,10 @@ src/
core/
pass.ts 兩類 pass 之分組、排序與管線執行器
cross-atom.ts 跨 Text 標點原子搬移/私有來源記錄/回退
finder.ts 遍歷/avoid/charify/邊界感知與跨節點 run
finder.ts 遍歷/avoid/功能閘/鄰字與跨節點原子映射
unicode.ts 字元類 + 標點子類 + 規則正則表
locale.ts 有效 lang / 分類 / 全角開明政策
dom.ts jz-* 工廠 / ownership / wrap / revert
dom.ts jz-* 工廠 / 所有權與屬性寫者 / 還原及受控解包鏈
split.ts 禁則/垂懸字之行內元素邊界切分
typeset/
spacing.ts 中西間隙
@@ -1029,9 +1053,10 @@ src/
css/
juzhen.css 入口(@import 各 partial)
_normalize.css _spacing.css _jiya.css _jinze.css _orphan.css
test/ jsdom DOM 正確性測試
demo/ 瀏覽器 layout/justify/簡繁 目視驗證頁
build.mjs esbuild:ESM + IIFE + CSS;tsc:d.ts
test/ jsdom DOM 測試 + 原生 WKWebView Swift 宿主/runner
demo/ layout/justify/簡繁 目視頁 + e2e.html 自動斷言頁
build.mjs esbuild:ESM + IIFE + CSS
package.json npm build 串聯 build.mjs 與 tsc 聲明生成
```
`biaodian.ts`/`emphasis.ts`/`ruby.ts`/`hanging.ts` 及同名 CSS partial 均不存在;
@@ -1042,12 +1067,13 @@ build.mjs esbuild:ESM + IIFE + CSS;tsc:d.ts
## 11. 構建與發佈
- **源碼**:TypeScript(strict),模組化。
- **構建**:`build.mjs` 以 esbuild 產出
- **JS/CSS 構建**:`node build.mjs` 以 esbuild 產出
- `dist/juzhen.mjs`(ESM,供打包器/TS)
- `dist/juzhen.iife.js`(IIFE,全域 `Juzhen`,原生 `<script>` 直跑——保留
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`,不能由測試入口存在推定當前驗收完成。
---
+23 -16
View File
@@ -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, style>` | `{}` | 逐 lang 政策 |
| `lang.policy` | `Partial<Record<LangClass, PunctStyle>>` | `{}` | 按語言分類覆寫:鍵僅為 `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`;當前管線不提供標點懸掛,不能由此推定
段落設定該屬性便會使受管標點出版心。
---
+6 -2
View File
@@ -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 項。
真實下游頁面仍須自行驗證其字型與版面。