-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
38f3a84
commit 6a17604
Showing
14 changed files
with
621 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<figure class="beoe mermaid"><svg xmlns="http://www.w3.org/2000/svg" id="m01-0" width="100%" aria-roledescription="flowchart-v2" style="font-family:arial,sans-serif;font-size:16px;fill:#333;max-width:41.671875px" viewBox="-8 -8 41.672 49"><style>#m01-0 .marker{fill:#333;stroke:#333}#m01-0 .marker.cross{stroke:#333}#m01-0 svg{font-family:arial,sans-serif;font-size:16px}#m01-0 .node circle,#m01-0 .node path,#m01-0 .node rect{fill:#ececff;stroke:#9370db;stroke-width:1px}#m01-0 :root{--mermaid-font-family:arial,sans-serif}</style><marker id="m01-0_flowchart-pointEnd" class="marker flowchart" markerHeight="12" markerUnits="userSpaceOnUse" markerWidth="12" orient="auto" refX="6" refY="5" viewBox="0 0 10 10"><path d="m0 0 10 5-10 5z" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></path></marker><marker id="m01-0_flowchart-pointStart" class="marker flowchart" markerHeight="12" markerUnits="userSpaceOnUse" markerWidth="12" orient="auto" refX="4.5" refY="5" viewBox="0 0 10 10"><path d="m0 5 10 5V0z" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></path></marker><marker id="m01-0_flowchart-circleEnd" class="marker flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="11" refY="5" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></circle></marker><marker id="m01-0_flowchart-circleStart" class="marker flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="-1" refY="5" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></circle></marker><marker id="m01-0_flowchart-crossEnd" class="marker cross flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="12" refY="5.2" viewBox="0 0 11 11"><path d="m1 1 9 9m0-9-9 9" class="arrowMarkerPath" style="stroke-width:2;stroke-dasharray:1,0"></path></marker><marker id="m01-0_flowchart-crossStart" class="marker cross flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="-1" refY="5.2" viewBox="0 0 11 11"><path d="m1 1 9 9m0-9-9 9" class="arrowMarkerPath" style="stroke-width:2;stroke-dasharray:1,0"></path></marker><g class="root"><g class="nodes"><g id="flowchart-A-0" class="node default default flowchart-label" data-id="A" data-node="true" transform="translate(12.836 16.5)"><rect width="25.672" height="33" x="-12.836" y="-16.5" class="basic label-container" rx="0" ry="0"></rect><g class="label" style="font-family:arial,sans-serif;color:#333;text-align:center" transform="translate(-5.336 -9)"><rect></rect><foreignObject width="10.672" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;white-space:nowrap"><span class="nodeLabel" style="fill:#333;color:#333">A</span></div></foreignObject></g></g></g></g></svg></figure> | ||
<figure class="beoe mermaid"><svg xmlns="http://www.w3.org/2000/svg" id="m01-0" width="100%" aria-roledescription="flowchart-v2" style="font-family:arial,sans-serif;font-size:16px;fill:#333;max-width:41.671875px" viewBox="-8 -8 41.672 49.5"><style>#m01-0 .marker{fill:#333;stroke:#333}#m01-0 .marker.cross{stroke:#333}#m01-0 svg{font-family:arial,sans-serif;font-size:16px}#m01-0 .node circle,#m01-0 .node path,#m01-0 .node rect{fill:#ececff;stroke:#9370db;stroke-width:1px}#m01-0 :root{--mermaid-font-family:arial,sans-serif}</style><marker id="m01-0_flowchart-pointEnd" class="marker flowchart" markerHeight="12" markerUnits="userSpaceOnUse" markerWidth="12" orient="auto" refX="6" refY="5" viewBox="0 0 10 10"><path d="m0 0 10 5-10 5z" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></path></marker><marker id="m01-0_flowchart-pointStart" class="marker flowchart" markerHeight="12" markerUnits="userSpaceOnUse" markerWidth="12" orient="auto" refX="4.5" refY="5" viewBox="0 0 10 10"><path d="m0 5 10 5V0z" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></path></marker><marker id="m01-0_flowchart-circleEnd" class="marker flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="11" refY="5" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></circle></marker><marker id="m01-0_flowchart-circleStart" class="marker flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="-1" refY="5" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width:1;stroke-dasharray:1,0"></circle></marker><marker id="m01-0_flowchart-crossEnd" class="marker cross flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="12" refY="5.2" viewBox="0 0 11 11"><path d="m1 1 9 9m0-9-9 9" class="arrowMarkerPath" style="stroke-width:2;stroke-dasharray:1,0"></path></marker><marker id="m01-0_flowchart-crossStart" class="marker cross flowchart" markerHeight="11" markerUnits="userSpaceOnUse" markerWidth="11" orient="auto" refX="-1" refY="5.2" viewBox="0 0 11 11"><path d="m1 1 9 9m0-9-9 9" class="arrowMarkerPath" style="stroke-width:2;stroke-dasharray:1,0"></path></marker><g class="root"><g class="nodes"><g id="flowchart-A-0" class="node default default flowchart-label" data-id="A" data-node="true" transform="translate(12.836 16.75)"><rect width="25.672" height="33.5" x="-12.836" y="-16.75" class="basic label-container" rx="0" ry="0"></rect><g class="label" style="font-family:arial,sans-serif;color:#333;text-align:center" transform="translate(-5.336 -9.25)"><rect></rect><foreignObject width="10.672" height="18.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;white-space:nowrap"><span class="nodeLabel" style="fill:#333;color:#333">A</span></div></foreignObject></g></g></g></g></svg></figure> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# @beoe/rehype-vizdom | ||
|
||
> [!WARNING] | ||
> Doesn't work because `@vizdom/vizdom-ts-esm` uses [WebAssembly/ES Module Integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) proposal, which is not supported by [Vite](https://github.com/vitejs/vite/discussions/7763) and the plugin [doesn't seem to work with Vitest](https://github.com/Menci/vite-plugin-wasm/issues/56#issuecomment-2253169420) | ||
Rehype plugin to generate [Vizdom](https://github.com/vizdom-dev/vizdom) diagrams (as inline SVGs) in place of code fences. This | ||
|
||
````md | ||
```dot | ||
digraph G { Hello -> World } | ||
``` | ||
```` | ||
|
||
will be converted to | ||
|
||
```html | ||
<figure class="beoe vizdom"> | ||
<svg>...</svg> | ||
</figure> | ||
``` | ||
|
||
which can look like this: | ||
|
||
**TODO**: add screenshot | ||
|
||
## Usage | ||
|
||
```js | ||
import rehypeGraphviz from "@beoe/rehype-vizdom"; | ||
|
||
const html = await unified() | ||
.use(remarkParse) | ||
.use(remarkRehype) | ||
.use(rehypeGraphviz) | ||
.use(rehypeStringify) | ||
.process(`markdown`); | ||
``` | ||
|
||
It support caching the same way as [@beoe/rehype-code-hook](/packages/rehype-code-hook/) does. | ||
|
||
## Tips | ||
|
||
### Styling and dark mode | ||
|
||
You can add dark mode with something like this: | ||
|
||
```css | ||
:root { | ||
--color-variable: #000; | ||
} | ||
@media (prefers-color-scheme: dark) { | ||
:root { | ||
--color-variable: #fff; | ||
} | ||
} | ||
.vizdom { | ||
text { | ||
fill: var(--color-variable); | ||
} | ||
[fill="black"] { | ||
fill: var(--color-variable); | ||
} | ||
[stroke="black"] { | ||
stroke: var(--color-variable); | ||
} | ||
} | ||
``` | ||
|
||
Plus you can pass [class](https://vizdom.org/docs/attrs/class/) to Edges and Nodes to implement advanced styling. | ||
|
||
### Transparent background | ||
|
||
To remove background use: | ||
|
||
```dot | ||
digraph G { | ||
bgcolor="transparent" | ||
} | ||
``` | ||
|
||
### To remove title | ||
|
||
To remove `title` (which shows as tooltip when you hover mouse) use: | ||
|
||
```dot | ||
digraph G { | ||
node[tooltip=" "] | ||
} | ||
``` | ||
|
||
### You can add links | ||
|
||
Inline SVG can contain HTML links: | ||
|
||
```dot | ||
digraph G { | ||
node[URL="https://example.com"] | ||
} | ||
``` | ||
|
||
## TODO |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
{ | ||
"name": "@beoe/rehype-vizdom", | ||
"type": "module", | ||
"version": "0.0.2", | ||
"description": "rehype vizdom plugin", | ||
"keywords": [ | ||
"rehype", | ||
"vizdom" | ||
], | ||
"author": "stereobooster", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/stereobooster/beoe.git", | ||
"directory": "packages/rehype-vizdom" | ||
}, | ||
"sideEffects": false, | ||
"exports": { | ||
"types": "./dist/index.d.js", | ||
"default": "./dist/index.js" | ||
}, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.js", | ||
"files": [ | ||
"dist" | ||
], | ||
"types": "./dist/index.d.js", | ||
"scripts": { | ||
"test": "vitest", | ||
"build": "rm -rf dist && tsc", | ||
"dev": "tsc --watch", | ||
"clean": "rm -rf dist" | ||
}, | ||
"dependencies": { | ||
"@beoe/rehype-code-hook": "workspace:*", | ||
"@ts-graphviz/ast": "^2.0.5", | ||
"@vizdom/vizdom-ts-esm": "^0.1.7", | ||
"svgo": "^3.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/hast": "^3.0.4", | ||
"rehype-stringify": "^10.0.0", | ||
"remark-parse": "^11.0.0", | ||
"remark-rehype": "^11.1.0", | ||
"unified": "^11.0.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import type { Plugin } from "unified"; | ||
import type { Root } from "hast"; | ||
import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; | ||
import { type Config as SvgoConfig } from "svgo"; | ||
import { parse, toModel } from "@ts-graphviz/ast"; | ||
import { processVizdomSvg } from "./vizdom.js"; | ||
import { DirectedGraph, VertexWeakRef } from "@vizdom/vizdom-ts-esm"; | ||
|
||
export async function getSvg(code: string) { | ||
const graph = new DirectedGraph(); | ||
const ast = parse(code); | ||
const model = toModel(ast); | ||
|
||
const nodes: Record<string, VertexWeakRef> = {}; | ||
model.nodes.forEach((node) => { | ||
nodes[node.id] = graph.new_vertex({ | ||
render: { | ||
id: node.id, | ||
label: node.attributes.get("label"), | ||
tooltip: node.attributes.get("tooltip"), | ||
fill_color: node.attributes.get("fillcolor") as string, | ||
font_color: node.attributes.get("fontcolor") as string, | ||
color: node.attributes.get("color") as string, | ||
font_size: node.attributes.get("fontsize"), | ||
pen_width: node.attributes.get("penwidth"), | ||
shape: node.attributes.get("shape") as any, | ||
style: node.attributes.get("style") as any, | ||
}, | ||
}); | ||
}); | ||
|
||
model.edges.forEach((edge) => { | ||
const from = edge.targets[0]; | ||
const to = edge.targets[0]; | ||
|
||
if (Array.isArray(from) || Array.isArray(to)) { | ||
throw new Error("what is it?"); | ||
} | ||
|
||
graph.new_edge(nodes[from.id], nodes[to.id], { | ||
render: { | ||
pen_width: edge.attributes.get("penwidth"), | ||
font_size: edge.attributes.get("fontsize"), | ||
style: edge.attributes.get("style") as any, | ||
// shape: edge.attributes.get("shape") as any, | ||
// curve: edge.attributes.get("curve") as any, | ||
arrow_head: edge.attributes.get("arrowhead") as any, | ||
dir: edge.attributes.get("dir") as any, | ||
color: edge.attributes.get("color") as string, | ||
font_color: edge.attributes.get("fontcolor") as string, | ||
fill_color: edge.attributes.get("fillcolor") as string, | ||
// id?: string; | ||
label: edge.attributes.get("label"), | ||
tooltip: edge.attributes.get("tooltip"), | ||
}, | ||
}); | ||
}); | ||
|
||
const positioned = graph.layout(); | ||
return await positioned.to_svg().to_string(); | ||
} | ||
|
||
export type RenderVizdomOptions = { | ||
code: string; | ||
class?: string; | ||
svgo?: SvgoConfig | boolean; | ||
}; | ||
|
||
export { processVizdomSvg }; | ||
|
||
export type RehypeVizdomConfig = { | ||
cache?: MapLike; | ||
class?: string; | ||
svgo?: SvgoConfig | boolean; | ||
}; | ||
|
||
export const rehypeVizdom: Plugin<[RehypeVizdomConfig?], Root> = ( | ||
options = {} | ||
) => { | ||
const salt = { class: options.class, svgo: options.svgo }; | ||
// @ts-expect-error | ||
return rehypeCodeHook({ | ||
...options, | ||
salt, | ||
language: "dot", | ||
code: ({ code }) => | ||
getSvg(code).then((str) => | ||
processVizdomSvg(str, options.class, options.svgo) | ||
), | ||
}); | ||
}; | ||
|
||
export default rehypeVizdom; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// SVGO is an experiment. I'm not sure it can compress a lot, plus it can break some diagrams | ||
import { optimize, type Config as SvgoConfig } from "svgo"; | ||
|
||
export type VizdomSvgOptions = { | ||
class?: string; | ||
}; | ||
|
||
const svgoConfig: SvgoConfig = { | ||
plugins: [ | ||
{ | ||
name: "preset-default", | ||
params: { | ||
overrides: { | ||
// disable a default plugin | ||
removeViewBox: false, | ||
}, | ||
}, | ||
}, | ||
], | ||
}; | ||
|
||
/** | ||
* removes `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` | ||
* removes `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"` | ||
* removes `width="..." height="..."` from svg tag | ||
* minifies SVG with `SVGO` | ||
* wraps in a figure with class `beoe vizdom` | ||
*/ | ||
export const processVizdomSvg = ( | ||
svg: string, | ||
className?: string, | ||
config?: SvgoConfig | boolean | ||
) => { | ||
svg = svg.split("\n").slice(6).join("\n"); | ||
if (config !== false) { | ||
svg = optimize( | ||
svg, | ||
config === undefined || config === true ? svgoConfig : config | ||
).data; | ||
} | ||
svg = svg.replace(/width="\d+[^"]+"\s+/, ""); | ||
svg = svg.replace(/height="\d+[^"]+"\s+/, ""); | ||
return `<figure class="beoe vizdom ${className || ""}">${svg}</figure>`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<figure class="beoe vizdom"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.41 116"><g class="graph" transform="translate(4 112)"><path fill="#fff" d="M-4 4v-116h79.41V4z"></path><g class="node"><ellipse cx="35.71" cy="-90" fill="none" stroke="#000" rx="32.49" ry="18"></ellipse><text x="35.71" y="-85.8" font-family="Times,serif" font-size="14" text-anchor="middle">Hello</text></g><g class="node"><ellipse cx="35.71" cy="-18" fill="none" stroke="#000" rx="35.71" ry="18"></ellipse><text x="35.71" y="-13.8" font-family="Times,serif" font-size="14" text-anchor="middle">World</text></g><g class="edge"><path fill="none" stroke="#000" d="M35.71-71.7v24.16"></path><path stroke="#000" d="m39.21-47.62-3.5 10-3.5-10z"></path></g></g></svg></figure> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```dot | ||
digraph G { Hello -> World } | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import fs from "node:fs/promises"; | ||
import { unified } from "unified"; | ||
import remarkParse from "remark-parse"; | ||
import remarkRehype from "remark-rehype"; | ||
import rehypeStringify from "rehype-stringify"; | ||
import { expect, it } from "vitest"; | ||
|
||
import rehypeGraphviz from "../src"; | ||
|
||
it("renders diagram", async () => { | ||
const file = await unified() | ||
.use(remarkParse) | ||
.use(remarkRehype) | ||
.use(rehypeGraphviz) | ||
.use(rehypeStringify) | ||
.process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); | ||
|
||
expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { expect, it } from "vitest"; | ||
|
||
import { processVizdomSvg } from "../src/vizdom"; | ||
|
||
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" | ||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||
<!-- Generated by vizdom version 10.0.1 (0) | ||
--> | ||
<!-- Title: G Pages: 1 --> | ||
<svg width="79pt" height="116pt" | ||
viewBox="0.00 0.00 79.41 116.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)"> | ||
<title>G</title> | ||
<polygon fill="white" stroke="none" points="-4,4 -4,-112 75.41,-112 75.41,4 -4,4"/> | ||
<!-- Hello --> | ||
</g> | ||
</svg>`; | ||
|
||
it("removes xml doctype", async () => { | ||
const result = processVizdomSvg(svg); | ||
|
||
expect(result).toMatchInlineSnapshot( | ||
`"<figure class="beoe vizdom "><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.41 116"><g class="graph" transform="translate(4 112)"><path fill="#fff" d="M-4 4v-116h79.41V4z"/></g></svg></figure>"` | ||
); | ||
}); | ||
|
||
it("removes width and height", async () => { | ||
const result = processVizdomSvg(svg); | ||
|
||
expect(result).not.toContain(`width=`); | ||
expect(result).not.toContain(`height=`); | ||
}); | ||
|
||
it("wraps in a figure with classes", async () => { | ||
const result = processVizdomSvg(svg); | ||
|
||
expect(result).toContain(`<figure class="beoe vizdom`); | ||
}); | ||
|
||
it("is possible to add class", async () => { | ||
const result = processVizdomSvg(svg, "not-content"); | ||
|
||
expect(result).toContain(`<figure class="beoe vizdom not-content`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"$schema": "https://json.schemastore.org/tsconfig", | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "./dist", | ||
"declaration": true, | ||
"noEmit": false | ||
}, | ||
"include": ["./src"], | ||
"rootDir": "./src" | ||
} |
Oops, something went wrong.