feat(rules): 建立共享排版契约并收束 WebKit 与 Unicode 基线
This commit is contained in:
+374
@@ -0,0 +1,374 @@
|
||||
<!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>
|
||||
|
||||
<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 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();
|
||||
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>
|
||||
Reference in New Issue
Block a user