// 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("所有權:本實例完整還原 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(); });