// createJuzhen 實例所有權:revert 只得復原本實例產物。 import assert from "node:assert/strict"; import { test } from "node:test"; import { setupDom } from "./helpers.mjs"; import { createJuzhen } from "../dist/juzhen.mjs"; const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); test("所有權:作者 data-jz 結構不可作為 revert 憑證", () => { const html = '
author
'; const { document } = setupDom(html); const before = document.body.innerHTML; createJuzhen().revert(document.body); assert.equal(document.body.innerHTML, before); }); test("所有權:實例 B 不得還原實例 A 之結構或 root 屬性", () => { const { document } = setupDom('

用Neovim寫,很好。

'); const before = document.body.innerHTML; const a = createJuzhen({ jiya: { halfWidth: "margin" }, justifyAtoms: false }); const b = createJuzhen(); a.render(document.body); const rendered = document.body.innerHTML; assert.equal(document.body.getAttribute("data-jz-halfwidth"), "margin"); assert.equal(document.body.getAttribute("data-jz-atoms"), "inline"); b.revert(document.body); assert.equal(document.body.innerHTML, rendered); assert.equal(document.body.getAttribute("data-jz-halfwidth"), "margin"); assert.equal(document.body.getAttribute("data-jz-atoms"), "inline"); a.revert(document.body); assert.equal(document.body.innerHTML, before); }); test("所有權:兩實例同值屬性之早到者先 revert 不移除後到者聲明", () => { const { document } = setupDom('

純文字

'); const opts = { spacing: false, jiya: { halfWidth: "margin" }, jinze: false, orphan: false, longWord: false, justifyAtoms: false, }; const a = createJuzhen(opts); const b = createJuzhen(opts); a.render(document.body); b.render(document.body); a.revert(document.body); assert.equal(document.body.getAttribute("data-jz-halfwidth"), "margin"); assert.equal(document.body.getAttribute("data-jz-atoms"), "inline"); assert.equal(document.querySelector("p").getAttribute("data-jz-block-level"), "paragraph"); b.revert(document.body); assert.equal(document.body.hasAttribute("data-jz-halfwidth"), false); assert.equal(document.body.hasAttribute("data-jz-atoms"), false); assert.equal(document.querySelector("p").hasAttribute("data-jz-block-level"), false); }); test("所有權:屬性寫者亂序 revert 還原最近活躍值或原值", () => { const options = { spacing: false, jiya: false, jinze: false, orphan: false, longWord: false, }; const first = setupDom("

純文字

"); const a = createJuzhen(options); const b = createJuzhen({ ...options, level: { text: "p" } }); a.render(first.document.body); b.render(first.document.body); a.revert(first.document.body); assert.equal(first.document.querySelector("p").getAttribute("data-jz-block-level"), "text"); b.revert(first.document.body); assert.equal(first.document.querySelector("p").hasAttribute("data-jz-block-level"), false); const second = setupDom("

純文字

"); const c = createJuzhen(options); const d = createJuzhen({ ...options, level: { text: "p" } }); c.render(second.document.body); d.render(second.document.body); d.revert(second.document.body); assert.equal(second.document.querySelector("p").getAttribute("data-jz-block-level"), "paragraph"); c.revert(second.document.body); assert.equal(second.document.querySelector("p").hasAttribute("data-jz-block-level"), false); }); test("所有權:外部改寫屬性後 revert fail closed,新寫者可以當前值重建基線", () => { const options = { spacing: false, jiya: false, jinze: false, orphan: false, longWord: false, justifyAtoms: false, }; const { document } = setupDom("

純文字

"); const a = createJuzhen(options); a.render(document.body); document.body.setAttribute("data-jz-atoms", "external"); document.querySelector("p").removeAttribute("data-jz-block-level"); const b = createJuzhen(options); b.render(document.body); assert.equal(document.body.getAttribute("data-jz-atoms"), "inline"); assert.equal(document.querySelector("p").getAttribute("data-jz-block-level"), "paragraph"); a.revert(document.body); assert.equal(document.body.getAttribute("data-jz-atoms"), "inline"); assert.equal(document.querySelector("p").getAttribute("data-jz-block-level"), "paragraph"); b.revert(document.body); assert.equal(document.body.getAttribute("data-jz-atoms"), "external"); assert.equal(document.querySelector("p").hasAttribute("data-jz-block-level"), false); }); test("所有權:子根 render 後對祖先 revert 完整清理", () => { const { document } = setupDom('

用Neovim寫,很好。

'); const before = document.body.innerHTML; const p = document.querySelector("p"); const jz = createJuzhen({ jiya: { halfWidth: "margin" }, justifyAtoms: false }); jz.render(p); assert.equal(p.getAttribute("data-jz-halfwidth"), "margin"); assert.ok(p.querySelector("[data-jz]")); jz.revert(document.body); assert.equal(document.body.innerHTML, before); }); test("所有權:局部 revert 完整撤回祖根與獨立子根的混合產物", () => { const { document } = setupDom('
' + '

甲文,內容。

乙文,內容。

'); const main = document.querySelector("main"); const part = document.getElementById("part"); const a = document.getElementById("a"); const b = document.getElementById("b"); const before = part.innerHTML; const jz = createJuzhen({ jiya: { halfWidth: "margin" }, justifyAtoms: false }); jz.render(main); jz.render(b); assert.ok(a.querySelector("jz-char")); assert.ok(b.querySelector("jz-char")); assert.equal(b.getAttribute("data-jz-halfwidth"), "margin"); jz.revert(part); assert.equal(part.innerHTML, before, "祖根寫入的兄弟產物與子根屬性皆撤回"); assert.equal(main.getAttribute("data-jz-halfwidth"), "margin", "請求範圍外祖根屬性保留"); jz.render(part); assert.ok(a.querySelector("jz-char"), "還原後可再次渲染"); jz.revert(main); assert.equal(part.innerHTML, before); }); test("所有權:混合根局部 revert 保留作者與其他實例的產物", () => { const { document } = setupDom('
' + '

