diff --git a/packages/demo/src/components/graphviz.ts b/packages/demo/src/components/graphviz.ts index e0fb594..0df9412 100644 --- a/packages/demo/src/components/graphviz.ts +++ b/packages/demo/src/components/graphviz.ts @@ -4,8 +4,8 @@ type D = { [node: string]: Path }; // interactivity for graphviz diagrams document.querySelectorAll(".graphviz").forEach((container) => { - const data = container.getAttribute("data-graph") - ? JSON.parse(container.getAttribute("data-graph")!) + const data = container.getAttribute("data-beoe") + ? JSON.parse(container.getAttribute("data-beoe")!) : null; if (!data) return; diff --git a/packages/rehype-graphviz/package.json b/packages/rehype-graphviz/package.json index 98b8e91..05616ff 100644 --- a/packages/rehype-graphviz/package.json +++ b/packages/rehype-graphviz/package.json @@ -32,11 +32,9 @@ "clean": "rm -rf dist" }, "dependencies": { + "@beoe/rehype-code-hook-img": "workspace:*", "@beoe/rehype-code-hook": "workspace:*", - "@beoe/fenceparser": "workspace:*", - "@hpcc-js/wasm": "^2.16.1", - "hast-util-from-html-isomorphic": "^2.0.0", - "svgo": "^3.2.0" + "@hpcc-js/wasm": "^2.16.1" }, "devDependencies": { "@types/hast": "^3.0.4", diff --git a/packages/rehype-graphviz/src/graphviz.ts b/packages/rehype-graphviz/src/graphviz.ts deleted file mode 100644 index ff17250..0000000 --- a/packages/rehype-graphviz/src/graphviz.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { fromHtmlIsomorphic } from "hast-util-from-html-isomorphic"; -// 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 GraphvizSvgOptions = { - class?: string; -}; - -const svgoConfig: SvgoConfig = { - plugins: [ - { - name: "preset-default", - params: { - overrides: { - // we need viewBox because width and height are removed - removeViewBox: false, - // we need ids for client-side interactivity - cleanupIds: false, - // this allows to style rects - // convertShapeToPath: false, - }, - }, - }, - // @ts-ignore - { - name: "removeAttrs", - params: { - attrs: ".*;(title|xlink:title)", - elemSeparator: ";", - preserveCurrentColor: false, - }, - }, - ], -}; - -/** - * removes `` - * removes ` { - svg = svg.split("\n").slice(6).join("\n"); - if (config !== false) { - // @ts-expect-error - svgoConfig.plugins[0].params.overrides.cleanupIds = !graph; - svg = optimize( - svg, - config === undefined || config === true ? svgoConfig : config - ).data; - } - svg = svg.replace(/width="\d+[^"]+"\s+/, ""); - svg = svg.replace(/height="\d+[^"]+"\s+/, ""); - const element = fromHtmlIsomorphic(svg, { fragment: true }); - return { - type: "element", - tagName: "figure", - properties: { - class: `beoe graphviz ${className || ""}`.trim(), - "data-graph": graph ? JSON.stringify(graph) : undefined, - }, - children: element.children, - }; -}; diff --git a/packages/rehype-graphviz/src/index.ts b/packages/rehype-graphviz/src/index.ts index 026b120..f5a7118 100644 --- a/packages/rehype-graphviz/src/index.ts +++ b/packages/rehype-graphviz/src/index.ts @@ -1,152 +1,14 @@ +// @ts-expect-error The inferred type of '...' cannot be named without a reference to import type { Plugin } from "unified"; +// @ts-expect-error The inferred type of '...' cannot be named without a reference to import type { Root } from "hast"; -import { processGraphvizSvg } from "./graphviz.js"; -import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; -import { type Config as SvgoConfig } from "svgo"; -import parse from "@beoe/fenceparser"; - -function processMeta(meta?: string): Record { - return meta ? parse(meta, { lowerCase: false }) : {}; -} - -// it is possible to add other formats, like Graphology etc. -type GraphFormat = "dagre" | "graphology"; - -// import { type Engine } from "@hpcc-js/wasm"; -type Engine = - | "circo" - | "dot" - | "fdp" - | "sfdp" - | "neato" - | "osage" - | "patchwork" - | "twopi" - | "nop" - | "nop2"; - -export type RenderGraphvizOptions = { - code: string; - class?: string; - /* it is possible to change layout with in dot code itself */ - engine?: Engine; - svgo?: SvgoConfig | boolean; - dataGraph?: GraphFormat; -}; - -// import { Graphviz } from "@hpcc-js/wasm"; -// const graphviz = await Graphviz.load(); -// export const renderGraphviz = (o: RenderGraphvizOptions) => -// processGraphvizSvg( -// graphviz.layout(o.code, "svg", o.engine || "dot"), -// o.class -// ); - -import { waitFor } from "@beoe/rehype-code-hook"; -/** - * If all graphviz diagrams are cached it would not even load module in memory. - * If there are diagrams, it would load module and first few renders would be async, - * but all consequent renders would be sync - */ -export const renderGraphviz = waitFor( - async () => { - // @ts-ignore - const Graphviz = (await import("@hpcc-js/wasm")).Graphviz; - return await Graphviz.load(); - }, - (graphviz) => (o: RenderGraphvizOptions) => { - let code = o.code; - let graph; - if (o.dataGraph) { - // without this I can't get consistent ids for JSON and SVG outputs - code = graphviz.unflatten(code); - code = graphviz.nop(code); - - const obj = JSON.parse( - graphviz.layout(code, "dot_json", o.engine || "dot") - ); - - if (o.dataGraph === "graphology") { - graph = { - attributes: { name: "g" }, - options: { - allowSelfLoops: true, - multi: true, - type: obj.directed ? "directed" : "mixed", - }, - nodes: obj.objects.map((node: any) => ({ - key: node._gvid + 1, - })), - edges: obj.edges.map((edge: any) => ({ - key: edge._gvid + 1, - source: edge.tail + 1, - target: edge.head + 1, - })), - }; - } - - if (o.dataGraph === "dagre") { - graph = { - options: { - directed: obj.directed, - multigraph: true, - compound: false, - }, - nodes: obj.objects.map((node: any) => ({ - v: node.unique_id + 1, - })), - edges: obj.edges.map((edge: any) => ({ - v: edge.tail + 1, - w: edge.head + 1, - name: edge._gvid + 1, - })), - }; - } - } - - return processGraphvizSvg( - graphviz.layout(code, "svg", o.engine || "dot"), - o.class, - o.svgo, - graph - ); - } -); - -export { processGraphvizSvg }; - -export type RehypeGraphvizConfig = { - cache?: MapLike; - class?: string; - svgo?: SvgoConfig | boolean; - // adds data-graph to each figure with JSON representation of graph - dataGraph?: GraphFormat; -}; - -export const rehypeGraphviz: Plugin<[RehypeGraphvizConfig?], Root> = ( - options = {} -) => { - const salt = { class: options.class, svgo: options.svgo }; - // @ts-expect-error - return rehypeCodeHook({ - ...options, - salt, - language: "dot", - code: ({ code, meta }) => { - const metaOptions = processMeta(meta); - const dataGraph = - metaOptions.datagraph !== undefined - ? metaOptions.datagraph - : options.dataGraph; - const cssClass = `${options.class || ""} ${ - metaOptions.class || "" - }`.trim(); - const svgo = - metaOptions.svgo !== undefined ? metaOptions.svgo : options.svgo; - - return renderGraphviz({ code, class: cssClass, svgo, dataGraph }); - }, - }); -}; +import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; +import { RehypeGraphvizConfig, renderGraphviz } from "./render.js"; + +export const rehypeGraphviz = rehypeCodeHookImg({ + language: "dot", + class: "graphviz", + render: (code, options) => renderGraphviz({ ...options, code }), +}); export default rehypeGraphviz; diff --git a/packages/rehype-graphviz/src/render.ts b/packages/rehype-graphviz/src/render.ts new file mode 100644 index 0000000..3dd26f3 --- /dev/null +++ b/packages/rehype-graphviz/src/render.ts @@ -0,0 +1,101 @@ +/** + * it is possible to add other formats + */ +export type DataFormat = "dagre" | "graphology"; + +// import { type Engine } from "@hpcc-js/wasm"; +type Engine = + | "circo" + | "dot" + | "fdp" + | "sfdp" + | "neato" + | "osage" + | "patchwork" + | "twopi" + | "nop" + | "nop2"; + +export type RehypeGraphvizConfig = { + dataGraph: DataFormat; + engine: Engine; +}; + +type RenderGraphvizOptions = RehypeGraphvizConfig & { + code: string; +}; + +import { waitFor } from "@beoe/rehype-code-hook"; +/** + * If all graphviz diagrams are cached it would not even load module in memory. + * If there are diagrams, it would load module and first few renders would be async, + * but all consequent renders would be sync + */ +export const renderGraphviz = waitFor( + async () => { + // @ts-ignore + const Graphviz = (await import("@hpcc-js/wasm")).Graphviz; + return await Graphviz.load(); + }, + (graphviz) => + ({ code, engine, dataGraph }: RenderGraphvizOptions) => { + let data; + if (dataGraph) { + // without this I can't get consistent ids for JSON and SVG outputs + code = graphviz.unflatten(code); + code = graphviz.nop(code); + + const obj = JSON.parse( + graphviz.layout(code, "dot_json", engine || "dot") + ); + + if (dataGraph === "graphology") { + data = { + attributes: { name: "g" }, + options: { + allowSelfLoops: true, + multi: true, + type: obj.directed ? "directed" : "mixed", + }, + nodes: obj.objects.map((node: any) => ({ + key: node._gvid + 1, + })), + edges: obj.edges.map((edge: any) => ({ + key: edge._gvid + 1, + source: edge.tail + 1, + target: edge.head + 1, + })), + }; + } + + if (dataGraph === "dagre") { + data = { + options: { + directed: obj.directed, + multigraph: true, + compound: false, + }, + nodes: obj.objects.map((node: any) => ({ + v: node.unique_id + 1, + })), + edges: obj.edges.map((edge: any) => ({ + v: edge.tail + 1, + w: edge.head + 1, + name: edge._gvid + 1, + })), + }; + } + } + + return { + svg: cleanup(graphviz.layout(code, "svg", engine || "dot")), + data, + }; + } +); + +/** + * removes `` + * removes ` svg.split("\n").slice(6).join("\n"); diff --git a/packages/rehype-graphviz/test/graphviz.test.ts b/packages/rehype-graphviz/test/graphviz.test.ts deleted file mode 100644 index 7fedcd8..0000000 --- a/packages/rehype-graphviz/test/graphviz.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { expect, it } from "vitest"; - -import { processGraphvizSvg } from "../src/graphviz"; - -const svg = ` - - - - - -G - - - -`; - -it("removes xml doctype", async () => { - const result = processGraphvizSvg(svg); - - expect(result).toMatchInlineSnapshot( - ` - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [], - "properties": { - "d": "M-4 4v-116h79.41V4z", - "fill": "#fff", - }, - "tagName": "path", - "type": "element", - }, - ], - "properties": { - "className": [ - "graph", - ], - "transform": "translate(4 112)", - }, - "tagName": "g", - "type": "element", - }, - ], - "properties": { - "viewBox": "0 0 79.41 116", - "xmlns": "http://www.w3.org/2000/svg", - }, - "tagName": "svg", - "type": "element", - }, - ], - "properties": { - "class": "beoe graphviz", - "data-graph": undefined, - }, - "tagName": "figure", - "type": "element", - } - ` - ); -}); - -it.skip("removes width and height", async () => { - const result = processGraphvizSvg(svg); -}); - -it("wraps in a figure with classes", async () => { - const result = processGraphvizSvg(svg); - - expect(result.type).toEqual("element"); - expect(result.tagName).toEqual("figure"); -}); - -it("is possible to add class", async () => { - const result = processGraphvizSvg(svg, "not-content"); - - expect(result.properties.class).toEqual("beoe graphviz not-content"); -}); diff --git a/packages/rehype-plantuml/package.json b/packages/rehype-plantuml/package.json index c174e2d..81f9637 100644 --- a/packages/rehype-plantuml/package.json +++ b/packages/rehype-plantuml/package.json @@ -32,11 +32,8 @@ "clean": "rm -rf dist" }, "dependencies": { - "@beoe/rehype-code-hook": "workspace:*", - "hastscript": "^9.0.0", - "mini-svg-data-uri": "^1.4.4", - "plantuml": "^0.0.2", - "svgo": "^3.2.0" + "@beoe/rehype-code-hook-img": "workspace:*", + "plantuml": "^0.0.2" }, "devDependencies": { "@types/hast": "^3.0.4", diff --git a/packages/rehype-plantuml/src/index.ts b/packages/rehype-plantuml/src/index.ts index 204ca46..becb307 100644 --- a/packages/rehype-plantuml/src/index.ts +++ b/packages/rehype-plantuml/src/index.ts @@ -1,102 +1,24 @@ +// @ts-expect-error The inferred type of '...' cannot be named without a reference to import type { Plugin } from "unified"; +// @ts-expect-error The inferred type of '...' cannot be named without a reference to import type { Root } from "hast"; -import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; +import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; // @ts-ignore import plantuml from "plantuml"; -import svgToMiniDataURI from "mini-svg-data-uri"; -import { h } from "hastscript"; -// 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"; -const svgoConfig: SvgoConfig = { - plugins: [ - { - name: "preset-default", - params: { - overrides: { - // we need viewbox for inline SVGs - removeViewBox: false, - // this breaks statediagram - convertShapeToPath: false, - }, - }, - }, - ], -}; +export type RehypePlantumlConfig = {}; -export type RehypePlantumlConfig = { - cache?: MapLike; - class?: string; - /** - * be carefull. It may break some diagrams. For example, stateDiagram-v2 - */ - svgo?: SvgoConfig | boolean; - strategy?: "inline" | "img"; -}; - -export const rehypePlantuml: Plugin<[RehypePlantumlConfig?], Root> = ( - options = {} -) => { - const { svgo, strategy, ...rest } = options; - - const salt = options; - - const render = async (code: string) => { +export const rehypePlantuml = rehypeCodeHookImg({ + language: "plantuml", + render: async (code: string) => { let svg: string = await plantuml(code); - if (svgo !== false) { - svg = optimize( - svg, - svgo === undefined || svgo === true ? svgoConfig : svgo - ).data; - } svg = svg.replace(`contentStyleType="text/css"`, ""); svg = svg.replace(`preserveAspectRatio="none"`, ""); svg = svg.replace(`zoomAndPan="magnify"`, ""); svg = svg.replace(/style="[^"]+"\s+/, ""); - const widthMatch = svg.match(/width="(\d+[^"]+)"\s+/); - const width = widthMatch ? widthMatch[1] : undefined; - - const heightMatch = svg.match(/height="(\d+[^"]+)"\s+/); - const height = heightMatch ? heightMatch[1] : undefined; - - svg = svg.replace(/width="\d+[^"]+"\s+/, ""); - svg = svg.replace(/height="\d+[^"]+"\s+/, ""); - return { svg, width, height }; - }; - - // @ts-expect-error - return rehypeCodeHook({ - ...rest, - salt, - language: "plantuml", - code: async ({ code }) => { - switch (strategy) { - case "img": { - const { svg, width, height } = await render(code); - return h( - "figure", - { - class: `beoe plantuml ${rest.class || ""}`, - }, - // wrap in additional div for svg-pan-zoom - h("img", { - width, - height, - alt: "", - src: svgToMiniDataURI(svg), - }) - ); - } - default: { - const { svg } = await render(code); - return `
${svg}
`; - } - } - }, - }); -}; + return { svg }; + }, +}); export default rehypePlantuml; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2bbedd..3e5f964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,21 +203,15 @@ importers: packages/rehype-graphviz: dependencies: - '@beoe/fenceparser': - specifier: workspace:* - version: link:../fenceparser '@beoe/rehype-code-hook': specifier: workspace:* version: link:../rehype-code-hook + '@beoe/rehype-code-hook-img': + specifier: workspace:* + version: link:../rehype-code-hook-img '@hpcc-js/wasm': specifier: ^2.16.1 version: 2.16.1 - hast-util-from-html-isomorphic: - specifier: ^2.0.0 - version: 2.0.0 - svgo: - specifier: ^3.2.0 - version: 3.2.0 devDependencies: '@types/hast': specifier: ^3.0.4 @@ -265,21 +259,12 @@ importers: packages/rehype-plantuml: dependencies: - '@beoe/rehype-code-hook': + '@beoe/rehype-code-hook-img': specifier: workspace:* - version: link:../rehype-code-hook - hastscript: - specifier: ^9.0.0 - version: 9.0.0 - mini-svg-data-uri: - specifier: ^1.4.4 - version: 1.4.4 + version: link:../rehype-code-hook-img plantuml: specifier: ^0.0.2 version: 0.0.2 - svgo: - specifier: ^3.2.0 - version: 3.2.0 devDependencies: '@types/hast': specifier: ^3.0.4 @@ -3051,11 +3036,6 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} - svgo@3.2.0: - resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==} - engines: {node: '>=14.0.0'} - hasBin: true - svgo@3.3.2: resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} engines: {node: '>=14.0.0'} @@ -7015,16 +6995,6 @@ snapshots: dependencies: has-flag: 3.0.0 - svgo@3.2.0: - dependencies: - '@trysound/sax': 0.2.0 - commander: 7.2.0 - css-select: 5.1.0 - css-tree: 2.3.1 - css-what: 6.1.0 - csso: 5.0.5 - picocolors: 1.0.0 - svgo@3.3.2: dependencies: '@trysound/sax': 0.2.0