feat(rules): 建立共享排版契约并收束 WebKit 与 Unicode 基线

This commit is contained in:
2026-09-04 21:44:11 +08:00
parent 8ee9895247
commit 993cf576c6
42 changed files with 8511 additions and 1422 deletions
+273
View File
@@ -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();
});