From 9975360d418811cc239b73bda9b2970af137efd7 Mon Sep 17 00:00:00 2001 From: cinereal Date: Sun, 4 Oct 2026 13:12:28 +0200 Subject: [PATCH 1/6] ndg-commonmark: add a `mermaid` option for diagram blocks With `mermaid` set, the processor renders each `mermaid` fenced code block as a `
` element with the diagram source as text. It does not highlight these blocks. The page must load Mermaid to draw the diagrams.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
---
 ndg-commonmark/src/processor/core.rs    | 35 +++++++++++++
 ndg-commonmark/src/processor/process.rs |  3 ++
 ndg-commonmark/src/processor/types.rs   | 18 +++++++
 ndg-commonmark/tests/mermaid.rs         | 70 +++++++++++++++++++++++++
 4 files changed, 126 insertions(+)
 create mode 100644 ndg-commonmark/tests/mermaid.rs

diff --git a/ndg-commonmark/src/processor/core.rs b/ndg-commonmark/src/processor/core.rs
index ef9252e3b..f8edba5a9 100644
--- a/ndg-commonmark/src/processor/core.rs
+++ b/ndg-commonmark/src/processor/core.rs
@@ -641,11 +641,46 @@ impl MarkdownProcessor {
   fn kuchiki_postprocess(&self, html: &str) -> String {
     // Use a standalone function to avoid borrowing issues
     kuchiki_postprocess_html(html, |document| {
+      if self.options.mermaid {
+        Self::process_mermaid_blocks(document);
+      }
       self.highlight_codeblocks_document(document);
       Self::apply_dom_transformations(document);
     })
   }
 
+  /// Replace each `mermaid` code block with a `
` element
+  /// that holds the diagram source as text. The Mermaid library draws these
+  /// elements on the client.
+  fn process_mermaid_blocks(document: &kuchikikiki::NodeRef) {
+    let mut to_modify = Vec::new();
+    for code_node in safe_select(document, "pre > code.language-mermaid") {
+      if let Some(pre_node) = code_node.parent() {
+        to_modify.push((pre_node, code_node.text_contents()));
+      }
+    }
+
+    for (pre_node, source) in to_modify {
+      let diagram = kuchikikiki::NodeRef::new_element(
+        markup5ever::QualName::new(
+          None,
+          markup5ever::ns!(html),
+          local_name!("pre"),
+        ),
+        vec![(
+          kuchikikiki::ExpandedName::new("", "class"),
+          kuchikikiki::Attribute {
+            prefix: None,
+            value:  "mermaid".into(),
+          },
+        )],
+      );
+      diagram.append(kuchikikiki::NodeRef::new_text(source));
+      pre_node.insert_after(diagram);
+      pre_node.detach();
+    }
+  }
+
   fn highlight_codeblocks_document(&self, document: &kuchikikiki::NodeRef) {
     if !self.options.highlight_code || self.syntax_manager.is_none() {
       return;
diff --git a/ndg-commonmark/src/processor/process.rs b/ndg-commonmark/src/processor/process.rs
index 5d43db829..6a03f15e3 100644
--- a/ndg-commonmark/src/processor/process.rs
+++ b/ndg-commonmark/src/processor/process.rs
@@ -163,6 +163,7 @@ fn options_for_preset(preset: ProcessorPreset) -> MarkdownOptions {
         auto_link_options:   true,
         tab_style:           TabStyle::None,
         valid_options:       None,
+        mermaid:             false,
       }
     },
     ProcessorPreset::Ndg => {
@@ -177,6 +178,7 @@ fn options_for_preset(preset: ProcessorPreset) -> MarkdownOptions {
         auto_link_options:   true,
         tab_style:           TabStyle::None,
         valid_options:       None,
+        mermaid:             false,
       }
     },
     ProcessorPreset::Nixpkgs => {
@@ -191,6 +193,7 @@ fn options_for_preset(preset: ProcessorPreset) -> MarkdownOptions {
         auto_link_options:   true,
         tab_style:           TabStyle::None,
         valid_options:       None,
+        mermaid:             false,
       }
     },
   }
diff --git a/ndg-commonmark/src/processor/types.rs b/ndg-commonmark/src/processor/types.rs
index 77b7c2203..265fc8edc 100644
--- a/ndg-commonmark/src/processor/types.rs
+++ b/ndg-commonmark/src/processor/types.rs
@@ -104,6 +104,14 @@ pub struct MarkdownOptions {
 
   /// How to handle hard tabs in code blocks.
   pub tab_style: TabStyle,
+
+  /// Render `mermaid` fenced code blocks as Mermaid diagrams.
+  ///
+  /// When `true`, each `mermaid` fenced code block becomes a
+  /// `
` element that holds the diagram source, without
+  /// syntax highlighting. The page must load the Mermaid library to draw the
+  /// diagrams.
+  pub mermaid: bool,
 }
 
 /// Configuration for handling hard tabs in code blocks.
@@ -135,6 +143,7 @@ impl MarkdownOptions {
       auto_link_options:   true,
       valid_options:       None,
       tab_style:           TabStyle::None,
+      mermaid:             false,
     }
   }
 
@@ -156,6 +165,7 @@ impl MarkdownOptions {
       auto_link_options: true,
       valid_options: None,
       tab_style: TabStyle::None,
+      mermaid: false,
     }
   }
 }
@@ -173,6 +183,7 @@ impl Default for MarkdownOptions {
       auto_link_options:   true,
       valid_options:       None,
       tab_style:           TabStyle::None,
+      mermaid:             false,
     }
   }
 }
@@ -424,6 +435,13 @@ impl MarkdownOptionsBuilder {
     self
   }
 
+  /// Enable or disable Mermaid diagram blocks.
+  #[must_use]
+  pub const fn mermaid(mut self, enabled: bool) -> Self {
+    self.options.mermaid = enabled;
+    self
+  }
+
   /// Build the final `MarkdownOptions`.
   #[must_use]
   pub fn build(self) -> MarkdownOptions {
diff --git a/ndg-commonmark/tests/mermaid.rs b/ndg-commonmark/tests/mermaid.rs
new file mode 100644
index 000000000..085bcdd3c
--- /dev/null
+++ b/ndg-commonmark/tests/mermaid.rs
@@ -0,0 +1,70 @@
+#![allow(clippy::expect_used, clippy::unwrap_used, reason = "Fine in tests")]
+use ndg_commonmark::{MarkdownOptions, MarkdownProcessor};
+
+const DIAGRAM: &str = "```mermaid\nflowchart LR\n  a --> b\n```\n";
+
+fn render(options: MarkdownOptions, md: &str) -> String {
+  MarkdownProcessor::new(options).render(md).html
+}
+
+#[test]
+fn mermaid_disabled_keeps_code_block() {
+  let html = render(
+    MarkdownOptions {
+      highlight_code: false,
+      ..Default::default()
+    },
+    DIAGRAM,
+  );
+  assert!(html.contains("language-mermaid"), "{html}");
+  assert!(!html.contains(r#"
"#), "{html}");
+}
+
+#[test]
+fn mermaid_enabled_renders_diagram_element() {
+  let html = render(
+    MarkdownOptions {
+      highlight_code: false,
+      mermaid: true,
+      ..Default::default()
+    },
+    DIAGRAM,
+  );
+  assert!(
+    html.contains("
flowchart LR\n  a --> b\n
"), + "{html}" + ); + assert!(!html.contains("language-mermaid"), "{html}"); +} + +#[test] +fn mermaid_enabled_skips_highlighting() { + let options = MarkdownOptions { + mermaid: true, + ..Default::default() + }; + let highlight_code = options.highlight_code; + let html = + render(options, &format!("{DIAGRAM}\n```nix\n{{ a = 1; }}\n```\n")); + assert!( + html.contains("
flowchart LR\n  a --> b\n
"), + "{html}" + ); + if highlight_code { + // Other code blocks are still highlighted. + assert!(html.contains(r#"
"#), "{html}");
+  }
+}
+
+#[test]
+fn mermaid_enabled_leaves_other_languages() {
+  let html = render(
+    MarkdownOptions {
+      highlight_code: false,
+      mermaid: true,
+      ..Default::default()
+    },
+    "```text\nflowchart LR\n```\n",
+  );
+  assert!(!html.contains(r#"
"#), "{html}");
+}

From d03deae77e48b8e3d3c90afda0ab06dcb4419db2 Mon Sep 17 00:00:00 2001
From: cinereal 
Date: Sun, 4 Oct 2026 13:12:28 +0200
Subject: [PATCH 2/6] ndg-config: add a `[mermaid]` section

`mermaid.enable` enables Mermaid diagrams. `mermaid.script` sets the Mermaid library as a URL or as a local file. It defaults to Mermaid 11 from jsDelivr. Validation fails if a local file does not exist.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
---
 crates/ndg-config/src/config.rs    | 41 +++++++++++++
 crates/ndg-config/src/lib.rs       |  1 +
 crates/ndg-config/src/mermaid.rs   | 95 ++++++++++++++++++++++++++++++
 crates/ndg-config/src/templates.rs | 11 ++++
 4 files changed, 148 insertions(+)
 create mode 100644 crates/ndg-config/src/mermaid.rs

diff --git a/crates/ndg-config/src/config.rs b/crates/ndg-config/src/config.rs
index 64947df0a..5d54965dd 100644
--- a/crates/ndg-config/src/config.rs
+++ b/crates/ndg-config/src/config.rs
@@ -13,6 +13,7 @@ use crate::{
   error::ConfigError,
   index,
   markdown,
+  mermaid,
   meta,
   options,
   postprocess,
@@ -114,6 +115,10 @@ pub struct Config {
   #[config(nested)]
   pub markdown: Option,
 
+  /// Mermaid diagram configuration.
+  #[config(nested)]
+  pub mermaid: Option,
+
   /// GitHub revision for linking to source files.
   #[config(key = "revision")]
   pub revision: String,
@@ -209,6 +214,7 @@ impl Default for Config {
       footer_text:           DEFAULT_FOOTER_TEXT.to_string(),
       highlight_code:        true,
       markdown:              None,
+      mermaid:               None,
       revision:              DEFAULT_REVISION.to_string(),
       included_files:        FxHashMap::default(),
       included_output_files: FxHashMap::default(),
@@ -234,6 +240,20 @@ impl Config {
     self.search.as_ref().is_none_or(|s| s.enable)
   }
 
+  /// Returns the Mermaid configuration when Mermaid diagrams are enabled.
+  ///
+  /// Mermaid diagrams are disabled when no `mermaid` configuration is present.
+  #[must_use]
+  pub fn enabled_mermaid(&self) -> Option<&mermaid::MermaidConfig> {
+    self.mermaid.as_ref().filter(|m| m.enable)
+  }
+
+  /// Returns whether Mermaid diagrams are enabled.
+  #[must_use]
+  pub fn is_mermaid_enabled(&self) -> bool {
+    self.enabled_mermaid().is_some()
+  }
+
   /// Returns the maximum heading level to index for search.
   #[must_use]
   pub fn search_max_heading_level(&self) -> u8 {
@@ -790,6 +810,18 @@ impl Config {
       }
     }
 
+    // A local Mermaid library file should exist if Mermaid is enabled
+    if let Some(mermaid::MermaidScript::File(script)) = self
+      .enabled_mermaid()
+      .map(mermaid::MermaidConfig::script_source)
+      && !script.is_file()
+    {
+      errors.push(format!(
+        "Mermaid script file does not exist: {}",
+        script.display()
+      ));
+    }
+
     // Nixdoc input paths should exist if specified
     for (index, nixdoc_input) in self.nixdoc_inputs.iter().enumerate() {
       if !nixdoc_input.exists() {
@@ -1145,6 +1177,15 @@ mod tests {
     assert_eq!(config.input_dir, Some(PathBuf::from("/new/path")));
   }
 
+  #[test]
+  fn test_apply_override_mermaid() {
+    let mut config = Config::default();
+    assert!(!config.is_mermaid_enabled());
+
+    config.apply_override("mermaid.enable", "true").unwrap();
+    assert!(config.is_mermaid_enabled());
+  }
+
   #[test]
   fn test_apply_override_nested_config() {
     let mut config = Config::default();
diff --git a/crates/ndg-config/src/lib.rs b/crates/ndg-config/src/lib.rs
index 66080aa7b..fd11e88f4 100644
--- a/crates/ndg-config/src/lib.rs
+++ b/crates/ndg-config/src/lib.rs
@@ -5,6 +5,7 @@ pub mod error;
 pub mod index;
 pub mod markdown;
 pub mod matchers;
+pub mod mermaid;
 pub mod meta;
 pub mod options;
 pub mod postprocess;
diff --git a/crates/ndg-config/src/mermaid.rs b/crates/ndg-config/src/mermaid.rs
new file mode 100644
index 000000000..96aff2f2e
--- /dev/null
+++ b/crates/ndg-config/src/mermaid.rs
@@ -0,0 +1,95 @@
+use std::path::Path;
+
+use ndg_macros::Configurable;
+use serde::{Deserialize, Serialize};
+
+/// The Mermaid library that ndg loads when `mermaid.script` is not set.
+pub const DEFAULT_MERMAID_SCRIPT: &str =
+  "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
+
+/// Configuration for Mermaid diagrams
+#[derive(Debug, Clone, Default, Serialize, Deserialize, Configurable)]
+#[serde(default, deny_unknown_fields)]
+pub struct MermaidConfig {
+  /// Whether to render `mermaid` fenced code blocks as diagrams
+  #[config(key = "enable")]
+  pub enable: bool,
+
+  /// Where to load the Mermaid library from
+  ///
+  /// A URL (`https://`, `http://` or `//`) is linked as-is. Any other value
+  /// is a path to a local file, which ndg copies to the `assets` directory.
+  /// Defaults to [`DEFAULT_MERMAID_SCRIPT`].
+  #[config(key = "script", allow_empty)]
+  pub script: Option,
+}
+
+/// The source of the Mermaid library.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MermaidScript<'a> {
+  /// A URL that pages link to as-is.
+  Url(&'a str),
+  /// A local file that ndg copies to the `assets` directory.
+  File(&'a Path),
+}
+
+impl MermaidConfig {
+  /// Returns the configured source of the Mermaid library.
+  #[must_use]
+  pub fn script_source(&self) -> MermaidScript<'_> {
+    let script = self.script.as_deref().unwrap_or(DEFAULT_MERMAID_SCRIPT);
+    if ["https://", "http://", "//"]
+      .iter()
+      .any(|prefix| script.starts_with(prefix))
+    {
+      MermaidScript::Url(script)
+    } else {
+      MermaidScript::File(Path::new(script))
+    }
+  }
+}
+
+#[cfg(test)]
+#[expect(clippy::unwrap_used, reason = "Fine in tests")]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_mermaid_config_defaults_to_disabled_cdn_script() {
+    let config = MermaidConfig::default();
+    assert!(!config.enable);
+    assert_eq!(
+      config.script_source(),
+      MermaidScript::Url(DEFAULT_MERMAID_SCRIPT)
+    );
+  }
+
+  #[test]
+  fn test_mermaid_config_apply_override() {
+    let mut config = MermaidConfig::default();
+    config.apply_override("enable", "true").unwrap();
+    config
+      .apply_override("script", "vendor/mermaid.min.js")
+      .unwrap();
+    assert!(config.enable);
+    assert_eq!(
+      config.script_source(),
+      MermaidScript::File(Path::new("vendor/mermaid.min.js"))
+    );
+  }
+
+  #[test]
+  fn test_mermaid_config_script_source_url() {
+    for url in [
+      "https://example.com/mermaid.min.js",
+      "http://example.com/mermaid.min.js",
+      "//example.com/mermaid.min.js",
+    ] {
+      let config = MermaidConfig {
+        script: Some(url.to_string()),
+        ..Default::default()
+      };
+      assert_eq!(config.script_source(), MermaidScript::Url(url));
+    }
+  }
+}
diff --git a/crates/ndg-config/src/templates.rs b/crates/ndg-config/src/templates.rs
index 92255a5f7..86fe29089 100644
--- a/crates/ndg-config/src/templates.rs
+++ b/crates/ndg-config/src/templates.rs
@@ -96,6 +96,14 @@ revision = "main"
 # [markdown]
 # extensions = ["math-dollars", "math-code", "math-latex"]
 
