Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
stereobooster committed Nov 20, 2024
1 parent 12eb52d commit 75b12e5
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 106 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion packages/demo/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions packages/demo/src/content/docs/examples/d2-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
61 changes: 34 additions & 27 deletions packages/rehype-code-hook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
92 changes: 18 additions & 74 deletions packages/rehype-d2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> =>
meta ? parseMeta(lexMeta(meta)) : {};
Expand All @@ -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,
Expand All @@ -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 });
Expand Down Expand Up @@ -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 `<figure class="beoe d2 ${cssClass}">
// <div>
// <div class="beoe-light">${svgLight}</div>
// <div class="beoe-dark">${svgDark}</div>
// </div>
// </figure>`;
// }
default: {
const { svg } = await render(code, metaOptions);
return `<figure class="beoe d2 ${cssClass}">${svg}</figure>`;
darkSvg = res.svg;
}
}
return { lightSvg, width, height, darkSvg };
});
},
});
};
Expand Down
142 changes: 142 additions & 0 deletions packages/rehype-d2/src/svgStrategy.ts
Original file line number Diff line number Diff line change
@@ -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 `<figure class="beoe ${cssClass}">
// <div>
// <div class="beoe-light">${lightSvg}</div>
// <div class="beoe-dark">${darkSvg}</div>
// </div>
// </figure>`;
// }
default: {
return `<figure class="beoe ${cssClass}">${lightSvg}</figure>`;
}
}
}

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<SvgStrategyCbResult>;
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"
)),
});
}
2 changes: 1 addition & 1 deletion packages/rehype-d2/test/fixtures/a-inline.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<figure class="beoe d2"><svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg class="d2-1843626214" viewBox="-101 -101 256 434"><rect width="256" height="434" x="-101" y="-101" fill="#FFF" class="fill-N7" rx="0"></rect><g class="shape"><rect width="53" height="66" x="1" fill="#F7F8FE" stroke="#0D32B2" class="stroke-B1 fill-B6" style="stroke-width:2"></rect></g><text x="27.5" y="38.5" fill="#0A0F25" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text><g class="shape"><rect width="54" height="66" y="166" fill="#F7F8FE" stroke="#0D32B2" class="stroke-B1 fill-B6" style="stroke-width:2"></rect></g><text x="27" y="204.5" fill="#0A0F25" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text><marker id="a" markerHeight="12" markerUnits="userSpaceOnUse" markerWidth="10" orient="auto" refX="7" refY="6" viewBox="0 0 10 12"><polygon fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1"></polygon></marker><path fill="none" stroke="#0D32B2" marker-end="url(#a)" d="M27 68v94" class="connection stroke-B1" mask="url(#b)" style="stroke-width:2"></path><mask id="b" width="256" height="434" x="-101" y="-101" maskUnits="userSpaceOnUse">,<rect width="256" height="434" x="-101" y="-101" fill="#fff"></rect>,<rect width="8" height="21" x="23.5" y="22.5" fill="rgba(0,0,0,0.75)"></rect>,<rect width="9" height="21" x="22.5" y="188.5" fill="rgba(0,0,0,0.75)"></rect>,</mask></svg></svg></figure>
<figure class="beoe d2"><svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-1843626214" viewBox="-101 -101 256 434"><rect width="256" height="434" x="-101" y="-101" stroke-width="0" rx="0" style="fill:#fff"></rect><style>.d2-1843626214 .text-bold{font-family:"d2-1843626214-font-bold"}@font-face{font-family:d2-1843626214-font-bold;src:url(data:application/font-woff;base64,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)}.connection,.shape{stroke-linejoin:round}.shape{shape-rendering:geometricPrecision}.connection{stroke-linecap:round}.d2-1843626214 .fill-N1{fill:#0a0f25}.d2-1843626214 .fill-B6{fill:#f7f8fe}.d2-1843626214 .stroke-B1{stroke:#0d32b2}</style><g id="x"><g class="shape"><path fill="#F7F8FE" stroke="#0D32B2" d="M1 0h53v66H1z" class="stroke-B1 fill-B6" style="stroke-width:2"></path></g><text x="27.5" y="38.5" fill="#0A0F25" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape"><path fill="#F7F8FE" stroke="#0D32B2" d="M0 166h54v66H0z" class="stroke-B1 fill-B6" style="stroke-width:2"></path></g><text x="27" y="204.5" fill="#0A0F25" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -> y)[0]"><marker id="mk-3488378134" markerHeight="12" markerUnits="userSpaceOnUse" markerWidth="10" orient="auto" refX="7" refY="6" viewBox="0 0 10 12"><path stroke-width="2" d="m0 0 10 6-10 6z" class="connection" style="fill:#0d32b2"></path></marker><path fill="none" stroke="#0D32B2" marker-end="url(#mk-3488378134)" d="M27 68v94" class="connection stroke-B1" mask="url(#d2-1843626214)" style="stroke-width:2"></path></g><mask id="d2-1843626214" width="256" height="434" x="-101" y="-101" maskUnits="userSpaceOnUse"><path fill="#fff" d="M-101-101h256v434h-256z"></path><path fill="rgba(0,0,0,0.75)" d="M23.5 22.5h8v21h-8zM22.5 188.5h9v21h-9z"></path></mask></svg></svg></figure>
Loading

0 comments on commit 75b12e5

Please sign in to comment.