docs: §6.5 功能分級/垂懸字落地;標點懸掛停用記錄(ARCHITECTURE/README)

ARCHITECTURE §6.5:功能分級、垂懸字(方案 C,預設開)標為已落地工作良好;標點懸掛標為
實作後停用,記錄四項不可行原因、三重防逃逸、未來原生 hanging-punctuation 路徑(§6.5.6)。
README:功能表加功能分級 level、垂懸字 orphan;標點懸掛列改為「已評估停用」說明;選項示例
與「標點懸掛」一節同步為停用。
This commit is contained in:
2026-06-09 23:26:06 +08:00
parent 04f2b25e4c
commit 89d1008755
2 changed files with 134 additions and 39 deletions
+88 -38
View File
@@ -480,17 +480,39 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
攀爬找流內鄰字(已於 `edgeRect` 處理 jz-* 包裹之攀出);屬 layout 層,jsdom 無法
驗,須瀏覽器目視。
### 6.5 標點懸掛 + 垂懸字避免 + 行端禁則 — 整體計劃(待實作)
### 6.5 標點懸掛 + 垂懸字避免 + 行端禁則 — 整體實作(已落地
三個相關機制之**整體實作計劃**(同步引入),皆處理「行端/段末之標點與字」;其分層職責須先
釐清,以免重複處理或衝突。**前置**須先落地「功能分級」(見 6.5.0a),因懸掛/垂懸字屬段落級、
三個相關機制之**整體實作**(同步引入),皆處理「行端/段末之標點與字」;其分層職責須先
釐清,以免重複處理或衝突。**前置**「功能分級」(見 6.5.0a),因懸掛/垂懸字屬段落級、
不應作用於標題/按鈕/單行文本。
> **實作狀態(2026-06**6.5.0a 功能分級、6.5.1 垂懸字(方案 C,預設開)已落地且工作良好。
> 6.5.2 行端模式重構為 squeezehangnonesqueeze 為現有行端半形、保留並改善);**惟標點懸掛
> hang 經實測確認不可行、已全面停用**(仍保留休眠程式碼)。
>
> **行歸屬判定 bug 修正**(連帶改善 squeeze,保留):原以「行盒 top 差 > EPS=4px」判同/異行,
> 但 jz-char 為 inline-block,其行盒 top 與相鄰純文本字形 top 因基線差約 5px、遠小於行高卻超
> 舊容差,致同行誤判為下一行。改判「垂直區間」:下一行=`b.top ≥ a.bottom`、上一行=
> `b.bottom ≤ a.top`(容 2px 亞像素)。
>
> **標點懸掛停用之原因**(純負 margin 機制不可行,headless + 真實瀏覽器實測):
> 1. 句號等標點墨色偏字框左下角;安全凸出量(< 1 個 CJK 字寬 1em)只把空白半框推出版心、
> **墨色仍在版心內**(視覺不懸掛)。
> 2. 凸出滿 1em 又**釋放一整字寬**、把下一字拉上本行、與懸掛標點**重疊**。
> 3. 懸掛與 inline-block 對齊原子**互斥**justifyAtoms:true 下負 margin 被 justify 吸收)。
> 4. `ResizeObserver` 重排時,「自然佈局標記→施加 margin→佈局再變」致標記**錯亂**。
>
> 真正的解是原生 CSS `hanging-punctuation`,惟 Chrome 不支援、且與本庫 inline-block 原子衝突。
> 故 hang 之 lineedge 程式碼與 CSS 保留休眠,並設**三重防逃逸**(`lineEnd` 鎖死 squeeze、
> `modeForChar` 降級、`applyEdge` 之 `HANG_ENABLED=false` 總開關)——縱使下游以任何方式要求亦
> 不啟用。詳見 6.5.6。
#### 6.5.0a 功能分級:文本級 vs 段落級 + 元素標記(前置基礎設施)
**動機**:懸掛/垂懸字/禁則等為**段落級**功能(依賴行/段布局),不應作用於標題、按鈕、單行
或單元素文本;而中西間隙、標點挤压為**文本級**,任意文本片段皆適用且想要。下游須能**方便標記**
元素屬哪級。此為貫穿性基礎設施,實作時應提升為正式架構節(§3.x);此處先記計劃。
元素屬哪級。此為貫穿性基礎設施(已實作:`Finder.levelFor`/`levelAllows`、各 pass `level`
元數據、選項 `level.text`/屬性 `data-jz-level`)。
**分類(含現有與待加功能)**
@@ -527,7 +549,7 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|---|---|---|---|---|
| 禁則(§6.4 jinze | **斷行層** | 把標點趕到正確的邊(點號/閉類→行末、開類→行首;連續標點不拆) | nowrap `jz-jinze` | 已實作 |
| 行端半形(lineedge 現有) | **渲染層·紧排** | 行首開類/行末閉類**收半形** | `jz-half-le`(halt) | 已實作(將併入行端模式) |
| 標點懸掛(本計劃) | **渲染層·凸出** | 行末點號/閉類**右凸**、行首開類**左凸**出版心 | 負 `margin` | 待實作 |
| 標點懸掛 | **渲染層·凸出** | 行末點號/閉類**右凸**、行首開類**左凸**出版心 | 負 `margin` | ⛔ 實作後停用(見 6.5.6 |
| 垂懸字避免(本計劃) | **斷行層** | 段末行 **CJK 實義字數 ≥ N** | nowrap `jz-orphan` | 待實作 |
- **正交性**:斷行層(禁則/垂懸字,管「哪些字符在哪行、不可斷」)與渲染層(半形/懸掛,管
@@ -541,7 +563,7 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
(更優),把 C 綁定**條件化**(僅末行實義字 ∈ [1,N−1] 才綁),借懸掛 layout pass 之測量協同
判定,C 不獨立測。末行實義字 ∈ [1,N−1] 僅垂懸字(換行)可解(懸掛不增實義字)。
#### 6.5.1 垂懸字避免(方案 C,斷行層,無測量)— 先做
#### 6.5.1 垂懸字避免(方案 C,斷行層,無測量)— 已實作
- **目標**:段末行 CJK 實義字數 ≥ N(預設 2;標點/符號不計)。單行段無效。
- **機制**standalone pass,遍歷作用域段落;自段末向前數 N 個 CJK 實義字(`ruleset.cjk` 判定、
@@ -549,8 +571,8 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
CSS `white-space: nowrap`)。
- **保證(數學、免測 layout)**:該整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型/
瀏覽器成立。平時無害(末行本多字時不改變),僅末行將塌為 <N 字時「拉字下來」。
- **選項**`orphan?: boolean | { chars?: number }`feature gate預設**待定**——建議預設開
chars=2per-block 經 `blocks``data-jz`)。lang 無關。
- **選項**`orphan?: boolean | { chars?: number }`feature gate**預設開 chars=2**per-block
經 `blocks``data-jz`;段落級,`data-jz-level="text"` 子樹跳過)。lang 無關。
- **邊界**:段落實義字 < N → 不綁。極窄容器(<N 字寬)→ 整體可能溢出(罕見,文檔提示)。
- **order=70**charify 20 後、layout pass 前;longWord 60 後)。
- **與禁則協同**:段末範圍可能含 jinze 已綁之 `jz-jinze`;置於 jinze(40) 後,將 jz-jinze 整體
@@ -559,7 +581,11 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
- **測試**:jsdom 結構(末 N 實義字+尾標點入 jz-orphan;符號不計;單行段/實義字<N 不綁);
headless 窄容器拖動驗末行恒 ≥N。
#### 6.5.2 標點懸掛(Hanging,渲染層,layout
#### 6.5.2 標點懸掛(Hanging,渲染層,layout— ⚠ 已實作後停用
> **停用(2026-06)**:本節原設計之懸掛經實測**不可行、已全面停用**(原因見 6.5 開頭與 6.5.6
> 三重防逃逸見 6.5.6)。lineedge 之 squeeze 模式(行端半形)保留並改善。下文為原設計記錄,
> 供日後改用原生 `hanging-punctuation` 重啟時參考。
- **目標**:行末 bd-stoppauseclose **右凸**出右緣;行首 bd-open **左凸**出左緣。文字邊緣齊。
- **不用 CSS `hanging-punctuation`**:瀏覽器支援差(僅 Safari 較全)+聚珍「原子透明」已對 jz-*
@@ -568,45 +594,69 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
`fonts.ready` 重算)。量測各標點 jz-char 是否在行首/行末(同 lineedge `edgeRect`/`firstRect`
跨 jz-* 包裹攀爬)。行末點號/閉類加 `jz-hang-r`→`margin-right: -<凸出量>`;行首開類加
`jz-hang-l`→`margin-left: -<凸出量>`。
- **與現有行端半形整合**:抽出「行端標點偵測」共用,按 `lineEnd` 模式加 `jz-half-le`squeeze
或 `jz-hang-*`hang)。重構後 squeeze 行為須不變(回歸測試保綠)。
- **凸出量**:取決於標點 advancehalt 半形 0.5em/全形 1emmargin 盒 0.5em)——**需 headless
實測校準**(風險)。目標令標點墨色邊緣齊版心。
- **lang 閘**clreq——繁體橫排預設不懸掛(`zh-Hant` 預設 squeeze/none);簡體可開。經
`options.hanging` + 逐 lang。預設**待定**(建議預設關,顯式開)。
- **自我應驗**:懸掛凸出改行寬→改斷行→行末標點變→自我應驗(同 gapTrim/lineedge)。緩解:
仿 gapTrim「起始全施/穩定迭代」教訓。**重點風險**。
- **order**layout pass,最後(整合進 lineedge 行端模式,或 order≈94)。
- **CSS**`jz-char.jz-hang-r{margin-right:-…}` `.jz-hang-l{margin-left:-…}`。`revert` 清 class
- **跨瀏覽器**:負 margin 一致;須 ChromeSafariFF 目視(R1/R2)。headless 量測行末標點是否
凸出右緣。
- **與現有行端半形整合**(已實作):抽出「行端標點偵測」共用,按 `lineEnd` 模式(由
`options.hanging` 解析:開→`hang`、關→`squeeze`)於 `relayout` 加 `jz-half-le`squeeze
或 `jz-hang-*`hang)。squeeze 行為不變(回歸保綠)。
- **凸出量按類**headless 實測校準,Noto Sans CJK SC):句末/句內點號(。,等墨色小、留白
多)近全形 `--jz-hang-stop`預設 1em,整體落版心外、正文齊邊);括號引號半形
`--jz-hang-bracket`(預設 0.5em,墨色偏側、留白半框)。實測句號右凸 +1.005em、開括號左凸
+0.5em,正確。CSS 變數可逐字型調校。
- **已 baseline 半形者不懸掛**jz-char 帶 `jz-half`(開明/半角式之括號等)已收為半形、無留白
可凸,JS 端 relayout 跳過(否則負 margin 把墨色整體拉出)。故 SC 開明式之括號維持半形不掛、
句末點號(不收半形)才掛
- ⚠ **懸掛與 inline-block 對齊原子互斥(實測,重要約束)**:預設 `justifyAtoms:true`jz-char
為 inline-block)下,懸掛之負 margin 被 justify 重分配吸收、標點**不凸出**headless 量測
凸出量 0);改 `justifyAtoms:false`jz-char 為 inline)則正確凸出(+1em)。故欲懸掛須設
`justifyAtoms:false`。二者皆渲染層、不可兼得(如同 squeeze/hang 互斥)。容器宜留右
padding ≥1em 容納凸出標點。
- **lang 閘**(已實作):繁體橫排預設不懸掛(`relayout` 之 `modeForChar` 對 `zh-Hant` 降回
squeezeclreq);簡體可掛。`options.hanging` 預設**關**(顯式開)。
- **自我應驗**(已解,§6.5 開頭發現 4):採**兩階段** relayout——先 `clearEdge` 清全部行端
margin/半形,於自然(無行端效果)佈局量測**全部**標點之行端狀態(不變更 DOM),再一次性
套用。每次量測皆見無污染佈局(仿 gapTrim「起始全清」教訓),終態穩定。
- **行歸屬判定**(已修,§6.5 開頭發現 1):以「垂直區間」`lowerLine`/`higherLine` 取代舊
「top 差 > EPS」,吸收 inline-block 行盒與純文本字形之基線差(~5px),免同行誤判。
- **order**`90 lineEdge`squeeze/hang 共此 pass)。`revert` 清 `jz-half-le`/`jz-hang-*`。
- **跨瀏覽器**:負 margin 一致;須 ChromeSafariFF 目視(R1/R2)。headlessChrome)已驗
行末標點凸出右緣、行首凸出左緣、繁體不掛、無中段誤觸。
#### 6.5.3 行端禁則(§6.4review
- 現狀避頭尾已與懸掛協同(禁則趕標點到邊、懸掛凸出邊緣)。review:①連續標點群組 `jz-jinze`
在行端懸掛時凸出最外側;②jz-orphan 與 jz-jinze 嵌套;③lineedge 偵測已跨 jz-jinze 攀爬。
預期 jinze 本身不需大改。
- 禁則避頭尾與 squeeze 行端半形協同(禁則趕標點到邊、行端收半形);jz-orphan 與 jz-jinze 嵌套
已處理(邊界字跨 jz-jinze 攀爬)。lineedge 偵測已跨 jz-jinze 攀爬。jinze 本身未大改。
(懸掛已停用,故無「懸掛凸出最外側」之協同需求。)
#### 6.5.4 統一 pass order(增補後)
#### 6.5.4 統一 pass order
`20 charify → 25 jiyaAdj → 40 jinze → 50 spacing → 60 longWord → 70 orphan(垂懸字)→
90 lineEdge(行端 squeeze/hang)→ 92 gapTrim`。
90 lineEdge(行端 squeezehang 已停用)→ 92 gapTrim`。
#### 6.5.5 實作順序(分步,風險遞增)
#### 6.5.5 實作順序與結果
0. **功能分級(前置,6.5.0a**pass 標 `level`、`finder.levelFor``data-jz-level`/選項
`level.text`;段落級 pass gate 跳過 text-level 元素。orphanhanging 依此確定適用範圍。
1. **垂懸字(方案 C)**:純結構無 layout、最獨立、風險最低 → 先做
2. **lineedge 重構為行端模式**:抽出偵測,現有半形=`squeeze`行為不變、回歸保綠)。
3. **懸掛 hang 模式**:加 `jz-hang-*` + 負 margin + 凸出量校準 + 自我應驗處理 + lang 閘
4. **邊界落地**:純標點末行本期 C 全包;記錄未來條件化路徑。
5. **文檔/docsdemo**resize-check 增懸掛、垂懸字檢查)
0. **功能分級(前置,6.5.0a**pass 標 `level`、`finder.levelFor`/`levelAllows`、
`data-jz-level`/選項 `level.text`;段落級 pass gate 跳過 text-level 元素。
1. **垂懸字(方案 C**standalone pass、order 70、純結構無 layout、預設開 chars=2。工作良好
2. **lineedge 重構為行端模式**:抽出偵測,squeeze 行為不變、回歸保綠;連帶修「行歸屬判定」
bug(垂直區間取代 top 差)。保留
3. ⛔ **懸掛 hang 模式**:實作後經 headless + 真實瀏覽器實測**確認不可行、全面停用**(原因見
6.5 開頭 + 6.5.6)。程式碼休眠 + 三重防逃逸
4. ✅ **邊界落地**:純標點末行由 orphan 之尾段綁定(含尾隨標點)全包消除。
5. ✅ **文檔/docsdemo**ARCHITECTUREREADMEdocs 分支標記懸掛停用;demoshowcase
resize-check)移除懸掛、保留功能分級與垂懸字。
#### 6.5.6 未決(實作時定)
#### 6.5.6 標點懸掛停用記錄與未來路徑
- 垂懸字預設開關(建議開 chars=2);懸掛 lang 預設(建議關)。
- 懸掛凸出量(headless 校準);懸掛自我應驗策略。
- jz-orphan jz-jinze 嵌套之邊界字定位。
- **停用結論**:純負 `margin` 機制無法視覺懸掛——(1) 句號墨色偏框左下,安全凸出量(< 1em)
只推出空白半框、墨色仍在版心內;(2) 凸出滿 1em 釋放一整字寬、拉升後字致重疊;(3) 與
inline-block 對齊原子互斥(justifyAtoms:true 下 margin justify 吸收);(4) ResizeObserver
重排致兩階段標記錯亂。四者皆實測確認。
- **三重防逃逸**(確保下游無法以任何方式啟用):① `index.ts` 鎖 `lineEnd: "squeeze"`(忽略
`hanging` 選項);② `lineedge.ts` `modeForChar` 無條件降 squeeze;③ `applyEdge` 之
`HANG_ENABLED = false` 總開關。三層任一即足,疊加為冗餘保險。`hanging` 選項仍被接受(API
相容)但無效果。單元測試覆蓋 optiondata-juzhenblocks 各路徑皆不產生 jz-hang-*。
- **未來路徑**:原生 CSS `hanging-punctuation``allow-end`/`first`/`last`)為正解,待 Chrome
支援(現僅 Safari),且須解決與 inline-block 原子之衝突(屆時或令行端標點局部退原子化)。
hang 之 lineedge 偵測碼、CSS`jz-hang-*`)、`LineEndMode` 型別皆保留休眠,重啟成本低。
### 6.6 標點修正(Biaodian normalize)— 後續(opt-in