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:
+11
-6
@@ -63,6 +63,14 @@ export class Finder
|
||||
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 /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
isAvoided(node: Node): boolean
|
||||
@@ -223,10 +231,7 @@ export class Finder
|
||||
{
|
||||
return pickEnd(n as Text);
|
||||
}
|
||||
if (
|
||||
n.nodeType === 1
|
||||
&& (this.styleSet.has((n as Element).nodeName) || (n as Element).nodeName === "JZ-HWS")
|
||||
)
|
||||
if (n.nodeType === 1 && this.isTransparentInline((n as Element).nodeName))
|
||||
{
|
||||
const r = descend(n as Element);
|
||||
if (r) { return r; }
|
||||
@@ -252,7 +257,7 @@ export class Finder
|
||||
{
|
||||
const e = n as Element;
|
||||
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);
|
||||
if (r) { return r; }
|
||||
@@ -263,7 +268,7 @@ export class Finder
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// 這些常數以「regex 字元類片段」形式給出(已就 RegExp 字元類做必要轉義),
|
||||
// 故可直接 `new RegExp("[" + X + "]")`。
|
||||
|
||||
import type { LangClass } from "./locale.js";
|
||||
|
||||
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
||||
export const 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;
|
||||
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)。
|
||||
擠壓量以負 margin 託管,跨瀏覽器一致(I4),不依賴 halt/text-spacing-trim。 */
|
||||
半形渲染:預設 halt(字型 OpenType);缺 halt 字型可切 margin 後備模式
|
||||
(width:0.5em+字身偏側位移,以 CSS 盒模型重現 halt,見下)。跨瀏覽器一致(I4)。 */
|
||||
|
||||
/* §8:每個 jz-char 為 justify 之單一原子項——句末標點只貢獻一份對齊伸縮量
|
||||
(I5)。inline-block 使標點與其字面留白同屬一項,對齊伸縮只落於項與項之間。
|
||||
@@ -17,6 +18,14 @@ jz-inner
|
||||
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`(橫排半形)收字身寬度——由字型按字形正確
|
||||
定位(簡體點號偏左、繁體居中、括號偏側皆正確),不拉鄰居故不重疊(修正
|
||||
負 margin 對居中字形如 !?/繁體 。, 之缺陷)。需字型含 halt(Noto CJK
|
||||
@@ -30,3 +39,72 @@ jz-char.jz-half-le jz-inner
|
||||
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() 零特異度,易被消費端覆寫;限於
|
||||
.juzhen 容器,非破壞、opt-in。簡繁日各取對應 Noto CJK,使 locl 字形正確。
|
||||
名稱同時涵蓋 webfont(Google Fonts「Noto Serif SC」等)與本機安裝
|
||||
(「Noto Serif CJK SC」「Source Han Serif SC」)——擠壓依賴字型之 OpenType
|
||||
`halt`,Noto/思源具備;務必確保實際命中含 halt 之字型(Safari 對缺 halt
|
||||
之 fallback 字型不會擠壓,故 demo 以 webfont 明確載入 Noto 作參考)。 */
|
||||
/* 依 lang 之字體堆疊(§6.9)。**僅作預設後備**,消費端自訂字型恆優先。
|
||||
兩項關鍵:
|
||||
1. 整個選擇器包進 :where() → 零特異度,消費端任何 `.juzhen { font-family }`
|
||||
(特異度 ≥ 0,1,0)即可覆寫。
|
||||
2. 只命中**帶 `lang` 屬性之元素**(容器或內層區塊),**不**直接命中 jz-inner/
|
||||
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;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
+29
-1
@@ -14,7 +14,7 @@ import { jinzePass } from "./typeset/jinze.js";
|
||||
import { gapTrimPass, lineEdgePass } from "./typeset/lineedge.js";
|
||||
import { longWordPass } from "./typeset/longword.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 { createCjkAutospace } from "./compat/v1.js";
|
||||
@@ -56,6 +56,20 @@ function resolveLongWord(v: boolean | LongWordOptions | undefined): { minLength:
|
||||
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。 */
|
||||
export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
||||
{
|
||||
@@ -68,6 +82,7 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
||||
policy: langOpt.policy || {},
|
||||
},
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
scope: {
|
||||
root: scope.root || null,
|
||||
include: scope.include || null,
|
||||
@@ -84,6 +99,7 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
||||
ruby: feature(opts.ruby, false),
|
||||
},
|
||||
longWord: resolveLongWord(opts.longWord),
|
||||
jiyaConfig: resolveJiya(opts.jiya),
|
||||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||||
finder: {
|
||||
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
||||
@@ -134,12 +150,24 @@ export function createJuzhen(opts: JuzhenOptions = {}): Juzhen
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) { return; }
|
||||
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
|
||||
{
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) { return; }
|
||||
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 { 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
|
||||
@@ -12,6 +13,18 @@ export interface LongWordOptions
|
||||
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
|
||||
{
|
||||
@@ -36,6 +49,12 @@ export interface JuzhenOptions
|
||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||
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)。 */
|
||||
scope?: {
|
||||
root?: Element | string;
|
||||
@@ -46,7 +65,7 @@ export interface JuzhenOptions
|
||||
// 全域逐功能開關。
|
||||
spacing?: boolean;
|
||||
longWord?: boolean | LongWordOptions;
|
||||
jiya?: boolean;
|
||||
jiya?: boolean | JiyaOptions;
|
||||
jinze?: boolean;
|
||||
hanging?: boolean;
|
||||
biaodian?: boolean;
|
||||
@@ -70,6 +89,7 @@ export interface ResolvedOptions
|
||||
{
|
||||
locale: LocaleConfig;
|
||||
autospaceClass: string;
|
||||
justifyAtoms: boolean;
|
||||
scope: {
|
||||
root: Element | string | null;
|
||||
include: string | null;
|
||||
@@ -86,6 +106,8 @@ export interface ResolvedOptions
|
||||
ruby: boolean;
|
||||
};
|
||||
longWord: { minLength: number; lang: string };
|
||||
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
||||
jiyaConfig: { halfWidth: "halt" | "margin"; bias: BiasTable };
|
||||
blocks: BlockRule[];
|
||||
finder: {
|
||||
styleInlines: Set<string>;
|
||||
|
||||
+16
-4
@@ -14,9 +14,9 @@
|
||||
// 同時為 §8:所有 jz-char 經 CSS 設 inline-block,成 justify 單一原子項(I5)。
|
||||
|
||||
import { bdClassOf, createJz, revertPass } from "../core/dom.js";
|
||||
import { resolveStyle } from "../core/locale.js";
|
||||
import type { PunctStyle } from "../core/locale.js";
|
||||
import { blankSides, classifyBiaodian } from "../core/unicode.js";
|
||||
import { classify, effectiveLang, resolveStyle } from "../core/locale.js";
|
||||
import type { LangClass, PunctStyle } from "../core/locale.js";
|
||||
import { blankSides, classifyBiaodian, inkBias } from "../core/unicode.js";
|
||||
import type { CharifyPass, RenderContext, StandalonePass, WrapRequest } from "../types.js";
|
||||
|
||||
const PASS = "jiya";
|
||||
@@ -64,13 +64,25 @@ export const jiyaPass: CharifyPass = {
|
||||
const data = node.data;
|
||||
const out: WrapRequest[] = [];
|
||||
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)
|
||||
{
|
||||
const bdClass = classifyBiaodian(data.charAt(i));
|
||||
const ch = data.charAt(i);
|
||||
const bdClass = classifyBiaodian(ch);
|
||||
if (!bdClass) { continue; }
|
||||
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 ];
|
||||
// 字形偏側 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({
|
||||
start: i,
|
||||
end: i + 1,
|
||||
|
||||
+20
-3
@@ -28,8 +28,16 @@ const RBR_BASIC = "\\)\\]\\}";
|
||||
const LBR_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_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 CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||
@@ -72,6 +80,7 @@ function panguSpace(text: string): string
|
||||
let s = text;
|
||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$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(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||
@@ -164,6 +173,14 @@ function processBlock(block: Element, finder: Finder): void
|
||||
while (spacedIdx < spaced.length)
|
||||
{
|
||||
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)
|
||||
{
|
||||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||||
@@ -183,17 +200,17 @@ function processBlock(block: Element, finder: Finder): void
|
||||
spacedIdx += 1;
|
||||
}
|
||||
|
||||
// 倒序執行,避免前面動作之 offset 被後面變更影響。
|
||||
// 倒序執行,避免前面動作之 offset 被後面變更影響。先刪空格(若有)、再包左字
|
||||
// 留隙(若有 before)。刪空格在較高 offset、不擾動更前之 before。
|
||||
for (let i = actions.length - 1; i >= 0; i -= 1)
|
||||
{
|
||||
const act = actions[i]!;
|
||||
if (!act.before) { continue; }
|
||||
if (act.space)
|
||||
{
|
||||
const tn = act.space.textNode;
|
||||
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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user