2d42c7ecf2
從零重寫之第一垂直切片(依 ARCHITECTURE.md 契約),涵蓋並超越 v1 全部功能。 核心基座(src/core):unicode 字元表+標點子類+字形側位;locale 語言與 全角/開明政策(全域 lang.style +逐 lang policy);dom(jz-* 工廠+可還原 標記);finder 遍歷引擎(avoid/scope.include/邏輯 run/pill 鄰字/分塊閘); pass 聚合管線。 功能 pass(src/typeset): - spacing:中西間隙採 margin 模型——包裹邊界左側字、以 margin-right 留隙, 不注入空白字元(textContent 完全不變、複製緊湊、justify 不暴增、不撐框、 行首乾淨)。含跨樣式邊界、pill、原檔空格移除。 - gapTrim(lineedge):layout pass,行末去隙,修正 justify 右緣內縮。 - longword:長英文詞 <span lang> + hyphens。 - jiya:標點 charify + 全角/開明/半角(全域 + 逐 lang);半形以字型 OpenType halt 渲染(按字形正確定位、不重疊,居中字形 !? 與繁體 locl 句逗點號皆正確); 行內連續擠壓。 - jinze:行首行末避頭尾(閉類/點號綁前字、開類綁後字,以詞元為邊界、長引文 中段可斷);亦為 justify 綁定載體。 - lineedge:行端擠壓 layout pass(括號引號;ResizeObserver 重算,瀏覽器專用)。 §8 justify:jz-char inline-block 原子,句末標點單份間距。 v1 相容層(compat/v1):createCjkAutospace().apply 映射,IIFE 全域別名。 構建:esbuild → ESM/IIFE/CSS;tsc → d.ts。測試:39 個 jsdom 案例。 demo:簡繁並列+justify+強制繁體開明+標點寬度量測,載 Noto webfont 作參考 (擠壓依賴字型 halt)。間隔號 · 不觸發中西間隙;200% 等詞元不被禁則切斷。
1463 lines
43 KiB
JavaScript
1463 lines
43 KiB
JavaScript
"use strict";
|
||
var Juzhen = (() => {
|
||
var __defProp = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
var __export = (target, all) => {
|
||
for (var name in all)
|
||
__defProp(target, name, { get: all[name], enumerable: true });
|
||
};
|
||
var __copyProps = (to, from, except, desc) => {
|
||
if (from && typeof from === "object" || typeof from === "function") {
|
||
for (let key of __getOwnPropNames(from))
|
||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
}
|
||
return to;
|
||
};
|
||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
|
||
// src/index.ts
|
||
var index_exports = {};
|
||
__export(index_exports, {
|
||
createCjkAutospace: () => createCjkAutospace,
|
||
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.skipAttr = opts.finder.skipAttribute;
|
||
this.userIsSkipped = opts.finder.isSkipped;
|
||
this.avoidSelector = opts.scope.avoid;
|
||
this.includeSelector = opts.scope.include;
|
||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||
}
|
||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||
pillMatches(el) {
|
||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||
}
|
||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||
isAvoided(node) {
|
||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||
return true;
|
||
}
|
||
let p = node.parentNode;
|
||
while (p && p.nodeType === 1) {
|
||
const el = p;
|
||
if (this.skipSet.has(el.nodeName)) {
|
||
return true;
|
||
}
|
||
if (el.namespaceURI === SVG_NS) {
|
||
return true;
|
||
}
|
||
if (el.isContentEditable) {
|
||
return true;
|
||
}
|
||
if (el.hasAttribute("data-jz-skip")) {
|
||
return true;
|
||
}
|
||
if (this.skipAttr && el.hasAttribute(this.skipAttr)) {
|
||
return true;
|
||
}
|
||
if (this.avoidSelector && el.matches && el.matches(this.avoidSelector)) {
|
||
return true;
|
||
}
|
||
p = p.parentNode;
|
||
}
|
||
return false;
|
||
}
|
||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||
inScope(node) {
|
||
if (!this.includeSelector) {
|
||
return true;
|
||
}
|
||
const el = node.nodeType === 1 ? node : node.parentElement;
|
||
if (!el) {
|
||
return false;
|
||
}
|
||
return !!(el.closest && el.closest(this.includeSelector));
|
||
}
|
||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / 非作用域)。 */
|
||
eachTextNode(root, cb) {
|
||
const ownerDoc = root.ownerDocument || (typeof document !== "undefined" ? document : null);
|
||
if (!ownerDoc) {
|
||
return;
|
||
}
|
||
const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||
const nodes = [];
|
||
let n = walker.nextNode();
|
||
while (n) {
|
||
const t = n;
|
||
if (!this.isAvoided(t) && this.inScope(t)) {
|
||
nodes.push(t);
|
||
}
|
||
n = walker.nextNode();
|
||
}
|
||
for (const node of nodes) {
|
||
cb(node);
|
||
}
|
||
}
|
||
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
|
||
eachBlock(root, cb) {
|
||
const sel = Array.from(this.blockSet).map((t) => t.toLowerCase()).join(",");
|
||
const blocks = [];
|
||
if (sel) {
|
||
root.querySelectorAll(sel).forEach((el) => blocks.push(el));
|
||
}
|
||
if (root.nodeType === 1 && this.blockSet.has(root.nodeName)) {
|
||
blocks.push(root);
|
||
}
|
||
for (const b of blocks) {
|
||
if (this.includeSelector && !(b.closest && b.closest(this.includeSelector))) {
|
||
continue;
|
||
}
|
||
cb(b);
|
||
}
|
||
}
|
||
/**
|
||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
|
||
* jz-* 透明穿越;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.isAvoided(el)) {
|
||
flush();
|
||
return;
|
||
}
|
||
if (this.skipSet.has(el.nodeName) || this.blockSet.has(el.nodeName)) {
|
||
flush();
|
||
return;
|
||
}
|
||
if (this.pillMatches(el)) {
|
||
flush();
|
||
return;
|
||
}
|
||
let c2 = el.firstChild;
|
||
while (c2) {
|
||
visit(c2);
|
||
c2 = c2.nextSibling;
|
||
}
|
||
};
|
||
let c = block.firstChild;
|
||
while (c) {
|
||
visit(c);
|
||
c = c.nextSibling;
|
||
}
|
||
flush();
|
||
return runs;
|
||
}
|
||
/**
|
||
* pill 之邏輯鄰字(架構 §4.1):經樣式鏈遞迴,遇 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 && (this.styleSet.has(n2.nodeName) || n2.nodeName === "JZ-HWS")) {
|
||
const r = descend(n2);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
}
|
||
n2 = direction === -1 ? n2.previousSibling : n2.nextSibling;
|
||
}
|
||
return null;
|
||
};
|
||
let n = direction === -1 ? el.previousSibling : el.nextSibling;
|
||
let p = el.parentNode;
|
||
for (; ; ) {
|
||
while (n) {
|
||
if (n.nodeType === 3) {
|
||
if (n.data.length > 0) {
|
||
return pickEnd(n);
|
||
}
|
||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||
continue;
|
||
}
|
||
if (n.nodeType === 1) {
|
||
const e = n;
|
||
if (this.isAvoided(e)) {
|
||
return null;
|
||
}
|
||
if (this.styleSet.has(e.nodeName) || e.nodeName === "JZ-HWS") {
|
||
const r = descend(e);
|
||
if (r) {
|
||
return r;
|
||
}
|
||
n = direction === -1 ? e.previousSibling : e.nextSibling;
|
||
continue;
|
||
}
|
||
return null;
|
||
}
|
||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||
}
|
||
if (!p || p.nodeType !== 1 || !this.styleSet.has(p.nodeName)) {
|
||
return null;
|
||
}
|
||
const pe = p;
|
||
n = direction === -1 ? pe.previousSibling : pe.nextSibling;
|
||
p = pe.parentNode;
|
||
}
|
||
}
|
||
/**
|
||
* 解析節點對某功能之有效啟用狀態(§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) {
|
||
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;
|
||
}
|
||
touched.add(parent);
|
||
const kind = el.getAttribute(KIND_ATTR);
|
||
if (kind === "marker") {
|
||
parent.removeChild(el);
|
||
} else {
|
||
while (el.firstChild) {
|
||
parent.insertBefore(el.firstChild, el);
|
||
}
|
||
parent.removeChild(el);
|
||
}
|
||
}
|
||
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 ANS_AFTER_CJK = ALPHA + "\u0370-\u03FF0-9@\\$%\\^&\\*\\-\\+\\\\=\xA1-\xB6\xB8-\xFF\u2150-\u218F\u2700-\u27BF";
|
||
var ANS_BEFORE_CJK = ALPHA + "\u0370-\u03FF0-9\\$%\\^&\\*\\-\\+\\\\=\xA1-\xB6\xB8-\xFF\u2150-\u218F\u2700-\u27BF";
|
||
var BD_STOP = "\u3002\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_SETS = [
|
||
["bd-stop", BD_STOP],
|
||
["bd-pause", BD_PAUSE],
|
||
["bd-open", BD_OPEN],
|
||
["bd-close", BD_CLOSE],
|
||
["bd-middle", BD_MIDDLE],
|
||
["bd-liga", BD_LIGA]
|
||
];
|
||
function classifyBiaodian(ch) {
|
||
for (const [name, set] of BD_SETS) {
|
||
if (set.indexOf(ch) >= 0) {
|
||
return name;
|
||
}
|
||
}
|
||
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 };
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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,
|
||
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;
|
||
for (let i = 0; i < data.length; i += 1) {
|
||
const bdClass = classifyBiaodian(data.charAt(i));
|
||
if (!bdClass) {
|
||
continue;
|
||
}
|
||
if (style === null) {
|
||
style = resolveStyle(node, ctx.options.locale);
|
||
}
|
||
const classes = baselineHalf(style, bdClass) ? [bdClass, "jz-half"] : [bdClass];
|
||
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,
|
||
enabled: (o) => o.features.jiya,
|
||
render(ctx) {
|
||
const { finder } = ctx;
|
||
const parents = /* @__PURE__ */ new Set();
|
||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||
if (c.parentNode) {
|
||
parents.add(c.parentNode);
|
||
}
|
||
});
|
||
for (const parent of parents) {
|
||
let child = parent.firstChild;
|
||
while (child) {
|
||
const next = child.nextSibling;
|
||
if (child.nodeType === 1 && child.nodeName === "JZ-CHAR" && next && next.nodeType === 1 && next.nodeName === "JZ-CHAR") {
|
||
const L = child;
|
||
const R = next;
|
||
if (finder.featureEnabledFor(L, PASS)) {
|
||
const lSides = blankSides(bdClassOf(L));
|
||
const rSides = blankSides(bdClassOf(R));
|
||
if (lSides.right) {
|
||
L.classList.add("jz-half");
|
||
} else if (rSides.left) {
|
||
R.classList.add("jz-half");
|
||
}
|
||
}
|
||
}
|
||
child = next;
|
||
}
|
||
}
|
||
},
|
||
revert() {
|
||
}
|
||
};
|
||
|
||
// 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 isTokenChar(ch) {
|
||
return !ANY_CJK.test(ch) && !/\s/.test(ch);
|
||
}
|
||
function leadingTokenEnd(d) {
|
||
if (ANY_CJK.test(d.charAt(0))) {
|
||
return 1;
|
||
}
|
||
let e = 1;
|
||
while (e < d.length && isTokenChar(d.charAt(e))) {
|
||
e += 1;
|
||
}
|
||
return e;
|
||
}
|
||
function trailingTokenStart(d) {
|
||
const last = d.length - 1;
|
||
if (ANY_CJK.test(d.charAt(last))) {
|
||
return last;
|
||
}
|
||
let s = last;
|
||
while (s > 0 && isTokenChar(d.charAt(s - 1))) {
|
||
s -= 1;
|
||
}
|
||
return s;
|
||
}
|
||
function splitBoundaries(t, needFirst, needLast) {
|
||
let node = t;
|
||
if (needFirst) {
|
||
const e = leadingTokenEnd(node.data);
|
||
if (e < node.data.length) {
|
||
node = node.splitText(e);
|
||
}
|
||
}
|
||
if (needLast) {
|
||
const s = trailingTokenStart(node.data);
|
||
if (s > 0) {
|
||
node.splitText(s);
|
||
}
|
||
}
|
||
}
|
||
function processParent(parent, finder) {
|
||
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];
|
||
if (u.nodeType !== 3) {
|
||
continue;
|
||
}
|
||
const needFirst = i2 > 0 && forbidden[i2 - 1];
|
||
const needLast = i2 < n - 1 && forbidden[i2];
|
||
if (needFirst || needLast) {
|
||
splitBoundaries(u, needFirst, needLast);
|
||
}
|
||
}
|
||
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,
|
||
enabled: (o) => o.features.jinze,
|
||
render(ctx) {
|
||
const parents = /* @__PURE__ */ new Set();
|
||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||
if (c.parentNode) {
|
||
parents.add(c.parentNode);
|
||
}
|
||
});
|
||
for (const parent of parents) {
|
||
processParent(parent, ctx.finder);
|
||
}
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS2, ctx.root);
|
||
}
|
||
};
|
||
|
||
// src/typeset/lineedge.ts
|
||
var LE = "jz-half-le";
|
||
var EPS = 4;
|
||
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 INLINE_WRAP = /* @__PURE__ */ new Set(["JZ-JINZE", "JZ-CHAR", "JZ-INNER", "JZ-HWS", "SPAN", "STRONG", "EM", "A", "B", "I", "U", "MARK"]);
|
||
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) {
|
||
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 (!INLINE_WRAP.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 relayout(root, finder) {
|
||
root.querySelectorAll("jz-char." + LE).forEach((e) => e.classList.remove(LE));
|
||
const chars = Array.from(root.querySelectorAll("jz-char")).filter(
|
||
(c) => finder.inScope(c) && !finder.isAvoided(c)
|
||
);
|
||
for (const el of chars) {
|
||
if (!finder.featureEnabledFor(el, "jiya")) {
|
||
continue;
|
||
}
|
||
const bd = bdClassOf(el);
|
||
if (bd !== "bd-open" && bd !== "bd-close") {
|
||
continue;
|
||
}
|
||
const r = firstRect(el);
|
||
if (!r) {
|
||
continue;
|
||
}
|
||
if (bd === "bd-open") {
|
||
const prev = edgeRect(el.previousSibling, "prev", el);
|
||
if (!prev || prev.top < r.top - EPS) {
|
||
el.classList.add(LE);
|
||
}
|
||
} else {
|
||
const next = edgeRect(el.nextSibling, "next", el);
|
||
if (!next || next.top > r.top + EPS) {
|
||
el.classList.add(LE);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var lineEdgePass = {
|
||
name: "jiyaLineEdge",
|
||
kind: "standalone",
|
||
order: 90,
|
||
enabled: (o) => o.features.jiya,
|
||
render(ctx) {
|
||
if (!hasLayout()) {
|
||
return;
|
||
}
|
||
installLayout(ctx.root, "lineEdge", () => relayout(ctx.root, ctx.finder));
|
||
},
|
||
revert(ctx) {
|
||
uninstallLayout(ctx.root, "lineEdge");
|
||
ctx.root.querySelectorAll("jz-char." + LE).forEach((e) => e.classList.remove(LE));
|
||
}
|
||
};
|
||
var TRIM = "jz-hws-trim";
|
||
function trimGaps(root, finder) {
|
||
root.querySelectorAll("jz-hws." + TRIM).forEach((e) => e.classList.remove(TRIM));
|
||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||
(g) => finder.inScope(g) && !finder.isAvoided(g)
|
||
);
|
||
for (const g of gaps) {
|
||
const r = firstRect(g);
|
||
if (!r) {
|
||
continue;
|
||
}
|
||
const next = edgeRect(g.nextSibling, "next", g);
|
||
if (!next || next.top > r.top + EPS) {
|
||
g.classList.add(TRIM);
|
||
}
|
||
}
|
||
}
|
||
var gapTrimPass = {
|
||
name: "spacingGapTrim",
|
||
kind: "standalone",
|
||
order: 92,
|
||
enabled: (o) => o.features.spacing,
|
||
render(ctx) {
|
||
if (!hasLayout()) {
|
||
return;
|
||
}
|
||
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder));
|
||
},
|
||
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,
|
||
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/spacing.ts
|
||
var PASS4 = "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 STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||
var DOTS_CJK = new RegExp("([\\.]{2,}|\u2026)([" + CJK + "])", "g");
|
||
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||
var CJK_TILDE = new RegExp("([" + CJK + "])(~+)(?!=)(?=[" + CJK + ALNUM + "])", "g");
|
||
var CJK_TILDE_EQUALS = new RegExp("([" + CJK + "])(~=)", "g");
|
||
var CJK_PERIOD = new RegExp("([" + CJK + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK + ALNUM + "])", "g");
|
||
var AN_PERIOD_CJK = new RegExp("([" + ALNUM + "])(\\.)([" + CJK + "])", "g");
|
||
var AN_COLON_CJK = new RegExp("([" + ALNUM + "])(:)([" + CJK + "])", "g");
|
||
var CJK_QUOTE = new RegExp("([" + CJK + "])([" + QUOTES_CLS + "])", "g");
|
||
var QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + CJK + "])", "g");
|
||
var QUOTE_AN = new RegExp("([\u201D])([" + ALNUM + "])", "g");
|
||
var CJK_QUOTE_AN = new RegExp("([" + CJK + '])(")([' + ALNUM + "])", "g");
|
||
var CJK_HASH = new RegExp("([" + CJK + "])(#([^ ]))", "g");
|
||
var HASH_CJK = new RegExp("(([^ ])#)([" + CJK + "])", "g");
|
||
var SINGLE_LETTER_GRADE = new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK + "])", "g");
|
||
var CJK_OPERATOR_ANS = new RegExp("([" + CJK + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g");
|
||
var ANS_OPERATOR_CJK = new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK + "])", "g");
|
||
var CJK_LESS_THAN = new RegExp("([" + CJK + "])(<)([" + ALNUM + "])", "g");
|
||
var LESS_THAN_CJK = new RegExp("([" + ALNUM + "])(<)([" + CJK + "])", "g");
|
||
var CJK_GREATER_THAN = new RegExp("([" + CJK + "])(>)([" + ALNUM + "])", "g");
|
||
var GREATER_THAN_CJK = new RegExp("([" + ALNUM + "])(>)([" + CJK + "])", "g");
|
||
var CJK_LEFT_BRACKET = new RegExp("([" + CJK + "])([" + LBR_EXT + "])", "g");
|
||
var RIGHT_BRACKET_CJK = new RegExp("([" + RBR_EXT + "])([" + CJK + "])", "g");
|
||
var AN_LEFT_BRACKET = new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g");
|
||
var RIGHT_BRACKET_AN = new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g");
|
||
var CJK_ANS = new RegExp("([" + CJK + "])([" + ANS_AFTER_CJK + "])", "g");
|
||
var ANS_CJK = new RegExp("([" + ANS_BEFORE_CJK + "])([" + CJK + "])", "g");
|
||
var 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) {
|
||
if (text.length <= 1 || !ANY_CJK.test(text)) {
|
||
return text;
|
||
}
|
||
let s = text;
|
||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||
s = s.replace(CJK_TILDE, "$1$2" + MARK);
|
||
s = s.replace(CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||
s = s.replace(CJK_PERIOD, "$1$2" + MARK);
|
||
s = s.replace(AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||
s = s.replace(AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||
s = s.replace(CJK_QUOTE, "$1" + MARK + "$2");
|
||
s = s.replace(QUOTE_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(QUOTE_AN, "$1" + MARK + "$2");
|
||
s = s.replace(CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||
s = s.replace(CJK_HASH, "$1" + MARK + "$2");
|
||
s = s.replace(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(SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||
s = s.replace(CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||
s = s.replace(PH_PATTERN, (_m, idx) => compounds[Number(idx)] || "");
|
||
s = s.replace(CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||
s = s.replace(RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||
s = s.replace(RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||
s = s.replace(CJK_ANS, "$1" + MARK + "$2");
|
||
s = s.replace(ANS_CJK, "$1" + MARK + "$2");
|
||
s = s.replace(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", PASS4, "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 processBlock(block, finder) {
|
||
const runs = finder.collectRuns(block);
|
||
for (const run of runs) {
|
||
const { text, map } = run;
|
||
const spaced = panguSpace(text);
|
||
if (spaced === text) {
|
||
continue;
|
||
}
|
||
const actions = [];
|
||
let origIdx = 0;
|
||
let spacedIdx = 0;
|
||
while (spacedIdx < spaced.length) {
|
||
const sc = spaced.charAt(spacedIdx);
|
||
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.before) {
|
||
continue;
|
||
}
|
||
if (act.space) {
|
||
const tn = act.space.textNode;
|
||
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
||
}
|
||
wrapGapLeft(act.before);
|
||
}
|
||
}
|
||
}
|
||
var PILL_NEIGHBOR = new RegExp("[A-Za-z0-9" + CJK + "]");
|
||
function processPills(root, finder, pillSelector) {
|
||
root.querySelectorAll(pillSelector).forEach((pill) => {
|
||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||
return;
|
||
}
|
||
if (!pill.parentNode) {
|
||
return;
|
||
}
|
||
const prev = finder.adjacentLogicalChar(pill, -1);
|
||
if (prev) {
|
||
handlePillSide(prev, pill, -1);
|
||
}
|
||
const next = finder.adjacentLogicalChar(pill, 1);
|
||
if (next) {
|
||
handlePillSide(next, pill, 1);
|
||
}
|
||
});
|
||
}
|
||
function handlePillSide(adj, pill, dir) {
|
||
if (dir === -1) {
|
||
if (PILL_NEIGHBOR.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 !== " " && PILL_NEIGHBOR.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 (PILL_NEIGHBOR.test(adj.ch)) {
|
||
wrapNodeInGap(pill);
|
||
return;
|
||
}
|
||
if (adj.ch === " " && adj.textNode.data.length >= 2) {
|
||
const ac = adj.textNode.data.charAt(1);
|
||
if (ac !== " " && PILL_NEIGHBOR.test(ac)) {
|
||
adj.textNode.data = adj.textNode.data.slice(1);
|
||
wrapNodeInGap(pill);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var spacingPass = {
|
||
name: PASS4,
|
||
kind: "standalone",
|
||
order: 50,
|
||
enabled: (o) => o.features.spacing,
|
||
render(ctx) {
|
||
const { finder, options } = ctx;
|
||
ctx.finder.eachBlock(ctx.root, (block) => {
|
||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||
return;
|
||
}
|
||
processBlock(block, finder);
|
||
});
|
||
processPills(ctx.root, finder, options.finder.pillSelector);
|
||
},
|
||
revert(ctx) {
|
||
revertPass(PASS4, ctx.root);
|
||
}
|
||
};
|
||
|
||
// src/compat/v1.ts
|
||
function mapLongWord(v) {
|
||
if (v === false) {
|
||
return false;
|
||
}
|
||
if (v === void 0 || v === true || v === null) {
|
||
return true;
|
||
}
|
||
const out = {};
|
||
if (typeof v.minLength === "number") {
|
||
out.minLength = v.minLength;
|
||
}
|
||
if (typeof v.lang === "string") {
|
||
out.lang = v.lang;
|
||
}
|
||
return out;
|
||
}
|
||
function createCjkAutospace(options = {}) {
|
||
const o = options || {};
|
||
const mapped = {
|
||
// finder 設定直通。
|
||
...o.styleInlines ? { styleInlines: o.styleInlines } : {},
|
||
...o.blockTags ? { blockTags: o.blockTags } : {},
|
||
...o.skipTags ? { skipTags: o.skipTags } : {},
|
||
pillSelector: o.pillSelector || "code, kbd",
|
||
autospaceClass: o.autospaceClass || "cjk-autospace",
|
||
skipAttribute: o.skipAttribute || "data-md-key",
|
||
...o.isSkipped ? { isSkipped: o.isSkipped } : {},
|
||
spacing: true,
|
||
longWord: mapLongWord(o.longWordWrap),
|
||
// v1 Pass D 不分 lang 地擠句內點號(開明式);compat 強制 kaiming
|
||
// 政策以避免功能倒退(預設 zh-Hant→quanjiao 不擠壓)。
|
||
jiya: o.punctuationSqueeze === false ? false : true,
|
||
// v1 無連續標點綁定,保持 DOM 與 v1 一致:關閉 jinze。
|
||
jinze: false,
|
||
lang: {
|
||
policy: {
|
||
"zh-Hant": "kaiming",
|
||
"zh-Hans": "kaiming",
|
||
"ja": "kaiming",
|
||
"other": "kaiming"
|
||
}
|
||
}
|
||
};
|
||
const jz = createJuzhen(mapped);
|
||
return {
|
||
apply(root) {
|
||
jz.render(root);
|
||
},
|
||
revert(root) {
|
||
jz.revert(root);
|
||
}
|
||
};
|
||
}
|
||
|
||
// src/index.ts
|
||
var DEFAULT_STYLE_INLINES = [
|
||
"STRONG",
|
||
"EM",
|
||
"A",
|
||
"B",
|
||
"I",
|
||
"U",
|
||
"S",
|
||
"MARK",
|
||
"INS",
|
||
"DEL",
|
||
"SUB",
|
||
"SUP",
|
||
"SMALL",
|
||
"ABBR",
|
||
"CITE",
|
||
"DFN",
|
||
"Q",
|
||
"SPAN",
|
||
"VAR",
|
||
"SAMP",
|
||
"TIME",
|
||
"BDO",
|
||
"BDI",
|
||
"RUBY",
|
||
"RB",
|
||
"RT",
|
||
"RP",
|
||
"WBR"
|
||
];
|
||
var DEFAULT_BLOCK_TAGS = [
|
||
"BODY",
|
||
"P",
|
||
"DIV",
|
||
"H1",
|
||
"H2",
|
||
"H3",
|
||
"H4",
|
||
"H5",
|
||
"H6",
|
||
"LI",
|
||
"DT",
|
||
"DD",
|
||
"BLOCKQUOTE",
|
||
"FIGURE",
|
||
"FIGCAPTION",
|
||
"TD",
|
||
"TH",
|
||
"CAPTION",
|
||
"SUMMARY",
|
||
"DETAILS",
|
||
"HEADER",
|
||
"FOOTER",
|
||
"SECTION",
|
||
"ARTICLE",
|
||
"ASIDE",
|
||
"NAV",
|
||
"MAIN",
|
||
"ADDRESS",
|
||
"PRE",
|
||
"UL",
|
||
"OL",
|
||
"DL",
|
||
"TABLE",
|
||
"TR",
|
||
"THEAD",
|
||
"TBODY",
|
||
"TFOOT",
|
||
"FORM",
|
||
"FIELDSET",
|
||
"LEGEND",
|
||
"LABEL",
|
||
"BR",
|
||
"HR"
|
||
];
|
||
var DEFAULT_SKIP_TAGS = [
|
||
"CODE",
|
||
"KBD",
|
||
"PRE",
|
||
"SAMP",
|
||
"TT",
|
||
"VAR",
|
||
"SCRIPT",
|
||
"STYLE",
|
||
"TEXTAREA",
|
||
"INPUT"
|
||
];
|
||
var PASSES = [
|
||
jiyaPass,
|
||
jiyaAdjacencyPass,
|
||
longWordPass,
|
||
spacingPass,
|
||
jinzePass,
|
||
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 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 || {}
|
||
},
|
||
autospaceClass: opts.autospaceClass || "",
|
||
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),
|
||
hanging: feature(opts.hanging, false),
|
||
biaodian: feature(opts.biaodian, false),
|
||
emphasis: feature(opts.emphasis, false),
|
||
ruby: feature(opts.ruby, false)
|
||
},
|
||
longWord: resolveLongWord(opts.longWord),
|
||
blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [],
|
||
finder: {
|
||
styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES),
|
||
blockTags: new Set(opts.blockTags || DEFAULT_BLOCK_TAGS),
|
||
skipTags: new Set(opts.skipTags || DEFAULT_SKIP_TAGS),
|
||
pillSelector: opts.pillSelector || "code, kbd",
|
||
skipAttribute: opts.skipAttribute || null,
|
||
isSkipped: typeof opts.isSkipped === "function" ? opts.isSkipped : null
|
||
}
|
||
};
|
||
}
|
||
function resolveRoot(arg, options) {
|
||
const pick = arg !== void 0 ? arg : options.scope.root;
|
||
if (pick && typeof pick !== "string") {
|
||
return pick;
|
||
}
|
||
if (typeof pick === "string" && typeof document !== "undefined") {
|
||
return document.querySelector(pick);
|
||
}
|
||
return typeof document !== "undefined" ? document.body : null;
|
||
}
|
||
function createJuzhen(opts = {}) {
|
||
const options = normalizeOptions(opts);
|
||
function makeCtx(root) {
|
||
return { root, options, finder: new Finder(options) };
|
||
}
|
||
return {
|
||
options,
|
||
render(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
runPasses(PASSES, makeCtx(el));
|
||
},
|
||
revert(root) {
|
||
const el = resolveRoot(root, options);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
revertPasses(PASSES, makeCtx(el));
|
||
}
|
||
};
|
||
}
|
||
return __toCommonJS(index_exports);
|
||
})();
|
||
if(typeof globalThis!=='undefined'){globalThis.Juzhen=Juzhen;if(Juzhen&&Juzhen.createCjkAutospace&&!globalThis.createCjkAutospace){globalThis.createCjkAutospace=Juzhen.createCjkAutospace;}}
|