Files
cjk-autospace/demo/e2e.html
T
admin 3a4fa1c932 fix: 修复行缘、切分还原与语言解析并同步文档
保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。

验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
2026-10-03 21:15:54 +08:00

802 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>聚珍浏览器验收</title>
<link rel="stylesheet" href="../dist/juzhen.css">
<style>
body
{
margin: 0;
padding: 2rem;
color: #172033;
background: #f5f7fb;
font: 16px/1.75 system-ui, sans-serif;
}
h1 { margin-top: 0; font-size: 1.5rem; }
#summary, .panel
{
max-width: 52rem;
margin: 1rem 0;
padding: 1rem 1.25rem;
border: 1px solid #cbd4e2;
border-radius: .5rem;
background: #fff;
}
#summary[data-state="pass"] { border-color: #238636; }
#summary[data-state="fail"] { border-color: #cf222e; }
#results { padding-left: 1.5rem; }
#results li[data-state="pass"] { color: #1a7f37; }
#results li[data-state="fail"] { color: #cf222e; font-weight: 600; }
#fixture
{
max-width: 18em;
font-family: "Noto Serif SC", "Noto Serif CJK SC", serif;
}
#fixture p { text-align: justify; }
code { font-family: ui-monospace, monospace; }
</style>
</head>
<body>
<h1>聚珍浏览器验收</h1>
<section id="summary" data-state="running" aria-live="polite">
<strong id="summary-text">正在运行……</strong>
<button id="run-core" type="button">运行核心验收</button>
<button id="run-observer" type="button" disabled>运行 MutationObserver 验收</button>
<ul id="results"></ul>
</section>
<main id="fixture" class="panel juzhen" lang="zh-Hans">
<p id="spacing-case">中文ABC与TypeScript混排。</p>
<p id="punctuation-case">开明式:中文,测试(括号)与句号。</p>
<p id="ivs-forward"><strong id="author-forward">甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-forward-no-orphan" data-juzhen-off="orphan"><strong>甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-forward-same-node" data-juzhen-off="orphan">&#x20000;&#xE0100;A</p>
<p id="ivs-forward-same-node-prefix" data-juzhen-off="orphan">甲&#x20000;&#xE0100;A</p>
<p id="ivs-reverse">A<strong id="author-reverse">&#x20000;</strong>&#xE0100;乙</p>
<p id="ivs-line-break" data-juzhen-off="orphan" style="width:2.5em;text-align:justify"><strong>甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-nowrap" data-juzhen-off="orphan" style="width:1em;white-space:nowrap"><strong>甲&#x20000;</strong>&#xE0100;A</p>
<p id="ivs-lineedge-open" lang="zh-Hant">「&#x20000;&#xE0100;甲</p>
<p id="ivs-lineedge-close" lang="zh-Hant">甲&#x20000;&#xE0100;」</p>
<p id="observer-case">观察初始内容</p>
</main>
<section id="halfwidth-fixture" class="panel juzhen" lang="zh-Hant" data-jz-level="text">
<div data-jz-halfwidth="halt">
<p id="halt-to-margin" data-jz-halfwidth="margin" data-jz-style="banjiao">甲,乙</p>
</div>
<div data-jz-halfwidth="margin">
<div data-jz-halfwidth="halt">
<p id="margin-to-halt" data-jz-style="banjiao">甲,乙</p>
<p id="nested-margin" data-jz-halfwidth="margin" data-jz-style="banjiao" style="text-decoration:underline">甲,乙</p>
</div>
<p id="fullwidth-bias" data-jz-style="quanjiao">甲,乙</p>
</div>
</section>
<section id="lifecycle-fixture" class="panel juzhen" lang="zh-Hans" data-jz-level="text">
<div id="partial-root"><p id="pending-a">原文,甲。</p><p id="pending-b">原文,乙。</p></div>
</section>
<section id="module-fixture" class="panel juzhen" lang="zh-Hant" data-jz-level="text">
<p id="source-pua">漢&#xE000;&#xE003;&#xE001;0&#xE002;GPT-4中</p>
<div class="module-pill"><p>甲ABC乙,丙。</p></div>
<p id="module-boundary" data-jz-level="paragraph"><span data-jz-skip>自管</span>,正常文字<span data-jz-level="text">界面文字</span></p>
<p id="pill-ivs-space"><strong>&#x20000;</strong><em>&#xE0100;</em> <code>x</code> <span>文</span></p>
<p id="jinze-ivs" data-jz-level="paragraph">前文<strong>&#x20000;</strong><em>&#xE0100;</em>,</p>
<p id="surrogate-spacing"><strong></strong><em></em><i></i><b></b>A</p>
<p id="locale-subtag-hans" lang="zh-CN-x-shant">甲,乙</p>
<p id="locale-subtag-hant" lang="zh-TW-x-hansford">甲,乙</p>
</section>
<section class="panel juzhen"><p id="inline-scope">甲<strong id="inline-root">,乙。</strong>後文</p></section>
<section id="layout-fixture" class="panel juzhen" lang="zh-Hant">
<p id="gap-small-neighbor" style="font-size:32px;text-align:center;white-space:nowrap">漢<span style="font-size:8px">A</span></p>
<p id="gap-sub-neighbor" style="font-size:32px;text-align:center;white-space:nowrap">漢<sub>A</sub></p>
</section>
<section id="punctuation-atom-fixture" class="panel juzhen" lang="zh-Hant">
<p id="punctuation-vs">甲<strong id="punctuation-base">,</strong><em id="punctuation-selector">&#xFE00;</em>「乙丙」</p>
<p id="punctuation-surrogate"><strong></strong><em></em><i></i><b></b>「甲</p>
<p id="punctuation-isolated"></p>
</section>
<script src="../dist/juzhen.iife.js"></script>
<script>
(function ()
{
"use strict";
var results = document.getElementById("results");
var summary = document.getElementById("summary");
var summaryText = document.getElementById("summary-text");
var coreButton = document.getElementById("run-core");
var observerButton = document.getElementById("run-observer");
var fixture = document.getElementById("fixture");
var authorForward = document.getElementById("author-forward");
var authorReverse = document.getElementById("author-reverse");
var originalHtml = fixture.innerHTML;
var originalText = fixture.textContent;
var clickCount = 0;
var failures = 0;
function record(name, condition, detail)
{
var item = document.createElement("li");
item.dataset.state = condition ? "pass" : "fail";
item.textContent = (condition ? "PASS " : "FAIL ") + name
+ (detail ? " — " + detail : "");
results.appendChild(item);
if (!condition) { failures += 1; }
}
function hasUnpairedSurrogate(text)
{
for (var i = 0; i < text.length; i += 1)
{
var unit = text.charCodeAt(i);
if (unit >= 0xD800 && unit <= 0xDBFF)
{
if (i + 1 >= text.length) { return true; }
var next = text.charCodeAt(i + 1);
if (next < 0xDC00 || next > 0xDFFF) { return true; }
i += 1;
}
else if (unit >= 0xDC00 && unit <= 0xDFFF)
{
return true;
}
}
return false;
}
function delay()
{
return new Promise(function (resolve)
{
setTimeout(resolve, 30);
});
}
authorForward.addEventListener("click", function () { clickCount += 1; });
authorReverse.addEventListener("click", function () { clickCount += 1; });
coreButton.addEventListener("click", async function ()
{
coreButton.disabled = true;
try
{
// 浏览器扩展可能在 DOMContentLoaded 后异步补写作者属性;以验收
// 按钮真正启动前的 DOM 为 revert 基线,避免把外部并发变更误报为库回归。
originalHtml = fixture.innerHTML;
originalText = fixture.textContent;
var jz = Juzhen.createJuzhen({
scope: { include: "#fixture" },
jiya: { halfWidth: "margin" },
});
jz.render(fixture);
var modeFixture = document.getElementById("halfwidth-fixture");
var modeJz = Juzhen.createJuzhen();
modeJz.render(modeFixture);
[ "halt-to-margin", "nested-margin" ].forEach(function (id)
{
var punctuation = document.getElementById(id).querySelector("jz-char");
var innerStyle = getComputedStyle(punctuation.querySelector("jz-inner"));
var widthEm = punctuation.getBoundingClientRect().width
/ parseFloat(getComputedStyle(punctuation).fontSize);
record(
id + ":最近 margin 标记控制半形宽度与字形",
Math.abs(widthEm - 0.5) <= 0.01
&& innerStyle.fontFeatureSettings === "normal"
&& innerStyle.display === "inline-block",
"width=" + widthEm.toFixed(3) + "em,features=" + innerStyle.fontFeatureSettings,
);
});
var haltInnerStyle = getComputedStyle(
document.getElementById("margin-to-halt").querySelector("jz-inner"),
);
record(
"margin-to-halt:恢复 halt 时撤销 margin 内盒与偏移",
haltInnerStyle.display === "inline"
&& parseFloat(haltInnerStyle.marginLeft) === 0
&& haltInnerStyle.fontFeatureSettings.includes('"halt"'),
);
var underline = getComputedStyle(
document.getElementById("nested-margin").querySelector("jz-inner"),
).textDecorationLine;
record(
"嵌套 margin 内盒保留段落下划线",
underline.includes("underline"),
"text-decoration-line=" + underline,
);
var fullwidth = document.getElementById("fullwidth-bias").querySelector("jz-char");
record(
"margin 模式不移动未压缩的全形标点",
!fullwidth.classList.contains("jz-half")
&& !fullwidth.classList.contains("jz-half-le")
&& parseFloat(getComputedStyle(fullwidth.querySelector("jz-inner")).marginLeft) === 0,
);
var atomFixture = document.getElementById("punctuation-atom-fixture");
var atomSurrogate = document.getElementById("punctuation-surrogate");
[ "\uD83D", "\uDE00", "\uDB40", "\uDD00" ].forEach(function (part, index)
{
atomSurrogate.children[index].textContent = part;
});
document.getElementById("punctuation-isolated").textContent = "\uD83DA\uDE00";
var atomOriginal = atomFixture.innerHTML;
var atomSelector = document.getElementById("punctuation-selector");
var atomClicks = 0;
atomSelector.addEventListener("click", function () { atomClicks += 1; });
var atomJz = Juzhen.createJuzhen({ charClass: { biaodian: { pause: "😀" } } });
atomJz.render(atomFixture);
var atomComma = document.getElementById("punctuation-vs").querySelector("jz-char.bd-pause");
record("跨节点标点与 VS 为完整原子并参与邻接挤压",
atomComma.textContent === ",\uFE00" && atomComma.classList.contains("jz-half")
&& document.getElementById("punctuation-vs").querySelector("jz-char.bd-open").classList.contains("jz-half"));
record("跨节点 surrogate 标点和 VS 不生成半字原子",
atomSurrogate.querySelector("jz-char.bd-pause").textContent === "😀\uDB40\uDD00"
&& !document.getElementById("punctuation-isolated").querySelector("jz-char"));
atomSelector.click();
record("跨标点原子保留作者元素身份及监听器",
document.getElementById("punctuation-selector") === atomSelector && atomClicks === 1);
atomJz.revert(atomFixture);
record("跨标点原子回退恢复来源分布", atomFixture.innerHTML === atomOriginal);
atomJz.disconnect();
var spacingOnlyOptions = { jiya: false, jinze: false, orphan: false, longWord: false };
var interactionRoot = document.createElement("p");
interactionRoot.setAttribute("data-jz-level", "text");
interactionRoot.innerHTML = '<span data-juzhen-off="spacing">甲 ABC 乙</span>'
+ '<code>x</code><span data-juzhen-off="spacing"> 丙</span>';
var interactionJz = Juzhen.createJuzhen(spacingOnlyOptions);
var interactionOriginal = interactionRoot.innerHTML;
interactionJz.render(interactionRoot);
record("行内 spacing 关闭时保留原文、作者空格与 pill 邻界",
interactionRoot.innerHTML === interactionOriginal);
interactionJz.revert(interactionRoot);
interactionRoot.innerHTML = '外ABC<span class="prose">甲DEF</span>文XYZ';
var includeJz = Juzhen.createJuzhen(Object.assign({}, spacingOnlyOptions,
{ scope: { include: ".prose" } }));
includeJz.render(interactionRoot);
record("行内 scope.include 子树获得间隙,外部保持原样",
interactionRoot.querySelectorAll("jz-hws").length === 1
&& interactionRoot.querySelector(".prose jz-hws").textContent === "甲"
&& interactionRoot.firstChild.textContent === "外ABC"
&& interactionRoot.lastChild.textContent === "文XYZ");
includeJz.revert(interactionRoot);
includeJz.disconnect();
interactionRoot.innerHTML = ',<span data-juzhen-off="jiya"></span>「';
interactionRoot.setAttribute("lang", "zh-Hant");
var disabledJiya = Juzhen.createJuzhen({ spacing: false, jinze: false, orphan: false, longWord: false });
disabledJiya.render(interactionRoot);
record("空功能禁用元素中断标点邻接",
interactionRoot.querySelectorAll("jz-char").length === 2
&& !interactionRoot.querySelector("jz-char.jz-half"));
disabledJiya.revert(interactionRoot);
disabledJiya.disconnect();
var carrierFailures = [];
[ "clone", "remove" ].forEach(function (change)
{
interactionRoot.innerHTML = '<strong>禰</strong><em>\uDB40\uDD00</em>A';
interactionJz.render(interactionRoot);
var gap = interactionRoot.querySelector("jz-hws");
if (change === "clone") { gap.replaceWith(gap.cloneNode(true)); }
else { gap.remove(); }
var authorText = interactionRoot.textContent;
interactionJz.revert(interactionRoot);
if (interactionRoot.textContent !== authorText) { carrierFailures.push(change); }
});
record("间隙载体克隆与删除后不重复或复活文字", carrierFailures.length === 0,
carrierFailures.join(","));
interactionJz.disconnect();
var ownerFailures = [];
[ [0,1,2], [1,0,2], [0,2,1], [1,2,0], [2,0,1], [2,1,0] ].forEach(function (order)
{
var paragraph = document.createElement("p");
[ "甲", "「", "禰", "\uFE00", "A" ].forEach(function (text)
{
paragraph.appendChild(document.createTextNode(text));
});
var owners = [
Juzhen.createJuzhen({ spacing: false, orphan: false, longWord: false }),
Juzhen.createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false }),
Juzhen.createJuzhen(spacingOnlyOptions),
];
owners.forEach(function (owner) { owner.render(paragraph); });
order.forEach(function (index) { owners[index].revert(paragraph); });
if (paragraph.textContent !== "甲「禰\uFE00A" || paragraph.querySelector("[data-jz]"))
{
ownerFailures.push(order.join("→"));
}
owners.forEach(function (owner) { owner.disconnect(); });
});
record("多实例传递解包的六种回退顺序保留 VS", ownerFailures.length === 0,
ownerFailures.join(","));
var reusedRoot = document.createElement("p");
[ "甲", "「", "禰", "\uFE00", "A" ].forEach(function (text)
{
reusedRoot.appendChild(document.createTextNode(text));
});
var reusedBinding = Juzhen.createJuzhen({ spacing: false, orphan: false, longWord: false });
var reusedOrphan = Juzhen.createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false });
var reusedSpacing = Juzhen.createJuzhen(spacingOnlyOptions);
reusedBinding.render(reusedRoot);
var retiredBinding = reusedRoot.querySelector("jz-jinze");
reusedOrphan.render(reusedRoot);
var retiredOrphan = reusedRoot.querySelector("jz-orphan");
reusedSpacing.render(reusedRoot);
reusedBinding.revert(reusedRoot);
retiredBinding.appendChild(retiredOrphan);
reusedOrphan.revert(retiredBinding);
var reusedText = retiredBinding.textContent;
reusedSpacing.revert(retiredBinding);
record("重新使用已撤回容器时回退有限结束并保留当前文字",
retiredBinding.textContent === reusedText);
[ reusedBinding, reusedOrphan, reusedSpacing ].forEach(function (owner) { owner.disconnect(); });
var layoutFixture = document.getElementById("layout-fixture");
var layoutJz = Juzhen.createJuzhen({ jiya: false, orphan: false, jinze: false, longWord: false });
layoutJz.render(layoutFixture);
[ "gap-small-neighbor", "gap-sub-neighbor" ].forEach(function (id)
{
var paragraph = document.getElementById(id);
var gap = paragraph.querySelector("jz-hws");
var neighbor = paragraph.querySelector("span, sub");
var gapRect = gap.getBoundingClientRect();
var nextRect = neighbor.getBoundingClientRect();
var overlaps = nextRect.top < gapRect.bottom - 2 && nextRect.bottom > gapRect.top + 2;
record(id + ":同行不同字号或基线保留间隙",
overlaps && !gap.classList.contains("jz-hws-trim"),
"gap=" + gapRect.top.toFixed(1) + ".." + gapRect.bottom.toFixed(1)
+ ",next=" + nextRect.top.toFixed(1) + ".." + nextRect.bottom.toFixed(1));
});
layoutJz.revert(layoutFixture);
layoutJz.disconnect();
var auditRoot = document.createElement("section");
auditRoot.lang = "zh-Hant";
auditRoot.style.cssText = "font-size:20px;width:600px;text-align:left";
document.body.appendChild(auditRoot);
[ false, true ].forEach(function (orphan)
{
auditRoot.innerHTML = '<p>前甲「乙丙」</p>';
var edgeOwner = Juzhen.createJuzhen({ orphan: orphan, spacing: false, longWord: false });
edgeOwner.render(auditRoot);
record("同行 orphan 不製造行首,orphan=" + orphan,
!auditRoot.querySelector(".bd-open").classList.contains("jz-half-le"));
edgeOwner.revert(auditRoot);
edgeOwner.disconnect();
});
[ "inline", "inline-block", "contents", "block" ].forEach(function (display)
{
auditRoot.innerHTML = '<p>前甲<x-box style="display:' + display + '">「乙」</x-box>丙</p>';
var edgeOwner = Juzhen.createJuzhen({ isolateSelector: "x-box", orphan: false, spacing: false, longWord: false });
edgeOwner.render(auditRoot);
record("自訂隔離元素按物理行內流判斷,display=" + display,
Array.from(auditRoot.querySelectorAll("jz-char")).every(function (char)
{
return char.classList.contains("jz-half-le") === (display === "block");
}));
edgeOwner.revert(auditRoot);
edgeOwner.disconnect();
});
[ false, true ].forEach(function (hidden)
{
auditRoot.innerHTML = '<p>甲」<br' + (hidden ? ' style="display:none"' : '') + '>「乙</p>';
var edgeOwner = Juzhen.createJuzhen({ orphan: false, spacing: false, longWord: false });
edgeOwner.render(auditRoot);
record("BR 是強制行緣,hidden=" + hidden,
Array.from(auditRoot.querySelectorAll("jz-char")).every(function (char)
{
return char.classList.contains("jz-half-le") === !hidden;
}));
edgeOwner.revert(auditRoot);
edgeOwner.disconnect();
});
auditRoot.innerHTML = '<p><strong class="original">甲乙丙</strong>,丁</p>';
var splitOwner = Juzhen.createJuzhen({ orphan: false, spacing: false, longWord: false });
splitOwner.render(auditRoot);
var splitFragment = auditRoot.querySelector('[data-jz-kind="splitoff"]');
var inserted = document.createElement("strong");
inserted.className = "external";
inserted.textContent = "新";
splitFragment.before(inserted);
splitOwner.revert(auditRoot);
record("splitoff 不將邊界字移入外部同標籤兄弟",
inserted.textContent === "新" && splitFragment.textContent === "丙"
&& splitFragment.className === "original" && !splitFragment.hasAttribute("data-jz")
&& auditRoot.textContent === "甲乙新丙,丁");
splitOwner.disconnect();
auditRoot.innerHTML = '<p lang="zh-Hans-x-hant">甲,乙</p><p lang="zh-TW-x-hans">甲,乙</p><p lang="">甲,乙</p>';
var localeOwner = Juzhen.createJuzhen({ orphan: false, jinze: false });
localeOwner.render(auditRoot);
var localeChars = auditRoot.querySelectorAll("jz-char");
record("語言私用部分不覆寫 script/region,空值為未知",
localeChars[0].classList.contains("jz-half")
&& !localeChars[1].classList.contains("jz-half")
&& !localeChars[2].classList.contains("jz-half"));
localeOwner.revert(auditRoot);
localeOwner.disconnect();
var splitMatrixPass = true;
var splitOrders = [ [0,1,2], [0,2,1], [1,0,2], [1,2,0], [2,0,1], [2,1,0] ];
[ '<strong>甲乙丙丁</strong>,戊己', '「<strong>甲乙丙丁</strong>」',
'<strong><em>甲乙丙丁</em></strong>,戊己',
'<strong class="author" style="color:red">甲乙丙丁</strong>,戊己',
'<strong>甲乙𠀀\uDB40\uDD00丙丁</strong>,戊己',
'<strong data-jz="author" data-jz-kind="author">甲乙丙丁</strong>,戊己'
].forEach(function (html)
{
splitOrders.forEach(function (order)
{
auditRoot.innerHTML = '<p>' + html + '</p>';
var p = auditRoot.firstElementChild;
var original = p.innerHTML;
var owners = [
Juzhen.createJuzhen({ jiya: false, spacing: false, orphan: false, longWord: false }),
Juzhen.createJuzhen({ jiya: false, spacing: false, jinze: false, longWord: false }),
Juzhen.createJuzhen({ spacing: false, orphan: false, longWord: false })
];
owners.forEach(function (owner) { owner.render(p); });
order.forEach(function (index) { owners[index].revert(p); });
if (p.innerHTML !== original) { splitMatrixPass = false; }
owners.forEach(function (owner) { owner.disconnect(); });
});
});
record("私有切分來源鏈支援多實例六種回退順序,36 組合", splitMatrixPass);
auditRoot.remove();
var moduleFixture = document.getElementById("module-fixture");
var splitSurrogate = document.getElementById("surrogate-spacing");
[ "\uD840", "\uDC00", "\uDB40", "\uDD00" ].forEach(function (part, index)
{
splitSurrogate.children[index].textContent = part;
});
var moduleHtml = moduleFixture.innerHTML
.replace("</em> <code>", "</em><code>")
.replace("</code> <span>", "</code><span>");
var puaSource = document.getElementById("source-pua").textContent;
var modulePill = moduleFixture.querySelector(".module-pill");
var modulePillHtml = modulePill.innerHTML;
var moduleJz = Juzhen.createJuzhen({ pillSelector: "code, kbd, .module-pill" });
moduleJz.render(moduleFixture);
record("作者私用区字符与伪 placeholder 保持原文",
document.getElementById("source-pua").textContent === puaSource);
record("pill 内部的块不被排版", modulePill.innerHTML === modulePillHtml);
var protectedRegion = document.getElementById("module-boundary");
record("段落绑定不进入 avoid 或 text 子树",
!protectedRegion.querySelector("[data-jz-skip]").closest("jz-jinze")
&& !protectedRegion.querySelector('[data-jz-level="text"]').closest("jz-orphan"));
var pillIvs = document.getElementById("pill-ivs-space");
record("pill 两侧跨透明节点空格与完整 IVS 间隙",
pillIvs.textContent === "\uD840\uDC00\uDB40\uDD00x文"
&& pillIvs.querySelectorAll("jz-hws").length === 2
&& pillIvs.querySelector("jz-hws").textContent === "\uD840\uDC00\uDB40\uDD00");
record("禁则连同跨节点 base 与 VS 绑定到闭标点",
document.getElementById("jinze-ivs").querySelector("jz-jinze").textContent
=== "\uD840\uDC00\uDB40\uDD00,");
record("跨节点 surrogate 的间隙位于完整原子之后",
splitSurrogate.querySelector("jz-hws").textContent === "\uD840\uDC00\uDB40\uDD00"
&& splitSurrogate.textContent === "\uD840\uDC00\uDB40\uDD00A");
record("语言脚本按完整子标签匹配",
document.getElementById("locale-subtag-hans").querySelector("jz-char").classList.contains("jz-half")
&& !document.getElementById("locale-subtag-hant").querySelector("jz-char").classList.contains("jz-half"));
moduleJz.revert(moduleFixture);
record("模块回退精确恢复作者结构,保留已规范化空格规则",
moduleFixture.innerHTML === moduleHtml);
var inlineRoot = document.getElementById("inline-root");
var inlineParent = inlineRoot.parentNode;
var inlineHtml = inlineParent.innerHTML;
var inlineJz = Juzhen.createJuzhen({ spacing: false, orphan: false, longWord: false });
inlineJz.render(inlineRoot);
var inlineContained = inlineRoot.parentNode === inlineParent
&& !inlineParent.querySelector(":scope > jz-jinze");
inlineJz.revert(inlineRoot);
record("行内 render 根的禁则不越界并可完整回退",
inlineContained && inlineParent.innerHTML === inlineHtml);
modeJz.revert(modeFixture);
var lifecycleFixture = document.getElementById("lifecycle-fixture");
var partialRoot = document.getElementById("partial-root");
var pendingA = document.getElementById("pending-a");
var pendingB = document.getElementById("pending-b");
var lifecycleJz = Juzhen.createJuzhen();
lifecycleJz.render(lifecycleFixture);
lifecycleJz.render(pendingB);
lifecycleJz.revert(partialRoot);
record(
"局部 revert 同时撤回祖根与子根生成的产物",
!partialRoot.querySelector("[data-jz]")
&& !partialRoot.querySelector("[data-jz-block-level]"),
);
lifecycleJz.render(lifecycleFixture);
lifecycleJz.observe(lifecycleFixture);
pendingA.textContent = "改后,新文。";
lifecycleJz.rerender(pendingB);
await delay();
record(
"手动局部重排保留兄弟块待交付的外部变化",
pendingA.querySelectorAll("jz-char").length === 2,
);
lifecycleJz.disconnect();
lifecycleJz.revert(lifecycleFixture);
var firstGapCount = fixture.querySelectorAll("jz-hws").length;
var firstCharCount = fixture.querySelectorAll("jz-char").length;
record("已生成中西间隙", firstGapCount > 0, firstGapCount + " 个");
record("已生成标点原子", firstCharCount > 0, firstCharCount + " 个");
record("copy-clean 文本保持", fixture.textContent === originalText);
record("无孤立 UTF-16 代理项", !hasUnpairedSurrogate(fixture.textContent));
var ivs = String.fromCodePoint(0x20000, 0xE0100);
var forward = document.getElementById("ivs-forward");
var forwardNoOrphan = document.getElementById("ivs-forward-no-orphan");
var forwardSameNode = document.getElementById("ivs-forward-same-node");
var forwardSameNodePrefix = document.getElementById("ivs-forward-same-node-prefix");
var reverse = document.getElementById("ivs-reverse");
var forwardCarrier = forward.querySelector("jz-hws");
var carrierKind = forwardCarrier?.getAttribute("data-jz-kind") ?? "none";
record("跨节点 IVS 正向完整", forward.textContent.includes(ivs + "A"));
record(
"关闭 orphan 时 IVS 正向间隙保留",
forwardNoOrphan.querySelectorAll("jz-hws").length === 1,
"gaps=" + forwardNoOrphan.querySelectorAll("jz-hws").length
+ ",HTML=" + forwardNoOrphan.innerHTML,
);
record(
"同一 Text 节点的 IVS 正向间隙保留",
forwardSameNode.querySelectorAll("jz-hws").length === 1,
"gaps=" + forwardSameNode.querySelectorAll("jz-hws").length
+ ",HTML=" + forwardSameNode.innerHTML,
);
record(
"带前导 CJK 的同节点 IVS 正向间隙保留",
forwardSameNodePrefix.querySelectorAll("jz-hws").length === 1,
"gaps=" + forwardSameNodePrefix.querySelectorAll("jz-hws").length
+ ",HTML=" + forwardSameNodePrefix.innerHTML,
);
record("跨节点 IVS 反向完整", reverse.textContent.includes("A" + ivs));
record(
"跨节点 IVS 在间隙载体中保持完整",
carrierKind === "wrap"
&& forwardCarrier?.textContent === ivs
&& forwardCarrier?.hasAttribute("data-jz-cross-node-atom")
&& Array.from(forwardCarrier.childNodes)
.filter(function (node) { return node.nodeType === Node.TEXT_NODE; })
.length === 2,
"forward gaps=" + forward.querySelectorAll("jz-hws").length
+ ",reverse gaps=" + reverse.querySelectorAll("jz-hws").length
+ ",carrier kind=" + carrierKind
+ ",carrier text=" + JSON.stringify(forwardCarrier?.textContent ?? null)
+ ",HTML=" + forward.innerHTML,
);
authorForward.click();
authorReverse.click();
record(
"作者元素身份与监听器保持",
document.getElementById("author-forward") === authorForward
&& document.getElementById("author-reverse") === authorReverse
&& document.querySelectorAll("#author-forward").length === 1
&& document.querySelectorAll("#author-reverse").length === 1
&& clickCount === 2,
);
var gap = fixture.querySelector("jz-hws");
var gapStyle = getComputedStyle(gap);
var gapRatio = parseFloat(gapStyle.marginRight) / parseFloat(gapStyle.fontSize);
record("间隙视觉宽度约为四分之一字", gapRatio >= 0.2 && gapRatio <= 0.3, gapRatio.toFixed(3) + "em");
var selection = window.getSelection();
var selectionRange = document.createRange();
selectionRange.selectNodeContents(forwardNoOrphan);
selection?.removeAllRanges();
selection?.addRange(selectionRange);
var selectionText = selection?.toString() ?? "";
record(
"Selection 文本不含生成断行字符",
selectionText === forwardNoOrphan.textContent,
"zwsp=" + selectionText.includes("\u200B")
+ ",length delta=" + (selectionText.length - forwardNoOrphan.textContent.length),
);
selection?.removeAllRanges();
await delay();
await delay();
var lineBreak = document.getElementById("ivs-line-break");
var lineGap = lineBreak.querySelector("jz-hws");
var lineNext = lineBreak.lastChild;
var lineGapRect = lineGap?.getBoundingClientRect();
var lineNextRange = document.createRange();
if (lineNext?.nodeType === Node.TEXT_NODE)
{
lineNextRange.setStart(lineNext, 0);
lineNextRange.setEnd(lineNext, 1);
}
var lineNextRect = lineNextRange.getBoundingClientRect();
var lineBoxRect = lineBreak.getBoundingClientRect();
var lineGapStyle = lineGap ? getComputedStyle(lineGap) : null;
var lineGapMargin = lineGapStyle
? parseFloat(lineGapStyle.marginRight) / parseFloat(lineGapStyle.fontSize)
: -1;
var nextLineIndent = (lineNextRect.left - lineBoxRect.left)
/ parseFloat(lineGapStyle?.fontSize || "16");
record(
"行末间隙载体留在前行并清零,下一行无缩进",
!!lineGapRect
&& lineNextRect.top > lineGapRect.top + 2
&& lineGap.classList.contains("jz-hws-trim")
&& Math.abs(lineGapMargin) <= 0.01
&& Math.abs(nextLineIndent) <= 0.05,
"trim=" + !!lineGap?.classList.contains("jz-hws-trim")
+ ",margin=" + lineGapMargin.toFixed(3)
+ "em,indent=" + nextLineIndent.toFixed(3) + "em",
);
var nowrapCase = document.getElementById("ivs-nowrap");
var nowrapGap = nowrapCase.querySelector("jz-hws");
var nowrapNext = nowrapCase.lastChild;
var nowrapNextRange = document.createRange();
if (nowrapNext?.nodeType === Node.TEXT_NODE)
{
nowrapNextRange.setStart(nowrapNext, 0);
nowrapNextRange.setEnd(nowrapNext, 1);
}
var nowrapGapRect = nowrapGap?.getBoundingClientRect();
var nowrapNextRect = nowrapNextRange.getBoundingClientRect();
record(
"生成断行机会不突破 nowrap",
!!nowrapGapRect && Math.abs(nowrapGapRect.top - nowrapNextRect.top) <= 2,
);
var ivsOpen = document.getElementById("ivs-lineedge-open").querySelector("jz-char");
var ivsClose = document.getElementById("ivs-lineedge-close").querySelector("jz-char");
record(
"IVS 相邻行首开标点保持行缘半形",
!!ivsOpen?.classList.contains("jz-half-le"),
);
record(
"IVS 相邻行尾闭标点保持行缘半形",
!!ivsClose?.classList.contains("jz-half-le"),
);
var comma = Array.from(fixture.querySelectorAll("jz-char"))
.find(function (item) { return item.textContent === ","; });
var commaStyle = comma ? getComputedStyle(comma) : null;
var commaRatio = comma && commaStyle
? comma.getBoundingClientRect().width / parseFloat(commaStyle.fontSize)
: 0;
record(
"margin 后备的句内逗号为半形",
commaRatio >= 0.45 && commaRatio <= 0.55,
commaRatio.toFixed(3) + "em",
);
jz.render(fixture);
record(
"重复 render 幂等",
fixture.querySelectorAll("jz-hws").length === firstGapCount
&& fixture.querySelectorAll("jz-char").length === firstCharCount,
);
jz.revert(fixture);
var internalComments = [];
var commentWalker = document.createTreeWalker(fixture, NodeFilter.SHOW_COMMENT);
while (commentWalker.nextNode())
{
if (commentWalker.currentNode.textContent?.startsWith("jz-spacing-") === true)
{
internalComments.push(commentWalker.currentNode);
}
}
record(
"revert 清除全部产物",
fixture.querySelectorAll("[data-jz]").length === 0
&& internalComments.length === 0
&& document.getElementById("author-forward") === authorForward
&& document.getElementById("author-reverse") === authorReverse
&& fixture.innerHTML === originalHtml
&& fixture.textContent === originalText,
);
jz.render(fixture);
observerButton.disabled = false;
observerButton.addEventListener("click", async function ()
{
observerButton.disabled = true;
try
{
jz.observe(fixture);
document.getElementById("observer-case").textContent = "观察Observer恢复";
await delay();
await delay();
var detachedRoot = document.createElement("main");
detachedRoot.innerHTML = '<p lang="zh-Hant">原文,甲乙。</p>';
var detachedJz = Juzhen.createJuzhen();
detachedJz.render(detachedRoot);
detachedJz.observe(detachedRoot);
detachedRoot.querySelector("p").textContent = "修改ABC,丙丁。";
var scopeRoot = document.createElement("main");
var scopeOutside = document.createElement("aside");
document.body.append(scopeRoot, scopeOutside);
var scopeJz = Juzhen.createJuzhen();
scopeJz.observe(scopeRoot);
var escaped = document.createElement("p");
escaped.textContent = "移出ABC,作者內容。";
scopeRoot.appendChild(escaped);
scopeOutside.appendChild(escaped);
var escapedOriginal = escaped.outerHTML;
await delay();
await delay();
record("未挂入 document 的观察根仍增量排版",
!!detachedRoot.querySelector("jz-hws") && !!detachedRoot.querySelector("jz-char"));
record("同批移出观察根的段落不被新增排版", escaped.outerHTML === escapedOriginal);
detachedJz.revert(detachedRoot);
detachedJz.disconnect();
scopeJz.revert(scopeRoot);
scopeJz.disconnect();
scopeRoot.remove();
scopeOutside.remove();
var relocateRoot = document.createElement("main");
relocateRoot.innerHTML = '<section><p>文字ABCDEF,甲乙。</p></section>'
+ '<section data-jz-level="text"></section>';
var relocateJz = Juzhen.createJuzhen();
relocateJz.render(relocateRoot);
relocateJz.observe(relocateRoot);
var relocatedParagraph = relocateRoot.querySelector("p");
relocateRoot.lastElementChild.appendChild(relocatedParagraph);
await delay();
record("已排版段落移入 text 范围后撤回旧段落约束",
relocatedParagraph.getAttribute("data-jz-block-level") === "text"
&& !relocatedParagraph.querySelector("jz-jinze,jz-orphan,[data-jz=longWord]")
&& !!relocatedParagraph.querySelector("jz-hws"));
var relocatedResult = relocatedParagraph.outerHTML;
relocateJz.disconnect();
relocateJz.rerender(relocateRoot);
record("移动段落的增量结果与完整重排一致",
relocatedResult === relocatedParagraph.outerHTML);
relocateJz.revert(relocateRoot);
record(
"MutationObserver 增量重排",
document.getElementById("observer-case").querySelectorAll("jz-hws").length > 0,
);
}
catch (error)
{
record(
"MutationObserver 增量重排",
false,
error instanceof Error ? error.message : String(error),
);
}
finally
{
jz.disconnect();
summary.dataset.state = failures === 0 ? "pass" : "fail";
summaryText.textContent = failures === 0
? "PASS:全部浏览器验收项通过"
: "FAIL:" + failures + " 项浏览器验收失败";
document.title = failures === 0
? "PASS — 聚珍浏览器验收"
: "FAIL — 聚珍浏览器验收";
}
});
}
catch (error)
{
record("验收脚本未抛错", false, error instanceof Error ? error.message : String(error));
}
summary.dataset.state = failures === 0 ? "pass" : "fail";
summaryText.textContent = failures === 0
? "核心验收通过;请运行 MutationObserver 验收"
: "FAIL:" + failures + " 项浏览器验收失败";
document.title = failures === 0
? "CORE PASS — 聚珍浏览器验收"
: "FAIL — 聚珍浏览器验收";
});
summaryText.textContent = "待运行核心验收";
document.title = "READY — 聚珍浏览器验收";
}());
</script>
</body>
</html>