@astrojs/markdoc 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -29
- package/components/Renderer.astro +1 -2
- package/components/TreeNode.ts +18 -95
- package/dist/config.d.ts +6 -1
- package/dist/config.js +2 -2
- package/dist/extensions/prism.d.ts +2 -0
- package/dist/extensions/prism.js +21 -0
- package/dist/extensions/shiki.d.ts +3 -0
- package/dist/extensions/shiki.js +100 -0
- package/dist/heading-ids.d.ts +9 -0
- package/dist/{nodes/heading.js → heading-ids.js} +10 -4
- package/dist/index.js +26 -51
- package/dist/runtime.d.ts +7 -3
- package/dist/runtime.js +21 -5
- package/package.json +9 -5
- package/dist/nodes/heading.d.ts +0 -10
- package/dist/nodes/index.d.ts +0 -4
- package/dist/nodes/index.js +0 -7
package/README.md
CHANGED
|
@@ -97,13 +97,9 @@ const { Content } = await entry.render();
|
|
|
97
97
|
|
|
98
98
|
`@astrojs/markdoc` offers configuration options to use all of Markdoc's features and connect UI components to your content.
|
|
99
99
|
|
|
100
|
-
###
|
|
100
|
+
### Use Astro components as Markdoc tags
|
|
101
101
|
|
|
102
|
-
You can
|
|
103
|
-
|
|
104
|
-
#### Render Markdoc tags as Astro components
|
|
105
|
-
|
|
106
|
-
You may configure [Markdoc tags][markdoc-tags] that map to components. You can configure a new tag by creating a `markdoc.config.mjs|ts` file at the root of your project and configuring the `tag` attribute.
|
|
102
|
+
You can configure [Markdoc tags][markdoc-tags] that map to `.astro` components. You can add a new tag by creating a `markdoc.config.mjs|ts` file at the root of your project and configuring the `tag` attribute.
|
|
107
103
|
|
|
108
104
|
This example renders an `Aside` component, and allows a `type` prop to be passed as a string:
|
|
109
105
|
|
|
@@ -141,9 +137,11 @@ Use tags like this fancy "aside" to add some *flair* to your docs.
|
|
|
141
137
|
{% /aside %}
|
|
142
138
|
```
|
|
143
139
|
|
|
144
|
-
|
|
140
|
+
### Custom headings
|
|
141
|
+
|
|
142
|
+
`@astrojs/markdoc` automatically adds anchor links to your headings, and [generates a list of `headings` via the content collections API](https://docs.astro.build/en/guides/content-collections/#rendering-content-to-html). To further customize how headings are rendered, you can apply an Astro component [as a Markdoc node][markdoc-nodes].
|
|
145
143
|
|
|
146
|
-
|
|
144
|
+
This example renders a `Heading.astro` component using the `render` property:
|
|
147
145
|
|
|
148
146
|
```js
|
|
149
147
|
// markdoc.config.mjs
|
|
@@ -154,26 +152,115 @@ export default defineMarkdocConfig({
|
|
|
154
152
|
nodes: {
|
|
155
153
|
heading: {
|
|
156
154
|
render: Heading,
|
|
155
|
+
// Preserve default anchor link generation
|
|
157
156
|
...nodes.heading,
|
|
158
157
|
},
|
|
159
158
|
},
|
|
160
159
|
})
|
|
161
160
|
```
|
|
162
161
|
|
|
163
|
-
All Markdown headings will render the `Heading.astro` component and pass `attributes` as component props
|
|
162
|
+
All Markdown headings will render the `Heading.astro` component and pass the following `attributes` as component props:
|
|
164
163
|
|
|
165
164
|
- `level: number` The heading level 1 - 6
|
|
166
165
|
- `id: string` An `id` generated from the heading's text contents. This corresponds to the `slug` generated by the [content `render()` function](https://docs.astro.build/en/guides/content-collections/#rendering-content-to-html).
|
|
167
166
|
|
|
168
167
|
For example, the heading `### Level 3 heading!` will pass `level: 3` and `id: 'level-3-heading'` as component props.
|
|
169
168
|
|
|
169
|
+
### Syntax highlighting
|
|
170
|
+
|
|
171
|
+
`@astrojs/markdoc` provides [Shiki](https://github.com/shikijs/shiki) and [Prism](https://github.com/PrismJS) extensions to highlight your code blocks.
|
|
172
|
+
|
|
173
|
+
#### Shiki
|
|
174
|
+
|
|
175
|
+
Apply the `shiki()` extension to your Markdoc config using the `extends` property. You can optionally pass a shiki configuration object:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
// markdoc.config.mjs
|
|
179
|
+
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
|
180
|
+
import shiki from '@astrojs/markdoc/shiki';
|
|
181
|
+
|
|
182
|
+
export default defineMarkdocConfig({
|
|
183
|
+
extends: [
|
|
184
|
+
shiki({
|
|
185
|
+
// Choose from Shiki's built-in themes (or add your own)
|
|
186
|
+
// Default: 'github-dark'
|
|
187
|
+
// https://github.com/shikijs/shiki/blob/main/docs/themes.md
|
|
188
|
+
theme: 'dracula',
|
|
189
|
+
// Enable word wrap to prevent horizontal scrolling
|
|
190
|
+
// Default: false
|
|
191
|
+
wrap: true,
|
|
192
|
+
// Pass custom languages
|
|
193
|
+
// Note: Shiki has countless langs built-in, including `.astro`!
|
|
194
|
+
// https://github.com/shikijs/shiki/blob/main/docs/languages.md
|
|
195
|
+
langs: [],
|
|
196
|
+
})
|
|
197
|
+
],
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Prism
|
|
202
|
+
|
|
203
|
+
Apply the `prism()` extension to your Markdoc config using the `extends` property.
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
// markdoc.config.mjs
|
|
207
|
+
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
|
208
|
+
import prism from '@astrojs/markdoc/prism';
|
|
209
|
+
|
|
210
|
+
export default defineMarkdocConfig({
|
|
211
|
+
extends: [prism()],
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
📚 To learn about configuring Prism stylesheets, [see our syntax highlighting guide](https://docs.astro.build/en/guides/markdown-content/#prism-configuration).
|
|
216
|
+
|
|
217
|
+
### Set the root HTML element
|
|
218
|
+
|
|
219
|
+
Markdoc wraps documents with an `<article>` tag by default. This can be changed from the `document` Markdoc node. This accepts an HTML element name or `null` if you prefer to remove the wrapper element:
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
// markdoc.config.mjs
|
|
223
|
+
import { defineMarkdocConfig, nodes } from '@astrojs/markdoc/config';
|
|
224
|
+
|
|
225
|
+
export default defineMarkdocConfig({
|
|
226
|
+
nodes: {
|
|
227
|
+
document: {
|
|
228
|
+
render: null, // default 'article'
|
|
229
|
+
...nodes.document, // Apply defaults for other options
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Custom Markdoc nodes / elements
|
|
236
|
+
|
|
237
|
+
You may want to render standard Markdown elements, such as paragraphs and bolded text, as Astro components. For this, you can configure a [Markdoc node][markdoc-nodes]. If a given node receives attributes, they will be available as component props.
|
|
238
|
+
|
|
239
|
+
This example renders blockquotes with a custom `Quote.astro` component:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
// markdoc.config.mjs
|
|
243
|
+
import { defineMarkdocConfig, nodes } from '@astrojs/markdoc/config';
|
|
244
|
+
import Quote from './src/components/Quote.astro';
|
|
245
|
+
|
|
246
|
+
export default defineMarkdocConfig({
|
|
247
|
+
nodes: {
|
|
248
|
+
blockquote: {
|
|
249
|
+
render: Quote,
|
|
250
|
+
// Apply Markdoc's defaults for other options
|
|
251
|
+
...nodes.blockquote,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
170
257
|
📚 [Find all of Markdoc's built-in nodes and node attributes on their documentation.](https://markdoc.dev/docs/nodes#built-in-nodes)
|
|
171
258
|
|
|
172
|
-
|
|
259
|
+
### Use client-side UI components
|
|
173
260
|
|
|
174
|
-
|
|
261
|
+
Tags and nodes are restricted to `.astro` files. To embed client-side UI components in Markdoc, [use a wrapper `.astro` component that renders a framework component](/en/core-concepts/framework-components/#nesting-framework-components) with your desired `client:` directive.
|
|
175
262
|
|
|
176
|
-
This example wraps a `Aside.tsx` component with a `ClientAside.astro`
|
|
263
|
+
This example wraps a React `Aside.tsx` component with a `ClientAside.astro` component:
|
|
177
264
|
|
|
178
265
|
```astro
|
|
179
266
|
---
|
|
@@ -184,17 +271,17 @@ import Aside from './Aside';
|
|
|
184
271
|
<Aside {...Astro.props} client:load />
|
|
185
272
|
```
|
|
186
273
|
|
|
187
|
-
This component can be passed to the `render` prop for any [tag][markdoc-tags] or [node][markdoc-nodes] in your config:
|
|
274
|
+
This Astro component can now be passed to the `render` prop for any [tag][markdoc-tags] or [node][markdoc-nodes] in your config:
|
|
188
275
|
|
|
189
276
|
```js
|
|
190
277
|
// markdoc.config.mjs
|
|
191
278
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
|
192
|
-
import
|
|
279
|
+
import ClientAside from './src/components/ClientAside.astro';
|
|
193
280
|
|
|
194
281
|
export default defineMarkdocConfig({
|
|
195
282
|
tags: {
|
|
196
283
|
aside: {
|
|
197
|
-
render:
|
|
284
|
+
render: ClientAside,
|
|
198
285
|
attributes: {
|
|
199
286
|
type: { type: String },
|
|
200
287
|
}
|
|
@@ -203,20 +290,6 @@ export default defineMarkdocConfig({
|
|
|
203
290
|
})
|
|
204
291
|
```
|
|
205
292
|
|
|
206
|
-
### Access frontmatter and content collection information from your templates
|
|
207
|
-
|
|
208
|
-
You can access content collection information from your Markdoc templates using the `$entry` variable. This includes the entry `slug`, `collection` name, and frontmatter `data` parsed by your content collection schema (if any). This example renders the `title` frontmatter property as a heading:
|
|
209
|
-
|
|
210
|
-
```md
|
|
211
|
-
---
|
|
212
|
-
title: Welcome to Markdoc 👋
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
# {% $entry.data.title %}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
The `$entry` object matches [the `CollectionEntry` type](https://docs.astro.build/en/reference/api-reference/#collection-entry-type), excluding the `.render()` property.
|
|
219
|
-
|
|
220
293
|
### Markdoc config
|
|
221
294
|
|
|
222
295
|
The `markdoc.config.mjs|ts` file accepts [all Markdoc configuration options](https://markdoc.dev/docs/config), including [tags](https://markdoc.dev/docs/tags) and [functions](https://markdoc.dev/docs/functions).
|
|
@@ -292,6 +365,23 @@ export default defineMarkdocConfig({
|
|
|
292
365
|
})
|
|
293
366
|
```
|
|
294
367
|
|
|
368
|
+
### Access frontmatter from your Markdoc content
|
|
369
|
+
|
|
370
|
+
To access frontmatter, you can pass the entry `data` property [as a variable](#pass-markdoc-variables) where you render your content:
|
|
371
|
+
|
|
372
|
+
```astro
|
|
373
|
+
---
|
|
374
|
+
import { getEntry } from 'astro:content';
|
|
375
|
+
|
|
376
|
+
const entry = await getEntry('docs', 'why-markdoc');
|
|
377
|
+
const { Content } = await entry.render();
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
<Content frontmatter={entry.data} />
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
This can now be accessed as `$frontmatter` in your Markdoc.
|
|
384
|
+
|
|
295
385
|
## Examples
|
|
296
386
|
|
|
297
387
|
* The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
//! astro-head-inject
|
|
3
2
|
import type { Config } from '@markdoc/markdoc';
|
|
4
3
|
import Markdoc from '@markdoc/markdoc';
|
|
5
4
|
import { ComponentNode, createTreeNode } from './TreeNode.js';
|
|
@@ -15,4 +14,4 @@ const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
|
|
15
14
|
const content = Markdoc.transform(ast, config);
|
|
16
15
|
---
|
|
17
16
|
|
|
18
|
-
<ComponentNode treeNode={
|
|
17
|
+
<ComponentNode treeNode={createTreeNode(content)} />
|
package/components/TreeNode.ts
CHANGED
|
@@ -6,24 +6,18 @@ import {
|
|
|
6
6
|
createComponent,
|
|
7
7
|
renderComponent,
|
|
8
8
|
render,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
createHeadAndContent,
|
|
12
|
-
unescapeHTML,
|
|
13
|
-
renderTemplate,
|
|
9
|
+
HTMLString,
|
|
10
|
+
isHTMLString,
|
|
14
11
|
} from 'astro/runtime/server/index.js';
|
|
15
12
|
|
|
16
13
|
export type TreeNode =
|
|
17
14
|
| {
|
|
18
15
|
type: 'text';
|
|
19
|
-
content: string;
|
|
16
|
+
content: string | HTMLString;
|
|
20
17
|
}
|
|
21
18
|
| {
|
|
22
19
|
type: 'component';
|
|
23
20
|
component: AstroInstance['default'];
|
|
24
|
-
collectedLinks?: string[];
|
|
25
|
-
collectedStyles?: string[];
|
|
26
|
-
collectedScripts?: string[];
|
|
27
21
|
props: Record<string, any>;
|
|
28
22
|
children: TreeNode[];
|
|
29
23
|
}
|
|
@@ -37,6 +31,7 @@ export type TreeNode =
|
|
|
37
31
|
export const ComponentNode = createComponent({
|
|
38
32
|
factory(result: any, { treeNode }: { treeNode: TreeNode }) {
|
|
39
33
|
if (treeNode.type === 'text') return render`${treeNode.content}`;
|
|
34
|
+
|
|
40
35
|
const slots = {
|
|
41
36
|
default: () =>
|
|
42
37
|
render`${treeNode.children.map((child) =>
|
|
@@ -44,85 +39,39 @@ export const ComponentNode = createComponent({
|
|
|
44
39
|
)}`,
|
|
45
40
|
};
|
|
46
41
|
if (treeNode.type === 'component') {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
renderUniqueStylesheet({
|
|
54
|
-
type: 'inline',
|
|
55
|
-
content: style,
|
|
56
|
-
})
|
|
57
|
-
)
|
|
58
|
-
.join('');
|
|
59
|
-
}
|
|
60
|
-
if (Array.isArray(treeNode.collectedLinks)) {
|
|
61
|
-
links = treeNode.collectedLinks
|
|
62
|
-
.map((link: any) => {
|
|
63
|
-
return renderUniqueStylesheet(result, {
|
|
64
|
-
href: link[0] === '/' ? link : '/' + link,
|
|
65
|
-
});
|
|
66
|
-
})
|
|
67
|
-
.join('');
|
|
68
|
-
}
|
|
69
|
-
if (Array.isArray(treeNode.collectedScripts)) {
|
|
70
|
-
scripts = treeNode.collectedScripts
|
|
71
|
-
.map((script: any) => renderScriptElement(script))
|
|
72
|
-
.join('');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const head = unescapeHTML(styles + links + scripts);
|
|
76
|
-
|
|
77
|
-
let headAndContent = createHeadAndContent(
|
|
78
|
-
head,
|
|
79
|
-
renderTemplate`${renderComponent(
|
|
80
|
-
result,
|
|
81
|
-
treeNode.component.name,
|
|
82
|
-
treeNode.component,
|
|
83
|
-
treeNode.props,
|
|
84
|
-
slots
|
|
85
|
-
)}`
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// Let the runtime know that this component is being used.
|
|
89
|
-
result.propagators.set(
|
|
90
|
-
{},
|
|
91
|
-
{
|
|
92
|
-
init() {
|
|
93
|
-
return headAndContent;
|
|
94
|
-
},
|
|
95
|
-
}
|
|
42
|
+
return renderComponent(
|
|
43
|
+
result,
|
|
44
|
+
treeNode.component.name,
|
|
45
|
+
treeNode.component,
|
|
46
|
+
treeNode.props,
|
|
47
|
+
slots
|
|
96
48
|
);
|
|
97
|
-
|
|
98
|
-
return headAndContent;
|
|
99
49
|
}
|
|
100
50
|
return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
|
|
101
51
|
},
|
|
102
|
-
propagation: '
|
|
52
|
+
propagation: 'none',
|
|
103
53
|
});
|
|
104
54
|
|
|
105
|
-
export
|
|
106
|
-
node
|
|
107
|
-
|
|
108
|
-
if (typeof node === 'string' || typeof node === 'number') {
|
|
55
|
+
export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode {
|
|
56
|
+
if (isHTMLString(node)) {
|
|
57
|
+
return { type: 'text', content: node as HTMLString };
|
|
58
|
+
} else if (typeof node === 'string' || typeof node === 'number') {
|
|
109
59
|
return { type: 'text', content: String(node) };
|
|
110
60
|
} else if (Array.isArray(node)) {
|
|
111
61
|
return {
|
|
112
62
|
type: 'component',
|
|
113
63
|
component: Fragment,
|
|
114
64
|
props: {},
|
|
115
|
-
children:
|
|
65
|
+
children: node.map((child) => createTreeNode(child)),
|
|
116
66
|
};
|
|
117
67
|
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
|
118
68
|
return { type: 'text', content: '' };
|
|
119
69
|
}
|
|
120
70
|
|
|
121
|
-
const children = await Promise.all(node.children.map((child) => createTreeNode(child)));
|
|
122
|
-
|
|
123
71
|
if (typeof node.name === 'function') {
|
|
124
72
|
const component = node.name;
|
|
125
73
|
const props = node.attributes;
|
|
74
|
+
const children = node.children.map((child) => createTreeNode(child));
|
|
126
75
|
|
|
127
76
|
return {
|
|
128
77
|
type: 'component',
|
|
@@ -130,38 +79,12 @@ export async function createTreeNode(
|
|
|
130
79
|
props,
|
|
131
80
|
children,
|
|
132
81
|
};
|
|
133
|
-
} else if (isPropagatedAssetsModule(node.name)) {
|
|
134
|
-
const { collectedStyles, collectedLinks, collectedScripts } = node.name;
|
|
135
|
-
const component = (await node.name.getMod())?.default ?? Fragment;
|
|
136
|
-
const props = node.attributes;
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
type: 'component',
|
|
140
|
-
component,
|
|
141
|
-
collectedStyles,
|
|
142
|
-
collectedLinks,
|
|
143
|
-
collectedScripts,
|
|
144
|
-
props,
|
|
145
|
-
children,
|
|
146
|
-
};
|
|
147
82
|
} else {
|
|
148
83
|
return {
|
|
149
84
|
type: 'element',
|
|
150
85
|
tag: node.name,
|
|
151
86
|
attributes: node.attributes,
|
|
152
|
-
children,
|
|
87
|
+
children: node.children.map((child) => createTreeNode(child)),
|
|
153
88
|
};
|
|
154
89
|
}
|
|
155
90
|
}
|
|
156
|
-
|
|
157
|
-
type PropagatedAssetsModule = {
|
|
158
|
-
__astroPropagation: true;
|
|
159
|
-
getMod: () => Promise<AstroInstance['default']>;
|
|
160
|
-
collectedStyles: string[];
|
|
161
|
-
collectedLinks: string[];
|
|
162
|
-
collectedScripts: string[];
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule {
|
|
166
|
-
return typeof module === 'object' && module != null && '__astroPropagation' in module;
|
|
167
|
-
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc';
|
|
2
2
|
import _Markdoc from '@markdoc/markdoc';
|
|
3
|
+
export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = MarkdocConfig & {
|
|
4
|
+
ctx?: C;
|
|
5
|
+
extends?: ResolvedAstroMarkdocConfig[];
|
|
6
|
+
};
|
|
7
|
+
export type ResolvedAstroMarkdocConfig = Omit<AstroMarkdocConfig, 'extends'>;
|
|
3
8
|
export declare const Markdoc: typeof _Markdoc;
|
|
4
9
|
export declare const nodes: {
|
|
5
10
|
heading: import("@markdoc/markdoc").Schema;
|
|
@@ -37,4 +42,4 @@ export declare const nodes: {
|
|
|
37
42
|
error: {};
|
|
38
43
|
node: {};
|
|
39
44
|
};
|
|
40
|
-
export declare function defineMarkdocConfig(config:
|
|
45
|
+
export declare function defineMarkdocConfig(config: AstroMarkdocConfig): AstroMarkdocConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import _Markdoc from "@markdoc/markdoc";
|
|
2
|
-
import {
|
|
2
|
+
import { heading } from "./heading-ids.js";
|
|
3
3
|
const Markdoc = _Markdoc;
|
|
4
|
-
const nodes = { ...Markdoc.nodes,
|
|
4
|
+
const nodes = { ...Markdoc.nodes, heading };
|
|
5
5
|
function defineMarkdocConfig(config) {
|
|
6
6
|
return config;
|
|
7
7
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { unescapeHTML } from "astro/runtime/server/index.js";
|
|
2
|
+
import { runHighlighterWithAstro } from "@astrojs/prism/dist/highlighter";
|
|
3
|
+
import { Markdoc } from "../config.js";
|
|
4
|
+
function prism() {
|
|
5
|
+
return {
|
|
6
|
+
nodes: {
|
|
7
|
+
fence: {
|
|
8
|
+
attributes: Markdoc.nodes.fence.attributes,
|
|
9
|
+
transform({ attributes: { language, content } }) {
|
|
10
|
+
const { html, classLanguage } = runHighlighterWithAstro(language, content);
|
|
11
|
+
return unescapeHTML(
|
|
12
|
+
`<pre class="${classLanguage}"><code class="${classLanguage}">${html}</code></pre>`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
prism as default
|
|
21
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { unescapeHTML } from "astro/runtime/server/index.js";
|
|
2
|
+
import Markdoc from "@markdoc/markdoc";
|
|
3
|
+
import { getHighlighter } from "shiki";
|
|
4
|
+
const compatThemes = {
|
|
5
|
+
"material-darker": "material-theme-darker",
|
|
6
|
+
"material-default": "material-theme",
|
|
7
|
+
"material-lighter": "material-theme-lighter",
|
|
8
|
+
"material-ocean": "material-theme-ocean",
|
|
9
|
+
"material-palenight": "material-theme-palenight"
|
|
10
|
+
};
|
|
11
|
+
const normalizeTheme = (theme) => {
|
|
12
|
+
if (typeof theme === "string") {
|
|
13
|
+
return compatThemes[theme] || theme;
|
|
14
|
+
} else if (compatThemes[theme.name]) {
|
|
15
|
+
return { ...theme, name: compatThemes[theme.name] };
|
|
16
|
+
} else {
|
|
17
|
+
return theme;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const ASTRO_COLOR_REPLACEMENTS = {
|
|
21
|
+
"#000001": "var(--astro-code-color-text)",
|
|
22
|
+
"#000002": "var(--astro-code-color-background)",
|
|
23
|
+
"#000004": "var(--astro-code-token-constant)",
|
|
24
|
+
"#000005": "var(--astro-code-token-string)",
|
|
25
|
+
"#000006": "var(--astro-code-token-comment)",
|
|
26
|
+
"#000007": "var(--astro-code-token-keyword)",
|
|
27
|
+
"#000008": "var(--astro-code-token-parameter)",
|
|
28
|
+
"#000009": "var(--astro-code-token-function)",
|
|
29
|
+
"#000010": "var(--astro-code-token-string-expression)",
|
|
30
|
+
"#000011": "var(--astro-code-token-punctuation)",
|
|
31
|
+
"#000012": "var(--astro-code-token-link)"
|
|
32
|
+
};
|
|
33
|
+
const PRE_SELECTOR = /<pre class="(.*?)shiki(.*?)"/;
|
|
34
|
+
const LINE_SELECTOR = /<span class="line"><span style="(.*?)">([\+|\-])/g;
|
|
35
|
+
const INLINE_STYLE_SELECTOR = /style="(.*?)"/;
|
|
36
|
+
const highlighterCache = /* @__PURE__ */ new Map();
|
|
37
|
+
async function shiki({
|
|
38
|
+
langs = [],
|
|
39
|
+
theme = "github-dark",
|
|
40
|
+
wrap = false
|
|
41
|
+
} = {}) {
|
|
42
|
+
theme = normalizeTheme(theme);
|
|
43
|
+
const cacheID = typeof theme === "string" ? theme : theme.name;
|
|
44
|
+
if (!highlighterCache.has(cacheID)) {
|
|
45
|
+
highlighterCache.set(
|
|
46
|
+
cacheID,
|
|
47
|
+
await getHighlighter({ theme }).then((hl) => {
|
|
48
|
+
hl.setColorReplacements(ASTRO_COLOR_REPLACEMENTS);
|
|
49
|
+
return hl;
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const highlighter = highlighterCache.get(cacheID);
|
|
54
|
+
for (const lang of langs) {
|
|
55
|
+
await highlighter.loadLanguage(lang);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
nodes: {
|
|
59
|
+
fence: {
|
|
60
|
+
attributes: Markdoc.nodes.fence.attributes,
|
|
61
|
+
transform({ attributes }) {
|
|
62
|
+
let lang;
|
|
63
|
+
if (typeof attributes.language === "string") {
|
|
64
|
+
const langExists = highlighter.getLoadedLanguages().includes(attributes.language);
|
|
65
|
+
if (langExists) {
|
|
66
|
+
lang = attributes.language;
|
|
67
|
+
} else {
|
|
68
|
+
console.warn(
|
|
69
|
+
`[Shiki highlighter] The language "${attributes.language}" doesn't exist, falling back to plaintext.`
|
|
70
|
+
);
|
|
71
|
+
lang = "plaintext";
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
lang = "plaintext";
|
|
75
|
+
}
|
|
76
|
+
let html = highlighter.codeToHtml(attributes.content, { lang });
|
|
77
|
+
html = html.replace(PRE_SELECTOR, `<pre class="$1astro-code$2"`);
|
|
78
|
+
if (attributes.language === "diff") {
|
|
79
|
+
html = html.replace(
|
|
80
|
+
LINE_SELECTOR,
|
|
81
|
+
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (wrap === false) {
|
|
85
|
+
html = html.replace(INLINE_STYLE_SELECTOR, 'style="$1; overflow-x: auto;"');
|
|
86
|
+
} else if (wrap === true) {
|
|
87
|
+
html = html.replace(
|
|
88
|
+
INLINE_STYLE_SELECTOR,
|
|
89
|
+
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return unescapeHTML(html);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
shiki as default
|
|
100
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Schema } from '@markdoc/markdoc';
|
|
2
|
+
import Slugger from 'github-slugger';
|
|
3
|
+
import type { AstroMarkdocConfig } from './config.js';
|
|
4
|
+
type HeadingIdConfig = AstroMarkdocConfig<{
|
|
5
|
+
headingSlugger: Slugger;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const heading: Schema;
|
|
8
|
+
export declare function setupHeadingConfig(): HeadingIdConfig;
|
|
9
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Markdoc from "@markdoc/markdoc";
|
|
2
2
|
import Slugger from "github-slugger";
|
|
3
|
-
import { getTextContent } from "
|
|
3
|
+
import { getTextContent } from "./runtime.js";
|
|
4
|
+
import { MarkdocError } from "./utils.js";
|
|
4
5
|
function getSlug(attributes, children, headingSlugger) {
|
|
5
6
|
if (attributes.id && typeof attributes.id === "string") {
|
|
6
7
|
return attributes.id;
|
|
@@ -18,16 +19,21 @@ const heading = {
|
|
|
18
19
|
level: { type: Number, required: true, default: 1 }
|
|
19
20
|
},
|
|
20
21
|
transform(node, config) {
|
|
21
|
-
var _a, _b;
|
|
22
|
+
var _a, _b, _c;
|
|
22
23
|
const { level, ...attributes } = node.transformAttributes(config);
|
|
23
24
|
const children = node.transformChildren(config);
|
|
25
|
+
if (!((_a = config.ctx) == null ? void 0 : _a.headingSlugger)) {
|
|
26
|
+
throw new MarkdocError({
|
|
27
|
+
message: "Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
24
30
|
const slug = getSlug(attributes, children, config.ctx.headingSlugger);
|
|
25
|
-
const render = ((
|
|
31
|
+
const render = ((_c = (_b = config.nodes) == null ? void 0 : _b.heading) == null ? void 0 : _c.render) ?? `h${level}`;
|
|
26
32
|
const tagProps = (
|
|
27
33
|
// For components, pass down `level` as a prop,
|
|
28
34
|
// alongside `__collectHeading` for our `headings` collector.
|
|
29
35
|
// Avoid accidentally rendering `level` as an HTML attribute otherwise!
|
|
30
|
-
typeof render === "
|
|
36
|
+
typeof render === "function" ? { ...attributes, id: slug, __collectHeading: true, level } : { ...attributes, id: slug }
|
|
31
37
|
);
|
|
32
38
|
return new Markdoc.Tag(render, tagProps, children);
|
|
33
39
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
4
4
|
import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from "./utils.js";
|
|
5
5
|
import { emitESMImage } from "astro/assets";
|
|
6
6
|
import { bold, red, yellow } from "kleur/colors";
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import { loadMarkdocConfig } from "./load-config.js";
|
|
8
9
|
import { setupConfig } from "./runtime.js";
|
|
9
10
|
function markdocIntegration(legacyConfig) {
|
|
@@ -25,6 +26,13 @@ function markdocIntegration(legacyConfig) {
|
|
|
25
26
|
updateConfig,
|
|
26
27
|
addContentEntryType
|
|
27
28
|
} = params;
|
|
29
|
+
updateConfig({
|
|
30
|
+
vite: {
|
|
31
|
+
ssr: {
|
|
32
|
+
external: ["@astrojs/markdoc/prism", "@astrojs/markdoc/shiki"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
28
36
|
markdocConfigResult = await loadMarkdocConfig(astroConfig);
|
|
29
37
|
const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
|
|
30
38
|
function getEntryInfo({ fileUrl, contents }) {
|
|
@@ -39,13 +47,12 @@ function markdocIntegration(legacyConfig) {
|
|
|
39
47
|
addContentEntryType({
|
|
40
48
|
extensions: [".mdoc"],
|
|
41
49
|
getEntryInfo,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
handlePropagation: false,
|
|
45
|
-
async getRenderModule({ entry, viteId }) {
|
|
50
|
+
async getRenderModule({ contents, fileUrl, viteId }) {
|
|
51
|
+
const entry = getEntryInfo({ contents, fileUrl });
|
|
46
52
|
const ast = Markdoc.parse(entry.body);
|
|
47
53
|
const pluginContext = this;
|
|
48
|
-
const markdocConfig = setupConfig(userMarkdocConfig
|
|
54
|
+
const markdocConfig = await setupConfig(userMarkdocConfig);
|
|
55
|
+
const filePath = fileURLToPath(fileUrl);
|
|
49
56
|
const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
|
|
50
57
|
return (
|
|
51
58
|
// Ignore `variable-undefined` errors.
|
|
@@ -55,10 +62,11 @@ function markdocIntegration(legacyConfig) {
|
|
|
55
62
|
);
|
|
56
63
|
});
|
|
57
64
|
if (validationErrors.length) {
|
|
58
|
-
const frontmatterBlockOffset = entry.
|
|
65
|
+
const frontmatterBlockOffset = entry.rawData.split("\n").length + 2;
|
|
66
|
+
const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
|
|
59
67
|
throw new MarkdocError({
|
|
60
68
|
message: [
|
|
61
|
-
`**${String(
|
|
69
|
+
`**${String(rootRelativePath)}** contains invalid content:`,
|
|
62
70
|
...validationErrors.map((e) => `- ${e.error.message}`)
|
|
63
71
|
].join("\n"),
|
|
64
72
|
location: {
|
|
@@ -73,15 +81,12 @@ function markdocIntegration(legacyConfig) {
|
|
|
73
81
|
await emitOptimizedImages(ast.children, {
|
|
74
82
|
astroConfig,
|
|
75
83
|
pluginContext,
|
|
76
|
-
filePath
|
|
84
|
+
filePath
|
|
77
85
|
});
|
|
78
86
|
}
|
|
79
|
-
const res = `import {
|
|
80
|
-
createComponent,
|
|
81
|
-
renderComponent,
|
|
82
|
-
} from 'astro/runtime/server/index.js';
|
|
87
|
+
const res = `import { jsx as h } from 'astro/jsx-runtime';
|
|
83
88
|
import { Renderer } from '@astrojs/markdoc/components';
|
|
84
|
-
import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime';
|
|
89
|
+
import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
|
|
85
90
|
import * as entry from ${JSON.stringify(viteId + "?astroContentCollectionEntry")};
|
|
86
91
|
${markdocConfigResult ? `import _userConfig from ${JSON.stringify(
|
|
87
92
|
markdocConfigResult.fileUrl.pathname
|
|
@@ -99,29 +104,19 @@ export function getHeadings() {
|
|
|
99
104
|
instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
|
|
100
105
|
""}
|
|
101
106
|
const headingConfig = userConfig.nodes?.heading;
|
|
102
|
-
const config =
|
|
107
|
+
const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
|
|
103
108
|
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
|
104
109
|
const content = Markdoc.transform(ast, config);
|
|
105
110
|
return collectHeadings(Array.isArray(content) ? content : content.children);
|
|
106
111
|
}
|
|
112
|
+
export async function Content (props) {
|
|
113
|
+
const config = await setupConfig({
|
|
114
|
+
...userConfig,
|
|
115
|
+
variables: { ...userConfig.variables, ...props },
|
|
116
|
+
}, entry);
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const config = setupConfig({
|
|
111
|
-
...userConfig,
|
|
112
|
-
variables: { ...userConfig.variables, ...props },
|
|
113
|
-
}, entry);
|
|
114
|
-
|
|
115
|
-
return renderComponent(
|
|
116
|
-
result,
|
|
117
|
-
Renderer.name,
|
|
118
|
-
Renderer,
|
|
119
|
-
{ stringifiedAst, config },
|
|
120
|
-
{}
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
propagation: 'self',
|
|
124
|
-
});`;
|
|
118
|
+
return h(Renderer, { config, stringifiedAst });
|
|
119
|
+
}`;
|
|
125
120
|
return { code: res };
|
|
126
121
|
},
|
|
127
122
|
contentModuleTypes: await fs.promises.readFile(
|
|
@@ -129,26 +124,6 @@ export const Content = createComponent({
|
|
|
129
124
|
"utf-8"
|
|
130
125
|
)
|
|
131
126
|
});
|
|
132
|
-
updateConfig({
|
|
133
|
-
vite: {
|
|
134
|
-
plugins: [
|
|
135
|
-
{
|
|
136
|
-
name: "@astrojs/markdoc:astro-propagated-assets",
|
|
137
|
-
enforce: "pre",
|
|
138
|
-
// Astro component styles and scripts should only be injected
|
|
139
|
-
// When a given Markdoc file actually uses that component.
|
|
140
|
-
// Add the `astroPropagatedAssets` flag to inject only when rendered.
|
|
141
|
-
resolveId(id, importer) {
|
|
142
|
-
if (importer === (markdocConfigResult == null ? void 0 : markdocConfigResult.fileUrl.pathname) && id.endsWith(".astro")) {
|
|
143
|
-
return this.resolve(id + "?astroPropagatedAssets", importer, {
|
|
144
|
-
skipSelf: true
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
127
|
},
|
|
153
128
|
"astro:server:setup": async ({ server }) => {
|
|
154
129
|
server.watcher.on("all", (event, entry) => {
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { MarkdownHeading } from '@astrojs/markdown-remark';
|
|
2
|
-
import { type
|
|
2
|
+
import { type RenderableTreeNode } from '@markdoc/markdoc';
|
|
3
3
|
import type { ContentEntryModule } from 'astro';
|
|
4
|
+
import type { AstroMarkdocConfig } from './config.js';
|
|
4
5
|
/** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
|
|
5
6
|
export { default as Markdoc } from '@markdoc/markdoc';
|
|
6
7
|
/**
|
|
7
8
|
* Merge user config with default config and set up context (ex. heading ID slugger)
|
|
8
|
-
* Called on each file's individual transform
|
|
9
|
+
* Called on each file's individual transform.
|
|
10
|
+
* TODO: virtual module to merge configs per-build instead of per-file?
|
|
9
11
|
*/
|
|
10
|
-
export declare function setupConfig(userConfig:
|
|
12
|
+
export declare function setupConfig(userConfig: AstroMarkdocConfig): Promise<Omit<AstroMarkdocConfig, 'extends'>>;
|
|
13
|
+
/** Used for synchronous `getHeadings()` function */
|
|
14
|
+
export declare function setupConfigSync(userConfig: AstroMarkdocConfig, entry: ContentEntryModule): Omit<AstroMarkdocConfig, 'extends'>;
|
|
11
15
|
/**
|
|
12
16
|
* Get text content as a string from a Markdoc transform AST
|
|
13
17
|
*/
|
package/dist/runtime.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import Markdoc from "@markdoc/markdoc";
|
|
2
|
-
import { setupHeadingConfig } from "./
|
|
2
|
+
import { setupHeadingConfig } from "./heading-ids.js";
|
|
3
3
|
import { default as default2 } from "@markdoc/markdoc";
|
|
4
|
-
function setupConfig(userConfig
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
async function setupConfig(userConfig) {
|
|
5
|
+
let defaultConfig = setupHeadingConfig();
|
|
6
|
+
if (userConfig.extends) {
|
|
7
|
+
for (let extension of userConfig.extends) {
|
|
8
|
+
if (extension instanceof Promise) {
|
|
9
|
+
extension = await extension;
|
|
10
|
+
}
|
|
11
|
+
defaultConfig = mergeConfig(defaultConfig, extension);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return mergeConfig(defaultConfig, userConfig);
|
|
15
|
+
}
|
|
16
|
+
function setupConfigSync(userConfig, entry) {
|
|
17
|
+
let defaultConfig = {
|
|
7
18
|
...setupHeadingConfig(),
|
|
8
19
|
variables: { entry }
|
|
9
20
|
};
|
|
@@ -13,6 +24,10 @@ function mergeConfig(configA, configB) {
|
|
|
13
24
|
return {
|
|
14
25
|
...configA,
|
|
15
26
|
...configB,
|
|
27
|
+
ctx: {
|
|
28
|
+
...configA.ctx,
|
|
29
|
+
...configB.ctx
|
|
30
|
+
},
|
|
16
31
|
tags: {
|
|
17
32
|
...configA.tags,
|
|
18
33
|
...configB.tags
|
|
@@ -73,5 +88,6 @@ export {
|
|
|
73
88
|
default2 as Markdoc,
|
|
74
89
|
collectHeadings,
|
|
75
90
|
getTextContent,
|
|
76
|
-
setupConfig
|
|
91
|
+
setupConfig,
|
|
92
|
+
setupConfigSync
|
|
77
93
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/markdoc",
|
|
3
|
-
"description": "Add support for Markdoc
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Add support for Markdoc in your Astro site",
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "withastro",
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"bugs": "https://github.com/withastro/astro/issues",
|
|
20
20
|
"homepage": "https://docs.astro.build/en/guides/integrations-guide/markdoc/",
|
|
21
21
|
"exports": {
|
|
22
|
+
"./prism": "./dist/extensions/prism.js",
|
|
23
|
+
"./shiki": "./dist/extensions/shiki.js",
|
|
22
24
|
".": "./dist/index.js",
|
|
23
25
|
"./components": "./components/index.ts",
|
|
24
26
|
"./runtime": "./dist/runtime.js",
|
|
@@ -32,7 +34,9 @@
|
|
|
32
34
|
"template"
|
|
33
35
|
],
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"
|
|
37
|
+
"shiki": "^0.14.1",
|
|
38
|
+
"@astrojs/prism": "^2.1.2",
|
|
39
|
+
"@markdoc/markdoc": "^0.3.0",
|
|
36
40
|
"esbuild": "^0.17.12",
|
|
37
41
|
"github-slugger": "^2.0.0",
|
|
38
42
|
"gray-matter": "^4.0.3",
|
|
@@ -40,7 +44,7 @@
|
|
|
40
44
|
"zod": "^3.17.3"
|
|
41
45
|
},
|
|
42
46
|
"peerDependencies": {
|
|
43
|
-
"astro": "^2.5.
|
|
47
|
+
"astro": "^2.5.6"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"@astrojs/markdown-remark": "^2.2.1",
|
|
@@ -53,7 +57,7 @@
|
|
|
53
57
|
"mocha": "^9.2.2",
|
|
54
58
|
"rollup": "^3.20.1",
|
|
55
59
|
"vite": "^4.3.1",
|
|
56
|
-
"astro": "2.5.
|
|
60
|
+
"astro": "2.5.6",
|
|
57
61
|
"astro-scripts": "0.0.14"
|
|
58
62
|
},
|
|
59
63
|
"engines": {
|
package/dist/nodes/heading.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type ConfigType, type Schema } from '@markdoc/markdoc';
|
|
2
|
-
import Slugger from 'github-slugger';
|
|
3
|
-
type ConfigTypeWithCtx = ConfigType & {
|
|
4
|
-
ctx: {
|
|
5
|
-
headingSlugger: Slugger;
|
|
6
|
-
};
|
|
7
|
-
};
|
|
8
|
-
export declare const heading: Schema;
|
|
9
|
-
export declare function setupHeadingConfig(): ConfigTypeWithCtx;
|
|
10
|
-
export {};
|
package/dist/nodes/index.d.ts
DELETED