fix: 修复行缘、切分还原与语言解析并同步文档
保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。 验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user