711 lines
38 KiB
HTML
711 lines
38 KiB
HTML
<!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">甲𠀀</strong>󠄀A</p>
|
||
<p id="ivs-forward-no-orphan" data-juzhen-off="orphan"><strong>甲𠀀</strong>󠄀A</p>
|
||
<p id="ivs-forward-same-node" data-juzhen-off="orphan">𠀀󠄀A</p>
|
||
<p id="ivs-forward-same-node-prefix" data-juzhen-off="orphan">甲𠀀󠄀A</p>
|
||
<p id="ivs-reverse">A<strong id="author-reverse">𠀀</strong>󠄀乙</p>
|
||
<p id="ivs-line-break" data-juzhen-off="orphan" style="width:2.5em;text-align:justify"><strong>甲𠀀</strong>󠄀A</p>
|
||
<p id="ivs-nowrap" data-juzhen-off="orphan" style="width:1em;white-space:nowrap"><strong>甲𠀀</strong>󠄀A</p>
|
||
<p id="ivs-lineedge-open" lang="zh-Hant">「𠀀󠄀甲</p>
|
||
<p id="ivs-lineedge-close" lang="zh-Hant">甲𠀀󠄀」</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">漢0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>𠀀</strong><em>󠄀</em> <code>x</code> <span>文</span></p>
|
||
<p id="jinze-ivs" data-jz-level="paragraph">前文<strong>𠀀</strong><em>󠄀</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">︀</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 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>
|