Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-pigs-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'myst-common': patch
---

Added `options?: PluginOptions` to `TransformSpec` type. Enables passing configuration options to transform plugins.
8 changes: 8 additions & 0 deletions .changeset/orange-rockets-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'myst-cli': patch
---

Integrated transform loading from options with proper plugin utilities.

Added mermaid transform import and configuration
Integrated mermaid transform into Typst build pipeline.
5 changes: 5 additions & 0 deletions .changeset/stale-ghosts-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'myst-spec-ext': patch
---

Added `label`, `identifier`, and `html_id` to `Image` type
29 changes: 29 additions & 0 deletions docs/diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,32 @@ flowchart LR
:::{note}
Both GitHub and JupyterLab ([#101](https://github.com/jupyter/enhancement-proposals/pull/101)) support the translation of a code-block ` ```mermaid ` to a mermaid diagram directly, this can also be used by default in MyST.
:::

## Rendering for Static Exports

MyST supports static rendering of Mermaid diagrams for static export formats (PDF, Word, LaTeX, Typst). This feature converts Mermaid syntax to base64-encoded SVG or PNG images during the build process, ensuring consistent rendering across all static output formats.

:::{important} Prerequisites
To use static Mermaid rendering, you need the Mermaid CLI installed:

```bash
npm install -g @mermaid-js/mermaid-cli
```

:::

:::{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.


```bash
# Automatic detection (recommended for CI)
CI=true npm run build

# Manual control
MERMAID_NO_SANDBOX=true npm run build
```
Comment on lines +45 to +51
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!


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?


:::
98 changes: 47 additions & 51 deletions packages/jtex/tests/download.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,53 @@ import { TemplateKind } from 'myst-common';
import MystTemplate, { downloadTemplate, resolveInputs, Session } from 'myst-templates';
import { renderTemplate } from '../src';

describe(
'Download Template',
() => {
it('Download default template', async () => {
const session = new Session();
const inputs = resolveInputs(session, { buildDir: '_build', kind: TemplateKind.tex });
await downloadTemplate(session, {
templatePath: inputs.templatePath,
templateUrl: inputs.templateUrl as string,
});
expect(fs.existsSync('_build/templates/tex/myst/plain_latex/template.zip')).toBe(true);
expect(fs.existsSync('_build/templates/tex/myst/plain_latex/template.yml')).toBe(true);
expect(fs.existsSync('_build/templates/tex/myst/plain_latex/template.tex')).toBe(true);
describe('Download Template', { timeout: 15000 }, () => {
it('Download default template', async () => {
Comment on lines +7 to +8
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.

const session = new Session();
const inputs = resolveInputs(session, { buildDir: '_build', kind: TemplateKind.tex });
await downloadTemplate(session, {
templatePath: inputs.templatePath,
templateUrl: inputs.templateUrl as string,
});
it('Bad template paths to throw', async () => {
const jtex = new MystTemplate(new Session(), {
template: 'not-there',
kind: TemplateKind.tex,
});
expect(() => jtex.prepare({} as any)).toThrow(/does not exist/);
expect(fs.existsSync('_build/templates/tex/myst/plain_latex/template.zip')).toBe(true);
expect(fs.existsSync('_build/templates/tex/myst/plain_latex/template.yml')).toBe(true);
expect(fs.existsSync('_build/templates/tex/myst/plain_latex/template.tex')).toBe(true);
});
it('Bad template paths to throw', async () => {
const jtex = new MystTemplate(new Session(), {
template: 'not-there',
kind: TemplateKind.tex,
});
it('Render out the template', async () => {
const jtex = new MystTemplate(new Session(), {
kind: TemplateKind.tex,
template: `${__dirname}/example`,
});
renderTemplate(jtex, {
contentOrPath: `${__dirname}/test.tex`,
outputPath: '_build/out/article.tex',
frontmatter: {
title: 'test',
description: 'test',
date: new Date(2022, 6, 22).toISOString(),
authors: [
{ name: 'Rowan Cockett', affiliations: ['Curvenote'] },
{ name: 'Steve Purves', affiliations: ['Curvenote'], orcid: '0000' },
],
},
options: {
keywords: '',
},
parts: {
abstract: 'My abstract!',
},
});
expect(fs.existsSync('_build/out/article.tex')).toBe(true);
const content = fs.readFileSync('_build/out/article.tex').toString();
expect(content.includes('Volcanic Archipelago')).toBe(true);
expect(content.includes('Rowan Cockett')).toBe(true);
expect(content.includes('My abstract!')).toBe(true);
expect(() => jtex.prepare({} as any)).toThrow(/does not exist/);
});
it('Render out the template', async () => {
const jtex = new MystTemplate(new Session(), {
kind: TemplateKind.tex,
template: `${__dirname}/example`,
});
},
{ timeout: 15000 },
);
renderTemplate(jtex, {
contentOrPath: `${__dirname}/test.tex`,
outputPath: '_build/out/article.tex',
frontmatter: {
title: 'test',
description: 'test',
date: new Date(2022, 6, 22).toISOString(),
authors: [
{ name: 'Rowan Cockett', affiliations: ['Curvenote'] },
{ name: 'Steve Purves', affiliations: ['Curvenote'], orcid: '0000' },
],
},
options: {
keywords: '',
},
parts: {
abstract: 'My abstract!',
},
});
expect(fs.existsSync('_build/out/article.tex')).toBe(true);
const content = fs.readFileSync('_build/out/article.tex').toString();
expect(content.includes('Volcanic Archipelago')).toBe(true);
expect(content.includes('Rowan Cockett')).toBe(true);
expect(content.includes('My abstract!')).toBe(true);
});
});
1 change: 1 addition & 0 deletions packages/myst-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
},
"dependencies": {
"@jupyterlab/services": "^7.3.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?

"@reduxjs/toolkit": "^2.1.0",
"adm-zip": "^0.5.10",
"boxen": "^7.1.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/myst-cli/src/build/docx/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { cleanOutput } from '../utils/cleanOutput.js';
import { getFileContent } from '../utils/getFileContent.js';
import { createFooter } from './footers.js';
import { createArticleTitle, createReferenceTitle } from './titles.js';
import { createMermaidImageMystPlugin } from '../../transforms/mermaid.js';

const DOCX_IMAGE_EXTENSIONS = [ImageExtensions.png, ImageExtensions.jpg, ImageExtensions.jpeg];

Expand Down Expand Up @@ -98,6 +99,7 @@ export async function runWordExport(
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.)

preFrontmatters: [
filterKeys(article, [...PAGE_FRONTMATTER_KEYS, ...Object.keys(FRONTMATTER_ALIASES)]),
],
Expand Down
2 changes: 2 additions & 0 deletions packages/myst-cli/src/build/jats/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { resolveFrontmatterParts } from '../../utils/resolveFrontmatterParts.js'
import type { ExportWithOutput, ExportFnOptions } from '../types.js';
import { cleanOutput } from '../utils/cleanOutput.js';
import { getFileContent } from '../utils/getFileContent.js';
import { createMermaidImageMystPlugin } from '../../transforms/mermaid.js';

/**
* Build a MyST project as JATS XML
Expand All @@ -39,6 +40,7 @@ export async function runJatsExport(
projectPath,
imageExtensions: KNOWN_IMAGE_EXTENSIONS,
extraLinkTransformers,
transforms: [createMermaidImageMystPlugin],
preFrontmatters: [
filterKeys(article, [...PAGE_FRONTMATTER_KEYS, ...Object.keys(FRONTMATTER_ALIASES)]),
], // only apply to article, not sub_articles
Expand Down
3 changes: 3 additions & 0 deletions packages/myst-cli/src/build/tex/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { createTempFolder } from '../../utils/createTempFolder.js';
import { resolveFrontmatterParts } from '../../utils/resolveFrontmatterParts.js';
import type { ExportWithOutput, ExportResults, ExportFnOptions } from '../types.js';
import { writeBibtexFromCitationRenderers } from '../utils/bibtex.js';
import { createMermaidImageMystPlugin } from '../../transforms/mermaid.js';

export const DEFAULT_BIB_FILENAME = 'main.bib';
const TEX_IMAGE_EXTENSIONS = [
Expand Down Expand Up @@ -140,6 +141,7 @@ export async function localArticleToTexRaw(
projectPath,
imageExtensions: TEX_IMAGE_EXTENSIONS,
extraLinkTransformers,
transforms: [createMermaidImageMystPlugin],
titleDepths: fileArticles.map((article) => article.level),
preFrontmatters: fileArticles.map((article) =>
filterKeys(article, [...PAGE_FRONTMATTER_KEYS, ...Object.keys(FRONTMATTER_ALIASES)]),
Expand Down Expand Up @@ -206,6 +208,7 @@ export async function localArticleToTexTemplated(
projectPath,
imageExtensions: TEX_IMAGE_EXTENSIONS,
extraLinkTransformers,
transforms: [createMermaidImageMystPlugin],
titleDepths: fileArticles.map((article) => article.level),
preFrontmatters: fileArticles.map((article) =>
filterKeys(article, [...PAGE_FRONTMATTER_KEYS, ...Object.keys(FRONTMATTER_ALIASES)]),
Expand Down
3 changes: 3 additions & 0 deletions packages/myst-cli/src/build/typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import version from '../version.js';
import { cleanOutput } from './utils/cleanOutput.js';
import type { ExportWithOutput, ExportResults, ExportFnOptions } from './types.js';
import { writeBibtexFromCitationRenderers } from './utils/bibtex.js';
import { createMermaidImageMystPlugin } from '../transforms/mermaid.js';

export const DEFAULT_BIB_FILENAME = 'main.bib';
const TYPST_IMAGE_EXTENSIONS = [
Expand Down Expand Up @@ -158,6 +159,7 @@ export async function localArticleToTypstRaw(
projectPath,
imageExtensions: TYPST_IMAGE_EXTENSIONS,
extraLinkTransformers,
transforms: [createMermaidImageMystPlugin],
titleDepths: fileArticles.map((article) => article.level),
preFrontmatters: fileArticles.map((article) =>
filterKeys(article, [...PAGE_FRONTMATTER_KEYS, ...Object.keys(FRONTMATTER_ALIASES)]),
Expand Down Expand Up @@ -229,6 +231,7 @@ export async function localArticleToTypstTemplated(
projectPath,
imageExtensions: TYPST_IMAGE_EXTENSIONS,
extraLinkTransformers,
transforms: [createMermaidImageMystPlugin],
titleDepths: fileArticles.map((article) => article.level),
preFrontmatters: fileArticles.map((article) =>
filterKeys(article, [...PAGE_FRONTMATTER_KEYS, ...Object.keys(FRONTMATTER_ALIASES)]),
Expand Down
5 changes: 5 additions & 0 deletions packages/myst-cli/src/build/utils/getFileContent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { resolve } from 'node:path';
import type { TransformSpec } from 'myst-common';
import { plural } from 'myst-common';
import { tic } from 'myst-cli-utils';
import type { LinkTransformer } from 'myst-transforms';
Expand All @@ -19,6 +20,7 @@ export async function getFileContent(
projectPath,
imageExtensions,
extraLinkTransformers,
transforms,
extraTransforms,
titleDepths,
preFrontmatters,
Expand All @@ -27,6 +29,8 @@ export async function getFileContent(
projectPath?: string;
imageExtensions: ImageExtensions[];
extraLinkTransformers?: LinkTransformer[];
transforms?: TransformSpec[];
/** @deprecated use transforms instead */
extraTransforms?: TransformFn[];
titleDepths?: number | (number | undefined)[];
preFrontmatters?: Record<string, any> | (Record<string, any> | undefined)[];
Expand Down Expand Up @@ -72,6 +76,7 @@ export async function getFileContent(
minifyMaxCharacters: 0,
index: project.index,
titleDepth,
transforms,
extraTransforms,
execute,
});
Expand Down
20 changes: 17 additions & 3 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import path from 'node:path';
import { tic } from 'myst-cli-utils';
import type { GenericParent, IExpressionResult, PluginUtils, References } from 'myst-common';
import type {
GenericParent,
IExpressionResult,
PluginUtils,
References,
TransformSpec,
} from 'myst-common';
import { fileError, fileWarn, RuleId, slugToUrl } from 'myst-common';
import type { PageFrontmatter } from 'myst-frontmatter';
import { SourceFileKind } from 'myst-spec-ext';
Expand Down Expand Up @@ -112,6 +118,8 @@ export async function transformMdast(
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.

/** @deprecated Use `session.plugins?.transforms` instead */
extraTransforms?: TransformFn[];
minifyMaxCharacters?: number;
index?: string;
Expand All @@ -125,6 +133,7 @@ export async function transformMdast(
pageSlug,
projectSlug,
imageExtensions,
transforms,
extraTransforms,
watchMode = false,
minifyMaxCharacters,
Expand Down Expand Up @@ -204,10 +213,15 @@ export async function transformMdast(
})
.use(inlineMathSimplificationPlugin, { replaceSymbol: false })
.use(mathPlugin, { macros: frontmatter.math });
// Load transform functions from options (we always run all of them)
transforms?.forEach((t) => {
pipe.use(t.plugin, t.options, pluginUtils);
});

// Load custom transform plugins
session.plugins?.transforms.forEach((t) => {
if (t.stage !== 'document') return;
pipe.use(t.plugin, undefined, pluginUtils);
pipe.use(t.plugin, t.options, pluginUtils);
});

pipe
Expand Down Expand Up @@ -361,7 +375,7 @@ export async function postProcessMdast(
const pipe = unified();
session.plugins?.transforms.forEach((t) => {
if (t.stage !== 'project') return;
pipe.use(t.plugin, undefined, pluginUtils);
pipe.use(t.plugin, t.options, pluginUtils);
});
await pipe.run(mdast, vfile);

Expand Down
1 change: 1 addition & 0 deletions packages/myst-cli/src/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './links.js';
export * from './mdast.js';
export * from './outputs.js';
export * from './inlineExpressions.js';
export * from './mermaid.js';
export * from './types.js';
Loading
Loading