[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/
|
||||
│ └── intro.mdoc
|
||||
| └── config.ts
|
||||
│ └── components/
|
||||
| ├── Aside.astro
|
||||
│ └── DocsContent.astro
|
||||
│ └── layouts/
|
||||
│ └── Layout.astro
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
│ └── components/Aside.astro
|
||||
│ └── layouts/Layout.astro
|
||||
│ └── pages/index.astro
|
||||
| └── env.d.ts
|
||||
├── astro.config.mjs
|
||||
├── markdoc.config.mjs
|
||||
├── README.md
|
||||
├── package.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
|
||||
|
||||
|
|
|
@ -3,17 +3,5 @@ import markdoc from '@astrojs/markdoc';
|
|||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
markdoc({
|
||||
tags: {
|
||||
aside: {
|
||||
render: 'Aside',
|
||||
attributes: {
|
||||
type: { type: String },
|
||||
title: { type: String },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
integrations: [markdoc()],
|
||||
});
|
||||
|
|
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": {
|
||||
"@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 DocsContent from '../components/DocsContent.astro';
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
const intro = await getEntryBySlug('docs', 'intro');
|
||||
const { Content } = await intro.render();
|
||||
---
|
||||
|
||||
<Layout title={intro.data.title}>
|
||||
<main>
|
||||
<h1>{intro.data.title}</h1>
|
||||
<!-- `DocsContent` is a thin wrapper around -->
|
||||
<!-- 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} />
|
||||
<Content variables={{ revealSecret: true }} />
|
||||
</main>
|
||||
</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?(
|
||||
this: rollup.PluginContext,
|
||||
params: {
|
||||
viteId: string;
|
||||
entry: ContentEntryModule;
|
||||
}
|
||||
): 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import markdoc from '@astrojs/markdoc';
|
||||
// markdoc.config.mjs
|
||||
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||
import Aside from './src/components/Aside.astro';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
markdoc({
|
||||
tags: {
|
||||
aside: {
|
||||
render: 'Aside',
|
||||
attributes: {
|
||||
// Component props as attribute definitions
|
||||
// See Markdoc's documentation on defining attributes
|
||||
// https://markdoc.dev/docs/attributes#defining-attributes
|
||||
type: { type: String },
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
export default defineMarkdocConfig({
|
||||
tags: {
|
||||
aside: {
|
||||
render: Aside,
|
||||
attributes: {
|
||||
// Markdoc requires type defs for each attribute.
|
||||
// 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
|
||||
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
|
||||
---
|
||||
import { getEntryBySlug } from 'astro:content';
|
||||
import Aside from '../components/Aside.astro';
|
||||
{% aside type="tip" %}
|
||||
|
||||
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
Use tags like this fancy "aside" to add some *flair* to your docs.
|
||||
|
||||
<Content
|
||||
components={{ Aside }}
|
||||
/>
|
||||
{% /aside %}
|
||||
```
|
||||
|
||||
#### 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:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import markdoc from '@astrojs/markdoc';
|
||||
// markdoc.config.mjs
|
||||
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||
import Heading from './src/components/Heading.astro';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
markdoc({
|
||||
nodes: {
|
||||
heading: {
|
||||
render: 'Heading',
|
||||
// Markdoc requires type defs for each attribute.
|
||||
// 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 }}
|
||||
/>
|
||||
export default defineMarkdocConfig({
|
||||
nodes: {
|
||||
heading: {
|
||||
render: Heading,
|
||||
attributes: {
|
||||
// Pass the attributes from Markdoc's default heading node
|
||||
// as component props.
|
||||
level: { type: String },
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
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';
|
||||
---
|
||||
|
||||
<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
|
||||
---
|
||||
// src/pages/why-markdoc.astro
|
||||
import { getEntryBySlug } from 'astro:content';
|
||||
import ClientAside from '../components/ClientAside.astro';
|
||||
```js
|
||||
// markdoc.config.mjs
|
||||
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||
import Aside from './src/components/Aside.astro';
|
||||
|
||||
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<Content
|
||||
components={{
|
||||
Aside: ClientAside,
|
||||
}}
|
||||
/>
|
||||
export default defineMarkdocConfig({
|
||||
tags: {
|
||||
aside: {
|
||||
render: Aside,
|
||||
attributes: {
|
||||
type: { type: String },
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import markdoc from '@astrojs/markdoc';
|
||||
// markdoc.config.mjs
|
||||
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
markdoc({
|
||||
functions: {
|
||||
getCountryEmoji: {
|
||||
transform(parameters) {
|
||||
const [country] = Object.values(parameters);
|
||||
const countryToEmojiMap = {
|
||||
japan: '🇯🇵',
|
||||
spain: '🇪🇸',
|
||||
france: '🇫🇷',
|
||||
}
|
||||
return countryToEmojiMap[country] ?? '🏳'
|
||||
},
|
||||
},
|
||||
export default defineMarkdocConfig({
|
||||
functions: {
|
||||
getCountryEmoji: {
|
||||
transform(parameters) {
|
||||
const [country] = Object.values(parameters);
|
||||
const countryToEmojiMap = {
|
||||
japan: '🇯🇵',
|
||||
spain: '🇪🇸',
|
||||
france: '🇫🇷',
|
||||
}
|
||||
return countryToEmojiMap[country] ?? '🏳'
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
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") %}!
|
||||
```
|
||||
|
||||
:::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.
|
||||
|
||||
### 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`.
|
||||
|
||||
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`:
|
||||
Variables can be passed as props via the `Content` component:
|
||||
|
||||
```astro
|
||||
---
|
||||
import Markdoc from '@markdoc/markdoc';
|
||||
import { Renderer } from '@astrojs/markdoc/components';
|
||||
import { getEntryBySlug } from 'astro:content';
|
||||
|
||||
const { body } = await getEntryBySlug('docs', 'with-ab-test');
|
||||
const ast = Markdoc.parse(body);
|
||||
const content = Markdoc.transform({
|
||||
variables: { abTestGroup: Astro.params.abTestGroup },
|
||||
}, ast);
|
||||
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<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
|
||||
|
@ -360,3 +322,5 @@ See [CHANGELOG.md](https://github.com/withastro/astro/tree/main/packages/integra
|
|||
[markdoc-tags]: https://markdoc.dev/docs/tags
|
||||
|
||||
[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 { AstroInstance } from 'astro';
|
||||
import { validateComponentsProp } from '../dist/utils.js';
|
||||
import type { Config } from '@markdoc/markdoc';
|
||||
import Markdoc from '@markdoc/markdoc';
|
||||
import { ComponentNode, createTreeNode } from './TreeNode.js';
|
||||
|
||||
type Props = {
|
||||
content: RenderableTreeNode;
|
||||
components?: Record<string, AstroInstance['default']>;
|
||||
config: Config;
|
||||
stringifiedAst: string;
|
||||
};
|
||||
|
||||
const { content, components } = Astro.props as Props;
|
||||
const { stringifiedAst, config } = Astro.props as Props;
|
||||
|
||||
// Will throw if components is invalid
|
||||
if (components) {
|
||||
validateComponentsProp(components);
|
||||
}
|
||||
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
||||
const content = Markdoc.transform(ast, config);
|
||||
---
|
||||
|
||||
<ComponentNode treeNode={createTreeNode(content, components)} />
|
||||
<ComponentNode treeNode={createTreeNode(content)} />
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import type { AstroInstance } from 'astro';
|
||||
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 { MarkdocError, isCapitalized } from '../dist/utils.js';
|
||||
import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
|
||||
|
||||
export type TreeNode =
|
||||
| {
|
||||
|
@ -47,26 +44,17 @@ export const ComponentNode = createComponent({
|
|||
propagation: 'none',
|
||||
});
|
||||
|
||||
const builtInComponents: Record<string, AstroInstance['default']> = {
|
||||
Image,
|
||||
};
|
||||
|
||||
export function createTreeNode(
|
||||
node: RenderableTreeNode,
|
||||
userComponents: Record<string, AstroInstance['default']> = {}
|
||||
): TreeNode {
|
||||
const components = { ...userComponents, ...builtInComponents };
|
||||
|
||||
export function createTreeNode(node: RenderableTreeNode): TreeNode {
|
||||
if (typeof node === 'string' || typeof node === 'number') {
|
||||
return { type: 'text', content: String(node) };
|
||||
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
||||
return { type: 'text', content: '' };
|
||||
}
|
||||
|
||||
if (node.name in components) {
|
||||
const component = components[node.name];
|
||||
if (typeof node.name === 'function') {
|
||||
const component = node.name;
|
||||
const props = node.attributes;
|
||||
const children = node.children.map((child) => createTreeNode(child, components));
|
||||
const children = node.children.map((child) => createTreeNode(child));
|
||||
|
||||
return {
|
||||
type: 'component',
|
||||
|
@ -74,17 +62,12 @@ export function createTreeNode(
|
|||
props,
|
||||
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 {
|
||||
return {
|
||||
type: 'element',
|
||||
tag: node.name,
|
||||
attributes: node.attributes,
|
||||
children: node.children.map((child) => createTreeNode(child, components)),
|
||||
children: node.children.map((child) => createTreeNode(child)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./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"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -32,17 +35,19 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@markdoc/markdoc": "^0.2.2",
|
||||
"esbuild": "^0.17.12",
|
||||
"gray-matter": "^4.0.3",
|
||||
"kleur": "^4.1.5",
|
||||
"zod": "^3.17.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/html-escaper": "^3.0.0",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
"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 {
|
||||
Config as ReadonlyMarkdocConfig,
|
||||
ConfigType as MarkdocConfig,
|
||||
Node,
|
||||
} from '@markdoc/markdoc';
|
||||
import type { Node } from '@markdoc/markdoc';
|
||||
import Markdoc from '@markdoc/markdoc';
|
||||
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type * as rollup from 'rollup';
|
||||
import {
|
||||
getAstroConfigPath,
|
||||
isValidUrl,
|
||||
MarkdocError,
|
||||
parseFrontmatter,
|
||||
prependForwardSlash,
|
||||
} from './utils.js';
|
||||
import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js';
|
||||
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
||||
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'> & {
|
||||
// `contentEntryType` is not a public API
|
||||
|
@ -25,24 +17,24 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|||
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
||||
};
|
||||
|
||||
export default function markdocIntegration(
|
||||
userMarkdocConfig: ReadonlyMarkdocConfig = {}
|
||||
): AstroIntegration {
|
||||
export default function markdocIntegration(legacyConfig: any): AstroIntegration {
|
||||
if (legacyConfig) {
|
||||
// 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 {
|
||||
name: '@astrojs/markdoc',
|
||||
hooks: {
|
||||
'astro:config:setup': async (params) => {
|
||||
const {
|
||||
updateConfig,
|
||||
config: astroConfig,
|
||||
addContentEntryType,
|
||||
} = params as SetupHookParams;
|
||||
const { config: astroConfig, addContentEntryType } = params as SetupHookParams;
|
||||
|
||||
updateConfig({
|
||||
vite: {
|
||||
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })],
|
||||
},
|
||||
});
|
||||
const configLoadResult = await loadMarkdocConfig(astroConfig);
|
||||
const userMarkdocConfig = configLoadResult?.config ?? {};
|
||||
|
||||
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
||||
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
||||
|
@ -56,49 +48,63 @@ export default function markdocIntegration(
|
|||
addContentEntryType({
|
||||
extensions: ['.mdoc'],
|
||||
getEntryInfo,
|
||||
async getRenderModule({ entry }) {
|
||||
validateRenderProperties(userMarkdocConfig, astroConfig);
|
||||
async getRenderModule({ entry, viteId }) {
|
||||
const ast = Markdoc.parse(entry.body);
|
||||
const pluginContext = this;
|
||||
const markdocConfig: MarkdocConfig = {
|
||||
...userMarkdocConfig,
|
||||
variables: {
|
||||
...userMarkdocConfig.variables,
|
||||
entry,
|
||||
},
|
||||
};
|
||||
const markdocConfig = applyDefaultConfig(userMarkdocConfig, { 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, {
|
||||
astroConfig,
|
||||
pluginContext,
|
||||
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);
|
||||
|
||||
return {
|
||||
code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
|
||||
content
|
||||
)};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`,
|
||||
const code = {
|
||||
code: `import { jsx as h } from 'astro/jsx-runtime';
|
||||
import { applyDefaultConfig } from '@astrojs/markdoc/default-config';
|
||||
import { Renderer } from '@astrojs/markdoc/components';
|
||||
import * as entry from ${JSON.stringify(viteId + '?astroContent')};${
|
||||
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(
|
||||
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/`
|
||||
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 type fsMod from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
@ -85,28 +83,6 @@ interface ErrorProperties {
|
|||
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'
|
||||
*/
|
||||
|
@ -114,38 +90,6 @@ export function prependForwardSlash(str: string) {
|
|||
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 {
|
||||
try {
|
||||
new URL(str);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
declare module 'astro:content' {
|
||||
interface Render {
|
||||
'.mdoc': Promise<{
|
||||
Content(props: {
|
||||
components?: Record<string, import('astro').AstroInstance['default']>;
|
||||
}): import('astro').MarkdownInstance<{}>['Content'];
|
||||
Content(props: Record<string, any>): import('astro').MarkdownInstance<{}>['Content'];
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { parseHTML } from 'linkedom';
|
||||
import { parse as parseDevalue } from 'devalue';
|
||||
import { expect } from 'chai';
|
||||
import { loadFixture, fixLineEndings } from '../../../astro/test/test-utils.js';
|
||||
|
@ -37,70 +36,20 @@ describe('Markdoc - Content Collections', () => {
|
|||
it('loads entry', async () => {
|
||||
const res = await baseFixture.fetch('/entry.json');
|
||||
const post = parseDevalue(await res.text());
|
||||
expect(formatPost(post)).to.deep.equal(simplePostEntry);
|
||||
expect(formatPost(post)).to.deep.equal(post1Entry);
|
||||
});
|
||||
|
||||
it('loads collection', async () => {
|
||||
const res = await baseFixture.fetch('/collection.json');
|
||||
const posts = parseDevalue(await res.text());
|
||||
expect(posts).to.not.be.null;
|
||||
|
||||
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
||||
simplePostEntry,
|
||||
withComponentsEntry,
|
||||
withConfigEntry,
|
||||
post1Entry,
|
||||
post2Entry,
|
||||
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', () => {
|
||||
|
@ -111,7 +60,7 @@ describe('Markdoc - Content Collections', () => {
|
|||
it('loads entry', async () => {
|
||||
const res = await baseFixture.readFile('/entry.json');
|
||||
const post = parseDevalue(res);
|
||||
expect(formatPost(post)).to.deep.equal(simplePostEntry);
|
||||
expect(formatPost(post)).to.deep.equal(post1Entry);
|
||||
});
|
||||
|
||||
it('loads collection', async () => {
|
||||
|
@ -119,140 +68,43 @@ describe('Markdoc - Content Collections', () => {
|
|||
const posts = parseDevalue(res);
|
||||
expect(posts).to.not.be.null;
|
||||
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
||||
simplePostEntry,
|
||||
withComponentsEntry,
|
||||
withConfigEntry,
|
||||
post1Entry,
|
||||
post2Entry,
|
||||
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() {
|
||||
return loadFixture({
|
||||
root,
|
||||
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',
|
||||
const post1Entry = {
|
||||
id: 'post-1.mdoc',
|
||||
slug: 'post-1',
|
||||
collection: 'blog',
|
||||
data: {
|
||||
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 = {
|
||||
id: 'with-components.mdoc',
|
||||
slug: 'with-components',
|
||||
const post2Entry = {
|
||||
id: 'post-2.mdoc',
|
||||
slug: 'post-2',
|
||||
collection: 'blog',
|
||||
data: {
|
||||
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 = {
|
||||
id: 'with-config.mdoc',
|
||||
slug: 'with-config',
|
||||
const post3Entry = {
|
||||
id: 'post-3.mdoc',
|
||||
slug: 'post-3',
|
||||
collection: 'blog',
|
||||
data: {
|
||||
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,
|
||||
"dependencies": {
|
||||
"@astrojs/markdoc": "workspace:*",
|
||||
"@markdoc/markdoc": "^0.2.2",
|
||||
"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';
|
||||
|
||||
export async function get() {
|
||||
const post = await getEntryBySlug('blog', 'simple');
|
||||
const post = await getEntryBySlug('blog', 'post-1');
|
||||
return {
|
||||
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";
|
||||
|
||||
const post = await getEntryBySlug('blog', 'simple');
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
@ -10,7 +11,7 @@ const { Content } = await post.render();
|
|||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content - Simple</title>
|
||||
<title>Content</title>
|
||||
</head>
|
||||
<body>
|
||||
<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 Code from '../components/Code.astro';
|
||||
import CustomMarquee from '../components/CustomMarquee.astro';
|
||||
|
||||
const post = await getEntryBySlug('blog', 'with-components');
|
||||
const { Content } = await post.render();
|
||||
|
@ -13,11 +11,9 @@ const { Content } = await post.render();
|
|||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content - with components</title>
|
||||
<title>Content</title>
|
||||
</head>
|
||||
<body>
|
||||
<Content
|
||||
components={{ CustomMarquee, Code }}
|
||||
/>
|
||||
<Content />
|
||||
</body>
|
||||
</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, "ES") %} Hola {% /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 http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content - with config</title>
|
||||
<title>Content</title>
|
||||
</head>
|
||||
<body>
|
||||
<Content />
|
||||
<Content runtimeVariable="working!" />
|
||||
</body>
|
||||
</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:
|
||||
'@astrojs/markdoc': ^0.0.5
|
||||
astro: ^2.1.7
|
||||
kleur: ^4.1.5
|
||||
dependencies:
|
||||
'@astrojs/markdoc': link:../../packages/integrations/markdoc
|
||||
astro: link:../../packages/astro
|
||||
kleur: 4.1.5
|
||||
|
||||
examples/with-markdown-plugins:
|
||||
specifiers:
|
||||
|
@ -3083,7 +3085,9 @@ importers:
|
|||
astro-scripts: workspace:*
|
||||
chai: ^4.3.6
|
||||
devalue: ^4.2.0
|
||||
esbuild: ^0.17.12
|
||||
gray-matter: ^4.0.3
|
||||
kleur: ^4.1.5
|
||||
linkedom: ^0.14.12
|
||||
mocha: ^9.2.2
|
||||
rollup: ^3.20.1
|
||||
|
@ -3091,7 +3095,9 @@ importers:
|
|||
zod: ^3.17.3
|
||||
dependencies:
|
||||
'@markdoc/markdoc': 0.2.2
|
||||
esbuild: 0.17.12
|
||||
gray-matter: 4.0.3
|
||||
kleur: 4.1.5
|
||||
zod: 3.20.6
|
||||
devDependencies:
|
||||
'@types/chai': 4.3.4
|
||||
|
@ -3109,15 +3115,10 @@ importers:
|
|||
packages/integrations/markdoc/test/fixtures/content-collections:
|
||||
specifiers:
|
||||
'@astrojs/markdoc': workspace:*
|
||||
'@markdoc/markdoc': ^0.2.2
|
||||
astro: workspace:*
|
||||
shiki: ^0.11.1
|
||||
dependencies:
|
||||
'@astrojs/markdoc': link:../../..
|
||||
'@markdoc/markdoc': 0.2.2
|
||||
astro: link:../../../../../astro
|
||||
devDependencies:
|
||||
shiki: 0.11.1
|
||||
|
||||
packages/integrations/markdoc/test/fixtures/entry-prop:
|
||||
specifiers:
|
||||
|
@ -3135,6 +3136,33 @@ importers:
|
|||
'@astrojs/markdoc': link:../../..
|
||||
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:
|
||||
specifiers:
|
||||
'@astrojs/markdown-remark': ^2.1.2
|
||||
|
|
Loading…
Reference in a new issue