feat: 聚珍切片 1 — 中西間隙/標點擠壓/禁則 + v1 相容層

從零重寫之第一垂直切片(依 ARCHITECTURE.md 契約),涵蓋並超越 v1 全部功能。

核心基座(src/core):unicode 字元表+標點子類+字形側位;locale 語言與
全角/開明政策(全域 lang.style +逐 lang policy);dom(jz-* 工廠+可還原
標記);finder 遍歷引擎(avoid/scope.include/邏輯 run/pill 鄰字/分塊閘);
pass 聚合管線。

功能 pass(src/typeset):
- spacing:中西間隙採 margin 模型——包裹邊界左側字、以 margin-right 留隙,
  不注入空白字元(textContent 完全不變、複製緊湊、justify 不暴增、不撐框、
  行首乾淨)。含跨樣式邊界、pill、原檔空格移除。
- gapTrim(lineedge):layout pass,行末去隙,修正 justify 右緣內縮。
- longword:長英文詞 <span lang> + hyphens。
- jiya:標點 charify + 全角/開明/半角(全域 + 逐 lang);半形以字型 OpenType
  halt 渲染(按字形正確定位、不重疊,居中字形 !? 與繁體 locl 句逗點號皆正確);
  行內連續擠壓。
- jinze:行首行末避頭尾(閉類/點號綁前字、開類綁後字,以詞元為邊界、長引文
  中段可斷);亦為 justify 綁定載體。
- lineedge:行端擠壓 layout pass(括號引號;ResizeObserver 重算,瀏覽器專用)。

