refactor: 移除標點懸掛(機制級死路)——負 margin 程式碼/CSS/型別一併刪

標點懸掛經實測為**機制級死路**(純 CSS 之寬度守恆矛盾):行端標點盒保持非零寬則墨色
出去多少版心內就空多少(行末實義字無法齊邊);收為零寬則釋放一整字寬、斷行器把下一字
拉上本行與標點重疊(且 justifyAtoms 預設之 inline-block 下負 margin 被 justify 吸收)。
CSS hack 接觸不到斷行器,此恒等式無解。唯一正解為原生 hanging-punctuation(僅 Safari
支援、且 jz-char 為 inline-block 原子非文本,原生懸掛不作用),屬遠期「原生管線」新功能、
與此處無共享,故不保留休眠碼。

移除:
  lineedge.ts — jz-hang-* class、HANG_* 常量、applyEdge hang 分支、modeForChar、
    HANG_ENABLED 總開關;行端僅餘 squeeze(半形)。
  _jiya.css — jz-hang-* 三條規則(margin 半形 fallback 完整保留)。
  types.ts — LineEndMode 型別、ResolvedOptions.lineEnd、features.hanging。
  index.ts — lineEnd 鎖。
  hanging 公開選項保留為 @deprecated no-op(API 相容)。

文檔:README「標點懸掛」節、ARCHITECTURE §6.5(開頭/§6.5.2/§6.5.4/§6.5.5/§6.5.6)、
demo(showcase §三/resize-check)全部由「停用/休眠」改為「已移除」+寬度守恆論證。

測試:防逃逸測試改寫為「hanging 為 no-op、輸出與不傳一致、各路徑不產生 jz-hang-*」。
全綠 101。dist 已無 jz-hang/lineEnd/HANG 殘留。
This commit is contained in:
2026-06-11 16:16:01 +08:00
parent 2890549f44
commit a3c94ddee5
12 changed files with 156 additions and 336 deletions
+32 -28
View File
@@ -925,46 +925,50 @@ test("垂懸字 元素最小綁定 revert:兩個 <strong> 併回單一、文
assert.equal(p.querySelector("strong").textContent, "一段很長的加粗文字", "strong 還原");
});
// ----- 標點懸掛(§6.5.2,行端模式接線;版面凸出量於 headless Chrome 驗證)-----
// ----- 標點懸掛已移除(§6.5.6):hanging 選項保留為 no-op,永不產生 jz-hang-* -----
// 純負 margin 機制為機制級死路(寬度守恆矛盾,CSS 接觸不到斷行器);原生
// hanging-punctuation 為唯一正解但僅 Safari 支援、且與 inline-block 原子互斥。
// 下列測試確保 API 相容(hanging:truedata-juzhenblocks 任一要求皆被接受但無效果)。
test("懸掛:已全面停用——hanging 預設關,行端模式 squeeze", () =>
{
const jz = createJuzhen();
assert.equal(jz.options.lineEnd, "squeeze", "行端模式恒 squeeze");
});
test("懸掛:防逃逸——即使 hanging:true,行端模式仍鎖死 squeeze(不啟用 hang", () =>
{
const jz = createJuzhen({ hanging: true });
// option 仍被接受(API 不破),但 lineEnd 鎖死 squeeze——hang 不可能激活(§6.5.6 停用)。
assert.equal(jz.options.lineEnd, "squeeze", "hanging:true 亦不轉 hang(無逃逸)");
});
test("懸掛:防逃逸——hanging:true render 後不產生任何 jz-hang-* 標記", () =>
test("懸掛已移除:hanging 選項被接受但無任何效果(API 相容,render 不拋)", () =>
{
const { document } = setupDom('<p lang="zh-Hans">春。夏。秋。冬。</p>');
createJuzhen({ hanging: true, justifyAtoms: false }).render(document.body);
const jz = createJuzhen({ hanging: true });
jz.render(document.body); // 不應拋錯;hanging 為 no-op
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "無 jz-hang-r");
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "無 jz-hang-l");
});
test("懸掛:防逃逸——下游 data-juzhen=\"hanging\" 亦不啟用(lineEnd 不受 per-node 影響", () =>
test("懸掛已移除:hanging:true 之輸出與不傳 hanging 完全一致(無行為差異", () =>
{
const { document } = setupDom('<p lang="zh-Hans" data-juzhen="spacing jiya jinze hanging">春。夏。秋。冬。</p>');
const jz = createJuzhen({ hanging: true, justifyAtoms: false });
jz.render(document.body);
assert.equal(jz.options.lineEnd, "squeeze", "lineEnd 恒 squeeze");
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "data-juzhen 要求亦無 jz-hang-r");
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "data-juzhen 要求亦無 jz-hang-l");
const html = '<p lang="zh-Hans">春。夏。秋。冬。</p>';
const a = setupDom(html);
createJuzhen({ justifyAtoms: false }).render(a.document.body);
const withoutHang = a.document.body.innerHTML;
const b = setupDom(html);
createJuzhen({ hanging: true, justifyAtoms: false }).render(b.document.body);
assert.equal(b.document.body.innerHTML, withoutHang, "hanging:true 不改變任何輸出");
});
test("懸掛:防逃逸——blocks features:[\"hanging\"] 亦不啟用", () =>
test("懸掛已移除:data-juzhen=\"hanging\" 與 blocks features:[hanging] 皆無效果", () =>
{
const { document } = setupDom('<p class="q" lang="zh-Hans">春。夏。秋。冬。</p>');
const { document } = setupDom(
'<p lang="zh-Hans" data-juzhen="spacing jiya jinze hanging">春。夏。秋。</p>'
+ '<p class="q" lang="zh-Hans">春。夏。秋。</p>',
);
createJuzhen({
hanging: true, justifyAtoms: false,
justifyAtoms: false,
blocks: [ { selector: ".q", features: [ "spacing", "jiya", "hanging" ] } ],
}).render(document.body);
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "blocks 要求亦無 jz-hang-r");
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "blocks 要求亦無 jz-hang-l");
assert.equal(countTag(document.body, "jz-char.jz-hang-r"), 0, "無 jz-hang-r");
assert.equal(countTag(document.body, "jz-char.jz-hang-l"), 0, "無 jz-hang-l");
});
test("行端半形:lineedge 仍正常擠壓(squeeze 保留)——jsdom 無版面故僅驗 pass 不拋", () =>
{
const { document } = setupDom('<p lang="zh-Hant">「測試」。</p>');
const jz = createJuzhen();
jz.render(document.body); // lineedge 於 jsdom(無 ResizeObserver)跳過,不應拋
jz.revert(document.body);
assert.equal(countTag(document.body, "jz-char"), 0, "revert 清除");
});