Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧜‍♀️ Add conversion for static exports of mermaid diagrams #1452

Closed
wants to merge 5 commits into from
Closed
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
7 changes: 7 additions & 0 deletions .changeset/silver-carrots-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'myst-transforms': minor
'myst-common': patch
'myst-cli': patch
---

Add new mermaid-conversion transform
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/myst-cli/src/build/tex/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export async function localArticleToTexRaw(
imageAltOutputFolder: 'files/',
imageExtensions: TEX_IMAGE_EXTENSIONS,
simplifyFigures: true,
mermaidAsImage: true,
});
return mdastToTex(session, mdast, references, frontmatter, null, false);
}),
Expand Down Expand Up @@ -252,6 +253,7 @@ export async function localArticleToTexTemplated(
imageAltOutputFolder: 'files/',
imageExtensions: TEX_IMAGE_EXTENSIONS,
simplifyFigures: true,
mermaidAsImage: true,
});

partDefinitions.forEach((def) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
inlineMathSimplificationPlugin,
checkLinkTextTransform,
indexIdentifierPlugin,
mermaidToImageTransform,
} from 'myst-transforms';
import { unified } from 'unified';
import { select, selectAll } from 'unist-util-select';
Expand Down Expand Up @@ -341,6 +342,7 @@ export async function finalizeMdast(
file: string,
{
imageWriteFolder,
mermaidAsImage,
useExistingImages,
imageAltOutputFolder,
imageExtensions,
Expand All @@ -350,6 +352,7 @@ export async function finalizeMdast(
maxSizeWebp,
}: {
imageWriteFolder: string;
mermaidAsImage?: boolean;
useExistingImages?: boolean;
imageAltOutputFolder?: string;
imageExtensions?: ImageExtensions[];
Expand All @@ -372,6 +375,9 @@ export async function finalizeMdast(
vfile,
});
if (!useExistingImages) {
if (mermaidAsImage) {
await mermaidToImageTransform(session, mdast, imageWriteFolder, vfile);
}
await transformImagesToDisk(session, mdast, file, imageWriteFolder, {
altOutputFolder: imageAltOutputFolder,
imageExtensions,
Expand Down
1 change: 1 addition & 0 deletions packages/myst-common/src/ruleids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export enum RuleId {
imageFormatConverts = 'image-format-converts',
imageCopied = 'image-copied',
imageFormatOptimizes = 'image-format-optimizes',
mermaidDiagramConverted = 'mermaid-diagram-converted',
// Math rules
mathLabelLifted = 'math-label-lifted',
mathEquationEnvRemoved = 'math-equation-env-removed',
Expand Down
7 changes: 4 additions & 3 deletions packages/myst-transforms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
"dependencies": {
"doi-utils": "^2.0.0",
"hast-util-from-html": "^2.0.1",
"hast-util-to-mdast": "^8.3.1",
"intersphinx": "^1.0.2",
"js-yaml": "^4.1.0",
"katex": "^0.15.2",
"hast-util-to-mdast": "^8.3.1",
"mdast-util-find-and-replace": "^2.1.0",
"myst-common": "^1.5.3",
"myst-frontmatter": "^1.5.3",
Expand All @@ -36,13 +36,14 @@
"unified": "^10.0.0",
"unist-builder": "^3.0.0",
"unist-util-find-after": "^4.0.0",
"unist-util-modify-children": "^3.1.0",
"unist-util-map": "^3.0.0",
"unist-util-modify-children": "^3.1.0",
"unist-util-remove": "^3.1.0",
"unist-util-select": "^4.0.3",
"unist-util-visit": "^4.1.0",
"vfile": "^5.0.0",
"vfile-message": "^3.1.2"
"vfile-message": "^3.1.2",
"which": "^3.0.1"
},
"devDependencies": {
"@types/katex": "^0.14.0"
Expand Down
1 change: 1 addition & 0 deletions packages/myst-transforms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export {
} from './code.js';
export { blockquotePlugin, blockquoteTransform } from './blockquote.js';
export { imageAltTextPlugin, imageAltTextTransform } from './images.js';
export { mermaidToImageTransform } from './mermaid.js';
export { buildIndexTransform, indexIdentifierPlugin, indexIdentifierTransform } from './indices.js';
export {
liftMystDirectivesAndRolesPlugin,
Expand Down
106 changes: 106 additions & 0 deletions packages/myst-transforms/src/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { selectAll } from 'unist-util-select';
import type { GenericParent, GenericNode } from 'myst-common';
import { RuleId } from 'myst-common';
import which from 'which';
import type { VFile } from 'vfile';
import type { LoggerDE } from 'myst-cli-utils';
import type { ISession } from 'myst-cli';
import { makeExecutable } from 'myst-cli-utils';
import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import { join } from 'node:path';
import { createTempFolder, addWarningForFile } from 'myst-cli';

function isMMDCCommandAvailable() {
return !!which.sync('mmdc', { nothrow: true });
}
type Literal = {
type: string;
value: string;
};

function createMMDCLogger(session: ISession): LoggerDE {
const logger = {
debug(data: string) {
const line = data.trim();
if (!line) return;
session.log.debug(data);
},
error(data: string) {
const line = data.trim();
if (!line) return;
session.log.error(data);
},
};
return logger;
}

async function convertMermaidToSVG(
session: ISession,
writeFolder: string,
data: string,
vfile: VFile,
) {
const hash = createHash('md5').update(data).digest('hex');
const tempFolder = createTempFolder(session);

const srcPath = join(tempFolder, `${hash}.mmd`);
await fs.writeFile(srcPath, data);

await fs.mkdir(writeFolder, { recursive: true });
const dstPath = join(writeFolder, `${hash}.pdf`);

const executable = `mmdc -i ${srcPath} -o ${dstPath}`;
const exec = makeExecutable(executable, createMMDCLogger(session));
try {
await exec();
} catch (err) {
addWarningForFile(
session,
vfile.path,
`Could not convert Mermaid diagram to svg: ${err}`,
'error',
{
ruleId: RuleId.mermaidDiagramConverted,
},
);
return null;
}

return dstPath;
}

/**
* Ensure caption content is nested in a paragraph.
*
* This function is idempotent.
*/
export async function mermaidToImageTransform(
session: ISession,
tree: GenericParent,
writeFolder: string,
vfile: VFile,
) {
if (!isMMDCCommandAvailable()) {
addWarningForFile(
session,
vfile.path,
`Could not find mmdc, required for conversion of Mermaid diagrams\n`,
'warn',
{ ruleId: RuleId.mermaidDiagramConverted },
);
return null;
}

const nodes = selectAll('mermaid', tree) as Literal[];
await Promise.all(
nodes.map(async (node) => {
const dst = await convertMermaidToSVG(session, writeFolder, node.value, vfile);
if (dst) {
const newNode = node as GenericNode;
newNode.type = 'image';
newNode.url = dst ?? '';
}
}),
);
}
Loading