feat: 標點半形 margin 後備模式+下游回饋修復
margin 後備(供缺 OpenType halt 之字型): - jiya.halfWidth: "halt"(預設)|"margin"。margin 以 width:0.5em+字身依墨色 偏側溢出定位,於 CSS 盒模型重現 halt 之半形 advance(真 0.5em;相鄰標點各 0.5em、合佔 1em,無負 margin 之鄰接疊加/單側重疊)。模式由 data-jz-halfwidth 子樹屬性決定(可逐區塊/逐 lang,含 halt-island 反置)。 - 偏側 inkBias 依字身墨色中心給 left/center/right/full;參考表據方正書宋 GBK 實測,consumer 可經 jiya.bias 逐 lang 逐字覆寫。 - _lang.css 字型堆疊僅作後備(全包 :where 零特異度、只命中帶 lang 屬性之元素、 不直接覆寫 jz-inner)→ 消費端 font-family 恆優先,標點隨之以消費端字型渲染。 - 新增 tools/measure-bias.py(量 glyph ink 中心、產偏側建議表)+ README 「字型適配工作流」。demo/fallback.html:方正書宋 GBK 對照 halt 失效 vs margin。 下游回饋修復: - 1a:adjacentLogicalChar 透明穿越 jz-jinze(同 jz-hws/樣式行內)→ pill 緊鄰 CJK 標點時,左緣間隙與作者空格剝除復原(jinze 先於 spacing 之交互)。 - 1b:CJK↔CJK 贅餘作者空格(含跨 inline 標籤)剝除、不留隙,與 CJK↔Latin 之 邊界正規化一致(DEL 哨符,全形空格 U+3000 不動)。 - 問題二:新增 justifyAtoms 選項;false → jz-char/jz-jinze 改 display:inline, 相容分頁器(Paged.js)對齊引擎(halt 半形與禁則 nowrap 保留)。 測試 43→49 全通過;dist 重建;README/ARCHITECTURE 同步更新契約。
This commit is contained in:
+39
-3
@@ -328,6 +328,16 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
語義字元,剪貼板得緊湊原文。
|
語義字元,剪貼板得緊湊原文。
|
||||||
- **切片**:涵蓋 v1 Pass A、Pass B(pill `code/kbd` 邊界亦走同一機制——左字或
|
- **切片**:涵蓋 v1 Pass A、Pass B(pill `code/kbd` 邊界亦走同一機制——左字或
|
||||||
pill 元素包入 `jz-hws`;pill 由 avoid 與鄰接判定處理)。
|
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 不動。
|
||||||
|
|
||||||
### 6.1a 行末去隙(gapTrim,justify 修正)— 切片 1
|
### 6.1a 行末去隙(gapTrim,justify 修正)— 切片 1
|
||||||
|
|
||||||
@@ -363,8 +373,25 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
不重疊。`font-feature-settings:"halt"` 各引擎一致(當初疑慮之不一致者為**自動**
|
不重疊。`font-feature-settings:"halt"` 各引擎一致(當初疑慮之不一致者為**自動**
|
||||||
的 `text-spacing-trim`/`chws`,非顯式 `halt`);缺 `halt` 之字型優雅降級為全形。
|
的 `text-spacing-trim`/`chws`,非顯式 `halt`);缺 `halt` 之字型優雅降級為全形。
|
||||||
鄰接/行端之「收哪一字」之**決策**仍由 `blankSides` 邏輯定,渲染統一為 `halt`。
|
鄰接/行端之「收哪一字」之**決策**仍由 `blankSides` 邏輯定,渲染統一為 `halt`。
|
||||||
|
- **`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 下同句行寬 `24.0em(halt 無效)→ 19.7em(margin 生效)`、單標點
|
||||||
|
`1.0em → 0.5em`。
|
||||||
- **切片**:涵蓋並超越 v1 Pass D(開明式),加入全角式、lang 切換、行內連續與
|
- **切片**:涵蓋並超越 v1 Pass D(開明式),加入全角式、lang 切換、行內連續與
|
||||||
行端擠壓。
|
行端擠壓、`halt`/`margin` 雙渲染。
|
||||||
|
|
||||||
### 6.4 禁則(Jinze,避頭尾)+ justify 綁定 — 切片 1(基礎)/後續(完整)
|
### 6.4 禁則(Jinze,避頭尾)+ justify 綁定 — 切片 1(基礎)/後續(完整)
|
||||||
|
|
||||||
@@ -462,6 +489,14 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
> inline-block 影響基線對齊與行內換行,須以 CSS 校正(`vertical-align`、行高),
|
> inline-block 影響基線對齊與行內換行,須以 CSS 校正(`vertical-align`、行高),
|
||||||
> 並**跨 Chrome/Safari/FF 目視驗證**(§12);此為切片 1 的重點驗證項。
|
> 並**跨 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` 字型。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 跨瀏覽器策略
|
## 9. 跨瀏覽器策略
|
||||||
@@ -470,8 +505,9 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
|||||||
`user-select`、`white-space`、`:where()`、`hyphens`,及字型 OpenType
|
`user-select`、`white-space`、`:where()`、`hyphens`,及字型 OpenType
|
||||||
`font-feature-settings:"halt"`(標點半形;各引擎一致,效果僅依字型是否含 halt,
|
`font-feature-settings:"halt"`(標點半形;各引擎一致,效果僅依字型是否含 halt,
|
||||||
Noto CJK 具備,缺則優雅降級為全形)——三大引擎(Blink/WebKit/Gecko)皆穩定
|
Noto CJK 具備,缺則優雅降級為全形)——三大引擎(Blink/WebKit/Gecko)皆穩定
|
||||||
支援。**注意**:標點半形不用負 `margin`(對居中字形會重疊),改用 `halt`(見
|
支援。**注意**:標點半形預設不用負 `margin`(對居中字形會重疊),改用 `halt`(見
|
||||||
§6.3 修訂)。
|
§6.3 修訂)。**缺 `halt` 字型之後備**:`jiya.halfWidth:"margin"` 以 `width:0.5em`+
|
||||||
|
字身依偏側(`jz-ink-*`)溢出定位,於 CSS 重現 halt、真半形 advance(見 §6.3)。
|
||||||
- **漸進增強(非必要)**:`text-spacing-trim`(Blink)、`hanging-punctuation`
|
- **漸進增強(非必要)**:`text-spacing-trim`(Blink)、`hanging-punctuation`
|
||||||
(WebKit)、`text-emphasis`、`text-autospace`——僅在支援時錦上添花,缺席不影響
|
(WebKit)、`text-emphasis`、`text-autospace`——僅在支援時錦上添花,缺席不影響
|
||||||
基本排版(I4)。
|
基本排版(I4)。
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ jz.render(document.querySelector("main"));
|
|||||||
|
|
||||||
| 功能 | 說明 |
|
| 功能 | 說明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **中西間隙** `spacing` | CJK↔西文邊界插入 copy-clean 之 `jz-hws`(`user-select:none`,剪貼板跳過)。含跨樣式行內邊界與 `code`/`kbd` pill 邊界。 |
|
| **中西間隙** `spacing` | CJK↔西文邊界插入 copy-clean 之 `jz-hws`(`user-select:none`,剪貼板跳過)。含跨樣式行內邊界與 `code`/`kbd` pill 邊界(pill 緊鄰標點時穿越 `jz-jinze` 仍補間隙)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 |
|
||||||
| **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 `<span lang="en">`,配合 CSS `hyphens:auto` 原生斷詞(DOM 無連字符 → 複製不污染)。 |
|
| **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 `<span lang="en">`,配合 CSS `hyphens:auto` 原生斷詞(DOM 無連字符 → 複製不污染)。 |
|
||||||
| **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點對向側收);③行端擠壓(`lineedge` layout pass)。半形以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。 |
|
| **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點對向側收);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 |
|
||||||
| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。 |
|
| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。 |
|
||||||
| **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 |
|
| **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 |
|
||||||
|
|
||||||
@@ -90,14 +90,27 @@ createJuzhen({
|
|||||||
},
|
},
|
||||||
spacing: true,
|
spacing: true,
|
||||||
longWord: { minLength: 6, lang: "en" },
|
longWord: { minLength: 6, lang: "en" },
|
||||||
jiya: true,
|
jiya: true, // 或物件:{ halfWidth: "halt" | "margin", bias }
|
||||||
jinze: true,
|
jinze: true,
|
||||||
|
justifyAtoms: true, // jz-char/jz-jinze 以 inline-block 作對齊原子;
|
||||||
|
// 分頁器(Paged.js)下設 false 改 inline(見下)
|
||||||
blocks: [ // 分塊功能選擇
|
blocks: [ // 分塊功能選擇
|
||||||
{ selector: ".code-comment", features: [ "spacing" ] },
|
{ selector: ".code-comment", features: [ "spacing" ] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 分頁器(Paged.js 等)相容:`justifyAtoms`
|
||||||
|
|
||||||
|
預設 `jz-char`/`jz-jinze` 為 `inline-block`,作 `justify` 之單一對齊原子(句末標點
|
||||||
|
單份間距)。但**分頁器**(如 Paged.js)之對齊引擎會把每個 inline-block 邊界當伸縮點、
|
||||||
|
與 CJK inter-ideograph 疊加,於窄欄混排時字距失控、整段散架。此時設
|
||||||
|
`justifyAtoms: false`——`jz-char`/`jz-jinze` 改 `display:inline`(半形 `halt` 與禁則
|
||||||
|
`nowrap` 綁定均保留)。常規流式版面無需設定。
|
||||||
|
|
||||||
|
> 注意:`margin` 後備半形依賴 `inline-block`(`width:0.5em`),故
|
||||||
|
> `justifyAtoms:false` 與 `margin` 模式不可兼得——分頁器場景請用含 `halt` 之字型。
|
||||||
|
|
||||||
### 分塊功能選擇(HTML 屬性,就近繼承)
|
### 分塊功能選擇(HTML 屬性,就近繼承)
|
||||||
|
|
||||||
```html
|
```html
|
||||||
@@ -108,6 +121,119 @@ createJuzhen({
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 標點半形:`halt` 預設與 `margin` 後備
|
||||||
|
|
||||||
|
開明/半角式把標點收成「半形」,有兩種渲染機制:
|
||||||
|
|
||||||
|
| 機制 | 設定 | 原理 | 適用 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **`halt`**(預設) | `jiya: true` | 字型 OpenType `halt`,由字型按字形正確定位、不重疊 | 字型含 `halt`(Noto CJK/思源系列) |
|
||||||
|
| **`margin`**(後備) | `jiya: { halfWidth: "margin" }` | `width:0.5em`+字身依墨色偏側溢出定位(以 CSS 重現 halt) | 字型**缺** `halt`(多數系統字型、GBK 老字型如方正書宋 GBK、PingFang) |
|
||||||
|
|
||||||
|
> 為何需要後備:`halt` 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測
|
||||||
|
> **方正書宋 GBK 僅含 `vert`、無 `halt`** → halt 模式不擠;切 `margin` 後備後,
|
||||||
|
> 同句行寬 `24.0em → 19.7em`、單一標點 `1.0em → 0.5em`,半形生效。
|
||||||
|
|
||||||
|
### 激活與粒度
|
||||||
|
|
||||||
|
模式由 **`data-jz-halfwidth` 屬性所在子樹**決定(CSS 後代選擇器),故粒度極靈活:
|
||||||
|
|
||||||
|
- **全域**:`jiya: { halfWidth: "margin" }` → `render()` 於處理 root 自動掛屬性。
|
||||||
|
- **逐區塊/逐語言**:在 HTML 任一祖先手寫 `data-jz-halfwidth="margin"`(或
|
||||||
|
`"halt"`),即令該子樹用對應機制——無需另建實例。常見如「全頁 `halt`,某用
|
||||||
|
無 `halt` 字型之區塊掛 `margin`」,或反之(`margin` 頁中嵌一塊 `halt` 島)。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 全頁 halt(預設),此塊改用無 halt 字型 → 局部 margin 後備 -->
|
||||||
|
<section data-jz-halfwidth="margin" style="font-family: '方正书宋_GBK'">…</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 機制與字形偏側表(`margin` 模式專用,可覆寫)
|
||||||
|
|
||||||
|
`margin` 模式把標點 `jz-char` 之 inline-block 寬收為 **0.5em(真半形 advance)**,字身
|
||||||
|
(`jz-inner`)以 `overflow` 溢出該盒,再依**墨色偏側**左移、使墨色落於 0.5em 盒內、
|
||||||
|
空白溢向外側——即以 CSS 盒模型重現 `halt` 之「收字身寬+按字形定位」。相鄰兩標點各
|
||||||
|
0.5em、合佔 1em,無鄰接疊加或單側重疊問題。
|
||||||
|
|
||||||
|
偏側須知每字之墨色中心。聚珍內建**參考表**(依方正書宋 GBK 實測之 glyph ink 位):
|
||||||
|
簡體點號 `,、。;:!?` 偏左;繁體點號居中;彎引號 `“”‘’`、角括號 `「」`、尖括號
|
||||||
|
`〈〉` 墨色強偏一側(開→`right`、閉→`left`);圓/方/書名等括號 `()【】《》…` 墨色
|
||||||
|
近中→`center`。於 charify 標為 `jz-ink-{left,center,right}`。因字型字身分布各異,可逐
|
||||||
|
`lang` 逐字覆寫:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
createJuzhen({
|
||||||
|
jiya: {
|
||||||
|
halfWidth: "margin",
|
||||||
|
bias: {
|
||||||
|
"zh-Hans": { ",": "center", "(": "right" }, // 覆寫參考預設
|
||||||
|
"zh-Hant": { "。": "center" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 偏側值(決定字身於 0.5em 盒內之左移):
|
||||||
|
// left 墨偏左 → 不移(字身左對齊)
|
||||||
|
// center 墨居中 → 左移 0.25em
|
||||||
|
// right 墨偏右 → 左移 0.5em(右半入盒)
|
||||||
|
// full 無可壓縮空白 → 不收、維持全形
|
||||||
|
```
|
||||||
|
|
||||||
|
> `halt` 模式不讀偏側表(由字型負責定位)。偏側表僅在 `margin` 後備模式生效。
|
||||||
|
> `demo/fallback.html` 用方正書宋 GBK 對照 halt 失效 vs margin 生效。配字工作流見下節。
|
||||||
|
|
||||||
|
### 字型適配工作流(margin 模式配字)
|
||||||
|
|
||||||
|
換用新字型時,依下列步驟確認半形渲染:
|
||||||
|
|
||||||
|
1. **驗 `halt`**:`python3 tools/measure-bias.py <font.ttf>`(需 `pip install fonttools`)。
|
||||||
|
首行報該字型是否含 OpenType `halt`。
|
||||||
|
- **有 `halt`** → 直接用預設(`jiya: true`,halt 模式),無需配字,到此為止。
|
||||||
|
- **無 `halt`**(多數系統字型、GBK 老字型)→ 用 `margin` 後備,續下。
|
||||||
|
2. **取偏側建議**:同一工具同時量各標點 glyph 之墨色中心、印出建議偏側表
|
||||||
|
(`left|center|right|full`)與可貼入 `jiya.bias` 之 JSON。
|
||||||
|
3. **套用**:`createJuzhen({ jiya: { halfWidth: "margin", bias: <貼入 JSON> } })`;
|
||||||
|
或全用內建參考表(多數宋體系 GBK 字型與參考表相符,未必需覆寫)。
|
||||||
|
4. **目視微調**:以 `demo/fallback.html`(或自有頁面)對照原文,逐字檢查——
|
||||||
|
- 標點與相鄰字**重疊** → 該字墨色比量測更偏該側,偏側往「不移」方向調
|
||||||
|
(`right`→`center`→`left`)。
|
||||||
|
- 標點與內容**仍有空隙、不夠半形** → 往「多移」方向調(`left`→`center`→`right`)。
|
||||||
|
- 某標點根本無可壓空白(字面已滿全形)→ 設 `full`,不收。
|
||||||
|
5. **記錄**:把該字型之 `bias` 覆寫存入專案設定,作為該字型之適配結果。
|
||||||
|
|
||||||
|
> 偏側是「字身於 0.5em 盒內左移多少」的三檔量化(0/0.25/0.5em),量測值近門檻
|
||||||
|
> (如 0.6 介於 center/right)時,以目視為準。`halt` 字型走 OpenType 由字型精確
|
||||||
|
> 定位,無此配字需求——`margin` 配字僅為缺 `halt` 字型之務實近似。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSS 基礎設定(消費端須知)
|
||||||
|
|
||||||
|
JS 只注入 `jz-*` 結構,**所有視覺尺寸在 `juzhen.css`**——故 CSS 不可或缺。消費端
|
||||||
|
須備妥下列幾項:
|
||||||
|
|
||||||
|
1. **引入樣式(必須)**:`<link rel="stylesheet" href="dist/juzhen.css">`(或 ESM
|
||||||
|
`import "cjk-autospace/css"`)。缺此則間隙/擠壓/justify 原子化全部失效。
|
||||||
|
2. **設定 `lang`(強烈建議)**:在內容容器標 `lang="zh-Hans"`/`"zh-Hant"`/`"ja"`,
|
||||||
|
驅動全角/開明政策與字形 `locl`。無 `lang` 時回退 `options.lang.default`。
|
||||||
|
3. **作用域類(用 `scope.include` 時)**:若設 `scope.include: ".juzhen"`,須給內容
|
||||||
|
容器加對應 class(如 `class="juzhen"`);`juzhen.css` 之 normalize/字型堆疊亦
|
||||||
|
收斂於此域,不外溢。
|
||||||
|
4. **長英文詞斷字**:`longWord` 包出 `<span lang="en">` 後,須由消費端 CSS 提供
|
||||||
|
`hyphens: auto`(含 `-webkit-hyphens`)才會原生斷詞顯示連字符。
|
||||||
|
5. **`justify`(選用)**:欲兩端對齊則自行設 `text-align: justify`;句末標點單份間距
|
||||||
|
與行末去隙由聚珍處理,無需額外設定。
|
||||||
|
6. **字型(影響擠壓)**:開明/半角式半形預設依字型 `halt`。若用含 `halt` 之字型
|
||||||
|
(Noto CJK/思源),預設即可;若用缺 `halt` 之字型,改用 `margin` 後備模式(見
|
||||||
|
上節)。`juzhen.css` 內建依 `:lang` 之 Noto Sans/Serif CJK 字型堆疊,但**僅作
|
||||||
|
後備**(全包於 `:where()`、零特異度,且只命中帶 `lang` 屬性之元素、不直接覆寫標點
|
||||||
|
字面容器 `jz-inner`)——**消費端在 `.juzhen` 或任一祖先設 `font-family` 即恆優先**,
|
||||||
|
標點亦隨之以消費端字型渲染(故缺 `halt` 字型務必配 `margin` 後備,否則標點不擠)。
|
||||||
|
7. **自動處理、無需消費端介入**:`juzhen.css` 已在 `.juzhen` 範圍內以 `@supports`
|
||||||
|
關閉 Blink 原生 `text-spacing-trim`/`text-autospace`(避免與託管擠壓重複作用),
|
||||||
|
並令注入間隙 `user-select: none`(複製乾淨)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1 向下相容
|
## v1 向下相容
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -121,15 +247,16 @@ IIFE 構建另掛 `globalThis.createCjkAutospace` 別名。**與 v1 之差異**
|
|||||||
|
|
||||||
- 間隙標記由 `<span class="cjk-autospace">` 變為 `<jz-hws class="cjk-autospace">`
|
- 間隙標記由 `<span class="cjk-autospace">` 變為 `<jz-hws class="cjk-autospace">`
|
||||||
(仍帶原 class,既有 `.cjk-autospace{user-select:none}` CSS 仍命中)。
|
(仍帶原 class,既有 `.cjk-autospace{user-select:none}` CSS 仍命中)。
|
||||||
- 標點擠壓由消費端 `font-feature-settings:"halt"` 改為 `juzhen.css` 之負 `margin`
|
- 標點擠壓改由 `juzhen.css` 託管(須引入),預設仍以字型 `halt` 渲染半形——與 v1
|
||||||
(需改用 `juzhen.css`;機制不同,同為開明式「句內點號半形」之效果)。
|
消費端 `font-feature-settings:"halt"` 同機制;缺 `halt` 之字型可切 `margin` 後備。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 目視驗證
|
## 目視驗證
|
||||||
|
|
||||||
`demo/index.html` 載入 `dist/` 產物,簡繁並列、可切 `justify`、附 copy-clean 測試區。
|
`demo/index.html` 載入 `dist/` 產物,簡繁並列、可切 `justify`、附 copy-clean 測試區。
|
||||||
版面相關結論(justify 單份間距、擠壓視覺量、行端、`inline-block` 基線)須於
|
`demo/fallback.html` 以**方正書宋 GBK**(無 `halt`)對照 `halt` 模式失效 vs `margin`
|
||||||
|
後備生效。版面相關結論(justify 單份間距、擠壓視覺量、行端、`inline-block` 基線)須於
|
||||||
Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。
|
Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>聚珍 — halt 缺失字型之 margin 後備模式(方正書宋 GBK)</title>
|
||||||
|
<link rel="stylesheet" href="../dist/juzhen.css">
|
||||||
|
<style>
|
||||||
|
/* 全頁強制使用 方正書宋 GBK(本機字型,無 OpenType halt),以測試後備模式。 */
|
||||||
|
body
|
||||||
|
{
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #fafafa;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
/* 字型類獨立於 .juzhen:A 用裸 .cn(瀏覽器原生排版),B/C 才掛 .juzhen
|
||||||
|
(含 normalize 之 text-spacing-trim:space-all)。否則 A 之原生標點被 space-all
|
||||||
|
撐開、顯得比 B 鬆,造成「B 似乎有效」之假象(實則 B 全形未擠、與真原文等寬)。 */
|
||||||
|
/* 字型設於容器即可——_lang.css 修正後不再直接強制 jz-inner 之字型,故消費端
|
||||||
|
字型經繼承正確套到標點字面(標點亦以 FZShuSong 渲染,halt 確實缺席)。 */
|
||||||
|
.cn { font-family: "方正书宋_GBK", "FZShuSong-Z01", serif; font-size: 46px; line-height: 1.5; }
|
||||||
|
.w { font-weight: 400; opacity: .85; }
|
||||||
|
h1 { font-size: 1rem; }
|
||||||
|
.meta { font-size: .72rem; color: #777; line-height: 1.7; max-width: 42em; }
|
||||||
|
.meta code { background: #eee; padding: 0 .25em; border-radius: 3px; }
|
||||||
|
.row { margin: .6rem 0; }
|
||||||
|
.tag { display: inline-block; font-size: .62rem; color: #fff; padding: .12rem .55rem; border-radius: 3px; letter-spacing: .03em; vertical-align: top; margin-top: .9rem; width: 7.5rem; }
|
||||||
|
.tag.raw { background: #555; }
|
||||||
|
.tag.bad { background: #c0392b; }
|
||||||
|
.tag.good { background: #27ae60; }
|
||||||
|
/* 把每行的 <p> 設為 inline-block + 背景,行盒寬度即直接反映擠壓效果。 */
|
||||||
|
.row p
|
||||||
|
{
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 0 .6rem;
|
||||||
|
padding: .15rem .1rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>方正書宋 GBK · margin 後備模式</h1>
|
||||||
|
<p class="meta">
|
||||||
|
此字型僅含 <code>vert</code>、<b>無 <code>halt</code></b>,故 halt 模式之開明式擠壓「完全無效」(標點仍佔全形)。
|
||||||
|
margin 後備模式以負 margin 依字形偏側收半形——本字型簡體點號(,、。;:!?)墨色皆偏左下,故收右側空白。
|
||||||
|
白底行盒寬度即直接反映效果:B 與 A 同寬(未擠),C 明顯窄一截(已擠)。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<span class="tag raw" id="tag-raw">A · 原文(瀏覽器原生)</span>
|
||||||
|
<div class="cn" id="raw" lang="zh-Hans" style="display:inline-block"><p>他问:“真的吗?”第一、第二、第三;(含括号)。</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<span class="tag bad" id="tag-halt">B · halt 模式<br>(無 halt → 不擠,等同原文)</span>
|
||||||
|
<div class="cn juzhen" id="halt" lang="zh-Hans" style="display:inline-block"><p>他问:“真的吗?”第一、第二、第三;(含括号)。</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<span class="tag good" id="tag-margin">C · margin 後備<br>(半形生效)</span>
|
||||||
|
<div class="cn juzhen" id="margin" lang="zh-Hans" style="display:inline-block"><p>他问:“真的吗?”第一、第二、第三;(含括号)。</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../dist/juzhen.iife.js"></script>
|
||||||
|
<script>
|
||||||
|
var base = { spacing: false, jinze: false, longWord: false };
|
||||||
|
// B:halt 模式(預設)——此字型無 halt,開明式擠壓不生效(對照組)。
|
||||||
|
Juzhen.createJuzhen(Object.assign({ scope: { include: "#halt" } }, base))
|
||||||
|
.render(document.getElementById("halt"));
|
||||||
|
// C:margin 後備模式——render 於 root 設 data-jz-halfwidth="margin"。
|
||||||
|
Juzhen.createJuzhen(Object.assign({ scope: { include: "#margin" }, jiya: { halfWidth: "margin" } }, base))
|
||||||
|
.render(document.getElementById("margin"));
|
||||||
|
// A 面板留原文(不 render)。
|
||||||
|
// 註:margin 模式以 width:0.5em+字身溢出定位,標點 advance 真為 0.5em;整句
|
||||||
|
// 行寬 halt 24.0em → margin 19.5em(10 標點各收 0.5em),可於瀏覽器 devtools 量。
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+4
@@ -29,6 +29,10 @@ export declare class Finder {
|
|||||||
constructor(opts: ResolvedOptions);
|
constructor(opts: ResolvedOptions);
|
||||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||||
pillMatches(el: Element): boolean;
|
pillMatches(el: Element): boolean;
|
||||||
|
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||||
|
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||||
|
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||||
|
private isTransparentInline;
|
||||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||||
isAvoided(node: Node): boolean;
|
isAvoided(node: Node): boolean;
|
||||||
|
|||||||
Vendored
+11
@@ -1,3 +1,4 @@
|
|||||||
|
import type { LangClass } from "./locale.js";
|
||||||
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
||||||
export declare const CJK: string;
|
export declare const CJK: string;
|
||||||
/** 西文字母與數字。 */
|
/** 西文字母與數字。 */
|
||||||
@@ -39,3 +40,13 @@ export declare function blankSides(bdClass: string | null): {
|
|||||||
left: boolean;
|
left: boolean;
|
||||||
right: boolean;
|
right: boolean;
|
||||||
};
|
};
|
||||||
|
/** 字形墨色偏側。 */
|
||||||
|
export type Bias = "left" | "center" | "right" | "full";
|
||||||
|
/** 逐 lang 逐字之偏側覆寫表(消費端傳入,覆寫參考預設)。 */
|
||||||
|
export type BiasTable = Partial<Record<LangClass, Record<string, Bias>>>;
|
||||||
|
/**
|
||||||
|
* 解析標點字元在某語言下之墨色偏側(margin 後備模式用)。
|
||||||
|
* 優先序:消費端 overrides[langClass][ch] > 括號表 > 內建點號參考表。
|
||||||
|
* 非可壓縮標點(間隔/連接號等)回傳 null(margin 模式不收)。
|
||||||
|
*/
|
||||||
|
export declare function inkBias(ch: string, langClass: LangClass, overrides?: BiasTable): Bias | null;
|
||||||
|
|||||||
Vendored
+52
-8
@@ -15,32 +15,32 @@ jz-em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* src/css/_lang.css */
|
/* src/css/_lang.css */
|
||||||
:where(.juzhen):lang(zh-Hans),
|
:where(.juzhen:lang(zh-Hans)),
|
||||||
:where(.juzhen) :lang(zh-Hans) {
|
:where(.juzhen [lang]:lang(zh-Hans)) {
|
||||||
font-family:
|
font-family:
|
||||||
"Noto Serif SC",
|
"Noto Serif SC",
|
||||||
"Noto Serif CJK SC",
|
"Noto Serif CJK SC",
|
||||||
"Source Han Serif SC",
|
"Source Han Serif SC",
|
||||||
serif;
|
serif;
|
||||||
}
|
}
|
||||||
:where(.juzhen):lang(zh-Hant),
|
:where(.juzhen:lang(zh-Hant)),
|
||||||
:where(.juzhen) :lang(zh-Hant) {
|
:where(.juzhen [lang]:lang(zh-Hant)) {
|
||||||
font-family:
|
font-family:
|
||||||
"Noto Serif TC",
|
"Noto Serif TC",
|
||||||
"Noto Serif CJK TC",
|
"Noto Serif CJK TC",
|
||||||
"Source Han Serif TC",
|
"Source Han Serif TC",
|
||||||
serif;
|
serif;
|
||||||
}
|
}
|
||||||
:where(.juzhen):lang(zh-HK),
|
:where(.juzhen:lang(zh-HK)),
|
||||||
:where(.juzhen) :lang(zh-HK) {
|
:where(.juzhen [lang]:lang(zh-HK)) {
|
||||||
font-family:
|
font-family:
|
||||||
"Noto Serif HK",
|
"Noto Serif HK",
|
||||||
"Noto Serif CJK HK",
|
"Noto Serif CJK HK",
|
||||||
"Noto Serif TC",
|
"Noto Serif TC",
|
||||||
serif;
|
serif;
|
||||||
}
|
}
|
||||||
:where(.juzhen):lang(ja),
|
:where(.juzhen:lang(ja)),
|
||||||
:where(.juzhen) :lang(ja) {
|
:where(.juzhen [lang]:lang(ja)) {
|
||||||
font-family:
|
font-family:
|
||||||
"Noto Serif JP",
|
"Noto Serif JP",
|
||||||
"Noto Serif CJK JP",
|
"Noto Serif CJK JP",
|
||||||
@@ -63,11 +63,52 @@ jz-char {
|
|||||||
jz-inner {
|
jz-inner {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
[data-jz-atoms=inline] jz-char {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
jz-char.jz-half jz-inner,
|
jz-char.jz-half jz-inner,
|
||||||
jz-char.jz-half-le jz-inner {
|
jz-char.jz-half-le jz-inner {
|
||||||
font-feature-settings: "halt" 1;
|
font-feature-settings: "halt" 1;
|
||||||
-webkit-font-feature-settings: "halt" 1;
|
-webkit-font-feature-settings: "halt" 1;
|
||||||
}
|
}
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half jz-inner,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le jz-inner {
|
||||||
|
font-feature-settings: normal;
|
||||||
|
-webkit-font-feature-settings: normal;
|
||||||
|
}
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half.jz-ink-left,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half.jz-ink-center,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half.jz-ink-right,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le.jz-ink-left,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le.jz-ink-center,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le.jz-ink-right {
|
||||||
|
width: 0.5em;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half.jz-ink-left jz-inner,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half.jz-ink-center jz-inner,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half.jz-ink-right jz-inner,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le.jz-ink-left jz-inner,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le.jz-ink-center jz-inner,
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-half-le.jz-ink-right jz-inner {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-ink-center jz-inner {
|
||||||
|
margin-left: -0.25em;
|
||||||
|
}
|
||||||
|
[data-jz-halfwidth=margin] jz-char.jz-ink-right jz-inner {
|
||||||
|
margin-left: -0.5em;
|
||||||
|
}
|
||||||
|
[data-jz-halfwidth=halt] jz-char.jz-half jz-inner,
|
||||||
|
[data-jz-halfwidth=halt] jz-char.jz-half-le jz-inner {
|
||||||
|
font-feature-settings: "halt" 1;
|
||||||
|
-webkit-font-feature-settings: "halt" 1;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
[data-jz-halfwidth=halt] jz-char.jz-half,
|
||||||
|
[data-jz-halfwidth=halt] jz-char.jz-half-le {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* src/css/_jinze.css */
|
/* src/css/_jinze.css */
|
||||||
jz-jinze {
|
jz-jinze {
|
||||||
@@ -75,5 +116,8 @@ jz-jinze {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
[data-jz-atoms=inline] jz-jinze {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
/* src/css/juzhen.css */
|
/* src/css/juzhen.css */
|
||||||
|
|||||||
Vendored
+100
-8
@@ -45,6 +45,12 @@ var Juzhen = (() => {
|
|||||||
pillMatches(el) {
|
pillMatches(el) {
|
||||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||||
}
|
}
|
||||||
|
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||||
|
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||||
|
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||||
|
isTransparentInline(name) {
|
||||||
|
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE";
|
||||||
|
}
|
||||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||||
isAvoided(node) {
|
isAvoided(node) {
|
||||||
@@ -207,7 +213,7 @@ var Juzhen = (() => {
|
|||||||
if (n2.nodeType === 3 && n2.data.length > 0) {
|
if (n2.nodeType === 3 && n2.data.length > 0) {
|
||||||
return pickEnd(n2);
|
return pickEnd(n2);
|
||||||
}
|
}
|
||||||
if (n2.nodeType === 1 && (this.styleSet.has(n2.nodeName) || n2.nodeName === "JZ-HWS")) {
|
if (n2.nodeType === 1 && this.isTransparentInline(n2.nodeName)) {
|
||||||
const r = descend(n2);
|
const r = descend(n2);
|
||||||
if (r) {
|
if (r) {
|
||||||
return r;
|
return r;
|
||||||
@@ -233,7 +239,7 @@ var Juzhen = (() => {
|
|||||||
if (this.isAvoided(e)) {
|
if (this.isAvoided(e)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.styleSet.has(e.nodeName) || e.nodeName === "JZ-HWS") {
|
if (this.isTransparentInline(e.nodeName)) {
|
||||||
const r = descend(e);
|
const r = descend(e);
|
||||||
if (r) {
|
if (r) {
|
||||||
return r;
|
return r;
|
||||||
@@ -245,7 +251,7 @@ var Juzhen = (() => {
|
|||||||
}
|
}
|
||||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||||
}
|
}
|
||||||
if (!p || p.nodeType !== 1 || !this.styleSet.has(p.nodeName)) {
|
if (!p || p.nodeType !== 1 || !this.isTransparentInline(p.nodeName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pe = p;
|
const pe = p;
|
||||||
@@ -546,6 +552,55 @@ var Juzhen = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var BRACKET_OPEN_RIGHT = "\u300C\u3008\u201C\u2018";
|
||||||
|
var BRACKET_CLOSE_LEFT = "\u300D\u3009\u201D\u2019";
|
||||||
|
var BIAS_BRACKETS = (() => {
|
||||||
|
const t = {};
|
||||||
|
for (const ch of BD_OPEN) {
|
||||||
|
t[ch] = BRACKET_OPEN_RIGHT.indexOf(ch) >= 0 ? "right" : "center";
|
||||||
|
}
|
||||||
|
for (const ch of BD_CLOSE) {
|
||||||
|
t[ch] = BRACKET_CLOSE_LEFT.indexOf(ch) >= 0 ? "left" : "center";
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
})();
|
||||||
|
var BIAS_HANS = {
|
||||||
|
"\uFF0C": "left",
|
||||||
|
"\u3001": "left",
|
||||||
|
"\u3002": "left",
|
||||||
|
"\uFF1B": "left",
|
||||||
|
"\uFF1A": "left",
|
||||||
|
"\uFF01": "left",
|
||||||
|
"\uFF1F": "left"
|
||||||
|
};
|
||||||
|
var BIAS_HANT = {
|
||||||
|
"\uFF0C": "center",
|
||||||
|
"\u3001": "center",
|
||||||
|
"\u3002": "center",
|
||||||
|
"\uFF1B": "center",
|
||||||
|
"\uFF1A": "center",
|
||||||
|
"\uFF01": "center",
|
||||||
|
"\uFF1F": "center"
|
||||||
|
};
|
||||||
|
var BIAS_DEFAULT = {
|
||||||
|
"zh-Hans": BIAS_HANS,
|
||||||
|
"zh-Hant": BIAS_HANT,
|
||||||
|
"ja": BIAS_HANT,
|
||||||
|
// 日文點號慣例居中,沿用繁體表
|
||||||
|
"other": BIAS_HANT
|
||||||
|
};
|
||||||
|
function inkBias(ch, langClass, overrides) {
|
||||||
|
const ov = overrides ? overrides[langClass] : void 0;
|
||||||
|
if (ov && ov[ch]) {
|
||||||
|
return ov[ch];
|
||||||
|
}
|
||||||
|
const bracket = BIAS_BRACKETS[ch];
|
||||||
|
if (bracket) {
|
||||||
|
return bracket;
|
||||||
|
}
|
||||||
|
const t = BIAS_DEFAULT[langClass] || BIAS_DEFAULT.other;
|
||||||
|
return t[ch] || null;
|
||||||
|
}
|
||||||
|
|
||||||
// src/typeset/jiya.ts
|
// src/typeset/jiya.ts
|
||||||
var PASS = "jiya";
|
var PASS = "jiya";
|
||||||
@@ -584,15 +639,25 @@ var Juzhen = (() => {
|
|||||||
const data = node.data;
|
const data = node.data;
|
||||||
const out = [];
|
const out = [];
|
||||||
let style = null;
|
let style = null;
|
||||||
|
let langClass = null;
|
||||||
|
const biasOverrides = ctx.options.jiyaConfig.bias;
|
||||||
for (let i = 0; i < data.length; i += 1) {
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
const bdClass = classifyBiaodian(data.charAt(i));
|
const ch = data.charAt(i);
|
||||||
|
const bdClass = classifyBiaodian(ch);
|
||||||
if (!bdClass) {
|
if (!bdClass) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (style === null) {
|
if (style === null) {
|
||||||
style = resolveStyle(node, ctx.options.locale);
|
style = resolveStyle(node, ctx.options.locale);
|
||||||
}
|
}
|
||||||
|
if (langClass === null) {
|
||||||
|
langClass = classify(effectiveLang(node, ctx.options.locale.default));
|
||||||
|
}
|
||||||
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
||||||
|
const bias = inkBias(ch, langClass, biasOverrides);
|
||||||
|
if (bias) {
|
||||||
|
classes.push("jz-ink-" + bias);
|
||||||
|
}
|
||||||
out.push({
|
out.push({
|
||||||
start: i,
|
start: i,
|
||||||
end: i + 1,
|
end: i + 1,
|
||||||
@@ -1017,8 +1082,10 @@ var Juzhen = (() => {
|
|||||||
var RBR_BASIC = "\\)\\]\\}";
|
var RBR_BASIC = "\\)\\]\\}";
|
||||||
var LBR_EXT = "\\(\\[\\{<>";
|
var LBR_EXT = "\\(\\[\\{<>";
|
||||||
var RBR_EXT = "\\)\\]\\}<>";
|
var RBR_EXT = "\\)\\]\\}<>";
|
||||||
|
var DEL = String.fromCharCode(57347);
|
||||||
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||||||
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||||||
|
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||||
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
|
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
|
||||||
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||||
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||||||
@@ -1056,6 +1123,7 @@ var Juzhen = (() => {
|
|||||||
let s = text;
|
let s = text;
|
||||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||||||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||||||
|
s = s.replace(STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||||||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||||
@@ -1127,6 +1195,12 @@ var Juzhen = (() => {
|
|||||||
let spacedIdx = 0;
|
let spacedIdx = 0;
|
||||||
while (spacedIdx < spaced.length) {
|
while (spacedIdx < spaced.length) {
|
||||||
const sc = spaced.charAt(spacedIdx);
|
const sc = spaced.charAt(spacedIdx);
|
||||||
|
if (sc === DEL) {
|
||||||
|
actions.push({ before: void 0, space: map[origIdx] });
|
||||||
|
origIdx += 1;
|
||||||
|
spacedIdx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (sc === MARK) {
|
if (sc === MARK) {
|
||||||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||||||
if (oc === " ") {
|
if (oc === " ") {
|
||||||
@@ -1143,14 +1217,13 @@ var Juzhen = (() => {
|
|||||||
}
|
}
|
||||||
for (let i = actions.length - 1; i >= 0; i -= 1) {
|
for (let i = actions.length - 1; i >= 0; i -= 1) {
|
||||||
const act = actions[i];
|
const act = actions[i];
|
||||||
if (!act.before) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (act.space) {
|
if (act.space) {
|
||||||
const tn = act.space.textNode;
|
const tn = act.space.textNode;
|
||||||
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
||||||
}
|
}
|
||||||
wrapGapLeft(act.before);
|
if (act.before) {
|
||||||
|
wrapGapLeft(act.before);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1387,6 +1460,15 @@ var Juzhen = (() => {
|
|||||||
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
|
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
|
||||||
return { minLength, lang };
|
return { minLength, lang };
|
||||||
}
|
}
|
||||||
|
function resolveJiya(v) {
|
||||||
|
if (v && typeof v === "object") {
|
||||||
|
return {
|
||||||
|
halfWidth: v.halfWidth === "margin" ? "margin" : "halt",
|
||||||
|
bias: v.bias || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { halfWidth: "halt", bias: {} };
|
||||||
|
}
|
||||||
function normalizeOptions(opts = {}) {
|
function normalizeOptions(opts = {}) {
|
||||||
const langOpt = opts.lang || {};
|
const langOpt = opts.lang || {};
|
||||||
const scope = opts.scope || {};
|
const scope = opts.scope || {};
|
||||||
@@ -1397,6 +1479,7 @@ var Juzhen = (() => {
|
|||||||
policy: langOpt.policy || {}
|
policy: langOpt.policy || {}
|
||||||
},
|
},
|
||||||
autospaceClass: opts.autospaceClass || "",
|
autospaceClass: opts.autospaceClass || "",
|
||||||
|
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||||
scope: {
|
scope: {
|
||||||
root: scope.root || null,
|
root: scope.root || null,
|
||||||
include: scope.include || null,
|
include: scope.include || null,
|
||||||
@@ -1413,6 +1496,7 @@ var Juzhen = (() => {
|
|||||||
ruby: feature(opts.ruby, false)
|
ruby: feature(opts.ruby, false)
|
||||||
},
|
},
|
||||||
longWord: resolveLongWord(opts.longWord),
|
longWord: resolveLongWord(opts.longWord),
|
||||||
|
jiyaConfig: resolveJiya(opts.jiya),
|
||||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||||
finder: {
|
finder: {
|
||||||
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
||||||
@@ -1447,6 +1531,12 @@ var Juzhen = (() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runPasses(PASSES, makeCtx(el));
|
runPasses(PASSES, makeCtx(el));
|
||||||
|
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||||
|
el.setAttribute("data-jz-halfwidth", "margin");
|
||||||
|
}
|
||||||
|
if (!options.justifyAtoms) {
|
||||||
|
el.setAttribute("data-jz-atoms", "inline");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
revert(root) {
|
revert(root) {
|
||||||
const el = resolveRoot(root, options);
|
const el = resolveRoot(root, options);
|
||||||
@@ -1454,6 +1544,8 @@ var Juzhen = (() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
revertPasses(PASSES, makeCtx(el));
|
revertPasses(PASSES, makeCtx(el));
|
||||||
|
el.removeAttribute("data-jz-halfwidth");
|
||||||
|
el.removeAttribute("data-jz-atoms");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+100
-8
@@ -17,6 +17,12 @@ var Finder = class {
|
|||||||
pillMatches(el) {
|
pillMatches(el) {
|
||||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||||
}
|
}
|
||||||
|
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||||
|
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||||
|
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||||
|
isTransparentInline(name) {
|
||||||
|
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE";
|
||||||
|
}
|
||||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||||
isAvoided(node) {
|
isAvoided(node) {
|
||||||
@@ -179,7 +185,7 @@ var Finder = class {
|
|||||||
if (n2.nodeType === 3 && n2.data.length > 0) {
|
if (n2.nodeType === 3 && n2.data.length > 0) {
|
||||||
return pickEnd(n2);
|
return pickEnd(n2);
|
||||||
}
|
}
|
||||||
if (n2.nodeType === 1 && (this.styleSet.has(n2.nodeName) || n2.nodeName === "JZ-HWS")) {
|
if (n2.nodeType === 1 && this.isTransparentInline(n2.nodeName)) {
|
||||||
const r = descend(n2);
|
const r = descend(n2);
|
||||||
if (r) {
|
if (r) {
|
||||||
return r;
|
return r;
|
||||||
@@ -205,7 +211,7 @@ var Finder = class {
|
|||||||
if (this.isAvoided(e)) {
|
if (this.isAvoided(e)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.styleSet.has(e.nodeName) || e.nodeName === "JZ-HWS") {
|
if (this.isTransparentInline(e.nodeName)) {
|
||||||
const r = descend(e);
|
const r = descend(e);
|
||||||
if (r) {
|
if (r) {
|
||||||
return r;
|
return r;
|
||||||
@@ -217,7 +223,7 @@ var Finder = class {
|
|||||||
}
|
}
|
||||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||||
}
|
}
|
||||||
if (!p || p.nodeType !== 1 || !this.styleSet.has(p.nodeName)) {
|
if (!p || p.nodeType !== 1 || !this.isTransparentInline(p.nodeName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pe = p;
|
const pe = p;
|
||||||
@@ -518,6 +524,55 @@ function blankSides(bdClass) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var BRACKET_OPEN_RIGHT = "\u300C\u3008\u201C\u2018";
|
||||||
|
var BRACKET_CLOSE_LEFT = "\u300D\u3009\u201D\u2019";
|
||||||
|
var BIAS_BRACKETS = (() => {
|
||||||
|
const t = {};
|
||||||
|
for (const ch of BD_OPEN) {
|
||||||
|
t[ch] = BRACKET_OPEN_RIGHT.indexOf(ch) >= 0 ? "right" : "center";
|
||||||
|
}
|
||||||
|
for (const ch of BD_CLOSE) {
|
||||||
|
t[ch] = BRACKET_CLOSE_LEFT.indexOf(ch) >= 0 ? "left" : "center";
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
})();
|
||||||
|
var BIAS_HANS = {
|
||||||
|
"\uFF0C": "left",
|
||||||
|
"\u3001": "left",
|
||||||
|
"\u3002": "left",
|
||||||
|
"\uFF1B": "left",
|
||||||
|
"\uFF1A": "left",
|
||||||
|
"\uFF01": "left",
|
||||||
|
"\uFF1F": "left"
|
||||||
|
};
|
||||||
|
var BIAS_HANT = {
|
||||||
|
"\uFF0C": "center",
|
||||||
|
"\u3001": "center",
|
||||||
|
"\u3002": "center",
|
||||||
|
"\uFF1B": "center",
|
||||||
|
"\uFF1A": "center",
|
||||||
|
"\uFF01": "center",
|
||||||
|
"\uFF1F": "center"
|
||||||
|
};
|
||||||
|
var BIAS_DEFAULT = {
|
||||||
|
"zh-Hans": BIAS_HANS,
|
||||||
|
"zh-Hant": BIAS_HANT,
|
||||||
|
"ja": BIAS_HANT,
|
||||||
|
// 日文點號慣例居中,沿用繁體表
|
||||||
|
"other": BIAS_HANT
|
||||||
|
};
|
||||||
|
function inkBias(ch, langClass, overrides) {
|
||||||
|
const ov = overrides ? overrides[langClass] : void 0;
|
||||||
|
if (ov && ov[ch]) {
|
||||||
|
return ov[ch];
|
||||||
|
}
|
||||||
|
const bracket = BIAS_BRACKETS[ch];
|
||||||
|
if (bracket) {
|
||||||
|
return bracket;
|
||||||
|
}
|
||||||
|
const t = BIAS_DEFAULT[langClass] || BIAS_DEFAULT.other;
|
||||||
|
return t[ch] || null;
|
||||||
|
}
|
||||||
|
|
||||||
// src/typeset/jiya.ts
|
// src/typeset/jiya.ts
|
||||||
var PASS = "jiya";
|
var PASS = "jiya";
|
||||||
@@ -556,15 +611,25 @@ var jiyaPass = {
|
|||||||
const data = node.data;
|
const data = node.data;
|
||||||
const out = [];
|
const out = [];
|
||||||
let style = null;
|
let style = null;
|
||||||
|
let langClass = null;
|
||||||
|
const biasOverrides = ctx.options.jiyaConfig.bias;
|
||||||
for (let i = 0; i < data.length; i += 1) {
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
const bdClass = classifyBiaodian(data.charAt(i));
|
const ch = data.charAt(i);
|
||||||
|
const bdClass = classifyBiaodian(ch);
|
||||||
if (!bdClass) {
|
if (!bdClass) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (style === null) {
|
if (style === null) {
|
||||||
style = resolveStyle(node, ctx.options.locale);
|
style = resolveStyle(node, ctx.options.locale);
|
||||||
}
|
}
|
||||||
|
if (langClass === null) {
|
||||||
|
langClass = classify(effectiveLang(node, ctx.options.locale.default));
|
||||||
|
}
|
||||||
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
||||||
|
const bias = inkBias(ch, langClass, biasOverrides);
|
||||||
|
if (bias) {
|
||||||
|
classes.push("jz-ink-" + bias);
|
||||||
|
}
|
||||||
out.push({
|
out.push({
|
||||||
start: i,
|
start: i,
|
||||||
end: i + 1,
|
end: i + 1,
|
||||||
@@ -989,8 +1054,10 @@ var LBR_BASIC = "\\(\\[\\{";
|
|||||||
var RBR_BASIC = "\\)\\]\\}";
|
var RBR_BASIC = "\\)\\]\\}";
|
||||||
var LBR_EXT = "\\(\\[\\{<>";
|
var LBR_EXT = "\\(\\[\\{<>";
|
||||||
var RBR_EXT = "\\)\\]\\}<>";
|
var RBR_EXT = "\\)\\]\\}<>";
|
||||||
|
var DEL = String.fromCharCode(57347);
|
||||||
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||||||
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||||||
|
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||||
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
|
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
|
||||||
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||||
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||||||
@@ -1028,6 +1095,7 @@ function panguSpace(text) {
|
|||||||
let s = text;
|
let s = text;
|
||||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||||||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||||||
|
s = s.replace(STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||||||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||||
@@ -1099,6 +1167,12 @@ function processBlock(block, finder) {
|
|||||||
let spacedIdx = 0;
|
let spacedIdx = 0;
|
||||||
while (spacedIdx < spaced.length) {
|
while (spacedIdx < spaced.length) {
|
||||||
const sc = spaced.charAt(spacedIdx);
|
const sc = spaced.charAt(spacedIdx);
|
||||||
|
if (sc === DEL) {
|
||||||
|
actions.push({ before: void 0, space: map[origIdx] });
|
||||||
|
origIdx += 1;
|
||||||
|
spacedIdx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (sc === MARK) {
|
if (sc === MARK) {
|
||||||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||||||
if (oc === " ") {
|
if (oc === " ") {
|
||||||
@@ -1115,14 +1189,13 @@ function processBlock(block, finder) {
|
|||||||
}
|
}
|
||||||
for (let i = actions.length - 1; i >= 0; i -= 1) {
|
for (let i = actions.length - 1; i >= 0; i -= 1) {
|
||||||
const act = actions[i];
|
const act = actions[i];
|
||||||
if (!act.before) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (act.space) {
|
if (act.space) {
|
||||||
const tn = act.space.textNode;
|
const tn = act.space.textNode;
|
||||||
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
||||||
}
|
}
|
||||||
wrapGapLeft(act.before);
|
if (act.before) {
|
||||||
|
wrapGapLeft(act.before);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1359,6 +1432,15 @@ function resolveLongWord(v) {
|
|||||||
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
|
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
|
||||||
return { minLength, lang };
|
return { minLength, lang };
|
||||||
}
|
}
|
||||||
|
function resolveJiya(v) {
|
||||||
|
if (v && typeof v === "object") {
|
||||||
|
return {
|
||||||
|
halfWidth: v.halfWidth === "margin" ? "margin" : "halt",
|
||||||
|
bias: v.bias || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { halfWidth: "halt", bias: {} };
|
||||||
|
}
|
||||||
function normalizeOptions(opts = {}) {
|
function normalizeOptions(opts = {}) {
|
||||||
const langOpt = opts.lang || {};
|
const langOpt = opts.lang || {};
|
||||||
const scope = opts.scope || {};
|
const scope = opts.scope || {};
|
||||||
@@ -1369,6 +1451,7 @@ function normalizeOptions(opts = {}) {
|
|||||||
policy: langOpt.policy || {}
|
policy: langOpt.policy || {}
|
||||||
},
|
},
|
||||||
autospaceClass: opts.autospaceClass || "",
|
autospaceClass: opts.autospaceClass || "",
|
||||||
|
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||||
scope: {
|
scope: {
|
||||||
root: scope.root || null,
|
root: scope.root || null,
|
||||||
include: scope.include || null,
|
include: scope.include || null,
|
||||||
@@ -1385,6 +1468,7 @@ function normalizeOptions(opts = {}) {
|
|||||||
ruby: feature(opts.ruby, false)
|
ruby: feature(opts.ruby, false)
|
||||||
},
|
},
|
||||||
longWord: resolveLongWord(opts.longWord),
|
longWord: resolveLongWord(opts.longWord),
|
||||||
|
jiyaConfig: resolveJiya(opts.jiya),
|
||||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||||
finder: {
|
finder: {
|
||||||
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
||||||
@@ -1419,6 +1503,12 @@ function createJuzhen(opts = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runPasses(PASSES, makeCtx(el));
|
runPasses(PASSES, makeCtx(el));
|
||||||
|
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||||
|
el.setAttribute("data-jz-halfwidth", "margin");
|
||||||
|
}
|
||||||
|
if (!options.justifyAtoms) {
|
||||||
|
el.setAttribute("data-jz-atoms", "inline");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
revert(root) {
|
revert(root) {
|
||||||
const el = resolveRoot(root, options);
|
const el = resolveRoot(root, options);
|
||||||
@@ -1426,6 +1516,8 @@ function createJuzhen(opts = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
revertPasses(PASSES, makeCtx(el));
|
revertPasses(PASSES, makeCtx(el));
|
||||||
|
el.removeAttribute("data-jz-halfwidth");
|
||||||
|
el.removeAttribute("data-jz-atoms");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+24
-2
@@ -1,11 +1,22 @@
|
|||||||
import type { Finder } from "./core/finder.js";
|
import type { Finder } from "./core/finder.js";
|
||||||
import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js";
|
import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js";
|
||||||
export type { LangClass, PunctStyle };
|
import type { Bias, BiasTable } from "./core/unicode.js";
|
||||||
|
export type { LangClass, PunctStyle, Bias, BiasTable };
|
||||||
/** 長英文詞包裝設定。 */
|
/** 長英文詞包裝設定。 */
|
||||||
export interface LongWordOptions {
|
export interface LongWordOptions {
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
lang?: string;
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
/** 標點擠壓設定(架構 §6.3)。布林 true 等同 { halfWidth: "halt" }。 */
|
||||||
|
export interface JiyaOptions {
|
||||||
|
/** 半形渲染機制:
|
||||||
|
* - "halt"(預設)— 用字型 OpenType `halt`,由字型按字形正確定位,需字型支援;
|
||||||
|
* - "margin" — 負 margin 後備模式,供缺 halt 之字型,依字形偏側表收半形。 */
|
||||||
|
halfWidth?: "halt" | "margin";
|
||||||
|
/** margin 後備模式之字形偏側覆寫表(逐 lang 逐字),覆寫內建參考預設。
|
||||||
|
* halt 模式不讀此欄。 */
|
||||||
|
bias?: BiasTable;
|
||||||
|
}
|
||||||
/** 分塊功能選擇:選擇器 → 該塊內生效之功能集(覆寫全域)。 */
|
/** 分塊功能選擇:選擇器 → 該塊內生效之功能集(覆寫全域)。 */
|
||||||
export interface BlockRule {
|
export interface BlockRule {
|
||||||
selector: string;
|
selector: string;
|
||||||
@@ -25,6 +36,11 @@ export interface JuzhenOptions {
|
|||||||
};
|
};
|
||||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||||
autospaceClass?: string;
|
autospaceClass?: string;
|
||||||
|
/** justify 原子化:jz-char/jz-jinze 是否以 inline-block 作對齊單一原子
|
||||||
|
* (預設 true)。設 false 改 display:inline——用於分頁器(如 Paged.js)之
|
||||||
|
* 對齊引擎會把每個 inline-block 邊界當伸縮點、與 CJK inter-ideograph 疊加
|
||||||
|
* 致窄欄散架之情形(halt 半形與 jinze nowrap 仍保留)。 */
|
||||||
|
justifyAtoms?: boolean;
|
||||||
/** 作用域限定(§3.6)。 */
|
/** 作用域限定(§3.6)。 */
|
||||||
scope?: {
|
scope?: {
|
||||||
root?: Element | string;
|
root?: Element | string;
|
||||||
@@ -33,7 +49,7 @@ export interface JuzhenOptions {
|
|||||||
};
|
};
|
||||||
spacing?: boolean;
|
spacing?: boolean;
|
||||||
longWord?: boolean | LongWordOptions;
|
longWord?: boolean | LongWordOptions;
|
||||||
jiya?: boolean;
|
jiya?: boolean | JiyaOptions;
|
||||||
jinze?: boolean;
|
jinze?: boolean;
|
||||||
hanging?: boolean;
|
hanging?: boolean;
|
||||||
biaodian?: boolean;
|
biaodian?: boolean;
|
||||||
@@ -52,6 +68,7 @@ export interface JuzhenOptions {
|
|||||||
export interface ResolvedOptions {
|
export interface ResolvedOptions {
|
||||||
locale: LocaleConfig;
|
locale: LocaleConfig;
|
||||||
autospaceClass: string;
|
autospaceClass: string;
|
||||||
|
justifyAtoms: boolean;
|
||||||
scope: {
|
scope: {
|
||||||
root: Element | string | null;
|
root: Element | string | null;
|
||||||
include: string | null;
|
include: string | null;
|
||||||
@@ -71,6 +88,11 @@ export interface ResolvedOptions {
|
|||||||
minLength: number;
|
minLength: number;
|
||||||
lang: string;
|
lang: string;
|
||||||
};
|
};
|
||||||
|
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
||||||
|
jiyaConfig: {
|
||||||
|
halfWidth: "halt" | "margin";
|
||||||
|
bias: BiasTable;
|
||||||
|
};
|
||||||
blocks: BlockRule[];
|
blocks: BlockRule[];
|
||||||
finder: {
|
finder: {
|
||||||
styleInlines: Set<string>;
|
styleInlines: Set<string>;
|
||||||
|
|||||||
+11
-6
@@ -63,6 +63,14 @@ export class Finder
|
|||||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||||
|
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||||
|
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||||
|
private isTransparentInline(name: string): boolean
|
||||||
|
{
|
||||||
|
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE";
|
||||||
|
}
|
||||||
|
|
||||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||||
isAvoided(node: Node): boolean
|
isAvoided(node: Node): boolean
|
||||||
@@ -223,10 +231,7 @@ export class Finder
|
|||||||
{
|
{
|
||||||
return pickEnd(n as Text);
|
return pickEnd(n as Text);
|
||||||
}
|
}
|
||||||
if (
|
if (n.nodeType === 1 && this.isTransparentInline((n as Element).nodeName))
|
||||||
n.nodeType === 1
|
|
||||||
&& (this.styleSet.has((n as Element).nodeName) || (n as Element).nodeName === "JZ-HWS")
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
const r = descend(n as Element);
|
const r = descend(n as Element);
|
||||||
if (r) { return r; }
|
if (r) { return r; }
|
||||||
@@ -252,7 +257,7 @@ export class Finder
|
|||||||
{
|
{
|
||||||
const e = n as Element;
|
const e = n as Element;
|
||||||
if (this.isAvoided(e)) { return null; }
|
if (this.isAvoided(e)) { return null; }
|
||||||
if (this.styleSet.has(e.nodeName) || e.nodeName === "JZ-HWS")
|
if (this.isTransparentInline(e.nodeName))
|
||||||
{
|
{
|
||||||
const r = descend(e);
|
const r = descend(e);
|
||||||
if (r) { return r; }
|
if (r) { return r; }
|
||||||
@@ -263,7 +268,7 @@ export class Finder
|
|||||||
}
|
}
|
||||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||||
}
|
}
|
||||||
if (!p || p.nodeType !== 1 || !this.styleSet.has((p as Element).nodeName))
|
if (!p || p.nodeType !== 1 || !this.isTransparentInline((p as Element).nodeName))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
// 這些常數以「regex 字元類片段」形式給出(已就 RegExp 字元類做必要轉義),
|
// 這些常數以「regex 字元類片段」形式給出(已就 RegExp 字元類做必要轉義),
|
||||||
// 故可直接 `new RegExp("[" + X + "]")`。
|
// 故可直接 `new RegExp("[" + X + "]")`。
|
||||||
|
|
||||||
|
import type { LangClass } from "./locale.js";
|
||||||
|
|
||||||
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
||||||
export const CJK = (
|
export const CJK = (
|
||||||
"⺀-⼀-" // CJK 部首補充 + 康熙部首
|
"⺀-⼀-" // CJK 部首補充 + 康熙部首
|
||||||
@@ -118,3 +120,81 @@ export function blankSides(bdClass: string | null): { left: boolean; right: bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- 字形偏側(ink bias)參考表 — margin 後備模式用(架構 §6.3/.feedback)-----
|
||||||
|
//
|
||||||
|
// 預設半形渲染用字型 OpenType `halt`:由字型按字形正確定位,無需知道字面偏側。
|
||||||
|
// 但消費端字型未必含 halt(部分系統字型、特製字型)。此時改用「margin 後備模式」:
|
||||||
|
// 以負 margin 收半形空白,**須知道每個標點字形之墨色偏側**才能收對側、不重疊:
|
||||||
|
//
|
||||||
|
// left 墨色偏左(空白在右) → 收右:margin-right: -0.5em
|
||||||
|
// right 墨色偏右(空白在左) → 收左:margin-left: -0.5em
|
||||||
|
// center 墨色居中(兩側各半空白) → 兩側各收:margin-left/right: -0.25em
|
||||||
|
// full 無可壓縮空白(字面已佔滿全形) → 不收(維持全形)
|
||||||
|
//
|
||||||
|
// 下表為**參考預設**,依簡/繁字形慣例給出(簡體點號偏左下、繁體居中;!?恆
|
||||||
|
// 居中;開類偏右、閉類偏左)。因實際字型字形多樣,消費端可經 options.jiya.bias
|
||||||
|
// 逐 lang 逐字覆寫(見 README「margin 後備模式」一節)。
|
||||||
|
//
|
||||||
|
// 注意:此偏側表只在 margin 後備模式生效;halt 模式(預設)不讀它。
|
||||||
|
|
||||||
|
/** 字形墨色偏側。 */
|
||||||
|
export type Bias = "left" | "center" | "right" | "full";
|
||||||
|
|
||||||
|
/** 逐 lang 逐字之偏側覆寫表(消費端傳入,覆寫參考預設)。 */
|
||||||
|
export type BiasTable = Partial<Record<LangClass, Record<string, Bias>>>;
|
||||||
|
|
||||||
|
// 括號/引號偏側依**字身墨色中心**(margin 後備模式之 width:0.5em 盒內定位用)。
|
||||||
|
// 經方正書宋 GBK 實測 glyph ink bbox:彎引號 “”‘’、角括號 「」、尖括號 〈〉 之墨色
|
||||||
|
// 強偏一側(開類 inkcenter≈0.64–0.75 偏右、閉類≈0.25–0.37 偏左)→ 開 right、閉 left;
|
||||||
|
// 圓/方/書名/六角括號 ()【】《》[]{}〔〕〖〗『』 墨色近中(≈0.38–0.62)→ center。
|
||||||
|
// 偏側決定字身於 0.5em 半形盒內之左移量(right 移 0.5、center 移 0.25、left 不移),
|
||||||
|
// 使墨色落於盒內、空白溢向外側而不與鄰字重疊。字型字身分布各異,消費端可經
|
||||||
|
// options.jiya.bias 逐字覆寫(見 README「字型適配工作流」)。
|
||||||
|
const BRACKET_OPEN_RIGHT = "「〈“‘"; // 開類中墨色強偏右者
|
||||||
|
const BRACKET_CLOSE_LEFT = "」〉”’"; // 閉類中墨色強偏左者
|
||||||
|
const BIAS_BRACKETS: Record<string, Bias> = (() =>
|
||||||
|
{
|
||||||
|
const t: Record<string, Bias> = {};
|
||||||
|
for (const ch of BD_OPEN) { t[ch] = BRACKET_OPEN_RIGHT.indexOf(ch) >= 0 ? "right" : "center"; }
|
||||||
|
for (const ch of BD_CLOSE) { t[ch] = BRACKET_CLOSE_LEFT.indexOf(ch) >= 0 ? "left" : "center"; }
|
||||||
|
return t;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 點號偏側因簡繁字形而異。簡體(GBK/宋體系)點號皆沉於左下角、墨色偏左、空白
|
||||||
|
// 在右 → 全判 left(經方正書宋 GBK 實測:,、。;:!? 墨色 inkL≈0.1–0.18、
|
||||||
|
// inkR≈0.58–0.68,一致偏左)。繁體(Noto/教育部字形)點號置中 → center。
|
||||||
|
const BIAS_HANS: Record<string, Bias> = {
|
||||||
|
",": "left", "、": "left", "。": "left",
|
||||||
|
";": "left", ":": "left", "!": "left", "?": "left",
|
||||||
|
};
|
||||||
|
const BIAS_HANT: Record<string, Bias> = {
|
||||||
|
",": "center", "、": "center", "。": "center",
|
||||||
|
";": "center", ":": "center", "!": "center", "?": "center",
|
||||||
|
};
|
||||||
|
|
||||||
|
const BIAS_DEFAULT: Record<LangClass, Record<string, Bias>> = {
|
||||||
|
"zh-Hans": BIAS_HANS,
|
||||||
|
"zh-Hant": BIAS_HANT,
|
||||||
|
"ja": BIAS_HANT, // 日文點號慣例居中,沿用繁體表
|
||||||
|
"other": BIAS_HANT,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析標點字元在某語言下之墨色偏側(margin 後備模式用)。
|
||||||
|
* 優先序:消費端 overrides[langClass][ch] > 括號表 > 內建點號參考表。
|
||||||
|
* 非可壓縮標點(間隔/連接號等)回傳 null(margin 模式不收)。
|
||||||
|
*/
|
||||||
|
export function inkBias(
|
||||||
|
ch: string,
|
||||||
|
langClass: LangClass,
|
||||||
|
overrides?: BiasTable,
|
||||||
|
): Bias | null
|
||||||
|
{
|
||||||
|
const ov = overrides ? overrides[langClass] : undefined;
|
||||||
|
if (ov && ov[ch]) { return ov[ch]; }
|
||||||
|
const bracket = BIAS_BRACKETS[ch];
|
||||||
|
if (bracket) { return bracket; }
|
||||||
|
const t = BIAS_DEFAULT[langClass] || BIAS_DEFAULT.other;
|
||||||
|
return t[ch] || null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,11 @@ jz-jinze
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* justifyAtoms:false(分頁器相容,如 Paged.js):改 inline,避免 inline-block
|
||||||
|
邊界被分頁對齊引擎當伸縮點、與 CJK inter-ideograph 疊加致窄欄散架。
|
||||||
|
white-space:nowrap 於 inline 仍綁定群組內不斷行(禁則保留)。 */
|
||||||
|
[data-jz-atoms="inline"] jz-jinze
|
||||||
|
{
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|||||||
+79
-1
@@ -1,5 +1,6 @@
|
|||||||
/* 標點擠壓 + justify 原子化(§6.3/§8)。
|
/* 標點擠壓 + justify 原子化(§6.3/§8)。
|
||||||
擠壓量以負 margin 託管,跨瀏覽器一致(I4),不依賴 halt/text-spacing-trim。 */
|
半形渲染:預設 halt(字型 OpenType);缺 halt 字型可切 margin 後備模式
|
||||||
|
(width:0.5em+字身偏側位移,以 CSS 盒模型重現 halt,見下)。跨瀏覽器一致(I4)。 */
|
||||||
|
|
||||||
/* §8:每個 jz-char 為 justify 之單一原子項——句末標點只貢獻一份對齊伸縮量
|
/* §8:每個 jz-char 為 justify 之單一原子項——句末標點只貢獻一份對齊伸縮量
|
||||||
(I5)。inline-block 使標點與其字面留白同屬一項,對齊伸縮只落於項與項之間。
|
(I5)。inline-block 使標點與其字面留白同屬一項,對齊伸縮只落於項與項之間。
|
||||||
@@ -17,6 +18,14 @@ jz-inner
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* justifyAtoms:false(分頁器相容):jz-char 改 inline。半形仍由 jz-inner 之 halt
|
||||||
|
渲染;惟 margin 後備模式之 width:0.5em 依賴 inline-block,故 inline 下半形改回
|
||||||
|
依賴字型 halt(缺 halt 字型 + 分頁器對齊二者不可兼得,見 README)。 */
|
||||||
|
[data-jz-atoms="inline"] jz-char
|
||||||
|
{
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
/* 半形擠壓:用字型 OpenType `halt`(橫排半形)收字身寬度——由字型按字形正確
|
/* 半形擠壓:用字型 OpenType `halt`(橫排半形)收字身寬度——由字型按字形正確
|
||||||
定位(簡體點號偏左、繁體居中、括號偏側皆正確),不拉鄰居故不重疊(修正
|
定位(簡體點號偏左、繁體居中、括號偏側皆正確),不拉鄰居故不重疊(修正
|
||||||
負 margin 對居中字形如 !?/繁體 。, 之缺陷)。需字型含 halt(Noto CJK
|
負 margin 對居中字形如 !?/繁體 。, 之缺陷)。需字型含 halt(Noto CJK
|
||||||
@@ -30,3 +39,72 @@ jz-char.jz-half-le jz-inner
|
|||||||
font-feature-settings: "halt" 1;
|
font-feature-settings: "halt" 1;
|
||||||
-webkit-font-feature-settings: "halt" 1;
|
-webkit-font-feature-settings: "halt" 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* margin 後備模式(.feedback):消費端字型缺 halt 時,render() 於 root 設
|
||||||
|
data-jz-halfwidth="margin"。**機制**:把 jz-char 之 inline-block 寬收為 0.5em
|
||||||
|
(真半形 advance——相鄰兩標點各 0.5em、合佔 1em,無負 margin 之鄰接疊加/單側
|
||||||
|
重疊問題),字身(jz-inner)以 overflow 溢出該盒、再依墨色偏側左移使墨色落於
|
||||||
|
0.5em 盒內、空白溢向外側——即以 CSS 盒模型重現 halt 之「收字身寬+按字形定位」。
|
||||||
|
left 墨偏左 → 不移(字身左對齊,墨在盒內、右側空白溢出)
|
||||||
|
center 墨居中 → 左移 0.25em(墨置盒中)
|
||||||
|
right 墨偏右 → 左移 0.5em(墨之右半落入盒,左側空白溢向前一字外側)
|
||||||
|
偏側由 JS 依「ink 中心參考表+消費端 options.jiya.bias 覆寫」於 charify 標為
|
||||||
|
jz-ink-{left,center,right}(見 core/unicode.ts inkBias)。先關 halt 免雙重作用。
|
||||||
|
只命中帶 jz-ink-* 者;bias=full(無 ink class)不收、維持全形。 */
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half jz-inner,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le jz-inner
|
||||||
|
{
|
||||||
|
font-feature-settings: normal;
|
||||||
|
-webkit-font-feature-settings: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half.jz-ink-left,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half.jz-ink-center,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half.jz-ink-right,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le.jz-ink-left,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le.jz-ink-center,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le.jz-ink-right
|
||||||
|
{
|
||||||
|
width: 0.5em;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half.jz-ink-left jz-inner,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half.jz-ink-center jz-inner,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half.jz-ink-right jz-inner,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le.jz-ink-left jz-inner,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le.jz-ink-center jz-inner,
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-half-le.jz-ink-right jz-inner
|
||||||
|
{
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-ink-center jz-inner
|
||||||
|
{
|
||||||
|
margin-left: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-jz-halfwidth="margin"] jz-char.jz-ink-right jz-inner
|
||||||
|
{
|
||||||
|
margin-left: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 粒度控制:模式由 data-jz-halfwidth **屬性所在子樹** 決定(CSS 後代選擇器),
|
||||||
|
故可掛於任意祖先——root(JS 全域預設)、某 lang 容器、單一段落皆可,無需重設
|
||||||
|
實例。下列 halt-reset 規則置於 margin 規則之後:當 margin 頁面中**巢狀**一塊
|
||||||
|
data-jz-halfwidth="halt"(如該塊改用含 halt 之字型)時,憑來源順序勝出,於該
|
||||||
|
島內復原 halt、撤銷半形盒。常見情境(halt 頁中嵌 margin 塊、或 margin 頁中嵌
|
||||||
|
halt 島)皆成立。 */
|
||||||
|
[data-jz-halfwidth="halt"] jz-char.jz-half jz-inner,
|
||||||
|
[data-jz-halfwidth="halt"] jz-char.jz-half-le jz-inner
|
||||||
|
{
|
||||||
|
font-feature-settings: "halt" 1;
|
||||||
|
-webkit-font-feature-settings: "halt" 1;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-jz-halfwidth="halt"] jz-char.jz-half,
|
||||||
|
[data-jz-halfwidth="halt"] jz-char.jz-half-le
|
||||||
|
{
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|||||||
+19
-14
@@ -1,30 +1,35 @@
|
|||||||
/* 依 lang 之字體堆疊(§6.9)。以 :where() 零特異度,易被消費端覆寫;限於
|
/* 依 lang 之字體堆疊(§6.9)。**僅作預設後備**,消費端自訂字型恆優先。
|
||||||
.juzhen 容器,非破壞、opt-in。簡繁日各取對應 Noto CJK,使 locl 字形正確。
|
兩項關鍵:
|
||||||
名稱同時涵蓋 webfont(Google Fonts「Noto Serif SC」等)與本機安裝
|
1. 整個選擇器包進 :where() → 零特異度,消費端任何 `.juzhen { font-family }`
|
||||||
(「Noto Serif CJK SC」「Source Han Serif SC」)——擠壓依賴字型之 OpenType
|
(特異度 ≥ 0,1,0)即可覆寫。
|
||||||
`halt`,Noto/思源具備;務必確保實際命中含 halt 之字型(Safari 對缺 halt
|
2. 只命中**帶 `lang` 屬性之元素**(容器或內層區塊),**不**直接命中 jz-inner/
|
||||||
之 fallback 字型不會擠壓,故 demo 以 webfont 明確載入 Noto 作參考)。 */
|
jz-char(它們無 lang 屬性,靠繼承取字型)——否則會在標點字面容器上強制 Noto,
|
||||||
|
蓋掉消費端字型(曾致 demo 失真:消費端設 FZShuSong 卻仍以 Noto 之 halt 擠壓)。
|
||||||
|
簡繁日各取對應 Noto CJK,使 locl 字形正確。名稱涵蓋 webfont(「Noto Serif SC」)
|
||||||
|
與本機(「Noto Serif CJK SC」「Source Han Serif SC」)。擠壓半形依字型 OpenType
|
||||||
|
`halt`:消費端若用缺 halt 之字型(如方正書宋 GBK),請改用 margin 後備模式
|
||||||
|
(見 README「halt/margin 後備」)。 */
|
||||||
|
|
||||||
:where(.juzhen):lang(zh-Hans),
|
:where(.juzhen:lang(zh-Hans)),
|
||||||
:where(.juzhen) :lang(zh-Hans)
|
:where(.juzhen [lang]:lang(zh-Hans))
|
||||||
{
|
{
|
||||||
font-family: "Noto Serif SC", "Noto Serif CJK SC", "Source Han Serif SC", serif;
|
font-family: "Noto Serif SC", "Noto Serif CJK SC", "Source Han Serif SC", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.juzhen):lang(zh-Hant),
|
:where(.juzhen:lang(zh-Hant)),
|
||||||
:where(.juzhen) :lang(zh-Hant)
|
:where(.juzhen [lang]:lang(zh-Hant))
|
||||||
{
|
{
|
||||||
font-family: "Noto Serif TC", "Noto Serif CJK TC", "Source Han Serif TC", serif;
|
font-family: "Noto Serif TC", "Noto Serif CJK TC", "Source Han Serif TC", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.juzhen):lang(zh-HK),
|
:where(.juzhen:lang(zh-HK)),
|
||||||
:where(.juzhen) :lang(zh-HK)
|
:where(.juzhen [lang]:lang(zh-HK))
|
||||||
{
|
{
|
||||||
font-family: "Noto Serif HK", "Noto Serif CJK HK", "Noto Serif TC", serif;
|
font-family: "Noto Serif HK", "Noto Serif CJK HK", "Noto Serif TC", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.juzhen):lang(ja),
|
:where(.juzhen:lang(ja)),
|
||||||
:where(.juzhen) :lang(ja)
|
:where(.juzhen [lang]:lang(ja))
|
||||||
{
|
{
|
||||||
font-family: "Noto Serif JP", "Noto Serif CJK JP", serif;
|
font-family: "Noto Serif JP", "Noto Serif CJK JP", serif;
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-1
@@ -14,7 +14,7 @@ import { jinzePass } from "./typeset/jinze.js";
|
|||||||
import { gapTrimPass, lineEdgePass } from "./typeset/lineedge.js";
|
import { gapTrimPass, lineEdgePass } from "./typeset/lineedge.js";
|
||||||
import { longWordPass } from "./typeset/longword.js";
|
import { longWordPass } from "./typeset/longword.js";
|
||||||
import { spacingPass } from "./typeset/spacing.js";
|
import { spacingPass } from "./typeset/spacing.js";
|
||||||
import type { JuzhenOptions, LongWordOptions, Pass, RenderContext, ResolvedOptions } from "./types.js";
|
import type { BiasTable, JiyaOptions, JuzhenOptions, LongWordOptions, Pass, RenderContext, ResolvedOptions } from "./types.js";
|
||||||
|
|
||||||
export type { JuzhenOptions, ResolvedOptions, Pass };
|
export type { JuzhenOptions, ResolvedOptions, Pass };
|
||||||
export { createCjkAutospace } from "./compat/v1.js";
|
export { createCjkAutospace } from "./compat/v1.js";
|
||||||
@@ -56,6 +56,20 @@ function resolveLongWord(v: boolean | LongWordOptions | undefined): { minLength:
|
|||||||
return { minLength, lang };
|
return { minLength, lang };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveJiya(
|
||||||
|
v: boolean | JiyaOptions | undefined,
|
||||||
|
): { halfWidth: "halt" | "margin"; bias: BiasTable }
|
||||||
|
{
|
||||||
|
if (v && typeof v === "object")
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
halfWidth: v.halfWidth === "margin" ? "margin" : "halt",
|
||||||
|
bias: v.bias || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { halfWidth: "halt", bias: {} };
|
||||||
|
}
|
||||||
|
|
||||||
/** 正規化公開選項為內部 ResolvedOptions。 */
|
/** 正規化公開選項為內部 ResolvedOptions。 */
|
||||||
export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
||||||
{
|
{
|
||||||
@@ -68,6 +82,7 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
|||||||
policy: langOpt.policy || {},
|
policy: langOpt.policy || {},
|
||||||
},
|
},
|
||||||
autospaceClass: opts.autospaceClass || "",
|
autospaceClass: opts.autospaceClass || "",
|
||||||
|
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||||
scope: {
|
scope: {
|
||||||
root: scope.root || null,
|
root: scope.root || null,
|
||||||
include: scope.include || null,
|
include: scope.include || null,
|
||||||
@@ -84,6 +99,7 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
|||||||
ruby: feature(opts.ruby, false),
|
ruby: feature(opts.ruby, false),
|
||||||
},
|
},
|
||||||
longWord: resolveLongWord(opts.longWord),
|
longWord: resolveLongWord(opts.longWord),
|
||||||
|
jiyaConfig: resolveJiya(opts.jiya),
|
||||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||||
finder: {
|
finder: {
|
||||||
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
||||||
@@ -134,12 +150,24 @@ export function createJuzhen(opts: JuzhenOptions = {}): Juzhen
|
|||||||
const el = resolveRoot(root, options);
|
const el = resolveRoot(root, options);
|
||||||
if (!el) { return; }
|
if (!el) { return; }
|
||||||
runPasses(PASSES, makeCtx(el));
|
runPasses(PASSES, makeCtx(el));
|
||||||
|
// margin 後備模式以 root 屬性作 CSS 開關(見 css/_jiya.css);halt 模式不設。
|
||||||
|
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin")
|
||||||
|
{
|
||||||
|
el.setAttribute("data-jz-halfwidth", "margin");
|
||||||
|
}
|
||||||
|
// justifyAtoms:false → jz-char/jz-jinze 改 inline(分頁器相容,見 css)。
|
||||||
|
if (!options.justifyAtoms)
|
||||||
|
{
|
||||||
|
el.setAttribute("data-jz-atoms", "inline");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
revert(root?: Element | string): void
|
revert(root?: Element | string): void
|
||||||
{
|
{
|
||||||
const el = resolveRoot(root, options);
|
const el = resolveRoot(root, options);
|
||||||
if (!el) { return; }
|
if (!el) { return; }
|
||||||
revertPasses(PASSES, makeCtx(el));
|
revertPasses(PASSES, makeCtx(el));
|
||||||
|
el.removeAttribute("data-jz-halfwidth");
|
||||||
|
el.removeAttribute("data-jz-atoms");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-2
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import type { Finder } from "./core/finder.js";
|
import type { Finder } from "./core/finder.js";
|
||||||
import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js";
|
import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js";
|
||||||
|
import type { Bias, BiasTable } from "./core/unicode.js";
|
||||||
|
|
||||||
export type { LangClass, PunctStyle };
|
export type { LangClass, PunctStyle, Bias, BiasTable };
|
||||||
|
|
||||||
/** 長英文詞包裝設定。 */
|
/** 長英文詞包裝設定。 */
|
||||||
export interface LongWordOptions
|
export interface LongWordOptions
|
||||||
@@ -12,6 +13,18 @@ export interface LongWordOptions
|
|||||||
lang?: string;
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 標點擠壓設定(架構 §6.3)。布林 true 等同 { halfWidth: "halt" }。 */
|
||||||
|
export interface JiyaOptions
|
||||||
|
{
|
||||||
|
/** 半形渲染機制:
|
||||||
|
* - "halt"(預設)— 用字型 OpenType `halt`,由字型按字形正確定位,需字型支援;
|
||||||
|
* - "margin" — 負 margin 後備模式,供缺 halt 之字型,依字形偏側表收半形。 */
|
||||||
|
halfWidth?: "halt" | "margin";
|
||||||
|
/** margin 後備模式之字形偏側覆寫表(逐 lang 逐字),覆寫內建參考預設。
|
||||||
|
* halt 模式不讀此欄。 */
|
||||||
|
bias?: BiasTable;
|
||||||
|
}
|
||||||
|
|
||||||
/** 分塊功能選擇:選擇器 → 該塊內生效之功能集(覆寫全域)。 */
|
/** 分塊功能選擇:選擇器 → 該塊內生效之功能集(覆寫全域)。 */
|
||||||
export interface BlockRule
|
export interface BlockRule
|
||||||
{
|
{
|
||||||
@@ -36,6 +49,12 @@ export interface JuzhenOptions
|
|||||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||||
autospaceClass?: string;
|
autospaceClass?: string;
|
||||||
|
|
||||||
|
/** justify 原子化:jz-char/jz-jinze 是否以 inline-block 作對齊單一原子
|
||||||
|
* (預設 true)。設 false 改 display:inline——用於分頁器(如 Paged.js)之
|
||||||
|
* 對齊引擎會把每個 inline-block 邊界當伸縮點、與 CJK inter-ideograph 疊加
|
||||||
|
* 致窄欄散架之情形(halt 半形與 jinze nowrap 仍保留)。 */
|
||||||
|
justifyAtoms?: boolean;
|
||||||
|
|
||||||
/** 作用域限定(§3.6)。 */
|
/** 作用域限定(§3.6)。 */
|
||||||
scope?: {
|
scope?: {
|
||||||
root?: Element | string;
|
root?: Element | string;
|
||||||
@@ -46,7 +65,7 @@ export interface JuzhenOptions
|
|||||||
// 全域逐功能開關。
|
// 全域逐功能開關。
|
||||||
spacing?: boolean;
|
spacing?: boolean;
|
||||||
longWord?: boolean | LongWordOptions;
|
longWord?: boolean | LongWordOptions;
|
||||||
jiya?: boolean;
|
jiya?: boolean | JiyaOptions;
|
||||||
jinze?: boolean;
|
jinze?: boolean;
|
||||||
hanging?: boolean;
|
hanging?: boolean;
|
||||||
biaodian?: boolean;
|
biaodian?: boolean;
|
||||||
@@ -70,6 +89,7 @@ export interface ResolvedOptions
|
|||||||
{
|
{
|
||||||
locale: LocaleConfig;
|
locale: LocaleConfig;
|
||||||
autospaceClass: string;
|
autospaceClass: string;
|
||||||
|
justifyAtoms: boolean;
|
||||||
scope: {
|
scope: {
|
||||||
root: Element | string | null;
|
root: Element | string | null;
|
||||||
include: string | null;
|
include: string | null;
|
||||||
@@ -86,6 +106,8 @@ export interface ResolvedOptions
|
|||||||
ruby: boolean;
|
ruby: boolean;
|
||||||
};
|
};
|
||||||
longWord: { minLength: number; lang: string };
|
longWord: { minLength: number; lang: string };
|
||||||
|
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
||||||
|
jiyaConfig: { halfWidth: "halt" | "margin"; bias: BiasTable };
|
||||||
blocks: BlockRule[];
|
blocks: BlockRule[];
|
||||||
finder: {
|
finder: {
|
||||||
styleInlines: Set<string>;
|
styleInlines: Set<string>;
|
||||||
|
|||||||
+16
-4
@@ -14,9 +14,9 @@
|
|||||||
// 同時為 §8:所有 jz-char 經 CSS 設 inline-block,成 justify 單一原子項(I5)。
|
// 同時為 §8:所有 jz-char 經 CSS 設 inline-block,成 justify 單一原子項(I5)。
|
||||||
|
|
||||||
import { bdClassOf, createJz, revertPass } from "../core/dom.js";
|
import { bdClassOf, createJz, revertPass } from "../core/dom.js";
|
||||||
import { resolveStyle } from "../core/locale.js";
|
import { classify, effectiveLang, resolveStyle } from "../core/locale.js";
|
||||||
import type { PunctStyle } from "../core/locale.js";
|
import type { LangClass, PunctStyle } from "../core/locale.js";
|
||||||
import { blankSides, classifyBiaodian } from "../core/unicode.js";
|
import { blankSides, classifyBiaodian, inkBias } from "../core/unicode.js";
|
||||||
import type { CharifyPass, RenderContext, StandalonePass, WrapRequest } from "../types.js";
|
import type { CharifyPass, RenderContext, StandalonePass, WrapRequest } from "../types.js";
|
||||||
|
|
||||||
const PASS = "jiya";
|
const PASS = "jiya";
|
||||||
@@ -64,13 +64,25 @@ export const jiyaPass: CharifyPass = {
|
|||||||
const data = node.data;
|
const data = node.data;
|
||||||
const out: WrapRequest[] = [];
|
const out: WrapRequest[] = [];
|
||||||
let style: PunctStyle | null = null;
|
let style: PunctStyle | null = null;
|
||||||
|
let langClass: LangClass | null = null;
|
||||||
|
const biasOverrides = ctx.options.jiyaConfig.bias;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 1)
|
for (let i = 0; i < data.length; i += 1)
|
||||||
{
|
{
|
||||||
const bdClass = classifyBiaodian(data.charAt(i));
|
const ch = data.charAt(i);
|
||||||
|
const bdClass = classifyBiaodian(ch);
|
||||||
if (!bdClass) { continue; }
|
if (!bdClass) { continue; }
|
||||||
if (style === null) { style = resolveStyle(node, ctx.options.locale); }
|
if (style === null) { style = resolveStyle(node, ctx.options.locale); }
|
||||||
|
if (langClass === null)
|
||||||
|
{
|
||||||
|
langClass = classify(effectiveLang(node, ctx.options.locale.default));
|
||||||
|
}
|
||||||
const classes = baselineHalf(style, bdClass) ? [ bdClass, "jz-half" ] : [ bdClass ];
|
const classes = baselineHalf(style, bdClass) ? [ bdClass, "jz-half" ] : [ bdClass ];
|
||||||
|
// 字形偏側 class(jz-ink-*)— 供 margin 後備模式收對側半形空白;halt 模式
|
||||||
|
// 之 CSS 不讀,無害。一律於 charify 標記(偏側為字形固有屬性,與何時擠壓
|
||||||
|
// 無關),鄰接/行端 pass 後續加 jz-half 即可沿用。
|
||||||
|
const bias = inkBias(ch, langClass, biasOverrides);
|
||||||
|
if (bias) { classes.push("jz-ink-" + bias); }
|
||||||
out.push({
|
out.push({
|
||||||
start: i,
|
start: i,
|
||||||
end: i + 1,
|
end: i + 1,
|
||||||
|
|||||||
+20
-3
@@ -28,8 +28,16 @@ const RBR_BASIC = "\\)\\]\\}";
|
|||||||
const LBR_EXT = "\\(\\[\\{<>";
|
const LBR_EXT = "\\(\\[\\{<>";
|
||||||
const RBR_EXT = "\\)\\]\\}<>";
|
const RBR_EXT = "\\)\\]\\}<>";
|
||||||
|
|
||||||
|
// 純刪除哨符(U+E003):標記「此處原作者空格應刪除、不留隙」(CJK↔CJK,問題 1b)。
|
||||||
|
// 與空格 1:1 替換以保持 spaced/text 對齊(processBlock 據此刪空格而不插 jz-hws)。
|
||||||
|
const DEL = String.fromCharCode(0xE003);
|
||||||
|
|
||||||
const STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
const STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||||||
const STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
const STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||||||
|
// CJK↔CJK 之單一作者空格(含跨樣式行內標籤,collectRuns 已透明合併):中文不用
|
||||||
|
// 詞間空格,此類空格幾乎必為贅餘 → 以 DEL 標記刪除(1b,邊界正規化一致性)。
|
||||||
|
// 用 lookahead 使連續 CJK 空格逐一命中;只匹配 ASCII 空格,全形空格 U+3000 不動。
|
||||||
|
const STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||||
|
|
||||||
const DOTS_CJK = new RegExp("([\\.]{2,}|…)([" + CJK + "])", "g");
|
const DOTS_CJK = new RegExp("([\\.]{2,}|…)([" + CJK + "])", "g");
|
||||||
const CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
const CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||||
@@ -72,6 +80,7 @@ function panguSpace(text: string): string
|
|||||||
let s = text;
|
let s = text;
|
||||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||||||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||||||
|
s = s.replace(STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||||
|
|
||||||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||||||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||||
@@ -164,6 +173,14 @@ function processBlock(block: Element, finder: Finder): void
|
|||||||
while (spacedIdx < spaced.length)
|
while (spacedIdx < spaced.length)
|
||||||
{
|
{
|
||||||
const sc = spaced.charAt(spacedIdx);
|
const sc = spaced.charAt(spacedIdx);
|
||||||
|
if (sc === DEL)
|
||||||
|
{
|
||||||
|
// CJK↔CJK 贅餘空格:純刪除、不留隙(before 留空)。
|
||||||
|
actions.push({ before: undefined, space: map[origIdx]! });
|
||||||
|
origIdx += 1;
|
||||||
|
spacedIdx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (sc === MARK)
|
if (sc === MARK)
|
||||||
{
|
{
|
||||||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||||||
@@ -183,17 +200,17 @@ function processBlock(block: Element, finder: Finder): void
|
|||||||
spacedIdx += 1;
|
spacedIdx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 倒序執行,避免前面動作之 offset 被後面變更影響。
|
// 倒序執行,避免前面動作之 offset 被後面變更影響。先刪空格(若有)、再包左字
|
||||||
|
// 留隙(若有 before)。刪空格在較高 offset、不擾動更前之 before。
|
||||||
for (let i = actions.length - 1; i >= 0; i -= 1)
|
for (let i = actions.length - 1; i >= 0; i -= 1)
|
||||||
{
|
{
|
||||||
const act = actions[i]!;
|
const act = actions[i]!;
|
||||||
if (!act.before) { continue; }
|
|
||||||
if (act.space)
|
if (act.space)
|
||||||
{
|
{
|
||||||
const tn = act.space.textNode;
|
const tn = act.space.textNode;
|
||||||
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
||||||
}
|
}
|
||||||
wrapGapLeft(act.before);
|
if (act.before) { wrapGapLeft(act.before); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,52 @@ test("政策:未設 style/policy 時回退內建預設(繁全角不擠、
|
|||||||
assert.ok(!ps[1].querySelector("jz-char.bd-pause").classList.contains("jz-half"), "繁體內建全角");
|
assert.ok(!ps[1].querySelector("jz-char.bd-pause").classList.contains("jz-half"), "繁體內建全角");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- 字形偏側(jz-ink-*)+ margin 後備模式(.feedback)-----
|
||||||
|
|
||||||
|
test("偏側:jz-ink-* 恆於 charify 標記(halt 預設模式亦然,root 不設 margin 屬性)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p lang="zh-Hans">好,「走」。</p>');
|
||||||
|
createJuzhen(JIYA_ONLY).render(document.body);
|
||||||
|
// 簡體點號偏左;角括號「」墨色強偏一側 → 開 right、閉 left(圓方括號才 center)。
|
||||||
|
assert.ok(document.querySelector("jz-char.bd-pause").classList.contains("jz-ink-left"), "簡體 ,偏左");
|
||||||
|
assert.ok(document.querySelector("jz-char.bd-open").classList.contains("jz-ink-right"), "角括號「墨偏右 → right");
|
||||||
|
assert.ok(document.querySelector("jz-char.bd-close").classList.contains("jz-ink-left"), "角括號」墨偏左 → left");
|
||||||
|
assert.equal(document.body.getAttribute("data-jz-halfwidth"), null, "halt 預設模式不設 root 屬性");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("偏側:繁體點號居中(jz-ink-center),!?恆居中", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p lang="zh-Hant">走,真?</p>');
|
||||||
|
createJuzhen(JIYA_ONLY).render(document.body);
|
||||||
|
assert.ok(document.querySelector("jz-char.bd-pause").classList.contains("jz-ink-center"), "繁體 ,居中");
|
||||||
|
assert.ok(document.querySelector("jz-char.bd-stop").classList.contains("jz-ink-center"), "?居中");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("margin 模式:render 於 root 設 data-jz-halfwidth=margin,revert 清除", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p lang="zh-Hans">好,走。</p>');
|
||||||
|
const jz = createJuzhen({ ...JIYA_ONLY, jiya: { halfWidth: "margin" } });
|
||||||
|
jz.render(document.body);
|
||||||
|
assert.equal(document.body.getAttribute("data-jz-halfwidth"), "margin", "margin 模式設 root 屬性");
|
||||||
|
// 偏側 class 與 jz-half 並存,CSS 始能定向收縮。
|
||||||
|
const comma = document.querySelector("jz-char.bd-pause");
|
||||||
|
assert.ok(comma.classList.contains("jz-half") && comma.classList.contains("jz-ink-left"), ",半形+偏左");
|
||||||
|
jz.revert(document.body);
|
||||||
|
assert.equal(document.body.getAttribute("data-jz-halfwidth"), null, "revert 清除 root 屬性");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("margin 模式:消費端 bias 覆寫參考表(如把簡體 ,改判居中)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p lang="zh-Hans">好,走</p>');
|
||||||
|
createJuzhen({
|
||||||
|
...JIYA_ONLY,
|
||||||
|
jiya: { halfWidth: "margin", bias: { "zh-Hans": { ",": "center" } } },
|
||||||
|
}).render(document.body);
|
||||||
|
const comma = document.querySelector("jz-char.bd-pause");
|
||||||
|
assert.ok(comma.classList.contains("jz-ink-center"), "覆寫後 ,判居中");
|
||||||
|
assert.ok(!comma.classList.contains("jz-ink-left"), "不再用參考預設之偏左");
|
||||||
|
});
|
||||||
|
|
||||||
// ----- 長英文詞(longWord)-----
|
// ----- 長英文詞(longWord)-----
|
||||||
|
|
||||||
test("longWord:[A-Za-z]{6,} 包進 span[lang],textContent 不變", () =>
|
test("longWord:[A-Za-z]{6,} 包進 span[lang],textContent 不變", () =>
|
||||||
@@ -291,6 +337,59 @@ test("分塊:data-juzhen-off 黑名單停用列出功能,其餘繼承", () =
|
|||||||
assert.equal(countTag(p, "jz-char"), 0, "jiya 被黑名單停用");
|
assert.equal(countTag(p, "jz-char"), 0, "jiya 被黑名單停用");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- 下游回饋修復(問題一 1a/1b、問題二)-----
|
||||||
|
|
||||||
|
test("1a:pill 後接 CJK 標點時,pill 左緣仍補 jz-hws(穿越 jz-jinze)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom("<p>代码<code>build</code>,后</p>");
|
||||||
|
createJuzhen({ pillSelector: "code, kbd", lang: { default: "zh-Hans", style: "quanjiao" } })
|
||||||
|
.render(document.body);
|
||||||
|
// jinze 已把 code+,納入 jz-jinze;spacing 仍須能找到 code 左鄰字「码」並補間隙。
|
||||||
|
const hws = document.querySelector("jz-hws");
|
||||||
|
assert.ok(hws && hws.textContent === "码", "pill 左側「码」包入 jz-hws(不因右側標點/jz-jinze 而遺失)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1a:pill 後接 CJK 標點且有作者空格 → 空格被剝除、左緣補間隙", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom("<p>代码 <code>build</code>,后</p>");
|
||||||
|
createJuzhen({ pillSelector: "code, kbd", lang: { default: "zh-Hans", style: "quanjiao" } })
|
||||||
|
.render(document.body);
|
||||||
|
assert.ok(document.querySelector("jz-hws"), "補了 jz-hws");
|
||||||
|
assert.equal(document.querySelector("p").textContent, "代码build,后", "作者空格被剝除(與無空格輸入一致)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1b:CJK↔CJK 贅餘作者空格被剝除(含跨樣式行內標籤)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p><span class="b">事件</span> 元件</p>');
|
||||||
|
createJuzhen({ lang: { default: "zh-Hans" } }).render(document.body);
|
||||||
|
assert.equal(document.querySelector("p").textContent, "事件元件", "跨 <span> 之 CJK↔CJK 空格剝除");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1b:CJK↔CJK 不插 jz-hws(僅剝空格,無留隙)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom("<p>事件 元件</p>");
|
||||||
|
createJuzhen({ spacing: true, jiya: false, jinze: false, longWord: false }).render(document.body);
|
||||||
|
assert.equal(countTag(document.body, "jz-hws"), 0, "CJK↔CJK 不需間隙,僅刪空格");
|
||||||
|
assert.equal(document.querySelector("p").textContent, "事件元件");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("問題二:justifyAtoms:false → root 設 data-jz-atoms=inline,revert 清除", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom("<p lang='zh-Hans'>测试,(甲)。</p>");
|
||||||
|
const jz = createJuzhen({ lang: { default: "zh-Hans" }, justifyAtoms: false });
|
||||||
|
jz.render(document.body);
|
||||||
|
assert.equal(document.body.getAttribute("data-jz-atoms"), "inline", "設 inline 原子屬性");
|
||||||
|
jz.revert(document.body);
|
||||||
|
assert.equal(document.body.getAttribute("data-jz-atoms"), null, "revert 清除");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("問題二:justifyAtoms 預設 true → 不設屬性(維持 inline-block 原子)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom("<p lang='zh-Hans'>测试,</p>");
|
||||||
|
createJuzhen({ lang: { default: "zh-Hans" } }).render(document.body);
|
||||||
|
assert.equal(document.body.getAttribute("data-jz-atoms"), null, "預設不設屬性");
|
||||||
|
});
|
||||||
|
|
||||||
// ----- v1 相容層(§3.5)-----
|
// ----- v1 相容層(§3.5)-----
|
||||||
|
|
||||||
test("compat:createCjkAutospace().apply() 可跑,產生中西間隙(margin 模型,copy-clean)", () =>
|
test("compat:createCjkAutospace().apply() 可跑,產生中西間隙(margin 模型,copy-clean)", () =>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""量測字型之標點 glyph 墨色位置,產出聚珍 margin 後備模式之偏側建議表。
|
||||||
|
|
||||||
|
用途:消費端字型缺 OpenType `halt`(如方正書宋 GBK、多數系統字型)時,margin 後備
|
||||||
|
模式需知每個標點之「墨色偏側」以於 0.5em 半形盒內正確定位字身。本工具讀字型輪廓,
|
||||||
|
量各標點 glyph 之 ink bounding box,依墨色中心建議 left/center/right/full,並印出
|
||||||
|
可貼入 `options.jiya.bias` 之 JSON 片段。
|
||||||
|
|
||||||
|
依賴:fonttools(`pip install fonttools`)。
|
||||||
|
用法:python3 tools/measure-bias.py /path/to/font.ttf [zh-Hans|zh-Hant|ja|other]
|
||||||
|
|
||||||
|
輸出之偏側語義(決定字身於 0.5em 盒內之左移;見 README「halt/margin 後備」):
|
||||||
|
left 墨偏左 → 不移(字身左對齊,墨在盒內、右側空白溢出)
|
||||||
|
center 墨居中 → 左移 0.25em
|
||||||
|
right 墨偏右 → 左移 0.5em(右半入盒,左側空白溢出)
|
||||||
|
full 無 glyph/空白 glyph → 不收、維持全形
|
||||||
|
門檻:ink 中心 < 0.375 → left;0.375–0.625 → center;> 0.625 → right。
|
||||||
|
判讀後仍建議於瀏覽器目視微調(字身側鬚、字面留白因字型而異)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from fontTools.pens.boundsPen import BoundsPen
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
|
||||||
|
# 聚珍 margin 模式會收半形之標點(句末點號僅半角式收,此處一併量供參考)。
|
||||||
|
PUNCT = {
|
||||||
|
"pause_stop": ",、。;:!?",
|
||||||
|
"open": "「『(《〈【〖〔[{“‘",
|
||||||
|
"close": "」』)》〉】〗〕]}”’",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_feature(font, tag):
|
||||||
|
for t in ("GSUB", "GPOS"):
|
||||||
|
if t in font and font[t].table.FeatureList:
|
||||||
|
for fr in font[t].table.FeatureList.FeatureRecord:
|
||||||
|
if fr.FeatureTag == tag:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def bias_of(font, upm, cmap, gs, ch):
|
||||||
|
cp = ord(ch)
|
||||||
|
if cp not in cmap:
|
||||||
|
return "full", None
|
||||||
|
g = gs[cmap[cp]]
|
||||||
|
pen = BoundsPen(gs)
|
||||||
|
g.draw(pen)
|
||||||
|
if pen.bounds is None:
|
||||||
|
return "full", None
|
||||||
|
x_min, _, x_max, _ = pen.bounds
|
||||||
|
center = ((x_min + x_max) / 2) / upm
|
||||||
|
if center < 0.375:
|
||||||
|
b = "left"
|
||||||
|
elif center > 0.625:
|
||||||
|
b = "right"
|
||||||
|
else:
|
||||||
|
b = "center"
|
||||||
|
return b, center
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
path = sys.argv[1]
|
||||||
|
lang = sys.argv[2] if len(sys.argv) > 2 else "zh-Hans"
|
||||||
|
|
||||||
|
font = TTFont(path)
|
||||||
|
upm = font["head"].unitsPerEm
|
||||||
|
cmap = font.getBestCmap()
|
||||||
|
gs = font.getGlyphSet()
|
||||||
|
|
||||||
|
halt = has_feature(font, "halt")
|
||||||
|
print(f"# 字型:{path}")
|
||||||
|
print(f"# OpenType halt:{'有(建議直接用 halt 預設模式,無需 margin 後備)' if halt else '無 → 需 margin 後備模式'}")
|
||||||
|
print(f"# unitsPerEm={upm}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
table = {}
|
||||||
|
for group, chars in PUNCT.items():
|
||||||
|
print(f"# {group}")
|
||||||
|
for ch in chars:
|
||||||
|
b, center = bias_of(font, upm, cmap, gs, ch)
|
||||||
|
cstr = f"{center:.2f}" if center is not None else " - "
|
||||||
|
print(f"# {ch} inkcenter={cstr} -> {b}")
|
||||||
|
table[ch] = b
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("// 貼入 options.jiya.bias(依瀏覽器目視再微調):")
|
||||||
|
print(json.dumps({lang: table}, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user