feat(levelmark): 塊級分類 CSS 鉤子 data-jz-block-level+增量塊粒度契約(下游建議七/說明八)
建議七:新增 levelMark pass(standalone,order 10,恆開)——render 時把 resolved 功能級別寫回「直接承載可處理文本」之塊級元素:data-jz-block-level="paragraph|text", revert 全數移除(I6 之 data-jz-* 清單涵蓋)。消費端段落級 CSS(首要 text-align: justify)自此與 jinze/orphan 共用單一真相源,免另立必然漂移之選擇器白名單。 - 僅標直接承載文本之塊:純容器(body/ul/table…,直接文本僅空白)不標——否則 body 恆判 paragraph、justify 鉤子經繼承污染全頁;此集合恰為段落級 pass 實際 作用之塊集。avoid 子樹/pill 內部文本不計,未被處理之塊一致不標。 - 輸入/輸出分離:作者輸入 data-jz-level 只讀不寫;輸出屬性本庫絕不讀取、亦不在 C2 OBSERVE_ATTRS 白名單(外部改動不觸發);增量路徑 revert 即清、render 即重標。 - 實現:一次 eachTextNode 走訪+短程上攀定塊(per-run 快取),O(文本);levelFor 已記憶化。上攀止於 ctx.root,不越界外標。 說明八:README/ARCHITECTURE §3.1 明載增量粒度契約——增量須以塊為粒度(換塊 內容、增刪塊、改分類屬性皆與全量重繪一致);不支持對聚珍已切分文本節點之外科式 characterData 直改(charify 以 fragment 替換原節點,舊引用已脫離文檔)——後處理 排版器之固有限制、非缺陷,以塊為單位替換內容即可。 測試 140→145(標記/純容器不標、text 解析三路(選擇器/屬性/祖先)、avoid/ pill 不標、revert 全清含 root、C2 class 翻轉自動刷新);tsc 聲明構建通過; headless Chrome 整管線煙測零報錯、段落屬性實際落 DOM。
This commit is contained in:
@@ -1522,3 +1522,89 @@ test("C2 觀察:向葉塊追加子塊 → 撤除容器陳舊 jz-orphan(IV-3
|
||||
assert.equal(directOrphan, false, "容器轉非葉後,其直接 jz-orphan 已撤除");
|
||||
assert.ok(np.querySelector("jz-orphan"), "新塊 p 自身之 jz-orphan 正常");
|
||||
});
|
||||
|
||||
// ----- 塊級分類鉤子 data-jz-block-level(下游建議七)-----
|
||||
|
||||
test("levelMark:直接承載文本之塊標 data-jz-block-level,純容器不標", () =>
|
||||
{
|
||||
const { document } = setupDom(
|
||||
'<div id="m" lang="zh-Hant">容器直接文字。<p>子段文字。</p></div>'
|
||||
+ "<ul><li>列表項文字。</li></ul>",
|
||||
);
|
||||
createJuzhen().render(document.body);
|
||||
const m = document.getElementById("m");
|
||||
assert.equal(m.getAttribute("data-jz-block-level"), "paragraph", "混合塊有直接文本 → 標");
|
||||
assert.equal(m.querySelector("p").getAttribute("data-jz-block-level"), "paragraph");
|
||||
assert.equal(document.querySelector("li").getAttribute("data-jz-block-level"), "paragraph");
|
||||
assert.equal(
|
||||
document.querySelector("ul").hasAttribute("data-jz-block-level"), false,
|
||||
"純容器(直接文本僅空白)不標",
|
||||
);
|
||||
assert.equal(
|
||||
document.body.hasAttribute("data-jz-block-level"), false,
|
||||
"body 無直接文本不標(justify 鉤子不經繼承污染全頁)",
|
||||
);
|
||||
});
|
||||
|
||||
test("levelMark:level.text 選擇器/data-jz-level 屬性 → 標 text;作者輸入屬性不被觸碰", () =>
|
||||
{
|
||||
const { document } = setupDom(
|
||||
'<h1>標題文字</h1><p data-jz-level="text">說明文字。</p>'
|
||||
+ '<div data-jz-level="text"><p id="n">內層段落。</p></div>',
|
||||
);
|
||||
createJuzhen({ level: { text: "h1" } }).render(document.body);
|
||||
assert.equal(document.querySelector("h1").getAttribute("data-jz-block-level"), "text", "選擇器命中");
|
||||
const p = document.querySelector("p[data-jz-level]");
|
||||
assert.equal(p.getAttribute("data-jz-block-level"), "text", "屬性命中");
|
||||
assert.equal(p.getAttribute("data-jz-level"), "text", "作者輸入 data-jz-level 原樣保留");
|
||||
assert.equal(
|
||||
document.getElementById("n").getAttribute("data-jz-block-level"), "text",
|
||||
"祖先 data-jz-level=text → 後代塊 resolved 為 text",
|
||||
);
|
||||
});
|
||||
|
||||
test("levelMark:avoid 子樹與 pill 內部文本不計——僅含此類內容之塊不標", () =>
|
||||
{
|
||||
const { document } = setupDom(
|
||||
"<div><code>onlyPill</code></div><p data-jz-skip>跳過內容。</p>",
|
||||
);
|
||||
createJuzhen().render(document.body);
|
||||
assert.equal(
|
||||
document.querySelector("div").hasAttribute("data-jz-block-level"), false,
|
||||
"pill 內部不可見 → 塊不標",
|
||||
);
|
||||
assert.equal(
|
||||
document.querySelector("p").hasAttribute("data-jz-block-level"), false,
|
||||
"avoid 塊不標(與「未被處理」一致)",
|
||||
);
|
||||
});
|
||||
|
||||
test("levelMark:revert 全數移除 data-jz-block-level(I6),root 自身亦清", () =>
|
||||
{
|
||||
const { document } = setupDom("直接文字。<p>段落文字。</p>");
|
||||
const jz = createJuzhen();
|
||||
jz.render(document.body);
|
||||
assert.equal(
|
||||
document.body.getAttribute("data-jz-block-level"), "paragraph",
|
||||
"body 有直接文本 → root 自身被標",
|
||||
);
|
||||
assert.ok(document.querySelector("p[data-jz-block-level]"), "段落已標");
|
||||
jz.revert(document.body);
|
||||
assert.equal(document.querySelectorAll("[data-jz-block-level]").length, 0, "子樹全清");
|
||||
assert.equal(document.body.hasAttribute("data-jz-block-level"), false, "root 自身已清");
|
||||
});
|
||||
|
||||
test("levelMark C2:class 變化令 level 翻轉 → 觀察自動刷新 resolved 標記", async () =>
|
||||
{
|
||||
const { document } = setupDom('<p lang="zh-Hant">正文文字,標點。</p>');
|
||||
const jz = createJuzhen({ level: { text: ".chrome" } });
|
||||
jz.render(document.body);
|
||||
jz.observe(document.body);
|
||||
const p = document.querySelector("p");
|
||||
assert.equal(p.getAttribute("data-jz-block-level"), "paragraph");
|
||||
p.classList.add("chrome");
|
||||
await tick();
|
||||
jz.disconnect();
|
||||
assert.equal(p.getAttribute("data-jz-block-level"), "text", "翻轉後 resolved 標記已刷新");
|
||||
assert.equal(countTag(p, "jz-jinze"), 0, "text 級不再跑段落級禁則(重分類一致)");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user