首页
/ mermaid 配置 API 深度解析:addDirective() 如何安全注入并合并渲染指令

mermaid 配置 API 深度解析:addDirective() 如何安全注入并合并渲染指令

2026-09-06 11:47:17作者:舒璇辛Bertina

本文以 addDirective() 接口文档 为主体,结合 配置模块源码单元测试,完整讲解 mermaid 指令(directive)注入机制:addDirective() 的函数签名、参数语义、三步内部处理流程(净化、字体迁移、深合并),以及 secure 键保护、XSS 过滤等安全边界。读完后,你将理解 %%init%% 指令从图源码文本到最终生效配置的完整链路,并能基于测试契约判断自定义指令为何被静默丢弃。

assignWithDepth 深度合并示意:指令配置以深度合并方式叠加到站点配置之上

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 全局配置对象的嵌套键值对(顶层为全局项,二级以图表类型为键,如 flowchartsequence
  • 返回值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,它对传入对象原地修改(删除非法字段),规则包括:

  1. 键白名单:只保留存在于 defaultConfig 导出键集合(configKeys)中的键,其余一律删除——这意味着指令无法注入任何未定义的配置路径;
  2. 危险键名拦截:以 __ 开头、包含 protoconstr 的键被删除,防止原型污染;
  3. 字典型配置特殊处理:像 nodeColors(sankey 节点颜色)、filenameIcons / extensionIcons(treeView 图标映射)这类键值可自定义的字典,不按白名单过滤键,而是用正则校验,例如颜色必须匹配 /^#[\da-f]{3,8}$|^rgb(...) 等模式(sanitizeDirective.ts#L9-L31);
  4. CSS 类字段净化themeCSSfontFamilyaltFontFamily 等字段会经过 sanitizeCss() 做花括号配平检查,不平衡时替换为 { /* ERROR: Unbalanced CSS */ }
  5. 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-L38should respect secure keys when applying directives 用例验证了这条契约:先把 fontSize 加入 secure、把 securityLevel 设为 strict,再 addDirective({ fontFamily: 'baf', fontSize: 54321, securityLevel: 'loose' }),断言结果是 fontFamily 生效、而 fontSizesecurityLevel 保持站点值不变。

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 的修改会在下一次 addDirectivereset 调用时被覆盖”。需要一次性修改当前配置的场景也建议改用指令通道,保证行为可追溯。
  • getUserDefinedConfig()config.ts#L227-L239)会按 initialize 配置 + 全部 directives 的顺序深合并,返回“用户自定义部分”,供需要区分默认值与用户值的调用方使用。

7. 测试用例验证的行为契约

config.spec.ts 围绕 addDirective() 覆盖了多组关键契约,可作为行为验收依据:

用例 位置 验证内容
secure 键不可被指令覆盖 config.spec.ts#L19-L38 securityLevel、登记进 securefontSize 保持站点值
指令净化后保留合法深字段 config.spec.ts#L76-L88 railroad.fontSize / railroad.fontFamilysanitizeDirective 后完整保留
多条指令按序叠加 config.spec.ts#L140-L160 连续压入 directive1directive2,后者字段覆盖前者
嵌套指令覆盖全局项 config.spec.ts#L350-L365 flowchart.htmlLabels: false 与顶层 htmlLabels 两种写法均生效

这些用例与 config.usecase.spec.ts 中的场景化测试相互印证,确认了“净化 + 深合并 + 安全过滤”三条链路在真实用例下的稳定性。

8. 小结

回到接口文档本身:addDirective(directive: MermaidConfig): void 这一行签名背后,是 mermaid 指令系统的完整安全与合并契约——

  1. 入口净化sanitizeDirective() 以键白名单 + 值正则 + CSS 检查拦截非法指令字段;
  2. 安全加固:重建配置时 sanitize() 二次过滤 secure 键、原型污染键与 XSS 敏感字符串;
  3. 有序深合并:指令按入栈顺序经 assignWithDepth 叠加到 siteConfig 之上,联动计算主题变量,生成 currentConfig
  4. 图间隔离:渲染链路以 reset() + addDirective() 固定组合保证每张图的指令互不泄漏。

继续深入可阅读:addDirective 接口文档MermaidConfig 接口定义指令用户文档配置模块源码净化实现配置单元测试

登录后查看全文
热门项目推荐
相关项目推荐