import assert from "node:assert/strict"; import { test } from "node:test"; import { setupDom } from "./helpers.mjs"; import { createJuzhen } from "../dist/juzhen.mjs"; function sameLine(document) { const view = document.defaultView; const previous = globalThis.ResizeObserver; const elementRects = view.Element.prototype.getClientRects; const rangeRects = view.Range.prototype.getClientRects; const rect = { top: 0, bottom: 20, left: 0, right: 20, width: 20, height: 20 }; globalThis.ResizeObserver = class { observe() {} disconnect() {} }; view.Element.prototype.getClientRects = () => [ rect ]; view.Range.prototype.getClientRects = () => [ rect ]; return () => { view.Element.prototype.getClientRects = elementRects; view.Range.prototype.getClientRects = rangeRects; if (previous === undefined) { delete globalThis.ResizeObserver; } else { globalThis.ResizeObserver = previous; } }; } for (const orphan of [ false, true ]) { test("line edge:同行 orphan 不製造行首 orphan=" + orphan, () => { const { dom, document } = setupDom('

前甲「乙丙」

'); const restore = sameLine(document); const jz = createJuzhen({ orphan, spacing: false, longWord: false }); try { jz.render(document.querySelector("main")); assert.equal(document.querySelector(".bd-open").classList.contains("jz-half-le"), false); } finally { jz.revert(); restore(); dom.window.close(); } }); } for (const display of [ "inline", "inline-block", "contents", "block" ]) { test("line edge:自訂隔離元素按實際物理盒攀爬 display=" + display, () => { const { dom, document } = setupDom('

前甲「乙」丙

'); const restore = sameLine(document); const jz = createJuzhen({ isolateSelector: "x-box", orphan: false, spacing: false, longWord: false }); try { jz.render(); for (const char of document.querySelectorAll("jz-char")) { assert.equal(char.classList.contains("jz-half-le"), display === "block"); } } finally { jz.revert(); restore(); dom.window.close(); } }); } for (const hidden of [ false, true ]) { test("line edge:BR 為強制行緣,隱藏時可穿越 hidden=" + hidden, () => { const { dom, document } = setupDom('

甲」「乙

'); const restore = sameLine(document); const jz = createJuzhen({ orphan: false, spacing: false, longWord: false }); try { jz.render(); for (const char of document.querySelectorAll("jz-char")) { assert.equal(char.classList.contains("jz-half-le"), !hidden); } } finally { jz.revert(); restore(); dom.window.close(); } }); } for (const action of [ "insert", "replace-source", "edit-fragment", "edit-source" ]) { test("splitoff:回退須驗來源身份並保留外部樣式 action=" + action, () => { const { dom, document } = setupDom('

甲乙丙,丁

'); const p = document.querySelector("p"); const source = p.querySelector("strong"); const jz = createJuzhen({ orphan: false, spacing: false, longWord: false }); try { jz.render(p); const fragment = p.querySelector('[data-jz-kind="splitoff"]'); assert.ok(fragment); if (action === "insert") { const added = document.createElement("strong"); added.className = "external"; added.textContent = "新"; fragment.before(added); } else if (action === "replace-source") { const replacement = source.cloneNode(false); replacement.textContent = "替"; source.replaceWith(replacement); } else if (action === "edit-fragment") { fragment.className = "edited"; } else { source.className = "edited"; } jz.revert(p); assert.equal(fragment.parentNode, p, "來源失效時保留片段當前父與樣式"); assert.equal(fragment.textContent, "丙"); assert.equal(fragment.className, action === "edit-fragment" ? "edited" : "original"); assert.equal(p.querySelector("[data-jz]"), null); if (action === "insert") { assert.equal(p.querySelector(".external").textContent, "新"); assert.equal(p.textContent, "甲乙新丙,丁"); } if (action === "replace-source") { assert.equal(p.textContent, "替丙,丁"); } jz.revert(p); assert.equal(fragment.textContent, "丙", "撤回所有權後再次 revert 不碰片段"); } finally { jz.revert(p); dom.window.close(); } }); } test("splitoff:同來源多層切分仍完整還原作者結構", () => { const { dom, document } = setupDom('

「甲乙丙丁」

'); const p = document.querySelector("p"); const original = p.innerHTML; const jz = createJuzhen(); try { jz.render(p); jz.revert(p); assert.equal(p.innerHTML, original); } finally { jz.revert(p); dom.window.close(); } }); for (const [ html, defaultLang, half ] of [ [ '

甲,乙

', "zh-Hans", false ], [ '

甲,乙

', "", false ], [ '

甲,乙

', "zh-Hant", true ], [ '

甲,乙

', "zh-Hans", false ], ]) { test("locale:空值及私用內容不冒充繼承語言 " + html, () => { const { dom, document } = setupDom(html); const jz = createJuzhen({ lang: { default: defaultLang }, jinze: false, orphan: false }); try { jz.render(document.querySelector("main")); assert.equal(document.querySelector("jz-char").classList.contains("jz-half"), half); } finally { jz.revert(); dom.window.close(); } }); } test("splitoff:多實例六種回退順序恢復原結構,36 組合", () => { const fixtures = [ '甲乙丙丁,戊己', '「甲乙丙丁」', '甲乙丙丁,戊己', '甲乙丙丁,戊己', '甲乙𠀀\uDB40\uDD00丙丁,戊己', '甲乙丙丁,戊己', ]; const orders = [ [ 0, 1, 2 ], [ 0, 2, 1 ], [ 1, 0, 2 ], [ 1, 2, 0 ], [ 2, 0, 1 ], [ 2, 1, 0 ] ]; for (const html of fixtures) { for (const order of orders) { const { dom, document } = setupDom('

' + html + '

'); const p = document.querySelector("p"); const original = p.innerHTML; const owners = [ createJuzhen({ jiya: false, spacing: false, orphan: false, longWord: false }), createJuzhen({ jiya: false, spacing: false, jinze: false, longWord: false }), createJuzhen({ spacing: false, orphan: false, longWord: false }), ]; try { owners.forEach((owner) => owner.render(p)); order.forEach((index) => owners[index].revert(p)); assert.equal(p.innerHTML, original, html + " order=" + order.join(",")); } finally { owners.forEach((owner) => owner.disconnect()); dom.window.close(); } } } });