甲文,內容。

' + '

乙文,內容。

他文,內容。

' + '作者' + '
'); const part = document.getElementById("part"); const foreign = document.getElementById("foreign"); const peer = createJuzhen(); peer.render(foreign); const before = part.innerHTML; const foreignBefore = foreign.innerHTML; const author = part.querySelector("span > jz-hws"); const local = createJuzhen({ scope: { avoid: "#foreign" } }); local.render(document.querySelector("main")); local.render(document.getElementById("b")); local.revert(part); assert.equal(part.innerHTML, before, "本實例全清且作者/peer 產物完整保留"); assert.equal(foreign.innerHTML, foreignBefore); assert.equal(part.querySelector("span > jz-hws"), author, "作者元素身份保留"); assert.equal(document.getElementById("a").getAttribute("data-jz-block-level"), "author-level"); assert.equal(foreign.getAttribute("data-jz-block-level"), "paragraph"); peer.revert(foreign); }); test("所有權:本實例完整還原 wrapper 與 splitoff", () => { const html = '

前文粗体文字,后续

'; const { document } = setupDom(html); const before = document.body.innerHTML; const jz = createJuzhen({ spacing: false, longWord: false, orphan: false }); jz.render(document.body); assert.ok(document.querySelector('[data-jz-kind="wrap"]')); assert.ok(document.querySelector('[data-jz-kind="splitoff"]')); jz.revert(document.body); assert.equal(document.body.innerHTML, before); }); test("所有權:已有實例重複 render 仍冪等", () => { const { document } = setupDom('

用Neovim寫,很好。

'); const jz = createJuzhen(); jz.render(document.body); const once = document.body.innerHTML; jz.render(document.body); assert.equal(document.body.innerHTML, once); }); test("所有權:已還原 wrapper 之舊憑證即時撤銷", () => { const { document } = setupDom("

中A文

"); const jz = createJuzhen({ jiya: false, longWord: false, jinze: false, orphan: false }); jz.render(document.body); const gap = document.querySelector("jz-hws"); assert.ok(gap); jz.revert(document.body); document.querySelector("p").appendChild(gap); const stale = gap.outerHTML; jz.revert(document.body); assert.ok(document.body.contains(gap)); assert.equal(gap.outerHTML, stale); }); test("所有權:已知 tag 改成無生產者之 marker kind 亦 fail closed", () => { const { document } = setupDom("

中A文

"); const jz = createJuzhen({ jiya: false, longWord: false, jinze: false, orphan: false }); jz.render(document.body); const gap = document.querySelector("jz-hws"); assert.ok(gap); gap.setAttribute("data-jz-kind", "marker"); const malformed = gap.outerHTML; jz.revert(document.body); assert.ok(document.body.contains(gap)); assert.equal(gap.outerHTML, malformed); }); test("所有權:observe 移除獨立 render 根時清理脫離子樹與佈局觀察者", async () => { const previousResizeObserver = globalThis.ResizeObserver; const observers = []; class MockResizeObserver { constructor() { this.targets = []; this.disconnectCount = 0; observers.push(this); } observe(target) { this.targets.push(target); } disconnect() { this.disconnectCount += 1; } } globalThis.ResizeObserver = MockResizeObserver; try { const { document } = setupDom('
'); const parent = document.getElementById("parent"); const jz = createJuzhen({ jiya: { halfWidth: "margin" }, justifyAtoms: false }); jz.observe(parent); const block = document.createElement("p"); block.lang = "zh-Hans"; block.innerHTML = "用Neovim寫,很好。"; const original = block.innerHTML; parent.appendChild(block); await tick(); const blockObservers = observers.filter((ro) => ro.targets.includes(block)); assert.equal(blockObservers.length, 2, "lineEdge 與 gapTrim 各持一觀察者"); assert.ok(block.querySelector("[data-jz]")); assert.equal(block.getAttribute("data-jz-halfwidth"), "margin"); assert.equal(block.getAttribute("data-jz-atoms"), "inline"); block.setAttribute("data-jz-atoms", "external"); block.remove(); await tick(); assert.ok(blockObservers.every((ro) => ro.disconnectCount === 1)); assert.equal(block.querySelector("[data-jz]"), null); assert.equal(block.innerHTML, original); assert.equal(block.hasAttribute("data-jz-halfwidth"), false); assert.equal(block.getAttribute("data-jz-atoms"), "external", "外部改寫 fail closed"); parent.appendChild(block); await tick(); assert.ok(block.querySelector("[data-jz]"), "脫離根清理後可再插入重新 render"); jz.revert(parent); jz.disconnect(); assert.equal(block.getAttribute("data-jz-atoms"), "external"); } finally { if (previousResizeObserver === undefined) { delete globalThis.ResizeObserver; } else { globalThis.ResizeObserver = previousResizeObserver; } } }); test("所有權:observe 移除非 render 根之行內內容不過度清理", async () => { const { document } = setupDom('

前中A文後

'); const parent = document.getElementById("parent"); const jz = createJuzhen({ jiya: false, jinze: false, orphan: false, longWord: false }); jz.render(parent); jz.observe(parent); const span = document.querySelector("span"); const rendered = span.innerHTML; assert.ok(span.querySelector("jz-hws")); span.remove(); await tick(); assert.equal(span.innerHTML, rendered, "非獨立 render 根之脫離內容保持原渲染結構"); assert.ok(span.querySelector("jz-hws")); document.querySelector("p").appendChild(span); await tick(); jz.revert(parent); jz.disconnect(); });