diff --git a/packages/demo/README.md b/packages/demo/README.md index f384beb..f8efcfa 100644 --- a/packages/demo/README.md +++ b/packages/demo/README.md @@ -5,3 +5,6 @@ - create special code hook, which would produce 2 tabs: - diagram - code for the diagram + - maybe + - https://github.com/withastro/starlight/blob/main/docs/src/components/component-preview.astro + - https://starlight.astro.build/components/code/ diff --git a/packages/demo/astro.config.mjs b/packages/demo/astro.config.mjs index c6c9ca6..a1562d0 100644 --- a/packages/demo/astro.config.mjs +++ b/packages/demo/astro.config.mjs @@ -14,7 +14,8 @@ const cache = await getCache(); const className = "not-content"; const conf = { cache, - strategy: "f-img-class-dark-mode", + strategy: "file", + darkScheme: "class", // do not use .beoe for Netlify deployments fsPath: "public/beoe", webPath: "/beoe", diff --git a/packages/demo/src/content/docs/examples/mermaid-test.md b/packages/demo/src/content/docs/examples/mermaid-test.md index a94d8b5..0c30ba0 100644 --- a/packages/demo/src/content/docs/examples/mermaid-test.md +++ b/packages/demo/src/content/docs/examples/mermaid-test.md @@ -3,17 +3,23 @@ title: mermaid test draft: true --- -## rehype-mermaid -```mermaid -graph LR - accTitle: Big Decisions - accDescr: Bob's Burgers process for making big decisions - A[Identify Big Decision] --> B{Make Big Decision} - B --> D[Be done] -``` +## links ```mermaid strategy=inline class=not-content +flowchart LR + A-->B + B-->C + C-->D + click A callback "Tooltip for a callback" + click B "https://www.github.com" "This is a tooltip for a link" + click C call callback() "Tooltip for a callback" + click D href "https://www.github.com" "This is a tooltip for a link" +``` + +## other + +```mermaid graph LR accTitle: Big Decisions accDescr: Bob's Burgers process for making big decisions diff --git a/packages/demo/src/content/docs/index.md b/packages/demo/src/content/docs/index.md index 7cc75c9..34e92fb 100644 --- a/packages/demo/src/content/docs/index.md +++ b/packages/demo/src/content/docs/index.md @@ -2,61 +2,21 @@ title: Diagram All The Things --- -## Strategies - -### inline - -- **pros** - - interactivity - - links (`a href`) - - Cmd + F - - with JS, for example, if we inline JSON data - - need to be careful with SVGO to not remove IDs etc. - - dark theme - - class based - - `prefers-color-scheme` - - allows to add dark theme to tools that don't support it out of the box (`graphviz`, `vizdom`, `gnuplot`) -- **cons** - - CSS conflicts - - CSS from website can affect diagram - - potential solution `.not-content` - - CSS from diagram can affect website (d2 diagrams) - - this can break dark theme or other diagrams on the smae page - - potential solution use unique prefix to make sure there is no clash - - Dark theme can be limited (black and white) - - DOM footprint - - easy to integrate, especially if there is no inline CSS in SVG - -### data-url - -- **pros** - - no CSS conflicts - - dark theme - - class based. Two images with two classes and only one shown at a time depending on the theme - - `prefers-color-scheme`: `picture` + `source media="(prefers-color-scheme: dark)` + `img` - - no DOM footprint -- **cons** - - no interactivity - - no DOM footprint, but overall weight of HTML is high as in previous strategy - - easy to integrate - only need to add a bit of CSS to support dark theme - -### file - -- **pros** - - no CSS conflicts - - dark theme - - class based. Two images with two classes and only one shown at a time depending on the theme - - `prefers-color-scheme`: `picture` + `source media="(prefers-color-scheme: dark)` + `img` - - no DOM footprint - - lighter weight of HTML compared to previous strategy -- **cons** - - no interactivity - - some tools don't support dark theme out of the box (`graphviz`, `vizdom`, `gnuplot`) - - harder to integrate - need to resolve where to write file, how to do cache busting... - ---- - -# TODO +## TODO + +- [ ] documentation about shared options + - `strategy` + - `darkScheme` + - `cache` + - `class` + - `svgo` +- [ ] documentation about meta + - `strategy`, `class` + - `alt` (not implemented yet) +- [ ] documentation and examples about interactivity +- [ ] tips about styles and other things +- [ ] revamp site, maybe `template: splash hero: ...` for index page +- [ ] publish new version ## Strategy @@ -72,7 +32,7 @@ title: Diagram All The Things ```html
- ... +
``` @@ -80,7 +40,7 @@ title: Diagram All The Things ```html
- ... +
``` @@ -105,7 +65,7 @@ title: Diagram All The Things ``` -You would need to add CSS, something like this +You would need to add CSS, something like this: ```CSS html[data-theme="light"] .beoe-dark { @@ -151,3 +111,69 @@ html[data-theme="dark"] .beoe-light { - Links (``) - Search text in the diagram (Cmd + F) - With JS, for example, if we inline JSON data + +## Accessibility + +````md +```some alt="..." + +``` +```` + +```html +... +``` + +I can add option to [figcaption](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption) as well. + +## Old notes + +### inline + +- **pros** + - interactivity + - links (`a href`) + - Cmd + F + - with JS, for example, if we inline JSON data + - need to be careful with SVGO to not remove IDs etc. + - dark theme + - class based + - `prefers-color-scheme` + - allows to add dark theme to tools that don't support it out of the box (`graphviz`, `vizdom`, `gnuplot`) +- **cons** + - CSS conflicts + - CSS from website can affect diagram + - potential solution `.not-content` + - CSS from diagram can affect website (d2 diagrams) + - this can break dark theme or other diagrams on the smae page + - potential solution use unique prefix to make sure there is no clash + - Dark theme can be limited (black and white) + - DOM footprint + - easy to integrate, especially if there is no inline CSS in SVG + +### data-url + +- **pros** + - no CSS conflicts + - dark theme + - class based. Two images with two classes and only one shown at a time depending on the theme + - `prefers-color-scheme`: `picture` + `source media="(prefers-color-scheme: dark)` + `img` + - no DOM footprint +- **cons** + - no interactivity + - no DOM footprint, but overall weight of HTML is high as in previous strategy + - easy to integrate - only need to add a bit of CSS to support dark theme + +### file + +- **pros** + - no CSS conflicts + - dark theme + - class based. Two images with two classes and only one shown at a time depending on the theme + - `prefers-color-scheme`: `picture` + `source media="(prefers-color-scheme: dark)` + `img` + - no DOM footprint + - lighter weight of HTML compared to previous strategy +- **cons** + - no interactivity + - some tools don't support dark theme out of the box (`graphviz`, `vizdom`, `gnuplot`) + - harder to integrate - need to resolve where to write file, how to do cache busting... diff --git a/packages/rehype-code-hook-img/README.md b/packages/rehype-code-hook-img/README.md index 6d3a7a7..acc2ce5 100644 --- a/packages/rehype-code-hook-img/README.md +++ b/packages/rehype-code-hook-img/README.md @@ -1,7 +1,3 @@ # @beoe/rehype-code-hook-img The same as `@beoe/rehype-code-hook`, but specifically designed for hooks that output images (e.g. diagrams). - -## TODO - -- [ ] Seriously. Strategies need better names diff --git a/packages/rehype-code-hook-img/src/index.ts b/packages/rehype-code-hook-img/src/index.ts index 994144b..79ceab9 100644 --- a/packages/rehype-code-hook-img/src/index.ts +++ b/packages/rehype-code-hook-img/src/index.ts @@ -38,11 +38,27 @@ export const rehypeCodeHookImg = ( defaults as any as T & BasePluginOptions, meta ); - const darkMode = - opts.strategy === "img-class-dark-mode" || - opts.strategy === "picture-dark-mode" || - opts.strategy === "f-img-class-dark-mode" || - opts.strategy === "f-picture-dark-mode"; + + if (opts.strategy === "img") { + opts.strategy = "data-url"; + console.warn("img strategy is deprecated, use data-url instead"); + } + if (opts.strategy === "img-class-dark-mode") { + opts.strategy = "data-url"; + opts.darkScheme = "class"; + console.warn( + "img-class-dark-mode strategy is deprecated, use data-url instead + darkScheme class" + ); + } + if (opts.strategy === "picture-dark-mode") { + opts.strategy = "data-url"; + opts.darkScheme = "media"; + console.warn( + "img-class-dark-mode strategy is deprecated, use data-url instead + darkScheme media" + ); + } + + const darkMode = opts.darkScheme != undefined; const result = render(code, { ...opts, darkMode }); return "then" in result ? result.then((x) => svgStrategy(opts, x)) diff --git a/packages/rehype-code-hook-img/src/types.ts b/packages/rehype-code-hook-img/src/types.ts index 48116aa..b313c39 100644 --- a/packages/rehype-code-hook-img/src/types.ts +++ b/packages/rehype-code-hook-img/src/types.ts @@ -46,16 +46,19 @@ export type BasePluginOptions = { svgo?: SvgoConfig | boolean; strategy?: Strategy; cache?: MapLike; + darkScheme?: Scheme; /** - * required for file-based strategy + * required for `file` strategy */ fsPath?: string; /** - * required for file-based strategy + * required for `file` strategy */ webPath?: string; }; +export type Scheme = "class" | "media"; + /** * maybe rename img to data-uri */ @@ -64,24 +67,29 @@ export type Strategy = * SVG directly in the HTML */ | "inline" + /** + * SVG in `src` as [data-url](https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data) + */ + | "data-url" + /** + * SVG stored as standalone file + */ + | "file" /** * SVG as data-uri in img + * @deprecated */ | "img" /** * SVG as data-uri in img and source inside of a picture + * @deprecated */ | "picture-dark-mode" /** * SVG as data-uri in two imgs with light and dark classes + * @deprecated */ - | "img-class-dark-mode" - /** - * Experiment - */ - | "f-img" - | "f-picture-dark-mode" - | "f-img-class-dark-mode"; + | "img-class-dark-mode"; export type CbResult = Result | Promise; diff --git a/packages/rehype-code-hook-img/src/utils.ts b/packages/rehype-code-hook-img/src/utils.ts index 86b60c6..f72d935 100644 --- a/packages/rehype-code-hook-img/src/utils.ts +++ b/packages/rehype-code-hook-img/src/utils.ts @@ -82,7 +82,14 @@ function figure(className: string | undefined, children: any[]) { } export function svgStrategy( - { class: className, strategy, svgo, fsPath, webPath }: BasePluginOptions, + { + class: className, + strategy, + svgo, + fsPath, + webPath, + darkScheme, + }: BasePluginOptions, { svg, data, darkSvg, width, height, alt }: Result ) { if (svgo !== false) { @@ -123,98 +130,128 @@ export function svgStrategy( return url; }; + const removeWidthHeight = (svg: string) => + svg + .replace(new RegExp(`width="${width}[^"]*"\\s+`), "") + .replace(new RegExp(`height="${height}[^"]*"\\s+`), ""); + + if (strategy == "file" && (fsPath == undefined || webPath == undefined)) { + console.warn( + "file strategy requires fsPath and webPath. Falling back to data-url" + ); + strategy = "data-url"; + } + switch (strategy) { - case "img": { + case "data-url": { + if (darkScheme == "class" && darkSvg) + return figure( + className, + // wrap in additional div for svg-pan-zoom + [ + h("div", [ + image({ svg, width, height, alt, class: "beoe-light" }), + image({ svg: darkSvg, width, height, alt, class: "beoe-dark" }), + ]), + ] + ); + + if (darkScheme == "media" && darkSvg) { + const imgLight = image({ svg, width, height, alt }); + const imgDark = h("source", { + width, + height, + src: svgToMiniDataURI(darkSvg), + media: `(prefers-color-scheme: dark)`, + }); + + return figure(className, [h("picture", [imgLight, imgDark])]); + } + return getImage(); } - case "img-class-dark-mode": { - if (!darkSvg) return getImage(); - - return figure( - className, - // wrap in additional div for svg-pan-zoom - [ - h("div", [ - image({ svg, width, height, alt, class: "beoe-light" }), - image({ svg: darkSvg, width, height, alt, class: "beoe-dark" }), - ]), - ] - ); - } - case "picture-dark-mode": { - if (!darkSvg) return getImage(); - - const imgLight = image({ svg, width, height, alt }); - const imgDark = h("source", { - width, - height, - src: svgToMiniDataURI(darkSvg), - media: `(prefers-color-scheme: dark)`, - }); + case "file": { + if (fsPath == undefined || webPath == undefined) return; - return figure(className, [h("picture", [imgLight, imgDark])]); - } - case "f-img": { - if (!fsPath || !webPath) return getImage(); + if (darkScheme == "class" && darkSvg) { + return Promise.all([fileUrl(svg), fileUrl(darkSvg)]).then( + ([url, darkUrl]) => + figure( + className, + // wrap in additional div for svg-pan-zoom + [ + h("div", [ + image({ width, height, alt, class: "beoe-light", url }), + image({ + width, + height, + alt, + class: "beoe-dark", + url: darkUrl, + }), + ]), + ] + ) + ); + } + + if (darkScheme == "media" && darkSvg) + return Promise.all([fileUrl(svg), fileUrl(darkSvg)]).then( + ([url, darkUrl]) => { + const imgLight = image({ width, height, alt, url }); + const imgDark = h("source", { + width, + height, + src: darkUrl, + media: `(prefers-color-scheme: dark)`, + }); + + return figure(className, [h("picture", [imgLight, imgDark])]); + } + ); return fileUrl(svg).then((url) => figure(className, [image({ width, height, alt, url })]) ); } - case "f-img-class-dark-mode": { - if (!darkSvg || !fsPath || !webPath) return getImage(); - - return Promise.all([fileUrl(svg), fileUrl(darkSvg)]).then( - ([url, darkUrl]) => - figure( - className, - // wrap in additional div for svg-pan-zoom - [ - h("div", [ - image({ width, height, alt, class: "beoe-light", url }), - image({ - width, - height, - alt, - class: "beoe-dark", - url: darkUrl, - }), - ]), - ] - ) - ); - } - case "f-picture-dark-mode": { - if (!darkSvg || !fsPath || !webPath) return getImage(); - - return Promise.all([fileUrl(svg), fileUrl(darkSvg)]).then( - ([url, darkUrl]) => { - const imgLight = image({ width, height, alt, url }); - const imgDark = h("source", { - width, - height, - src: darkUrl, - media: `(prefers-color-scheme: dark)`, - }); - - return figure(className, [h("picture", [imgLight, imgDark])]); - } - ); - } default: { - svg = svg.replace(new RegExp(`width="${width}[^"]*"\\s+`), ""); - svg = svg.replace(new RegExp(`height="${height}[^"]*"\\s+`), ""); - - const element = fromHtmlIsomorphic(svg, { fragment: true }); - return { - type: "element", - tagName: "figure", - properties: { + if (darkScheme == "class" && darkSvg) { + const element = fromHtmlIsomorphic(removeWidthHeight(svg), { + fragment: true, + }); + const darkElement = fromHtmlIsomorphic(removeWidthHeight(darkSvg), { + fragment: true, + }); + return h( + "figure", + { + class: className, + "data-beoe": data ? JSON.stringify(data) : undefined, + }, + [ + h("div", [ + h("div", { class: "beoe-light" }, element.children), + h("div", { class: "beoe-dark" }, darkElement.children), + ]), + ] + ); + } + + if (darkScheme == "media") { + console.warn("darkScheme media doesn't work for inline strategy"); + } + + const element = fromHtmlIsomorphic(removeWidthHeight(svg), { + fragment: true, + }); + return h( + "figure", + { class: className, "data-beoe": data ? JSON.stringify(data) : undefined, }, - children: element.children, - }; + element.children + ); } } }