Skip to content

Conversation

@rowanc1
Copy link
Member

@rowanc1 rowanc1 commented Jun 27, 2025

Fixes #2006

Requires that you have mmdc installed, the CLI warns you and I have added some docs. Works with typst, tex, docx and jats.

Image

image

Replaces #1452

@rowanc1 rowanc1 added the enhancement New feature or request label Jun 27, 2025
@changeset-bot
Copy link

changeset-bot bot commented Jun 27, 2025

🦋 Changeset detected

Latest commit: 916724a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
myst-common Patch
myst-cli Patch
myst-spec-ext Patch
myst-config Patch
myst-frontmatter Patch
mystmd Patch
myst-migrate Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rowanc1 rowanc1 changed the title 🧜‍♀️ Create images of mermaid diagrams for static exports. 🧜‍♀️ Create images of mermaid diagrams for static exports Jun 27, 2025
Comment on lines +7 to +8
describe('Download Template', { timeout: 15000 }, () => {
it('Download default template', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just putting the timeout as the second arg.

Comment on lines +45 to +51
```bash
# Automatic detection (recommended for CI)
CI=true npm run build

# Manual control
MERMAID_NO_SANDBOX=true npm run build
```
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI was way harder to get going than I thought. Should be automatic now though!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@rowanc1 rowanc1 requested a review from fwkoch June 27, 2025 05:10
@rowanc1 rowanc1 mentioned this pull request Jun 27, 2025
Copy link
Member

@fwkoch fwkoch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this out - it works very smoothly! First I did not have mmdc installed - my diagram rendered with myst start but the PDF export reported an error with install instructions. Then I installed mmdc and tried the export again and the diagram conversion worked:
image

My only requested change is removing @mermaid-js/mermaid-cli from our dependencies; to me, it feels heavy and unnecessary - it's easy for users to install the CLI separately.

Other than that - there's a few high-level aspects that gave me pause (new transforms API for processMdast functions; different way of finalizing images for "static" output outside of finalizeMdast function). I made some comments and am happy to discuss further, but I don't think those discussions need to block landing this feature.

},
"dependencies": {
"@jupyterlab/services": "^7.0.0",
"@mermaid-js/mermaid-cli": "^10.9.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this dependency is only here to install mmdc CLI, not ever imported in the codebase. Is that correct?

If so, I think we should remove it as a dependency for all users. We already have installation instructions in the error message when mermaid conversion fails.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are dependencies determined for the overall mystmd package? Presumably, you could have mmdc as a runtime dependency?

imageExtensions?: ImageExtensions[];
watchMode?: boolean;
execute?: boolean;
transforms?: TransformSpec[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This small change is actually a pretty substantial change to the API - It's a completely new way to specify transforms within the code. I think it is non-controversial and for-the-better: matching how external plugins are defined and executed is great.

But worth noting this alone is a new feature, entirely independent of the mermaid stuff.

projectPath,
imageExtensions: DOCX_IMAGE_EXTENSIONS,
extraLinkTransformers,
transforms: [createMermaidImageMystPlugin],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injecting this as transforms for each export type is interesting.

The current pattern for "we need to transform something so it works in a static export" is put it in finalizeMdast under the flag simplifyFigures: true, e.g. https://github.com/jupyter-book/mystmd/blob/main/packages/myst-cli/src/process/mdast.ts#L441

Using a transform instead is maybe (probably...?) better: the interface is cleaner and follows existing plugin work, and we are not just dumping more random stuff in the finalizeMdast function. However, this now splits transforms with similar purpose ("simplify images for print") across 2 ways of doing things.

Probably fine to leave as-is, and maybe start moving the finalizeMdast steps to transforms (although that's not trivial - many of those steps need to happen as the very last thing during processing and right now transforms are only run at specific, earlier points). (Also... that begs the question - should the mermaid transform be run later during processing? Probably not...? But there could be edge cases where a mermaid transform is added later and never converted to an image.)

Copy link
Collaborator

@stefanv stefanv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is neat, @rowanc1! 🚀 Also a super helpful example for me to learn from 🙏.

My comments are just that, feel free to disregard. Also curious to learn the answer to Franklin's question.


:::{note .dropdown} For CI Environments

The Mermaid CLI uses Puppeteer which may require special configuration in CI environments. MyST automatically handles this by detecting the `CI` environment variable or you can manually control it:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is being handled here?

The example is a bit confusing, because CI is already set on both GH and GitLab, so user would never set it themselves.

Comment on lines +45 to +51
```bash
# Automatic detection (recommended for CI)
CI=true npm run build

# Manual control
MERMAID_NO_SANDBOX=true npm run build
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

MERMAID_NO_SANDBOX=true npm run build
```

This automatically creates a Puppeteer configuration file with `--no-sandbox` flag to resolve sandbox issues in Linux CI environments.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this answers part of my question from above. Does generating the config equate to disabling the sandbox? If so, maybe something like:

By default, Mermaid runs Puppeteer in a sandbox. However, in CI it needs to run directly in the current session. When the CI environment variable is set (which is the case for GitHub and GitLab), then myst will disable the sandbox by generating a suitable Puppeteer configuration file. You can also control this behavior explicitly with `MERMAID_DISABLE_SANDBOX=true/false`.

X_NO_Y namings don't read correctly in my head, but could be just me. Should this be PUPPETEER_DISABLE_SANDBOX or MERMAID_DISABLE_SANDBOX?


expect(result.type).toBe('image');
if (result.type === 'image') {
expect(result.url).toMatch(/^data:image\/svg\+xml;base64,/);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

},
"dependencies": {
"@jupyterlab/services": "^7.0.0",
"@mermaid-js/mermaid-cli": "^10.9.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are dependencies determined for the overall mystmd package? Presumably, you could have mmdc as a runtime dependency?

const outputFile = path.join(tempFolder, `mermaid-output-${hash}.${format}`);

if (!isMermaidCliAvailable()) {
throw new Error('Mermaid CLI is not available');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new Error('Mermaid CLI is not available');
throw new Error('Mermaid CLI is not available: try installing using `npm install -g @mermaid-js/mermaid-cli`');

If not this, then point users to the docs page where it is discussed?

try {
// Create puppeteer config file for CI environments
// See https://github.com/mermaid-js/mermaid-cli/blob/master/docs/linux-sandbox-issue.md
const useNoSandbox = process.env.MERMAID_NO_SANDBOX === 'true' || process.env.CI === 'true';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the docs, it says the user gets to control the behavior. It sounded like "regardless of whether in CI or not". So, what happens when MERMAID_NO_SANDBOX === 'false' and CI === true? Maybe the condition should be:

const useNoSandbox =
  process.env.MERMAID_NO_SANDBOX !== undefined
    ? process.env.MERMAID_NO_SANDBOX === 'true'
    : process.env.CI === 'true';


// Render using Mermaid CLI with stdin input
await execAsync(
`echo '${mermaidCode.replace(/'/g, "'\"'\"'")}' | mmdc -i - -o "${outputFile}" -t ${theme} -b transparent${puppeteerConfigFlag}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes, this shell escaping is hard to parse.

Perhaps we need a utility function that promisifies spawn and accepts standard input, a-la https://stackoverflow.com/a/72863312?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, is the escaping of -o sufficient for strange filenames?

): Promise<string> {
// Create unique temporary output file name
const hash = crypto.createHash('md5').update(mermaidCode).digest('hex');
const tempFolder = createTempFolder();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have these clean up after themselves, like in Python. Using something like https://github.com/Mcsavvy/contextlib or a custom implementation:

withTempDir(callback)

fileError(file, message, {
ruleId: RuleId.imageFormatConverts,
node,
note: 'To install the Mermaid CLI, run `npm install -g @mermaid-js/mermaid-cli`',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct error here? We already throw above if CLI not found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support export mermaid diagrams to docx as images.

4 participants