@astrojs/markdoc 0.2.3 → 0.3.1

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 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
- ### Using components
100
+ ### Use Astro components as Markdoc tags
101
101
 
102
- You can add Astro components to your Markdoc using both [Markdoc tags][markdoc-tags] and HTML element [nodes][markdoc-nodes].
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
- #### Render Markdoc nodes / HTML elements as Astro components
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
- You may also want to map standard HTML elements like headings and paragraphs to components. For this, you can configure a custom [Markdoc node][markdoc-nodes]. This example overrides Markdoc's `heading` node to render a `Heading` component, and passes through Astro's default heading properties to define attributes and generate heading ids / slugs:
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. For headings, Astro provides the following attributes by default:
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
- #### Use client-side UI components
259
+ ### Use client-side UI components
173
260
 
174
- Today, the `components` prop does not support the `client:` directive for hydrating components. To embed client-side components, create a wrapper `.astro` file to import your component and apply a `client:` directive manually.
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` wrapper:
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 Aside from './src/components/Aside.astro';
279
+ import ClientAside from './src/components/ClientAside.astro';
193
280
 
194
281
  export default defineMarkdocConfig({
195
282
  tags: {
196
283
  aside: {
197
- render: Aside,
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,4 +1,5 @@
1
1
  ---
2
+ //! astro-head-inject
2
3
  import type { Config } from '@markdoc/markdoc';
3
4
  import Markdoc from '@markdoc/markdoc';
4
5
  import { ComponentNode, createTreeNode } from './TreeNode.js';
@@ -14,4 +15,4 @@ const ast = Markdoc.Ast.fromJSON(stringifiedAst);
14
15
  const content = Markdoc.transform(ast, config);
15
16
  ---
16
17
 
17
- <ComponentNode treeNode={createTreeNode(content)} />
18
+ <ComponentNode treeNode={await createTreeNode(content)} />
@@ -2,16 +2,30 @@ import type { AstroInstance } from 'astro';
2
2
  import { Fragment } from 'astro/jsx-runtime';
3
3
  import type { RenderableTreeNode } from '@markdoc/markdoc';
4
4
  import Markdoc from '@markdoc/markdoc';
5
- import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
5
+ import {
6
+ createComponent,
7
+ renderComponent,
8
+ render,
9
+ renderScriptElement,
10
+ renderUniqueStylesheet,
11
+ createHeadAndContent,
12
+ unescapeHTML,
13
+ renderTemplate,
14
+ HTMLString,
15
+ isHTMLString,
16
+ } from 'astro/runtime/server/index.js';
6
17
 
7
18
  export type TreeNode =
8
19
  | {
9
20
  type: 'text';
10
- content: string;
21
+ content: string | HTMLString;
11
22
  }
12
23
  | {
13
24
  type: 'component';
14
25
  component: AstroInstance['default'];
26
+ collectedLinks?: string[];
27
+ collectedStyles?: string[];
28
+ collectedScripts?: string[];
15
29
  props: Record<string, any>;
16
30
  children: TreeNode[];
17
31
  }
@@ -25,6 +39,7 @@ export type TreeNode =
25
39
  export const ComponentNode = createComponent({
26
40
  factory(result: any, { treeNode }: { treeNode: TreeNode }) {
27
41
  if (treeNode.type === 'text') return render`${treeNode.content}`;
42
+
28
43
  const slots = {
29
44
  default: () =>
30
45
  render`${treeNode.children.map((child) =>
@@ -32,37 +47,86 @@ export const ComponentNode = createComponent({
32
47
  )}`,
33
48
  };
34
49
  if (treeNode.type === 'component') {
35
- return renderComponent(
36
- result,
37
- treeNode.component.name,
38
- treeNode.component,
39
- treeNode.props,
40
- slots
50
+ let styles = '',
51
+ links = '',
52
+ scripts = '';
53
+ if (Array.isArray(treeNode.collectedStyles)) {
54
+ styles = treeNode.collectedStyles
55
+ .map((style: any) =>
56
+ renderUniqueStylesheet(result, {
57
+ type: 'inline',
58
+ content: style,
59
+ })
60
+ )
61
+ .join('');
62
+ }
63
+ if (Array.isArray(treeNode.collectedLinks)) {
64
+ links = treeNode.collectedLinks
65
+ .map((link: any) => {
66
+ return renderUniqueStylesheet(result, {
67
+ type: 'external',
68
+ src: link[0] === '/' ? link : '/' + link,
69
+ });
70
+ })
71
+ .join('');
72
+ }
73
+ if (Array.isArray(treeNode.collectedScripts)) {
74
+ scripts = treeNode.collectedScripts
75
+ .map((script: any) => renderScriptElement(script))
76
+ .join('');
77
+ }
78
+
79
+ const head = unescapeHTML(styles + links + scripts);
80
+
81
+ let headAndContent = createHeadAndContent(
82
+ head,
83
+ renderTemplate`${renderComponent(
84
+ result,
85
+ treeNode.component.name,
86
+ treeNode.component,
87
+ treeNode.props,
88
+ slots
89
+ )}`
90
+ );
91
+
92
+ // Let the runtime know that this component is being used.
93
+ result.propagators.set(
94
+ {},
95
+ {
96
+ init() {
97
+ return headAndContent;
98
+ },
99
+ }
41
100
  );
101
+
102
+ return headAndContent;
42
103
  }
43
104
  return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
44
105
  },
45
- propagation: 'none',
106
+ propagation: 'self',
46
107
  });
47
108
 
48
- export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode {
49
- if (typeof node === 'string' || typeof node === 'number') {
109
+ export async function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode {
110
+ if (isHTMLString(node)) {
111
+ return { type: 'text', content: node as HTMLString };
112
+ } else if (typeof node === 'string' || typeof node === 'number') {
50
113
  return { type: 'text', content: String(node) };
51
114
  } else if (Array.isArray(node)) {
52
115
  return {
53
116
  type: 'component',
54
117
  component: Fragment,
55
118
  props: {},
56
- children: node.map((child) => createTreeNode(child)),
119
+ children: await Promise.all(node.map((child) => createTreeNode(child))),
57
120
  };
58
121
  } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
59
122
  return { type: 'text', content: '' };
60
123
  }
61
124
 
125
+ const children = await Promise.all(node.children.map((child) => createTreeNode(child)));
126
+
62
127
  if (typeof node.name === 'function') {
63
128
  const component = node.name;
64
129
  const props = node.attributes;
65
- const children = node.children.map((child) => createTreeNode(child));
66
130
 
67
131
  return {
68
132
  type: 'component',
@@ -70,12 +134,38 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
70
134
  props,
71
135
  children,
72
136
  };
137
+ } else if (isPropagatedAssetsModule(node.name)) {
138
+ const { collectedStyles, collectedLinks, collectedScripts } = node.name;
139
+ const component = (await node.name.getMod())?.default ?? Fragment;
140
+ const props = node.attributes;
141
+
142
+ return {
143
+ type: 'component',
144
+ component,
145
+ collectedStyles,
146
+ collectedLinks,
147
+ collectedScripts,
148
+ props,
149
+ children,
150
+ };
73
151
  } else {
74
152
  return {
75
153
  type: 'element',
76
154
  tag: node.name,
77
155
  attributes: node.attributes,
78
- children: node.children.map((child) => createTreeNode(child)),
156
+ children,
79
157
  };
80
158
  }
81
159
  }
160
+
161
+ type PropagatedAssetsModule = {
162
+ __astroPropagation: true;
163
+ getMod: () => Promise<AstroInstance['default']>;
164
+ collectedStyles: string[];
165
+ collectedLinks: string[];
166
+ collectedScripts: string[];
167
+ };
168
+
169
+ function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule {
170
+ return typeof module === 'object' && module != null && '__astroPropagation' in module;
171
+ }
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: MarkdocConfig): MarkdocConfig;
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 { nodes as astroNodes } from "./nodes/index.js";
2
+ import { heading } from "./heading-ids.js";
3
3
  const Markdoc = _Markdoc;
4
- const nodes = { ...Markdoc.nodes, ...astroNodes };
4
+ const nodes = { ...Markdoc.nodes, heading };
5
5
  function defineMarkdocConfig(config) {
6
6
  return config;
7
7
  }
@@ -0,0 +1,2 @@
1
+ import { type AstroMarkdocConfig } from '../config.js';
2
+ export default function prism(): AstroMarkdocConfig;
@@ -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,3 @@
1
+ import type { ShikiConfig } from 'astro';
2
+ import type { AstroMarkdocConfig } from '../config.js';
3
+ export default function shiki({ langs, theme, wrap, }?: ShikiConfig): Promise<AstroMarkdocConfig>;
@@ -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 "../runtime.js";
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 = ((_b = (_a = config.nodes) == null ? void 0 : _a.heading) == null ? void 0 : _b.render) ?? `h${level}`;
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 === "function" ? { ...attributes, id: slug, __collectHeading: true, level } : { ...attributes, id: slug }
36
+ typeof render === "string" ? { ...attributes, id: slug } : { ...attributes, id: slug, __collectHeading: true, level }
31
37
  );
32
38
  return new Markdoc.Tag(render, tagProps, children);
33
39
  }
package/dist/index.js CHANGED
@@ -1,11 +1,26 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
2
  import fs from "node:fs";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
- import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from "./utils.js";
4
+ import {
5
+ createNameHash,
6
+ hasContentFlag,
7
+ isValidUrl,
8
+ MarkdocError,
9
+ parseFrontmatter,
10
+ prependForwardSlash,
11
+ PROPAGATED_ASSET_FLAG
12
+ } from "./utils.js";
5
13
  import { emitESMImage } from "astro/assets";
6
14
  import { bold, red, yellow } from "kleur/colors";
15
+ import path from "node:path";
16
+ import { normalizePath } from "vite";
7
17
  import { loadMarkdocConfig } from "./load-config.js";
8
18
  import { setupConfig } from "./runtime.js";
19
+ const markdocTokenizer = new Markdoc.Tokenizer({
20
+ // Strip <!-- comments --> from rendered output
21
+ // Without this, they're rendered as strings!
22
+ allowComments: true
23
+ });
9
24
  function markdocIntegration(legacyConfig) {
10
25
  if (legacyConfig) {
11
26
  console.log(
@@ -16,12 +31,20 @@ function markdocIntegration(legacyConfig) {
16
31
  process.exit(0);
17
32
  }
18
33
  let markdocConfigResult;
34
+ let markdocConfigResultId = "";
19
35
  return {
20
36
  name: "@astrojs/markdoc",
21
37
  hooks: {
22
38
  "astro:config:setup": async (params) => {
23
- const { config: astroConfig, addContentEntryType } = params;
39
+ const {
40
+ config: astroConfig,
41
+ updateConfig,
42
+ addContentEntryType
43
+ } = params;
24
44
  markdocConfigResult = await loadMarkdocConfig(astroConfig);
45
+ if (markdocConfigResult) {
46
+ markdocConfigResultId = normalizePath(fileURLToPath(markdocConfigResult.fileUrl));
47
+ }
25
48
  const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
26
49
  function getEntryInfo({ fileUrl, contents }) {
27
50
  const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
@@ -35,10 +58,16 @@ function markdocIntegration(legacyConfig) {
35
58
  addContentEntryType({
36
59
  extensions: [".mdoc"],
37
60
  getEntryInfo,
38
- async getRenderModule({ entry, viteId }) {
39
- const ast = Markdoc.parse(entry.body);
61
+ // Markdoc handles script / style propagation
62
+ // for Astro components internally
63
+ handlePropagation: false,
64
+ async getRenderModule({ contents, fileUrl, viteId }) {
65
+ const entry = getEntryInfo({ contents, fileUrl });
66
+ const tokens = markdocTokenizer.tokenize(entry.body);
67
+ const ast = Markdoc.parse(tokens);
40
68
  const pluginContext = this;
41
- const markdocConfig = setupConfig(userMarkdocConfig, entry);
69
+ const markdocConfig = await setupConfig(userMarkdocConfig);
70
+ const filePath = fileURLToPath(fileUrl);
42
71
  const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
43
72
  return (
44
73
  // Ignore `variable-undefined` errors.
@@ -48,10 +77,11 @@ function markdocIntegration(legacyConfig) {
48
77
  );
49
78
  });
50
79
  if (validationErrors.length) {
51
- const frontmatterBlockOffset = entry._internal.rawData.split("\n").length + 2;
80
+ const frontmatterBlockOffset = entry.rawData.split("\n").length + 2;
81
+ const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
52
82
  throw new MarkdocError({
53
83
  message: [
54
- `**${String(entry.collection)} \u2192 ${String(entry.id)}** contains invalid content:`,
84
+ `**${String(rootRelativePath)}** contains invalid content:`,
55
85
  ...validationErrors.map((e) => `- ${e.error.message}`)
56
86
  ].join("\n"),
57
87
  location: {
@@ -66,15 +96,17 @@ function markdocIntegration(legacyConfig) {
66
96
  await emitOptimizedImages(ast.children, {
67
97
  astroConfig,
68
98
  pluginContext,
69
- filePath: entry._internal.filePath
99
+ filePath
70
100
  });
71
101
  }
72
- const res = `import { jsx as h } from 'astro/jsx-runtime';
102
+ const res = `import {
103
+ createComponent,
104
+ renderComponent,
105
+ } from 'astro/runtime/server/index.js';
73
106
  import { Renderer } from '@astrojs/markdoc/components';
74
- import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime';
75
- import * as entry from ${JSON.stringify(viteId + "?astroContentCollectionEntry")};
107
+ import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
76
108
  ${markdocConfigResult ? `import _userConfig from ${JSON.stringify(
77
- markdocConfigResult.fileUrl.pathname
109
+ markdocConfigResultId
78
110
  )};
79
111
  const userConfig = _userConfig ?? {};` : "const userConfig = {};"}${astroConfig.experimental.assets ? `
80
112
  import { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';
@@ -89,19 +121,29 @@ export function getHeadings() {
89
121
  instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
90
122
  ""}
91
123
  const headingConfig = userConfig.nodes?.heading;
92
- const config = setupConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
124
+ const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {});
93
125
  const ast = Markdoc.Ast.fromJSON(stringifiedAst);
94
126
  const content = Markdoc.transform(ast, config);
95
127
  return collectHeadings(Array.isArray(content) ? content : content.children);
96
128
  }
97
- export async function Content (props) {
98
- const config = setupConfig({
99
- ...userConfig,
100
- variables: { ...userConfig.variables, ...props },
101
- }, entry);
102
129
 
103
- return h(Renderer, { config, stringifiedAst });
104
- }`;
130
+ export const Content = createComponent({
131
+ async factory(result, props) {
132
+ const config = await setupConfig({
133
+ ...userConfig,
134
+ variables: { ...userConfig.variables, ...props },
135
+ });
136
+
137
+ return renderComponent(
138
+ result,
139
+ Renderer.name,
140
+ Renderer,
141
+ { stringifiedAst, config },
142
+ {}
143
+ );
144
+ },
145
+ propagation: 'self',
146
+ });`;
105
147
  return { code: res };
106
148
  },
