mermaid 配置 API 深度解析:addDirective() 如何安全注入并合并渲染指令
本文以 addDirective() 接口文档 为主体,结合 配置模块源码 与 单元测试,完整讲解 mermaid 指令(directive)注入机制:addDirective() 的函数签名、参数语义、三步内部处理流程(净化、字体迁移、深合并),以及 secure 键保护、XSS 过滤等安全边界。读完后,你将理解 %%init%% 指令从图源码文本到最终生效配置的完整链路,并能基于测试契约判断自定义指令为何被静默丢弃。
1. 接口定位与函数签名
addDirective() 是 mermaid 配置模块(packages/mermaid/src/config.ts)对外暴露的核心函数之一,在配置 API 索引页 docs/config/setup/config/README.md 的 Functions 小节中列出。原文档给出的接口定义为:
/**
* Pushes in a directive to the configuration
*
* @param directive - The directive to push in
*/
addDirective(directive: MermaidConfig): void
- 定义位置:packages/mermaid/src/config.ts#L173
- 参数:
directive,类型为 MermaidConfig,即“要推入的指令”,其字段结构是 mermaid 全局配置对象的嵌套键值对(顶层为全局项,二级以图表类型为键,如flowchart、sequence) - 返回值:
void(函数不返回结果,通过内部状态重建当前配置)
从接口文档的注释看,addDirective 的语义是“把一条指令压入配置体系”。用户层面的对应物就是图源码里的 %%init%% { ... } 前置指令,其语法与类型约束见 指令文档。渲染主流程中,前端指令 JSON 正是通过该函数进入配置系统——docs/diagrams/mermaid-api-sequence.mmd 中的时序图也标注了 Preprocessor->>Config: addDirective(processed.config) 这一步。
2. 配置分层:指令所处的合并层级
理解 addDirective() 之前,先看清 config.ts 中维护的四份配置状态:
export const defaultConfig: MermaidConfig = Object.freeze(config); // 冻结的默认配置,只读
let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig); // 站点级配置(setSiteConfig 设置)
let configFromInitialize: MermaidConfig; // mermaid.initialize() 传入的配置
let directives: MermaidConfig[] = []; // 指令数组,addDirective 压入的目标
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig); // 当前生效配置(渲染时读取)
四者的合并优先级从低到高为:
| 层级 | 变量 | 设置方式 | 生命周期 |
|---|---|---|---|
| 1. 默认配置 | defaultConfig |
构建时冻结,不可变 | 进程全程 |
| 2. 站点配置 | siteConfig |
setSiteConfig() / updateSiteConfig() |
应用级,跨渲染保留 |
| 3. 指令 | directives |
addDirective() |
每次渲染前被 reset() 清空 |
| 4. 当前配置 | currentConfig |
由上面三层经 updateCurrentConfig() 重建 |
每次渲染后更新 |
addDirective() 正是第 3 层的唯一写入入口。它压入的指令会叠加在第 2 层之上,最终决定第 4 层 currentConfig 的内容。
3. 源码逐行解读:addDirective() 的三步处理
以下是 config.ts#L168-L186 的完整实现,可拆为三个步骤:
/**
* Pushes in a directive to the configuration
*
* @param directive - The directive to push in
*/
export const addDirective = (directive: MermaidConfig) => {
sanitizeDirective(directive); // 步骤 1:就地净化
// If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
if (directive.fontFamily && !directive.themeVariables?.fontFamily) { // 步骤 2:字体字段迁移
directive.themeVariables = {
...directive.themeVariables,
fontFamily: directive.fontFamily,
};
}
directives.push(directive); // 步骤 3a:压入指令数组
updateCurrentConfig(siteConfig, directives); // 步骤 3b:重建 currentConfig
};
3.1 步骤一:sanitizeDirective() 就地净化
入口在 packages/mermaid/src/utils/sanitizeDirective.ts,它对传入对象原地修改(删除非法字段),规则包括:
- 键白名单:只保留存在于
defaultConfig导出键集合(configKeys)中的键,其余一律删除——这意味着指令无法注入任何未定义的配置路径; - 危险键名拦截:以
__开头、包含proto或constr的键被删除,防止原型污染; - 字典型配置特殊处理:像
nodeColors(sankey 节点颜色)、filenameIcons/extensionIcons(treeView 图标映射)这类键值可自定义的字典,不按白名单过滤键,而是用正则校验值,例如颜色必须匹配/^#[\da-f]{3,8}$|^rgb(...)等模式(sanitizeDirective.ts#L9-L31); - CSS 类字段净化:
themeCSS、fontFamily、altFontFamily等字段会经过sanitizeCss()做花括号配平检查,不平衡时替换为{ /* ERROR: Unbalanced CSS */ }; - themeVariables 值过滤:
themeVariables下的值必须匹配/^[\d "#%(),.;A-Za-z]+$/,否则被置为空字符串(sanitizeDirective.ts#L89-L96)。
3.2 步骤二:fontFamily 到 themeVariables 的迁移
若指令设置了顶层 fontFamily 但没有设置 themeVariables.fontFamily,函数会把前者复制进后者。这一步保证字体指令统一通过主题变量通道生效,避免“改了 fontFamily 但渲染管线只读 themeVariables”导致指令失效的分支。
3.3 步骤三:压栈并重建 currentConfig
directives.push(directive) 之后调用 updateCurrentConfig(),其重建逻辑是:
const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
let cfg: MermaidConfig = assignWithDepth({}, siteCfg); // 以 siteConfig 为基底
// Join directives:按入栈顺序用 assignWithDepth 依次深合并
let sumOfDirectives: MermaidConfig = {};
for (const d of _directives) {
sanitize(d); // 再过一道 secure 键过滤
sumOfDirectives = assignWithDepth(sumOfDirectives, d);
}
cfg = assignWithDepth(cfg, sumOfDirectives); // 指令叠加到站点配置之上
// 若指令中指定了合法 theme,则用主题基线 + 指令的 themeVariables 计算最终主题变量
if (sumOfDirectives.theme && sumOfDirectives.theme in theme) { /* ... getThemeVariables ... */ }
currentConfig = cfg;
checkConfig(currentConfig); // 触发弃用项警告检查
return currentConfig;
};
要点:
- 多指令叠加有序:
directives是数组,后压入的指令通过assignWithDepth深合并覆盖先前的值,因此“同一图表多条%%init%%指令合并后以最后一次取值为准”的行为(指令文档 中的示例)在此得到实现层面的印证; - 深合并而非浅覆盖:
assignWithDepth保证只修改部分字段的指令不会冲掉同一路径下其他默认值(例如只设railroad.fontSize不影响railroad.fontFamily的默认值); - 主题联动:指令一旦设置
theme为内置主题名,最终themeVariables由该主题的基线变量与指令自带themeVariables合并计算,而不是裸覆盖。
4. 安全边界:secure 键、原型污染与 XSS 过滤
addDirective() 的净化是双层防线。第一层是上面提到的 sanitizeDirective()(键白名单 + 值校验);第二层在重建配置时由 sanitize() 对每条指令再执行一次,它专门处理secure 键:
export const sanitize = (options: any) => {
// Checking that options are not in the list of excluded options
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
if (Object.hasOwn(options, key)) {
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
delete options[key];
}
});
// 删除 __ 开头的键(原型污染);删除含 < > 或 url(data: 的字符串(XSS / data: SVG 注入)
/* ... */
};
规则含义:
secure本身,以及站点配置在secure数组中登记的键,指令永远无法覆盖——例如站点把securityLevel登记为 secure 后,任何%%init%% {"securityLevel": "loose"}都会被静默剔除;- 所有字符串值中不允许出现
<、>或url(data:片段,注释明确说明这是为了阻断 base64 编码 SVG 内联脚本的注入路径; - 日志中刻意不用模板字符串打印被拒值,避免恶意值借日志字符串化执行代码(源码注释 config.ts#L138-L139)。
config.spec.ts#L19-L38 的 should respect secure keys when applying directives 用例验证了这条契约:先把 fontSize 加入 secure、把 securityLevel 设为 strict,再 addDirective({ fontFamily: 'baf', fontSize: 54321, securityLevel: 'loose' }),断言结果是 fontFamily 生效、而 fontSize 与 securityLevel 保持站点值不变。
5. 渲染链路中的实际调用:每张图独立注入
addDirective() 的主要调用方是渲染主流程。mermaidAPI.ts#L72-L77 中的 processAndSetConfigs() 展示了标准用法:
function processAndSetConfigs(text: string) {
const processed = preprocessDiagram(text); // 解析图源码,抽出 %%init%% 指令 JSON
configApi.reset(); // 清空上一张图遗留的指令
configApi.addDirective(processed.config ?? {}); // 注入本张图的指令
return processed;
}
parse()(mermaidAPI.ts#L91-L95)与渲染入口都经过这一函数,形成“先 reset() 后 addDirective()”的固定组合。其工程意义:
- 图间隔离:每次解析都从
siteConfig干净基底重建currentConfig,一张图的%%init%%不会泄漏到下一张图; - 空指令安全:
processed.config ?? {}保证没有前置指令的图表也能走同一链路(空对象压栈不影响合并结果); - 除主链路外,parseToLayoutData.ts#L24 等布局辅助路径也会以
configApi.addDirective(config ?? {})的方式为布局数据构建注入配置,说明该函数是配置注入的统一收口。
6. 与 reset()、setConfig() 的关系及废弃说明
- reset():把
directives数组清空并重建currentConfig,参数默认回落到siteConfig。它是addDirective()的“反操作”,两者在渲染链路中成对出现。 - setConfig():源码已标记
@deprecated,原因正是与addDirective()的关系——注释写明“对currentConfig的修改会在下一次addDirective或reset调用时被覆盖”。需要一次性修改当前配置的场景也建议改用指令通道,保证行为可追溯。 - getUserDefinedConfig()(config.ts#L227-L239)会按
initialize 配置 + 全部 directives的顺序深合并,返回“用户自定义部分”,供需要区分默认值与用户值的调用方使用。
7. 测试用例验证的行为契约
config.spec.ts 围绕 addDirective() 覆盖了多组关键契约,可作为行为验收依据:
| 用例 | 位置 | 验证内容 |
|---|---|---|
| secure 键不可被指令覆盖 | config.spec.ts#L19-L38 | securityLevel、登记进 secure 的 fontSize 保持站点值 |
| 指令净化后保留合法深字段 | config.spec.ts#L76-L88 | railroad.fontSize / railroad.fontFamily 在 sanitizeDirective 后完整保留 |
| 多条指令按序叠加 | config.spec.ts#L140-L160 | 连续压入 directive1、directive2,后者字段覆盖前者 |
| 嵌套指令覆盖全局项 | config.spec.ts#L350-L365 | flowchart.htmlLabels: false 与顶层 htmlLabels 两种写法均生效 |
这些用例与 config.usecase.spec.ts 中的场景化测试相互印证,确认了“净化 + 深合并 + 安全过滤”三条链路在真实用例下的稳定性。
8. 小结
回到接口文档本身:addDirective(directive: MermaidConfig): void 这一行签名背后,是 mermaid 指令系统的完整安全与合并契约——
- 入口净化:
sanitizeDirective()以键白名单 + 值正则 + CSS 检查拦截非法指令字段; - 安全加固:重建配置时
sanitize()二次过滤 secure 键、原型污染键与 XSS 敏感字符串; - 有序深合并:指令按入栈顺序经
assignWithDepth叠加到siteConfig之上,联动计算主题变量,生成currentConfig; - 图间隔离:渲染链路以
reset()+addDirective()固定组合保证每张图的指令互不泄漏。
继续深入可阅读:addDirective 接口文档、MermaidConfig 接口定义、指令用户文档、配置模块源码、净化实现 与 配置单元测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
