3a4fa1c932
保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。 验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
190 lines
7.8 KiB
JavaScript
190 lines
7.8 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { test } from "node:test";
|
||
import { setupDom } from "./helpers.mjs";
|
||
import { createJuzhen } from "../dist/juzhen.mjs";
|
||
|
||
function sameLine(document)
|
||
{
|
||
const view = document.defaultView;
|
||
const previous = globalThis.ResizeObserver;
|
||
const elementRects = view.Element.prototype.getClientRects;
|
||
const rangeRects = view.Range.prototype.getClientRects;
|
||
const rect = { top: 0, bottom: 20, left: 0, right: 20, width: 20, height: 20 };
|
||
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
|
||
view.Element.prototype.getClientRects = () => [ rect ];
|
||
view.Range.prototype.getClientRects = () => [ rect ];
|
||
return () =>
|
||
{
|
||
view.Element.prototype.getClientRects = elementRects;
|
||
view.Range.prototype.getClientRects = rangeRects;
|
||
if (previous === undefined) { delete globalThis.ResizeObserver; }
|
||
else { globalThis.ResizeObserver = previous; }
|
||
};
|
||
}
|
||
|
||
for (const orphan of [ false, true ])
|
||
{
|
||
test("line edge:同行 orphan 不製造行首 orphan=" + orphan, () =>
|
||
{
|
||
const { dom, document } = setupDom('<main lang="zh-Hant"><p>前甲「乙丙」</p></main>');
|
||
const restore = sameLine(document);
|
||
const jz = createJuzhen({ orphan, spacing: false, longWord: false });
|
||
try
|
||
{
|
||
jz.render(document.querySelector("main"));
|
||
assert.equal(document.querySelector(".bd-open").classList.contains("jz-half-le"), false);
|
||
}
|
||
finally { jz.revert(); restore(); dom.window.close(); }
|
||
});
|
||
}
|
||
|
||
for (const display of [ "inline", "inline-block", "contents", "block" ])
|
||
{
|
||
test("line edge:自訂隔離元素按實際物理盒攀爬 display=" + display, () =>
|
||
{
|
||
const { dom, document } = setupDom('<p lang="zh-Hant">前甲<x-box style="display:'
|
||
+ display + '">「乙」</x-box>丙</p>');
|
||
const restore = sameLine(document);
|
||
const jz = createJuzhen({ isolateSelector: "x-box", orphan: false, spacing: false, longWord: false });
|
||
try
|
||
{
|
||
jz.render();
|
||
for (const char of document.querySelectorAll("jz-char"))
|
||
{
|
||
assert.equal(char.classList.contains("jz-half-le"), display === "block");
|
||
}
|
||
}
|
||
finally { jz.revert(); restore(); dom.window.close(); }
|
||
});
|
||
}
|
||
|
||
for (const hidden of [ false, true ])
|
||
{
|
||
test("line edge:BR 為強制行緣,隱藏時可穿越 hidden=" + hidden, () =>
|
||
{
|
||
const { dom, document } = setupDom('<p lang="zh-Hant">甲」<br'
|
||
+ (hidden ? ' style="display:none"' : '') + '>「乙</p>');
|
||
const restore = sameLine(document);
|
||
const jz = createJuzhen({ orphan: false, spacing: false, longWord: false });
|
||
try
|
||
{
|
||
jz.render();
|
||
for (const char of document.querySelectorAll("jz-char"))
|
||
{
|
||
assert.equal(char.classList.contains("jz-half-le"), !hidden);
|
||
}
|
||
}
|
||
finally { jz.revert(); restore(); dom.window.close(); }
|
||
});
|
||
}
|
||
|
||
for (const action of [ "insert", "replace-source", "edit-fragment", "edit-source" ])
|
||
{
|
||
test("splitoff:回退須驗來源身份並保留外部樣式 action=" + action, () =>
|
||
{
|
||
const { dom, document } = setupDom('<p><strong class="original">甲乙丙</strong>,丁</p>');
|
||
const p = document.querySelector("p");
|
||
const source = p.querySelector("strong");
|
||
const jz = createJuzhen({ orphan: false, spacing: false, longWord: false });
|
||
try
|
||
{
|
||
jz.render(p);
|
||
const fragment = p.querySelector('[data-jz-kind="splitoff"]');
|
||
assert.ok(fragment);
|
||
if (action === "insert")
|
||
{
|
||
const added = document.createElement("strong");
|
||
added.className = "external";
|
||
added.textContent = "新";
|
||
fragment.before(added);
|
||
}
|
||
else if (action === "replace-source")
|
||
{
|
||
const replacement = source.cloneNode(false);
|
||
replacement.textContent = "替";
|
||
source.replaceWith(replacement);
|
||
}
|
||
else if (action === "edit-fragment") { fragment.className = "edited"; }
|
||
else { source.className = "edited"; }
|
||
jz.revert(p);
|
||
assert.equal(fragment.parentNode, p, "來源失效時保留片段當前父與樣式");
|
||
assert.equal(fragment.textContent, "丙");
|
||
assert.equal(fragment.className, action === "edit-fragment" ? "edited" : "original");
|
||
assert.equal(p.querySelector("[data-jz]"), null);
|
||
if (action === "insert")
|
||
{
|
||
assert.equal(p.querySelector(".external").textContent, "新");
|
||
assert.equal(p.textContent, "甲乙新丙,丁");
|
||
}
|
||
if (action === "replace-source") { assert.equal(p.textContent, "替丙,丁"); }
|
||
jz.revert(p);
|
||
assert.equal(fragment.textContent, "丙", "撤回所有權後再次 revert 不碰片段");
|
||
}
|
||
finally { jz.revert(p); dom.window.close(); }
|
||
});
|
||
}
|
||
|
||
test("splitoff:同來源多層切分仍完整還原作者結構", () =>
|
||
{
|
||
const { dom, document } = setupDom('<p>「<strong><em>甲乙丙丁</em></strong>」</p>');
|
||
const p = document.querySelector("p");
|
||
const original = p.innerHTML;
|
||
const jz = createJuzhen();
|
||
try { jz.render(p); jz.revert(p); assert.equal(p.innerHTML, original); }
|
||
finally { jz.revert(p); dom.window.close(); }
|
||
});
|
||
|
||
for (const [ html, defaultLang, half ] of [
|
||
[ '<main lang="zh-Hans"><p lang="">甲,乙</p></main>', "zh-Hans", false ],
|
||
[ '<main><p>甲,乙</p></main>', "", false ],
|
||
[ '<main lang="zh-Hant"><p lang="zh-Hans-x-hant">甲,乙</p></main>', "zh-Hant", true ],
|
||
[ '<main lang="zh-Hans"><p lang="zh-TW-x-hans">甲,乙</p></main>', "zh-Hans", false ],
|
||
])
|
||
{
|
||
test("locale:空值及私用內容不冒充繼承語言 " + html, () =>
|
||
{
|
||
const { dom, document } = setupDom(html);
|
||
const jz = createJuzhen({ lang: { default: defaultLang }, jinze: false, orphan: false });
|
||
try
|
||
{
|
||
jz.render(document.querySelector("main"));
|
||
assert.equal(document.querySelector("jz-char").classList.contains("jz-half"), half);
|
||
}
|
||
finally { jz.revert(); dom.window.close(); }
|
||
});
|
||
}
|
||
|
||
test("splitoff:多實例六種回退順序恢復原結構,36 組合", () =>
|
||
{
|
||
const fixtures = [
|
||
'<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>,戊己',
|
||
];
|
||
const orders = [ [ 0, 1, 2 ], [ 0, 2, 1 ], [ 1, 0, 2 ], [ 1, 2, 0 ], [ 2, 0, 1 ], [ 2, 1, 0 ] ];
|
||
for (const html of fixtures)
|
||
{
|
||
for (const order of orders)
|
||
{
|
||
const { dom, document } = setupDom('<p lang="zh-Hant">' + html + '</p>');
|
||
const p = document.querySelector("p");
|
||
const original = p.innerHTML;
|
||
const owners = [
|
||
createJuzhen({ jiya: false, spacing: false, orphan: false, longWord: false }),
|
||
createJuzhen({ jiya: false, spacing: false, jinze: false, longWord: false }),
|
||
createJuzhen({ spacing: false, orphan: false, longWord: false }),
|
||
];
|
||
try
|
||
{
|
||
owners.forEach((owner) => owner.render(p));
|
||
order.forEach((index) => owners[index].revert(p));
|
||
assert.equal(p.innerHTML, original, html + " order=" + order.join(","));
|
||
}
|
||
finally { owners.forEach((owner) => owner.disconnect()); dom.window.close(); }
|
||
}
|
||
}
|
||
});
|