fix: 行內排版透明性——pill 前標點誤隙+行內元素整體掉行+審計(Bug1/Bug2/T4-T6)
統一「行內元素對排版透明」原則在各遍歷原語之口徑,修復兩個下游 Bug 及三項同根隱患。
Bug1(pill 前標點誤加間隙):
isTransparentInline 補 JZ-CHAR/JZ-INNER。`漢字。<code>` 經禁則綁定後,
adjacentLogicalChar 之 descend 遇不透明 jz-char 既不進入亦不終止、而跳過它越過
標點取到「字」誤補 jz-hws;透明後回傳標點「。」(非 pill 鄰字)→ 不補隙,與
collectRuns 既有透明遞迴對齊。
Bug2(行內樣式元素整體掉行):
新增 core/split.ts 之 isolateBoundaryToken/splitElementAt——沿祖先鏈逐層把邊界
詞元剝入淺克隆(保留標籤/class/style/lang、去 id;標 data-jz-kind="splitoff"),
jinze/orphan 改最小綁定:`<strong>長串</strong>,` 只綁末字+逗號(非整串入 nowrap
inline-block 致行末整體掉行);段末長 strong 不再整綁溢出容器。revertPass 增延後
階段把 splitoff 併回原元素(先解包 wrap、後併 splitoff),render→revert 還原一致。
純樣式標籤白名單可切,a/abbr/ruby/帶 id 退化整綁。
審計(同根隱患):
T4 lineedge edgeRect 攀爬集改由 options.finder.styleInlines 派生(棄硬編碼子集,
免遺漏 ins/del/sub 等致行端誤判)。
T5 jiyaAdjacency 遇 pill(pillMatches)斷相鄰、不穿越(內容對排版不可見)。
T6 spacing processPills 補 per-node 功能閘(data-juzhen-off=spacing 區域內不補隙)。
文檔:README 切片表(spacing/jinze/orphan 三行)+ARCHITECTURE §4.1 透明性原則、
§4.4 splitoff 還原、§6.4 行內元素最小綁定、§5 jz-orphan 詞彙。
測試 90→102(Bug1×4、Bug2×6、T5/T6×2)。headless Chrome 實證 Bug2:段末 strong
最小切分、orphan 寬 60px 不溢出版心。
This commit is contained in:
Vendored
+253
-34
@@ -48,10 +48,18 @@ var Juzhen = (() => {
|
||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||
}
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
||||
* 鄰字仍須可達;I7)。
|
||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
isTransparentInline(name) {
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE";
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
@@ -471,8 +479,11 @@ var Juzhen = (() => {
|
||||
if (!parent) {
|
||||
continue;
|
||||
}
|
||||
touched.add(parent);
|
||||
const kind = el.getAttribute(KIND_ATTR);
|
||||
if (kind === "splitoff") {
|
||||
continue;
|
||||
}
|
||||
touched.add(parent);
|
||||
if (kind === "marker") {
|
||||
parent.removeChild(el);
|
||||
} else {
|
||||
@@ -482,6 +493,30 @@ var Juzhen = (() => {
|
||||
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();
|
||||
@@ -803,6 +838,8 @@ var Juzhen = (() => {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.pillMatches(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
@@ -820,27 +857,60 @@ var Juzhen = (() => {
|
||||
}
|
||||
};
|
||||
|
||||
// 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";
|
||||
// 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",
|
||||
"MARK",
|
||||
"SMALL",
|
||||
"INS",
|
||||
"DEL",
|
||||
"SUB",
|
||||
"SUP"
|
||||
]);
|
||||
function canSplit(el) {
|
||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id");
|
||||
}
|
||||
function forbiddenBreak(a, b, finder) {
|
||||
if (isCharEl(b)) {
|
||||
const c = bdClassOf(b);
|
||||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(b, PASS2)) {
|
||||
return true;
|
||||
function firstText(el) {
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
const r = firstText(c);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isCharEl(a)) {
|
||||
const c = bdClassOf(a);
|
||||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(a, PASS2)) {
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
function lastText(el) {
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
const r = lastText(c);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
function isTokenChar(ch, anyCjk) {
|
||||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||||
@@ -866,16 +936,135 @@ var Juzhen = (() => {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function splitTreeAfter(top, textNode, offset, pass) {
|
||||
if (offset <= 0 && firstText(top) === 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) {
|
||||
if (!canSplit(el)) {
|
||||
return el;
|
||||
}
|
||||
const text = side === "tail" ? lastText(el) : firstText(el);
|
||||
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);
|
||||
if (!clone) {
|
||||
return el;
|
||||
}
|
||||
return side === "tail" ? clone : el;
|
||||
}
|
||||
function splitElementAt(el, textNode, offset, pass) {
|
||||
if (!canSplit(el)) {
|
||||
return null;
|
||||
}
|
||||
return splitTreeAfter(el, textNode, offset, pass);
|
||||
}
|
||||
|
||||
// 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 = leadingTokenEnd(node.data, anyCjk);
|
||||
const e = leadingTokenEnd2(node.data, anyCjk);
|
||||
if (e < node.data.length) {
|
||||
node = node.splitText(e);
|
||||
}
|
||||
}
|
||||
if (needLast) {
|
||||
const s = trailingTokenStart(node.data, anyCjk);
|
||||
const s = trailingTokenStart2(node.data, anyCjk);
|
||||
if (s > 0) {
|
||||
node.splitText(s);
|
||||
}
|
||||
@@ -905,13 +1094,21 @@ var Juzhen = (() => {
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
if (needFirst) {
|
||||
isolateBoundaryToken(el, "head", anyCjk, PASS2);
|
||||
}
|
||||
}
|
||||
}
|
||||
const kids = [];
|
||||
@@ -1004,7 +1201,14 @@ var Juzhen = (() => {
|
||||
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"]);
|
||||
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;
|
||||
@@ -1028,7 +1232,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function edgeRect(start, side, from) {
|
||||
function edgeRect(start, side, from, wrapSet) {
|
||||
let n = start;
|
||||
let parentRef = from.parentNode;
|
||||
for (; ; ) {
|
||||
@@ -1042,7 +1246,7 @@ var Juzhen = (() => {
|
||||
if (!parentRef || parentRef.nodeType !== 1) {
|
||||
return null;
|
||||
}
|
||||
if (!INLINE_WRAP.has(parentRef.nodeName)) {
|
||||
if (!wrapSet.has(parentRef.nodeName)) {
|
||||
return null;
|
||||
}
|
||||
n = side === "prev" ? parentRef.previousSibling : parentRef.nextSibling;
|
||||
@@ -1087,6 +1291,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
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)
|
||||
);
|
||||
@@ -1120,12 +1325,12 @@ var Juzhen = (() => {
|
||||
continue;
|
||||
}
|
||||
if (isHead) {
|
||||
const prev = edgeRect(el.previousSibling, "prev", el);
|
||||
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
||||
if (!prev || higherLine(r, prev)) {
|
||||
decisions.push({ el, edge: "head", mode });
|
||||
}
|
||||
} else {
|
||||
const next = edgeRect(el.nextSibling, "next", el);
|
||||
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
||||
if (!next || lowerLine(r, next)) {
|
||||
decisions.push({ el, edge: "tail", mode });
|
||||
}
|
||||
@@ -1153,7 +1358,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
};
|
||||
var TRIM = "jz-hws-trim";
|
||||
function trimGaps(root, finder) {
|
||||
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")
|
||||
);
|
||||
@@ -1163,7 +1368,7 @@ var Juzhen = (() => {
|
||||
gaps.forEach((g) => g.classList.add(TRIM));
|
||||
for (const g of gaps) {
|
||||
const r = lastRect(g);
|
||||
const next = r ? edgeRect(g.nextSibling, "next", g) : null;
|
||||
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);
|
||||
@@ -1180,7 +1385,8 @@ var Juzhen = (() => {
|
||||
if (!hasLayout()) {
|
||||
return;
|
||||
}
|
||||
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder));
|
||||
const wrapSet = buildWrapSet(ctx.options);
|
||||
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder, wrapSet));
|
||||
},
|
||||
revert(ctx) {
|
||||
uninstallLayout(ctx.root, "gapTrim");
|
||||
@@ -1300,6 +1506,16 @@ var Juzhen = (() => {
|
||||
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
|
||||
);
|
||||
if (clone) {
|
||||
startChild = clone;
|
||||
}
|
||||
}
|
||||
const orphan = createJz("jz-orphan", PASS4, "wrap");
|
||||
block.insertBefore(orphan, startChild);
|
||||
@@ -1510,6 +1726,9 @@ var Juzhen = (() => {
|
||||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
}
|
||||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||||
return;
|
||||
}
|
||||
if (!pill.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vendored
+253
-34
@@ -20,10 +20,18 @@ var Finder = class {
|
||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||
}
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
||||
* 鄰字仍須可達;I7)。
|
||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
isTransparentInline(name) {
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE";
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
@@ -443,8 +451,11 @@ function revertPass(pass, root) {
|
||||
if (!parent) {
|
||||
continue;
|
||||
}
|
||||
touched.add(parent);
|
||||
const kind = el.getAttribute(KIND_ATTR);
|
||||
if (kind === "splitoff") {
|
||||
continue;
|
||||
}
|
||||
touched.add(parent);
|
||||
if (kind === "marker") {
|
||||
parent.removeChild(el);
|
||||
} else {
|
||||
@@ -454,6 +465,30 @@ function revertPass(pass, root) {
|
||||
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();
|
||||
@@ -775,6 +810,8 @@ var jiyaAdjacencyPass = {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.pillMatches(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
@@ -792,27 +829,60 @@ var jiyaAdjacencyPass = {
|
||||
}
|
||||
};
|
||||
|
||||
// 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";
|
||||
// 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",
|
||||
"MARK",
|
||||
"SMALL",
|
||||
"INS",
|
||||
"DEL",
|
||||
"SUB",
|
||||
"SUP"
|
||||
]);
|
||||
function canSplit(el) {
|
||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id");
|
||||
}
|
||||
function forbiddenBreak(a, b, finder) {
|
||||
if (isCharEl(b)) {
|
||||
const c = bdClassOf(b);
|
||||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(b, PASS2)) {
|
||||
return true;
|
||||
function firstText(el) {
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
const r = firstText(c);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isCharEl(a)) {
|
||||
const c = bdClassOf(a);
|
||||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(a, PASS2)) {
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
function lastText(el) {
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
const r = lastText(c);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
function isTokenChar(ch, anyCjk) {
|
||||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||||
@@ -838,16 +908,135 @@ function trailingTokenStart(d, anyCjk) {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function splitTreeAfter(top, textNode, offset, pass) {
|
||||
if (offset <= 0 && firstText(top) === 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) {
|
||||
if (!canSplit(el)) {
|
||||
return el;
|
||||
}
|
||||
const text = side === "tail" ? lastText(el) : firstText(el);
|
||||
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);
|
||||
if (!clone) {
|
||||
return el;
|
||||
}
|
||||
return side === "tail" ? clone : el;
|
||||
}
|
||||
function splitElementAt(el, textNode, offset, pass) {
|
||||
if (!canSplit(el)) {
|
||||
return null;
|
||||
}
|
||||
return splitTreeAfter(el, textNode, offset, pass);
|
||||
}
|
||||
|
||||
// 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 = leadingTokenEnd(node.data, anyCjk);
|
||||
const e = leadingTokenEnd2(node.data, anyCjk);
|
||||
if (e < node.data.length) {
|
||||
node = node.splitText(e);
|
||||
}
|
||||
}
|
||||
if (needLast) {
|
||||
const s = trailingTokenStart(node.data, anyCjk);
|
||||
const s = trailingTokenStart2(node.data, anyCjk);
|
||||
if (s > 0) {
|
||||
node.splitText(s);
|
||||
}
|
||||
@@ -877,13 +1066,21 @@ function processParent(parent, finder, anyCjk) {
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
if (needFirst) {
|
||||
isolateBoundaryToken(el, "head", anyCjk, PASS2);
|
||||
}
|
||||
}
|
||||
}
|
||||
const kids = [];
|
||||
@@ -976,7 +1173,14 @@ function uninstallLayout(root, key) {
|
||||
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"]);
|
||||
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;
|
||||
@@ -1000,7 +1204,7 @@ function rectOf(n, side) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function edgeRect(start, side, from) {
|
||||
function edgeRect(start, side, from, wrapSet) {
|
||||
let n = start;
|
||||
let parentRef = from.parentNode;
|
||||
for (; ; ) {
|
||||
@@ -1014,7 +1218,7 @@ function edgeRect(start, side, from) {
|
||||
if (!parentRef || parentRef.nodeType !== 1) {
|
||||
return null;
|
||||
}
|
||||
if (!INLINE_WRAP.has(parentRef.nodeName)) {
|
||||
if (!wrapSet.has(parentRef.nodeName)) {
|
||||
return null;
|
||||
}
|
||||
n = side === "prev" ? parentRef.previousSibling : parentRef.nextSibling;
|
||||
@@ -1059,6 +1263,7 @@ function modeForChar(el, options) {
|
||||
}
|
||||
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)
|
||||
);
|
||||
@@ -1092,12 +1297,12 @@ function relayout(root, finder, options) {
|
||||
continue;
|
||||
}
|
||||
if (isHead) {
|
||||
const prev = edgeRect(el.previousSibling, "prev", el);
|
||||
const prev = edgeRect(el.previousSibling, "prev", el, wrapSet);
|
||||
if (!prev || higherLine(r, prev)) {
|
||||
decisions.push({ el, edge: "head", mode });
|
||||
}
|
||||
} else {
|
||||
const next = edgeRect(el.nextSibling, "next", el);
|
||||
const next = edgeRect(el.nextSibling, "next", el, wrapSet);
|
||||
if (!next || lowerLine(r, next)) {
|
||||
decisions.push({ el, edge: "tail", mode });
|
||||
}
|
||||
@@ -1125,7 +1330,7 @@ var lineEdgePass = {
|
||||
}
|
||||
};
|
||||
var TRIM = "jz-hws-trim";
|
||||
function trimGaps(root, finder) {
|
||||
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")
|
||||
);
|
||||
@@ -1135,7 +1340,7 @@ function trimGaps(root, finder) {
|
||||
gaps.forEach((g) => g.classList.add(TRIM));
|
||||
for (const g of gaps) {
|
||||
const r = lastRect(g);
|
||||
const next = r ? edgeRect(g.nextSibling, "next", g) : null;
|
||||
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);
|
||||
@@ -1152,7 +1357,8 @@ var gapTrimPass = {
|
||||
if (!hasLayout()) {
|
||||
return;
|
||||
}
|
||||
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder));
|
||||
const wrapSet = buildWrapSet(ctx.options);
|
||||
installLayout(ctx.root, "gapTrim", () => trimGaps(ctx.root, ctx.finder, wrapSet));
|
||||
},
|
||||
revert(ctx) {
|
||||
uninstallLayout(ctx.root, "gapTrim");
|
||||
@@ -1272,6 +1478,16 @@ function processBlock(block, finder, blockSet, anyCjk, biaodian, chars) {
|
||||
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
|
||||
);
|
||||
if (clone) {
|
||||
startChild = clone;
|
||||
}
|
||||
}
|
||||
const orphan = createJz("jz-orphan", PASS4, "wrap");
|
||||
block.insertBefore(orphan, startChild);
|
||||
@@ -1482,6 +1698,9 @@ function processPills(root, finder, pillSelector, pillNeighbor) {
|
||||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
}
|
||||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||||
return;
|
||||
}
|
||||
if (!pill.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user