├── src ├── components │ ├── PreOverride.astro │ ├── LeafDirective.astro │ ├── ContainerDirective.astro │ ├── decode.ts │ ├── TextDirective.astro │ ├── Mermaid.astro │ ├── H2Override.astro │ ├── BlockquoteOverride.astro │ ├── LinkOverride.astro │ ├── CodeOverride.astro │ ├── ImageOverride.astro │ └── Aside.astro ├── env.d.ts ├── content │ ├── examples │ │ ├── heading.mdx │ │ ├── link.mdx │ │ ├── blockquote.mdx │ │ ├── image.mdx │ │ ├── code.mdx │ │ ├── directive.mdx │ │ └── Ghostscript_Tiger.svg │ └── config.ts ├── pages │ ├── index.astro │ └── examples │ │ └── [...slug].astro └── layouts │ ├── Content.astro │ └── Layout.astro ├── tsconfig.json ├── .vscode ├── extensions.json └── launch.json ├── .gitignore ├── public └── favicon.svg ├── package.json ├── astro.config.mjs ├── alternative-proposal.md ├── README.md └── extended-proposal.md /src/components/PreOverride.astro: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "exclude": ["dist"] 4 | } 5 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/components/LeafDirective.astro: -------------------------------------------------------------------------------- 1 | --- 2 | console.log('LeafDirective', Astro.props); 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/content/examples/heading.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: heading 3 | --- 4 | 5 | ## Test heading 6 | 7 | ## Heading with permanent anchor \{#custom-id\} 8 | -------------------------------------------------------------------------------- /src/components/ContainerDirective.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Aside from "./Aside.astro"; 3 | 4 | const { directive } = Astro.props; 5 | // console.log('ContainerDirective', Astro.props); 6 | --- 7 | 8 | 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/content/examples/link.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: link 3 | --- 4 | 5 | [standard internal link](/) 6 | 7 | [standard external link](https://example.com) 8 | 9 | autolink: https://example.com 10 | 11 | [reference-style link][foo] 12 | 13 | [foo]: https://example.com "Optional Title Here" 14 | -------------------------------------------------------------------------------- /src/content/examples/blockquote.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: blockquote 3 | --- 4 | 5 | ## Github-style callout (or aside) 6 | 7 | ```md 8 | > [!NOTE] 9 | > something 10 | ``` 11 | 12 | > [!NOTE] 13 | > something 14 | 15 | ## Basic blockquote 16 | 17 | ```md 18 | > quote 19 | ``` 20 | 21 | > quote 22 | -------------------------------------------------------------------------------- /src/components/decode.ts: -------------------------------------------------------------------------------- 1 | // more robust implementation https://github.com/mdevils/html-entities 2 | export function decode(x: string) { 3 | return x 4 | .replaceAll(">", ">") 5 | .replaceAll("<", "<") 6 | .replaceAll("&", "&") 7 | .replaceAll(""", '"') 8 | .replaceAll("'", "'"); 9 | } 10 | -------------------------------------------------------------------------------- /src/content/examples/image.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: image 3 | --- 4 | 5 | ## local image 6 | 7 | ![local image](./Ghostscript_Tiger.svg) 8 | 9 | ## local image with title 10 | 11 | ![local image with tile](./Ghostscript_Tiger.svg "test") 12 | 13 | ## remote image 14 | 15 | ![remote image](https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /src/components/TextDirective.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { YouTube } from "astro-embed"; 3 | import { Icon } from "astro-icon/components"; 4 | 5 | const { directive, id, ...rest } = Astro.props; 6 | // console.log("TextDirective", Astro.props); 7 | --- 8 | 9 | {directive === "youtube" && } 10 | 11 | {directive === "icon" && } 12 | -------------------------------------------------------------------------------- /src/components/Mermaid.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { createMermaidRenderer } from "mermaid-isomorphic"; 3 | 4 | type Props = { 5 | diagram: string; 6 | }; 7 | 8 | const renderer = createMermaidRenderer(); 9 | const diagram = Astro.props.diagram; 10 | const [result] = await renderer([diagram]); 11 | // @ts-expect-error 12 | const { svg } = result.value; 13 | --- 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Content from "../layouts/Content.astro"; 3 | import { getCollection } from "astro:content"; 4 | const examples = await getCollection("examples"); 5 | --- 6 | 7 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /src/layouts/Content.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "./Layout.astro"; 3 | 4 | interface Props { 5 | title: string; 6 | } 7 | 8 | const { title } = Astro.props; 9 | --- 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 27 | -------------------------------------------------------------------------------- /src/content/config.ts: -------------------------------------------------------------------------------- 1 | // 1. Import utilities from `astro:content` 2 | import { z, defineCollection } from "astro:content"; 3 | 4 | // 2. Define a `type` and `schema` for each collection 5 | const examplesCollection = defineCollection({ 6 | type: "content", // v2.5.0 and later 7 | schema: z.object({ 8 | title: z.string(), 9 | }), 10 | }); 11 | 12 | // 3. Export a single `collections` object to register your collection(s) 13 | export const collections = { 14 | examples: examplesCollection, 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/H2Override.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | id: string; 4 | }; 5 | 6 | const id = Astro.props.id; 7 | --- 8 | 9 |

