diff --git a/packages/site_shared/lib/_sass/components/_mermaid.scss b/packages/site_shared/lib/_sass/components/_mermaid.scss new file mode 100644 index 00000000000..eebefd8372a --- /dev/null +++ b/packages/site_shared/lib/_sass/components/_mermaid.scss @@ -0,0 +1,55 @@ +// 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 while loading or during SSR + pre.mermaid { + margin: 0; + padding: 0; + background: transparent; + border: none; + font-family: var(--site-code-fontFamily); + } + + // 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; + + // Use Google Sans Flex for diagram labels + text, + .label, + .nodeLabel, + .edgeLabel { + font-family: var(--site-ui-fontFamily), sans-serif !important; + } + + // Sharper node outlines matching site borders + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + stroke-width: 1.5px; + fill: var(--site-secondaryContainer-bgColor) !important; + } + + // Edge lines & arrows + .edgePath .path { + stroke-width: 1.5px; + } + } +} diff --git a/packages/site_shared/lib/components/common/client/mermaid_diagram.dart b/packages/site_shared/lib/components/common/client/mermaid_diagram.dart new file mode 100644 index 00000000000..1cfbd496578 --- /dev/null +++ b/packages/site_shared/lib/components/common/client/mermaid_diagram.dart @@ -0,0 +1,111 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:mermaid_core/mermaid_core.dart'; +import 'package:universal_web/js_interop.dart'; +import 'package:universal_web/web.dart' as web; + +@client +final class MermaidViewer extends StatefulComponent { + const MermaidViewer({required this.diagram, super.key}); + + final String diagram; + + @override + State createState() => _MermaidViewerState(); +} + +final class _MermaidViewerState extends State { + String? _svg; + web.MutationObserver? _themeObserver; + + @override + void initState() { + super.initState(); + final isDark = + kIsWeb && (web.document.body?.classList.contains('dark-mode') ?? false); + _svg = _renderDiagram(isDark: isDark); + + if (kIsWeb) { + _observeTheme(); + } + } + + @override + void didUpdateComponent(MermaidViewer oldComponent) { + super.didUpdateComponent(oldComponent); + if (oldComponent.diagram != component.diagram) { + final isDark = + kIsWeb && + (web.document.body?.classList.contains('dark-mode') ?? false); + _svg = _renderDiagram(isDark: isDark); + } + } + + @override + void dispose() { + _themeObserver?.disconnect(); + super.dispose(); + } + + void _observeTheme() { + final body = web.document.body; + if (body == null) return; + + var isDark = body.classList.contains('dark-mode'); + _themeObserver = web.MutationObserver( + ((JSArray _, web.MutationObserver _) { + final newIsDark = body.classList.contains('dark-mode'); + if (newIsDark != isDark) { + isDark = newIsDark; + setState(() { + _svg = _renderDiagram(isDark: isDark); + }); + } + }).toJS, + ); + + _themeObserver?.observe( + body, + web.MutationObserverInit( + attributes: true, + attributeFilter: ['class'.toJS].toJS, + ), + ); + } + + String? _renderDiagram({required bool isDark}) { + try { + final theme = + isDark ? MermaidTheme.darkTheme : MermaidTheme.defaultTheme; + final mermaid = Mermaid( + measurer: const ApproximateTextMeasurer(), + theme: theme, + ); + final scene = mermaid.render(component.diagram); + return renderSceneToSvg(scene); + } catch (e) { + if (kDebugMode) { + print('Failed to render Mermaid diagram: $e'); + } + return null; + } + } + + @override + Component build(BuildContext context) { + return div( + classes: 'mermaid-container', + [ + if (_svg case final svg?) + RawText(svg) + else + // Fallback during SSR or while loading + pre( + classes: 'mermaid', + attributes: {'data-source': component.diagram}, + [.text(component.diagram)], + ), + ], + ); + } +} diff --git a/packages/site_shared/lib/page_extensions.dart b/packages/site_shared/lib/page_extensions.dart index 3b889b65c0b..e99249c0e16 100644 --- a/packages/site_shared/lib/page_extensions.dart +++ b/packages/site_shared/lib/page_extensions.dart @@ -9,4 +9,5 @@ export 'src/extensions/attribute_processor.dart'; export 'src/extensions/code_block_processor.dart'; export 'src/extensions/header_extractor.dart'; export 'src/extensions/header_processor.dart'; +export 'src/extensions/mermaid_processor.dart'; export 'src/extensions/table_processor.dart'; diff --git a/packages/site_shared/lib/src/extensions/mermaid_processor.dart b/packages/site_shared/lib/src/extensions/mermaid_processor.dart new file mode 100644 index 00000000000..b5c721037e0 --- /dev/null +++ b/packages/site_shared/lib/src/extensions/mermaid_processor.dart @@ -0,0 +1,34 @@ +import 'package:jaspr_content/jaspr_content.dart'; + +import '../../components/common/client/mermaid_diagram.dart'; + +final class MermaidProcessor implements PageExtension { + const MermaidProcessor(); + + @override + Future> apply(Page page, List nodes) async => + _processNodes(nodes); + + List _processNodes(List 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(MermaidViewer(diagram: diagram)) + else if (node is ElementNode) + ElementNode( + node.tag, + node.attributes, + node.children != null ? _processNodes(node.children!) : null, + ) + else + node, + ]; + } +} diff --git a/packages/site_shared/lib/src/markdown/markdown_parser.dart b/packages/site_shared/lib/src/markdown/markdown_parser.dart index 60165d98478..dc15c277bc4 100644 --- a/packages/site_shared/lib/src/markdown/markdown_parser.dart +++ b/packages/site_shared/lib/src/markdown/markdown_parser.dart @@ -18,10 +18,12 @@ import 'alert_syntax.dart'; import 'attribute_syntax.dart'; import 'fenced_code_block_syntax.dart'; import 'header_syntax.dart'; +import 'mermaid_syntax.dart'; /// The `package:markdown` block syntaxes to apply when parsing Markdown. const List _blockSyntaxes = [ JasprHtmlBlockSyntax(), + MermaidBlockSyntax(), CustomFencedCodeBlockSyntax(), HeaderWithAttributesSyntax(), AttributeBlockSyntax(), diff --git a/packages/site_shared/lib/src/markdown/mermaid_syntax.dart b/packages/site_shared/lib/src/markdown/mermaid_syntax.dart new file mode 100644 index 00000000000..e476e33b059 --- /dev/null +++ b/packages/site_shared/lib/src/markdown/mermaid_syntax.dart @@ -0,0 +1,59 @@ +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 `
` containing +/// a `
` element that hydrates on the client
+/// via [MermaidViewer].
+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 = [];
+
+    // 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';
+  }
+}
diff --git a/packages/site_shared/pubspec.yaml b/packages/site_shared/pubspec.yaml
index 4251c74efae..a43473195f7 100644
--- a/packages/site_shared/pubspec.yaml
+++ b/packages/site_shared/pubspec.yaml
@@ -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: round3-fixes-and-packaging
+      path: packages/mermaid_core
   meta: ^1.18.2
   nanoid2: ^2.0.1
   opal: ^0.2.4
diff --git a/sites/docs/lib/_sass/_site.scss b/sites/docs/lib/_sass/_site.scss
index 99092c6a676..8160f161328 100644
--- a/sites/docs/lib/_sass/_site.scss
+++ b/sites/docs/lib/_sass/_site.scss
@@ -38,6 +38,7 @@
 @use 'package:site_shared/_sass/components/cookie-notice';
 @use 'package:site_shared/_sass/components/dropdown';
 @use 'package:site_shared/_sass/components/menu-toggle';
+@use 'package:site_shared/_sass/components/mermaid';
 @use 'package:site_shared/_sass/components/progress-ring';
 @use 'package:site_shared/_sass/components/quiz';
 @use 'package:site_shared/_sass/components/site-switcher';
diff --git a/sites/docs/lib/main.client.options.dart b/sites/docs/lib/main.client.options.dart
index 23c1b571e81..ac296cdcf47 100644
--- a/sites/docs/lib/main.client.options.dart
+++ b/sites/docs/lib/main.client.options.dart
@@ -30,6 +30,8 @@ import 'package:site_shared/components/common/client/download_button.dart'
     deferred as _download_button;
 import 'package:site_shared/components/common/client/feedback.dart'
     deferred as _feedback;
+import 'package:site_shared/components/common/client/mermaid_diagram.dart'
+    deferred as _mermaid_diagram;
 import 'package:site_shared/components/common/client/on_this_page_button.dart'
     deferred as _on_this_page_button;
 import 'package:site_shared/components/common/client/page_header_options.dart'
@@ -140,6 +142,10 @@ ClientOptions get defaultClientOptions => ClientOptions(
       (p) => _feedback.FeedbackComponent(issueUrl: p['issueUrl'] as String),
       loader: _feedback.loadLibrary,
     ),
+    'site_shared:mermaid_diagram': ClientLoader(
+      (p) => _mermaid_diagram.MermaidViewer(diagram: p['diagram'] as String),
+      loader: _mermaid_diagram.loadLibrary,
+    ),
     'site_shared:on_this_page_button': ClientLoader(
       (p) => _on_this_page_button.OnThisPageButton(),
       loader: _on_this_page_button.loadLibrary,
diff --git a/sites/docs/lib/main.server.options.dart b/sites/docs/lib/main.server.options.dart
index fcbd5e0a090..03bb51110df 100644
--- a/sites/docs/lib/main.server.options.dart
+++ b/sites/docs/lib/main.server.options.dart
@@ -30,6 +30,8 @@ import 'package:site_shared/components/common/client/download_button.dart'
     as _download_button;
 import 'package:site_shared/components/common/client/feedback.dart'
     as _feedback;
+import 'package:site_shared/components/common/client/mermaid_diagram.dart'
+    as _mermaid_diagram;
 import 'package:site_shared/components/common/client/on_this_page_button.dart'
     as _on_this_page_button;
 import 'package:site_shared/components/common/client/page_header_options.dart'
@@ -114,6 +116,11 @@ ServerOptions get defaultServerOptions => ServerOptions(
       'site_shared:feedback',
       params: __feedbackFeedbackComponent,
     ),
+    _mermaid_diagram.MermaidViewer:
+        ClientTarget<_mermaid_diagram.MermaidViewer>(
+          'site_shared:mermaid_diagram',
+          params: __mermaid_diagramMermaidViewer,
+        ),
     _on_this_page_button.OnThisPageButton:
         ClientTarget<_on_this_page_button.OnThisPageButton>(
           'site_shared:on_this_page_button',
@@ -180,6 +187,9 @@ Map __download_buttonDownloadButton(
 Map __feedbackFeedbackComponent(
   _feedback.FeedbackComponent c,
 ) => {'issueUrl': c.issueUrl};
+Map __mermaid_diagramMermaidViewer(
+  _mermaid_diagram.MermaidViewer c,
+) => {'diagram': c.diagram};
 Map __page_header_optionsPageHeaderOptions(
   _page_header_options.PageHeaderOptions c,
 ) => {
diff --git a/sites/docs/lib/src/extensions/registry.dart b/sites/docs/lib/src/extensions/registry.dart
index 158f0a3a9c9..51993e1576c 100644
--- a/sites/docs/lib/src/extensions/registry.dart
+++ b/sites/docs/lib/src/extensions/registry.dart
@@ -16,6 +16,7 @@ const List allNodeProcessingExtensions = [
   HeaderExtractorExtension(),
   HeaderWrapperExtension(),
   TableWrapperExtension(),
+  MermaidProcessor(),
   CodeBlockProcessor(defaultTitle: 'Runnable Flutter example'),
   GlossaryLinkProcessor(),
   TutorialNavigationExtension(),
diff --git a/sites/docs/src/content/ai/evals.md b/sites/docs/src/content/ai/evals.md
index e4985cc52b3..e8b56a5e769 100644
--- a/sites/docs/src/content/ai/evals.md
+++ b/sites/docs/src/content/ai/evals.md
@@ -28,3 +28,42 @@ Evals measure both deterministic code correctness
 (compilation, lints, automated tests) and qualitative performance
 (reasoning, safety, and conciseness) using automated model judges
 and expert human grading.
+
+```mermaid
+flowchart TB
+    c1:::myStyle-->a2
+    subgraph one
+    a1-->a2
+    end
+    subgraph two
+    b1-->b2
+    end
+    subgraph three
+    c1-->c2
+    end
+    one --> two
+    three --> two
+    two --> c2
+```
+
+More test
+
+```mermaid
+flowchart LR
+  subgraph tasks
+    direction TB
+    t1(Do first thing) --> t2(do second thing)
+  end 
+  User["`As an **App developer**`"] -->|i want to| Goal[fix overflow errors]
+  Goal-->tasks
+```
+
+
+```mermaid
+graph TD
+    A[Start] --> B{Is it working?}
+    B -->|Yes| C[Ship it]
+    B -->|No| D[Debug]
+    D --> B
+    C --> E[Celebrate]
+```
\ No newline at end of file
diff --git a/sites/docs/src/content/contribute/docs/markdown.md b/sites/docs/src/content/contribute/docs/markdown.md
index fa62002c26b..ca9ac2939b5 100644
--- a/sites/docs/src/content/contribute/docs/markdown.md
+++ b/sites/docs/src/content/contribute/docs/markdown.md
@@ -47,3 +47,62 @@ To learn more about customizing code blocks,
 check out the dedicated documentation on [Code blocks][].
 
 [Code blocks]: /contribute/docs/code-blocks
+
+
+## Mermaid diagrams
+
+To render flowcharts, sequence diagrams, and other charts within a Markdown file,
+use a fenced code block with the `mermaid` language identifier:
+
+```markdown
+```mermaid
+flowchart LR
+    A[Start] --> B(Process)
+    B --> C{Decision}
+    C -->|Yes| D[Done]
+    C -->|No| B
+```
+```
+
+### Styling Mermaid diagrams
+
+Mermaid diagrams automatically adapt to the site's light and dark themes
+and inherit the default `Google Sans Flex` typography.
+
+#### In-diagram styling (Recommended)
+
+When you need custom colors for specific nodes,
+define them directly in the diagram using Mermaid's built-in
+[`classDef` and `:::styleName` syntax][]:
+
+```mermaid
+flowchart LR
+    A:::highlightNode --> B
+    classDef highlightNode fill:#f96,stroke:#333,stroke-width:2px;
+```
+
+#### SCSS styling
+
+To adjust the global diagram layout, border, or background across the site,
+edit [`_mermaid.scss`][]:
+
+```scss
+.mermaid-container {
+  // Container styling (padding, margins, background, border)
+
+  // Fallback while loading or during SSR
+  pre.mermaid {
+    // Pre-loading styles
+  }
+
+  svg {
+    // Custom SVG element overrides (requires !important)
+    .highlightNode rect {
+      fill: var(--site-primary-color) !important;
+    }
+  }
+}
+```
+
+[`classDef` and `:::styleName` syntax]: https://mermaid.js.org/syntax/flowchart.html#styling-and-classes
+[`_mermaid.scss`]: https://github.com/flutter/website/blob/main/packages/site_shared/lib/_sass/components/_mermaid.scss
diff --git a/sites/www/lib/main.client.options.dart b/sites/www/lib/main.client.options.dart
index 27a2638f576..0372da80744 100644
--- a/sites/www/lib/main.client.options.dart
+++ b/sites/www/lib/main.client.options.dart
@@ -34,6 +34,8 @@ import 'package:site_shared/components/common/client/collapse_button.dart'
     deferred as _collapse_button;
 import 'package:site_shared/components/common/client/copy_button.dart'
     deferred as _copy_button;
+import 'package:site_shared/components/common/client/mermaid_diagram.dart'
+    deferred as _mermaid_diagram;
 import 'package:site_shared/components/dartpad/dartpad_injector.dart'
     deferred as _dartpad_injector;
 import 'package:site_shared/components/utils/component_ref.dart'
@@ -181,6 +183,10 @@ ClientOptions get defaultClientOptions => ClientOptions(
       ),
       loader: _copy_button.loadLibrary,
     ),
+    'site_shared:mermaid_diagram': ClientLoader(
+      (p) => _mermaid_diagram.MermaidViewer(diagram: p['diagram'] as String),
+      loader: _mermaid_diagram.loadLibrary,
+    ),
     'site_shared:dartpad_injector': ClientLoader(
       (p) => _dartpad_injector.DartPadInjector(
         title: p['title'] as String,
diff --git a/sites/www/lib/main.server.options.dart b/sites/www/lib/main.server.options.dart
index 55b5d4f6e2d..84a754ecf76 100644
--- a/sites/www/lib/main.server.options.dart
+++ b/sites/www/lib/main.server.options.dart
@@ -30,6 +30,8 @@ import 'package:site_shared/components/common/client/collapse_button.dart'
     as _collapse_button;
 import 'package:site_shared/components/common/client/copy_button.dart'
     as _copy_button;
+import 'package:site_shared/components/common/client/mermaid_diagram.dart'
+    as _mermaid_diagram;
 import 'package:site_shared/components/dartpad/dartpad_injector.dart'
     as _dartpad_injector;
 
@@ -103,6 +105,11 @@ ServerOptions get defaultServerOptions => ServerOptions(
       'site_shared:copy_button',
       params: __copy_buttonCopyButton,
     ),
+    _mermaid_diagram.MermaidViewer:
+        ClientTarget<_mermaid_diagram.MermaidViewer>(
+          'site_shared:mermaid_diagram',
+          params: __mermaid_diagramMermaidViewer,
+        ),
     _dartpad_injector.DartPadInjector:
         ClientTarget<_dartpad_injector.DartPadInjector>(
           'site_shared:dartpad_injector',
@@ -159,6 +166,9 @@ Map __copy_buttonCopyButton(_copy_button.CopyButton c) => {
   'classes': c.classes,
   'title': c.title,
 };
+Map __mermaid_diagramMermaidViewer(
+  _mermaid_diagram.MermaidViewer c,
+) => {'diagram': c.diagram};
 Map __dartpad_injectorDartPadInjector(
   _dartpad_injector.DartPadInjector c,
 ) => {
diff --git a/sites/www/lib/styles/styles.scss b/sites/www/lib/styles/styles.scss
index f68b8bceeb2..163c61871d3 100644
--- a/sites/www/lib/styles/styles.scss
+++ b/sites/www/lib/styles/styles.scss
@@ -51,3 +51,4 @@
 @use 'package:site_shared/_sass/components/blog';
 @use 'package:site_shared/_sass/components/code';
 @use 'package:site_shared/_sass/components/dropdown';
+@use 'package:site_shared/_sass/components/mermaid';