feat: 全角句點 . 納入句末挤压 + 連續標點挤压規則(雙側獨立+透明穿越)
本會話標點挤压一致性兩項(改內置默認;皆以 Noto CJK + 真實 Chromium 量測驗證)。 一、全角句點 .(U+FF0E)納入句末標點 - BD_STOP 由 。!? 擴為 。.!?——納入全角句點 .(異於表意句號 。U+3002)。實測 Noto CJK SC/TC 之 halt(GPOS SinglePos)皆覆蓋 .、挤压收半形 advance 0.5em,與 。 一致。inkBias 同步補 .(簡 left/繁 center),margin 後備亦可挤。 - 政策行為與 。!? 同:全角式與開明式句末保持全角 1em,banjiao(或鄰接被收)挤半。 - 設計原則:halt 即字型之「挤压意願」——字型不提供某標點 halt(字形占滿、不宜挤,如某些 字型之全角 ?、〈〉)則信任字型、保持全角不強挤;margin 後備僅用於整體無 halt 之字型。 二、連續標點挤压規則:雙側獨立 + 透明穿越(修正既有不一致) - 規則(用戶確定):相鄰兩標點,**對每一標點**獨立判斷——若其「有空白之側」緊鄰另一標點, 即挤掉該側空白(收半形)。原 jiyaAdjacencyPass 為「二選一只收一側」,致: · ,「(pause 右空白 + open 左空白)原只收 ,→ 全角式 1.5em;應雙側皆收 → 1em。 改 else-if 為雙獨立 if。修後:,「 全角式與開明式皆 1em;。」 全角式 1.5em/開明式 1em (」左為墨不收);·— 兩式皆 2em(不宜挤之例外)。 - 透明穿越:原以「同 parent 直接兄弟」掃描,漏掉跨 strong/em 等行內邊界之連續標點(致 跨邊界標點對未挤、達 2em)。改為從 root 之透明遍歷(穿越樣式行內;block 邊界與 avoid 子樹斷相鄰;jz-char 為原子不下探),跨 inline 邊界之連續標點亦正確配對。 - src/core/unicode.ts:BD_STOP +.;inkBias 表 +.。 - src/typeset/jiya.ts:jiyaAdjacencyPass 重寫(雙側收 + root 透明遍歷)。 - test:. 納入+連續挤压(雙側/句末單側/跨 inline/多標點鏈)回歸,70 測試通過。 - README/ARCHITECTURE:. 與連續挤压規則、halt 挤压意願原則;**涉長度處標明全角式/ 開明式**(。」 全角 1.5em/開明 1em 等)。dist 重建(僅 JS,juzhen.css 不變)。
This commit is contained in:
+21
-3
@@ -419,7 +419,7 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
||||
class 觸發(I3;改用 class 而非 CSS `:lang()`,以避免 `:lang(zh-Hans)` 不匹配
|
||||
`zh-CN` 等地區標籤之陷阱)。三機制並集落地:①**baseline 政策**(全角不擠/
|
||||
開明擠句內點號+括號引號/半角全擠);②**行內連續擠壓**(`jiyaAdjacencyPass`,
|
||||
相鄰標點對向側收一字,lang 無關);③**行端擠壓**(`lineedge.ts` layout pass)。
|
||||
lang 無關,見下「連續標點挤壓規則」);③**行端擠壓**(`lineedge.ts` layout pass)。
|
||||
- **跨瀏覽器(I4)— 修訂**:半形改以字型 OpenType **`halt`**(`font-feature-
|
||||
settings:"halt"`)渲染,**非**負 `margin`。原因:負 margin 假設可壓縮空白全在
|
||||
單側,對**居中字形**(`!?`、繁體 `locl` 下之 `。,`)會把字面拉入鄰字而**重疊**;
|
||||
@@ -427,6 +427,24 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
||||
不重疊。`font-feature-settings:"halt"` 各引擎一致(當初疑慮之不一致者為**自動**
|
||||
的 `text-spacing-trim`/`chws`,非顯式 `halt`);缺 `halt` 之字型優雅降級為全形。
|
||||
鄰接/行端之「收哪一字」之**決策**仍由 `blankSides` 邏輯定,渲染統一為 `halt`。
|
||||
- **句末標點集含全角句點 `.`(下游回饋)**:`BD_STOP` 為 `。.!?`——除表意句號 `。`
|
||||
(U+3002)外,納入**全角句點 `.`**(U+FF0E)。實測 Noto CJK SC/TC 之 `halt`(GPOS
|
||||
SinglePos 調 advance)皆覆蓋 `.`、挤壓收 0.5em,與 `。` 一致;`inkBias` 同步補 `.`
|
||||
(簡 left/繁 center),margin 後備亦可挤。政策行為與 `。!?` 同:quanjiao/kaiming 句末
|
||||
保持全角、banjiao 挤半。
|
||||
- **halt 即字型之「挤壓意願」(設計原則)**:字型對某標點提供 `halt` =該字形適合挤半;
|
||||
**不提供**=該字形不宜挤(占滿字框,如 Noto TC 之全角 `?` 與 SC 不同、`〈〉` 無 halt)。
|
||||
故聚珍**信任字型 halt 覆蓋**:有 halt 字型中個別無 halt 之字符,**保持全角、不強挤**(優雅
|
||||
降級),不為其特設規則。`margin` 後備僅用於**整體無 halt** 之字型(GBK 老字型等),以
|
||||
`inkBias` 逐字型調教近似 halt——非用來強挤「有 halt 字型刻意不挤」之字符。
|
||||
- **連續標點挤壓規則(雙側獨立 + 透明穿越;下游回饋)**:相鄰兩標點,**對每一標點**獨立
|
||||
判斷——若其「有空白之側」緊鄰另一標點,則挤掉該側空白(收半形 `jz-half`)。故 `,「`
|
||||
(pause 右空白 + open 左空白)兩側皆收 → 全角式與開明式皆合占 1em;`。」`(stop 右空白;
|
||||
」左為墨)只收 `。`→ 全角式 1.5em/開明式 1em(」baseline 已半);`·—`(皆無指向對方之
|
||||
空白)皆不收 → 兩式皆 2em(不宜挤之例外)。多標點鏈逐對處理。`jiyaAdjacencyPass` 以**從 root 之透明遍歷**(穿越 strong/
|
||||
em… 等樣式行內;block 邊界與 avoid 子樹斷相鄰;jz-char 為原子不下探)取代原「同 parent
|
||||
直接兄弟」掃描,使**跨 inline 邊界**之連續標點亦正確配對(原漏致跨 strong/em 之標點對未挤、
|
||||
達 2em)。
|
||||
- **`margin` 後備模式(.feedback 增補)**:消費端字型未必含 `halt`(多數系統字型、
|
||||
GBK 老字型,實測**方正書宋 GBK 僅含 `vert`、無 `halt`** → halt 模式不擠)。故提供
|
||||
後備:`jiya: { halfWidth: "margin" }`。**機制**(演進後):把 `jz-char` 之 inline-block
|
||||
@@ -442,8 +460,8 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
||||
後代選擇器)——`render()` 在全域 margin 時自動掛於 root;亦可手寫於任一祖先作逐區塊
|
||||
/逐語言控制(含 `margin` 頁中嵌 `halt` 島之 halt-reset,憑來源順序勝出)。bias
|
||||
class 恆於 charify 標記,halt 模式不讀,故模式切換為純 CSS、無需重 render。實測:
|
||||
方正書宋 GBK 下同句行寬 `24.0em(halt 無效)→ 19.7em(margin 生效)`、單標點
|
||||
`1.0em → 0.5em`。
|
||||
方正書宋 GBK(開明式 zh-Hans)下同句行寬 `24.0em(halt 無效)→ 19.7em(margin 生效)`、
|
||||
單一句內點號 `1.0em → 0.5em`。
|
||||
- **切片**:涵蓋並超越 v1 Pass D(開明式),加入全角式、lang 切換、行內連續與
|
||||
行端擠壓、`halt`/`margin` 雙渲染。
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ jz.render(document.querySelector("main"));
|
||||
|---|---|
|
||||
| **中西間隙** `spacing` | CJK↔西文邊界插入 copy-clean 之 `jz-hws`(`user-select:none`,剪貼板跳過)。「西文」涵蓋拉丁(含擴充 A/B,拼音聲調 `ā ǎ ē`)、希臘、西里爾、數字、貨幣(`€ ₩ ₿`)、數學運算符等;CJK 標點(`。,「」()……` 等)不誤判為西文、不生隙。含跨樣式行內邊界與 `code`/`kbd` pill 邊界(pill 緊鄰標點時穿越 `jz-jinze` 仍補間隙;pill 跨行右隙、間隙落行末之自我應驗皆已修)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 |
|
||||
| **長英文詞** `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`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 |
|
||||
| **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點**雙側獨立**收——各標點若其有空白之側緊鄰另一標點即收半,故 `,「` 全角式與開明式皆 1em、`。」` 全角式 1.5em/開明式 1em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 |
|
||||
| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。 |
|
||||
| **justify 單份間距** | 每個 `jz-char` 為 `inline-block` 原子項,句末標點只貢獻一份對齊伸縮量。 |
|
||||
|
||||
@@ -132,7 +132,14 @@ createJuzhen({
|
||||
|
||||
> 為何需要後備:`halt` 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測
|
||||
> **方正書宋 GBK 僅含 `vert`、無 `halt`** → halt 模式不擠;切 `margin` 後備後,
|
||||
> 同句行寬 `24.0em → 19.7em`、單一標點 `1.0em → 0.5em`,半形生效。
|
||||
> (開明式 zh-Hans)同句行寬 `24.0em → 19.7em`、單一句內點號 `1.0em → 0.5em`,半形生效。
|
||||
|
||||
> **句末標點含全角句點 `.`**:句末點號為 `。.!?`——除表意句號 `。`(U+3002)外納入
|
||||
> 全角句點 `.`(U+FF0E,實測 Noto CJK 有 `halt`)。政策行為與 `。!?` 同:全角式與開明式
|
||||
> 句末**保持全角 1em**;唯 banjiao(或與鄰標點連續被收)時 `halt` 收為半形 advance 0.5em。
|
||||
> **`halt` 即字型之「挤壓意願」**:字型不提供某標點之 `halt`(其字形占滿字框、不宜挤半,
|
||||
> 如某些字型之全角 `?`、`〈〉`)時,聚珍**信任字型、保持全角不強挤**(優雅降級);
|
||||
> `margin` 後備僅用於**整體無 `halt`** 之字型,逐字型以 `jiya.bias` 調教。
|
||||
|
||||
### 激活與粒度
|
||||
|
||||
|
||||
Vendored
+45
-24
@@ -515,7 +515,7 @@ var Juzhen = (() => {
|
||||
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
|
||||
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
|
||||
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||||
var BD_STOP = "\u3002\uFF01\uFF1F";
|
||||
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
|
||||
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||||
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
|
||||
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
|
||||
@@ -603,6 +603,7 @@ var Juzhen = (() => {
|
||||
"\uFF0C": "left",
|
||||
"\u3001": "left",
|
||||
"\u3002": "left",
|
||||
"\uFF0E": "left",
|
||||
"\uFF1B": "left",
|
||||
"\uFF1A": "left",
|
||||
"\uFF01": "left",
|
||||
@@ -612,6 +613,7 @@ var Juzhen = (() => {
|
||||
"\uFF0C": "center",
|
||||
"\u3001": "center",
|
||||
"\u3002": "center",
|
||||
"\uFF0E": "center",
|
||||
"\uFF1B": "center",
|
||||
"\uFF1A": "center",
|
||||
"\uFF01": "center",
|
||||
@@ -718,33 +720,52 @@ var Juzhen = (() => {
|
||||
order: 25,
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx) {
|
||||
const { finder } = ctx;
|
||||
const parents = /* @__PURE__ */ new Set();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||
if (c.parentNode) {
|
||||
parents.add(c.parentNode);
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
let prev = null;
|
||||
const squeeze = (L, R) => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
let child = parent.firstChild;
|
||||
while (child) {
|
||||
const next = child.nextSibling;
|
||||
if (child.nodeType === 1 && child.nodeName === "JZ-CHAR" && next && next.nodeType === 1 && next.nodeName === "JZ-CHAR") {
|
||||
const L = child;
|
||||
const R = next;
|
||||
if (finder.featureEnabledFor(L, PASS)) {
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
} else if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
}
|
||||
if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
};
|
||||
const visit = (node) => {
|
||||
for (let c = node.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3) {
|
||||
if (c.data.length > 0) {
|
||||
prev = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
const el = c;
|
||||
const nm = el.nodeName;
|
||||
if (nm === "JZ-CHAR") {
|
||||
if (prev) {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else {
|
||||
visit(el);
|
||||
}
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(ctx.root);
|
||||
},
|
||||
revert() {
|
||||
}
|
||||
|
||||
Vendored
+45
-24
@@ -487,7 +487,7 @@ var ANY_CJK = new RegExp("[" + CJK + "]");
|
||||
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
|
||||
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
|
||||
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||||
var BD_STOP = "\u3002\uFF01\uFF1F";
|
||||
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
|
||||
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||||
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
|
||||
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
|
||||
@@ -575,6 +575,7 @@ var BIAS_HANS = {
|
||||
"\uFF0C": "left",
|
||||
"\u3001": "left",
|
||||
"\u3002": "left",
|
||||
"\uFF0E": "left",
|
||||
"\uFF1B": "left",
|
||||
"\uFF1A": "left",
|
||||
"\uFF01": "left",
|
||||
@@ -584,6 +585,7 @@ var BIAS_HANT = {
|
||||
"\uFF0C": "center",
|
||||
"\u3001": "center",
|
||||
"\u3002": "center",
|
||||
"\uFF0E": "center",
|
||||
"\uFF1B": "center",
|
||||
"\uFF1A": "center",
|
||||
"\uFF01": "center",
|
||||
@@ -690,33 +692,52 @@ var jiyaAdjacencyPass = {
|
||||
order: 25,
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx) {
|
||||
const { finder } = ctx;
|
||||
const parents = /* @__PURE__ */ new Set();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||
if (c.parentNode) {
|
||||
parents.add(c.parentNode);
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
let prev = null;
|
||||
const squeeze = (L, R) => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
let child = parent.firstChild;
|
||||
while (child) {
|
||||
const next = child.nextSibling;
|
||||
if (child.nodeType === 1 && child.nodeName === "JZ-CHAR" && next && next.nodeType === 1 && next.nodeName === "JZ-CHAR") {
|
||||
const L = child;
|
||||
const R = next;
|
||||
if (finder.featureEnabledFor(L, PASS)) {
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
} else if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
}
|
||||
if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
};
|
||||
const visit = (node) => {
|
||||
for (let c = node.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3) {
|
||||
if (c.data.length > 0) {
|
||||
prev = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
const el = c;
|
||||
const nm = el.nodeName;
|
||||
if (nm === "JZ-CHAR") {
|
||||
if (prev) {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else {
|
||||
visit(el);
|
||||
}
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(ctx.root);
|
||||
},
|
||||
revert() {
|
||||
}
|
||||
|
||||
+6
-4
@@ -60,8 +60,10 @@ export const ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||||
// 注意:分類依「字形角色」,而開明式/全角式之別依「句法角色」。slice 1 之
|
||||
// 擠壓集(句內點號)由 BD_PAUSE 表達——此即 v1 Pass D 所擠之 ,、;:。
|
||||
|
||||
/** 句末點號:。!?(開明式與全角式皆保持全形,不擠壓)。 */
|
||||
export const BD_STOP = "。!?";
|
||||
/** 句末點號:。.!?(開明式與全角式皆保持全形,不擠壓;banjiao 擠半)。
|
||||
* 含全角句點 .(U+FF0E,異於表意句號 。U+3002)——實測 Noto CJK SC/TC 皆有 halt、
|
||||
* 挤壓收 0.5em,故與 。!? 同列為句末點號(下游回饋)。 */
|
||||
export const BD_STOP = "。.!?";
|
||||
|
||||
/** 句內點號:,、;:(開明式擠成半形;全角式保持全形)。 */
|
||||
export const BD_PAUSE = ",、;:";
|
||||
@@ -245,11 +247,11 @@ const BIAS_BRACKETS: Record<string, Bias> = (() =>
|
||||
// 在右 → 全判 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",
|
||||
";": "left", ":": "left", "!": "left", "?": "left",
|
||||
};
|
||||
const BIAS_HANT: Record<string, Bias> = {
|
||||
",": "center", "、": "center", "。": "center",
|
||||
",": "center", "、": "center", "。": "center", ".": "center",
|
||||
";": "center", ":": "center", "!": "center", "?": "center",
|
||||
};
|
||||
|
||||
|
||||
+44
-33
@@ -107,14 +107,17 @@ export const jiyaPass: CharifyPass = {
|
||||
|
||||
// ----- 行內連續擠壓(機制 A)-----
|
||||
//
|
||||
// 對 DOM 直接相鄰(nextSibling 即下一 jz-char、其間無文本)之標點對 (L, R):
|
||||
// 收掉「指向彼此」之那一側——L 若有右空白則收 L(半形),否則 R 若有左空白
|
||||
// 則收 R。每相鄰對只收一字 → 該對約 1.5em。鏈狀連續標點逐對處理。加 jz-half
|
||||
// 冪等(並集),與 baseline 疊加(已半形則 no-op)。class 掛於 charify 之
|
||||
// jz-char,隨其解包清除,本 pass 無獨立產物。
|
||||
//
|
||||
// 半形以字型 halt 渲染(居中字形如 ?!亦正確、不重疊);例:?!→ 只收 ?,
|
||||
// !保持全形 1em。
|
||||
// 對「邏輯相鄰」之標點對 (L, R)——其間只隔**透明行內邊界**(樣式行內 strong/em…),
|
||||
// 無文本、無非透明元素、不跨 block——挤掉兩者「指向彼此」之空白:**兩側獨立判斷**
|
||||
// (非二選一)——L 右側有空白則收 L(半形),R 左側有空白則收 R。故:
|
||||
// ・,「(pause 右空白 + open 左空白):兩側皆收 → 合占 1em。
|
||||
// ・。」(stop 右空白 + close 右空白,」左為墨):只收 。→ 全角式 1.5em/開明式
|
||||
// 1em(」baseline 已半)。
|
||||
// ・·—(中點/連接,皆無指向對方之空白):皆不收 → 2em(不宜挤之例外)。
|
||||
// 機制依 clreq「相鄰空白重疊即挤」。**透明穿越**:以從 root 之遍歷取代原「同 parent
|
||||
// 直接兄弟」掃描,使跨 strong/em 邊界之連續標點亦正確配對(下游回饋;block 邊界與
|
||||
// avoid 子樹斷相鄰)。半形以 halt 渲染、冪等並集,與 baseline 疊加。class 掛於
|
||||
// charify 之 jz-char,隨其解包清除,本 pass 無獨立產物。
|
||||
|
||||
export const jiyaAdjacencyPass: StandalonePass = {
|
||||
name: "jiyaAdjacency",
|
||||
@@ -123,37 +126,45 @@ export const jiyaAdjacencyPass: StandalonePass = {
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx: RenderContext): void
|
||||
{
|
||||
const { finder } = ctx;
|
||||
const parents = new Set<Node>();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) =>
|
||||
{
|
||||
if (c.parentNode) { parents.add(c.parentNode); }
|
||||
});
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
let prev: Element | null = null;
|
||||
|
||||
for (const parent of parents)
|
||||
const squeeze = (L: Element, R: Element): void =>
|
||||
{
|
||||
let child: Node | null = parent.firstChild;
|
||||
while (child)
|
||||
if (!finder.featureEnabledFor(L, PASS)) { return; }
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) { L.classList.add("jz-half"); }
|
||||
if (rSides.left) { R.classList.add("jz-half"); }
|
||||
};
|
||||
|
||||
const visit = (node: Node): void =>
|
||||
{
|
||||
for (let c: Node | null = node.firstChild; c; c = c.nextSibling)
|
||||
{
|
||||
const next = child.nextSibling;
|
||||
if (
|
||||
child.nodeType === 1 && (child as Element).nodeName === "JZ-CHAR"
|
||||
&& next && next.nodeType === 1 && (next as Element).nodeName === "JZ-CHAR"
|
||||
)
|
||||
if (c.nodeType === 3)
|
||||
{
|
||||
const L = child as Element;
|
||||
const R = next as Element;
|
||||
if (finder.featureEnabledFor(L, PASS))
|
||||
{
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) { L.classList.add("jz-half"); }
|
||||
else if (rSides.left) { R.classList.add("jz-half"); }
|
||||
}
|
||||
if ((c as Text).data.length > 0) { prev = null; } // 文本斷相鄰
|
||||
continue;
|
||||
}
|
||||
child = next;
|
||||
if (c.nodeType !== 1) { continue; } // 註解等:略過、不斷
|
||||
const el = c as Element;
|
||||
const nm = el.nodeName;
|
||||
if (nm === "JZ-CHAR")
|
||||
{
|
||||
if (prev) { squeeze(prev, el); }
|
||||
prev = el; // jz-char 為原子,不下探(內含 jz-inner)
|
||||
}
|
||||
else if (finder.isAvoided(el)) { prev = null; } // avoid/skip 子樹:斷、不下探
|
||||
else if (blockSet.has(nm))
|
||||
{
|
||||
prev = null; visit(el); prev = null; // block 邊界:前後皆斷相鄰,內部另計
|
||||
}
|
||||
else { visit(el); } // 行內(樣式行內 / pill 等):透明穿越、相鄰性延續
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(ctx.root);
|
||||
},
|
||||
revert(): void
|
||||
{
|
||||
|
||||
+61
-12
@@ -199,6 +199,49 @@ test("jiya 機制 A:連續開類,後者半形(「「→ 第二個 jz-half
|
||||
assert.ok(opens[1].classList.contains("jz-half"), "第二個開類內側 → 半形");
|
||||
});
|
||||
|
||||
test("jiya 機制 A:,「→ 兩側皆收半形(雙側獨立判斷,合占 1em)", () =>
|
||||
{
|
||||
// ,右側有空白且右鄰標點「→ 收,;「左側有空白且左鄰標點,→ 收「。兩者皆半 = 1em。
|
||||
const { document } = setupDom('<p lang="zh-Hant">甲,「乙</p>');
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body);
|
||||
const comma = document.querySelector("jz-char.bd-pause"); // ,
|
||||
const open = document.querySelector("jz-char.bd-open"); // 「
|
||||
assert.ok(comma.classList.contains("jz-half"), ",右空白鄰標點 → 半形");
|
||||
assert.ok(open.classList.contains("jz-half"), "「左空白鄰標點 → 半形(雙側收,非二選一)");
|
||||
});
|
||||
|
||||
test("jiya 機制 A:。」→ 僅 。半形(」左為墨、無空白可挤,全角式合占 1.5em)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hant">甲。」乙</p>');
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body);
|
||||
const stop = document.querySelector("jz-char.bd-stop"); // 。
|
||||
const close = document.querySelector("jz-char.bd-close"); // 」
|
||||
assert.ok(stop.classList.contains("jz-half"), "。右空白鄰標點 → 半形");
|
||||
assert.ok(!close.classList.contains("jz-half"), "」左為墨、無空白 → 不收(全角式 1.5em)");
|
||||
});
|
||||
|
||||
test("jiya 機制 A:跨透明 inline 邊界之連續標點亦正確擠(,「 分置 strong/em,雙側皆收)", () =>
|
||||
{
|
||||
// 下游回饋:原「同 parent 直接兄弟」掃描漏掉跨 strong/em 邊界之連續標點。
|
||||
const { document } = setupDom('<p lang="zh-Hant">甲<strong>,</strong><em>「</em>乙</p>');
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body);
|
||||
assert.ok(document.querySelector("jz-char.bd-pause").classList.contains("jz-half"), "跨邊界 ,半形");
|
||||
assert.ok(document.querySelector("jz-char.bd-open").classList.contains("jz-half"), "跨邊界 「半形(透明穿越)");
|
||||
});
|
||||
|
||||
test("jiya 機制 A:多標點鏈 ,」。逐對判斷(,半/」半/。全)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hant">甲,」。乙</p>');
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
const comma = p.querySelector("jz-char.bd-pause"); // ,右空白鄰」→ 半
|
||||
const close = p.querySelector("jz-char.bd-close"); // 」右空白鄰。→ 半
|
||||
const stop = p.querySelector("jz-char.bd-stop"); // 。左為墨、右鄰漢字 → 全
|
||||
assert.ok(comma.classList.contains("jz-half"), ",→ 半");
|
||||
assert.ok(close.classList.contains("jz-half"), "」右空白鄰標點。→ 半");
|
||||
assert.ok(!stop.classList.contains("jz-half"), "。左為墨、右側無標點鄰 → 全");
|
||||
});
|
||||
|
||||
test("jiya:revert 還原標點(無 jz-char,textContent 不變)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">好,走。</p>');
|
||||
@@ -511,21 +554,27 @@ test("CSS:聚珍不內建 font-family(字型交消費端),半形顯式
|
||||
|
||||
// ----- charClass:自定義字符分類規則集(向下兼容+擴充)-----
|
||||
|
||||
test("charClass:預設不傳時,英文全角句號 . 未分類(不 charify、不挤、不補隙)", () =>
|
||||
test("jiya:英文全角句號 . 已內建為句末標點 bd-stop(本次納入;Noto CJK 有 halt)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">中.文</p>');
|
||||
// . U+FF0E(異於表意句號 。U+3002)已納入內置 BD_STOP,預設即 charify 為 bd-stop。
|
||||
const { document } = setupDom('<p lang="zh-Hans">中.文。</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" } }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.equal(countTag(p, "jz-char"), 0, ". 預設未分類為標點,不 charify");
|
||||
assert.equal(countTag(p, "jz-hws"), 0, ". 預設非西文,無中西間隙");
|
||||
const dot = Array.from(p.querySelectorAll("jz-char")).find((c) => c.textContent === ".");
|
||||
assert.ok(dot && dot.classList.contains("bd-stop"), ". 預設 charify 為 bd-stop");
|
||||
// 開明式句末不挤(與 。一致);banjiao 才挤——此處僅確認分類與政策一致。
|
||||
assert.equal(dot.classList.contains("jz-half"), false, "開明式句末標點不挤半(保持全角,同 。)");
|
||||
});
|
||||
|
||||
test("charClass.biaodian.stop:把 . 歸為全寬句末標點 → 被 jiya charify 為 bd-stop", () =>
|
||||
test("charClass.biaodian.stop:把仍未分類之 ‥ 歸為全寬句末標點 → charify 為 bd-stop", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">中.文.</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { stop: "." } } }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.ok(countTag(p, "jz-char.bd-stop") >= 1, ". 應 charify 為 bd-stop");
|
||||
// ‥ U+2025 預設未分類;以 charClass 歸句末標點以示「追加」機制。
|
||||
const base = setupDom('<p lang="zh-Hans">中‥文</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" } }).render(base.document.body);
|
||||
assert.equal(countTag(base.document.querySelector("p"), "jz-char"), 0, "‥ 預設未分類、不 charify");
|
||||
const { document } = setupDom('<p lang="zh-Hans">中‥文</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { stop: "‥" } } }).render(document.body);
|
||||
assert.ok(countTag(document.querySelector("p"), "jz-char.bd-stop") >= 1, "‥ 經 charClass 歸 bd-stop");
|
||||
});
|
||||
|
||||
test("charClass.western:把未分類符號 ★ 歸為西文 → 與 CJK 邊界補 jz-hws", () =>
|
||||
@@ -586,10 +635,10 @@ test("charClass.western:追加不破壞內置——€(內置)與 ★(
|
||||
|
||||
test("charClass.biaodian:多子類同傳,各自生效(stop/pause)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">甲.乙•丙</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { stop: ".", pause: "•" } } }).render(document.body);
|
||||
const { document } = setupDom('<p lang="zh-Hans">甲‥乙•丙</p>');
|
||||
createJuzhen({ lang: { style: "kaiming" }, charClass: { biaodian: { stop: "‥", pause: "•" } } }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.ok(countTag(p, "jz-char.bd-stop") >= 1, ". → bd-stop");
|
||||
assert.ok(countTag(p, "jz-char.bd-stop") >= 1, "‥ → bd-stop");
|
||||
const pause = p.querySelector("jz-char.bd-pause");
|
||||
assert.ok(pause && pause.classList.contains("jz-half"), "• → bd-pause 且開明式挤半形");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user