fix: 行內元素三分類(透明/隔離/整體)——jinze 下潛肢解 KaTeX 修復(下游問題六);v1 相容層移除
下游問題六:行內公式緊鄰禁則標點時,jinze 為綁鄰字透明下潛 .katex(全樹皆 SPAN,
僅憑標籤名判透明),把公式內側末字連同完整殼層逐層克隆切出(splitoff),上標漂移
錯位;pillSelector/skipAttribute/scope.avoid 三層防護全被刺穿。復現另揭兩向量:
相鄰 pill 時 descend 刺穿致 jz-hws 插入公式內部;pill 內部標點被 jiya charify。
確立三分類(§4.1,所有遍歷原語共用同一判定):
透明(styleInlines+jz-*)— 穿越/run 延續/可克隆切分;
隔離(isolateSelector,預設 "mark",新增選項)— 邊界斷 run/斷相鄰/不可切分、
兩端補隙(同 pill),內部照常處理(自成脈絡);
整體(pillSelector)— 內部對一切 pass 不可見(insidePill 過濾),禁則時作為
原子鄰字單位整體綁入 jz-jinze。
機制修復:
finder — transparentInline 改以元素判定(先排除 pill/isolate/avoid 再查標籤集);
avoidsSelf 自身判定(isAvoided 僅查祖先,自身命中 avoid 曾被穿越);descend 三態
(邊界字/無內容續找兄弟/blocked 終止——不可越界跳找更遠之字);eachTextNode
過濾 pill 內部;collectRuns 隔離元素內部自成 run。
core/split — canSplit 諮詢 finder(pill/isolate/avoid 不可切 → 退化整綁);
MARK 移出 SPLITTABLE(背景盒切分有可見接縫)。
jiyaAdjacency — pill/avoid(自身判定)斷相鄰不下探;isolate 斷而內部自成脈絡。
spacing Pass B — 邊界選擇器擴為 pill ∪ isolate;巢於 pill 內者跳過。
orphan — avoidsSelf 邊界;avoid 塊不綁(舊漏判:avoid 子樹內裸文本曾被計數包綁)。
v1 相容層移除(消費端已全數遷移 v2 API):src/compat/v1.ts、IIFE 全域別名
createCjkAutospace、死選項 autospaceClass(經查從未被任何 pass 讀取)一併刪;
I8 退役。dist/compat 殘留聲明清除、dist/core/split.d.ts 補入庫(前次遺漏)。
測試 112 全綠(問題六 ×7、isolate ×6、compat 移除 ×1);headless Chrome 實證
公式完整(splitoff=0、內部無 jz-hws/jz-char)、mark 與公式整體入 jz-jinze。
文檔:README 三分類節+選項示例+功能表(hanging 行補「已移除」);ARCHITECTURE
§4.1 三分類/§3.5 相容層移除/§6.1/§6.3/§6.4/I8 退役。
This commit is contained in:
+155
-27
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { setupDom, countTag } from "./helpers.mjs";
|
||||
import { createJuzhen, createCjkAutospace } from "../dist/juzhen.mjs";
|
||||
import { createJuzhen } from "../dist/juzhen.mjs";
|
||||
|
||||
// ----- 中西間隙(spacing)-----
|
||||
|
||||
@@ -598,32 +598,6 @@ test("問題二:justifyAtoms 預設 true → 不設屬性(維持 inline-bloc
|
||||
assert.equal(document.body.getAttribute("data-jz-atoms"), null, "預設不設屬性");
|
||||
});
|
||||
|
||||
// ----- v1 相容層(§3.5)-----
|
||||
|
||||
test("compat:createCjkAutospace().apply() 可跑,產生中西間隙(margin 模型,copy-clean)", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>我用Neovim</p>");
|
||||
createCjkAutospace().apply(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.ok(p.querySelector("jz-hws"), "間隙仍產生(jz-hws 包左側字 + margin)");
|
||||
assert.equal(p.textContent, "我用Neovim", "textContent 不變(無注入空白)");
|
||||
});
|
||||
|
||||
test("compat:punctuationSqueeze:false 關閉 jiya", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hant">好,走</p>');
|
||||
createCjkAutospace({ punctuationSqueeze: false }).apply(document.body);
|
||||
assert.equal(countTag(document.body, "jz-char"), 0);
|
||||
});
|
||||
|
||||
test("compat:預設強制 kaiming,句內點號半形(jz-half)(不因預設 zh-Hant 而退化)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hant">好,走</p>');
|
||||
createCjkAutospace().apply(document.body);
|
||||
const comma = document.querySelector("jz-char.bd-pause");
|
||||
assert.ok(comma && comma.classList.contains("jz-half"), "compat 不退化:仍擠句內");
|
||||
});
|
||||
|
||||
// ----- text-indent 漏溢防護(下游回報)-----
|
||||
|
||||
test("CSS:jz-* 原子歸零「區塊容器首/末行級」繼承屬性,免 inline-block 重套(透明原則)", () =>
|
||||
@@ -972,3 +946,157 @@ test("行端半形:lineedge 仍正常擠壓(squeeze 保留)——jsdom 無
|
||||
jz.revert(document.body);
|
||||
assert.equal(countTag(document.body, "jz-char"), 0, "revert 清除");
|
||||
});
|
||||
|
||||
// ----- 行內元素三分類(§4.1):透明/隔離(isolate)/整體(pill)(下游問題六)-----
|
||||
|
||||
// KaTeX 式巢狀 span 結構(全樹皆 SPAN——僅憑標籤名判透明會整棵下潛肢解)。
|
||||
const KATEX = '<span class="katex"><span class="katex-html">'
|
||||
+ '<span class="base"><span class="mord">4.4</span><span class="mbin">x</span>'
|
||||
+ '<span class="mord">10</span><span class="msupsub"><span class="vlist">'
|
||||
+ '<span class="mord">-17</span></span></span></span></span></span>';
|
||||
const KATEX_OPTS = { pillSelector: "code, kbd, .katex", lang: { default: "zh-Hans", style: "kaiming" } };
|
||||
|
||||
test("問題六:pill(.katex)後接句內點號——整體綁入 jz-jinze,絕不肢解(無 splitoff)", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>結果為 " + KATEX + ",在浮點中</p>");
|
||||
createJuzhen(KATEX_OPTS).render(document.body);
|
||||
assert.equal(document.querySelectorAll(".katex").length, 1, "公式仍為單一元素(未裂)");
|
||||
assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0, "無克隆切分殼層");
|
||||
const jinze = document.querySelector("jz-jinze");
|
||||
assert.ok(jinze && jinze.querySelector(".katex"), "整個 .katex 作為原子鄰字單位入 jz-jinze");
|
||||
assert.ok(jinze.querySelector("jz-char.bd-pause"), "逗號與公式同組(避頭綁定)");
|
||||
});
|
||||
|
||||
test("問題六:開類+pill+閉類(雙側禁斷)——公式仍完整、不裂為多塊", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>(" + KATEX + ")在浮點中</p>");
|
||||
createJuzhen(KATEX_OPTS).render(document.body);
|
||||
assert.equal(document.querySelectorAll(".katex").length, 1, "雙側綁定亦不肢解");
|
||||
assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0);
|
||||
});
|
||||
|
||||
test("問題六:scope.avoid 自身命中(.katex)——avoid 邊界以自身判定,不被透明穿越", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>結果為 " + KATEX + ",在浮點中</p>");
|
||||
createJuzhen({ scope: { avoid: ".katex" }, lang: { default: "zh-Hans", style: "kaiming" } })
|
||||
.render(document.body);
|
||||
assert.equal(document.querySelectorAll(".katex").length, 1, "avoid 元素未被切分");
|
||||
assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0);
|
||||
assert.equal(countTag(document.body, ".katex jz-char"), 0, "avoid 內部不 charify");
|
||||
});
|
||||
|
||||
test("問題六:skipAttribute 自身命中——同 avoid,不切分不下潛", () =>
|
||||
{
|
||||
const html = KATEX.replace('class="katex"', 'class="katex" data-md-key="x"');
|
||||
const { document } = setupDom("<p>結果為 " + html + ",在浮點中</p>");
|
||||
createJuzhen({ skipAttribute: "data-md-key", lang: { default: "zh-Hans", style: "kaiming" } })
|
||||
.render(document.body);
|
||||
assert.equal(document.querySelectorAll(".katex").length, 1);
|
||||
assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0);
|
||||
});
|
||||
|
||||
test("問題六:相鄰兩 pill——descend 不刺穿,jz-hws 絕不插入公式內部", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>公式" + KATEX + KATEX + "文字</p>");
|
||||
createJuzhen(KATEX_OPTS).render(document.body);
|
||||
assert.equal(countTag(document.body, ".katex jz-hws"), 0, "公式內部無間隙標記");
|
||||
assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0);
|
||||
});
|
||||
|
||||
test("pill 內部對一切 pass 不可見:內部標點不 charify、內部不插間隙", () =>
|
||||
{
|
||||
const { document } = setupDom(
|
||||
'<p>公式<span class="katex"><span class="mord">內文,標點Word混排</span></span>文字</p>',
|
||||
);
|
||||
createJuzhen(KATEX_OPTS).render(document.body);
|
||||
assert.equal(countTag(document.body, ".katex jz-char"), 0, "pill 內部標點不 charify");
|
||||
assert.equal(countTag(document.body, ".katex jz-hws"), 0, "pill 內部不插中西間隙");
|
||||
assert.equal(countTag(document.body, ".katex span[lang]"), 0, "pill 內部不做長詞包裝");
|
||||
});
|
||||
|
||||
test("問題六 revert:pill 整綁後還原——DOM 結構回原樣(公式逐字節一致)", () =>
|
||||
{
|
||||
// 無作者空格之夾具:spacing 對 pill 左緣作者空格之吸收(正規化為受管間隙)
|
||||
// 屬既有設計、revert 不復活,故 I6 斷言聚焦結構還原。
|
||||
const { document } = setupDom("<p>結果為" + KATEX + ",在浮點中</p>");
|
||||
const before = document.querySelector("p").innerHTML;
|
||||
const jz = createJuzhen(KATEX_OPTS);
|
||||
jz.render(document.body);
|
||||
jz.revert(document.body);
|
||||
assert.equal(document.querySelector("p").innerHTML, before, "render→revert 還原一致(I6)");
|
||||
});
|
||||
|
||||
// ----- 隔離(isolate,預設 <mark>):邊界如 pill、內部照常 -----
|
||||
|
||||
test("isolate:mark 兩端與 CJK 相鄰補間隙(兩端需要空白)", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>高亮<mark>重點</mark>文字</p>");
|
||||
createJuzhen({ jiya: false, jinze: false, longWord: false }).render(document.body);
|
||||
const p = document.querySelector("p");
|
||||
// 左緣:包「亮」之 jz-hws;右緣:包整個 mark 之 jz-hws。
|
||||
assert.equal(countTag(p, "jz-hws"), 2, "mark 兩端各一處間隙");
|
||||
assert.ok(p.querySelector("jz-hws > mark"), "右緣間隙以包裹 mark 自身承載");
|
||||
});
|
||||
|
||||
test("isolate:mark 內部照常處理(內部標點 charify、內部中西補隙)", () =>
|
||||
{
|
||||
const { document } = setupDom(
|
||||
'<p lang="zh-Hans"><mark>內文,混排Word之後</mark></p>',
|
||||
);
|
||||
createJuzhen({ jinze: false, longWord: false }).render(document.body);
|
||||
assert.ok(document.querySelector("mark jz-char.bd-pause"), "內部標點照常 charify");
|
||||
assert.ok(document.querySelector("mark jz-hws"), "內部中西邊界照常補隙");
|
||||
});
|
||||
|
||||
test("isolate:mark 不被克隆切分——mark+逗號整綁入 jz-jinze(背景切分有可見接縫)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">前文<mark>高亮一段話</mark>,後文</p>');
|
||||
createJuzhen().render(document.body);
|
||||
assert.equal(document.querySelectorAll("mark").length, 1, "mark 未被切分");
|
||||
assert.equal(countTag(document.body, '[data-jz-kind="splitoff"]'), 0);
|
||||
const jinze = document.querySelector("jz-jinze");
|
||||
assert.ok(jinze && jinze.querySelector("mark"), "整個 mark 入 jz-jinze(退化整綁)");
|
||||
});
|
||||
|
||||
test("isolate:跨 mark 邊界之相鄰標點不互擠(斷相鄰,內部自成脈絡)", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">說:<mark>「重點」</mark>之外。</p>');
|
||||
createJuzhen({ jinze: false }).render(document.body);
|
||||
// :與「之間隔 mark 邊界——不得配對互擠(:右空白不因「而收)。
|
||||
const colon = document.querySelector("jz-char.bd-pause");
|
||||
assert.ok(colon, ":已 charify");
|
||||
// 開明式 baseline 本就半形;驗證點在「不因鄰接而額外互動」——此處以
|
||||
// 全角式重跑驗證邊界斷相鄰。
|
||||
const dom2 = setupDom('<p lang="zh-Hant">說:<mark>「重點」</mark>之外。</p>');
|
||||
createJuzhen({ jinze: false, lang: { style: "quanjiao" } }).render(dom2.document.body);
|
||||
dom2.document.querySelectorAll("jz-char").forEach((c) =>
|
||||
{
|
||||
assert.ok(!c.classList.contains("jz-half"), "全角式+邊界斷相鄰:無任何互擠半形");
|
||||
});
|
||||
});
|
||||
|
||||
test("isolate:isolateSelector 傳 \"\" 停用——mark 回歸透明(無邊界間隙、可切分)", () =>
|
||||
{
|
||||
const { document } = setupDom("<p>高亮<mark>重點</mark>文字</p>");
|
||||
createJuzhen({ isolateSelector: "", jiya: false, jinze: false, longWord: false })
|
||||
.render(document.body);
|
||||
assert.equal(countTag(document.body, "jz-hws"), 0, "純 CJK 透明穿越,無間隙");
|
||||
});
|
||||
|
||||
test("isolate revert:mark 整綁+邊界間隙還原——DOM 回原樣", () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hans">前文<mark>高亮</mark>,混排Word後</p>');
|
||||
const before = document.querySelector("p").innerHTML;
|
||||
const jz = createJuzhen();
|
||||
jz.render(document.body);
|
||||
jz.revert(document.body);
|
||||
assert.equal(document.querySelector("p").innerHTML, before, "render→revert 還原一致(I6)");
|
||||
});
|
||||
|
||||
// ----- v1 相容層移除 -----
|
||||
|
||||
test("v1 相容層已移除:createCjkAutospace 不再導出(消費端已全數遷移 v2 API)", async () =>
|
||||
{
|
||||
const mod = await import("../dist/juzhen.mjs");
|
||||
assert.equal(mod.createCjkAutospace, undefined, "compat 導出已刪");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user