Skip to content

Commit

Permalink
feat: 머메이드 플러그인
Browse files Browse the repository at this point in the history
  • Loading branch information
yongsk0066 committed Oct 14, 2023
1 parent affccb5 commit 55f04bb
Show file tree
Hide file tree
Showing 15 changed files with 2,062 additions and 33 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.github
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true
}
19 changes: 18 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import lit from "@astrojs/lit";
import mdx from "@astrojs/mdx";
import { defineConfig } from 'astro/config';
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeMathjax from "rehype-mathjax";
import mermaid from "./plugin/remark-mermaid";

// https://astro.build/config
export default defineConfig({
integrations: [lit(), mdx()],
integrations: [lit(), mdx({
optimize: true,
remarkPlugins: [
remarkMath,
mermaid,
],
rehypePlugins: [
()=> rehypeKatex({
output: "mathml",
strict:false,
}),
// rehypeMathjax
],
})],
site: 'https://yongsk0066.github.io',
base: '/',
});
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@
"dependencies": {
"@astrojs/lit": "^3.0.0",
"@astrojs/mdx": "^1.1.0",
"@mermaid-js/mermaid-cli": "^10.5.0",
"@webcomponents/template-shadowroot": "^0.2.1",
"astro": "^3.1.2",
"fs-extra": "^11.1.1",
"lit": "^2.7.0",
"three": "^0.157.0"
"rehype-katex": "^7.0.0",
"rehype-mathjax": "^5.0.0",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"three": "^0.157.0",
"to-vfile": "^8.0.0",
"unified": "^11.0.3",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.1",
"which": "^4.0.0"
},
"devDependencies": {
"@types/three": "^0.156.0",
Expand Down
199 changes: 199 additions & 0 deletions plugin/remark-mermaid/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import fs from 'fs/promises';
import { visit } from 'unist-util-visit';
import { render, renderFromFile, getDestinationDir, createMermaidDiv } from './utils';

const PLUGIN_NAME = 'remark-mermaid';

/**
* Is this title `mermaid:`?
*
* @param {string} title
* @return {boolean}
*/
const isMermaid = (title) => title === 'mermaid:';

/**
* 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
* @return {object}
*/
const replaceUrlWithGraph = async (node, vFile) => {
const { title, url, position } = node;
const { destinationDir } = vFile.data;

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

try {
// eslint-disable-next-line no-param-reassign
node.url = await renderFromFile(`${vFile.dirname}/${url}`, destinationDir);
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}
*/
const replaceLinkWithEmbedded = async (node, index, parent, vFile) => {
const { title, url, position } = node;
let newNode;

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

try {
const value = await fs.promises.readFile(`${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
* @return {function}
*/
const visitCodeBlock = async (ast, vFile, isSimple) => {
return visit(ast, 'code', async (node, index, parent) => {
const { lang, value, position } = node;
const destinationDir = getDestinationDir(vFile);
let newNode;

// If this codeblock is not mermaid, bail.
if (lang !== 'mermaid') {
return node;
}

// 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 = await render(value, destinationDir);
console.log("graphSvgFilename", graphSvgFilename)
vFile.info(`${lang} code block replaced with graph`, position, PLUGIN_NAME);
} catch (error) {
vFile.message(error, position, PLUGIN_NAME);
return node;
}

newNode = {
type: 'image',
title: '`mermaid` image',
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
* @return {function}
*/
const visitLink = (ast, vFile, isSimple) =>{
if (isSimple) {
return visit(ast, 'link', (node, index, parent) =>
replaceLinkWithEmbedded(node, index, parent, vFile)
);
}

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

/**
* 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
* @return {function}
*/
const visitImage = (ast, vFile, isSimple) => {
if (isSimple) {
return visit(ast, 'image', (node, index, parent) =>
replaceLinkWithEmbedded(node, index, parent, vFile)
);
}

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

/**
* 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 {object} options
* @return {function}
*/
const mermaid = (options = {}) => {
const simpleMode = options.simple ?? false;

const transformer = (ast, vFile, next) => {
visitCodeBlock(ast, vFile, simpleMode);
visitLink(ast, vFile, simpleMode);
visitImage(ast, vFile, simpleMode);

if (typeof next === 'function') {
return next(null, ast, vFile);
}

return ast;
};
return transformer;
}

export default mermaid;
86 changes: 86 additions & 0 deletions plugin/remark-mermaid/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { execSync } from 'child_process';
import { promises as fs } from 'fs';
import { createHmac } from 'crypto';
import * as path from 'path';
import which from 'which';

const PLUGIN_NAME = 'remark-mermaid';

/**
* Accepts the `source` of the graph as a string, and render an SVG using
* mermaid.cli. Returns the path to the rendered SVG.
*
* @param {string} source
* @param {string} destination
* @return {string}
*/
export const render = async (source, destination) => {
const unique = createHmac('sha1', PLUGIN_NAME).update(source).digest('hex');
const mmdcExecutable = which.sync('mmdc');
const mmdPath = path.join(destination, `${unique}.mmd`);
const svgFilename = `${unique}.svg`;
const svgPath = path.join(destination, svgFilename);

try {
await fs.writeFile(mmdPath, source);
execSync(`${mmdcExecutable} -i ${mmdPath} -o ${svgPath} -b transparent`);
} catch (error) {
throw new Error(`Failed to render Mermaid graph: ${error.message}`);
} finally {
await fs.unlink(mmdPath);
}

return `/src/assets/images/${svgFilename}`;
};

/**
* Accepts the `source` of the graph as a string, and render an SVG using
* mermaid.cli. Returns the path to the rendered SVG.
*
* @param {string} destination
* @param {string} source
* @return {string}
*/
export const renderFromFile = (inputFile, destination) => {
const unique = createHmac('sha1', PLUGIN_NAME).update(inputFile).digest('hex');
const mmdcExecutable = which.sync('mmdc');
const svgFilename = `${unique}.svg`;
const svgPath = path.join(destination, svgFilename);

// Invoke mermaid.cli
try {
execSync(`${mmdcExecutable} -i ${inputFile} -o ${svgPath} -b transparent`);
} catch (error) {
throw new Error(`Failed to render Mermaid graph from file: ${error.message}`);
}
return `./${svgFilename}`;
}

/**
* Returns the destination for the SVG to be rendered at, explicity defined
* using `vFile.data.destinationDir`, or falling back to the file's current
* directory.
*
* @param {vFile} vFile
* @return {string}
*/
export const getDestinationDir = (vFile) => {
return path.join(path.resolve(), '/src/assets/images')
// return vFile.data.destinationDir ?? vFile.dirname;
}

/**
* Given the contents, returns a MDAST representation of a HTML node.
*
* @param {string} contents
* @return {object}
*/
export const createMermaidDiv = (contents) => {
return {
type: 'html',
value: `<div class="mermaid">
${contents}
</div>`,
};
}

Loading

0 comments on commit 55f04bb

Please sign in to comment.