refactor: 專案整理階段二——透明判定單一真相源+跨透明邊界避頭尾+avoid 排除歸一
承階段一之下游審計。核心貫穿問題:「透明穿越」判定實有兩套口徑散落各處,抵觸 finder 檔頭「所有遍歷共用同一判定」之聲明。用戶抉擇:**嚴格白名單**(未知行內標籤=邊界, 透明須顯式登記 styleInlines)。 透明判定單一真相源(2a): ・transparentInline 提升為公開 isTransparent,作邏輯文本流之單一透明判定。 ・collectRuns 改由 isTransparent 判定——未在 styleInlines 之未知行內標籤(如 <button>/自訂 元素)當隔離式邊界(斷 run、內部自成脈絡);isolate 與未知行內併為同一分支。 ・jiyaAdjacency 同改用 finder.isTransparent;adjacentLogicalChar 沿用(僅隨改名)。 ・分層澄清(承 #4 教訓,不強行一統):邏輯文本流統一為 isTransparent;另兩層各有正當之 不同邊界、刻意不併入——結構切分安全(split canSplit:更窄白名單,<a>/ABBR 不可切)與 物理版面(lineedge:getClientRects 物理相鄰,須跨 isolate 找真實鄰接)。檔頭改分層表述。 跨透明元素邊界避頭尾綁定(2b,修下游審計 #3): ・避頭類標點若落於透明元素邊緣(「你好<strong>。世界</strong>」之 。位於 strong 首位、與 「好」不同 DOM parent),舊版 forbiddenBreak 僅認直接 jz-char、render 僅處理 jz-char 之直接 父(strong),外層 p 之「好↔strong」綁定從不被評估 → 。漏綁、可誤置行首。 ・偵測:edgeCharEl 經 finder.isTransparent 穿越透明元素定位邊緣 jz-char。 ・父級處理:render 沿透明元素邊緣上攀納入外層 parent,父集深者先處理。綁定端沿用 isolateBoundaryToken(可切則最小綁定;邊界字為已 charify 之 jz-char 無法越過時退化整綁, 安全、與 <a> 同型)。 avoid 排除機制歸一(2c): ・isAvoided 改單一組合入口(自身即邊界 或 祖先鏈任一邊界),免除「avoidsSelf ‖ isAvoided」 易漏一半之誤用(階段一 #2 即此類);avoidsSelf 保留供遍歷原語逐元素下探自判(免 O(n²))。 ・processPills/orphan/spacing root 收斂為 isAvoided(x);doc 補齊七種 avoid 來源與優先序。 一致性小項(2e):scope.include 型別去多餘 `| null`;PASSES 陣列補註(執行序由 pass.order 分群排序決定);gapTrim 補 featureEnabledFor("spacing"),與 lineEdge relayout 逐節點閘對稱。 評估後不做(2d):五處幂等判斷(jiya 祖先/longword 父/spacing 父/orphan 子/jinze 父)檢查 不同位置關係、無單一 isJz 形狀可全覆蓋;真正缺陷(jinze 二次 render 巢狀)已於階段一以 isJz 修復,其餘各自正確,強行歸一徒增風險(承 #4 教訓:正當差異不硬併)。 新增契約/回歸測試(button 嚴格白名單、跨透明邊界避頭);121 測試全綠、構建乾淨。
This commit is contained in:
Vendored
+17
-11
@@ -53,15 +53,21 @@ export declare class Finder {
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
private transparentInline;
|
||||
/** 該元素**自身**是否為 avoid 邊界(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / skipAttribute / avoid 選擇器 / isSkipped)。isAvoided 沿祖先
|
||||
* 鏈逐層呼叫本判定;遍歷原語另須對「正要進入」之元素直接判自身——舊版僅查
|
||||
* 祖先,元素自身命中 avoid 時被當透明穿越(下游問題六:scope.avoid 對 .katex
|
||||
* 形同虛設)。 */
|
||||
isTransparent(el: Element): boolean;
|
||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||
* ③ SVG 命名空間 ④ contentEditable
|
||||
* ⑤ data-jz-skip 屬性(內建) ⑥ skipAttribute(消費端指定屬性名)
|
||||
* ⑦ scope.avoid 選擇器
|
||||
* **此為自身判定**,供遍歷原語(collectRuns/jiyaAdjacency/canSplit/isTransparent)於
|
||||
* 下探時逐元素自判(祖先由遞迴涵蓋)。**一次性**問「某節點是否被 avoid(自身或祖先)」
|
||||
* 請改用 isAvoided(單一組合入口),勿只用本方法而漏判祖先、或只用 isAvoided 而漏判自身
|
||||
* (下游問題六/階段一 #2:漏一半致 avoid 對自身形同虛設)。 */
|
||||
avoidsSelf(el: Element): boolean;
|
||||
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
|
||||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||||
/** 某節點是否被 avoid——**自身即邊界**(元素經 avoidsSelf 之七源;非元素節點僅判
|
||||
* isSkipped)**或**祖先鏈上任一為 avoid 邊界。此為「是否 avoid」之**單一組合入口(安全
|
||||
* 預設)**:一次性檢查用此,免除「avoidsSelf ‖ isAvoided」易漏一半之誤用。遍歷原語逐
|
||||
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
|
||||
isAvoided(node: Node): boolean;
|
||||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||||
inScope(node: Node): boolean;
|
||||
@@ -70,9 +76,9 @@ export declare class Finder {
|
||||
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
|
||||
eachBlock(root: Element, cb: (block: Element) => void): void;
|
||||
/**
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
|
||||
* jz-* 透明穿越;block/pill/avoid/間隙 marker(jz-hws)中斷 run。
|
||||
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):透明行內(isTransparent:styleInlines
|
||||
* + wrap 類 jz-*)穿越、run 延續;block/pill/avoid 中斷 run;isolate 與**未知行內
|
||||
* 標籤**(嚴格白名單)中斷 run 且內部自成獨立 run。回傳 text.length >= 2 之 run。
|
||||
*/
|
||||
collectRuns(block: Element): LogicalRun[];
|
||||
/**
|
||||
|
||||
Vendored
+114
-53
@@ -79,18 +79,22 @@ var Juzhen = (() => {
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
transparentInline(el) {
|
||||
isTransparent(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
|
||||
* 形同虛設)。 */
|
||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||
* ③ SVG 命名空間 ④ contentEditable
|
||||
* ⑤ data-jz-skip 屬性(內建) ⑥ skipAttribute(消費端指定屬性名)
|
||||
* ⑦ scope.avoid 選擇器
|
||||
* **此為自身判定**,供遍歷原語(collectRuns/jiyaAdjacency/canSplit/isTransparent)於
|
||||
* 下探時逐元素自判(祖先由遞迴涵蓋)。**一次性**問「某節點是否被 avoid(自身或祖先)」
|
||||
* 請改用 isAvoided(單一組合入口),勿只用本方法而漏判祖先、或只用 isAvoided 而漏判自身
|
||||
* (下游問題六/階段一 #2:漏一半致 avoid 對自身形同虛設)。 */
|
||||
avoidsSelf(el) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(el)) {
|
||||
return true;
|
||||
@@ -115,10 +119,16 @@ var Juzhen = (() => {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
|
||||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||||
/** 某節點是否被 avoid——**自身即邊界**(元素經 avoidsSelf 之七源;非元素節點僅判
|
||||
* isSkipped)**或**祖先鏈上任一為 avoid 邊界。此為「是否 avoid」之**單一組合入口(安全
|
||||
* 預設)**:一次性檢查用此,免除「avoidsSelf ‖ isAvoided」易漏一半之誤用。遍歷原語逐
|
||||
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
|
||||
isAvoided(node) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
if (node.nodeType === 1) {
|
||||
if (this.avoidsSelf(node)) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return true;
|
||||
}
|
||||
let p = node.parentNode;
|
||||
@@ -179,9 +189,9 @@ var Juzhen = (() => {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
|
||||
* jz-* 透明穿越;block/pill/avoid/間隙 marker(jz-hws)中斷 run。
|
||||
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):透明行內(isTransparent:styleInlines
|
||||
* + wrap 類 jz-*)穿越、run 延續;block/pill/avoid 中斷 run;isolate 與**未知行內
|
||||
* 標籤**(嚴格白名單)中斷 run 且內部自成獨立 run。回傳 text.length >= 2 之 run。
|
||||
*/
|
||||
collectRuns(block) {
|
||||
const runs = [];
|
||||
@@ -222,21 +232,21 @@ var Juzhen = (() => {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
if (this.isolateMatches(el)) {
|
||||
flush();
|
||||
let ic = el.firstChild;
|
||||
while (ic) {
|
||||
visit(ic);
|
||||
ic = ic.nextSibling;
|
||||
if (this.isTransparent(el)) {
|
||||
let c2 = el.firstChild;
|
||||
while (c2) {
|
||||
visit(c2);
|
||||
c2 = c2.nextSibling;
|
||||
}
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
let c2 = el.firstChild;
|
||||
while (c2) {
|
||||
visit(c2);
|
||||
c2 = c2.nextSibling;
|
||||
flush();
|
||||
let ic = el.firstChild;
|
||||
while (ic) {
|
||||
visit(ic);
|
||||
ic = ic.nextSibling;
|
||||
}
|
||||
flush();
|
||||
};
|
||||
let c = block.firstChild;
|
||||
while (c) {
|
||||
@@ -268,7 +278,7 @@ var Juzhen = (() => {
|
||||
return pickEnd(n2);
|
||||
}
|
||||
if (n2.nodeType === 1) {
|
||||
if (!this.transparentInline(n2)) {
|
||||
if (!this.isTransparent(n2)) {
|
||||
return "blocked";
|
||||
}
|
||||
const r = descend(n2);
|
||||
@@ -293,7 +303,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
if (n.nodeType === 1) {
|
||||
const e = n;
|
||||
if (!this.transparentInline(e)) {
|
||||
if (!this.isTransparent(e)) {
|
||||
return null;
|
||||
}
|
||||
const r = descend(e);
|
||||
@@ -308,7 +318,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
if (!p || p.nodeType !== 1 || !this.transparentInline(p)) {
|
||||
if (!p || p.nodeType !== 1 || !this.isTransparent(p)) {
|
||||
return null;
|
||||
}
|
||||
const pe = p;
|
||||
@@ -851,8 +861,7 @@ var Juzhen = (() => {
|
||||
level: "text",
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx) {
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
const { finder } = ctx;
|
||||
let prev = null;
|
||||
const squeeze = (L, R) => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
@@ -888,20 +897,14 @@ var Juzhen = (() => {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.pillMatches(el)) {
|
||||
prev = null;
|
||||
} else if (finder.avoidsSelf(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isolateMatches(el)) {
|
||||
} else if (finder.pillMatches(el) || finder.avoidsSelf(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isTransparent(el)) {
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1069,21 +1072,66 @@ var Juzhen = (() => {
|
||||
function isCharEl(n) {
|
||||
return !!n && n.nodeType === 1 && n.nodeName === "JZ-CHAR";
|
||||
}
|
||||
function firstMeaningfulChild(el) {
|
||||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3 && c.data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function lastMeaningfulChild(el) {
|
||||
for (let c = el.lastChild; c; c = c.previousSibling) {
|
||||
if (c.nodeType === 3 && c.data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function atEdge(node, el, side) {
|
||||
return node === (side === "head" ? firstMeaningfulChild(el) : lastMeaningfulChild(el));
|
||||
}
|
||||
function edgeCharEl(node, side, finder) {
|
||||
let n = node;
|
||||
while (n && n.nodeType === 1) {
|
||||
if (isCharEl(n)) {
|
||||
return n;
|
||||
}
|
||||
if (!finder.isTransparent(n)) {
|
||||
return null;
|
||||
}
|
||||
n = side === "head" ? firstMeaningfulChild(n) : lastMeaningfulChild(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function forbiddenBreak(a, b, finder) {
|
||||
if (isCharEl(b)) {
|
||||
const c = bdClassOf(b);
|
||||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(b, PASS2)) {
|
||||
const head = edgeCharEl(b, "head", finder);
|
||||
if (head) {
|
||||
const c = bdClassOf(head);
|
||||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(head, PASS2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isCharEl(a)) {
|
||||
const c = bdClassOf(a);
|
||||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(a, PASS2)) {
|
||||
const tail = edgeCharEl(a, "tail", finder);
|
||||
if (tail) {
|
||||
const c = bdClassOf(tail);
|
||||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(tail, PASS2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function depthOf(node) {
|
||||
let d = 0;
|
||||
let p = node.parentNode;
|
||||
while (p) {
|
||||
d += 1;
|
||||
p = p.parentNode;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
function isTokenChar2(ch, anyCjk) {
|
||||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||||
}
|
||||
@@ -1195,18 +1243,31 @@ var Juzhen = (() => {
|
||||
level: "paragraph",
|
||||
enabled: (o) => o.features.jinze,
|
||||
render(ctx) {
|
||||
const { finder } = 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")) {
|
||||
if (!finder.levelAllows(c, "paragraph")) {
|
||||
return;
|
||||
}
|
||||
if (c.parentNode) {
|
||||
parents.add(c.parentNode);
|
||||
let child = c;
|
||||
let parent = c.parentNode;
|
||||
while (parent && parent.nodeType === 1) {
|
||||
parents.add(parent);
|
||||
const pe = parent;
|
||||
if (!finder.isTransparent(pe)) {
|
||||
break;
|
||||
}
|
||||
if (!atEdge(child, pe, "head") && !atEdge(child, pe, "tail")) {
|
||||
break;
|
||||
}
|
||||
child = pe;
|
||||
parent = pe.parentNode;
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
processParent(parent, ctx.finder, anyCjk);
|
||||
const ordered = Array.from(parents).sort((a, b) => depthOf(b) - depthOf(a));
|
||||
for (const parent of ordered) {
|
||||
processParent(parent, finder, anyCjk);
|
||||
}
|
||||
},
|
||||
revert(ctx) {
|
||||
@@ -1388,7 +1449,7 @@ var Juzhen = (() => {
|
||||
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")
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing")
|
||||
);
|
||||
if (!gaps.length) {
|
||||
return;
|
||||
@@ -1574,7 +1635,7 @@ var Juzhen = (() => {
|
||||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||||
return;
|
||||
}
|
||||
if (finder.avoidsSelf(block) || finder.isAvoided(block)) {
|
||||
if (finder.isAvoided(block)) {
|
||||
return;
|
||||
}
|
||||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||||
@@ -1755,7 +1816,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
function processPills(root, finder, boundarySelector, pillNeighbor) {
|
||||
root.querySelectorAll(boundarySelector).forEach((pill) => {
|
||||
if (finder.avoidsSelf(pill) || finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||||
if (finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
}
|
||||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||||
@@ -1817,7 +1878,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
processBlock2(block, finder, R);
|
||||
});
|
||||
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.avoidsSelf(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||||
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(", ");
|
||||
|
||||
Vendored
+114
-53
@@ -52,18 +52,22 @@ var Finder = class {
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
transparentInline(el) {
|
||||
isTransparent(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
|
||||
* 形同虛設)。 */
|
||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||
* ③ SVG 命名空間 ④ contentEditable
|
||||
* ⑤ data-jz-skip 屬性(內建) ⑥ skipAttribute(消費端指定屬性名)
|
||||
* ⑦ scope.avoid 選擇器
|
||||
* **此為自身判定**,供遍歷原語(collectRuns/jiyaAdjacency/canSplit/isTransparent)於
|
||||
* 下探時逐元素自判(祖先由遞迴涵蓋)。**一次性**問「某節點是否被 avoid(自身或祖先)」
|
||||
* 請改用 isAvoided(單一組合入口),勿只用本方法而漏判祖先、或只用 isAvoided 而漏判自身
|
||||
* (下游問題六/階段一 #2:漏一半致 avoid 對自身形同虛設)。 */
|
||||
avoidsSelf(el) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(el)) {
|
||||
return true;
|
||||
@@ -88,10 +92,16 @@ var Finder = class {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
|
||||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||||
/** 某節點是否被 avoid——**自身即邊界**(元素經 avoidsSelf 之七源;非元素節點僅判
|
||||
* isSkipped)**或**祖先鏈上任一為 avoid 邊界。此為「是否 avoid」之**單一組合入口(安全
|
||||
* 預設)**:一次性檢查用此,免除「avoidsSelf ‖ isAvoided」易漏一半之誤用。遍歷原語逐
|
||||
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
|
||||
isAvoided(node) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
if (node.nodeType === 1) {
|
||||
if (this.avoidsSelf(node)) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return true;
|
||||
}
|
||||
let p = node.parentNode;
|
||||
@@ -152,9 +162,9 @@ var Finder = class {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
|
||||
* jz-* 透明穿越;block/pill/avoid/間隙 marker(jz-hws)中斷 run。
|
||||
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):透明行內(isTransparent:styleInlines
|
||||
* + wrap 類 jz-*)穿越、run 延續;block/pill/avoid 中斷 run;isolate 與**未知行內
|
||||
* 標籤**(嚴格白名單)中斷 run 且內部自成獨立 run。回傳 text.length >= 2 之 run。
|
||||
*/
|
||||
collectRuns(block) {
|
||||
const runs = [];
|
||||
@@ -195,21 +205,21 @@ var Finder = class {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
if (this.isolateMatches(el)) {
|
||||
flush();
|
||||
let ic = el.firstChild;
|
||||
while (ic) {
|
||||
visit(ic);
|
||||
ic = ic.nextSibling;
|
||||
if (this.isTransparent(el)) {
|
||||
let c2 = el.firstChild;
|
||||
while (c2) {
|
||||
visit(c2);
|
||||
c2 = c2.nextSibling;
|
||||
}
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
let c2 = el.firstChild;
|
||||
while (c2) {
|
||||
visit(c2);
|
||||
c2 = c2.nextSibling;
|
||||
flush();
|
||||
let ic = el.firstChild;
|
||||
while (ic) {
|
||||
visit(ic);
|
||||
ic = ic.nextSibling;
|
||||
}
|
||||
flush();
|
||||
};
|
||||
let c = block.firstChild;
|
||||
while (c) {
|
||||
@@ -241,7 +251,7 @@ var Finder = class {
|
||||
return pickEnd(n2);
|
||||
}
|
||||
if (n2.nodeType === 1) {
|
||||
if (!this.transparentInline(n2)) {
|
||||
if (!this.isTransparent(n2)) {
|
||||
return "blocked";
|
||||
}
|
||||
const r = descend(n2);
|
||||
@@ -266,7 +276,7 @@ var Finder = class {
|
||||
}
|
||||
if (n.nodeType === 1) {
|
||||
const e = n;
|
||||
if (!this.transparentInline(e)) {
|
||||
if (!this.isTransparent(e)) {
|
||||
return null;
|
||||
}
|
||||
const r = descend(e);
|
||||
@@ -281,7 +291,7 @@ var Finder = class {
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
if (!p || p.nodeType !== 1 || !this.transparentInline(p)) {
|
||||
if (!p || p.nodeType !== 1 || !this.isTransparent(p)) {
|
||||
return null;
|
||||
}
|
||||
const pe = p;
|
||||
@@ -824,8 +834,7 @@ var jiyaAdjacencyPass = {
|
||||
level: "text",
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx) {
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
const { finder } = ctx;
|
||||
let prev = null;
|
||||
const squeeze = (L, R) => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
@@ -861,20 +870,14 @@ var jiyaAdjacencyPass = {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.pillMatches(el)) {
|
||||
prev = null;
|
||||
} else if (finder.avoidsSelf(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isolateMatches(el)) {
|
||||
} else if (finder.pillMatches(el) || finder.avoidsSelf(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isTransparent(el)) {
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1042,21 +1045,66 @@ var AVOID_TAIL = /* @__PURE__ */ new Set(["bd-open"]);
|
||||
function isCharEl(n) {
|
||||
return !!n && n.nodeType === 1 && n.nodeName === "JZ-CHAR";
|
||||
}
|
||||
function firstMeaningfulChild(el) {
|
||||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3 && c.data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function lastMeaningfulChild(el) {
|
||||
for (let c = el.lastChild; c; c = c.previousSibling) {
|
||||
if (c.nodeType === 3 && c.data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function atEdge(node, el, side) {
|
||||
return node === (side === "head" ? firstMeaningfulChild(el) : lastMeaningfulChild(el));
|
||||
}
|
||||
function edgeCharEl(node, side, finder) {
|
||||
let n = node;
|
||||
while (n && n.nodeType === 1) {
|
||||
if (isCharEl(n)) {
|
||||
return n;
|
||||
}
|
||||
if (!finder.isTransparent(n)) {
|
||||
return null;
|
||||
}
|
||||
n = side === "head" ? firstMeaningfulChild(n) : lastMeaningfulChild(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function forbiddenBreak(a, b, finder) {
|
||||
if (isCharEl(b)) {
|
||||
const c = bdClassOf(b);
|
||||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(b, PASS2)) {
|
||||
const head = edgeCharEl(b, "head", finder);
|
||||
if (head) {
|
||||
const c = bdClassOf(head);
|
||||
if (c && AVOID_HEAD.has(c) && finder.featureEnabledFor(head, PASS2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isCharEl(a)) {
|
||||
const c = bdClassOf(a);
|
||||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(a, PASS2)) {
|
||||
const tail = edgeCharEl(a, "tail", finder);
|
||||
if (tail) {
|
||||
const c = bdClassOf(tail);
|
||||
if (c && AVOID_TAIL.has(c) && finder.featureEnabledFor(tail, PASS2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function depthOf(node) {
|
||||
let d = 0;
|
||||
let p = node.parentNode;
|
||||
while (p) {
|
||||
d += 1;
|
||||
p = p.parentNode;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
function isTokenChar2(ch, anyCjk) {
|
||||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||||
}
|
||||
@@ -1168,18 +1216,31 @@ var jinzePass = {
|
||||
level: "paragraph",
|
||||
enabled: (o) => o.features.jinze,
|
||||
render(ctx) {
|
||||
const { finder } = 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")) {
|
||||
if (!finder.levelAllows(c, "paragraph")) {
|
||||
return;
|
||||
}
|
||||
if (c.parentNode) {
|
||||
parents.add(c.parentNode);
|
||||
let child = c;
|
||||
let parent = c.parentNode;
|
||||
while (parent && parent.nodeType === 1) {
|
||||
parents.add(parent);
|
||||
const pe = parent;
|
||||
if (!finder.isTransparent(pe)) {
|
||||
break;
|
||||
}
|
||||
if (!atEdge(child, pe, "head") && !atEdge(child, pe, "tail")) {
|
||||
break;
|
||||
}
|
||||
child = pe;
|
||||
parent = pe.parentNode;
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
processParent(parent, ctx.finder, anyCjk);
|
||||
const ordered = Array.from(parents).sort((a, b) => depthOf(b) - depthOf(a));
|
||||
for (const parent of ordered) {
|
||||
processParent(parent, finder, anyCjk);
|
||||
}
|
||||
},
|
||||
revert(ctx) {
|
||||
@@ -1361,7 +1422,7 @@ var lineEdgePass = {
|
||||
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")
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing")
|
||||
);
|
||||
if (!gaps.length) {
|
||||
return;
|
||||
@@ -1547,7 +1608,7 @@ var orphanPass = {
|
||||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||||
return;
|
||||
}
|
||||
if (finder.avoidsSelf(block) || finder.isAvoided(block)) {
|
||||
if (finder.isAvoided(block)) {
|
||||
return;
|
||||
}
|
||||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||||
@@ -1728,7 +1789,7 @@ function processBlock2(block, finder, R) {
|
||||
}
|
||||
function processPills(root, finder, boundarySelector, pillNeighbor) {
|
||||
root.querySelectorAll(boundarySelector).forEach((pill) => {
|
||||
if (finder.avoidsSelf(pill) || finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||||
if (finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
}
|
||||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||||
@@ -1790,7 +1851,7 @@ var spacingPass = {
|
||||
}
|
||||
processBlock2(block, finder, R);
|
||||
});
|
||||
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.avoidsSelf(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||||
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(", ");
|
||||
|
||||
Vendored
+1
-1
@@ -58,7 +58,7 @@ export interface JuzhenOptions {
|
||||
/** 作用域限定(§3.6)。 */
|
||||
scope?: {
|
||||
root?: Element | string;
|
||||
include?: string | null;
|
||||
include?: string;
|
||||
avoid?: string;
|
||||
};
|
||||
spacing?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user