Skip to content

Stage 3: Mermaid Rendering

obsidian_export.pipeline.stage3_mermaid

Stage 3: Extract and render Mermaid diagrams to PNG via mmdc.

render_mermaid_blocks

render_mermaid_blocks(body: str, config: MermaidConfig, tmpdir: Path) -> str

Replace ```mermaid blocks with rendered PNG image references.

Raises FileNotFoundError if mmdc binary is not found. Raises subprocess.CalledProcessError if mmdc rendering fails.

Source code in obsidian_export/pipeline/stage3_mermaid.py
def render_mermaid_blocks(body: str, config: MermaidConfig, tmpdir: Path) -> str:
    """Replace ```mermaid blocks with rendered PNG image references.

    Raises FileNotFoundError if mmdc binary is not found.
    Raises subprocess.CalledProcessError if mmdc rendering fails.
    """
    mmdc = config.mmdc_bin

    counter = count(1)

    def replace_block(m: re.Match) -> str:
        """Replace a fenced mermaid code block with a rendered PNG image reference.

        Receives a match whose group(1) is the mermaid diagram source. Writes the
        source to a temporary file, invokes mmdc to render it as PNG, and returns
        a markdown image reference to the output file.
        """
        if not mmdc.exists():
            raise FileNotFoundError(
                f"mmdc binary not found at {mmdc}. Install: npm install --prefix .mmdc @mermaid-js/mermaid-cli"
            )
        diagram_src = m.group(1)
        idx = next(counter)

        src_file = tmpdir / f"diagram_{idx}.mmd"
        out_file = tmpdir / f"diagram_{idx}.png"
        src_file.write_text(diagram_src, encoding="utf-8")

        cmd = [
            str(mmdc),
            "--input",
            str(src_file),
            "--output",
            str(out_file),
            "--scale",
            str(config.scale),
            "--backgroundColor",
            "transparent",
        ]
        if config.puppeteer_config and config.puppeteer_config.exists():
            cmd.extend(["--puppeteerConfigFile", str(config.puppeteer_config)])

        try:
            subprocess.run(cmd, check=True, capture_output=True)
        except subprocess.CalledProcessError as exc:
            stderr = exc.stderr.decode(errors="replace") if exc.stderr else "(no stderr)"
            raise MermaidRenderError(f"mmdc failed (exit {exc.returncode}) rendering diagram {idx}:\n{stderr}") from exc

        return f"![Diagram {idx}]({out_file})"

    return _MERMAID_BLOCK_RE.sub(replace_block, body)