From 76c1a4eac264df81533da8650c8596f3c1f16e1f Mon Sep 17 00:00:00 2001 From: stereobooster Date: Sat, 4 May 2024 22:35:23 +0200 Subject: [PATCH] tmp --- README.md | 4 +- packages/demo/astro.config.mjs | 5 +- .../src/content/docs/examples/graphviz.mdx | 72 ++++++++ .../demo/src/content/docs/examples/mermaid.md | 4 + packages/demo/src/styles/custom.css | 9 +- packages/rehype-graphviz/src/index.ts | 26 ++- packages/rehype-mermaid/package.json | 9 +- packages/rehype-mermaid/src/index.ts | 170 ++++++++++++------ pnpm-lock.yaml | 106 +++++++++++ 9 files changed, 342 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 6c2ca82..b99d62e 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,10 @@ Ideas for other diagrams: https://stereobooster.com/posts/text-to-diagram/. ## TODO +- [ ] implement dark mode for mermaid with picture/classes +- [ ] SVGO? - [ ] fix `@beoe/astro-graphviz` - - why I can't use `cache` package? + - why I can't use `@beoe/cache` package? - [ ] example of gnuplot custom diagram - any diagram which expects `input.dat`, for example https://gnuplot.sourceforge.net/demo_svg_5.4/histograms.html - maybe do it like [asciidoctor does for penrose](https://docs.asciidoctor.org/diagram-extension/latest/diagram_types/penrose/)? diff --git a/packages/demo/astro.config.mjs b/packages/demo/astro.config.mjs index df0802d..fdb6940 100644 --- a/packages/demo/astro.config.mjs +++ b/packages/demo/astro.config.mjs @@ -32,7 +32,10 @@ export default defineConfig({ markdown: { rehypePlugins: [ [rehypeGraphviz, { class: className }], - [rehypeMermaid, { cache, class: className }], + [ + rehypeMermaid, + { cache, class: className, strategy: "class-dark-mode" }, + ], [rehypeGnuplot, { cache, class: className }], ], }, diff --git a/packages/demo/src/content/docs/examples/graphviz.mdx b/packages/demo/src/content/docs/examples/graphviz.mdx index 7eb8f2e..aa41824 100644 --- a/packages/demo/src/content/docs/examples/graphviz.mdx +++ b/packages/demo/src/content/docs/examples/graphviz.mdx @@ -30,6 +30,78 @@ digraph finite_state_machine { } ``` +```dot +graph grid { + bgcolor="transparent"; + label="grid" + labelloc = "t" + node [shape=plaintext] + // arbitrary path on rigid grid + A0 -- B1 -- C2 -- D3 -- E4 -- F5 -- G6 -- H7 + H0 -- G1 -- F2 -- E3 -- D4 -- C5 -- B6 -- A7 + + edge [weight=1000 style=dashed color=dimgrey] + + // uncomment to hide the grid + //edge [style=invis] + + A0 -- A1 -- A2 -- A3 -- A4 -- A5 -- A6 -- A7 + B0 -- B1 -- B2 -- B3 -- B4 -- B5 -- B6 -- B7 + C0 -- C1 -- C2 -- C3 -- C4 -- C5 -- C6 -- C7 + D0 -- D1 -- D2 -- D3 -- D4 -- D5 -- D6 -- D7 + E0 -- E1 -- E2 -- E3 -- E4 -- E5 -- E6 -- E7 + F0 -- F1 -- F2 -- F3 -- F4 -- F5 -- F6 -- F7 + G0 -- G1 -- G2 -- G3 -- G4 -- G5 -- G6 -- G7 + H0 -- H1 -- H2 -- H3 -- H4 -- H5 -- H6 -- H7 + + rank=same {A0 -- B0 -- C0 -- D0 -- E0 -- F0 -- G0 -- H0} + rank=same {A1 -- B1 -- C1 -- D1 -- E1 -- F1 -- G1 -- H1} + rank=same {A2 -- B2 -- C2 -- D2 -- E2 -- F2 -- G2 -- H2} + rank=same {A3 -- B3 -- C3 -- D3 -- E3 -- F3 -- G3 -- H3} + rank=same {A4 -- B4 -- C4 -- D4 -- E4 -- F4 -- G4 -- H4} + rank=same {A5 -- B5 -- C5 -- D5 -- E5 -- F5 -- G5 -- H5} + rank=same {A6 -- B6 -- C6 -- D6 -- E6 -- F6 -- G6 -- H6} + rank=same {A7 -- B7 -- C7 -- D7 -- E7 -- F7 -- G7 -- H7} +} +``` + +```dot +digraph { + bgcolor="transparent"; + graph [rankdir=LR]; + node [shape=record]; + 0 [label="0 | [• S, $]\n[S → • a S b, $]\n[S → •, $]"]; + 1 [label="1 | [S •, $]"]; + 2 [label="2 | [S → a • S b, $]\n[S → • a S b, b]\n[S → •, b]"]; + 3 [label="3 | [S → a S • b, $]"]; + 4 [label="4 | [S → a • S b, b]\n[S → • a S b, b]\n[S → •, b]"]; + 5 [label="5 | [S → a S b •, $]"]; + 6 [label="6 | [S → a S • b, b]"]; + 7 [label="7 | [S → a S b •, b]"]; + 0 -> 1 [label=S]; + 0 -> 2 [label=a]; + 2 -> 3 [label=S]; + 2 -> 4 [label=a]; + 3 -> 5 [label=b]; + 4 -> 6 [label=S]; + 4 -> 4 [label=a]; + 6 -> 7 [label=b]; +} +``` + +```dot +graph { + layout=patchwork + node [style=filled] + "$2" [area=200 fillcolor=gold] + "$1" [area=100 fillcolor=gold] + "50c" [area= 50 fillcolor=silver] + "20c" [area= 20 fillcolor=silver] + "10c" [area= 10 fillcolor=silver] + "5c" [area= 5 fillcolor=silver] +} +``` + ## astro-component import { Graphviz } from "@beoe/astro-graphviz"; diff --git a/packages/demo/src/content/docs/examples/mermaid.md b/packages/demo/src/content/docs/examples/mermaid.md index dc1cf80..aee7aca 100644 --- a/packages/demo/src/content/docs/examples/mermaid.md +++ b/packages/demo/src/content/docs/examples/mermaid.md @@ -93,6 +93,8 @@ classDiagram } ``` +**TODO**: this doesn't work with dark mode + ```mermaid --- title: Simple sample @@ -199,6 +201,8 @@ gitGraph commit ``` +**TODO**: this doesn't work with dark mode + ```mermaid C4Context title System Context diagram for Internet Banking System diff --git a/packages/demo/src/styles/custom.css b/packages/demo/src/styles/custom.css index 03fcf9a..a3a4f73 100644 --- a/packages/demo/src/styles/custom.css +++ b/packages/demo/src/styles/custom.css @@ -18,11 +18,10 @@ } } -.mermaid svg { - background: #fff; - line-height: 1; +html[data-theme="light"] .beoe-dark { + display: none; } -.beoe { - @apply not-content; +html[data-theme="dark"] .beoe-light { + display: none; } diff --git a/packages/rehype-graphviz/src/index.ts b/packages/rehype-graphviz/src/index.ts index 94d51d4..5674c95 100644 --- a/packages/rehype-graphviz/src/index.ts +++ b/packages/rehype-graphviz/src/index.ts @@ -3,12 +3,32 @@ import type { Root } from "hast"; import { processGraphvizSvg } from "./graphviz.js"; import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; -export type RenderGraphvizOptions = { code: string; class?: string }; +// 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 file itself */ + engine?: Engine; +}; import { Graphviz } from "@hpcc-js/wasm"; const graphviz = await Graphviz.load(); export const renderGraphviz = (o: RenderGraphvizOptions) => - processGraphvizSvg(graphviz.dot(o.code), o.class); + processGraphvizSvg( + graphviz.layout(o.code, "svg", o.engine || "dot"), + o.class + ); // import { waitFor } from "@beoe/rehype-code-hook"; /** @@ -36,7 +56,7 @@ export type RehypeGraphvizConfig = { export const rehypeGraphviz: Plugin<[RehypeGraphvizConfig?], Root> = ( options = {} ) => { - const salt = { class: options.class }; + const salt = { class: options.class }; // @ts-expect-error return rehypeCodeHook({ ...options, diff --git a/packages/rehype-mermaid/package.json b/packages/rehype-mermaid/package.json index dc4ee5d..25a2788 100644 --- a/packages/rehype-mermaid/package.json +++ b/packages/rehype-mermaid/package.json @@ -33,13 +33,16 @@ }, "dependencies": { "@beoe/rehype-code-hook": "workspace:*", - "mermaid-isomorphic": "^2.1.2" + "hastscript": "^9.0.0", + "mermaid-isomorphic": "^2.1.2", + "mini-svg-data-uri": "^1.4.4", + "svgo": "^3.2.0" }, "devDependencies": { "@types/hast": "^3.0.4", - "unified": "^11.0.4", "rehype-stringify": "^10.0.0", "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.0" + "remark-rehype": "^11.1.0", + "unified": "^11.0.4" } } diff --git a/packages/rehype-mermaid/src/index.ts b/packages/rehype-mermaid/src/index.ts index b82aa60..1fc80c5 100644 --- a/packages/rehype-mermaid/src/index.ts +++ b/packages/rehype-mermaid/src/index.ts @@ -5,80 +5,150 @@ import { createMermaidRenderer, type CreateMermaidRendererOptions, type RenderOptions, + type RenderResult, } from "mermaid-isomorphic"; +import svgToMiniDataURI from "mini-svg-data-uri"; +import { optimize, type Config as SvgoConfig } from "svgo"; +import { h } from "hastscript"; export type RehypeMermaidConfig = RenderOptions & CreateMermaidRendererOptions & { cache?: MapLike; class?: string; + svgo?: SvgoConfig | false; + strategy?: "inline" | "picture" | "class-dark-mode"; }; +function pictureDarkMode( + light: RenderResult, + dark: RenderResult, + klas?: string +) { + const imgLight = h("img", { + width: light.width, + height: light.height, + alt: light.description || light.title || "", + src: svgToMiniDataURI(light.svg), + }); + + const imgDark = h("source", { + width: dark.width, + height: dark.height, + src: svgToMiniDataURI(dark.svg), + media: `(prefers-color-scheme: dark)`, + }); + + return h( + "figure", + { + class: `beoe mermaid ${klas || ""}`, + }, + h("picture", [imgLight, imgDark]) + ); +} + +function classDarkMode(light: RenderResult, dark: RenderResult, klas?: string) { + const imgLight = h("img", { + width: light.width, + height: light.height, + alt: light.description || light.title || "", + src: svgToMiniDataURI(light.svg), + class: "beoe-light", + }); + + const imgDark = h("img", { + width: dark.width, + height: dark.height, + alt: dark.description || dark.title || "", + src: svgToMiniDataURI(dark.svg), + class: "beoe-dark", + }); + + return h( + "figure", + { + class: `beoe mermaid ${klas || ""}`, + }, + [imgLight, imgDark] + ); +} + export const rehypeMermaid: Plugin<[RehypeMermaidConfig?], Root> = ( options = {} ) => { - const { css, mermaidConfig, prefix, browser, launchOptions, ...rest } = - options; + const { + css, + mermaidConfig, + prefix, + browser, + launchOptions, + svgo, + strategy, + ...rest + } = options; const renerOptions = { css, mermaidConfig: mermaidConfig || {}, prefix }; const createOptions = { browser, launchOptions }; const renderDiagrams = createMermaidRenderer(createOptions); - const salt = { ...renerOptions, ...createOptions, class: rest.class }; + const salt = { + ...renerOptions, + ...createOptions, + class: rest.class, + svgo, + strategy, + }; + + async function render(code: string, dark?: boolean) { + const config = structuredClone(renerOptions); + // otherwise all diagrams would have same ID and styles would collide + config.prefix = `m${Math.random().toString().replace(".", "")}`; + + if (dark) { + // config.mermaidConfig.darkMode = true; + config.mermaidConfig.theme = "dark"; + } + + const [x] = await renderDiagrams([code], config); + + if (x.status === "fulfilled") { + if (svgo !== false) { + x.value.svg = optimize(x.value.svg, svgo).data; + } + return x.value; + } else { + throw new Error(x.reason); + } + } + // @ts-expect-error return rehypeCodeHook({ ...rest, salt, language: "mermaid", - code: ({ code }) => { - const c = structuredClone(renerOptions); - // otherwise all diagrams would have same ID and styles would collide - c.prefix = `m${Math.random().toString().replace(".", "")}`; - return renderDiagrams([code], c).then(([x]) => { - if (x.status === "fulfilled") { + code: async ({ code }) => { + switch (strategy) { + case "class-dark-mode": { + const [light, dark] = await Promise.all([ + render(code), + render(code, true), + ]); + return classDarkMode(light, dark, options.class); + } + case "picture": { + const [light, dark] = await Promise.all([ + render(code), + render(code, true), + ]); + return pictureDarkMode(light, dark, options.class); + } + default: { + const light = await render(code); return `
${ - x.value.svg + light.svg }
`; - } else { - throw new Error(x.reason); } - }); + } }, }); }; export default rehypeMermaid; - -// Tried to combine dark/light mode CSS in one style. It doesn't work -// const dark = structuredClone(renerOptions); -// dark.mermaidConfig.theme = "dark"; -// -// code: ({ code }) => { -// return Promise.all([ -// renderDiagrams([code], renerOptions), -// renderDiagrams([code], dark), -// ]).then(([[x], [y]]) => { -// if (x.status === "fulfilled" && y.status === "fulfilled") { -// const xStyle = /`; -// -// const svg = x.value.svg.replace(xStyle?.[0] || "", joinedStyles); -// -// return `
${svg}
`; -// } else { -// // @ts-expect-error -// throw new Error(x.reason || y.resaon); -// } -// }); -// }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bde66e..38419f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,9 +183,18 @@ importers: '@beoe/rehype-code-hook': specifier: workspace:* version: link:../rehype-code-hook + hastscript: + specifier: ^9.0.0 + version: 9.0.0 mermaid-isomorphic: specifier: ^2.1.2 version: 2.1.2 + mini-svg-data-uri: + specifier: ^1.4.4 + version: 1.4.4 + svgo: + specifier: ^3.2.0 + version: 3.2.0 devDependencies: '@types/hast': specifier: ^3.0.4 @@ -1798,6 +1807,11 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: false + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: false + /@types/acorn@4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} dependencies: @@ -2704,15 +2718,53 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + /css-selector-parser@3.0.5: resolution: {integrity: sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==} dev: false + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.0 + dev: false + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.0 + dev: false + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: false + /cytoscape-cose-bilkent@4.1.0(cytoscape@3.29.2): resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} peerDependencies: @@ -3092,10 +3144,37 @@ packages: /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + /dompurify@3.1.1: resolution: {integrity: sha512-tVP8C/GJwnABOn/7cx/ymx/hXpmBfWIPihC1aOEvS8GbMqy3pgeYtJk1HXN3CO7tu+8bpY18f6isjR5Cymj0TQ==} dev: false + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + /dset@3.1.3: resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} engines: {node: '>=4'} @@ -4250,6 +4329,14 @@ packages: dependencies: '@types/mdast': 4.0.3 + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: false + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4807,6 +4894,11 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + /mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + dev: false + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -5793,6 +5885,20 @@ packages: resolution: {integrity: sha512-HQ6YvK7LlyDDJwYvnGbqwtJ9jlUqG+8KzCY8baYtg3TO274LBL58wD1+9eHW+29Bv8opAhAQHwJtWWNVKi7iiA==} dev: false + /svgo@3.2.0: + resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==} + engines: {node: '>=14.0.0'} + hasBin: true + 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 + dev: false + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: