Files
cjk-autospace/dist/juzhen.iife.js
T
admin 6c57461b61 fix: 標點擠壓句界例外——句末點號接開類保留右空白(。「 全角/開明皆 1.5em)
機制 A 行內連續擠壓原對 (L,R) 兩側獨立收:L 有右空白即收,不看 R 是誰,致句末
點號接開類(。「/?「)兩側皆收、合占 1em,丟失句界視覺。修為:L 為句末點號
(bd-stop)且 R 為開類(bd-open)時保留 L 右空白、僅收開類左側 → 全角式與開明式
皆 1.5em,對稱於 。」。例外僅限句末點號+開類;句內點號 ,「 與雙括號 」「/》《
仍兩側皆收 1em。

半角式不回歸:句末點號於 banjiao 由 baseline(機制 C)已收半形,守衛僅跳過
adjacency 之重複追加、不移除 baseline 之 jz-half;實測 32 可壓縮標點全半形、句界
組合句末點號仍半形(0 回歸)。唯非可壓縮 ·・‧/…— 各式皆全寬,屬設計。

唯一改動點 jiyaAdjacencyPass.squeeze() 加 stopBeforeOpen 守衛;lineedge/baseline
/jinze/spacing 不波及。新增 3 契約測試(116 全綠);README/ARCHITECTURE §6.3
同步句界例外。
2026-06-13 15:35:03 +08:00

2049 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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/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 內部曾被 jiyaspacing 直接變更)。 */
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-charjz-inner 標點原子)。穿越這些
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
* 鄰字仍須可達;I7)。
* ⚠ 以**元素**而非標籤名判定:pillisolate/avoid 縱使標籤名在樣式集亦為邊界、
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
* 肢解;scope.avoidskipAttribute 命中自身者同理)。
* ⚠ jz-charjz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug`汉字。<code>` 經 jinze
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`pill 之左鄰字應
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
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-* 透明穿越;blockpillavoid/間隙 markerjz-hws)中斷 run。
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
*/
collectRuns(block) {
const runs = [];
let curText = "";
let curMap = [];
const flush = () => {
if (curText.length >= 2) {
runs.push({ text: curText, map: curMap });
}
curText = "";
curMap = [];
};
const visit = (node) => {
if (!node) {
return;
}
if (node.nodeType === 3) {
const t = node;
if (this.isAvoided(t) || !this.inScope(t)) {
return;
}
const data = t.data;
for (let i = 0; i < data.length; i += 1) {
curText += data.charAt(i);
curMap.push({ textNode: t, offset: i });
}
return;
}
if (node.nodeType !== 1) {
return;
}
const el = node;
if (this.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):經樣式鏈遞迴,遇 pillblockavoid/間隙
* marker 即止。direction = -1 取前鄰字,+1 取後鄰字。
*/
adjacentLogicalChar(el, direction) {
const pickEnd = (tn) => {
if (direction === -1) {
return {
textNode: tn,
offset: tn.data.length - 1,
ch: tn.data.charAt(tn.data.length - 1)
};
}
return { textNode: tn, offset: 0, ch: tn.data.charAt(0) };
};
const descend = (into) => {
let n2 = direction === -1 ? into.lastChild : into.firstChild;
while (n2) {
if (n2.nodeType === 3 && n2.data.length > 0) {
return pickEnd(n2);
}
if (n2.nodeType === 1) {
if (!this.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.attrs) {
for (const k in opts.attrs) {
const v = opts.attrs[k];
if (v !== void 0) {
el.setAttribute(k, v);
}
}
}
if (opts.text !== void 0) {
el.textContent = opts.text;
}
return el;
}
function revertPass(pass, root) {
const nodes = Array.from(
root.querySelectorAll("[" + PASS_ATTR + '="' + pass + '"]')
);
const touched = /* @__PURE__ */ new Set();
for (let i = nodes.length - 1; i >= 0; i -= 1) {
const el = nodes[i];
const parent = el.parentNode;
if (!parent) {
continue;
}
const kind = el.getAttribute(KIND_ATTR);
if (kind === "splitoff") {
continue;
}
touched.add(parent);
if (kind === "marker") {
parent.removeChild(el);
} else {
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
}
const splitoffs = Array.from(
root.querySelectorAll(
"[" + PASS_ATTR + '="' + pass + '"][' + KIND_ATTR + '="splitoff"]'
)
);
for (const so of splitoffs) {
const parent = so.parentNode;
if (!parent) {
continue;
}
touched.add(parent);
const prev = so.previousSibling;
if (prev && prev.nodeType === 1 && prev.nodeName === so.nodeName) {
while (so.firstChild) {
prev.appendChild(so.firstChild);
}
parent.removeChild(so);
} else {
while (so.firstChild) {
parent.insertBefore(so.firstChild, so);
}
parent.removeChild(so);
}
}
for (const p of touched) {
if (p.normalize) {
p.normalize();
}
}
}
var BD_CLASSES = ["bd-open", "bd-close", "bd-pause", "bd-stop", "bd-middle", "bd-liga"];
function bdClassOf(el) {
for (const c of BD_CLASSES) {
if (el.classList.contains(c)) {
return c;
}
}
return null;
}
// src/core/locale.ts
var DEFAULT_POLICY = {
"zh-Hant": "quanjiao",
"zh-Hans": "kaiming",
"ja": "quanjiao",
"other": "quanjiao"
};
function effectiveLang(node, fallback) {
let p = node.parentNode;
while (p && p.nodeType === 1) {
const el = p;
const v = el.getAttribute("lang");
if (v) {
return v.toLowerCase();
}
p = p.parentNode;
}
return (fallback || "").toLowerCase();
}
function classify(lang) {
const l = (lang || "").toLowerCase();
if (l === "ja" || l.indexOf("ja-") === 0) {
return "ja";
}
if (l === "zh" || l.indexOf("zh-") === 0 || l.indexOf("zh") === 0) {
if (l.indexOf("hant") >= 0) {
return "zh-Hant";
}
if (l.indexOf("hans") >= 0) {
return "zh-Hans";
}
if (/-(tw|hk|mo)\b/.test(l)) {
return "zh-Hant";
}
if (l === "zh") {
return "zh-Hant";
}
return "zh-Hans";
}
return "other";
}
function styleFor(langClass, config) {
const perLang = config.policy ? config.policy[langClass] : void 0;
if (perLang) {
return perLang;
}
if (config.style) {
return config.style;
}
return DEFAULT_POLICY[langClass];
}
function resolveStyle(node, config) {
const lang = effectiveLang(node, config.default);
return styleFor(classify(lang), config);
}
// src/core/unicode.ts
var CJK = "\u2E80-\u2EFF\u2F00-\u2FDF\u3040-\u309F\u30A0-\u30FA\u30FC-\u30FF\u3100-\u312F\u3200-\u32FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF";
var ALNUM = "A-Za-z0-9";
var ALPHA = "A-Za-z";
var ANY_CJK = new RegExp("[" + CJK + "]");
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
var BD_MIDDLE = "\xB7\u30FB\u2027";
var BD_LIGA = "\u2026\u2014";
var BD_ALL = BD_STOP + BD_PAUSE + BD_OPEN + BD_CLOSE + BD_MIDDLE + BD_LIGA;
var BD_ORDER = [
"stop",
"pause",
"open",
"close",
"middle",
"liga"
];
var DEFAULT_RULESET = {
cjk: CJK,
ansAfter: ANS_AFTER_CJK,
ansBefore: ANS_BEFORE_CJK,
biaodian: {
stop: BD_STOP,
pause: BD_PAUSE,
open: BD_OPEN,
close: BD_CLOSE,
middle: BD_MIDDLE,
liga: BD_LIGA
}
};
function resolveRuleset(override) {
if (!override) {
return DEFAULT_RULESET;
}
const west = override.western || "";
const bd = { ...DEFAULT_RULESET.biaodian };
if (override.biaodian) {
for (const sub of BD_ORDER) {
const extra = override.biaodian[sub];
if (extra) {
bd[sub] = bd[sub] + extra;
}
}
}
return {
cjk: CJK + (override.cjk || ""),
ansAfter: ANS_AFTER_CJK + west,
ansBefore: ANS_BEFORE_CJK + west,
biaodian: bd
};
}
function classifyBiaodian(ch, biaodian = DEFAULT_RULESET.biaodian) {
for (const sub of BD_ORDER) {
if (biaodian[sub].indexOf(ch) >= 0) {
return "bd-" + sub;
}
}
return null;
}
function blankSides(bdClass) {
switch (bdClass) {
case "bd-open": {
return { left: true, right: false };
}
case "bd-close":
case "bd-pause":
case "bd-stop": {
return { left: false, right: true };
}
default: {
return { left: false, right: false };
}
}
}
var BRACKET_OPEN_RIGHT = "\u300C\u3008\u201C\u2018";
var BRACKET_CLOSE_LEFT = "\u300D\u3009\u201D\u2019";
var BIAS_BRACKETS = (() => {
const t = {};
for (const ch of BD_OPEN) {
t[ch] = BRACKET_OPEN_RIGHT.indexOf(ch) >= 0 ? "right" : "center";
}
for (const ch of BD_CLOSE) {
t[ch] = BRACKET_CLOSE_LEFT.indexOf(ch) >= 0 ? "left" : "center";
}
return t;
})();
var BIAS_HANS = {
"\uFF0C": "left",
"\u3001": "left",
"\u3002": "left",
"\uFF0E": "left",
"\uFF1B": "left",
"\uFF1A": "left",
"\uFF01": "left",
"\uFF1F": "left"
};
var BIAS_HANT = {
"\uFF0C": "center",
"\u3001": "center",
"\u3002": "center",
"\uFF0E": "center",
"\uFF1B": "center",
"\uFF1A": "center",
"\uFF01": "center",
"\uFF1F": "center"
};
var BIAS_DEFAULT = {
"zh-Hans": BIAS_HANS,
"zh-Hant": BIAS_HANT,
"ja": BIAS_HANT,
// 日文點號慣例居中,沿用繁體表
"other": BIAS_HANT
};
function inkBias(ch, langClass, overrides) {
const ov = overrides ? overrides[langClass] : void 0;
if (ov && ov[ch]) {
return ov[ch];
}
const bracket = BIAS_BRACKETS[ch];
if (bracket) {
return bracket;
}
const t = BIAS_DEFAULT[langClass] || BIAS_DEFAULT.other;
return t[ch] || null;
}
// src/typeset/jiya.ts
var PASS = "jiya";
function baselineHalf(style, bdClass) {
if (style === "quanjiao") {
return false;
}
if (style === "banjiao") {
return bdClass === "bd-pause" || bdClass === "bd-stop" || bdClass === "bd-open" || bdClass === "bd-close";
}
return bdClass === "bd-pause" || bdClass === "bd-open" || bdClass === "bd-close";
}
function inJzChar(node) {
let p = node.parentNode;
while (p && p.nodeType === 1) {
const name = p.nodeName;
if (name === "JZ-CHAR" || name === "JZ-INNER") {
return true;
}
p = p.parentNode;
}
return false;
}
var jiyaPass = {
name: PASS,
kind: "charify",
order: 20,
level: "text",
enabled: (o) => o.features.jiya,
collect(node, ctx) {
if (inJzChar(node)) {
return [];
}
if (!ctx.finder.featureEnabledFor(node, PASS)) {
return [];
}
const data = node.data;
const out = [];
let style = null;
let langClass = null;
const biasOverrides = ctx.options.jiyaConfig.bias;
const biaodian = ctx.options.ruleset.biaodian;
for (let i = 0; i < data.length; i += 1) {
const ch = data.charAt(i);
const bdClass = classifyBiaodian(ch, biaodian);
if (!bdClass) {
continue;
}
if (style === null) {
style = resolveStyle(node, ctx.options.locale);
}
if (langClass === null) {
langClass = classify(effectiveLang(node, ctx.options.locale.default));
}
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
const bias = inkBias(ch, langClass, biasOverrides);
if (bias) {
classes.push("jz-ink-" + bias);
}
out.push({
start: i,
end: i + 1,
pass: PASS,
build: (text) => {
const outer = createJz("jz-char", PASS, "wrap", { classes });
const inner = createJz("jz-inner", PASS, "wrap", { text });
outer.appendChild(inner);
return outer;
}
});
}
return out;
},
revert(ctx) {
revertPass(PASS, ctx.root);
}
};
var jiyaAdjacencyPass = {
name: "jiyaAdjacency",
kind: "standalone",
order: 25,
level: "text",
enabled: (o) => o.features.jiya,
render(ctx) {
const { finder, options } = ctx;
const blockSet = options.finder.blockTags;
let prev = null;
const squeeze = (L, R) => {
if (!finder.featureEnabledFor(L, PASS)) {
return;
}
const 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) {
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.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
return;
}
if (!finder.featureEnabledFor(pill, PASS5)) {
return;
}
if (!pill.parentNode) {
return;
}
const prev = finder.adjacentLogicalChar(pill, -1);
if (prev) {
handlePillSide(prev, pill, -1, pillNeighbor);
}
const next = finder.adjacentLogicalChar(pill, 1);
if (next) {
handlePillSide(next, pill, 1, pillNeighbor);
}
});
}
function handlePillSide(adj, pill, dir, pillNeighbor) {
if (dir === -1) {
if (pillNeighbor.test(adj.ch)) {
wrapGapLeft({ textNode: adj.textNode, offset: adj.offset });
return;
}
if (adj.ch === " " && adj.offset >= 1) {
const tn = adj.textNode;
const bc = tn.data.charAt(adj.offset - 1);
if (bc !== " " && pillNeighbor.test(bc)) {
tn.data = tn.data.slice(0, adj.offset) + tn.data.slice(adj.offset + 1);
wrapGapLeft({ textNode: tn, offset: adj.offset - 1 });
}
}
} else {
if (pillNeighbor.test(adj.ch)) {
wrapNodeInGap(pill);
return;
}
if (adj.ch === " " && adj.textNode.data.length >= 2) {
const ac = adj.textNode.data.charAt(1);
if (ac !== " " && pillNeighbor.test(ac)) {
adj.textNode.data = adj.textNode.data.slice(1);
wrapNodeInGap(pill);
}
}
}
}
var spacingPass = {
name: PASS5,
kind: "standalone",
order: 50,
level: "text",
enabled: (o) => o.features.spacing,
render(ctx) {
const { finder, options } = ctx;
const R = makeRules(options.ruleset);
ctx.finder.eachBlock(ctx.root, (block) => {
if (!finder.featureEnabledFor(block, PASS5)) {
return;
}
processBlock2(block, finder, R);
});
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.isAvoided(ctx.root)) {
processBlock2(ctx.root, finder, R);
}
const boundarySelector = [options.finder.pillSelector, options.finder.isolateSelector].filter(Boolean).join(", ");
if (boundarySelector) {
processPills(ctx.root, finder, boundarySelector, R.pillNeighbor);
}
},
revert(ctx) {
revertPass(PASS5, ctx.root);
}
};
// src/index.ts
var DEFAULT_STYLE_INLINES = [
"STRONG",
"EM",
"A",
"B",
"I",
"U",
"S",
"MARK",
"INS",
"DEL",
"SUB",
"SUP",
"SMALL",
"ABBR",
"CITE",
"DFN",
"Q",
"SPAN",
"VAR",
"SAMP",
"TIME",
"BDO",
"BDI",
"RUBY",
"RB",
"RT",
"RP",
"WBR"
];
var DEFAULT_BLOCK_TAGS = [
"BODY",
"P",
"DIV",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"LI",
"DT",
"DD",
"BLOCKQUOTE",
"FIGURE",
"FIGCAPTION",
"TD",
"TH",
"CAPTION",
"SUMMARY",
"DETAILS",
"HEADER",
"FOOTER",
"SECTION",
"ARTICLE",
"ASIDE",
"NAV",
"MAIN",
"ADDRESS",
"PRE",
"UL",
"OL",
"DL",
"TABLE",
"TR",
"THEAD",
"TBODY",
"TFOOT",
"FORM",
"FIELDSET",
"LEGEND",
"LABEL",
"BR",
"HR"
];
var DEFAULT_SKIP_TAGS = [
"CODE",
"KBD",
"PRE",
"SAMP",
"TT",
"VAR",
"SCRIPT",
"STYLE",
"TEXTAREA",
"INPUT"
];
var PASSES = [
jiyaPass,
jiyaAdjacencyPass,
longWordPass,
spacingPass,
jinzePass,
orphanPass,
lineEdgePass,
gapTrimPass
];
function feature(v, dflt) {
return v === void 0 ? dflt : !!v;
}
function resolveLongWord(v) {
if (v === false) {
return { minLength: 6, lang: "en" };
}
if (v === void 0 || v === true || v === null) {
return { minLength: 6, lang: "en" };
}
const minLength = typeof v.minLength === "number" && v.minLength >= 2 ? v.minLength : 6;
const lang = typeof v.lang === "string" && v.lang ? v.lang : "en";
return { minLength, lang };
}
function resolveOrphan(v) {
if (v && typeof v === "object") {
const chars = typeof v.chars === "number" && v.chars >= 1 ? Math.floor(v.chars) : 2;
return { chars };
}
return { chars: 2 };
}
function resolveJiya(v) {
if (v && typeof v === "object") {
return {
halfWidth: v.halfWidth === "margin" ? "margin" : "halt",
bias: v.bias || {}
};
}
return { halfWidth: "halt", bias: {} };
}
function normalizeOptions(opts = {}) {
const langOpt = opts.lang || {};
const scope = opts.scope || {};
return {
locale: {
default: langOpt.default || "zh-Hant",
...langOpt.style ? { style: langOpt.style } : {},
policy: langOpt.policy || {}
},
ruleset: resolveRuleset(opts.charClass),
justifyAtoms: opts.justifyAtoms === false ? false : true,
levelText: opts.level && opts.level.text || null,
scope: {
root: scope.root || null,
include: scope.include || null,
avoid: scope.avoid || null
},
features: {
spacing: feature(opts.spacing, true),
longWord: opts.longWord === false ? false : feature(opts.longWord, true),
jiya: feature(opts.jiya, true),
jinze: feature(opts.jinze, true),
orphan: opts.orphan === false ? false : feature(opts.orphan, true),
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");
}
};
}
return __toCommonJS(index_exports);
})();
if(typeof globalThis!=='undefined'){globalThis.Juzhen=Juzhen;}