From 96f5ac32e144bd8d5c07ecf5c784bc4daff94c67 Mon Sep 17 00:00:00 2001 From: commilitia Date: Tue, 2 Jun 2026 17:18:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8E=9F=E5=AD=90=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=AE=B5=E8=90=BD=E7=B9=BC=E6=89=BF=E5=B1=AC?= =?UTF-8?q?=E6=80=A7=E6=BC=8F=E6=BA=A2=E4=BF=AE=E5=BE=A9=EF=BC=88text-inde?= =?UTF-8?q?nt=EF=BC=8Fhanging-punctuation=EF=BC=8Ftext-decoration=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下游回報:段落設 text-indent 時,標點附近冒出巨大間距。根因為聚珍把標點拆成 inline-block 原子,繼承屬性被每個原子重複套用(過套用),且裝飾盒跳過原子(欠套用)。 以「同類問題整體界定」原則統一在庫內修復,使原子對消費端段落樣式透明。 範圍(headless Chrome 逐屬性實測界定,非逐案補丁): 過套用(繼承屬性被 inline-block 原子各自重套),於原子歸零: - text-indent: 0——段落首行縮排傳入後每原子內部各縮排一次,shrink-to-fit 寬膨脹為 縮排量+字形。實測:歸零後段落首行縮排保持 2em、原子寬由 ~3em 回到 0.5em/1em。 - hanging-punctuation: none——同屬區塊容器首/末行機制;Chrome 未實作、Safari 觸發, 且與規劃中之標點懸掛相衝,按機制一併歸零。 欠套用(text-decoration 非繼承、由段落裝飾盒繪製並跳過原子致下劃線斷缺),令原子 inherit 自繪接續: - jz-char/jz-jinze 設 text-decoration: inherit;margin 模式 jz-inner 亦為 inline-block,於 _jiya.css 補一條。像素掃描實測:halt/margin 兩模式下劃線 coverage 由 ~0.25 回到 1.0,且 margin 下劃線隨半形收為 0.5em advance。 不處理者(實測不洩漏或本應逐字穿透):letter-spacing/word-spacing/text-align 系/ text-transform/line-height/color/font/word-break/overflow-wrap/hyphens/ white-space/tab-size/text-emphasis/text-shadow/font-variant/font-stretch。 - src/css/_normalize.css:過套用歸零 + text-decoration 透明(含原則與實測依據注釋)。 - src/css/_jiya.css:margin 模式 jz-inner 裝飾接續。 - test/juzhen.test.mjs:三項回歸斷言(51 測試通過)。 - README.md CSS 基礎設定第 8 項、ARCHITECTURE.md「原子透明原則」一節。 - dist/ 重建。 --- ARCHITECTURE.md | 32 ++++++++++++++++++++++++++++++++ README.md | 5 +++++ dist/juzhen.css | 9 +++++++++ src/css/_jiya.css | 8 ++++++++ src/css/_normalize.css | 32 ++++++++++++++++++++++++++++++++ test/juzhen.test.mjs | 27 +++++++++++++++++++++++++++ 6 files changed, 113 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b4469ff..428ecc6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -497,6 +497,38 @@ class 約定:`bd`(標點)、`bd-open/close/cop/stop/sep/liga`、`cjk`、`j 不動 JS。注意:`margin` 後備半形依賴 inline-block 之 `width:0.5em`,與 `justifyAtoms:false` 不可兼得——分頁器場景須用含 `halt` 字型。 +**原子透明原則(繼承屬性漏溢,下游回饋)**:`jz-char`/`jz-jinze`(及 `margin` 模式之 +`jz-inner`)為 inline-block,而 inline-block 自成區塊容器。凡「作用於區塊容器首/末行」之 +**繼承屬性**,會被每個單字標點原子各自重新套用一次,使「用聚珍」與「未用聚珍」表現不一致。 +**作用範圍判斷**:此類間距/位移源於我們生成之原子,屬本庫責任,由庫內統一免疫(消費端無需 +配置);且須以「同類機制」整體界定範圍——要麼全修、要麼全交下游,不做逐案補丁。修法 +(`_normalize.css`)於原子上歸零此類屬性,只切斷其向原子內部之再套用,段落本身之同名屬性不 +受影響(仍作用於段落自身行盒)。 + +經 headless Chrome 逐屬性實測,屬此類者僅兩項,皆已歸零: + +- `text-indent: 0`——段落首行縮排傳入後,每原子內部首行各縮排一次,shrink-to-fit 寬膨脹為 + `縮排量+字形`,標點附近冒出巨大間距。實測:歸零後段落首行縮排仍保持 2em,原子寬由 ~3em + 回到 0.5em/1em。 +- `hanging-punctuation: none`——同屬區塊容器首/末行機制,每原子各自懸掛自身標點。Chrome + 未實作(測不出)、Safari 會觸發,且與規劃中之「標點懸掛」(§任務 6)相衝,故按機制一併歸零。 + +**不歸零者**(實測不洩漏或本應逐字穿透):`letter-spacing`/`word-spacing`(原子邊界處理 +一致、無翻倍,實測 delta 與基線同);`text-align`/`text-align-last`/`text-justify` +(halt 為 shrink-to-fit、margin 為內容溢出,皆無對齊餘量,兩模式實測 `jz-inner` 偏移不變); +`text-transform`/`line-height`/`color`/`font`(逐字內容或縱向屬性,須穿透原子); +`word-break`/`overflow-wrap`/`hyphens`/`white-space`/`tab-size`/`text-emphasis`/ +`text-shadow`/`font-variant`/`font-stretch`(逐屬性實測 delta 皆與基線同,不洩漏)。 + +**透明之另一面——欠套用(`text-decoration`)**:與上述「過套用」相反,`text-decoration` +**非繼承**屬性,由段落之「裝飾盒」統一繪製並跨後代延伸;但原子級 inline-block 會被裝飾盒 +**跳過**,致段落下劃線在每個標點原子下斷缺(像素掃描實測:純文字 coverage 1.0、聚珍僅 +~0.25)。故反向處理——令原子 `text-decoration: inherit` 承段落裝飾自繪,因共用基線與字型 +度量而無縫接續(`_normalize.css` 設 `jz-char`/`jz-jinze`;`margin` 模式 `jz-inner` 亦為 +inline-block,於 `_jiya.css` 補一條)。實測:halt/margin 兩模式 coverage 皆回 1.0,且 +`margin` 下劃線隨半形收為 0.5em advance。**此為「同類問題」整體界定之一部分**:凡因原子化 +而生之版面瑕疵,要麼庫內統一修復、要麼統一交下游;本庫選前者,使原子對消費端樣式透明。 + --- ## 9. 跨瀏覽器策略 diff --git a/README.md b/README.md index 0079f07..0e4723d 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,11 @@ JS 只注入 `jz-*` 結構,**所有視覺尺寸在 `juzhen.css`**——故 CSS 7. **自動處理、無需消費端介入**:`juzhen.css` 已在 `.juzhen` 範圍內以 `@supports` 關閉 Blink 原生 `text-spacing-trim`/`text-autospace`(避免與託管擠壓重複作用), 並令注入間隙 `user-select: none`(複製乾淨)。 +8. **段落屬性照常設、原子透明**:聚珍把標點拆成 inline-block 原子,但已確保這些原子對 + 消費端段落樣式透明——可在段落正常設 `text-indent`(首行縮排)、`hanging-punctuation`、 + `text-decoration`(下劃線)等;庫內已歸零會被原子重複套用之繼承屬性、並令原子承接段落 + 裝飾,故首行縮排正常、下劃線連續不斷,無需任何額外配置(機制見 ARCHITECTURE「原子透明 + 原則」)。 --- diff --git a/dist/juzhen.css b/dist/juzhen.css index 73c7f66..62edb89 100644 --- a/dist/juzhen.css +++ b/dist/juzhen.css @@ -12,6 +12,12 @@ jz-jinze, jz-em { font: inherit; color: inherit; + text-indent: 0; + hanging-punctuation: none; +} +jz-char, +jz-jinze { + text-decoration: inherit; } /* src/css/_lang.css */ @@ -99,6 +105,9 @@ jz-char.jz-half-le jz-inner { [data-jz-halfwidth=margin] jz-char.jz-ink-right jz-inner { margin-left: -0.5em; } +[data-jz-halfwidth=margin] jz-char jz-inner { + text-decoration: inherit; +} [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; diff --git a/src/css/_jiya.css b/src/css/_jiya.css index 135d96c..89e4c76 100644 --- a/src/css/_jiya.css +++ b/src/css/_jiya.css @@ -89,6 +89,14 @@ jz-char.jz-half-le jz-inner margin-left: -0.5em; } +/* margin 模式 jz-inner 為 inline-block,會被段落裝飾盒跳過致下劃線於盒內斷缺 + (見 _normalize.css「原子透明之另一面」)。令其承段落裝飾自繪、接續下劃線。 + halt 模式 jz-inner 為 inline、裝飾自然穿透,故僅此模式需要。 */ +[data-jz-halfwidth="margin"] jz-char jz-inner +{ + text-decoration: inherit; +} + /* 粒度控制:模式由 data-jz-halfwidth **屬性所在子樹** 決定(CSS 後代選擇器), 故可掛於任意祖先——root(JS 全域預設)、某 lang 容器、單一段落皆可,無需重設 實例。下列 halt-reset 規則置於 margin 規則之後:當 margin 頁面中**巢狀**一塊 diff --git a/src/css/_normalize.css b/src/css/_normalize.css index 6c36e96..be4fc64 100644 --- a/src/css/_normalize.css +++ b/src/css/_normalize.css @@ -27,4 +27,36 @@ jz-em { font: inherit; color: inherit; + /* 原子透明原則:jz-char/jz-jinze(及 margin 模式之 jz-inner)為 inline-block, + 而 inline-block 自成區塊容器。凡「作用於區塊容器首/末行」之繼承屬性,會被每個 + 單字標點原子各自重新套用一次,使「用聚珍」與「未用聚珍」表現不一致。故須於原子 + 上歸零此類屬性——只切斷其向原子內部之再套用;段落本身之同名屬性不受影響(仍作用 + 於段落自身之行盒)。範圍以「同類機制」整體界定,非逐案補丁。 + · text-indent:段落首行縮排傳入後,每個原子內部首行各縮排一次,shrink-to-fit + 寬膨脹為 縮排量+字形,標點附近冒出巨大間距(下游回報)。歸零後實測:段落首行 + 縮排保持,原子寬回到 0.5em/1em。 + · hanging-punctuation:同屬區塊容器首/末行機制,每個原子各自懸掛自身標點。 + Chrome 未實作(測不出),Safari 會觸發;且與本庫規劃之「標點懸掛」相衝,故一併 + 歸零(不支援處為 no-op,支援處既擋原子重套、亦不礙段落級懸掛)。 + 不歸零者(經 headless Chrome 實測不洩漏,或本應逐字放行):letter-spacing/ + word-spacing(原子邊界處理一致、無翻倍);text-align/text-align-last/ + text-justify(halt 為 shrink-to-fit、margin 為內容溢出,二者皆無對齊餘量, + 兩模式實測 jz-inner 偏移不變);text-transform/line-height/color/font(逐字 + 內容或縱向屬性,須穿透原子)。 */ + text-indent: 0; + hanging-punctuation: none; +} + +/* 原子透明之另一面——欠套用(與上一塊之過套用相反):text-decoration **非**繼承屬性, + 而是由祖先(段落)之「裝飾盒」統一繪製、跨後代延伸;但原子級 inline-block(jz-char/ + jz-jinze,及 margin 模式之 jz-inner)會被裝飾盒**跳過**,致段落下劃線在每個標點原子下 + 斷缺(實測:純文字 coverage 1.0、聚珍僅 ~0.25)。令原子 text-decoration: inherit, + 使其承段落之裝飾自繪——因共用基線與字型度量,與漢字下劃線無縫接續(headless Chrome + 像素掃描實測:halt/margin 兩模式 coverage 皆回 1.0,且 margin 下劃線隨半形收為 + 0.5em advance,正確)。margin 模式 jz-inner 亦為 inline-block、同遭跳過,故於該模式 + 補一條(halt 模式 jz-inner 為 inline、裝飾自然穿透,無需)。 */ +jz-char, +jz-jinze +{ + text-decoration: inherit; } diff --git a/test/juzhen.test.mjs b/test/juzhen.test.mjs index 326b062..2cb0f6c 100644 --- a/test/juzhen.test.mjs +++ b/test/juzhen.test.mjs @@ -2,6 +2,8 @@ // bd-* class、邊界、冪等、revert 還原、copy-clean、lang 政策分支、作用域。 import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { setupDom, countTag } from "./helpers.mjs"; @@ -415,3 +417,28 @@ test("compat:預設強制 kaiming,句內點號半形(jz-half)(不因 const comma = document.querySelector("jz-char.bd-pause"); assert.ok(comma && comma.classList.contains("jz-half"), "compat 不退化:仍擠句內"); }); + +// ----- text-indent 漏溢防護(下游回報)----- + +test("CSS:jz-* 原子歸零「區塊容器首/末行級」繼承屬性,免 inline-block 重套(透明原則)", () => +{ + // jsdom 無版面,無法量寬;改驗構建 CSS 確含原子歸零規則。 + // 機制見 src/css/_normalize.css:此類繼承屬性會被 inline-block 原子(jz-char/ + // jz-jinze)重新套用於其內部首/末行,故須於原子上歸零(段落本身不受影響)。 + // 整類僅兩項(headless Chrome 逐屬性實測:letter-spacing/word-spacing/text-align + // 系皆不洩漏)。 + const cssPath = fileURLToPath(new URL("../dist/juzhen.css", import.meta.url)); + const css = readFileSync(cssPath, "utf8"); + assert.match(css, /text-indent\s*:\s*0/, "構建 CSS 應含 text-indent:0 重設"); + assert.match(css, /hanging-punctuation\s*:\s*none/, "構建 CSS 應含 hanging-punctuation:none 重設"); +}); + +test("CSS:jz-* 原子 text-decoration:inherit,補裝飾盒跳過原子致下劃線斷缺(透明原則·欠套用面)", () => +{ + // 另一機制:text-decoration 非繼承、由段落裝飾盒繪製並跳過原子級 inline-block, + // 致下劃線於每個標點原子下斷缺。令原子 inherit 自繪、接續(headless Chrome 像素掃描 + // 實測 halt/margin coverage 皆回 1.0)。 + const cssPath = fileURLToPath(new URL("../dist/juzhen.css", import.meta.url)); + const css = readFileSync(cssPath, "utf8"); + assert.match(css, /text-decoration\s*:\s*inherit/, "構建 CSS 應含 text-decoration:inherit"); +});