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/graphviz-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@doc-kit/generator-react': minor
---

Render ` ```dot ` code blocks as Graphviz diagrams when the optional `@hpcc-js/wasm-graphviz` dependency is installed, with `${light|dark}` colors rendering a diagram per color scheme.
9 changes: 9 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@
"vite": "~8.3.1"
},
"devDependencies": {
"@hpcc-js/wasm-graphviz": "^1.22.0",
"dedent": "^1.7.2"
},
"peerDependencies": {
"@hpcc-js/wasm-graphviz": "^1.22.0"
},
"peerDependenciesMeta": {
"@hpcc-js/wasm-graphviz": {
"optional": true
}
}
}
14 changes: 14 additions & 0 deletions packages/react/src/html/ui/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,18 @@ main {
}
}
}

/* Graphviz diagrams; with colors for each color scheme, there is one per
scheme, and the one of the current scheme shows */
.diagram svg {
display: block;
max-width: 100%;
height: auto;
margin: 1rem auto;
}

:where([data-theme='dark']) & .diagram-light,
:where(:root:not([data-theme='dark'])) & .diagram-dark {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { u } from 'unist-builder';

import transformDiagrams, { renderDiagram } from '../diagrams.mjs';

const transform = transformDiagrams();

describe('transformDiagrams', () => {
it('renders ```dot code blocks as inline SVG', () => {
const tree = u('root', [u('code', { lang: 'dot' }, 'digraph { a -> b }')]);

transform(tree);

const [node] = tree.children;
assert.equal(node.type, 'html');
assert.match(
node.value,
/^<div class="diagram">[\s\S]*<svg[\s\S]*<\/svg><\/div>$/
);
assert.doesNotMatch(node.value, /\n\s*\n/);
});

it('leaves other code blocks alone', () => {
const code = u('code', { lang: 'js' }, 'a -> b');
const tree = u('root', [code]);

transform(tree);

assert.equal(tree.children[0], code);
});
});

describe('renderDiagram', () => {
it('renders a diagram per color scheme for themed colors', () => {
const html = renderDiagram('digraph { a [color="${#000000|#ffffff}"] }');

assert.match(html, /class="diagram diagram-light"/);
assert.match(html, /class="diagram diagram-dark"/);
assert.match(html.split('diagram-dark')[0], /#000000/);
assert.match(html.split('diagram-dark')[1], /#ffffff/);
});
});
76 changes: 76 additions & 0 deletions packages/react/src/jsx-ast/utils/plugins/diagrams.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

import { visit } from 'unist-util-visit';

/**
* Graphviz, when the optional `@hpcc-js/wasm-graphviz` dependency is
* installed. Loaded up front, as the Markdown pipeline runs synchronously.
*
* @type {import('@hpcc-js/wasm-graphviz').Graphviz | undefined}
*/
const graphviz = await import('@hpcc-js/wasm-graphviz')
.then(({ Graphviz }) => Graphviz.load())
.catch(() => undefined);

/** A color for each color scheme: `${#3c3c43|#dfdfd6}` */
const THEMED_COLOR = /\$\{([^|}]*)\|([^}]*)\}/g;

/**
* Renders DOT source as an inline SVG.
*
* @param {string} dot
*/
const render = dot =>
graphviz
.dot(dot, 'svg_inline')
// A blank line would end the HTML block the SVG is embedded in
.replace(/\n\s*\n/g, '\n')
.trim();

/**
* Renders a diagram. One using colors for each color scheme renders twice,
* one SVG per scheme, the page's styles showing the one of the current
* scheme.
*
* @param {string} dot
*/
export const renderDiagram = dot => {
if (!dot.includes('${')) {
return `<div class="diagram">${render(dot)}</div>`;
}

return ['light', 'dark']
.map(scheme => {
const colors = dot.replace(THEMED_COLOR, (_, light, dark) =>
scheme === 'light' ? light : dark
);

return `<div class="diagram diagram-${scheme}">${render(colors)}</div>`;
})
.join('\n');
};

/**
* Renders ```dot code blocks as Graphviz diagrams, when Graphviz is
* installed; otherwise, they stay code blocks.
*
* @param {import('mdast').Root} tree
*/
const transformer = tree => {
if (!graphviz) {
return;
}

visit(tree, 'code', (node, index, parent) => {
if (node.lang !== 'dot') {
return;
}

parent.children[index] = { type: 'html', value: renderDiagram(node.value) };
});
};

/**
* The remark plugin rendering Graphviz diagrams.
*/
export default () => transformer;
2 changes: 2 additions & 0 deletions packages/react/src/jsx-ast/utils/remark.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { visit } from 'unist-util-visit';

import { AST_NODE_TYPES } from '../constants.mjs';
import transformAlerts from './plugins/alerts.mjs';
import transformDiagrams from './plugins/diagrams.mjs';
import transformElements from './plugins/transformer.mjs';

const passThrough = ['element', ...Object.values(AST_NODE_TYPES.MDX)];
Expand Down Expand Up @@ -58,6 +59,7 @@ export const getRemarkRecma = lazy(() =>
unified()
.use(remarkParse)
.use(transformAlerts)
.use(transformDiagrams)
// We make Rehype ignore existing HTML nodes, and JSX nodes
// as these are nodes we manually created during the generation process
// We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

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

Loading