From e64ac140eb0e91addd6a9b212c0dc76940307f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20R=20Pearson?= Date: Mon, 3 Jun 2019 19:26:55 +0700 Subject: [PATCH] feat(gatsby-remark-graphviz): custom SVG attributes and default styling (#11624) let's get this merged, thanks for your patience! --- examples/using-remark/gatsby-config.js | 1 + examples/using-remark/package.json | 1 + .../src/pages/2019-02-06---graphviz/index.md | 398 ++++++++++++++++++ packages/gatsby-remark-graphviz/README.md | 27 ++ packages/gatsby-remark-graphviz/package.json | 1 + packages/gatsby-remark-graphviz/src/index.js | 23 +- 6 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 examples/using-remark/src/pages/2019-02-06---graphviz/index.md diff --git a/examples/using-remark/gatsby-config.js b/examples/using-remark/gatsby-config.js index 535995e6878cc..9083a095577c8 100644 --- a/examples/using-remark/gatsby-config.js +++ b/examples/using-remark/gatsby-config.js @@ -60,6 +60,7 @@ module.exports = { }, }, `gatsby-remark-autolink-headers`, + `gatsby-remark-graphviz`, // graphviz before prismjs `gatsby-remark-prismjs`, `gatsby-remark-katex`, ], diff --git a/examples/using-remark/package.json b/examples/using-remark/package.json index e4c6c981fb4c1..62817615bacd0 100644 --- a/examples/using-remark/package.json +++ b/examples/using-remark/package.json @@ -16,6 +16,7 @@ "gatsby-remark-autolink-headers": "^2.0.16", "gatsby-remark-copy-linked-files": "^2.0.11", "gatsby-remark-embed-snippet": "^3.2.4", + "gatsby-remark-graphviz": "^1.0.9", "gatsby-remark-images": "^3.0.10", "gatsby-remark-katex": "^3.0.4", "gatsby-remark-prismjs": "^3.2.8 ", diff --git a/examples/using-remark/src/pages/2019-02-06---graphviz/index.md b/examples/using-remark/src/pages/2019-02-06---graphviz/index.md new file mode 100644 index 0000000000000..887161ebf91e2 --- /dev/null +++ b/examples/using-remark/src/pages/2019-02-06---graphviz/index.md @@ -0,0 +1,398 @@ +--- +title: "Graphviz" +date: "2019-02-06" +draft: false +author: Jay Gatsby +tags: + - remark + - React + - Graphviz +--- + +## Rendering dot code blocks + +By adding [gatsby-remark-graphviz](https://www.gatsbyjs.org/packages/gatsby-remark-graphviz/) to your Gatsby site, you can create graphs powered by [Viz.js](https://github.com/mdaines/viz.js) by adding `dot` code blocks in your Markdown files: + + ```dot + digraph graphname { + a -> b; + b -> c; + a -> c; + } + ``` + +Will render as: + +```dot +digraph graphname { + a -> b; + b -> c; + a -> c; +} +``` + +A code block without a `dot` or `circo` will not be processed: + +``` +digraph graphname { + a -> b; + b -> c; + a -> c; +} +``` + +## Adding custom attributes + +You can add custom attributes to the resulting SVG: + + ```dot id="small-digraph" style="border: solid 3px tomato; box-shadow: 5px 5px 5px; padding: 15px; box-sizing: content-box" class="graphviz-figure" data-mydata123 + digraph graphname { + a -> b; + b -> c; + a -> c; + } + ``` + +Will render as: + +```dot id="small-digraph" style="border: solid 3px tomato; box-shadow: 5px 5px 5px; padding: 15px; box-sizing: content-box" class="graphviz-figure" data-mydata123 +digraph graphname { + a -> b; + b -> c; + a -> c; +} +``` + +Don't be shy, go ahead and inspect that SVG and see all the attributes added to it. + +## Width, height and responsiveness + +You can control the layout, spacing and size of the rendered SVG by using [Graphviz attributes](https://graphviz.gitlab.io/_pages/doc/info/attrs.html) like this: + + ```dot + digraph graphname { + graph [size="1.5,1.5"]; + a -> b; + b -> c; + a -> c; + } + ``` + +This will give you a slighly smaller SVG: + +```dot +digraph graphname { + graph [size="1.5,1.5"]; + a -> b; + b -> c; + a -> c; +} +``` + +Alternatively, you can overwrite those values by passing custom SVG attributes like this: + + ```dot width="178pt" height="auto" + digraph graphname { + a -> b; + b -> c; + a -> c; + } + ``` + +Whoa! + +```dot width="178pt" height="auto" +digraph graphname { + a -> b; + b -> c; + a -> c; +} +``` + +By default, gatsby-remark-graphviz is adding the following inline style to every rendered SVG: + +```css +max-width: 100%; +height: auto; +``` + +This will make graphs work as expected most of the time - small graphs will remain small and big ones will shrink to fit the parent's box. Graphs can get really big ([from Gatsby the docs](https://www.gatsbyjs.org/docs/behind-the-scenes/)): + +```dot id="gatsby-diagram" +digraph graphname { + + node [ style = filled, fillcolor = white ]; + + ## Legend + + subgraph cluster_legend { + label = "Legend"; + gatsby [ label = "Gatsby", width=1 ]; + redux [ label = "redux namespace", shape = box, fillcolor = skyblue, width=1 ]; + cache [ label = "site/.cache/", shape = cylinder, fillcolor = moccasin, width=1 ]; + public [ label ="site/public/", shape = cylinder, fillcolor = palegreen, width=1 ]; + siteData [ label = "site/external data", shape = cylinder, fillcolor = gray, width=1 ]; + + siteData -> gatsby [ style = invis ]; + gatsby -> redux [ style = invis ] ; + redux -> cache [ style = invis ]; + cache -> public [ style = invis ]; + } + + ## Source Nodes + + dataSource [ label = "data sources. e.g file, contentful", shape = cylinder, fillcolor = gray ]; + sourceNodes [ label = "source nodes" URL = "/docs/node-creation/" ]; + nodes [ label = "nodes", shape = box, fillcolor = skyblue, URL = "/docs/node-creation/" ]; + nodesTouched [ label = "touchedNodes", shape = box, fillcolor = skyblue, URL = "/docs/node-creation/#freshstale-nodes" ]; + rootNodeMap [ label = "rootNodeMap", shape = box, fillcolor = skyblue, URL = "/docs/node-tracking/" ]; + + dataSource -> sourceNodes; + sourceNodes -> nodes; + sourceNodes -> nodesTouched; + sourceNodes -> rootNodeMap; + + ## Schema + + pluginResolvers [ label = "plugin resolvers", shape = cylinder, fillcolor = gray, URL = "/docs/schema-input-gql/#inferring-input-filters-from-plugin-fields" ]; + generateSchema [ label = "generate schema", URL = "/docs/schema-generation/" ]; + schema [ label = "schema\l (inc resolvers)", shape = box, fillcolor = skyblue ]; + + nodes -> generateSchema; + nodes -> schema; + pluginResolvers -> generateSchema; + rootNodeMap -> generateSchema; + generateSchema -> schema; + + ## Pages + + componentFiles [ label = "React components\l (src/template.js)", shape = cylinder, fillcolor = gray ]; + createPages [ label = "site.createPages", URL = "/docs/page-creation/" ]; + pages [ label = "pages", shape = box, fillcolor = skyblue ]; + components [ label = "components", shape = box, fillcolor = skyblue ]; + + schema -> createPages; + componentFiles -> createPages; + createPages -> pages; + createPages -> components; + + ## Query + + fragments [ label = "query fragments *.js", shape = cylinder, fillcolor = gray ]; + runQueries [ label = "extract and run queries", URL = "/docs/query-behind-the-scenes/" ]; + componentsWithQueries [ label = "components\l (with queries)", shape = box, fillcolor = skyblue ]; + queryResults [ label = "JSON result\l /public/static/d/dataPath", shape = cylinder, fillcolor = palegreen, URL = "/docs/query-execution/#save-query-results-to-redux-and-disk" ]; + dataPaths [ label = "jsonDataPaths", shape = box, fillcolor = skyblue ]; + + fragments -> runQueries; + schema -> runQueries; + pages -> runQueries; + components -> runQueries; + runQueries -> componentsWithQueries; + runQueries -> queryResults; + runQueries -> dataPaths; + + ## Write Pages + + writePages [ label = "writePages", URL = "/docs/write-pages/" ]; + dataJson [ label = "data.json", shape = cylinder, fillcolor = moccasin ]; + asyncRequires [ label = "async-requires.js", shape = cylinder, fillcolor = moccasin ]; + syncRequires [ label = "sync-requires.js", shape = cylinder, fillcolor = moccasin ]; + pagesJson [ label = "pages.json", shape = cylinder, fillcolor = moccasin ]; + + dataPaths -> writePages; + components -> writePages; + pages -> writePages; + writePages -> dataJson; + writePages -> asyncRequires; + writePages -> syncRequires; + writePages -> pagesJson; + + ## App.js + + appWebpack [ label = "configure webpack\l (`build-javascript`)", URL = "/docs/production-app/#webpack-config" ]; + productionApp [ label = "production-app.js", shape = cylinder, fillcolor = moccasin, URL = "/docs/production-app/#production-appjs" ]; + buildJavascript [ label = "build-javascript.js", URL = "/docs/production-app/" ]; + componentChunks [ label = "component chunks\l component---src-blog-[hash].js", shape = cylinder, fillcolor = palegreen, URL = "/docs/how-code-splitting-works/" ]; + appChunk [ label = "app-[hash].js", shape = cylinder, fillcolor = palegreen ]; + webpackStats [ label = "webpack.stats.json", shape = cylinder, fillcolor = palegreen, URL = "/docs/how-code-splitting-works/#webpackstatsjson" ]; + chunkMap [ label = "chunk-map.json", shape = cylinder, fillcolor = palegreen, URL = "/docs/how-code-splitting-works/#chunk-mapjson" ]; + + appWebpack -> buildJavascript; + asyncRequires -> productionApp; + dataJson -> productionApp; + productionApp -> buildJavascript; + buildJavascript -> componentChunks; + buildJavascript -> appChunk; + buildJavascript -> webpackStats; + buildJavascript -> chunkMap; + + queryResults -> componentChunks; + + ## Generate html + + htmlWebpack [ label = "configure webpack\l (`build-html`)", URL = "/docs/html-generation/#webpack" ]; + staticEntry [ label = "static-entry.js", shape = cylinder, fillcolor = moccasin, URL = "/docs/html-generation/#static-entryjs" ]; + buildHtml [ label = "build-html.js", URL = "/docs/html-generation/" ]; + pageRenderer [ label = "page-renderer.js", shape = cylinder, fillcolor = palegreen ]; + htmlFiles [ label = "html files\l (index.html)", shape = cylinder, fillcolor = palegreen ]; + + htmlWebpack -> buildHtml; + syncRequires -> staticEntry; + dataJson -> staticEntry; + webpackStats -> staticEntry; + chunkMap -> staticEntry; + staticEntry -> buildHtml; + buildHtml -> pageRenderer; + pages -> buildHtml; + pageRenderer -> buildHtml; + buildHtml -> htmlFiles; +} +``` + +You can overwrite the `style` attribute if you don't like that behaviour: + + ```dot style="" + digraph graphname { + + node [ style = filled, fillcolor = white ]; + + ## Legend + + subgraph cluster_legend { + ... + ``` + +There: + +```dot style="" +digraph graphname { + + node [ style = filled, fillcolor = white ]; + + ## Legend + + subgraph cluster_legend { + label = "Legend"; + gatsby [ label = "Gatsby", width=1 ]; + redux [ label = "redux namespace", shape = box, fillcolor = skyblue, width=1 ]; + cache [ label = "site/.cache/", shape = cylinder, fillcolor = moccasin, width=1 ]; + public [ label ="site/public/", shape = cylinder, fillcolor = palegreen, width=1 ]; + siteData [ label = "site/external data", shape = cylinder, fillcolor = gray, width=1 ]; + + siteData -> gatsby [ style = invis ]; + gatsby -> redux [ style = invis ] ; + redux -> cache [ style = invis ]; + cache -> public [ style = invis ]; + } + + ## Source Nodes + + dataSource [ label = "data sources. e.g file, contentful", shape = cylinder, fillcolor = gray ]; + sourceNodes [ label = "source nodes" URL = "/docs/node-creation/" ]; + nodes [ label = "nodes", shape = box, fillcolor = skyblue, URL = "/docs/node-creation/" ]; + nodesTouched [ label = "touchedNodes", shape = box, fillcolor = skyblue, URL = "/docs/node-creation/#freshstale-nodes" ]; + rootNodeMap [ label = "rootNodeMap", shape = box, fillcolor = skyblue, URL = "/docs/node-tracking/" ]; + + dataSource -> sourceNodes; + sourceNodes -> nodes; + sourceNodes -> nodesTouched; + sourceNodes -> rootNodeMap; + + ## Schema + + pluginResolvers [ label = "plugin resolvers", shape = cylinder, fillcolor = gray, URL = "/docs/schema-input-gql/#inferring-input-filters-from-plugin-fields" ]; + generateSchema [ label = "generate schema", URL = "/docs/schema-generation/" ]; + schema [ label = "schema\l (inc resolvers)", shape = box, fillcolor = skyblue ]; + + nodes -> generateSchema; + nodes -> schema; + pluginResolvers -> generateSchema; + rootNodeMap -> generateSchema; + generateSchema -> schema; + + ## Pages + + componentFiles [ label = "React components\l (src/template.js)", shape = cylinder, fillcolor = gray ]; + createPages [ label = "site.createPages", URL = "/docs/page-creation/" ]; + pages [ label = "pages", shape = box, fillcolor = skyblue ]; + components [ label = "components", shape = box, fillcolor = skyblue ]; + + schema -> createPages; + componentFiles -> createPages; + createPages -> pages; + createPages -> components; + + ## Query + + fragments [ label = "query fragments *.js", shape = cylinder, fillcolor = gray ]; + runQueries [ label = "extract and run queries", URL = "/docs/query-behind-the-scenes/" ]; + componentsWithQueries [ label = "components\l (with queries)", shape = box, fillcolor = skyblue ]; + queryResults [ label = "JSON result\l /public/static/d/dataPath", shape = cylinder, fillcolor = palegreen, URL = "/docs/query-execution/#save-query-results-to-redux-and-disk" ]; + dataPaths [ label = "jsonDataPaths", shape = box, fillcolor = skyblue ]; + + fragments -> runQueries; + schema -> runQueries; + pages -> runQueries; + components -> runQueries; + runQueries -> componentsWithQueries; + runQueries -> queryResults; + runQueries -> dataPaths; + + ## Write Pages + + writePages [ label = "writePages", URL = "/docs/write-pages/" ]; + dataJson [ label = "data.json", shape = cylinder, fillcolor = moccasin ]; + asyncRequires [ label = "async-requires.js", shape = cylinder, fillcolor = moccasin ]; + syncRequires [ label = "sync-requires.js", shape = cylinder, fillcolor = moccasin ]; + pagesJson [ label = "pages.json", shape = cylinder, fillcolor = moccasin ]; + + dataPaths -> writePages; + components -> writePages; + pages -> writePages; + writePages -> dataJson; + writePages -> asyncRequires; + writePages -> syncRequires; + writePages -> pagesJson; + + ## App.js + + appWebpack [ label = "configure webpack\l (`build-javascript`)", URL = "/docs/production-app/#webpack-config" ]; + productionApp [ label = "production-app.js", shape = cylinder, fillcolor = moccasin, URL = "/docs/production-app/#production-appjs" ]; + buildJavascript [ label = "build-javascript.js", URL = "/docs/production-app/" ]; + componentChunks [ label = "component chunks\l component---src-blog-[hash].js", shape = cylinder, fillcolor = palegreen, URL = "/docs/how-code-splitting-works/" ]; + appChunk [ label = "app-[hash].js", shape = cylinder, fillcolor = palegreen ]; + webpackStats [ label = "webpack.stats.json", shape = cylinder, fillcolor = palegreen, URL = "/docs/how-code-splitting-works/#webpackstatsjson" ]; + chunkMap [ label = "chunk-map.json", shape = cylinder, fillcolor = palegreen, URL = "/docs/how-code-splitting-works/#chunk-mapjson" ]; + + appWebpack -> buildJavascript; + asyncRequires -> productionApp; + dataJson -> productionApp; + productionApp -> buildJavascript; + buildJavascript -> componentChunks; + buildJavascript -> appChunk; + buildJavascript -> webpackStats; + buildJavascript -> chunkMap; + + queryResults -> componentChunks; + + ## Generate html + + htmlWebpack [ label = "configure webpack\l (`build-html`)", URL = "/docs/html-generation/#webpack" ]; + staticEntry [ label = "static-entry.js", shape = cylinder, fillcolor = moccasin, URL = "/docs/html-generation/#static-entryjs" ]; + buildHtml [ label = "build-html.js", URL = "/docs/html-generation/" ]; + pageRenderer [ label = "page-renderer.js", shape = cylinder, fillcolor = palegreen ]; + htmlFiles [ label = "html files\l (index.html)", shape = cylinder, fillcolor = palegreen ]; + + htmlWebpack -> buildHtml; + syncRequires -> staticEntry; + dataJson -> staticEntry; + webpackStats -> staticEntry; + chunkMap -> staticEntry; + staticEntry -> buildHtml; + buildHtml -> pageRenderer; + pages -> buildHtml; + pageRenderer -> buildHtml; + buildHtml -> htmlFiles; +} +``` diff --git a/packages/gatsby-remark-graphviz/README.md b/packages/gatsby-remark-graphviz/README.md index 14f7b8663a3e1..3efdd165b7198 100644 --- a/packages/gatsby-remark-graphviz/README.md +++ b/packages/gatsby-remark-graphviz/README.md @@ -42,6 +42,33 @@ Which will be rendered using viz.js and the output html will replace the code bl ![rendered-graph](/packages/gatsby-remark-graphviz/rendered-graph.svg) +Custom attributes can be passed to the rendered SVG: + + ```dot id="my-id" class="my-class" + digraph graphname { + a -> b; + b -> c; + a -> c; + } + ``` + +By default, the following inline style is applied to all rendered SVGs in order to make them responsive: + +```css +max-width: 100%; +height: auto; +``` + +This can be overwritten by using the custom attributes feature: + + ```dot style="" + digraph graphname { + a -> b; + b -> c; + a -> c; + } + ``` + ## Caveats In your gatsby-config.js, make sure you place this plugin before other remark plugins that modify code blocks (like prism). diff --git a/packages/gatsby-remark-graphviz/package.json b/packages/gatsby-remark-graphviz/package.json index ad17fc1fcd75b..f4342bea63f43 100644 --- a/packages/gatsby-remark-graphviz/package.json +++ b/packages/gatsby-remark-graphviz/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@babel/runtime": "^7.0.0", + "cheerio": "^1.0.0-rc.2", "unist-util-visit": "^1.4.0", "viz.js": "^2.0.0" }, diff --git a/packages/gatsby-remark-graphviz/src/index.js b/packages/gatsby-remark-graphviz/src/index.js index 651afc5b67806..ade6e91f727ff 100644 --- a/packages/gatsby-remark-graphviz/src/index.js +++ b/packages/gatsby-remark-graphviz/src/index.js @@ -1,6 +1,7 @@ const visit = require(`unist-util-visit`) const Viz = require(`viz.js`) const { Module, render } = require(`viz.js/full.render.js`) +const cheerio = require(`cheerio`) const viz = new Viz({ Module, render }) @@ -10,25 +11,39 @@ module.exports = async ({ markdownAST }, pluginOptions = {}) => { let codeNodes = [] visit(markdownAST, `code`, node => { + const chunks = (node.lang || ``).match( + /^(?\S+)(\s+(?.+))?/ + ) // Only act on languages supported by graphviz - if (validLanguages.includes(node.lang)) { - codeNodes.push(node) + if (chunks && validLanguages.includes(chunks.groups.lang)) { + node.lang = chunks.groups.lang + codeNodes.push({ node, attrString: chunks.groups.attrString }) } return node }) await Promise.all( - codeNodes.map(async node => { + codeNodes.map(async ({ node, attrString }) => { const { value, lang } = node try { // Perform actual render const svgString = await viz.renderString(value, { engine: lang }) + // Add default inline styling + const $ = cheerio.load(svgString) + $(`svg`).attr(`style`, `max-width: 100%; height: auto;`) + + // Merge custom attributes if provided by user (adds and overwrites) + if (attrString) { + const attrElement = cheerio.load(``) + $(`svg`).attr(attrElement(`element`).attr()) + } + // Mutate the current node. Converting from a code block to // HTML (with svg content) node.type = `html` - node.value = svgString + node.value = $.html(`svg`) } catch (error) { console.log( `Error during viz.js execution. Leaving code block unchanged`