// 塊級分類鉤子(下游建議七):把 resolved 功能級別寫回 DOM,供消費端 CSS 查詢。 // // 消費端的段落級樣式(首要:CJK 兩端對齊 text-align:justify)語義上應與本庫的 // 段落級 pass(jinze 避頭尾/orphan 垂懸)作用於同一集合;但 level 判定原本只存 // 於內部快取、不寫回 DOM,消費端只能另行維護一份 CSS 白名單,兩份分類必然漂移 // (下游實例:.callout 綁了避頭尾卻不在 justify 名單,避頭尾與兩端對齊不一致)。 // 本 pass 把單一真相源暴露出去: // // render → 對每個「直接承載可處理文本」之塊寫 data-jz-block-level="paragraph|text" // revert → 全數移除(I6) // // **僅標直接承載文本之塊**:純容器(body/ul/table…——直接文本僅空白)不標。 // 否則 body 恆被判 paragraph,消費端 `[data-jz-block-level="paragraph"]` 之 // justify 經繼承污染全頁,鉤子即廢。此集合恰為段落級 pass 實際作用之塊集 // (collectRuns 對純容器收不到 run)。avoid 子樹/pill 內部/域外文本不計 // (eachTextNode 同口徑),故未被處理之塊一致不標。 // // 輸入/輸出分離:作者輸入屬性 data-jz-level 只讀不寫;本屬性為 resolved 輸出、 // 本庫絕不讀之(亦不在 C2 OBSERVE_ATTRS 白名單,外部改動不觸發重處理)。是否 // 對齊仍由消費端 CSS 決定(justify 非段落之純函數——窄欄邊注可 opt-out),本庫 // 只供分類、不介入樣式。 // // order=10:standalone 群之首(於 charify 群後執行;charify 之 jz-* 包裹皆行內 // 元素,不改變「文本之最近塊祖先」,故時點無影響)。 import { revertOwnedAttributes, setOwnedAttribute } from "../core/dom.js"; import type { RenderContext, StandalonePass } from "../types.js"; const ATTR = "data-jz-block-level"; export const levelMarkPass: StandalonePass = { name: "levelMark", kind: "standalone", order: 10, level: "text", // 不受段落級閘限制:text 級之塊亦須標記(標記本身即分級輸出) enabled: () => true, // 分類鉤子非排版功能,恆開(純屬性輸出、零版面影響) render(ctx: RenderContext): void { const { finder, options } = ctx; const blockSet = options.finder.blockTags; // 近塊定位之 per-run 快取(兄弟文本節點共用 parentElement,免重複上攀)。 // 上攀止於 ctx.root:eachTextNode 之文本必在 root 內、鏈上元素亦然,故命中 // 之塊恆在 render 作用域內,revert(root) 必能移除;root 非塊時不越界外標。 const nearest = new Map(); const nearestBlock = ( (p: Element): Element | null => { const cached = nearest.get(p); if (cached !== undefined) { return cached; } let e: Element | null = p; let block: Element | null = null; while (e) { if (blockSet.has(e.nodeName)) { block = e; break; } if (e === ctx.root) { break; } e = e.parentElement; } nearest.set(p, block); return block; } ); finder.eachTextNode(ctx.root, (t) => { if (!/\S/.test(t.data)) { return; } // 純空白(縮排/換行)不算承載文本 const p = t.parentElement; const block = p ? nearestBlock(p) : null; if (!block) { return; } const level = finder.levelFor(block); setOwnedAttribute(block, ATTR, level, ctx.ownerToken); }); }, revert(ctx: RenderContext): void { revertOwnedAttributes(ctx.root, ATTR, ctx.ownerToken); }, };