§8 justify:jz-char inline-block 原子,句末標點單份間距。
v1 相容層(compat/v1):createCjkAutospace().apply 映射,IIFE 全域別名。
構建:esbuild → ESM/IIFE/CSS;tsc → d.ts。測試:39 個 jsdom 案例。
demo:簡繁並列+justify+強制繁體開明+標點寬度量測,載 Noto webfont 作參考
(擠壓依賴字型 halt)。間隔號 · 不觸發中西間隙;200% 等詞元不被禁則切斷。
This commit is contained in:
2026-06-01 21:31:07 +08:00
parent 8098991b6b
commit 2d42c7ecf2
46 changed files with 7737 additions and 27 deletions
+57 -27
View File
@@ -76,8 +76,9 @@ jz.revert(root?); // 還原
```
{
// 全域
lang: { default: "zh-Hant", policy: { "zh-Hant": "quanjiao", "zh-Hans": "kaiming" } },
// 全域。style = 全域擠壓預設(quanjiaokaimingbanjiao);policy = 逐 lang
// 覆寫。優先序:policy[lang] > style > 內建預設(繁→全角、簡→開明)。
lang: { default: "zh-Hant", style: "kaiming", policy: { "zh-Hant": "quanjiao" } },
classPrefix: "jz",
// 作用域限定(§3.6
@@ -313,13 +314,28 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
- **Han**:插入 `<h-hws hidden>`,內含**真實空格** + `::before{content:' '}`
**未加 `user-select:none`** → 選取/複製會帶上空格(其已知缺陷)。
- **我們**finder 邊界感知地在 CJK↔西文邊界插入 `jz-hws`;間隙寬度由 CSS
`::before` 或內含空白 + 負微調)給定,元素 `user-select:none`I1**修正
Han 的選中缺陷**)。WHERE 之規則沿用 v1 pangu 規則表(已驗證)
- **跨瀏覽器**`user-select`inline 普遍支援
- **lang**:間隙量可依 `:lang` 微調(如日文與中文略異),預設一致。
- **調整**copy-cleanI1)+ justify 安全(I5/§8)。涵蓋 v1 Pass A、Pass B
pill `code/kbd` 邊界亦走同一機制,pill 由 avoid 與鄰接判定處理)。
- **我們margin 模型)**finder 邊界感知地把邊界**左側之一個字**(CJK 或西文)
包進 `jz-hws`,間隙由 CSS `margin-right`(0.25em)提供——**不注入任何空白字元**。
WHERE 之規則沿用 v1 pangu 規則表(已驗證);作者原寫之 CJK↔西文空格一律移除
(改由 margin 留隙)
- **為何不用空白字元/inline-blockjustify 三難)**:純 CSS 下,CJK↔西文間隙在
`justify` 必有一個邊緣瑕疵——①真實空白→被當詞間空白**暴增**;②inline-block 固定
寬→罕見落**行首縮排**;③`margin-right`→**行末右緣內縮 0.25em**。選 ③(margin),
因其行首乾淨、不撐框、不暴增、`textContent` 完全不變(純 copy-clean,複製緊湊),
並以 §6.1a 之 layout pass 消除行末內縮。
- **跨瀏覽器**`margin`inline 普遍支援。
- **copy-cleanI1,較 Han 更純)**:間隙為純視覺 margin,`textContent` 不增不減
語義字元,剪貼板得緊湊原文。
- **切片**:涵蓋 v1 Pass A、Pass Bpill `code/kbd` 邊界亦走同一機制——左字或
pill 元素包入 `jz-hws`pill 由 avoid 與鄰接判定處理)。
### 6.1a 行末去隙(gapTrimjustify 修正)— 切片 1
- **問題**margin-right 落在行末時,justify 下使右緣內縮 0.25em。
- **我們**layout pass `gapTrim` 量測各 `jz-hws`,對「其字落於行末(後一內容在
更低行)」者加 `jz-hws-trim`CSS `margin-right:0`),該行遂齊右、無內縮。掛
`ResizeObserver`+字型載入後重算。純版面、瀏覽器專用(jsdom 跳過)。與 `lineedge`
共用量測工具與 layout 安裝器。
### 6.2 長英文詞斷詞包裝 — 切片 1
@@ -334,25 +350,36 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
- **Han**charify 標點為 `<h-char class="bd-jiya …"><h-inner>`,以負
`letter-spacing``margin` 擠壓;連續、行端、簡繁差異以 CSS + `:lang()` 落實
`bd-cop:lang(zh-Hant)` → 不擠=全角式)。
- **我們**:同構——charify → `jz-char.bd-jiya` + `jz-inner`擠壓量寫在
`juzhen.css`依 `bd-*` class 與 `:lang()` 套用。**全角式**zh-Hant`bd-cop`
不擠)/**開明式**zh-Hans`bd-cop` 擠半形),政策由 `locale.style` 與
`:lang()` 一致驅動(I3)。
- **跨瀏覽器(I4**:用負 `margin``letter-spacing`**不依賴** `halt`
`text-spacing-trim`,故 ChromeSafariFF 一致。(註:v1 Pass D 用 `halt`
跨瀏覽器不一致;此處即為改進。)
- **調整**:行端擠壓以 `jz-cs` 補償空白於行邊界折疊之機制達成(CSS 主導,免
逐行量測);justify 安全見 §8。
- **切片**:涵蓋並超越 v1 Pass D(開明式),且加入全角式與 lang 切換。
- **我們**:同構——charify → `jz-char.bd-*` + `jz-inner`半形與否寫在
`juzhen.css`由 JS 解析 `locale.style`(讀 lang)後加 `jz-half``jz-half-le`
class 觸發(I3;改用 class 而非 CSS `:lang()`,以避免 `:lang(zh-Hans)` 不匹配
`zh-CN` 等地區標籤之陷阱)。三機制並集落地:①**baseline 政策**(全角不擠/
開明擠句內點號+括號引號/半角全擠);②**行內連續擠壓**(`jiyaAdjacencyPass`
相鄰標點對向側收一字,lang 無關);③**行端擠壓**(`lineedge.ts` layout pass)。
- **跨瀏覽器(I4)— 修訂**:半形改以字型 OpenType **`halt`**`font-feature-
settings:"halt"`)渲染,**非**負 `margin`。原因:負 margin 假設可壓縮空白全在
單側,對**居中字形**`!?`、繁體 `locl` 下之 `。,`)會把字面拉入鄰字而**重疊**
`halt` 收的是字身寬度、由字型按字形正確定位(簡左/繁中/括號偏側皆對),故
不重疊。`font-feature-settings:"halt"` 各引擎一致(當初疑慮之不一致者為**自動**
的 `text-spacing-trim``chws`,非顯式 `halt`);缺 `halt` 之字型優雅降級為全形。
鄰接/行端之「收哪一字」之**決策**仍由 `blankSides` 邏輯定,渲染統一為 `halt`。
- **切片**:涵蓋並超越 v1 Pass D(開明式),加入全角式、lang 切換、行內連續與
行端擠壓。
### 6.4 禁則(Jinze,避頭尾)+ justify 綁定 — 切片 1(基礎)/後續(完整)
- **Han**`h-jinze` 綁定行首行尾不可出現之標點與鄰字,避免標點落行首/行尾。
- **我們**`jz-jinze` 綁定為不可斷單元CSS `display:inline-block` 或等價,內部
不開 justify gap)。**此亦為 §8 justify 單份間距之載體**:句末標點與其字面留白
成一原子單元,justify 伸縮只落單元之間。
- **切片**:切片 1 先做「足以保證 §8 與標點不破壞間距」之最小綁定;完整避頭尾
(所有禁則對象、行首縮排聯動)列後續,但用同一 `jz-jinze` 結構,無返工。
- **我們**`jz-jinze`CSS `display:inline-block; white-space:nowrap`)綁定不可斷
單元。**避頭尾已實作**`jinze.ts`):因 jiya 把標點原子化為 inline-block,原子
邊界成可斷點、破壞瀏覽器原生 UAX#14 避頭尾,故補回——把「不可置行首」之標點
(閉類/點號/間隔/連接省略)綁到**前一字**、「不可置行末」之開類綁到**後一字**,
且**只隔離單一邊界字**(長引文中段仍正常斷行,不溢出)。實作為兩階段:依原始
相鄰算禁斷邊界→切邊界字;重掃併入 nowrap 群組。亦為 §8 justify 綁定載體。
- **切片**:切片 1 完成行首行末避頭尾(閉類/點號避頭、開類避尾);行首縮排聯動、
更細之禁則對象列後續,沿用同一 `jz-jinze` 結構。
- **已知交互(待瀏覽器精修)**:`lineedge` 之行端偵測需跨 `jz-jinze` 群組邊界向上
攀爬找流內鄰字(已於 `edgeRect` 處理 jz-* 包裹之攀出);屬 layout 層,jsdom 無法
驗,須瀏覽器目視。
### 6.5 標點懸掛(Hanging)— 後續
@@ -439,9 +466,12 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
## 9. 跨瀏覽器策略
- **一致性關鍵層**(間隙、擠壓、justify):只用 `margin`、`letter-spacing`、
`inline-block`、`user-select`、`:lang()`、`:where()`、`hyphens`——三大引擎
BlinkWebKitGecko)皆穩定支援。
- **一致性關鍵層**(間隙、擠壓、justify):只用 `margin`、`inline-block`、
`user-select`、`white-space`、`:where()`、`hyphens`,及字型 OpenType
`font-feature-settings:"halt"`(標點半形;各引擎一致,效果僅依字型是否含 halt,
Noto CJK 具備,缺則優雅降級為全形)——三大引擎(Blink/WebKitGecko)皆穩定
支援。**注意**:標點半形不用負 `margin`(對居中字形會重疊),改用 `halt`(見
§6.3 修訂)。
- **漸進增強(非必要)**`text-spacing-trim`Blink)、`hanging-punctuation`
WebKit)、`text-emphasis`、`text-autospace`——僅在支援時錦上添花,缺席不影響
基本排版(I4)。