/** * rehype 排版类白名单:文章里允许用 `` / `

` 做受限排版, * 但**只放行白名单类名**,其余 class 一律剔除;同时剥离 style 与 on* 事件属性(纵深防御)。 * * 为什么需要:Astro 默认不 sanitize markdown 里的 raw HTML——若直接放行 class,作者(或 * Sveltia 后台协作者)可写入任意类名/颜色破坏灰阶骨架,甚至借 style/on* 注入。 * 白名单与 global.css 的「受限排版类」段一一对应,加类时两处同改。 */ const ALLOWED = new Set([ // 颜色(走 tokens,明暗双态可读) 'ink-1', 'ink-2', 'ink-3', 'ink-muted', 'ink-accent', 'ink-accent-soft', 'ink-warm', 'ink-cool', 'ink-faint', 'c-blue', 'c-blue-soft', 'c-teal', 'c-teal-soft', 'c-green', 'c-green-soft', 'c-yellow', 'c-yellow-soft', 'c-orange', 'c-orange-soft', 'c-red', 'c-red-soft', 'c-pink', 'c-pink-soft', 'c-purple', 'c-purple-soft', // 字族(与分轨排版共存的四个既有字族) 'font-serif', 'font-kai', 'font-sans', 'font-mono', // 系统字族回退类(不下载字体,命中访客本机字体) 'font-song', 'font-kai-sys', 'font-hei-sys', 'font-fangsong', 'font-kai-light', // 文学排版 'drop-cap', 'quote-line', 'fig-cap', ]); export default function rehypeClassWhitelist() { return (tree: any) => { const walk = (node: any) => { if (node?.type === 'element') { const props = node.properties ?? (node.properties = {}); // 剥离内联样式与事件属性:排版能力只经白名单类提供 delete props.style; for (const key of Object.keys(props)) { if (/^on/i.test(key)) delete props[key]; } const raw = props.className; if (raw !== undefined) { const list = Array.isArray(raw) ? raw : String(raw).split(/\s+/); const kept = list.map((c: string) => String(c).trim()).filter((c: string) => ALLOWED.has(c)); if (kept.length > 0) props.className = kept; else delete props.className; } } for (const child of node?.children ?? []) walk(child); }; walk(tree); }; }