77 lines
3.8 KiB
TypeScript
77 lines
3.8 KiB
TypeScript
// 塊級分類鉤子(下游建議七):把 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<Element, Element | null>();
|
||
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);
|
||
},
|
||
};
|