Files
cjk-autospace/src/typeset/levelmark.ts
T

77 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 塊級分類鉤子(下游建議七):把 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);
},
};