Files
cjk-autospace/dist/juzhen.mjs
T
admin 2890549f44 fix: 行內排版透明性——pill 前標點誤隙+行內元素整體掉行+審計(Bug1/Bug2/T4-T6)
統一「行內元素對排版透明」原則在各遍歷原語之口徑,修復兩個下游 Bug 及三項同根隱患。

Bug1(pill 前標點誤加間隙):
  isTransparentInline 補 JZ-CHAR/JZ-INNER。`漢字。<code>` 經禁則綁定後,
  adjacentLogicalChar 之 descend 遇不透明 jz-char 既不進入亦不終止、而跳過它越過
  標點取到「字」誤補 jz-hws;透明後回傳標點「。」(非 pill 鄰字)→ 不補隙,與
  collectRuns 既有透明遞迴對齊。

Bug2(行內樣式元素整體掉行):
  新增 core/split.ts 之 isolateBoundaryToken/splitElementAt——沿祖先鏈逐層把邊界
  詞元剝入淺克隆(保留標籤/class/style/lang、去 id;標 data-jz-kind="splitoff"),
  jinze/orphan 改最小綁定:`<strong>長串</strong>,` 只綁末字+逗號(非整串入 nowrap
  inline-block 致行末整體掉行);段末長 strong 不再整綁溢出容器。revertPass 增延後
  階段把 splitoff 併回原元素(先解包 wrap、後併 splitoff),render→revert 還原一致。
  純樣式標籤白名單可切,a/abbr/ruby/帶 id 退化整綁。

審計(同根隱患):
  T4 lineedge edgeRect 攀爬集改由 options.finder.styleInlines 派生(棄硬編碼子集,
     免遺漏 ins/del/sub 等致行端誤判)。
  T5 jiyaAdjacency 遇 pill(pillMatches)斷相鄰、不穿越(內容對排版不可見)。
  T6 spacing processPills 補 per-node 功能閘(data-juzhen-off=spacing 區域內不補隙)。

文檔:README 切片表(spacing/jinze/orphan 三行)+ARCHITECTURE §4.1 透明性原則、
  §4.4 splitoff 還原、§6.4 行內元素最小綁定、§5 jz-orphan 詞彙。

測試 90→102(Bug1×4、Bug2×6、T5/T6×2)。headless Chrome 實證 Bug2:段末 strong
最小切分、orphan 寬 60px 不溢出版心。
2026-06-11 16:06:17 +08:00

