Files
cjk-autospace/dist/juzhen.mjs
T
admin 8ee9895247 feat(levelmark): 塊級分類 CSS 鉤子 data-jz-block-level+增量塊粒度契約(下游建議七/說明八)
建議七:新增 levelMark pass(standalone,order 10,恆開)——render 時把 resolved
功能級別寫回「直接承載可處理文本」之塊級元素:data-jz-block-level="paragraph|text",
revert 全數移除(I6 之 data-jz-* 清單涵蓋)。消費端段落級 CSS(首要 text-align:
justify)自此與 jinze/orphan 共用單一真相源,免另立必然漂移之選擇器白名單。

- 僅標直接承載文本之塊:純容器(body/ul/table…,直接文本僅空白)不標——否則
  body 恆判 paragraph、justify 鉤子經繼承污染全頁;此集合恰為段落級 pass 實際
  作用之塊集。avoid 子樹/pill 內部文本不計,未被處理之塊一致不標。
- 輸入/輸出分離:作者輸入 data-jz-level 只讀不寫;輸出屬性本庫絕不讀取、亦不在
  C2 OBSERVE_ATTRS 白名單(外部改動不觸發);增量路徑 revert 即清、render 即重標。
- 實現:一次 eachTextNode 走訪+短程上攀定塊(per-run 快取),O(文本);levelFor
  已記憶化。上攀止於 ctx.root,不越界外標。

說明八:README/ARCHITECTURE §3.1 明載增量粒度契約——增量須以塊為粒度(換塊
內容、增刪塊、改分類屬性皆與全量重繪一致);不支持對聚珍已切分文本節點之外科式
characterData 直改(charify 以 fragment 替換原節點,舊引用已脫離文檔)——後處理
排版器之固有限制、非缺陷,以塊為單位替換內容即可。

測試 140→145(標記/純容器不標、text 解析三路(選擇器/屬性/祖先)、avoid/
pill 不標、revert 全清含 root、C2 class 翻轉自動刷新);tsc 聲明構建通過;
headless Chrome 整管線煙測零報錯、段落屬性實際落 DOM。
2026-07-16 16:42:56 +08:00

