3a4fa1c932
保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。 验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
3891 lines
126 KiB
JavaScript
3891 lines
126 KiB
JavaScript
"use strict";
|
||
var Juzhen = (() => {
|
||
var __defProp = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
var __export = (target, all) => {
|
||
for (var name in all)
|
||
__defProp(target, name, { get: all[name], enumerable: true });
|
||
};
|
||
var __copyProps = (to, from, except, desc) => {
|
||
if (from && typeof from === "object" || typeof from === "function") {
|
||
for (let key of __getOwnPropNames(from))
|
||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
}
|
||
return to;
|
||
};
|
||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
|
||
// src/index.ts
|
||
var index_exports = {};
|
||
__export(index_exports, {
|
||
createJuzhen: () => createJuzhen,
|
||
normalizeOptions: () => normalizeOptions
|
||
});
|
||
|
||
// 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\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B73F}\\u{2B740}-\\u{2B81F}\\u{2B820}-\\u{2CEAF}\\u{2CEB0}-\\u{2EBEF}\\u{2EBF0}-\\u{2EE5F}\\u{2F800}-\\u{2FA1F}\\u{30000}-\\u{3134F}\\u{31350}-\\u{323AF}\\u{323B0}-\\u{3347F}";
|
||
var VARIATION_SELECTORS = "\\uFE00-\\uFE0F\\u{E0100}-\\u{E01EF}";
|
||
function codePointAtUtf16(text, offset) {
|
||
const value = text.codePointAt(offset);
|
||
return value === void 0 ? "" : String.fromCodePoint(value);
|
||
}
|
||
function previousCodePointStart(text, offset) {
|
||
if (offset <= 0) {
|
||
return 0;
|
||
}
|
||
const last = text.charCodeAt(offset - 1);
|
||
if (last >= 56320 && last <= 57343 && offset >= 2) {
|
||
const first = text.charCodeAt(offset - 2);
|
||
if (first >= 55296 && first <= 56319) {
|
||
return offset - 2;
|
||
}
|
||
}
|
||
return offset - 1;
|
||
}
|
||
function isVariationSelector(ch) {
|
||
const value = ch.codePointAt(0);
|
||
return value !== void 0 && (value >= 65024 && value <= 65039 || value >= 917760 && value <= 917999);
|
||
}
|
||
function variationSequenceAtUtf16(text, offset) {
|
||
const base = codePointAtUtf16(text, offset);
|
||
if (!base || isVariationSelector(base)) {
|
||
return base;
|
||
}
|
||
const selector = codePointAtUtf16(text, offset + base.length);
|
||
return isVariationSelector(selector) ? base + selector : base;
|
||
}
|
||
function previousVariationSequenceStart(text, offset) {
|
||
const last = previousCodePointStart(text, offset);
|
||
const lastChar = codePointAtUtf16(text, last);
|
||
if (!isVariationSelector(lastChar) || last <= 0) {
|
||
return last;
|
||
}
|
||
const base = previousCodePointStart(text, last);
|
||
return isVariationSelector(codePointAtUtf16(text, base)) ? last : base;
|
||
}
|
||
function splitVariationSequences(text) {
|
||
const out = [];
|
||
for (let offset = 0; offset < text.length; ) {
|
||
const sequence = variationSequenceAtUtf16(text, offset);
|
||
out.push(sequence);
|
||
offset += sequence.length;
|
||
}
|
||
return out;
|
||
}
|
||
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) {
|
||
const base = codePointAtUtf16(ch, 0);
|
||
if (!base) {
|
||
return null;
|
||
}
|
||
for (const sub of BD_ORDER) {
|
||
const chars = biaodian[sub];
|
||
let at = chars.indexOf(base);
|
||
while (at >= 0) {
|
||
const previous = chars.charCodeAt(at - 1);
|
||
const first = base.charCodeAt(0);
|
||
const startsInsidePair = previous >= 55296 && previous <= 56319 && first >= 56320 && first <= 57343;
|
||
if (!startsInsidePair && codePointAtUtf16(chars, at) === base) {
|
||
return "bd-" + sub;
|
||
}
|
||
at = chars.indexOf(base, at + 1);
|
||
}
|
||
}
|
||
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/core/finder.ts
|
||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||
function isHighSurrogate(ch) {
|
||
return ch.length === 1 && ch.charCodeAt(0) >= 55296 && ch.charCodeAt(0) <= 56319;
|
||
}
|
||
function isLowSurrogate(ch) {
|
||
return ch.length === 1 && ch.charCodeAt(0) >= 56320 && ch.charCodeAt(0) <= 57343;
|
||
}
|
||
function appendParts(parts, extra) {
|
||
for (const part of extra) {
|
||
const previous = parts[parts.length - 1];
|
||
if (previous?.textNode === part.textNode && previous.end === part.start) {
|
||
previous.end = part.end;
|
||
} else {
|
||
parts.push({ ...part });
|
||
}
|
||
}
|
||
}
|
||
function acceptsVariationSelector(atom) {
|
||
return !isVariationSelector(codePointAtUtf16(atom.text, 0)) && !isVariationSelector(codePointAtUtf16(atom.text, previousCodePointStart(atom.text, atom.text.length)));
|
||
}
|
||
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)/祖先游走
|
||
* (pill/avoid ancestor)快取失效。**須清整棵子樹、非僅該元素**:level/feature/祖先游走
|
||
* 之結果依賴祖先鏈,某祖先之 data-jz-level/data-juzhen/class 變化會令其**後代**之快取值變脏,
|
||
* 即便後代自身未動。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 內部曾被 jiya/spacing 直接變更)。 */
|
||
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 之 pill/isolate → **avoid**
|
||
* (自身排除最高)。**邏輯文本流之遍歷原語**(collectRuns/jiyaAdjacency/adjacentLogicalChar
|
||
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit = SPLITTABLE 白名單 ∩
|
||
* transparent(結構切分為獨立層,故更窄)。
|
||
*
|
||
* **記憶化**(S3):分類含至多三次 el.matches()(pill/isolate/avoid 選擇器),且被
|
||
* collectRuns/jiyaAdjacency/jinze/orphan/split 各 pass 逐元素重算——以 categoryCache
|
||
* 記憶化砍除跨 pass 重複。安全性:單次 render 內一元素之分類**恆定**(class/skip 屬性/
|
||
* nodeName 皆不因 render 變更;jz-* 包裹為新建元素、按 nodeName 分類亦恆定),且 Finder
|
||
* 隨實例持久化,跨 render/revert 復用;屬性或位置變化由 invalidateSubtree 清除受影響
|
||
* 子樹,不能沿用舊祖先鏈結果。
|
||
*/
|
||
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-char/jz-inner 標點原子**)。前四步已排除 avoid/pill/isolate/block。
|
||
* jz-char/jz-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 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||
* ③ SVG 命名空間 ④ contentEditable
|
||
* ⑤ data-jz-skip 屬性(內建) ⑥ skipAttribute(消費端指定屬性名)
|
||
* ⑦ scope.avoid 選擇器
|
||
* **此為自身判定**,供遍歷原語(collectRuns/jiyaAdjacency/canSplit/isTransparent)於
|
||
* 下探時逐元素自判(祖先由遞迴涵蓋)。**一次性**問「某節點是否被 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);pill/avoid 內部不可見。 */
|
||
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.isAvoided(b) || this.insidePill(b) || this.pillMatches(b)) {
|
||
continue;
|
||
}
|
||
if (this.includeSelector && !(b.closest && b.closest(this.includeSelector))) {
|
||
continue;
|
||
}
|
||
cb(b);
|
||
}
|
||
}
|
||
/**
|
||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):透明行內(isTransparent:styleInlines
|
||
* + wrap 類 jz-*)穿越、run 延續;block/pill/avoid 中斷 run;isolate 與**未知行內
|
||
* 標籤**(嚴格白名單)中斷 run 且內部自成獨立 run。預設至少兩原子;includeSingletons
|
||
* 供段落邊界查詢保留單原子 run,不改間隙處理之預設。
|
||
*/
|
||
collectRuns(block, includeSingletons = false, feature2) {
|
||
const runs = [];
|
||
let curText = "";
|
||
let curMap = [];
|
||
const flush = () => {
|
||
if (curMap.length >= (includeSingletons ? 1 : 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) || feature2 && !this.featureEnabledFor(t, feature2)) {
|
||
flush();
|
||
return;
|
||
}
|
||
const data = t.data;
|
||
for (let i = 0; i < data.length; ) {
|
||
const ch = codePointAtUtf16(data, i);
|
||
curText += ch;
|
||
const part = { textNode: t, start: i, end: i + ch.length };
|
||
const previous = curMap[curMap.length - 1];
|
||
if (previous && isHighSurrogate(previous.text) && isLowSurrogate(ch)) {
|
||
previous.text += ch;
|
||
appendParts(previous.parts, [part]);
|
||
const base = curMap[curMap.length - 2];
|
||
if (isVariationSelector(previous.text) && base && acceptsVariationSelector(base)) {
|
||
base.text += previous.text;
|
||
appendParts(base.parts, previous.parts);
|
||
curMap.pop();
|
||
}
|
||
} else if (isVariationSelector(ch) && previous && acceptsVariationSelector(previous)) {
|
||
previous.text += ch;
|
||
appendParts(previous.parts, [part]);
|
||
} else {
|
||
curMap.push({ text: ch, parts: [part] });
|
||
}
|
||
i += ch.length;
|
||
}
|
||
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": {
|
||
const boundary = feature2 && !this.featureEnabledFor(el, feature2);
|
||
if (boundary) {
|
||
flush();
|
||
}
|
||
recurse();
|
||
if (boundary) {
|
||
flush();
|
||
}
|
||
return;
|
||
}
|
||
default: {
|
||
flush();
|
||
recurse();
|
||
flush();
|
||
return;
|
||
}
|
||
}
|
||
};
|
||
let c = block.firstChild;
|
||
while (c) {
|
||
visit(c);
|
||
c = c.nextSibling;
|
||
}
|
||
flush();
|
||
return runs;
|
||
}
|
||
/** 從節點邊界或 Text offset 取相鄰碼點;只穿越透明、域內內容,scopeRoot 為硬邊界。 */
|
||
adjacentTextCodePoint(from, direction, scopeRoot, offset, feature2) {
|
||
if (scopeRoot && (from === scopeRoot || !scopeRoot.contains(from))) {
|
||
return null;
|
||
}
|
||
const pick = (tn, at) => {
|
||
if (this.isAvoided(tn) || !this.inScope(tn) || feature2 && !this.featureEnabledFor(tn, feature2)) {
|
||
return null;
|
||
}
|
||
if (direction === -1) {
|
||
if (at <= 0) {
|
||
return null;
|
||
}
|
||
return { textNode: tn, start: previousCodePointStart(tn.data, at), end: at };
|
||
}
|
||
if (at >= tn.data.length) {
|
||
return null;
|
||
}
|
||
return { textNode: tn, start: at, end: at + codePointAtUtf16(tn.data, at).length };
|
||
};
|
||
if (from.nodeType === 3 && offset !== void 0) {
|
||
const tn = from;
|
||
if (this.isAvoided(tn) || !this.inScope(tn) || feature2 && !this.featureEnabledFor(tn, feature2)) {
|
||
return null;
|
||
}
|
||
if (direction === -1 ? offset > 0 : offset < tn.data.length) {
|
||
return pick(tn, offset);
|
||
}
|
||
}
|
||
let cursor = from;
|
||
for (; ; ) {
|
||
if (cursor === scopeRoot) {
|
||
return null;
|
||
}
|
||
let next = direction === -1 ? cursor.previousSibling : cursor.nextSibling;
|
||
while (!next) {
|
||
const parent = cursor.parentElement;
|
||
if (!parent || parent === scopeRoot || !this.isTransparent(parent) || !this.inScope(parent) || feature2 && !this.featureEnabledFor(parent, feature2)) {
|
||
return null;
|
||
}
|
||
cursor = parent;
|
||
next = direction === -1 ? cursor.previousSibling : cursor.nextSibling;
|
||
}
|
||
for (; ; ) {
|
||
if (scopeRoot && !scopeRoot.contains(next)) {
|
||
return null;
|
||
}
|
||
if (next.nodeType === 3) {
|
||
const tn = next;
|
||
if (this.isAvoided(tn) || !this.inScope(tn) || feature2 && !this.featureEnabledFor(tn, feature2)) {
|
||
return null;
|
||
}
|
||
if (tn.data.length) {
|
||
return pick(tn, direction === -1 ? tn.data.length : 0);
|
||
}
|
||
} else if (next.nodeType === 1) {
|
||
const el = next;
|
||
if (!this.isTransparent(el) || !this.inScope(el) || feature2 && !this.featureEnabledFor(el, feature2)) {
|
||
return null;
|
||
}
|
||
const child = direction === -1 ? el.lastChild : el.firstChild;
|
||
if (child) {
|
||
next = child;
|
||
continue;
|
||
}
|
||
}
|
||
cursor = next;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
/** 補全透明 Text 邊界拆開的 surrogate pair;孤立碼元保留自身,不跨任何邊界拼接。 */
|
||
adjacentCodePoint(from, direction, scopeRoot, offset, feature2) {
|
||
const first = this.adjacentTextCodePoint(from, direction, scopeRoot, offset, feature2);
|
||
if (!first) {
|
||
return null;
|
||
}
|
||
const char = (part) => part.textNode.data.slice(part.start, part.end);
|
||
const firstChar = char(first);
|
||
let parts = [first];
|
||
if (direction === -1 ? isLowSurrogate(firstChar) : isHighSurrogate(firstChar)) {
|
||
const second = this.adjacentTextCodePoint(
|
||
first.textNode,
|
||
direction,
|
||
scopeRoot,
|
||
direction === -1 ? first.start : first.end,
|
||
feature2
|
||
);
|
||
if (second && (direction === -1 ? isHighSurrogate(char(second)) : isLowSurrogate(char(second)))) {
|
||
parts = direction === -1 ? [second, first] : [first, second];
|
||
}
|
||
}
|
||
return { text: parts.map(char).join(""), parts };
|
||
}
|
||
/** pill 之完整鄰字(base+至多一個 VS);after 供查單個作者空格另一側,不越 scopeRoot。 */
|
||
adjacentLogicalChar(el, direction, scopeRoot, after, feature2) {
|
||
const edge = after?.parts[direction === -1 ? 0 : after.parts.length - 1];
|
||
const first = this.adjacentCodePoint(
|
||
edge?.textNode || el,
|
||
direction,
|
||
scopeRoot,
|
||
edge ? direction === -1 ? edge.start : edge.end : void 0,
|
||
feature2
|
||
);
|
||
if (!first) {
|
||
return null;
|
||
}
|
||
let atoms = [first];
|
||
const firstChar = first.text;
|
||
if (direction === -1 && isVariationSelector(firstChar) || direction === 1 && !isVariationSelector(firstChar)) {
|
||
const edge2 = first.parts[direction === -1 ? 0 : first.parts.length - 1];
|
||
const second = this.adjacentCodePoint(
|
||
edge2.textNode,
|
||
direction,
|
||
scopeRoot,
|
||
direction === -1 ? edge2.start : edge2.end,
|
||
feature2
|
||
);
|
||
if (second && (direction === -1 ? !isVariationSelector(second.text) : isVariationSelector(second.text))) {
|
||
atoms = direction === -1 ? [second, first] : [first, second];
|
||
}
|
||
}
|
||
const ch = atoms.map((atom) => atom.text).join("");
|
||
const parts = [];
|
||
for (const atom of atoms) {
|
||
appendParts(parts, atom.parts);
|
||
}
|
||
return { textNode: parts[0].textNode, offset: parts[0].start, ch, parts };
|
||
}
|
||
/**
|
||
* 節點之功能級別(§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/dom.ts
|
||
var KIND_ATTR = "data-jz-kind";
|
||
var PASS_ATTR = "data-jz";
|
||
var WRAP_TAGS = /* @__PURE__ */ new Map([
|
||
["spacing", /* @__PURE__ */ new Set(["JZ-HWS"])],
|
||
["jiya", /* @__PURE__ */ new Set(["JZ-CHAR", "JZ-INNER"])],
|
||
["jinze", /* @__PURE__ */ new Set(["JZ-JINZE"])],
|
||
["longWord", /* @__PURE__ */ new Set(["SPAN"])],
|
||
["orphan", /* @__PURE__ */ new Set(["JZ-ORPHAN"])]
|
||
]);
|
||
var SPLITOFF_TAGS = /* @__PURE__ */ new Set([
|
||
"STRONG",
|
||
"EM",
|
||
"B",
|
||
"I",
|
||
"U",
|
||
"S",
|
||
"SPAN",
|
||
"SMALL",
|
||
"INS",
|
||
"DEL",
|
||
"SUB",
|
||
"SUP"
|
||
]);
|
||
var ATTRIBUTE_WRITERS = /* @__PURE__ */ new WeakMap();
|
||
var OWNER_ATTRIBUTES = /* @__PURE__ */ new WeakMap();
|
||
var UNWRAPPED_PARENTS = /* @__PURE__ */ new WeakMap();
|
||
var SPLITOFF_SOURCES = /* @__PURE__ */ new WeakMap();
|
||
function registerSplitoffSource(clone, source, pass, ownedElements) {
|
||
if (!ownedElements.has(clone) || !SPLITOFF_TAGS.has(clone.nodeName) || clone.nodeName !== source.nodeName) {
|
||
return;
|
||
}
|
||
const earlier = SPLITOFF_SOURCES.get(source);
|
||
const inherited = earlier && source.getAttribute(PASS_ATTR) === earlier.pass && source.getAttribute(KIND_ATTR) === "splitoff" ? earlier : null;
|
||
SPLITOFF_SOURCES.set(clone, {
|
||
source,
|
||
pass,
|
||
originalPass: inherited ? inherited.originalPass : clone.getAttribute(PASS_ATTR),
|
||
originalKind: inherited ? inherited.originalKind : clone.getAttribute(KIND_ATTR)
|
||
});
|
||
}
|
||
function sameSplitAttributes(a, b) {
|
||
const attrs = (el) => el.getAttributeNames().filter((name) => name !== PASS_ATTR && name !== KIND_ATTR);
|
||
const names = attrs(a);
|
||
return names.length === attrs(b).length && names.every((name) => a.getAttribute(name) === b.getAttribute(name));
|
||
}
|
||
function splitOrigin(node) {
|
||
let current = resolveUnwrappedParent(node);
|
||
const visited = /* @__PURE__ */ new Set();
|
||
while (current) {
|
||
if (visited.has(current)) {
|
||
return null;
|
||
}
|
||
visited.add(current);
|
||
if (current.nodeType !== 1) {
|
||
return current;
|
||
}
|
||
const el = current;
|
||
const record = SPLITOFF_SOURCES.get(el);
|
||
if (!record || el.getAttribute(PASS_ATTR) !== record.pass || el.getAttribute(KIND_ATTR) !== "splitoff") {
|
||
return current;
|
||
}
|
||
current = resolveUnwrappedParent(record.source);
|
||
}
|
||
return null;
|
||
}
|
||
function recordUnwrappedParent(el, parent, ownedElements) {
|
||
const pass = el.getAttribute(PASS_ATTR);
|
||
if (ownedElements.has(el) && pass && knownProduct(pass, el.getAttribute(KIND_ATTR), el) && !el.parentNode && !el.hasChildNodes()) {
|
||
UNWRAPPED_PARENTS.set(el, parent);
|
||
}
|
||
}
|
||
function resolveUnwrappedParent(parent) {
|
||
let current = parent;
|
||
const visited = /* @__PURE__ */ new Set();
|
||
for (; ; ) {
|
||
if (visited.has(current)) {
|
||
return null;
|
||
}
|
||
visited.add(current);
|
||
const next = UNWRAPPED_PARENTS.get(current);
|
||
if (!next) {
|
||
return current;
|
||
}
|
||
if (current.parentNode || current.hasChildNodes()) {
|
||
UNWRAPPED_PARENTS.delete(current);
|
||
return current;
|
||
}
|
||
current = next;
|
||
}
|
||
}
|
||
function trackOwnedAttribute(owner, name, el) {
|
||
let attrs = OWNER_ATTRIBUTES.get(owner);
|
||
if (!attrs) {
|
||
attrs = /* @__PURE__ */ new Map();
|
||
OWNER_ATTRIBUTES.set(owner, attrs);
|
||
}
|
||
let elements = attrs.get(name);
|
||
if (!elements) {
|
||
elements = /* @__PURE__ */ new Set();
|
||
attrs.set(name, elements);
|
||
}
|
||
elements.add(el);
|
||
}
|
||
function untrackOwnedAttribute(owner, name, el) {
|
||
const attrs = OWNER_ATTRIBUTES.get(owner);
|
||
const elements = attrs?.get(name);
|
||
if (!elements) {
|
||
return;
|
||
}
|
||
elements.delete(el);
|
||
if (elements.size === 0) {
|
||
attrs.delete(name);
|
||
}
|
||
}
|
||
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(ownedElements, tag, pass, kind, opts = {}) {
|
||
const el = doc().createElement(tag);
|
||
ownedElements.add(el);
|
||
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 setOwnedAttribute(el, name, value, owner) {
|
||
let attrs = ATTRIBUTE_WRITERS.get(el);
|
||
if (!attrs) {
|
||
attrs = /* @__PURE__ */ new Map();
|
||
ATTRIBUTE_WRITERS.set(el, attrs);
|
||
}
|
||
let state = attrs.get(name);
|
||
if (!state) {
|
||
state = { original: el.getAttribute(name), writers: [] };
|
||
attrs.set(name, state);
|
||
} else {
|
||
const top = state.writers[state.writers.length - 1];
|
||
if (top && el.getAttribute(name) !== top.value) {
|
||
for (const writer of state.writers) {
|
||
untrackOwnedAttribute(writer.owner, name, el);
|
||
}
|
||
state = { original: el.getAttribute(name), writers: [] };
|
||
attrs.set(name, state);
|
||
}
|
||
}
|
||
state.writers = state.writers.filter((writer) => writer.owner !== owner);
|
||
state.writers.push({ owner, value });
|
||
trackOwnedAttribute(owner, name, el);
|
||
el.setAttribute(name, value);
|
||
}
|
||
function revertOwnedAttribute(el, name, owner) {
|
||
const attrs = ATTRIBUTE_WRITERS.get(el);
|
||
const state = attrs?.get(name);
|
||
if (!state) {
|
||
untrackOwnedAttribute(owner, name, el);
|
||
return;
|
||
}
|
||
const index = state.writers.findIndex((writer) => writer.owner === owner);
|
||
if (index < 0) {
|
||
untrackOwnedAttribute(owner, name, el);
|
||
return;
|
||
}
|
||
const wasTop = index === state.writers.length - 1;
|
||
const top = state.writers[state.writers.length - 1];
|
||
const externallyChanged = el.getAttribute(name) !== top.value;
|
||
state.writers.splice(index, 1);
|
||
untrackOwnedAttribute(owner, name, el);
|
||
if (!externallyChanged && wasTop) {
|
||
const next = state.writers[state.writers.length - 1];
|
||
const restored = next ? next.value : state.original;
|
||
if (restored === null) {
|
||
el.removeAttribute(name);
|
||
} else {
|
||
el.setAttribute(name, restored);
|
||
}
|
||
}
|
||
if (state.writers.length === 0) {
|
||
attrs.delete(name);
|
||
}
|
||
}
|
||
function revertOwnedAttributes(root, name, owner) {
|
||
const elements = OWNER_ATTRIBUTES.get(owner)?.get(name);
|
||
if (!elements) {
|
||
return;
|
||
}
|
||
for (const el of Array.from(elements)) {
|
||
if (el === root || root.contains(el)) {
|
||
revertOwnedAttribute(el, name, owner);
|
||
}
|
||
}
|
||
}
|
||
function knownProduct(pass, kind, el) {
|
||
if (kind === "wrap") {
|
||
return !!WRAP_TAGS.get(pass)?.has(el.nodeName);
|
||
}
|
||
if (kind === "splitoff") {
|
||
return (pass === "jinze" || pass === "orphan") && SPLITOFF_TAGS.has(el.nodeName);
|
||
}
|
||
return false;
|
||
}
|
||
function revertPass(pass, root, ownedElements) {
|
||
const nodes = Array.from(
|
||
root.querySelectorAll("[" + PASS_ATTR + '="' + pass + '"]')
|
||
).filter((el) => ownedElements.has(el));
|
||
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 (!knownProduct(pass, kind, el)) {
|
||
continue;
|
||
}
|
||
if (kind === "splitoff") {
|
||
continue;
|
||
}
|
||
touched.add(parent);
|
||
while (el.firstChild) {
|
||
parent.insertBefore(el.firstChild, el);
|
||
}
|
||
parent.removeChild(el);
|
||
recordUnwrappedParent(el, parent, ownedElements);
|
||
ownedElements.delete(el);
|
||
}
|
||
const splitoffs = Array.from(
|
||
root.querySelectorAll(
|
||
"[" + PASS_ATTR + '="' + pass + '"][' + KIND_ATTR + '="splitoff"]'
|
||
)
|
||
).filter((el) => ownedElements.has(el) && knownProduct(pass, "splitoff", el));
|
||
for (const so of splitoffs) {
|
||
const parent = so.parentNode;
|
||
if (!parent) {
|
||
continue;
|
||
}
|
||
touched.add(parent);
|
||
const record = SPLITOFF_SOURCES.get(so);
|
||
if (!record || record.pass !== pass) {
|
||
continue;
|
||
}
|
||
const source = resolveUnwrappedParent(record.source);
|
||
const prev = so.previousSibling;
|
||
const origin = source ? splitOrigin(source) : null;
|
||
if (origin && prev?.nodeType === 1 && splitOrigin(prev) === origin && prev.nodeName === so.nodeName && sameSplitAttributes(so, prev)) {
|
||
while (so.firstChild) {
|
||
prev.appendChild(so.firstChild);
|
||
}
|
||
parent.removeChild(so);
|
||
recordUnwrappedParent(so, prev, ownedElements);
|
||
} else {
|
||
for (const [name, value] of [
|
||
[PASS_ATTR, record.originalPass],
|
||
[KIND_ATTR, record.originalKind]
|
||
]) {
|
||
if (value === null) {
|
||
so.removeAttribute(name);
|
||
} else {
|
||
so.setAttribute(name, value);
|
||
}
|
||
}
|
||
}
|
||
SPLITOFF_SOURCES.delete(so);
|
||
ownedElements.delete(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/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) {
|
||
for (const pass of passes) {
|
||
pass.prepare?.(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, ctx.ownedElements);
|
||
}
|
||
}
|
||
function applyRequests(node, requests, ownedElements) {
|
||
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)));
|
||
}
|
||
const built = req.build(data.slice(req.start, req.end));
|
||
ownedElements.add(built);
|
||
frag.appendChild(built);
|
||
cursor = req.end;
|
||
}
|
||
if (cursor === 0) {
|
||
return;
|
||
}
|
||
if (cursor < data.length) {
|
||
frag.appendChild(ownerDoc.createTextNode(data.slice(cursor)));
|
||
}
|
||
parent.replaceChild(frag, node);
|
||
}
|
||
|
||
// src/core/cross-atom.ts
|
||
var MOVES = /* @__PURE__ */ new WeakMap();
|
||
function isolate(part) {
|
||
let text = part.textNode;
|
||
if (part.start > 0) {
|
||
text = text.splitText(part.start);
|
||
}
|
||
if (text.length > part.end - part.start) {
|
||
text.splitText(part.end - part.start);
|
||
}
|
||
return text;
|
||
}
|
||
function insertionPoint(part) {
|
||
if (part.anchor.parentNode === part.parent) {
|
||
return part.anchor;
|
||
}
|
||
if (part.next?.parentNode === part.parent) {
|
||
return part.next;
|
||
}
|
||
if (part.next !== null) {
|
||
return void 0;
|
||
}
|
||
if (part.previous?.parentNode === part.parent) {
|
||
return part.previous === part.parent.lastChild ? null : void 0;
|
||
}
|
||
if (part.previous === null && part.next === null && part.anchor.parentNode !== null && part.anchor.parentNode !== part.parent && !part.parent.hasChildNodes()) {
|
||
return null;
|
||
}
|
||
return void 0;
|
||
}
|
||
function restorePart(part, inner) {
|
||
if (part.text.parentNode === part.parent) {
|
||
return true;
|
||
}
|
||
if (part.text.parentNode !== inner) {
|
||
return false;
|
||
}
|
||
const before = insertionPoint(part);
|
||
if (before === void 0) {
|
||
return false;
|
||
}
|
||
part.parent.insertBefore(part.text, before);
|
||
return true;
|
||
}
|
||
function wrapCrossAtom(span, outer, inner, pass, ctx) {
|
||
const first = isolate(span.parts[0]);
|
||
const parent = first.parentNode;
|
||
if (!parent) {
|
||
return;
|
||
}
|
||
const move = {
|
||
pass,
|
||
outer,
|
||
inner,
|
||
parent,
|
||
previous: first.previousSibling,
|
||
next: first.nextSibling,
|
||
root: ctx.root,
|
||
ancestors: [],
|
||
parts: []
|
||
};
|
||
for (let p = ctx.root.parentElement; p; p = p.parentElement) {
|
||
move.ancestors.push(p);
|
||
}
|
||
outer.appendChild(inner);
|
||
parent.insertBefore(outer, first);
|
||
inner.appendChild(first);
|
||
try {
|
||
for (const source of span.parts.slice(1)) {
|
||
const text = isolate(source);
|
||
const sourceParent = text.parentNode;
|
||
if (!sourceParent) {
|
||
throw new Error("juzhen: \u8DE8\u7BC0\u9EDE\u6A19\u9EDE\u4F86\u6E90\u5DF2\u812B\u96E2\u3002");
|
||
}
|
||
const anchor = text.ownerDocument.createComment("jz-" + pass + "-atom-anchor");
|
||
const innerAnchor = text.ownerDocument.createComment("jz-" + pass + "-atom-part");
|
||
const part = { text, anchor, innerAnchor, parent: sourceParent, previous: text.previousSibling, next: text.nextSibling };
|
||
move.parts.push(part);
|
||
sourceParent.insertBefore(anchor, text);
|
||
inner.append(innerAnchor, text);
|
||
}
|
||
let moves = MOVES.get(ctx.ownerToken);
|
||
if (!moves) {
|
||
moves = /* @__PURE__ */ new Set();
|
||
MOVES.set(ctx.ownerToken, moves);
|
||
}
|
||
moves.add(move);
|
||
} catch (error) {
|
||
for (const part of move.parts) {
|
||
restorePart(part, inner);
|
||
part.anchor.remove();
|
||
part.innerAnchor.remove();
|
||
}
|
||
while (inner.firstChild) {
|
||
parent.insertBefore(inner.firstChild, outer);
|
||
}
|
||
outer.remove();
|
||
ctx.ownedElements.delete(inner);
|
||
ctx.ownedElements.delete(outer);
|
||
throw error;
|
||
}
|
||
}
|
||
function ownedWrap(el, tag, move, ctx) {
|
||
return ctx.ownedElements.has(el) && el.nodeName === tag && el.getAttribute("data-jz") === move.pass && el.getAttribute("data-jz-kind") === "wrap";
|
||
}
|
||
function placeOuter(move) {
|
||
if (move.outer.parentNode === move.parent) {
|
||
return true;
|
||
}
|
||
let before = null;
|
||
if (move.next?.parentNode === move.parent) {
|
||
before = move.next;
|
||
} else if (move.next !== null) {
|
||
return false;
|
||
} else if (move.previous?.parentNode === move.parent) {
|
||
if (move.previous !== move.parent.lastChild) {
|
||
return false;
|
||
}
|
||
} else if (move.parent.hasChildNodes()) {
|
||
return false;
|
||
}
|
||
move.parent.insertBefore(move.outer, before);
|
||
return true;
|
||
}
|
||
function revertCrossAtoms(pass, ctx) {
|
||
const touched = /* @__PURE__ */ new Set();
|
||
const moves = MOVES.get(ctx.ownerToken);
|
||
if (!moves) {
|
||
return touched;
|
||
}
|
||
const within = (node) => node === ctx.root || ctx.root.contains(node);
|
||
for (const move of Array.from(moves)) {
|
||
if (move.pass !== pass) {
|
||
continue;
|
||
}
|
||
if (!(within(move.root) || within(move.outer) || within(move.parent) || move.ancestors.includes(ctx.root) || move.parts.some((part) => within(part.parent) || within(part.anchor)))) {
|
||
continue;
|
||
}
|
||
const parent = resolveUnwrappedParent(move.parent);
|
||
const sources = move.parts.map((part) => resolveUnwrappedParent(part.parent));
|
||
if (!parent || sources.some((source) => source === null)) {
|
||
for (const part of move.parts) {
|
||
part.anchor.remove();
|
||
part.innerAnchor.remove();
|
||
}
|
||
moves.delete(move);
|
||
continue;
|
||
}
|
||
move.parent = parent;
|
||
move.parts.forEach((part, index) => {
|
||
part.parent = sources[index];
|
||
});
|
||
const live = (node) => move.root.contains(node) || node.isConnected;
|
||
const validCarrier = ownedWrap(move.outer, "JZ-CHAR", move, ctx) && ownedWrap(move.inner, "JZ-INNER", move, ctx) && move.inner.parentNode === move.outer && live(move.outer);
|
||
if (!validCarrier) {
|
||
for (const part of move.parts) {
|
||
part.anchor.remove();
|
||
part.innerAnchor.remove();
|
||
}
|
||
ctx.ownedElements.delete(move.inner);
|
||
if (!live(move.outer)) {
|
||
ctx.ownedElements.delete(move.outer);
|
||
}
|
||
moves.delete(move);
|
||
continue;
|
||
}
|
||
if (validCarrier) {
|
||
for (const part of move.parts) {
|
||
if (live(part.parent) && restorePart(part, move.inner)) {
|
||
touched.add(part.parent);
|
||
} else if (part.text.parentNode === move.inner) {
|
||
part.text.remove();
|
||
}
|
||
}
|
||
}
|
||
for (const part of move.parts) {
|
||
part.anchor.remove();
|
||
part.innerAnchor.remove();
|
||
}
|
||
if (ownedWrap(move.outer, "JZ-CHAR", move, ctx) && placeOuter(move)) {
|
||
if (move.inner.parentNode === move.outer && ownedWrap(move.inner, "JZ-INNER", move, ctx)) {
|
||
while (move.inner.firstChild) {
|
||
move.outer.insertBefore(move.inner.firstChild, move.inner);
|
||
}
|
||
move.inner.remove();
|
||
recordUnwrappedParent(move.inner, move.outer, ctx.ownedElements);
|
||
ctx.ownedElements.delete(move.inner);
|
||
}
|
||
while (move.outer.firstChild) {
|
||
move.parent.insertBefore(move.outer.firstChild, move.outer);
|
||
}
|
||
move.outer.remove();
|
||
recordUnwrappedParent(move.outer, move.parent, ctx.ownedElements);
|
||
ctx.ownedElements.delete(move.outer);
|
||
touched.add(move.parent);
|
||
}
|
||
moves.delete(move);
|
||
}
|
||
if (!moves.size) {
|
||
MOVES.delete(ctx.ownerToken);
|
||
}
|
||
return touched;
|
||
}
|
||
|
||
// src/core/locale.ts
|
||
var DEFAULT_POLICY = {
|
||
"zh-Hant": "quanjiao",
|
||
"zh-Hans": "kaiming",
|
||
"ja": "quanjiao",
|
||
"other": "quanjiao"
|
||
};
|
||
function effectiveLang(node, fallback) {
|
||
let p = node.nodeType === 1 ? node : node.parentNode;
|
||
while (p && p.nodeType === 1) {
|
||
const el = p;
|
||
const v = el.getAttribute("lang");
|
||
if (v !== null) {
|
||
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) {
|
||
const subtags = l.split("-");
|
||
let at = 1;
|
||
for (let count = 0; count < 3 && /^[a-z]{3}$/.test(subtags[at] || ""); count++) {
|
||
at++;
|
||
}
|
||
const script = /^[a-z]{4}$/.test(subtags[at] || "") ? subtags[at++] : void 0;
|
||
if (script === "hant") {
|
||
return "zh-Hant";
|
||
}
|
||
if (script === "hans") {
|
||
return "zh-Hans";
|
||
}
|
||
if (["tw", "hk", "mo"].includes(subtags[at] || "")) {
|
||
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/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,
|
||
prepare(ctx) {
|
||
const { finder, options } = ctx;
|
||
const roots = [ctx.root];
|
||
const selector = Array.from(options.finder.blockTags).map((tag) => tag.toLowerCase()).join(",");
|
||
if (selector) {
|
||
roots.push(...Array.from(ctx.root.querySelectorAll(selector)));
|
||
}
|
||
for (const root of roots) {
|
||
if (finder.isAvoided(root) || finder.insidePill(root) || finder.pillMatches(root)) {
|
||
continue;
|
||
}
|
||
for (const run of finder.collectRuns(root, true, PASS)) {
|
||
for (let i = run.map.length - 1; i >= 0; i -= 1) {
|
||
const atom = run.map[i];
|
||
if (atom.parts.length < 2 || atom.parts.some((part) => inJzChar(part.textNode) || !finder.featureEnabledFor(part.textNode, PASS))) {
|
||
continue;
|
||
}
|
||
const first = atom.parts[0].textNode;
|
||
const bdClass = classifyBiaodian(atom.text, options.ruleset.biaodian);
|
||
if (!bdClass) {
|
||
continue;
|
||
}
|
||
const style = resolveStyle(first, options.locale);
|
||
const lang = classify(effectiveLang(first, options.locale.default));
|
||
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
||
const bias = inkBias(codePointAtUtf16(atom.text, 0), lang, options.jiyaConfig.bias);
|
||
if (bias) {
|
||
classes.push("jz-ink-" + bias);
|
||
}
|
||
const outer = createJz(ctx.ownedElements, "jz-char", PASS, "wrap", { classes });
|
||
const inner = createJz(ctx.ownedElements, "jz-inner", PASS, "wrap");
|
||
wrapCrossAtom(atom, outer, inner, PASS, ctx);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
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; ) {
|
||
const ch = variationSequenceAtUtf16(data, i);
|
||
const bdClass = classifyBiaodian(ch, biaodian);
|
||
if (!bdClass) {
|
||
i += ch.length;
|
||
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(codePointAtUtf16(ch, 0), langClass, biasOverrides);
|
||
if (bias) {
|
||
classes.push("jz-ink-" + bias);
|
||
}
|
||
out.push({
|
||
start: i,
|
||
end: i + ch.length,
|
||
pass: PASS,
|
||
build: (text) => {
|
||
const outer = createJz(
|
||
ctx.ownedElements,
|
||
"jz-char",
|
||
PASS,
|
||
"wrap",
|
||
{ classes }
|
||
);
|
||
const inner = createJz(
|
||
ctx.ownedElements,
|
||
"jz-inner",
|
||
PASS,
|
||
"wrap",
|
||
{ text }
|
||
);
|
||
outer.appendChild(inner);
|
||
return outer;
|
||
}
|
||
});
|
||
i += ch.length;
|
||
}
|
||
return out;
|
||
},
|
||
revert(ctx) {
|
||
const touched = revertCrossAtoms(PASS, ctx);
|
||
revertPass(PASS, ctx.root, ctx.ownedElements);
|
||
for (const parent of touched) {
|
||
parent.normalize?.();
|
||
}
|
||
}
|
||
};
|
||
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) || !finder.featureEnabledFor(R, 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 (finder.isAvoided(c) || !finder.inScope(c) || !finder.featureEnabledFor(c, PASS) || c.data.length > 0) {
|
||
prev = null;
|
||
}
|
||
continue;
|
||
}
|
||
if (c.nodeType !== 1) {
|
||
continue;
|
||
}
|
||
const el = c;
|
||
const nm = el.nodeName;
|
||
if (nm === "JZ-CHAR") {
|
||
if (!ctx.ownedElements.has(el) || !finder.featureEnabledFor(el, PASS)) {
|
||
prev = null;
|
||
continue;
|
||
}
|
||
if (prev) {
|
||
squeeze(prev, el);
|
||
}
|
||
prev = el;
|
||
} else {
|
||
switch (finder.category(el)) {
|
||
case "avoid":
|
||
case "pill": {
|
||
prev = null;
|
||
break;
|
||
}
|
||
case "transparent": {
|
||
const boundary = !finder.featureEnabledFor(el, PASS);
|
||
if (boundary) {
|
||
prev = null;
|
||
}
|
||
visit(el);
|
||
if (boundary) {
|
||
prev = null;
|
||
}
|
||
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) {
|
||
const first = variationSequenceAtUtf16(d, 0);
|
||
if (anyCjk.test(first)) {
|
||
return first.length;
|
||
}
|
||
let e = first.length;
|
||
while (e < d.length) {
|
||
const ch = variationSequenceAtUtf16(d, e);
|
||
if (!isTokenChar(ch, anyCjk)) {
|
||
break;
|
||
}
|
||
e += ch.length;
|
||
}
|
||
return e;
|
||
}
|
||
function trailingTokenStart(d, anyCjk) {
|
||
let s = previousVariationSequenceStart(d, d.length);
|
||
if (anyCjk.test(variationSequenceAtUtf16(d, s))) {
|
||
return s;
|
||
}
|
||
while (s > 0) {
|
||
const previous = previousVariationSequenceStart(d, s);
|
||
if (!isTokenChar(variationSequenceAtUtf16(d, previous), anyCjk)) {
|
||
break;
|
||
}
|
||
s = previous;
|
||
}
|
||
return s;
|
||
}
|
||
function splitTreeAfter(top, textNode, offset, pass, finder, ownedElements) {
|
||
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");
|
||
ownedElements.add(clone);
|
||
registerSplitoffSource(clone, anc, pass, ownedElements);
|
||
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, ownedElements) {
|
||
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, ownedElements);
|
||
if (!clone) {
|
||
return el;
|
||
}
|
||
return side === "tail" ? clone : el;
|
||
}
|
||
function splitElementAt(el, textNode, offset, pass, finder, ownedElements) {
|
||
if (!canSplit(el, finder)) {
|
||
return null;
|
||
}
|
||
return splitTreeAfter(el, textNode, offset, pass, finder, ownedElements);
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
if (c.nodeType !== 1 && c.nodeType !== 3) {
|
||
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;
|
||
}
|
||
if (c.nodeType !== 1 && c.nodeType !== 3) {
|
||
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 bindingEnabled(node, finder, side) {
|
||
if (node.nodeType === 1 || node.nodeType === 3) {
|
||
if (!finder.inScope(node)) {
|
||
return false;
|
||
}
|
||
}
|
||
if (finder.isAvoided(node)) {
|
||
return false;
|
||
}
|
||
if (node.nodeType === 3) {
|
||
return finder.levelAllows(node, "paragraph") && finder.featureEnabledFor(node, PASS2);
|
||
}
|
||
if (node.nodeType !== 1) {
|
||
return true;
|
||
}
|
||
const el = node;
|
||
const category = finder.category(el);
|
||
if (category === "block") {
|
||
return false;
|
||
}
|
||
if (category === "pill" || side && category !== "transparent") {
|
||
return finder.levelAllows(el, "paragraph") && finder.featureEnabledFor(el, PASS2);
|
||
}
|
||
if (side) {
|
||
const child = side === "head" ? firstMeaningfulChild(el) : lastMeaningfulChild(el);
|
||
return child ? bindingEnabled(child, finder, side) : true;
|
||
}
|
||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 3 && c.data.length === 0) {
|
||
continue;
|
||
}
|
||
if (!bindingEnabled(c, finder)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
function atomicBoundaries(parent, finder) {
|
||
const boundaries = /* @__PURE__ */ new Map();
|
||
const directChildren = /* @__PURE__ */ new Map();
|
||
const directChild = (node) => {
|
||
const chain = [];
|
||
let current = node;
|
||
let result = null;
|
||
while (current && current !== parent) {
|
||
if (directChildren.has(current)) {
|
||
result = directChildren.get(current);
|
||
break;
|
||
}
|
||
chain.push(current);
|
||
if (current.parentNode === parent) {
|
||
result = current;
|
||
break;
|
||
}
|
||
current = current.parentNode;
|
||
}
|
||
for (const part of chain) {
|
||
directChildren.set(part, result);
|
||
}
|
||
return result;
|
||
};
|
||
for (const run of finder.collectRuns(parent, true)) {
|
||
for (const atom of run.map) {
|
||
let previous = null;
|
||
for (const part of atom.parts) {
|
||
const current = directChild(part.textNode);
|
||
if (previous && current && previous !== current) {
|
||
let next = boundaries.get(previous);
|
||
if (!next) {
|
||
next = /* @__PURE__ */ new Set();
|
||
boundaries.set(previous, next);
|
||
}
|
||
next.add(current);
|
||
}
|
||
previous = current;
|
||
}
|
||
}
|
||
}
|
||
return boundaries;
|
||
}
|
||
function forbiddenBreak(a, b, finder, atoms, wholeUnits = false) {
|
||
if (!bindingEnabled(a, finder, wholeUnits ? void 0 : "tail") || !bindingEnabled(b, finder, wholeUnits ? void 0 : "head")) {
|
||
return false;
|
||
}
|
||
if (atoms.get(a)?.has(b)) {
|
||
return true;
|
||
}
|
||
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) {
|
||
const first = variationSequenceAtUtf16(d, 0);
|
||
if (anyCjk.test(first)) {
|
||
return first.length;
|
||
}
|
||
let e = first.length;
|
||
while (e < d.length) {
|
||
const ch = variationSequenceAtUtf16(d, e);
|
||
if (!isTokenChar2(ch, anyCjk)) {
|
||
break;
|
||
}
|
||
e += ch.length;
|
||
}
|
||
return e;
|
||
}
|
||
function trailingTokenStart2(d, anyCjk) {
|
||
let s = previousVariationSequenceStart(d, d.length);
|
||
if (anyCjk.test(variationSequenceAtUtf16(d, s))) {
|
||
return s;
|
||
}
|
||
while (s > 0) {
|
||
const previous = previousVariationSequenceStart(d, s);
|
||
if (!isTokenChar2(variationSequenceAtUtf16(d, previous), anyCjk)) {
|
||
break;
|
||
}
|
||
s = previous;
|
||
}
|
||
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, ownedElements) {
|
||
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 = [];
|
||
const atoms = atomicBoundaries(parent, finder);
|
||
for (let i2 = 0; i2 < n - 1; i2 += 1) {
|
||
forbidden.push(forbiddenBreak(units[i2], units[i2 + 1], finder, atoms));
|
||
}
|
||
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, ownedElements);
|
||
}
|
||
if (needFirst) {
|
||
isolateBoundaryToken(el, "head", anyCjk, PASS2, finder, ownedElements);
|
||
}
|
||
}
|
||
}
|
||
const kids = [];
|
||
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 1 || c.nodeType === 3 && c.data.length > 0) {
|
||
kids.push(c);
|
||
}
|
||
}
|
||
const splitAtoms = atomicBoundaries(parent, finder);
|
||
let i = 0;
|
||
while (i < kids.length) {
|
||
const run = [kids[i]];
|
||
while (i + 1 < kids.length && forbiddenBreak(kids[i], kids[i + 1], finder, splitAtoms, true)) {
|
||
run.push(kids[i + 1]);
|
||
i += 1;
|
||
}
|
||
if (run.length >= 2) {
|
||
const group = createJz(ownedElements, "jz-jinze", PASS2, "wrap");
|
||
parent.insertBefore(group, run[0]);
|
||
const last = run[run.length - 1];
|
||
let node = run[0];
|
||
while (node) {
|
||
const next = node.nextSibling;
|
||
group.appendChild(node);
|
||
if (node === last) {
|
||
break;
|
||
}
|
||
node = next;
|
||
}
|
||
}
|
||
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 + "]", "u");
|
||
const parents = /* @__PURE__ */ new Set();
|
||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||
if (!ctx.ownedElements.has(c)) {
|
||
return;
|
||
}
|
||
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 (pe === ctx.root) {
|
||
break;
|
||
}
|
||
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, ctx.ownedElements);
|
||
}
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS2, ctx.root, ctx.ownedElements);
|
||
}
|
||
};
|
||
|
||
// 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);
|
||
setOwnedAttribute(block, ATTR, level, ctx.ownerToken);
|
||
});
|
||
},
|
||
revert(ctx) {
|
||
revertOwnedAttributes(ctx.root, ATTR, ctx.ownerToken);
|
||
}
|
||
};
|
||
|
||
// 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, owner, key, run) {
|
||
run();
|
||
let owners = observers.get(root);
|
||
if (!owners) {
|
||
owners = /* @__PURE__ */ new Map();
|
||
observers.set(root, owners);
|
||
}
|
||
let m = owners.get(owner);
|
||
if (!m) {
|
||
m = /* @__PURE__ */ new Map();
|
||
owners.set(owner, m);
|
||
}
|
||
const existing = m.get(key);
|
||
if (existing) {
|
||
existing.disconnect();
|
||
}
|
||
const runCurrent = () => {
|
||
if (m.get(key) === ro) {
|
||
run();
|
||
}
|
||
};
|
||
const ro = new ResizeObserver(runCurrent);
|
||
m.set(key, ro);
|
||
ro.observe(root);
|
||
const fonts = root.ownerDocument.fonts;
|
||
if (fonts && fonts.ready && typeof fonts.ready.then === "function") {
|
||
fonts.ready.then(runCurrent, () => void 0);
|
||
}
|
||
}
|
||
function uninstallLayout(root, owner, key) {
|
||
const owners = observers.get(root);
|
||
const m = owners?.get(owner);
|
||
if (!m) {
|
||
return;
|
||
}
|
||
const ro = m.get(key);
|
||
if (ro) {
|
||
ro.disconnect();
|
||
m.delete(key);
|
||
}
|
||
if (m.size === 0) {
|
||
owners.delete(owner);
|
||
}
|
||
}
|
||
var JZ_WRAP_TAGS = ["JZ-JINZE", "JZ-CHAR", "JZ-INNER", "JZ-HWS", "JZ-ORPHAN"];
|
||
function buildWrapSet(options) {
|
||
const s = new Set(JZ_WRAP_TAGS);
|
||
for (const name of options.finder.styleInlines) {
|
||
s.add(name);
|
||
}
|
||
return s;
|
||
}
|
||
function inlineFlow(el, wrapSet) {
|
||
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display;
|
||
if (display) {
|
||
return display === "contents" || display.startsWith("inline") || display.startsWith("ruby");
|
||
}
|
||
return wrapSet.has(el.nodeName);
|
||
}
|
||
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" ? previousVariationSequenceStart(t.data, t.data.length) : 0;
|
||
const length = variationSequenceAtUtf16(t.data, at).length;
|
||
range.setStart(t, at);
|
||
range.setEnd(t, at + length);
|
||
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) {
|
||
if (n.nodeType === 1 && n.nodeName === "BR" && n.ownerDocument.defaultView?.getComputedStyle(n).display !== "none") {
|
||
return null;
|
||
}
|
||
const r = rectOf(n, side);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
n = side === "prev" ? n.previousSibling : n.nextSibling;
|
||
}
|
||
if (!parentRef || parentRef.nodeType !== 1) {
|
||
return null;
|
||
}
|
||
if (!inlineFlow(parentRef, wrapSet)) {
|
||
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(ctx) {
|
||
for (const cls of ALL_EDGE) {
|
||
ctx.root.querySelectorAll("jz-char." + cls).forEach((e) => {
|
||
if (ctx.ownedElements.has(e)) {
|
||
ctx.setOwnedClass(e, cls, false);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
function relayout(ctx) {
|
||
clearEdge(ctx);
|
||
const wrapSet = buildWrapSet(ctx.options);
|
||
const chars = Array.from(ctx.root.querySelectorAll("jz-char")).filter(
|
||
(c) => ctx.ownedElements.has(c) && ctx.finder.inScope(c) && !ctx.finder.isAvoided(c)
|
||
);
|
||
const decisions = [];
|
||
for (const el of chars) {
|
||
if (!ctx.finder.levelAllows(el, "paragraph")) {
|
||
continue;
|
||
}
|
||
if (!ctx.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) {
|
||
ctx.setOwnedClass(d.el, LE, true);
|
||
}
|
||
}
|
||
var lineEdgePass = {
|
||
name: "jiyaLineEdge",
|
||
kind: "standalone",
|
||
order: 90,
|
||
level: "paragraph",
|
||
enabled: (o) => o.features.jiya,
|
||
render(ctx) {
|
||
if (!hasLayout()) {
|
||
return;
|
||
}
|
||
installLayout(ctx.root, ctx.ownerToken, "lineEdge", () => {
|
||
relayout(ctx);
|
||
});
|
||
},
|
||
revert(ctx) {
|
||
uninstallLayout(ctx.root, ctx.ownerToken, "lineEdge");
|
||
clearEdge(ctx);
|
||
}
|
||
};
|
||
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(ctx, wrapSet) {
|
||
const alignCache = /* @__PURE__ */ new Map();
|
||
const gaps = Array.from(ctx.root.querySelectorAll("jz-hws")).filter(
|
||
(g) => ctx.ownedElements.has(g) && ctx.finder.inScope(g) && !ctx.finder.isAvoided(g) && ctx.finder.levelAllows(g, "paragraph") && ctx.finder.featureEnabledFor(g, "spacing") && !skippableAlign(g, alignCache)
|
||
);
|
||
if (!gaps.length) {
|
||
return;
|
||
}
|
||
gaps.forEach((g) => ctx.setOwnedClass(g, TRIM, true));
|
||
for (const g of gaps) {
|
||
const r = lastRect(g);
|
||
const next = r ? edgeRect(g.nextSibling, "next", g, wrapSet) : null;
|
||
const atLineEnd = !r || !next || lowerLine(r, next);
|
||
if (!atLineEnd) {
|
||
ctx.setOwnedClass(g, TRIM, false);
|
||
}
|
||
}
|
||
}
|
||
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, ctx.ownerToken, "gapTrim", () => {
|
||
trimGaps(ctx, wrapSet);
|
||
});
|
||
},
|
||
revert(ctx) {
|
||
uninstallLayout(ctx.root, ctx.ownerToken, "gapTrim");
|
||
ctx.root.querySelectorAll("jz-hws." + TRIM).forEach((e) => {
|
||
if (ctx.ownedElements.has(e)) {
|
||
ctx.setOwnedClass(e, TRIM, false);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
// 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;
|
||
ctx.ownedElements.add(span);
|
||
return span;
|
||
}
|
||
});
|
||
}
|
||
return out;
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS3, ctx.root, ctx.ownedElements);
|
||
}
|
||
};
|
||
|
||
// 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 atoms = /* @__PURE__ */ new Map();
|
||
for (const run of finder.collectRuns(block, true)) {
|
||
for (const atom of run.map) {
|
||
const first = atom.parts[0];
|
||
let starts = atoms.get(first.textNode);
|
||
if (!starts) {
|
||
starts = /* @__PURE__ */ new Map();
|
||
atoms.set(first.textNode, starts);
|
||
}
|
||
starts.set(first.start, atom.text);
|
||
}
|
||
}
|
||
const blockedChildren = /* @__PURE__ */ new Set();
|
||
const boundary = (directChild) => {
|
||
out.length = 0;
|
||
blockedChildren.add(directChild);
|
||
};
|
||
const visit = (node, directChild) => {
|
||
if (node.nodeType === 3) {
|
||
const t = node;
|
||
if (finder.isAvoided(t) || !finder.levelAllows(t, "paragraph") || !finder.featureEnabledFor(t, PASS4)) {
|
||
boundary(directChild);
|
||
return;
|
||
}
|
||
for (const [offset, ch] of atoms.get(t) || []) {
|
||
if (anyCjk.test(ch) && !classifyBiaodian(ch, biaodian)) {
|
||
out.push({ directChild, textNode: t, offset });
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (node.nodeType !== 1) {
|
||
return;
|
||
}
|
||
const el = node;
|
||
if (finder.avoidsSelf(el) || blockSet.has(el.nodeName)) {
|
||
boundary(directChild);
|
||
return;
|
||
}
|
||
if (finder.pillMatches(el)) {
|
||
return;
|
||
}
|
||
if (!finder.levelAllows(el, "paragraph") || !finder.featureEnabledFor(el, PASS4)) {
|
||
boundary(directChild);
|
||
}
|
||
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 { subst: out, blockedChildren };
|
||
}
|
||
function processBlock(block, finder, blockSet, anyCjk, biaodian, chars, ownedElements) {
|
||
if (hasOrphan(block)) {
|
||
return;
|
||
}
|
||
const { subst, blockedChildren } = 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,
|
||
ownedElements
|
||
);
|
||
if (clone) {
|
||
startChild = clone;
|
||
} else if (blockedChildren.has(startChild)) {
|
||
return;
|
||
}
|
||
}
|
||
const orphan = createJz(ownedElements, "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 + "]", "u");
|
||
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,
|
||
ctx.ownedElements
|
||
);
|
||
});
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS4, ctx.root, ctx.ownedElements);
|
||
}
|
||
};
|
||
|
||
// src/typeset/spacing.ts
|
||
var PASS5 = "spacing";
|
||
var OPS_HYPHEN = "\\+\\*=&\\-";
|
||
var OPS_GRADE = "\\+\\-\\*";
|
||
var QUOTES_CLS = '`"\u05F4';
|
||
var LBR_BASIC = "\\(\\[\\{";
|
||
var RBR_BASIC = "\\)\\]\\}";
|
||
var LBR_EXT = "\\(\\[\\{<>";
|
||
var RBR_EXT = "\\)\\]\\}<>";
|
||
function makeRules(rs) {
|
||
const CJK2 = rs.cjk;
|
||
const ANS_AFTER = rs.ansAfter;
|
||
const ANS_BEFORE = rs.ansBefore;
|
||
const CJK_ATOM = "(?:[" + CJK2 + "](?:[" + VARIATION_SELECTORS + "])?)";
|
||
const CJK_OR_ALNUM = "(?:" + CJK_ATOM + "|[" + ALNUM + "])";
|
||
return {
|
||
anyCjk: new RegExp(CJK_ATOM, "u"),
|
||
pillNeighbor: new RegExp("^(?:[" + ALNUM + "]|" + CJK_ATOM + ")$", "u"),
|
||
markerForbidden: new RegExp("[" + CJK2 + ANS_AFTER + ANS_BEFORE + "]", "u"),
|
||
STRIP_CJK_SPACE_ANS: new RegExp("(" + CJK_ATOM + ") ([" + ANS_AFTER + "])", "gu"),
|
||
STRIP_ANS_SPACE_CJK: new RegExp("([" + ANS_BEFORE + "]) (" + CJK_ATOM + ")", "gu"),
|
||
STRIP_CJK_SPACE_CJK: new RegExp("(" + CJK_ATOM + ") (?=" + CJK_ATOM + ")", "gu"),
|
||
DOTS_CJK: new RegExp("([\\.]{2,})(" + CJK_ATOM + ")", "gu"),
|
||
CJK_PUNCTUATION: new RegExp("(" + CJK_ATOM + ")([!;,\\?:]+)(?=" + CJK_OR_ALNUM + ")", "gu"),
|
||
AN_PUNCTUATION_CJK: new RegExp("([" + ALNUM + "])([!;,\\?]+)(" + CJK_ATOM + ")", "gu"),
|
||
CJK_TILDE: new RegExp("(" + CJK_ATOM + ")(~+)(?!=)(?=" + CJK_OR_ALNUM + ")", "gu"),
|
||
CJK_TILDE_EQUALS: new RegExp("(" + CJK_ATOM + ")(~=)", "gu"),
|
||
CJK_PERIOD: new RegExp("(" + CJK_ATOM + ")(\\.)(?![" + ALNUM + "\\./])(?=" + CJK_OR_ALNUM + ")", "gu"),
|
||
AN_PERIOD_CJK: new RegExp("([" + ALNUM + "])(\\.)(" + CJK_ATOM + ")", "gu"),
|
||
AN_COLON_CJK: new RegExp("([" + ALNUM + "])(:)(" + CJK_ATOM + ")", "gu"),
|
||
CJK_QUOTE: new RegExp("(" + CJK_ATOM + ")([" + QUOTES_CLS + "])", "gu"),
|
||
QUOTE_CJK: new RegExp("([" + QUOTES_CLS + "])(" + CJK_ATOM + ")", "gu"),
|
||
CJK_QUOTE_AN: new RegExp("(" + CJK_ATOM + ')(")([' + ALNUM + "])", "gu"),
|
||
CJK_HASH: new RegExp("(" + CJK_ATOM + ")(#([^ ]))", "gu"),
|
||
HASH_CJK: new RegExp("(([^ ])#)(" + CJK_ATOM + ")", "gu"),
|
||
SINGLE_LETTER_GRADE: new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])(" + CJK_ATOM + ")", "gu"),
|
||
CJK_OPERATOR_ANS: new RegExp("(" + CJK_ATOM + ")([" + OPS_HYPHEN + "])([" + ALNUM + "])", "gu"),
|
||
ANS_OPERATOR_CJK: new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])(" + CJK_ATOM + ")", "gu"),
|
||
CJK_LESS_THAN: new RegExp("(" + CJK_ATOM + ")(<)([" + ALNUM + "])", "gu"),
|
||
LESS_THAN_CJK: new RegExp("([" + ALNUM + "])(<)(" + CJK_ATOM + ")", "gu"),
|
||
CJK_GREATER_THAN: new RegExp("(" + CJK_ATOM + ")(>)([" + ALNUM + "])", "gu"),
|
||
GREATER_THAN_CJK: new RegExp("([" + ALNUM + "])(>)(" + CJK_ATOM + ")", "gu"),
|
||
CJK_LEFT_BRACKET: new RegExp("(" + CJK_ATOM + ")([" + LBR_EXT + "])", "gu"),
|
||
RIGHT_BRACKET_CJK: new RegExp("([" + RBR_EXT + "])(" + CJK_ATOM + ")", "gu"),
|
||
// 英數字串段前之字元一併匹配,才能在不用 lookbehind 下排除 `.foo(`。
|
||
// 括號只作 lookahead,避免 `a(b(` 之前一次匹配吞掉 `(` 而漏掉第二處。
|
||
AN_LEFT_BRACKET: new RegExp(
|
||
"(^|[^." + ALNUM + "])([" + ALNUM + "]+)(?=[" + LBR_BASIC + "])",
|
||
"g"
|
||
),
|
||
RIGHT_BRACKET_AN: new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g"),
|
||
CJK_ANS: new RegExp("(" + CJK_ATOM + ")([" + ANS_AFTER + "])", "gu"),
|
||
ANS_CJK: new RegExp("([" + ANS_BEFORE + "])(" + CJK_ATOM + ")", "gu"),
|
||
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 ASTRAL_CJK_SHADOW = "\u6F22";
|
||
function markersFor(text, R) {
|
||
const occupied = new Set(text);
|
||
const markers = [];
|
||
const tryRange = (start, end) => {
|
||
for (let cp = start; cp <= end; cp += 1) {
|
||
const ch = String.fromCodePoint(cp);
|
||
if (occupied.has(ch) || R.markerForbidden.test(ch)) {
|
||
continue;
|
||
}
|
||
markers.push(ch);
|
||
if (markers.length === 4) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
let found = tryRange(57344, 63743) || tryRange(983040, 1048573) || tryRange(1048576, 1114109) || tryRange(64976, 65007);
|
||
for (let plane = 0; !found && plane <= 16; plane += 1) {
|
||
found = tryRange(plane * 65536 + 65534, plane * 65536 + 65535);
|
||
}
|
||
if (!found) {
|
||
throw new Error("juzhen: \u6E90\u6587\u672C\u8207\u5B57\u7B26\u898F\u5247\u5DF2\u4F54\u7528\u5168\u90E8\u9593\u9699\u6A19\u8A18\u5019\u9078\u3002");
|
||
}
|
||
return {
|
||
gap: markers[0],
|
||
compoundOpen: markers[1],
|
||
compoundClose: markers[2],
|
||
deletion: markers[3]
|
||
};
|
||
}
|
||
function shadowAstralCjkAtoms(atoms, R) {
|
||
const shadowed = new Array(atoms.length);
|
||
for (let i = 0; i < atoms.length; i += 1) {
|
||
const atom = atoms[i];
|
||
const base = atom.codePointAt(0);
|
||
const match = base !== void 0 && base > 65535 ? R.anyCjk.exec(atom) : null;
|
||
shadowed[i] = match?.index === 0 ? ASTRAL_CJK_SHADOW : atom;
|
||
}
|
||
return shadowed.join("");
|
||
}
|
||
function panguSpace(text, R, markers) {
|
||
const MARK = markers.gap;
|
||
const PH_OPEN = markers.compoundOpen;
|
||
const PH_CLOSE = markers.compoundClose;
|
||
const DEL = markers.deletion;
|
||
const PH_PATTERN = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
|
||
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$2" + MARK);
|
||
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;
|
||
}
|
||
var MOVED_PARTS = /* @__PURE__ */ new WeakMap();
|
||
var MOVED_ANCHOR = "jz-spacing-anchor";
|
||
var MOVED_GAP_ANCHOR = "jz-spacing-gap-anchor";
|
||
var CROSS_NODE_ATTR = "data-jz-cross-node-atom";
|
||
function wrapNodeInGap(node, ownedElements) {
|
||
const parent = node.parentNode;
|
||
if (!parent || parent.nodeName === "JZ-HWS") {
|
||
return;
|
||
}
|
||
const gap = createJz(ownedElements, "jz-hws", PASS5, "wrap");
|
||
parent.insertBefore(gap, node);
|
||
gap.appendChild(node);
|
||
}
|
||
function isolateTextSpan(part) {
|
||
let tn = part.textNode;
|
||
if (!tn.parentNode) {
|
||
return null;
|
||
}
|
||
if (part.start > 0) {
|
||
tn = tn.splitText(part.start);
|
||
}
|
||
const length = part.end - part.start;
|
||
if (tn.data.length > length) {
|
||
tn.splitText(length);
|
||
}
|
||
return tn;
|
||
}
|
||
function wrapSingleSpanInGap(part, ownedElements) {
|
||
const containingGap = part.textNode.parentElement?.closest("jz-hws");
|
||
if (containingGap) {
|
||
return ownedElements.has(containingGap) && containingGap.getAttribute("data-jz") === PASS5 && containingGap.getAttribute("data-jz-kind") === "wrap" ? containingGap : null;
|
||
}
|
||
const tn = isolateTextSpan(part);
|
||
if (!tn) {
|
||
return null;
|
||
}
|
||
const parent = tn.parentNode;
|
||
if (!parent) {
|
||
return null;
|
||
}
|
||
const gap = createJz(ownedElements, "jz-hws", PASS5, "wrap");
|
||
try {
|
||
parent.insertBefore(gap, tn);
|
||
gap.appendChild(tn);
|
||
return gap;
|
||
} catch (error) {
|
||
if (tn.parentNode === gap) {
|
||
parent.insertBefore(tn, gap);
|
||
}
|
||
if (gap.parentNode) {
|
||
gap.parentNode.removeChild(gap);
|
||
}
|
||
ownedElements.delete(gap);
|
||
parent.normalize?.();
|
||
throw error;
|
||
}
|
||
}
|
||
function isOwnedCrossGap(gap, ownedElements) {
|
||
return ownedElements.has(gap) && gap.getAttribute("data-jz") === PASS5 && gap.getAttribute("data-jz-kind") === "wrap" && gap.hasAttribute(CROSS_NODE_ATTR);
|
||
}
|
||
function trustedInsertionPoint(record) {
|
||
if (record.anchor.parentNode === record.originalParent) {
|
||
return record.anchor;
|
||
}
|
||
if (record.originalNextSibling?.parentNode === record.originalParent) {
|
||
return record.originalNextSibling;
|
||
}
|
||
if (record.originalNextSibling !== null) {
|
||
return void 0;
|
||
}
|
||
if (record.originalPreviousSibling?.parentNode === record.originalParent) {
|
||
return record.originalPreviousSibling === record.originalParent.lastChild ? null : void 0;
|
||
}
|
||
if (record.originalPreviousSibling === null && record.originalNextSibling === null && record.anchor.parentNode !== null && record.anchor.parentNode !== record.originalParent && !record.originalParent.hasChildNodes()) {
|
||
return null;
|
||
}
|
||
return void 0;
|
||
}
|
||
function restoreMovedText(record) {
|
||
if (record.textNode.parentNode === record.originalParent) {
|
||
return true;
|
||
}
|
||
if (record.textNode.parentNode !== record.gap) {
|
||
return false;
|
||
}
|
||
const before = trustedInsertionPoint(record);
|
||
if (before === void 0) {
|
||
return false;
|
||
}
|
||
record.originalParent.insertBefore(record.textNode, before);
|
||
return true;
|
||
}
|
||
function movePartIntoGap(part, gap, block, scopeRoot) {
|
||
const textNode = isolateTextSpan(part);
|
||
const parent = textNode?.parentNode;
|
||
const gapParent = gap.parentNode;
|
||
if (!textNode || !parent || !gapParent) {
|
||
return null;
|
||
}
|
||
const anchor = textNode.ownerDocument.createComment(MOVED_ANCHOR);
|
||
const gapAnchor = textNode.ownerDocument.createComment(MOVED_GAP_ANCHOR);
|
||
const record = {
|
||
anchor,
|
||
gapAnchor,
|
||
gap,
|
||
textNode,
|
||
originalParent: parent,
|
||
originalPreviousSibling: textNode.previousSibling,
|
||
originalNextSibling: textNode.nextSibling,
|
||
gapOriginalParent: gapParent,
|
||
gapOriginalPreviousSibling: gap.previousSibling,
|
||
gapOriginalNextSibling: gap.nextSibling,
|
||
block,
|
||
scopeRoot,
|
||
scopeAncestors: (() => {
|
||
const ancestors = [];
|
||
for (let parent2 = scopeRoot.parentElement; parent2; parent2 = parent2.parentElement) {
|
||
ancestors.push(parent2);
|
||
}
|
||
return ancestors;
|
||
})()
|
||
};
|
||
try {
|
||
parent.insertBefore(anchor, textNode);
|
||
gap.appendChild(gapAnchor);
|
||
gap.appendChild(textNode);
|
||
return record;
|
||
} catch (error) {
|
||
restoreMovedText(record);
|
||
if (anchor.parentNode) {
|
||
anchor.parentNode.removeChild(anchor);
|
||
}
|
||
if (gapAnchor.parentNode) {
|
||
gapAnchor.parentNode.removeChild(gapAnchor);
|
||
}
|
||
parent.normalize?.();
|
||
throw error;
|
||
}
|
||
}
|
||
function wrapGapLeft(span, ownedElements, ownerToken, block, scopeRoot) {
|
||
const containingGaps = /* @__PURE__ */ new Set();
|
||
for (const part of span.parts) {
|
||
const containing = part.textNode.parentElement?.closest("jz-hws");
|
||
if (containing) {
|
||
containingGaps.add(containing);
|
||
}
|
||
}
|
||
if (containingGaps.size > 0) {
|
||
if (containingGaps.size === 1) {
|
||
const existing = Array.from(containingGaps)[0];
|
||
if (isOwnedCrossGap(existing, ownedElements) && span.parts.every((part) => existing.contains(part.textNode))) {
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
const gap = wrapSingleSpanInGap(span.parts[0], ownedElements);
|
||
if (!gap) {
|
||
return;
|
||
}
|
||
if (span.parts.length === 1) {
|
||
return;
|
||
}
|
||
const moved = [];
|
||
try {
|
||
for (let i = 1; i < span.parts.length; i += 1) {
|
||
const record = movePartIntoGap(span.parts[i], gap, block, scopeRoot);
|
||
if (!record) {
|
||
throw new Error("juzhen: \u8DE8\u7BC0\u9EDE\u5B57\u5143\u642C\u79FB\u5931\u6557\u3002");
|
||
}
|
||
moved.push(record);
|
||
}
|
||
gap.setAttribute(CROSS_NODE_ATTR, "");
|
||
let records = MOVED_PARTS.get(ownerToken);
|
||
if (!records) {
|
||
records = /* @__PURE__ */ new Set();
|
||
MOVED_PARTS.set(ownerToken, records);
|
||
}
|
||
for (const record of moved) {
|
||
records.add(record);
|
||
}
|
||
} catch (error) {
|
||
for (let i = moved.length - 1; i >= 0; i -= 1) {
|
||
const record = moved[i];
|
||
restoreMovedText(record);
|
||
if (record.anchor.parentNode) {
|
||
record.anchor.parentNode.removeChild(record.anchor);
|
||
}
|
||
if (record.gapAnchor.parentNode) {
|
||
record.gapAnchor.parentNode.removeChild(record.gapAnchor);
|
||
}
|
||
record.originalParent.normalize?.();
|
||
}
|
||
gap.removeAttribute(CROSS_NODE_ATTR);
|
||
const gapParent = gap.parentNode;
|
||
if (gapParent) {
|
||
while (gap.firstChild) {
|
||
gapParent.insertBefore(gap.firstChild, gap);
|
||
}
|
||
gapParent.removeChild(gap);
|
||
gapParent.normalize?.();
|
||
}
|
||
ownedElements.delete(gap);
|
||
throw error;
|
||
}
|
||
}
|
||
function recordWithinRoot(record, root) {
|
||
const within = (node) => node === root || root.contains(node);
|
||
const detached = !record.anchor.isConnected && !record.gap.isConnected && !record.gapAnchor.isConnected && !record.originalParent.isConnected;
|
||
return root === record.scopeRoot || root.contains(record.scopeRoot) || record.scopeAncestors.some((ancestor) => root === ancestor || root.contains(ancestor)) || within(record.gap) || within(record.anchor) || within(record.originalParent) || within(record.block) || detached && root === root.ownerDocument.body;
|
||
}
|
||
function placeGapAtOriginalParent(record) {
|
||
if (record.gap.parentNode === record.gapOriginalParent) {
|
||
return true;
|
||
}
|
||
let before = null;
|
||
if (record.gapOriginalNextSibling?.parentNode === record.gapOriginalParent) {
|
||
before = record.gapOriginalNextSibling;
|
||
} else if (record.gapOriginalNextSibling !== null) {
|
||
return false;
|
||
} else if (record.gapOriginalPreviousSibling?.parentNode === record.gapOriginalParent) {
|
||
if (record.gapOriginalPreviousSibling !== record.gapOriginalParent.lastChild) {
|
||
return false;
|
||
}
|
||
before = null;
|
||
} else if (record.gapOriginalParent.lastChild !== record.gapOriginalPreviousSibling || record.gapOriginalPreviousSibling === null && record.gapOriginalNextSibling === null && record.gapOriginalParent.hasChildNodes()) {
|
||
return false;
|
||
}
|
||
record.gapOriginalParent.insertBefore(record.gap, before);
|
||
return true;
|
||
}
|
||
function unwrapCrossGap(record, ownedElements, touched) {
|
||
const gap = record.gap;
|
||
if (!isOwnedCrossGap(gap, ownedElements)) {
|
||
return;
|
||
}
|
||
if (!placeGapAtOriginalParent(record)) {
|
||
if (!gap.isConnected) {
|
||
ownedElements.delete(gap);
|
||
}
|
||
return;
|
||
}
|
||
const parent = gap.parentNode;
|
||
if (!parent) {
|
||
ownedElements.delete(gap);
|
||
return;
|
||
}
|
||
while (gap.firstChild) {
|
||
parent.insertBefore(gap.firstChild, gap);
|
||
}
|
||
parent.removeChild(gap);
|
||
recordUnwrappedParent(gap, parent, ownedElements);
|
||
ownedElements.delete(gap);
|
||
touched.add(parent);
|
||
}
|
||
function restoreMovedParts(ctx) {
|
||
const touched = /* @__PURE__ */ new Set();
|
||
const records = MOVED_PARTS.get(ctx.ownerToken);
|
||
if (!records) {
|
||
return touched;
|
||
}
|
||
const gaps = /* @__PURE__ */ new Map();
|
||
for (const record of Array.from(records)) {
|
||
if (!recordWithinRoot(record, ctx.root)) {
|
||
continue;
|
||
}
|
||
const sourceParent = resolveUnwrappedParent(record.originalParent);
|
||
const gapParent = resolveUnwrappedParent(record.gapOriginalParent);
|
||
if (!sourceParent || !gapParent) {
|
||
record.anchor.remove();
|
||
record.gapAnchor.remove();
|
||
records.delete(record);
|
||
continue;
|
||
}
|
||
record.originalParent = sourceParent;
|
||
record.gapOriginalParent = gapParent;
|
||
const live = (node) => record.scopeRoot.contains(node) || node.isConnected;
|
||
if (!isOwnedCrossGap(record.gap, ctx.ownedElements) || !live(record.gap)) {
|
||
record.anchor.remove();
|
||
record.gapAnchor.remove();
|
||
if (!live(record.gap)) {
|
||
ctx.ownedElements.delete(record.gap);
|
||
}
|
||
records.delete(record);
|
||
continue;
|
||
}
|
||
const restored = live(record.originalParent) && restoreMovedText(record);
|
||
if (!restored && record.textNode.parentNode === record.gap) {
|
||
record.gap.removeChild(record.textNode);
|
||
}
|
||
if (restored) {
|
||
touched.add(record.originalParent);
|
||
}
|
||
if (record.anchor.parentNode) {
|
||
record.anchor.parentNode.removeChild(record.anchor);
|
||
}
|
||
if (record.gapAnchor.parentNode) {
|
||
record.gapAnchor.parentNode.removeChild(record.gapAnchor);
|
||
}
|
||
gaps.set(record.gap, record);
|
||
records.delete(record);
|
||
}
|
||
for (const record of gaps.values()) {
|
||
unwrapCrossGap(record, ctx.ownedElements, touched);
|
||
}
|
||
if (records.size === 0) {
|
||
MOVED_PARTS.delete(ctx.ownerToken);
|
||
}
|
||
return touched;
|
||
}
|
||
function deleteSpan(span) {
|
||
for (let i = span.parts.length - 1; i >= 0; i -= 1) {
|
||
const part = span.parts[i];
|
||
part.textNode.data = part.textNode.data.slice(0, part.start) + part.textNode.data.slice(part.end);
|
||
}
|
||
}
|
||
function processBlock2(block, scopeRoot, finder, R, ownedElements, ownerToken) {
|
||
const runs = finder.collectRuns(block, false, PASS5);
|
||
for (const run of runs) {
|
||
const { map } = run;
|
||
const originalChars = map.map((span) => span.text);
|
||
const ruleText = shadowAstralCjkAtoms(originalChars, R);
|
||
if (originalChars.length < 2 || !R.anyCjk.test(ruleText)) {
|
||
continue;
|
||
}
|
||
const markers = markersFor(ruleText, R);
|
||
const spaced = panguSpace(ruleText, R, markers);
|
||
if (spaced === ruleText) {
|
||
continue;
|
||
}
|
||
const actions = [];
|
||
let origIdx = 0;
|
||
for (const sc of splitVariationSequences(spaced)) {
|
||
if (sc === markers.deletion) {
|
||
actions.push({ before: void 0, space: map[origIdx] });
|
||
origIdx += 1;
|
||
continue;
|
||
}
|
||
if (sc === markers.gap) {
|
||
const oc = origIdx < originalChars.length ? originalChars[origIdx] : null;
|
||
if (oc === " ") {
|
||
actions.push({ before: map[origIdx - 1], space: map[origIdx] });
|
||
origIdx += 1;
|
||
} else {
|
||
actions.push({ before: map[origIdx - 1], space: null });
|
||
}
|
||
continue;
|
||
}
|
||
origIdx += 1;
|
||
}
|
||
for (let i = actions.length - 1; i >= 0; i -= 1) {
|
||
const act = actions[i];
|
||
if (act.space) {
|
||
deleteSpan(act.space);
|
||
}
|
||
if (act.before) {
|
||
wrapGapLeft(act.before, ownedElements, ownerToken, block, scopeRoot);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function processPills(root, finder, boundarySelector, pillNeighbor, ownedElements, ownerToken) {
|
||
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, root, void 0, PASS5);
|
||
if (prev) {
|
||
handlePillSide(prev, pill, -1, finder, pillNeighbor, ownedElements, ownerToken, root);
|
||
}
|
||
const next = finder.adjacentLogicalChar(pill, 1, root, void 0, PASS5);
|
||
if (next) {
|
||
handlePillSide(next, pill, 1, finder, pillNeighbor, ownedElements, ownerToken, root);
|
||
}
|
||
});
|
||
}
|
||
function handlePillSide(adj, pill, dir, finder, pillNeighbor, ownedElements, ownerToken, scopeRoot) {
|
||
if (!adj.parts.every((part) => finder.featureEnabledFor(part.textNode, PASS5))) {
|
||
return;
|
||
}
|
||
let neighbor = adj;
|
||
if (adj.ch === " ") {
|
||
const beyond = finder.adjacentLogicalChar(pill, dir, scopeRoot, adj, PASS5);
|
||
if (!beyond || !pillNeighbor.test(beyond.ch) || !beyond.parts.every((part) => finder.featureEnabledFor(part.textNode, PASS5))) {
|
||
return;
|
||
}
|
||
const space = adj.parts[0];
|
||
deleteSpan({ text: " ", parts: adj.parts });
|
||
for (const part of beyond.parts) {
|
||
if (part.textNode === space.textNode && part.start >= space.end) {
|
||
part.start -= 1;
|
||
part.end -= 1;
|
||
}
|
||
}
|
||
beyond.offset = beyond.parts[0].start;
|
||
neighbor = beyond;
|
||
}
|
||
if (!pillNeighbor.test(neighbor.ch)) {
|
||
return;
|
||
}
|
||
if (dir === -1) {
|
||
wrapGapLeft(
|
||
{ text: neighbor.ch, parts: neighbor.parts },
|
||
ownedElements,
|
||
ownerToken,
|
||
pill.parentElement ?? scopeRoot,
|
||
scopeRoot
|
||
);
|
||
} else {
|
||
wrapNodeInGap(pill, ownedElements);
|
||
}
|
||
}
|
||
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);
|
||
const processedRoots = /* @__PURE__ */ new Set();
|
||
ctx.finder.eachBlock(ctx.root, (block) => {
|
||
processBlock2(block, ctx.root, finder, R, ctx.ownedElements, ctx.ownerToken);
|
||
processedRoots.add(block);
|
||
});
|
||
const inlineRoots = [ctx.root];
|
||
if (options.scope.include) {
|
||
inlineRoots.push(...Array.from(ctx.root.querySelectorAll(options.scope.include)));
|
||
}
|
||
for (const root of inlineRoots) {
|
||
let covered = processedRoots.has(root);
|
||
for (let parent = root.parentElement; !covered && parent; parent = parent.parentElement) {
|
||
covered = processedRoots.has(parent);
|
||
if (parent === ctx.root) {
|
||
break;
|
||
}
|
||
}
|
||
if (options.finder.blockTags.has(root.nodeName) || covered || !finder.inScope(root) || finder.isAvoided(root) || finder.category(root) === "pill" || finder.insidePill(root)) {
|
||
continue;
|
||
}
|
||
processBlock2(root, ctx.root, finder, R, ctx.ownedElements, ctx.ownerToken);
|
||
processedRoots.add(root);
|
||
}
|
||
const boundarySelector = [options.finder.pillSelector, options.finder.isolateSelector].filter(Boolean).join(", ");
|
||
if (boundarySelector) {
|
||
processPills(
|
||
ctx.root,
|
||
finder,
|
||
boundarySelector,
|
||
R.pillNeighbor,
|
||
ctx.ownedElements,
|
||
ctx.ownerToken
|
||
);
|
||
}
|
||
},
|
||
revert(ctx) {
|
||
const touched = restoreMovedParts(ctx);
|
||
revertPass(PASS5, ctx.root, ctx.ownedElements);
|
||
for (const parent of touched) {
|
||
parent.normalize?.();
|
||
}
|
||
}
|
||
};
|
||
|
||
// 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" && Number.isSafeInteger(v.minLength) && 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" && Number.isFinite(v.chars) && 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 === void 0 ? "zh-Hant" : langOpt.default,
|
||
...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 特判(與 jiya/jinze 同構)。
|
||
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);
|
||
const ownedElements = /* @__PURE__ */ new WeakSet();
|
||
const ownerToken = {};
|
||
const renderedRoots = /* @__PURE__ */ new Set();
|
||
function makeCtx(root) {
|
||
return { root, options, finder, ownedElements, ownerToken, setOwnedClass };
|
||
}
|
||
function renderImpl(el) {
|
||
renderedRoots.add(el);
|
||
runPasses(PASSES, makeCtx(el));
|
||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||
setOwnedAttribute(el, "data-jz-halfwidth", "margin", ownerToken);
|
||
}
|
||
if (!options.justifyAtoms) {
|
||
setOwnedAttribute(el, "data-jz-atoms", "inline", ownerToken);
|
||
}
|
||
}
|
||
function rootsWithin(el) {
|
||
return Array.from(renderedRoots).filter((root) => root === el || el.contains(root)).sort((a, b) => a.contains(b) ? 1 : b.contains(a) ? -1 : 0);
|
||
}
|
||
function unwindRoots(roots) {
|
||
for (const root of roots) {
|
||
revertPasses(PASSES, makeCtx(root));
|
||
revertOwnedAttribute(root, "data-jz-halfwidth", ownerToken);
|
||
revertOwnedAttribute(root, "data-jz-atoms", ownerToken);
|
||
renderedRoots.delete(root);
|
||
finder.invalidate(root);
|
||
}
|
||
}
|
||
function revertImpl(el) {
|
||
const roots = rootsWithin(el);
|
||
unwindRoots(roots);
|
||
if (!roots.includes(el)) {
|
||
revertPasses(PASSES, makeCtx(el));
|
||
}
|
||
finder.invalidate(el);
|
||
}
|
||
let observer = null;
|
||
let observeRoot = null;
|
||
let observerPauseDepth = 0;
|
||
let observerActive = false;
|
||
const expectedClassMutations = [];
|
||
let expectedClassMutationHead = 0;
|
||
const observeConfig = {
|
||
subtree: true,
|
||
childList: true,
|
||
characterData: true,
|
||
attributes: true,
|
||
attributeOldValue: true,
|
||
attributeFilter: OBSERVE_ATTRS.slice()
|
||
};
|
||
function clearExpectedClassMutations() {
|
||
expectedClassMutations.length = 0;
|
||
expectedClassMutationHead = 0;
|
||
}
|
||
function setOwnedClass(element, className, present) {
|
||
if (!ownedElements.has(element) || element.classList.contains(className) === present) {
|
||
return;
|
||
}
|
||
const oldValue = element.getAttribute("class");
|
||
const tokens = Array.from(element.classList).filter((token) => token !== className);
|
||
if (present) {
|
||
tokens.push(className);
|
||
}
|
||
const newValue = tokens.join(" ");
|
||
const shouldExpect = observerActive && observerPauseDepth === 0 && !!observeRoot && observeRoot.contains(element);
|
||
const expectation = shouldExpect ? { target: element, oldValue, newValue } : null;
|
||
if (expectation) {
|
||
expectedClassMutations.push(expectation);
|
||
}
|
||
try {
|
||
if (present) {
|
||
element.classList.add(className);
|
||
} else {
|
||
element.classList.remove(className);
|
||
}
|
||
} catch (error) {
|
||
if (expectation) {
|
||
expectedClassMutations.pop();
|
||
}
|
||
throw error;
|
||
}
|
||
if (expectation && element.getAttribute("class") !== newValue) {
|
||
expectedClassMutations.pop();
|
||
}
|
||
}
|
||
function classMutationNewValues(records) {
|
||
const values = new Array(records.length);
|
||
const nextValues = /* @__PURE__ */ new Map();
|
||
for (let i = records.length - 1; i >= 0; i -= 1) {
|
||
const record = records[i];
|
||
if (record.type !== "attributes" || record.attributeName !== "class") {
|
||
continue;
|
||
}
|
||
const target = record.target;
|
||
values[i] = nextValues.has(target) ? nextValues.get(target) : target.getAttribute("class");
|
||
nextValues.set(target, record.oldValue);
|
||
}
|
||
return values;
|
||
}
|
||
function consumeExpectedClassMutation(record, newValue) {
|
||
if (record.type !== "attributes" || record.attributeName !== "class") {
|
||
return false;
|
||
}
|
||
const expected = expectedClassMutations[expectedClassMutationHead];
|
||
if (!expected || expected.target !== record.target || expected.oldValue !== record.oldValue || expected.newValue !== newValue) {
|
||
return false;
|
||
}
|
||
expectedClassMutationHead += 1;
|
||
return true;
|
||
}
|
||
function compactExpectedClassMutations() {
|
||
if (expectedClassMutationHead === 0) {
|
||
return;
|
||
}
|
||
if (expectedClassMutationHead === expectedClassMutations.length) {
|
||
clearExpectedClassMutations();
|
||
return;
|
||
}
|
||
if (expectedClassMutationHead >= 1024 && expectedClassMutationHead * 2 >= expectedClassMutations.length) {
|
||
expectedClassMutations.splice(0, expectedClassMutationHead);
|
||
expectedClassMutationHead = 0;
|
||
}
|
||
}
|
||
function pauseObserver() {
|
||
observerPauseDepth += 1;
|
||
if (observerPauseDepth === 1) {
|
||
const records = observer ? observer.takeRecords() : [];
|
||
const errorWindow = observeRoot?.ownerDocument.defaultView;
|
||
observerActive = false;
|
||
if (observer) {
|
||
observer.disconnect();
|
||
}
|
||
try {
|
||
if (records.length) {
|
||
onMutations(records);
|
||
}
|
||
} catch (error) {
|
||
const report = () => {
|
||
throw error;
|
||
};
|
||
if (errorWindow) {
|
||
errorWindow.queueMicrotask(report);
|
||
} else {
|
||
queueMicrotask(report);
|
||
}
|
||
} finally {
|
||
clearExpectedClassMutations();
|
||
}
|
||
}
|
||
}
|
||
function resumeObserver() {
|
||
if (observerPauseDepth <= 0) {
|
||
return;
|
||
}
|
||
observerPauseDepth -= 1;
|
||
if (observerPauseDepth === 0 && observer && observeRoot) {
|
||
observer.takeRecords();
|
||
clearExpectedClassMutations();
|
||
observer.observe(observeRoot, observeConfig);
|
||
observerActive = true;
|
||
}
|
||
}
|
||
function withObserverPaused(run) {
|
||
pauseObserver();
|
||
try {
|
||
return run();
|
||
} finally {
|
||
resumeObserver();
|
||
}
|
||
}
|
||
function withinObservedRoot(node) {
|
||
return !!observeRoot && (node === observeRoot || observeRoot.contains(node));
|
||
}
|
||
function nearestBlock(node) {
|
||
if (!withinObservedRoot(node)) {
|
||
return null;
|
||
}
|
||
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 = [];
|
||
const removedRoots = /* @__PURE__ */ new Set();
|
||
const removedSubtrees = records.flatMap((record) => record.type === "childList" ? Array.from(record.removedNodes) : []);
|
||
const classNewValues = classMutationNewValues(records);
|
||
try {
|
||
for (let recordIndex = 0; recordIndex < records.length; recordIndex += 1) {
|
||
const m = records[recordIndex];
|
||
if (consumeExpectedClassMutation(m, classNewValues[recordIndex])) {
|
||
continue;
|
||
}
|
||
if (m.type === "childList") {
|
||
m.addedNodes.forEach((n) => {
|
||
if (!withinObservedRoot(n)) {
|
||
return;
|
||
}
|
||
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) {
|
||
m.removedNodes.forEach((node) => {
|
||
if (node.nodeType !== 1) {
|
||
return;
|
||
}
|
||
for (const root of rootsWithin(node)) {
|
||
removedRoots.add(root);
|
||
}
|
||
});
|
||
const b = nearestBlock(m.target);
|
||
if (b) {
|
||
rerenderBlocks.add(b);
|
||
}
|
||
}
|
||
} else {
|
||
const b = nearestBlock(m.target);
|
||
if (b) {
|
||
rerenderBlocks.add(b);
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
compactExpectedClassMutations();
|
||
}
|
||
if (!rerenderBlocks.size && !renderNew.length && !removedRoots.size) {
|
||
return;
|
||
}
|
||
withObserverPaused(() => {
|
||
unwindRoots(Array.from(removedRoots).sort((a, b) => a.contains(b) ? 1 : b.contains(a) ? -1 : 0));
|
||
const all = Array.from(rerenderBlocks).filter(withinObservedRoot);
|
||
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 (!withinObservedRoot(el)) {
|
||
continue;
|
||
}
|
||
if (blocks.some((b) => b.contains(el))) {
|
||
continue;
|
||
}
|
||
const relocated = removedSubtrees.some((node) => node === el || node.contains(el));
|
||
const hasOwnedContent = ownedElements.has(el) || Array.from(el.querySelectorAll("[data-jz]")).some((node) => ownedElements.has(node));
|
||
if (relocated || hasOwnedContent || rootsWithin(el).length) {
|
||
revertImpl(el);
|
||
} else {
|
||
finder.invalidate(el);
|
||
}
|
||
renderImpl(el);
|
||
}
|
||
});
|
||
}
|
||
return {
|
||
options,
|
||
render(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
withObserverPaused(() => renderImpl(el));
|
||
},
|
||
revert(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
withObserverPaused(() => revertImpl(el));
|
||
},
|
||
rerender(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
withObserverPaused(() => {
|
||
revertImpl(el);
|
||
renderImpl(el);
|
||
});
|
||
},
|
||
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;
|
||
}
|
||
observerActive = false;
|
||
if (observer) {
|
||
observer.disconnect();
|
||
}
|
||
clearExpectedClassMutations();
|
||
observeRoot = el;
|
||
observer = new MutationObserver(onMutations);
|
||
if (observerPauseDepth === 0) {
|
||
observer.observe(el, observeConfig);
|
||
observerActive = true;
|
||
}
|
||
},
|
||
disconnect() {
|
||
observerActive = false;
|
||
if (observer) {
|
||
observer.disconnect();
|
||
observer = null;
|
||
observeRoot = null;
|
||
}
|
||
clearExpectedClassMutations();
|
||
}
|
||
};
|
||
}
|
||
return __toCommonJS(index_exports);
|
||
})();
|
||
if(typeof globalThis!=='undefined'){globalThis.Juzhen=Juzhen;}
|