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
+4
@@ -29,6 +29,10 @@ export declare class Finder {
|
||||
constructor(opts: ResolvedOptions);
|
||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||
pillMatches(el: Element): boolean;
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||
private isTransparentInline;
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
isAvoided(node: Node): boolean;
|
||||
|
||||
Vendored
+11
@@ -1,3 +1,4 @@
|
||||
import type { LangClass } from "./locale.js";
|
||||
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
||||
export declare const CJK: string;
|
||||
/** 西文字母與數字。 */
|
||||
@@ -39,3 +40,13 @@ export declare function blankSides(bdClass: string | null): {
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
};
|
||||
/** 字形墨色偏側。 */
|
||||
export type Bias = "left" | "center" | "right" | "full";
|
||||
/** 逐 lang 逐字之偏側覆寫表(消費端傳入,覆寫參考預設)。 */
|
||||
export type BiasTable = Partial<Record<LangClass, Record<string, Bias>>>;
|
||||
/**
|
||||
* 解析標點字元在某語言下之墨色偏側(margin 後備模式用)。
|
||||
* 優先序:消費端 overrides[langClass][ch] > 括號表 > 內建點號參考表。
|
||||
* 非可壓縮標點(間隔/連接號等)回傳 null(margin 模式不收)。
|
||||
*/
|
||||
export declare function inkBias(ch: string, langClass: LangClass, overrides?: BiasTable): Bias | null;
|
||||
|
||||
Vendored
+52
-8
@@ -15,32 +15,32 @@ jz-em {
|
||||
}
|
||||
|
||||
/* src/css/_lang.css */
|
||||
: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",
|
||||
@@ -63,11 +63,52 @@ jz-char {
|
||||
jz-inner {
|
||||
display: inline;
|
||||
}
|
||||
[data-jz-atoms=inline] jz-char {
|
||||
display: inline;
|
||||
}
|
||||
jz-char.jz-half jz-inner,
|
||||
jz-char.jz-half-le jz-inner {
|
||||
font-feature-settings: "halt" 1;
|
||||
-webkit-font-feature-settings: "halt" 1;
|
||||
}
|
||||
[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=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;
|
||||
}
|
||||
|
||||
/* src/css/_jinze.css */
|
||||
jz-jinze {
|
||||
@@ -75,5 +116,8 @@ jz-jinze {
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
[data-jz-atoms=inline] jz-jinze {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* src/css/juzhen.css */
|
||||
|
||||
Vendored
+100
-8
@@ -45,6 +45,12 @@ var Juzhen = (() => {
|
||||
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) {
|
||||
@@ -207,7 +213,7 @@ var Juzhen = (() => {
|
||||
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;
|
||||
@@ -233,7 +239,7 @@ var Juzhen = (() => {
|
||||
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;
|
||||
@@ -245,7 +251,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
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;
|
||||
@@ -546,6 +552,55 @@ var Juzhen = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
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";
|
||||
@@ -584,15 +639,25 @@ var Juzhen = (() => {
|
||||
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,
|
||||
@@ -1017,8 +1082,10 @@ var Juzhen = (() => {
|
||||
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");
|
||||
@@ -1056,6 +1123,7 @@ var Juzhen = (() => {
|
||||
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");
|
||||
@@ -1127,6 +1195,12 @@ var Juzhen = (() => {
|
||||
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 === " ") {
|
||||
@@ -1143,14 +1217,13 @@ var Juzhen = (() => {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1387,6 +1460,15 @@ var Juzhen = (() => {
|
||||
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 || {};
|
||||
@@ -1397,6 +1479,7 @@ var Juzhen = (() => {
|
||||
policy: langOpt.policy || {}
|
||||
},
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
scope: {
|
||||
root: scope.root || null,
|
||||
include: scope.include || null,
|
||||
@@ -1413,6 +1496,7 @@ var Juzhen = (() => {
|
||||
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),
|
||||
@@ -1447,6 +1531,12 @@ var Juzhen = (() => {
|
||||
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);
|
||||
@@ -1454,6 +1544,8 @@ var Juzhen = (() => {
|
||||
return;
|
||||
}
|
||||
revertPasses(PASSES, makeCtx(el));
|
||||
el.removeAttribute("data-jz-halfwidth");
|
||||
el.removeAttribute("data-jz-atoms");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+24
-2
@@ -1,11 +1,22 @@
|
||||
import type { Finder } from "./core/finder.js";
|
||||
import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js";
|
||||
export type { LangClass, PunctStyle };
|
||||
import type { Bias, BiasTable } from "./core/unicode.js";
|
||||
export type { LangClass, PunctStyle, Bias, BiasTable };
|
||||
/** 長英文詞包裝設定。 */
|
||||
export interface LongWordOptions {
|
||||
minLength?: number;
|
||||
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 {
|
||||
selector: string;
|
||||
@@ -25,6 +36,11 @@ 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;
|
||||
@@ -33,7 +49,7 @@ export interface JuzhenOptions {
|
||||
};
|
||||
spacing?: boolean;
|
||||
longWord?: boolean | LongWordOptions;
|
||||
jiya?: boolean;
|
||||
jiya?: boolean | JiyaOptions;
|
||||
jinze?: boolean;
|
||||
hanging?: boolean;
|
||||
biaodian?: boolean;
|
||||
@@ -52,6 +68,7 @@ export interface JuzhenOptions {
|
||||
export interface ResolvedOptions {
|
||||
locale: LocaleConfig;
|
||||
autospaceClass: string;
|
||||
justifyAtoms: boolean;
|
||||
scope: {
|
||||
root: Element | string | null;
|
||||
include: string | null;
|
||||
@@ -71,6 +88,11 @@ export interface ResolvedOptions {
|
||||
minLength: number;
|
||||
lang: string;
|
||||
};
|
||||
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
||||
jiyaConfig: {
|
||||
halfWidth: "halt" | "margin";
|
||||
bias: BiasTable;
|
||||
};
|
||||
blocks: BlockRule[];
|
||||
finder: {
|
||||
styleInlines: Set<string>;
|
||||
|
||||
Reference in New Issue
Block a user