2475 lines
70 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.isolateSelector = opts.finder.isolateSelector;
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();
this.categoryCache = /* @__PURE__ */ new WeakMap();
this.pillAncestorCache = /* @__PURE__ */ new WeakMap();
this.avoidAncestorCache = /* @__PURE__ */ new WeakMap();
}
/**
* 清除 el 及其**整棵子樹**之元素鍵快取(C1 失效原語)。持久化 Finder(跨 render 復用快取)
* 下,元素之屬性或內容變化會令其分類(category)/級別(level)/功能(feature)/祖先游走
* pillavoid ancestor)快取失效。**須清整棵子樹、非僅該元素**:level/feature/祖先游走
* 之結果依賴祖先鏈,某祖先之 data-jz-leveldata-juzhenclass 變化會令其**後代**之快取值變脏,
* 即便後代自身未動。querySelectorAll('*') 走子樹(O(子樹),native、無遞迴棧風險)。移除之元素
* 由 WeakMap 自動回收,無需顯式清。 */
invalidate(el) {
this.forget(el);
el.querySelectorAll("*").forEach((c) => this.forget(c));
}
forget(e) {
this.categoryCache.delete(e);
this.pillAncestorCache.delete(e);
this.avoidAncestorCache.delete(e);
this.levelCache.delete(e);
this.featureCache.delete(e);
}
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
pillMatches(el) {
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
}
/** 該元素是否匹配 isolate 選擇器(三分類之「隔離」:帶盒樣式之行內,如背景色
* <mark>)。邊界口徑同 pill(斷 run/斷相鄰/不可切分/兩端補隙),內部照常處理。 */
isolateMatches(el) {
return !!(el.matches && this.isolateSelector && el.matches(this.isolateSelector));
}
/** 節點是否位於 pill **內部**(僅查祖先,不含自身)。pill 為整體元素:內部對
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
* .katex 內部曾被 jiyaspacing 直接變更)。 */
insidePill(node) {
const p = node.parentElement;
return p ? this.ancestorOrSelfPill(p) : false;
}
/** el 或其祖先鏈任一為 pill(記憶化)。沿祖先鏈迭代(非遞迴,任意深度棧安全)至最近之
* 已快取/pill 命中/鏈頂,途中非 pill 元素入棧、以斷點之值一次回填——每元素恰算一次,
* 兄弟共用祖先鏈快取。insidePill 為 pillMatches 之最大來源(S3 延伸)。 */
ancestorOrSelfPill(el) {
const chain = [];
let cur = el;
let base = false;
while (cur) {
const c = this.pillAncestorCache.get(cur);
if (c !== void 0) {
base = c;
break;
}
if (this.pillMatches(cur)) {
this.pillAncestorCache.set(cur, true);
base = true;
break;
}
chain.push(cur);
cur = cur.parentElement;
}
for (const e of chain) {
this.pillAncestorCache.set(e, base);
}
return base;
}
/**
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
* 選擇器同命中時之定案規則):`avoid pill isolate block transparent opaque`
* opaque=未在 styleInlines 之未知行內標籤,嚴格白名單下當隔離式邊界)。故:同時命中
* pill 與 isolate → **pill**pill 覆蓋 isolate);帶 data-jz-skip 之 pillisolate → **avoid**
* (自身排除最高)。**邏輯文本流之遍歷原語**collectRunsjiyaAdjacencyadjacentLogicalChar
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit SPLITTABLE 白名單 ∩
* transparent(結構切分為獨立層,故更窄)。
*
* **記憶化**S3):分類含至多三次 el.matches()pillisolateavoid 選擇器),且被
* collectRunsjiyaAdjacencyjinzeorphansplit 各 pass 逐元素重算——以 categoryCache
* 記憶化砍除跨 pass 重複。安全性:單次 render 內一元素之分類**恆定**class/skip 屬性/
* nodeName 皆不因 render 變更;jz-* 包裹為新建元素、按 nodeName 分類亦恆定),且 Finder
* 隨 makeCtx 每次 render 重建 → 快取僅存活單次 render,無跨 render 失效問題(持久化屬 C1,
* 另議)。
*/
category(el) {
const cached = this.categoryCache.get(el);
if (cached) {
return cached;
}
const result = this.computeCategory(el);
this.categoryCache.set(el, result);
return result;
}
computeCategory(el) {
if (this.avoidsSelf(el)) {
return "avoid";
}
if (this.pillMatches(el)) {
return "pill";
}
if (this.isolateMatches(el)) {
return "isolate";
}
if (this.blockSet.has(el.nodeName)) {
return "block";
}
if (this.isTransparentTag(el)) {
return "transparent";
}
return "opaque";
}
/** category 之透明判定末步:styleInlines 標籤,或聚珍自身之 wrap 類 jz-*jz-hws 間隙、
* jz-jinze 禁則群組、**jz-charjz-inner 標點原子**)。前四步已排除 avoid/pill/isolate/block。
* jz-charjz-inner 必須列入:否則 adjacentLogicalChar 之 descend 遇標點原子既不進入亦不
* 終止、而**跳過**它續找更前之字(`汉字。<code>` 綁定後 pill 左鄰字應為「。」卻誤取「字」
* 補 jz-hws);jz-char 僅含標點(非 pillNeighbor),透明後 descend 回傳標點、不補隙。 */
isTransparentTag(el) {
const name = el.nodeName;
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
}
/** el 是否為邏輯文本流可透明穿越者(= category "transparent")。 */
isTransparent(el) {
return this.category(el) === "transparent";
}
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRESAMPSCRIPT…,可覆寫)
* ③ SVG 命名空間 ④ contentEditable
* ⑤ data-jz-skip 屬性(內建) ⑥ skipAttribute(消費端指定屬性名)
* ⑦ scope.avoid 選擇器
* **此為自身判定**,供遍歷原語(collectRunsjiyaAdjacencycanSplitisTransparent)於
* 下探時逐元素自判(祖先由遞迴涵蓋)。**一次性**問「某節點是否被 avoid(自身或祖先)」
* 請改用 isAvoided(單一組合入口),勿只用本方法而漏判祖先、或只用 isAvoided 而漏判自身
* (下游問題六/階段一 #2:漏一半致 avoid 對自身形同虛設)。 */
avoidsSelf(el) {
if (this.userIsSkipped && this.userIsSkipped(el)) {
return true;
}
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;
}
return false;
}
/** 某節點是否被 avoid——**自身即邊界**(元素經 avoidsSelf 之七源;非元素節點僅判
* isSkipped**或**祖先鏈上任一為 avoid 邊界。此為「是否 avoid」之**單一組合入口(安全
* 預設)**:一次性檢查用此,免除「avoidsSelf ‖ isAvoided」易漏一半之誤用。遍歷原語逐
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
isAvoided(node) {
if (node.nodeType === 1) {
return this.ancestorOrSelfAvoided(node);
}
if (this.userIsSkipped && this.userIsSkipped(node)) {
return true;
}
const p = node.parentElement;
return p ? this.ancestorOrSelfAvoided(p) : false;
}
/** el 或其祖先鏈任一為 avoid 邊界(記憶化,迭代回填,同 ancestorOrSelfPill)。 */
ancestorOrSelfAvoided(el) {
const chain = [];
let cur = el;
let base = false;
while (cur) {
const c = this.avoidAncestorCache.get(cur);
if (c !== void 0) {
base = c;
break;
}
if (this.avoidsSelf(cur)) {
this.avoidAncestorCache.set(cur, true);
base = true;
break;
}
chain.push(cur);
cur = cur.parentElement;
}
for (const e of chain) {
this.avoidAncestorCache.set(e, base);
}
return base;
}
/** 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 / pill 內部 / 非作用域)。 */
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.insidePill(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.1I7):透明行內(isTransparentstyleInlines
* wrap 類 jz-*)穿越、run 延續;blockpillavoid 中斷 runisolate 與**未知行內
* 標籤**(嚴格白名單)中斷 run 且內部自成獨立 run。回傳 text.length >= 2 之 run。
*/
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;
const recurse = () => {
let c2 = el.firstChild;
while (c2) {
visit(c2);
c2 = c2.nextSibling;
}
};
switch (this.category(el)) {
case "avoid":
case "block":
case "pill": {
flush();
return;
}
case "transparent": {
recurse();
return;
}
default: {
flush();
recurse();
flush();
return;
}
}
};
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) {
if (!this.isTransparent(n2)) {
return "blocked";
}
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.isTransparent(e)) {
return null;
}
const r = descend(e);
if (r === "blocked") {
return null;
}
if (r) {
return r;
}
n = direction === -1 ? e.previousSibling : e.nextSibling;
continue;
}
n = direction === -1 ? n.previousSibling : n.nextSibling;
}
if (!p || p.nodeType !== 1 || !this.isTransparent(p)) {
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.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;
}
function isJz(node, pass) {
if (!node || node.nodeType !== 1) {
return false;
}
const el = node;
if (!el.hasAttribute(PASS_ATTR)) {
return false;
}
return pass === void 0 || el.getAttribute(PASS_ATTR) === pass;
}
// 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 isPunctStyle(v) {
return v === "quanjiao" || v === "kaiming" || v === "banjiao";
}
function resolveStyleOverride(node, stylePolicy) {
let el = node.nodeType === 1 ? node : node.parentElement;
while (el) {
const attr = el.getAttribute ? el.getAttribute("data-jz-style") : null;
if (isPunctStyle(attr)) {
return attr;
}
if (stylePolicy) {
for (const rule of stylePolicy) {
if (rule.selector && el.matches && el.matches(rule.selector)) {
return rule.style;
}
}
}
el = el.parentElement;
}
return null;
}
function resolveStyle(node, config) {
const override = resolveStyleOverride(node, config.stylePolicy);
if (override) {
return override;
}
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 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_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 } = ctx;
let prev = null;
const squeeze = (L, R) => {
if (!finder.featureEnabledFor(L, PASS)) {
return;
}
const lClass = bdClassOf(L);
const rClass = bdClassOf(R);
const lSides = blankSides(lClass);
const rSides = blankSides(rClass);
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
if (lSides.right && !stopBeforeOpen) {
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 {
switch (finder.category(el)) {
case "avoid":
case "pill": {
prev = null;
break;
}
case "transparent": {
visit(el);
break;
}
default: {
prev = null;
visit(el);
prev = null;
break;
}
}
}
}
};
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",
"SMALL",
"INS",
"DEL",
"SUB",
"SUP"
]);
function canSplit(el, finder) {
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && finder.category(el) === "transparent";
}
function firstText(el, finder) {
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, finder)) {
return null;
}
const r = firstText(c, finder);
if (r) {
return r;
}
}
}
return null;
}
function lastText(el, finder) {
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, finder)) {
return null;
}
const r = lastText(c, finder);
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, finder) {
for (let a = textNode.parentNode; a && a.nodeType === 1; a = a.parentNode) {
if (!canSplit(a, finder)) {
return null;
}
if (a === top) {
break;
}
}
if (offset <= 0 && firstText(top, finder) === 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, finder) {
if (!canSplit(el, finder)) {
return el;
}
const text = side === "tail" ? lastText(el, finder) : firstText(el, finder);
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, finder);
if (!clone) {
return el;
}
return side === "tail" ? clone : el;
}
function splitElementAt(el, textNode, offset, pass, finder) {
if (!canSplit(el, finder)) {
return null;
}
return splitTreeAfter(el, textNode, offset, pass, finder);
}
// 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 firstMeaningfulChild(el) {
for (let c = el.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 3 && c.data.length === 0) {
continue;
}
return c;
}
return null;
}
function lastMeaningfulChild(el) {
for (let c = el.lastChild; c; c = c.previousSibling) {
if (c.nodeType === 3 && c.data.length === 0) {
continue;
}
return c;
}
return null;
}
function atEdge(node, el, side) {
return node === (side === "head" ? firstMeaningfulChild(el) : lastMeaningfulChild(el));
}
function edgeCharEl(node, side, finder) {
let n = node;
while (n && n.nodeType === 1) {
if (isCharEl(n)) {
return n;
}
if (!finder.isTransparent(n)) {
return null;
}
n = side === "head" ? firstMeaningfulChild(n) : lastMeaningfulChild(n);
}
return null;
}
function forbiddenBreak(a, b, finder) {
const head = edgeCharEl(b, "head", finder);
if (head) {
const c = bdClassOf(head);
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(head, PASS2)) {
return true;
}
}
const tail = edgeCharEl(a, "tail", finder);
if (tail) {
const c = bdClassOf(tail);
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(tail, PASS2)) {
return true;
}
}
return false;
}
function depthOf(node) {
let d = 0;
let p = node.parentNode;
while (p) {
d += 1;
p = p.parentNode;
}
return d;
}
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) {
if (isJz(parent, PASS2)) {
return;
}
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, finder);
}
if (needFirst) {
isolateBoundaryToken(el, "head", anyCjk, PASS2, finder);
}
}
}
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 { finder } = ctx;
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
const parents = /* @__PURE__ */ new Set();
ctx.root.querySelectorAll("jz-char").forEach((c) => {
if (!finder.levelAllows(c, "paragraph")) {
return;
}
let child = c;
let parent = c.parentNode;
while (parent && parent.nodeType === 1) {
parents.add(parent);
const pe = parent;
if (!finder.isTransparent(pe)) {
break;
}
if (!atEdge(child, pe, "head") && !atEdge(child, pe, "tail")) {
break;
}
child = pe;
parent = pe.parentNode;
}
});
const ordered = Array.from(parents).sort((a, b) => depthOf(b) - depthOf(a));
for (const parent of ordered) {
processParent(parent, finder, anyCjk);
}
},
revert(ctx) {
revertPass(PASS2, ctx.root);
}
};
// src/typeset/levelmark.ts
var ATTR = "data-jz-block-level";
var levelMarkPass = {
name: "levelMark",
kind: "standalone",
order: 10,
level: "text",
// 不受段落級閘限制:text 級之塊亦須標記(標記本身即分級輸出)
enabled: () => true,
// 分類鉤子非排版功能,恆開(純屬性輸出、零版面影響)
render(ctx) {
const { finder, options } = ctx;
const blockSet = options.finder.blockTags;
const nearest = /* @__PURE__ */ new Map();
const nearestBlock = (p) => {
const cached = nearest.get(p);
if (cached !== void 0) {
return cached;
}
let e = p;
let block = null;
while (e) {
if (blockSet.has(e.nodeName)) {
block = e;
break;
}
if (e === ctx.root) {
break;
}
e = e.parentElement;
}
nearest.set(p, block);
return block;
};
finder.eachTextNode(ctx.root, (t) => {
if (!/\S/.test(t.data)) {
return;
}
const p = t.parentElement;
const block = p ? nearestBlock(p) : null;
if (!block) {
return;
}
const level = finder.levelFor(block);
if (block.getAttribute(ATTR) !== level) {
block.setAttribute(ATTR, level);
}
});
},
revert(ctx) {
if (ctx.root.hasAttribute(ATTR)) {
ctx.root.removeAttribute(ATTR);
}
ctx.root.querySelectorAll("[" + ATTR + "]").forEach((el) => el.removeAttribute(ATTR));
}
};
// src/typeset/lineedge.ts
var LE = "jz-half-le";
var ALL_EDGE = [LE];
var EPS = 2;
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
var TAIL_CLASSES = /* @__PURE__ */ new Set(["bd-close"]);
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;
}
function clearEdge(root) {
for (const cls of ALL_EDGE) {
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
}
}
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 bd = bdClassOf(el);
if (!bd) {
continue;
}
const isHead = HEAD_CLASSES.has(bd);
const isTail = TAIL_CLASSES.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 });
}
} else {
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
if (!next || lowerLine(r, next)) {
decisions.push({ el });
}
}
}
for (const d of decisions) {
d.el.classList.add(LE);
}
}
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 skippableAlign(g, cache) {
const key = g.parentElement;
if (!key) {
return false;
}
const cached = cache.get(key);
if (cached !== void 0) {
return cached;
}
let result = false;
const view = g.ownerDocument ? g.ownerDocument.defaultView : null;
if (view) {
const cs = view.getComputedStyle(g);
const ltr = cs.direction !== "rtl";
result = cs.textAlign === "left" || cs.textAlign === "start" && ltr;
}
cache.set(key, result);
return result;
}
function trimGaps(root, finder, wrapSet) {
const alignCache = /* @__PURE__ */ new Map();
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing") && !skippableAlign(g, alignCache)
);
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.avoidsSelf(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,
finder
);
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;
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
const nonLeaf = /* @__PURE__ */ new Set();
if (sel) {
ctx.root.querySelectorAll(sel).forEach((b) => {
let p = b.parentElement;
while (p) {
if (blockSet.has(p.nodeName)) {
nonLeaf.add(p);
break;
}
p = p.parentElement;
}
});
}
finder.eachBlock(ctx.root, (block) => {
if (!finder.levelAllows(block, "paragraph")) {
return;
}
if (!finder.featureEnabledFor(block, PASS4)) {
return;
}
if (finder.isAvoided(block)) {
return;
}
if (nonLeaf.has(block)) {
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, boundarySelector, pillNeighbor) {
root.querySelectorAll(boundarySelector).forEach((pill) => {
if (finder.isAvoided(pill) || finder.insidePill(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);
}
const boundarySelector = [options.finder.pillSelector, options.finder.isolateSelector].filter(Boolean).join(", ");
if (boundarySelector) {
processPills(ctx.root, finder, boundarySelector, R.pillNeighbor);
}
},
revert(ctx) {
revertPass(PASS5, ctx.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 = [
"PRE",
"SAMP",
"TT",
"VAR",
"SCRIPT",
"STYLE",
"TEXTAREA",
"INPUT"
];
var PASSES = [
jiyaPass,
jiyaAdjacencyPass,
longWordPass,
spacingPass,
jinzePass,
orphanPass,
lineEdgePass,
gapTrimPass,
levelMarkPass
];
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 || {},
stylePolicy: Array.isArray(langOpt.stylePolicy) ? langOpt.stylePolicy.slice() : []
},
ruleset: resolveRuleset(opts.charClass),
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),
// feature(v,dflt) 對 v===false 即回 false,故無需 ===false 特判(與 jiyajinze 同構)。
longWord: feature(opts.longWord, true),
jiya: feature(opts.jiya, true),
jinze: feature(opts.jinze, true),
orphan: feature(opts.orphan, true),
biaodian: feature(opts.biaodian, false),
emphasis: feature(opts.emphasis, false),
ruby: feature(opts.ruby, false)
},
longWord: resolveLongWord(opts.longWord),
orphan: resolveOrphan(opts.orphan),
// 行端僅做半形擠壓(lineedge:行首開類收左、行尾閉類收右)。**標點懸掛已移除**
// (§6.5.6):純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);
// opts.hanging 仍被接受以保 API 相容,但無任何效果。
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",
// 內外隔離(§4.1 三分類):預設 <mark>(瀏覽器預設帶背景,切分有可見
// 接縫、邊界需留隙)。顯式判 undefined,使 "" 可停用。
isolateSelector: opts.isolateSelector !== void 0 ? opts.isolateSelector : "mark",
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;
}
var OBSERVE_ATTRS = [
"class",
"lang",
"id",
"contenteditable",
"data-jz-skip",
"data-jz-level",
"data-jz-style",
"data-juzhen",
"data-juzhen-off"
];
function createJuzhen(opts = {}) {
const options = normalizeOptions(opts);
const finder = new Finder(options);
function makeCtx(root) {
return { root, options, finder };
}
function renderImpl(el) {
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");
}
}
function revertImpl(el) {
revertPasses(PASSES, makeCtx(el));
el.removeAttribute("data-jz-halfwidth");
el.removeAttribute("data-jz-atoms");
finder.invalidate(el);
}
let observer = null;
let observeRoot = null;
const observeConfig = {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: OBSERVE_ATTRS.slice()
};
function pauseObserver() {
if (observer) {
observer.disconnect();
}
}
function resumeObserver() {
if (observer && observeRoot) {
observer.takeRecords();
observer.observe(observeRoot, observeConfig);
}
}
function nearestBlock(node) {
let e = node.nodeType === 1 ? node : node.parentElement;
while (e) {
if (options.finder.blockTags.has(e.nodeName)) {
return e;
}
if (e === observeRoot) {
break;
}
e = e.parentElement;
}
return observeRoot;
}
function onMutations(records) {
const rerenderBlocks = /* @__PURE__ */ new Set();
const renderNew = [];
for (const m of records) {
if (m.type === "childList") {
m.addedNodes.forEach((n) => {
if (n.nodeType === 1 && options.finder.blockTags.has(n.nodeName)) {
const np = n;
const P = np.parentElement;
const pIsBlock = !!P && options.finder.blockTags.has(P.nodeName);
const pHadLeafOrphan = !!P && Array.prototype.some.call(
P.children,
(c) => c.nodeName === "JZ-ORPHAN"
);
if (pIsBlock && pHadLeafOrphan) {
rerenderBlocks.add(P);
} else if (!pIsBlock && P) {
const b = nearestBlock(P);
if (b) {
rerenderBlocks.add(b);
}
} else {
renderNew.push(np);
}
} else if (n.nodeType === 1 || n.nodeType === 3) {
const b = nearestBlock(m.target);
if (b) {
rerenderBlocks.add(b);
}
}
});
if (m.removedNodes.length) {
const b = nearestBlock(m.target);
if (b) {
rerenderBlocks.add(b);
}
}
} else {
const b = nearestBlock(m.target);
if (b) {
rerenderBlocks.add(b);
}
}
}
if (!rerenderBlocks.size && !renderNew.length) {
return;
}
pauseObserver();
const all = Array.from(rerenderBlocks).filter((b) => b.isConnected);
const blocks = all.filter((b) => !all.some((o) => o !== b && o.contains(b)));
for (const b of blocks) {
revertImpl(b);
renderImpl(b);
}
for (const el of renderNew) {
if (!el.isConnected) {
continue;
}
if (blocks.some((b) => b.contains(el))) {
continue;
}
renderImpl(el);
}
resumeObserver();
}
return {
options,
render(root) {
const el = resolveRoot(root, options);
if (!el) {
return;
}
pauseObserver();
renderImpl(el);
resumeObserver();
},
revert(root) {
const el = resolveRoot(root, options);
if (!el) {
return;
}
pauseObserver();
revertImpl(el);
resumeObserver();
},
rerender(root) {
const el = resolveRoot(root, options);
if (!el) {
return;
}
pauseObserver();
revertImpl(el);
renderImpl(el);
resumeObserver();
},
invalidate(root) {
const el = resolveRoot(root, options);
if (el) {
finder.invalidate(el);
}
},
observe(root) {
if (typeof MutationObserver === "undefined") {
return;
}
const el = resolveRoot(root, options);
if (!el) {
return;
}
if (observer) {
observer.disconnect();
}
observeRoot = el;
observer = new MutationObserver(onMutations);
observer.observe(el, observeConfig);
},
disconnect() {
if (observer) {
observer.disconnect();
observer = null;
observeRoot = null;
}
}
};
}
export {
createJuzhen,
normalizeOptions
};