From 75b12e56da95cd3b992cfdb7765b1a8cee60492f Mon Sep 17 00:00:00 2001 From: stereobooster Date: Wed, 20 Nov 2024 12:57:01 +0100 Subject: [PATCH] refactoring --- README.md | 7 + packages/demo/astro.config.mjs | 2 +- .../demo/src/content/docs/examples/d2-test.md | 4 +- packages/rehype-code-hook/src/index.ts | 61 ++++---- packages/rehype-d2/src/index.ts | 92 +++--------- packages/rehype-d2/src/svgStrategy.ts | 142 ++++++++++++++++++ .../rehype-d2/test/fixtures/a-inline.html | 2 +- .../rehype-d2/test/fixtures/a1-datauri.html | 2 +- 8 files changed, 206 insertions(+), 106 deletions(-) create mode 100644 packages/rehype-d2/src/svgStrategy.ts diff --git a/README.md b/README.md index 4727dda..e4290e1 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,10 @@ Ideas for other diagrams: [Text to Diagram](https://stereobooster.com/posts/text ## Logo The logo is an illustration from [Oliver Byrne's Elements of Euclid: The First Six Books with Coloured Diagrams and Symbols](https://www.c82.net/euclid/). + +## TODO + +- [ ] try to implement external file strategy (with vfile) +- [ ] reuse [svgStrategy](packages/rehype-d2/src/svgStrategy.ts) in all plugins +- [ ] add `processMeta` to all plugins, so it would be possible to configure `class` and `strategy` +- [ ] maybe rename strategies diff --git a/packages/demo/astro.config.mjs b/packages/demo/astro.config.mjs index 9c234aa..422a65c 100644 --- a/packages/demo/astro.config.mjs +++ b/packages/demo/astro.config.mjs @@ -44,7 +44,7 @@ export default defineConfig({ // { cache, class: className, strategy: "img-class-dark-mode" }, // ], [rehypeGnuplot, { cache, class: className }], - [rehypeD2, {}], //, { cache, class: className }], + [rehypeD2, { cache, strategy: "img-class-dark-mode", class: className }], ], }, vite: { diff --git a/packages/demo/src/content/docs/examples/d2-test.md b/packages/demo/src/content/docs/examples/d2-test.md index fce009e..2fd1869 100644 --- a/packages/demo/src/content/docs/examples/d2-test.md +++ b/packages/demo/src/content/docs/examples/d2-test.md @@ -34,9 +34,9 @@ direction: right a2 -> b -> c -> d -> e ``` -### `img-class-dark-mode` +### `strategy=inline` -```d2 theme=101 darkTheme=200 strategy=img-class-dark-mode +```d2 strategy=inline direction: right a -> b -> c -> d -> e ``` diff --git a/packages/rehype-code-hook/src/index.ts b/packages/rehype-code-hook/src/index.ts index 8246766..9e3af2e 100644 --- a/packages/rehype-code-hook/src/index.ts +++ b/packages/rehype-code-hook/src/index.ts @@ -37,7 +37,9 @@ function replace( // @ts-expect-error element = element.children[0]; } else { - console.warn("When you pass string to rehype-code-hook make sure it has only one root element") + console.warn( + "When you pass string to rehype-code-hook make sure it has only one root element" + ); } } // @ts-expect-error @@ -138,36 +140,41 @@ export const rehypeCodeHook: Plugin<[RehypeCodeHookOptions], Root> = ( let newNode: NewNode; if (options.cache) { - let propsWithSalt: string | Buffer = serialize({ - ...props, - cb, - salt: options.salt, - }); - if (options.hashTostring === true) { - propsWithSalt = propsWithSalt.toString(); - } - newNode = options.cache.get(propsWithSalt); - if (newNode === EMPTY_CACHE) return CONTINUE; - if (newNode === undefined) { - newNode = options.code(props); - if (isThenable(newNode)) { - // while promise not resilved there will be cache misses - // TODO: should I cache promises in memory until they setled? - newNode.then((x) => { - if (x === undefined) { - options.cache!.set(propsWithSalt, EMPTY_CACHE); + try { + let propsWithSalt: string | Buffer = serialize({ + ...props, + cb, + salt: options.salt, + }); + if (options.hashTostring === true) { + propsWithSalt = propsWithSalt.toString(); + } + newNode = options.cache.get(propsWithSalt); + if (newNode === EMPTY_CACHE) return CONTINUE; + if (newNode === undefined) { + newNode = options.code(props); + if (isThenable(newNode)) { + // while promise not resilved there will be cache misses + // TODO: should I cache promises in memory until they setled? + newNode.then((x) => { + if (x === undefined) { + options.cache!.set(propsWithSalt, EMPTY_CACHE); + } else { + options.cache!.set(propsWithSalt, x); + } + return x; + }); + } else { + if (newNode === undefined) { + options.cache.set(propsWithSalt, EMPTY_CACHE); } else { - options.cache!.set(propsWithSalt, x); + options.cache.set(propsWithSalt, newNode); } - return x; - }); - } else { - if (newNode === undefined) { - options.cache.set(propsWithSalt, EMPTY_CACHE); - } else { - options.cache.set(propsWithSalt, newNode); } } + } catch (e) { + console.warn(`Can't serialize key for cache ${e}`); + newNode = options.code(props); } } else { newNode = options.code(props); diff --git a/packages/rehype-d2/src/index.ts b/packages/rehype-d2/src/index.ts index 8857af1..e67de42 100644 --- a/packages/rehype-d2/src/index.ts +++ b/packages/rehype-d2/src/index.ts @@ -2,11 +2,10 @@ import type { Plugin } from "unified"; import type { Root } from "hast"; import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; import { d2, D2Options } from "./d2.js"; -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"; import { lex as lexMeta, parse as parseMeta } from "fenceparser"; +import { Strategy, svgStrategyCbAsync } from "./svgStrategy.js"; const processMeta = (meta?: string): Record => meta ? parseMeta(lexMeta(meta)) : {}; @@ -32,31 +31,15 @@ export type RehypeD2Config = { * be carefull. It may break some diagrams */ svgo?: SvgoConfig | boolean; - strategy?: "inline" | "img" | "img-class-dark-mode"; + strategy?: Strategy; d2Options?: D2Options; }; type RenderOptions = D2Options & { svgo?: SvgoConfig | boolean; - strategy?: "inline" | "img" | "img-class-dark-mode"; + strategy?: Strategy; }; -function image({ - svg, - ...rest -}: { - width?: string; - height?: string; - alt?: string; - svg: string; - class?: string; -}) { - return h("img", { - src: svgToMiniDataURI(svg), - ...rest, - }); -} - export const rehypeD2: Plugin<[RehypeD2Config?], Root> = (options = {}) => { const { svgo: svgoDefault, @@ -65,7 +48,7 @@ export const rehypeD2: Plugin<[RehypeD2Config?], Root> = (options = {}) => { ...rest } = options; - const salt = options; + const salt = { svgoDefault, strategyDefault, defaultOptions }; const render = async (code: string, options: RenderOptions) => { let svg = await d2(code, { ...(defaultOptions || {}), ...options }); @@ -97,65 +80,26 @@ export const rehypeD2: Plugin<[RehypeD2Config?], Root> = (options = {}) => { code: async ({ code, meta }) => { const metaOptions = processMeta(meta); const strategy = metaOptions.strategy ?? strategyDefault; - const cssClass = `${options.class || ""} ${ + const cssClass = `d2 ${options.class || ""} ${ metaOptions.class || "" }`.trim(); - switch (strategy) { - case "img": { - const { svg, width, height } = await render(code, metaOptions); - return h( - "figure", - { - class: `beoe d2 ${cssClass}`, - }, - [image({ svg, width, height })] - ); - } - case "img-class-dark-mode": { - const { - svg: svgLight, - width, - height, - } = await render(code, metaOptions); - const { svg: svgDark } = await render(code, { + return svgStrategyCbAsync(strategy, cssClass, async (darkMode) => { + let darkSvg: string | undefined; + const { + svg: lightSvg, + width, + height, + } = await render(code, metaOptions); + if (darkMode) { + const res = await render(code, { ...metaOptions, - theme: metaOptions.darktheme ?? defaultOptions?.darkTheme, + theme: metaOptions.darktheme ?? defaultOptions?.darkTheme ?? 200, }); - - return h( - "figure", - { - class: `beoe d2 ${cssClass}`, - }, - // wrap in additional div for svg-pan-zoom - [ - h("div", [ - image({ svg: svgLight, width, height, class: "beoe-light" }), - image({ svg: svgDark, width, height, class: "beoe-dark" }), - ]), - ] - ); - } - // this doesn't work - // case "inline-class-dark-mode": { - // const { svg: svgLight } = await render(code, metaOptions); - // const { svg: svgDark } = await render(code, { - // ...metaOptions, - // theme: metaOptions.darktheme ?? defaultOptions?.darkTheme, - // }); - // return `
- //
- //
${svgLight}
- //
${svgDark}
- //
- //
`; - // } - default: { - const { svg } = await render(code, metaOptions); - return `
${svg}
`; + darkSvg = res.svg; } - } + return { lightSvg, width, height, darkSvg }; + }); }, }); }; diff --git a/packages/rehype-d2/src/svgStrategy.ts b/packages/rehype-d2/src/svgStrategy.ts new file mode 100644 index 0000000..a2536e8 --- /dev/null +++ b/packages/rehype-d2/src/svgStrategy.ts @@ -0,0 +1,142 @@ +import { h } from "hastscript"; +import svgToMiniDataURI from "mini-svg-data-uri"; + +function image({ + svg, + ...rest +}: { + width?: string | number; + height?: string | number; + alt?: string; + svg: string; + class?: string; +}) { + return h("img", { + src: svgToMiniDataURI(svg), + ...rest, + }); +} + +function figure(cssClass: string, children: any[]) { + return h( + "figure", + { + class: `beoe ${cssClass}`, + }, + children + ); +} + +/** + * Maybe rename img to data-uri + */ +export type Strategy = + // SVG directly in the HTML + | "inline" + // this doesn't make sense + // | "inline-class-dark-mode" + // SVG as data-uri in img + | "img" + // SVG as data-uri in img and source inside of a picture + | "picture-dark-mode" + // SVG as data-uri in two imgs with light and dark classes + | "img-class-dark-mode"; + +type SvgStrategyBase = { + strategy?: Strategy; + cssClass: string; +}; + +type SvgStrategyCbResult = { + width?: string | number; + height?: string | number; + lightSvg: string; + darkSvg?: string; +}; + +type SvgStrategyOptions = SvgStrategyCbResult & SvgStrategyBase; + +export function svgStrategy({ + strategy, + cssClass, + lightSvg, + darkSvg, + width, + height, +}: SvgStrategyOptions) { + switch (strategy) { + case "img": { + return figure(cssClass, [image({ svg: lightSvg, width, height })]); + } + case "img-class-dark-mode": { + if (!darkSvg) + return figure(cssClass, [image({ svg: lightSvg, width, height })]); + + return figure( + cssClass, + // wrap in additional div for svg-pan-zoom + [ + h("div", [ + image({ svg: lightSvg, width, height, class: "beoe-light" }), + image({ svg: darkSvg, width, height, class: "beoe-dark" }), + ]), + ] + ); + } + case "img-class-dark-mode": { + if (!darkSvg) + return figure(cssClass, [image({ svg: lightSvg, width, height })]); + + const imgLight = image({ svg: lightSvg, width, height }); + const imgDark = h("source", { + width, + height, + src: svgToMiniDataURI(darkSvg), + media: `(prefers-color-scheme: dark)`, + }); + + return figure(cssClass, [h("picture", [imgLight, imgDark])]); + } + // case "inline-class-dark-mode": { + // return `
+ //
+ //
${lightSvg}
+ //
${darkSvg}
+ //
+ //
`; + // } + default: { + return `
${lightSvg}
`; + } + } +} + +type SvgStrategyCb = (darkMode: boolean) => SvgStrategyCbResult; +export function svgStrategyCb( + strategy: Strategy, + cssClass: string, + cb: SvgStrategyCb +) { + return svgStrategy({ + strategy, + cssClass, + ...cb( + strategy === "img-class-dark-mode" || strategy === "picture-dark-mode" + ), + }); +} + +type SvgStrategyCbAsync = (darkMode: boolean) => Promise; +export async function svgStrategyCbAsync( + strategy: Strategy, + cssClass: string, + cb: SvgStrategyCbAsync +) { + return svgStrategy({ + strategy, + cssClass, + ...(await cb( + strategy === "img-class-dark-mode" || strategy === "picture-dark-mode" + )), + }); +} diff --git a/packages/rehype-d2/test/fixtures/a-inline.html b/packages/rehype-d2/test/fixtures/a-inline.html index c56516c..8bb083a 100644 --- a/packages/rehype-d2/test/fixtures/a-inline.html +++ b/packages/rehype-d2/test/fixtures/a-inline.html @@ -1 +1 @@ -
xy,,,,
\ No newline at end of file +
xy
\ No newline at end of file diff --git a/packages/rehype-d2/test/fixtures/a1-datauri.html b/packages/rehype-d2/test/fixtures/a1-datauri.html index 3738d23..92a5594 100644 --- a/packages/rehype-d2/test/fixtures/a1-datauri.html +++ b/packages/rehype-d2/test/fixtures/a1-datauri.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file