docs: ARCHITECTURE.md — 聚珍(Juzhen)架構契約
從零重寫之實作契約,先於程式碼。涵蓋: - 設計不變量 I1–I10(複製乾淨、JS/CSS 分離、依 lang、跨瀏覽器一致並顯式禁用 不一致原語、justify 單份間距、冪等可還原、邊界感知、v1 向下相容、遍歷聚合、 作用域與分塊)。 - createJuzhen API;3 階段聚合管線(collect/apply,把 ~9 次走訪壓到 3 次)。 - 核心基座 finder/unicode/locale/dom;jz-* 元素詞彙。 - 對照 Han.css 逐功能點實作方案(間隙/長詞/擠壓/禁則/懸掛/標點修正/著重號/注音)。 - 切片 1=v1 對等之映射;§3.5 v1 相容層;§3.6 作用域與分塊功能選擇。 - §8 justify 句末單份間距解法;§9.1 顯式禁用層;§14 風險+預案。
This commit is contained in:
+566
@@ -0,0 +1,566 @@
|
|||||||
|
# 聚珍(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` 形如:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
// 全域
|
||||||
|
lang: { default: "zh-Hant", policy: { "zh-Hant": "quanjiao", "zh-Hans": "kaiming" } },
|
||||||
|
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 表 + 回呼注入。
|
||||||
|
|
||||||
|
### 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` 彎引號:`“”‘’`
|
||||||
|
- **規則正則**:中西間隙邊界、擠壓相鄰組合、禁則對象等,皆以此表組裝。
|
||||||
|
|
||||||
|
> 借鑑 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()`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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-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`** → 選取/複製會帶上空格(其已知缺陷)。
|
||||||
|
- **我們**:finder 邊界感知地在 CJK↔西文邊界插入 `jz-hws`;間隙寬度由 CSS
|
||||||
|
(`::before` 或內含空白 + 負微調)給定,元素 `user-select:none`(I1,**修正
|
||||||
|
Han 的選中缺陷**)。WHERE 之規則沿用 v1 pangu 規則表(已驗證)。
|
||||||
|
- **跨瀏覽器**:`user-select`/inline 普遍支援。
|
||||||
|
- **lang**:間隙量可依 `:lang` 微調(如日文與中文略異),預設一致。
|
||||||
|
- **調整**:copy-clean(I1)+ justify 安全(I5/§8)。涵蓋 v1 Pass A、Pass B
|
||||||
|
(pill `code/kbd` 邊界亦走同一機制,pill 由 avoid 與鄰接判定處理)。
|
||||||
|
|
||||||
|
### 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-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`,故 Chrome/Safari/FF 一致。(註:v1 Pass D 用 `halt`,
|
||||||
|
跨瀏覽器不一致;此處即為改進。)
|
||||||
|
- **調整**:行端擠壓以 `jz-cs` 補償空白於行邊界折疊之機制達成(CSS 主導,免
|
||||||
|
逐行量測);justify 安全見 §8。
|
||||||
|
- **切片**:涵蓋並超越 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` 結構,無返工。
|
||||||
|
|
||||||
|
### 6.5 標點懸掛(Hanging)— 後續
|
||||||
|
|
||||||
|
- **Han**:行尾標點懸掛出版心,需量測。
|
||||||
|
- **我們**:layout pass——量測各行末,對行末可懸掛標點施負位移;掛
|
||||||
|
`ResizeObserver` 重算。`order=90`(最後)。lang 閘:**繁體橫排預設不懸掛**
|
||||||
|
(clreq;`zh-Hant` 預設關),可開。
|
||||||
|
- **預留**:依賴 `jz-char`/`jz-cs` 既有結構 + 一個 layout 後鉤子;切片 1 的
|
||||||
|
charify 已備妥對象,加入時不動既有 pass。
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
- **我們**:`juzhen.css` 內以 `:where()`(零特異度,易被消費端覆寫)提供 CJK
|
||||||
|
normalize 與依 `:lang` 之 Noto Sans/Serif CJK SC/TC/HK/JP 字體堆疊。非破壞、
|
||||||
|
opt-in(掛在 `.juzhen` 容器或 `[data-juzhen]` 下)。
|
||||||
|
- **跨瀏覽器**:`:where()`/`:lang()` 普遍支援。
|
||||||
|
|
||||||
|
### 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 的重點驗證項。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 跨瀏覽器策略
|
||||||
|
|
||||||
|
- **一致性關鍵層**(間隙、擠壓、justify):只用 `margin`、`letter-spacing`、
|
||||||
|
`inline-block`、`user-select`、`:lang()`、`:where()`、`hyphens`——三大引擎
|
||||||
|
(Blink/WebKit/Gecko)皆穩定支援。
|
||||||
|
- **漸進增強(非必要)**:`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 _lang.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 不變量)須先更新本文件再改碼。*
|
||||||
Reference in New Issue
Block a user