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:
Vendored
+100
-8
@@ -17,6 +17,12 @@ var Finder = class {
|
||||
pillMatches(el) {
|
||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||
}
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||
isTransparentInline(name) {
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE";
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
isAvoided(node) {
|
||||
@@ -179,7 +185,7 @@ var Finder = class {
|
||||
if (n2.nodeType === 3 && n2.data.length > 0) {
|
||||
return pickEnd(n2);
|
||||
}
|
||||
if (n2.nodeType === 1 && (this.styleSet.has(n2.nodeName) || n2.nodeName === "JZ-HWS")) {
|
||||
if (n2.nodeType === 1 && this.isTransparentInline(n2.nodeName)) {
|
||||
const r = descend(n2);
|
||||
if (r) {
|
||||
return r;
|
||||
@@ -205,7 +211,7 @@ var Finder = class {
|
||||
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;
|
||||
@@ -217,7 +223,7 @@ var Finder = class {
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
if (!p || p.nodeType !== 1 || !this.styleSet.has(p.nodeName)) {
|
||||
if (!p || p.nodeType !== 1 || !this.isTransparentInline(p.nodeName)) {
|
||||
return null;
|
||||
}
|
||||
const pe = p;
|
||||
@@ -518,6 +524,55 @@ function blankSides(bdClass) {
|
||||
}
|
||||
}
|
||||
}
|
||||
var BRACKET_OPEN_RIGHT = "\u300C\u3008\u201C\u2018";
|
||||
var BRACKET_CLOSE_LEFT = "\u300D\u3009\u201D\u2019";
|
||||
var BIAS_BRACKETS = (() => {
|
||||
const t = {};
|
||||
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;
|
||||
})();
|
||||
var BIAS_HANS = {
|
||||
"\uFF0C": "left",
|
||||
"\u3001": "left",
|
||||
"\u3002": "left",
|
||||
"\uFF1B": "left",
|
||||
"\uFF1A": "left",
|
||||
"\uFF01": "left",
|
||||
"\uFF1F": "left"
|
||||
};
|
||||
var BIAS_HANT = {
|
||||
"\uFF0C": "center",
|
||||
"\u3001": "center",
|
||||
"\u3002": "center",
|
||||
"\uFF1B": "center",
|
||||
"\uFF1A": "center",
|
||||
"\uFF01": "center",
|
||||
"\uFF1F": "center"
|
||||
};
|
||||
var BIAS_DEFAULT = {
|
||||
"zh-Hans": BIAS_HANS,
|
||||
"zh-Hant": BIAS_HANT,
|
||||
"ja": BIAS_HANT,
|
||||
// 日文點號慣例居中,沿用繁體表
|
||||
"other": BIAS_HANT
|
||||
};
|
||||
function inkBias(ch, langClass, overrides) {
|
||||
const ov = overrides ? overrides[langClass] : void 0;
|
||||
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;
|
||||
}
|
||||
|
||||
// src/typeset/jiya.ts
|
||||
var PASS = "jiya";
|
||||
@@ -556,15 +611,25 @@ var jiyaPass = {
|
||||
const data = node.data;
|
||||
const out = [];
|
||||
let style = null;
|
||||
let langClass = 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];
|
||||
const bias = inkBias(ch, langClass, biasOverrides);
|
||||
if (bias) {
|
||||
classes.push("jz-ink-" + bias);
|
||||
}
|
||||
out.push({
|
||||
start: i,
|
||||
end: i + 1,
|
||||
@@ -989,8 +1054,10 @@ var LBR_BASIC = "\\(\\[\\{";
|
||||
var RBR_BASIC = "\\)\\]\\}";
|
||||
var LBR_EXT = "\\(\\[\\{<>";
|
||||
var RBR_EXT = "\\)\\]\\}<>";
|
||||
var DEL = String.fromCharCode(57347);
|
||||
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||||
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||||
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
|
||||
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||||
@@ -1028,6 +1095,7 @@ function panguSpace(text) {
|
||||
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);
|
||||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||
@@ -1099,6 +1167,12 @@ function processBlock(block, finder) {
|
||||
let spacedIdx = 0;
|
||||
while (spacedIdx < spaced.length) {
|
||||
const sc = spaced.charAt(spacedIdx);
|
||||
if (sc === DEL) {
|
||||
actions.push({ before: void 0, space: map[origIdx] });
|
||||
origIdx += 1;
|
||||
spacedIdx += 1;
|
||||
continue;
|
||||
}
|
||||
if (sc === MARK) {
|
||||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||||
if (oc === " ") {
|
||||
@@ -1115,14 +1189,13 @@ function processBlock(block, finder) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1359,6 +1432,15 @@ function resolveLongWord(v) {
|
||||
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
|
||||
return { minLength, lang };
|
||||
}
|
||||
function resolveJiya(v) {
|
||||
if (v && typeof v === "object") {
|
||||
return {
|
||||
halfWidth: v.halfWidth === "margin" ? "margin" : "halt",
|
||||
bias: v.bias || {}
|
||||
};
|
||||
}
|
||||
return { halfWidth: "halt", bias: {} };
|
||||
}
|
||||
function normalizeOptions(opts = {}) {
|
||||
const langOpt = opts.lang || {};
|
||||
const scope = opts.scope || {};
|
||||
@@ -1369,6 +1451,7 @@ function normalizeOptions(opts = {}) {
|
||||
policy: langOpt.policy || {}
|
||||
},
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
scope: {
|
||||
root: scope.root || null,
|
||||
include: scope.include || null,
|
||||
@@ -1385,6 +1468,7 @@ function normalizeOptions(opts = {}) {
|
||||
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),
|
||||
@@ -1419,6 +1503,12 @@ function createJuzhen(opts = {}) {
|
||||
return;
|
||||
}
|
||||
runPasses(PASSES, makeCtx(el));
|
||||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||
el.setAttribute("data-jz-halfwidth", "margin");
|
||||
}
|
||||
if (!options.justifyAtoms) {
|
||||
el.setAttribute("data-jz-atoms", "inline");
|
||||
}
|
||||
},
|
||||
revert(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
@@ -1426,6 +1516,8 @@ function createJuzhen(opts = {}) {
|
||||
return;
|
||||
}
|
||||
revertPasses(PASSES, makeCtx(el));
|
||||
el.removeAttribute("data-jz-halfwidth");
|
||||
el.removeAttribute("data-jz-atoms");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user