+# Mermaid diagrams
+# When enabled, `mermaid` fenced code blocks are rendered as diagrams.
+# `script` is a URL or a local path to the Mermaid library. A local file is
+# copied to the assets directory. It defaults to Mermaid 11 from jsDelivr.
+# [mermaid]
+# enable = true
+# script = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"
+
 # Index page configuration
 # [index]
 # Whether to use README.md as the homepage when index.md is not present.
@@ -281,6 +289,9 @@ pub const DEFAULT_JSON_TEMPLATE: &str = r#"{
   "markdown": {
     "extensions": []
   },
+  "mermaid": {
+    "enable": false
+  },
   "syntax_queries_path": "queries",
   "tab_style": "none",
   "revision": "main",

From 3fe50560c6e3509fdbf12ba78551593da67d6eff Mon Sep 17 00:00:00 2001
From: cinereal 
Date: Sun, 4 Oct 2026 13:12:28 +0200
Subject: [PATCH 3/6] ndg: render Mermaid diagrams

When Mermaid is enabled, NDG renders `mermaid` code blocks for Mermaid and adds `assets/mermaid-init.js` to each page. The script loads the Mermaid library only on a page with a diagram, and draws the diagrams that the page adds later. NDG copies a local Mermaid library to `assets/` without postprocessing.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
---
 crates/ndg-html/src/template.rs               | 21 ++++-
 crates/ndg-html/tests/custom_scripts.rs       | 83 ++++++++++++++++++
 crates/ndg-templates/src/lib.rs               |  2 +
 .../ndg-templates/templates/mermaid-init.js   | 56 ++++++++++++
 .../templates/mermaid-init.test.js            | 86 +++++++++++++++++++
 crates/ndg-utils/src/assets.rs                | 38 +++++++-
 crates/ndg-utils/src/markdown.rs              |  1 +
 ndg/tests/integration.rs                      | 57 +++++++++++-
 8 files changed, 341 insertions(+), 3 deletions(-)
 create mode 100644 crates/ndg-templates/templates/mermaid-init.js
 create mode 100644 crates/ndg-templates/templates/mermaid-init.test.js

