Skip to content

Commit

Permalink
Replace Mermaid plugin to be backed by Playwright
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronMoat committed Aug 29, 2024
1 parent 4709861 commit b3abf69
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 714 deletions.
13 changes: 13 additions & 0 deletions .changeset/neat-wasps-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'scoobie': major
---

Replace Mermaid plugin to be backed by Playwright

Previously, the Mermaid plugin was backed by Puppeteer. This change replaces this via `mermaid-isomorphic`, in turn backed by Playwright.

There are some consequences for Mermaid users:

- Output changes (you should review and tweak these)
- You'll no longer need to install puppeteer and manage it in e.g. your Dockerfiles
- Before running builds with this change, you'll need to install `playwright` and run `<packageManager> playwright install chromium`. This could form a postinstall script.
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
"svgo": "^3.0.0",
"svgo-loader": "^4.0.0",
"unist-util-visit": "^2.0.3",
"unist-util-visit-parents": "^3.1.1",
"webpack-merge": "^6.0.0",
"which": "^4.0.0"
},
"devDependencies": {
"@changesets/cli": "2.27.7",
"@changesets/get-github-info": "0.6.0",
"@mdx-js/loader": "^1.6.22",
"@mermaid-js/mermaid-cli": "10.9.1",
"@storybook/addon-essentials": "8.2.9",
"@storybook/addon-webpack5-compiler-babel": "3.0.3",
"@storybook/react": "8.2.9",
Expand All @@ -40,6 +40,8 @@
"@types/react-dom": "18.3.0",
"braid-design-system": "32.23.0",
"loki": "0.35.0",
"mermaid-isomorphic": "2.2.1",
"playwright": "1.46.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet-async": "1.3.0",
Expand All @@ -58,17 +60,17 @@
],
"peerDependencies": {
"@mdx-js/loader": "^1.6.22",
"@mermaid-js/mermaid-cli": ">= 8.13.7 < 11",
"braid-design-system": ">= 31.21.0",
"mermaid-isomorphic": "^2.2.1",
"react": ">= 17 < 19",
"react-router-dom": ">= 5.3.0",
"sku": ">= 13.0.0 < 14"
},
"peerDependenciesMeta": {
"@mermaid-js/mermaid-cli": {
"@mdx-js/loader": {
"optional": true
},
"@mdx-js/loader": {
"mermaid-isomorphic": {
"optional": true
}
},
Expand All @@ -87,7 +89,8 @@
"storybook:build": "storybook build --output-dir dist-storybook",
"storybook:start": "storybook dev",
"test": "sku test",
"test:ci": "sku test --coverage"
"test:ci": "sku test --coverage",
"postinstall": "playwright install chromium"
},
"loki": {
"configurations": {
Expand Down
23 changes: 0 additions & 23 deletions remark/mermaid/LICENSE

This file was deleted.

13 changes: 0 additions & 13 deletions remark/mermaid/README.md

This file was deleted.

255 changes: 38 additions & 217 deletions remark/mermaid/index.js
Original file line number Diff line number Diff line change
@@ -1,232 +1,53 @@
/* eslint-disable no-sync */

const path = require('path');

const fs = require('fs-extra');
const visit = require('unist-util-visit');

const { MERMAID_DIR, PLUGIN_NAME } = require('./constants');
const utils = require('./utils');

const render = utils.render;
const renderFromFile = utils.renderFromFile;
const createMermaidDiv = utils.createMermaidDiv;

/**
* Does this URL have a `.mermaid` or `.mmd` file extension?
*
* @param {string} url
* @return {boolean}
*/
const isMermaid = (url) =>
typeof url === 'string' && ['.mermaid', '.mmd'].includes(path.extname(url));

/**
* Given a node which contains a `url` property (eg. Link or Image), follow
* the link, generate a graph and then replace the link with the link to the
* generated graph. Checks to ensure node has a title of `mermaid:` before doing.
*
* @param {object} node
* @param {vFile} vFile
* @param {string} rootDir
* @return {object}
*/
function replaceUrlWithGraph(node, vFile, rootDir) {
const { position, url } = node;

// If the node isn't mermaid, ignore it.
if (!isMermaid(url)) {
return node;
}

try {
// eslint-disable-next-line no-param-reassign
node.url = renderFromFile(`${vFile.dirname}/${url}`, rootDir);

vFile.info(
'mermaid link replaced with link to graph',
position,
PLUGIN_NAME,
);
} catch (error) {
vFile.message(error, position, PLUGIN_NAME);
}

return node;
}

/**
* Given a link to a mermaid diagram, grab the contents from the link and put it
* into a div that Mermaid JS can act upon.
*
* @param {object} node
* @param {integer} index
* @param {object} parent
* @param {vFile} vFile
* @return {object}
*/
function replaceLinkWithEmbedded(node, index, parent, vFile) {
const { position, url } = node;
let newNode;

// If the node isn't mermaid, ignore it.
if (!isMermaid(url)) {
return node;
}

try {
const value = fs.readFileSync(`${vFile.dirname}/${url}`, {
encoding: 'utf-8',
});

newNode = createMermaidDiv(value);
parent.children.splice(index, 1, newNode);
vFile.info('mermaid link replaced with div', position, PLUGIN_NAME);
} catch (error) {
vFile.message(error, position, PLUGIN_NAME);
return node;
}

return node;
}

/**
* Given the MDAST ast, look for all fenced codeblocks that have a language of
* `mermaid` and pass that to mermaid.cli to render the image. Replaces the
* codeblocks with an image of the rendered graph.
*
* @param {object} ast
* @param {vFile} vFile
* @param {boolean} isSimple
* @param {string} rootDir
* @return {function}
*/
function visitCodeBlock(ast, vFile, isSimple, rootDir) {
return visit(ast, 'code', (node, index, parent) => {
const { lang, meta, position, value } = node;
let newNode;
const visitParents = require('unist-util-visit-parents');

// If this codeblock is not mermaid, bail.
if (lang !== 'mermaid') {
return node;
}
const { MERMAID_DIR } = require('./constants');
const { createMermaidRenderer } = require('./utils');

// Are we just transforming to a <div>, or replacing with an image?
if (isSimple) {
newNode = createMermaidDiv(value);

vFile.info(`${lang} code block replaced with div`, position, PLUGIN_NAME);

// Otherwise, let's try and generate a graph!
} else {
let graphSvgFilename;
try {
graphSvgFilename = render(value, rootDir);

vFile.info(
`${lang} code block replaced with graph`,
position,
PLUGIN_NAME,
);
} catch (error) {
vFile.message(error, position, PLUGIN_NAME);
return node;
}

newNode = {
type: 'image',
// Directive for Scoobie to skip custom SVG styling (e.g. box shadow).
title: `${meta || ''} =style=none`,
url: graphSvgFilename,
};
}

parent.children.splice(index, 1, newNode);

return node;
});
}

/**
* If links have a title attribute called `mermaid:`, follow the link and
* depending on `isSimple`, either generate and link to the graph, or simply
* wrap the graph contents in a div.
*
* @param {object} ast
* @param {vFile} vFile
* @param {boolean} isSimple
* @param {string} rootDir
* @return {function}
*/
function visitLink(ast, vFile, isSimple, rootDir) {
if (isSimple) {
return visit(ast, 'link', (node, index, parent) =>
replaceLinkWithEmbedded(node, index, parent, vFile),
);
}

return visit(ast, 'link', (node) =>
replaceUrlWithGraph(node, vFile, rootDir),
);
}

/**
* If images have a title attribute called `mermaid:`, follow the link and
* depending on `isSimple`, either generate and link to the graph, or simply
* wrap the graph contents in a div.
*
* @param {object} ast
* @param {vFile} vFile
* @param {boolean} isSimple
* @param {string} rootDir
* @return {function}
*/
function visitImage(ast, vFile, isSimple, rootDir) {
if (isSimple) {
return visit(ast, 'image', (node, index, parent) =>
replaceLinkWithEmbedded(node, index, parent, vFile),
);
}

return visit(ast, 'image', (node) =>
replaceUrlWithGraph(node, vFile, rootDir),
);
}

/**
* Returns the transformer which acts on the MDAST tree and given VFile.
*
* If `options.simple` is passed as a truthy value, the plugin will convert
* to `<div class="mermaid">` rather than a SVG image.
*
* @link https://github.com/unifiedjs/unified#function-transformernode-file-next
* @link https://github.com/syntax-tree/mdast
* @link https://github.com/vfile/vfile
*
* @param {{ rootDir: string, simple?: boolean }} options
* @return {function}
*/
function mermaid(options) {
const simpleMode = options.simple || false;
const render = createMermaidRenderer(options);

/**
* @param {object} ast MDAST
* @param {vFile} vFile
* @param {function} next
* @return {object}
*/
return function transformer(ast, vFile, next) {
return function transformer(ast, file) {
fs.ensureDirSync(path.join(options.rootDir, MERMAID_DIR));

visitCodeBlock(ast, vFile, simpleMode, options.rootDir);
visitLink(ast, vFile, simpleMode, options.rootDir);
visitImage(ast, vFile, simpleMode, options.rootDir);
const instances = [];

if (typeof next === 'function') {
return next(null, ast, vFile);
}
visitParents(ast, { type: 'code', lang: 'mermaid' }, (node, ancestors) => {
instances.push([...ancestors, node]);
});

return ast;
return render(
instances.map((ancestors) => ancestors.at(-1)),
options,
).then((results) => {
for (const [i, ancestors] of instances.entries()) {
const result = results[i];
const node = ancestors.at(-1);
const parent = ancestors.at(-2);
const nodeIndex = parent.children.indexOf(node);

if (result.status === 'fulfilled') {
const { svgNodeUrl } = result.value;
parent.children[nodeIndex] = {
type: 'image',
// Directive for Scoobie to skip custom SVG styling (e.g. box shadow).
title: `${node.meta || ''} =style=none`,
url: svgNodeUrl,
};
} else {
const message = file.message(result.reason, {
ruleId: 'remark-mermaidjs',
source: 'remark-mermaidjs',
ancestors,
});
message.fatal = true;
throw message;
}
}
});
};
}

Expand Down
Loading

0 comments on commit b3abf69

Please sign in to comment.