- 可视化选择(核心痛点解决):Sveltia 原生支持 registerEditorComponent
的 inline 形态且 onInsert 能拿到 selectedText → 实现「选中文字 → Insert ▸
排版 → 选字体/颜色 → 自动包 span class」完整链路;因 inline 组件只能进
Insert 菜单(field-based 虽可上工具栏但拿不到 selectedText),两个独立组件
会互相抢 pattern 匹配,故收敛为单入口 + 18 项分组面板(键盘可达/Esc 取消/
onEdit 回填当前类)
- public/admin/typography-classes.js 为单一事实源(扩展与文档共用)
- 颜色扩充 4 类:ink-warm / ink-cool / ink-faint / ink-accent-soft,全部亮暗
双态实测 ≥4.5:1(ink-faint 取白底达标的最淡值 4.69/4.75)
- 字体:体积账否决 webfont 新增(@fontsource zcool-xiaowei 用到即 +647.7KB,
远超 150KB 阈值;smiley-sans 无可用版本、sarasa 包为空壳),改为落地 4 个
**系统回退类**(font-song/font-kai-sys/font-hei-sys/font-fangsong,零下载,
文档注明依赖访客设备)
- 白名单 17 类同步;docs/写作指南.md 三表更新
- docs/research/10-页面后台化方案.md:五方案对比 + 推荐方案 E(页面骨架 blocks
/ 区块字段 / 局部自由区 / 样式即数据四层)+ 关于页九类区块 + 消毒与验收断言
+ 分阶段清单(约 7.2d)
验证: 16/16 颜色 computed 与对比度实测;选择器 7/7(桩 CMS 覆盖注册/插入/
编辑三层);构建 48 页 exit 0;E2E 37/37
Spec: none (排版能力 + 方案文档)
Co-authored-by: GLM-5.3
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
/**
|
|
* rehype 排版类白名单:文章里允许用 `<span class="...">` / `<p class="fig-cap">` 做受限排版,
|
|
* 但**只放行白名单类名**,其余 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);
|
|
};
|
|
}
|