refactor: 移除標點懸掛(機制級死路)——負 margin 程式碼/CSS/型別一併刪
標點懸掛經實測為**機制級死路**(純 CSS 之寬度守恆矛盾):行端標點盒保持非零寬則墨色
出去多少版心內就空多少(行末實義字無法齊邊);收為零寬則釋放一整字寬、斷行器把下一字
拉上本行與標點重疊(且 justifyAtoms 預設之 inline-block 下負 margin 被 justify 吸收)。
CSS hack 接觸不到斷行器,此恒等式無解。唯一正解為原生 hanging-punctuation(僅 Safari
支援、且 jz-char 為 inline-block 原子非文本,原生懸掛不作用),屬遠期「原生管線」新功能、
與此處無共享,故不保留休眠碼。
移除:
lineedge.ts — jz-hang-* class、HANG_* 常量、applyEdge hang 分支、modeForChar、
HANG_ENABLED 總開關;行端僅餘 squeeze(半形)。
_jiya.css — jz-hang-* 三條規則(margin 半形 fallback 完整保留)。
types.ts — LineEndMode 型別、ResolvedOptions.lineEnd、features.hanging。
index.ts — lineEnd 鎖。
hanging 公開選項保留為 @deprecated no-op(API 相容)。
文檔:README「標點懸掛」節、ARCHITECTURE §6.5(開頭/§6.5.2/§6.5.4/§6.5.5/§6.5.6)、
demo(showcase §三/resize-check)全部由「停用/休眠」改為「已移除」+寬度守恆論證。
測試:防逃逸測試改寫為「hanging 為 no-op、輸出與不傳一致、各路徑不產生 jz-hang-*」。
全綠 101。dist 已無 jz-hang/lineEnd/HANG 殘留。
This commit is contained in:
+55
-78
@@ -511,25 +511,21 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
不應作用於標題/按鈕/單行文本。
|
不應作用於標題/按鈕/單行文本。
|
||||||
|
|
||||||
> **實作狀態(2026-06)**:6.5.0a 功能分級、6.5.1 垂懸字(方案 C,預設開)已落地且工作良好。
|
> **實作狀態(2026-06)**:6.5.0a 功能分級、6.5.1 垂懸字(方案 C,預設開)已落地且工作良好。
|
||||||
> 6.5.2 行端模式重構為 squeeze|hang|none(squeeze 為現有行端半形、保留並改善);**惟標點懸掛
|
> 6.5.2 行端僅做半形擠壓(squeeze,行首開類收左/行尾閉類收右);**標點懸掛 hang 經實測為機制級
|
||||||
> hang 經實測確認不可行、已全面停用**(仍保留休眠程式碼)。
|
> 死路、已移除**(負 margin 之 lineedge 程式碼、CSS、`lineEnd`/`LineEndMode`/`HANG_ENABLED`
|
||||||
|
> 等一併刪除;`hanging` 選項保留為 no-op)。
|
||||||
>
|
>
|
||||||
> **行歸屬判定 bug 修正**(連帶改善 squeeze,保留):原以「行盒 top 差 > EPS=4px」判同/異行,
|
> **行歸屬判定 bug 修正**(squeeze 之改善,保留):原以「行盒 top 差 > EPS=4px」判同/異行,
|
||||||
> 但 jz-char 為 inline-block,其行盒 top 與相鄰純文本字形 top 因基線差約 5px、遠小於行高卻超
|
> 但 jz-char 為 inline-block,其行盒 top 與相鄰純文本字形 top 因基線差約 5px、遠小於行高卻超
|
||||||
> 舊容差,致同行誤判為下一行。改判「垂直區間」:下一行=`b.top ≥ a.bottom`、上一行=
|
> 舊容差,致同行誤判為下一行。改判「垂直區間」:下一行=`b.top ≥ a.bottom`、上一行=
|
||||||
> `b.bottom ≤ a.top`(容 2px 亞像素)。
|
> `b.bottom ≤ a.top`(容 2px 亞像素)。
|
||||||
>
|
>
|
||||||
> **標點懸掛停用之原因**(純負 margin 機制不可行,headless + 真實瀏覽器實測):
|
> **標點懸掛移除之根因**(純 CSS 之寬度守恆矛盾,headless + 真實瀏覽器實測):行端標點盒若
|
||||||
> 1. 句號等標點墨色偏字框左下角;安全凸出量(< 1 個 CJK 字寬 1em)只把空白半框推出版心、
|
> 保持非零寬,墨色出去多少、版心內就空出多少(行末實義字無法齊版心右緣);若收為零寬,釋放
|
||||||
> **墨色仍在版心內**(視覺不懸掛)。
|
> 一整字寬、斷行器把下一字拉上本行與標點重疊(且 justifyAtoms 預設之 inline-block 下負 margin
|
||||||
> 2. 凸出滿 1em 又**釋放一整字寬**、把下一字拉上本行、與懸掛標點**重疊**。
|
> 被 justify 吸收)。**CSS hack 接觸不到斷行器,此恒等式無解**。唯一正解為原生
|
||||||
> 3. 懸掛與 inline-block 對齊原子**互斥**(justifyAtoms:true 下負 margin 被 justify 吸收)。
|
> `hanging-punctuation`(僅 Safari 支援、且 jz-char 為 inline-block 原子非文本,原生懸掛不作用),
|
||||||
> 4. `ResizeObserver` 重排時,「自然佈局標記→施加 margin→佈局再變」致標記**錯亂**。
|
> 屬遠期「原生管線」新功能、與已移除程式碼無共享。詳見 6.5.6。
|
||||||
>
|
|
||||||
> 真正的解是原生 CSS `hanging-punctuation`,惟 Chrome 不支援、且與本庫 inline-block 原子衝突。
|
|
||||||
> 故 hang 之 lineedge 程式碼與 CSS 保留休眠,並設**三重防逃逸**(`lineEnd` 鎖死 squeeze、
|
|
||||||
> `modeForChar` 降級、`applyEdge` 之 `HANG_ENABLED=false` 總開關)——縱使下游以任何方式要求亦
|
|
||||||
> 不啟用。詳見 6.5.6。
|
|
||||||
|
|
||||||
#### 6.5.0a 功能分級:文本級 vs 段落級 + 元素標記(前置基礎設施)
|
#### 6.5.0a 功能分級:文本級 vs 段落級 + 元素標記(前置基礎設施)
|
||||||
|
|
||||||
@@ -573,19 +569,17 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 禁則(§6.4 jinze) | **斷行層** | 把標點趕到正確的邊(點號/閉類→行末、開類→行首;連續標點不拆) | nowrap `jz-jinze` | 已實作 |
|
| 禁則(§6.4 jinze) | **斷行層** | 把標點趕到正確的邊(點號/閉類→行末、開類→行首;連續標點不拆) | nowrap `jz-jinze` | 已實作 |
|
||||||
| 行端半形(lineedge 現有) | **渲染層·紧排** | 行首開類/行末閉類**收半形** | `jz-half-le`(halt) | 已實作(將併入行端模式) |
|
| 行端半形(lineedge 現有) | **渲染層·紧排** | 行首開類/行末閉類**收半形** | `jz-half-le`(halt) | 已實作(將併入行端模式) |
|
||||||
| 標點懸掛 | **渲染層·凸出** | 行末點號/閉類**右凸**、行首開類**左凸**出版心 | 負 `margin` | ⛔ 實作後停用(見 6.5.6) |
|
| 標點懸掛 | **渲染層·凸出** | 行末點號/閉類**右凸**、行首開類**左凸**出版心 | ~~負 `margin`~~ | ❌ 已移除(機制級死路,見 6.5.6) |
|
||||||
| 垂懸字避免(本計劃) | **斷行層** | 段末行 **CJK 實義字數 ≥ N** | nowrap `jz-orphan` | 待實作 |
|
| 垂懸字避免 | **斷行層** | 段末行 **CJK 實義字數 ≥ N** | nowrap `jz-orphan` | 已實作 |
|
||||||
|
|
||||||
- **正交性**:斷行層(禁則/垂懸字,管「哪些字符在哪行、不可斷」)與渲染層(半形/懸掛,管
|
- **正交性**:斷行層(禁則/垂懸字,管「哪些字符在哪行、不可斷」)與渲染層(半形/懸掛,管
|
||||||
「邊緣標點凸出/收縮」)作用於不同 CSS 維度(`white-space` vs `margin`),可疊加共存。
|
「邊緣標點凸出/收縮」)作用於不同 CSS 維度(`white-space` vs `margin`),可疊加共存。
|
||||||
- **協同**:禁則決定標點落行首或行末 → 半形/懸掛對落在邊緣者施收縮/凸出。
|
- **協同**:禁則決定標點落行首或行末 → 半形/懸掛對落在邊緣者施收縮/凸出。
|
||||||
- **行端「半形 vs 懸掛」互斥**(同屬渲染層):將 lineedge 行端處理升級為可配模式
|
- **行端僅半形擠壓**:lineedge 行端處理只做 squeeze(行首開類收左、行尾閉類收右半形)。
|
||||||
`lineEnd: "squeeze"(現有半形)|"hang"(懸掛)|"none"`,二者共用行端標點偵測碼。
|
原計劃之「懸掛」模式已移除(機制級死路,§6.5.6),故無「半形 vs 懸掛」之模式切換。
|
||||||
- **垂懸字 vs 懸掛邊界**(已研究):垂懸字限「末行實義字 ≥ N」、懸掛限「邊緣標點凸出」。唯一
|
- **垂懸字 vs 懸掛邊界**(歷史記錄):曾研究兩者於「純標點末行」之重疊(懸掛吸回 vs 垂懸字
|
||||||
重疊=**純標點末行**(實義字=0):懸掛可「吸回」(不動正文)、垂懸字綁定可「拉字下來」。
|
拉字)。懸掛移除後此協同不再適用;方案 C(無測量)**附帶**消除純標點末行即可(副作用,
|
||||||
約定:方案 C(無測量)**附帶**消除純標點末行(副作用,現有益無害);未來若改由懸掛吸回
|
現有益無害)。
|
||||||
(更優),把 C 綁定**條件化**(僅末行實義字 ∈ [1,N−1] 才綁),借懸掛 layout pass 之測量協同
|
|
||||||
判定,C 不獨立測。末行實義字 ∈ [1,N−1] 僅垂懸字(換行)可解(懸掛不增實義字)。
|
|
||||||
|
|
||||||
#### 6.5.1 垂懸字避免(方案 C,斷行層,無測量)— 已實作
|
#### 6.5.1 垂懸字避免(方案 C,斷行層,無測量)— 已實作
|
||||||
|
|
||||||
@@ -605,44 +599,16 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
- **測試**:jsdom 結構(末 N 實義字+尾標點入 jz-orphan;符號不計;單行段/實義字<N 不綁);
|
- **測試**:jsdom 結構(末 N 實義字+尾標點入 jz-orphan;符號不計;單行段/實義字<N 不綁);
|
||||||
headless 窄容器拖動驗末行恒 ≥N。
|
headless 窄容器拖動驗末行恒 ≥N。
|
||||||
|
|
||||||
#### 6.5.2 標點懸掛(Hanging,渲染層,layout)— ⚠ 已實作後停用
|
#### 6.5.2 標點懸掛(Hanging)— ❌ 已移除(機制級死路)
|
||||||
|
|
||||||
> **停用(2026-06)**:本節原設計之懸掛經實測**不可行、已全面停用**(原因見 6.5 開頭與 6.5.6;
|
> **已移除(2026-06)**:原計劃之懸掛(行端標點負 margin 凸出版心)經 headless + 真實瀏覽器
|
||||||
> 三重防逃逸見 6.5.6)。lineedge 之 squeeze 模式(行端半形)保留並改善。下文為原設計記錄,
|
> 實測確認為**機制級死路**(寬度守恆矛盾,純 CSS 接觸不到斷行器),程式碼與 CSS 已刪。完整
|
||||||
> 供日後改用原生 `hanging-punctuation` 重啟時參考。
|
> 論證、已移除清單、未來原生路徑見 **§6.5.6**。`hanging` 選項保留為 no-op(API 相容)。
|
||||||
|
|
||||||
- **目標**:行末 bd-stop/pause/close **右凸**出右緣;行首 bd-open **左凸**出左緣。文字邊緣齊。
|
lineedge 行端處理只餘 **squeeze**(行端半形):量測各 jz-char 是否在行首(開類)/行末(閉類),
|
||||||
- **不用 CSS `hanging-punctuation`**:瀏覽器支援差(僅 Safari 較全)+聚珍「原子透明」已對 jz-*
|
加 `jz-half-le`(CSS 同 halt)。兩階段免自我應驗(先 `clearEdge`、自然佈局量測全部、再一次套用);
|
||||||
`hanging-punctuation: none`。故**自管負 margin**。
|
行歸屬以「垂直區間」`lowerLine`/`higherLine` 判定(吸收 inline-block 行盒與純文本基線差 ~5px)。
|
||||||
- **機制**:layout pass(複用 lineedge `installLayout`:立即跑+`ResizeObserver(root)`+
|
order `90 lineEdge`,`revert` 清 `jz-half-le`。純版面、jsdom 跳過、須瀏覽器目視。
|
||||||
`fonts.ready` 重算)。量測各標點 jz-char 是否在行首/行末(同 lineedge `edgeRect`/`firstRect`,
|
|
||||||
跨 jz-* 包裹攀爬)。行末點號/閉類加 `jz-hang-r`→`margin-right: -<凸出量>`;行首開類加
|
|
||||||
`jz-hang-l`→`margin-left: -<凸出量>`。
|
|
||||||
- **與現有行端半形整合**(已實作):抽出「行端標點偵測」共用,按 `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
|
#### 6.5.3 行端禁則(§6.4)review
|
||||||
|
|
||||||
@@ -653,34 +619,45 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
#### 6.5.4 統一 pass order
|
#### 6.5.4 統一 pass order
|
||||||
|
|
||||||
`20 charify → 25 jiyaAdj → 40 jinze → 50 spacing → 60 longWord → 70 orphan(垂懸字)→
|
`20 charify → 25 jiyaAdj → 40 jinze → 50 spacing → 60 longWord → 70 orphan(垂懸字)→
|
||||||
90 lineEdge(行端 squeeze;hang 已停用)→ 92 gapTrim`。
|
90 lineEdge(行端 squeeze 半形)→ 92 gapTrim`。
|
||||||
|
|
||||||
#### 6.5.5 實作順序與結果
|
#### 6.5.5 實作順序與結果
|
||||||
|
|
||||||
0. ✅ **功能分級(前置,6.5.0a)**:pass 標 `level`、`finder.levelFor`/`levelAllows`、
|
0. ✅ **功能分級(前置,6.5.0a)**:pass 標 `level`、`finder.levelFor`/`levelAllows`、
|
||||||
`data-jz-level`/選項 `level.text`;段落級 pass gate 跳過 text-level 元素。
|
`data-jz-level`/選項 `level.text`;段落級 pass gate 跳過 text-level 元素。
|
||||||
1. ✅ **垂懸字(方案 C)**:standalone pass、order 70、純結構無 layout、預設開 chars=2。工作良好。
|
1. ✅ **垂懸字(方案 C)**:standalone pass、order 70、純結構無 layout、預設開 chars=2。工作良好。
|
||||||
2. ✅ **lineedge 重構為行端模式**:抽出偵測,squeeze 行為不變、回歸保綠;連帶修「行歸屬判定」
|
2. ✅ **lineedge 行端半形(squeeze)**:量測行首開類/行末閉類加 `jz-half-le`;連帶修「行歸屬
|
||||||
bug(垂直區間取代 top 差)。保留。
|
判定」bug(垂直區間取代 top 差)。保留。
|
||||||
3. ⛔ **懸掛 hang 模式**:實作後經 headless + 真實瀏覽器實測**確認不可行、全面停用**(原因見
|
3. ❌ **懸掛 hang**:實作後經 headless + 真實瀏覽器實測確認為**機制級死路、已移除**(負 margin
|
||||||
6.5 開頭 + 6.5.6)。程式碼休眠 + 三重防逃逸。
|
程式碼/CSS/`LineEndMode`/`lineEnd`/`features.hanging` 一併刪;`hanging` 選項保留 no-op)。
|
||||||
|
論證見 6.5.6。
|
||||||
4. ✅ **邊界落地**:純標點末行由 orphan 之尾段綁定(含尾隨標點)全包消除。
|
4. ✅ **邊界落地**:純標點末行由 orphan 之尾段綁定(含尾隨標點)全包消除。
|
||||||
5. ✅ **文檔/docs/demo**:ARCHITECTURE/README/docs 分支標記懸掛停用;demo(showcase/
|
5. ✅ **行內透明性修復(下游 Bug)**:pill 前標點誤隙(finder 透明)、行內元素整體掉行
|
||||||
resize-check)移除懸掛、保留功能分級與垂懸字。
|
(core/split 克隆切分)、行端攀爬集派生(T4)等,見 §4.1/§6.4。
|
||||||
|
6. ✅ **文檔/docs/demo**:ARCHITECTURE/README/docs 分支記懸掛移除;demo 同步。
|
||||||
|
|
||||||
#### 6.5.6 標點懸掛停用記錄與未來路徑
|
#### 6.5.6 標點懸掛移除記錄與未來路徑
|
||||||
|
|
||||||
- **停用結論**:純負 `margin` 機制無法視覺懸掛——(1) 句號墨色偏框左下,安全凸出量(< 1em)
|
- **移除結論(寬度守恆論證)**:純 CSS 手段(負 margin/相對位移/零寬盒)存在守恆矛盾,
|
||||||
只推出空白半框、墨色仍在版心內;(2) 凸出滿 1em 釋放一整字寬、拉升後字致重疊;(3) 與
|
機制級不可行。設行末標點墨色須落版心外、且行末實義字須齊版心右緣:
|
||||||
inline-block 對齊原子互斥(justifyAtoms:true 下 margin 被 justify 吸收);(4) ResizeObserver
|
- 行末盒**保持非零寬**(負 margin 收一部分、或 `position:relative` 純視覺外移)→ 墨色可移出,
|
||||||
重排致兩階段標記錯亂。四者皆實測確認。
|
但盒仍佔行末寬 → 版心內留等寬空缺,「行末齊邊」不成立(0.5em 實測之「看不出懸掛」即此相)。
|
||||||
- **三重防逃逸**(確保下游無法以任何方式啟用):① `index.ts` 鎖 `lineEnd: "squeeze"`(忽略
|
- 行末盒**收為零寬**(margin-right:-1em 等)→ 釋放一整字寬 → 斷行器把下一字拉上本行與標點
|
||||||
`hanging` 選項);② `lineedge.ts` `modeForChar` 無條件降 squeeze;③ `applyEdge` 之
|
重疊(headless 實測每標點疊一字);且 `justifyAtoms:true` 之 inline-block 下負 margin 被
|
||||||
`HANG_ENABLED = false` 總開關。三層任一即足,疊加為冗餘保險。`hanging` 選項仍被接受(API
|
justify 吸收。
|
||||||
相容)但無效果。單元測試覆蓋 option/data-juzhen/blocks 各路徑皆不產生 jz-hang-*。
|
|
||||||
- **未來路徑**:原生 CSS `hanging-punctuation`(`allow-end`/`first`/`last`)為正解,待 Chrome
|
**墨色出去多少、版心內就空出多少;想不空就得讓斷行器重排這塊寬度,而 CSS hack 接觸不到
|
||||||
支援(現僅 Safari),且須解決與 inline-block 原子之衝突(屆時或令行端標點局部退原子化)。
|
斷行器。** 補償墊片、兩階段試錯都改變不了此恒等式,只會重蹈 ResizeObserver 標記錯亂之覆轍。
|
||||||
hang 之 lineedge 偵測碼、CSS(`jz-hang-*`)、`LineEndMode` 型別皆保留休眠,重啟成本低。
|
- **已移除之物**:`lineedge.ts` 之 `jz-hang-*` class/`HANG_*` 常量/`applyEdge` hang 分支/
|
||||||
|
`modeForChar`/`HANG_ENABLED`;`_jiya.css` 之 `jz-hang-*` 規則;`types.ts` 之 `LineEndMode`/
|
||||||
|
`ResolvedOptions.lineEnd`/`features.hanging`;`index.ts` 之 `lineEnd` 鎖。行端僅餘半形擠壓
|
||||||
|
(`jz-half-le`)。`hanging` 公開選項保留為 **@deprecated no-op**(API 相容;測試覆蓋
|
||||||
|
option/data-juzhen/blocks 各路徑皆不產生 jz-hang-* 且輸出與不傳一致)。
|
||||||
|
- **未來路徑(與已移除碼無共享)**:原生 CSS `hanging-punctuation` 為唯一正解,惟 Chrome/
|
||||||
|
Firefox 至今不支援(僅 Safari);且本庫 `jz-char` 為 inline-block 原子、非文本,原生懸掛
|
||||||
|
不作用之。未來若做,須是**繞開 jz-char 的「原生管線」新功能**——`text-spacing-trim` 接管
|
||||||
|
擠壓 + `hanging-punctuation` 接管懸掛,按 `CSS.supports` 能力門控,整體退出 jz-char 機制。
|
||||||
|
與已移除之負 margin 程式碼**沒有一行可複用**,故不保留休眠碼。
|
||||||
|
|
||||||
### 6.6 標點修正(Biaodian normalize)— 後續(opt-in)
|
### 6.6 標點修正(Biaodian normalize)— 後續(opt-in)
|
||||||
|
|
||||||
|
|||||||
@@ -146,23 +146,22 @@ createJuzhen({
|
|||||||
createJuzhen({ level: { text: "h1,h2,h3,button,.ui" } }); // 選擇器一鍵標記
|
createJuzhen({ level: { text: "h1,h2,h3,button,.ui" } }); // 選擇器一鍵標記
|
||||||
```
|
```
|
||||||
|
|
||||||
### 標點懸掛(`hanging`)— 已評估停用
|
### 標點懸掛(`hanging`)— 已移除
|
||||||
|
|
||||||
原計劃之標點懸掛(行端標點凸出版心)經 headless + 真實瀏覽器實測**確認不可行、已全面停用**。
|
原計劃之標點懸掛(行端標點凸出版心)經實測為**機制級死路、已移除**(負 margin 程式碼與
|
||||||
`hanging` 選項仍被接受(API 相容)但**無任何效果**——縱使下游以 `hanging:true`/`data-juzhen`/
|
CSS 皆刪)。`hanging` 選項仍被接受(API 相容)但**無任何效果**——`hanging:true`/
|
||||||
`blocks` 等任何方式要求,亦不會啟用(行端恒 squeeze;三重防逃逸:`lineEnd` 鎖死、`modeForChar`
|
`data-juzhen`/`blocks` 任一要求皆被靜默忽略,行端僅做半形擠壓。
|
||||||
降級、`applyEdge` 總開關)。
|
|
||||||
|
|
||||||
**為何不可行**(純負 `margin` 機制):
|
**為何機制級不可行**(純 CSS 之寬度守恆矛盾):行端標點盒若保持非零寬,則「墨色出去多少、
|
||||||
|
版心內就空出多少」(行末實義字無法齊版心右緣);若收為零寬,則釋放一整字寬、斷行器把下一字
|
||||||
|
拉上本行與標點重疊(且 `justifyAtoms` 預設之 inline-block 下負 margin 被 justify 吸收)。
|
||||||
|
**墨色出去多少版心內就空多少;想不空就得讓斷行器重排這塊寬度,而 CSS hack 接觸不到斷行器**。
|
||||||
|
補償墊片、兩階段試錯都改變不了此恒等式。
|
||||||
|
|
||||||
1. 句號等標點墨色偏字框左下角;安全凸出量(< 1 個 CJK 字寬 1em)只把空白半框推出版心、
|
唯一正解為原生 CSS `hanging-punctuation`,惟 **Chrome/Firefox 至今不支援**(僅 Safari),且
|
||||||
**墨色仍在版心內**(視覺不懸掛)。
|
本庫之 `jz-char` 為 inline-block 原子、非文本,原生懸掛不作用——未來若做,須是繞開 `jz-char`
|
||||||
2. 凸出滿 1em 又**釋放一整字寬**、把下一字拉上本行、與懸掛標點**重疊**。
|
的「原生管線」新功能(`text-spacing-trim` + `hanging-punctuation`,按能力門控),與已移除之
|
||||||
3. `ResizeObserver` 重排時,「自然佈局標記→施加 margin→佈局再變」致標記**錯亂**。
|
程式碼無共享。詳見 `ARCHITECTURE.md §6.5.6`。
|
||||||
|
|
||||||
真正的解是原生 CSS `hanging-punctuation`,惟 **Chrome 至今不支援**(僅 Safari 較全),且與本庫
|
|
||||||
`inline-block` 對齊原子結構衝突。故 hang 之程式碼與 CSS **保留休眠**,俟原生支援成熟再議。
|
|
||||||
詳見 `ARCHITECTURE.md §6.5.6`。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
<script src="../dist/juzhen.iife.js"></script>
|
<script src="../dist/juzhen.iife.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 全頁一次渲染(quanjiao 全角式,與下游回饋之政策一致);各框由其 lang 屬性
|
// 全頁一次渲染(quanjiao 全角式,與下游回饋之政策一致);各框由其 lang 屬性
|
||||||
// 驅動簡/繁字形。(標點懸掛已停用,見 ARCHITECTURE §6.5.6。)
|
// 驅動簡/繁字形。(標點懸掛已移除,見 ARCHITECTURE §6.5.6。)
|
||||||
Juzhen.createJuzhen({ scope: { include: ".juzhen" }, lang: { style: "quanjiao" } })
|
Juzhen.createJuzhen({ scope: { include: ".juzhen" }, lang: { style: "quanjiao" } })
|
||||||
.render(document.body);
|
.render(document.body);
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -44,7 +44,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>聚珍 — 新功能效果演示(定寬對照)</h1>
|
<h1>聚珍 — 新功能效果演示(定寬對照)</h1>
|
||||||
<p class="lede">本頁以<strong>定寬文本框</strong>(不可拖動)並排對照,逐場景展示兩項新功能:<strong>功能分級</strong>(文本級/段落級)、<strong>垂懸字避免</strong>(<code>orphan</code>)。各框寬度經 headless 校準,配 Noto Sans CJK 可穩定重現所述效果。勾選下方開關可描出聚珍注入之結構(紫=垂懸字綁定、紅=中西間隙、藍=半形標點)。第三項曾規劃之「標點懸掛」經實測停用,說明見文末。</p>
|
<p class="lede">本頁以<strong>定寬文本框</strong>(不可拖動)並排對照,逐場景展示兩項新功能:<strong>功能分級</strong>(文本級/段落級)、<strong>垂懸字避免</strong>(<code>orphan</code>)。各框寬度經 headless 校準,配 Noto Sans CJK 可穩定重現所述效果。勾選下方開關可描出聚珍注入之結構(紫=垂懸字綁定、紅=中西間隙、藍=半形標點)。第三項曾規劃之「標點懸掛」經實測為機制級死路、已移除,說明見文末。</p>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<label><input type="checkbox" id="t-orphan"> 顯示垂懸字綁定(紫 <code>jz-orphan</code>)</label>
|
<label><input type="checkbox" id="t-orphan"> 顯示垂懸字綁定(紫 <code>jz-orphan</code>)</label>
|
||||||
@@ -110,9 +110,9 @@
|
|||||||
<p class="hint">邊界:段落實義字 < N(或單行段)→ 不綁、無副作用。極窄容器(<N 字寬)→ 該尾段可能溢出(罕見)。</p>
|
<p class="hint">邊界:段落實義字 < N(或單行段)→ 不綁、無副作用。極窄容器(<N 字寬)→ 該尾段可能溢出(罕見)。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===================== 三、標點懸掛(已停用) ===================== -->
|
<!-- ===================== 三、標點懸掛(已移除) ===================== -->
|
||||||
<h2>三、標點懸掛(<code>hanging</code>)— 已評估停用</h2>
|
<h2>三、標點懸掛(<code>hanging</code>)— 已移除</h2>
|
||||||
<p class="note">原計劃之標點懸掛(行端標點凸出版心)經 headless + 真實瀏覽器實測<strong>確認不可行</strong>,已<strong>全面停用</strong>(<code>hanging</code> 選項仍被接受但無任何效果)。原因:純負 <code>margin</code> 機制下,句號墨色偏字框左下角,安全凸出量(< 1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);凸出滿 1em 又釋放一整字寬、把下一字拉上本行致重疊;且 <code>ResizeObserver</code> 重排時標記錯亂。真正的解是原生 CSS <code>hanging-punctuation</code>,惟 Chrome 至今不支援、且與本庫 inline-block 對齊原子結構衝突。程式碼與 CSS 保留休眠,俟原生支援成熟再議。詳見 <code>ARCHITECTURE.md §6.5.6</code>。</p>
|
<p class="note">原計劃之標點懸掛(行端標點凸出版心)經 headless + 真實瀏覽器實測確認為<strong>機制級死路</strong>,已<strong>移除</strong>(負 <code>margin</code> 程式碼與 CSS 皆刪;<code>hanging</code> 選項保留為 no-op,無任何效果)。<strong>寬度守恆矛盾</strong>:行端標點盒若保持非零寬,墨色出去多少、版心內就空出多少(行末實義字無法齊版心右緣);若收為零寬,釋放一整字寬、斷行器把下一字拉上本行與標點重疊(且 <code>justifyAtoms</code> 預設之 inline-block 下負 margin 被 justify 吸收)。<strong>CSS hack 接觸不到斷行器,此恒等式無解。</strong>唯一正解為原生 CSS <code>hanging-punctuation</code>,惟 Chrome/Firefox 至今不支援(僅 Safari),且本庫 <code>jz-char</code> 為 inline-block 原子非文本、原生懸掛不作用之——未來若做須是繞開 jz-char 的「原生管線」新功能。詳見 <code>ARCHITECTURE.md §6.5.6</code>。</p>
|
||||||
|
|
||||||
<footer style="margin-top:2.4rem;color:#888;font-size:.78rem;border-top:1px solid #eee;padding-top:.6rem">
|
<footer style="margin-top:2.4rem;color:#888;font-size:.78rem;border-top:1px solid #eee;padding-top:.6rem">
|
||||||
聚珍(Juzhen)· 新功能效果演示。另見 <a href="./resize-check.html">resize-check.html</a>(可拖動容器之穩定性檢查)。寬度配 Noto Sans CJK 校準;換字型時斷行點略異,效果方向不變。
|
聚珍(Juzhen)· 新功能效果演示。另見 <a href="./resize-check.html">resize-check.html</a>(可拖動容器之穩定性檢查)。寬度配 Noto Sans CJK 校準;換字型時斷行點略異,效果方向不變。
|
||||||
|
|||||||
Vendored
-10
@@ -85,16 +85,6 @@ jz-char.jz-half-le jz-inner {
|
|||||||
[data-jz-halfwidth=halt] jz-char.jz-half-le {
|
[data-jz-halfwidth=halt] jz-char.jz-half-le {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
jz-char.bd-stop.jz-hang-r,
|
|
||||||
jz-char.bd-pause.jz-hang-r {
|
|
||||||
margin-right: calc(-1 * var(--jz-hang-stop, 0.5em));
|
|
||||||
}
|
|
||||||
jz-char.bd-close.jz-hang-r {
|
|
||||||
margin-right: calc(-1 * var(--jz-hang-bracket, 0.5em));
|
|
||||||
}
|
|
||||||
jz-char.bd-open.jz-hang-l {
|
|
||||||
margin-left: calc(-1 * var(--jz-hang-bracket, 0.5em));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* src/css/_jinze.css */
|
/* src/css/_jinze.css */
|
||||||
jz-jinze {
|
jz-jinze {
|
||||||
|
|||||||
Vendored
+9
-43
@@ -1160,13 +1160,10 @@ var Juzhen = (() => {
|
|||||||
|
|
||||||
// src/typeset/lineedge.ts
|
// src/typeset/lineedge.ts
|
||||||
var LE = "jz-half-le";
|
var LE = "jz-half-le";
|
||||||
var HANG_L = "jz-hang-l";
|
var ALL_EDGE = [LE];
|
||||||
var HANG_R = "jz-hang-r";
|
|
||||||
var ALL_EDGE = [LE, HANG_L, HANG_R];
|
|
||||||
var EPS = 2;
|
var EPS = 2;
|
||||||
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
|
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
|
||||||
var TAIL_SQUEEZE = /* @__PURE__ */ new Set(["bd-close"]);
|
var TAIL_CLASSES = /* @__PURE__ */ new Set(["bd-close"]);
|
||||||
var TAIL_HANG = /* @__PURE__ */ new Set(["bd-close", "bd-stop", "bd-pause"]);
|
|
||||||
var observers = /* @__PURE__ */ new WeakMap();
|
var observers = /* @__PURE__ */ new WeakMap();
|
||||||
function hasLayout() {
|
function hasLayout() {
|
||||||
return typeof ResizeObserver !== "undefined";
|
return typeof ResizeObserver !== "undefined";
|
||||||
@@ -1267,28 +1264,11 @@ var Juzhen = (() => {
|
|||||||
const rects = el.getClientRects();
|
const rects = el.getClientRects();
|
||||||
return rects.length ? rects[rects.length - 1] : null;
|
return rects.length ? rects[rects.length - 1] : null;
|
||||||
}
|
}
|
||||||
var HANG_ENABLED = false;
|
|
||||||
function applyEdge(el, edge, mode) {
|
|
||||||
if (mode === "squeeze") {
|
|
||||||
el.classList.add(LE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === "hang" && HANG_ENABLED) {
|
|
||||||
el.classList.add(edge === "head" ? HANG_L : HANG_R);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function clearEdge(root) {
|
function clearEdge(root) {
|
||||||
for (const cls of ALL_EDGE) {
|
for (const cls of ALL_EDGE) {
|
||||||
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
|
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function modeForChar(el, options) {
|
|
||||||
const base = options.lineEnd;
|
|
||||||
if (base !== "hang") {
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
return "squeeze";
|
|
||||||
}
|
|
||||||
function relayout(root, finder, options) {
|
function relayout(root, finder, options) {
|
||||||
clearEdge(root);
|
clearEdge(root);
|
||||||
const wrapSet = buildWrapSet(options);
|
const wrapSet = buildWrapSet(options);
|
||||||
@@ -1303,20 +1283,12 @@ var Juzhen = (() => {
|
|||||||
if (!finder.featureEnabledFor(el, "jiya")) {
|
if (!finder.featureEnabledFor(el, "jiya")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mode = modeForChar(el, options);
|
|
||||||
if (mode === "none") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (mode === "hang" && el.classList.contains("jz-half")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const bd = bdClassOf(el);
|
const bd = bdClassOf(el);
|
||||||
if (!bd) {
|
if (!bd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const tailSet = mode === "hang" ? TAIL_HANG : TAIL_SQUEEZE;
|
|
||||||
const isHead = HEAD_CLASSES.has(bd);
|
const isHead = HEAD_CLASSES.has(bd);
|
||||||
const isTail = tailSet.has(bd);
|
const isTail = TAIL_CLASSES.has(bd);
|
||||||
if (!isHead && !isTail) {
|
if (!isHead && !isTail) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1327,17 +1299,17 @@ var Juzhen = (() => {
|
|||||||
if (isHead) {
|
if (isHead) {
|
||||||
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
||||||
if (!prev || higherLine(r, prev)) {
|
if (!prev || higherLine(r, prev)) {
|
||||||
decisions.push({ el, edge: "head", mode });
|
decisions.push({ el });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
||||||
if (!next || lowerLine(r, next)) {
|
if (!next || lowerLine(r, next)) {
|
||||||
decisions.push({ el, edge: "tail", mode });
|
decisions.push({ el });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const d of decisions) {
|
for (const d of decisions) {
|
||||||
applyEdge(d.el, d.edge, d.mode);
|
d.el.classList.add(LE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var lineEdgePass = {
|
var lineEdgePass = {
|
||||||
@@ -2002,21 +1974,15 @@ var Juzhen = (() => {
|
|||||||
jiya: feature(opts.jiya, true),
|
jiya: feature(opts.jiya, true),
|
||||||
jinze: feature(opts.jinze, true),
|
jinze: feature(opts.jinze, true),
|
||||||
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
|
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
|
||||||
hanging: feature(opts.hanging, false),
|
|
||||||
biaodian: feature(opts.biaodian, false),
|
biaodian: feature(opts.biaodian, false),
|
||||||
emphasis: feature(opts.emphasis, false),
|
emphasis: feature(opts.emphasis, false),
|
||||||
ruby: feature(opts.ruby, false)
|
ruby: feature(opts.ruby, false)
|
||||||
},
|
},
|
||||||
longWord: resolveLongWord(opts.longWord),
|
longWord: resolveLongWord(opts.longWord),
|
||||||
orphan: resolveOrphan(opts.orphan),
|
orphan: resolveOrphan(opts.orphan),
|
||||||
// 行端模式恒為 squeeze(行端半形)。**標點懸掛 hang 已全面停用**(§6.5.2/§6.5.6):
|
// 行端僅做半形擠壓(lineedge:行首開類收左、行尾閉類收右)。**標點懸掛已移除**
|
||||||
// 經 headless + 真實瀏覽器實測,純負 margin 機制不可行——句號墨色偏框左下角,安全
|
// (§6.5.6):純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);
|
||||||
// 凸出量(<1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);凸出滿 1em 又會釋放
|
// opts.hanging 仍被接受以保 API 相容,但無任何效果。
|
||||||
// 一字寬、把下一字拉上本行致重疊;且 ResizeObserver 重排時兩階段標記錯亂。原生
|
|
||||||
// hanging-punctuation 為正解但 Chrome 不支援、且與本庫 inline-block 原子衝突。
|
|
||||||
// 故此處**唯一關卡**鎖死為 squeeze——即使 opts.hanging 為 true 亦不啟用 hang(無逃逸);
|
|
||||||
// hang 之 CSS/lineedge 程式碼保留(休眠),俟原生支援成熟再議。
|
|
||||||
lineEnd: "squeeze",
|
|
||||||
jiyaConfig: resolveJiya(opts.jiya),
|
jiyaConfig: resolveJiya(opts.jiya),
|
||||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||||
finder: {
|
finder: {
|
||||||
|
|||||||
Vendored
+9
-43
@@ -1132,13 +1132,10 @@ var jinzePass = {
|
|||||||
|
|
||||||
// src/typeset/lineedge.ts
|
// src/typeset/lineedge.ts
|
||||||
var LE = "jz-half-le";
|
var LE = "jz-half-le";
|
||||||
var HANG_L = "jz-hang-l";
|
var ALL_EDGE = [LE];
|
||||||
var HANG_R = "jz-hang-r";
|
|
||||||
var ALL_EDGE = [LE, HANG_L, HANG_R];
|
|
||||||
var EPS = 2;
|
var EPS = 2;
|
||||||
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
|
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
|
||||||
var TAIL_SQUEEZE = /* @__PURE__ */ new Set(["bd-close"]);
|
var TAIL_CLASSES = /* @__PURE__ */ new Set(["bd-close"]);
|
||||||
var TAIL_HANG = /* @__PURE__ */ new Set(["bd-close", "bd-stop", "bd-pause"]);
|
|
||||||
var observers = /* @__PURE__ */ new WeakMap();
|
var observers = /* @__PURE__ */ new WeakMap();
|
||||||
function hasLayout() {
|
function hasLayout() {
|
||||||
return typeof ResizeObserver !== "undefined";
|
return typeof ResizeObserver !== "undefined";
|
||||||
@@ -1239,28 +1236,11 @@ function lastRect(el) {
|
|||||||
const rects = el.getClientRects();
|
const rects = el.getClientRects();
|
||||||
return rects.length ? rects[rects.length - 1] : null;
|
return rects.length ? rects[rects.length - 1] : null;
|
||||||
}
|
}
|
||||||
var HANG_ENABLED = false;
|
|
||||||
function applyEdge(el, edge, mode) {
|
|
||||||
if (mode === "squeeze") {
|
|
||||||
el.classList.add(LE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === "hang" && HANG_ENABLED) {
|
|
||||||
el.classList.add(edge === "head" ? HANG_L : HANG_R);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function clearEdge(root) {
|
function clearEdge(root) {
|
||||||
for (const cls of ALL_EDGE) {
|
for (const cls of ALL_EDGE) {
|
||||||
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
|
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function modeForChar(el, options) {
|
|
||||||
const base = options.lineEnd;
|
|
||||||
if (base !== "hang") {
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
return "squeeze";
|
|
||||||
}
|
|
||||||
function relayout(root, finder, options) {
|
function relayout(root, finder, options) {
|
||||||
clearEdge(root);
|
clearEdge(root);
|
||||||
const wrapSet = buildWrapSet(options);
|
const wrapSet = buildWrapSet(options);
|
||||||
@@ -1275,20 +1255,12 @@ function relayout(root, finder, options) {
|
|||||||
if (!finder.featureEnabledFor(el, "jiya")) {
|
if (!finder.featureEnabledFor(el, "jiya")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mode = modeForChar(el, options);
|
|
||||||
if (mode === "none") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (mode === "hang" && el.classList.contains("jz-half")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const bd = bdClassOf(el);
|
const bd = bdClassOf(el);
|
||||||
if (!bd) {
|
if (!bd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const tailSet = mode === "hang" ? TAIL_HANG : TAIL_SQUEEZE;
|
|
||||||
const isHead = HEAD_CLASSES.has(bd);
|
const isHead = HEAD_CLASSES.has(bd);
|
||||||
const isTail = tailSet.has(bd);
|
const isTail = TAIL_CLASSES.has(bd);
|
||||||
if (!isHead && !isTail) {
|
if (!isHead && !isTail) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1299,17 +1271,17 @@ function relayout(root, finder, options) {
|
|||||||
if (isHead) {
|
if (isHead) {
|
||||||
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
||||||
if (!prev || higherLine(r, prev)) {
|
if (!prev || higherLine(r, prev)) {
|
||||||
decisions.push({ el, edge: "head", mode });
|
decisions.push({ el });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
||||||
if (!next || lowerLine(r, next)) {
|
if (!next || lowerLine(r, next)) {
|
||||||
decisions.push({ el, edge: "tail", mode });
|
decisions.push({ el });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const d of decisions) {
|
for (const d of decisions) {
|
||||||
applyEdge(d.el, d.edge, d.mode);
|
d.el.classList.add(LE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var lineEdgePass = {
|
var lineEdgePass = {
|
||||||
@@ -1974,21 +1946,15 @@ function normalizeOptions(opts = {}) {
|
|||||||
jiya: feature(opts.jiya, true),
|
jiya: feature(opts.jiya, true),
|
||||||
jinze: feature(opts.jinze, true),
|
jinze: feature(opts.jinze, true),
|
||||||
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
|
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
|
||||||
hanging: feature(opts.hanging, false),
|
|
||||||
biaodian: feature(opts.biaodian, false),
|
biaodian: feature(opts.biaodian, false),
|
||||||
emphasis: feature(opts.emphasis, false),
|
emphasis: feature(opts.emphasis, false),
|
||||||
ruby: feature(opts.ruby, false)
|
ruby: feature(opts.ruby, false)
|
||||||
},
|
},
|
||||||
longWord: resolveLongWord(opts.longWord),
|
longWord: resolveLongWord(opts.longWord),
|
||||||
orphan: resolveOrphan(opts.orphan),
|
orphan: resolveOrphan(opts.orphan),
|
||||||
// 行端模式恒為 squeeze(行端半形)。**標點懸掛 hang 已全面停用**(§6.5.2/§6.5.6):
|
// 行端僅做半形擠壓(lineedge:行首開類收左、行尾閉類收右)。**標點懸掛已移除**
|
||||||
// 經 headless + 真實瀏覽器實測,純負 margin 機制不可行——句號墨色偏框左下角,安全
|
// (§6.5.6):純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);
|
||||||
// 凸出量(<1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);凸出滿 1em 又會釋放
|
// opts.hanging 仍被接受以保 API 相容,但無任何效果。
|
||||||
// 一字寬、把下一字拉上本行致重疊;且 ResizeObserver 重排時兩階段標記錯亂。原生
|
|
||||||
// hanging-punctuation 為正解但 Chrome 不支援、且與本庫 inline-block 原子衝突。
|
|
||||||
// 故此處**唯一關卡**鎖死為 squeeze——即使 opts.hanging 為 true 亦不啟用 hang(無逃逸);
|
|
||||||
// hang 之 CSS/lineedge 程式碼保留(休眠),俟原生支援成熟再議。
|
|
||||||
lineEnd: "squeeze",
|
|
||||||
jiyaConfig: resolveJiya(opts.jiya),
|
jiyaConfig: resolveJiya(opts.jiya),
|
||||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||||
finder: {
|
finder: {
|
||||||
|
|||||||
+2
-31
@@ -121,34 +121,5 @@ jz-char.jz-half-le jz-inner
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 標點懸掛(§6.5.2,hang 模式;由 lineEdge layout pass 標 jz-hang-*)。
|
/* 註:原標點懸掛(jz-hang-* 負 margin 凸出版心)已移除——純 CSS 機制為機制級死路
|
||||||
行尾點號/閉類右凸出右緣(jz-hang-r)、行首開類左凸出左緣(jz-hang-l),令文字格線
|
(寬度守恆矛盾,見 ARCHITECTURE §6.5.6)。行端僅做半形擠壓(jz-half-le,上方規則)。 */
|
||||||
齊版心、標點溢出。**自管負 margin**——不用 CSS hanging-punctuation(瀏覽器支援差+
|
|
||||||
已被原子透明 reset)。
|
|
||||||
凸出量 **0.5em**(句末/句內點號 --jz-hang-stop、括號引號 --jz-hang-bracket,CSS 變數
|
|
||||||
可逐字型調校)。**關鍵約束(headless 實測):凸出量必須 < 一個 CJK 字寬(1em)**——
|
|
||||||
負 margin 會減少該行斷行寬度,若釋放出 ≥1em 空間,下一字會被拉上本行、與懸掛標點
|
|
||||||
重疊(實測 1em 時每個行末標點皆與後字重疊一整字)。0.5em 釋放半字、不足容下後字,
|
|
||||||
故後字不上移、標點穩定凸出半形。代價:正文格線距版心約剩 0.5em(非完全齊邊)——純
|
|
||||||
負 margin 機制無法在不拉升後字下做到全凸,全凸須改用原生 hanging-punctuation(見 §6.5.6)。
|
|
||||||
已 baseline 半形者(jz-half)不懸掛(JS 端跳過:無留白可凸;連續標點經鄰接挤压為半形
|
|
||||||
者亦在此列)。繁體預設不懸掛(逐 lang 閘)。
|
|
||||||
⚠ **需 justifyAtoms:false(jz-char 為 inline)**:預設 inline-block 之 justify 對齊
|
|
||||||
原子模型下,負 margin 會被 justify 重分配吸收、標點不凸出(headless 實測)。懸掛與
|
|
||||||
inline-block 對齊原子互斥;欲懸掛須設 justifyAtoms:false(見 README)。容器宜留左右
|
|
||||||
padding ≥0.5em 以容納凸出之標點。 */
|
|
||||||
jz-char.bd-stop.jz-hang-r,
|
|
||||||
jz-char.bd-pause.jz-hang-r
|
|
||||||
{
|
|
||||||
margin-right: calc(-1 * var(--jz-hang-stop, 0.5em));
|
|
||||||
}
|
|
||||||
|
|
||||||
jz-char.bd-close.jz-hang-r
|
|
||||||
{
|
|
||||||
margin-right: calc(-1 * var(--jz-hang-bracket, 0.5em));
|
|
||||||
}
|
|
||||||
|
|
||||||
jz-char.bd-open.jz-hang-l
|
|
||||||
{
|
|
||||||
margin-left: calc(-1 * var(--jz-hang-bracket, 0.5em));
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-9
@@ -109,21 +109,15 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
|||||||
jiya: feature(opts.jiya, true),
|
jiya: feature(opts.jiya, true),
|
||||||
jinze: feature(opts.jinze, true),
|
jinze: feature(opts.jinze, true),
|
||||||
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
|
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
|
||||||
hanging: feature(opts.hanging, false),
|
|
||||||
biaodian: feature(opts.biaodian, false),
|
biaodian: feature(opts.biaodian, false),
|
||||||
emphasis: feature(opts.emphasis, false),
|
emphasis: feature(opts.emphasis, false),
|
||||||
ruby: feature(opts.ruby, false),
|
ruby: feature(opts.ruby, false),
|
||||||
},
|
},
|
||||||
longWord: resolveLongWord(opts.longWord),
|
longWord: resolveLongWord(opts.longWord),
|
||||||
orphan: resolveOrphan(opts.orphan),
|
orphan: resolveOrphan(opts.orphan),
|
||||||
// 行端模式恒為 squeeze(行端半形)。**標點懸掛 hang 已全面停用**(§6.5.2/§6.5.6):
|
// 行端僅做半形擠壓(lineedge:行首開類收左、行尾閉類收右)。**標點懸掛已移除**
|
||||||
// 經 headless + 真實瀏覽器實測,純負 margin 機制不可行——句號墨色偏框左下角,安全
|
// (§6.5.6):純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);
|
||||||
// 凸出量(<1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);凸出滿 1em 又會釋放
|
// opts.hanging 仍被接受以保 API 相容,但無任何效果。
|
||||||
// 一字寬、把下一字拉上本行致重疊;且 ResizeObserver 重排時兩階段標記錯亂。原生
|
|
||||||
// hanging-punctuation 為正解但 Chrome 不支援、且與本庫 inline-block 原子衝突。
|
|
||||||
// 故此處**唯一關卡**鎖死為 squeeze——即使 opts.hanging 為 true 亦不啟用 hang(無逃逸);
|
|
||||||
// hang 之 CSS/lineedge 程式碼保留(休眠),俟原生支援成熟再議。
|
|
||||||
lineEnd: "squeeze",
|
|
||||||
jiyaConfig: resolveJiya(opts.jiya),
|
jiyaConfig: resolveJiya(opts.jiya),
|
||||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||||
finder: {
|
finder: {
|
||||||
|
|||||||
+3
-10
@@ -20,10 +20,6 @@ export interface OrphanOptions
|
|||||||
chars?: number;
|
chars?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 行端標點模式(§6.5.2):squeeze=行端半形(現有,預設);hang=標點懸掛右/左凸;
|
|
||||||
* none=不處理。squeeze 與 hang 同屬渲染層、互斥(共用行端偵測碼)。 */
|
|
||||||
export type LineEndMode = "squeeze" | "hang" | "none";
|
|
||||||
|
|
||||||
/** 標點擠壓設定(架構 §6.3)。布林 true 等同 { halfWidth: "halt" }。 */
|
/** 標點擠壓設定(架構 §6.3)。布林 true 等同 { halfWidth: "halt" }。 */
|
||||||
export interface JiyaOptions
|
export interface JiyaOptions
|
||||||
{
|
{
|
||||||
@@ -92,9 +88,9 @@ export interface JuzhenOptions
|
|||||||
jiya?: boolean | JiyaOptions;
|
jiya?: boolean | JiyaOptions;
|
||||||
jinze?: boolean;
|
jinze?: boolean;
|
||||||
orphan?: boolean | OrphanOptions;
|
orphan?: boolean | OrphanOptions;
|
||||||
/** @deprecated 標點懸掛**已全面停用**(§6.5.6):純負 margin 機制不可行(墨色不出版心/
|
/** @deprecated 標點懸掛**已移除**(§6.5.6):純負 margin 機制經實測為機制級死路(寬度
|
||||||
* 滿凸拉升後字重疊/resize 重排錯亂),原生 hanging-punctuation 為正解但 Chrome 不支援。
|
* 守恆矛盾,CSS 接觸不到斷行器),原生 hanging-punctuation 為唯一正解但僅 Safari 支援、
|
||||||
* 此選項仍被接受以保 API 相容,但**無任何效果**(行端恒 squeeze);俟原生支援成熟再議。 */
|
* 且與本庫 inline-block 原子互斥。此選項仍被接受以保 API 相容,但**無任何效果**。 */
|
||||||
hanging?: boolean;
|
hanging?: boolean;
|
||||||
biaodian?: boolean;
|
biaodian?: boolean;
|
||||||
emphasis?: boolean;
|
emphasis?: boolean;
|
||||||
@@ -133,7 +129,6 @@ export interface ResolvedOptions
|
|||||||
jiya: boolean;
|
jiya: boolean;
|
||||||
jinze: boolean;
|
jinze: boolean;
|
||||||
orphan: boolean;
|
orphan: boolean;
|
||||||
hanging: boolean;
|
|
||||||
biaodian: boolean;
|
biaodian: boolean;
|
||||||
emphasis: boolean;
|
emphasis: boolean;
|
||||||
ruby: boolean;
|
ruby: boolean;
|
||||||
@@ -141,8 +136,6 @@ export interface ResolvedOptions
|
|||||||
longWord: { minLength: number; lang: string };
|
longWord: { minLength: number; lang: string };
|
||||||
/** 垂懸字避免設定(解析後)。 */
|
/** 垂懸字避免設定(解析後)。 */
|
||||||
orphan: { chars: number };
|
orphan: { chars: number };
|
||||||
/** 行端標點模式(解析後;§6.5.2)。 */
|
|
||||||
lineEnd: LineEndMode;
|
|
||||||
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
||||||
jiyaConfig: { halfWidth: "halt" | "margin"; bias: BiasTable };
|
jiyaConfig: { halfWidth: "halt" | "margin"; bias: BiasTable };
|
||||||
blocks: BlockRule[];
|
blocks: BlockRule[];
|
||||||
|
|||||||
+25
-65
@@ -1,32 +1,29 @@
|
|||||||
// 行端標點處理(架構 §6.4/§6.5;clreq 行首行尾擠壓 + 標點懸掛)。機制 B。
|
// 行端標點處理(架構 §6.4/§6.5;clreq 行首行尾擠壓)。機制 B。
|
||||||
// standalone layout pass:量測各 jz-char 之行盒位置,偵測「行首開類」「行尾閉類/
|
// standalone layout pass:量測各 jz-char 之行盒位置,偵測「行首開類」「行尾閉類」,
|
||||||
// 點號」,按**行端模式**套用——
|
// 收為行端半形(jz-half-le,CSS 同 halt):行首開類收左、行尾閉類收右。掛 ResizeObserver
|
||||||
// · squeeze(現有半形,預設):行首開類收左、行尾閉類收右半形(jz-half-le,同 halt);
|
|
||||||
// · hang(標點懸掛,§6.5.2):行首開類左凸、行尾閉類/點號右凸出版心(jz-hang-*,負 margin);
|
|
||||||
// · none:不處理。
|
|
||||||
// 行端偵測碼共用、僅「合格 bd 子類集」與「套用之 class」隨模式而異。掛 ResizeObserver
|
|
||||||
// 於 root,容器寬度變動時重算;字型載入後亦重算。
|
// 於 root,容器寬度變動時重算;字型載入後亦重算。
|
||||||
//
|
//
|
||||||
|
// 註:原計劃之**標點懸掛**(行端標點負 margin 凸出版心)經實測為機制級死路(寬度守恆
|
||||||
|
// 矛盾:行端盒非零寬則墨色出去多少版心內就空多少、零寬則釋放一字寬致下一字拉上重疊;
|
||||||
|
// 純 CSS 接觸不到斷行器),已於 §6.5.6 移除。唯一正解為原生 hanging-punctuation(僅
|
||||||
|
// Safari 支援、且與本庫 inline-block 原子互斥),屬遠期「原生管線」新功能、與此處無共享。
|
||||||
|
//
|
||||||
// ⚠ 純版面、無法於 jsdom 驗證(getClientRects 回傳 0);故無 ResizeObserver
|
// ⚠ 純版面、無法於 jsdom 驗證(getClientRects 回傳 0);故無 ResizeObserver
|
||||||
// 之環境(jsdom)直接跳過,僅於真實瀏覽器生效(架構 §12/R1/R2)。須跨
|
// 之環境(jsdom)直接跳過,僅於真實瀏覽器生效(架構 §12/R1/R2)。須跨
|
||||||
// Chrome/Safari/Firefox 目視核對。
|
// Chrome/Safari/Firefox 目視核對。
|
||||||
|
|
||||||
import { bdClassOf } from "../core/dom.js";
|
import { bdClassOf } from "../core/dom.js";
|
||||||
import type { Finder } from "../core/finder.js";
|
import type { Finder } from "../core/finder.js";
|
||||||
import type { LineEndMode, RenderContext, ResolvedOptions, StandalonePass } from "../types.js";
|
import type { RenderContext, ResolvedOptions, StandalonePass } from "../types.js";
|
||||||
|
|
||||||
const LE = "jz-half-le"; // 行端半形(squeeze 模式;CSS 同 halt)
|
const LE = "jz-half-le"; // 行端半形(CSS 同 halt)
|
||||||
const HANG_L = "jz-hang-l"; // 行首開類左凸(hang 模式;負 margin-left)
|
const ALL_EDGE = [ LE ];
|
||||||
const HANG_R = "jz-hang-r"; // 行尾閉類/點號右凸(hang 模式;負 margin-right)
|
|
||||||
const ALL_EDGE = [ LE, HANG_L, HANG_R ];
|
|
||||||
const EPS = 2; // px:行盒比較之亞像素容差。
|
const EPS = 2; // px:行盒比較之亞像素容差。
|
||||||
|
|
||||||
// 行端合格之 bd 子類(隨模式):head=行首左緣偵測,tail=行尾右緣偵測。
|
// 行端合格之 bd 子類:head=行首左緣偵測(開類),tail=行尾右緣偵測(閉類)。
|
||||||
// squeeze 僅括號引號(沿用現有:點號於行端保持全形,避免孤立句末誤收);
|
// 僅括號引號;點號於行端保持全形(避免孤立句末誤收,實測困惑來源)。
|
||||||
// hang 之 tail 增點號(行末句號/逗號右凸為懸掛之經典用例)。
|
|
||||||
const HEAD_CLASSES = new Set([ "bd-open" ]);
|
const HEAD_CLASSES = new Set([ "bd-open" ]);
|
||||||
const TAIL_SQUEEZE = new Set([ "bd-close" ]);
|
const TAIL_CLASSES = new Set([ "bd-close" ]);
|
||||||
const TAIL_HANG = new Set([ "bd-close", "bd-stop", "bd-pause" ]);
|
|
||||||
|
|
||||||
// 每 root 各 layout pass(以 key 區分)一個 ResizeObserver。
|
// 每 root 各 layout pass(以 key 區分)一個 ResizeObserver。
|
||||||
const observers = new WeakMap<Element, Map<string, ResizeObserver>>();
|
const observers = new WeakMap<Element, Map<string, ResizeObserver>>();
|
||||||
@@ -156,20 +153,6 @@ function lastRect(el: Element): DOMRect | null
|
|||||||
return rects.length ? rects[rects.length - 1]! : null;
|
return rects.length ? rects[rects.length - 1]! : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠ **標點懸掛總開關(防逃逸第三層)**。hang 已全面停用(§6.5.2/§6.5.6):經實測,純
|
|
||||||
// 負 margin 機制不可行(墨色不出版心/滿凸拉升後字重疊/resize 重排錯亂)。此常數為 false
|
|
||||||
// 時,applyEdge 永不施加 jz-hang-*,與「index 鎖 lineEnd=squeeze」「modeForChar 降 squeeze」
|
|
||||||
// 三重保證:縱使下游以任何方式(hanging 選項/data-juzhen/blocks/直接改 lineEnd)要求,
|
|
||||||
// 亦絕無可能產生懸掛標記。hang 之程式碼與 CSS 保留休眠,俟原生 hanging-punctuation 成熟再議。
|
|
||||||
const HANG_ENABLED = false;
|
|
||||||
|
|
||||||
/** 套用行端結果之 class(按模式):head=行首左緣、tail=行尾右緣。 */
|
|
||||||
function applyEdge(el: Element, edge: "head" | "tail", mode: LineEndMode): void
|
|
||||||
{
|
|
||||||
if (mode === "squeeze") { el.classList.add(LE); return; }
|
|
||||||
if (mode === "hang" && HANG_ENABLED) { el.classList.add(edge === "head" ? HANG_L : HANG_R); }
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearEdge(root: Element): void
|
function clearEdge(root: Element): void
|
||||||
{
|
{
|
||||||
for (const cls of ALL_EDGE)
|
for (const cls of ALL_EDGE)
|
||||||
@@ -178,30 +161,16 @@ function clearEdge(root: Element): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 該標點 jz-char 之有效行端模式。
|
interface EdgeDecision { el: Element; }
|
||||||
* ⚠ **防逃逸**:標點懸掛 hang 已全面停用(見 index.ts/§6.5.6)——縱使 base 為 "hang"
|
|
||||||
* (理論上不會,index 已鎖 squeeze),此處一律降回 squeeze,確保不產生任何 jz-hang-*。
|
|
||||||
* hang 之逐 lang 閘邏輯保留於註解供日後重啟:繁體橫排降 squeeze、簡體可 hang。 */
|
|
||||||
function modeForChar(el: Element, options: ResolvedOptions): LineEndMode
|
|
||||||
{
|
|
||||||
const base = options.lineEnd;
|
|
||||||
if (base !== "hang") { return base; }
|
|
||||||
// hang 已停用:無條件降 squeeze(防逃逸)。下行為日後重啟之逐 lang 閘(現不可達):
|
|
||||||
// if (classify(effectiveLang(el, options.locale.default)) === "zh-Hant") return "squeeze";
|
|
||||||
return "squeeze";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EdgeDecision { el: Element; edge: "head" | "tail"; mode: LineEndMode; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 行端偵測 + 模式套用(共用偵測碼;合格 bd 集與套用 class 隨模式而異)。
|
* 行端偵測 + 半形套用(行首開類收左、行尾閉類收右)。
|
||||||
*
|
*
|
||||||
* **兩階段免自我應驗**(§6.5.2 重點風險):先 clearEdge 清除全部行端 margin/半形,
|
* **兩階段免自我應驗**:先 clearEdge 清除全部行端半形,再於「自然(無行端效果)佈局」下
|
||||||
* 再於「自然(無行端效果)佈局」下量測**全部**標點之行端狀態(階段 1,不變更 DOM),
|
* 量測**全部**標點之行端狀態(階段 1,不變更 DOM),最後一次性套用 jz-half-le(階段 2)。
|
||||||
* 最後一次性套用(階段 2)。如此每次量測皆見無污染之自然佈局——既不受自身、亦不受
|
* 如此每次量測皆見無污染之自然佈局——不受自身或他者之 halt 收縮影響(避免迴圈內逐一 add
|
||||||
* 其他標點之 hang 負 margin/halt 收縮影響(避免迴圈內逐一 add class 致下游量測偏移,
|
* class 致下游量測偏移,同 gapTrim 之雙向污染教訓)。ResizeObserver 僅觀 root 尺寸、不因
|
||||||
* 同 gapTrim 之雙向污染教訓)。ResizeObserver 僅觀 root 尺寸、不因套用後內部重排再
|
* 套用後內部重排再觸發,故終態穩定。 */
|
||||||
* 觸發,故終態穩定(基於自然斷行決定懸掛者,正是所欲)。 */
|
|
||||||
function relayout(root: Element, finder: Finder, options: ResolvedOptions): void
|
function relayout(root: Element, finder: Finder, options: ResolvedOptions): void
|
||||||
{
|
{
|
||||||
clearEdge(root);
|
clearEdge(root);
|
||||||
@@ -217,36 +186,27 @@ function relayout(root: Element, finder: Finder, options: ResolvedOptions): void
|
|||||||
// 行端為段落級(§6.5.0a):text-level 子樹(單行標題等)不處理行端。
|
// 行端為段落級(§6.5.0a):text-level 子樹(單行標題等)不處理行端。
|
||||||
if (!finder.levelAllows(el, "paragraph")) { continue; }
|
if (!finder.levelAllows(el, "paragraph")) { continue; }
|
||||||
if (!finder.featureEnabledFor(el, "jiya")) { continue; }
|
if (!finder.featureEnabledFor(el, "jiya")) { continue; }
|
||||||
const mode = modeForChar(el, options);
|
|
||||||
if (mode === "none") { continue; }
|
|
||||||
// hang 模式跳過已 baseline 半形(jz-half)之標點:其字身已收為 0.5em、無留白可
|
|
||||||
// 凸出,再加負 margin 會把墨色整體拉出版心。故僅全形標點(有留白者)懸掛;
|
|
||||||
// 已半形者保持原狀(行端本就緊湊)。
|
|
||||||
if (mode === "hang" && el.classList.contains("jz-half")) { continue; }
|
|
||||||
const bd = bdClassOf(el);
|
const bd = bdClassOf(el);
|
||||||
if (!bd) { continue; }
|
if (!bd) { continue; }
|
||||||
const tailSet = mode === "hang" ? TAIL_HANG : TAIL_SQUEEZE;
|
|
||||||
const isHead = HEAD_CLASSES.has(bd);
|
const isHead = HEAD_CLASSES.has(bd);
|
||||||
const isTail = tailSet.has(bd);
|
const isTail = TAIL_CLASSES.has(bd);
|
||||||
if (!isHead && !isTail) { continue; }
|
if (!isHead && !isTail) { continue; }
|
||||||
// squeeze 不處理點號(行尾/行首點號保持全形,避免孤立句末誤收,實測困惑來源):
|
|
||||||
// 已由 TAIL_SQUEEZE 僅含 bd-close 保證;hang 之 tail 始含點號(懸掛經典用例)。
|
|
||||||
const r = firstRect(el);
|
const r = firstRect(el);
|
||||||
if (!r) { continue; }
|
if (!r) { continue; }
|
||||||
|
|
||||||
if (isHead)
|
if (isHead)
|
||||||
{
|
{
|
||||||
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
||||||
if (!prev || higherLine(r, prev)) { decisions.push({ el, edge: "head", mode }); }
|
if (!prev || higherLine(r, prev)) { decisions.push({ el }); }
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
||||||
if (!next || lowerLine(r, next)) { decisions.push({ el, edge: "tail", mode }); }
|
if (!next || lowerLine(r, next)) { decisions.push({ el }); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const d of decisions) { applyEdge(d.el, d.edge, d.mode); }
|
for (const d of decisions) { d.el.classList.add(LE); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lineEdgePass: StandalonePass = {
|
export const lineEdgePass: StandalonePass = {
|
||||||
|
|||||||
+32
-28
@@ -925,46 +925,50 @@ test("垂懸字 元素最小綁定 revert:兩個 <strong> 併回單一、文
|
|||||||
assert.equal(p.querySelector("strong").textContent, "一段很長的加粗文字", "strong 還原");
|
assert.equal(p.querySelector("strong").textContent, "一段很長的加粗文字", "strong 還原");
|
||||||
});
|
});
|
||||||
|
|
||||||
// ----- 標點懸掛(§6.5.2,行端模式接線;版面凸出量於 headless Chrome 驗證)-----
|
// ----- 標點懸掛已移除(§6.5.6):hanging 選項保留為 no-op,永不產生 jz-hang-* -----
|
||||||
|
// 純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);原生
|
||||||
|
// hanging-punctuation 為唯一正解但僅 Safari 支援、且與 inline-block 原子互斥。
|
||||||
|
// 下列測試確保 API 相容(hanging:true/data-juzhen/blocks 任一要求皆被接受但無效果)。
|
||||||
|
|
||||||
test("懸掛:已全面停用——hanging 預設關,行端模式 squeeze", () =>
|
test("懸掛已移除:hanging 選項被接受但無任何效果(API 相容,render 不拋)", () =>
|
||||||
{
|
|
||||||
const jz = createJuzhen();
|
|
||||||
assert.equal(jz.options.lineEnd, "squeeze", "行端模式恒 squeeze");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("懸掛:防逃逸——即使 hanging:true,行端模式仍鎖死 squeeze(不啟用 hang)", () =>
|
|
||||||
{
|
|
||||||
const jz = createJuzhen({ hanging: true });
|
|
||||||
// option 仍被接受(API 不破),但 lineEnd 鎖死 squeeze——hang 不可能激活(§6.5.6 停用)。
|
|
||||||
assert.equal(jz.options.lineEnd, "squeeze", "hanging:true 亦不轉 hang(無逃逸)");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("懸掛:防逃逸——hanging:true render 後不產生任何 jz-hang-* 標記", () =>
|
|
||||||
{
|
{
|
||||||
const { document } = setupDom('<p lang="zh-Hans">春。夏。秋。冬。</p>');
|
const { document } = setupDom('<p lang="zh-Hans">春。夏。秋。冬。</p>');
|
||||||
createJuzhen({ hanging: true, justifyAtoms: false }).render(document.body);
|
const jz = createJuzhen({ hanging: true });
|
||||||
|
jz.render(document.body); // 不應拋錯;hanging 為 no-op
|
||||||
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "無 jz-hang-r");
|
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "無 jz-hang-r");
|
||||||
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "無 jz-hang-l");
|
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "無 jz-hang-l");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("懸掛:防逃逸——下游 data-juzhen=\"hanging\" 亦不啟用(lineEnd 不受 per-node 影響)", () =>
|
test("懸掛已移除:hanging:true 之輸出與不傳 hanging 完全一致(無行為差異)", () =>
|
||||||
{
|
{
|
||||||
const { document } = setupDom('<p lang="zh-Hans" data-juzhen="spacing jiya jinze hanging">春。夏。秋。冬。</p>');
|
const html = '<p lang="zh-Hans">春。夏。秋。冬。</p>';
|
||||||
const jz = createJuzhen({ hanging: true, justifyAtoms: false });
|
const a = setupDom(html);
|
||||||
jz.render(document.body);
|
createJuzhen({ justifyAtoms: false }).render(a.document.body);
|
||||||
assert.equal(jz.options.lineEnd, "squeeze", "lineEnd 恒 squeeze");
|
const withoutHang = a.document.body.innerHTML;
|
||||||
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "data-juzhen 要求亦無 jz-hang-r");
|
const b = setupDom(html);
|
||||||
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "data-juzhen 要求亦無 jz-hang-l");
|
createJuzhen({ hanging: true, justifyAtoms: false }).render(b.document.body);
|
||||||
|
assert.equal(b.document.body.innerHTML, withoutHang, "hanging:true 不改變任何輸出");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("懸掛:防逃逸——blocks features:[\"hanging\"] 亦不啟用", () =>
|
test("懸掛已移除:data-juzhen=\"hanging\" 與 blocks features:[hanging] 皆無效果", () =>
|
||||||
{
|
{
|
||||||
const { document } = setupDom('<p class="q" lang="zh-Hans">春。夏。秋。冬。</p>');
|
const { document } = setupDom(
|
||||||
|
'<p lang="zh-Hans" data-juzhen="spacing jiya jinze hanging">春。夏。秋。</p>'
|
||||||
|
+ '<p class="q" lang="zh-Hans">春。夏。秋。</p>',
|
||||||
|
);
|
||||||
createJuzhen({
|
createJuzhen({
|
||||||
hanging: true, justifyAtoms: false,
|
justifyAtoms: false,
|
||||||
blocks: [ { selector: ".q", features: [ "spacing", "jiya", "hanging" ] } ],
|
blocks: [ { selector: ".q", features: [ "spacing", "jiya", "hanging" ] } ],
|
||||||
}).render(document.body);
|
}).render(document.body);
|
||||||
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "blocks 要求亦無 jz-hang-r");
|
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "無 jz-hang-r");
|
||||||
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "blocks 要求亦無 jz-hang-l");
|
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "無 jz-hang-l");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("行端半形:lineedge 仍正常擠壓(squeeze 保留)——jsdom 無版面故僅驗 pass 不拋", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p lang="zh-Hant">「測試」。</p>');
|
||||||
|
const jz = createJuzhen();
|
||||||
|
jz.render(document.body); // lineedge 於 jsdom(無 ResizeObserver)跳過,不應拋
|
||||||
|
jz.revert(document.body);
|
||||||
|
assert.equal(countTag(document.body, "jz-char"), 0, "revert 清除");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user