diff --git a/crates/ndg-html/src/template.rs b/crates/ndg-html/src/template.rs
index bde077a11..b48d64aef 100644
--- a/crates/ndg-html/src/template.rs
+++ b/crates/ndg-html/src/template.rs
@@ -11,7 +11,7 @@ use color_eyre::eyre::{Context, Result, bail};
 use html_escape::{encode_double_quoted_attribute, encode_text};
 use indexmap::IndexMap;
 use ndg_commonmark::{Header, MarkdownProcessor};
-use ndg_config::{Config, sidebar::SidebarOrdering};
+use ndg_config::{Config, mermaid::MermaidScript, sidebar::SidebarOrdering};
 use ndg_manpage::types::NixOption;
 use ndg_templates as templates;
 use ndg_utils::{
@@ -1640,6 +1640,25 @@ fn generate_custom_scripts(
     )?;
   }
 
+  if let Some(mermaid) = config.enabled_mermaid() {
+    // `copy_assets` copies a local Mermaid script to `assets/`.
+    let mermaid_src = match mermaid.script_source() {
+      MermaidScript::Url(url) => url.to_owned(),
+      MermaidScript::File(path) => {
+        let Some(file_name) = path.file_name() else {
+          return Ok(custom_scripts);
+        };
+        format!("{}assets/{}", root_prefix, file_name.to_string_lossy())
+      },
+    };
+    write!(
+      custom_scripts,
+      "",
+      encode_double_quoted_attribute(&mermaid_src)
+    )?;
+  }
+
   Ok(custom_scripts)
 }
 
