fix(web): 修復排版邊界與增量生命週期

This commit is contained in:
2026-10-02 19:08:02 +08:00
parent 1c2fb0ed59
commit b1a16c4969
39 changed files with 4763 additions and 673 deletions
+32 -13
View File
@@ -6,7 +6,8 @@
> (Chrome/Edge/Safari/WebKit/Firefox)。
倉庫名仍為 `cjk-autospace`;公開 API 為 `createJuzhen` / 全域 `Juzhen`;注入之
自訂元素前綴 `jz-`。完整設計見 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。
自訂元素前綴 `jz-`。消費端契約見本文的[用法](#用法)與[標記說明](#行內元素三分類透明隔離整體),
升級影響見 [`UPDATES.md`](./UPDATES.md)。完整設計見 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。
> 本版(2.x)為從零重寫。v1 之 `createCjkAutospace` 相容層**已移除**(最後一批
> v1 消費端已遷移 v2 API);v1 原始碼保存於 `v1-autospace` 分支。
@@ -71,6 +72,7 @@ import vectorsSchema from "cjk-autospace/rules/vectors-schema" with { type: "jso
```html
<link rel="stylesheet" href="dist/juzhen.css">
<main class="prose juzhen" lang="zh-Hant">需要排版的正文。</main>
<script src="dist/juzhen.iife.js"></script>
<script>
const jz = Juzhen.createJuzhen({ scope: { include: ".prose" } });
@@ -90,7 +92,7 @@ jz.render(document.querySelector("main"));
```
**JS 與 CSS 缺一不可**:JS 只注入 `jz-*` 結構,所有視覺尺寸(間隙寬度、擠壓量、
`justify` 原子化、字體堆疊)都在 `juzhen.css`。
`justify` 原子化)都在 `juzhen.css`;字型由呼叫端 CSS 設定。
### 動態頁面:增量恢復(局部變化免全樹重掃)
@@ -114,6 +116,13 @@ jz.observe(document.body); // 之後 DOM 變動即自動增量恢復
- **`observe(root?)`/`disconnect()`**:以 `MutationObserver` 自動增量。juzhen 自身之注入
於處理期間暫停觀察、**絕不自觸發**;新增之塊直接 render(免全容器重掃)。需 `MutationObserver`
(瀏覽器/jsdom 有;無則 no-op)。
觀察根可尚未掛入 document;更新按目前是否仍在該根內判定。同批新增後移出根的節點,
或已離開根的舊變更目標,不會被此 observer 排版。
搬入的既有塊先撤回本實例於舊父環境的產物,再依新祖先的級別、功能及語言重排;
純新增塊仍直接 render。插入子樹的祖先快取也會失效。
手動 `render`/`revert`/`rerender` 暫停觀察前會先結算已排隊的變更(可能觸發其他區塊
的排版),再執行本次手動操作;因此局部回退仍最後生效,也不會丟失兄弟區塊更新。
結算中的 observer 錯誤仍由宿主非同步報告,手動操作自己的錯誤則照常同步拋出。
- **`invalidate(root?)`**:清該子樹之內部快取。**`revert` 已自動失效其子樹**,故僅在「改了既有
元素之分類相關屬性(`class`/`data-jz-*`/`lang` 等)**但不走 `revert`**」時才需顯式調用,以
免下次 render 沿用陳舊分類。內容(子節點/文本)變化不改分類、無需 `invalidate`(但須
@@ -137,6 +146,10 @@ jz.observe(document.body); // 之後 DOM 變動即自動增量恢復
> 一致、無 `jz-*` 殘留,唯該空格已正規化(此差異穩定、不隨反覆 render/revert 累積)。若消費端需
> 保留原始作者空格以供編輯,應自存原文、勿以 `revert` 當「還原到未處理源碼」。
外部克隆/替換/刪除跨節點載體後,`revert` 保留作者當前內容,不補回已刪文字;
克隆不繼承本實例所有權。欲修改原文,先 `revert` 再修改,或直接替換整塊內容。
其他聚珍實例合法撤回包裹時,來源記錄會沿受控解包鏈更新,任意回退次序不丟 base/VS。
---
## 切片 1 已實作功能
@@ -146,8 +159,8 @@ jz.observe(document.body); // 之後 DOM 變動即自動增量恢復
| **中西間隙** `spacing` | CJK↔西文邊界以 copy-clean 之 `jz-hws` 與 `margin-right` 留隙;**不插入空白字元**,故剪貼板不多出空格。唯跨節點 IVS 產物以 `::after` 產生的 `U+200B` 斷行機會設 `user-select:none`;它不進入 DOM/`textContent`。「西文」涵蓋拉丁(含擴充 A/B,拼音聲調 `ā ǎ ē`)、希臘、西里爾、數字、貨幣(`€ ₩ ₿`)、數學運算符等;CJK 標點(`。,「」()……` 等)不誤判為西文、不生隙。含跨樣式行內邊界與 pill/隔離元素邊界(`code`/`kbd`/`.katex` 與 `mark`,兩端補隙口徑一致;pill 緊鄰標點時穿越 `jz-jinze` 仍補間隙;pill 跨行右隙、間隙落行末之自我應驗皆已修)。**pill 之邏輯鄰字為標點時(如 `漢字。<code>`)不誤加間隙**(標點非 pill 鄰字;`jz-char`/`jz-inner` 透明穿越,與 `collectRuns` 一致)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 |
| **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 `<span lang="en">`,配合 CSS `hyphens:auto` 原生斷詞(DOM 無連字符 → 複製不污染)。 |
| **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點**雙側獨立**收——各標點若其有空白之側緊鄰另一標點即收半,故 `,「` 全角式與開明式皆 1em、`。」` 全角式 1.5em/開明式 1em;**唯句末點號接開類為句界**——`。「`/`?「` 保留句末點號右空白、僅收開類,全角式與開明式皆 1.5em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 |
| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。邊界字落於**透明**行內樣式元素(`strong`/`em` 等)內時**克隆切分**只綁邊界詞元(避免整個元素入 `nowrap` 致行末整體掉行、上行留巨大空隙),切出之克隆標 `data-jz-kind="splitoff"`、`revert` 時自動併回原元素;不可切元素(`a`/`abbr`/`ruby`/帶 `id`,以及 **pill/隔離/avoid**——見「行內元素三分類」)退化為**整元素綁定**——pill(如行內公式 `.katex`)即作為原子鄰字單位整體綁入 `jz-jinze`,絕不下潛肢解其內部。 |
| **垂懸字避免** `orphan` | 段末行 CJK 實義字 ≥ N(預設 2;標點/符號不計)。方案 C(斷行層、無測量):把段末「第 N 個實義字起至段末」包入 `jz-orphan`(`white-space:nowrap`),整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型成立。單行段無害。邊界字落於行內元素內時同樣**克隆切分**(只綁末 N 字、避免整個長元素入 `nowrap` 溢出容器;機制同禁則)。**預設開**(`orphan:false` 關,`{chars:n}` 調)。段落級功能。 |
| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。邊界字落於**透明**行內樣式元素(`strong`/`em` 等)內時**克隆切分**只綁邊界詞元(避免整個元素入 `nowrap` 致行末整體掉行、上行留巨大空隙),切出之克隆標 `data-jz-kind="splitoff"`、`revert` 時自動併回原元素;不可切元素(`a`/`abbr`/`ruby`/帶 `id`,以及 **pill/隔離**——見「行內元素三分類」)退化為**整元素綁定**——pill(如行內公式 `.katex`)即作為原子鄰字單位整體綁入 `jz-jinze`,絕不下潛肢解其內部。兩側須符合作用域、paragraph 級別與 `jinze` 功能閘;不可切時也不把 avoid/text/off 後代整綁帶入。 |
| **垂懸字避免** `orphan` | 段末可處理的連續區域 CJK 實義字 ≥ N(預設 2;標點/符號不計,完整碼點及 base+VS 算一字)。方案 C(斷行層、無測量):把段末「第 N 個實義字起至段末」包入 `jz-orphan`(`white-space:nowrap`),整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型成立。單行段無害。邊界字落於行內元素內時同樣**克隆切分**(只綁末 N 字、避免整個長元素入 `nowrap` 溢出容器;機制同禁則)。**預設開**(`orphan:false` 關,`{chars:n}` 調)。段落級功能。avoid/text/off/`isSkipped` 區域中斷計數,只綁其後的合法尾部;不足 N 字或不可切容器含排除內容時不綁。 |
| **功能分級** `level` | 功能分文本級(中西間隙、標點擠壓——任意文本片段適用)與段落級(禁則、垂懸字、長詞——需行/段布局)。塊級元素預設全功能;`data-jz-level="text"` 子樹或選項 `level:{text:"選擇器"}`(如標題/按鈕)僅跑文本級。 |
| **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 |
@@ -215,10 +228,10 @@ createJuzhen({
| 選項 | 型別 | 預設 | 作用 |
|---|---|---|---|
| `spacing` | bool | `true` | 中西間隙 |
| `longWord` | bool|`{minLength,lang}` | `true`(`{6,"en"}`) | 長英文詞斷字 |
| `longWord` | bool|`{minLength,lang}` | `true`(`{6,"en"}`) | 長英文詞斷字;`minLength` 須為 ≥2 的有限安全整數,無效值回退 6 |
| `jiya` | bool|`{halfWidth,bias}` | `true` | 標點擠壓(*詳見「標點半形」*) |
| `jinze` | bool | `true` | 禁則避頭尾 |
| `orphan` | bool|`{chars}` | `true`(`{chars:2}`) | 垂懸字避免 |
| `orphan` | bool|`{chars}` | `true`(`{chars:2}`) | 垂懸字避免;`chars` 須有限且 ≥1,向下取整,無效值回退 2 |
| `justifyAtoms` | bool | `true` | inline-block 對齊原子(分頁器設 `false`) |
| `level.text` | 選擇器 | `null` | 命中子樹僅跑文本級(*詳見「功能分級」*) |
| `blocks` | `{selector,features}[]` | `[]` | 分塊功能選擇 |
@@ -367,6 +380,10 @@ jz.render();
因此同一元素同時寫白名單與黑名單時,**白名單優先**;`data-juzhen=""` 是空白名單,
不是恢復預設。全域停用的功能不會被局部白名單重新啟用。
`spacing` 與 `jiya` 的門控也作用於普通行內內容;禁用區域不刪作者空格、不參與
外部間隙/標點鄰接,空禁用元素同樣中斷邊界。子白名單可在自身子樹重新啟用。
`scope.include` 可選行內子樹;文本級間隙不要求其父塊也命中選擇器。
### 功能分級:文本級 vs 段落級(`level`/`data-jz-level`)
**text/paragraph 決定允許哪些排版功能,不是 HTML 標籤分類,也不設定 CSS display。**
@@ -485,12 +502,14 @@ CSS 皆刪)。`hanging` 選項仍被接受(API 相容)但**無任何效果
### 激活與粒度
模式由 **`data-jz-halfwidth` 屬性所在子樹**決定(CSS 後代選擇器),故粒度極靈活:
模式由**最近的有效 `data-jz-halfwidth="halt|margin"` 標記**決定;CSS 繼承模式參數,
不依標記距離競爭選擇器。未標記時用 halt;非法值忽略並繼承祖先模式。
- **全域**:`jiya: { halfWidth: "margin" }` → `render()` 於處理 root 自動掛屬性。
- **逐區塊/逐語言**:在 HTML 任一祖先手寫 `data-jz-halfwidth="margin"`(或
`"halt"`),即令該子樹用對應機制——無需另建實例。常見如「全頁 `halt`,某用
無 `halt` 字型之區塊掛 `margin`」,或反之(`margin` 頁中嵌一塊 `halt` 島)。
`"halt"`),即令該子樹用對應機制——無需另建實例。可任意交替巢狀 halt/margin;
切回 halt 時同時撤銷 margin 盒寬、內盒與墨色偏移。未壓縮的全形標點不套偏側位移。
模式切換是純 CSS,變更此屬性即可,無須 rerender。
```html
<!-- 全頁 halt(預設),此塊改用無 halt 字型 → 局部 margin 後備 -->
@@ -564,9 +583,9 @@ JS 只注入 `jz-*` 結構,**所有視覺尺寸在 `juzhen.css`**——故 CSS
`import "cjk-autospace/css"`)。缺此則間隙/擠壓/justify 原子化全部失效。
2. **設定 `lang`(強烈建議)**:在內容容器標 `lang="zh-Hans"`/`"zh-Hant"`/`"ja"`,
驅動全角/開明政策與字形 `locl`。無 `lang` 時回退 `options.lang.default`。
3. **作用域類(用 `scope.include` 時)**:若設 `scope.include: ".juzhen"`,須給內容
容器加對應 class(如 `class="juzhen"`);`juzhen.css` 之 normalize 亦收斂於此域,
不外溢。
3. **作用域類**:受管內容容器須加 `class="juzhen"`,以啟用內建原生間距禁用樣式。
`scope.include` 只控制 JS 處理範圍,不會改寫 CSS 選擇器;若使用 `.prose` 等自訂
選擇器,請同時加類別(如 `class="prose juzhen"`)。
4. **長英文詞斷字**:`longWord` 包出 `<span lang="en">` 後,須由消費端 CSS 提供
`hyphens: auto`(含 `-webkit-hyphens`)才會原生斷詞顯示連字符。
5. **`justify`(選用)**:欲兩端對齊則自行設 `text-align: justify`;句末標點單份間距
@@ -578,7 +597,7 @@ JS 只注入 `jz-*` 結構,**所有視覺尺寸在 `juzhen.css`**——故 CSS
之字型**(如方正書宋 GBK)須改用 `jiya.halfWidth:"margin"` 後備,並按字型以
`jiya.bias` 逐字微調墨色偏側(見「字型適配工作流」)。
7. **自動處理、無需消費端介入**:`juzhen.css` 已在 `.juzhen` 範圍內以 `@supports`
關閉 Blink 原生 `text-spacing-trim`/`text-autospace`(避免與託管擠壓重複作用),
關閉原生 `text-autospace`(避免與受管間隙重複作用);不設定 `text-spacing-trim`,
間隙不注入空白字元;僅跨節點 IVS gap 之 `U+200B ::after`
設 `user-select:none`,避免該斷行提示進入選取/複製。
8. **段落屬性照常設、原子透明**:聚珍把標點拆成 inline-block 原子,但已確保這些原子對