-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Add mermaid support #13741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add mermaid support #13741
Changes from all commits
a5c7f55
54f30b0
196e378
9942358
ecd987c
cb5b96a
038621f
4fb208c
575f9eb
48fb363
bbddf76
2fb4585
bb978e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // Copyright 2026 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| .mermaid-container { | ||
| display: flex; | ||
| justify-content: center; | ||
| margin: 1.75rem 0; | ||
| padding: 1.5rem 1rem; | ||
| overflow-x: auto; | ||
| background-color: var(--site-raised-bgColor-translucent); | ||
| border: 1px solid var(--site-outline-variant); | ||
| border-radius: var(--site-radius); | ||
|
|
||
| // Fallback shown only if server rendering the diagram failed | ||
| pre.mermaid { | ||
| margin: 0; | ||
| padding: 0; | ||
| background: transparent; | ||
| border: none; | ||
| font-family: var(--site-code-fontFamily); | ||
| } | ||
|
|
||
| // Both theme variants are rendered on the server; show only the one | ||
| // matching the site's current theme. | ||
| .mermaid-theme-dark { | ||
| display: none; | ||
| } | ||
|
|
||
| @at-root body.dark-mode & { | ||
| .mermaid-theme-light { | ||
| display: none; | ||
| } | ||
|
|
||
| .mermaid-theme-dark { | ||
| display: block; | ||
| } | ||
| } | ||
|
|
||
| // Rendered SVG styling | ||
| // You must use !important to override styles for specific elements within | ||
| // the rendered SVG. For styling individual graphs, use Mermaid's built-in | ||
| // classRef system | ||
| svg { | ||
| max-width: 100%; | ||
| height: auto; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // Copyright 2026 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:jaspr/dom.dart'; | ||
| import 'package:jaspr/jaspr.dart'; | ||
| import 'package:mermaid_core/mermaid_core.dart'; | ||
|
|
||
| /// Renders a Mermaid diagram as server-generated SVG. | ||
| /// | ||
| /// Both light and dark variants are rendered at build time and included in | ||
| /// the page, so CSS can show the variant matching the site's current theme | ||
| /// without shipping a Mermaid renderer to the client. | ||
| final class MermaidDiagram extends StatelessComponent { | ||
| const MermaidDiagram({required this.diagram, super.key}); | ||
|
|
||
| /// The Mermaid diagram definition to render. | ||
| final String diagram; | ||
|
|
||
| @override | ||
| Component build(BuildContext context) { | ||
| final lightSvg = _renderDiagram(theme: MermaidTheme.defaultTheme); | ||
| final darkSvg = _renderDiagram(theme: MermaidTheme.darkTheme); | ||
|
|
||
| return div( | ||
| classes: 'mermaid-container', | ||
| [ | ||
| if (lightSvg != null && darkSvg != null) ...[ | ||
| div(classes: 'mermaid-theme mermaid-theme-light', [ | ||
| RawText(lightSvg), | ||
| ]), | ||
| div(classes: 'mermaid-theme mermaid-theme-dark', [ | ||
| RawText(darkSvg), | ||
| ]), | ||
| ] else | ||
| // If rendering fails, preserve the source in a fallback element. | ||
| pre( | ||
| classes: 'mermaid', | ||
| attributes: {'data-source': diagram}, | ||
| [.text(diagram)], | ||
| ), | ||
| ], | ||
| ); | ||
| } | ||
|
|
||
| /// Renders [diagram] as SVG using the specified [theme]. | ||
| String? _renderDiagram({required MermaidTheme theme}) { | ||
| try { | ||
| // Server rendering doesn't have browser text metrics available, so use | ||
| // the library's deterministic approximation when laying out labels. | ||
| final mermaid = Mermaid( | ||
| measurer: const ApproximateTextMeasurer(), | ||
| theme: theme, | ||
| ); | ||
| final scene = mermaid.render(diagram); | ||
| return renderSceneToSvg(scene); | ||
| } catch (error) { | ||
| if (kDebugMode) { | ||
| print('Failed to render Mermaid diagram: $error'); | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Copyright 2026 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:jaspr_content/jaspr_content.dart'; | ||
|
|
||
| import '../../components/common/mermaid_diagram.dart'; | ||
|
|
||
| final class MermaidProcessor implements PageExtension { | ||
| const MermaidProcessor(); | ||
|
|
||
| @override | ||
| Future<List<Node>> apply(Page page, List<Node> nodes) async => | ||
| _processNodes(nodes); | ||
|
|
||
| List<Node> _processNodes(List<Node> nodes) { | ||
| return [ | ||
| for (final node in nodes) | ||
| if (node case ElementNode( | ||
| tag: 'div', | ||
| attributes: {'class': 'mermaid-container'}, | ||
| children: [ | ||
| ElementNode(attributes: {'data-source': final diagram}), | ||
| ..., | ||
| ], | ||
| )) | ||
| ComponentNode(MermaidDiagram(diagram: diagram)) | ||
| else if (node is ElementNode) | ||
| ElementNode( | ||
| node.tag, | ||
| node.attributes, | ||
| node.children != null ? _processNodes(node.children!) : null, | ||
| ) | ||
| else | ||
| node, | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright 2026 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:markdown/markdown.dart' as md; | ||
|
|
||
| /// A custom Markdown block syntax for diagrams authored | ||
| /// between ```mermaid code fences. | ||
| /// | ||
| /// Example: | ||
| /// | ||
| /// ````markdown | ||
| /// ```mermaid | ||
| /// flowchart TD | ||
| /// A --> B | ||
| /// ``` | ||
| /// ```` | ||
| /// | ||
| /// This renders as a `<div class="mermaid-container">` containing | ||
| /// a `<pre class="mermaid">` element with the diagram source, which | ||
| /// `MermaidProcessor` replaces with a server-rendered `MermaidDiagram`. | ||
| final class MermaidBlockSyntax extends md.BlockSyntax { | ||
| const MermaidBlockSyntax(); | ||
|
|
||
| // Matches opening fence: ```mermaid (with optional trailing whitespace/config) | ||
| @override | ||
| RegExp get pattern => RegExp(r'^\s{0,3}`{3,}mermaid(?:\s.*)?$'); | ||
|
|
||
| static final _closingFencePattern = RegExp(r'^\s{0,3}`{3,}\s*$'); | ||
|
|
||
| @override | ||
| bool canParse(md.BlockParser parser) { | ||
| return pattern.hasMatch(parser.current.content); | ||
| } | ||
|
|
||
| @override | ||
| md.Node? parse(md.BlockParser parser) { | ||
| // Advance past the opening ```mermaid line | ||
| parser.advance(); | ||
|
|
||
| final lines = <String>[]; | ||
|
|
||
| // Collect diagram definition until the closing ``` | ||
| while (!parser.isDone) { | ||
| final line = parser.current.content; | ||
| if (_closingFencePattern.hasMatch(line)) { | ||
| parser.advance(); // Consume closing fence | ||
| break; | ||
| } | ||
| lines.add(line); | ||
| parser.advance(); | ||
| } | ||
|
|
||
| final rawContent = lines.join('\n'); | ||
|
|
||
| // Return HTML AST node for the diagram container | ||
| final pre = md.Element.text('pre', rawContent) | ||
| ..attributes['class'] = 'mermaid' | ||
| ..attributes['data-source'] = rawContent; | ||
|
|
||
| return md.Element('div', [pre])..attributes['class'] = 'mermaid-container'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,11 @@ dependencies: | |
| jaspr_content: ^0.5.3 | ||
| markdown: ^7.3.1 | ||
| markdown_description_list: ^0.2.0 | ||
| mermaid_core: | ||
| git: | ||
| url: https://github.com/orestesgaolin/mermaid.git | ||
| ref: 03759ce31539f87a1487e84d132d3ad1e8efa3cd | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider trying to update to 90c39dd66b5e798c12dd77ec8b3fba4d583f0a05. It seems it might include a fix to an issue you mentioned.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That commit doesn't seem to be ready, it has broken dependencies internally, I'm going to file an issue |
||
| path: packages/mermaid_core | ||
| meta: ^1.18.2 | ||
| nanoid2: ^2.0.1 | ||
| opal: ^0.2.4 | ||
|
|
||
|
parlough marked this conversation as resolved.
|
Uh oh!
There was an error while loading. Please reload this page.