2050 lines
57 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/core/finder.ts
var SVG_NS = "http://www.w3.org/2000/svg";
var Finder = class {
constructor(opts) {
this.opts = opts;
this.styleSet = opts.finder.styleInlines;
this.blockSet = opts.finder.blockTags;
this.skipSet = opts.finder.skipTags;
this.pillSelector = opts.finder.pillSelector;
this.skipAttr = opts.finder.skipAttribute;
this.userIsSkipped = opts.finder.isSkipped;
this.levelTextSelector = opts.levelText;
this.avoidSelector = opts.scope.avoid;
this.includeSelector = opts.scope.include;
this.featureCache = /* @__PURE__ */ new WeakMap();
this.levelCache = /* @__PURE__ */ new WeakMap();
}
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
pillMatches(el) {
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
}
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
* jz-*jz-hws 間隙、jz-jinze 禁則群組、jz-charjz-inner 標點原子)。穿越這些
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
* 鄰字仍須可達;I7)。
* ⚠ jz-charjz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug`汉字。<code>` 經 jinze
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`pill 之左鄰字應
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
isTransparentInline(name) {
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
}
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
isAvoided(node) {
if (this.userIsSkipped && this.userIsSkipped(node)) {
return true;
}
let p = node.parentNode;
while (p && p.nodeType === 1) {
const el = p;
if (this.skipSet.has(el.nodeName)) {
return true;
}
if (el.namespaceURI === SVG_NS) {
return true;
}
if (el.isContentEditable) {
return true;
}
if (el.hasAttribute("data-jz-skip")) {
return true;
}
if (this.skipAttr && el.hasAttribute(this.skipAttr)) {
return true;
}
if (this.avoidSelector && el.matches && el.matches(this.avoidSelector)) {
return true;
}
p = p.parentNode;
}
return false;
}
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
inScope(node) {
if (!this.includeSelector) {
return true;
}
const el = node.nodeType === 1 ? node : node.parentElement;
if (!el) {
return false;
}
return !!(el.closest && el.closest(this.includeSelector));
}
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / 非作用域)。 */
eachTextNode(root, cb) {
const ownerDoc = root.ownerDocument || (typeof document !== "undefined" ? document : null);
if (!ownerDoc) {
return;
}
const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const nodes = [];
let n = walker.nextNode();
while (n) {
const t = n;
if (!this.isAvoided(t) && this.inScope(t)) {
nodes.push(t);
}
n = walker.nextNode();
}
for (const node of nodes) {
cb(node);
}
}
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
eachBlock(root, cb) {
const sel = Array.from(this.blockSet).map((t) => t.toLowerCase()).join(",");
const blocks = [];
if (sel) {
root.querySelectorAll(sel).forEach((el) => blocks.push(el));
}
if (root.nodeType === 1 && this.blockSet.has(root.nodeName)) {
blocks.push(root);
}
for (const b of blocks) {
if (this.includeSelector && !(b.closest && b.closest(this.includeSelector))) {
continue;
}
cb(b);
}
}
/**
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
* jz-* 透明穿越;blockpillavoid/間隙 markerjz-hws)中斷 run。
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
*/
collectRuns(block) {
const runs = [];
let curText = "";
let curMap = [];
const flush = () => {
if (curText.length >= 2) {
runs.push({ text: curText, map: curMap });
}
curText = "";
curMap = [];
};
const visit = (node) => {
if (!node) {
return;
}
if (node.nodeType === 3) {
const t = node;
if (this.isAvoided(t) || !this.inScope(t)) {
return;
}
const data = t.data;
for (let i = 0; i < data.length; i += 1) {
curText += data.charAt(i);
curMap.push({ textNode: t, offset: i });
}
return;
}
if (node.nodeType !== 1) {
return;
}
const el = node;
if (this.isAvoided(el)) {
flush();
return;
}
if (this.skipSet.has(el.nodeName) || this.blockSet.has(el.nodeName)) {
flush();
return;
}
if (this.pillMatches(el)) {
flush();
return;
}
let c2 = el.firstChild;
while (c2) {
visit(c2);
c2 = c2.nextSibling;
}
};
let c = block.firstChild;
while (c) {
visit(c);
c = c.nextSibling;
}
flush();
return runs;
}
/**
* pill 之邏輯鄰字(架構 §4.1):經樣式鏈遞迴,遇 pillblockavoid/間隙
* marker 即止。direction = -1 取前鄰字,+1 取後鄰字。
*/
adjacentLogicalChar(el, direction) {
const pickEnd = (tn) => {
if (direction === -1) {
return {
textNode: tn,
offset: tn.data.length - 1,
ch: tn.data.charAt(tn.data.length - 1)
};
}
return { textNode: tn, offset: 0, ch: tn.data.charAt(0) };
};
const descend = (into) => {
let n2 = direction === -1 ? into.lastChild : into.firstChild;
while (n2) {
if (n2.nodeType === 3 && n2.data.length > 0) {
return pickEnd(n2);
}
if (n2.nodeType === 1 && this.isTransparentInline(n2.nodeName)) {
const r = descend(n2);
if (r) {
return r;
}
}
n2 = direction === -1 ? n2.previousSibling : n2.nextSibling;
}
return null;
};
let n = direction === -1 ? el.previousSibling : el.nextSibling;
let p = el.parentNode;
for (; ; ) {
while (n) {
if (n.nodeType === 3) {
if (n.data.length > 0) {
return pickEnd(n);
}
n = direction === -1 ? n.previousSibling : n.nextSibling;
continue;
}
if (n.nodeType === 1) {
const e = n;
if (this.isAvoided(e)) {
return null;
}
if (this.isTransparentInline(e.nodeName)) {
const r = descend(e);
if (r) {
return r;
}
n = direction === -1 ? e.previousSibling : e.nextSibling;
continue;
}
return null;
}
n = direction === -1 ? n.previousSibling : n.nextSibling;
}
if (!p || p.nodeType !== 1 || !this.isTransparentInline(p.nodeName)) {
return null;
}
const pe = p;
n = direction === -1 ? pe.previousSibling : pe.nextSibling;
p = pe.parentNode;
}
}
/**
* 節點之功能級別(§6.5.0a)。自 node 向上找**最近**之顯式信號:元素屬性
* `data-jz-level="text|paragraph"`(最優先),或命中 `level.text` 選擇器(→ text);
* 皆無則預設 paragraph(全功能)。結果以解析後之元素為鍵快取。
*/
levelFor(node) {
const start = node.nodeType === 1 ? node : node.parentElement;
if (!start) {
return "paragraph";
}
const cached = this.levelCache.get(start);
if (cached) {
return cached;
}
let el = start;
let result = "paragraph";
while (el) {
const attr = el.getAttribute ? el.getAttribute("data-jz-level") : null;
if (attr === "text" || attr === "paragraph") {
result = attr;
break;
}
if (this.levelTextSelector && el.matches && el.matches(this.levelTextSelector)) {
result = "text";
break;
}
el = el.parentElement;
}
this.levelCache.set(start, result);
return result;
}
/**
* 某級別之 pass 是否可作用於該節點(§6.5.0a gate)。文本級 pass 不受限(恆 true);
* 段落級 pass 僅作用於段落級元素(text-level 子樹跳過)。與 featureEnabledFor 疊加:
* level 為粗粒度語義組(先判),blocks/屬性為細粒度逐功能(後調)。
*/
levelAllows(node, passLevel) {
if (passLevel === "text") {
return true;
}
return this.levelFor(node) === "paragraph";
}
/**
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
* blocks 選擇器),無則回退全域 features。結果以元素為鍵快取。
*/
featureEnabledFor(node, name) {
const el = node.nodeType === 1 ? node : node.parentElement;
const globalOn = this.globalFeature(name);
if (!el) {
return globalOn;
}
const cached = this.featureCache.get(el);
if (cached && name in cached) {
return cached[name];
}
const result = this.resolveFeature(el, name, globalOn);
const bucket = cached || {};
bucket[name] = result;
this.featureCache.set(el, bucket);
return result;
}
globalFeature(name) {
const f = this.opts.features;
return !!f[name];
}
resolveFeature(start, name, globalOn) {
let el = start;
while (el) {
if (el.hasAttribute("data-juzhen")) {
const list = (el.getAttribute("data-juzhen") || "").split(/\s+/).filter(Boolean);
return list.indexOf(name) >= 0;
}
if (el.hasAttribute("data-juzhen-off")) {
const off = (el.getAttribute("data-juzhen-off") || "").split(/\s+/).filter(Boolean);
if (off.indexOf(name) >= 0) {
return false;
}
}
for (const rule of this.opts.blocks) {
if (rule.selector && el.matches && el.matches(rule.selector)) {
return rule.features.indexOf(name) >= 0;
}
}
el = el.parentElement;
}
return globalOn;
}
};
// src/core/pass.ts
function runPasses(passes, ctx) {
const enabled = passes.filter((p) => p.enabled(ctx.options));
const charify = enabled.filter((p) => p.kind === "charify").sort((a, b) => a.order - b.order);
const standalone = enabled.filter((p) => p.kind === "standalone").sort((a, b) => a.order - b.order);
if (charify.length > 0) {
runCharify(charify, ctx);
}
for (const p of standalone) {
p.render(ctx);
}
}
function revertPasses(passes, ctx) {
const ordered = passes.slice().sort((a, b) => b.order - a.order);
for (const p of ordered) {
p.revert(ctx);
}
}
function runCharify(passes, ctx) {
const work = [];
ctx.finder.eachTextNode(ctx.root, (node) => {
let requests = [];
for (const p of passes) {
if (!ctx.finder.levelAllows(node, p.level)) {
continue;
}
const got = p.collect(node, ctx);
if (got.length > 0) {
requests = requests.concat(got);
}
}
if (requests.length > 0) {
work.push({ node, requests });
}
});
for (const item of work) {
applyRequests(item.node, item.requests);
}
}
function applyRequests(node, requests) {
const parent = node.parentNode;
if (!parent) {
return;
}
const sorted = requests.slice().sort((a, b) => a.start - b.start || a.end - b.end);
const data = node.data;
const ownerDoc = node.ownerDocument;
if (!ownerDoc) {
return;
}
const frag = ownerDoc.createDocumentFragment();
let cursor = 0;
for (const req of sorted) {
if (req.start < cursor) {
continue;
}
if (req.start < 0 || req.end > data.length || req.end <= req.start) {
continue;
}
if (req.start > cursor) {
frag.appendChild(ownerDoc.createTextNode(data.slice(cursor, req.start)));
}
frag.appendChild(req.build(data.slice(req.start, req.end)));
cursor = req.end;
}
if (cursor === 0) {
return;
}
if (cursor < data.length) {
frag.appendChild(ownerDoc.createTextNode(data.slice(cursor)));
}
parent.replaceChild(frag, node);
}
// src/core/dom.ts
var KIND_ATTR = "data-jz-kind";
var PASS_ATTR = "data-jz";
function doc() {
if (typeof document === "undefined") {
throw new Error("juzhen: \u7121 document\uFF08\u9700\u65BC\u700F\u89BD\u5668\u6216 jsdom \u74B0\u5883\u57F7\u884C\uFF09\u3002");
}
return document;
}
function createJz(tag, pass, kind, opts = {}) {
const el = doc().createElement(tag);
el.setAttribute(PASS_ATTR, pass);
el.setAttribute(KIND_ATTR, kind);
if (opts.classes) {
const list = Array.isArray(opts.classes) ? opts.classes : [opts.classes];
for (const c of list) {
if (c) {
el.classList.add(c);
}
}
}
if (opts.attrs) {
for (const k in opts.attrs) {
const v = opts.attrs[k];
if (v !== void 0) {
el.setAttribute(k, v);
}
}
}
if (opts.text !== void 0) {
el.textContent = opts.text;
}
return el;
}
function revertPass(pass, root) {
const nodes = Array.from(
root.querySelectorAll("[" + PASS_ATTR + '="' + pass + '"]')
);
const touched = /* @__PURE__ */ new Set();
for (let i = nodes.length - 1; i >= 0; i -= 1) {
const el = nodes[i];
const parent = el.parentNode;
if (!parent) {
continue;
}
const kind = el.getAttribute(KIND_ATTR);
if (kind === "splitoff") {
continue;
}
touched.add(parent);
if (kind === "marker") {
parent.removeChild(el);
} else {
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
}
const splitoffs = Array.from(
root.querySelectorAll(
"[" + PASS_ATTR + '="' + pass + '"][' + KIND_ATTR + '="splitoff"]'
)
);
for (const so of splitoffs) {
const parent = so.parentNode;
if (!parent) {
continue;
}
touched.add(parent);
const prev = so.previousSibling;
if (prev && prev.nodeType === 1 && prev.nodeName === so.nodeName) {
while (so.firstChild) {
prev.appendChild(so.firstChild);
}
parent.removeChild(so);
} else {
while (so.firstChild) {
parent.insertBefore(so.firstChild, so);
}
parent.removeChild(so);
}
}
for (const p of touched) {
if (p.normalize) {
p.normalize();
}
}
}
var BD_CLASSES = ["bd-open", "bd-close", "bd-pause", "bd-stop", "bd-middle", "bd-liga"];
function bdClassOf(el) {
for (const c of BD_CLASSES) {
if (el.classList.contains(c)) {
return c;
}
}
return null;
}
// src/core/locale.ts
var DEFAULT_POLICY = {
"zh-Hant": "quanjiao",
"zh-Hans": "kaiming",
"ja": "quanjiao",
"other": "quanjiao"
};
function effectiveLang(node, fallback) {
let p = node.parentNode;
while (p && p.nodeType === 1) {
const el = p;
const v = el.getAttribute("lang");
if (v) {
return v.toLowerCase();
}
p = p.parentNode;
}
return (fallback || "").toLowerCase();
}
function classify(lang) {
const l = (lang || "").toLowerCase();
if (l === "ja" || l.indexOf("ja-") === 0) {
return "ja";
}
if (l === "zh" || l.indexOf("zh-") === 0 || l.indexOf("zh") === 0) {
if (l.indexOf("hant") >= 0) {
return "zh-Hant";
}
if (l.indexOf("hans") >= 0) {
return "zh-Hans";
}
if (/-(tw|hk|mo)\b/.test(l)) {
return "zh-Hant";
}
if (l === "zh") {
return "zh-Hant";
}
return "zh-Hans";
}
return "other";
}
function styleFor(langClass, config) {
const perLang = config.policy ? config.policy[langClass] : void 0;
if (perLang) {
return perLang;
}
if (config.style) {
return config.style;
}
return DEFAULT_POLICY[langClass];
}
function resolveStyle(node, config) {
const lang = effectiveLang(node, config.default);
return styleFor(classify(lang), config);
}
// src/core/unicode.ts
var CJK = "\u2E80-\u2EFF\u2F00-\u2FDF\u3040-\u309F\u30A0-\u30FA\u30FC-\u30FF\u3100-\u312F\u3200-\u32FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF";
var ALNUM = "A-Za-z0-9";
var ALPHA = "A-Za-z";
var ANY_CJK = new RegExp("[" + CJK + "]");
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
var BD_MIDDLE = "\xB7\u30FB\u2027";
var BD_LIGA = "\u2026\u2014";
var BD_ALL = BD_STOP + BD_PAUSE + BD_OPEN + BD_CLOSE + BD_MIDDLE + BD_LIGA;
var BD_ORDER = [
"stop",
"pause",
"open",
"close",
"middle",
"liga"
];
var DEFAULT_RULESET = {
cjk: CJK,
ansAfter: ANS_AFTER_CJK,
ansBefore: ANS_BEFORE_CJK,
biaodian: {
stop: BD_STOP,
pause: BD_PAUSE,
open: BD_OPEN,
close: BD_CLOSE,
middle: BD_MIDDLE,
liga: BD_LIGA
}
};
function resolveRuleset(override) {
if (!override) {
return DEFAULT_RULESET;
}
const west = override.western || "";
const bd = { ...DEFAULT_RULESET.biaodian };
if (override.biaodian) {
for (const sub of BD_ORDER) {
const extra = override.biaodian[sub];
if (extra) {
bd[sub] = bd[sub] + extra;
}
}
}
return {
cjk: CJK + (override.cjk || ""),
ansAfter: ANS_AFTER_CJK + west,
ansBefore: ANS_BEFORE_CJK + west,
biaodian: bd
};
}
function classifyBiaodian(ch, biaodian = DEFAULT_RULESET.biaodian) {
for (const sub of BD_ORDER) {
if (biaodian[sub].indexOf(ch) >= 0) {
return "bd-" + sub;
}
}
return null;
}
function blankSides(bdClass) {
switch (bdClass) {
case "bd-open": {
return { left: true, right: false };
}
case "bd-close":
case "bd-pause":
case "bd-stop": {
return { left: false, right: true };
}
default: {
return { left: false, right: false };
}
}
}
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",
"\uFF0E": "left",
"\uFF1B": "left",
"\uFF1A": "left",
"\uFF01": "left",
"\uFF1F": "left"
};
var BIAS_HANT = {
"\uFF0C": "center",
"\u3001": "center",
"\u3002": "center",
"\uFF0E": "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";
function baselineHalf(style, bdClass) {
if (style === "quanjiao") {
return false;
}
if (style === "banjiao") {
return bdClass === "bd-pause" || bdClass === "bd-stop" || bdClass === "bd-open" || bdClass === "bd-close";
}
return bdClass === "bd-pause" || bdClass === "bd-open" || bdClass === "bd-close";
}
function inJzChar(node) {
let p = node.parentNode;
while (p && p.nodeType === 1) {
const name = p.nodeName;
if (name === "JZ-CHAR" || name === "JZ-INNER") {
return true;
}
p = p.parentNode;
}
return false;
}
var jiyaPass = {
name: PASS,
kind: "charify",
order: 20,
level: "text",
enabled: (o) => o.features.jiya,
collect(node, ctx) {
if (inJzChar(node)) {
return [];
}
if (!ctx.finder.featureEnabledFor(node, PASS)) {
return [];
}
const data = node.data;
const out = [];
let style = null;
let langClass = null;
const biasOverrides = ctx.options.jiyaConfig.bias;
const biaodian = ctx.options.ruleset.biaodian;
for (let i = 0; i < data.length; i += 1) {
const ch = data.charAt(i);
const bdClass = classifyBiaodian(ch, biaodian);
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,
pass: PASS,
build: (text) => {
const outer = createJz("jz-char", PASS, "wrap", { classes });
const inner = createJz("jz-inner", PASS, "wrap", { text });
outer.appendChild(inner);
return outer;
}
});
}
return out;
},
revert(ctx) {
revertPass(PASS, ctx.root);
}
};
var jiyaAdjacencyPass = {
name: "jiyaAdjacency",
kind: "standalone",
order: 25,
level: "text",
enabled: (o) => o.features.jiya,
render(ctx) {
const { finder, options } = ctx;
const blockSet = options.finder.blockTags;
let prev = null;
const squeeze = (L, R) => {
if (!finder.featureEnabledFor(L, PASS)) {
return;
}
const lSides = blankSides(bdClassOf(L));
const rSides = blankSides(bdClassOf(R));
if (lSides.right) {
L.classList.add("jz-half");
}
if (rSides.left) {
R.classList.add("jz-half");
}
};
const visit = (node) => {
for (let c = node.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 3) {
if (c.data.length > 0) {
prev = null;
}
continue;
}
if (c.nodeType !== 1) {
continue;
}
const el = c;
const nm = el.nodeName;
if (nm === "JZ-CHAR") {
if (prev) {
squeeze(prev, el);
}
prev = el;
} else if (finder.pillMatches(el)) {
prev = null;
} else if (finder.isAvoided(el)) {
prev = null;
} else if (blockSet.has(nm)) {
prev = null;
visit(el);
prev = null;
} else {
visit(el);
}
}
};
visit(ctx.root);
},
revert() {
}
};
// src/core/split.ts
var PASS_ATTR2 = "data-jz";
var KIND_ATTR2 = "data-jz-kind";
var SPLITTABLE = /* @__PURE__ */ new Set([
"STRONG",
"EM",
"B",
"I",
"U",
"S",
"SPAN",
"MARK",
"SMALL",
"INS",
"DEL",
"SUB",
"SUP"
]);
function canSplit(el) {
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id");
}
function firstText(el) {
for (let c = el.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 3 && c.data.length > 0) {
return c;
}
if (c.nodeType === 1) {
if (!canSplit(c)) {
return null;
}
const r = firstText(c);
if (r) {
return r;
}
}
}
return null;
}
function lastText(el) {
for (let c = el.lastChild; c; c = c.previousSibling) {
if (c.nodeType === 3 && c.data.length > 0) {
return c;
}
if (c.nodeType === 1) {
if (!canSplit(c)) {
return null;
}
const r = lastText(c);
if (r) {
return r;
}
}
}
return null;
}
function isTokenChar(ch, anyCjk) {
return !anyCjk.test(ch) && !/\s/.test(ch);
}
function leadingTokenEnd(d, anyCjk) {
if (anyCjk.test(d.charAt(0))) {
return 1;
}
let e = 1;
while (e < d.length && isTokenChar(d.charAt(e), anyCjk)) {
e += 1;
}
return e;
}
function trailingTokenStart(d, anyCjk) {
const last = d.length - 1;
if (anyCjk.test(d.charAt(last))) {
return last;
}
let s = last;
while (s > 0 && isTokenChar(d.charAt(s - 1), anyCjk)) {
s -= 1;
}
return s;
}
function splitTreeAfter(top, textNode, offset, pass) {
if (offset <= 0 && firstText(top) === textNode) {
return null;
}
let rightAtLevel;
if (offset >= textNode.data.length) {
rightAtLevel = textNode.nextSibling;
} else if (offset <= 0) {
rightAtLevel = textNode;
} else {
rightAtLevel = textNode.splitText(offset);
}
let ancestor = textNode.parentNode;
let topClone = null;
while (ancestor && ancestor.nodeType === 1) {
const anc = ancestor;
let clone = null;
if (rightAtLevel) {
clone = anc.cloneNode(false);
clone.removeAttribute("id");
clone.setAttribute(PASS_ATTR2, pass);
clone.setAttribute(KIND_ATTR2, "splitoff");
let n = rightAtLevel;
while (n) {
const next = n.nextSibling;
clone.appendChild(n);
n = next;
}
}
if (anc === top) {
if (!clone) {
return null;
}
if (top.parentNode) {
top.parentNode.insertBefore(clone, top.nextSibling);
}
topClone = clone;
break;
}
if (clone) {
if (anc.parentNode) {
anc.parentNode.insertBefore(clone, anc.nextSibling);
}
rightAtLevel = clone;
} else {
rightAtLevel = anc.nextSibling;
}
ancestor = anc.parentNode;
}
return topClone;
}
function isolateBoundaryToken(el, side, anyCjk, pass) {
if (!canSplit(el)) {
return el;
}
const text = side === "tail" ? lastText(el) : firstText(el);
if (!text) {
return el;
}
const point = side === "tail" ? { textNode: text, offset: trailingTokenStart(text.data, anyCjk) } : { textNode: text, offset: leadingTokenEnd(text.data, anyCjk) };
const clone = splitTreeAfter(el, point.textNode, point.offset, pass);
if (!clone) {
return el;
}
return side === "tail" ? clone : el;
}
function splitElementAt(el, textNode, offset, pass) {
if (!canSplit(el)) {
return null;
}
return splitTreeAfter(el, textNode, offset, pass);
}
// src/typeset/jinze.ts
var PASS2 = "jinze";
var AVOID_HEAD = /* @__PURE__ */ new Set(["bd-close", "bd-pause", "bd-stop", "bd-middle", "bd-liga"]);
var AVOID_TAIL = /* @__PURE__ */ new Set(["bd-open"]);
function isCharEl(n) {
return !!n && n.nodeType === 1 && n.nodeName === "JZ-CHAR";
}
function forbiddenBreak(a, b, finder) {
if (isCharEl(b)) {
const c = bdClassOf(b);
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(b, PASS2)) {
return true;
}
}
if (isCharEl(a)) {
const c = bdClassOf(a);
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(a, PASS2)) {
return true;
}
}
return false;
}
function isTokenChar2(ch, anyCjk) {
return !anyCjk.test(ch) && !/\s/.test(ch);
}
function leadingTokenEnd2(d, anyCjk) {
if (anyCjk.test(d.charAt(0))) {
return 1;
}
let e = 1;
while (e < d.length && isTokenChar2(d.charAt(e), anyCjk)) {
e += 1;
}
return e;
}
function trailingTokenStart2(d, anyCjk) {
const last = d.length - 1;
if (anyCjk.test(d.charAt(last))) {
return last;
}
let s = last;
while (s > 0 && isTokenChar2(d.charAt(s - 1), anyCjk)) {
s -= 1;
}
return s;
}
function splitBoundaries(t, needFirst, needLast, anyCjk) {
let node = t;
if (needFirst) {
const e = leadingTokenEnd2(node.data, anyCjk);
if (e < node.data.length) {
node = node.splitText(e);
}
}
if (needLast) {
const s = trailingTokenStart2(node.data, anyCjk);
if (s > 0) {
node.splitText(s);
}
}
}
function processParent(parent, finder, anyCjk) {
for (let c = parent.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 1 && c.nodeName === "JZ-JINZE") {
return;
}
}
const units = [];
for (let c = parent.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 3 && c.data.length > 0) {
units.push(c);
} else if (c.nodeType === 1) {
units.push(c);
}
}
const n = units.length;
if (n < 2) {
return;
}
const forbidden = [];
for (let i2 = 0; i2 < n - 1; i2 += 1) {
forbidden.push(forbiddenBreak(units[i2], units[i2 + 1], finder));
}
for (let i2 = 0; i2 < n; i2 += 1) {
const u = units[i2];
const needFirst = i2 > 0 && forbidden[i2 - 1];
const needLast = i2 < n - 1 && forbidden[i2];
if (!needFirst && !needLast) {
continue;
}
if (u.nodeType === 3) {
splitBoundaries(u, needFirst, needLast, anyCjk);
} else if (u.nodeType === 1) {
const el = u;
if (needLast) {
isolateBoundaryToken(el, "tail", anyCjk, PASS2);
}
if (needFirst) {
isolateBoundaryToken(el, "head", anyCjk, PASS2);
}
}
}
const kids = [];
for (let c = parent.firstChild; c; c = c.nextSibling) {
kids.push(c);
}
let i = 0;
while (i < kids.length) {
const run = [kids[i]];
while (i + 1 < kids.length && forbiddenBreak(kids[i], kids[i + 1], finder)) {
run.push(kids[i + 1]);
i += 1;
}
if (run.length >= 2) {
const group = createJz("jz-jinze", PASS2, "wrap");
parent.insertBefore(group, run[0]);
for (const node of run) {
group.appendChild(node);
}
}
i += 1;
}
}
var jinzePass = {
name: PASS2,
kind: "standalone",
order: 40,
level: "paragraph",
enabled: (o) => o.features.jinze,
render(ctx) {
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
const parents = /* @__PURE__ */ new Set();
ctx.root.querySelectorAll("jz-char").forEach((c) => {
if (!ctx.finder.levelAllows(c, "paragraph")) {
return;
}
if (c.parentNode) {
parents.add(c.parentNode);
}
});
for (const parent of parents) {
processParent(parent, ctx.finder, anyCjk);
}
},
revert(ctx) {
revertPass(PASS2, ctx.root);
}
};
// src/typeset/lineedge.ts
var LE = "jz-half-le";
var HANG_L = "jz-hang-l";
var HANG_R = "jz-hang-r";
var ALL_EDGE = [LE, HANG_L, HANG_R];
var EPS = 2;
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
var TAIL_SQUEEZE = /* @__PURE__ */ new Set(["bd-close"]);
var TAIL_HANG = /* @__PURE__ */ new Set(["bd-close", "bd-stop", "bd-pause"]);
var observers = /* @__PURE__ */ new WeakMap();
function hasLayout() {
return typeof ResizeObserver !== "undefined";
}
function installLayout(root, key, run) {
run();
let m = observers.get(root);
if (!m) {
m = /* @__PURE__ */ new Map();
observers.set(root, m);
}
const existing = m.get(key);
if (existing) {
existing.disconnect();
}
const ro = new ResizeObserver(() => run());
ro.observe(root);
m.set(key, ro);
const fonts = root.ownerDocument.fonts;
if (fonts && fonts.ready && typeof fonts.ready.then === "function") {
fonts.ready.then(run, () => void 0);
}
}
function uninstallLayout(root, key) {
const m = observers.get(root);
if (!m) {
return;
}
const ro = m.get(key);
if (ro) {
ro.disconnect();
m.delete(key);
}
}
var JZ_WRAP_TAGS = ["JZ-JINZE", "JZ-CHAR", "JZ-INNER", "JZ-HWS"];
function buildWrapSet(options) {
const s = new Set(JZ_WRAP_TAGS);
for (const name of options.finder.styleInlines) {
s.add(name);
}
return s;
}
function rectOf(n, side) {
if (n.nodeType === 3 && n.data.length > 0) {
const t = n;
const doc2 = t.ownerDocument;
if (!doc2) {
return null;
}
const range = doc2.createRange();
const at = side === "prev" ? t.data.length - 1 : 0;
range.setStart(t, at);
range.setEnd(t, at + 1);
const rects = range.getClientRects();
if (rects.length) {
return rects[side === "prev" ? rects.length - 1 : 0];
}
} else if (n.nodeType === 1) {
const rects = n.getClientRects();
if (rects.length) {
return rects[side === "prev" ? rects.length - 1 : 0];
}
}
return null;
}
function edgeRect(start, side, from, wrapSet) {
let n = start;
let parentRef = from.parentNode;
for (; ; ) {
while (n) {
const r = rectOf(n, side);
if (r) {
return r;
}
n = side === "prev" ? n.previousSibling : n.nextSibling;
}
if (!parentRef || parentRef.nodeType !== 1) {
return null;
}
if (!wrapSet.has(parentRef.nodeName)) {
return null;
}
n = side === "prev" ? parentRef.previousSibling : parentRef.nextSibling;
parentRef = parentRef.parentNode;
}
}
function firstRect(el) {
const rects = el.getClientRects();
return rects.length ? rects[0] : null;
}
function lowerLine(a, b) {
return b.top >= a.bottom - EPS;
}
function higherLine(a, b) {
return b.bottom <= a.top + EPS;
}
function lastRect(el) {
const rects = el.getClientRects();
return rects.length ? rects[rects.length - 1] : null;
}
var HANG_ENABLED = false;
function applyEdge(el, edge, mode) {
if (mode === "squeeze") {
el.classList.add(LE);
return;
}
if (mode === "hang" && HANG_ENABLED) {
el.classList.add(edge === "head" ? HANG_L : HANG_R);
}
}
function clearEdge(root) {
for (const cls of ALL_EDGE) {
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
}
}
function modeForChar(el, options) {
const base = options.lineEnd;
if (base !== "hang") {
return base;
}
return "squeeze";
}
function relayout(root, finder, options) {
clearEdge(root);
const wrapSet = buildWrapSet(options);
const chars = Array.from(root.querySelectorAll("jz-char")).filter(
(c) => finder.inScope(c) && !finder.isAvoided(c)
);
const decisions = [];
for (const el of chars) {
if (!finder.levelAllows(el, "paragraph")) {
continue;
}
if (!finder.featureEnabledFor(el, "jiya")) {
continue;
}
const mode = modeForChar(el, options);
if (mode === "none") {
continue;
}
if (mode === "hang" && el.classList.contains("jz-half")) {
continue;
}
const bd = bdClassOf(el);
if (!bd) {
continue;
}
const tailSet = mode === "hang" ? TAIL_HANG : TAIL_SQUEEZE;
const isHead = HEAD_CLASSES.has(bd);
const isTail = tailSet.has(bd);
if (!isHead && !isTail) {
continue;
}
const r = firstRect(el);
if (!r) {
continue;
}
if (isHead) {
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
if (!prev || higherLine(r, prev)) {
decisions.push({ el, edge: "head", mode });
}
} else {
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
if (!next || lowerLine(r, next)) {
decisions.push({ el, edge: "tail", mode });
}
}
}
for (const d of decisions) {
applyEdge(d.el, d.edge, d.mode);
}
}
var lineEdgePass = {
name: "jiyaLineEdge",
kind: "standalone",
order: 90,
level: "paragraph",
enabled: (o) => o.features.jiya,
render(ctx) {
if (!hasLayout()) {
return;
}
installLayout(ctx.root, "lineEdge", () => relayout(ctx.root, ctx.finder, ctx.options));
},
revert(ctx) {
uninstallLayout(ctx.root, "lineEdge");
clearEdge(ctx.root);
}
};
var TRIM = "jz-hws-trim";
function trimGaps(root, finder, wrapSet) {
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph")
);
if (!gaps.length) {
return;
}
gaps.forEach((g) => g.classList.add(TRIM));
for (const g of gaps) {
const r = lastRect(g);
const next = r ? edgeRect(g.nextSibling, "next", g, wrapSet) : null;
const atLineEnd = !r || !next || next.top > r.top + EPS;
if (!atLineEnd) {
g.classList.remove(TRIM);
}
}
}
var gapTrimPass = {
name: "spacingGapTrim",
kind: "standalone",
order: 92,
level: "paragraph",
enabled: (o) => o.features.spacing,
render(ctx) {
if (!hasLayout()) {
return;
}
const wrapSet = buildWrapSet(ctx.options);
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder, wrapSet));
},
revert(ctx) {
uninstallLayout(ctx.root, "gapTrim");
ctx.root.querySelectorAll("jz-hws." + TRIM).forEach((e) => e.classList.remove(TRIM));
}
};
// src/typeset/longword.ts
var PASS3 = "longWord";
function inLangSpan(node) {
const p = node.parentNode;
if (!p || p.nodeType !== 1) {
return false;
}
const el = p;
if (el.nodeName !== "SPAN") {
return false;
}
return el.hasAttribute("lang");
}
var longWordPass = {
name: PASS3,
kind: "charify",
order: 60,
level: "paragraph",
enabled: (o) => o.features.longWord,
collect(node, ctx) {
if (inLangSpan(node)) {
return [];
}
if (!ctx.finder.featureEnabledFor(node, PASS3)) {
return [];
}
const cfg = ctx.options.longWord;
const re = new RegExp("[A-Za-z]{" + cfg.minLength + ",}", "g");
const data = node.data;
const out = [];
let m;
while ((m = re.exec(data)) !== null) {
const start = m.index;
const end = m.index + m[0].length;
out.push({
start,
end,
pass: PASS3,
build: (text) => {
const span = node.ownerDocument.createElement("span");
span.setAttribute("lang", cfg.lang);
span.setAttribute("data-jz", PASS3);
span.setAttribute("data-jz-kind", "wrap");
span.textContent = text;
return span;
}
});
}
return out;
},
revert(ctx) {
revertPass(PASS3, ctx.root);
}
};
// src/typeset/orphan.ts
var PASS4 = "orphan";
function hasOrphan(block) {
for (let c = block.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 1 && c.nodeName === "JZ-ORPHAN") {
return true;
}
}
return false;
}
function collectSubst(block, finder, blockSet, anyCjk, biaodian) {
const out = [];
const visit = (node, directChild) => {
if (node.nodeType === 3) {
const t = node;
const d = t.data;
for (let i = 0; i < d.length; i += 1) {
const ch = d.charAt(i);
if (anyCjk.test(ch) && !classifyBiaodian(ch, biaodian)) {
out.push({ directChild, textNode: t, offset: i });
}
}
return;
}
if (node.nodeType !== 1) {
return;
}
const el = node;
if (finder.isAvoided(el)) {
return;
}
if (blockSet.has(el.nodeName) || finder.pillMatches(el)) {
return;
}
for (let c = el.firstChild; c; c = c.nextSibling) {
visit(c, directChild);
}
};
for (let dc = block.firstChild; dc; dc = dc.nextSibling) {
visit(dc, dc);
}
return out;
}
function processBlock(block, finder, blockSet, anyCjk, biaodian, chars) {
if (hasOrphan(block)) {
return;
}
const subst = collectSubst(block, finder, blockSet, anyCjk, biaodian);
if (subst.length < chars) {
return;
}
const boundary = subst[subst.length - chars];
let startChild = boundary.directChild;
if (boundary.textNode && boundary.textNode === startChild) {
if (boundary.offset > 0) {
startChild = boundary.textNode.splitText(boundary.offset);
}
} else if (boundary.textNode && startChild.nodeType === 1) {
const clone = splitElementAt(
startChild,
boundary.textNode,
boundary.offset,
PASS4
);
if (clone) {
startChild = clone;
}
}
const orphan = createJz("jz-orphan", PASS4, "wrap");
block.insertBefore(orphan, startChild);
let n = startChild;
while (n) {
const next = n.nextSibling;
orphan.appendChild(n);
n = next;
}
}
var orphanPass = {
name: PASS4,
kind: "standalone",
order: 70,
level: "paragraph",
enabled: (o) => o.features.orphan,
render(ctx) {
const { finder, options } = ctx;
const blockSet = options.finder.blockTags;
const anyCjk = new RegExp("[" + options.ruleset.cjk + "]");
const biaodian = options.ruleset.biaodian;
const chars = options.orphan.chars;
finder.eachBlock(ctx.root, (block) => {
if (!finder.levelAllows(block, "paragraph")) {
return;
}
if (!finder.featureEnabledFor(block, PASS4)) {
return;
}
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
if (sel && block.querySelector(sel)) {
return;
}
processBlock(block, finder, blockSet, anyCjk, biaodian, chars);
});
},
revert(ctx) {
revertPass(PASS4, ctx.root);
}
};
// src/typeset/spacing.ts
var PASS5 = "spacing";
var MARK = "\uE000";
var PH_OPEN = "\uE001";
var PH_CLOSE = "\uE002";
var OPS_HYPHEN = "\\+\\*=&\\-";
var OPS_GRADE = "\\+\\-\\*";
var QUOTES_CLS = '`"\u05F4';
var LBR_BASIC = "\\(\\[\\{";
var RBR_BASIC = "\\)\\]\\}";
var LBR_EXT = "\\(\\[\\{<>";
var RBR_EXT = "\\)\\]\\}<>";
var DEL = String.fromCharCode(57347);
function makeRules(rs) {
const CJK2 = rs.cjk;
const ANS_AFTER = rs.ansAfter;
const ANS_BEFORE = rs.ansBefore;
return {
anyCjk: new RegExp("[" + CJK2 + "]"),
pillNeighbor: new RegExp("[" + ALNUM + CJK2 + "]"),
STRIP_CJK_SPACE_ANS: new RegExp("([" + CJK2 + "]) ([" + ANS_AFTER + "])", "g"),
STRIP_ANS_SPACE_CJK: new RegExp("([" + ANS_BEFORE + "]) ([" + CJK2 + "])", "g"),
STRIP_CJK_SPACE_CJK: new RegExp("([" + CJK2 + "]) (?=[" + CJK2 + "])", "g"),
DOTS_CJK: new RegExp("([\\.]{2,})([" + CJK2 + "])", "g"),
CJK_PUNCTUATION: new RegExp("([" + CJK2 + "])([!;,\\?:]+)(?=[" + CJK2 + ALNUM + "])", "g"),
AN_PUNCTUATION_CJK: new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK2 + "])", "g"),
CJK_TILDE: new RegExp("([" + CJK2 + "])(~+)(?!=)(?=[" + CJK2 + ALNUM + "])", "g"),
CJK_TILDE_EQUALS: new RegExp("([" + CJK2 + "])(~=)", "g"),
CJK_PERIOD: new RegExp("([" + CJK2 + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK2 + ALNUM + "])", "g"),
AN_PERIOD_CJK: new RegExp("([" + ALNUM + "])(\\.)([" + CJK2 + "])", "g"),
AN_COLON_CJK: new RegExp("([" + ALNUM + "])(:)([" + CJK2 + "])", "g"),
CJK_QUOTE: new RegExp("([" + CJK2 + "])([" + QUOTES_CLS + "])", "g"),
QUOTE_CJK: new RegExp("([" + QUOTES_CLS + "])([" + CJK2 + "])", "g"),
CJK_QUOTE_AN: new RegExp("([" + CJK2 + '])(")([' + ALNUM + "])", "g"),
CJK_HASH: new RegExp("([" + CJK2 + "])(#([^ ]))", "g"),
HASH_CJK: new RegExp("(([^ ])#)([" + CJK2 + "])", "g"),
SINGLE_LETTER_GRADE: new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK2 + "])", "g"),
CJK_OPERATOR_ANS: new RegExp("([" + CJK2 + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g"),
ANS_OPERATOR_CJK: new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK2 + "])", "g"),
CJK_LESS_THAN: new RegExp("([" + CJK2 + "])(<)([" + ALNUM + "])", "g"),
LESS_THAN_CJK: new RegExp("([" + ALNUM + "])(<)([" + CJK2 + "])", "g"),
CJK_GREATER_THAN: new RegExp("([" + CJK2 + "])(>)([" + ALNUM + "])", "g"),
GREATER_THAN_CJK: new RegExp("([" + ALNUM + "])(>)([" + CJK2 + "])", "g"),
CJK_LEFT_BRACKET: new RegExp("([" + CJK2 + "])([" + LBR_EXT + "])", "g"),
RIGHT_BRACKET_CJK: new RegExp("([" + RBR_EXT + "])([" + CJK2 + "])", "g"),
AN_LEFT_BRACKET: new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g"),
RIGHT_BRACKET_AN: new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g"),
CJK_ANS: new RegExp("([" + CJK2 + "])([" + ANS_AFTER + "])", "g"),
ANS_CJK: new RegExp("([" + ANS_BEFORE + "])([" + CJK2 + "])", "g"),
S_A: new RegExp("(%)([" + ALPHA + "])", "g")
};
}
var COMPOUND_WORD = /\b(?:[A-Za-z0-9]*[a-z][A-Za-z0-9]*-[A-Za-z0-9]+|[A-Za-z0-9]+-[A-Za-z0-9]*[a-z][A-Za-z0-9]*|[A-Za-z]+-[0-9]+|[A-Za-z]+[0-9]+-[A-Za-z0-9]+)(?:-[A-Za-z0-9]+)*\b/g;
var PH_PATTERN = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
function panguSpace(text, R) {
if (text.length <= 1 || !R.anyCjk.test(text)) {
return text;
}
let s = text;
s = s.replace(R.STRIP_CJK_SPACE_ANS, "$1$2");
s = s.replace(R.STRIP_ANS_SPACE_CJK, "$1$2");
s = s.replace(R.STRIP_CJK_SPACE_CJK, "$1" + DEL);
s = s.replace(R.DOTS_CJK, "$1" + MARK + "$2");
s = s.replace(R.CJK_PUNCTUATION, "$1$2" + MARK);
s = s.replace(R.AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
s = s.replace(R.CJK_TILDE, "$1$2" + MARK);
s = s.replace(R.CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
s = s.replace(R.CJK_PERIOD, "$1$2" + MARK);
s = s.replace(R.AN_PERIOD_CJK, "$1$2" + MARK + "$3");
s = s.replace(R.AN_COLON_CJK, "$1$2" + MARK + "$3");
s = s.replace(R.CJK_QUOTE, "$1" + MARK + "$2");
s = s.replace(R.QUOTE_CJK, "$1" + MARK + "$2");
s = s.replace(R.CJK_QUOTE_AN, "$1$2" + MARK + "$3");
s = s.replace(R.CJK_HASH, "$1" + MARK + "$2");
s = s.replace(R.HASH_CJK, "$1" + MARK + "$3");
const compounds = [];
s = s.replace(COMPOUND_WORD, (m) => {
compounds.push(m);
return PH_OPEN + (compounds.length - 1) + PH_CLOSE;
});
s = s.replace(R.SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
s = s.replace(R.CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
s = s.replace(R.ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
s = s.replace(R.CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
s = s.replace(R.LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
s = s.replace(R.CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
s = s.replace(R.GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
s = s.replace(PH_PATTERN, (_m, idx) => compounds[Number(idx)] || "");
s = s.replace(R.CJK_LEFT_BRACKET, "$1" + MARK + "$2");
s = s.replace(R.RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
s = s.replace(R.AN_LEFT_BRACKET, "$1" + MARK + "$2");
s = s.replace(R.RIGHT_BRACKET_AN, "$1" + MARK + "$2");
s = s.replace(R.CJK_ANS, "$1" + MARK + "$2");
s = s.replace(R.ANS_CJK, "$1" + MARK + "$2");
s = s.replace(R.S_A, "$1" + MARK + "$2");
return s;
}
function wrapNodeInGap(node) {
const parent = node.parentNode;
if (!parent || parent.nodeName === "JZ-HWS") {
return;
}
const gap = createJz("jz-hws", PASS5, "wrap");
parent.insertBefore(gap, node);
gap.appendChild(node);
}
function wrapGapLeft(pos) {
let tn = pos.textNode;
if (!tn.parentNode || tn.parentNode.nodeName === "JZ-HWS") {
return;
}
if (pos.offset > 0) {
tn = tn.splitText(pos.offset);
}
if (tn.data.length > 1) {
tn.splitText(1);
}
wrapNodeInGap(tn);
}
function processBlock2(block, finder, R) {
const runs = finder.collectRuns(block);
for (const run of runs) {
const { text, map } = run;
const spaced = panguSpace(text, R);
if (spaced === text) {
continue;
}
const actions = [];
let origIdx = 0;
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 === " ") {
actions.push({ before: map[origIdx - 1], space: map[origIdx] });
origIdx += 1;
} else {
actions.push({ before: map[origIdx - 1], space: null });
}
spacedIdx += 1;
continue;
}
origIdx += 1;
spacedIdx += 1;
}
for (let i = actions.length - 1; i >= 0; i -= 1) {
const act = actions[i];
if (act.space) {
const tn = act.space.textNode;
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
}
if (act.before) {
wrapGapLeft(act.before);
}
}
}
}
function processPills(root, finder, pillSelector, pillNeighbor) {
root.querySelectorAll(pillSelector).forEach((pill) => {
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
return;
}
if (!finder.featureEnabledFor(pill, PASS5)) {
return;
}
if (!pill.parentNode) {
return;
}
const prev = finder.adjacentLogicalChar(pill, -1);
if (prev) {
handlePillSide(prev, pill, -1, pillNeighbor);
}
const next = finder.adjacentLogicalChar(pill, 1);
if (next) {
handlePillSide(next, pill, 1, pillNeighbor);
}
});
}
function handlePillSide(adj, pill, dir, pillNeighbor) {
if (dir === -1) {
if (pillNeighbor.test(adj.ch)) {
wrapGapLeft({ textNode: adj.textNode, offset: adj.offset });
return;
}
if (adj.ch === " " && adj.offset >= 1) {
const tn = adj.textNode;
const bc = tn.data.charAt(adj.offset - 1);
if (bc !== " " && pillNeighbor.test(bc)) {
tn.data = tn.data.slice(0, adj.offset) + tn.data.slice(adj.offset + 1);
wrapGapLeft({ textNode: tn, offset: adj.offset - 1 });
}
}
} else {
if (pillNeighbor.test(adj.ch)) {
wrapNodeInGap(pill);
return;
}
if (adj.ch === " " && adj.textNode.data.length >= 2) {
const ac = adj.textNode.data.charAt(1);
if (ac !== " " && pillNeighbor.test(ac)) {
adj.textNode.data = adj.textNode.data.slice(1);
wrapNodeInGap(pill);
}
}
}
}
var spacingPass = {
name: PASS5,
kind: "standalone",
order: 50,
level: "text",
enabled: (o) => o.features.spacing,
render(ctx) {
const { finder, options } = ctx;
const R = makeRules(options.ruleset);
ctx.finder.eachBlock(ctx.root, (block) => {
if (!finder.featureEnabledFor(block, PASS5)) {
return;
}
processBlock2(block, finder, R);
});
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.isAvoided(ctx.root)) {
processBlock2(ctx.root, finder, R);
}
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
},
revert(ctx) {
revertPass(PASS5, ctx.root);
}
};
// src/compat/v1.ts
function mapLongWord(v) {
if (v === false) {
return false;
}
if (v === void 0 || v === true || v === null) {
return true;
}
const out = {};
if (typeof v.minLength === "number") {
out.minLength = v.minLength;
}
if (typeof v.lang === "string") {
out.lang = v.lang;
}
return out;
}
function createCjkAutospace(options = {}) {
const o = options || {};
const mapped = {
// finder 設定直通。
...o.styleInlines ? { styleInlines: o.styleInlines } : {},
...o.blockTags ? { blockTags: o.blockTags } : {},
...o.skipTags ? { skipTags: o.skipTags } : {},
pillSelector: o.pillSelector || "code, kbd",
autospaceClass: o.autospaceClass || "cjk-autospace",
skipAttribute: o.skipAttribute || "data-md-key",
...o.isSkipped ? { isSkipped: o.isSkipped } : {},
spacing: true,
longWord: mapLongWord(o.longWordWrap),
// v1 Pass D 不分 lang 地擠句內點號(開明式);compat 強制 kaiming
// 政策以避免功能倒退(預設 zh-Hant→quanjiao 不擠壓)。
jiya: o.punctuationSqueeze === false ? false : true,
// v1 無連續標點綁定,保持 DOM 與 v1 一致:關閉 jinze。
jinze: false,
lang: {
policy: {
"zh-Hant": "kaiming",
"zh-Hans": "kaiming",
"ja": "kaiming",
"other": "kaiming"
}
}
};
const jz = createJuzhen(mapped);
return {
apply(root) {
jz.render(root);
},
revert(root) {
jz.revert(root);
}
};
}
// src/index.ts
var DEFAULT_STYLE_INLINES = [
"STRONG",
"EM",
"A",
"B",
"I",
"U",
"S",
"MARK",
"INS",
"DEL",
"SUB",
"SUP",
"SMALL",
"ABBR",
"CITE",
"DFN",
"Q",
"SPAN",
"VAR",
"SAMP",
"TIME",
"BDO",
"BDI",
"RUBY",
"RB",
"RT",
"RP",
"WBR"
];
var DEFAULT_BLOCK_TAGS = [
"BODY",
"P",
"DIV",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"LI",
"DT",
"DD",
"BLOCKQUOTE",
"FIGURE",
"FIGCAPTION",
"TD",
"TH",
"CAPTION",
"SUMMARY",
"DETAILS",
"HEADER",
"FOOTER",
"SECTION",
"ARTICLE",
"ASIDE",
"NAV",
"MAIN",
"ADDRESS",
"PRE",
"UL",
"OL",
"DL",
"TABLE",
"TR",
"THEAD",
"TBODY",
"TFOOT",
"FORM",
"FIELDSET",
"LEGEND",
"LABEL",
"BR",
"HR"
];
var DEFAULT_SKIP_TAGS = [
"CODE",
"KBD",
"PRE",
"SAMP",
"TT",
"VAR",
"SCRIPT",
"STYLE",
"TEXTAREA",
"INPUT"
];
var PASSES = [
jiyaPass,
jiyaAdjacencyPass,
longWordPass,
spacingPass,
jinzePass,
orphanPass,
lineEdgePass,
gapTrimPass
];
function feature(v, dflt) {
return v === void 0 ? dflt : !!v;
}
function resolveLongWord(v) {
if (v === false) {
return { minLength: 6, lang: "en" };
}
if (v === void 0 || v === true || v === null) {
return { minLength: 6, lang: "en" };
}
const minLength = typeof v.minLength === "number" && v.minLength >= 2 ? v.minLength : 6;
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
return { minLength, lang };
}
function resolveOrphan(v) {
if (v && typeof v === "object") {
const chars = typeof v.chars === "number" && v.chars >= 1 ? Math.floor(v.chars) : 2;
return { chars };
}
return { chars: 2 };
}
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 || {};
return {
locale: {
default: langOpt.default || "zh-Hant",
...langOpt.style ? { style: langOpt.style } : {},
policy: langOpt.policy || {}
},
ruleset: resolveRuleset(opts.charClass),
autospaceClass: opts.autospaceClass || "",
justifyAtoms: opts.justifyAtoms === false ? false : true,
levelText: opts.level && opts.level.text || null,
scope: {
root: scope.root || null,
include: scope.include || null,
avoid: scope.avoid || null
},
features: {
spacing: feature(opts.spacing, true),
longWord: opts.longWord === false ? false : feature(opts.longWord, true),
jiya: feature(opts.jiya, true),
jinze: feature(opts.jinze, true),
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
hanging: feature(opts.hanging, false),
biaodian: feature(opts.biaodian, false),
emphasis: feature(opts.emphasis, false),
ruby: feature(opts.ruby, false)
},
longWord: resolveLongWord(opts.longWord),
orphan: resolveOrphan(opts.orphan),
// 行端模式恒為 squeeze(行端半形)。**標點懸掛 hang 已全面停用**(§6.5.2/§6.5.6):
// 經 headless 真實瀏覽器實測,純負 margin 機制不可行——句號墨色偏框左下角,安全
// 凸出量(<1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);凸出滿 1em 又會釋放
// 一字寬、把下一字拉上本行致重疊;且 ResizeObserver 重排時兩階段標記錯亂。原生
// hanging-punctuation 為正解但 Chrome 不支援、且與本庫 inline-block 原子衝突。
// 故此處**唯一關卡**鎖死為 squeeze——即使 opts.hanging 為 true 亦不啟用 hang(無逃逸);
// hang 之 CSSlineedge 程式碼保留(休眠),俟原生支援成熟再議。
lineEnd: "squeeze",
jiyaConfig: resolveJiya(opts.jiya),
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
finder: {
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
blockTags: new Set(opts.blockTags || DEFAULT_BLOCK_TAGS),
skipTags: new Set(opts.skipTags || DEFAULT_SKIP_TAGS),
pillSelector: opts.pillSelector || "code, kbd",
skipAttribute: opts.skipAttribute || null,
isSkipped: typeof opts.isSkipped === "function" ? opts.isSkipped : null
}
};
}
function resolveRoot(arg, options) {
const pick = arg !== void 0 ? arg : options.scope.root;
if (pick && typeof pick !== "string") {
return pick;
}
if (typeof pick === "string" && typeof document !== "undefined") {
return document.querySelector(pick);
}
return typeof document !== "undefined" ? document.body : null;
}
function createJuzhen(opts = {}) {
const options = normalizeOptions(opts);
function makeCtx(root) {
return { root, options, finder: new Finder(options) };
}
return {
options,
render(root) {
const el = resolveRoot(root, options);
if (!el) {
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);
if (!el) {
return;
}
revertPasses(PASSES, makeCtx(el));
el.removeAttribute("data-jz-halfwidth");
el.removeAttribute("data-jz-atoms");
}
};
}
export {
createCjkAutospace,
createJuzhen,
normalizeOptions
};