fix: 標點擠壓句界例外——句末點號接開類保留右空白(。「 全角/開明皆 1.5em)
機制 A 行內連續擠壓原對 (L,R) 兩側獨立收:L 有右空白即收,不看 R 是誰,致句末 點號接開類(。「/?「)兩側皆收、合占 1em,丟失句界視覺。修為:L 為句末點號 (bd-stop)且 R 為開類(bd-open)時保留 L 右空白、僅收開類左側 → 全角式與開明式 皆 1.5em,對稱於 。」。例外僅限句末點號+開類;句內點號 ,「 與雙括號 」「/》《 仍兩側皆收 1em。 半角式不回歸:句末點號於 banjiao 由 baseline(機制 C)已收半形,守衛僅跳過 adjacency 之重複追加、不移除 baseline 之 jz-half;實測 32 可壓縮標點全半形、句界 組合句末點號仍半形(0 回歸)。唯非可壓縮 ·・‧/…— 各式皆全寬,屬設計。 唯一改動點 jiyaAdjacencyPass.squeeze() 加 stopBeforeOpen 守衛;lineedge/baseline /jinze/spacing 不波及。新增 3 契約測試(116 全綠);README/ARCHITECTURE §6.3 同步句界例外。
This commit is contained in:
+4
-1
@@ -467,7 +467,10 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j
|
||||
判斷——若其「有空白之側」緊鄰另一標點,則挤掉該側空白(收半形 `jz-half`)。故 `,「`
|
||||
(pause 右空白 + open 左空白)兩側皆收 → 全角式與開明式皆合占 1em;`。」`(stop 右空白;
|
||||
」左為墨)只收 `。`→ 全角式 1.5em/開明式 1em(」baseline 已半);`·—`(皆無指向對方之
|
||||
空白)皆不收 → 兩式皆 2em(不宜挤之例外)。多標點鏈逐對處理。`jiyaAdjacencyPass` 以**從 root 之透明遍歷**(穿越 strong/
|
||||
空白)皆不收 → 兩式皆 2em(不宜挤之例外)。**句界例外**:句末點號(`。.!?`)後接開類
|
||||
(`。「`/`?「`)為句界,**保留句末點號右側空白、不收 L**,僅收開類左側 → 全角式與開明式皆
|
||||
1.5em(對稱於 `。」`,半角式句末點號本已半形故仍 1em)。例外**僅限**句末點號+開類;句內點號
|
||||
`,「` 與雙括號 `」「`/`》《`(同句相連/貼合,非句界)仍兩側皆收 1em。多標點鏈逐對處理。`jiyaAdjacencyPass` 以**從 root 之透明遍歷**(穿越 strong/
|
||||
em… 等樣式行內;block/pill/avoid(自身判定)斷相鄰且不下探,isolate 斷相鄰但內部
|
||||
自成脈絡照常配對——§4.1 三分類;jz-char 為原子不下探)取代原「同 parent
|
||||
直接兄弟」掃描,使**跨 inline 邊界**之連續標點亦正確配對(原漏致跨 strong/em 之標點對未挤、
|
||||
|
||||
@@ -67,7 +67,7 @@ jz.render(document.querySelector("main"));
|
||||
|---|---|
|
||||
| **中西間隙** `spacing` | CJK↔西文邊界插入 copy-clean 之 `jz-hws`(`user-select:none`,剪貼板跳過)。「西文」涵蓋拉丁(含擴充 A/B,拼音聲調 `ā ǎ ē`)、希臘、西里爾、數字、貨幣(`€ ₩ ₿`)、數學運算符等;CJK 標點(`。,「」()……` 等)不誤判為西文、不生隙。含跨樣式行內邊界與 pill/隔離元素邊界(`code`/`kbd`/`.katex` 與 `mark`,兩端補隙口徑一致;pill 緊鄰標點時穿越 `jz-jinze` 仍補間隙;pill 跨行右隙、間隙落行末之自我應驗皆已修)。**pill 之邏輯鄰字為標點時(如 `漢字。<code>`)不誤加間隙**(標點非 pill 鄰字;`jz-char`/`jz-inner` 透明穿越,與 `collectRuns` 一致)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 |
|
||||
| **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 `<span lang="en">`,配合 CSS `hyphens:auto` 原生斷詞(DOM 無連字符 → 複製不污染)。 |
|
||||
| **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點**雙側獨立**收——各標點若其有空白之側緊鄰另一標點即收半,故 `,「` 全角式與開明式皆 1em、`。」` 全角式 1.5em/開明式 1em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 |
|
||||
| **標點擠壓** `jiya` | 標點 charify 為 `jz-char`+`bd-*` class。三機制:①baseline 政策(全角式不擠/開明式擠句內點號+括號引號/半角式全擠);②行內連續擠壓(相鄰標點**雙側獨立**收——各標點若其有空白之側緊鄰另一標點即收半,故 `,「` 全角式與開明式皆 1em、`。」` 全角式 1.5em/開明式 1em;**唯句末點號接開類為句界**——`。「`/`?「` 保留句末點號右空白、僅收開類,全角式與開明式皆 1.5em;透明穿越 `strong`/`em` 等行內邊界);③行端擠壓(`lineedge` layout pass)。半形預設以字型 `halt` 渲染(按字形正確定位,居中字形如 `!?`/繁體 `。,` 不重疊;不依賴 `text-spacing-trim`)。**字型缺 `halt` 時可切 `margin` 後備模式**(見下節)。 |
|
||||
| **禁則** `jinze` | 行首行末避頭尾:閉類/點號綁前一字(不置行首)、開類綁後一字(不置行末),只隔離邊界字故長引文中段仍可斷。亦為 `justify` 綁定載體。邊界字落於**透明**行內樣式元素(`strong`/`em` 等)內時**克隆切分**只綁邊界詞元(避免整個元素入 `nowrap` 致行末整體掉行、上行留巨大空隙),切出之克隆標 `data-jz-kind="splitoff"`、`revert` 時自動併回原元素;不可切元素(`a`/`abbr`/`ruby`/帶 `id`,以及 **pill/隔離/avoid**——見「行內元素三分類」)退化為**整元素綁定**——pill(如行內公式 `.katex`)即作為原子鄰字單位整體綁入 `jz-jinze`,絕不下潛肢解其內部。 |
|
||||
| **垂懸字避免** `orphan` | 段末行 CJK 實義字 ≥ N(預設 2;標點/符號不計)。方案 C(斷行層、無測量):把段末「第 N 個實義字起至段末」包入 `jz-orphan`(`white-space:nowrap`),整體不可斷 → 末行恒含之 ⇒ 末行實義字 ≥ N,任意寬度/字型成立。單行段無害。邊界字落於行內元素內時同樣**克隆切分**(只綁末 N 字、避免整個長元素入 `nowrap` 溢出容器;機制同禁則)。**預設開**(`orphan:false` 關,`{chars:n}` 調)。段落級功能。 |
|
||||
| ~~**標點懸掛** `hanging`~~ | **已移除**(§6.5.6):純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器),原生 `hanging-punctuation` 為唯一正解但僅 Safari 支援、且與本庫 inline-block 原子互斥。`hanging` 選項仍被接受(API 相容)但**無任何效果**;相關程式碼/CSS 已全數刪除,原生路徑屬遠期「原生管線」新功能。 |
|
||||
|
||||
Vendored
+6
-3
@@ -858,9 +858,12 @@ var Juzhen = (() => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
return;
|
||||
}
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
const lClass = bdClassOf(L);
|
||||
const rClass = bdClassOf(R);
|
||||
const lSides = blankSides(lClass);
|
||||
const rSides = blankSides(rClass);
|
||||
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
|
||||
if (lSides.right && !stopBeforeOpen) {
|
||||
L.classList.add("jz-half");
|
||||
}
|
||||
if (rSides.left) {
|
||||
|
||||
Vendored
+6
-3
@@ -831,9 +831,12 @@ var jiyaAdjacencyPass = {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
return;
|
||||
}
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
const lClass = bdClassOf(L);
|
||||
const rClass = bdClassOf(R);
|
||||
const lSides = blankSides(lClass);
|
||||
const rSides = blankSides(rClass);
|
||||
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
|
||||
if (lSides.right && !stopBeforeOpen) {
|
||||
L.classList.add("jz-half");
|
||||
}
|
||||
if (rSides.left) {
|
||||
|
||||
+11
-3
@@ -114,6 +114,9 @@ export const jiyaPass: CharifyPass = {
|
||||
// ・,「(pause 右空白 + open 左空白):兩側皆收 → 合占 1em。
|
||||
// ・。」(stop 右空白 + close 右空白,」左為墨):只收 。→ 全角式 1.5em/開明式
|
||||
// 1em(」baseline 已半)。
|
||||
// ・。「(句末點號 + 開類):此為**句界**——保留句末點號右側空白(不收 L),只收
|
||||
// 開類左側 → 全角/開明皆 1.5em(對稱於 。」)。**唯一例外**:限句末點號(。.!?)
|
||||
// 後接開類;句內點號 ,「 與雙括號 」「/》《 仍兩側皆收 1em(屬同句/貼合,非句界)。
|
||||
// ・·—(中點/連接,皆無指向對方之空白):皆不收 → 2em(不宜挤之例外)。
|
||||
// 機制依 clreq「相鄰空白重疊即挤」。**透明穿越**:以從 root 之遍歷取代原「同 parent
|
||||
// 直接兄弟」掃描,使跨 strong/em 邊界之連續標點亦正確配對(下游回饋;block 邊界與
|
||||
@@ -135,9 +138,14 @@ export const jiyaAdjacencyPass: StandalonePass = {
|
||||
const squeeze = (L: Element, R: Element): void =>
|
||||
{
|
||||
if (!finder.featureEnabledFor(L, PASS)) { return; }
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) { L.classList.add("jz-half"); }
|
||||
const lClass = bdClassOf(L);
|
||||
const rClass = bdClassOf(R);
|
||||
const lSides = blankSides(lClass);
|
||||
const rSides = blankSides(rClass);
|
||||
// 句界例外:句末點號後接開類(。「)保留句末點號右側空白,不收 L(→ 1.5em,
|
||||
// 對稱於 。」)。僅此一例;句內點號 ,「 與雙括號 」「 仍照常兩側收。
|
||||
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
|
||||
if (lSides.right && !stopBeforeOpen) { L.classList.add("jz-half"); }
|
||||
if (rSides.left) { R.classList.add("jz-half"); }
|
||||
};
|
||||
|
||||
|
||||
@@ -241,6 +241,48 @@ test("jiya 機制 A:。」→ 僅 。半形(」左為墨、無空白可挤
|
||||
assert.ok(!close.classList.contains("jz-half"), "」左為墨、無空白 → 不收(全角式 1.5em)");
|
||||
});
|
||||
|
||||
test("jiya 機制 A 句界:。「→ 句末點號保留右空白(。全形、「半形;全角/開明皆 1.5em)", () =>
|
||||
{
|
||||
for (const lang of [ "zh-Hant", "zh-Hans" ]) // 全角式/開明式皆 1.5em
|
||||
{
|
||||
const { document } = setupDom(`<p lang="${lang}">甲。「乙</p>`);
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body);
|
||||
const stop = document.querySelector("jz-char.bd-stop"); // 。
|
||||
const open = document.querySelector("jz-char.bd-open"); // 「
|
||||
assert.ok(!stop.classList.contains("jz-half"), `${lang}:。句末點號接開類為句界 → 保留右空白(全形)`);
|
||||
assert.ok(open.classList.contains("jz-half"), `${lang}:「開類左空白仍收(半形)→ 合占 1.5em`);
|
||||
}
|
||||
});
|
||||
|
||||
test("jiya 機制 A 句界:例外僅限句末點號+開類——,「 與 」「 仍兩側皆收 1em", () =>
|
||||
{
|
||||
// 句內點號(pause)與閉類後接開類不屬句界,照舊兩側收。
|
||||
const c = setupDom('<p lang="zh-Hant">甲,「乙</p>');
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(c.document.body);
|
||||
assert.ok(c.document.querySelector("jz-char.bd-pause").classList.contains("jz-half"), ",仍收(句內點號非句界)");
|
||||
assert.ok(c.document.querySelector("jz-char.bd-open").classList.contains("jz-half"), ",「之「仍收");
|
||||
|
||||
const b = setupDom('<p lang="zh-Hant">甲」「乙</p>');
|
||||
createJuzhen({ spacing: false, longWord: false, jinze: false }).render(b.document.body);
|
||||
assert.ok(b.document.querySelector("jz-char.bd-close").classList.contains("jz-half"), "」仍收(雙括號非句界)");
|
||||
assert.ok(b.document.querySelector("jz-char.bd-open").classList.contains("jz-half"), "」「之「仍收");
|
||||
});
|
||||
|
||||
test("jiya 機制 A 句界:半角式不回歸——。「 句末點號仍半形(守衛僅跳過 adjacency 追加,baseline 已半)", () =>
|
||||
{
|
||||
// 半角式所有可壓縮標點皆半形(baseline 機制 C)。句界守衛只跳過 adjacency 之重複
|
||||
// 追加,絕不移除 baseline 之 jz-half,故半角式句末點號不得退回全寬。
|
||||
const { document } = setupDom("<p>甲。「乙</p>");
|
||||
createJuzhen({
|
||||
lang: { default: "zh-Hans", style: "banjiao" },
|
||||
spacing: false, longWord: false, jinze: false,
|
||||
}).render(document.body);
|
||||
const stop = document.querySelector("jz-char.bd-stop"); // 。
|
||||
const open = document.querySelector("jz-char.bd-open"); // 「
|
||||
assert.ok(stop.classList.contains("jz-half"), "半角式 。仍半形(baseline 已收,守衛不回退)");
|
||||
assert.ok(open.classList.contains("jz-half"), "半角式 「仍半形 → 合占 1em");
|
||||
});
|
||||
|
||||
test("jiya 機制 A:跨透明 inline 邊界之連續標點亦正確擠(,「 分置 strong/em,雙側皆收)", () =>
|
||||
{
|
||||
// 下游回饋:原「同 parent 直接兄弟」掃描漏掉跨 strong/em 邊界之連續標點。
|
||||
|
||||
Reference in New Issue
Block a user