From 89d1008755f30bcb7c17b24f7809b6f1bc73b0db Mon Sep 17 00:00:00 2001 From: commilitia Date: Tue, 9 Jun 2026 23:26:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=C2=A76.5=20=E5=8A=9F=E8=83=BD=E5=88=86?= =?UTF-8?q?=E7=B4=9A=EF=BC=8F=E5=9E=82=E6=87=B8=E5=AD=97=E8=90=BD=E5=9C=B0?= =?UTF-8?q?=EF=BC=9B=E6=A8=99=E9=BB=9E=E6=87=B8=E6=8E=9B=E5=81=9C=E7=94=A8?= =?UTF-8?q?=E8=A8=98=E9=8C=84=EF=BC=88ARCHITECTURE=EF=BC=8FREADME=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARCHITECTURE §6.5:功能分級、垂懸字(方案 C,預設開)標為已落地工作良好;標點懸掛標為 實作後停用,記錄四項不可行原因、三重防逃逸、未來原生 hanging-punctuation 路徑(§6.5.6)。 README:功能表加功能分級 level、垂懸字 orphan;標點懸掛列改為「已評估停用」說明;選項示例 與「標點懸掛」一節同步為停用。 --- ARCHITECTURE.md | 126 +++++++++++++++++++++++++++++++++--------------- README.md | 47 +++++++++++++++++- 2 files changed, 134 insertions(+), 39 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0046002..56c9d2b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 行端模式重構為 squeeze|hang|none(squeeze 為現有行端半形、保留並改善);**惟標點懸掛 +> 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,任意寬度/字型/ 瀏覽器成立。平時無害(末行本多字時不改變),僅末行將塌為 **停用(2026-06)**:本節原設計之懸掛經實測**不可行、已全面停用**(原因見 6.5 開頭與 6.5.6; +> 三重防逃逸見 6.5.6)。lineedge 之 squeeze 模式(行端半形)保留並改善。下文為原設計記錄, +> 供日後改用原生 `hanging-punctuation` 重啟時參考。 - **目標**:行末 bd-stop/pause/close **右凸**出右緣;行首 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 行為須不變(回歸測試保綠)。 -- **凸出量**:取決於標點 advance(halt 半形 0.5em/全形 1em/margin 盒 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 一致;須 Chrome/Safari/FF 目視(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` 降回 + squeeze,clreq);簡體可掛。`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 一致;須 Chrome/Safari/FF 目視(R1/R2)。headless(Chrome)已驗 + 行末標點凸出右緣、行首凸出左緣、繁體不掛、無中段誤觸。 #### 6.5.3 行端禁則(§6.4)review -- 現狀避頭尾已與懸掛協同(禁則趕標點到邊、懸掛凸出邊緣)。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(行端 squeeze;hang 已停用)→ 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 元素。orphan/hanging 依此確定適用範圍。 -1. **垂懸字(方案 C)**:純結構、無 layout、最獨立、風險最低 → 先做。 -2. **lineedge 重構為行端模式**:抽出偵測,現有半形=`squeeze`(行為不變、回歸保綠)。 -3. **懸掛 hang 模式**:加 `jz-hang-*` + 負 margin + 凸出量校準 + 自我應驗處理 + lang 閘。 -4. **邊界落地**:純標點末行本期 C 全包;記錄未來條件化路徑。 -5. **文檔/docs/demo**(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. ✅ **文檔/docs/demo**:ARCHITECTURE/README/docs 分支標記懸掛停用;demo(showcase/ + 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 + 相容)但無效果。單元測試覆蓋 option/data-juzhen/blocks 各路徑皆不產生 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) diff --git a/README.md b/README.md index dc79dbe..98a120f 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,12 @@ jz.render(document.querySelector("main")); | **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 ``,配合 CSS `hyphens:auto` 原生斷詞(DOM 無連字符 → 複製不污染)。 | | **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點**雙側獨立**收——各標點若其有空白之側緊鄰另一標點即收半,故 `,「` 全角式與開明式皆 1em、`。」` 全角式 1.5em/開明式 1em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 | | **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。 | +| **垂懸字避免** `orphan` | 段末行 CJK 實義字 ≥ N(預設 2;標點/符號不計)。方案 C(斷行層、無測量):把段末「第 N 個實義字起至段末」包入 `jz-orphan`(`white-space:nowrap`),整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型成立。單行段無害。**預設開**(`orphan:false` 關,`{chars:n}` 調)。段落級功能。 | +| ~~**標點懸掛** `hanging`~~ | **已評估停用**(§6.5.6):純負 margin 機制不可行(墨色不出版心/滿凸拉升後字重疊/resize 重排錯亂),原生 `hanging-punctuation` 為正解但 Chrome 不支援。`hanging` 選項仍被接受但**無任何效果**;程式碼休眠待原生支援成熟。 | +| **功能分級** `level` | 功能分文本級(中西間隙、標點擠壓——任意文本片段適用)與段落級(禁則、垂懸字、懸掛、長詞——需行/段布局)。塊級元素預設全功能;`data-jz-level="text"` 子樹或選項 `level:{text:"選擇器"}`(如標題/按鈕)僅跑文本級。 | | **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 | -後續(架構已預留,不返工):標點懸掛、標點修正(簡↔繁)、著重號、注音/Ruby。 +後續(架構已預留,不返工):標點修正(簡↔繁)、著重號、注音/Ruby。 --- @@ -92,6 +95,9 @@ createJuzhen({ longWord: { minLength: 6, lang: "en" }, jiya: true, // 或物件:{ halfWidth: "halt" | "margin", bias } jinze: true, + orphan: true, // 垂懸字避免(預設開);或 { chars: 2 };false 關 + // hanging:已停用、無效果(見「標點懸掛」一節) + level: { text: "h1,h2,h3,button,.ui" }, // 此選擇器子樹僅跑文本級功能(見下) justifyAtoms: true, // jz-char/jz-jinze 以 inline-block 作對齊原子; // 分頁器(Paged.js)下設 false 改 inline(見下) blocks: [ // 分塊功能選擇 @@ -119,6 +125,45 @@ createJuzhen({
``` +### 功能分級:文本級 vs 段落級(`level`/`data-jz-level`) + +功能分兩級: + +- **文本級**(中西間隙 `spacing`、標點擠壓 `jiya`):僅依字符鄰接,**任意文本片段**(單行 + 標題、按鈕、單一行內元素)皆適用且想要。 +- **段落級**(禁則 `jinze`、垂懸字 `orphan`、標點懸掛 `hanging`、長詞 `longWord`):依賴 + 行/段布局(換行、行端、段末),對單行/非段落文本無意義甚至有害。 + +塊級元素(`p`/`div`/`li`/`h1`…)**預設段落級(全功能)**。把標題/按鈕/UI 文字標為 +**文本級**(要間隙與標點半形、不要懸掛/避孤字/避頭尾): + +```html +

標題:只跑中西間隙與標點擠壓

+

+``` + +```ts +createJuzhen({ level: { text: "h1,h2,h3,button,.ui" } }); // 選擇器一鍵標記 +``` + +### 標點懸掛(`hanging`)— 已評估停用 + +原計劃之標點懸掛(行端標點凸出版心)經 headless + 真實瀏覽器實測**確認不可行、已全面停用**。 +`hanging` 選項仍被接受(API 相容)但**無任何效果**——縱使下游以 `hanging:true`/`data-juzhen`/ +`blocks` 等任何方式要求,亦不會啟用(行端恒 squeeze;三重防逃逸:`lineEnd` 鎖死、`modeForChar` +降級、`applyEdge` 總開關)。 + +**為何不可行**(純負 `margin` 機制): + +1. 句號等標點墨色偏字框左下角;安全凸出量(< 1 個 CJK 字寬 1em)只把空白半框推出版心、 + **墨色仍在版心內**(視覺不懸掛)。 +2. 凸出滿 1em 又**釋放一整字寬**、把下一字拉上本行、與懸掛標點**重疊**。 +3. `ResizeObserver` 重排時,「自然佈局標記→施加 margin→佈局再變」致標記**錯亂**。 + +真正的解是原生 CSS `hanging-punctuation`,惟 **Chrome 至今不支援**(僅 Safari 較全),且與本庫 +`inline-block` 對齊原子結構衝突。故 hang 之程式碼與 CSS **保留休眠**,俟原生支援成熟再議。 +詳見 `ARCHITECTURE.md §6.5.6`。 + --- ## 標點半形:`halt` 預設與 `margin` 後備