107
149
  contentModuleTypes: await fs.promises.readFile(
@@ -109,10 +151,53 @@ export async function Content (props) {
109
151
  "utf-8"
110
152
  )
111
153
  });
154
+ let rollupOptions = {};
155
+ if (markdocConfigResult) {
156
+ rollupOptions = {
157
+ output: {
158
+ // Split Astro components from your `markdoc.config`
159
+ // to only inject component styles and scripts at runtime.
160
+ manualChunks(id, { getModuleInfo }) {
161
+ var _a, _b;
162
+ if (markdocConfigResult && hasContentFlag(id, PROPAGATED_ASSET_FLAG) && ((_b = (_a = getModuleInfo(id)) == null ? void 0 : _a.importers) == null ? void 0 : _b.includes(markdocConfigResultId))) {
163
+ return createNameHash(id, [id]);
164
+ }
165
+ }
166
+ }
167
+ };
168
+ }
169
+ updateConfig({
170
+ vite: {
171
+ vite: {
172
+ ssr: {
173
+ external: ["@astrojs/markdoc/prism", "@astrojs/markdoc/shiki"]
174
+ }
175
+ },
176
+ build: {
177
+ rollupOptions
178
+ },
179
+ plugins: [
180
+ {
181
+ name: "@astrojs/markdoc:astro-propagated-assets",
182
+ enforce: "pre",
183
+ // Astro component styles and scripts should only be injected
184
+ // When a given Markdoc file actually uses that component.
185
+ // Add the `astroPropagatedAssets` flag to inject only when rendered.
186
+ resolveId(id, importer) {
187
+ if (importer === markdocConfigResultId && id.endsWith(".astro")) {
188
+ return this.resolve(id + "?astroPropagatedAssets", importer, {
189
+ skipSelf: true
190
+ });
191
+ }
192
+ }
193
+ }
194
+ ]
195
+ }
196
+ });
112
197
  },
