diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 79a86d8..a732f37 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 之標點對未挤、 diff --git a/README.md b/README.md index 5c967f6..1b541ff 100644 --- a/README.md +++ b/README.md @@ -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 之邏輯鄰字為標點時(如 `漢字。`)不誤加間隙**(標點非 pill 鄰字;`jz-char`/`jz-inner` 透明穿越,與 `collectRuns` 一致)。贅餘作者空格一律正規化:CJK↔西文吸收為受管間隙、CJK↔CJK(含跨 inline 標籤)剝除不留隙。 | | **長英文詞** `longWord` | `[A-Za-z]{6,}` 包入 ``,配合 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 已全數刪除,原生路徑屬遠期「原生管線」新功能。 | diff --git a/dist/juzhen.iife.js b/dist/juzhen.iife.js index 78192f5..b1ab048 100644 --- a/dist/juzhen.iife.js +++ b/dist/juzhen.iife.js @@ -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) { diff --git a/dist/juzhen.mjs b/dist/juzhen.mjs index 2a6f828..89ad1b8 100644 --- a/dist/juzhen.mjs +++ b/dist/juzhen.mjs @@ -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) { diff --git a/src/typeset/jiya.ts b/src/typeset/jiya.ts index c0d7273..fee3fb5 100644 --- a/src/typeset/jiya.ts +++ b/src/typeset/jiya.ts @@ -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"); } }; diff --git a/test/juzhen.test.mjs b/test/juzhen.test.mjs index 2208de4..09def1b 100644 --- a/test/juzhen.test.mjs +++ b/test/juzhen.test.mjs @@ -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(`

甲。「乙

`); + 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('

甲,「乙

'); + 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('

甲」「乙

'); + 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("

甲。「乙

"); + 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 邊界之連續標點。