2890549f44
統一「行內元素對排版透明」原則在各遍歷原語之口徑,修復兩個下游 Bug 及三項同根隱患。
Bug1(pill 前標點誤加間隙):
isTransparentInline 補 JZ-CHAR/JZ-INNER。`漢字。<code>` 經禁則綁定後,
adjacentLogicalChar 之 descend 遇不透明 jz-char 既不進入亦不終止、而跳過它越過
標點取到「字」誤補 jz-hws;透明後回傳標點「。」(非 pill 鄰字)→ 不補隙,與
collectRuns 既有透明遞迴對齊。
Bug2(行內樣式元素整體掉行):
新增 core/split.ts 之 isolateBoundaryToken/splitElementAt——沿祖先鏈逐層把邊界
詞元剝入淺克隆(保留標籤/class/style/lang、去 id;標 data-jz-kind="splitoff"),
jinze/orphan 改最小綁定:`<strong>長串</strong>,` 只綁末字+逗號(非整串入 nowrap
inline-block 致行末整體掉行);段末長 strong 不再整綁溢出容器。revertPass 增延後
階段把 splitoff 併回原元素(先解包 wrap、後併 splitoff),render→revert 還原一致。
純樣式標籤白名單可切,a/abbr/ruby/帶 id 退化整綁。
審計(同根隱患):
T4 lineedge edgeRect 攀爬集改由 options.finder.styleInlines 派生(棄硬編碼子集,
免遺漏 ins/del/sub 等致行端誤判)。
T5 jiyaAdjacency 遇 pill(pillMatches)斷相鄰、不穿越(內容對排版不可見)。
T6 spacing processPills 補 per-node 功能閘(data-juzhen-off=spacing 區域內不補隙)。
文檔:README 切片表(spacing/jinze/orphan 三行)+ARCHITECTURE §4.1 透明性原則、
§4.4 splitoff 還原、§6.4 行內元素最小綁定、§5 jz-orphan 詞彙。
測試 90→102(Bug1×4、Bug2×6、T5/T6×2)。headless Chrome 實證 Bug2:段末 strong
最小切分、orphan 寬 60px 不溢出版心。
940 lines
63 KiB
Markdown
940 lines
63 KiB
Markdown
# 聚珍(Juzhen)架構文檔
|
||
|
||
> 現代簡/繁中文網頁排版套件。Han.css 之現代化重寫,沿其功能版圖,並針對
|
||
> 我們關注的數種情況(複製乾淨、依 `lang` 全角/開明切換、justify 句末標點
|
||
> 單份間距、跨瀏覽器一致)作調整。
|
||
>
|
||
> 倉庫名 `cjk-autospace` 不變;公開 API 為 `createJuzhen` / 全域 `Juzhen`;
|
||
> 自訂元素前綴 `jz-`。本文件先於任何程式碼,作為實作契約——後續每個功能點
|
||
> 皆按此落地,以避免不一致與重大返工。
|
||
|
||
---
|
||
|
||
## 1. 目標與範圍
|
||
|
||
1. **第一垂直切片 = v1(cjk-autospace)全部功能**,在新引擎上重寫:中西間隙
|
||
(Pass A/B)、長英文詞斷詞包裝(Pass C)、開明式標點擠壓(Pass D)。
|
||
2. **架構須預留完整復刻 Han.css 的能力**:標點修正、擠壓、懸掛、禁則、著重號、
|
||
注音/ruby、樣式正常化、字體偵測、計數器等,皆能在不改動切片 1 既有程式碼
|
||
的前提下,以「新增一個 pass」的方式加入。
|
||
3. **跨瀏覽器一致**(Chrome/Edge/Safari/WebKit/Firefox):一致性關鍵的版面
|
||
行為,只用普遍支援的 CSS 原語;原生新特性僅作非必要增強。
|
||
|
||
非目標:不依賴 Han.css/Heti 等既有庫之執行期程式碼(MIT 條款下可借鑑其思路與
|
||
分類表,但成品自含)。
|
||
|
||
---
|
||
|
||
## 2. 設計不變量(所有 pass 必須遵守)
|
||
|
||
這些不變量是「避免返工」的核心。任何新功能點都不得違反。
|
||
|
||
- **I1 複製乾淨(copy-clean)**:注入之間隙與標記一律 `user-select: none`,且不
|
||
改變邏輯 `textContent` 的語義;若某 pass 必須改寫文字(如標點替換),則須為
|
||
顯式 opt-in 且可 `revert` 還原。預設「只包裝、不改寫文字」。
|
||
- **I2 JS/CSS 職責分離**:JS 只注入**結構**(`jz-*` 元素、class、`data-jz-*`
|
||
屬性);**所有視覺尺寸(margin/letter-spacing/寬度/懸掛位移)一律寫在
|
||
`juzhen.css`**。同一段 DOM 在 justify 與否、主題與否下都成立。此分離是擴充性
|
||
與跨瀏覽器調校的關鍵。
|
||
- **I3 依 `lang` 行為**:每個 pass 解析節點的有效 `lang`(最近祖先),行為與
|
||
CSS 皆以 `:lang()` 與 locale 政策驅動。全角式/開明式之別即由此決定。
|
||
- **I4 跨瀏覽器一致**:間隙、擠壓等一致性關鍵層只用 `margin`/`letter-spacing`/
|
||
`inline-block`/`user-select` 等普遍原語。`text-spacing-trim`/
|
||
`hanging-punctuation`/`text-emphasis` 等僅作漸進增強,絕不作為唯一機制。**且當
|
||
某原生原語在各引擎支援不一致、會與我們託管之版面衝突或重複作用時,須在
|
||
`.juzhen` 範圍內以 `@supports` 顯式禁用之**(如關閉 Blink 的 `text-spacing-trim`/
|
||
`text-autospace`),使三引擎行為收斂於同一路徑(見 §9.1)。
|
||
- **I5 justify 單份間距**:間隙與擠壓的結構設計,須保證每個標點在
|
||
`text-align: justify` 下只貢獻**一份**對齊伸縮量(見 §8)。
|
||
- **I6 冪等與可還原**:每個 pass 對其產物打標記,重跑跳過;提供 `revert` 乾淨
|
||
移除,DOM 回到 pass 前狀態(至少邏輯等價)。
|
||
- **I7 邊界感知**:跨樣式行內元素的鄰接(如 `<strong>是</strong>A`)須能識別並
|
||
正確處理——由核心 finder 統一提供,各 pass 不自行重複造輪。
|
||
- **I8 向下相容 v1**:既有 `createCjkAutospace(options).apply(root)` 消費端
|
||
(build.sh 單檔串接、Nvim peek、Claude 模板)**不得 break、無功能倒退**。行為
|
||
**不要求與 v1 完全一致**,但須等價可用;以相容層達成(見 §3.5)。
|
||
- **I9 遍歷聚合**:邏輯上「一功能一 pass」,但執行上**相容之 pass 共享單次遍歷**
|
||
(見 §3.4),以減少全樹走訪次數;聚合不得犧牲 I6(冪等可還原)與逐功能開關。
|
||
- **I10 作用域與分塊**:作用範圍可由 `scope.include` 選擇器限定(opt-in);功能集
|
||
可逐塊覆寫(屬性或 `blocks` 映射),由 pass 在 `collect` 內以 per-node 閘解析
|
||
(見 §3.6)。CSS 視覺規則須與 JS 作用域同域收斂,不外溢。
|
||
|
||
---
|
||
|
||
## 3. 整體架構
|
||
|
||
### 3.1 對外 API
|
||
|
||
```
|
||
const jz = createJuzhen(options);
|
||
jz.render(root?); // 對 root(預設 document.body)套用啟用之 pass
|
||
jz.revert(root?); // 還原
|
||
// 全域: window.Juzhen.createJuzhen(...)
|
||
```
|
||
|
||
`options` 形如:
|
||
|
||
```
|
||
{
|
||
// 全域。style = 全域擠壓預設(quanjiao|kaiming|banjiao);policy = 逐 lang
|
||
// 覆寫。優先序:policy[lang] > style > 內建預設(繁→全角、簡→開明)。
|
||
lang: { default: "zh-Hant", style: "kaiming", policy: { "zh-Hant": "quanjiao" } },
|
||
classPrefix: "jz",
|
||
|
||
// 作用域限定(§3.6)
|
||
scope: {
|
||
root: document.body, // 預設處理根;render(root) 可逐次覆寫
|
||
include: null, // 啟用選擇器;設則僅處理命中子樹(opt-in)
|
||
// 如 ".juzhen, [data-juzhen]"
|
||
avoid: "code, kbd, pre, samp, textarea, [contenteditable]", // 追加於內建 avoid
|
||
},
|
||
|
||
// 全域逐功能開關(true/false 或細部物件),預設見各 pass
|
||
spacing: true, // 中西間隙
|
||
longWord: { minLength: 6, lang: "en" },
|
||
jiya: true, // 標點擠壓
|
||
jinze: true, // 禁則(含 justify 綁定)
|
||
hanging: false, // 標點懸掛(預設關,繁體橫排不宜)
|
||
biaodian: false, // 標點修正(opt-in,可能改寫文字)
|
||
emphasis: false,
|
||
ruby: false,
|
||
|
||
// 分塊功能選擇(§3.6):選擇器 → 該塊內生效之功能集(覆寫全域)
|
||
blocks: [
|
||
{ selector: ".prose", features: ["spacing", "jiya", "jinze", "emphasis"] },
|
||
{ selector: ".code-comment", features: ["spacing"] }, // 此塊只間隙、不擠壓
|
||
],
|
||
}
|
||
```
|
||
|
||
### 3.2 Pass 管線
|
||
|
||
每個功能點是一個 `Pass`:
|
||
|
||
```
|
||
interface Pass {
|
||
name: string;
|
||
phase: "P0" | "P1" | "P2"; // 遍歷階段,供聚合(§3.4)
|
||
order: number; // phase 內 apply 先後(charify→擠壓→間隙→…)
|
||
enabled(options): boolean;
|
||
// 聚合模式:collect 純判定(同階段共享單次走訪),apply 批次變更
|
||
collect(node: Text, ctx: RenderContext): Target[];
|
||
apply(targets: Target[], ctx: RenderContext): void;
|
||
revert(ctx: RenderContext): void;
|
||
}
|
||
```
|
||
|
||
需獨立走訪/layout 之 pass(如 hanging)可改採 `render(ctx)` 整段式而不參與
|
||
collect 聚合;執行器兩種皆容納。
|
||
|
||
`createJuzhen` 內建一個**有序、可開關**的 pass 註冊表。`render` 依 `order` 執行
|
||
已啟用者。順序契約(避免互相破壞):
|
||
|
||
```
|
||
10 biaodian 標點修正(若開,最先;可能改寫文字/包裝)
|
||
20 charify 標點字元包裝(jz-char/jz-inner)——擠壓、禁則、著重號之共同前置
|
||
30 jiya 標點擠壓(依 class + :lang 之 CSS)
|
||
40 jinze 禁則 + justify 綁定
|
||
50 spacing 中西間隙(jz-hws)
|
||
60 longWord 長英文詞 lang 包裝
|
||
70 emphasis 著重號
|
||
80 ruby 注音/ruby
|
||
90 hanging 標點懸掛(需 layout,最後;掛 ResizeObserver)
|
||
```
|
||
|
||
> 順序理由:charify 先把標點變成可定址的 `jz-char`,擠壓/禁則/著重號才有對象;
|
||
> 間隙在標點結構穩定後插入;懸掛依賴最終行盒,必須最後且 layout 後再算。
|
||
|
||
### 3.3 RenderContext
|
||
|
||
`render`/`revert` 收到統一上下文:`{ root, options, finder, locale }`。各 pass
|
||
共用同一 `finder` 實例與 locale 解析,確保 avoid 名單、邊界規則、語言判定一致。
|
||
|
||
### 3.4 遍歷聚合(減少 pass 遍歷次數,I9)
|
||
|
||
樸素做法是每個 pass 各跑一次 `TreeWalker`,9 個 pass 即 9 次全樹走訪。為此引入
|
||
**遍歷階段(phase)**:相容之 pass 共享單次走訪。Pass 介面拆為
|
||
`collect(node, ctx) → targets[]`(純判定、不變更 DOM)與 `apply(targets, ctx)`
|
||
(批次變更),執行器:
|
||
|
||
```
|
||
for phase in phases:
|
||
walk(root) 一次:對每個文本節點,依序呼叫該 phase 內各 pass 的 collect()
|
||
依 order 對收集到的 targets 做 apply()(兩階段:先收集後變更,避免遍歷中改 DOM)
|
||
```
|
||
|
||
階段劃分(把 ~9 次走訪壓到 3 次):
|
||
|
||
| phase | 內含 pass | 走訪 | 備註 |
|
||
|---|---|---|---|
|
||
| P0 文字修正 | biaodian(opt-in) | 1 次 | 唯一可能改寫文字者,須最先 |
|
||
| P1 行內結構 | charify/jiya 標記/jinze/spacing/longWord/emphasis | 1 次 | 共享走訪,`apply` 依 order |
|
||
| P2 版面後處理 | hanging | 0 次走訪+layout | 需最終行盒,掛 ResizeObserver |
|
||
|
||
> 聚合僅是**執行優化**:邏輯上仍「一功能一 pass」、可獨立開關與 `revert`(I6);
|
||
> 只是 P1 諸 pass 的 `collect` 在同一走訪內完成。若某 pass 不相容(需獨立走訪),
|
||
> 退化為自跑一次,不影響正確性。
|
||
|
||
### 3.5 v1 向下相容層(I8,`src/compat/v1.ts`)
|
||
|
||
導出 `createCjkAutospace(options)`,把 v1 API 映射到 `createJuzhen`,**保證不
|
||
break、無功能倒退**,行為等價但不要求完全一致:
|
||
|
||
| v1 選項/行為 | 映射 |
|
||
|---|---|
|
||
| `.apply(root)` | → `.render(root)` |
|
||
| `pillSelector` | → pill 鄰接處理 + avoid |
|
||
| `styleInlines`/`blockTags`/`skipTags` | → finder 設定 |
|
||
| `autospaceClass`(預設 `cjk-autospace`) | → 套用於 `jz-hws` 之 class,使既有
|
||
`.cjk-autospace{…}` CSS 仍命中間隙元素 |
|
||
| `skipAttribute`/`isSkipped` | → avoid 判定 |
|
||
| `longWordWrap` | → `longWord` |
|
||
| `punctuationSqueeze`(v1 halt 句內半角) | → `jiya`(margin 開明/全角;行為近似,
|
||
非逐位等同——符合「不要求完全一致」) |
|
||
|
||
IIFE 構建額外掛 `globalThis.createCjkAutospace` 別名,使
|
||
`globalThis.createCjkAutospace` 之消費端(Nvim peek/模板)零改動可跑。相容層之
|
||
差異(間隙元素由 `span.cjk-autospace` 變為 `jz-hws[class]`、擠壓由 halt 變 margin)
|
||
於 README 明列。
|
||
|
||
### 3.6 作用域與分塊功能選擇(I10)
|
||
|
||
**作用域限定**——控制「在頁面何處生效」,避免污染無關內容:
|
||
|
||
- `scope.root`:處理根(節點或選擇器),預設 `document.body`;`render(root)` 可逐次
|
||
覆寫;允許多實例/多根並存。
|
||
- `scope.include`:**啟用選擇器**(opt-in 模式)。設定後,**僅處理命中該選擇器之
|
||
子樹**(如 `".juzhen, [data-juzhen]"`),其餘一概不動——此即「限定作用範圍」。
|
||
- `scope.avoid`:排除選擇器,與內建 avoid 合併。
|
||
- **CSS 同域**:`juzhen.css` 之視覺規則一律收斂於同一啟用選擇器(如 `.juzhen` 下),
|
||
與 JS 作用域一致,杜絕樣式外溢(呼應 I2、§9.1 之禁用層亦限於此域)。
|
||
|
||
**分塊功能選擇**——不同塊啟用不同功能集:
|
||
|
||
- **宣告式(屬性,就近繼承)**:
|
||
- `data-juzhen="spacing jiya"`——白名單:此塊內僅啟用列出之功能。
|
||
- `data-juzhen-off="jiya hanging"`——黑名單:此塊停用列出者,其餘繼承。
|
||
- `lang`——驅動全角/開明(正交於功能開關)。
|
||
- **程式式**:`options.blocks: [{ selector, features }]`,選擇器 → 功能集。
|
||
- **解析(per-node 閘)**:對每個節點,向上找最近帶功能指令之祖先(屬性或命中
|
||
`blocks` 選擇器者)決定其**有效功能集**,無則用全域 `options`;render 期間以
|
||
元素為鍵快取。每個 pass 之 `collect(node)` 先問 `featureEnabledFor(node, name)`,
|
||
否則跳過——**故分塊閘只是單次走訪內的 per-node 判定,不增遍歷、不破壞聚合
|
||
(§3.4)**。
|
||
- **與 revert 之關係**:`revert` 依標記移除某功能產物,**不受當前閘狀態影響**,故
|
||
「關閉某功能 + revert」恆能乾淨還原(I6)。
|
||
|
||
---
|
||
|
||
## 4. 核心基座(`src/core/`)
|
||
|
||
### 4.1 `finder.ts`——文本遍歷引擎(取代 Fibre.js)
|
||
|
||
職責(所有 pass 的共同地基):
|
||
|
||
- **遍歷**:`TreeWalker(SHOW_TEXT)` 走 `root` 下文本節點。
|
||
- **avoid**:跳過 avoid 名單之子樹(內建:`code/kbd/pre/samp/textarea/script/
|
||
style/SVG/[contenteditable]/[data-jz-skip]`,可由 `scope.avoid` 追加)。
|
||
- **scope.include**:若設啟用選擇器,僅走訪命中之子樹(opt-in,§3.6)。
|
||
- **featureEnabledFor(node, name)**:解析該節點之有效功能集(§3.6),供各 pass 之
|
||
`collect` per-node 閘用;結果以元素為鍵快取。
|
||
- **charify**:把符合條件之字元/字串包進指定 `jz-*` 元素(兩階段:先收集目標、
|
||
後變更,避免遍歷中改 DOM 失效)。
|
||
- **邊界感知 replace(I7)**:對「跨相鄰文本節點、可跨樣式行內元素」的邏輯字串
|
||
做正則 find,回呼決定如何包裝/插入標記。樣式行內元素(`strong/em/a/span/…`)
|
||
透明穿越;block/pill/avoid 中斷邏輯串。此即 v1 Pass A「剝離樣式收集邏輯文本
|
||
再反映射」之一般化,提升為可重用引擎。
|
||
|
||
設計要點:finder 不知道任何具體排版規則;它只提供「在何處、跨何種邊界、對哪些
|
||
字元」的遍歷與安全變更原語。規則由各 pass 以 unicode 表 + 回呼注入。
|
||
|
||
**「行內透明」原則之一致性(下游 Bug 修復)**:「邏輯文本應透明穿越之行內元素」一語
|
||
須在所有遍歷原語中**口徑一致**,否則同一段文本在不同原語下被切成不同邏輯串、生成相
|
||
互矛盾之結果。`isTransparentInline` 為單一真相源,列入:樣式行內標籤、`jz-hws`/
|
||
`jz-jinze`(wrap 群組)、**`jz-char`/`jz-inner`(標點原子)**。後者必須透明,否則
|
||
`adjacentLogicalChar` 之 `descend` 遇標點原子既不進入亦不終止、而是**跳過**它續找更前
|
||
之兄弟——`漢字。<code>` 經禁則綁定後,pill 之左鄰字應為「。」(標點,非 pill 鄰字、不
|
||
補間隙),舊邏輯卻越過 `jz-char` 取到「字」誤補 `jz-hws`。修復後與 `collectRuns` 之既
|
||
有透明遞迴對齊。同理 `lineedge` 之 `edgeRect` 攀爬集由 `options.finder.styleInlines`
|
||
**派生**(非硬編碼子集),免遺漏 `ins`/`del`/`sub` 等致行端誤判(T4)。
|
||
|
||
### 4.2 `unicode.ts`——字元分類與規則表
|
||
|
||
集中所有 Unicode 範圍與標點子類,供各 pass 共用,杜絕分類散落不一致:
|
||
|
||
- **CJK**:表意文字、假名、注音符號等範圍(沿用並擴充 v1 之 `PANGU_CJK`)。
|
||
- **西文**:拉丁字母、數字、常見符號(v1 之 `AN_CHARS` 等)。
|
||
- **標點(biaodian)子類**(對應 CSS class):
|
||
- `bd-open` 開始括號/引號:`「『(《〈【〖〔[{` 等
|
||
- `bd-close` 結束括號/引號:`」』)》〉】〗〕]}` 等
|
||
- `bd-cop` 句逗號(comma/period,字面偏移因簡繁而異):`,。.`
|
||
- `bd-stop` 句末其餘:`!?;:⁇‼` 等
|
||
- `bd-sep` 分隔/間隔號:`·・‧`
|
||
- `bd-liga` 連續省略/破折:`…—` 等
|
||
- `quote` 彎引號:`“”‘’`
|
||
- **規則正則**:中西間隙邊界、擠壓相鄰組合、禁則對象等,皆以此表組裝。
|
||
|
||
**規則集抽象(`Ruleset`/`charClass`,可配置)**:字符分類不再各 pass 硬編碼,而統一為
|
||
一個**規則集**——`{ cjk, ansAfter, ansBefore, biaodian{6 子類} }`,各為 regex 字元類片段。
|
||
`DEFAULT_RULESET` 即內置默認(上述 CJK/西文/BD_* 值)。消費端經 `options.charClass`
|
||
(`{ cjk?, western?, biaodian? }`)**擴充**,`resolveRuleset(override)` 以**追加**語義合併
|
||
為最終規則集,置於 `ResolvedOptions.ruleset`。各 pass 由此取分類:
|
||
|
||
- `spacing`:`makeRules(ruleset)` 由 `cjk`/`ansAfter`/`ansBefore` **per-instance 編譯**
|
||
全部間隙正則(createJuzhen 各一次,非每 block);`ALNUM`/`ALPHA`/運算符/括號等結構性
|
||
ASCII 集為固定常數、不隨規則集變。
|
||
- `jiya`:`classifyBiaodian(ch, ruleset.biaodian)` 決定 charify 與 bd-* 子類。
|
||
- `jinze`:`anyCjk = RegExp(ruleset.cjk)` 決定詞元邊界之 CJK 判定。
|
||
|
||
**設計取捨**:① **向下兼容**——不傳 `charClass` 則 `ruleset === DEFAULT_RULESET`,行為與
|
||
重構前完全一致(現有測試全綠)。② **追加語義(v1)**——擴充內置類(把未分類字符如英文全角
|
||
句號 `.`U+FF0E 歸入、或納新符號);不支援重分類/移除內置字符(範圍式集合難以挖洞、且需
|
||
求罕見,列為後續)。③ 結構性 ASCII 不開放(非「字符類型」)。
|
||
|
||
> 借鑑 Han 之 `regex/typeset.js`、`regex/unicode.js` 與 Heti 之分類字集,重寫為
|
||
> 現代 TS 常數表+可配置規則集。
|
||
|
||
### 4.3 `locale.ts`——語言與全角/開明政策
|
||
|
||
- `effectiveLang(node)`:最近祖先 `lang`(小寫),無則回退 `options.lang.default`。
|
||
- `classify(lang)`:歸為 `zh-Hans` / `zh-Hant`(含 `zh-TW`/`zh-HK`)/ `ja` / 其他;
|
||
BCP-47 前綴邊界比對(`zh` 命中 `zh-Hant`,不命中 `zhx`)。
|
||
- `style(lang)`:回傳該語言之標點寬度政策——`quanjiao`(全角式)|`kaiming`
|
||
(開明式)|`banjiao`(半角式)。預設:`zh-Hant`/`zh-HK` → 全角式;`zh-Hans`
|
||
→ 開明式;可由 `options.lang.policy` 覆寫(即你要的「依 lang 自由選擇」)。
|
||
|
||
CSS 端對應以 `:lang()` 選擇器落實同一政策(I2/I3)。
|
||
|
||
### 4.4 `dom.ts`——`jz-*` 元素工廠與還原
|
||
|
||
`createEl('jz-char', {class, attrs})`、`wrap(node, el)`、`splitAt`、`insertMarker`、
|
||
冪等標記(`data-jz="<pass>"`)、`revertPass(name, root)`(移除該 pass 之產物、
|
||
解包 `jz-*`、合併回文本節點並 `normalize()`)。
|
||
|
||
`data-jz-kind` 區分產物還原方式:`marker`(純注入,移除)、`wrap`(包裝原內容,解包)、
|
||
**`splitoff`**(行內元素克隆切分之邊界詞元,見 `core/split.ts`/§6.4)。`revertPass` 分
|
||
**兩階段**處理:主迴圈逆序解包 marker/wrap,第二階段再把 splitoff 併回前兄弟(同標籤
|
||
原元素)——順序必要,否則 splitoff 之前兄弟可能仍是 wrap 殼(如 `jz-jinze`)而非原元素。
|
||
|
||
---
|
||
|
||
## 5. 渲染元素詞彙(`jz-*`)
|
||
|
||
自訂元素皆含連字號(合法 custom element 名),預設 `display: inline`(未知元素
|
||
天然 inline),語義中性,可被 `revert` 移除:
|
||
|
||
| 元素 | 用途 |
|
||
|---|---|
|
||
| `jz-hws` | 中西間隙。`user-select:none`,copy-clean,justify 安全(§8) |
|
||
| `jz-char` | 包裝單一標點/漢字,承載 `bd-*` class 與 `data-jz` 屬性 |
|
||
| `jz-inner` | `jz-char` 內層字面容器,供 margin/letter-spacing 擠壓而不動外盒 |
|
||
| `jz-cs` | 字間補償空白(連續/行端擠壓用,於行邊界折疊) |
|
||
| `jz-jinze` | 禁則群組:綁定不可斷單元(亦為 justify 綁定載體) |
|
||
| `jz-orphan` | 垂懸字避免群組:段末末 N 實義字 `nowrap`(§6.5.1) |
|
||
| `jz-em` | 著重號載體 |
|
||
| `jz-ruby`/`jz-rb`/`jz-rt` | ruby/注音 |
|
||
|
||
class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`jiya`、
|
||
`jiya-consecutive`、`hangable`、`jinze-{tou,wei,touwei}` 等。
|
||
|
||
---
|
||
|
||
## 6. 功能點逐項實作方案(對照 Han.css)
|
||
|
||
每項列出:Han 做法 → 我們做法 → 跨瀏覽器 → lang → 我們的調整 → 切片歸屬。
|
||
|
||
### 6.1 中西間隙(HWS,漢字↔西文)— 切片 1
|
||
|
||
- **Han**:插入 `<h-hws hidden>`,內含**真實空格** + `::before{content:' '}`,
|
||
**未加 `user-select:none`** → 選取/複製會帶上空格(其已知缺陷)。
|
||
- **我們(margin 模型)**:finder 邊界感知地把邊界**左側之一個字**(CJK 或西文)
|
||
包進 `jz-hws`,間隙由 CSS `margin-right`(0.25em)提供——**不注入任何空白字元**。
|
||
WHERE 之規則沿用 v1 pangu 規則表(已驗證);作者原寫之 CJK↔西文空格一律移除
|
||
(改由 margin 留隙)。
|
||
- **為何不用空白字元/inline-block(justify 三難)**:純 CSS 下,CJK↔西文間隙在
|
||
`justify` 必有一個邊緣瑕疵——①真實空白→被當詞間空白**暴增**;②inline-block 固定
|
||
寬→罕見落**行首縮排**;③`margin-right`→**行末右緣內縮 0.25em**。選 ③(margin),
|
||
因其行首乾淨、不撐框、不暴增、`textContent` 完全不變(純 copy-clean,複製緊湊),
|
||
並以 §6.1a 之 layout pass 消除行末內縮。
|
||
- **跨瀏覽器**:`margin`/inline 普遍支援。
|
||
- **copy-clean(I1,較 Han 更純)**:間隙為純視覺 margin,`textContent` 不增不減
|
||
語義字元,剪貼板得緊湊原文。
|
||
- **切片**:涵蓋 v1 Pass A、Pass B(pill `code/kbd` 邊界亦走同一機制——左字或
|
||
pill 元素包入 `jz-hws`;pill 由 avoid 與鄰接判定處理)。
|
||
- **邊界正規化一致性(下游回饋 1a/1b 修)**:
|
||
- **1a pill 穿越 jz-jinze**:jinze(order 40)先於 spacing(order 50),會把尾隨
|
||
標點之 pill 納入 `jz-jinze`。`adjacentLogicalChar` 須**透明穿越 `jz-jinze`**
|
||
(與 `jz-hws`、樣式行內同列,見 `finder.isTransparentInline`),否則 pill 左鄰字
|
||
不可達、左緣間隙與作者空格剝除雙失。
|
||
- **1b CJK↔CJK 贅餘空格**:中文不用詞間空格,故 CJK 表意字之間的單一 ASCII 作者
|
||
空格(含跨樣式行內標籤,`collectRuns` 已透明合併)一律**剝除、不留隙**(以
|
||
`DEL` 哨符 1:1 替換空格保持對齊,`processBlock` 據此刪空格而不插 `jz-hws`)。
|
||
與 CJK↔Latin 之剝除一致——凡 inline 邊界之贅餘作者空格皆統一處理。全形空格
|
||
U+3000 不動。
|
||
- **西文字集之完整化(下游回饋 D;列舉法)**:CJK↔西文之邊界由 `unicode.ts` 之
|
||
`ANS_AFTER_CJK`/`ANS_BEFORE_CJK`(共用片段 `WEST_SHARED`)界定。原僅涵蓋拉丁基本
|
||
+拉丁-1+部分希臘,致**貨幣符號**(`€` U+20AC 等整塊 U+20A0–20BF)、**拼音聲調**
|
||
(`ā ǎ ē` 等拉丁擴充 A/B U+0100–024F)、**西里爾**、**數學運算符**(U+2200–22FF)
|
||
不被視為西文 → 鄰接 CJK 不補隙、手工空格亦不剝除。改採**逐區塊列舉**補全(見
|
||
`WEST_SHARED` 註記),並把希臘由原 U+0370–037D 補全至整塊 U+0370–03FF(原漏
|
||
`Α-Ω α-ω` 本身)。**取捨**:選列舉(whitelist)而非 `\p{…}` 類別法,因類別法之
|
||
集合減法須 `v` flag(ES2024),會破壞 2016/Windows 10 時代相容性下限;列舉法所有
|
||
範圍皆 BMP、以 `\uXXXX` 表示於字元類內無需任何 flag。代價:新區塊需手補。
|
||
- **全角省略號不誤判(下游回饋 B)**:`DOTS_CJK` 原以 alternation `([\.]{2,}|…)`
|
||
匹配「省略號後接 CJK 補隙」,但 `|…` 誤把**全角省略號 U+2026** 納入;全角 `…/……`
|
||
屬 CJK 標點(已由 jiya charify 為 `bd-liga`),後接 CJK 不應有中西間隙。移除 `|…`,
|
||
僅留 ASCII `...`。
|
||
|
||
### 6.1a 行末去隙(gapTrim,justify 修正)— 切片 1
|
||
|
||
- **問題**:margin-right 落在行末時,justify 下使右緣內縮 0.25em。
|
||
- **我們**:layout pass `gapTrim` 量測各 `jz-hws`,對「其字落於行末(後一內容在
|
||
更低行)」者加 `jz-hws-trim`(CSS `margin-right:0`),該行遂齊右、無內縮。掛
|
||
`ResizeObserver`+字型載入後重算。純版面、瀏覽器專用(jsdom 跳過)。與 `lineedge`
|
||
共用量測工具與 layout 安裝器。
|
||
- **自我應驗誤判(下游回饋 C/容器寬相關之「最後一個間隙消失」)**:間隙之 margin-right
|
||
會**參與斷行**。若依「後字是否在更低行」判行末,而後字落點又被 margin 推擠,便自相矛盾
|
||
——間隙 margin 把後字擠到次行 → 判行末 → trim 去 margin → 後字回同行,振盪停在「已
|
||
trim、後字同行、間隙消失」之錯誤終態(`ResizeObserver` 只觀 root 尺寸、不因內部 margin
|
||
變化重觸發,故卡死;resize 容器則該處重判、間隙「恢復」或轉移)。**污染雙向**:不僅當前
|
||
間隙自身 margin,**下游間隙之 margin** 亦會令夾在中間之內容(如 `或<gap>$1299<gap>`)
|
||
放不下其行、致後字假性換行。
|
||
- **修法**:`trimGaps` 改為**起始全部 trim**(margin 全 0),再**由左至右逐一定案**。量測
|
||
某間隙時其前皆已定案、其自身與其後全部仍 trim,故後字落點不受任何 margin 污染,純粹反映
|
||
「無本間隙 margin 時後字是否仍被擠到次行」:仍在更低行(或無後字)→ 真行末、維持 trim;
|
||
在同行 → 行內容得下、非真行末 → 還原間隙。逐一必要(斷行左→右因果);以末行 rect
|
||
(`lastRect`,margin 實際落點)為準,兼顧跨行 pill(行尾 `code` 折行亦曾致右側間隙誤消)。
|
||
實 Chromium 多寬度(8–22em、ragged/justify)掃描確認:偽 trim 由「某些寬度存在」歸零;
|
||
真行末仍 trim(justify 齊右不變)。
|
||
|
||
### 6.1b 標點稽核:CJK 標點不生誤隙(下游回饋)
|
||
|
||
- **不變量**:任何現代漢語標點介於 CJK 之間,皆**不**補中西間隙。實 Chromium 稽核全部
|
||
常用標點(。,、;:?!…… —— ·~ 簡繁引號 “”‘’「」『』、各式括號 ()【】《》〈〉
|
||
[]{}〔〕〖〗 等)× 簡繁兩政策共 104 例,jz-hws 皆 0、textContent 不變。
|
||
- **修正之偏例**:①全角省略號 `……`(§6.1 之 B:`DOTS_CJK` 之 `|…` 誤匹配,已移除);
|
||
②收尾彎引號 `”`(原 pangu `QUOTE_AN` 規則令 `”` 後接拉丁/數字補隙,使其成為唯一會補隙
|
||
之收尾標點,與 `」』)》】〕` 皆不補不一致;clreq 下全角收尾標點字身已內含右側留白,後接
|
||
拉丁不需額外隙——已移除 `QUOTE_AN` 以求一致、消除誤隙)。
|
||
|
||
### 6.2 長英文詞斷詞包裝 — 切片 1
|
||
|
||
- **我們**:把 `[A-Za-z]{N,}` 長串包進 `<span lang="en">`,配合消費端
|
||
`hyphens:auto` 由瀏覽器原生斷詞渲染連字符(DOM 不含連字符 → 複製不污染)。
|
||
直接沿用 v1 Pass C。
|
||
- **跨瀏覽器**:`hyphens` 各家支援良好(含 `-webkit-`)。
|
||
- **切片**:v1 對等。
|
||
|
||
### 6.3 標點擠壓(Jiya)— 切片 1
|
||
|
||
- **Han**:charify 標點為 `<h-char class="bd-jiya …"><h-inner>`,以負
|
||
`letter-spacing`/`margin` 擠壓;連續、行端、簡繁差異以 CSS + `:lang()` 落實
|
||
(`bd-cop:lang(zh-Hant)` → 不擠=全角式)。
|
||
- **我們**:同構——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`。
|
||
- **句末標點集含全角句點 `.`(下游回饋)**:`BD_STOP` 為 `。.!?`——除表意句號 `。`
|
||
(U+3002)外,納入**全角句點 `.`**(U+FF0E)。實測 Noto CJK SC/TC 之 `halt`(GPOS
|
||
SinglePos 調 advance)皆覆蓋 `.`、挤壓收 0.5em,與 `。` 一致;`inkBias` 同步補 `.`
|
||
(簡 left/繁 center),margin 後備亦可挤。政策行為與 `。!?` 同:quanjiao/kaiming 句末
|
||
保持全角、banjiao 挤半。
|
||
- **halt 即字型之「挤壓意願」(設計原則)**:字型對某標點提供 `halt` =該字形適合挤半;
|
||
**不提供**=該字形不宜挤(占滿字框,如 Noto TC 之全角 `?` 與 SC 不同、`〈〉` 無 halt)。
|
||
故聚珍**信任字型 halt 覆蓋**:有 halt 字型中個別無 halt 之字符,**保持全角、不強挤**(優雅
|
||
降級),不為其特設規則。`margin` 後備僅用於**整體無 halt** 之字型(GBK 老字型等),以
|
||
`inkBias` 逐字型調教近似 halt——非用來強挤「有 halt 字型刻意不挤」之字符。
|
||
- **連續標點挤壓規則(雙側獨立 + 透明穿越;下游回饋)**:相鄰兩標點,**對每一標點**獨立
|
||
判斷——若其「有空白之側」緊鄰另一標點,則挤掉該側空白(收半形 `jz-half`)。故 `,「`
|
||
(pause 右空白 + open 左空白)兩側皆收 → 全角式與開明式皆合占 1em;`。」`(stop 右空白;
|
||
」左為墨)只收 `。`→ 全角式 1.5em/開明式 1em(」baseline 已半);`·—`(皆無指向對方之
|
||
空白)皆不收 → 兩式皆 2em(不宜挤之例外)。多標點鏈逐對處理。`jiyaAdjacencyPass` 以**從 root 之透明遍歷**(穿越 strong/
|
||
em… 等樣式行內;block 邊界與 avoid 子樹斷相鄰;jz-char 為原子不下探)取代原「同 parent
|
||
直接兄弟」掃描,使**跨 inline 邊界**之連續標點亦正確配對(原漏致跨 strong/em 之標點對未挤、
|
||
達 2em)。
|
||
- **`margin` 後備模式(.feedback 增補)**:消費端字型未必含 `halt`(多數系統字型、
|
||
GBK 老字型,實測**方正書宋 GBK 僅含 `vert`、無 `halt`** → halt 模式不擠)。故提供
|
||
後備:`jiya: { halfWidth: "margin" }`。**機制**(演進後):把 `jz-char` 之 inline-block
|
||
寬收為 `width:0.5em`(真半形 advance——相鄰兩標點各 0.5em、合 1em,無負 margin 之
|
||
鄰接疊加/單側重疊問題),字身 `jz-inner` 以 `overflow` 溢出、再依墨色偏側左移
|
||
(right −0.5em/center −0.25em/left 0),使墨色落入 0.5em 盒、空白溢向外側——即以
|
||
CSS 盒模型重現 `halt`。偏側由 `core/unicode.ts` 之 `inkBias(ch, langClass, overrides)`
|
||
依**字身墨色中心**給出 `left|center|right|full`(簡體點號偏左、繁體居中;彎引號
|
||
`“”`/角括號 `「」`/尖括號 `〈〉` 墨強偏一側→開 right、閉 left;圓方書名括號
|
||
`()【】《》…` 近中→center),charify 時標為 `jz-ink-{left,center,right}`。消費端可
|
||
經 `jiya.bias` 逐 lang 逐字覆寫;配字工作流與量測工具 `tools/measure-bias.py` 見 README。
|
||
- **激活與粒度(I3/I10)**:模式由 `data-jz-halfwidth` 屬性**所在子樹**決定(CSS
|
||
後代選擇器)——`render()` 在全域 margin 時自動掛於 root;亦可手寫於任一祖先作逐區塊
|
||
/逐語言控制(含 `margin` 頁中嵌 `halt` 島之 halt-reset,憑來源順序勝出)。bias
|
||
class 恆於 charify 標記,halt 模式不讀,故模式切換為純 CSS、無需重 render。實測:
|
||
方正書宋 GBK(開明式 zh-Hans)下同句行寬 `24.0em(halt 無效)→ 19.7em(margin 生效)`、
|
||
單一句內點號 `1.0em → 0.5em`。
|
||
- **切片**:涵蓋並超越 v1 Pass D(開明式),加入全角式、lang 切換、行內連續與
|
||
行端擠壓、`halt`/`margin` 雙渲染。
|
||
|
||
### 6.4 禁則(Jinze,避頭尾)+ justify 綁定 — 切片 1(基礎)/後續(完整)
|
||
|
||
- **Han**:`h-jinze` 綁定行首行尾不可出現之標點與鄰字,避免標點落行首/行尾。
|
||
- **我們**:`jz-jinze`(CSS `display:inline-block; white-space:nowrap`)綁定不可斷
|
||
單元。**避頭尾已實作**(`jinze.ts`):因 jiya 把標點原子化為 inline-block,原子
|
||
邊界成可斷點、破壞瀏覽器原生 UAX#14 避頭尾,故補回——把「不可置行首」之標點
|
||
(閉類/點號/間隔/連接省略)綁到**前一字**、「不可置行末」之開類綁到**後一字**,
|
||
且**只隔離單一邊界字**(長引文中段仍正常斷行,不溢出)。實作為兩階段:依原始
|
||
相鄰算禁斷邊界→切邊界字;重掃併入 nowrap 群組。亦為 §8 justify 綁定載體。
|
||
- **行內元素最小綁定(`core/split.ts`,下游 Bug 修復)**:邊界字落於**樣式行內元素**
|
||
(`strong`/`em`/`span` 等)內時,舊版整個元素入 `jz-jinze`——`<strong>很長一串</strong>,`
|
||
會把整串粗體塞進 `nowrap` inline-block,行末放不下即**整體掉行、上行留巨大空隙**。
|
||
改以 `isolateBoundaryToken(el, side)` **克隆切分**:沿「文本節點→邊界元素」祖先鏈逐層
|
||
把邊界詞元剝入一個淺克隆(保留標籤/class/style/lang,去 id),只綁該克隆(最小綁定),
|
||
原元素其餘部分留原位、渲染視覺無縫。克隆標 `data-jz-kind="splitoff"`,`revertPass` 之**延後
|
||
階段**(先解包 wrap、後併 splitoff,確保前兄弟為原元素而非 wrap 殼)併回原元素、還原一致
|
||
(I6)。僅純樣式標籤白名單可切;`a`/`abbr`/`ruby`/帶 id 者退化為整元素綁定(安全)。
|
||
- **切片**:切片 1 完成行首行末避頭尾(閉類/點號避頭、開類避尾);行首縮排聯動、
|
||
更細之禁則對象列後續,沿用同一 `jz-jinze` 結構。
|
||
- **已知交互(待瀏覽器精修)**:`lineedge` 之行端偵測需跨 `jz-jinze` 群組邊界向上
|
||
攀爬找流內鄰字(已於 `edgeRect` 處理 jz-* 包裹之攀出);屬 layout 層,jsdom 無法
|
||
驗,須瀏覽器目視。
|
||
|
||
### 6.5 標點懸掛 + 垂懸字避免 + 行端禁則 — 整體實作(已落地)
|
||
|
||
三個相關機制之**整體實作**(同步引入),皆處理「行端/段末之標點與字」;其分層職責須先
|
||
釐清,以免重複處理或衝突。**前置**為「功能分級」(見 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 段落級 + 元素標記(前置基礎設施)
|
||
|
||
**動機**:懸掛/垂懸字/禁則等為**段落級**功能(依賴行/段布局),不應作用於標題、按鈕、單行
|
||
或單元素文本;而中西間隙、標點挤压為**文本級**,任意文本片段皆適用且想要。下游須能**方便標記**
|
||
元素屬哪級。此為貫穿性基礎設施(已實作:`Finder.levelFor`/`levelAllows`、各 pass `level`
|
||
元數據、選項 `level.text`/屬性 `data-jz-level`)。
|
||
|
||
**分類(含現有與待加功能)**:
|
||
|
||
| 級別 | 功能 | 判據/依賴 |
|
||
|---|---|---|
|
||
| **文本級** | `spacing`(中西間隙)、`jiya` baseline 政策半形、`jiya` 連續挤压(adjacency) | 僅字符鄰接,對單行/單元素文本即有意義 |
|
||
| **段落級** | `jinze` 避頭尾、行端(半形/懸掛)、`gapTrim`、`orphan` 垂懸字、`longWord` 斷詞 | 需行/段布局(換行、行端、段末、justify)才有意義 |
|
||
| 貫穿 | `charClass`、原子透明、normalize | 基礎設施,不分級 |
|
||
|
||
直覺:文本級=「字符級排版」(間隙、標點形態);段落級=「行/段級排版」(換行、行端、段末)。
|
||
|
||
**標記 API(下游)**:
|
||
- 預設推斷:塊級元素(`blockTags`:p/div/li/td…)→ **段落級(全功能)**。
|
||
- 顯式降級:`data-jz-level="text"` 於元素/祖先 → 該子樹**僅文本級**(段落級功能自動關);
|
||
`data-jz-level="paragraph"`(或不標)→ 全功能。
|
||
- 選項 `level?: { text?: string }`:選擇器一鍵標記(如 `level:{ text: "h1,h2,h3,button,.ui" }`)。
|
||
- 典型:標題/按鈕/UI 文字標 **text-level**(要間隙與標點半形、不要懸掛/避孤字/避頭尾);
|
||
正文 `<p>` 預設全功能。
|
||
|
||
**實現**:
|
||
- 每個 pass 加元數據 `level: "text" | "paragraph"`。
|
||
- `finder.levelFor(el)`:查 `data-jz-level` 祖先 + 選項 `level.text` 選擇器 + 預設(塊→段落)。
|
||
- gate:段落級 pass 對 text-level 元素跳過;文本級 pass 不受限。與現有 `blocks`/
|
||
`featureEnabledFor` 疊加(level=粗粒度語義組先判,blocks=細粒度逐功能後調)。
|
||
- **文本級功能須支持非塊/單元素**:`spacing` 現用 `eachBlock`,須確認 scope root 為 inline/
|
||
單元素時亦能處理(或放寬走訪),使單行標題等之中西間隙、標點半形生效。
|
||
|
||
**與本批關係**:先落地分級(pass 標 level、`levelFor`、`data-jz-level`/選項)→ orphan/hanging
|
||
註冊為段落級,自動只作用段落級元素、跳過 text-level 者。實作順序插於 6.5.5 之首。
|
||
|
||
#### 6.5.0 四機制之分工與邊界(核心約定,實作前必讀)
|
||
|
||
| 機制 | 作用層 | 唯一職責 | 手段 | 狀態 |
|
||
|---|---|---|---|---|
|
||
| 禁則(§6.4 jinze) | **斷行層** | 把標點趕到正確的邊(點號/閉類→行末、開類→行首;連續標點不拆) | nowrap `jz-jinze` | 已實作 |
|
||
| 行端半形(lineedge 現有) | **渲染層·紧排** | 行首開類/行末閉類**收半形** | `jz-half-le`(halt) | 已實作(將併入行端模式) |
|
||
| 標點懸掛 | **渲染層·凸出** | 行末點號/閉類**右凸**、行首開類**左凸**出版心 | 負 `margin` | ⛔ 實作後停用(見 6.5.6) |
|
||
| 垂懸字避免(本計劃) | **斷行層** | 段末行 **CJK 實義字數 ≥ N** | nowrap `jz-orphan` | 待實作 |
|
||
|
||
- **正交性**:斷行層(禁則/垂懸字,管「哪些字符在哪行、不可斷」)與渲染層(半形/懸掛,管
|
||
「邊緣標點凸出/收縮」)作用於不同 CSS 維度(`white-space` vs `margin`),可疊加共存。
|
||
- **協同**:禁則決定標點落行首或行末 → 半形/懸掛對落在邊緣者施收縮/凸出。
|
||
- **行端「半形 vs 懸掛」互斥**(同屬渲染層):將 lineedge 行端處理升級為可配模式
|
||
`lineEnd: "squeeze"(現有半形)|"hang"(懸掛)|"none"`,二者共用行端標點偵測碼。
|
||
- **垂懸字 vs 懸掛邊界**(已研究):垂懸字限「末行實義字 ≥ N」、懸掛限「邊緣標點凸出」。唯一
|
||
重疊=**純標點末行**(實義字=0):懸掛可「吸回」(不動正文)、垂懸字綁定可「拉字下來」。
|
||
約定:方案 C(無測量)**附帶**消除純標點末行(副作用,現有益無害);未來若改由懸掛吸回
|
||
(更優),把 C 綁定**條件化**(僅末行實義字 ∈ [1,N−1] 才綁),借懸掛 layout pass 之測量協同
|
||
判定,C 不獨立測。末行實義字 ∈ [1,N−1] 僅垂懸字(換行)可解(懸掛不增實義字)。
|
||
|
||
#### 6.5.1 垂懸字避免(方案 C,斷行層,無測量)— 已實作
|
||
|
||
- **目標**:段末行 CJK 實義字數 ≥ N(預設 2;標點/符號不計)。單行段無效。
|
||
- **機制**:standalone pass,遍歷作用域段落;自段末向前數 N 個 CJK 實義字(`ruleset.cjk` 判定、
|
||
跳過標點/空白/西文計數),把「第 N 個實義字起至段末」(含其間/尾隨標點)包入 `<jz-orphan>`
|
||
(CSS `white-space: nowrap`)。
|
||
- **保證(數學、免測 layout)**:該整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型/
|
||
瀏覽器成立。平時無害(末行本多字時不改變),僅末行將塌為 <N 字時「拉字下來」。
|
||
- **選項**:`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 整體
|
||
納入 jz-orphan,並處理邊界字定位**跨 jz-jinze 攀爬**(實作細節)。免重複包裹。
|
||
- **CSS**:`jz-orphan { white-space: nowrap; }`。`revert`:wrap-class,隨 `revertPass` 還原。
|
||
- **測試**:jsdom 結構(末 N 實義字+尾標點入 jz-orphan;符號不計;單行段/實義字<N 不綁);
|
||
headless 窄容器拖動驗末行恒 ≥N。
|
||
|
||
#### 6.5.2 標點懸掛(Hanging,渲染層,layout)— ⚠ 已實作後停用
|
||
|
||
> **停用(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-*
|
||
`hanging-punctuation: none`。故**自管負 margin**。
|
||
- **機制**:layout pass(複用 lineedge `installLayout`:立即跑+`ResizeObserver(root)`+
|
||
`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
|
||
|
||
- 禁則避頭尾與 squeeze 行端半形協同(禁則趕標點到邊、行端收半形);jz-orphan 與 jz-jinze 嵌套
|
||
已處理(邊界字跨 jz-jinze 攀爬)。lineedge 偵測已跨 jz-jinze 攀爬。jinze 本身未大改。
|
||
(懸掛已停用,故無「懸掛凸出最外側」之協同需求。)
|
||
|
||
#### 6.5.4 統一 pass order
|
||
|
||
`20 charify → 25 jiyaAdj → 40 jinze → 50 spacing → 60 longWord → 70 orphan(垂懸字)→
|
||
90 lineEdge(行端 squeeze;hang 已停用)→ 92 gapTrim`。
|
||
|
||
#### 6.5.5 實作順序與結果
|
||
|
||
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 標點懸掛停用記錄與未來路徑
|
||
|
||
- **停用結論**:純負 `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)
|
||
|
||
- **Han**:`subst`/`biaodian` 修正標點變體、位置。
|
||
- **我們**:opt-in pass,預設**只分類+打 class**(不改文字,I1);顯式開啟才做
|
||
變體替換(簡↔繁標點、全半形修正),且可 `revert`。
|
||
- **預留**:`order=10` 最先;產物為 `jz-char` 之 class/屬性,與 charify 銜接。
|
||
|
||
### 6.7 著重號(Emphasis)— 後續
|
||
|
||
- **Han**:逐字包裝加圈點。
|
||
- **我們**:優先用原生 `text-emphasis`(Chrome/FF/Safari 支援良好);需精細
|
||
控制時退回 `jz-em` 逐字包裝 + CSS 圓點。lang 感知(位置簡繁略異)。
|
||
- **預留**:charify 能力直接複用。
|
||
|
||
### 6.8 注音/Ruby — 後續(最複雜)
|
||
|
||
- **我們**:基於原生 `<ruby>` + `jz-ruby/jz-rt` 處理注音符號直排與聲調定位。
|
||
- **預留**:獨立 pass,`order=80`;不影響其餘。
|
||
|
||
### 6.9 樣式正常化/字型歸屬 — 貫穿(CSS)
|
||
|
||
- **normalize**:`juzhen.css` 內以 `@supports`/`:where()` 提供 CJK normalize(關閉
|
||
與託管擠壓重複作用之原生 `text-spacing-trim`/`text-autospace`、間隙 `user-select`
|
||
等),限 `.juzhen` 域、非破壞。
|
||
- **字型不歸聚珍管(設計決策,下游回饋)**:曾內建依 `:lang` 之 Noto 字型堆疊作「後備」,
|
||
但字型屬消費端領域、內建即越權(且曾蓋掉消費端字型釀 bug)。故**移除 `_lang.css`**,
|
||
逐語言 `font-family` 完全交消費端 CSS。聚珍僅守三項非管理性本分:① `_normalize.css`
|
||
之 `jz-* { font: inherit }` 使原子繼承消費端字型、不強加;② `_jiya.css` 半形之
|
||
`font-feature-settings` 顯式併入 `"locl" 1`(連同 `halt`),確保泛 CJK 字型之地域標點
|
||
字形不因設 halt 而失;③ render 只讀不改 `lang`。body 文本之 `locl` 本就不受聚珍影響
|
||
(未設任何 feature),由消費端字型+正確 `lang` 決定。消費端範式見 README「字型與 locl」。
|
||
- **B 案(字型參數)之否決**:以 JS 參數管字型須注入 CSS、違反「JS 注入結構/CSS 給視覺」
|
||
分工;而「缺 halt 字型須逐字型微調 margin」之指引由既有 `jiya.halfWidth`/`jiya.bias`
|
||
參數+文檔承擔,無需新參數。
|
||
- **跨瀏覽器**:`:where()`/`:lang()`/`font-feature-settings` 普遍支援。
|
||
|
||
### 6.10 計數器/緊排(counter/well-knit)— 低優先
|
||
|
||
- 後續以純 CSS 模組加入,不涉 JS 引擎。
|
||
|
||
---
|
||
|
||
## 7. 第一垂直切片(v1 對等)
|
||
|
||
**範圍**:核心基座(finder/unicode/locale/dom/pass 管線)+ 以下功能 pass:
|
||
|
||
| v1 | 切片 1 對應 | 機制 |
|
||
|---|---|---|
|
||
| Pass A 中西間隙 | `spacing` | `jz-hws`,copy-clean,justify 安全 |
|
||
| Pass B pill 邊界 | `spacing`(同引擎) | avoid + 鄰接判定 |
|
||
| Pass C 長英文詞 | `longWord` | `<span lang>` + `hyphens` |
|
||
| Pass D 開明式擠壓 | `jiya` + `jinze` | 跨瀏覽器負 margin + `:lang` 全角/開明 + justify 綁定 |
|
||
|
||
**交付**:可 `build` 出 ESM/IIFE/CSS/d.ts;jsdom 測試覆蓋 DOM 變換正確性
|
||
(包裝、class、邊界、冪等、copy-clean、lang 政策);justify/簡繁 demo 頁供瀏覽器
|
||
目視驗證版面(layout 行為無法於 jsdom 驗證,見 §12)。
|
||
|
||
**擴充保證**:切片 1 所用之 finder/charify/pass 管線/`jz-*` 詞彙/CSS-JS 契約,
|
||
即後續所有 Han 功能所用者;新增功能 = 新增一個遵守 §2 不變量的 pass,不改切片 1。
|
||
|
||
---
|
||
|
||
## 8. justify 句末標點單份間距解法(要求 #3)
|
||
|
||
**問題**:`text-align: justify` 下,UA 在「對齊機會點」插入伸縮量。全角句末標點
|
||
(如 `。`)字面只占字框一部分、自帶半形留白;若 UA 在其**兩側**都開伸縮,標點
|
||
看似分到「兩份」間距(伸縮 gap + 自身留白)。
|
||
|
||
**解法**(CSS 主導,I2/I5):
|
||
|
||
1. charify 句末/句逗標點為 `<jz-char class="bd …"><jz-inner>。</jz-inner>`。
|
||
2. `jz-char` 設 `display: inline-block`:使其成為 justify 的**單一原子項**,內部
|
||
不開伸縮 gap,標點與其字面留白同屬一項,對齊伸縮只落在**項與項之間** → 標點
|
||
只貢獻一份。
|
||
3. **開明式**下,`jz-inner` 以負 letter-spacing 擠去半形留白,該項本身即半形,
|
||
justify 一份 gap 即足。
|
||
4. **全角式**下,留白保留於原子項內,仍只算一份;以 `jz-jinze` 綁定確保不在留白
|
||
側額外開機會點。
|
||
|
||
> inline-block 影響基線對齊與行內換行,須以 CSS 校正(`vertical-align`、行高),
|
||
> 並**跨 Chrome/Safari/FF 目視驗證**(§12);此為切片 1 的重點驗證項。
|
||
|
||
**分頁器相容(下游回饋問題二)**:常規流式版面 inline-block 原子正常,但**分頁器**
|
||
(Paged.js)之對齊引擎會把每個 inline-block 邊界當伸縮點、與 CJK inter-ideograph
|
||
疊加,於窄欄混排散架。故提供 `options.justifyAtoms`(預設 true);設 false 時 render
|
||
於 root 掛 `data-jz-atoms="inline"`,CSS 令 `jz-char`/`jz-jinze` 改 `display:inline`
|
||
(halt 半形與 jinze nowrap 仍保留)。此即 R1 預案之一(解耦對齊原子化),切換僅 CSS、
|
||
不動 JS。注意:`margin` 後備半形依賴 inline-block 之 `width:0.5em`,與
|
||
`justifyAtoms:false` 不可兼得——分頁器場景須用含 `halt` 字型。
|
||
|
||
**原子透明原則(繼承屬性漏溢,下游回饋)**:`jz-char`/`jz-jinze`(及 `margin` 模式之
|
||
`jz-inner`)為 inline-block,而 inline-block 自成區塊容器。凡「作用於區塊容器首/末行」之
|
||
**繼承屬性**,會被每個單字標點原子各自重新套用一次,使「用聚珍」與「未用聚珍」表現不一致。
|
||
**作用範圍判斷**:此類間距/位移源於我們生成之原子,屬本庫責任,由庫內統一免疫(消費端無需
|
||
配置);且須以「同類機制」整體界定範圍——要麼全修、要麼全交下游,不做逐案補丁。修法
|
||
(`_normalize.css`)於原子上歸零此類屬性,只切斷其向原子內部之再套用,段落本身之同名屬性不
|
||
受影響(仍作用於段落自身行盒)。
|
||
|
||
經 headless Chrome 逐屬性實測,屬此類者僅兩項,皆已歸零:
|
||
|
||
- `text-indent: 0`——段落首行縮排傳入後,每原子內部首行各縮排一次,shrink-to-fit 寬膨脹為
|
||
`縮排量+字形`,標點附近冒出巨大間距。實測:歸零後段落首行縮排仍保持 2em,原子寬由 ~3em
|
||
回到 0.5em/1em。
|
||
- `hanging-punctuation: none`——同屬區塊容器首/末行機制,每原子各自懸掛自身標點。Chrome
|
||
未實作(測不出)、Safari 會觸發,且與規劃中之「標點懸掛」(§任務 6)相衝,故按機制一併歸零。
|
||
|
||
**不歸零者**(實測不洩漏或本應逐字穿透):`letter-spacing`/`word-spacing`(原子邊界處理
|
||
一致、無翻倍,實測 delta 與基線同);`text-align`/`text-align-last`/`text-justify`
|
||
(halt 為 shrink-to-fit、margin 為內容溢出,皆無對齊餘量,兩模式實測 `jz-inner` 偏移不變);
|
||
`text-transform`/`line-height`/`color`/`font`(逐字內容或縱向屬性,須穿透原子);
|
||
`word-break`/`overflow-wrap`/`hyphens`/`white-space`/`tab-size`/`text-emphasis`/
|
||
`text-shadow`/`font-variant`/`font-stretch`(逐屬性實測 delta 皆與基線同,不洩漏)。
|
||
|
||
**透明之另一面——欠套用(`text-decoration`)**:與上述「過套用」相反,`text-decoration`
|
||
**非繼承**屬性,由段落之「裝飾盒」統一繪製並跨後代延伸;但原子級 inline-block 會被裝飾盒
|
||
**跳過**,致段落下劃線在每個標點原子下斷缺(像素掃描實測:純文字 coverage 1.0、聚珍僅
|
||
~0.25)。故反向處理——令原子 `text-decoration: inherit` 承段落裝飾自繪,因共用基線與字型
|
||
度量而無縫接續(`_normalize.css` 設 `jz-char`/`jz-jinze`;`margin` 模式 `jz-inner` 亦為
|
||
inline-block,於 `_jiya.css` 補一條)。實測:halt/margin 兩模式 coverage 皆回 1.0,且
|
||
`margin` 下劃線隨半形收為 0.5em advance。**此為「同類問題」整體界定之一部分**:凡因原子化
|
||
而生之版面瑕疵,要麼庫內統一修復、要麼統一交下游;本庫選前者,使原子對消費端樣式透明。
|
||
|
||
---
|
||
|
||
## 9. 跨瀏覽器策略
|
||
|
||
- **一致性關鍵層**(間隙、擠壓、justify):只用 `margin`、`inline-block`、
|
||
`user-select`、`white-space`、`:where()`、`hyphens`,及字型 OpenType
|
||
`font-feature-settings:"halt"`(標點半形;各引擎一致,效果僅依字型是否含 halt,
|
||
Noto CJK 具備,缺則優雅降級為全形)——三大引擎(Blink/WebKit/Gecko)皆穩定
|
||
支援。**注意**:標點半形預設不用負 `margin`(對居中字形會重疊),改用 `halt`(見
|
||
§6.3 修訂)。**缺 `halt` 字型之後備**:`jiya.halfWidth:"margin"` 以 `width:0.5em`+
|
||
字身依偏側(`jz-ink-*`)溢出定位,於 CSS 重現 halt、真半形 advance(見 §6.3)。
|
||
- **漸進增強(非必要)**:`text-spacing-trim`(Blink)、`hanging-punctuation`
|
||
(WebKit)、`text-emphasis`、`text-autospace`——僅在支援時錦上添花,缺席不影響
|
||
基本排版(I4)。
|
||
- **驗證矩陣**:Chrome(Blink,含 VSCode/Electron)、Safari/WebKit、Firefox
|
||
(Gecko)。demo 頁含三引擎可開的測試案例。
|
||
|
||
### 9.1 顯式禁用不一致原語(I4)
|
||
|
||
當原生原語在各引擎支援不一,或會與我們託管之版面**重複作用**(雙重擠壓/間隙)
|
||
時,在 `.juzhen` 範圍內以 `@supports` 偵測並**主動關閉**,使所有引擎收斂於我們的
|
||
一致路徑:
|
||
|
||
```css
|
||
@supports (text-spacing-trim: space-all) {
|
||
.juzhen { text-spacing-trim: space-all; } /* 關閉 Blink 原生 CJK 標點 kerning */
|
||
}
|
||
@supports (text-autospace: no-autospace) {
|
||
.juzhen { text-autospace: no-autospace; } /* 關閉原生中西自動間距 */
|
||
}
|
||
```
|
||
|
||
政策:凡屬「一致性關鍵層」者(間隙、擠壓),一律由我們統一處理並禁用對應原生
|
||
原語;凡屬「漸進增強」且不致衝突者(如 Safari 之 `hanging-punctuation`,在我們未
|
||
託管懸掛時),方任其生效。此清單隨功能演進維護於 `css/_normalize.css`。
|
||
|
||
---
|
||
|
||
## 10. 模組/檔案佈局
|
||
|
||
```
|
||
src/
|
||
index.ts createJuzhen / Juzhen / pass 註冊 / render / revert
|
||
core/
|
||
pass.ts Pass 介面 + 管線執行器
|
||
finder.ts 遍歷/avoid/charify/邊界感知 replace
|
||
unicode.ts 字元類 + 標點子類 + 規則正則表
|
||
locale.ts 有效 lang / 分類 / 全角開明政策
|
||
dom.ts jz-* 工廠 / wrap / split / marker / revert
|
||
typeset/
|
||
spacing.ts 中西間隙 [切片 1]
|
||
longword.ts 長英文詞包裝 [切片 1]
|
||
jiya.ts 標點擠壓 + 全角開明 [切片 1]
|
||
jinze.ts 禁則 + justify 綁定 [切片 1 基礎]
|
||
hanging.ts 標點懸掛 [後續]
|
||
biaodian.ts 標點修正 [後續]
|
||
emphasis.ts 著重號 [後續]
|
||
ruby.ts 注音/ruby [後續]
|
||
css/
|
||
juzhen.css 入口(@import 各 partial)
|
||
_normalize.css _spacing.css _jiya.css _jinze.css
|
||
_hanging.css _emphasis.css _ruby.css (字型不內建,交消費端)
|
||
test/ jsdom DOM 正確性測試
|
||
demo/ 瀏覽器 layout/justify/簡繁 目視驗證頁
|
||
build.mjs esbuild:ESM + IIFE + CSS;tsc:d.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 構建與發佈
|
||
|
||
- **源碼**:TypeScript(strict),模組化。
|
||
- **構建**:`build.mjs` 以 esbuild 產出
|
||
- `dist/juzhen.mjs`(ESM,供打包器/TS)
|
||
- `dist/juzhen.iife.js`(IIFE,全域 `Juzhen`,原生 `<script>` 直跑——保留
|
||
build.sh/Nvim peek 之單檔串接消費)
|
||
- `dist/juzhen.css`(合併之樣式)
|
||
- `tsc --emitDeclarationOnly` → `dist/juzhen.d.ts`
|
||
- **發佈物**:`dist/` 提交入庫(消費端可直接取單檔/CSS)。
|
||
|
||
---
|
||
|
||
## 12. 測試與驗證策略
|
||
|
||
- **jsdom(自動)**:驗證 DOM 變換——包裝結構、`bd-*` class、邊界處理、冪等、
|
||
`revert` 還原、copy-clean(`textContent` 不被注入污染)、lang 政策分支。
|
||
- **瀏覽器(目視,demo 頁)**:layout 相關**無法**於 jsdom 驗證——justify 單份
|
||
間距、擠壓視覺量、行端折疊、懸掛、inline-block 基線。demo 頁載 Noto CJK,簡繁
|
||
並列、可切 justify、附選取/複製測試區,於 Chrome/Safari/FF 目視核對。
|
||
- 文件明確標注哪些結論來自自動測試、哪些待瀏覽器目視。
|
||
|
||
---
|
||
|
||
## 13. 擴充性保證(如何加新功能而不返工)
|
||
|
||
新增任一 Han 功能點,標準流程:
|
||
|
||
1. 在 `unicode.ts` 補該功能所需字元類/正則(集中、不散落)。
|
||
2. 新增 `typeset/<feature>.ts`,實作 `Pass`(`render`/`revert`/`order`)。
|
||
3. 在 `index.ts` 註冊,加 `options.<feature>` 開關。
|
||
4. 在 `css/_<feature>.css` 寫視覺規則(I2:尺寸只在 CSS)。
|
||
5. 補 jsdom 測試 + demo 案例。
|
||
|
||
因所有功能共用 finder/charify/locale/`jz-*`/CSS-JS 契約,且順序由 `order`
|
||
集中編排,新增不觸動既有 pass——此即「預留完整復刻 Han.css 能力」之具體保證。
|
||
|
||
---
|
||
|
||
## 14. 風險與未決項
|
||
|
||
原則:**風險上線後持續觀察,但下列預案先行設計就緒**,一旦觀察指標踩線即可切換,
|
||
不致臨時返工。
|
||
|
||
- **R1 justify/inline-block 基線**(§8)。觀察:句末標點在三引擎 justify 下是否
|
||
單份間距、inline-block 是否擾動基線/行高/行內換行。**預案**:改採 Han 路線之
|
||
`jz-cs` 補償空白法(標點維持 inline,以行邊界可折疊之 `jz-cs` 控制伸縮側),
|
||
結構已預留,切換僅換 CSS 機制不動 JS。
|
||
- **R2 行端擠壓免量測**。觀察:純 CSS(`jz-cs` 折疊)能否在三引擎一致達成行首行尾
|
||
擠壓。**預案**:退化為「連續+行內擠壓用 CSS(一致)+ 行端擠壓交由 layout pass
|
||
量測或漸進增強原語」,行端與連續解耦,互不影響。
|
||
- **R3 標點修正改寫文字 vs I1**。觀察:opt-in 開啟後複製/序列化是否仍可接受。
|
||
**預案**:預設只分類不改寫;改寫模式強制可 `revert`,並提供「僅視覺替換(CSS
|
||
字符替身)不動文字」之次級模式作折衷。
|
||
- **R4 注音/ruby 複雜度**。觀察:原生 `<ruby>` 對注音直排/聲調定位之不足。
|
||
**預案**:獨立 pass、最後做;先以原生 `<ruby>` 覆蓋多數,注音特例再以 `jz-ruby`
|
||
子結構補強,不影響其餘 pass。
|
||
- **R5 DOM 重量**。觀察:長文 charify 之節點數與 render 耗時。**預案**:兩階段批次
|
||
變更(§3.4)+ 僅包裝必要標點 + 可選分塊/`requestIdleCallback` 漸進渲染;demo
|
||
量測超標才啟用分塊。
|
||
|
||
---
|
||
|
||
*本文件為實作契約。任何偏離(尤其 §2 不變量)須先更新本文件再改碼。*
|