diff --git a/crates/ndg-html/tests/custom_scripts.rs b/crates/ndg-html/tests/custom_scripts.rs
index d923cc9bf..81db75e7b 100644
--- a/crates/ndg-html/tests/custom_scripts.rs
+++ b/crates/ndg-html/tests/custom_scripts.rs
@@ -4,6 +4,7 @@ use std::path::PathBuf;
 mod common;
 
 use common::test_config;
+use ndg_config::mermaid::{DEFAULT_MERMAID_SCRIPT, MermaidConfig};
 use ndg_html::template::render;
 use tempfile::TempDir;
 
@@ -38,3 +39,85 @@ fn test_custom_script_links_to_copied_asset() {
     "Script should not link to its source path"
   );
 }
+
+fn render_with_mermaid(
+  script: Option<&str>,
+  output_dir: &std::path::Path,
+) -> String {
+  let config = ndg_config::Config {
+    mermaid: Some(MermaidConfig {
+      enable: true,
+      script: script.map(str::to_owned),
+    }),
+    ..test_config(output_dir)
+  };
+
+  render(
+    &config,
+    "

Test content

", + "Test Page", + &[], + &PathBuf::from("docs/test.html"), + None, + ) + .expect("Failed to render page") +} + +#[test] +fn test_mermaid_loader_uses_default_cdn() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let html = render_with_mermaid(None, temp_dir.path()); + + assert!( + html.contains(&format!( + "" + )), + "Loader should point to the default Mermaid script" + ); +} + +#[test] +fn test_mermaid_loader_links_to_copied_script() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let html = render_with_mermaid( + Some("/nix/store/abc-mermaid/mermaid.min.js"), + temp_dir.path(), + ); + + assert!( + html.contains("data-mermaid-src=\"../assets/mermaid.min.js\""), + "Loader should point to the copied Mermaid script" + ); + assert!( + !html.contains("/nix/store/abc-mermaid"), + "Loader should not point to the source path of the script" + ); +} + +#[test] +fn test_mermaid_loader_absent_when_disabled() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let config = ndg_config::Config { + mermaid: Some(MermaidConfig { + enable: false, + script: None, + }), + ..test_config(temp_dir.path()) + }; + + let html = render( + &config, + "

Test content