10 | 16 |

17 | 18 | 30 | -------------------------------------------------------------------------------- /src/components/BlockquoteOverride.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Aside from "./Aside.astro"; 3 | 4 | let text = await Astro.slots.render("default"); 5 | const matches = text.trim().match(/^\\[!([^\]]*)\]/); 6 | const aside = Boolean(matches); 7 | let type = "note" as any; 8 | if (matches) { 9 | type = matches[1].toLowerCase(); 10 | text = text.replace(matches[0], "

"); 11 | } 12 | --- 13 | 14 | { 15 | aside ? ( 16 |

19 | ) : ( 20 |
21 | 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/LinkOverride.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | href: string; 4 | title?: string; 5 | }; 6 | 7 | const { href, title } = Astro.props; 8 | const text = await Astro.slots.render("default"); 9 | 10 | const isExternal = href.startsWith("http"); 11 | const props = isExternal ? { 12 | target: "_blank", 13 | rel: "noopener" 14 | } : {} 15 | --- 16 | 17 | 18 | {isExternal && } 19 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /src/content/examples/code.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: code 3 | --- 4 | 5 | ## code-fences to syntax highlight 6 | 7 | ````md 8 | ```ts 9 | const x = 1; 10 | ``` 11 | ```` 12 | 13 | ```ts 14 | const x = 1; 15 | ``` 16 | 17 | ## inline code 18 | 19 | ```md 20 | `test` 21 | ``` 22 | 23 | `test` 24 | 25 | ## code-fences to diagram 26 | 27 | ````md 28 | ```mermaid 29 | flowchart LR 30 | id 31 | ``` 32 | ```` 33 | 34 | ```mermaid 35 | flowchart LR 36 | id 37 | ``` 38 | 39 | ## code-fences extra 40 | 41 | ````md 42 | ```some thing else 43 | extra 44 | ``` 45 | ```` 46 | 47 | ```some thing else 48 | extra 49 | ``` 50 | -------------------------------------------------------------------------------- /src/content/examples/directive.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: directive 3 | --- 4 | 5 | ## TextDirectives 6 | 7 | ```md 8 | :icon{#material-symbols:auto-awesome-outline} or 9 | :icon{id=material-symbols:auto-awesome-outline} 10 | ``` 11 | 12 | :icon{#material-symbols:auto-awesome-outline} 13 | 14 | ```md 15 | :youtube{#TtRtkTzHVBU} or 16 | :youtube{id=TtRtkTzHVBU} 17 | ``` 18 | 19 | :youtube{#TtRtkTzHVBU} 20 | 21 | ## LeafDirectives 22 | 23 | ```md 24 | ::noexample[text]{something=x} 25 | ``` 26 | 27 | ## ContainerDirective 28 | 29 | ```md 30 | :::tip 31 | something 32 | ::: 33 | ``` 34 | 35 | :::tip 36 | something 37 | ::: 38 | -------------------------------------------------------------------------------- /src/components/CodeOverride.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code } from "astro:components"; 3 | import { decode } from "./decode"; 4 | import Mermaid from "./Mermaid.astro"; 5 | 6 | type Props = { 7 | class?: string; 8 | metastring?: string; 9 | }; 10 | 11 | const props = Astro.props; 12 | const inline = props.class === undefined; 13 | const lang = props.class?.replace("language-", "") as any; 14 | const code = decode(await Astro.slots.render("default")); 15 | --- 16 | 17 | { 18 | lang === "mermaid" ? ( 19 | 20 | ) : ( 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ImageOverride.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { ImageMetadata } from "astro"; 3 | // import { Image } from "astro:assets"; 4 | import { Picture } from "astro:assets"; 5 | 6 | type Props = { 7 | src: string | ImageMetadata; 8 | alt: string; 9 | title?: string; 10 | }; 11 | 12 | const { src, alt, title } = Astro.props; 13 | --- 14 | 15 |
16 | { 17 | typeof src === "string" ? ( 18 | {alt} 19 | ) : ( 20 | 21 | ) 22 | } 23 | {title &&
{title}
} 24 |
25 | 26 | 31 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customize-markdown", 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 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.5.10", 14 | "@astrojs/mdx": "^2.3.1", 15 | "@iconify-json/material-symbols": "^1.1.78", 16 | "astro": "^4.16.1", 17 | "astro-embed": "^0.7.1", 18 | "astro-icon": "^1.1.0", 19 | "mermaid-isomorphic": "^2.1.2", 20 | "playwright": "^1.42.1", 21 | "playwright-chromium": "^1.42.1", 22 | "remark-directive": "^3.0.0", 23 | "remark-heading-id": "^1.0.1", 24 | "sharp": "^0.33.3", 25 | "typescript": "^5.4.5", 26 | "unist-util-visit": "^5.0.0" 27 | } 28 | } -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import icon from "astro-icon"; 3 | import mdx from "@astrojs/mdx"; 4 | import remarkHeadingId from "remark-heading-id"; 5 | import remarkDirective from "remark-directive"; 6 | import { visit } from "unist-util-visit"; 7 | 8 | function smallRemarkAdapter() { 9 | return function (tree) { 10 | visit(tree, function (node) { 11 | if ( 12 | node.type === "containerDirective" || 13 | node.type === "leafDirective" || 14 | node.type === "textDirective" 15 | ) { 16 | node.data = node.data || {}; 17 | node.data.hName = node.type; 18 | node.data.hProperties = { 19 | directive: node.name, 20 | ...(node.attributes || {}), 21 | }; 22 | } 23 | }); 24 | }; 25 | } 26 | 27 | // https://astro.build/config 28 | export default defineConfig({ 29 | integrations: [ 30 | mdx({ 31 | syntaxHighlight: false, 32 | remarkPlugins: [remarkDirective, smallRemarkAdapter, remarkHeadingId], 33 | }), 34 | icon(), 35 | ], 36 | }); 37 | -------------------------------------------------------------------------------- /alternative-proposal.md: -------------------------------------------------------------------------------- 1 | # Alternative proposal 2 | 3 | ## Introduction 4 | 5 | Idea is very similar to [Hugo render hooks](https://gohugo.io/render-hooks/). But in contrary to the initial proposal it doesn't involve Astro components, but instead it proposes to simplify working with remark/rehype plugins. It can be done through writing some kind API layer on top. 6 | 7 | For example, [markdown-it](https://github.com/markdown-it/markdown-it) has much simpler API: 8 | 9 | ```ts 10 | const markdownItInstance = markdownIt({ 11 | highlight(value, lang) { 12 | return highlightToHtml(lang, value); 13 | }, 14 | }); 15 | ``` 16 | 17 | So the idea is to create remark / rehype plugin which would expose those simple callbacks for most popular use-cases. 18 | 19 | Input of callback can be specific to tags. 20 | 21 | Output of callback can be: 22 | 23 | - `undefined` - leave node as is 24 | - `string | hast` - replace node with content 25 | - `Promise` - replace node with content of the promise "resolution" 26 | - It seems that remark / rehype supports promises (but I need to verify this) 27 | -------------------------------------------------------------------------------- /src/components/Aside.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | 4 | type Props = { 5 | type: "note" | "tip" | "important" | "warning" | "caution"; 6 | }; 7 | 8 | const { type } = Astro.props; 9 | 10 | const icons = { 11 | note: "material-symbols:info", 12 | tip: "material-symbols:lightbulb-outline", 13 | important: "material-symbols:chat-info-outline", 14 | warning: "material-symbols:warning", 15 | caution: "material-symbols:stop-circle-outline", 16 | }; 17 | --- 18 | 19 | 23 | 24 | 64 | -------------------------------------------------------------------------------- /src/pages/examples/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Content.astro"; 3 | import { getCollection } from "astro:content"; 4 | 5 | // 1. Generate a new path for every collection entry 6 | export async function getStaticPaths() { 7 | const examples = await getCollection("examples"); 8 | return examples.map((example) => { 9 | return { 10 | params: { slug: example.slug }, 11 | props: { entry: example }, 12 | }; 13 | }); 14 | } 15 | // 2. For your template, you can get the entry directly from the prop 16 | const { entry } = Astro.props; 17 | const { Content } = await entry.render(); 18 | 19 | import CodeOverride from "../../components/CodeOverride.astro"; 20 | import ImageOverride from "../../components/ImageOverride.astro"; 21 | import PreOverride from "../../components/PreOverride.astro"; 22 | import BlockquoteOverride from "../../components/BlockquoteOverride.astro"; 23 | import LinkOverride from "../../components/LinkOverride.astro"; 24 | import H2Override from "../../components/H2Override.astro"; 25 | import LeafDirective from "../../components/LeafDirective.astro"; 26 | import ContainerDirective from "../../components/ContainerDirective.astro"; 27 | import TextDirective from "../../components/TextDirective.astro"; 28 | 29 | const components = { 30 | code: CodeOverride, 31 | img: ImageOverride, 32 | pre: PreOverride, 33 | blockquote: BlockquoteOverride, 34 | a: LinkOverride, 35 | h2: H2Override, 36 | leafdirective: LeafDirective, 37 | containerdirective: ContainerDirective, 38 | textdirective: TextDirective, 39 | }; 40 | --- 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing override for MDX with components 2 | 3 | Testing how different overrides work in Astro: 4 | 5 | - [ ] Code - doesn't work as expected 6 | - [ ] `inline` not passed - no way to distinguish inline-code and block-code 7 | - had to do hack with `props.class === undefined` 8 | - [ ] `lang` not passed 9 | - had to do hack with `class?.replace("language-", "")` 10 | - [ ] block-code get's wrapped in extra `pre` 11 | - had to do hack with overriding `pre` and render it witout tag 12 | - [ ] `` - content is HTML string, which means that one needs to unescape it 13 | - is there a way to use something like `allowDangerousHTML` to pass raw strings to some nodes? 14 | - [x] `metastring` 15 | - [x] [remark-directive](https://github.com/remarkjs/remark-directive) 16 | - works if you add `smallRemarkAdapter`, then `leafdirective`, `containerdirective` and `textdirective` can be handled with MDX overrides 17 | - [x] Image - works as expected 18 | - [x] `src` 19 | - [x] `alt` 20 | - [x] `title` 21 | - [x] Link - works as expected 22 | - [x] `href` 23 | - [x] `title` 24 | - [x] `` 25 | - [x] Blockquote 26 | - [x] `` 27 | - content is HTML string though 28 | - [x] Heading 29 | - [x] `id` 30 | - [x] `` 31 | - [x] `{#custom-id}` (works with remark plugin) 32 | - `remark-custom-heading-id` didn't work in my case 33 | - `remark-heading-id` works, but in MDX one needs to escape curly-braces `\{#custom-id\}` 34 | - see https://github.com/withastro/roadmap/discussions/329 35 | 36 | ## Inspiration 37 | 38 | Currently overrides only supported for MDX, but not for markdown. [There is a proposal to support component overrides for markdown as well](https://github.com/withastro/roadmap/discussions/769). 39 | 40 | Ideally it should be as easy as [Hugo render hooks](https://gohugo.io/render-hooks/). 41 | 42 | ## Why does it matter? 43 | 44 | ### Case study 1: Aside in Starlight 45 | 46 | Starlight provides [`