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:
2026-06-02 01:21:52 +08:00
parent 6834029c31
commit 7b4a051a1f
20 changed files with 1028 additions and 66 deletions
+29 -1
View File
@@ -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-charjz-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");
},
};
}