", + "Test Page", + &[], + &PathBuf::from("docs/test.html"), + None, + ) + .expect("Failed to render page"); + + assert!( + !html.contains("mermaid-init.js"), + "Loader should not be added when Mermaid is disabled" + ); +} diff --git a/crates/ndg-templates/src/lib.rs b/crates/ndg-templates/src/lib.rs index deca59685..b41c98c1c 100644 --- a/crates/ndg-templates/src/lib.rs +++ b/crates/ndg-templates/src/lib.rs @@ -23,6 +23,7 @@ pub const SEARCH_JS: &str = include_str!("../templates/search.js"); pub const SEARCH_WORKER_JS: &str = include_str!("../templates/search-worker.js"); pub const MAIN_JS: &str = include_str!("../templates/main.js"); +pub const MERMAID_INIT_JS: &str = include_str!("../templates/mermaid-init.js"); #[must_use] pub fn all_templates() -> HashMap<&'static str, &'static str> { @@ -42,6 +43,7 @@ pub fn all_templates() -> HashMap<&'static str, &'static str> { templates.insert("search.js", SEARCH_JS); templates.insert("search-worker.js", SEARCH_WORKER_JS); templates.insert("main.js", MAIN_JS); + templates.insert("mermaid-init.js", MERMAID_INIT_JS); templates }) .clone() diff --git a/crates/ndg-templates/templates/mermaid-init.js b/crates/ndg-templates/templates/mermaid-init.js new file mode 100644 index 000000000..9d9b11323 --- /dev/null +++ b/crates/ndg-templates/templates/mermaid-init.js @@ -0,0 +1,56 @@ +/* + * Draws Mermaid diagrams. When `mermaid.enable` is set, ndg renders each + * `mermaid` fenced code block as a `
` element and adds
+ * this script to each page. The `data-mermaid-src` attribute of the script
+ * element gives the location of the Mermaid library.
+ *
+ * The script loads the library only when the page has a diagram, because the
+ * library is large. It also draws the diagrams that the page adds later, for
+ * example when the options page loads more options.
+ */
+(() => {
+  const script = document.currentScript;
+  // Resolve the location now: client-side navigation can change the page URL.
+  const source = new URL(script.dataset.mermaidSrc, document.baseURI).href;
+  const selector = "pre.mermaid:not([data-processed])";
+  let library;
+  let queue = Promise.resolve();
+
+  function loadLibrary() {
+    library ??= new Promise((resolve, reject) => {
+      const element = document.createElement("script");
+      element.src = source;
+      element.addEventListener("load", () => {
+        const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+        globalThis.mermaid.initialize({
+          startOnLoad: false,
+          securityLevel: "strict",
+          theme: dark ? "dark" : "default",
+        });
+        resolve(globalThis.mermaid);
+      });
+      element.addEventListener("error", () =>
+        reject(new Error(`Failed to load Mermaid from ${source}`)),
+      );
+      document.head.append(element);
+    });
+    return library;
+  }
+
+  function draw() {
+    if (!document.querySelector(selector)) return;
+    // Draw one batch at a time, so that no diagram is drawn twice.
+    queue = queue
+      .then(async () => {
+        const mermaid = await loadLibrary();
+        await mermaid.run({ querySelector: selector });
+      })
+      .catch((error) => console.error(error));
+  }
+
+  draw();
+  new MutationObserver(draw).observe(document.body, {
+    childList: true,
+    subtree: true,
+  });
+})();
diff --git a/crates/ndg-templates/templates/mermaid-init.test.js b/crates/ndg-templates/templates/mermaid-init.test.js
new file mode 100644
index 000000000..a98084d71
--- /dev/null
+++ b/crates/ndg-templates/templates/mermaid-init.test.js
@@ -0,0 +1,86 @@
+import source from "./mermaid-init.js" with { type: "text" };
+
+function assert(condition, message) {
+  if (!condition) throw new Error(message);
+}
+
+function loadMermaidInit({ diagrams, dark = false }) {
+  const appended = [];
+  const runs = [];
+  const initialized = [];
+  let observe;
+  const mermaid = {
+    initialize(config) {
+      initialized.push(config);
+    },
+    run({ querySelector }) {
+      runs.push(querySelector);
+      diagrams.processed = true;
+      return Promise.resolve();
+    },
+  };
+  const globals = {};
+  const document = {
+    baseURI: "https://example.com/docs/page.html",
+    currentScript: { dataset: { mermaidSrc: "../assets/mermaid.min.js" } },
+    body: {},
+    head: {
+      append(element) {
+        appended.push(element);
+        globals.mermaid = mermaid;
+        element.listeners.load();
+      },
+    },
+    createElement: () => ({
+      listeners: {},
+      addEventListener(event, listener) {
+        this.listeners[event] = listener;
+      },
+    }),
+    querySelector: () =>
+      diagrams.count > 0 && !diagrams.processed ? {} : null,
+  };
+  const window = { matchMedia: () => ({ matches: dark }) };
+  class MutationObserver {
+    constructor(callback) {
+      observe = callback;
+    }
+    observe() {}
+  }
+  new Function(
+    "document",
+    "window",
+    "MutationObserver",
+    "globalThis",
+    source,
+  )(document, window, MutationObserver, globals);
+  return { appended, runs, initialized, mutate: () => observe() };
+}
+
+const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
+
+Deno.test("a page without diagrams does not load Mermaid", async () => {
+  const page = loadMermaidInit({ diagrams: { count: 0 } });
+  await settle();
+  assert(page.appended.length === 0, "the library is not loaded");
+});
+
+Deno.test("a page with diagrams loads Mermaid once and draws", async () => {
+  const diagrams = { count: 1 };
+  const page = loadMermaidInit({ diagrams, dark: true });
+  await settle();
+  assert(page.appended.length === 1, "the library is loaded once");
+  assert(
+    page.appended[0].src === "https://example.com/assets/mermaid.min.js",
+    "the library location is resolved against the page",
+  );
+  assert(page.initialized[0].theme === "dark", "the dark theme is selected");
+  assert(page.initialized[0].startOnLoad === false, "Mermaid does not start");
+  assert(page.runs.length === 1, "the diagrams are drawn");
+
+  diagrams.processed = false;
+  page.mutate();
+  await settle();
+  assert(page.appended.length === 1, "the library is not loaded again");
+  assert(page.runs.length === 2, "new diagrams are drawn");
+});
diff --git a/crates/ndg-utils/src/assets.rs b/crates/ndg-utils/src/assets.rs
index 7659bf974..6bb75a51d 100644
--- a/crates/ndg-utils/src/assets.rs
+++ b/crates/ndg-utils/src/assets.rs
@@ -2,7 +2,7 @@ use std::{fs, path::Path};
 
 use color_eyre::eyre::{self, Context, Result};
 use log::debug;
-use ndg_config::Config;
+use ndg_config::{Config, mermaid::MermaidScript};
 use ndg_templates as templates;
 use walkdir::WalkDir;
 
@@ -13,6 +13,7 @@ const SEARCH_CORE_JS: &str = templates::SEARCH_CORE_JS;
 const SEARCH_JS: &str = templates::SEARCH_JS;
 const SEARCH_WORKER_JS: &str = templates::SEARCH_WORKER_JS;
 const MAIN_JS: &str = templates::MAIN_JS;
+const MERMAID_INIT_JS: &str = templates::MERMAID_INIT_JS;
 
 /// Copies all required assets (CSS, JS, custom assets, scripts) to the output
 /// directory.
@@ -20,6 +21,7 @@ const MAIN_JS: &str = templates::MAIN_JS;
 /// This includes:
 /// - The main stylesheet (default or template/custom)
 /// - Main JavaScript files (main.js, search.js only if enabled)
+/// - The Mermaid loader and a local Mermaid library, if Mermaid is enabled
 /// - Any custom assets from the configured assets directory
 /// - Any custom script files specified in the configuration
 /// - The favicon file if configured
@@ -72,6 +74,8 @@ pub fn copy_assets(config: &Config) -> Result<()> {
   // Copy script files to assets directory
   copy_script_files(config, &assets_dir)?;
 
+  copy_mermaid_assets(config, &assets_dir)?;
+
   // Create search.js for search functionality
   if config.is_search_enabled() {
     copy_template_asset(config, &assets_dir, "search-core.js", SEARCH_CORE_JS)?;
@@ -96,6 +100,38 @@ pub fn copy_assets(config: &Config) -> Result<()> {
   Ok(())
 }
 
+/// Copies the Mermaid loader and, if the Mermaid script is a local file, the
+/// Mermaid library to the assets directory.
+///
+/// The library is copied as is, because it is already minified.
+///
+/// # Errors
+///
+/// Returns an error if an asset cannot be read or written.
+fn copy_mermaid_assets(config: &Config, assets_dir: &Path) -> Result<()> {
+  let Some(mermaid) = config.enabled_mermaid() else {
+    return Ok(());
+  };
+
+  copy_template_asset(config, assets_dir, "mermaid-init.js", MERMAID_INIT_JS)?;
+
+  if let MermaidScript::File(path) = mermaid.script_source() {
+    let file_name = path.file_name().ok_or_else(|| {
+      eyre::eyre!("Mermaid script has no file name: {}", path.display())
+    })?;
+    let dest_path = assets_dir.join(file_name);
+    fs::copy(path, &dest_path).wrap_err_with(|| {
+      format!(
+        "Failed to copy Mermaid script from {} to {}",
+        path.display(),
+        dest_path.display()
+      )
+    })?;
+  }
+
+  Ok(())
+}
+
 /// Copies a template asset to the assets directory, allowing user override if
 /// present.
 ///
diff --git a/crates/ndg-utils/src/markdown.rs b/crates/ndg-utils/src/markdown.rs
index b71b7ebc9..fbd86410e 100644
--- a/crates/ndg-utils/src/markdown.rs
+++ b/crates/ndg-utils/src/markdown.rs
@@ -662,6 +662,7 @@ pub fn create_processor(
   let mut builder = MarkdownOptionsBuilder::new()
     .gfm(true)
     .highlight_code(config.highlight_code)
+    .mermaid(config.is_mermaid_enabled())
     .extensions(
       config
         .markdown
diff --git a/ndg/tests/integration.rs b/ndg/tests/integration.rs
index 2d61f04ba..ea17057dc 100644
--- a/ndg/tests/integration.rs
+++ b/ndg/tests/integration.rs
@@ -2,7 +2,7 @@
 use std::{fs, process::Command};
 
 use ndg::{
-  config::{Config, postprocess::PostprocessConfig},
+  config::{Config, mermaid::MermaidConfig, postprocess::PostprocessConfig},
   utils::assets::copy_assets,
 };
 use ndg_commonmark::{MarkdownExtension, MarkdownOptions, MarkdownProcessor};
@@ -1265,3 +1265,58 @@ fn test_readme_as_homepage_disabled() {
      disabled"
   );
 }
+
+#[test]
+fn test_mermaid_assets_copied_when_enabled() {
+  let temp_dir = tempdir().expect("Failed to create temp dir in test");
+  let output_dir = temp_dir.path().join("output");
+  let library = temp_dir.path().join("mermaid.min.js");
+  let library_content = "globalThis.mermaid = {};";
+  fs::write(&library, library_content)
+    .expect("Failed to write mermaid.min.js in test");
+
+  let config = Config {
+    output_dir: output_dir.clone(),
+    mermaid: Some(MermaidConfig {
+      enable: true,
+      script: Some(library.to_string_lossy().into_owned()),
+    }),
+    postprocess: Some(PostprocessConfig {
+      minify_js: true,
+      ..Default::default()
+    }),
+    ..Default::default()
+  };
+
+  copy_assets(&config).expect("Failed to copy assets in test");
+
+  let assets_dir = output_dir.join("assets");
+  assert!(
+    assets_dir.join("mermaid-init.js").is_file(),
+    "Mermaid loader should be copied"
+  );
+  assert_eq!(
+    fs::read_to_string(assets_dir.join("mermaid.min.js"))
+      .expect("Failed to read copied Mermaid script in test"),
+    library_content,
+    "Mermaid script should be copied without postprocessing"
+  );
+}
+
+#[test]
+fn test_mermaid_assets_absent_when_disabled() {
+  let temp_dir = tempdir().expect("Failed to create temp dir in test");
+  let output_dir = temp_dir.path().join("output");
+
+  let config = Config {
+    output_dir: output_dir.clone(),
+    ..Default::default()
+  };
+
+  copy_assets(&config).expect("Failed to copy assets in test");
+
+  assert!(
+    !output_dir.join("assets").join("mermaid-init.js").exists(),
+    "Mermaid loader should not be copied when Mermaid is disabled"
+  );
+}

From 80181852fdb23dd9174420ad2c6a551682783e06 Mon Sep 17 00:00:00 2001
From: cinereal 
Date: Sun, 4 Oct 2026 13:12:28 +0200
Subject: [PATCH 4/6] nix: add a `mermaid` option to `ndg-builder`

The option enables Mermaid with the library from `mdbook-mermaid`, so that the site does not need a CDN.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
---
 nix/internal/checks.nix              | 21 +++++++++++++++++++++
 nix/packages/ndg-builder/package.nix | 10 ++++++++++
 2 files changed, 31 insertions(+)

diff --git a/nix/internal/checks.nix b/nix/internal/checks.nix
index bd70d7010..13c94aaa6 100644
--- a/nix/internal/checks.nix
+++ b/nix/internal/checks.nix
@@ -31,6 +31,27 @@ in {
     };
   };
 
+  mermaid = let
+    site = ndg-builder.override {
+      mermaid = true;
+      inputDir = pkgs.writeTextDir "diagram.md" ''
+        # Diagram
+
+        ```mermaid
+        flowchart LR
+          a --> b
+        ```
+      '';
+    };
+  in
+    pkgs.runCommandLocal "ndg-mermaid" {} ''
+      grep -F '
flowchart LR' ${site}/diagram.html
+      grep -F 'data-mermaid-src="assets/mermaid.min.js"' ${site}/diagram.html
+      test -s ${site}/assets/mermaid.min.js
+      test -s ${site}/assets/mermaid-init.js
+      touch $out
+    '';
+
   nrd-options-commonmark = pkgs.testers.runNixOSTest {
     name = "ndg-nrd-options-commonmark";
 
diff --git a/nix/packages/ndg-builder/package.nix b/nix/packages/ndg-builder/package.nix
index 967dd3c1d..45ee6487b 100644
--- a/nix/packages/ndg-builder/package.nix
+++ b/nix/packages/ndg-builder/package.nix
@@ -3,6 +3,7 @@
   lib,
   # Build Dependencies
   ndg,
+  mdbook-mermaid,
   runCommandLocal,
   nixosOptionsDoc,
   writers,
@@ -106,6 +107,9 @@
   optionsDepth ? 2,
   generateSearch ? true,
   highlightCode ? true,
+  # Render `mermaid` code blocks as diagrams, with the Mermaid library from
+  # `mdbook-mermaid`, so that the site does not need a CDN.
+  mermaid ? false,
   extraConfig ? {},
   # ZIM archive generation
   buildZim ? false,
@@ -155,6 +159,12 @@ in
       (optionalAttrs (manpageUrls != null) {manpage_urls_path = manpageUrls;})
       (optionalAttrs (stylesheets != []) {stylesheet_paths = stylesheets;})
       (optionalAttrs (scripts != []) {script_paths = scripts;})
+      (optionalAttrs mermaid {
+        mermaid = {
+          enable = true;
+          script = "${mdbook-mermaid.src}/src/bin/assets/mermaid.min.js";
+        };
+      })
       (optionalAttrs (extraConfig != {}) extraConfig)
     ]);
   in

From 16979ab109e0aac7818e4b4641fa73ca13c54950 Mon Sep 17 00:00:00 2001
From: cinereal 
Date: Sun, 4 Oct 2026 13:12:28 +0200
Subject: [PATCH 5/6] docs: document Mermaid diagrams

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
---
 CHANGELOG.md  |  8 ++++++++
 ndg/README.md | 39 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index dfa5d9134..ec5b0d797 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,14 @@ changes.
   `sidebar.group_by_dir` report errors
   ([#290](https://github.com/feel-co/ndg/issues/290)).
 
+- Mermaid diagrams. With `mermaid.enable = true`, NDG renders `mermaid` code
+  blocks as `
` elements and adds `assets/mermaid-init.js`
+  to each page. The script loads the Mermaid library only on pages with a
+  diagram. `mermaid.script` sets the library as a URL or a local file, which NDG
+  copies to `assets/`. It defaults to Mermaid 11 from jsDelivr. The
+  `ndg-builder` Nix package has a `mermaid` option that uses the library from
+  `mdbook-mermaid`.
+
 ### Fixed
 
 - Pages now link each script in `script_paths` at `assets/`, where
diff --git a/ndg/README.md b/ndg/README.md
index 3e833969f..d7f8a67ca 100644
--- a/ndg/README.md
+++ b/ndg/README.md
@@ -489,6 +489,45 @@ download KaTeX's prebuilt distribution and serve its JavaScript, stylesheet, and
 `fonts/` directory from `assets/` instead. See the [KaTeX browser setup] and
 [rendering API] documentation.
 
+### Rendering Mermaid diagrams
+
+NDG can render [Mermaid] diagrams from fenced code blocks with the `mermaid`
+language:
+
+````markdown
+```mermaid
+flowchart LR
+  config["ndg.toml"] --> ndg --> html["HTML"]
+```
+````
+
+Enable this in `ndg.toml`:
+
+```toml
+[mermaid]
+enable = true
+```
+
+NDG then renders each `mermaid` code block as a `
` element
+without syntax highlighting, and adds `assets/mermaid-init.js` to each page. The
+script loads the Mermaid library only on pages that have a diagram, and selects
+the dark Mermaid theme when the browser prefers a dark color scheme.
+
+By default, the library is Mermaid 11 from jsDelivr. To use a different version
+or to serve the library from your site, set `script` to a URL or a local path:
+
+```toml
+[mermaid]
+enable = true
+script = "vendor/mermaid.min.js"
+```
+
+NDG copies a local file to the generated `assets/` directory. The Nix builder
+`ndg-builder` has a `mermaid` option, which uses the library from the
+`mdbook-mermaid` package, so that the site does not need a CDN.
+
+[Mermaid]: https://mermaid.js.org
+
 #### Advanced Configuration Details
 
 **Parallel Processing (`jobs`):**

From 3d5097dc61450f6bde0397da7dccff2e1ea1b5ec Mon Sep 17 00:00:00 2001
From: cinereal 
Date: Sun, 4 Oct 2026 15:02:37 +0200
Subject: [PATCH 6/6] ndg: draw Mermaid diagrams again when the theme changes

The script selects the Mermaid theme from `data-theme` on ``, or from the system theme. On `ndg:themechange`, it restores the source of each diagram and draws it again.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
---
 .../ndg-templates/templates/mermaid-init.js   | 48 +++++++++++--
 .../templates/mermaid-init.test.js            | 68 ++++++++++++++++++-
 2 files changed, 107 insertions(+), 9 deletions(-)

diff --git a/crates/ndg-templates/templates/mermaid-init.js b/crates/ndg-templates/templates/mermaid-init.js
index 9d9b11323..e382245cf 100644
--- a/crates/ndg-templates/templates/mermaid-init.js
+++ b/crates/ndg-templates/templates/mermaid-init.js
@@ -7,26 +7,39 @@
  * The script loads the library only when the page has a diagram, because the
  * library is large. It also draws the diagrams that the page adds later, for
  * example when the options page loads more options.
+ *
+ * The diagrams use the theme of ``, or the color scheme of the
+ * system when the page has no `data-theme`. On an `ndg:themechange` event, the
+ * script draws the diagrams again in the new theme.
  */
 (() => {
   const script = document.currentScript;
   // Resolve the location now: client-side navigation can change the page URL.
   const source = new URL(script.dataset.mermaidSrc, document.baseURI).href;
   const selector = "pre.mermaid:not([data-processed])";
+  // The text of each diagram, because Mermaid replaces it with the drawing.
+  const sources = new WeakMap();
   let library;
   let queue = Promise.resolve();
 
+  function initialize(mermaid) {
+    const theme = document.documentElement.dataset.theme;
+    const dark = theme
+      ? theme === "dark"
+      : window.matchMedia("(prefers-color-scheme: dark)").matches;
+    mermaid.initialize({
+      startOnLoad: false,
+      securityLevel: "strict",
+      theme: dark ? "dark" : "default",
+    });
+  }
+
   function loadLibrary() {
     library ??= new Promise((resolve, reject) => {
       const element = document.createElement("script");
       element.src = source;
       element.addEventListener("load", () => {
-        const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
-        globalThis.mermaid.initialize({
-          startOnLoad: false,
-          securityLevel: "strict",
-          theme: dark ? "dark" : "default",
-        });
+        initialize(globalThis.mermaid);
         resolve(globalThis.mermaid);
       });
       element.addEventListener("error", () =>
@@ -43,12 +56,35 @@
     queue = queue
       .then(async () => {
         const mermaid = await loadLibrary();
+        for (const element of document.querySelectorAll(selector)) {
+          if (!sources.has(element)) sources.set(element, element.textContent);
+        }
+        await mermaid.run({ querySelector: selector });
+      })
+      .catch((error) => console.error(error));
+  }
+
+  function redraw() {
+    // Without the library, no diagram is drawn yet.
+    if (!library) return;
+    queue = queue
+      .then(async () => {
+        const mermaid = await library;
+        initialize(mermaid);
+        for (const element of document.querySelectorAll(
+          "pre.mermaid[data-processed]",
+        )) {
+          if (!sources.has(element)) continue;
+          element.textContent = sources.get(element);
+          element.removeAttribute("data-processed");
+        }
         await mermaid.run({ querySelector: selector });
       })
       .catch((error) => console.error(error));
   }
 
   draw();
+  document.addEventListener("ndg:themechange", redraw);
   new MutationObserver(draw).observe(document.body, {
     childList: true,
     subtree: true,
diff --git a/crates/ndg-templates/templates/mermaid-init.test.js b/crates/ndg-templates/templates/mermaid-init.test.js
index a98084d71..926e5ee1b 100644
--- a/crates/ndg-templates/templates/mermaid-init.test.js
+++ b/crates/ndg-templates/templates/mermaid-init.test.js
@@ -4,26 +4,43 @@ function assert(condition, message) {
   if (!condition) throw new Error(message);
 }
 
-function loadMermaidInit({ diagrams, dark = false }) {
+function loadMermaidInit({ diagrams, dark = false, theme }) {
   const appended = [];
   const runs = [];
   const initialized = [];
+  const listeners = {};
   let observe;
   const mermaid = {
     initialize(config) {
       initialized.push(config);
     },
     run({ querySelector }) {
-      runs.push(querySelector);
+      runs.push({ querySelector, text: diagrams.text });
+      diagrams.text = "";
       diagrams.processed = true;
       return Promise.resolve();
     },
   };
   const globals = {};
+  const element = {
+    get textContent() {
+      return diagrams.text;
+    },
+    set textContent(text) {
+      diagrams.text = text;
+    },
+    removeAttribute(name) {
+      if (name === "data-processed") diagrams.processed = false;
+    },
+  };
   const document = {
     baseURI: "https://example.com/docs/page.html",
     currentScript: { dataset: { mermaidSrc: "../assets/mermaid.min.js" } },
+    documentElement: { dataset: theme ? { theme } : {} },
     body: {},
+    addEventListener(event, listener) {
+      listeners[event] = listener;
+    },
     head: {
       append(element) {
         appended.push(element);
@@ -39,6 +56,12 @@ function loadMermaidInit({ diagrams, dark = false }) {
     }),
     querySelector: () =>
       diagrams.count > 0 && !diagrams.processed ? {} : null,
+    // One element stands for all the diagrams.
+    querySelectorAll(selector) {
+      if (diagrams.count === 0) return [];
+      if (diagrams.processed === selector.includes(":not(")) return [];
+      return [element];
+    },
   };
   const window = { matchMedia: () => ({ matches: dark }) };
   class MutationObserver {
@@ -54,7 +77,16 @@ function loadMermaidInit({ diagrams, dark = false }) {
     "globalThis",
     source,
   )(document, window, MutationObserver, globals);
-  return { appended, runs, initialized, mutate: () => observe() };
+  return {
+    appended,
+    runs,
+    initialized,
+    mutate: () => observe(),
+    changeTheme(theme) {
+      document.documentElement.dataset.theme = theme;
+      listeners["ndg:themechange"]();
+    },
+  };
 }
 
 const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -79,8 +111,38 @@ Deno.test("a page with diagrams loads Mermaid once and draws", async () => {
   assert(page.runs.length === 1, "the diagrams are drawn");
 
   diagrams.processed = false;
+  diagrams.text = "graph TD; C-->D";
   page.mutate();
   await settle();
   assert(page.appended.length === 1, "the library is not loaded again");
   assert(page.runs.length === 2, "new diagrams are drawn");
 });
+
+Deno.test("a selected theme overrides the system", async () => {
+  const page = loadMermaidInit({ diagrams: { count: 1 }, theme: "light" });
+  await settle();
+  assert(page.initialized[0].theme === "default", "the light theme is used");
+});
+
+Deno.test("a theme change draws the diagrams again", async () => {
+  const diagrams = { count: 1, text: "graph TD; A-->B" };
+  const page = loadMermaidInit({ diagrams });
+  await settle();
+  assert(page.initialized[0].theme === "default", "the light theme is used");
+
+  page.changeTheme("dark");
+  await settle();
+  assert(page.initialized[1].theme === "dark", "the dark theme is selected");
+  assert(page.runs.length === 2, "the diagrams are drawn again");
+  assert(
+    page.runs[1].text === "graph TD; A-->B",
+    "the diagrams are drawn from their text, not the old drawing",
+  );
+});
+
+Deno.test("a theme change before the first drawing does nothing", async () => {
+  const page = loadMermaidInit({ diagrams: { count: 0 } });
+  page.changeTheme("dark");
+  await settle();
+  assert(page.appended.length === 0, "the library is not loaded");
+});