import assert from "node:assert/strict"; import { test } from "node:test"; import { setupDom } from "./helpers.mjs"; import { createJuzhen } from "../dist/juzhen.mjs"; test("pill:塊本身與其後代塊的內部不進入 standalone pass", () => { for (const html of [ '

甲ABC乙,丙。

正常ABC,文字。

', '

甲ABC乙,丙。

正常ABC,文字。

', ]) { const { dom, document } = setupDom(html); const original = document.body.innerHTML; const pill = document.querySelector(".pill"); const before = pill.innerHTML; const jz = createJuzhen({ pillSelector: ".pill" }); try { jz.render(document.body); assert.equal(pill.innerHTML, before, "pill 內部保持原樣"); assert.ok(document.querySelector("main > p:not(.pill) jz-hws"), "正常兄弟塊仍處理"); jz.revert(document.body); assert.equal(pill.innerHTML, before); } finally { jz.disconnect(); dom.window.close(); } } }); const exclusions = [ [ "avoid", "data-jz-skip" ], [ "text", 'data-jz-level="text"' ], [ "off", 'data-juzhen-off="orphan jinze"' ], [ "predicate", 'class="skip-text"' ], ]; const skippedText = (node) => node.nodeType === 3 && node.parentElement?.classList.contains("skip-text"); test("jinze:排除或關閉段落功能的兩側不建立禁斷綁定", () => { for (const [ label, attr ] of exclusions) { for (const side of [ "before-close", "after-open" ]) { const protectedHtml = '自管'; const html = side === "before-close" ? '

' + protectedHtml + ',正常文字。

' : '

正常文字(' + protectedHtml + '

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const protectedEl = document.querySelector("span"); const jz = createJuzhen({ spacing: false, orphan: false, longWord: false, isSkipped: skippedText }); try { jz.render(document.body); assert.equal(protectedEl.closest("jz-jinze"), null, label + "/" + side); assert.equal(protectedEl.innerHTML, "自管"); jz.revert(document.body); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } } } }); test("jinze:透明元素內的排除內容不可被整元素退化綁定帶入", () => { const html = '

甲自管乙,正常。

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const protectedEl = document.querySelector("[data-jz-skip]"); const jz = createJuzhen({ spacing: false, orphan: false, longWord: false }); try { jz.render(document.body); assert.equal(protectedEl.closest("jz-jinze"), null); jz.revert(document.body); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } }); test("jinze:註解與空 Text 不阻斷禁則,回退保留註解身份和次序", () => { const html = '

甲,乙。

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const p = document.querySelector("p"); const anchor = p.childNodes[1]; const empty = document.createTextNode(""); p.insertBefore(empty, anchor); const jz = createJuzhen({ spacing: false, orphan: false, longWord: false }); try { jz.render(p); const group = p.querySelector("jz-jinze"); assert.equal(group.textContent, "甲,"); assert.ok(group.contains(anchor)); assert.ok(group.contains(empty)); jz.revert(p); assert.equal(p.childNodes[1], anchor); assert.equal(anchor.previousSibling.textContent, "甲"); assert.equal(anchor.nextSibling.textContent, ",乙。"); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } }); test("jinze:行內 render 根不把外部兄弟搬入生成群組", () => { const html = '

甲,乙。後文

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const root = document.getElementById("root"); const parent = root.parentNode; const before = root.previousSibling; const after = root.nextSibling; const jz = createJuzhen({ spacing: false, orphan: false, longWord: false }); try { jz.render(root); assert.equal(root.parentNode, parent); assert.equal(root.previousSibling, before); assert.equal(root.nextSibling, after); assert.equal(parent.querySelectorAll(":scope > jz-jinze").length, 0); jz.revert(root); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } }); test("orphan:排除/text/off 的尾部內容不計數,也不包裹", () => { for (const [ label, attr ] of exclusions) { const html = '

正常文字自管文字

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const jz = createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false, isSkipped: skippedText }); try { jz.render(document.body); assert.equal(document.querySelector("jz-orphan"), null, label + " 尾部是綁定邊界"); jz.revert(document.body); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } } }); test("orphan:只綁最後排除邊界之後的合資格末尾", () => { for (const [ label, attr ] of exclusions) { for (const tail of [ "末", "最後" ]) { const html = '

前文自管文字' + tail + '

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const jz = createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false, isSkipped: skippedText }); try { jz.render(document.body); const orphan = document.querySelector("jz-orphan"); assert.equal(orphan?.textContent ?? null, tail === "末" ? null : tail, label + "/" + tail); assert.equal(document.querySelector("span").closest("jz-orphan"), null); jz.revert(document.body); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } } } }); test("orphan:不可切容器不以整綁繞過排除邊界,可切容器只切合資格末尾", () => { for (const tag of [ 'span', 'a href="#"' ]) { const closeTag = tag.split(" ")[0]; const html = '

前文<' + tag + '>自管最後

'; const { dom, document } = setupDom(html); const original = document.body.innerHTML; const protectedEl = document.querySelector("[data-jz-skip]"); const jz = createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false }); try { jz.render(document.body); assert.equal(protectedEl.closest("jz-orphan"), null); assert.equal(document.querySelector("jz-orphan")?.textContent ?? null, closeTag === "span" ? "最後" : null); jz.revert(document.body); assert.equal(document.body.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } } }); test("jinze:完整跨節點 base/VS/surrogate 原子一起綁到閉標點", () => { for (const parts of [ [ "𠀀", "\u{E0100}" ], [ "\uD840", "\uDC00", "\uDB40", "\uDD00" ] ]) { const { dom, document } = setupDom("

"); const p = document.querySelector("p"); p.append("前文"); const authors = parts.map((part) => { const span = document.createElement("span"); span.textContent = part; p.appendChild(span); return span; }); p.append(","); const original = p.innerHTML; const jz = createJuzhen({ spacing: false, orphan: false, longWord: false }); try { jz.render(p); assert.equal(p.querySelector("jz-jinze").textContent, "𠀀\u{E0100},"); const rendered = p.innerHTML; jz.render(p); assert.equal(p.innerHTML, rendered); jz.revert(p); assert.equal(p.innerHTML, original); assert.deepEqual(Array.from(p.querySelectorAll("span")), authors); } finally { jz.disconnect(); dom.window.close(); } } }); test("orphan:split surrogate 的補充平面字計為一個實義字", () => { const { dom, document } = setupDom('

前文\uD840\uDC00末

'); const p = document.querySelector("p"); const original = p.innerHTML; const jz = createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false }); try { jz.render(p); assert.equal(p.querySelector("jz-orphan").textContent, "𠀀末"); jz.revert(p); assert.equal(p.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } });