feat(rules): 建立共享排版契约并收束 WebKit 与 Unicode 基线
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
// 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 = '<section data-jz="spacing"><jz-hws data-jz="spacing" '
|
||||
+ 'data-jz-kind="wrap">author</jz-hws></section>';
|
||||
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('<p lang="zh-Hans">用Neovim寫,很好。</p>');
|
||||
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('<p lang="zh-Hans">純文字</p>');
|
||||
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("<p>純文字</p>");
|
||||
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("<p>純文字</p>");
|
||||
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("<p>純文字</p>");
|
||||
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('<div><p lang="zh-Hans">用Neovim寫,很好。</p></div>');
|
||||
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 = '<p lang="zh-Hans">前文<strong>粗体文字</strong>,后续</p>';
|
||||
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('<p lang="zh-Hans">用Neovim寫,很好。</p>');
|
||||
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("<p>中A文</p>");
|
||||
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("<p>中A文</p>");
|
||||
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('<div id="parent"></div>');
|
||||
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('<div id="parent"><p>前<span>中A文</span>後</p></div>');
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user