主题
添加新图表 📊
🌐 Adding a New Diagram/Chart 📊
Mermaid 中的一种图表类型是一个插件。你需要编写一个解析器、一个数据库、一个渲染器和一个样式函数,然后把它们注册到一个 ID 下,Mermaid 会帮你处理检测、懒加载、主题和安全过滤。
🌐 A diagram type in Mermaid is a plugin. You write a parser, a database, a renderer, and a styles function, register them under an id, and Mermaid handles detection, lazy loading, theming, and sanitization for you.
用例图是新工作的参考实现。当本指南说“看看用例”时,文件在 packages/mermaid/src/diagrams/usecase/ 中。和这些步骤一起阅读它们:文件很短,并且展示的是当前的规范,而不是老旧图表仍保留的历史规范。
🌐 The use case diagram is the reference implementation for new work. When this guide says "look at usecase", the files are in packages/mermaid/src/diagrams/usecase/. Read them alongside these steps: they are short, and they show the current conventions rather than the historical ones that older diagrams still carry.
一个图表由什么组成
🌐 What a diagram is made of
每个图表都从一个入口文件导出一个 DiagramDefinition(diagram-api/types.ts)。整个 usecaseDiagram.ts 是这样的:
🌐 Each diagram exports a DiagramDefinition (diagram-api/types.ts) from a single entry file. The whole of usecaseDiagram.ts is this:
ts
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './parser/usecase.chevrotain.js';
import { db } from './usecaseDb.js';
import { renderer } from './usecaseRenderer.js';
import styles from './styles.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
styles,
};| 部件 | 功能 |
|---|---|
| parser | 把图表文本转换成数据库调用,否则会给出有用的错误信息 |
| db | 保存解析后的模型并交给渲染器 |
| renderer | 根据数据库里的内容绘制 SVG |
| styles | 把主题变量映射到你的图表的 CSS |
| detector | 一个正则测试,用来识别图表的第一行。它在自己的文件里 |
你的文件夹里的所有东西都适用两个规则:
🌐 Two rules apply to everything in your folder:
你的图表必须是自包含的。绝不要从其他图表的文件夹导入。你可以从 diagrams/common/ 和 rendering-util/ 导入,仅此而已。跨图表导入会造成耦合,后续会影响不相关的图表,所以审查人会在这一点上阻止通过。
🌐 Your diagram must be self-contained. Never import from another diagram's folder. You may import from diagrams/common/ and from rendering-util/, and that is the whole list. Cross-diagram imports create coupling that breaks unrelated diagrams later, so a reviewer will block on this.
你的数据库不能把一个渲染的状态带到下一个渲染。Diagram.fromText() 会从注册的定义中读取 db,并在解析之前调用 db.clear?.(),有两种支持的方式可以满足这个要求。
🌐 Your db must not carry state from one render into the next. Diagram.fromText() reads db off the registered definition and calls db.clear?.() before parsing, and there are two supported ways to satisfy that.
一个 getter,每次读取都会构建一个新的数据库,所以每次渲染都有自己的:
🌐 A getter, which builds a new db on every read, so each render gets its own:
ts
export const diagram: DiagramDefinition = {
parser,
get db() {
return new TreeMapDB();
},
renderer,
styles,
};或者一个共享的数据库,其 clear() 会重置它拥有的每个字段,加上 diagrams/common/commonDb.ts 中的共享可访问状态:
🌐 Or one shared db whose clear() resets every field it owns, plus the shared accessibility state in diagrams/common/commonDb.ts:
ts
export const diagram: DiagramDefinition = { parser, db, renderer, styles };在新图中更倾向于使用 getter。隔离然后就是结构化的 —— 后来添加的字段在 clear() 中不会被忘记,而这正是共享形式出错的方式。这里作为参考的用例图采用了共享形式,并且为此付出了代价:它把每个可变字段都保存在一个 state 对象上,而 clear() 会整体替换它,而不是一个一个地重置字段。
🌐 Prefer the getter in a new diagram. Isolation is then structural — a field added later cannot be forgotten in clear(), which is the way the shared form goes wrong. The use case diagram used as the reference here takes the shared form, and pays for it by keeping every mutable field on one state object that clear() replaces wholesale, rather than resetting fields one by one.
无论哪种方式,底层规则都是一样的:所有可变状态都存放在数据库中,永远不要放在模块作用域。模块级状态在两种形式的 clear() 中都会存在,而且同一类型的两个图表放在同一页上会相互影响。
🌐 Either way the rule underneath is the same: all mutable state lives on the db, never in module scope. Module-level state survives clear() in both forms, and two diagrams of the same type on one page will leak into each other.
步骤 1:语法和解析
🌐 Step 1: Grammar and parsing
新的图表语法应该使用 Chevrotain,与图表本身一起放在 packages/mermaid/src/diagrams/<diagram>/parser/ 下。用例图是参考实现:一个词法分析器、一个 CstParser,以及一个构建图表模型的 CST 访问器,共享 diagrams/common/parser/runChevrotainParse.ts 来运行词法/解析器组合并报告带有源位置的错误。
将语法放在图表旁边与现有的 JISON 布局相呼应,这样图表保持自包含,解析器也不需要从单独的包中释放出来。
🌐 Keeping the grammar next to the diagram mirrors the existing JISON layout, so a diagram stays self-contained and the parser does not have to be released from a separate package.
几个现有的图表(架构、gitGraph、信息、数据包、饼图、雷达图、树图)现在改用 packages/parser 中的 Langium 语法,而旧的图表使用 JISON。两者仍然受支持,所以修复 bug 时直接修改原图表就行,不需要重写,但它们都不是新工作的目标。这些 PR 展示了 Langium 的做法:
🌐 Several existing diagrams (architecture, gitGraph, info, packet, pie, radar, treemap) instead use Langium grammars in packages/parser, and older diagrams use JISON. Both remain supported, so modify them in place for bug fixes rather than rewriting, but neither is the target for new work. These PRs show the Langium approach:
无论你使用哪种方式,非法输入都必须产生带有行和列信息的解析错误,绝不是堆栈跟踪。Mermaid 会在别人的页面中运行,而抛出的异常会导致页面崩溃,而不是图表出错。
🌐 Whichever you use, invalid input has to produce a parse error with a line and column, never a stack trace. Mermaid runs inside other people's pages, and a thrown exception there is a broken page rather than a broken diagram.
步骤 2:数据库
🌐 Step 2: The database
数据库收集解析器找到的内容,并为渲染器提供 getter。看看 usecaseDb.ts。在你自己的访问器旁边,重新导出 diagrams/common/commonDb.ts 中共享的标题和无障碍设置,这样作者在其他地方使用的 title、accTitle 和 accDescr 语法在这里也能一样使用:
🌐 The db collects what the parser found and exposes getters for the renderer. Look at usecaseDb.ts. Alongside your own accessors, re-export the shared title and accessibility setters from diagrams/common/commonDb.ts so that authors get the same title, accTitle, and accDescr syntax they get everywhere else:
js
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
setDiagramTitle,
getDiagramTitle,
clear as commonClear,
} from '../common/commonDb.js';你自己的 clear() 应该调用 commonClear(),同时重置你自己的字段。
🌐 Your own clear() should call commonClear() as well as resetting your own fields.
步骤3:渲染器
🌐 Step 3: The renderer
编写一个渲染器,用来根据数据库中的内容绘制图表。usecaseRenderer.ts 是一个很好的起点,而 sequenceRenderer.js 是比流程图渲染器更通用的旧示例。这个渲染器应该放在你的 diagram 文件夹里。
🌐 Write a renderer that draws the diagram from what the db holds. usecaseRenderer.ts is a good starting point, and sequenceRenderer.js is a more generic older example than the flowchart renderer. The renderer belongs in your diagram folder.
有两件事很容易被忽略,而且在审核时都会被标出来。
🌐 Two things are easy to miss and both get flagged in review.
应用配置好的边距,然后把尺寸交给共享的辅助工具,这样你的图表就会像其他图表一样缩放,而不是渲染成某个不相关的大小:
🌐 Apply the configured padding and hand the sizing to the shared helper, so your diagram scales like every other diagram instead of rendering at some unrelated size:
ts
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
setupViewPortForSVG(svg, padding, 'usecaseDiagram', config.useMaxWidth);如果你的绘图方式允许,支持手绘模式。配置包含一个 look,图表会直接检查它:
🌐 Support hand-drawn mode if your drawing approach allows it. The config carries a look, and diagrams check it directly:
ts
const isHandDrawn = look === 'handDrawn';如果第三方库让手绘输出变得不可能,那也是可以接受的答案,但请在你的图表文档页面中说明,这样用户就不会猜测了。
🌐 If a third party library makes hand-drawn output impossible, that is an acceptable answer, but say so in your diagram's documentation page so users are not left guessing.
步骤4:检测和注册
🌐 Step 4: Detection and registration
检测存放在与图表相邻的独立文件中,而不是在 detectType.ts。一个检测器是一个正则测试加上一个延迟加载器,它会导出一个 ExternalDiagramDefinition:
🌐 Detection lives in its own file next to the diagram, not in detectType.ts. A detector is a regex test plus a lazy loader, and it exports an ExternalDiagramDefinition:
ts
const id = 'usecase';
const detector: DiagramDetector = (txt) => {
return /^\s*usecase-beta(?:\s|$)/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./usecaseDiagram.js');
return { id, diagram };
};
export const usecase: ExternalDiagramDefinition = { id, detector, loader };然后在 diagram-api/diagram-orchestration.ts 中导入它,并将其添加到 registerLazyLoadedDiagrams(...) 调用中。这里顺序很重要:第一个返回 true 的检测器会生效,所以如果早期放置了一个宽松的模式,会覆盖其他图表。加载器之所以能让 Mermaid 的包保持小巧,是因为只有当有人写图时,你的图表才会被抓取。
🌐 Then import it in diagram-api/diagram-orchestration.ts and add it to the registerLazyLoadedDiagrams(...) call. Order matters there: the first detector that returns true wins, so a loose pattern placed early will swallow other diagrams. The loader is what keeps Mermaid's bundle small, because your diagram is only fetched once someone writes one.
id 变成了 aria roledescription,所以选择一个可以大声描述该图表的词。对于 UML 部署图,“UMLDeploymentDiagram” 可以使用,因为屏幕阅读器会将其读作“U-M-L Deployment diagram”,而“deploymentDiagram” 也是如此。单独使用“deployment” 不够明确。
这个 ID 不必和你在语法中选择的关键词匹配,虽然它们一致会更好。
🌐 The id does not have to match the keyword you chose in the grammar, though it helps when they agree.
步骤5:主题设置
🌐 Step 5: Theming
Mermaid 有一个集成的主题引擎,详细说明请参见文档。
🌐 Mermaid has an integrated theming engine, described in more detail in the docs.
你的图表在你的图表文件夹中提供了一个 styles.ts 的 getStyles 函数。它使用解析后的主题选项调用,并返回 CSS:
🌐 Your diagram provides a getStyles function in styles.ts in your diagram folder. It is called with the resolved theme options and returns CSS:
js
const getStyles = (options) =>
`
.line {
stroke-width: 1;
stroke: ${options.lineColor};
stroke-dasharray: 2;
}
// ...
`;没有什么需要手动连接的。registerDiagram() 会把你的 styles 传给 addStylesForDiagram(),然后样式引擎就会从那里接手。
🌐 There is nothing to wire up by hand. registerDiagram() passes your styles to addStylesForDiagram(), and the styling engine picks it up from there.
每种颜色都必须来自 options。在默认主题下,硬编码的十六进制值看起来没问题,但在暗色模式下就会出问题,所以审查者会把硬编码颜色当作缺陷。具体的值定义在 src/themes/ 下的主题文件里;如果你的图表需要一个还不存在的变量,就在那里添加,这样五个主题都能定义它。
🌐 Every color must come from options. A hardcoded hex value looks fine in the default theme and then breaks in dark mode, so reviewers treat hardcoded colors as a defect. The values themselves are defined in the theme files under src/themes/; if your diagram needs a variable that does not exist yet, add it there so all five themes define it.
第6步:配置
🌐 Step 6: Configuration
如果你的图表有选项,把它们添加到 src/schemas/config.schema.yaml,既作为图表配置键列表中的一项,也作为它自己的配置块。然后重新生成类型:
🌐 If your diagram has options, add them to src/schemas/config.schema.yaml, both as an entry in the list of diagram config keys and as its own config block. Then regenerate the types:
bash
pnpm run --filter mermaid types:build-config绝不要手动编辑 config.type.ts。它是自动生成的,CI 会根据模式进行校验,手动编辑会导致审核被阻塞。
🌐 Never edit config.type.ts by hand. It is generated, CI verifies it against the schema, and a manual edit is a blocking review finding.
可访问性
🌐 Accessibility
Mermaid 会自动为图表的 SVG HTML 元素添加以下可访问性信息:
🌐 Mermaid automatically adds the following accessibility information for the diagram SVG HTML element:
- aria-roledescription
- 可访问标题
- 可访问描述
aria-roledescription
aria-roledescription 会自动设置为 图表类型 并插入到 SVG 元素中。
🌐 The aria-roledescription is automatically set to the diagram type and inserted into the SVG element.
请参阅 可访问丰富互联网应用 W3 标准 中的 aria-roledescription 定义
🌐 See the definition of aria-roledescription in the Accessible Rich Internet Applications W3 standard.
可访问的标题和描述
🌐 accessible title and description
可访问标题和描述的语法在可访问性文档部分中有描述。
🌐 The syntax for accessible titles and descriptions is described in the Accessibility documentation section.
一旦你的数据库重新导出在步骤 2中显示的设置器,你就可以免费获得两者。这些值会在 mermaidAPI 的 render 函数中插入到 SVG 元素里。
🌐 You get both for free once your db re-exports the setters shown in Step 2. The values are inserted into the SVG element in the render function in mermaidAPI.
第7步:测试
🌐 Step 7: Tests
没有测试的新图表不会被合并。有三种类型,而且都不花很长时间。
🌐 A new diagram without tests will not be merged. There are three kinds, and none of them takes long.
解析器和数据库的单元测试放在代码旁边,命名为 *.spec.ts。覆盖你文档中描述的语法,也要覆盖无效输入:一个对胡乱输入默默接受的图,比一个拒绝它的图要糟糕。用 vitest run packages/mermaid/src/diagrams/<diagram> 运行它们。
🌐 Unit tests for the parser and db go next to the code as *.spec.ts. Cover the syntax you documented, and cover invalid input too: a diagram that accepts nonsense silently is worse than one that rejects it. Run them with vitest run packages/mermaid/src/diagrams/<diagram>.
视觉回归测试来自 .mmd 固件。将每个场景放一个文件在 e2e/diagrams/<diagram>/,这就是整个工作流程:e2e/rendering/mmd-snapshots.spec.ts 会遍历该目录,渲染每个固件,并进行快照,按文件夹分组结果。截图名称在整个目录树中必须唯一,如果两个固件冲突,则运行会快速失败。e2e/sheet-order.json 用于保持顺序。用 pnpm e2e 运行测试套件。覆盖真实的图表,而不是一个最小的冒烟测试,如果你的样式稍有复杂,每个主题都要包含一个固件。
文档测试可以让你的示例保持真实。usecase.docs.spec.ts 读取发布的 syntax/usecase.md,提取每一个 ```mermaid-example 块,并对其进行解析。照着这个模式走,你的文档就不会出现示例已经不能用了的情况。
🌐 A documentation test keeps your examples honest. usecase.docs.spec.ts reads the published syntax/usecase.md, extracts every ```mermaid-example block, and parses it. Copy that pattern and your documentation cannot drift into examples that no longer work.
第8步:文档、演示和示例
🌐 Step 8: Documentation, demos, and examples
把你的语法页面写成 packages/mermaid/src/docs/syntax/<diagram>.md。只编辑 src/docs/ 下的文件;顶层的 /docs 文件夹是生成的,你在那里做的修改会被覆盖。用占位符标记版本,就像 usecase.md 用 # Use case diagrams (<MERMAID_RELEASE_VERSION>+) 做的那样,然后发布流程会替换成真实的版本号。
在 .vitepress/config.ts 的 sidebarSyntax() 下把页面添加到侧边栏。没有侧边栏条目的页面只能通过 URL 访问,实际上意味着没人会去看它。
🌐 Add the page to the sidebar in .vitepress/config.ts under sidebarSyntax(). A page with no sidebar entry is reachable only by URL, which in practice means nobody reads it.
在 demos/<diagram>.html 添加一个演示页面,并从 demos/index.html 链接它,参考现有的任何演示。
至少向 @mermaid-js/examples 包添加一个条目,这就是像 mermaid.live 这样的工具用来帮助大家入门的东西。复制一个现有文件,比如 packages/examples/src/examples/flowchart.ts,进行修改,然后在 packages/examples/src/index.ts 中导入它,并将其添加到 examples 数组中。把一个示例标记为默认,并添加更多示例来展示各自的功能。
🌐 Add at least one entry to the @mermaid-js/examples package, which is what tools like mermaid.live use to help people get started. Duplicate an existing file such as packages/examples/src/examples/flowchart.ts, adapt it, then import it in packages/examples/src/index.ts and add it to the examples array. Mark one example as the default, and add more to show off individual features.
如果你的语法引入了新关键字,把它们加到 .cspell/mermaid-terms.txt。pre-commit 钩子会运行 CSpell,否则会拒绝提交。
🌐 If your syntax introduces new keywords, add them to .cspell/mermaid-terms.txt. The pre-commit hook runs CSpell and will otherwise reject the commit.
第9步:变更集和拉取请求
🌐 Step 9: Changeset and pull request
运行 pnpm changeset,选择 mermaid 包和一个 minor 版本提升,然后写一个以 feat: 开头的描述。
🌐 Run pnpm changeset, choose the mermaid package and a minor bump, and write a description prefixed with feat:.
针对 develop 提交 PR,并关联它解决的问题。新的图表类型本身就比较大,这没问题,但不要把无关的重构也放在同一个分支里。
🌐 Open the PR against develop and link the issue it resolves. New diagram types are large by nature, and that is fine, but keep unrelated refactors out of the same branch.
审稿人清单
🌐 Reviewer's checklist
这就是审稿人会检查的内容。自己先过一遍是得到简短评论的最快方法。
🌐 This is what a reviewer checks. Going through it yourself first is the fastest way to a short review.
- [ ] 解析器使用 Chevrotain,和
diagrams/<diagram>/parser/放在一起 - [ ] 无效输入会产生带位置的解析错误,而不会崩溃
- [ ]
DiagramDefinition导出了解析器、数据库、渲染器和样式 - [ ] 探测器在它自己的文件中,注册在
diagram-orchestration.ts,排列顺序以避免遮挡其他图表 - [ ] 图表 ID 作为 aria roledescription 阅读起来很好
- [ ] db 没有模块级别的状态,而
clear()会重置所有东西,包括commonClear() - [ ] 不能从其他图表的文件夹导入
- [ ] 渲染器应用了填充以及从
useMaxWidth到setupViewPortForSVG - [ ] 手绘模式已实现,或其缺失已记录
- [ ]
styles.ts使用主题选项,没有硬编码的颜色 - [ ] 已向
config.schema.yaml和config.type.ts添加配置选项并重新生成,从未手动编辑 - [ ]
common/commonDb.ts重新导出的可访问性设置器 - [ ] 解析器和数据库的单元测试,涵盖无效输入
- [ ]
e2e/diagrams/<diagram>/中用于视觉回归的.mmd固定装置 - [ ] 文档规范涵盖的文档示例
- [ ]
src/docs/syntax/下的语法页面,包括MERMAID_RELEASE_VERSION和一个侧边栏条目 - [ ] 演示页面和
demos/index.html链接 - [ ] 例子已添加到
@mermaid-js/examples,其中一个标为默认 - [ ] 新关键词已添加到
.cspell/mermaid-terms.txt - [ ] 变更集已创建(
minor,feat:) - [ ] PR 针对
develop并关联了它的问题