├── .node-version ├── packages ├── docs │ ├── README.md │ ├── shared │ │ └── x.d2 │ ├── src │ │ ├── content │ │ │ ├── docs │ │ │ │ ├── other │ │ │ │ │ ├── cache.md │ │ │ │ │ ├── fenceparser.md │ │ │ │ │ ├── sqlitecache.md │ │ │ │ │ ├── rehype-code-hook.md │ │ │ │ │ └── remark-code-hook.md │ │ │ │ ├── notes │ │ │ │ │ ├── graph-libraries.md │ │ │ │ │ └── space-time.md │ │ │ │ ├── examples │ │ │ │ │ ├── penrose-test.md │ │ │ │ │ └── graphviz-test.md │ │ │ │ ├── start-here │ │ │ │ │ ├── getting-started.md │ │ │ │ │ ├── strategy.md │ │ │ │ │ ├── accessibility.mdx │ │ │ │ │ ├── styling-with-css.mdx │ │ │ │ │ ├── configuration.md │ │ │ │ │ └── dark-scheme.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── diagrams │ │ │ │ │ ├── mermaid.mdx │ │ │ │ │ ├── penrose.mdx │ │ │ │ │ ├── d2.mdx │ │ │ │ │ └── graphviz.mdx │ │ │ └── config.ts │ │ ├── env.d.ts │ │ ├── components │ │ │ ├── panzoom.ts │ │ │ ├── iframeTarget.ts │ │ │ ├── PageFrame.astro │ │ │ └── d2.ts │ │ └── styles │ │ │ └── custom.css │ ├── .vscode │ │ ├── extensions.json │ │ └── launch.json │ ├── tsconfig.json │ ├── .gitignore │ ├── public │ │ ├── favicon.svg │ │ └── logo.svg │ ├── package.json │ └── astro.config.mjs ├── rehype-code-hook │ ├── test │ │ └── fixtures │ │ │ ├── b.md │ │ │ ├── a2.out.html │ │ │ ├── a3.out.html │ │ │ ├── a.md │ │ │ └── a1.out.html │ ├── tsconfig.json │ ├── src │ │ └── waitFor.ts │ └── package.json ├── remark-code-hook │ ├── test │ │ └── fixtures │ │ │ ├── b.md │ │ │ ├── a2.out.html │ │ │ ├── a3.out.html │ │ │ ├── a.md │ │ │ └── a1.out.html │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── rehype-d2 │ ├── test │ │ ├── fixtures │ │ │ └── a.md │ │ └── index.test.ts │ ├── tsconfig.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── d2.ts │ └── package.json ├── fenceparser │ ├── .gitignore │ ├── src │ │ ├── error.ts │ │ ├── index.ts │ │ └── lex.ts │ ├── tsconfig.json │ ├── test │ │ ├── utils.ts │ │ └── index.test.ts │ ├── package.json │ ├── LICENSE │ ├── CHANGELOG.md │ └── README.md ├── rehype-code-hook-img │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ ├── b.md │ │ │ ├── c.md │ │ │ ├── class.out.html │ │ │ ├── strategy_inline.out.html │ │ │ ├── strategy_undefined.out.html │ │ │ ├── strategy_img.out.html │ │ │ ├── strategy_data-url.out.html │ │ │ ├── strategy_in_meta.out.html │ │ │ ├── strategy_img-class-dark-mode.out.html │ │ │ └── strategy_picture-dark-mode.out.html │ │ └── index.test.ts │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── types.ts ├── rehype-mermaid │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ └── a.html │ │ └── index.test.ts │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── rehype-gnuplot │ ├── test │ │ ├── fixtures │ │ │ └── a.md │ │ └── index.test.ts │ ├── tsconfig.json │ ├── README.md │ ├── src │ │ └── index.ts │ └── package.json ├── rehype-graphviz │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ └── a.html │ │ └── index.test.ts │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── render.ts │ ├── README.md │ └── package.json ├── rehype-vizdom │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ └── a.html │ │ └── index.test.ts │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── render.ts │ ├── README.md │ └── package.json ├── pan-zoom │ ├── src │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── utilsDom.ts │ │ ├── utilsMath.ts │ │ └── PanZoomUi.ts │ ├── tsconfig.json │ ├── package.json │ ├── css │ │ └── PanZoomUi.css │ └── logo │ │ ├── logo.svg │ │ └── logo-dark.svg ├── cache │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── config.ts │ └── README.md ├── sqlitecache │ ├── tsconfig.json │ └── package.json └── rehype-penrose │ ├── tsconfig.json │ ├── test │ ├── fixtures │ │ └── a.md │ └── index.test.ts │ ├── src │ ├── penrose.ts │ └── index.ts │ ├── package.json │ ├── README.md │ └── bin │ └── penrose.js ├── pnpm-workspace.yaml ├── experiments ├── rehype-color-chips │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ └── a.html │ │ └── index.test.ts │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ ├── package.json │ └── README.md ├── rehype-starry-night │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ └── a.html │ │ └── index.test.ts │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ ├── README.md │ └── package.json ├── astro-graphviz │ ├── index.ts │ ├── cache.ts │ ├── tsconfig.json │ ├── rehype.mjs │ ├── Graphviz.astro │ ├── README.md │ └── package.json ├── rehype-plantuml │ ├── test │ │ ├── fixtures │ │ │ ├── a.md │ │ │ ├── a-inline.html │ │ │ └── a1-datauri.html │ │ └── index.test.ts │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ ├── package.json │ ├── README.md │ └── example.svg └── rehype-pintora │ ├── test │ ├── fixtures │ │ ├── a.md │ │ ├── a.html │ │ └── a.out.svg │ ├── index.test.ts │ └── pintora.test.ts │ ├── tsconfig.json │ ├── README.md │ ├── src │ ├── pintora.ts │ └── index.ts │ └── package.json ├── turbo.json ├── vitest.config.ts ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── ci.yml ├── README.md ├── notes └── notes.md ├── .gitignore └── logo ├── logo-dark.svg └── logo.svg /.node-version: -------------------------------------------------------------------------------- 1 | v20.18.1 -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | -------------------------------------------------------------------------------- /packages/docs/shared/x.d2: -------------------------------------------------------------------------------- 1 | x: { 2 | shape: circle 3 | } -------------------------------------------------------------------------------- /packages/rehype-code-hook/test/fixtures/b.md: -------------------------------------------------------------------------------- 1 | `test` 2 | -------------------------------------------------------------------------------- /packages/remark-code-hook/test/fixtures/b.md: -------------------------------------------------------------------------------- 1 | `test` 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | `#fff000` 2 | -------------------------------------------------------------------------------- /packages/rehype-code-hook/test/fixtures/a2.out.html: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /packages/rehype-code-hook/test/fixtures/a3.out.html: -------------------------------------------------------------------------------- 1 | hast -------------------------------------------------------------------------------- /packages/remark-code-hook/test/fixtures/a2.out.html: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /packages/rehype-d2/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```d2 2 | x -> y 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/fenceparser/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /packages/remark-code-hook/test/fixtures/a3.out.html: -------------------------------------------------------------------------------- 1 | Alpha bravo charlie. -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```test 2 | test 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | graph TD; A; 3 | ``` 4 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const x = "<"; 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/b.md: -------------------------------------------------------------------------------- 1 | ```test class=y 2 | test 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-gnuplot/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```gnuplot 2 | plot [-10:10] sin(x) 3 | ``` 4 | -------------------------------------------------------------------------------- /experiments/astro-graphviz/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Graphviz } from "./Graphviz.astro"; 2 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```plantuml 2 | Bob->Alice : Hello! 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/c.md: -------------------------------------------------------------------------------- 1 | ```test strategy=data-url 2 | test 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-code-hook/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```js {3-4} fileName=test /a/ 2 | test 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```dot 2 | digraph G { Hello -> World } 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```vizdom 2 | digraph G { Hello -> World } 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/remark-code-hook/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```js {3-4} fileName=test /a/ 2 | test 3 | ``` 4 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/other/cache.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/cache" 3 | draft: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/rehype-code-hook/test/fixtures/a1.out.html: -------------------------------------------------------------------------------- 1 |
test
2 | 
-------------------------------------------------------------------------------- /packages/remark-code-hook/test/fixtures/a1.out.html: -------------------------------------------------------------------------------- 1 |
test
2 | 
-------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/class.out.html: -------------------------------------------------------------------------------- 1 |
light
-------------------------------------------------------------------------------- /packages/docs/src/content/docs/other/fenceparser.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/fenceparser" 3 | draft: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/other/sqlitecache.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/sqlitecache" 3 | draft: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_inline.out.html: -------------------------------------------------------------------------------- 1 |
light
-------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_undefined.out.html: -------------------------------------------------------------------------------- 1 |
light
-------------------------------------------------------------------------------- /experiments/astro-graphviz/cache.ts: -------------------------------------------------------------------------------- 1 | import { getCache } from "@beoe/cache"; 2 | 3 | export const cache = await getCache(); 4 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/other/rehype-code-hook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/rehype-code-hook" 3 | draft: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/other/remark-code-hook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/remark-code-hook" 3 | draft: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/test/fixtures/a.html: -------------------------------------------------------------------------------- 1 |

#fff000

-------------------------------------------------------------------------------- /packages/pan-zoom/src/index.ts: -------------------------------------------------------------------------------- 1 | export { PanZoomUi, type PanZoomUiProps } from "./PanZoomUi.js"; 2 | export { PanZoom, type PanZoomProps } from "./PanZoom.js"; 3 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```pintora 2 | activityDiagram 3 | title: Activity Diagram Simple Action 4 | :Action 1; 5 | :Action 2; 6 | ``` 7 | -------------------------------------------------------------------------------- /experiments/astro-graphviz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true 6 | } 7 | } -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_img.out.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_data-url.out.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_in_meta.out.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/rehype-code-hook-img/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-code-hook-img 2 | 3 | The same as `@beoe/rehype-code-hook`, but specifically designed for hooks that output images (e.g. diagrams). 4 | -------------------------------------------------------------------------------- /packages/fenceparser/src/error.ts: -------------------------------------------------------------------------------- 1 | export class FenceparserError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | Object.setPrototypeOf(this, FenceparserError.prototype) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "exclude": ["dist"], 4 | // "compilerOptions": { 5 | // "baseUrl": ".", 6 | // "paths": { 7 | // "@assets/*": ["src/assets/*"] 8 | // } 9 | // } 10 | } -------------------------------------------------------------------------------- /packages/fenceparser/src/index.ts: -------------------------------------------------------------------------------- 1 | import {lex} from './lex.js' 2 | import {parse, ParseOptions} from './parse.js' 3 | 4 | export {FenceparserError} from './error.js' 5 | export {lex, parse} 6 | export default (input: string, opts?: ParseOptions) => parse(lex(input), opts) 7 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/test/fixtures/a.html: -------------------------------------------------------------------------------- 1 |
const x = "<";
2 | 
-------------------------------------------------------------------------------- /packages/docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_img-class-dark-mode.out.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/fenceparser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/fixtures/strategy_picture-dark-mode.out.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/rehype-d2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/sqlitecache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { docsLoader } from "@astrojs/starlight/loaders"; 3 | import { docsSchema } from "@astrojs/starlight/schema"; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /packages/rehype-code-hook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-gnuplot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-penrose/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/remark-code-hook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["./src"], 10 | "rootDir": "./src" 11 | } 12 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-pintora 2 | 3 | Rehype plugin to generate [Pintora](https://pintorajs.vercel.app/) diagrams in place of code fences. 4 | 5 | Tried it out of curiosity (and it is kind of easy to do with `@beoe/rehype-code-hook`). Didn't like the result. And I don't have use case for it, so not gonna push it forward. 6 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | public/beoe 23 | -------------------------------------------------------------------------------- /packages/docs/src/components/panzoom.ts: -------------------------------------------------------------------------------- 1 | import "@beoe/pan-zoom/css/PanZoomUi.css"; 2 | import { PanZoomUi } from "@beoe/pan-zoom"; 3 | 4 | document.querySelectorAll(".beoe").forEach((container) => { 5 | const element = container.firstElementChild; 6 | if (!element) return; 7 | // @ts-expect-error 8 | new PanZoomUi({ element, container }).on(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/pan-zoom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "declaration": true, 8 | "noEmit": false 9 | }, 10 | "include": ["./src"], 11 | "rootDir": "./src" 12 | } 13 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "clean": {}, 5 | "tsc": { 6 | "dependsOn": ["^build"] 7 | }, 8 | "build": { 9 | "dependsOn": ["^build"], 10 | "outputs": ["dist/**"] 11 | }, 12 | "dev": { 13 | "dependsOn": ["^build"], 14 | "cache": false, 15 | "persistent": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /experiments/astro-graphviz/rehype.mjs: -------------------------------------------------------------------------------- 1 | import { getCache } from "@beoe/cache"; 2 | import { 3 | rehypeGraphviz as defaultRehypeGraphviz, 4 | graphvizSvg, 5 | } from "@beoe/rehype-graphviz"; 6 | 7 | export { graphvizSvg }; 8 | 9 | const cache = await getCache(); 10 | 11 | export const rehypeGraphviz = (options = {}) => { 12 | // @ts-expect-error 13 | return defaultRehypeGraphviz({ 14 | cache, 15 | ...options, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/docs/src/components/iframeTarget.ts: -------------------------------------------------------------------------------- 1 | // to make sure anchors open in top frame 2 | document 3 | .querySelectorAll(".beoe iframe") 4 | // @ts-expect-error 5 | .forEach((iframe: HTMLIFrameElement) => { 6 | const addTargets = () => 7 | iframe.contentDocument 8 | ?.querySelectorAll("a") 9 | .forEach((x) => x.setAttribute("target", "_top")); 10 | iframe.addEventListener("load", addTargets); 11 | addTargets(); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/notes/graph-libraries.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Graph libraries 3 | draft: true 4 | --- 5 | 6 | - graphology is about 70kb uncompressed 7 | - `@dagrejs/graphlib` is about 12kb 8 | - [graph-data-structure](https://www.npmjs.com/package/graph-data-structure) is about 4kb (but it doesn't support ids for edges) 9 | - [DirectedGraph](https://data-structure-typed-docs.vercel.app/classes/DirectedGraph.html) is about 40kb 10 | - [Other options](https://www.npmjs.com/search?q=graph-theory) 11 | -------------------------------------------------------------------------------- /packages/rehype-penrose/test/fixtures/a.md: -------------------------------------------------------------------------------- 1 | ```penrose style="euclidean.style" domain="euclidean.domain" width=800 height=800 2 | Plane P 3 | Point p, q, r, s 4 | In(p, P) 5 | In(q, P) 6 | In(r, P) 7 | In(s, P) 8 | Let a := Segment(p, q) 9 | Let b := Segment(p, r) 10 | Point m := Midpoint(a) 11 | In(m, P) 12 | Angle theta := InteriorAngle(q, p, r) 13 | Let t := Triangle(p, r, s) 14 | Ray w := Bisector(theta) 15 | Segment h := PerpendicularBisector(a, m) 16 | AutoLabel p, q, r, s, m 17 | Label P $E^2$ 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/docs/src/components/PageFrame.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Props } from "@astrojs/starlight/props"; 3 | import Default from "@astrojs/starlight/components/PageFrame.astro"; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/examples/penrose-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: penrose 3 | draft: true 4 | --- 5 | 6 | ```penrose style="euclidean.style" domain="euclidean.domain" width=800 height=800 7 | Plane P 8 | Point p, q, r, s 9 | In(p, P) 10 | In(q, P) 11 | In(r, P) 12 | In(s, P) 13 | Let a := Segment(p, q) 14 | Let b := Segment(p, r) 15 | Point m := Midpoint(a) 16 | In(m, P) 17 | Angle theta := InteriorAngle(q, p, r) 18 | Let t := Triangle(p, r, s) 19 | Ray w := Bisector(theta) 20 | Segment h := PerpendicularBisector(a, m) 21 | AutoLabel p, q, r, s, m 22 | Label P $E^2$ 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 6 | import { render, RehypeVizdomConfig } from "./render.js"; 7 | 8 | export const rehypeVizdom = rehypeCodeHookImg({ 9 | language: "vizdom", 10 | render, 11 | }); 12 | 13 | export default rehypeVizdom; 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | exclude: ["**/vendor/**", "**/node_modules/**", "**/experiments/**"], 8 | coverage: { 9 | provider: "v8", 10 | include: ["**/packages/*/src/**/*.ts"], 11 | exclude: ["**/node_modules/**", "**/dist/**", "**/scripts/**"], 12 | }, 13 | poolOptions: { 14 | threads: { 15 | singleThread: true, 16 | }, 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "module": "node16", 7 | "lib": ["ES2022"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "Node16", 12 | "esModuleInterop": true, 13 | "declaration": false, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | "types": ["node"] 23 | } 24 | } -------------------------------------------------------------------------------- /experiments/rehype-pintora/src/pintora.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@pintora/cli"; 2 | // import type { CLIRenderOptions } from '@pintora/cli/lib/render' 3 | // import type { PintoraConfig } from '@pintora/core/lib/config' 4 | 5 | export const pintoraSvg = async (code: string, className?: string) => { 6 | let svg = (await render({ code, mimeType: "image/svg+xml" })) as string; 7 | 8 | // can't remove width and height, because there is no viewport 9 | // svg = svg.replace(/\s+width="\d+[^"]+"/, ""); 10 | // svg = svg.replace(/\s+height="\d+[^"]+"/, ""); 11 | return `
${svg}
`; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 6 | import { RehypeGraphvizConfig, renderGraphviz } from "./render.js"; 7 | 8 | export const rehypeGraphviz = rehypeCodeHookImg({ 9 | language: "dot", 10 | class: "graphviz", 11 | render: (code, options) => renderGraphviz({ ...options, code }), 12 | }); 13 | 14 | export default rehypeGraphviz; 15 | -------------------------------------------------------------------------------- /packages/fenceparser/test/utils.ts: -------------------------------------------------------------------------------- 1 | import type {FenceparserError} from '../src' 2 | 3 | // from https://stackoverflow.com/a/30551462 4 | const p = (xs: string[]): Array => { 5 | if (!xs.length) return [[]] 6 | return xs.flatMap((x) => p(xs.filter((v) => v !== x)).map((vs) => [x, ...vs])) 7 | } 8 | 9 | interface TestCase { 10 | input: string[] 11 | output?: Record | null 12 | error?: FenceparserError 13 | } 14 | 15 | export const prepareCases = (cases: TestCase[]) => { 16 | return cases 17 | .map(({input, ...props}) => 18 | p(input).map((p) => ({input: p.join(' '), ...props})), 19 | ) 20 | .reduce((a, b) => [...a, ...b]) 21 | } 22 | -------------------------------------------------------------------------------- /packages/pan-zoom/src/utils.ts: -------------------------------------------------------------------------------- 1 | export type Coords = Array<[number, number]>; 2 | 3 | export function center(coords: Coords) { 4 | return coords.length > 1 5 | ? coords 6 | .reduce(([xAcc, yAcc], [x, y]) => [xAcc + x, yAcc + y], [0, 0]) 7 | .map((k) => k / coords.length) 8 | : coords[0]; 9 | } 10 | 11 | export function centerDiff(co1: Coords, co2: Coords) { 12 | const c1 = center(co1); 13 | const c2 = center(co2); 14 | return [c1[0] - c2[0], c1[1] - c2[1]] as const; 15 | } 16 | 17 | export function distance(c: Coords) { 18 | return c.length === 2 19 | ? Math.sqrt(Math.pow(c[0][0] - c[1][0], 2) + Math.pow(c[0][1] - c[1][1], 2)) 20 | : 0; 21 | } 22 | -------------------------------------------------------------------------------- /packages/docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/start-here/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | Basic idea. You have Markdown like this: 8 | 9 | ````md 10 | ```some-diagram 11 | diagram text 12 | ``` 13 | ```` 14 | 15 | ...and you will get an output like this (image of your diagram): 16 | 17 | ```html 18 | ... 19 | ``` 20 | 21 | or 22 | 23 | ```html 24 | 25 | ``` 26 | 27 | Diagrams can be embedded in HTML differently, each with its own advantages and trade-offs. [Read more here](/start-here/tag/). 28 | 29 | ## TODO 30 | 31 | - [ ] add support for image tags 32 | 33 | ```md 34 | ![alt](path/to/diagram.some-diagram) 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypeGraphviz from "../src"; 9 | 10 | it("renders diagram", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeGraphviz) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypeGraphviz from "../src"; 9 | 10 | it("renders diagram", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeGraphviz) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 19 | }); 20 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypeColorChips from "../src"; 9 | 10 | it("highlits code", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeColorChips) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 19 | }); 20 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypePintora from "../src"; 9 | 10 | it.skip("renders diagram", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypePintora) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/rehype-gnuplot/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypeGnuplot from "../src/index.js"; 9 | 10 | it("renders diagram", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeGnuplot) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 19 | }); 20 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypeStarryNight from "../src"; 9 | 10 | it("highlits code", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeStarryNight) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 19 | }); 20 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "unified"; 2 | import type { Root } from "hast"; 3 | import { pintoraSvg } from "./pintora.js"; 4 | import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; 5 | 6 | export { pintoraSvg }; 7 | 8 | export type RehypePintoraConfig = { 9 | cache?: MapLike; 10 | class?: string; 11 | }; 12 | 13 | export const rehypePintora: Plugin<[RehypePintoraConfig?], Root> = ( 14 | options = {} 15 | ) => { 16 | const salt = { class: options.class }; 17 | // @ts-expect-error 18 | return rehypeCodeHook({ 19 | ...options, 20 | salt, 21 | language: "pintora", 22 | code: ({ code }) => pintoraSvg(code, options.class), 23 | }); 24 | }; 25 | 26 | export default rehypePintora; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beoe", 3 | "version": "1.0.0", 4 | "description": "Diagram All The Things", 5 | "author": "stereobooster", 6 | "license": "MIT", 7 | "private": true, 8 | "type": "module", 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "dependencies": { 13 | "@types/node": "^22.13.14", 14 | "turbo": "^2.4.4", 15 | "typescript": "^5.8.2", 16 | "vite-tsconfig-paths": "^5.1.4", 17 | "vitest": "^3.0.9" 18 | }, 19 | "scripts": { 20 | "test": "vitest", 21 | "build": "turbo run build --concurrency 15", 22 | "dev": "turbo run dev --concurrency 15", 23 | "clean": "turbo run clean --concurrency 15", 24 | "tsc": "turbo run tsc --concurrency 15" 25 | }, 26 | "packageManager": "pnpm@9.2.0" 27 | } 28 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/test/fixtures/a.html: -------------------------------------------------------------------------------- 1 |
HelloWorld
-------------------------------------------------------------------------------- /experiments/astro-graphviz/Graphviz.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { processGraphvizSvg } from "@beoe/rehype-graphviz"; 3 | // Error: Command failed with exit code 1. (no details) 4 | // import { serialize } from "node:v8"; 5 | // import { getCache } from "@beoe/cache"; 6 | 7 | import { Graphviz } from "@hpcc-js/wasm"; 8 | const graphviz = await Graphviz.load(); 9 | 10 | // const cache = await getCache(); 11 | 12 | interface Props { 13 | code: string; 14 | class?: string; 15 | } 16 | 17 | let svg: string; 18 | // const cacheKey = serialize(Astro.props); 19 | // svg = cache.get(cacheKey); 20 | // if (svg === undefined) { 21 | // TODO: this is broken 22 | svg = processGraphvizSvg(graphviz.dot(Astro.props.code), Astro.props.class); 23 | // cache.set(cacheKey, svg); 24 | // } 25 | --- 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-vizdom 2 | 3 | Rehype plugin to generate [Vizdom](https://github.com/vizdom-dev/vizdom) diagrams in place of code fences. This 4 | 5 | ````md 6 | ```vizdom 7 | digraph G { Hello -> World } 8 | ``` 9 | ```` 10 | 11 | will be converted to 12 | 13 | ```html 14 |
15 | ... 16 |
17 | ``` 18 | 19 | which can look like this: 20 | 21 | **TODO**: add screenshot 22 | 23 | ## Usage 24 | 25 | ```js 26 | import rehypeVizdom from "@beoe/rehype-vizdom"; 27 | 28 | const html = await unified() 29 | .use(remarkParse) 30 | .use(remarkRehype) 31 | .use(rehypeVizdom) 32 | .use(rehypeStringify) 33 | .process(`markdown`); 34 | ``` 35 | 36 | [Online documentation](https://beoe.stereobooster.com/diagrams/vizdom/) provides more details. 37 | -------------------------------------------------------------------------------- /packages/rehype-d2/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-d2 2 | 3 | Rehype plugin to generate [d2](https://d2lang.com) diagrams in place of code fences. This: 4 | 5 | ````md 6 | ```d2 7 | x -> y: hello world 8 | ``` 9 | ```` 10 | 11 | will be converted to 12 | 13 | ```html 14 |
15 | ... 16 |
17 | ``` 18 | 19 | which can look like this: 20 | 21 | example of how generated diagram looks 22 | 23 | ## Usage 24 | 25 | ```js 26 | import rehypeD2 from "@beoe/rehype-d2"; 27 | 28 | const html = await unified() 29 | .use(remarkParse) 30 | .use(remarkRehype) 31 | .use(rehypeD2) 32 | .use(rehypeStringify) 33 | .process(`markdown`); 34 | ``` 35 | 36 | [Online documentation](https://beoe.stereobooster.com/diagrams/d2/) provides more details. 37 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/test/pintora.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import { pintoraSvg } from "../src/pintora"; 4 | 5 | const diagram = `activityDiagram 6 | title: Activity Diagram Simple Action 7 | :Action 1; 8 | :Action 2;` 9 | 10 | it.skip("renders SVG", async () => { 11 | const result = await pintoraSvg(diagram); 12 | 13 | await expect(result).toMatchFileSnapshot("./fixtures/a.out.svg"); 14 | }); 15 | 16 | it.skip("removes width and height", async () => { 17 | const result = await pintoraSvg(diagram); 18 | 19 | expect(result).not.toContain(`svg width=`); 20 | expect(result).not.toContain(`svg height=`); 21 | }); 22 | 23 | it.skip("wraps in a figure with classes", async () => { 24 | const result = await pintoraSvg(diagram); 25 | 26 | expect(result).toContain(`
15 | ... 16 |
17 | ``` 18 | 19 | which looks like this: 20 | 21 | ![example of how generated graph looks](./example.svg) 22 | 23 | ## Usage 24 | 25 | ```js 26 | import rehypeGnuplot from "@beoe/rehype-gnuplot"; 27 | 28 | const html = await unified() 29 | .use(remarkParse) 30 | .use(remarkRehype) 31 | .use(rehypeGnuplot) 32 | .use(rehypeStringify) 33 | .process(`markdown`); 34 | ``` 35 | 36 | [Online documentation](https://beoe.stereobooster.com/diagrams/gnuplot/) provides more details. 37 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-mermaid 2 | 3 | Rehype plugin to generate [Mermaid](https://mermaid.js.org/) diagrams in place of code fences. This: 4 | 5 | ````md 6 | ```mermaid 7 | flowchart LR 8 | start --> stop 9 | ``` 10 | ```` 11 | 12 | will be converted to 13 | 14 | ```html 15 |
16 | ... 17 |
18 | ``` 19 | 20 | which can look like this: 21 | 22 | ```mermaid 23 | flowchart LR 24 | start --> stop 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```js 30 | import rehypeMermaid from "@beoe/rehype-mermaid"; 31 | 32 | const html = await unified() 33 | .use(remarkParse) 34 | .use(remarkRehype) 35 | .use(rehypeMermaid) 36 | .use(rehypeStringify) 37 | .process(`markdown`); 38 | ``` 39 | 40 | [Online documentation](https://beoe.stereobooster.com/diagrams/mermaid/) provides more details. 41 | -------------------------------------------------------------------------------- /packages/rehype-penrose/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypePenrose from "../src"; 9 | import { fileURLToPath } from "node:url"; 10 | import { resolve } from "node:path"; 11 | 12 | const shared = resolve(fileURLToPath(import.meta.url), `../shared`); 13 | 14 | it.skip("renders diagram", async () => { 15 | const file = await unified() 16 | .use(remarkParse) 17 | .use(remarkRehype) 18 | .use(rehypePenrose, { shared }) 19 | .use(rehypeStringify) 20 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 21 | 22 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/rehype-code-hook/src/waitFor.ts: -------------------------------------------------------------------------------- 1 | const pending: WeakMap> = new WeakMap(); 2 | const resolved: WeakMap = new WeakMap(); 3 | 4 | export const waitFor = 5 | ( 6 | toWait: () => Promise, 7 | toExecute: (x: A) => (x: B) => C | Promise 8 | ) => 9 | (x: B): C | Promise => { 10 | if (resolved.has(toWait)) { 11 | return resolved.get(toWait)(x); 12 | } 13 | 14 | if (!pending.has(toWait)) { 15 | pending.set( 16 | toWait, 17 | toWait() 18 | .then((y) => { 19 | resolved.set(toWait, toExecute(y)); 20 | pending.delete(toWait); 21 | }) 22 | .catch((e) => { 23 | pending.delete(toWait); 24 | throw e; 25 | }) 26 | ); 27 | } 28 | 29 | return pending.get(toWait)!.then(() => resolved.get(toWait)(x)); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/fenceparser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/fenceparser", 3 | "version": "2.3.1", 4 | "description": "A well-tested parser for parsing metadata out of fenced code blocks in Markdown", 5 | "keywords": [ 6 | "markdown" 7 | ], 8 | "license": "MIT", 9 | "author": "Frenco ", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stereobooster/beoe.git", 13 | "directory": "packages/fenceparser" 14 | }, 15 | "type": "module", 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.js" 20 | } 21 | }, 22 | "main": "./dist/index.js", 23 | "module": "./dist/index.js", 24 | "files": [ 25 | "dist" 26 | ], 27 | "types": "./dist/index.d.ts", 28 | "scripts": { 29 | "test": "exit 0", 30 | "build": "rm -rf dist && tsc", 31 | "dev": "tsc --watch", 32 | "clean": "rm -rf dist" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/pan-zoom/src/utilsDom.ts: -------------------------------------------------------------------------------- 1 | export const identity = new DOMMatrix([1, 0, 0, 1, 0, 0]); 2 | 3 | export function ttm(m: DOMMatrix) { 4 | return m.toString(); 5 | } 6 | 7 | export function transformXY(m: DOMMatrix, x: number, y: number) { 8 | const newPoint = new DOMPointReadOnly(x, y).matrixTransform(m); 9 | return [newPoint.x, newPoint.y] as const; 10 | } 11 | 12 | export function getScale(m: DOMMatrix) { 13 | return m.a; 14 | } 15 | 16 | export function scale(m: DOMMatrix, sf: number) { 17 | return m.scale(sf); 18 | } 19 | 20 | export function scaleAt(m: DOMMatrix, sf: number, x: number, y: number) { 21 | const scaleMatrix = scale(m, sf); 22 | const [nx1, ny1] = transformXY(m.inverse(), x, y); 23 | const [nx2, ny2] = transformXY(scaleMatrix.inverse(), x, y); 24 | return translate(scaleMatrix, nx2 - nx1, ny2 - ny1); 25 | } 26 | 27 | export function translate(m: DOMMatrix, dx: number, dy: number) { 28 | return m.translate(dx, dy); 29 | } 30 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "unified"; 2 | import type { Root } from "hast"; 3 | import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; 4 | import { h } from "hastscript"; 5 | import vc from "validate-color"; 6 | 7 | export type RehypeColorChipsConfig = { 8 | cache?: MapLike; 9 | }; 10 | 11 | export const rehypeColorChips: Plugin<[RehypeColorChipsConfig?], Root> = ( 12 | options = {} 13 | ) => { 14 | // @ts-expect-error 15 | return rehypeCodeHook({ 16 | ...options, 17 | inline: true, 18 | code: ({ language, code }) => { 19 | // @ts-expect-error 20 | if (language !== undefined || !vc(code)) return undefined; 21 | 22 | return h("code", [ 23 | code, 24 | h("span", { 25 | className: [`gfm-color-chip`], 26 | style: `background-color:${code};`, 27 | }), 28 | ]); 29 | }, 30 | }); 31 | }; 32 | 33 | export default rehypeColorChips; 34 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 6 | // @ts-ignore 7 | import plantuml from "plantuml"; 8 | 9 | export type RehypePlantumlConfig = {}; 10 | 11 | export const rehypePlantuml = rehypeCodeHookImg({ 12 | language: "plantuml", 13 | render: async (code: string) => { 14 | let svg: string = await plantuml(code); 15 | svg = svg.replace(`contentStyleType="text/css"`, ""); 16 | svg = svg.replace(`preserveAspectRatio="none"`, ""); 17 | svg = svg.replace(`zoomAndPan="magnify"`, ""); 18 | svg = svg.replace(/style="[^"]+"\s+/, ""); 19 | 20 | return { svg }; 21 | }, 22 | }); 23 | 24 | export default rehypePlantuml; 25 | -------------------------------------------------------------------------------- /packages/cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/cache", 3 | "type": "module", 4 | "version": "0.1.1", 5 | "description": "Shareable persistent LRU TTL cache based on SQLite", 6 | "keywords": [ 7 | "cache", 8 | "sqlite" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/cache" 16 | }, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.js" 21 | } 22 | }, 23 | "main": "./dist/index.js", 24 | "module": "./dist/index.js", 25 | "files": [ 26 | "dist" 27 | ], 28 | "types": "./dist/index.d.ts", 29 | "scripts": { 30 | "test": "exit 0", 31 | "build": "rm -rf dist && tsc", 32 | "dev": "tsc --watch", 33 | "clean": "rm -rf dist" 34 | }, 35 | "dependencies": { 36 | "@beoe/sqlitecache": "workspace:*", 37 | "cosmiconfig": "^9.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /experiments/astro-graphviz/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/astro-graphviz 2 | 3 | > [!WARNING] 4 | > this is broken 5 | 6 | Astro component to generate [Graphviz](https://graphviz.org/) diagrams as inline SVG. 7 | 8 | ## Uage 9 | 10 | In MDX or Astro: 11 | 12 | ```mdx 13 | import { Graphviz } from "@beoe/astro-graphviz"; 14 | 15 | end 19 | }`} 20 | /> 21 | ``` 22 | 23 | It also provides rehype plugin (re-exports [@beoe/rehype-graphviz](/packages/rehype-graphviz/)) with cache preconfigured: 24 | 25 | ```js 26 | import { rehypeGraphviz } from "@beoe/astro-graphviz/rehype"; 27 | 28 | // https://astro.build/config 29 | export default defineConfig({ 30 | markdown: { 31 | rehypePlugins: [rehypeGraphviz], 32 | }, 33 | }); 34 | ``` 35 | 36 | ## Tips 37 | 38 | See [@beoe/rehype-graphviz](/packages/rehype-graphviz/) 39 | 40 | ## TODO 41 | 42 | - use `@beoe/cache` in Astro component 43 | - add type declarations for `@beoe/astro-graphviz/rehype` 44 | -------------------------------------------------------------------------------- /packages/sqlitecache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/sqlitecache", 3 | "type": "module", 4 | "version": "0.1.1", 5 | "description": "Persistent LRU TTL cache based on SQLite", 6 | "keywords": [ 7 | "cache", 8 | "sqlite" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/sqlitecache" 16 | }, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.js" 21 | } 22 | }, 23 | "main": "./dist/index.js", 24 | "module": "./dist/index.js", 25 | "files": [ 26 | "dist" 27 | ], 28 | "types": "./dist/index.d.ts", 29 | "scripts": { 30 | "test": "vitest", 31 | "build": "rm -rf dist && tsc", 32 | "dev": "tsc --watch", 33 | "clean": "rm -rf dist" 34 | }, 35 | "dependencies": { 36 | "better-sqlite3": "^11.9.1" 37 | }, 38 | "devDependencies": { 39 | "@types/better-sqlite3": "^7.6.12" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro", 11 | "postinstall": "playwright install chromium" 12 | }, 13 | "dependencies": { 14 | "@astrojs/check": "^0.9.4", 15 | "@astrojs/starlight": "^0.32.5", 16 | "@beoe/cache": "workspace:*", 17 | "@beoe/pan-zoom": "workspace:*", 18 | "@beoe/rehype-d2": "workspace:*", 19 | "@beoe/rehype-gnuplot": "workspace:*", 20 | "@beoe/rehype-graphviz": "workspace:*", 21 | "@beoe/rehype-mermaid": "workspace:*", 22 | "@beoe/rehype-penrose": "workspace:*", 23 | "@beoe/rehype-vizdom": "workspace:*", 24 | "@dagrejs/graphlib": "^2.2.4", 25 | "astro": "5.5.5", 26 | "playwright": "^1.51.1", 27 | "sharp": "^0.33.5", 28 | "starlight-package-managers": "^0.10.0", 29 | "typescript": "^5.8.2", 30 | "vite-plugin-qrcode": "^0.2.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/rehype-d2/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypeD2 from "../src"; 9 | 10 | it("renders diagram", async () => { 11 | const file = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeD2) 15 | .use(rehypeStringify) 16 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 17 | 18 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a-inline.html"); 19 | }); 20 | 21 | it("renders diagram", async () => { 22 | const file = await unified() 23 | .use(remarkParse) 24 | .use(remarkRehype) 25 | .use(rehypeD2, { strategy: "data-url" }) 26 | .use(rehypeStringify) 27 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 28 | 29 | await expect(file.toString()).toMatchFileSnapshot( 30 | "./fixtures/a1-datauri.html" 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/fenceparser/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest' 2 | import parse, {lex, parse as pureParse} from '../src' 3 | import cases, {n} from './cases' 4 | 5 | describe('test an edge case for parser', () => { 6 | it('parser should return null if input is empty', () => { 7 | expect(pureParse([])).toEqual({}) 8 | }) 9 | }) 10 | 11 | describe(`${n} tests with ordering permutations`, () => { 12 | for (const test of cases) { 13 | it(`${test.error ? 'Exception: ' : ''}${test.input}`, () => { 14 | if (test.error) { 15 | expect(() => parse(test.input)).toThrowError(test.error) 16 | } else if (test.output) { 17 | // `pureParse(lex` is same as `parse` 18 | expect(pureParse(lex(test.input))).toEqual(test.output) 19 | } 20 | }) 21 | } 22 | }) 23 | 24 | describe('lowerCase', () => { 25 | it('lower-cases options by default', () => { 26 | expect(parse('Test=Test')).toEqual({test: "Test"}) 27 | }) 28 | it('lower-cases options by default', () => { 29 | expect(parse('Test=Test', {lowerCase: false})).toEqual({Test: "Test"}) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "unified"; 2 | import type { Root } from "hast"; 3 | import { rehypeCodeHook, type MapLike } from "@beoe/rehype-code-hook"; 4 | import { common, createStarryNight } from "@wooorm/starry-night"; 5 | import { h } from "hastscript"; 6 | 7 | const starryNight = await createStarryNight(common); 8 | 9 | export type RehypeStarryNightConfig = { 10 | cache?: MapLike; 11 | }; 12 | 13 | export const rehypeStarryNight: Plugin<[RehypeStarryNightConfig?], Root> = ( 14 | options = {} 15 | ) => { 16 | // @ts-expect-error 17 | return rehypeCodeHook({ 18 | ...options, 19 | code: ({ language, code, inline }) => { 20 | if (inline === true || language === undefined) return undefined; 21 | const scope = starryNight.flagToScope(language); 22 | if (scope === undefined) return undefined; 23 | 24 | return h("pre", [ 25 | h("code", { class: `starry-night language-${language}` }, [ 26 | starryNight.highlight(code, scope), 27 | ]), 28 | ]); 29 | }, 30 | }); 31 | }; 32 | 33 | export default rehypeStarryNight; 34 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-graphviz 2 | 3 | Rehype plugin to generate [Graphviz](https://graphviz.org/) diagrams in place of code fences. This 4 | 5 | ````md 6 | ```dot 7 | digraph G { Hello -> World } 8 | ``` 9 | ```` 10 | 11 | will be converted to 12 | 13 | ```html 14 |
15 | ... 16 |
17 | ``` 18 | 19 | which can look like this: 20 | 21 | **TODO**: add screenshot 22 | 23 | ## Usage 24 | 25 | ```js 26 | import rehypeGraphviz from "@beoe/rehype-graphviz"; 27 | 28 | const html = await unified() 29 | .use(remarkParse) 30 | .use(remarkRehype) 31 | .use(rehypeGraphviz) 32 | .use(rehypeStringify) 33 | .process(`markdown`); 34 | ``` 35 | 36 | [Online documentation](https://beoe.stereobooster.com/diagrams/graphviz/) provides more details. 37 | 38 | ## TODO 39 | 40 | - expose options to load font metrics and images 41 | - `Warning: no hard-coded metrics for 'Helvetica,Arial,sans-serif'. Falling back to 'Times' metrics` 42 | - check `tred` and `unflatten` functions 43 | - https://hpcc-systems.github.io/hpcc-js-wasm/graphviz/src/graphviz/classes/Graphviz.html 44 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { afterAll, beforeAll, expect, it, vi } from "vitest"; 7 | 8 | import rehypeMermaid from "../src"; 9 | 10 | beforeAll(() => { 11 | const m = { 12 | random: vi.fn(() => 0.1), 13 | trunc: Math.trunc, 14 | abs: Math.abs, 15 | max: Math.max, 16 | floor: Math.floor, 17 | pow: Math.pow, 18 | round: Math.round, 19 | min: Math.min, 20 | }; 21 | 22 | vi.stubGlobal("Math", m); 23 | }); 24 | 25 | afterAll(() => { 26 | vi.unstubAllGlobals(); 27 | }); 28 | 29 | // this test fails in CI 30 | it.skip("renders diagram", async () => { 31 | const file = await unified() 32 | .use(remarkParse) 33 | .use(remarkRehype) 34 | .use(rehypeMermaid) 35 | .use(rehypeStringify) 36 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 37 | 38 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a.html"); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/fenceparser/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Frenco Jobs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /experiments/astro-graphviz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/astro-graphviz", 3 | "version": "0.0.1", 4 | "description": "Astro graphviz component", 5 | "keywords": [ 6 | "astro", 7 | "astro-component", 8 | "graphviz" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/astro-graphviz" 16 | }, 17 | "type": "module", 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.js" 22 | } 23 | }, 24 | "files": [ 25 | "index.ts", 26 | "Graphviz.astro", 27 | "rehype.mjs" 28 | ], 29 | "scripts": { 30 | "test": "exit 0", 31 | "build": "exit 0", 32 | "dev": "exit 0", 33 | "clean": "exit 0" 34 | }, 35 | "dependencies": { 36 | "@beoe/cache": "workspace:*", 37 | "@beoe/rehype-graphviz": "workspace:*", 38 | "@hpcc-js/wasm": "^2.16.1" 39 | }, 40 | "devDependencies": { 41 | "astro": "^4.16.1" 42 | }, 43 | "peerDependencies": { 44 | "astro": "^2.0.0 || ^3.0.0 || ^4.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/cache/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SQLiteCache } from "@beoe/sqlitecache"; 2 | import process from "node:process"; 3 | import { getConfig, Config, MapLike, defineConfig } from "./config.js"; 4 | export { Config, defineConfig }; 5 | 6 | let cachePromise: Promise; 7 | 8 | const getCacheOriginal = async () => { 9 | const { override, ...cfg } = await getConfig(); 10 | if (override) { 11 | return override; 12 | } 13 | 14 | const cache = new SQLiteCache(cfg); 15 | 16 | if (!cfg.readonly) { 17 | // Not sure if this is the best way to do it... 18 | process.on("exit", () => (cache as SQLiteCache).close()); 19 | // catches ctrl+c event 20 | process.on("SIGINT", () => process.exit()); 21 | // catches "kill pid" (for example: nodemon restart) 22 | process.on("SIGUSR1", () => process.exit()); 23 | process.on("SIGUSR2", () => process.exit()); 24 | // catches uncaught exceptions 25 | process.on("uncaughtException", () => process.exit()); 26 | } 27 | 28 | return cache; 29 | }; 30 | 31 | export const getCache = () => { 32 | if (!cachePromise) { 33 | cachePromise = getCacheOriginal(); 34 | } 35 | 36 | return cachePromise; 37 | }; 38 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it } from "vitest"; 7 | 8 | import rehypePlantuml from "../src"; 9 | 10 | // snapshots are different in CI and locally 11 | 12 | it.skip("renders diagram", async () => { 13 | const file = await unified() 14 | .use(remarkParse) 15 | .use(remarkRehype) 16 | .use(rehypePlantuml) 17 | .use(rehypeStringify) 18 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 19 | 20 | await expect(file.toString()).toMatchFileSnapshot("./fixtures/a-inline.html"); 21 | }); 22 | 23 | it.skip("renders diagram", async () => { 24 | const file = await unified() 25 | .use(remarkParse) 26 | .use(remarkRehype) 27 | .use(rehypePlantuml, { strategy: "data-url" }) 28 | .use(rehypeStringify) 29 | .process(await fs.readFile(new URL("./fixtures/a.md", import.meta.url))); 30 | 31 | await expect(file.toString()).toMatchFileSnapshot( 32 | "./fixtures/a1-datauri.html" 33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-starry-night 2 | 3 | Rehype plugin to highlight code with the help of [starry-night](https://github.com/wooorm/starry-night). This: 4 | 5 | ````md 6 | ```md 7 | # Hello, world! 8 | ``` 9 | ```` 10 | 11 | will be converted to: 12 | 13 | ```html 14 |
# Hello, world!
15 | ``` 16 | 17 | which can look like this: 18 | 19 | ```md 20 | # Hello, world! 21 | ``` 22 | 23 | ## Status 24 | 25 | Tried it out of curiosity (and it is kind of easy to do with `@beoe/rehype-code-hook`). There are other plugins that has better support, for example [`@shikijs/rehype`](https://shiki.matsu.io/packages/rehype). 26 | 27 | ## Usage 28 | 29 | ```js 30 | import rehypeStarryNight from "@beoe/rehype-starry-night"; 31 | 32 | const html = await unified() 33 | .use(remarkParse) 34 | .use(remarkRehype) 35 | .use(rehypeMermaid) 36 | .use(rehypeStarryNight) 37 | .process(`markdown`); 38 | ``` 39 | 40 | Don't forget to add CSS, for example: 41 | 42 | ```html 43 | 47 | ``` 48 | -------------------------------------------------------------------------------- /packages/remark-code-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/remark-code-hook", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "description": "remark plugin for code fences", 6 | "keywords": [ 7 | "remark" 8 | ], 9 | "author": "stereobooster", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stereobooster/beoe.git", 14 | "directory": "packages/remark-code-hook" 15 | }, 16 | "sideEffects": false, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.js" 21 | } 22 | }, 23 | "main": "./dist/index.js", 24 | "module": "./dist/index.js", 25 | "files": [ 26 | "dist" 27 | ], 28 | "types": "./dist/index.d.ts", 29 | "scripts": { 30 | "test": "vitest", 31 | "build": "rm -rf dist && tsc", 32 | "dev": "tsc --watch", 33 | "clean": "rm -rf dist" 34 | }, 35 | "dependencies": { 36 | "@types/mdast": "^4.0.4", 37 | "unified": "^11.0.5", 38 | "unist-util-visit": "^5.0.0" 39 | }, 40 | "devDependencies": { 41 | "rehype-raw": "^7.0.0", 42 | "rehype-stringify": "^10.0.1", 43 | "remark-parse": "^11.0.0", 44 | "remark-rehype": "^11.1.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/pan-zoom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/pan-zoom", 3 | "version": "0.0.4", 4 | "description": "Pan and zoom for SVG images", 5 | "author": "stereobooster", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/stereobooster/beoe.git", 10 | "directory": "packages/pan-zoom" 11 | }, 12 | "keywords": [ 13 | "svg", 14 | "pan", 15 | "zoom", 16 | "gestures", 17 | "touchscreen" 18 | ], 19 | "type": "module", 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "default": "./dist/index.js", 24 | "import": "./dist/index.js" 25 | }, 26 | "./css/*.css": { 27 | "import": "./css/*.css", 28 | "require": "./css/*.css" 29 | } 30 | }, 31 | "main": "./dist/index.js", 32 | "module": "./dist/index.js", 33 | "files": [ 34 | "./dist/*", 35 | "./css/*" 36 | ], 37 | "types": "./dist/index.d.ts", 38 | "scripts": { 39 | "test": "vitest", 40 | "prepublishOnly": "npm run build", 41 | "build": "rm -rf dist && tsc", 42 | "dev": "tsc --watch", 43 | "clean": "rm -rf dist", 44 | "tsc": "tsc" 45 | }, 46 | "devDependencies": { 47 | "mathjs": "^14.4.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/rehype-gnuplot/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 6 | import { waitFor } from "@beoe/rehype-code-hook"; 7 | 8 | // because gnuplot-wasm doesn't provide TS signatures 9 | type R = (x: string) => { svg: string }; 10 | /** 11 | * If all gnuplot diagrams are cached it would not even load module in memory. 12 | * If there are diagrams, it would load module and first few renders would be async, 13 | * but all consequent renders would be sync 14 | */ 15 | export const render = waitFor( 16 | async () => { 17 | // @ts-ignore 18 | const gnuplot = (await import("gnuplot-wasm")).default; 19 | return (gnuplot() as Promise<{ render: R }>).then(({ render }) => render); 20 | }, 21 | (render) => (code: string) => render(code) 22 | ); 23 | 24 | export type RehypeGnuplotConfig = {}; 25 | 26 | export const rehypeGnuplot = rehypeCodeHookImg({ 27 | language: "gnuplot", 28 | render, 29 | }); 30 | 31 | export default rehypeGnuplot; 32 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-plantuml", 3 | "type": "module", 4 | "version": "0.3.0", 5 | "description": "rehype plantuml plugin", 6 | "keywords": [ 7 | "rehype", 8 | "plantuml" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-plantuml" 16 | }, 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.js" 22 | } 23 | }, 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.js", 26 | "files": [ 27 | "dist" 28 | ], 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "test": "vitest", 32 | "build": "rm -rf dist && tsc", 33 | "dev": "tsc --watch", 34 | "clean": "rm -rf dist" 35 | }, 36 | "dependencies": { 37 | "@beoe/rehype-code-hook-img": "workspace:*", 38 | "plantuml": "^0.0.2" 39 | }, 40 | "devDependencies": { 41 | "@types/hast": "^3.0.4", 42 | "rehype-stringify": "^10.0.1", 43 | "remark-parse": "^11.0.0", 44 | "remark-rehype": "^11.1.1", 45 | "unified": "^11.0.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/rehype-penrose/src/penrose.ts: -------------------------------------------------------------------------------- 1 | // import { type compile } from "@penrose/core"; 2 | import { spawn } from "node:child_process"; 3 | import { fileURLToPath } from "url"; 4 | import { resolve } from "node:path"; 5 | const executablePath = resolve( 6 | fileURLToPath(import.meta.url), 7 | `../../bin/penrose.js` 8 | ); 9 | 10 | // type CompileOptions = Parameters[0]; 11 | type CompileOptions = { 12 | substance: string; 13 | style: string; 14 | domain: string; 15 | variation: string; 16 | excludeWarnings?: string[]; 17 | width: number; 18 | height: number; 19 | }; 20 | 21 | export const penrose = (opts: CompileOptions): Promise => { 22 | return new Promise((resolve, reject) => { 23 | let res = ""; 24 | 25 | const bin = spawn(executablePath, [], { 26 | windowsHide: true, 27 | }); 28 | bin.stdout.on("data", (data: Buffer) => { 29 | res += data.toString(); 30 | }); 31 | bin.stderr.on("data", (data: Buffer) => reject(data.toString())); 32 | bin.on("close", (code) => 33 | code === 0 34 | ? resolve(res) 35 | : reject(`child process exited with code ${code}`) 36 | ); 37 | 38 | bin.stdin.write(JSON.stringify(opts)); 39 | bin.stdin.end(); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/fenceparser/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fenceparser 2 | 3 | ## 2.3.0 4 | 5 | ### Minor Changes 6 | 7 | - added option to preserve case of key-words. Previously it was always lowercasing key-words 8 | 9 | ## 2.2.0 10 | 11 | ### Minor Changes 12 | 13 | - 4c3a51d: Optimize the library heavily reducing size from 5kb to less than 1kb, while still keeping all the tests passed. Minor refactors are also added, with a new error class `FenceparserError` exported. 14 | 15 | ## 2.1.1 16 | 17 | ### Patch Changes 18 | 19 | - 2955219: Add build step to ci fixing missing dist directory error. 20 | 21 | ## 2.1.0 22 | 23 | ### Minor Changes 24 | 25 | - 28d431d: Add support for CJS and ESM. We now use changeset to manage CHANGELOG and publish. 26 | 27 | ## 2.0.0 28 | 29 | - Remove TSDX 30 | - Opt-in to ES Modules 31 | 32 | ## 1.1.1 33 | 34 | - Add [minimalistic](https://npm.im/minimalistic) as default prettier config and format code 35 | 36 | ## 1.1.0 37 | 38 | - Return `Record` instead of `Map` 39 | 40 | ## 1.0.2 41 | 42 | - Add package information such as `description` and `keywords` 43 | 44 | ## 1.0.1 45 | 46 | - Export `lex` and `parse` functions individually 47 | 48 | ## 1.0.0 49 | 50 | - Initial release 51 | 52 | ## 0.1.0 53 | 54 | - Prelease 55 | -------------------------------------------------------------------------------- /packages/rehype-code-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-code-hook", 3 | "type": "module", 4 | "version": "0.1.1", 5 | "description": "rehype plugin for code fences", 6 | "keywords": [ 7 | "rehype" 8 | ], 9 | "author": "stereobooster", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stereobooster/beoe.git", 14 | "directory": "packages/rehype-code-hook" 15 | }, 16 | "sideEffects": false, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.js" 21 | } 22 | }, 23 | "main": "./dist/index.js", 24 | "module": "./dist/index.js", 25 | "files": [ 26 | "dist" 27 | ], 28 | "types": "./dist/index.d.ts", 29 | "scripts": { 30 | "test": "vitest", 31 | "build": "rm -rf dist && tsc", 32 | "dev": "tsc --watch", 33 | "clean": "rm -rf dist" 34 | }, 35 | "dependencies": { 36 | "@types/hast": "^3.0.4", 37 | "hast-util-from-html-isomorphic": "^2.0.0", 38 | "hast-util-to-text": "^4.0.2", 39 | "unified": "^11.0.5", 40 | "unist-util-visit": "^5.0.0" 41 | }, 42 | "devDependencies": { 43 | "rehype-stringify": "^10.0.1", 44 | "remark-parse": "^11.0.0", 45 | "remark-rehype": "^11.1.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-pintora", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "description": "rehype pintora plugin", 6 | "keywords": [ 7 | "rehype", 8 | "pintora" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-pintora" 16 | }, 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.js" 22 | } 23 | }, 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.js", 26 | "files": [ 27 | "dist" 28 | ], 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "test": "vitest", 32 | "build": "rm -rf dist && tsc", 33 | "dev": "tsc --watch", 34 | "clean": "rm -rf dist" 35 | }, 36 | "dependencies": { 37 | "@beoe/rehype-code-hook": "workspace:*", 38 | "@pintora/cli": "^0.7.4", 39 | "@pintora/standalone": "^0.7.3" 40 | }, 41 | "devDependencies": { 42 | "@types/hast": "^3.0.4", 43 | "unified": "^11.0.4", 44 | "rehype-stringify": "^10.0.0", 45 | "remark-parse": "^11.0.0", 46 | "remark-rehype": "^11.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-color-chips", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "description": "rehype color-chips plugin", 6 | "keywords": [ 7 | "rehype", 8 | "color-chips" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-color-chips" 16 | }, 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.js" 22 | } 23 | }, 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.js", 26 | "files": [ 27 | "dist" 28 | ], 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "test": "vitest", 32 | "build": "rm -rf dist && tsc", 33 | "dev": "tsc --watch", 34 | "clean": "rm -rf dist" 35 | }, 36 | "dependencies": { 37 | "@beoe/rehype-code-hook": "workspace:*", 38 | "hastscript": "^9.0.0", 39 | "validate-color": "^2.2.4" 40 | }, 41 | "devDependencies": { 42 | "@types/hast": "^3.0.4", 43 | "rehype-stringify": "^10.0.0", 44 | "remark-parse": "^11.0.0", 45 | "remark-rehype": "^11.1.0", 46 | "unified": "^11.0.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /experiments/rehype-starry-night/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-starry-night", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "description": "rehype starry-night plugin", 6 | "keywords": [ 7 | "rehype", 8 | "starry-night" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-starry-night" 16 | }, 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.js" 22 | } 23 | }, 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.js", 26 | "files": [ 27 | "dist" 28 | ], 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "test": "vitest", 32 | "build": "rm -rf dist && tsc", 33 | "dev": "tsc --watch", 34 | "clean": "rm -rf dist" 35 | }, 36 | "dependencies": { 37 | "@beoe/rehype-code-hook": "workspace:*", 38 | "@wooorm/starry-night": "^3.3.0", 39 | "hastscript": "^9.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/hast": "^3.0.4", 43 | "rehype-stringify": "^10.0.0", 44 | "remark-parse": "^11.0.0", 45 | "remark-rehype": "^11.1.0", 46 | "unified": "^11.0.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-vizdom", 3 | "type": "module", 4 | "version": "0.4.2", 5 | "description": "rehype vizdom plugin", 6 | "keywords": [ 7 | "rehype", 8 | "vizdom" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-vizdom" 16 | }, 17 | "homepage": "https://beoe.stereobooster.com/diagrams/vizdom/", 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.js" 23 | } 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "files": [ 28 | "dist" 29 | ], 30 | "types": "./dist/index.d.ts", 31 | "scripts": { 32 | "test": "vitest", 33 | "build": "rm -rf dist && tsc", 34 | "dev": "tsc --watch", 35 | "clean": "rm -rf dist" 36 | }, 37 | "dependencies": { 38 | "@beoe/rehype-code-hook-img": "workspace:^", 39 | "@vizdom/vizdom-ts-node": "^0.1.19" 40 | }, 41 | "devDependencies": { 42 | "@types/hast": "^3.0.4", 43 | "rehype-stringify": "^10.0.1", 44 | "remark-parse": "^11.0.0", 45 | "remark-rehype": "^11.1.1", 46 | "unified": "^11.0.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/rehype-d2/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 6 | import { D2Options, d2 } from "./d2.js"; 7 | 8 | export type RehypeD2Config = { 9 | d2Options?: Omit & { 10 | theme?: string; 11 | darkTheme?: string; 12 | }; 13 | }; 14 | 15 | export const rehypeD2 = rehypeCodeHookImg({ 16 | language: "d2", 17 | render: async (code: string, opts) => { 18 | const { darkMode, d2Options, ...rest } = opts; 19 | const newD2Options = { ...d2Options, ...rest }; 20 | if (newD2Options.theme !== undefined) { 21 | // @ts-ignore 22 | newD2Options.themeID = parseFloat(newD2Options.theme); 23 | } 24 | const { svg, data } = await d2(code, newD2Options); 25 | let darkSvg; 26 | if (darkMode) { 27 | darkSvg = ( 28 | await d2(code, { 29 | ...newD2Options, 30 | // @ts-ignore 31 | themeID: parseFloat(newD2Options?.darkTheme ?? "200"), 32 | }) 33 | ).svg; 34 | } 35 | return { svg, darkSvg, data }; 36 | }, 37 | }); 38 | 39 | export default rehypeD2; 40 | -------------------------------------------------------------------------------- /packages/rehype-d2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-d2", 3 | "type": "module", 4 | "version": "0.5.4", 5 | "description": "rehype d2 plugin", 6 | "keywords": [ 7 | "rehype", 8 | "d2" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-d2" 16 | }, 17 | "sideEffects": false, 18 | "homepage": "https://beoe.stereobooster.com/diagrams/d2/", 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.js" 23 | } 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "files": [ 28 | "dist" 29 | ], 30 | "types": "./dist/index.d.ts", 31 | "scripts": { 32 | "test": "vitest", 33 | "build": "rm -rf dist && tsc", 34 | "dev": "tsc --watch", 35 | "clean": "rm -rf dist" 36 | }, 37 | "dependencies": { 38 | "@beoe/rehype-code-hook-img": "workspace:^", 39 | "@terrastruct/d2": "^0.1.23", 40 | "async-memoize-one": "^1.1.8", 41 | "tinyglobby": "^0.2.12" 42 | }, 43 | "devDependencies": { 44 | "@types/hast": "^3.0.4", 45 | "rehype-stringify": "^10.0.1", 46 | "remark-parse": "^11.0.0", 47 | "remark-rehype": "^11.1.1", 48 | "unified": "^11.0.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/rehype-gnuplot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-gnuplot", 3 | "type": "module", 4 | "version": "0.5.0", 5 | "description": "rehype gnuplot plugin", 6 | "keywords": [ 7 | "rehype", 8 | "gnuplot" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-gnuplot" 16 | }, 17 | "sideEffects": false, 18 | "homepage": "https://beoe.stereobooster.com/diagrams/gnuplot/", 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.js" 23 | } 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "files": [ 28 | "dist" 29 | ], 30 | "types": "./dist/index.d.ts", 31 | "scripts": { 32 | "test": "vitest", 33 | "build": "rm -rf dist && tsc", 34 | "dev": "tsc --watch", 35 | "clean": "rm -rf dist" 36 | }, 37 | "dependencies": { 38 | "@beoe/rehype-code-hook": "workspace:^", 39 | "@beoe/rehype-code-hook-img": "workspace:^", 40 | "gnuplot-wasm": "^0.1.0" 41 | }, 42 | "devDependencies": { 43 | "@types/hast": "^3.0.4", 44 | "rehype-stringify": "^10.0.1", 45 | "remark-parse": "^11.0.0", 46 | "remark-rehype": "^11.1.1", 47 | "unified": "^11.0.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/rehype-penrose/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-penrose", 3 | "type": "module", 4 | "version": "0.2.2", 5 | "description": "rehype penrose plugin", 6 | "keywords": [ 7 | "rehype", 8 | "penrose" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-penrose" 16 | }, 17 | "homepage": "https://beoe.stereobooster.com/diagrams/penrose/", 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.js" 23 | } 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "files": [ 28 | "dist", 29 | "bin" 30 | ], 31 | "types": "./dist/index.d.ts", 32 | "scripts": { 33 | "test": "vitest", 34 | "build": "rm -rf dist && tsc", 35 | "dev": "tsc --watch", 36 | "clean": "rm -rf dist" 37 | }, 38 | "dependencies": { 39 | "@beoe/rehype-code-hook-img": "workspace:^", 40 | "@penrose/core": "^3.2.0", 41 | "global-jsdom": "^26.0.0" 42 | }, 43 | "devDependencies": { 44 | "@types/hast": "^3.0.4", 45 | "rehype-stringify": "^10.0.1", 46 | "remark-parse": "^11.0.0", 47 | "remark-rehype": "^11.1.1", 48 | "unified": "^11.0.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-graphviz", 3 | "type": "module", 4 | "version": "0.4.2", 5 | "description": "rehype graphviz plugin", 6 | "keywords": [ 7 | "rehype", 8 | "graphviz" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-graphviz" 16 | }, 17 | "homepage": "https://beoe.stereobooster.com/diagrams/graphviz/", 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.js" 23 | } 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "files": [ 28 | "dist" 29 | ], 30 | "types": "./dist/index.d.ts", 31 | "scripts": { 32 | "test": "vitest", 33 | "build": "rm -rf dist && tsc", 34 | "dev": "tsc --watch", 35 | "clean": "rm -rf dist" 36 | }, 37 | "dependencies": { 38 | "@beoe/rehype-code-hook": "workspace:^", 39 | "@beoe/rehype-code-hook-img": "workspace:^", 40 | "@hpcc-js/wasm": "^2.22.4" 41 | }, 42 | "devDependencies": { 43 | "@types/hast": "^3.0.4", 44 | "rehype-stringify": "^10.0.1", 45 | "remark-parse": "^11.0.0", 46 | "remark-rehype": "^11.1.1", 47 | "unified": "^11.0.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /experiments/rehype-color-chips/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-color-chips 2 | 3 | Rehype plugin to add "color chips" to inline code if it is a color. This: 4 | 5 | ```md 6 | `#bada55` 7 | ``` 8 | 9 | will be converted to: 10 | 11 | ```html 12 | #bada55 15 | ``` 16 | 17 | which can look like this: 18 | 19 | **TODO**: add screenshot 20 | 21 | ## Status 22 | 23 | Tried it out of curiosity (and it is kind of easy to do with `@beoe/rehype-code-hook`). There are other plugins for this: [rehype-color-chips](https://github.com/shreshthmohan/rehype-color-chips). 24 | 25 | Because it was an experiment I used all possible color variations from `validate-color` (`validateHTMLColor`, `validateHTMLColorSpecialName`, `validateHTMLColorName`), which I assume can have quite expensive Regexp. In production it is, probably, better to use cheaper checks. 26 | 27 | ## Usage 28 | 29 | ```js 30 | import rehypeColorChips from "@beoe/rehype-color-chips"; 31 | 32 | const html = await unified() 33 | .use(remarkParse) 34 | .use(remarkRehype) 35 | .use(rehypeMermaid) 36 | .use(rehypeColorChips) 37 | .process(`markdown`); 38 | ``` 39 | 40 | Don't forget to add CSS, for example: 41 | 42 | ```css 43 | .gfm-color-chip { 44 | width: 0.75rem; 45 | height: 0.75rem; 46 | margin-left: 0.3rem; 47 | display: inline-block; 48 | border-radius: 50%; 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-plantuml 2 | 3 | Rehype plugin to generate [plantuml](https://www.plantuml.com/) diagrams in place of code fences. This: 4 | 5 | ````md 6 | ```plantuml 7 | Bob->Alice : Hello! 8 | ``` 9 | ```` 10 | 11 | will be converted to 12 | 13 | ```html 14 |
15 | ... 16 |
17 | ``` 18 | 19 | which can look like this: 20 | 21 | example of how generated diagram looks 22 | 23 | ## PlantUML installation options 24 | 25 | There are several ways how to run PlantUML 26 | 27 | - with public server e.g. `https://www.plantuml.com/plantuml/svg/` 28 | - for example, see [remark-simple-plantuml](https://github.com/akebifiky/remark-simple-plantuml) 29 | - with docker 30 | - install PlantUML locally e.g. install Java, Graphviz and download copy of `plantuml.jar` 31 | 32 | I don't like any of those options, but for now settled down with the **last one**. 33 | 34 | ## Usage 35 | 36 | You need to install Java and Graphviz in order to use this plugin. 37 | 38 | ```js 39 | import rehypePlantuml from "@beoe/rehype-plantuml"; 40 | 41 | const html = await unified() 42 | .use(remarkParse) 43 | .use(remarkRehype) 44 | .use(rehypePlantuml) 45 | .use(rehypeStringify) 46 | .process(`markdown`); 47 | ``` 48 | 49 | It support caching the same way as [@beoe/rehype-code-hook](/packages/rehype-code-hook/) does. 50 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-mermaid", 3 | "type": "module", 4 | "version": "0.4.2", 5 | "description": "rehype mermaid plugin", 6 | "keywords": [ 7 | "rehype", 8 | "mermaid" 9 | ], 10 | "author": "stereobooster", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/stereobooster/beoe.git", 15 | "directory": "packages/rehype-mermaid" 16 | }, 17 | "homepage": "https://beoe.stereobooster.com/diagrams/mermaid/", 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.js" 23 | } 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "files": [ 28 | "dist" 29 | ], 30 | "types": "./dist/index.d.ts", 31 | "scripts": { 32 | "test": "vitest", 33 | "build": "rm -rf dist && tsc", 34 | "dev": "tsc --watch", 35 | "clean": "rm -rf dist" 36 | }, 37 | "dependencies": { 38 | "@beoe/rehype-code-hook-img": "workspace:^", 39 | "mermaid-isomorphic": "^3.0.2", 40 | "svgo": "^3.3.2" 41 | }, 42 | "devDependencies": { 43 | "@types/hast": "^3.0.4", 44 | "playwright": "^1.51.1", 45 | "rehype-stringify": "^10.0.1", 46 | "remark-parse": "^11.0.0", 47 | "remark-rehype": "^11.1.1", 48 | "unified": "^11.0.5" 49 | }, 50 | "engines": { 51 | "node": ">=20.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beoe/rehype-code-hook-img", 3 | "type": "module", 4 | "version": "0.4.1", 5 | "description": "rehype plugin for code fences", 6 | "keywords": [ 7 | "rehype" 8 | ], 9 | "author": "stereobooster", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stereobooster/beoe.git", 14 | "directory": "packages/rehype-code-hook-img" 15 | }, 16 | "sideEffects": false, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.js" 21 | } 22 | }, 23 | "main": "./dist/index.js", 24 | "module": "./dist/index.js", 25 | "files": [ 26 | "dist" 27 | ], 28 | "types": "./dist/index.d.ts", 29 | "scripts": { 30 | "test": "vitest", 31 | "build": "rm -rf dist && tsc", 32 | "dev": "tsc --watch", 33 | "clean": "rm -rf dist" 34 | }, 35 | "dependencies": { 36 | "@beoe/fenceparser": "workspace:^", 37 | "@beoe/rehype-code-hook": "workspace:^", 38 | "@node-rs/xxhash": "^1.7.6", 39 | "@types/hast": "^3.0.4", 40 | "hast-util-from-html-isomorphic": "^2.0.0", 41 | "hastscript": "^9.0.1", 42 | "mini-svg-data-uri": "^1.4.4", 43 | "svgo": "^3.3.2", 44 | "unified": "^11.0.5" 45 | }, 46 | "devDependencies": { 47 | "rehype-stringify": "^10.0.1", 48 | "remark-parse": "^11.0.0", 49 | "remark-rehype": "^11.1.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/start-here/strategy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Strategy 3 | sidebar: 4 | order: 4 5 | --- 6 | 7 | Global configuration: 8 | 9 | ```js 10 | use(rehypeDiagram, { 11 | strategy: "...", // one of 3 options 12 | }); 13 | ``` 14 | 15 | **or** local configuration 16 | 17 | ````md 18 | ```some-diagram strategy=... 19 | diagram text 20 | ``` 21 | ```` 22 | 23 | `inline` is a default option. 24 | 25 | ## Options 26 | 27 | ### `inline` 28 | 29 | ```html 30 |
...
31 | ``` 32 | 33 | ### `data-url` 34 | 35 | ```html 36 |
37 | 38 |
39 | ``` 40 | 41 | ### `file` 42 | 43 | ```html 44 |
45 | 46 |
47 | ``` 48 | 49 | **Note**: this strategy requires two additional options: 50 | 51 | ```js 52 | use(rehypeDiagram, { 53 | strategy: "file", 54 | // where to store files on the disk 55 | fsPath: "public/beoe", 56 | // path to files in a browser 57 | webPath: "/beoe", 58 | }); 59 | ``` 60 | 61 | add `fsPath` (`public/beoe`) to `.gitignore` 62 | 63 | **Note**: if you deploy to Netlify, do not use path that starts with `.`. 64 | 65 | ## Pros and cons 66 | 67 | | | `inline` | `data-url` | `file` | 68 | | -------------- | -------- | ---------- | ------ | 69 | | DOM footprint | high | low | low | 70 | | HTML footprint | high | high | low | 71 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/test/fixtures/a-inline.html: -------------------------------------------------------------------------------- 1 |
BobBobAliceAliceHello!
-------------------------------------------------------------------------------- /packages/rehype-penrose/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/rehype-penrose 2 | 3 | Rehype plugin to generate [penrose](https://penrose.cs.cmu.edu/) diagrams in place of code fences. This 4 | 5 | ````md 6 | ```penrose style="euclidean.style" domain="euclidean.domain" width=800 height=800 7 | Plane P 8 | Point p, q, r, s 9 | In(p, P) 10 | In(q, P) 11 | In(r, P) 12 | In(s, P) 13 | Let a := Segment(p, q) 14 | Let b := Segment(p, r) 15 | Point m := Midpoint(a) 16 | In(m, P) 17 | Angle theta := InteriorAngle(q, p, r) 18 | Let t := Triangle(p, r, s) 19 | Ray w := Bisector(theta) 20 | Segment h := PerpendicularBisector(a, m) 21 | AutoLabel p, q, r, s, m 22 | Label P $E^2$ 23 | ``` 24 | ```` 25 | 26 | will be converted to 27 | 28 | ```html 29 |
30 | ... 31 |
32 | ``` 33 | 34 | which looks like this: 35 | 36 | ![example of how generated graph looks](./example.svg) 37 | 38 | **Note**: 39 | 40 | You need to add **domain** and **style** files to `shared` folder, for example: 41 | 42 | - [`euclidean.domain`](./test/shared/euclidean.domain) 43 | - [`euclidean.style`](./test/shared/euclidean.style) 44 | 45 | ## Usage 46 | 47 | ```js 48 | import rehypePenrose from "@beoe/rehype-penrose"; 49 | 50 | const html = await unified() 51 | .use(remarkParse) 52 | .use(remarkRehype) 53 | .use(rehypePenrose, { shared: "shared" }) 54 | .use(rehypeStringify) 55 | .process(`markdown`); 56 | ``` 57 | 58 | [Online documentation](https://beoe.stereobooster.com/diagrams/penrose/) provides more details. 59 | 60 | ## TODO 61 | 62 | - [ ] fix SVGO issue 63 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | BobBobAliceAliceHello! 4 | -------------------------------------------------------------------------------- /packages/cache/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/cache 2 | 3 | > [!WARNING] 4 | > One of ideas was to use this as shared cache inside Astro components. But for now it doesn't work. If it is used inside Astro component build process (prerendering) fails. Though it does work in rehype/remark plugins. And in Astro components in development mode. 5 | > 6 | > Probably would need to create custom Astro integration (virtual module), but hard to tell because I don't understand why it fails 7 | 8 | Thin wrapper arround [@beoe/sqlitecache](/packages/sqlitecache/) so the same cache can be shared between different rehype/remark plugins and Astro components 9 | 10 | Because cache would be "hidden" inside other packages it exposes way to configure instance through local file (with cosmiconfig). 11 | 12 | By default it stores cache in `node_modules/.beoe` folder. This will make it easy to use, `node_modules` often cached in CI and added to `.gitignore`. On the other hand I haven't seen this approach before, there can be downsides that I haven't thought about. 13 | 14 | You can change where cache is stored using configuration (`beoe.config.mjs`): 15 | 16 | ```mjs 17 | import { defineConfig } from "@beoe/cache"; 18 | 19 | export default defineConfig({ 20 | database: "other_folder/cache.sqlite", 21 | }); 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | import { getCache } from "@beoe/cache"; 28 | 29 | const cache = await getCache(); 30 | ``` 31 | 32 | ## TODO 33 | 34 | - tests 35 | - write documentation about configuration options 36 | 37 | ## Alternatives 38 | 39 | - [astro-build-cache](https://github.com/jcayzac/copepod-modules/tree/main/packages/astro-build-cache) 40 | -------------------------------------------------------------------------------- /packages/cache/src/config.ts: -------------------------------------------------------------------------------- 1 | import { cosmiconfig } from "cosmiconfig"; 2 | import { mkdirSync } from "node:fs"; 3 | import { join, dirname } from "node:path"; 4 | import { cwd } from "node:process"; 5 | 6 | import { type SQLiteCacheOptions } from "@beoe/sqlitecache"; 7 | 8 | export type MapLike = { 9 | get(key: K): V | undefined; 10 | set(key: K, value: V): void; 11 | }; 12 | 13 | export type Config = SQLiteCacheOptions & { 14 | // in case you don't want to use built-in SQLiteCache 15 | override?: MapLike; 16 | }; 17 | 18 | // For inspiration https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts 19 | // can also add validation https://stereobooster.com/posts/runtime-type-validators/ 20 | export function defineConfig(cfg: Config) { 21 | return cfg; 22 | } 23 | 24 | const moduleName = "beoe"; 25 | const explorer = cosmiconfig(moduleName, { 26 | searchPlaces: [ 27 | // "package.json", 28 | `${moduleName}.config.js`, 29 | `${moduleName}.config.ts`, 30 | `${moduleName}.config.mjs`, 31 | `${moduleName}.config.cjs`, 32 | ], 33 | }); 34 | 35 | export async function getConfig() { 36 | const defaultCfg: Config = { 37 | database: join(cwd(), "node_modules/.beoe/cache.sqlite"), 38 | maxItems: 1024, 39 | // readonly: true 40 | }; 41 | 42 | let cfg: Partial = {}; 43 | try { 44 | const res = await explorer.search(); 45 | cfg = res?.config; 46 | } catch (e) {} 47 | 48 | const res = { ...defaultCfg, ...cfg }; 49 | 50 | if (res.database && res.database !== ":memory:") 51 | mkdirSync(dirname(res.database!), { recursive: true }); 52 | 53 | return res; 54 | } 55 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/start-here/accessibility.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Accessibility 3 | sidebar: 4 | order: 6 5 | --- 6 | 7 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 8 | 9 | ## Alt 10 | 11 | Works only with strategy [`img`](/start-here/tag/#img) (`alt`) and [`iframe`](/start-here/tag/#iframe) (`title`) 12 | 13 | 14 | 15 | ````md 16 | ```d2 strategy=file pad=20 alt="testing alt" 17 | direction: right 18 | a -> b -> c -> d -> e 19 | ``` 20 | ```` 21 | 22 | 23 | ```d2 strategy=file pad=20 alt="testing alt" 24 | direction: right 25 | a -> b -> c -> d -> e 26 | ``` 27 | 28 | 29 | 30 | ```html 31 | ... 32 | testing alt 33 | ... 34 | ``` 35 | 36 | 37 | 38 | ## Figcaption 39 | 40 | [figcaption](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption) can be implemented. 41 | 42 | ## Mermaid 43 | 44 | Mermaid has special params for accessibility 45 | 46 | 47 | 48 | ````md 49 | ```mermaid strategy=inline 50 | graph LR 51 | accTitle: Big Decisions 52 | accDescr: Bob's Burgers process for making big decisions 53 | a --> b 54 | ``` 55 | ```` 56 | 57 | 58 | 59 | ```xml 60 | 61 | Bob's Burgers process for making big decisions 62 | 63 | ``` 64 | 65 | 66 | 67 | ```mermaid strategy=inline 68 | graph LR 69 | accTitle: Big Decisions 70 | accDescr: Bob's Burgers process for making big decisions 71 | a --> b 72 | ``` 73 | 74 | 75 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "BEOE" 3 | template: splash 4 | hero: 5 | image: 6 | html:
7 | 8 | 9 |
10 | title: "BEOE" 11 | tagline: Easily add diagrams to Markdown using Rehype, Remark plugins, and more. 12 | actions: 13 | - text: Tell me more 14 | link: /start-here/getting-started/ 15 | icon: right-arrow 16 | - text: View on GitHub 17 | link: https://github.com/stereobooster/beoe 18 | icon: external 19 | variant: minimal 20 | --- 21 | 22 | import { CardGrid, Card } from "@astrojs/starlight/components"; 23 | 24 | 25 | 26 | A versatile diagramming options, such as **Mermaid**, **D2**, and more enabling 27 | users to create, edit, and visualize a wide range of diagram types. 28 | 29 | 30 | Supports both traditional dark mode via prefers-color-scheme and class-based dark mode, similar to this website. 31 | 32 | 33 | All diagrams are server-generated (SSG), ensuring functionality without JavaScript, which enhances load times and overall performance. 34 | 35 | 36 | - Supports caching to accelerate the re-rendering of markdown files 37 | - Provides pan and zoom functionality for diagrams 38 | - Allows adding interactivity to diagrams as a progressive enhancement 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/notes/space-time.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Space and time classification 3 | --- 4 | 5 | Time dimension - shows relationship in time i.e. earlier (prior to), later (afterwards) 6 | 7 | Space dimension - shows relationship in space i.e. above, under, in front, behind, etc. 8 | 9 | ## Space 10 | 11 | ### Mermaid 12 | 13 | - [Architecture Diagrams](https://mermaid.js.org/syntax/architecture.html) 14 | - [Entity Relationship Diagrams](https://mermaid.js.org/syntax/entityRelationshipDiagram.html) ? 15 | 16 | ### c4 17 | 18 | - [System context diagram](https://c4model.com/diagrams/system-context) 19 | - [Container diagram](https://c4model.com/diagrams/container) 20 | - [Component diagram](https://c4model.com/diagrams/component) 21 | - [Deployment diagram](https://c4model.com/diagrams/deployment) 22 | - [System landscape diagram](https://c4model.com/diagrams/system-landscape) 23 | 24 | ### d2 25 | 26 | - [Containers](https://d2lang.com/tour/containers) 27 | - [SQL Tables](https://d2lang.com/tour/sql-tables) 28 | 29 | ## Time 30 | 31 | ### Mermaid 32 | 33 | - [State diagrams](https://mermaid.js.org/syntax/stateDiagram.html) 34 | - [Gantt diagrams](https://mermaid.js.org/syntax/gantt.html) 35 | - [Timeline Diagram](https://mermaid.js.org/syntax/timeline.html) 36 | 37 | ### Other 38 | 39 | - Flowchart (algorithm) diagram, aka workflow, aka process 40 | - UML Activity Diagram 41 | 42 | ## Space + Time 43 | 44 | Sequence diagrams - space dimension is horizontal line, time dimension is on vertical line. 45 | 46 | ### Mermaid 47 | 48 | - [Sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html) 49 | 50 | ### d2 51 | 52 | - [Sequence Diagrams](https://d2lang.com/tour/sequence-diagrams) 53 | -------------------------------------------------------------------------------- /packages/rehype-penrose/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 6 | import { join } from "node:path"; 7 | import { penrose } from "./penrose.js"; 8 | 9 | export type PenroseOptions = { 10 | /** 11 | * path to shared folder for `.domain` and `.style` files 12 | */ 13 | shared: string; 14 | /** 15 | * Width of diagram 16 | */ 17 | width?: number; 18 | /** 19 | * Height of diagram 20 | */ 21 | height?: number; 22 | /** 23 | * Name (or path) for `.style` file 24 | */ 25 | style?: string; 26 | /** 27 | * Name (or path) for `.domain` file 28 | */ 29 | domain?: string; 30 | variation?: string; 31 | namespace?: string; 32 | }; 33 | 34 | export const rehypePenrose = rehypeCodeHookImg({ 35 | language: "penrose", 36 | svgo: false, 37 | render: async (code: string, opts) => { 38 | if (!opts.domain || !opts.style) 39 | throw new Error("domain and style required"); 40 | if (!opts.shared) opts.shared = ""; 41 | if (!opts.width) opts.width = 400; 42 | if (!opts.height) opts.height = 400; 43 | 44 | return { 45 | svg: await penrose({ 46 | substance: code, 47 | style: join(opts.shared, opts.style), 48 | domain: join(opts.shared, opts.domain), 49 | variation: opts.variation || "", 50 | width: opts.width, 51 | height: opts.height, 52 | }), 53 | }; 54 | }, 55 | }); 56 | 57 | export default rehypePenrose; 58 | -------------------------------------------------------------------------------- /packages/pan-zoom/css/PanZoomUi.css: -------------------------------------------------------------------------------- 1 | /* Core */ 2 | 3 | .beoe { 4 | overflow: hidden; 5 | touch-action: pan-x pan-y; 6 | -webkit-user-select: none; 7 | user-select: none; 8 | cursor: grab; 9 | } 10 | 11 | .beoe svg, 12 | .beoe img, 13 | .beoe iframe, 14 | .beoe embed { 15 | /* need to center smaller images to fix bug in zoom functionality */ 16 | margin: auto; 17 | display: block; 18 | /* need to fit bigger images */ 19 | max-width: 100%; 20 | height: auto; 21 | } 22 | 23 | .beoe img { 24 | pointer-events: none; 25 | } 26 | 27 | .beoe iframe { 28 | width: 100%; 29 | } 30 | 31 | /* UI */ 32 | 33 | .beoe { 34 | position: relative; 35 | } 36 | 37 | .beoe .buttons { 38 | position: absolute; 39 | right: 1rem; 40 | bottom: 1rem; 41 | opacity: 0; 42 | transition-property: opacity; 43 | transition-duration: 300ms; 44 | } 45 | 46 | .beoe:hover .buttons { 47 | opacity: 1; 48 | } 49 | 50 | @media (hover: none) { 51 | .beoe .buttons { 52 | display: none; 53 | } 54 | } 55 | 56 | .beoe .touchscreen-warning { 57 | position: absolute; 58 | right: 0rem; 59 | bottom: 0rem; 60 | left: 0rem; 61 | top: 0rem; 62 | pointer-events: none; 63 | display: none; 64 | opacity: 0; 65 | transition-property: opacity; 66 | transition-duration: 300ms; 67 | container-type: inline-size; 68 | } 69 | 70 | /* fit text to container */ 71 | .beoe .touchscreen-warning p { 72 | font-size: min(10cqw, 10cqh); 73 | } 74 | 75 | .beoe .touchscreen-warning.active { 76 | opacity: 1; 77 | } 78 | 79 | @media (hover: none) { 80 | .beoe .touchscreen-warning { 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | background-color: rgba(0, 0, 0, 0.5); 85 | color: white; 86 | font-size: 3rem; 87 | text-align: center; 88 | padding: 1rem; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/start-here/styling-with-css.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Styling with CSS 3 | sidebar: 4 | order: 7 5 | --- 6 | 7 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 8 | 9 | Works only with strategy [`inline`](/start-here/strategy/#inline). 10 | 11 | **Basic idea**: you can target with CSS any SVG node, something like this: 12 | 13 | ```css 14 | .some-diagram .node { 15 | /* styles */ 16 | } 17 | ``` 18 | 19 | **But there are more tricks**. If there are no classes that you can target, you can as well target attributes: 20 | 21 | ```css 22 | .graphviz { 23 | text { 24 | fill: var(--sl-color-white); 25 | } 26 | [fill="black"], 27 | [fill="#000"] { 28 | fill: var(--sl-color-white); 29 | } 30 | [stroke="black"], 31 | [stroke="#000"] { 32 | stroke: var(--sl-color-white); 33 | } 34 | } 35 | ``` 36 | 37 | Graphviz and Vizdom allow to expose HTML classes, that can be used for further styling: 38 | 39 | 40 | 41 | ```dot strategy=inline 42 | digraph g { 43 | bgcolor="transparent"; 44 | rankdir=LR; 45 | node [shape=rect]; 46 | a -> b -> c -> d -> e -> f 47 | a[class="style-me"] 48 | } 49 | ``` 50 | 51 | 52 | ```vizdom strategy=inline 53 | digraph g { 54 | bgcolor="transparent"; 55 | rankdir=LR; 56 | node [shape=rect]; 57 | a -> b -> c -> d -> e -> f 58 | a[class="style-me"] 59 | } 60 | ``` 61 | 62 | 63 | ````md 64 | ```dot strategy=inline 65 | digraph g { 66 | a -> b 67 | a[class="style-me"] 68 | } 69 | ``` 70 | ```` 71 | 72 | 73 | ```css 74 | .graphviz .style-me path { 75 | fill: lightblue; 76 | } 77 | .vizdom .style-me :first-child { 78 | fill: lightblue; 79 | } 80 | ``` 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/start-here/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | sidebar: 4 | order: 5 5 | --- 6 | 7 | All Rehype diagram plugins support (at least) following configurations: 8 | 9 | - [`strategy`](/start-here/strategy/) (optional). Default `inline` 10 | - [`darkScheme`](/start-here/dark-scheme/) (optional). Default `undefined` 11 | - [`tag`](/start-here/tag/) (optional). Default `img`. Ignored for `strategy=inline` 12 | - `cache` (optional) - Map-like storage to speed up consequent renders of page. You can use standard JS `Map`, but probably it is better to use [@beoe/cache](https://github.com/stereobooster/beoe/tree/main/packages/cache/). 13 | - `class` (optional) - allows to setup additional classes for diagrams. For example, `.not-content` for inline diagrams if you use [tailwindcss-typography](https://github.com/tailwindlabs/tailwindcss-typography) 14 | - `svgo` (optional) - options for [SVGO](https://github.com/svg/svgo). Use `false` to disable optimization with SVGO 15 | 16 | All configurations can be set either globally or locally (via fence-code meta). Global configurations applies to all diagrams. Local configurations applies only to one diagram. Local configurations always override global, with exception for `class` option (it applies all provided classes). 17 | 18 | Also you won't be able to set `cache` and `svgo` via local configurations, except disabling them with `false` value. 19 | 20 | ## Global configuration 21 | 22 | For example, 23 | 24 | ```js 25 | import { getCache } from "@beoe/cache"; 26 | const cache = await getCache(); 27 | 28 | use(rehypeDiagram, { 29 | strategy: "file", 30 | darkScheme: "class", 31 | class: "something", 32 | cache, 33 | }); 34 | ``` 35 | 36 | ## Local configuration 37 | 38 | ````md 39 | ```some-diagram strategy=inline darkScheme=false class=interactive 40 | ... 41 | ``` 42 | ```` 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | timeout-minutes: 15 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 2 20 | 21 | # https://stackoverflow.com/questions/60491837/saving-cache-on-job-failure-in-github-actions 22 | - name: Cache turbo build restore 23 | uses: actions/cache/restore@v3 24 | with: 25 | path: .turbo 26 | key: ${{ runner.os }}-turbo-${{ github.sha }} 27 | restore-keys: | 28 | ${{ runner.os }}-turbo- 29 | 30 | - uses: pnpm/action-setup@v3 31 | with: 32 | version: 9 33 | run_install: false 34 | 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version-file: ".node-version" 38 | cache: "pnpm" 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | 43 | # for Mermaid 44 | - name: Install Playwright Browsers 45 | run: pnpx playwright install chromium 46 | 47 | # for PlantUML 48 | # - uses: actions/setup-java@v4 49 | # with: 50 | # distribution: "microsoft" 51 | # java-version: "21" 52 | # - uses: ts-graphviz/setup-graphviz@v2 53 | 54 | # for d2 55 | - run: curl -fsSL https://d2lang.com/install.sh | sh -s -- 56 | 57 | - name: Build 58 | run: pnpm build --cache-dir=.turbo 59 | 60 | - name: Cache turbo build save 61 | uses: actions/cache/save@v3 62 | # if: always() 63 | with: 64 | path: .turbo 65 | key: ${{ runner.os }}-turbo-${{ github.sha }} 66 | 67 | - name: Test 68 | run: pnpm test 69 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/diagrams/mermaid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/rehype-mermaid" 3 | --- 4 | 5 | import { PackageManagers } from "starlight-package-managers"; 6 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 7 | 8 | Rehype plugin to generate [Mermaid](https://mermaid.js.org/) diagrams in place of code fences. Example: 9 | 10 | 11 | 12 | 13 | ```mermaid 14 | flowchart LR 15 | start --> stop 16 | ``` 17 | 18 | 19 | 20 | 21 | ````md 22 | ```mermaid 23 | flowchart LR 24 | start --> stop 25 | ``` 26 | ```` 27 | 28 | 29 | 30 | 31 | ## Installation 32 | 33 | 34 | 35 | You may want also add this to `package.json`: 36 | 37 | ```json 38 | "scripts": { 39 | "postinstall": "playwright install chromium" 40 | } 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```js 46 | import rehypeMermaid from "@beoe/rehype-mermaid"; 47 | 48 | const html = await unified() 49 | .use(remarkParse) 50 | .use(remarkRehype) 51 | .use(rehypeMermaid, { 52 | /* options */ 53 | }) 54 | .use(rehypeStringify) 55 | .process(`markdown`); 56 | ``` 57 | 58 | Check out other [options](/start-here/configuration/). 59 | 60 | ### Configuration 61 | 62 | You probaly want to use [`file`](/start-here/strategy/#file) strategy and one of dark schemes (`class` or `media`). Unless you need [interactivity](/start-here/interactivity/). 63 | 64 | ### Other 65 | 66 | Accepts other options like, `css` and `mermaidConfig`, which are documented in [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic). 67 | 68 | ## `rehype-mermaid` 69 | 70 | There is battle-tested and well maintained plugin [rehype-mermaid](https://github.com/remcohaszing/rehype-mermaid). Both plugins use the same [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic) behind the scene. 71 | -------------------------------------------------------------------------------- /packages/pan-zoom/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/src/render.ts: -------------------------------------------------------------------------------- 1 | import { DotParser } from "@vizdom/vizdom-ts-node"; 2 | 3 | /** 4 | * it is possible to add other formats 5 | */ 6 | export type DataFormat = "dagre" | "graphology"; 7 | 8 | export type RehypeVizdomConfig = { 9 | graphFormat: DataFormat 10 | }; 11 | 12 | export const render = async (code: string, options: RehypeVizdomConfig) => { 13 | const parser = new DotParser(); 14 | const dotGraph = parser.parse(code); 15 | const directedGraph = dotGraph.to_directed(); 16 | const positioned = directedGraph.layout(); 17 | const svg = await positioned.to_svg().to_string(); 18 | 19 | let data; 20 | if (options.graphFormat) { 21 | const obj = positioned.to_json().to_obj(); 22 | if (options.graphFormat === "graphology") { 23 | data = { 24 | attributes: { name: "g" }, 25 | options: { allowSelfLoops: true, multi: true, type: "directed" }, 26 | nodes: obj.nodes.map((node) => ({ 27 | key: node.unique_id, 28 | // attributes: { 29 | // label: node.label, 30 | // x: node.x, 31 | // y: node.y, 32 | // width: node.width, 33 | // height: node.height, 34 | // url: node.url, 35 | // }, 36 | })), 37 | edges: obj.edges.map((edge) => ({ 38 | key: edge.unique_id, 39 | source: edge.source, 40 | target: edge.target, 41 | // attributes: { 42 | // label: edge.label, 43 | // url: edge.url, 44 | // }, 45 | })), 46 | }; 47 | } 48 | 49 | if (options.graphFormat === "dagre") { 50 | data = { 51 | options: { 52 | directed: true, 53 | multigraph: true, 54 | compound: false, 55 | }, 56 | nodes: obj.nodes.map((node) => ({ 57 | v: node.unique_id, 58 | // value: {}, 59 | })), 60 | edges: obj.edges.map((edge) => ({ 61 | v: edge.source, 62 | w: edge.target, 63 | name: edge.unique_id, 64 | // value: {}, 65 | })), 66 | }; 67 | } 68 | } 69 | 70 | return { svg, data }; 71 | }; 72 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { unified } from "unified"; 3 | import remarkParse from "remark-parse"; 4 | import remarkRehype from "remark-rehype"; 5 | import rehypeStringify from "rehype-stringify"; 6 | import { expect, it, describe, vi } from "vitest"; 7 | 8 | import { rehypeCodeHookImg } from "../src/index.js"; 9 | import { Strategy } from "../src/types.js"; 10 | 11 | type Example = { input: string; output: string; testHook: any; options?: any }; 12 | 13 | async function example({ input, output, testHook, options }: Example) { 14 | const file = await unified() 15 | .use(remarkParse) 16 | .use(remarkRehype) 17 | .use(testHook, options) 18 | .use(rehypeStringify) 19 | .process( 20 | await fs.readFile(new URL(`./fixtures/${input}.md`, import.meta.url)) 21 | ); 22 | 23 | await expect(file.toString()).toMatchFileSnapshot( 24 | `./fixtures/${output}.out.html` 25 | ); 26 | } 27 | 28 | const testHook = rehypeCodeHookImg({ 29 | language: "test", 30 | render: (_a, _b) => ({ 31 | svg: "light", 32 | darkSvg: "dark", 33 | width: 1, 34 | height: 2, 35 | }), 36 | }); 37 | 38 | describe("strategy", () => { 39 | ( 40 | [ 41 | undefined, 42 | "inline", 43 | "data-url", 44 | // "file" 45 | // "img", 46 | // "picture-dark-mode", 47 | // "img-class-dark-mode", 48 | ] as Strategy[] 49 | ).forEach((strategy) => { 50 | it(`${strategy}`, async () => { 51 | await example({ 52 | input: "a", 53 | output: `strategy_${strategy}`, 54 | testHook, 55 | options: { strategy }, 56 | }); 57 | }); 58 | }); 59 | 60 | it("strategy in meta", async () => { 61 | await example({ 62 | input: "c", 63 | output: "strategy_in_meta", 64 | testHook, 65 | options: { strategy: "inline" }, 66 | }); 67 | }); 68 | }); 69 | 70 | describe("class", () => { 71 | it("can pass class", async () => { 72 | await example({ 73 | input: "b", 74 | output: "class", 75 | testHook, 76 | options: { class: "x" }, 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/pan-zoom/logo/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/rehype-penrose/bin/penrose.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "global-jsdom/register"; 4 | import { compile, optimize, toSVG, showError } from "@penrose/core"; 5 | import { readFile } from "node:fs/promises"; 6 | import { join, resolve } from "node:path"; 7 | import { readFileSync } from "node:fs"; 8 | 9 | // https://github.com/penrose/penrose/blob/main/packages/roger/index.ts 10 | const resolvePath = (stylePath) => { 11 | const stylePrefix = join(stylePath, ".."); 12 | return async (filePath) => { 13 | // Handle absolute URLs 14 | if (/^(http|https):\/\/[^ "]+$/.test(filePath)) { 15 | const fileURL = new URL(filePath).href; 16 | try { 17 | const fileReq = await fetch(fileURL); 18 | return fileReq.text(); 19 | } catch (e) { 20 | console.error(e); 21 | return undefined; 22 | } 23 | } 24 | 25 | // Relative paths 26 | const joined = resolve(stylePrefix, filePath); 27 | return readFile(joined, "utf8"); 28 | }; 29 | }; 30 | 31 | const opts = JSON.parse(readFileSync(0, "utf8")); 32 | const path = opts.style; 33 | 34 | opts.domain = await readFile(opts.domain, "utf8"); 35 | opts.style = `canvas { 36 | width = ${opts.width} 37 | height = ${opts.height} 38 | } 39 | ${await readFile(opts.style, "utf8")}`; 40 | 41 | const compiled = await compile(opts); 42 | if (compiled.isErr()) { 43 | console.error(showError(compiled.error)); 44 | process.exit(1); 45 | } 46 | const optimized = optimize(compiled.value); 47 | if (optimized.isErr()) { 48 | console.error(showError(optimized.error)); 49 | process.exit(1); 50 | } 51 | const svgElement = await toSVG(optimized.value, resolvePath(path), "rehype"); 52 | 53 | // https://github.com/stereobooster/venn-nodejs/blob/main/bin/venn-nodejs.js 54 | // import serialize from "w3c-xmlserializer"; 55 | // const s = new XMLSerializer(); 56 | // const svg = s.serializeToString(svgElement); 57 | const svg = svgElement.outerHTML; 58 | 59 | function chunkString(str, length) { 60 | return str.match(new RegExp(".{1," + length + "}", "g")); 61 | } 62 | 63 | // writeFileSync("tmp.svg", svg, "utf8") 64 | chunkString(svg, 1024).forEach((str) => process.stdout.write(str)); 65 | process.exit(0); 66 | -------------------------------------------------------------------------------- /packages/docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import starlight from "@astrojs/starlight"; 3 | import { qrcode } from "vite-plugin-qrcode"; 4 | 5 | import { getCache } from "@beoe/cache"; 6 | import { rehypeMermaid } from "@beoe/rehype-mermaid"; 7 | import { rehypeGraphviz } from "@beoe/rehype-graphviz"; 8 | import { rehypeGnuplot } from "@beoe/rehype-gnuplot"; 9 | import { rehypeVizdom } from "@beoe/rehype-vizdom"; 10 | import { rehypeD2 } from "@beoe/rehype-d2"; 11 | import { rehypePenrose } from "@beoe/rehype-penrose"; 12 | 13 | const cache = await getCache(); 14 | // requerd for correct displaying mobile warning 15 | const className = "not-content"; 16 | const conf = { 17 | cache, 18 | strategy: "file", 19 | darkScheme: "class", 20 | // do not use .beoe for Netlify deployments 21 | fsPath: "public/beoe", 22 | webPath: "/beoe", 23 | }; 24 | 25 | const sidebar = [ 26 | { 27 | label: "Start here", 28 | autogenerate: { directory: "start-here" }, 29 | }, 30 | { 31 | label: "Diagrams", 32 | autogenerate: { directory: "diagrams" }, 33 | }, 34 | { 35 | label: "Other", 36 | autogenerate: { directory: "other" }, 37 | }, 38 | { 39 | label: "Notes", 40 | autogenerate: { directory: "notes" }, 41 | }, 42 | ]; 43 | if (import.meta.env.DEV) { 44 | sidebar.push({ 45 | label: "Examples", 46 | autogenerate: { directory: "examples" }, 47 | }); 48 | } 49 | 50 | // https://astro.build/config 51 | export default defineConfig({ 52 | integrations: [ 53 | starlight({ 54 | title: "BEOE", 55 | social: { 56 | github: "https://github.com/stereobooster/beoe", 57 | }, 58 | sidebar, 59 | customCss: ["./src/styles/custom.css"], 60 | components: { 61 | PageFrame: "./src/components/PageFrame.astro", 62 | }, 63 | }), 64 | ], 65 | markdown: { 66 | rehypePlugins: [ 67 | [rehypeGraphviz, { ...conf, strategy: "inline" }], 68 | [rehypeVizdom, { ...conf, strategy: "inline" }], 69 | [rehypeMermaid, conf], 70 | [rehypeGnuplot, conf], 71 | [rehypeD2, { ...conf, shared: "shared/**/*.d2" }], 72 | [rehypePenrose, { ...conf, shared: "shared" }], 73 | ], 74 | }, 75 | vite: { 76 | plugins: [qrcode()], 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /packages/docs/src/components/d2.ts: -------------------------------------------------------------------------------- 1 | import { json, alg } from "@dagrejs/graphlib"; 2 | 3 | const css = `.shadow { opacity: 0.4; } 4 | .shape { cursor: default; }`; 5 | 6 | // interactivity for d2 diagrams 7 | document.querySelectorAll(".d2.shadow").forEach(async (container: Element) => { 8 | const data = container.getAttribute("data-beoe") 9 | ? JSON.parse(container.getAttribute("data-beoe")!) 10 | : null; 11 | 12 | const iframe = container.querySelector("iframe"); 13 | if (iframe) { 14 | if (!iframe.contentDocument) 15 | await new Promise((resolve) => 16 | iframe.addEventListener("load", () => resolve(0)) 17 | ); 18 | container = iframe.contentDocument!.querySelector("svg")! as Element; 19 | const styleSheet = iframe.contentDocument!.styleSheets[0]; 20 | if (styleSheet) 21 | css 22 | .split("\n") 23 | .forEach((row) => 24 | styleSheet.insertRule(row, styleSheet.cssRules.length) 25 | ); 26 | } 27 | 28 | if (!data) return; 29 | const graph = json.read(data); 30 | 31 | function clear() { 32 | container 33 | .querySelectorAll("g[id]") 34 | .forEach((node) => node.classList.remove("shadow")); 35 | } 36 | 37 | function highlight(id: string) { 38 | container 39 | .querySelectorAll("g[id]") 40 | .forEach((node) => node.classList.add("shadow")); 41 | alg.postorder(graph, [id]).forEach((node) => { 42 | container 43 | .querySelector(`#${CSS.escape(node)}`) 44 | ?.classList.remove("shadow"); 45 | graph.outEdges(node)?.forEach(({ name }) => { 46 | container 47 | .querySelector(`#${CSS.escape(name!)}`) 48 | ?.classList.remove("shadow"); 49 | }); 50 | }); 51 | } 52 | 53 | // highlight on hover 54 | let currentHover: string | null = null; 55 | container.addEventListener("mouseover", (e) => { 56 | // @ts-expect-error 57 | const node = e.target?.closest(".shape"); 58 | 59 | if (node) { 60 | const id = node.parentElement.getAttribute("id"); 61 | if (currentHover == id) return; 62 | clear(); 63 | highlight(id); 64 | currentHover = id; 65 | } else { 66 | if (currentHover == null) return; 67 | clear(); 68 | currentHover = null; 69 | } 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/pan-zoom/src/utilsMath.ts: -------------------------------------------------------------------------------- 1 | import * as math from "mathjs"; 2 | 3 | export const identity = math.matrix([ 4 | [1, 0, 0], 5 | [0, 1, 0], 6 | [0, 0, 1], 7 | ]); 8 | 9 | // this causes pixelation in Safari 10 | // export function ttm(m: math.Matrix) { 11 | // return `matrix3d(${[ 12 | // [m.get([0, 0]), m.get([1, 0]), 0, 0], 13 | // [m.get([0, 1]), m.get([1, 1]), 0, 0], 14 | // [0, 0, 1, 0], 15 | // [m.get([0, 2]), m.get([1, 2]), 0, 1], 16 | // ]})`; 17 | // } 18 | 19 | export function ttm(m: math.Matrix) { 20 | return `matrix(${[ 21 | m.get([0, 0]), 22 | m.get([1, 0]), 23 | m.get([0, 1]), 24 | m.get([1, 1]), 25 | m.get([0, 2]), 26 | m.get([1, 2]), 27 | ]})`; 28 | } 29 | 30 | export function transformXY(m: math.Matrix, x: number, y: number) { 31 | const nm = math.multiply(m, [x, y, 1]); 32 | return [nm.get([0]), nm.get([1])] as const; 33 | } 34 | 35 | export function getScale(m: math.Matrix) { 36 | return m.get([0, 0]); 37 | } 38 | 39 | export function scale(m: math.Matrix, sf: number) { 40 | return math.multiply( 41 | m, 42 | math.matrix([ 43 | [sf, 0, 0], 44 | [0, sf, 0], 45 | [0, 0, 1], 46 | ]) 47 | ); 48 | } 49 | 50 | export function scaleAt(m: math.Matrix, sf: number, x: number, y: number) { 51 | const scaleMatrix = scale(m, sf); 52 | const [nx1, ny1] = transformXY(math.inv(m), x, y); 53 | const [nx2, ny2] = transformXY(math.inv(scaleMatrix), x, y); 54 | return translate(scaleMatrix, nx2 - nx1, ny2 - ny1); 55 | } 56 | 57 | export function translate(m: math.Matrix, dx: number, dy: number) { 58 | return math.multiply( 59 | m, 60 | math.matrix([ 61 | [1, 0, dx], 62 | [0, 1, dy], 63 | [0, 0, 1], 64 | ]) 65 | ); 66 | } 67 | 68 | export function fdm(m: DOMMatrix) { 69 | return math.matrix([ 70 | [m.a, m.c, m.e], 71 | [m.b, m.d, m.f], 72 | [0, 0, 1], 73 | ]); 74 | } 75 | 76 | export function tdm(m: math.Matrix) { 77 | return new DOMMatrix([ 78 | m.get([0, 0]), 79 | m.get([1, 0]), 80 | m.get([0, 1]), 81 | m.get([1, 1]), 82 | m.get([0, 2]), 83 | m.get([1, 2]), 84 | ]); 85 | } 86 | 87 | // export function m3d(m: DOMMatrix) { 88 | // return `matrix3d(${[ 89 | // [m.m11, m.m12, m.m13, m.m14], 90 | // [m.m21, m.m22, m.m23, m.m24], 91 | // [m.m31, m.m32, m.m33, m.m34], 92 | // [m.m41, m.m42, m.m43, m.m44], 93 | // ]})`; 94 | // } 95 | -------------------------------------------------------------------------------- /experiments/rehype-plantuml/test/fixtures/a1-datauri.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BEOE 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 | 10 | It should be easy to add diagrams to your Markdown documentation. There are many solutions that can render diagrams inside Markdown; for example, see [Asciidoctor](https://docs.asciidoctor.org/diagram-extension/latest/). 11 | 12 | However, if you want to add a custom diagram, it can be tedious to implement. **Core idea**: If you have a function that can produce a diagram as SVG, it should be trivial to use it in Astro (or other SSGs that use remark/rehype). 13 | 14 | It should be easy to create documentation with many diagrams, like in [**B**yrne's **E**lements **o**f **E**uclid](https://www.c82.net/euclid/). 15 | 16 | ## Status 17 | 18 | I have implemented core packages and added some examples. However, I still need to add more tests, and address the remaining TODOs. 19 | 20 | ## Packages 21 | 22 | ### Diagrams 23 | 24 | | | rehype | 25 | | -------- | --------------------------------------------------- | 26 | | Graphviz | [@beoe/rehype-graphviz](/packages/rehype-graphviz/) | 27 | | Mermaid | [@beoe/rehype-mermaid](/packages/rehype-mermaid/) | 28 | | Gnuplot | [@beoe/rehype-gnuplot](/packages/rehype-gnuplot/) | 29 | | Vizdom | [@beoe/rehype-vizdom](/packages/rehype-vizdom/) | 30 | | D2 | [@beoe/rehype-d2](/packages/rehype-d2/) | 31 | | Penrose | [@beoe/rehype-penrose](/packages/rehype-penrose/) | 32 | | ... | | 33 | 34 | Ideas for other diagrams: [Text to Diagram](https://stereobooster.com/posts/text-to-diagram/). 35 | 36 | ### Core 37 | 38 | - [x] [@beoe/rehype-code-hook](/packages/rehype-code-hook/) 39 | - [x] [@beoe/sqlitecache](/packages/sqlitecache/) 40 | - [x] [@beoe/cache](/packages/cache/) 41 | - [x] [@beoe/remark-code-hook](/packages/remark-code-hook/) 42 | - [x] [@beoe/pan-zoom](/packages/pan-zoom/) 43 | - [x] [@beoe/fenceparser](/packages/fenceparser/) 44 | - [x] [docs](/packages/docs/) [![Netlify Status](https://api.netlify.com/api/v1/badges/14126476-0cdb-4802-9a5b-e7f598221650/deploy-status)](https://app.netlify.com/sites/beoe/deploys) 45 | 46 | ## Logo 47 | 48 | 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/). 49 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/src/index.ts: -------------------------------------------------------------------------------- 1 | import { rehypeCodeHook } from "@beoe/rehype-code-hook"; 2 | import { metaWithDefaults, svgStrategy } from "./utils.js"; 3 | import { BasePluginOptions, BaseOptions, Cb, CbInput } from "./types.js"; 4 | import type { Plugin } from "unified"; 5 | import type { Root } from "hast"; 6 | 7 | export type { BaseOptions, BasePluginOptions }; 8 | 9 | export type RehypeCodeHookImgOptions = { 10 | render: Cb; 11 | /** 12 | * if given hook will be called only for this language 13 | */ 14 | language: string; 15 | } & Omit; 16 | 17 | export const rehypeCodeHookImg = ( 18 | options: RehypeCodeHookImgOptions 19 | ) => { 20 | const { 21 | render, 22 | language, 23 | class: pluginDefaultClass, 24 | ...pluginDefaults 25 | } = options; 26 | 27 | const hook: Plugin<[(T & BasePluginOptions)?], Root> = ( 28 | { cache, ...defaults } = {} as T 29 | ) => { 30 | defaults = { ...pluginDefaults, ...defaults }; 31 | // @ts-expect-error 32 | return rehypeCodeHook({ 33 | salt: defaults, 34 | language, 35 | code: ({ code, meta }) => { 36 | const opts = metaWithDefaults( 37 | pluginDefaultClass || language, 38 | defaults as any as T & BasePluginOptions, 39 | meta 40 | ); 41 | 42 | if (opts.strategy === "img") { 43 | opts.strategy = "data-url"; 44 | console.warn("img strategy is deprecated, use data-url instead"); 45 | } 46 | if (opts.strategy === "img-class-dark-mode") { 47 | opts.strategy = "data-url"; 48 | opts.darkScheme = "class"; 49 | console.warn( 50 | "img-class-dark-mode strategy is deprecated, use data-url instead + darkScheme class" 51 | ); 52 | } 53 | if (opts.strategy === "picture-dark-mode") { 54 | opts.strategy = "data-url"; 55 | opts.darkScheme = "media"; 56 | console.warn( 57 | "img-class-dark-mode strategy is deprecated, use data-url instead + darkScheme media" 58 | ); 59 | } 60 | 61 | const darkMode = opts.darkScheme != undefined; 62 | const result = render(code, { ...opts, darkMode }); 63 | return "then" in result 64 | ? result.then((x) => svgStrategy(opts, x)) 65 | : svgStrategy(opts, result); 66 | }, 67 | }); 68 | }; 69 | 70 | return hook; 71 | }; 72 | 73 | export default rehypeCodeHookImg; 74 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 2 | import type { Plugin } from "unified"; 3 | // @ts-expect-error The inferred type of '...' cannot be named without a reference to 4 | import type { Root } from "hast"; 5 | import { 6 | createMermaidRenderer, 7 | MermaidRenderer, 8 | type CreateMermaidRendererOptions, 9 | type RenderOptions, 10 | } from "mermaid-isomorphic"; 11 | import { BaseOptions, rehypeCodeHookImg } from "@beoe/rehype-code-hook-img"; 12 | 13 | import { type Config as SvgoConfig } from "svgo"; 14 | 15 | const svgo: SvgoConfig = { 16 | plugins: [ 17 | { 18 | name: "preset-default", 19 | params: { 20 | overrides: { 21 | // we need viewbox for inline SVGs 22 | removeViewBox: false, 23 | // this breaks statediagram 24 | convertShapeToPath: false, 25 | }, 26 | }, 27 | }, 28 | ], 29 | }; 30 | 31 | export type RehypeMermaidConfig = RenderOptions & CreateMermaidRendererOptions; 32 | 33 | let renderDiagrams: MermaidRenderer; 34 | 35 | async function render( 36 | code: string, 37 | options: RehypeMermaidConfig & BaseOptions 38 | ) { 39 | const { css, mermaidConfig, prefix, browserType, launchOptions, darkMode } = 40 | options; 41 | const createOptions = { browserType, launchOptions }; 42 | const renerOptions = { css, mermaidConfig: mermaidConfig || {}, prefix }; 43 | 44 | if (!renderDiagrams) renderDiagrams = createMermaidRenderer(createOptions); 45 | 46 | // otherwise all diagrams would have same ID and styles would collide 47 | renerOptions.prefix = `m${Math.random().toString().replace(".", "")}`; 48 | const [x] = await renderDiagrams([code], renerOptions); 49 | if (x.status !== "fulfilled") throw new Error(x.reason); 50 | const { svg, width, height, title: alt } = x.value; 51 | // there is also x.value.description 52 | 53 | let darkSvg: string | undefined; 54 | if (darkMode) { 55 | renerOptions.mermaidConfig.theme = "dark"; 56 | renerOptions.prefix = `m${Math.random().toString().replace(".", "")}`; 57 | const [x] = await renderDiagrams([code], renerOptions); 58 | if (x.status !== "fulfilled") throw new Error(x.reason); 59 | darkSvg = x.value.svg; 60 | } 61 | 62 | return { svg, darkSvg, width, height, alt }; 63 | } 64 | 65 | export const rehypeMermaid = rehypeCodeHookImg({ 66 | language: "mermaid", 67 | render, 68 | svgo, 69 | }); 70 | 71 | export default rehypeMermaid; 72 | -------------------------------------------------------------------------------- /experiments/rehype-pintora/test/fixtures/a.html: -------------------------------------------------------------------------------- 1 |
Action 1Action 2Activity Diagram Simple Action
-------------------------------------------------------------------------------- /experiments/rehype-pintora/test/fixtures/a.out.svg: -------------------------------------------------------------------------------- 1 |
Action 1Action 2Activity Diagram Simple Action
-------------------------------------------------------------------------------- /packages/docs/src/content/docs/start-here/dark-scheme.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dark scheme 3 | sidebar: 4 | order: 3 5 | --- 6 | 7 | Global configuration: 8 | 9 | ```js 10 | use(rehypeDiagram, { 11 | darkScheme: "...", // one of 2 options 12 | }); 13 | ``` 14 | 15 | **or** local configuration 16 | 17 | ````md 18 | ```some-diagram darkScheme=... 19 | diagram text 20 | ``` 21 | ```` 22 | 23 | `undefined` is a default option. 24 | 25 | ## Options 26 | 27 | ### `undefined` 28 | 29 | ```html 30 |
only one image
31 | ``` 32 | 33 | ### `class` 34 | 35 | ```html 36 |
37 |
38 | 39 | 40 |
41 |
42 | ``` 43 | 44 | You would need to add CSS, something like this: 45 | 46 | ```css 47 | html[data-theme="light"] .beoe-dark { 48 | display: none; 49 | } 50 | 51 | html[data-theme="dark"] .beoe-light { 52 | display: none; 53 | } 54 | ``` 55 | 56 | ### `media` 57 | 58 | ```html 59 |
60 | 61 | 62 | 63 | 64 |
65 | ``` 66 | 67 | ## Tag vs dark scheme 68 | 69 | | | `class` | `media` | [via css](/start-here/styling-with-css/) | 70 | | ------------------------------------------------------------------------------------------------------ | -------------------- | ------- | ---------------------------------------- | 71 | | `svg` ([`inline`](/start-here/strategy/#inline)) | yes with caveats (1) | no | **possible** | 72 | | `img` ([`data-url`](/start-here/strategy/#data-url), [`file`](/start-here/strategy/#file)) | yes | **yes** | no | 73 | | `iframe`, `embed` ([`data-url`](/start-here/strategy/#data-url), [`file`](/start-here/strategy/#file)) | yes | no | no | 74 | 75 | (1) - for example, works for Mermaid, but [doesn't work for D2](https://github.com/terrastruct/d2/pull/1803) (check with light mode): 76 | 77 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 78 | 79 | 80 | 81 | 82 | ```d2 strategy=inline darkScheme=class pad=20 83 | direction: right 84 | a -> b -> c -> d -> e 85 | ``` 86 | 87 | 88 | 89 | 90 | ````md 91 | ```d2 strategy=inline darkScheme=class pad=20 92 | direction: right 93 | a -> b -> c -> d -> e 94 | ``` 95 | ```` 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /packages/pan-zoom/src/PanZoomUi.ts: -------------------------------------------------------------------------------- 1 | import { PanZoom, PanZoomProps } from "./PanZoom.js"; 2 | 3 | const defaultClasses = { 4 | zoomIn: "zoom-in", 5 | reset: "reset", 6 | zoomOut: "zoom-out", 7 | buttons: "buttons", 8 | tsWarning: "touchscreen-warning", 9 | tsWarningActive: "active", 10 | }; 11 | 12 | const defaultMessage = "Use two fingers to pan and zoom"; 13 | 14 | export type PanZoomUiProps = PanZoomProps & { 15 | classes?: typeof defaultClasses; 16 | /** 17 | * @default "Use two fingers to pan and zoom" 18 | */ 19 | message?: string; 20 | }; 21 | 22 | export class PanZoomUi { 23 | #buttons: HTMLElement; 24 | #warning: HTMLElement; 25 | #instance: PanZoom; 26 | #container: HTMLElement; 27 | 28 | constructor({ 29 | container, 30 | classes = defaultClasses, 31 | message = defaultMessage, 32 | ...rest 33 | }: PanZoomUiProps) { 34 | this.#container = container; 35 | this.#instance = new PanZoom({ container, ...rest }); 36 | 37 | const buttons = document.createElement("div"); 38 | buttons.innerHTML = ` 39 | 40 | 41 | 42 | `; 43 | buttons.className = classes.buttons; 44 | buttons.querySelectorAll("button").forEach((button, i) => { 45 | if (i == 0) 46 | button.addEventListener("click", (e) => { 47 | e.stopPropagation(); 48 | this.#instance.zoom(1.1); 49 | }); 50 | if (i == 1) 51 | button.addEventListener("click", (e) => { 52 | e.stopPropagation(); 53 | this.#instance.reset(); 54 | }); 55 | if (i == 2) 56 | button.addEventListener("click", (e) => { 57 | e.stopPropagation(); 58 | this.#instance.zoom(0.9); 59 | }); 60 | }); 61 | buttons.addEventListener("dblclick", (e) => { 62 | e.preventDefault(); 63 | e.stopPropagation(); 64 | }); 65 | this.#buttons = buttons; 66 | 67 | const warningText = document.createElement("p"); 68 | warningText.innerText = message; 69 | 70 | const warning = document.createElement("div"); 71 | warning.className = classes.tsWarning; 72 | warning.append(warningText); 73 | 74 | this.#instance.onOneFingerDrag((flag) => 75 | flag 76 | ? warning.classList.add(classes.tsWarningActive) 77 | : warning.classList.remove(classes.tsWarningActive) 78 | ); 79 | this.#warning = warning; 80 | } 81 | 82 | on() { 83 | this.#instance.on(); 84 | this.#container.append(this.#warning); 85 | this.#container.append(this.#buttons); 86 | } 87 | 88 | off() { 89 | this.#instance.off(); 90 | this.#warning.remove(); 91 | this.#buttons.remove(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/rehype-mermaid/test/fixtures/a.html: -------------------------------------------------------------------------------- 1 |
A
-------------------------------------------------------------------------------- /packages/fenceparser/src/lex.ts: -------------------------------------------------------------------------------- 1 | import {FenceparserError} from './error.js' 2 | 3 | // patterns 4 | const SPACES = /[\s\t\n\r]/ 5 | const OPS = /[{}[\]=,:]/ 6 | const QUOTES = /["']/ 7 | const ALPHAS = /[a-zA-Z_$-]/ 8 | const NUMBERS = /[0-9]/ 9 | const ALPHANUMERIC = /[a-zA-Z_$-]|[0-9]/ 10 | 11 | type Input = string 12 | type Output = Array 13 | 14 | export const lex = (input: Input): Output => { 15 | let start = 0 16 | let current = 0 17 | const output: Output = [] 18 | 19 | const peek = (n = 0) => input[current + n]! 20 | const advance = () => input[current++]! 21 | const ending = () => current >= input.length 22 | 23 | // parse quoted string 24 | function string(quote: string) { 25 | // loop until we find the matching quote or reach the end of the input 26 | while (peek() !== quote && !ending()) advance() 27 | // is end of input? we haven't found the matching quote 28 | if (ending()) throw new FenceparserError('Unterminated string') 29 | // otherwise, we advance another step to claim the matching quote 30 | // because start is inclusive, and current is exclusive 31 | advance() 32 | output.push(input.substring(start, current)) 33 | } 34 | 35 | // parse number 36 | function number() { 37 | // loop as long as the next character is a number 38 | while (NUMBERS.test(peek())) advance() 39 | 40 | // if it's a range, of n-p format 41 | if (peek() === '-' && NUMBERS.test(peek(1))) { 42 | advance() // claim the '-' 43 | while (NUMBERS.test(peek())) advance() // claim the rest of the numbers 44 | output.push(input.substring(start, current)) 45 | return 46 | } 47 | 48 | // if it's a fraction 49 | if (peek() === '.' && NUMBERS.test(peek(1))) { 50 | advance() // claim '.' 51 | while (NUMBERS.test(peek())) advance() // claim the rest of the numbers 52 | } 53 | 54 | output.push(parseFloat(input.substring(start, current))) 55 | } 56 | 57 | // parse alphanumeric keywords to identifiers and values 58 | function keyword() { 59 | // loop as long as the next character is a number or an alpha 60 | // alphanumeric becase we accept numbers in identifiers as long as 61 | // they're not at the start, just like in JavaScript 62 | while (ALPHANUMERIC.test(peek()) && !ending()) advance() 63 | output.push(input.substring(start, current)) 64 | } 65 | 66 | function scan() { 67 | while (!ending()) { 68 | start = current 69 | const next = advance() 70 | 71 | if (SPACES.test(next)) continue 72 | else if (OPS.test(next)) output.push(next) 73 | else if (QUOTES.test(next)) string(next) 74 | else if (NUMBERS.test(next)) number() 75 | else if (ALPHAS.test(next)) keyword() 76 | // unrecognized pattern, throw 77 | else throw new FenceparserError(`Unexpected character ${next}`) 78 | } 79 | return output 80 | } 81 | 82 | return scan() 83 | } 84 | -------------------------------------------------------------------------------- /packages/rehype-code-hook-img/src/types.ts: -------------------------------------------------------------------------------- 1 | import { MapLike } from "@beoe/rehype-code-hook"; 2 | import { type Config as SvgoConfig } from "svgo"; 3 | 4 | export type jsonifiable = 5 | | string 6 | | number 7 | | boolean 8 | | null 9 | | jsonifiable[] 10 | | readonly jsonifiable[] 11 | | { [key: string]: jsonifiable } 12 | | undefined; 13 | 14 | export type Result = { 15 | svg: string; 16 | /** 17 | * used for img `width` 18 | */ 19 | width?: string | number; 20 | /** 21 | * used for img `height` 22 | */ 23 | height?: string | number; 24 | /** 25 | * used for dark mode 26 | */ 27 | darkSvg?: string; 28 | /** 29 | * used for `data-beoe` to pass JSON representation of diagram for interactivity 30 | */ 31 | data?: jsonifiable; 32 | /** 33 | * used for img `alt` 34 | */ 35 | alt?: string; 36 | // maybe aria-describedby? 37 | // maybe figcaption? 38 | }; 39 | 40 | export type BaseOptions = { 41 | darkMode?: boolean; 42 | }; 43 | 44 | export type BasePluginOptions = { 45 | class?: string; 46 | svgo?: SvgoConfig | boolean; 47 | strategy?: Strategy; 48 | cache?: MapLike; 49 | darkScheme?: Scheme; 50 | /** 51 | * @default img 52 | */ 53 | tag?: Tag; 54 | /** 55 | * required for `file` strategy 56 | */ 57 | fsPath?: string; 58 | /** 59 | * required for `file` strategy 60 | */ 61 | webPath?: string; 62 | }; 63 | 64 | export type Scheme = "class" | "media"; 65 | 66 | export type Tag = 67 | | "img" 68 | /** 69 | * experimental https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content/General_embedding_technologies 70 | */ 71 | | "iframe" 72 | /** 73 | * experimental https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed 74 | */ 75 | | "embed" 76 | /** 77 | * experimental https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object 78 | */ 79 | | "object"; 80 | 81 | export type Strategy = 82 | /** 83 | * SVG directly in the HTML 84 | */ 85 | | "inline" 86 | /** 87 | * SVG in `src` as [data-url](https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data) 88 | */ 89 | | "data-url" 90 | /** 91 | * SVG as standalone file in `img` 92 | */ 93 | | "file" 94 | /** 95 | * SVG as data-uri in img 96 | * @deprecated 97 | */ 98 | | "img" 99 | /** 100 | * SVG as data-uri in img and source inside of a picture 101 | * @deprecated 102 | */ 103 | | "picture-dark-mode" 104 | /** 105 | * SVG as data-uri in two imgs with light and dark classes 106 | * @deprecated 107 | */ 108 | | "img-class-dark-mode"; 109 | 110 | export type CbResult = Result | Promise; 111 | 112 | export type CbInput = Record; 113 | 114 | export type Cb = ( 115 | code: string, 116 | opt: T & BaseOptions 117 | ) => CbResult; 118 | -------------------------------------------------------------------------------- /packages/rehype-d2/src/d2.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore https://github.com/terrastruct/d2/issues/2286 2 | import { D2 } from "@terrastruct/d2"; 3 | import { glob, GlobOptions } from "tinyglobby"; 4 | // @ts-expect-error 5 | import memoizeOne from "async-memoize-one"; 6 | import fs from "node:fs/promises"; 7 | 8 | export type D2Options = { 9 | layout?: "dagre" | "elk"; 10 | sketch?: boolean; 11 | themeID?: number; 12 | graphFormat?: "graphology" | "dagre"; 13 | shared?: string | string[] | GlobOptions; 14 | }; 15 | 16 | const mGlob = memoizeOne(async (x: string | string[] | GlobOptions) => { 17 | // @ts-expect-error 18 | const paths = await glob(x); 19 | const result: Record = Object.create(null); 20 | // probably not the best way to do it, but for small number of files should be ok 21 | await Promise.all( 22 | paths.map((path) => 23 | fs 24 | .readFile(path, { encoding: "utf-8" }) 25 | .then((content) => (result[path] = content)) 26 | ) 27 | ); 28 | return result; 29 | }); 30 | 31 | export async function d2(code: string, options: D2Options) { 32 | const d2Instance = new D2(); 33 | 34 | const fs: Record = options.shared 35 | ? { ...(await mGlob(options.shared)), index: code } 36 | : { index: code }; 37 | const result = await d2Instance.compile({ fs, options }); 38 | 39 | let data; 40 | if (options.graphFormat) { 41 | if (options.graphFormat === "graphology") { 42 | data = { 43 | attributes: { name: "g" }, 44 | options: { allowSelfLoops: true, multi: true, type: "directed" }, 45 | nodes: result.diagram.shapes.map((node: any) => ({ 46 | key: node.id, 47 | // attributes: { 48 | // label: node.label, 49 | // x: node.pos.x, 50 | // y: node.pos.y, 51 | // width: node.width, 52 | // height: node.height, 53 | // url: node.url, 54 | // }, 55 | })), 56 | edges: result.diagram.connections.map((edge: any) => ({ 57 | key: edge.id, 58 | source: edge.src, 59 | target: edge.dst, 60 | // attributes: { 61 | // label: edge.label, 62 | // url: edge.url, 63 | // }, 64 | })), 65 | }; 66 | } 67 | 68 | if (options.graphFormat === "dagre") { 69 | data = { 70 | options: { 71 | directed: true, 72 | multigraph: true, 73 | compound: false, 74 | }, 75 | nodes: result.diagram.shapes.map((node: any) => ({ 76 | v: node.id, 77 | // value: {}, 78 | })), 79 | edges: result.diagram.connections.map((edge: any) => ({ 80 | v: edge.src, 81 | w: edge.dst, 82 | name: edge.id, 83 | // value: {}, 84 | })), 85 | }; 86 | } 87 | } 88 | 89 | const svg = (await d2Instance.render(result.diagram, options)) as string; 90 | return { svg, data }; 91 | } 92 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/diagrams/penrose.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/rehype-penrose" 3 | --- 4 | 5 | import { PackageManagers } from "starlight-package-managers"; 6 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 7 | 8 | Rehype plugin to generate [Penrose](https://penrose.cs.cmu.edu/) diagrams in place of code fences. Example: 9 | 10 | 11 | 12 | 13 | ```penrose style="euclidean.style" domain="euclidean.domain" width=800 height=800 14 | Plane P 15 | Point p, q, r, s 16 | In(p, P) 17 | In(q, P) 18 | In(r, P) 19 | In(s, P) 20 | Let a := Segment(p, q) 21 | Let b := Segment(p, r) 22 | Point m := Midpoint(a) 23 | In(m, P) 24 | Angle theta := InteriorAngle(q, p, r) 25 | Let t := Triangle(p, r, s) 26 | Ray w := Bisector(theta) 27 | Segment h := PerpendicularBisector(a, m) 28 | AutoLabel p, q, r, s, m 29 | Label P $E^2$ 30 | ``` 31 | 32 | 33 | 34 | 35 | ````md 36 | ```penrose style="euclidean.style" domain="euclidean.domain" width=800 height=800 37 | Plane P 38 | Point p, q, r, s 39 | In(p, P) 40 | In(q, P) 41 | In(r, P) 42 | In(s, P) 43 | Let a := Segment(p, q) 44 | Let b := Segment(p, r) 45 | Point m := Midpoint(a) 46 | In(m, P) 47 | Angle theta := InteriorAngle(q, p, r) 48 | Let t := Triangle(p, r, s) 49 | Ray w := Bisector(theta) 50 | Segment h := PerpendicularBisector(a, m) 51 | AutoLabel p, q, r, s, m 52 | Label P $E^2$ 53 | ``` 54 | ```` 55 | 56 | 57 | 58 | 59 | ## Installation 60 | 61 | 62 | 63 | ## Usage 64 | 65 | ```js 66 | import rehypeD2 from "@beoe/rehype-penrose"; 67 | 68 | const html = await unified() 69 | .use(remarkParse) 70 | .use(remarkRehype) 71 | .use(rehypeD2, { 72 | shared: "shared", 73 | /* options */ 74 | }) 75 | .use(rehypeStringify) 76 | .process(`markdown`); 77 | ``` 78 | 79 | Check out other [options](/start-here/configuration/). 80 | 81 | ### Configuration 82 | 83 | You probaly want to use [`file`](/start-here/strategy/#file) strategy. 84 | 85 | ### `PenroseOptions` 86 | 87 | ```ts 88 | export type PenroseOptions = { 89 | /** 90 | * path to shared folder for `.domain` and `.style` files 91 | */ 92 | shared: string; 93 | /** 94 | * Width of diagram 95 | */ 96 | width?: number; 97 | /** 98 | * Height of diagram 99 | */ 100 | height?: number; 101 | /** 102 | * Name (or path) for `.style` file 103 | */ 104 | style?: string; 105 | /** 106 | * Name (or path) for `.domain` file 107 | */ 108 | domain?: string; 109 | variation?: string; 110 | namespace?: string; 111 | }; 112 | ``` 113 | 114 | You can set it globally: 115 | 116 | ```ts 117 | use(rehypePenrose, { 118 | width: 400, 119 | height: 400, 120 | }); 121 | ``` 122 | 123 | Or locally: 124 | 125 | ````md 126 | ```penrose width=400 height=400 127 | ... 128 | ``` 129 | ```` 130 | -------------------------------------------------------------------------------- /packages/rehype-graphviz/src/render.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * it is possible to add other formats 3 | */ 4 | export type DataFormat = "dagre" | "graphology"; 5 | 6 | // import { type Engine } from "@hpcc-js/wasm"; 7 | type Engine = 8 | | "circo" 9 | | "dot" 10 | | "fdp" 11 | | "sfdp" 12 | | "neato" 13 | | "osage" 14 | | "patchwork" 15 | | "twopi" 16 | | "nop" 17 | | "nop2"; 18 | 19 | export type RehypeGraphvizConfig = { 20 | graphFormat: DataFormat; 21 | engine: Engine; 22 | }; 23 | 24 | type RenderGraphvizOptions = RehypeGraphvizConfig & { 25 | code: string; 26 | }; 27 | 28 | import { waitFor } from "@beoe/rehype-code-hook"; 29 | /** 30 | * If all graphviz diagrams are cached it would not even load module in memory. 31 | * If there are diagrams, it would load module and first few renders would be async, 32 | * but all consequent renders would be sync 33 | */ 34 | export const renderGraphviz = waitFor( 35 | async () => { 36 | // @ts-ignore 37 | const Graphviz = (await import("@hpcc-js/wasm")).Graphviz; 38 | return await Graphviz.load(); 39 | }, 40 | (graphviz) => 41 | ({ code, engine, graphFormat }: RenderGraphvizOptions) => { 42 | let data; 43 | if (graphFormat) { 44 | // without this I can't get consistent ids for JSON and SVG outputs 45 | code = graphviz.unflatten(code); 46 | code = graphviz.nop(code); 47 | 48 | const obj = JSON.parse( 49 | graphviz.layout(code, "dot_json", engine || "dot") 50 | ); 51 | 52 | if (graphFormat === "graphology") { 53 | data = { 54 | attributes: { name: "g" }, 55 | options: { 56 | allowSelfLoops: true, 57 | multi: true, 58 | type: obj.directed ? "directed" : "mixed", 59 | }, 60 | nodes: obj.objects.map((node: any) => ({ 61 | key: node._gvid + 1, 62 | })), 63 | edges: obj.edges.map((edge: any) => ({ 64 | key: edge._gvid + 1, 65 | source: edge.tail + 1, 66 | target: edge.head + 1, 67 | })), 68 | }; 69 | } 70 | 71 | if (graphFormat === "dagre") { 72 | data = { 73 | options: { 74 | directed: obj.directed, 75 | multigraph: true, 76 | compound: false, 77 | }, 78 | nodes: obj.objects.map((node: any) => ({ 79 | v: node.unique_id + 1, 80 | })), 81 | edges: obj.edges.map((edge: any) => ({ 82 | v: edge.tail + 1, 83 | w: edge.head + 1, 84 | name: edge._gvid + 1, 85 | })), 86 | }; 87 | } 88 | } 89 | 90 | return { 91 | svg: cleanup(graphviz.layout(code, "svg", engine || "dot")), 92 | data, 93 | }; 94 | } 95 | ); 96 | 97 | /** 98 | * removes `` 99 | * removes ` svg.split("\n").slice(6).join("\n"); 102 | -------------------------------------------------------------------------------- /packages/docs/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | /* needed if you use strategy: "img-class-dark-mode" */ 2 | html[data-theme="light"] .beoe-dark { 3 | display: none; 4 | } 5 | 6 | html[data-theme="dark"] .beoe-light { 7 | display: none; 8 | } 9 | 10 | /* dark mode */ 11 | .graphviz { 12 | text { 13 | fill: var(--sl-color-white); 14 | } 15 | [fill="black"], 16 | [fill="#000"] { 17 | fill: var(--sl-color-white); 18 | } 19 | [stroke="black"], 20 | [stroke="#000"] { 21 | stroke: var(--sl-color-white); 22 | } 23 | } 24 | 25 | /* ants interactivity */ 26 | .graphviz[data-beoe] { 27 | .edge.active path:first-child { 28 | stroke-dasharray: 5 5; 29 | animation-name: dash; 30 | animation-duration: 1000ms; 31 | stroke-dashoffset: 0; 32 | animation-iteration-count: infinite; 33 | animation-timing-function: linear; 34 | } 35 | 36 | .node { 37 | cursor: default; 38 | } 39 | 40 | .node *:first-child { 41 | fill: var(--sl-color-black); 42 | } 43 | 44 | .node.selected *:first-child { 45 | stroke-width: 2px; 46 | } 47 | } 48 | @media (prefers-reduced-motion) { 49 | .graphviz[data-beoe] { 50 | .edge.active path:first-child { 51 | animation-duration: 4000ms; 52 | } 53 | } 54 | } 55 | 56 | /* dark mode */ 57 | .vizdom { 58 | :not([fill]) { 59 | fill: var(--sl-color-white); 60 | } 61 | [fill="black"], 62 | [fill="#000"] { 63 | fill: var(--sl-color-white); 64 | } 65 | [stroke="black"], 66 | [stroke="#000"] { 67 | stroke: var(--sl-color-white); 68 | } 69 | [fill="white"], 70 | [fill="#fff"] { 71 | fill: var(--sl-color-black); 72 | } 73 | [stroke="white"], 74 | [stroke="#fff"] { 75 | stroke: var(--sl-color-black); 76 | } 77 | /* this works if disable svgo option to convert everything to paths */ 78 | .node rect { 79 | rx: 7px; 80 | } 81 | } 82 | 83 | /* shadow interactivity */ 84 | .vizdom[data-beoe].shadow { 85 | .shadow { 86 | opacity: 0.4; 87 | } 88 | 89 | .node { 90 | cursor: default; 91 | } 92 | } 93 | 94 | /* ants interactivity */ 95 | @keyframes dash { 96 | from { 97 | stroke-dashoffset: 40; 98 | } 99 | to { 100 | stroke-dashoffset: 0; 101 | } 102 | } 103 | .vizdom[data-beoe].ants { 104 | .edge.active a path:first-child { 105 | stroke-dasharray: 5 5; 106 | animation-name: dash; 107 | animation-duration: 1000ms; 108 | stroke-dashoffset: 0; 109 | animation-iteration-count: infinite; 110 | animation-timing-function: linear; 111 | } 112 | 113 | .node.selected a *:first-child { 114 | stroke-width: 2px; 115 | } 116 | 117 | .node { 118 | cursor: pointer; 119 | } 120 | } 121 | @media (prefers-reduced-motion) { 122 | .vizdom[data-beoe].ants { 123 | .edge.active a path:first-child { 124 | animation-duration: 4000ms; 125 | } 126 | } 127 | } 128 | 129 | /* for demo */ 130 | .graphviz .style-me path { 131 | fill: lightblue; 132 | } 133 | .vizdom .style-me :first-child { 134 | fill: lightblue; 135 | } 136 | 137 | .d2[data-beoe].shadow { 138 | .shadow { 139 | opacity: 0.4; 140 | } 141 | 142 | .shape { 143 | cursor: default; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /packages/rehype-vizdom/test/fixtures/a.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/remark-code-hook/README.md: -------------------------------------------------------------------------------- 1 | # @beoe/remark-code-hook 2 | 3 | Remark plugin to make it easier to write custom processors for code e.g.: 4 | 5 | ````md 6 | Block-code: 7 | 8 | ```js 9 | const x = 1; 10 | ``` 11 | 12 | Inline-code: 13 | `x` 14 | ```` 15 | 16 | This plugin is usefull if you want to create remark plugin to: 17 | 18 | - do what you would typically do at rehype level, but can't: 19 | - highlight code, like `@shikijs/rehype`, `rehype-prism`, `rehype-highlight` etc. 20 | - render diagrams, like `rehype-mermaid` 21 | - do something else, like `rehype-color-chips` 22 | - do something like [obsidian-dataview](https://blacksmithgu.github.io/obsidian-dataview/) 23 | 24 | ## Usage 25 | 26 | Basic example looks like this: 27 | 28 | ```js 29 | import { remarkCodeHook } from "@beoe/remark-code-hook"; 30 | import { generateSvg } from "./generateSvg.js"; 31 | 32 | export const rehypeExampleDiagram = (options = {}) => { 33 | return remarkCodeHook({ 34 | ...options, 35 | code: ({ code }) => generateSvg(code), 36 | }); 37 | }; 38 | ``` 39 | 40 | If you have code like this: 41 | 42 | ````md 43 | ```js {1,10} 44 | const x = 1; 45 | ``` 46 | ```` 47 | 48 | `code` callback would be called with: 49 | 50 | ```js 51 | { 52 | code: "const x = 1;\n", 53 | inline: false, 54 | language: "js", 55 | meta: "{1,10}" 56 | } 57 | ``` 58 | 59 | If you have code like this: 60 | 61 | ```md 62 | `const x = 1;` 63 | ``` 64 | 65 | `code` callback would be called with: 66 | 67 | ```js 68 | { 69 | code: "const x = 1;", 70 | inline: true, 71 | language: undefined, 72 | meta: undefined 73 | } 74 | ``` 75 | 76 | Now it is time to render your thing: 77 | 78 | - you can check `code`, `inline`, `language`, `meta` and if this is not the block you are looking for you can return `undefined` - and block would be unchanged 79 | - if you decided to render something you can return `string` (for example SVG or HTML), you can return MDAST fragment or you can return promise of string or MDAST 80 | - if return value is MDAST fragment it will replace whole code block 81 | - if return value is string it would be interpreted as raw-html and will replace whole code block 82 | - if return value is promise it will wait until it resolves and do one of steps above 83 | 84 | You can configure your plugin to be called only for specific cases, for example: 85 | 86 | - only for language `example`: `remarkCodeHook({language: "example",..})` 87 | - only for inline code: `remarkCodeHook({inline: true,..})` 88 | 89 | To enable caching you need to pass `Map`-like storage: 90 | 91 | ```js 92 | remarkCodeHook({ code, cache: new Map(), hashTostring: true }); 93 | ``` 94 | 95 | I checked it with [@beoe/cache](/packages/cache/), but it suppose to work with any storage that has `Map` like interface. You may pass additional `salt` param to reset cache, for example when configuration of your plugin changed. 96 | 97 | ## Tips 98 | 99 | ### Meta string 100 | 101 | If you need to parse `meta` param you can use, for example: 102 | 103 | - https://github.com/Microflash/fenceparser 104 | - https://github.com/frencojobs/fenceparser 105 | 106 | ### Raw HTML 107 | 108 | If you want to return raw HTML in plugin you would need to use: 109 | 110 | - `.use(remarkRehype, { allowDangerousHtml: true })` 111 | - `.use(rehypeRaw)` 112 | -------------------------------------------------------------------------------- /notes/notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | ## Experiments 4 | 5 | - [ ] rehype-tree-sitter 6 | - https://github.com/haze/rehype-tree-sitter 7 | - https://tree-sitter.github.io/tree-sitter/syntax-highlighting 8 | - https://andrewtbiehl.com/blog/jekyll-tree-sitter 9 | - https://github.com/devongovett/tree-sitter-highlight 10 | - https://www.npmjs.com/package/web-tree-sitter 11 | - [ ] venn.js (Astro component) 12 | - https://github.com/stereobooster/venn-isomorphic 13 | - [ ] edeap 14 | - https://github.com/stereobooster/edeap 15 | - [ ] svgbob (rehype) 16 | - https://github.com/agoose77/svgbob-wasm 17 | - [ ] pikchr (rehype) 18 | - https://github.com/fabiospampinato/pikchr-wasm 19 | - [ ] Crazy idea: write transpiler from Mermaid to Graphviz. At least `flowchart` should be possible to convert 20 | - There is [flow.jison](https://github.com/mermaid-js/mermaid/blob/3809732e48a0822fad596d0815a6dc0e166dda94/packages/mermaid/src/diagrams/flowchart/parser/flow.jison) 21 | - [ ] regex syntax highliting 22 | - https://github.com/slevithan/regex-colorizer 23 | - [ ] railroad diagrams 24 | - https://github.com/tabatkins/railroad-diagrams 25 | - https://github.com/warpdesign/brackets-regex-diagram 26 | - https://github.com/klorenz/atom-regex-railroad-diagrams 27 | - [ ] ER diagram based on Graphviz 28 | - https://github.com/troelskn/ergen/blob/master/ergen 29 | - https://github.com/BurntSushi/erd 30 | 31 | ## TODO 32 | 33 | - [ ] make `@beoe/cache` usable in Astro components 34 | - alternative approach pass it to the component as prop and the wrap whole component locally? 35 | - [ ] `@beoe/astro-mermaid` 36 | - [ ] `@beoe/astro-gnuplot` 37 | - [ ] example of gnuplot custom diagram 38 | - any diagram which expects `input.dat`, for example https://gnuplot.sourceforge.net/demo_svg_5.4/histograms.html 39 | - maybe do it like [asciidoctor does for penrose](https://docs.asciidoctor.org/diagram-extension/latest/diagram_types/penrose/)? 40 | - [ ] example of graphviz custom diagram 41 | - https://astro-digital-garden.stereobooster.com/recipes/timeline-diagram/ 42 | - [ ] I think there is a problem with `turbo`. It has problems if there are more than 10 packages. Maybe try `Nx`? 43 | - [ ] write tip about `not-content` from [tailwindcss-typography](https://github.com/tailwindlabs/tailwindcss-typography) 44 | - [ ] Do astro components need to render something in case of an error? 45 | - [ ] maybe move to this monorepo [venn-isomorphic](https://github.com/stereobooster/venn-isomorphic) 46 | - [ ] maybe move to this monorepo [gnuplot-wasm](https://github.com/stereobooster/gnuplot-wasm) 47 | 48 | ## svgo alternatives 49 | 50 | - https://github.com/svg/svgo 51 | - https://www.npmjs.com/package/@minify-html/node 52 | - https://github.com/pleshevskiy/node-svgcleaner 53 | - https://github.com/RazrFalcon/svgcleaner 54 | 55 | ## textDimensions 56 | 57 | Alternatives to client-side `textDimensions`: 58 | 59 | - [resvg-js](https://github.com/yisibl/resvg-js)? 60 | - [canvas](https://github.com/Brooooooklyn/canvas) 61 | - [measureText](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText) 62 | 63 | npm packages: 64 | 65 | - [get-text-width](https://www.npmjs.com/package/get-text-width) 66 | - [js-server-text-width](https://github.com/Evgenus/js-server-text-width) 67 | - [text-width](https://www.npmjs.com/package/text-width) 68 | - [string-pixel-width](https://github.com/adambisek/string-pixel-width) 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | .DS_Store 175 | .turbo 176 | 177 | # build output 178 | dist/ 179 | 180 | # generated types 181 | .astro/ 182 | 183 | # dependencies 184 | node_modules/ 185 | 186 | # logs 187 | npm-debug.log* 188 | yarn-debug.log* 189 | yarn-error.log* 190 | pnpm-debug.log* 191 | 192 | # environment variables 193 | .env 194 | .env.production 195 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/diagrams/d2.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/rehype-d2" 3 | --- 4 | 5 | import { PackageManagers } from "starlight-package-managers"; 6 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 7 | 8 | Rehype plugin to generate [D2](https://d2lang.com/) diagrams in place of code fences. Example: 9 | 10 | 11 | 12 | 13 | ```d2 14 | direction: right 15 | x.y.z -> a.b.c: Label 16 | ``` 17 | 18 | 19 | 20 | 21 | ````md 22 | ```d2 23 | direction: right 24 | x.y.z -> a.b.c: Label 25 | ``` 26 | ```` 27 | 28 | 29 | 30 | 31 | ## Installation 32 | 33 | 34 | 35 | ## Usage 36 | 37 | ```js 38 | import rehypeD2 from "@beoe/rehype-d2"; 39 | 40 | const html = await unified() 41 | .use(remarkParse) 42 | .use(remarkRehype) 43 | .use(rehypeD2, { 44 | /* options */ 45 | }) 46 | .use(rehypeStringify) 47 | .process(`markdown`); 48 | ``` 49 | 50 | Check out other [options](/start-here/configuration/). 51 | 52 | ### Configuration 53 | 54 | You probaly want to use [`file`](/start-here/strategy/#file) strategy and one of dark schemes (`class` or `media`). Unless you need [interactivity](/start-here/interactivity/). 55 | 56 | ### `d2Options` 57 | 58 | ```ts 59 | export type D2Options = { 60 | /** 61 | * @default 0 62 | * Set the diagram theme ID. 63 | */ 64 | theme?: string; 65 | /** 66 | * @default -1 67 | * The theme to use when the viewer's browser is in dark mode. 68 | * When left unset --theme is used for both light and dark mode. 69 | * Be aware that explicit styles set in D2 code will still be 70 | * applied and this may produce unexpected results. We plan on 71 | * resolving this by making style maps in D2 light/dark mode 72 | * specific. See https://github.com/terrastruct/d2/issues/831. 73 | */ 74 | darkTheme?: string; 75 | /** 76 | * Set the diagram layout engine to the passed string. For a 77 | * list of available options, run layout. 78 | */ 79 | layout?: "dagre" | "elk"; 80 | /** 81 | * @default false 82 | * Renders the diagram to look like it was sketched by hand. 83 | */ 84 | sketch?: boolean; 85 | /** 86 | * @default undefined 87 | * Glob pattern for files to be used for [`import`](https://d2lang.com/tour/imports/). 88 | * `GlobOptions` are from [tinyglobby](https://github.com/SuperchupuDev/tinyglobby#options). 89 | */ 90 | shared?: string | string[] | GlobOptions; 91 | }; 92 | ``` 93 | 94 | You can set it globally: 95 | 96 | ```ts 97 | use(rehypeD2, { 98 | d2Options: { layout: "dagre" }, 99 | }); 100 | ``` 101 | 102 | Or locally: 103 | 104 | ````md 105 | ```d2 layout=elk 106 | ... 107 | ``` 108 | ```` 109 | 110 | ## Issues 111 | 112 | - [x] [Support links in connections](https://github.com/terrastruct/d2/pull/1955) 113 | - [x] [JS package](https://github.com/terrastruct/d2/discussions/234#discussioncomment-11286029) 114 | - [x] [Export JSON graph](https://github.com/terrastruct/d2/discussions/2224) 115 | - [x] [support `import`](https://github.com/terrastruct/d2/issues/2301) 116 | - [x] [d2js: Additional render options](https://github.com/terrastruct/d2/pull/2343), like `pad` 117 | - [ ] [Class-based dark mode](https://github.com/terrastruct/d2/pull/1803) 118 | - [ ] [Remove embedded fonts](https://github.com/terrastruct/d2/discussions/132) 119 | - [ ] [Smaller embedded icons](https://github.com/terrastruct/d2/discussions/2223) 120 | -------------------------------------------------------------------------------- /packages/fenceparser/README.md: -------------------------------------------------------------------------------- 1 | Fork of https://github.com/frencojobs/fenceparser 2 | 3 | --- 4 | 5 | 6 | 7 |

A tiny, well-tested parser for parsing metadata out of fenced code blocks in Markdown.

8 | 9 |
10 | 11 | ## Overview ・ npm bundle size Codecov 12 | 13 | Assuming you have this code fence in your Markdown, 14 | 15 | 16 | ````md 17 | ```ts twoslash {1-3, 5} title="Hello, World" 18 | ```` 19 | 20 | 21 | Using [remark](https://github.com/remarkjs/remark) will yield two information about that code block, `lang` and `meta` like this. 22 | 23 | ```json 24 | { 25 | "lang": "ts", 26 | "meta": "twoslash {1-3, 5} title=\"Hello, World\"" 27 | } 28 | ``` 29 | 30 | Use `fenceparser` to parse the `meta` string out to a useful object. 31 | 32 | ```js 33 | import parse from 'fenceparser' 34 | 35 | console.log(parse(meta)) 36 | 37 | // { 38 | // twoslash: true, 39 | // highlight: { '1-3': true, '5': true }, 40 | // title: 'Hello, World' 41 | // } 42 | ``` 43 | 44 | > The parser won't intentionally handle parsing the language part since it is usually handled by the Markdown parsers. 45 | 46 | But if you want to allow loose syntax grammars such as `ts{1-3, 5}` as well as `ts {1-3, 5}` which is used by [gatsby-remark-vscode](https://github.com/andrewbranch/gatsby-remark-vscode) as an example, remark won't parse the language correctly. 47 | 48 | 49 | ```json5 50 | { 51 | "lang": "ts{1-3,", // because remark uses space to split 52 | "meta": "5}" 53 | } 54 | ``` 55 | 56 | 57 | In these cases, you can use the the library's `lex` function to get a properly tokenized array. You may then take out the first element as `lang`. For example, 58 | 59 | ```js 60 | import {lex, parse} from 'fenceparser' 61 | // Notice this ^ parse is not the same the default export function 62 | 63 | const full = [node.lang, node.meta].join(' ') // Join them back 64 | 65 | const tokens = lex(full) 66 | const lang = tokens.shift() // ts 67 | const meta = parse(tokens) // { highlight: {'1-3': true, '5': true} } 68 | ``` 69 | 70 | ## Syntax 71 | 72 | The syntax grammar is loosely based on techniques used by various syntax-highlighters. Rules are such that 73 | 74 | - Valid HTML attributes can be used, `attribute`, `data-attribute`, etc. 75 | - Just like in HTML, top-level attribute names are case insensitive 76 | - Attributes without values are assigned as `true` 77 | - Attribute values can be single or double quoted strings, int/float numbers, booleans, objects or arrays 78 | - Non-quoted strings are valid as long as they are not separated by a whitespace or a line-break, `attr=--theme-color` 79 | - Objects can accept valid attributes as children, or valid attributes with value assigned by `:` keyword, `{1-3, 5, ids: {7}}` 80 | - Arrays are just like JavaScript's arrays 81 | - Objects without attribute keys `{1-3} {7}` are merged and assigned to the `highlight` object 82 | - No trailing commas 83 | 84 | ## Acknowledgements 85 | 86 | 1. This project is made initially to use with [Twoslash](https://github.com/shikijs/twoslash). 87 | 2. The initial implementations of lexer and parser are based on the examples from the book [Crafting Interpreters](http://craftinginterpreters.com). 88 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/examples/graphviz-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: graphviz test 3 | draft: true 4 | --- 5 | 6 | ## rehype-plugin 7 | 8 | ```dot 9 | digraph finite_state_machine { 10 | bgcolor="transparent"; 11 | fontname="Helvetica,Arial,sans-serif"; 12 | node [fontname="Helvetica,Arial,sans-serif"] 13 | edge [fontname="Helvetica,Arial,sans-serif"] 14 | rankdir=LR; 15 | node [shape = doublecircle]; 0 3 4 8; 16 | node [shape = circle]; 17 | 0 -> 2 [label = "SS(B)"]; 18 | 0 -> 1 [label = "SS(S)"]; 19 | 1 -> 3 [label = "S($end)"]; 20 | 2 -> 6 [label = "SS(b)"]; 21 | 2 -> 5 [label = "SS(a)"]; 22 | 2 -> 4 [label = "S(A)"]; 23 | 5 -> 7 [label = "S(b)"]; 24 | 5 -> 5 [label = "S(a)"]; 25 | 6 -> 6 [label = "S(b)"]; 26 | 6 -> 5 [label = "S(a)"]; 27 | 7 -> 8 [label = "S(b)"]; 28 | 7 -> 5 [label = "S(a)"]; 29 | 8 -> 6 [label = "S(b)"]; 30 | 8 -> 5 [label = "S(a)"]; 31 | } 32 | ``` 33 | 34 | ```dot 35 | graph grid { 36 | bgcolor="transparent"; 37 | label="grid" 38 | labelloc = "t" 39 | node [shape=plaintext] 40 | // arbitrary path on rigid grid 41 | A0 -- B1 -- C2 -- D3 -- E4 -- F5 -- G6 -- H7 42 | H0 -- G1 -- F2 -- E3 -- D4 -- C5 -- B6 -- A7 43 | 44 | edge [weight=1000 style=dashed color=dimgrey] 45 | 46 | // uncomment to hide the grid 47 | //edge [style=invis] 48 | 49 | A0 -- A1 -- A2 -- A3 -- A4 -- A5 -- A6 -- A7 50 | B0 -- B1 -- B2 -- B3 -- B4 -- B5 -- B6 -- B7 51 | C0 -- C1 -- C2 -- C3 -- C4 -- C5 -- C6 -- C7 52 | D0 -- D1 -- D2 -- D3 -- D4 -- D5 -- D6 -- D7 53 | E0 -- E1 -- E2 -- E3 -- E4 -- E5 -- E6 -- E7 54 | F0 -- F1 -- F2 -- F3 -- F4 -- F5 -- F6 -- F7 55 | G0 -- G1 -- G2 -- G3 -- G4 -- G5 -- G6 -- G7 56 | H0 -- H1 -- H2 -- H3 -- H4 -- H5 -- H6 -- H7 57 | 58 | rank=same {A0 -- B0 -- C0 -- D0 -- E0 -- F0 -- G0 -- H0} 59 | rank=same {A1 -- B1 -- C1 -- D1 -- E1 -- F1 -- G1 -- H1} 60 | rank=same {A2 -- B2 -- C2 -- D2 -- E2 -- F2 -- G2 -- H2} 61 | rank=same {A3 -- B3 -- C3 -- D3 -- E3 -- F3 -- G3 -- H3} 62 | rank=same {A4 -- B4 -- C4 -- D4 -- E4 -- F4 -- G4 -- H4} 63 | rank=same {A5 -- B5 -- C5 -- D5 -- E5 -- F5 -- G5 -- H5} 64 | rank=same {A6 -- B6 -- C6 -- D6 -- E6 -- F6 -- G6 -- H6} 65 | rank=same {A7 -- B7 -- C7 -- D7 -- E7 -- F7 -- G7 -- H7} 66 | } 67 | ``` 68 | 69 | ```dot 70 | digraph { 71 | bgcolor="transparent"; 72 | graph [rankdir=LR]; 73 | node [shape=record]; 74 | 0 [label="0 | [• S, $]\n[S → • a S b, $]\n[S → •, $]"]; 75 | 1 [label="1 | [S •, $]"]; 76 | 2 [label="2 | [S → a • S b, $]\n[S → • a S b, b]\n[S → •, b]"]; 77 | 3 [label="3 | [S → a S • b, $]"]; 78 | 4 [label="4 | [S → a • S b, b]\n[S → • a S b, b]\n[S → •, b]"]; 79 | 5 [label="5 | [S → a S b •, $]"]; 80 | 6 [label="6 | [S → a S • b, b]"]; 81 | 7 [label="7 | [S → a S b •, b]"]; 82 | 0 -> 1 [label=S]; 83 | 0 -> 2 [label=a]; 84 | 2 -> 3 [label=S]; 85 | 2 -> 4 [label=a]; 86 | 3 -> 5 [label=b]; 87 | 4 -> 6 [label=S]; 88 | 4 -> 4 [label=a]; 89 | 6 -> 7 [label=b]; 90 | } 91 | ``` 92 | 93 | ```dot 94 | graph { 95 | layout=patchwork 96 | node [style=filled] 97 | "$2" [area=200 fillcolor=gold] 98 | "$1" [area=100 fillcolor=gold] 99 | "50c" [area= 50 fillcolor=silver] 100 | "20c" [area= 20 fillcolor=silver] 101 | "10c" [area= 10 fillcolor=silver] 102 | "5c" [area= 5 fillcolor=silver] 103 | } 104 | ``` 105 | 106 | ## Image 107 | 108 | ```dot 109 | digraph { 110 | a[image="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"]; 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/diagrams/graphviz.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@beoe/rehype-graphviz" 3 | --- 4 | 5 | import { PackageManagers } from "starlight-package-managers"; 6 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 7 | 8 | Rehype plugin to generate [Graphviz](https://graphviz.org/) diagrams in place of code fences. Example: 9 | 10 | 11 | 12 | 13 | ```dot 14 | digraph finite_state_machine { 15 | bgcolor="transparent"; 16 | fontname="Helvetica,Arial,sans-serif"; 17 | node [fontname="Helvetica,Arial,sans-serif"] 18 | edge [fontname="Helvetica,Arial,sans-serif"] 19 | rankdir=LR; 20 | node [shape = doublecircle]; 0 3 4 8; 21 | node [shape = circle]; 22 | 0 -> 2 [label = "SS(B)"]; 23 | 0 -> 1 [label = "SS(S)"]; 24 | 1 -> 3 [label = "S($end)"]; 25 | 2 -> 6 [label = "SS(b)"]; 26 | 2 -> 5 [label = "SS(a)"]; 27 | 2 -> 4 [label = "S(A)"]; 28 | 5 -> 7 [label = "S(b)"]; 29 | 5 -> 5 [label = "S(a)"]; 30 | 6 -> 6 [label = "S(b)"]; 31 | 6 -> 5 [label = "S(a)"]; 32 | 7 -> 8 [label = "S(b)"]; 33 | 7 -> 5 [label = "S(a)"]; 34 | 8 -> 6 [label = "S(b)"]; 35 | 8 -> 5 [label = "S(a)"]; 36 | } 37 | ``` 38 | 39 | 40 | 41 | 42 | ````md 43 | ```dot 44 | digraph finite_state_machine { 45 | bgcolor="transparent"; 46 | fontname="Helvetica,Arial,sans-serif"; 47 | node [fontname="Helvetica,Arial,sans-serif"] 48 | edge [fontname="Helvetica,Arial,sans-serif"] 49 | rankdir=LR; 50 | node [shape = doublecircle]; 0 3 4 8; 51 | node [shape = circle]; 52 | 0 -> 2 [label = "SS(B)"]; 53 | 0 -> 1 [label = "SS(S)"]; 54 | 1 -> 3 [label = "S($end)"]; 55 | 2 -> 6 [label = "SS(b)"]; 56 | 2 -> 5 [label = "SS(a)"]; 57 | 2 -> 4 [label = "S(A)"]; 58 | 5 -> 7 [label = "S(b)"]; 59 | 5 -> 5 [label = "S(a)"]; 60 | 6 -> 6 [label = "S(b)"]; 61 | 6 -> 5 [label = "S(a)"]; 62 | 7 -> 8 [label = "S(b)"]; 63 | 7 -> 5 [label = "S(a)"]; 64 | 8 -> 6 [label = "S(b)"]; 65 | 8 -> 5 [label = "S(a)"]; 66 | } 67 | ``` 68 | ```` 69 | 70 | 71 | 72 | 73 | ## Installation 74 | 75 | 76 | 77 | ## Usage 78 | 79 | ```js 80 | import rehypeGraphviz from "@beoe/rehype-graphviz"; 81 | 82 | const html = await unified() 83 | .use(remarkParse) 84 | .use(remarkRehype) 85 | .use(rehypeGraphviz, { 86 | /* options */ 87 | }) 88 | .use(rehypeStringify) 89 | .process(`markdown`); 90 | ``` 91 | 92 | Check out other [options](/start-here/configuration/). 93 | 94 | ### Configuration 95 | 96 | You probaly want to use [`inline`](/start-here/strategy/#inline) strategy and implement dark scheme via CSS: 97 | 98 | ```css 99 | .graphviz { 100 | text { 101 | fill: var(--sl-color-white); 102 | } 103 | [fill="black"], 104 | [fill="#000"] { 105 | fill: var(--sl-color-white); 106 | } 107 | [stroke="black"], 108 | [stroke="#000"] { 109 | stroke: var(--sl-color-white); 110 | } 111 | } 112 | ``` 113 | 114 | ## Tips 115 | 116 | `inline` strategy allows to use [interactivity](/start-here/interactivity/). 117 | 118 | ### Transparent background 119 | 120 | To remove background use: 121 | 122 | ````md 123 | ```dot 124 | digraph G { 125 | bgcolor="transparent" 126 | } 127 | ``` 128 | ```` 129 | 130 | ## TODO 131 | 132 | - [ ] support images 133 | 134 | ```js 135 | graphviz.neato('digraph { a[image="./resources/hpcc-logo.png"]; }', "svg", { 136 | images: [ 137 | { path: "./resources/hpcc-logo.png", width: "272px", height: "92px" }, 138 | ], 139 | }); 140 | ``` 141 | -------------------------------------------------------------------------------- /logo/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 67 | 71 | 75 | 76 | 77 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 116 | 117 | -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 67 | 71 | 75 | 76 | 77 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 117 | 118 | -------------------------------------------------------------------------------- /packages/docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 67 | 71 | 75 | 76 | 77 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 117 | 118 | --------------------------------------------------------------------------------