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:
+25
-65
@@ -1,32 +1,29 @@
|
||||
// 行端標點處理(架構 §6.4/§6.5;clreq 行首行尾擠壓 + 標點懸掛)。機制 B。
|
||||
// standalone layout pass:量測各 jz-char 之行盒位置,偵測「行首開類」「行尾閉類/
|
||||
// 點號」,按**行端模式**套用——
|
||||
// · squeeze(現有半形,預設):行首開類收左、行尾閉類收右半形(jz-half-le,同 halt);
|
||||
// · hang(標點懸掛,§6.5.2):行首開類左凸、行尾閉類/點號右凸出版心(jz-hang-*,負 margin);
|
||||
// · none:不處理。
|
||||
// 行端偵測碼共用、僅「合格 bd 子類集」與「套用之 class」隨模式而異。掛 ResizeObserver
|
||||
// 行端標點處理(架構 §6.4/§6.5;clreq 行首行尾擠壓)。機制 B。
|
||||
// standalone layout pass:量測各 jz-char 之行盒位置,偵測「行首開類」「行尾閉類」,
|
||||
// 收為行端半形(jz-half-le,CSS 同 halt):行首開類收左、行尾閉類收右。掛 ResizeObserver
|
||||
// 於 root,容器寬度變動時重算;字型載入後亦重算。
|
||||
//
|
||||
// 註:原計劃之**標點懸掛**(行端標點負 margin 凸出版心)經實測為機制級死路(寬度守恆
|
||||
// 矛盾:行端盒非零寬則墨色出去多少版心內就空多少、零寬則釋放一字寬致下一字拉上重疊;
|
||||
// 純 CSS 接觸不到斷行器),已於 §6.5.6 移除。唯一正解為原生 hanging-punctuation(僅
|
||||
// Safari 支援、且與本庫 inline-block 原子互斥),屬遠期「原生管線」新功能、與此處無共享。
|
||||
//
|
||||
// ⚠ 純版面、無法於 jsdom 驗證(getClientRects 回傳 0);故無 ResizeObserver
|
||||
// 之環境(jsdom)直接跳過,僅於真實瀏覽器生效(架構 §12/R1/R2)。須跨
|
||||
// Chrome/Safari/Firefox 目視核對。
|
||||
|
||||
import { bdClassOf } from "../core/dom.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 HANG_L = "jz-hang-l"; // 行首開類左凸(hang 模式;負 margin-left)
|
||||
const HANG_R = "jz-hang-r"; // 行尾閉類/點號右凸(hang 模式;負 margin-right)
|
||||
const ALL_EDGE = [ LE, HANG_L, HANG_R ];
|
||||
const LE = "jz-half-le"; // 行端半形(CSS 同 halt)
|
||||
const ALL_EDGE = [ LE ];
|
||||
const EPS = 2; // px:行盒比較之亞像素容差。
|
||||
|
||||
// 行端合格之 bd 子類(隨模式):head=行首左緣偵測,tail=行尾右緣偵測。
|
||||
// squeeze 僅括號引號(沿用現有:點號於行端保持全形,避免孤立句末誤收);
|
||||
// hang 之 tail 增點號(行末句號/逗號右凸為懸掛之經典用例)。
|
||||
// 行端合格之 bd 子類:head=行首左緣偵測(開類),tail=行尾右緣偵測(閉類)。
|
||||
// 僅括號引號;點號於行端保持全形(避免孤立句末誤收,實測困惑來源)。
|
||||
const HEAD_CLASSES = new Set([ "bd-open" ]);
|
||||
const TAIL_SQUEEZE = new Set([ "bd-close" ]);
|
||||
const TAIL_HANG = new Set([ "bd-close", "bd-stop", "bd-pause" ]);
|
||||
const TAIL_CLASSES = new Set([ "bd-close" ]);
|
||||
|
||||
// 每 root 各 layout pass(以 key 區分)一個 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;
|
||||
}
|
||||
|
||||
// ⚠ **標點懸掛總開關(防逃逸第三層)**。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
|
||||
{
|
||||
for (const cls of ALL_EDGE)
|
||||
@@ -178,30 +161,16 @@ function clearEdge(root: Element): void
|
||||
}
|
||||
}
|
||||
|
||||
/** 該標點 jz-char 之有效行端模式。
|
||||
* ⚠ **防逃逸**:標點懸掛 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; }
|
||||
interface EdgeDecision { el: Element; }
|
||||
|
||||
/**
|
||||
* 行端偵測 + 模式套用(共用偵測碼;合格 bd 集與套用 class 隨模式而異)。
|
||||
* 行端偵測 + 半形套用(行首開類收左、行尾閉類收右)。
|
||||
*
|
||||
* **兩階段免自我應驗**(§6.5.2 重點風險):先 clearEdge 清除全部行端 margin/半形,
|
||||
* 再於「自然(無行端效果)佈局」下量測**全部**標點之行端狀態(階段 1,不變更 DOM),
|
||||
* 最後一次性套用(階段 2)。如此每次量測皆見無污染之自然佈局——既不受自身、亦不受
|
||||
* 其他標點之 hang 負 margin/halt 收縮影響(避免迴圈內逐一 add class 致下游量測偏移,
|
||||
* 同 gapTrim 之雙向污染教訓)。ResizeObserver 僅觀 root 尺寸、不因套用後內部重排再
|
||||
* 觸發,故終態穩定(基於自然斷行決定懸掛者,正是所欲)。 */
|
||||
* **兩階段免自我應驗**:先 clearEdge 清除全部行端半形,再於「自然(無行端效果)佈局」下
|
||||
* 量測**全部**標點之行端狀態(階段 1,不變更 DOM),最後一次性套用 jz-half-le(階段 2)。
|
||||
* 如此每次量測皆見無污染之自然佈局——不受自身或他者之 halt 收縮影響(避免迴圈內逐一 add
|
||||
* class 致下游量測偏移,同 gapTrim 之雙向污染教訓)。ResizeObserver 僅觀 root 尺寸、不因
|
||||
* 套用後內部重排再觸發,故終態穩定。 */
|
||||
function relayout(root: Element, finder: Finder, options: ResolvedOptions): void
|
||||
{
|
||||
clearEdge(root);
|
||||
@@ -217,36 +186,27 @@ function relayout(root: Element, finder: Finder, options: ResolvedOptions): void
|
||||
// 行端為段落級(§6.5.0a):text-level 子樹(單行標題等)不處理行端。
|
||||
if (!finder.levelAllows(el, "paragraph")) { 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);
|
||||
if (!bd) { continue; }
|
||||
const tailSet = mode === "hang" ? TAIL_HANG : TAIL_SQUEEZE;
|
||||
const isHead = HEAD_CLASSES.has(bd);
|
||||
const isTail = tailSet.has(bd);
|
||||
const isTail = TAIL_CLASSES.has(bd);
|
||||
if (!isHead && !isTail) { continue; }
|
||||
// squeeze 不處理點號(行尾/行首點號保持全形,避免孤立句末誤收,實測困惑來源):
|
||||
// 已由 TAIL_SQUEEZE 僅含 bd-close 保證;hang 之 tail 始含點號(懸掛經典用例)。
|
||||
const r = firstRect(el);
|
||||
if (!r) { continue; }
|
||||
|
||||
if (isHead)
|
||||
{
|
||||
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
|
||||
{
|
||||
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 = {
|
||||
|
||||
Reference in New Issue
Block a user