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
+91
View File
@@ -352,6 +352,97 @@
});
layoutJz.revert(layoutFixture);
layoutJz.disconnect();
var auditRoot = document.createElement("section");
auditRoot.lang = "zh-Hant";
auditRoot.style.cssText = "font-size:20px;width:600px;text-align:left";
document.body.appendChild(auditRoot);
[ false, true ].forEach(function (orphan)
{
auditRoot.innerHTML = '<p>前甲「乙丙」</p>';
var edgeOwner = Juzhen.createJuzhen({ orphan: orphan, spacing: false, longWord: false });
edgeOwner.render(auditRoot);
record("同行 orphan 不製造行首,orphan=" + orphan,
!auditRoot.querySelector(".bd-open").classList.contains("jz-half-le"));
edgeOwner.revert(auditRoot);
edgeOwner.disconnect();
});
[ "inline", "inline-block", "contents", "block" ].forEach(function (display)
{
auditRoot.innerHTML = '<p>前甲<x-box style="display:' + display + '">「乙」</x-box>丙</p>';
var edgeOwner = Juzhen.createJuzhen({ isolateSelector: "x-box", orphan: false, spacing: false, longWord: false });
edgeOwner.render(auditRoot);
record("自訂隔離元素按物理行內流判斷,display=" + display,
Array.from(auditRoot.querySelectorAll("jz-char")).every(function (char)
{
return char.classList.contains("jz-half-le") === (display === "block");
}));
edgeOwner.revert(auditRoot);
edgeOwner.disconnect();
});
[ false, true ].forEach(function (hidden)
{
auditRoot.innerHTML = '<p>甲」<br' + (hidden ? ' style="display:none"' : '') + '>「乙</p>';
var edgeOwner = Juzhen.createJuzhen({ orphan: false, spacing: false, longWord: false });
edgeOwner.render(auditRoot);
record("BR 是強制行緣,hidden=" + hidden,
Array.from(auditRoot.querySelectorAll("jz-char")).every(function (char)
{
return char.classList.contains("jz-half-le") === !hidden;
}));
edgeOwner.revert(auditRoot);
edgeOwner.disconnect();
});
auditRoot.innerHTML = '<p><strong class="original">甲乙丙</strong>,丁</p>';
var splitOwner = Juzhen.createJuzhen({ orphan: false, spacing: false, longWord: false });
splitOwner.render(auditRoot);
var splitFragment = auditRoot.querySelector('[data-jz-kind="splitoff"]');
var inserted = document.createElement("strong");
inserted.className = "external";
inserted.textContent = "新";
splitFragment.before(inserted);
splitOwner.revert(auditRoot);
record("splitoff 不將邊界字移入外部同標籤兄弟",
inserted.textContent === "新" && splitFragment.textContent === "丙"
&& splitFragment.className === "original" && !splitFragment.hasAttribute("data-jz")
&& auditRoot.textContent === "甲乙新丙,丁");
splitOwner.disconnect();
auditRoot.innerHTML = '<p lang="zh-Hans-x-hant">甲,乙</p><p lang="zh-TW-x-hans">甲,乙</p><p lang="">甲,乙</p>';
var localeOwner = Juzhen.createJuzhen({ orphan: false, jinze: false });
localeOwner.render(auditRoot);
var localeChars = auditRoot.querySelectorAll("jz-char");
record("語言私用部分不覆寫 script/region,空值為未知",
localeChars[0].classList.contains("jz-half")
&& !localeChars[1].classList.contains("jz-half")
&& !localeChars[2].classList.contains("jz-half"));
localeOwner.revert(auditRoot);
localeOwner.disconnect();
var splitMatrixPass = true;
var splitOrders = [ [0,1,2], [0,2,1], [1,0,2], [1,2,0], [2,0,1], [2,1,0] ];
[ '<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>,戊己'
].forEach(function (html)
{
splitOrders.forEach(function (order)
{
auditRoot.innerHTML = '<p>' + html + '</p>';
var p = auditRoot.firstElementChild;
var original = p.innerHTML;
var owners = [
Juzhen.createJuzhen({ jiya: false, spacing: false, orphan: false, longWord: false }),
Juzhen.createJuzhen({ jiya: false, spacing: false, jinze: false, longWord: false }),
Juzhen.createJuzhen({ spacing: false, orphan: false, longWord: false })
];
owners.forEach(function (owner) { owner.render(p); });
order.forEach(function (index) { owners[index].revert(p); });
if (p.innerHTML !== original) { splitMatrixPass = false; }
owners.forEach(function (owner) { owner.disconnect(); });
});
});
record("私有切分來源鏈支援多實例六種回退順序,36 組合", splitMatrixPass);
auditRoot.remove();
var moduleFixture = document.getElementById("module-fixture");
var splitSurrogate = document.getElementById("surrogate-spacing");
[ "\uD840", "\uDC00", "\uDB40", "\uDD00" ].forEach(function (part, index)
+6 -5
View File
@@ -16,8 +16,8 @@
font-family: system-ui, sans-serif;
}
/* 字型類獨立於 .juzhen:A 用裸 .cn(瀏覽器原生排版),B/C 才掛 .juzhen
(含 normalize 之 text-spacing-trim:space-all)。否則 A 之原生標點被 space-all
撐開、顯得比 B 鬆,造成「B 似乎有效」之假象(實則 B 全形未擠、與真原文等寬)。 */
(禁用原生 text-autospace)。聚珍不設定 text-spacing-trim;A 保留瀏覽器原生
排版,B/C 使用相同受管基線以比較 halt 與 margin。 */
/* 字型設於容器即可——聚珍不管字型(無 _lang.css)、亦不強制 jz-inner(`font:
inherit`),故消費端字型經繼承正確套到標點字面(標點亦以 FZShuSong 渲染,halt
確實缺席)。此即「字型由消費端負責」之示例。 */
@@ -47,8 +47,9 @@
<body>
<h1>方正書宋 GBK · margin 後備模式</h1>
<p class="meta">
此字型僅含 <code>vert</code>、<b>無 <code>halt</code></b>,故 halt 模式之開明式擠壓「完全無效」(標點仍佔全形)。
margin 後備模式以負 margin 依字形偏側收半形——本字型簡體點號(,、。;:!?)墨色皆偏左下,故收右側空白。
本頁需本機安裝指定版本的方正書宋 GBK;若發生字型替代,下列比較不能作該字型驗收。
已測版本僅含 <code>vert</code>、<b>無 <code>halt</code></b>,故 halt 模式之開明式擠壓「完全無效」(標點仍佔全形)。
margin 後備模式以 0.5em 外盒收半形、內盒依墨色偏側定位——本字型簡體點號(,、。;:!?)墨色皆偏左下,故收右側空白。
白底行盒寬度即直接反映效果:B 與 A 同寬(未擠),C 明顯窄一截(已擠)。
</p>
@@ -78,7 +79,7 @@
.render(document.getElementById("margin"));
// A 面板留原文(不 render)。
// 註:margin 模式以 width:0.5em+字身溢出定位,標點 advance 真為 0.5em;整句
// 行寬 halt 24.0em → margin 19.5em(10 標點各收 0.5em),可於瀏覽器 devtools 量。
// 整句行寬差取決於實際壓縮字符及目標字型,可於瀏覽器 devtools 量;不以固定 em 值驗收。
</script>
</body>
</html>
+11 -1
View File
@@ -108,24 +108,32 @@
});
}
var jz = makeJz();
var rendered = false;
var statusEl = document.getElementById("status");
function render()
{
jz.revert(document.body); // 先還原(重套政策時清除上次產物;首次為 no-op)
jz = makeJz();
jz.render(document.body);
rendered = true;
statusEl.textContent = "已 render(" + document.querySelectorAll("jz-hws").length + " 间隙、" + document.querySelectorAll("jz-char").length + " 标点原子;繁体政策:"
+ (forceKaiming ? "开明式" : "全角式") + ")";
}
function revert()
{
jz.revert(document.body);
rendered = false;
statusEl.textContent = "已 revert(DOM 还原)";
}
function refreshLayout()
{
// 對齊/寬度改變不一定改變 body 尺寸,須顯式重算;revert 後保持停用。
if (rendered) { jz.rerender(document.body); }
}
document.getElementById("force-kaiming").addEventListener("change", function (e)
{
forceKaiming = e.target.checked;
render();
if (rendered) { render(); }
});
// 量測每個 jz-char 之實際渲染寬度(em)。輸出純 ASCII(碼位+class+數值),
// 並按 lang/codepoint/class 去重,便於回報診斷 halt 是否生效、收哪側。
@@ -167,6 +175,7 @@
document.getElementById("justify").addEventListener("change", function (e)
{
document.body.classList.toggle("justify", e.target.checked);
refreshLayout();
});
document.getElementById("wide").addEventListener("change", function (e)
{
@@ -174,6 +183,7 @@
{
el.style.maxWidth = e.target.checked ? "none" : "";
});
refreshLayout();
});
render();
</script>
+7 -4
View File
@@ -76,14 +76,17 @@
<script src="../dist/juzhen.iife.js"></script>
<script>
// 全頁一次渲染(quanjiao 全角式,與下游回饋之政策一致);各框由其 lang 屬性
// 驅動簡/繁字形。(標點懸掛已移除,見 ARCHITECTURE §6.5.6。)
Juzhen.createJuzhen({ scope: { include: ".juzhen" }, lang: { style: "quanjiao" } })
.render(document.body);
// 各獨立拖曳框作為 render 根,讓 ResizeObserver 直接觀察其寬度。
// quanjiao 全角式;各框由 lang 驅動簡/繁字形。
var jz = Juzhen.createJuzhen({ scope: { include: ".juzhen" }, lang: { style: "quanjiao" } });
var boxes = Array.from(document.querySelectorAll(".box.juzhen"));
boxes.forEach(function (box) { jz.render(box); });
document.getElementById("t-justify").addEventListener("change", function (e)
{
document.body.classList.toggle("justify", e.target.checked);
// 對齊改變未必產生 resize,不能只等 observer。
boxes.forEach(function (box) { jz.rerender(box); });
});
document.getElementById("t-gaps").addEventListener("change", function (e)
{
+1 -1
View File
@@ -107,7 +107,7 @@
<p class="look">末行=「<strong>願意」。</strong>」——保 <strong>2 個實義字</strong>(願意);尾隨 <code>」。</code> 隨尾段綁入但不佔 N(開紫框見綁定範圍含標點,且整個 <code>「我願意」</code> 禁則群組跨 <code>jz-jinze</code> 不被切割)。</p>
</div>
</div>
<p class="hint">邊界:段落實義字 &lt; N(或單行段)→ 不綁、無副作用。極窄容器(&lt;N 字寬)→ 該尾段可能溢出(罕見)。</p>
<p class="hint">邊界:段落實義字 &lt; N → 不綁。單行段仍可能建立尾段綁定,但不需拉字換行、效果無害。極窄容器(&lt;N 字寬)→ 該尾段可能溢出(罕見)。</p>
</div>
<!-- ===================== 三、標點懸掛(已移除) ===================== -->