[Markdoc] New config format with runtime variable support! (#6653)
* deps: esbuild * feat: support direct component imports for render! * deps: add devalue back * refactor: remove unused components prop * refactor: load experimental assets config separately * fix: upate Content type def to support props * refactor: replace astro stub with inline data * feat: pass through viteId to getRenderMod * fix: add back $entry var with defaults convention * chore: remove unneeded validateRenderProps * chore: remove uneeded validateComponents * fix: remove userMarkdocConfig prop * chore: add helpful error for legacy config * deps: kleur * fix: add back `isCapitalized` * fix: log instead of throw to avoid scary stacktrace * chore: delete more old logic (nice) * chore: delete MORE unused utils * chore: comment on separate assets config * chore: remove console.log * chore: general code cleanup * test: new render config * docs: new README * fix: add expect-error on astro:assets * feat: add defineMarkdocConfig helper * docs: update example README * test: add runtime variable * chore: lint * chore: changeset * chore: add component import deletion * docs: add notes on Vite fork * fix: astro check * chore: add `.mts` to markdoc config formats
This commit is contained in:
parent
c13d428a78
commit
7c439868a3
44 changed files with 734 additions and 633 deletions
42
.changeset/metal-cameras-bow.md
Normal file
42
.changeset/metal-cameras-bow.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
'@astrojs/markdoc': minor
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Simplify Markdoc configuration with a new `markdoc.config.mjs` file. This lets you import Astro components directly to render as Markdoc tags and nodes, without the need for the previous `components` property. This new configuration also unlocks passing variables to your Markdoc from the `Content` component ([see the new docs](https://docs.astro.build/en/guides/integrations-guide/markdoc/#pass-markdoc-variables)).
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Move any existing Markdoc config from your `astro.config` to a new `markdoc.config.mjs` file at the root of your project. This should be applied as a default export, with the optional `defineMarkdocConfig()` helper for autocomplete in your editor.
|
||||||
|
|
||||||
|
This example configures an `aside` Markdoc tag. Note that components should be imported and applied to the `render` attribute _directly,_ instead of passing the name as a string:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// markdoc.config.mjs
|
||||||
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
|
import Aside from './src/components/Aside.astro';
|
||||||
|
|
||||||
|
export default defineMarkdocConfig({
|
||||||
|
tags: {
|
||||||
|
aside: {
|
||||||
|
render: Aside,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You should also remove the `components` prop from your `Content` components. Since components are imported into your config directly, this is no longer needed.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
---
|
||||||
|
- import Aside from '../components/Aside.astro';
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
|
||||||
|
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Content
|
||||||
|
- components={{ Aside }}
|
||||||
|
/>
|
||||||
|
```
|
|
@ -23,23 +23,20 @@ Inside of your Astro project, you'll see the following folders and files:
|
||||||
└── docs/
|
└── docs/
|
||||||
│ └── intro.mdoc
|
│ └── intro.mdoc
|
||||||
| └── config.ts
|
| └── config.ts
|
||||||
│ └── components/
|
│ └── components/Aside.astro
|
||||||
| ├── Aside.astro
|
│ └── layouts/Layout.astro
|
||||||
│ └── DocsContent.astro
|
│ └── pages/index.astro
|
||||||
│ └── layouts/
|
|
||||||
│ └── Layout.astro
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
| └── env.d.ts
|
| └── env.d.ts
|
||||||
├── astro.config.mjs
|
├── astro.config.mjs
|
||||||
|
├── markdoc.config.mjs
|
||||||
├── README.md
|
├── README.md
|
||||||
├── package.json
|
├── package.json
|
||||||
└── tsconfig.json
|
└── tsconfig.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Markdoc (`.mdoc`) files can be used in content collections to author your Markdown content alongside Astro and server-rendered UI framework components (React, Vue, Svelte, and more). See `src/content/docs/` for an example file.
|
Markdoc (`.mdoc`) files can be used in content collections. See `src/content/docs/` for an example file.
|
||||||
|
|
||||||
You can also apply Astro components and server-rendered UI components (React, Vue, Svelte, etc) to your Markdoc files. See `src/content/DocsContent.astro` for an example.
|
You can also render Astro components from your Markdoc files using [tags](https://markdoc.dev/docs/tags). See the `markdoc.config.mjs` file for an example configuration.
|
||||||
|
|
||||||
## 🧞 Commands
|
## 🧞 Commands
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,5 @@ import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [
|
integrations: [markdoc()],
|
||||||
markdoc({
|
|
||||||
tags: {
|
|
||||||
aside: {
|
|
||||||
render: 'Aside',
|
|
||||||
attributes: {
|
|
||||||
type: { type: String },
|
|
||||||
title: { type: String },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
14
examples/with-markdoc/markdoc.config.mjs
Normal file
14
examples/with-markdoc/markdoc.config.mjs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
|
import Aside from './src/components/Aside.astro';
|
||||||
|
|
||||||
|
export default defineMarkdocConfig({
|
||||||
|
tags: {
|
||||||
|
aside: {
|
||||||
|
render: Aside,
|
||||||
|
attributes: {
|
||||||
|
type: { type: String },
|
||||||
|
title: { type: String },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -12,6 +12,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/markdoc": "^0.0.5",
|
"@astrojs/markdoc": "^0.0.5",
|
||||||
"astro": "^2.1.7"
|
"astro": "^2.1.7",
|
||||||
|
"kleur": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
---
|
|
||||||
import Aside from './Aside.astro';
|
|
||||||
import type { CollectionEntry } from 'astro:content';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
entry: CollectionEntry<'docs'>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { entry } = Astro.props;
|
|
||||||
const { Content } = await entry.render();
|
|
||||||
---
|
|
||||||
|
|
||||||
<Content
|
|
||||||
components={{
|
|
||||||
// Pass a mapping from the component name
|
|
||||||
// To an Astro or UI component import
|
|
||||||
// See your `astro.config.mjs` for
|
|
||||||
// for the Markdoc tag mapping
|
|
||||||
Aside,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style is:global>
|
|
||||||
table {
|
|
||||||
margin-block: 2rem;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
table td {
|
|
||||||
padding-block: 0.3rem;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,18 +1,25 @@
|
||||||
---
|
---
|
||||||
import { getEntryBySlug } from 'astro:content';
|
import { getEntryBySlug } from 'astro:content';
|
||||||
import DocsContent from '../components/DocsContent.astro';
|
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
|
||||||
const intro = await getEntryBySlug('docs', 'intro');
|
const intro = await getEntryBySlug('docs', 'intro');
|
||||||
|
const { Content } = await intro.render();
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={intro.data.title}>
|
<Layout title={intro.data.title}>
|
||||||
<main>
|
<main>
|
||||||
<h1>{intro.data.title}</h1>
|
<h1>{intro.data.title}</h1>
|
||||||
<!-- `DocsContent` is a thin wrapper around -->
|
<Content variables={{ revealSecret: true }} />
|
||||||
<!-- the `Content` component provided by Content Collections, -->
|
|
||||||
<!-- with added configuration for components. -->
|
|
||||||
<!-- This allows you to share global components wherever you render your Markdoc. -->
|
|
||||||
<DocsContent entry={intro} />
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
table {
|
||||||
|
margin-block: 2rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding-block: 0.3rem;
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1056,6 +1056,7 @@ export interface ContentEntryType {
|
||||||
getRenderModule?(
|
getRenderModule?(
|
||||||
this: rollup.PluginContext,
|
this: rollup.PluginContext,
|
||||||
params: {
|
params: {
|
||||||
|
viteId: string;
|
||||||
entry: ContentEntryModule;
|
entry: ContentEntryModule;
|
||||||
}
|
}
|
||||||
): rollup.LoadResult | Promise<rollup.LoadResult>;
|
): rollup.LoadResult | Promise<rollup.LoadResult>;
|
||||||
|
|
|
@ -139,7 +139,7 @@ export const _internal = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return contentRenderer.bind(this)({ entry });
|
return contentRenderer.bind(this)({ entry, viteId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,53 +99,46 @@ const { Content } = await entry.render();
|
||||||
|
|
||||||
### Using components
|
### Using components
|
||||||
|
|
||||||
You can add Astro and UI framework components (React, Vue, Svelte, etc.) to your Markdoc using both [Markdoc tags][markdoc-tags] and HTML element [nodes][markdoc-nodes].
|
You can add Astro components to your Markdoc using both [Markdoc tags][markdoc-tags] and HTML element [nodes][markdoc-nodes].
|
||||||
|
|
||||||
#### Render Markdoc tags as Astro components
|
#### Render Markdoc tags as Astro components
|
||||||
|
|
||||||
You may configure [Markdoc tags][markdoc-tags] that map to components. You can configure a new tag from your `astro.config` using the `tags` attribute.
|
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.
|
||||||
|
|
||||||
|
This example renders an `Aside` component, and allows a `type` prop to be passed as a string:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// astro.config.mjs
|
// markdoc.config.mjs
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
import markdoc from '@astrojs/markdoc';
|
import Aside from './src/components/Aside.astro';
|
||||||
|
|
||||||
// https://astro.build/config
|
export default defineMarkdocConfig({
|
||||||
export default defineConfig({
|
tags: {
|
||||||
integrations: [
|
aside: {
|
||||||
markdoc({
|
render: Aside,
|
||||||
tags: {
|
attributes: {
|
||||||
aside: {
|
// Markdoc requires type defs for each attribute.
|
||||||
render: 'Aside',
|
// These should mirror the `Props` type of the component
|
||||||
attributes: {
|
// you are rendering.
|
||||||
// Component props as attribute definitions
|
// See Markdoc's documentation on defining attributes
|
||||||
// See Markdoc's documentation on defining attributes
|
// https://markdoc.dev/docs/attributes#defining-attributes
|
||||||
// https://markdoc.dev/docs/attributes#defining-attributes
|
type: { type: String },
|
||||||
type: { type: String },
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, you can wire this render name (`'Aside'`) to a component from the `components` prop via the `<Content />` component. Note the object key name (`Aside` in this case) should match the render name:
|
This component can now be used in your Markdoc files with the `{% aside %}` tag. Children will be passed to your component's default slot:
|
||||||
|
|
||||||
|
```md
|
||||||
|
# Welcome to Markdoc 👋
|
||||||
|
|
||||||
```astro
|
{% aside type="tip" %}
|
||||||
---
|
|
||||||
import { getEntryBySlug } from 'astro:content';
|
|
||||||
import Aside from '../components/Aside.astro';
|
|
||||||
|
|
||||||
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
Use tags like this fancy "aside" to add some *flair* to your docs.
|
||||||
const { Content } = await entry.render();
|
|
||||||
---
|
|
||||||
|
|
||||||
<Content
|
{% /aside %}
|
||||||
components={{ Aside }}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Render Markdoc nodes / HTML elements as Astro components
|
#### Render Markdoc nodes / HTML elements as Astro components
|
||||||
|
@ -153,46 +146,22 @@ const { Content } = await entry.render();
|
||||||
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, passing the built-in `level` attribute as a prop:
|
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, passing the built-in `level` attribute as a prop:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// astro.config.mjs
|
// markdoc.config.mjs
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
import markdoc from '@astrojs/markdoc';
|
import Heading from './src/components/Heading.astro';
|
||||||
|
|
||||||
// https://astro.build/config
|
export default defineMarkdocConfig({
|
||||||
export default defineConfig({
|
nodes: {
|
||||||
integrations: [
|
heading: {
|
||||||
markdoc({
|
render: Heading,
|
||||||
nodes: {
|
attributes: {
|
||||||
heading: {
|
// Pass the attributes from Markdoc's default heading node
|
||||||
render: 'Heading',
|
// as component props.
|
||||||
// Markdoc requires type defs for each attribute.
|
level: { type: String },
|
||||||
// These should mirror the `Props` type of the component
|
}
|
||||||
// you are rendering.
|
},
|
||||||
// See Markdoc's documentation on defining attributes
|
},
|
||||||
// https://markdoc.dev/docs/attributes#defining-attributes
|
})
|
||||||
attributes: {
|
|
||||||
level: { type: String },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, you can map the string passed to render (`'Heading'` in this example) to a component import. This is configured from the `<Content />` component used to render your Markdoc using the `components` prop:
|
|
||||||
|
|
||||||
```astro
|
|
||||||
---
|
|
||||||
import { getEntryBySlug } from 'astro:content';
|
|
||||||
import Heading from '../components/Heading.astro';
|
|
||||||
|
|
||||||
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
|
||||||
const { Content } = await entry.render();
|
|
||||||
---
|
|
||||||
|
|
||||||
<Content
|
|
||||||
components={{ Heading }}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, all Markdown headings will render with the `Heading.astro` component. This example uses a level 3 heading, automatically passing `level: 3` as the component prop:
|
Now, all Markdown headings will render with the `Heading.astro` component. This example uses a level 3 heading, automatically passing `level: 3` as the component prop:
|
||||||
|
@ -215,26 +184,26 @@ This example wraps a `Aside.tsx` component with a `ClientAside.astro` wrapper:
|
||||||
import Aside from './Aside';
|
import Aside from './Aside';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Aside client:load />
|
<Aside {...Astro.props} client:load />
|
||||||
```
|
```
|
||||||
|
|
||||||
This component [can be applied via the `components` prop](#render-markdoc-nodes--html-elements-as-astro-components):
|
This component can be passed to the `render` prop for any [tag][markdoc-tags] or [node][markdoc-nodes] in your config:
|
||||||
|
|
||||||
```astro
|
```js
|
||||||
---
|
// markdoc.config.mjs
|
||||||
// src/pages/why-markdoc.astro
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
import { getEntryBySlug } from 'astro:content';
|
import Aside from './src/components/Aside.astro';
|
||||||
import ClientAside from '../components/ClientAside.astro';
|
|
||||||
|
|
||||||
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
export default defineMarkdocConfig({
|
||||||
const { Content } = await entry.render();
|
tags: {
|
||||||
---
|
aside: {
|
||||||
|
render: Aside,
|
||||||
<Content
|
attributes: {
|
||||||
components={{
|
type: { type: String },
|
||||||
Aside: ClientAside,
|
}
|
||||||
}}
|
},
|
||||||
/>
|
},
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Access frontmatter and content collection information from your templates
|
### Access frontmatter and content collection information from your templates
|
||||||
|
@ -253,35 +222,29 @@ The `$entry` object matches [the `CollectionEntry` type](https://docs.astro.buil
|
||||||
|
|
||||||
### Markdoc config
|
### Markdoc config
|
||||||
|
|
||||||
The Markdoc integration accepts [all Markdoc configuration options](https://markdoc.dev/docs/config), including [tags](https://markdoc.dev/docs/tags) and [functions](https://markdoc.dev/docs/functions).
|
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).
|
||||||
|
|
||||||
You can pass these options from the `markdoc()` integration in your `astro.config`. This example adds a global `getCountryEmoji` function:
|
You can pass these options from the default export in your `markdoc.config.mjs|ts` file:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// astro.config.mjs
|
// markdoc.config.mjs
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
import markdoc from '@astrojs/markdoc';
|
|
||||||
|
|
||||||
// https://astro.build/config
|
export default defineMarkdocConfig({
|
||||||
export default defineConfig({
|
functions: {
|
||||||
integrations: [
|
getCountryEmoji: {
|
||||||
markdoc({
|
transform(parameters) {
|
||||||
functions: {
|
const [country] = Object.values(parameters);
|
||||||
getCountryEmoji: {
|
const countryToEmojiMap = {
|
||||||
transform(parameters) {
|
japan: '🇯🇵',
|
||||||
const [country] = Object.values(parameters);
|
spain: '🇪🇸',
|
||||||
const countryToEmojiMap = {
|
france: '🇫🇷',
|
||||||
japan: '🇯🇵',
|
}
|
||||||
spain: '🇪🇸',
|
return countryToEmojiMap[country] ?? '🏳'
|
||||||
france: '🇫🇷',
|
|
||||||
}
|
|
||||||
return countryToEmojiMap[country] ?? '🏳'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
],
|
},
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, you can call this function from any Markdoc content entry:
|
Now, you can call this function from any Markdoc content entry:
|
||||||
|
@ -290,47 +253,46 @@ Now, you can call this function from any Markdoc content entry:
|
||||||
¡Hola {% getCountryEmoji("spain") %}!
|
¡Hola {% getCountryEmoji("spain") %}!
|
||||||
```
|
```
|
||||||
|
|
||||||
:::note
|
|
||||||
These options will be applied during [the Markdoc "transform" phase](https://markdoc.dev/docs/render#transform). This is run **at build time** (rather than server request time) both for static and SSR Astro projects. If you need to define configuration at runtime (ex. SSR variables), [see the next section](#define-markdoc-configuration-at-runtime).
|
|
||||||
:::
|
|
||||||
|
|
||||||
📚 [See the Markdoc documentation](https://markdoc.dev/docs/functions#creating-a-custom-function) for more on using variables or functions in your content.
|
📚 [See the Markdoc documentation](https://markdoc.dev/docs/functions#creating-a-custom-function) for more on using variables or functions in your content.
|
||||||
|
|
||||||
### Define Markdoc configuration at runtime
|
### Pass Markdoc variables
|
||||||
|
|
||||||
You may need to define Markdoc configuration at the component level, rather than the `astro.config.mjs` level. This is useful when mapping props and SSR parameters to [Markdoc variables](https://markdoc.dev/docs/variables).
|
You may need to pass [variables][markdoc-variables] to your content. This is useful when passing SSR parameters like A/B tests.
|
||||||
|
|
||||||
Astro recommends running the Markdoc transform step manually. This allows you to define your configuration and call Markdoc's rendering functions in a `.astro` file directly, ignoring any Markdoc config in your `astro.config.mjs`.
|
Variables can be passed as props via the `Content` component:
|
||||||
|
|
||||||
You will need to install the `@markdoc/markdoc` package into your project first:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Using NPM
|
|
||||||
npm install @markdoc/markdoc
|
|
||||||
# Using Yarn
|
|
||||||
yarn add @markdoc/markdoc
|
|
||||||
# Using PNPM
|
|
||||||
pnpm add @markdoc/markdoc
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, you can define Markdoc configuration options using `Markdock.transform()`.
|
|
||||||
|
|
||||||
This example defines an `abTestGroup` Markdoc variable based on an SSR param, transforming the raw entry `body`. The result is rendered using the `Renderer` component provided by `@astrojs/markdoc`:
|
|
||||||
|
|
||||||
```astro
|
```astro
|
||||||
---
|
---
|
||||||
import Markdoc from '@markdoc/markdoc';
|
|
||||||
import { Renderer } from '@astrojs/markdoc/components';
|
|
||||||
import { getEntryBySlug } from 'astro:content';
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
|
||||||
const { body } = await getEntryBySlug('docs', 'with-ab-test');
|
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||||
const ast = Markdoc.parse(body);
|
const { Content } = await entry.render();
|
||||||
const content = Markdoc.transform({
|
|
||||||
variables: { abTestGroup: Astro.params.abTestGroup },
|
|
||||||
}, ast);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Renderer {content} components={{ /* same `components` prop used by the `Content` component */ }} />
|
<!--Pass the `abTest` param as a variable-->
|
||||||
|
<Content abTestGroup={Astro.params.abTestGroup} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, `abTestGroup` is available as a variable in `docs/why-markdoc.mdoc`:
|
||||||
|
|
||||||
|
```md
|
||||||
|
{% if $abTestGroup === 'image-optimization-lover' %}
|
||||||
|
|
||||||
|
Let me tell you about image optimization...
|
||||||
|
|
||||||
|
{% /if %}
|
||||||
|
```
|
||||||
|
|
||||||
|
To make a variable global to all Markdoc files, you can use the `variables` attribute from your `markdoc.config.mjs|ts`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
|
|
||||||
|
export default defineMarkdocConfig({
|
||||||
|
variables: {
|
||||||
|
environment: process.env.IS_PROD ? 'prod' : 'dev',
|
||||||
|
}
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
@ -360,3 +322,5 @@ See [CHANGELOG.md](https://github.com/withastro/astro/tree/main/packages/integra
|
||||||
[markdoc-tags]: https://markdoc.dev/docs/tags
|
[markdoc-tags]: https://markdoc.dev/docs/tags
|
||||||
|
|
||||||
[markdoc-nodes]: https://markdoc.dev/docs/nodes
|
[markdoc-nodes]: https://markdoc.dev/docs/nodes
|
||||||
|
|
||||||
|
[markdoc-variables]: https://markdoc.dev/docs/variables
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
---
|
---
|
||||||
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
import type { Config } from '@markdoc/markdoc';
|
||||||
import type { AstroInstance } from 'astro';
|
import Markdoc from '@markdoc/markdoc';
|
||||||
import { validateComponentsProp } from '../dist/utils.js';
|
|
||||||
import { ComponentNode, createTreeNode } from './TreeNode.js';
|
import { ComponentNode, createTreeNode } from './TreeNode.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: RenderableTreeNode;
|
config: Config;
|
||||||
components?: Record<string, AstroInstance['default']>;
|
stringifiedAst: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { content, components } = Astro.props as Props;
|
const { stringifiedAst, config } = Astro.props as Props;
|
||||||
|
|
||||||
// Will throw if components is invalid
|
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
||||||
if (components) {
|
const content = Markdoc.transform(ast, config);
|
||||||
validateComponentsProp(components);
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<ComponentNode treeNode={createTreeNode(content, components)} />
|
<ComponentNode treeNode={createTreeNode(content)} />
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import type { AstroInstance } from 'astro';
|
import type { AstroInstance } from 'astro';
|
||||||
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
||||||
import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
|
|
||||||
// @ts-expect-error Cannot find module 'astro:markdoc-assets' or its corresponding type declarations
|
|
||||||
import { Image } from 'astro:markdoc-assets';
|
|
||||||
import Markdoc from '@markdoc/markdoc';
|
import Markdoc from '@markdoc/markdoc';
|
||||||
import { MarkdocError, isCapitalized } from '../dist/utils.js';
|
import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
|
||||||
|
|
||||||
export type TreeNode =
|
export type TreeNode =
|
||||||
| {
|
| {
|
||||||
|
@ -47,26 +44,17 @@ export const ComponentNode = createComponent({
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
const builtInComponents: Record<string, AstroInstance['default']> = {
|
export function createTreeNode(node: RenderableTreeNode): TreeNode {
|
||||||
Image,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createTreeNode(
|
|
||||||
node: RenderableTreeNode,
|
|
||||||
userComponents: Record<string, AstroInstance['default']> = {}
|
|
||||||
): TreeNode {
|
|
||||||
const components = { ...userComponents, ...builtInComponents };
|
|
||||||
|
|
||||||
if (typeof node === 'string' || typeof node === 'number') {
|
if (typeof node === 'string' || typeof node === 'number') {
|
||||||
return { type: 'text', content: String(node) };
|
return { type: 'text', content: String(node) };
|
||||||
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
||||||
return { type: 'text', content: '' };
|
return { type: 'text', content: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.name in components) {
|
if (typeof node.name === 'function') {
|
||||||
const component = components[node.name];
|
const component = node.name;
|
||||||
const props = node.attributes;
|
const props = node.attributes;
|
||||||
const children = node.children.map((child) => createTreeNode(child, components));
|
const children = node.children.map((child) => createTreeNode(child));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'component',
|
type: 'component',
|
||||||
|
@ -74,17 +62,12 @@ export function createTreeNode(
|
||||||
props,
|
props,
|
||||||
children,
|
children,
|
||||||
};
|
};
|
||||||
} else if (isCapitalized(node.name)) {
|
|
||||||
throw new MarkdocError({
|
|
||||||
message: `Unable to render ${JSON.stringify(node.name)}.`,
|
|
||||||
hint: 'Did you add this to the "components" prop on your <Content /> component?',
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
type: 'element',
|
type: 'element',
|
||||||
tag: node.name,
|
tag: node.name,
|
||||||
attributes: node.attributes,
|
attributes: node.attributes,
|
||||||
children: node.children.map((child) => createTreeNode(child, components)),
|
children: node.children.map((child) => createTreeNode(child)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
"./components": "./components/index.ts",
|
"./components": "./components/index.ts",
|
||||||
|
"./default-config": "./dist/default-config.js",
|
||||||
|
"./config": "./dist/config.js",
|
||||||
|
"./experimental-assets-config": "./dist/experimental-assets-config.js",
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -32,17 +35,19 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@markdoc/markdoc": "^0.2.2",
|
"@markdoc/markdoc": "^0.2.2",
|
||||||
|
"esbuild": "^0.17.12",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"kleur": "^4.1.5",
|
||||||
"zod": "^3.17.3"
|
"zod": "^3.17.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"astro": "workspace:*"
|
"astro": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "workspace:*",
|
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
"@types/html-escaper": "^3.0.0",
|
"@types/html-escaper": "^3.0.0",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
|
"astro": "workspace:*",
|
||||||
"astro-scripts": "workspace:*",
|
"astro-scripts": "workspace:*",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
"devalue": "^4.2.0",
|
"devalue": "^4.2.0",
|
||||||
|
|
5
packages/integrations/markdoc/src/config.ts
Normal file
5
packages/integrations/markdoc/src/config.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc';
|
||||||
|
|
||||||
|
export function defineMarkdocConfig(config: MarkdocConfig): MarkdocConfig {
|
||||||
|
return config;
|
||||||
|
}
|
18
packages/integrations/markdoc/src/default-config.ts
Normal file
18
packages/integrations/markdoc/src/default-config.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc';
|
||||||
|
import type { ContentEntryModule } from 'astro';
|
||||||
|
|
||||||
|
export function applyDefaultConfig(
|
||||||
|
config: MarkdocConfig,
|
||||||
|
ctx: {
|
||||||
|
entry: ContentEntryModule;
|
||||||
|
}
|
||||||
|
): MarkdocConfig {
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
variables: {
|
||||||
|
entry: ctx.entry,
|
||||||
|
...config.variables,
|
||||||
|
},
|
||||||
|
// TODO: heading ID calculation, Shiki syntax highlighting
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { Config as MarkdocConfig } from '@markdoc/markdoc';
|
||||||
|
import Markdoc from '@markdoc/markdoc';
|
||||||
|
//@ts-expect-error Cannot find module 'astro:assets' or its corresponding type declarations.
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
// Separate module to only import `astro:assets` when
|
||||||
|
// `experimental.assets` flag is set in a project.
|
||||||
|
// TODO: merge with `./default-config.ts` when `experimental.assets` is baselined.
|
||||||
|
export const experimentalAssetsConfig: MarkdocConfig = {
|
||||||
|
nodes: {
|
||||||
|
image: {
|
||||||
|
attributes: {
|
||||||
|
...Markdoc.nodes.image.attributes,
|
||||||
|
__optimizedSrc: { type: 'Object' },
|
||||||
|
},
|
||||||
|
transform(node, config) {
|
||||||
|
const attributes = node.transformAttributes(config);
|
||||||
|
const children = node.transformChildren(config);
|
||||||
|
|
||||||
|
if (node.type === 'image' && '__optimizedSrc' in node.attributes) {
|
||||||
|
const { __optimizedSrc, ...rest } = node.attributes;
|
||||||
|
return new Markdoc.Tag(Image, { ...rest, src: __optimizedSrc }, children);
|
||||||
|
} else {
|
||||||
|
return new Markdoc.Tag('img', attributes, children);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,23 +1,15 @@
|
||||||
import type {
|
import type { Node } from '@markdoc/markdoc';
|
||||||
Config as ReadonlyMarkdocConfig,
|
|
||||||
ConfigType as MarkdocConfig,
|
|
||||||
Node,
|
|
||||||
} from '@markdoc/markdoc';
|
|
||||||
import Markdoc from '@markdoc/markdoc';
|
import Markdoc from '@markdoc/markdoc';
|
||||||
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import type * as rollup from 'rollup';
|
import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js';
|
||||||
import {
|
|
||||||
getAstroConfigPath,
|
|
||||||
isValidUrl,
|
|
||||||
MarkdocError,
|
|
||||||
parseFrontmatter,
|
|
||||||
prependForwardSlash,
|
|
||||||
} from './utils.js';
|
|
||||||
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
||||||
import { emitESMImage } from 'astro/assets';
|
import { emitESMImage } from 'astro/assets';
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import { loadMarkdocConfig } from './load-config.js';
|
||||||
|
import { applyDefaultConfig } from './default-config.js';
|
||||||
|
import { bold, red } from 'kleur/colors';
|
||||||
|
import type * as rollup from 'rollup';
|
||||||
|
|
||||||
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
||||||
// `contentEntryType` is not a public API
|
// `contentEntryType` is not a public API
|
||||||
|
@ -25,24 +17,24 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
||||||
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function markdocIntegration(
|
export default function markdocIntegration(legacyConfig: any): AstroIntegration {
|
||||||
userMarkdocConfig: ReadonlyMarkdocConfig = {}
|
if (legacyConfig) {
|
||||||
): AstroIntegration {
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`${red(
|
||||||
|
bold('[Markdoc]')
|
||||||
|
)} Passing Markdoc config from your \`astro.config\` is no longer supported. Configuration should be exported from a \`markdoc.config.mjs\` file. See the configuration docs for more: https://docs.astro.build/en/guides/integrations-guide/markdoc/#configuration`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/markdoc',
|
name: '@astrojs/markdoc',
|
||||||
hooks: {
|
hooks: {
|
||||||
'astro:config:setup': async (params) => {
|
'astro:config:setup': async (params) => {
|
||||||
const {
|
const { config: astroConfig, addContentEntryType } = params as SetupHookParams;
|
||||||
updateConfig,
|
|
||||||
config: astroConfig,
|
|
||||||
addContentEntryType,
|
|
||||||
} = params as SetupHookParams;
|
|
||||||
|
|
||||||
updateConfig({
|
const configLoadResult = await loadMarkdocConfig(astroConfig);
|
||||||
vite: {
|
const userMarkdocConfig = configLoadResult?.config ?? {};
|
||||||
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
||||||
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
||||||
|
@ -56,49 +48,63 @@ export default function markdocIntegration(
|
||||||
addContentEntryType({
|
addContentEntryType({
|
||||||
extensions: ['.mdoc'],
|
extensions: ['.mdoc'],
|
||||||
getEntryInfo,
|
getEntryInfo,
|
||||||
async getRenderModule({ entry }) {
|
async getRenderModule({ entry, viteId }) {
|
||||||
validateRenderProperties(userMarkdocConfig, astroConfig);
|
|
||||||
const ast = Markdoc.parse(entry.body);
|
const ast = Markdoc.parse(entry.body);
|
||||||
const pluginContext = this;
|
const pluginContext = this;
|
||||||
const markdocConfig: MarkdocConfig = {
|
const markdocConfig = applyDefaultConfig(userMarkdocConfig, { entry });
|
||||||
...userMarkdocConfig,
|
|
||||||
variables: {
|
|
||||||
...userMarkdocConfig.variables,
|
|
||||||
entry,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (astroConfig.experimental?.assets) {
|
const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
|
||||||
|
// Ignore `variable-undefined` errors.
|
||||||
|
// Variables can be configured at runtime,
|
||||||
|
// so we cannot validate them at build time.
|
||||||
|
return e.error.id !== 'variable-undefined';
|
||||||
|
});
|
||||||
|
if (validationErrors.length) {
|
||||||
|
throw new MarkdocError({
|
||||||
|
message: [
|
||||||
|
`**${String(entry.collection)} → ${String(entry.id)}** failed to validate:`,
|
||||||
|
...validationErrors.map((e) => e.error.id),
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (astroConfig.experimental.assets) {
|
||||||
await emitOptimizedImages(ast.children, {
|
await emitOptimizedImages(ast.children, {
|
||||||
astroConfig,
|
astroConfig,
|
||||||
pluginContext,
|
pluginContext,
|
||||||
filePath: entry._internal.filePath,
|
filePath: entry._internal.filePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
markdocConfig.nodes ??= {};
|
|
||||||
markdocConfig.nodes.image = {
|
|
||||||
...Markdoc.nodes.image,
|
|
||||||
transform(node, config) {
|
|
||||||
const attributes = node.transformAttributes(config);
|
|
||||||
const children = node.transformChildren(config);
|
|
||||||
|
|
||||||
if (node.type === 'image' && '__optimizedSrc' in node.attributes) {
|
|
||||||
const { __optimizedSrc, ...rest } = node.attributes;
|
|
||||||
return new Markdoc.Tag('Image', { ...rest, src: __optimizedSrc }, children);
|
|
||||||
} else {
|
|
||||||
return new Markdoc.Tag('img', attributes, children);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = Markdoc.transform(ast, markdocConfig);
|
const code = {
|
||||||
|
code: `import { jsx as h } from 'astro/jsx-runtime';
|
||||||
return {
|
import { applyDefaultConfig } from '@astrojs/markdoc/default-config';
|
||||||
code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
|
import { Renderer } from '@astrojs/markdoc/components';
|
||||||
content
|
import * as entry from ${JSON.stringify(viteId + '?astroContent')};${
|
||||||
)};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`,
|
configLoadResult
|
||||||
|
? `\nimport userConfig from ${JSON.stringify(configLoadResult.fileUrl.pathname)};`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
astroConfig.experimental.assets
|
||||||
|
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
const stringifiedAst = ${JSON.stringify(
|
||||||
|
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
|
||||||
|
)};
|
||||||
|
export async function Content (props) {
|
||||||
|
const config = applyDefaultConfig(${
|
||||||
|
configLoadResult
|
||||||
|
? '{ ...userConfig, variables: { ...userConfig.variables, ...props } }'
|
||||||
|
: '{ variables: props }'
|
||||||
|
}, { entry });${
|
||||||
|
astroConfig.experimental.assets
|
||||||
|
? `\nconfig.nodes = { ...experimentalAssetsConfig.nodes, ...config.nodes };`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
return h(Renderer, { stringifiedAst, config }); };`,
|
||||||
};
|
};
|
||||||
|
return code;
|
||||||
},
|
},
|
||||||
contentModuleTypes: await fs.promises.readFile(
|
contentModuleTypes: await fs.promises.readFile(
|
||||||
new URL('../template/content-module-types.d.ts', import.meta.url),
|
new URL('../template/content-module-types.d.ts', import.meta.url),
|
||||||
|
@ -156,87 +162,3 @@ function shouldOptimizeImage(src: string) {
|
||||||
// Optimize anything that is NOT external or an absolute path to `public/`
|
// Optimize anything that is NOT external or an absolute path to `public/`
|
||||||
return !isValidUrl(src) && !src.startsWith('/');
|
return !isValidUrl(src) && !src.startsWith('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRenderProperties(markdocConfig: ReadonlyMarkdocConfig, astroConfig: AstroConfig) {
|
|
||||||
const tags = markdocConfig.tags ?? {};
|
|
||||||
const nodes = markdocConfig.nodes ?? {};
|
|
||||||
|
|
||||||
for (const [name, config] of Object.entries(tags)) {
|
|
||||||
validateRenderProperty({ type: 'tag', name, config, astroConfig });
|
|
||||||
}
|
|
||||||
for (const [name, config] of Object.entries(nodes)) {
|
|
||||||
validateRenderProperty({ type: 'node', name, config, astroConfig });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRenderProperty({
|
|
||||||
name,
|
|
||||||
config,
|
|
||||||
type,
|
|
||||||
astroConfig,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
config: { render?: string };
|
|
||||||
type: 'node' | 'tag';
|
|
||||||
astroConfig: Pick<AstroConfig, 'root'>;
|
|
||||||
}) {
|
|
||||||
if (typeof config.render === 'string' && config.render.length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid ${type} configuration: ${JSON.stringify(
|
|
||||||
name
|
|
||||||
)}. The "render" property cannot be an empty string.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof config.render === 'string' && !isCapitalized(config.render)) {
|
|
||||||
const astroConfigPath = getAstroConfigPath(fs, fileURLToPath(astroConfig.root));
|
|
||||||
throw new MarkdocError({
|
|
||||||
message: `Invalid ${type} configuration: ${JSON.stringify(
|
|
||||||
name
|
|
||||||
)}. The "render" property must reference a capitalized component name.`,
|
|
||||||
hint: 'If you want to render to an HTML element, see our docs on rendering Markdoc manually: https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components',
|
|
||||||
location: astroConfigPath
|
|
||||||
? {
|
|
||||||
file: astroConfigPath,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCapitalized(str: string) {
|
|
||||||
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: remove when `experimental.assets` is baselined.
|
|
||||||
*
|
|
||||||
* `astro:assets` will fail to resolve if the `experimental.assets` flag is not enabled.
|
|
||||||
* This ensures a fallback for the Markdoc renderer to safely import at the top level.
|
|
||||||
* @see ../components/TreeNode.ts
|
|
||||||
*/
|
|
||||||
function safeAssetsVirtualModulePlugin({
|
|
||||||
astroConfig,
|
|
||||||
}: {
|
|
||||||
astroConfig: Pick<AstroConfig, 'experimental'>;
|
|
||||||
}): VitePlugin {
|
|
||||||
const virtualModuleId = 'astro:markdoc-assets';
|
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'astro:markdoc-safe-assets-virtual-module',
|
|
||||||
resolveId(id) {
|
|
||||||
if (id === virtualModuleId) {
|
|
||||||
return resolvedVirtualModuleId;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
load(id) {
|
|
||||||
if (id !== resolvedVirtualModuleId) return;
|
|
||||||
|
|
||||||
if (astroConfig.experimental?.assets) {
|
|
||||||
return `export { Image } from 'astro:assets';`;
|
|
||||||
} else {
|
|
||||||
return `export const Image = () => { throw new Error('Cannot use the Image component without the \`experimental.assets\` flag.'); }`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
102
packages/integrations/markdoc/src/load-config.ts
Normal file
102
packages/integrations/markdoc/src/load-config.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import type { AstroConfig } from 'astro';
|
||||||
|
import type { Config as MarkdocConfig } from '@markdoc/markdoc';
|
||||||
|
import { build as esbuild } from 'esbuild';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
|
||||||
|
const SUPPORTED_MARKDOC_CONFIG_FILES = [
|
||||||
|
'markdoc.config.js',
|
||||||
|
'markdoc.config.mjs',
|
||||||
|
'markdoc.config.mts',
|
||||||
|
'markdoc.config.ts',
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function loadMarkdocConfig(astroConfig: Pick<AstroConfig, 'root'>) {
|
||||||
|
let markdocConfigUrl: URL | undefined;
|
||||||
|
for (const filename of SUPPORTED_MARKDOC_CONFIG_FILES) {
|
||||||
|
const filePath = new URL(filename, astroConfig.root);
|
||||||
|
if (!fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
markdocConfigUrl = filePath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!markdocConfigUrl) return;
|
||||||
|
|
||||||
|
const { code, dependencies } = await bundleConfigFile({
|
||||||
|
markdocConfigUrl,
|
||||||
|
astroConfig,
|
||||||
|
});
|
||||||
|
const config: MarkdocConfig = await loadConfigFromBundledFile(astroConfig.root, code);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
fileUrl: markdocConfigUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forked from Vite's `bundleConfigFile` function
|
||||||
|
* with added handling for `.astro` imports,
|
||||||
|
* and removed unused Deno patches.
|
||||||
|
* @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961
|
||||||
|
*/
|
||||||
|
async function bundleConfigFile({
|
||||||
|
markdocConfigUrl,
|
||||||
|
astroConfig,
|
||||||
|
}: {
|
||||||
|
markdocConfigUrl: URL;
|
||||||
|
astroConfig: Pick<AstroConfig, 'root'>;
|
||||||
|
}): Promise<{ code: string; dependencies: string[] }> {
|
||||||
|
const result = await esbuild({
|
||||||
|
absWorkingDir: fileURLToPath(astroConfig.root),
|
||||||
|
entryPoints: [fileURLToPath(markdocConfigUrl)],
|
||||||
|
outfile: 'out.js',
|
||||||
|
write: false,
|
||||||
|
target: ['node16'],
|
||||||
|
platform: 'node',
|
||||||
|
packages: 'external',
|
||||||
|
bundle: true,
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: 'inline',
|
||||||
|
metafile: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'stub-astro-imports',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /.*\.astro$/ }, () => {
|
||||||
|
return {
|
||||||
|
// Stub with an unused default export
|
||||||
|
path: 'data:text/javascript,export default true',
|
||||||
|
external: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const { text } = result.outputFiles[0];
|
||||||
|
return {
|
||||||
|
code: text,
|
||||||
|
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forked from Vite config loader, replacing CJS-based path concat
|
||||||
|
* with ESM only
|
||||||
|
* @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074
|
||||||
|
*/
|
||||||
|
async function loadConfigFromBundledFile(root: URL, code: string): Promise<MarkdocConfig> {
|
||||||
|
// Write it to disk, load it with native Node ESM, then delete the file.
|
||||||
|
const tmpFileUrl = new URL(`markdoc.config.timestamp-${Date.now()}.mjs`, root);
|
||||||
|
fs.writeFileSync(tmpFileUrl, code);
|
||||||
|
try {
|
||||||
|
return (await import(tmpFileUrl.pathname)).default;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpFileUrl);
|
||||||
|
} catch {
|
||||||
|
// already removed if this function is called twice simultaneously
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
import type { AstroInstance } from 'astro';
|
|
||||||
import z from 'astro/zod';
|
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
import type fsMod from 'node:fs';
|
import type fsMod from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
@ -85,28 +83,6 @@ interface ErrorProperties {
|
||||||
frame?: string;
|
frame?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches `search` function used for resolving `astro.config` files.
|
|
||||||
* Used by Markdoc for error handling.
|
|
||||||
* @see 'astro/src/core/config/config.ts'
|
|
||||||
*/
|
|
||||||
export function getAstroConfigPath(fs: typeof fsMod, root: string): string | undefined {
|
|
||||||
const paths = [
|
|
||||||
'astro.config.mjs',
|
|
||||||
'astro.config.js',
|
|
||||||
'astro.config.ts',
|
|
||||||
'astro.config.mts',
|
|
||||||
'astro.config.cjs',
|
|
||||||
'astro.config.cts',
|
|
||||||
].map((p) => path.join(root, p));
|
|
||||||
|
|
||||||
for (const file of paths) {
|
|
||||||
if (fs.existsSync(file)) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see 'astro/src/core/path.ts'
|
* @see 'astro/src/core/path.ts'
|
||||||
*/
|
*/
|
||||||
|
@ -114,38 +90,6 @@ export function prependForwardSlash(str: string) {
|
||||||
return str[0] === '/' ? str : '/' + str;
|
return str[0] === '/' ? str : '/' + str;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateComponentsProp(components: Record<string, AstroInstance['default']>) {
|
|
||||||
try {
|
|
||||||
componentsPropValidator.parse(components);
|
|
||||||
} catch (e) {
|
|
||||||
throw new MarkdocError({
|
|
||||||
message:
|
|
||||||
e instanceof z.ZodError
|
|
||||||
? e.issues[0].message
|
|
||||||
: 'Invalid `components` prop. Ensure you are passing an object of components to <Content />',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentsPropValidator = z.record(
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.min(1, 'Invalid `components` prop. Component names cannot be empty!')
|
|
||||||
.refine(
|
|
||||||
(value) => isCapitalized(value),
|
|
||||||
(value) => ({
|
|
||||||
message: `Invalid \`components\` prop: ${JSON.stringify(
|
|
||||||
value
|
|
||||||
)}. Component name must be capitalized. If you want to render HTML elements as components, try using a Markdoc node (https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components)`,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
z.any()
|
|
||||||
);
|
|
||||||
|
|
||||||
export function isCapitalized(str: string) {
|
|
||||||
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidUrl(str: string): boolean {
|
export function isValidUrl(str: string): boolean {
|
||||||
try {
|
try {
|
||||||
new URL(str);
|
new URL(str);
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
declare module 'astro:content' {
|
declare module 'astro:content' {
|
||||||
interface Render {
|
interface Render {
|
||||||
'.mdoc': Promise<{
|
'.mdoc': Promise<{
|
||||||
Content(props: {
|
Content(props: Record<string, any>): import('astro').MarkdownInstance<{}>['Content'];
|
||||||
components?: Record<string, import('astro').AstroInstance['default']>;
|
|
||||||
}): import('astro').MarkdownInstance<{}>['Content'];
|
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { parseHTML } from 'linkedom';
|
|
||||||
import { parse as parseDevalue } from 'devalue';
|
import { parse as parseDevalue } from 'devalue';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { loadFixture, fixLineEndings } from '../../../astro/test/test-utils.js';
|
import { loadFixture, fixLineEndings } from '../../../astro/test/test-utils.js';
|
||||||
|
@ -37,70 +36,20 @@ describe('Markdoc - Content Collections', () => {
|
||||||
it('loads entry', async () => {
|
it('loads entry', async () => {
|
||||||
const res = await baseFixture.fetch('/entry.json');
|
const res = await baseFixture.fetch('/entry.json');
|
||||||
const post = parseDevalue(await res.text());
|
const post = parseDevalue(await res.text());
|
||||||
expect(formatPost(post)).to.deep.equal(simplePostEntry);
|
expect(formatPost(post)).to.deep.equal(post1Entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads collection', async () => {
|
it('loads collection', async () => {
|
||||||
const res = await baseFixture.fetch('/collection.json');
|
const res = await baseFixture.fetch('/collection.json');
|
||||||
const posts = parseDevalue(await res.text());
|
const posts = parseDevalue(await res.text());
|
||||||
expect(posts).to.not.be.null;
|
expect(posts).to.not.be.null;
|
||||||
|
|
||||||
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
||||||
simplePostEntry,
|
post1Entry,
|
||||||
withComponentsEntry,
|
post2Entry,
|
||||||
withConfigEntry,
|
post3Entry,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders content - simple', async () => {
|
|
||||||
const res = await baseFixture.fetch('/content-simple');
|
|
||||||
const html = await res.text();
|
|
||||||
const { document } = parseHTML(html);
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
expect(h2.textContent).to.equal('Simple post');
|
|
||||||
const p = document.querySelector('p');
|
|
||||||
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders content - with config', async () => {
|
|
||||||
const fixture = await getFixtureWithConfig();
|
|
||||||
const server = await fixture.startDevServer();
|
|
||||||
|
|
||||||
const res = await fixture.fetch('/content-with-config');
|
|
||||||
const html = await res.text();
|
|
||||||
const { document } = parseHTML(html);
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
expect(h2.textContent).to.equal('Post with config');
|
|
||||||
const textContent = html;
|
|
||||||
|
|
||||||
expect(textContent).to.not.include('Hello');
|
|
||||||
expect(textContent).to.include('Hola');
|
|
||||||
expect(textContent).to.include(`Konnichiwa`);
|
|
||||||
|
|
||||||
await server.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders content - with components', async () => {
|
|
||||||
const fixture = await getFixtureWithComponents();
|
|
||||||
const server = await fixture.startDevServer();
|
|
||||||
|
|
||||||
const res = await fixture.fetch('/content-with-components');
|
|
||||||
const html = await res.text();
|
|
||||||
const { document } = parseHTML(html);
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
expect(h2.textContent).to.equal('Post with components');
|
|
||||||
|
|
||||||
// Renders custom shortcode component
|
|
||||||
const marquee = document.querySelector('marquee');
|
|
||||||
expect(marquee).to.not.be.null;
|
|
||||||
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
|
||||||
|
|
||||||
// Renders Astro Code component
|
|
||||||
const pre = document.querySelector('pre');
|
|
||||||
expect(pre).to.not.be.null;
|
|
||||||
expect(pre.className).to.equal('astro-code');
|
|
||||||
|
|
||||||
await server.stop();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('build', () => {
|
describe('build', () => {
|
||||||
|
@ -111,7 +60,7 @@ describe('Markdoc - Content Collections', () => {
|
||||||
it('loads entry', async () => {
|
it('loads entry', async () => {
|
||||||
const res = await baseFixture.readFile('/entry.json');
|
const res = await baseFixture.readFile('/entry.json');
|
||||||
const post = parseDevalue(res);
|
const post = parseDevalue(res);
|
||||||
expect(formatPost(post)).to.deep.equal(simplePostEntry);
|
expect(formatPost(post)).to.deep.equal(post1Entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads collection', async () => {
|
it('loads collection', async () => {
|
||||||
|
@ -119,140 +68,43 @@ describe('Markdoc - Content Collections', () => {
|
||||||
const posts = parseDevalue(res);
|
const posts = parseDevalue(res);
|
||||||
expect(posts).to.not.be.null;
|
expect(posts).to.not.be.null;
|
||||||
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
||||||
simplePostEntry,
|
post1Entry,
|
||||||
withComponentsEntry,
|
post2Entry,
|
||||||
withConfigEntry,
|
post3Entry,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders content - simple', async () => {
|
|
||||||
const html = await baseFixture.readFile('/content-simple/index.html');
|
|
||||||
const { document } = parseHTML(html);
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
expect(h2.textContent).to.equal('Simple post');
|
|
||||||
const p = document.querySelector('p');
|
|
||||||
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders content - with config', async () => {
|
|
||||||
const fixture = await getFixtureWithConfig();
|
|
||||||
await fixture.build();
|
|
||||||
|
|
||||||
const html = await fixture.readFile('/content-with-config/index.html');
|
|
||||||
const { document } = parseHTML(html);
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
expect(h2.textContent).to.equal('Post with config');
|
|
||||||
const textContent = html;
|
|
||||||
|
|
||||||
expect(textContent).to.not.include('Hello');
|
|
||||||
expect(textContent).to.include('Hola');
|
|
||||||
expect(textContent).to.include(`Konnichiwa`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders content - with components', async () => {
|
|
||||||
const fixture = await getFixtureWithComponents();
|
|
||||||
await fixture.build();
|
|
||||||
|
|
||||||
const html = await fixture.readFile('/content-with-components/index.html');
|
|
||||||
const { document } = parseHTML(html);
|
|
||||||
const h2 = document.querySelector('h2');
|
|
||||||
expect(h2.textContent).to.equal('Post with components');
|
|
||||||
|
|
||||||
// Renders custom shortcode component
|
|
||||||
const marquee = document.querySelector('marquee');
|
|
||||||
expect(marquee).to.not.be.null;
|
|
||||||
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
|
||||||
|
|
||||||
// Renders Astro Code component
|
|
||||||
const pre = document.querySelector('pre');
|
|
||||||
expect(pre).to.not.be.null;
|
|
||||||
expect(pre.className).to.equal('astro-code');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getFixtureWithConfig() {
|
const post1Entry = {
|
||||||
return loadFixture({
|
id: 'post-1.mdoc',
|
||||||
root,
|
slug: 'post-1',
|
||||||
integrations: [
|
|
||||||
markdoc({
|
|
||||||
variables: {
|
|
||||||
countries: ['ES', 'JP'],
|
|
||||||
},
|
|
||||||
functions: {
|
|
||||||
includes: {
|
|
||||||
transform(parameters) {
|
|
||||||
const [array, value] = Object.values(parameters);
|
|
||||||
return Array.isArray(array) ? array.includes(value) : false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFixtureWithComponents() {
|
|
||||||
return loadFixture({
|
|
||||||
root,
|
|
||||||
integrations: [
|
|
||||||
markdoc({
|
|
||||||
nodes: {
|
|
||||||
fence: {
|
|
||||||
render: 'Code',
|
|
||||||
attributes: {
|
|
||||||
language: { type: String },
|
|
||||||
content: { type: String },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
mq: {
|
|
||||||
render: 'CustomMarquee',
|
|
||||||
attributes: {
|
|
||||||
direction: {
|
|
||||||
type: String,
|
|
||||||
default: 'left',
|
|
||||||
matches: ['left', 'right', 'up', 'down'],
|
|
||||||
errorLevel: 'critical',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const simplePostEntry = {
|
|
||||||
id: 'simple.mdoc',
|
|
||||||
slug: 'simple',
|
|
||||||
collection: 'blog',
|
collection: 'blog',
|
||||||
data: {
|
data: {
|
||||||
schemaWorks: true,
|
schemaWorks: true,
|
||||||
title: 'Simple post',
|
title: 'Post 1',
|
||||||
},
|
},
|
||||||
body: '\n## Simple post\n\nThis is a simple Markdoc post.\n',
|
body: '\n## Post 1\n\nThis is the contents of post 1.\n',
|
||||||
};
|
};
|
||||||
|
|
||||||
const withComponentsEntry = {
|
const post2Entry = {
|
||||||
id: 'with-components.mdoc',
|
id: 'post-2.mdoc',
|
||||||
slug: 'with-components',
|
slug: 'post-2',
|
||||||
collection: 'blog',
|
collection: 'blog',
|
||||||
data: {
|
data: {
|
||||||
schemaWorks: true,
|
schemaWorks: true,
|
||||||
title: 'Post with components',
|
title: 'Post 2',
|
||||||
},
|
},
|
||||||
body: '\n## Post with components\n\nThis uses a custom marquee component with a shortcode:\n\n{% mq direction="right" %}\nI\'m a marquee too!\n{% /mq %}\n\nAnd a code component for code blocks:\n\n```js\nconst isRenderedWithShiki = true;\n```\n',
|
body: '\n## Post 2\n\nThis is the contents of post 2.\n',
|
||||||
};
|
};
|
||||||
|
|
||||||
const withConfigEntry = {
|
const post3Entry = {
|
||||||
id: 'with-config.mdoc',
|
id: 'post-3.mdoc',
|
||||||
slug: 'with-config',
|
slug: 'post-3',
|
||||||
collection: 'blog',
|
collection: 'blog',
|
||||||
data: {
|
data: {
|
||||||
schemaWorks: true,
|
schemaWorks: true,
|
||||||
title: 'Post with config',
|
title: 'Post 3',
|
||||||
},
|
},
|
||||||
body: '\n## Post with config\n\n{% if includes($countries, "EN") %} Hello {% /if %}\n{% if includes($countries, "ES") %} Hola {% /if %}\n{% if includes($countries, "JP") %} Konnichiwa {% /if %}\n',
|
body: '\n## Post 3\n\nThis is the contents of post 3.\n',
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/markdoc": "workspace:*",
|
"@astrojs/markdoc": "workspace:*",
|
||||||
"@markdoc/markdoc": "^0.2.2",
|
|
||||||
"astro": "workspace:*"
|
"astro": "workspace:*"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"shiki": "^0.11.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-1.mdoc
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-1.mdoc
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: Post 1
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post 1
|
||||||
|
|
||||||
|
This is the contents of post 1.
|
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-2.mdoc
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-2.mdoc
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: Post 2
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post 2
|
||||||
|
|
||||||
|
This is the contents of post 2.
|
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-3.mdoc
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-3.mdoc
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: Post 3
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post 3
|
||||||
|
|
||||||
|
This is the contents of post 3.
|
|
@ -3,7 +3,7 @@ import { stringify } from 'devalue';
|
||||||
import { stripRenderFn } from '../../utils.js';
|
import { stripRenderFn } from '../../utils.js';
|
||||||
|
|
||||||
export async function get() {
|
export async function get() {
|
||||||
const post = await getEntryBySlug('blog', 'simple');
|
const post = await getEntryBySlug('blog', 'post-1');
|
||||||
return {
|
return {
|
||||||
body: stringify(stripRenderFn(post)),
|
body: stringify(stripRenderFn(post)),
|
||||||
};
|
};
|
||||||
|
|
7
packages/integrations/markdoc/test/fixtures/render-simple/astro.config.mjs
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/render-simple/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [markdoc()],
|
||||||
|
});
|
9
packages/integrations/markdoc/test/fixtures/render-simple/package.json
vendored
Normal file
9
packages/integrations/markdoc/test/fixtures/render-simple/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@test/markdoc-render-simple",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/markdoc": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
import { getEntryBySlug } from "astro:content";
|
import { getEntryBySlug } from "astro:content";
|
||||||
|
|
||||||
const post = await getEntryBySlug('blog', 'simple');
|
const post = await getEntryBySlug('blog', 'simple');
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render();
|
||||||
---
|
---
|
||||||
|
@ -10,7 +11,7 @@ const { Content } = await post.render();
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Content - Simple</title>
|
<title>Content</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Content />
|
<Content />
|
7
packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [markdoc()],
|
||||||
|
});
|
28
packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.mjs
vendored
Normal file
28
packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.mjs
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Code from './src/components/Code.astro';
|
||||||
|
import CustomMarquee from './src/components/CustomMarquee.astro';
|
||||||
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
|
|
||||||
|
export default defineMarkdocConfig({
|
||||||
|
nodes: {
|
||||||
|
fence: {
|
||||||
|
render: Code,
|
||||||
|
attributes: {
|
||||||
|
language: { type: String },
|
||||||
|
content: { type: String },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
mq: {
|
||||||
|
render: CustomMarquee,
|
||||||
|
attributes: {
|
||||||
|
direction: {
|
||||||
|
type: String,
|
||||||
|
default: 'left',
|
||||||
|
matches: ['left', 'right', 'up', 'down'],
|
||||||
|
errorLevel: 'critical',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
12
packages/integrations/markdoc/test/fixtures/render-with-components/package.json
vendored
Normal file
12
packages/integrations/markdoc/test/fixtures/render-with-components/package.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "@test/markdoc-render-with-components",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/markdoc": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"shiki": "^0.11.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
---
|
---
|
||||||
import { getEntryBySlug } from "astro:content";
|
import { getEntryBySlug } from "astro:content";
|
||||||
import Code from '../components/Code.astro';
|
|
||||||
import CustomMarquee from '../components/CustomMarquee.astro';
|
|
||||||
|
|
||||||
const post = await getEntryBySlug('blog', 'with-components');
|
const post = await getEntryBySlug('blog', 'with-components');
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render();
|
||||||
|
@ -13,11 +11,9 @@ const { Content } = await post.render();
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Content - with components</title>
|
<title>Content</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Content
|
<Content />
|
||||||
components={{ CustomMarquee, Code }}
|
|
||||||
/>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
7
packages/integrations/markdoc/test/fixtures/render-with-config/astro.config.mjs
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/render-with-config/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [markdoc()],
|
||||||
|
});
|
15
packages/integrations/markdoc/test/fixtures/render-with-config/markdoc.config.mjs
vendored
Normal file
15
packages/integrations/markdoc/test/fixtures/render-with-config/markdoc.config.mjs
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||||
|
|
||||||
|
export default defineMarkdocConfig({
|
||||||
|
variables: {
|
||||||
|
countries: ['ES', 'JP'],
|
||||||
|
},
|
||||||
|
functions: {
|
||||||
|
includes: {
|
||||||
|
transform(parameters) {
|
||||||
|
const [array, value] = Object.values(parameters);
|
||||||
|
return Array.isArray(array) ? array.includes(value) : false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
9
packages/integrations/markdoc/test/fixtures/render-with-config/package.json
vendored
Normal file
9
packages/integrations/markdoc/test/fixtures/render-with-config/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@test/markdoc-render-with-config",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/markdoc": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,3 +7,7 @@ title: Post with config
|
||||||
{% if includes($countries, "EN") %} Hello {% /if %}
|
{% if includes($countries, "EN") %} Hello {% /if %}
|
||||||
{% if includes($countries, "ES") %} Hola {% /if %}
|
{% if includes($countries, "ES") %} Hola {% /if %}
|
||||||
{% if includes($countries, "JP") %} Konnichiwa {% /if %}
|
{% if includes($countries, "JP") %} Konnichiwa {% /if %}
|
||||||
|
|
||||||
|
## Runtime variables
|
||||||
|
|
||||||
|
{% $runtimeVariable %} {% #runtime-variable %}
|
|
@ -11,9 +11,9 @@ const { Content } = await post.render();
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Content - with config</title>
|
<title>Content</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Content />
|
<Content runtimeVariable="working!" />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
124
packages/integrations/markdoc/test/render.test.js
Normal file
124
packages/integrations/markdoc/test/render.test.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from '../../../astro/test/test-utils.js';
|
||||||
|
|
||||||
|
async function getFixture(name) {
|
||||||
|
return await loadFixture({
|
||||||
|
root: new URL(`./fixtures/${name}/`, import.meta.url),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Markdoc - render', () => {
|
||||||
|
describe('dev', () => {
|
||||||
|
it('renders content - simple', async () => {
|
||||||
|
const fixture = await getFixture('render-simple');
|
||||||
|
const server = await fixture.startDevServer();
|
||||||
|
|
||||||
|
const res = await fixture.fetch('/');
|
||||||
|
const html = await res.text();
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Simple post');
|
||||||
|
const p = document.querySelector('p');
|
||||||
|
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
||||||
|
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with config', async () => {
|
||||||
|
const fixture = await getFixture('render-with-config');
|
||||||
|
const server = await fixture.startDevServer();
|
||||||
|
|
||||||
|
const res = await fixture.fetch('/');
|
||||||
|
const html = await res.text();
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with config');
|
||||||
|
const textContent = html;
|
||||||
|
|
||||||
|
expect(textContent).to.not.include('Hello');
|
||||||
|
expect(textContent).to.include('Hola');
|
||||||
|
expect(textContent).to.include(`Konnichiwa`);
|
||||||
|
|
||||||
|
const runtimeVariable = document.querySelector('#runtime-variable');
|
||||||
|
expect(runtimeVariable?.textContent?.trim()).to.equal('working!');
|
||||||
|
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with components', async () => {
|
||||||
|
const fixture = await getFixture('render-with-components');
|
||||||
|
const server = await fixture.startDevServer();
|
||||||
|
|
||||||
|
const res = await fixture.fetch('/');
|
||||||
|
const html = await res.text();
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with components');
|
||||||
|
|
||||||
|
// Renders custom shortcode component
|
||||||
|
const marquee = document.querySelector('marquee');
|
||||||
|
expect(marquee).to.not.be.null;
|
||||||
|
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
||||||
|
|
||||||
|
// Renders Astro Code component
|
||||||
|
const pre = document.querySelector('pre');
|
||||||
|
expect(pre).to.not.be.null;
|
||||||
|
expect(pre.className).to.equal('astro-code');
|
||||||
|
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('build', () => {
|
||||||
|
it('renders content - simple', async () => {
|
||||||
|
const fixture = await getFixture('render-simple');
|
||||||
|
await fixture.build();
|
||||||
|
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Simple post');
|
||||||
|
const p = document.querySelector('p');
|
||||||
|
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with config', async () => {
|
||||||
|
const fixture = await getFixture('render-with-config');
|
||||||
|
await fixture.build();
|
||||||
|
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with config');
|
||||||
|
const textContent = html;
|
||||||
|
|
||||||
|
expect(textContent).to.not.include('Hello');
|
||||||
|
expect(textContent).to.include('Hola');
|
||||||
|
expect(textContent).to.include(`Konnichiwa`);
|
||||||
|
|
||||||
|
const runtimeVariable = document.querySelector('#runtime-variable');
|
||||||
|
expect(runtimeVariable?.textContent?.trim()).to.equal('working!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with components', async () => {
|
||||||
|
const fixture = await getFixture('render-with-components');
|
||||||
|
await fixture.build();
|
||||||
|
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with components');
|
||||||
|
|
||||||
|
// Renders custom shortcode component
|
||||||
|
const marquee = document.querySelector('marquee');
|
||||||
|
expect(marquee).to.not.be.null;
|
||||||
|
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
||||||
|
|
||||||
|
// Renders Astro Code component
|
||||||
|
const pre = document.querySelector('pre');
|
||||||
|
expect(pre).to.not.be.null;
|
||||||
|
expect(pre.className).to.equal('astro-code');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -330,9 +330,11 @@ importers:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/markdoc': ^0.0.5
|
'@astrojs/markdoc': ^0.0.5
|
||||||
astro: ^2.1.7
|
astro: ^2.1.7
|
||||||
|
kleur: ^4.1.5
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/markdoc': link:../../packages/integrations/markdoc
|
'@astrojs/markdoc': link:../../packages/integrations/markdoc
|
||||||
astro: link:../../packages/astro
|
astro: link:../../packages/astro
|
||||||
|
kleur: 4.1.5
|
||||||
|
|
||||||
examples/with-markdown-plugins:
|
examples/with-markdown-plugins:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
@ -3083,7 +3085,9 @@ importers:
|
||||||
astro-scripts: workspace:*
|
astro-scripts: workspace:*
|
||||||
chai: ^4.3.6
|
chai: ^4.3.6
|
||||||
devalue: ^4.2.0
|
devalue: ^4.2.0
|
||||||
|
esbuild: ^0.17.12
|
||||||
gray-matter: ^4.0.3
|
gray-matter: ^4.0.3
|
||||||
|
kleur: ^4.1.5
|
||||||
linkedom: ^0.14.12
|
linkedom: ^0.14.12
|
||||||
mocha: ^9.2.2
|
mocha: ^9.2.2
|
||||||
rollup: ^3.20.1
|
rollup: ^3.20.1
|
||||||
|
@ -3091,7 +3095,9 @@ importers:
|
||||||
zod: ^3.17.3
|
zod: ^3.17.3
|
||||||
dependencies:
|
dependencies:
|
||||||
'@markdoc/markdoc': 0.2.2
|
'@markdoc/markdoc': 0.2.2
|
||||||
|
esbuild: 0.17.12
|
||||||
gray-matter: 4.0.3
|
gray-matter: 4.0.3
|
||||||
|
kleur: 4.1.5
|
||||||
zod: 3.20.6
|
zod: 3.20.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/chai': 4.3.4
|
'@types/chai': 4.3.4
|
||||||
|
@ -3109,15 +3115,10 @@ importers:
|
||||||
packages/integrations/markdoc/test/fixtures/content-collections:
|
packages/integrations/markdoc/test/fixtures/content-collections:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/markdoc': workspace:*
|
'@astrojs/markdoc': workspace:*
|
||||||
'@markdoc/markdoc': ^0.2.2
|
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
shiki: ^0.11.1
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/markdoc': link:../../..
|
'@astrojs/markdoc': link:../../..
|
||||||
'@markdoc/markdoc': 0.2.2
|
|
||||||
astro: link:../../../../../astro
|
astro: link:../../../../../astro
|
||||||
devDependencies:
|
|
||||||
shiki: 0.11.1
|
|
||||||
|
|
||||||
packages/integrations/markdoc/test/fixtures/entry-prop:
|
packages/integrations/markdoc/test/fixtures/entry-prop:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
@ -3135,6 +3136,33 @@ importers:
|
||||||
'@astrojs/markdoc': link:../../..
|
'@astrojs/markdoc': link:../../..
|
||||||
astro: link:../../../../../astro
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
|
packages/integrations/markdoc/test/fixtures/render-simple:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/markdoc': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/markdoc': link:../../..
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
|
packages/integrations/markdoc/test/fixtures/render-with-components:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/markdoc': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
shiki: ^0.11.1
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/markdoc': link:../../..
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
devDependencies:
|
||||||
|
shiki: 0.11.1
|
||||||
|
|
||||||
|
packages/integrations/markdoc/test/fixtures/render-with-config:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/markdoc': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/markdoc': link:../../..
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
packages/integrations/mdx:
|
packages/integrations/mdx:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/markdown-remark': ^2.1.2
|
'@astrojs/markdown-remark': ^2.1.2
|
||||||
|
|
Loading…
Reference in a new issue