30a11b5d6e
下游審計(4 路並行測繪 + 逐條源碼複驗)之階段一:落地已複驗確認之缺陷與低風險 清理;架構統一(透明判定單一真相源)與文檔重整留待階段二/三。 修復(複驗確認): ・jinze 二次 render(未 revert)產生巢狀 jz-jinze、違反 I6 冪等——守衛改判 parent 自身即本 pass 產物(isJz(parent, PASS)),已綁定內容不再重包(實測 jz-jinze 3→6 已修)。 ・processPills 漏檢 avoidsSelf——自身命中 avoid(如 <code data-jz-skip>)之 pill 仍被補 間隙(isAvoided 只查祖先、不含自身,同「下游問題六」型)。processPills + root 自處理 分支補 avoidsSelf 檢查。 ・三分類互斥:CODE/KBD 自 DEFAULT_SKIP_TAGS 移除——二者已由預設 pillSelector "code, kbd" 以 pill 機制涵蓋(內部不可見+兩端補隙+斷 run/相鄰),再列 skipTags 致 同元素既 pill 又 avoid、pill 兩端間隙被 avoid 抹掉(違反互斥,且令上一項修復誤傷普通 code)。移除後普通 code 為純 pill、正常補隙;唯自身 data-jz-skip 者不補(即上項修復)。 清理: ・刪零引用死代碼 createMarker/ANY_CJK/BD_ALL/BD_CLASS_NAMES/CreateOptions.attrs; isJz 由 jinze 冪等守衛啟用、脫離死代碼。 ・死旗標 biaodian/emphasis/ruby 補 @reserved 註記(比照 hanging;點明與 charClass.biaodian 撞名異義,避免混淆)。 ・JzKind 補註 data-jz-kind 第三值 "splitoff"(split.ts 直接設、不經 createJz); longWord/orphan 冗餘 ===false 特判簡化(feature() 已對 false 回 false)。 複驗後重新定性(不照單全收): ・jinze 跨透明元素邊界避頭尾漏綁(你好<strong>。世界</strong> 之句末點號漏綁)——**真 缺陷**,但正解須改 jinze 之父級處理模型(穿越透明包裹上攀評估跨層綁定),與階段二 「透明判定單一真相源」一併處理更穩;forbiddenBreak 下探草稿已還原、缺口以註解標記。 ・lineedge edgeRect 攀穿 isolate——**非缺陷**:edgeRect 為物理版面測量(getClientRects), isolate 斷的是邏輯 run/相鄰,其內部字元之物理行首/行尾判定須跨 <mark> 找真實相鄰 內容(否則「文字<mark>「引」</mark>」之 「 誤判行首而錯收左半)。初評誤判、已還原並補註。 新增 3 回歸測試(jinze 冪等、預設 code 為 pill 非 skipTags、自身 avoid 之 pill 不補隙); 119 測試全綠、tsc + esbuild 構建乾淨。
2025 lines
57 KiB
JavaScript
2025 lines
57 KiB
JavaScript
// src/core/finder.ts
|
||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||
var Finder = class {
|
||
constructor(opts) {
|
||
this.opts = opts;
|
||
this.styleSet = opts.finder.styleInlines;
|
||
this.blockSet = opts.finder.blockTags;
|
||
this.skipSet = opts.finder.skipTags;
|
||
this.pillSelector = opts.finder.pillSelector;
|
||
this.isolateSelector = opts.finder.isolateSelector;
|
||
this.skipAttr = opts.finder.skipAttribute;
|
||
this.userIsSkipped = opts.finder.isSkipped;
|
||
this.levelTextSelector = opts.levelText;
|
||
this.avoidSelector = opts.scope.avoid;
|
||
this.includeSelector = opts.scope.include;
|
||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||
}
|
||
/** 該元素是否匹配 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) {
|
||
let p = node.parentNode;
|
||
while (p && p.nodeType === 1) {
|
||
if (this.pillMatches(p)) {
|
||
return true;
|
||
}
|
||
p = p.parentNode;
|
||
}
|
||
return false;
|
||
}
|
||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
||
* 鄰字仍須可達;I7)。
|
||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||
transparentInline(el) {
|
||
if (this.pillMatches(el) || this.isolateMatches(el) || this.avoidsSelf(el)) {
|
||
return false;
|
||
}
|
||
const name = el.nodeName;
|
||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||
}
|
||
/** 該元素**自身**是否為 avoid 邊界(內建 skip 名單 / SVG / contentEditable /
|
||
* data-jz-skip / skipAttribute / avoid 選擇器 / isSkipped)。isAvoided 沿祖先
|
||
* 鏈逐層呼叫本判定;遍歷原語另須對「正要進入」之元素直接判自身——舊版僅查
|
||
* 祖先,元素自身命中 avoid 時被當透明穿越(下游問題六:scope.avoid 對 .katex
|
||
* 形同虛設)。 */
|
||
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;自身不計——元素
|
||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||
isAvoided(node) {
|
||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||
return true;
|
||
}
|
||
let p = node.parentNode;
|
||
while (p && p.nodeType === 1) {
|
||
if (this.avoidsSelf(p)) {
|
||
return true;
|
||
}
|
||
p = p.parentNode;
|
||
}
|
||
return false;
|
||
}
|
||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||
inScope(node) {
|
||
if (!this.includeSelector) {
|
||
return true;
|
||
}
|
||
const el = node.nodeType === 1 ? node : node.parentElement;
|
||
if (!el) {
|
||
return false;
|
||
}
|
||
return !!(el.closest && el.closest(this.includeSelector));
|
||
}
|
||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / pill 內部 / 非作用域)。 */
|
||
eachTextNode(root, cb) {
|
||
const ownerDoc = root.ownerDocument || (typeof document !== "undefined" ? document : null);
|
||
if (!ownerDoc) {
|
||
return;
|
||
}
|
||
const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||
const nodes = [];
|
||
let n = walker.nextNode();
|
||
while (n) {
|
||
const t = n;
|
||
if (!this.isAvoided(t) && !this.insidePill(t) && this.inScope(t)) {
|
||
nodes.push(t);
|
||
}
|
||
n = walker.nextNode();
|
||
}
|
||
for (const node of nodes) {
|
||
cb(node);
|
||
}
|
||
}
|
||
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
|
||
eachBlock(root, cb) {
|
||
const sel = Array.from(this.blockSet).map((t) => t.toLowerCase()).join(",");
|
||
const blocks = [];
|
||
if (sel) {
|
||
root.querySelectorAll(sel).forEach((el) => blocks.push(el));
|
||
}
|
||
if (root.nodeType === 1 && this.blockSet.has(root.nodeName)) {
|
||
blocks.push(root);
|
||
}
|
||
for (const b of blocks) {
|
||
if (this.includeSelector && !(b.closest && b.closest(this.includeSelector))) {
|
||
continue;
|
||
}
|
||
cb(b);
|
||
}
|
||
}
|
||
/**
|
||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
|
||
* jz-* 透明穿越;block/pill/avoid/間隙 marker(jz-hws)中斷 run。
|
||
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
|
||
*/
|
||
collectRuns(block) {
|
||
const runs = [];
|
||
let curText = "";
|
||
let curMap = [];
|
||
const flush = () => {
|
||
if (curText.length >= 2) {
|
||
runs.push({ text: curText, map: curMap });
|
||
}
|
||
curText = "";
|
||
curMap = [];
|
||
};
|
||
const visit = (node) => {
|
||
if (!node) {
|
||
return;
|
||
}
|
||
if (node.nodeType === 3) {
|
||
const t = node;
|
||
if (this.isAvoided(t) || !this.inScope(t)) {
|
||
return;
|
||
}
|
||
const data = t.data;
|
||
for (let i = 0; i < data.length; i += 1) {
|
||
curText += data.charAt(i);
|
||
curMap.push({ textNode: t, offset: i });
|
||
}
|
||
return;
|
||
}
|
||
if (node.nodeType !== 1) {
|
||
return;
|
||
}
|
||
const el = node;
|
||
if (this.avoidsSelf(el) || this.blockSet.has(el.nodeName)) {
|
||
flush();
|
||
return;
|
||
}
|
||
if (this.pillMatches(el)) {
|
||
flush();
|
||
return;
|
||
}
|
||
if (this.isolateMatches(el)) {
|
||
flush();
|
||
let ic = el.firstChild;
|
||
while (ic) {
|
||
visit(ic);
|
||
ic = ic.nextSibling;
|
||
}
|
||
flush();
|
||
return;
|
||
}
|
||
let c2 = el.firstChild;
|
||
while (c2) {
|
||
visit(c2);
|
||
c2 = c2.nextSibling;
|
||
}
|
||
};
|
||
let c = block.firstChild;
|
||
while (c) {
|
||
visit(c);
|
||
c = c.nextSibling;
|
||
}
|
||
flush();
|
||
return runs;
|
||
}
|
||
/**
|
||
* pill 之邏輯鄰字(架構 §4.1):經樣式鏈遞迴,遇 pill/block/avoid/間隙
|
||
* marker 即止。direction = -1 取前鄰字,+1 取後鄰字。
|
||
*/
|
||
adjacentLogicalChar(el, direction) {
|
||
const pickEnd = (tn) => {
|
||
if (direction === -1) {
|
||
return {
|
||
textNode: tn,
|
||
offset: tn.data.length - 1,
|
||
ch: tn.data.charAt(tn.data.length - 1)
|
||
};
|
||
}
|
||
return { textNode: tn, offset: 0, ch: tn.data.charAt(0) };
|
||
};
|
||
const descend = (into) => {
|
||
let n2 = direction === -1 ? into.lastChild : into.firstChild;
|
||
while (n2) {
|
||
if (n2.nodeType === 3 && n2.data.length > 0) {
|
||
return pickEnd(n2);
|
||
}
|
||
if (n2.nodeType === 1) {
|
||
if (!this.transparentInline(n2)) {
|
||
return "blocked";
|
||
}
|
||
const r = descend(n2);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
}
|
||
n2 = direction === -1 ? n2.previousSibling : n2.nextSibling;
|
||
}
|
||
return null;
|
||
};
|
||
let n = direction === -1 ? el.previousSibling : el.nextSibling;
|
||
let p = el.parentNode;
|
||
for (; ; ) {
|
||
while (n) {
|
||
if (n.nodeType === 3) {
|
||
if (n.data.length > 0) {
|
||
return pickEnd(n);
|
||
}
|
||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||
continue;
|
||
}
|
||
if (n.nodeType === 1) {
|
||
const e = n;
|
||
if (!this.transparentInline(e)) {
|
||
return null;
|
||
}
|
||
const r = descend(e);
|
||
if (r === "blocked") {
|
||
return null;
|
||
}
|
||
if (r) {
|
||
return r;
|
||
}
|
||
n = direction === -1 ? e.previousSibling : e.nextSibling;
|
||
continue;
|
||
}
|
||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||
}
|
||
if (!p || p.nodeType !== 1 || !this.transparentInline(p)) {
|
||
return null;
|
||
}
|
||
const pe = p;
|
||
n = direction === -1 ? pe.previousSibling : pe.nextSibling;
|
||
p = pe.parentNode;
|
||
}
|
||
}
|
||
/**
|
||
* 節點之功能級別(§6.5.0a)。自 node 向上找**最近**之顯式信號:元素屬性
|
||
* `data-jz-level="text|paragraph"`(最優先),或命中 `level.text` 選擇器(→ text);
|
||
* 皆無則預設 paragraph(全功能)。結果以解析後之元素為鍵快取。
|
||
*/
|
||
levelFor(node) {
|
||
const start = node.nodeType === 1 ? node : node.parentElement;
|
||
if (!start) {
|
||
return "paragraph";
|
||
}
|
||
const cached = this.levelCache.get(start);
|
||
if (cached) {
|
||
return cached;
|
||
}
|
||
let el = start;
|
||
let result = "paragraph";
|
||
while (el) {
|
||
const attr = el.getAttribute ? el.getAttribute("data-jz-level") : null;
|
||
if (attr === "text" || attr === "paragraph") {
|
||
result = attr;
|
||
break;
|
||
}
|
||
if (this.levelTextSelector && el.matches && el.matches(this.levelTextSelector)) {
|
||
result = "text";
|
||
break;
|
||
}
|
||
el = el.parentElement;
|
||
}
|
||
this.levelCache.set(start, result);
|
||
return result;
|
||
}
|
||
/**
|
||
* 某級別之 pass 是否可作用於該節點(§6.5.0a gate)。文本級 pass 不受限(恆 true);
|
||
* 段落級 pass 僅作用於段落級元素(text-level 子樹跳過)。與 featureEnabledFor 疊加:
|
||
* level 為粗粒度語義組(先判),blocks/屬性為細粒度逐功能(後調)。
|
||
*/
|
||
levelAllows(node, passLevel) {
|
||
if (passLevel === "text") {
|
||
return true;
|
||
}
|
||
return this.levelFor(node) === "paragraph";
|
||
}
|
||
/**
|
||
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
||
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
||
* blocks 選擇器),無則回退全域 features。結果以元素為鍵快取。
|
||
*/
|
||
featureEnabledFor(node, name) {
|
||
const el = node.nodeType === 1 ? node : node.parentElement;
|
||
const globalOn = this.globalFeature(name);
|
||
if (!el) {
|
||
return globalOn;
|
||
}
|
||
const cached = this.featureCache.get(el);
|
||
if (cached && name in cached) {
|
||
return cached[name];
|
||
}
|
||
const result = this.resolveFeature(el, name, globalOn);
|
||
const bucket = cached || {};
|
||
bucket[name] = result;
|
||
this.featureCache.set(el, bucket);
|
||
return result;
|
||
}
|
||
globalFeature(name) {
|
||
const f = this.opts.features;
|
||
return !!f[name];
|
||
}
|
||
resolveFeature(start, name, globalOn) {
|
||
let el = start;
|
||
while (el) {
|
||
if (el.hasAttribute("data-juzhen")) {
|
||
const list = (el.getAttribute("data-juzhen") || "").split(/\s+/).filter(Boolean);
|
||
return list.indexOf(name) >= 0;
|
||
}
|
||
if (el.hasAttribute("data-juzhen-off")) {
|
||
const off = (el.getAttribute("data-juzhen-off") || "").split(/\s+/).filter(Boolean);
|
||
if (off.indexOf(name) >= 0) {
|
||
return false;
|
||
}
|
||
}
|
||
for (const rule of this.opts.blocks) {
|
||
if (rule.selector && el.matches && el.matches(rule.selector)) {
|
||
return rule.features.indexOf(name) >= 0;
|
||
}
|
||
}
|
||
el = el.parentElement;
|
||
}
|
||
return globalOn;
|
||
}
|
||
};
|
||
|
||
// src/core/pass.ts
|
||
function runPasses(passes, ctx) {
|
||
const enabled = passes.filter((p) => p.enabled(ctx.options));
|
||
const charify = enabled.filter((p) => p.kind === "charify").sort((a, b) => a.order - b.order);
|
||
const standalone = enabled.filter((p) => p.kind === "standalone").sort((a, b) => a.order - b.order);
|
||
if (charify.length > 0) {
|
||
runCharify(charify, ctx);
|
||
}
|
||
for (const p of standalone) {
|
||
p.render(ctx);
|
||
}
|
||
}
|
||
function revertPasses(passes, ctx) {
|
||
const ordered = passes.slice().sort((a, b) => b.order - a.order);
|
||
for (const p of ordered) {
|
||
p.revert(ctx);
|
||
}
|
||
}
|
||
function runCharify(passes, ctx) {
|
||
const work = [];
|
||
ctx.finder.eachTextNode(ctx.root, (node) => {
|
||
let requests = [];
|
||
for (const p of passes) {
|
||
if (!ctx.finder.levelAllows(node, p.level)) {
|
||
continue;
|
||
}
|
||
const got = p.collect(node, ctx);
|
||
if (got.length > 0) {
|
||
requests = requests.concat(got);
|
||
}
|
||
}
|
||
if (requests.length > 0) {
|
||
work.push({ node, requests });
|
||
}
|
||
});
|
||
for (const item of work) {
|
||
applyRequests(item.node, item.requests);
|
||
}
|
||
}
|
||
function applyRequests(node, requests) {
|
||
const parent = node.parentNode;
|
||
if (!parent) {
|
||
return;
|
||
}
|
||
const sorted = requests.slice().sort((a, b) => a.start - b.start || a.end - b.end);
|
||
const data = node.data;
|
||
const ownerDoc = node.ownerDocument;
|
||
if (!ownerDoc) {
|
||
return;
|
||
}
|
||
const frag = ownerDoc.createDocumentFragment();
|
||
let cursor = 0;
|
||
for (const req of sorted) {
|
||
if (req.start < cursor) {
|
||
continue;
|
||
}
|
||
if (req.start < 0 || req.end > data.length || req.end <= req.start) {
|
||
continue;
|
||
}
|
||
if (req.start > cursor) {
|
||
frag.appendChild(ownerDoc.createTextNode(data.slice(cursor, req.start)));
|
||
}
|
||
frag.appendChild(req.build(data.slice(req.start, req.end)));
|
||
cursor = req.end;
|
||
}
|
||
if (cursor === 0) {
|
||
return;
|
||
}
|
||
if (cursor < data.length) {
|
||
frag.appendChild(ownerDoc.createTextNode(data.slice(cursor)));
|
||
}
|
||
parent.replaceChild(frag, node);
|
||
}
|
||
|
||
// src/core/dom.ts
|
||
var KIND_ATTR = "data-jz-kind";
|
||
var PASS_ATTR = "data-jz";
|
||
function doc() {
|
||
if (typeof document === "undefined") {
|
||
throw new Error("juzhen: \u7121 document\uFF08\u9700\u65BC\u700F\u89BD\u5668\u6216 jsdom \u74B0\u5883\u57F7\u884C\uFF09\u3002");
|
||
}
|
||
return document;
|
||
}
|
||
function createJz(tag, pass, kind, opts = {}) {
|
||
const el = doc().createElement(tag);
|
||
el.setAttribute(PASS_ATTR, pass);
|
||
el.setAttribute(KIND_ATTR, kind);
|
||
if (opts.classes) {
|
||
const list = Array.isArray(opts.classes) ? opts.classes : [opts.classes];
|
||
for (const c of list) {
|
||
if (c) {
|
||
el.classList.add(c);
|
||
}
|
||
}
|
||
}
|
||
if (opts.text !== void 0) {
|
||
el.textContent = opts.text;
|
||
}
|
||
return el;
|
||
}
|
||
function revertPass(pass, root) {
|
||
const nodes = Array.from(
|
||
root.querySelectorAll("[" + PASS_ATTR + '="' + pass + '"]')
|
||
);
|
||
const touched = /* @__PURE__ */ new Set();
|
||
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
||
const el = nodes[i];
|
||
const parent = el.parentNode;
|
||
if (!parent) {
|
||
continue;
|
||
}
|
||
const kind = el.getAttribute(KIND_ATTR);
|
||
if (kind === "splitoff") {
|
||
continue;
|
||
}
|
||
touched.add(parent);
|
||
if (kind === "marker") {
|
||
parent.removeChild(el);
|
||
} else {
|
||
while (el.firstChild) {
|
||
parent.insertBefore(el.firstChild, el);
|
||
}
|
||
parent.removeChild(el);
|
||
}
|
||
}
|
||
const splitoffs = Array.from(
|
||
root.querySelectorAll(
|
||
"[" + PASS_ATTR + '="' + pass + '"][' + KIND_ATTR + '="splitoff"]'
|
||
)
|
||
);
|
||
for (const so of splitoffs) {
|
||
const parent = so.parentNode;
|
||
if (!parent) {
|
||
continue;
|
||
}
|
||
touched.add(parent);
|
||
const prev = so.previousSibling;
|
||
if (prev && prev.nodeType === 1 && prev.nodeName === so.nodeName) {
|
||
while (so.firstChild) {
|
||
prev.appendChild(so.firstChild);
|
||
}
|
||
parent.removeChild(so);
|
||
} else {
|
||
while (so.firstChild) {
|
||
parent.insertBefore(so.firstChild, so);
|
||
}
|
||
parent.removeChild(so);
|
||
}
|
||
}
|
||
for (const p of touched) {
|
||
if (p.normalize) {
|
||
p.normalize();
|
||
}
|
||
}
|
||
}
|
||
var BD_CLASSES = ["bd-open", "bd-close", "bd-pause", "bd-stop", "bd-middle", "bd-liga"];
|
||
function bdClassOf(el) {
|
||
for (const c of BD_CLASSES) {
|
||
if (el.classList.contains(c)) {
|
||
return c;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function isJz(node, pass) {
|
||
if (!node || node.nodeType !== 1) {
|
||
return false;
|
||
}
|
||
const el = node;
|
||
if (!el.hasAttribute(PASS_ATTR)) {
|
||
return false;
|
||
}
|
||
return pass === void 0 || el.getAttribute(PASS_ATTR) === pass;
|
||
}
|
||
|
||
// src/core/locale.ts
|
||
var DEFAULT_POLICY = {
|
||
"zh-Hant": "quanjiao",
|
||
"zh-Hans": "kaiming",
|
||
"ja": "quanjiao",
|
||
"other": "quanjiao"
|
||
};
|
||
function effectiveLang(node, fallback) {
|
||
let p = node.parentNode;
|
||
while (p && p.nodeType === 1) {
|
||
const el = p;
|
||
const v = el.getAttribute("lang");
|
||
if (v) {
|
||
return v.toLowerCase();
|
||
}
|
||
p = p.parentNode;
|
||
}
|
||
return (fallback || "").toLowerCase();
|
||
}
|
||
function classify(lang) {
|
||
const l = (lang || "").toLowerCase();
|
||
if (l === "ja" || l.indexOf("ja-") === 0) {
|
||
return "ja";
|
||
}
|
||
if (l === "zh" || l.indexOf("zh-") === 0 || l.indexOf("zh") === 0) {
|
||
if (l.indexOf("hant") >= 0) {
|
||
return "zh-Hant";
|
||
}
|
||
if (l.indexOf("hans") >= 0) {
|
||
return "zh-Hans";
|
||
}
|
||
if (/-(tw|hk|mo)\b/.test(l)) {
|
||
return "zh-Hant";
|
||
}
|
||
if (l === "zh") {
|
||
return "zh-Hant";
|
||
}
|
||
return "zh-Hans";
|
||
}
|
||
return "other";
|
||
}
|
||
function styleFor(langClass, config) {
|
||
const perLang = config.policy ? config.policy[langClass] : void 0;
|
||
if (perLang) {
|
||
return perLang;
|
||
}
|
||
if (config.style) {
|
||
return config.style;
|
||
}
|
||
return DEFAULT_POLICY[langClass];
|
||
}
|
||
function resolveStyle(node, config) {
|
||
const lang = effectiveLang(node, config.default);
|
||
return styleFor(classify(lang), config);
|
||
}
|
||
|
||
// src/core/unicode.ts
|
||
var CJK = "\u2E80-\u2EFF\u2F00-\u2FDF\u3040-\u309F\u30A0-\u30FA\u30FC-\u30FF\u3100-\u312F\u3200-\u32FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF";
|
||
var ALNUM = "A-Za-z0-9";
|
||
var ALPHA = "A-Za-z";
|
||
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
|
||
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
|
||
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
|
||
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
|
||
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
|
||
var BD_MIDDLE = "\xB7\u30FB\u2027";
|
||
var BD_LIGA = "\u2026\u2014";
|
||
var BD_ORDER = [
|
||
"stop",
|
||
"pause",
|
||
"open",
|
||
"close",
|
||
"middle",
|
||
"liga"
|
||
];
|
||
var DEFAULT_RULESET = {
|
||
cjk: CJK,
|
||
ansAfter: ANS_AFTER_CJK,
|
||
ansBefore: ANS_BEFORE_CJK,
|
||
biaodian: {
|
||
stop: BD_STOP,
|
||
pause: BD_PAUSE,
|
||
open: BD_OPEN,
|
||
close: BD_CLOSE,
|
||
middle: BD_MIDDLE,
|
||
liga: BD_LIGA
|
||
}
|
||
};
|
||
function resolveRuleset(override) {
|
||
if (!override) {
|
||
return DEFAULT_RULESET;
|
||
}
|
||
const west = override.western || "";
|
||
const bd = { ...DEFAULT_RULESET.biaodian };
|
||
if (override.biaodian) {
|
||
for (const sub of BD_ORDER) {
|
||
const extra = override.biaodian[sub];
|
||
if (extra) {
|
||
bd[sub] = bd[sub] + extra;
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
cjk: CJK + (override.cjk || ""),
|
||
ansAfter: ANS_AFTER_CJK + west,
|
||
ansBefore: ANS_BEFORE_CJK + west,
|
||
biaodian: bd
|
||
};
|
||
}
|
||
function classifyBiaodian(ch, biaodian = DEFAULT_RULESET.biaodian) {
|
||
for (const sub of BD_ORDER) {
|
||
if (biaodian[sub].indexOf(ch) >= 0) {
|
||
return "bd-" + sub;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function blankSides(bdClass) {
|
||
switch (bdClass) {
|
||
case "bd-open": {
|
||
return { left: true, right: false };
|
||
}
|
||
case "bd-close":
|
||
case "bd-pause":
|
||
case "bd-stop": {
|
||
return { left: false, right: true };
|
||
}
|
||
default: {
|
||
return { left: false, right: false };
|
||
}
|
||
}
|
||
}
|
||
var BRACKET_OPEN_RIGHT = "\u300C\u3008\u201C\u2018";
|
||
var BRACKET_CLOSE_LEFT = "\u300D\u3009\u201D\u2019";
|
||
var BIAS_BRACKETS = (() => {
|
||
const t = {};
|
||
for (const ch of BD_OPEN) {
|
||
t[ch] = BRACKET_OPEN_RIGHT.indexOf(ch) >= 0 ? "right" : "center";
|
||
}
|
||
for (const ch of BD_CLOSE) {
|
||
t[ch] = BRACKET_CLOSE_LEFT.indexOf(ch) >= 0 ? "left" : "center";
|
||
}
|
||
return t;
|
||
})();
|
||
var BIAS_HANS = {
|
||
"\uFF0C": "left",
|
||
"\u3001": "left",
|
||
"\u3002": "left",
|
||
"\uFF0E": "left",
|
||
"\uFF1B": "left",
|
||
"\uFF1A": "left",
|
||
"\uFF01": "left",
|
||
"\uFF1F": "left"
|
||
};
|
||
var BIAS_HANT = {
|
||
"\uFF0C": "center",
|
||
"\u3001": "center",
|
||
"\u3002": "center",
|
||
"\uFF0E": "center",
|
||
"\uFF1B": "center",
|
||
"\uFF1A": "center",
|
||
"\uFF01": "center",
|
||
"\uFF1F": "center"
|
||
};
|
||
var BIAS_DEFAULT = {
|
||
"zh-Hans": BIAS_HANS,
|
||
"zh-Hant": BIAS_HANT,
|
||
"ja": BIAS_HANT,
|
||
// 日文點號慣例居中,沿用繁體表
|
||
"other": BIAS_HANT
|
||
};
|
||
function inkBias(ch, langClass, overrides) {
|
||
const ov = overrides ? overrides[langClass] : void 0;
|
||
if (ov && ov[ch]) {
|
||
return ov[ch];
|
||
}
|
||
const bracket = BIAS_BRACKETS[ch];
|
||
if (bracket) {
|
||
return bracket;
|
||
}
|
||
const t = BIAS_DEFAULT[langClass] || BIAS_DEFAULT.other;
|
||
return t[ch] || null;
|
||
}
|
||
|
||
// src/typeset/jiya.ts
|
||
var PASS = "jiya";
|
||
function baselineHalf(style, bdClass) {
|
||
if (style === "quanjiao") {
|
||
return false;
|
||
}
|
||
if (style === "banjiao") {
|
||
return bdClass === "bd-pause" || bdClass === "bd-stop" || bdClass === "bd-open" || bdClass === "bd-close";
|
||
}
|
||
return bdClass === "bd-pause" || bdClass === "bd-open" || bdClass === "bd-close";
|
||
}
|
||
function inJzChar(node) {
|
||
let p = node.parentNode;
|
||
while (p && p.nodeType === 1) {
|
||
const name = p.nodeName;
|
||
if (name === "JZ-CHAR" || name === "JZ-INNER") {
|
||
return true;
|
||
}
|
||
p = p.parentNode;
|
||
}
|
||
return false;
|
||
}
|
||
var jiyaPass = {
|
||
name: PASS,
|
||
kind: "charify",
|
||
order: 20,
|
||
level: "text",
|
||
enabled: (o) => o.features.jiya,
|
||
collect(node, ctx) {
|
||
if (inJzChar(node)) {
|
||
return [];
|
||
}
|
||
if (!ctx.finder.featureEnabledFor(node, PASS)) {
|
||
return [];
|
||
}
|
||
const data = node.data;
|
||
const out = [];
|
||
let style = null;
|
||
let langClass = null;
|
||
const biasOverrides = ctx.options.jiyaConfig.bias;
|
||
const biaodian = ctx.options.ruleset.biaodian;
|
||
for (let i = 0; i < data.length; i += 1) {
|
||
const ch = data.charAt(i);
|
||
const bdClass = classifyBiaodian(ch, biaodian);
|
||
if (!bdClass) {
|
||
continue;
|
||
}
|
||
if (style === null) {
|
||
style = resolveStyle(node, ctx.options.locale);
|
||
}
|
||
if (langClass === null) {
|
||
langClass = classify(effectiveLang(node, ctx.options.locale.default));
|
||
}
|
||
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
||
const bias = inkBias(ch, langClass, biasOverrides);
|
||
if (bias) {
|
||
classes.push("jz-ink-" + bias);
|
||
}
|
||
out.push({
|
||
start: i,
|
||
end: i + 1,
|
||
pass: PASS,
|
||
build: (text) => {
|
||
const outer = createJz("jz-char", PASS, "wrap", { classes });
|
||
const inner = createJz("jz-inner", PASS, "wrap", { text });
|
||
outer.appendChild(inner);
|
||
return outer;
|
||
}
|
||
});
|
||
}
|
||
return out;
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS, ctx.root);
|
||
}
|
||
};
|
||
var jiyaAdjacencyPass = {
|
||
name: "jiyaAdjacency",
|
||
kind: "standalone",
|
||
order: 25,
|
||
level: "text",
|
||
enabled: (o) => o.features.jiya,
|
||
render(ctx) {
|
||
const { finder, options } = ctx;
|
||
const blockSet = options.finder.blockTags;
|
||
let prev = null;
|
||
const squeeze = (L, R) => {
|
||
if (!finder.featureEnabledFor(L, PASS)) {
|
||
return;
|
||
}
|
||
const lClass = bdClassOf(L);
|
||
const rClass = bdClassOf(R);
|
||
const lSides = blankSides(lClass);
|
||
const rSides = blankSides(rClass);
|
||
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
|
||
if (lSides.right && !stopBeforeOpen) {
|
||
L.classList.add("jz-half");
|
||
}
|
||
if (rSides.left) {
|
||
R.classList.add("jz-half");
|
||
}
|
||
};
|
||
const visit = (node) => {
|
||
for (let c = node.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 3) {
|
||
if (c.data.length > 0) {
|
||
prev = null;
|
||
}
|
||
continue;
|
||
}
|
||
if (c.nodeType !== 1) {
|
||
continue;
|
||
}
|
||
const el = c;
|
||
const nm = el.nodeName;
|
||
if (nm === "JZ-CHAR") {
|
||
if (prev) {
|
||
squeeze(prev, el);
|
||
}
|
||
prev = el;
|
||
} else if (finder.pillMatches(el)) {
|
||
prev = null;
|
||
} else if (finder.avoidsSelf(el)) {
|
||
prev = null;
|
||
} else if (finder.isolateMatches(el)) {
|
||
prev = null;
|
||
visit(el);
|
||
prev = null;
|
||
} else if (blockSet.has(nm)) {
|
||
prev = null;
|
||
visit(el);
|
||
prev = null;
|
||
} else {
|
||
visit(el);
|
||
}
|
||
}
|
||
};
|
||
visit(ctx.root);
|
||
},
|
||
revert() {
|
||
}
|
||
};
|
||
|
||
// src/core/split.ts
|
||
var PASS_ATTR2 = "data-jz";
|
||
var KIND_ATTR2 = "data-jz-kind";
|
||
var SPLITTABLE = /* @__PURE__ */ new Set([
|
||
"STRONG",
|
||
"EM",
|
||
"B",
|
||
"I",
|
||
"U",
|
||
"S",
|
||
"SPAN",
|
||
"SMALL",
|
||
"INS",
|
||
"DEL",
|
||
"SUB",
|
||
"SUP"
|
||
]);
|
||
function canSplit(el, finder) {
|
||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && !finder.pillMatches(el) && !finder.isolateMatches(el) && !finder.avoidsSelf(el);
|
||
}
|
||
function firstText(el, finder) {
|
||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 3 && c.data.length > 0) {
|
||
return c;
|
||
}
|
||
if (c.nodeType === 1) {
|
||
if (!canSplit(c, finder)) {
|
||
return null;
|
||
}
|
||
const r = firstText(c, finder);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function lastText(el, finder) {
|
||
for (let c = el.lastChild; c; c = c.previousSibling) {
|
||
if (c.nodeType === 3 && c.data.length > 0) {
|
||
return c;
|
||
}
|
||
if (c.nodeType === 1) {
|
||
if (!canSplit(c, finder)) {
|
||
return null;
|
||
}
|
||
const r = lastText(c, finder);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function isTokenChar(ch, anyCjk) {
|
||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||
}
|
||
function leadingTokenEnd(d, anyCjk) {
|
||
if (anyCjk.test(d.charAt(0))) {
|
||
return 1;
|
||
}
|
||
let e = 1;
|
||
while (e < d.length && isTokenChar(d.charAt(e), anyCjk)) {
|
||
e += 1;
|
||
}
|
||
return e;
|
||
}
|
||
function trailingTokenStart(d, anyCjk) {
|
||
const last = d.length - 1;
|
||
if (anyCjk.test(d.charAt(last))) {
|
||
return last;
|
||
}
|
||
let s = last;
|
||
while (s > 0 && isTokenChar(d.charAt(s - 1), anyCjk)) {
|
||
s -= 1;
|
||
}
|
||
return s;
|
||
}
|
||
function splitTreeAfter(top, textNode, offset, pass, finder) {
|
||
if (offset <= 0 && firstText(top, finder) === textNode) {
|
||
return null;
|
||
}
|
||
let rightAtLevel;
|
||
if (offset >= textNode.data.length) {
|
||
rightAtLevel = textNode.nextSibling;
|
||
} else if (offset <= 0) {
|
||
rightAtLevel = textNode;
|
||
} else {
|
||
rightAtLevel = textNode.splitText(offset);
|
||
}
|
||
let ancestor = textNode.parentNode;
|
||
let topClone = null;
|
||
while (ancestor && ancestor.nodeType === 1) {
|
||
const anc = ancestor;
|
||
let clone = null;
|
||
if (rightAtLevel) {
|
||
clone = anc.cloneNode(false);
|
||
clone.removeAttribute("id");
|
||
clone.setAttribute(PASS_ATTR2, pass);
|
||
clone.setAttribute(KIND_ATTR2, "splitoff");
|
||
let n = rightAtLevel;
|
||
while (n) {
|
||
const next = n.nextSibling;
|
||
clone.appendChild(n);
|
||
n = next;
|
||
}
|
||
}
|
||
if (anc === top) {
|
||
if (!clone) {
|
||
return null;
|
||
}
|
||
if (top.parentNode) {
|
||
top.parentNode.insertBefore(clone, top.nextSibling);
|
||
}
|
||
topClone = clone;
|
||
break;
|
||
}
|
||
if (clone) {
|
||
if (anc.parentNode) {
|
||
anc.parentNode.insertBefore(clone, anc.nextSibling);
|
||
}
|
||
rightAtLevel = clone;
|
||
} else {
|
||
rightAtLevel = anc.nextSibling;
|
||
}
|
||
ancestor = anc.parentNode;
|
||
}
|
||
return topClone;
|
||
}
|
||
function isolateBoundaryToken(el, side, anyCjk, pass, finder) {
|
||
if (!canSplit(el, finder)) {
|
||
return el;
|
||
}
|
||
const text = side === "tail" ? lastText(el, finder) : firstText(el, finder);
|
||
if (!text) {
|
||
return el;
|
||
}
|
||
const point = side === "tail" ? { textNode: text, offset: trailingTokenStart(text.data, anyCjk) } : { textNode: text, offset: leadingTokenEnd(text.data, anyCjk) };
|
||
const clone = splitTreeAfter(el, point.textNode, point.offset, pass, finder);
|
||
if (!clone) {
|
||
return el;
|
||
}
|
||
return side === "tail" ? clone : el;
|
||
}
|
||
function splitElementAt(el, textNode, offset, pass, finder) {
|
||
if (!canSplit(el, finder)) {
|
||
return null;
|
||
}
|
||
return splitTreeAfter(el, textNode, offset, pass, finder);
|
||
}
|
||
|
||
// src/typeset/jinze.ts
|
||
var PASS2 = "jinze";
|
||
var AVOID_HEAD = /* @__PURE__ */ new Set(["bd-close", "bd-pause", "bd-stop", "bd-middle", "bd-liga"]);
|
||
var AVOID_TAIL = /* @__PURE__ */ new Set(["bd-open"]);
|
||
function isCharEl(n) {
|
||
return !!n && n.nodeType === 1 && n.nodeName === "JZ-CHAR";
|
||
}
|
||
function forbiddenBreak(a, b, finder) {
|
||
if (isCharEl(b)) {
|
||
const c = bdClassOf(b);
|
||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(b, PASS2)) {
|
||
return true;
|
||
}
|
||
}
|
||
if (isCharEl(a)) {
|
||
const c = bdClassOf(a);
|
||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(a, PASS2)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function isTokenChar2(ch, anyCjk) {
|
||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||
}
|
||
function leadingTokenEnd2(d, anyCjk) {
|
||
if (anyCjk.test(d.charAt(0))) {
|
||
return 1;
|
||
}
|
||
let e = 1;
|
||
while (e < d.length && isTokenChar2(d.charAt(e), anyCjk)) {
|
||
e += 1;
|
||
}
|
||
return e;
|
||
}
|
||
function trailingTokenStart2(d, anyCjk) {
|
||
const last = d.length - 1;
|
||
if (anyCjk.test(d.charAt(last))) {
|
||
return last;
|
||
}
|
||
let s = last;
|
||
while (s > 0 && isTokenChar2(d.charAt(s - 1), anyCjk)) {
|
||
s -= 1;
|
||
}
|
||
return s;
|
||
}
|
||
function splitBoundaries(t, needFirst, needLast, anyCjk) {
|
||
let node = t;
|
||
if (needFirst) {
|
||
const e = leadingTokenEnd2(node.data, anyCjk);
|
||
if (e < node.data.length) {
|
||
node = node.splitText(e);
|
||
}
|
||
}
|
||
if (needLast) {
|
||
const s = trailingTokenStart2(node.data, anyCjk);
|
||
if (s > 0) {
|
||
node.splitText(s);
|
||
}
|
||
}
|
||
}
|
||
function processParent(parent, finder, anyCjk) {
|
||
if (isJz(parent, PASS2)) {
|
||
return;
|
||
}
|
||
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 1 && c.nodeName === "JZ-JINZE") {
|
||
return;
|
||
}
|
||
}
|
||
const units = [];
|
||
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 3 && c.data.length > 0) {
|
||
units.push(c);
|
||
} else if (c.nodeType === 1) {
|
||
units.push(c);
|
||
}
|
||
}
|
||
const n = units.length;
|
||
if (n < 2) {
|
||
return;
|
||
}
|
||
const forbidden = [];
|
||
for (let i2 = 0; i2 < n - 1; i2 += 1) {
|
||
forbidden.push(forbiddenBreak(units[i2], units[i2 + 1], finder));
|
||
}
|
||
for (let i2 = 0; i2 < n; i2 += 1) {
|
||
const u = units[i2];
|
||
const needFirst = i2 > 0 && forbidden[i2 - 1];
|
||
const needLast = i2 < n - 1 && forbidden[i2];
|
||
if (!needFirst && !needLast) {
|
||
continue;
|
||
}
|
||
if (u.nodeType === 3) {
|
||
splitBoundaries(u, needFirst, needLast, anyCjk);
|
||
} else if (u.nodeType === 1) {
|
||
const el = u;
|
||
if (needLast) {
|
||
isolateBoundaryToken(el, "tail", anyCjk, PASS2, finder);
|
||
}
|
||
if (needFirst) {
|
||
isolateBoundaryToken(el, "head", anyCjk, PASS2, finder);
|
||
}
|
||
}
|
||
}
|
||
const kids = [];
|
||
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
||
kids.push(c);
|
||
}
|
||
let i = 0;
|
||
while (i < kids.length) {
|
||
const run = [kids[i]];
|
||
while (i + 1 < kids.length && forbiddenBreak(kids[i], kids[i + 1], finder)) {
|
||
run.push(kids[i + 1]);
|
||
i += 1;
|
||
}
|
||
if (run.length >= 2) {
|
||
const group = createJz("jz-jinze", PASS2, "wrap");
|
||
parent.insertBefore(group, run[0]);
|
||
for (const node of run) {
|
||
group.appendChild(node);
|
||
}
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
var jinzePass = {
|
||
name: PASS2,
|
||
kind: "standalone",
|
||
order: 40,
|
||
level: "paragraph",
|
||
enabled: (o) => o.features.jinze,
|
||
render(ctx) {
|
||
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
||
const parents = /* @__PURE__ */ new Set();
|
||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||
if (!ctx.finder.levelAllows(c, "paragraph")) {
|
||
return;
|
||
}
|
||
if (c.parentNode) {
|
||
parents.add(c.parentNode);
|
||
}
|
||
});
|
||
for (const parent of parents) {
|
||
processParent(parent, ctx.finder, anyCjk);
|
||
}
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS2, ctx.root);
|
||
}
|
||
};
|
||
|
||
// src/typeset/lineedge.ts
|
||
var LE = "jz-half-le";
|
||
var ALL_EDGE = [LE];
|
||
var EPS = 2;
|
||
var HEAD_CLASSES = /* @__PURE__ */ new Set(["bd-open"]);
|
||
var TAIL_CLASSES = /* @__PURE__ */ new Set(["bd-close"]);
|
||
var observers = /* @__PURE__ */ new WeakMap();
|
||
function hasLayout() {
|
||
return typeof ResizeObserver !== "undefined";
|
||
}
|
||
function installLayout(root, key, run) {
|
||
run();
|
||
let m = observers.get(root);
|
||
if (!m) {
|
||
m = /* @__PURE__ */ new Map();
|
||
observers.set(root, m);
|
||
}
|
||
const existing = m.get(key);
|
||
if (existing) {
|
||
existing.disconnect();
|
||
}
|
||
const ro = new ResizeObserver(() => run());
|
||
ro.observe(root);
|
||
m.set(key, ro);
|
||
const fonts = root.ownerDocument.fonts;
|
||
if (fonts && fonts.ready && typeof fonts.ready.then === "function") {
|
||
fonts.ready.then(run, () => void 0);
|
||
}
|
||
}
|
||
function uninstallLayout(root, key) {
|
||
const m = observers.get(root);
|
||
if (!m) {
|
||
return;
|
||
}
|
||
const ro = m.get(key);
|
||
if (ro) {
|
||
ro.disconnect();
|
||
m.delete(key);
|
||
}
|
||
}
|
||
var JZ_WRAP_TAGS = ["JZ-JINZE", "JZ-CHAR", "JZ-INNER", "JZ-HWS"];
|
||
function buildWrapSet(options) {
|
||
const s = new Set(JZ_WRAP_TAGS);
|
||
for (const name of options.finder.styleInlines) {
|
||
s.add(name);
|
||
}
|
||
return s;
|
||
}
|
||
function rectOf(n, side) {
|
||
if (n.nodeType === 3 && n.data.length > 0) {
|
||
const t = n;
|
||
const doc2 = t.ownerDocument;
|
||
if (!doc2) {
|
||
return null;
|
||
}
|
||
const range = doc2.createRange();
|
||
const at = side === "prev" ? t.data.length - 1 : 0;
|
||
range.setStart(t, at);
|
||
range.setEnd(t, at + 1);
|
||
const rects = range.getClientRects();
|
||
if (rects.length) {
|
||
return rects[side === "prev" ? rects.length - 1 : 0];
|
||
}
|
||
} else if (n.nodeType === 1) {
|
||
const rects = n.getClientRects();
|
||
if (rects.length) {
|
||
return rects[side === "prev" ? rects.length - 1 : 0];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function edgeRect(start, side, from, wrapSet) {
|
||
let n = start;
|
||
let parentRef = from.parentNode;
|
||
for (; ; ) {
|
||
while (n) {
|
||
const r = rectOf(n, side);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
n = side === "prev" ? n.previousSibling : n.nextSibling;
|
||
}
|
||
if (!parentRef || parentRef.nodeType !== 1) {
|
||
return null;
|
||
}
|
||
if (!wrapSet.has(parentRef.nodeName)) {
|
||
return null;
|
||
}
|
||
n = side === "prev" ? parentRef.previousSibling : parentRef.nextSibling;
|
||
parentRef = parentRef.parentNode;
|
||
}
|
||
}
|
||
function firstRect(el) {
|
||
const rects = el.getClientRects();
|
||
return rects.length ? rects[0] : null;
|
||
}
|
||
function lowerLine(a, b) {
|
||
return b.top >= a.bottom - EPS;
|
||
}
|
||
function higherLine(a, b) {
|
||
return b.bottom <= a.top + EPS;
|
||
}
|
||
function lastRect(el) {
|
||
const rects = el.getClientRects();
|
||
return rects.length ? rects[rects.length - 1] : null;
|
||
}
|
||
function clearEdge(root) {
|
||
for (const cls of ALL_EDGE) {
|
||
root.querySelectorAll("jz-char." + cls).forEach((e) => e.classList.remove(cls));
|
||
}
|
||
}
|
||
function relayout(root, finder, options) {
|
||
clearEdge(root);
|
||
const wrapSet = buildWrapSet(options);
|
||
const chars = Array.from(root.querySelectorAll("jz-char")).filter(
|
||
(c) => finder.inScope(c) && !finder.isAvoided(c)
|
||
);
|
||
const decisions = [];
|
||
for (const el of chars) {
|
||
if (!finder.levelAllows(el, "paragraph")) {
|
||
continue;
|
||
}
|
||
if (!finder.featureEnabledFor(el, "jiya")) {
|
||
continue;
|
||
}
|
||
const bd = bdClassOf(el);
|
||
if (!bd) {
|
||
continue;
|
||
}
|
||
const isHead = HEAD_CLASSES.has(bd);
|
||
const isTail = TAIL_CLASSES.has(bd);
|
||
if (!isHead && !isTail) {
|
||
continue;
|
||
}
|
||
const r = firstRect(el);
|
||
if (!r) {
|
||
continue;
|
||
}
|
||
if (isHead) {
|
||
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
||
if (!prev || higherLine(r, prev)) {
|
||
decisions.push({ el });
|
||
}
|
||
} else {
|
||
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
||
if (!next || lowerLine(r, next)) {
|
||
decisions.push({ el });
|
||
}
|
||
}
|
||
}
|
||
for (const d of decisions) {
|
||
d.el.classList.add(LE);
|
||
}
|
||
}
|
||
var lineEdgePass = {
|
||
name: "jiyaLineEdge",
|
||
kind: "standalone",
|
||
order: 90,
|
||
level: "paragraph",
|
||
enabled: (o) => o.features.jiya,
|
||
render(ctx) {
|
||
if (!hasLayout()) {
|
||
return;
|
||
}
|
||
installLayout(ctx.root, "lineEdge", () => relayout(ctx.root, ctx.finder, ctx.options));
|
||
},
|
||
revert(ctx) {
|
||
uninstallLayout(ctx.root, "lineEdge");
|
||
clearEdge(ctx.root);
|
||
}
|
||
};
|
||
var TRIM = "jz-hws-trim";
|
||
function trimGaps(root, finder, wrapSet) {
|
||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph")
|
||
);
|
||
if (!gaps.length) {
|
||
return;
|
||
}
|
||
gaps.forEach((g) => g.classList.add(TRIM));
|
||
for (const g of gaps) {
|
||
const r = lastRect(g);
|
||
const next = r ? edgeRect(g.nextSibling, "next", g, wrapSet) : null;
|
||
const atLineEnd = !r || !next || next.top > r.top + EPS;
|
||
if (!atLineEnd) {
|
||
g.classList.remove(TRIM);
|
||
}
|
||
}
|
||
}
|
||
var gapTrimPass = {
|
||
name: "spacingGapTrim",
|
||
kind: "standalone",
|
||
order: 92,
|
||
level: "paragraph",
|
||
enabled: (o) => o.features.spacing,
|
||
render(ctx) {
|
||
if (!hasLayout()) {
|
||
return;
|
||
}
|
||
const wrapSet = buildWrapSet(ctx.options);
|
||
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder, wrapSet));
|
||
},
|
||
revert(ctx) {
|
||
uninstallLayout(ctx.root, "gapTrim");
|
||
ctx.root.querySelectorAll("jz-hws." + TRIM).forEach((e) => e.classList.remove(TRIM));
|
||
}
|
||
};
|
||
|
||
// src/typeset/longword.ts
|
||
var PASS3 = "longWord";
|
||
function inLangSpan(node) {
|
||
const p = node.parentNode;
|
||
if (!p || p.nodeType !== 1) {
|
||
return false;
|
||
}
|
||
const el = p;
|
||
if (el.nodeName !== "SPAN") {
|
||
return false;
|
||
}
|
||
return el.hasAttribute("lang");
|
||
}
|
||
var longWordPass = {
|
||
name: PASS3,
|
||
kind: "charify",
|
||
order: 60,
|
||
level: "paragraph",
|
||
enabled: (o) => o.features.longWord,
|
||
collect(node, ctx) {
|
||
if (inLangSpan(node)) {
|
||
return [];
|
||
}
|
||
if (!ctx.finder.featureEnabledFor(node, PASS3)) {
|
||
return [];
|
||
}
|
||
const cfg = ctx.options.longWord;
|
||
const re = new RegExp("[A-Za-z]{" + cfg.minLength + ",}", "g");
|
||
const data = node.data;
|
||
const out = [];
|
||
let m;
|
||
while ((m = re.exec(data)) !== null) {
|
||
const start = m.index;
|
||
const end = m.index + m[0].length;
|
||
out.push({
|
||
start,
|
||
end,
|
||
pass: PASS3,
|
||
build: (text) => {
|
||
const span = node.ownerDocument.createElement("span");
|
||
span.setAttribute("lang", cfg.lang);
|
||
span.setAttribute("data-jz", PASS3);
|
||
span.setAttribute("data-jz-kind", "wrap");
|
||
span.textContent = text;
|
||
return span;
|
||
}
|
||
});
|
||
}
|
||
return out;
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS3, ctx.root);
|
||
}
|
||
};
|
||
|
||
// src/typeset/orphan.ts
|
||
var PASS4 = "orphan";
|
||
function hasOrphan(block) {
|
||
for (let c = block.firstChild; c; c = c.nextSibling) {
|
||
if (c.nodeType === 1 && c.nodeName === "JZ-ORPHAN") {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function collectSubst(block, finder, blockSet, anyCjk, biaodian) {
|
||
const out = [];
|
||
const visit = (node, directChild) => {
|
||
if (node.nodeType === 3) {
|
||
const t = node;
|
||
const d = t.data;
|
||
for (let i = 0; i < d.length; i += 1) {
|
||
const ch = d.charAt(i);
|
||
if (anyCjk.test(ch) && !classifyBiaodian(ch, biaodian)) {
|
||
out.push({ directChild, textNode: t, offset: i });
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (node.nodeType !== 1) {
|
||
return;
|
||
}
|
||
const el = node;
|
||
if (finder.avoidsSelf(el)) {
|
||
return;
|
||
}
|
||
if (blockSet.has(el.nodeName) || finder.pillMatches(el)) {
|
||
return;
|
||
}
|
||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||
visit(c, directChild);
|
||
}
|
||
};
|
||
for (let dc = block.firstChild; dc; dc = dc.nextSibling) {
|
||
visit(dc, dc);
|
||
}
|
||
return out;
|
||
}
|
||
function processBlock(block, finder, blockSet, anyCjk, biaodian, chars) {
|
||
if (hasOrphan(block)) {
|
||
return;
|
||
}
|
||
const subst = collectSubst(block, finder, blockSet, anyCjk, biaodian);
|
||
if (subst.length < chars) {
|
||
return;
|
||
}
|
||
const boundary = subst[subst.length - chars];
|
||
let startChild = boundary.directChild;
|
||
if (boundary.textNode && boundary.textNode === startChild) {
|
||
if (boundary.offset > 0) {
|
||
startChild = boundary.textNode.splitText(boundary.offset);
|
||
}
|
||
} else if (boundary.textNode && startChild.nodeType === 1) {
|
||
const clone = splitElementAt(
|
||
startChild,
|
||
boundary.textNode,
|
||
boundary.offset,
|
||
PASS4,
|
||
finder
|
||
);
|
||
if (clone) {
|
||
startChild = clone;
|
||
}
|
||
}
|
||
const orphan = createJz("jz-orphan", PASS4, "wrap");
|
||
block.insertBefore(orphan, startChild);
|
||
let n = startChild;
|
||
while (n) {
|
||
const next = n.nextSibling;
|
||
orphan.appendChild(n);
|
||
n = next;
|
||
}
|
||
}
|
||
var orphanPass = {
|
||
name: PASS4,
|
||
kind: "standalone",
|
||
order: 70,
|
||
level: "paragraph",
|
||
enabled: (o) => o.features.orphan,
|
||
render(ctx) {
|
||
const { finder, options } = ctx;
|
||
const blockSet = options.finder.blockTags;
|
||
const anyCjk = new RegExp("[" + options.ruleset.cjk + "]");
|
||
const biaodian = options.ruleset.biaodian;
|
||
const chars = options.orphan.chars;
|
||
finder.eachBlock(ctx.root, (block) => {
|
||
if (!finder.levelAllows(block, "paragraph")) {
|
||
return;
|
||
}
|
||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||
return;
|
||
}
|
||
if (finder.avoidsSelf(block) || finder.isAvoided(block)) {
|
||
return;
|
||
}
|
||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||
if (sel && block.querySelector(sel)) {
|
||
return;
|
||
}
|
||
processBlock(block, finder, blockSet, anyCjk, biaodian, chars);
|
||
});
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS4, ctx.root);
|
||
}
|
||
};
|
||
|
||
// src/typeset/spacing.ts
|
||
var PASS5 = "spacing";
|
||
var MARK = "\uE000";
|
||
var PH_OPEN = "\uE001";
|
||
var PH_CLOSE = "\uE002";
|
||
var OPS_HYPHEN = "\\+\\*=&\\-";
|
||
var OPS_GRADE = "\\+\\-\\*";
|
||
var QUOTES_CLS = '`"\u05F4';
|
||
var LBR_BASIC = "\\(\\[\\{";
|
||
var RBR_BASIC = "\\)\\]\\}";
|
||
var LBR_EXT = "\\(\\[\\{<>";
|
||
var RBR_EXT = "\\)\\]\\}<>";
|
||
var DEL = String.fromCharCode(57347);
|
||
function makeRules(rs) {
|
||
const CJK2 = rs.cjk;
|
||
const ANS_AFTER = rs.ansAfter;
|
||
const ANS_BEFORE = rs.ansBefore;
|
||
return {
|
||
anyCjk: new RegExp("[" + CJK2 + "]"),
|
||
pillNeighbor: new RegExp("[" + ALNUM + CJK2 + "]"),
|
||
STRIP_CJK_SPACE_ANS: new RegExp("([" + CJK2 + "]) ([" + ANS_AFTER + "])", "g"),
|
||
STRIP_ANS_SPACE_CJK: new RegExp("([" + ANS_BEFORE + "]) ([" + CJK2 + "])", "g"),
|
||
STRIP_CJK_SPACE_CJK: new RegExp("([" + CJK2 + "]) (?=[" + CJK2 + "])", "g"),
|
||
DOTS_CJK: new RegExp("([\\.]{2,})([" + CJK2 + "])", "g"),
|
||
CJK_PUNCTUATION: new RegExp("([" + CJK2 + "])([!;,\\?:]+)(?=[" + CJK2 + ALNUM + "])", "g"),
|
||
AN_PUNCTUATION_CJK: new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK2 + "])", "g"),
|
||
CJK_TILDE: new RegExp("([" + CJK2 + "])(~+)(?!=)(?=[" + CJK2 + ALNUM + "])", "g"),
|
||
CJK_TILDE_EQUALS: new RegExp("([" + CJK2 + "])(~=)", "g"),
|
||
CJK_PERIOD: new RegExp("([" + CJK2 + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK2 + ALNUM + "])", "g"),
|
||
AN_PERIOD_CJK: new RegExp("([" + ALNUM + "])(\\.)([" + CJK2 + "])", "g"),
|
||
AN_COLON_CJK: new RegExp("([" + ALNUM + "])(:)([" + CJK2 + "])", "g"),
|
||
CJK_QUOTE: new RegExp("([" + CJK2 + "])([" + QUOTES_CLS + "])", "g"),
|
||
QUOTE_CJK: new RegExp("([" + QUOTES_CLS + "])([" + CJK2 + "])", "g"),
|
||
CJK_QUOTE_AN: new RegExp("([" + CJK2 + '])(")([' + ALNUM + "])", "g"),
|
||
CJK_HASH: new RegExp("([" + CJK2 + "])(#([^ ]))", "g"),
|
||
HASH_CJK: new RegExp("(([^ ])#)([" + CJK2 + "])", "g"),
|
||
SINGLE_LETTER_GRADE: new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK2 + "])", "g"),
|
||
CJK_OPERATOR_ANS: new RegExp("([" + CJK2 + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g"),
|
||
ANS_OPERATOR_CJK: new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK2 + "])", "g"),
|
||
CJK_LESS_THAN: new RegExp("([" + CJK2 + "])(<)([" + ALNUM + "])", "g"),
|
||
LESS_THAN_CJK: new RegExp("([" + ALNUM + "])(<)([" + CJK2 + "])", "g"),
|
||
CJK_GREATER_THAN: new RegExp("([" + CJK2 + "])(>)([" + ALNUM + "])", "g"),
|
||
GREATER_THAN_CJK: new RegExp("([" + ALNUM + "])(>)([" + CJK2 + "])", "g"),
|
||
CJK_LEFT_BRACKET: new RegExp("([" + CJK2 + "])([" + LBR_EXT + "])", "g"),
|
||
RIGHT_BRACKET_CJK: new RegExp("([" + RBR_EXT + "])([" + CJK2 + "])", "g"),
|
||
AN_LEFT_BRACKET: new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g"),
|
||
RIGHT_BRACKET_AN: new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g"),
|
||
CJK_ANS: new RegExp("([" + CJK2 + "])([" + ANS_AFTER + "])", "g"),
|
||
ANS_CJK: new RegExp("([" + ANS_BEFORE + "])([" + CJK2 + "])", "g"),
|
||
S_A: new RegExp("(%)([" + ALPHA + "])", "g")
|
||
};
|
||
}
|
||
var COMPOUND_WORD = /\b(?:[A-Za-z0-9]*[a-z][A-Za-z0-9]*-[A-Za-z0-9]+|[A-Za-z0-9]+-[A-Za-z0-9]*[a-z][A-Za-z0-9]*|[A-Za-z]+-[0-9]+|[A-Za-z]+[0-9]+-[A-Za-z0-9]+)(?:-[A-Za-z0-9]+)*\b/g;
|
||
var PH_PATTERN = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
|
||
function panguSpace(text, R) {
|
||
if (text.length <= 1 || !R.anyCjk.test(text)) {
|
||
return text;
|
||
}
|
||
let s = text;
|
||
s = s.replace(R.STRIP_CJK_SPACE_ANS, "$1$2");
|
||
s = s.replace(R.STRIP_ANS_SPACE_CJK, "$1$2");
|
||
s = s.replace(R.STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||
s = s.replace(R.DOTS_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(R.CJK_PUNCTUATION, "$1$2" + MARK);
|
||
s = s.replace(R.AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||
s = s.replace(R.CJK_TILDE, "$1$2" + MARK);
|
||
s = s.replace(R.CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||
s = s.replace(R.CJK_PERIOD, "$1$2" + MARK);
|
||
s = s.replace(R.AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||
s = s.replace(R.AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||
s = s.replace(R.CJK_QUOTE, "$1" + MARK + "$2");
|
||
s = s.replace(R.QUOTE_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(R.CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||
s = s.replace(R.CJK_HASH, "$1" + MARK + "$2");
|
||
s = s.replace(R.HASH_CJK, "$1" + MARK + "$3");
|
||
const compounds = [];
|
||
s = s.replace(COMPOUND_WORD, (m) => {
|
||
compounds.push(m);
|
||
return PH_OPEN + (compounds.length - 1) + PH_CLOSE;
|
||
});
|
||
s = s.replace(R.SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||
s = s.replace(R.CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(R.ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(R.CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(R.LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(R.CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(R.GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(PH_PATTERN, (_m, idx) => compounds[Number(idx)] || "");
|
||
s = s.replace(R.CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||
s = s.replace(R.RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(R.AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||
s = s.replace(R.RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||
s = s.replace(R.CJK_ANS, "$1" + MARK + "$2");
|
||
s = s.replace(R.ANS_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(R.S_A, "$1" + MARK + "$2");
|
||
return s;
|
||
}
|
||
function wrapNodeInGap(node) {
|
||
const parent = node.parentNode;
|
||
if (!parent || parent.nodeName === "JZ-HWS") {
|
||
return;
|
||
}
|
||
const gap = createJz("jz-hws", PASS5, "wrap");
|
||
parent.insertBefore(gap, node);
|
||
gap.appendChild(node);
|
||
}
|
||
function wrapGapLeft(pos) {
|
||
let tn = pos.textNode;
|
||
if (!tn.parentNode || tn.parentNode.nodeName === "JZ-HWS") {
|
||
return;
|
||
}
|
||
if (pos.offset > 0) {
|
||
tn = tn.splitText(pos.offset);
|
||
}
|
||
if (tn.data.length > 1) {
|
||
tn.splitText(1);
|
||
}
|
||
wrapNodeInGap(tn);
|
||
}
|
||
function processBlock2(block, finder, R) {
|
||
const runs = finder.collectRuns(block);
|
||
for (const run of runs) {
|
||
const { text, map } = run;
|
||
const spaced = panguSpace(text, R);
|
||
if (spaced === text) {
|
||
continue;
|
||
}
|
||
const actions = [];
|
||
let origIdx = 0;
|
||
let spacedIdx = 0;
|
||
while (spacedIdx < spaced.length) {
|
||
const sc = spaced.charAt(spacedIdx);
|
||
if (sc === DEL) {
|
||
actions.push({ before: void 0, space: map[origIdx] });
|
||
origIdx += 1;
|
||
spacedIdx += 1;
|
||
continue;
|
||
}
|
||
if (sc === MARK) {
|
||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||
if (oc === " ") {
|
||
actions.push({ before: map[origIdx - 1], space: map[origIdx] });
|
||
origIdx += 1;
|
||
} else {
|
||
actions.push({ before: map[origIdx - 1], space: null });
|
||
}
|
||
spacedIdx += 1;
|
||
continue;
|
||
}
|
||
origIdx += 1;
|
||
spacedIdx += 1;
|
||
}
|
||
for (let i = actions.length - 1; i >= 0; i -= 1) {
|
||
const act = actions[i];
|
||
if (act.space) {
|
||
const tn = act.space.textNode;
|
||
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
||
}
|
||
if (act.before) {
|
||
wrapGapLeft(act.before);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function processPills(root, finder, boundarySelector, pillNeighbor) {
|
||
root.querySelectorAll(boundarySelector).forEach((pill) => {
|
||
if (finder.avoidsSelf(pill) || finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||
return;
|
||
}
|
||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||
return;
|
||
}
|
||
if (!pill.parentNode) {
|
||
return;
|
||
}
|
||
const prev = finder.adjacentLogicalChar(pill, -1);
|
||
if (prev) {
|
||
handlePillSide(prev, pill, -1, pillNeighbor);
|
||
}
|
||
const next = finder.adjacentLogicalChar(pill, 1);
|
||
if (next) {
|
||
handlePillSide(next, pill, 1, pillNeighbor);
|
||
}
|
||
});
|
||
}
|
||
function handlePillSide(adj, pill, dir, pillNeighbor) {
|
||
if (dir === -1) {
|
||
if (pillNeighbor.test(adj.ch)) {
|
||
wrapGapLeft({ textNode: adj.textNode, offset: adj.offset });
|
||
return;
|
||
}
|
||
if (adj.ch === " " && adj.offset >= 1) {
|
||
const tn = adj.textNode;
|
||
const bc = tn.data.charAt(adj.offset - 1);
|
||
if (bc !== " " && pillNeighbor.test(bc)) {
|
||
tn.data = tn.data.slice(0, adj.offset) + tn.data.slice(adj.offset + 1);
|
||
wrapGapLeft({ textNode: tn, offset: adj.offset - 1 });
|
||
}
|
||
}
|
||
} else {
|
||
if (pillNeighbor.test(adj.ch)) {
|
||
wrapNodeInGap(pill);
|
||
return;
|
||
}
|
||
if (adj.ch === " " && adj.textNode.data.length >= 2) {
|
||
const ac = adj.textNode.data.charAt(1);
|
||
if (ac !== " " && pillNeighbor.test(ac)) {
|
||
adj.textNode.data = adj.textNode.data.slice(1);
|
||
wrapNodeInGap(pill);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var spacingPass = {
|
||
name: PASS5,
|
||
kind: "standalone",
|
||
order: 50,
|
||
level: "text",
|
||
enabled: (o) => o.features.spacing,
|
||
render(ctx) {
|
||
const { finder, options } = ctx;
|
||
const R = makeRules(options.ruleset);
|
||
ctx.finder.eachBlock(ctx.root, (block) => {
|
||
if (!finder.featureEnabledFor(block, PASS5)) {
|
||
return;
|
||
}
|
||
processBlock2(block, finder, R);
|
||
});
|
||
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.avoidsSelf(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||
processBlock2(ctx.root, finder, R);
|
||
}
|
||
const boundarySelector = [options.finder.pillSelector, options.finder.isolateSelector].filter(Boolean).join(", ");
|
||
if (boundarySelector) {
|
||
processPills(ctx.root, finder, boundarySelector, R.pillNeighbor);
|
||
}
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS5, ctx.root);
|
||
}
|
||
};
|
||
|
||
// src/index.ts
|
||
var DEFAULT_STYLE_INLINES = [
|
||
"STRONG",
|
||
"EM",
|
||
"A",
|
||
"B",
|
||
"I",
|
||
"U",
|
||
"S",
|
||
"MARK",
|
||
"INS",
|
||
"DEL",
|
||
"SUB",
|
||
"SUP",
|
||
"SMALL",
|
||
"ABBR",
|
||
"CITE",
|
||
"DFN",
|
||
"Q",
|
||
"SPAN",
|
||
"VAR",
|
||
"SAMP",
|
||
"TIME",
|
||
"BDO",
|
||
"BDI",
|
||
"RUBY",
|
||
"RB",
|
||
"RT",
|
||
"RP",
|
||
"WBR"
|
||
];
|
||
var DEFAULT_BLOCK_TAGS = [
|
||
"BODY",
|
||
"P",
|
||
"DIV",
|
||
"H1",
|
||
"H2",
|
||
"H3",
|
||
"H4",
|
||
"H5",
|
||
"H6",
|
||
"LI",
|
||
"DT",
|
||
"DD",
|
||
"BLOCKQUOTE",
|
||
"FIGURE",
|
||
"FIGCAPTION",
|
||
"TD",
|
||
"TH",
|
||
"CAPTION",
|
||
"SUMMARY",
|
||
"DETAILS",
|
||
"HEADER",
|
||
"FOOTER",
|
||
"SECTION",
|
||
"ARTICLE",
|
||
"ASIDE",
|
||
"NAV",
|
||
"MAIN",
|
||
"ADDRESS",
|
||
"PRE",
|
||
"UL",
|
||
"OL",
|
||
"DL",
|
||
"TABLE",
|
||
"TR",
|
||
"THEAD",
|
||
"TBODY",
|
||
"TFOOT",
|
||
"FORM",
|
||
"FIELDSET",
|
||
"LEGEND",
|
||
"LABEL",
|
||
"BR",
|
||
"HR"
|
||
];
|
||
var DEFAULT_SKIP_TAGS = [
|
||
"PRE",
|
||
"SAMP",
|
||
"TT",
|
||
"VAR",
|
||
"SCRIPT",
|
||
"STYLE",
|
||
"TEXTAREA",
|
||
"INPUT"
|
||
];
|
||
var PASSES = [
|
||
jiyaPass,
|
||
jiyaAdjacencyPass,
|
||
longWordPass,
|
||
spacingPass,
|
||
jinzePass,
|
||
orphanPass,
|
||
lineEdgePass,
|
||
gapTrimPass
|
||
];
|
||
function feature(v, dflt) {
|
||
return v === void 0 ? dflt : !!v;
|
||
}
|
||
function resolveLongWord(v) {
|
||
if (v === false) {
|
||
return { minLength: 6, lang: "en" };
|
||
}
|
||
if (v === void 0 || v === true || v === null) {
|
||
return { minLength: 6, lang: "en" };
|
||
}
|
||
const minLength = typeof v.minLength === "number" && v.minLength >= 2 ? v.minLength : 6;
|
||
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
|
||
return { minLength, lang };
|
||
}
|
||
function resolveOrphan(v) {
|
||
if (v && typeof v === "object") {
|
||
const chars = typeof v.chars === "number" && v.chars >= 1 ? Math.floor(v.chars) : 2;
|
||
return { chars };
|
||
}
|
||
return { chars: 2 };
|
||
}
|
||
function resolveJiya(v) {
|
||
if (v && typeof v === "object") {
|
||
return {
|
||
halfWidth: v.halfWidth === "margin" ? "margin" : "halt",
|
||
bias: v.bias || {}
|
||
};
|
||
}
|
||
return { halfWidth: "halt", bias: {} };
|
||
}
|
||
function normalizeOptions(opts = {}) {
|
||
const langOpt = opts.lang || {};
|
||
const scope = opts.scope || {};
|
||
return {
|
||
locale: {
|
||
default: langOpt.default || "zh-Hant",
|
||
...langOpt.style ? { style: langOpt.style } : {},
|
||
policy: langOpt.policy || {}
|
||
},
|
||
ruleset: resolveRuleset(opts.charClass),
|
||
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;
|
||
}
|
||
function createJuzhen(opts = {}) {
|
||
const options = normalizeOptions(opts);
|
||
function makeCtx(root) {
|
||
return { root, options, finder: new Finder(options) };
|
||
}
|
||
return {
|
||
options,
|
||
render(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
runPasses(PASSES, makeCtx(el));
|
||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||
el.setAttribute("data-jz-halfwidth", "margin");
|
||
}
|
||
if (!options.justifyAtoms) {
|
||
el.setAttribute("data-jz-atoms", "inline");
|
||
}
|
||
},
|
||
revert(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
revertPasses(PASSES, makeCtx(el));
|
||
el.removeAttribute("data-jz-halfwidth");
|
||
el.removeAttribute("data-jz-atoms");
|
||
}
|
||
};
|
||
}
|
||
export {
|
||
createJuzhen,
|
||
normalizeOptions
|
||
};
|