fix: 修复行缘、切分还原与语言解析并同步文档

保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。

验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
This commit is contained in:
2026-10-03 21:15:54 +08:00
parent 024ad5da93
commit 3a4fa1c932
33 changed files with 983 additions and 140 deletions
+84 -16
View File
@@ -328,8 +328,8 @@ var Juzhen = (() => {
* collectRuns/jiyaAdjacency/jinze/orphan/split 各 pass 逐元素重算——以 categoryCache
* 記憶化砍除跨 pass 重複。安全性:單次 render 內一元素之分類**恆定**(class/skip 屬性/
* nodeName 皆不因 render 變更;jz-* 包裹為新建元素、按 nodeName 分類亦恆定),且 Finder
* 隨 makeCtx 每次 render 重建 → 快取僅存活單次 render,無跨 render 失效問題(持久化屬 C1,
* 另議)。
* 隨實例持久化,跨 render/revert 復用;屬性或位置變化由 invalidateSubtree 清除受影響
* 子樹,不能沿用舊祖先鏈結果。
*/
category(el) {
const cached = this.categoryCache.get(el);
@@ -837,6 +837,45 @@ var Juzhen = (() => {
var ATTRIBUTE_WRITERS = /* @__PURE__ */ new WeakMap();
var OWNER_ATTRIBUTES = /* @__PURE__ */ new WeakMap();
var UNWRAPPED_PARENTS = /* @__PURE__ */ new WeakMap();
var SPLITOFF_SOURCES = /* @__PURE__ */ new WeakMap();
function registerSplitoffSource(clone, source, pass, ownedElements) {
if (!ownedElements.has(clone) || !SPLITOFF_TAGS.has(clone.nodeName) || clone.nodeName !== source.nodeName) {
return;
}
const earlier = SPLITOFF_SOURCES.get(source);
const inherited = earlier && source.getAttribute(PASS_ATTR) === earlier.pass && source.getAttribute(KIND_ATTR) === "splitoff" ? earlier : null;
SPLITOFF_SOURCES.set(clone, {
source,
pass,
originalPass: inherited ? inherited.originalPass : clone.getAttribute(PASS_ATTR),
originalKind: inherited ? inherited.originalKind : clone.getAttribute(KIND_ATTR)
});
}
function sameSplitAttributes(a, b) {
const attrs = (el) => el.getAttributeNames().filter((name) => name !== PASS_ATTR && name !== KIND_ATTR);
const names = attrs(a);
return names.length === attrs(b).length && names.every((name) => a.getAttribute(name) === b.getAttribute(name));
}
function splitOrigin(node) {
let current = resolveUnwrappedParent(node);
const visited = /* @__PURE__ */ new Set();
while (current) {
if (visited.has(current)) {
return null;
}
visited.add(current);
if (current.nodeType !== 1) {
return current;
}
const el = current;
const record = SPLITOFF_SOURCES.get(el);
if (!record || el.getAttribute(PASS_ATTR) !== record.pass || el.getAttribute(KIND_ATTR) !== "splitoff") {
return current;
}
current = resolveUnwrappedParent(record.source);
}
return null;
}
function recordUnwrappedParent(el, parent, ownedElements) {
const pass = el.getAttribute(PASS_ATTR);
if (ownedElements.has(el) && pass && knownProduct(pass, el.getAttribute(KIND_ATTR), el) && !el.parentNode && !el.hasChildNodes()) {
@@ -1022,20 +1061,32 @@ var Juzhen = (() => {
continue;
}
touched.add(parent);
const record = SPLITOFF_SOURCES.get(so);
if (!record || record.pass !== pass) {
continue;
}
const source = resolveUnwrappedParent(record.source);
const prev = so.previousSibling;
if (prev && prev.nodeType === 1 && prev.nodeName === so.nodeName) {
const origin = source ? splitOrigin(source) : null;
if (origin && prev?.nodeType === 1 && splitOrigin(prev) === origin && prev.nodeName === so.nodeName && sameSplitAttributes(so, prev)) {
while (so.firstChild) {
prev.appendChild(so.firstChild);
}
parent.removeChild(so);
recordUnwrappedParent(so, prev, ownedElements);
} else {
while (so.firstChild) {
parent.insertBefore(so.firstChild, so);
for (const [name, value] of [
[PASS_ATTR, record.originalPass],
[KIND_ATTR, record.originalKind]
]) {
if (value === null) {
so.removeAttribute(name);
} else {
so.setAttribute(name, value);
}
}
parent.removeChild(so);
recordUnwrappedParent(so, parent, ownedElements);
}
SPLITOFF_SOURCES.delete(so);
ownedElements.delete(so);
}
for (const p of touched) {
@@ -1355,11 +1406,11 @@ var Juzhen = (() => {
"other": "quanjiao"
};
function effectiveLang(node, fallback) {
let p = node.parentNode;
let p = node.nodeType === 1 ? node : node.parentNode;
while (p && p.nodeType === 1) {
const el = p;
const v = el.getAttribute("lang");
if (v) {
if (v !== null) {
return v.toLowerCase();
}
p = p.parentNode;
@@ -1372,13 +1423,19 @@ var Juzhen = (() => {
return "ja";
}
if (l === "zh" || l.indexOf("zh-") === 0) {
if (/(?:^|-)hant(?:-|$)/.test(l)) {
const subtags = l.split("-");
let at = 1;
for (let count = 0; count < 3 && /^[a-z]{3}$/.test(subtags[at] || ""); count++) {
at++;
}
const script = /^[a-z]{4}$/.test(subtags[at] || "") ? subtags[at++] : void 0;
if (script === "hant") {
return "zh-Hant";
}
if (/(?:^|-)hans(?:-|$)/.test(l)) {
if (script === "hans") {
return "zh-Hans";
}
if (/-(tw|hk|mo)\b/.test(l)) {
if (["tw", "hk", "mo"].includes(subtags[at] || "")) {
return "zh-Hant";
}
if (l === "zh") {
@@ -1753,9 +1810,10 @@ var Juzhen = (() => {
if (rightAtLevel) {
clone = anc.cloneNode(false);
clone.removeAttribute("id");
ownedElements.add(clone);
registerSplitoffSource(clone, anc, pass, ownedElements);
clone.setAttribute(PASS_ATTR2, pass);
clone.setAttribute(KIND_ATTR2, "splitoff");
ownedElements.add(clone);
let n = rightAtLevel;
while (n) {
const next = n.nextSibling;
@@ -2240,7 +2298,7 @@ var Juzhen = (() => {
owners.delete(owner);
}
}
var JZ_WRAP_TAGS = ["JZ-JINZE", "JZ-CHAR", "JZ-INNER", "JZ-HWS"];
var JZ_WRAP_TAGS = ["JZ-JINZE", "JZ-CHAR", "JZ-INNER", "JZ-HWS", "JZ-ORPHAN"];
function buildWrapSet(options) {
const s = new Set(JZ_WRAP_TAGS);
for (const name of options.finder.styleInlines) {
@@ -2248,6 +2306,13 @@ var Juzhen = (() => {
}
return s;
}
function inlineFlow(el, wrapSet) {
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display;
if (display) {
return display === "contents" || display.startsWith("inline") || display.startsWith("ruby");
}
return wrapSet.has(el.nodeName);
}
function rectOf(n, side) {
if (n.nodeType === 3 && n.data.length > 0) {
const t = n;
@@ -2277,6 +2342,9 @@ var Juzhen = (() => {
let parentRef = from.parentNode;
for (; ; ) {
while (n) {
if (n.nodeType === 1 && n.nodeName === "BR" && n.ownerDocument.defaultView?.getComputedStyle(n).display !== "none") {
return null;
}
const r = rectOf(n, side);
if (r) {
return r;
@@ -2286,7 +2354,7 @@ var Juzhen = (() => {
if (!parentRef || parentRef.nodeType !== 1) {
return null;
}
if (!wrapSet.has(parentRef.nodeName)) {
if (!inlineFlow(parentRef, wrapSet)) {
return null;
}
n = side === "prev" ? parentRef.previousSibling : parentRef.nextSibling;
@@ -3402,7 +3470,7 @@ var Juzhen = (() => {
const scope = opts.scope || {};
return {
locale: {
default: langOpt.default || "zh-Hant",
default: langOpt.default === void 0 ? "zh-Hant" : langOpt.default,
...langOpt.style ? { style: langOpt.style } : {},
policy: langOpt.policy || {},
stylePolicy: Array.isArray(langOpt.stylePolicy) ? langOpt.stylePolicy.slice() : []