113
198
  "astro:server:setup": async ({ server }) => {
114
199
  server.watcher.on("all", (event, entry) => {
115
- if (pathToFileURL(entry).pathname === (markdocConfigResult == null ? void 0 : markdocConfigResult.fileUrl.pathname)) {
200
+ if (prependForwardSlash(pathToFileURL(entry).pathname) === markdocConfigResultId) {
116
201
  console.log(
117
202
  yellow(
118
203
  `${bold("[Markdoc]")} Restart the dev server for config changes to take effect.`
package/dist/runtime.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import type { MarkdownHeading } from '@astrojs/markdown-remark';
2
- import { type ConfigType as MarkdocConfig, type RenderableTreeNode } from '@markdoc/markdoc';
3
- import type { ContentEntryModule } from 'astro';
2
+ import { type RenderableTreeNode } from '@markdoc/markdoc';
3
+ import type { AstroMarkdocConfig } from './config.js';
4
4
  /** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
5
5
  export { default as Markdoc } from '@markdoc/markdoc';
6
6
  /**
7
7
  * Merge user config with default config and set up context (ex. heading ID slugger)
8
- * Called on each file's individual transform
8
+ * Called on each file's individual transform.
9
+ * TODO: virtual module to merge configs per-build instead of per-file?
9
10
  */
10
- export declare function setupConfig(userConfig: MarkdocConfig, entry: ContentEntryModule): MarkdocConfig;
11
+ export declare function setupConfig(userConfig: AstroMarkdocConfig): Promise<Omit<AstroMarkdocConfig, 'extends'>>;
12
+ /** Used for synchronous `getHeadings()` function */
13
+ export declare function setupConfigSync(userConfig: AstroMarkdocConfig): Omit<AstroMarkdocConfig, 'extends'>;
11
14
  /**
12
15
  * Get text content as a string from a Markdoc transform AST
13
16
  */
package/dist/runtime.js CHANGED
@@ -1,18 +1,30 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
- import { setupHeadingConfig } from "./nodes/index.js";
2
+ import { setupHeadingConfig } from "./heading-ids.js";
3
3
  import { default as default2 } from "@markdoc/markdoc";
4
- function setupConfig(userConfig, entry) {
5
- const defaultConfig = {
6
- // `setupXConfig()` could become a "plugin" convention as well?
7
- ...setupHeadingConfig(),
8
- variables: { entry }
9
- };
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) {
17
+ const defaultConfig = setupHeadingConfig();
10
18
  return mergeConfig(defaultConfig, userConfig);
11
19
  }
12
20
  function mergeConfig(configA, configB) {
13
21
  return {
14
22
  ...configA,
15
23
  ...configB,
24
+ ctx: {
25
+ ...configA.ctx,
26
+ ...configB.ctx
27
+ },
16
28
  tags: {
17
29
  ...configA.tags,
18
30
  ...configB.tags
@@ -73,5 +85,6 @@ export {
73
85
  default2 as Markdoc,
74
86
  collectHeadings,
75
87
  getTextContent,
76
- setupConfig
88
+ setupConfig,
89
+ setupConfigSync
77
90
  };
package/dist/utils.d.ts CHANGED
@@ -37,4 +37,18 @@ interface ErrorProperties {
37
37
  */
38
38
  export declare function prependForwardSlash(str: string): string;
39
39
  export declare function isValidUrl(str: string): boolean;
40
+ /**
41
+ * Identifies Astro components with propagated assets
42
+ * @see 'packages/astro/src/content/consts.ts'
43
+ */
44
+ export declare const PROPAGATED_ASSET_FLAG = "astroPropagatedAssets";
45
+ /**
46
+ * @see 'packages/astro/src/content/utils.ts'
47
+ */
48
+ export declare function hasContentFlag(viteId: string, flag: string): boolean;
49
+ /**
50
+ * Create build hash for manual Rollup chunks.
51
+ * @see 'packages/astro/src/core/build/plugins/plugin-css.ts'
52
+ */
53
+ export declare function createNameHash(baseId: string, hashIds: string[]): string;
40
54
  export {};
package/dist/utils.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import matter from "gray-matter";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
2
4
  function parseFrontmatter(fileContents, filePath) {
3
5
  try {
4
6
  matter.clearCache();
@@ -52,8 +54,26 @@ function isValidUrl(str) {
52
54
  return false;
53
55
  }
54
56
  }
57
+ const PROPAGATED_ASSET_FLAG = "astroPropagatedAssets";
58
+ function hasContentFlag(viteId, flag) {
59
+ const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
60
+ return flags.has(flag);
61
+ }
62
+ function createNameHash(baseId, hashIds) {
63
+ const baseName = baseId ? path.parse(baseId).name : "index";
64
+ const hash = crypto.createHash("sha256");
65
+ for (const id of hashIds) {
66
+ hash.update(id, "utf-8");
67
+ }
68
+ const h = hash.digest("hex").slice(0, 8);
69
+ const proposedName = baseName + "." + h;
70
+ return proposedName;
71
+ }
55
72
  export {
56
73
  MarkdocError,
74
+ PROPAGATED_ASSET_FLAG,
75
+ createNameHash,
76
+ hasContentFlag,
57
77
  isValidUrl,
58
78
  parseFrontmatter,
59
79
  prependForwardSlash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/markdoc",
3
- "description": "Add support for Markdoc pages in your Astro site",
4
- "version": "0.2.3",
3
+ "description": "Add support for Markdoc in your Astro site",
4
+ "version": "0.3.1",
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
- "@markdoc/markdoc": "^0.2.2",
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.5"
47
+ "astro": "^2.5.7"
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.5",
60
+ "astro": "2.5.7",
57
61
  "astro-scripts": "0.0.14"
58
62
  },
59
63
  "engines": {
@@ -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 {};
@@ -1,4 +0,0 @@
1
- export { setupHeadingConfig } from './heading.js';
2
- export declare const nodes: {
3
- heading: import("@markdoc/markdoc").Schema;
4
- };
@@ -1,7 +0,0 @@
1
- import { heading } from "./heading.js";
2
- import { setupHeadingConfig } from "./heading.js";
3
- const nodes = { heading };
4
- export {
5
- nodes,
6
- setupHeadingConfig
7
- };