@astrojs/markdoc 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +37 -0
- package/README.md +17 -3
- package/components/Renderer.astro +2 -3
- package/components/TreeNode.ts +90 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +108 -30
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +9 -0
- package/package.json +6 -3
- package/src/index.ts +150 -30
- package/src/utils.ts +9 -0
- package/test/entry-prop.test.js +58 -0
- package/test/fixtures/entry-prop/astro.config.mjs +7 -0
- package/test/fixtures/entry-prop/node_modules/.bin/astro +17 -0
- package/test/fixtures/entry-prop/package.json +9 -0
- package/test/fixtures/entry-prop/src/content/blog/entry.mdoc +9 -0
- package/test/fixtures/entry-prop/src/content/config.ts +9 -0
- package/test/fixtures/entry-prop/src/pages/index.astro +19 -0
- package/test/fixtures/image-assets/astro.config.mjs +10 -0
- package/test/fixtures/image-assets/node_modules/.bin/astro +17 -0
- package/test/fixtures/image-assets/package.json +9 -0
- package/test/fixtures/image-assets/src/assets/alias/cityscape.jpg +0 -0
- package/test/fixtures/image-assets/src/assets/relative/oar.jpg +0 -0
- package/test/fixtures/image-assets/src/content/docs/intro.mdoc +7 -0
- package/test/fixtures/image-assets/src/pages/index.astro +19 -0
- package/test/fixtures/image-assets/src/public/favicon.svg +9 -0
- package/test/image-assets.test.js +76 -0
- package/components/RenderNode.astro +0 -30
- package/components/astroNode.ts +0 -51
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[
|
|
2
|
-
[
|
|
3
|
-
[
|
|
4
|
-
[
|
|
5
|
-
[
|
|
1
|
+
[34m@astrojs/markdoc:build: [0mcache hit, replaying output [2m0cf4b43fcaef6989[0m
|
|
2
|
+
[34m@astrojs/markdoc:build: [0m
|
|
3
|
+
[34m@astrojs/markdoc:build: [0m> @astrojs/markdoc@0.0.5 build /home/runner/work/astro/astro/packages/integrations/markdoc
|
|
4
|
+
[34m@astrojs/markdoc:build: [0m> astro-scripts build "src/**/*.ts" && tsc
|
|
5
|
+
[34m@astrojs/markdoc:build: [0m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @astrojs/markdoc
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#6630](https://github.com/withastro/astro/pull/6630) [`cfcf2e2ff`](https://github.com/withastro/astro/commit/cfcf2e2ffdaa68ace5c84329c05b83559a29d638) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Support automatic image optimization for Markdoc images when using `experimental.assets`. You can [follow our Assets guide](https://docs.astro.build/en/guides/assets/#enabling-assets-in-your-project) to enable this feature in your project. Then, start using relative or aliased image sources in your Markdoc files for automatic optimization:
|
|
8
|
+
|
|
9
|
+
```md
|
|
10
|
+
<!--Relative paths-->
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
<!--Or configured aliases-->
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [[`b7194103e`](https://github.com/withastro/astro/commit/b7194103e39267bf59dcd6ba00f522e424219d16), [`cfcf2e2ff`](https://github.com/withastro/astro/commit/cfcf2e2ffdaa68ace5c84329c05b83559a29d638), [`45da39a86`](https://github.com/withastro/astro/commit/45da39a8642d64eb318840b18dfc2b5ccc6561bc), [`7daef9a29`](https://github.com/withastro/astro/commit/7daef9a2993b5d457f3d243a1ebfd1dd383b3327)]:
|
|
20
|
+
- astro@2.1.7
|
|
21
|
+
|
|
22
|
+
## 0.0.4
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- [#6588](https://github.com/withastro/astro/pull/6588) [`f42f47dc6`](https://github.com/withastro/astro/commit/f42f47dc6a91cdb6534dab0ecbf9e8e85f00ba40) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Allow access to content collection entry information (including parsed frontmatter and the entry slug) from your Markdoc using the `$entry` variable:
|
|
27
|
+
|
|
28
|
+
```mdx
|
|
29
|
+
---
|
|
30
|
+
title: Hello Markdoc!
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# {% $entry.data.title %}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- [#6607](https://github.com/withastro/astro/pull/6607) [`86273b588`](https://github.com/withastro/astro/commit/86273b5881cc61ebee11d40280b4c0aba8f4bb2e) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: Update Markdoc renderer internals to remove unneeded dependencies
|
|
37
|
+
|
|
38
|
+
- [#6622](https://github.com/withastro/astro/pull/6622) [`b37b86540`](https://github.com/withastro/astro/commit/b37b865400e77e92878d7e150244acce47e933c6) Thanks [@paulrudy](https://github.com/paulrudy)! - Fix README instructions for installing Markdoc manually.
|
|
39
|
+
|
|
3
40
|
## 0.0.3
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -237,6 +237,20 @@ const { Content } = await entry.render();
|
|
|
237
237
|
/>
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
### Access frontmatter and content collection information from your templates
|
|
241
|
+
|
|
242
|
+
You can access content collection information from your Markdoc templates using the `$entry` variable. This includes the entry `slug`, `collection` name, and frontmatter `data` parsed by your content collection schema (if any). This example renders the `title` frontmatter property as a heading:
|
|
243
|
+
|
|
244
|
+
```md
|
|
245
|
+
---
|
|
246
|
+
title: Welcome to Markdoc 👋
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
# {% $entry.data.title %}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The `$entry` object matches [the `CollectionEntry` type](https://docs.astro.build/en/reference/api-reference/#collection-entry-type), excluding the `.render()` property.
|
|
253
|
+
|
|
240
254
|
### Markdoc config
|
|
241
255
|
|
|
242
256
|
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).
|
|
@@ -292,11 +306,11 @@ You will need to install the `@markdoc/markdoc` package into your project first:
|
|
|
292
306
|
|
|
293
307
|
```sh
|
|
294
308
|
# Using NPM
|
|
295
|
-
|
|
309
|
+
npm install @markdoc/markdoc
|
|
296
310
|
# Using Yarn
|
|
297
|
-
yarn
|
|
311
|
+
yarn add @markdoc/markdoc
|
|
298
312
|
# Using PNPM
|
|
299
|
-
pnpm
|
|
313
|
+
pnpm add @markdoc/markdoc
|
|
300
314
|
```
|
|
301
315
|
|
|
302
316
|
Now, you can define Markdoc configuration options using `Markdock.transform()`.
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
|
3
3
|
import type { AstroInstance } from 'astro';
|
|
4
4
|
import { validateComponentsProp } from '../dist/utils.js';
|
|
5
|
-
import {
|
|
6
|
-
import RenderNode from './RenderNode.astro';
|
|
5
|
+
import { ComponentNode, createTreeNode } from './TreeNode.js';
|
|
7
6
|
|
|
8
7
|
type Props = {
|
|
9
8
|
content: RenderableTreeNode;
|
|
@@ -18,4 +17,4 @@ if (components) {
|
|
|
18
17
|
}
|
|
19
18
|
---
|
|
20
19
|
|
|
21
|
-
<
|
|
20
|
+
<ComponentNode treeNode={createTreeNode(content, components)} />
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { AstroInstance } from 'astro';
|
|
2
|
+
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
|
3
|
+
import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
|
|
4
|
+
// @ts-expect-error Cannot find module 'astro:markdoc-assets' or its corresponding type declarations
|
|
5
|
+
import { Image } from 'astro:markdoc-assets';
|
|
6
|
+
import Markdoc from '@markdoc/markdoc';
|
|
7
|
+
import { MarkdocError, isCapitalized } from '../dist/utils.js';
|
|
8
|
+
|
|
9
|
+
export type TreeNode =
|
|
10
|
+
| {
|
|
11
|
+
type: 'text';
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
type: 'component';
|
|
16
|
+
component: AstroInstance['default'];
|
|
17
|
+
props: Record<string, any>;
|
|
18
|
+
children: TreeNode[];
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: 'element';
|
|
22
|
+
tag: string;
|
|
23
|
+
attributes: Record<string, any>;
|
|
24
|
+
children: TreeNode[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ComponentNode = createComponent({
|
|
28
|
+
factory(result: any, { treeNode }: { treeNode: TreeNode }) {
|
|
29
|
+
if (treeNode.type === 'text') return render`${treeNode.content}`;
|
|
30
|
+
const slots = {
|
|
31
|
+
default: () =>
|
|
32
|
+
render`${treeNode.children.map((child) =>
|
|
33
|
+
renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child })
|
|
34
|
+
)}`,
|
|
35
|
+
};
|
|
36
|
+
if (treeNode.type === 'component') {
|
|
37
|
+
return renderComponent(
|
|
38
|
+
result,
|
|
39
|
+
treeNode.component.name,
|
|
40
|
+
treeNode.component,
|
|
41
|
+
treeNode.props,
|
|
42
|
+
slots
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
|
|
46
|
+
},
|
|
47
|
+
propagation: 'none',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const builtInComponents: Record<string, AstroInstance['default']> = {
|
|
51
|
+
Image,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function createTreeNode(
|
|
55
|
+
node: RenderableTreeNode,
|
|
56
|
+
userComponents: Record<string, AstroInstance['default']> = {}
|
|
57
|
+
): TreeNode {
|
|
58
|
+
const components = { ...userComponents, ...builtInComponents };
|
|
59
|
+
|
|
60
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
61
|
+
return { type: 'text', content: String(node) };
|
|
62
|
+
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
|
63
|
+
return { type: 'text', content: '' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (node.name in components) {
|
|
67
|
+
const component = components[node.name];
|
|
68
|
+
const props = node.attributes;
|
|
69
|
+
const children = node.children.map((child) => createTreeNode(child, components));
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
type: 'component',
|
|
73
|
+
component,
|
|
74
|
+
props,
|
|
75
|
+
children,
|
|
76
|
+
};
|
|
77
|
+
} else if (isCapitalized(node.name)) {
|
|
78
|
+
throw new MarkdocError({
|
|
79
|
+
message: `Unable to render ${JSON.stringify(node.name)}.`,
|
|
80
|
+
hint: 'Did you add this to the "components" prop on your <Content /> component?',
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
return {
|
|
84
|
+
type: 'element',
|
|
85
|
+
tag: node.name,
|
|
86
|
+
attributes: node.attributes,
|
|
87
|
+
children: node.children.map((child) => createTreeNode(child, components)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Config } from '@markdoc/markdoc';
|
|
1
|
+
import type { Config as ReadonlyMarkdocConfig } from '@markdoc/markdoc';
|
|
2
2
|
import type { AstroIntegration } from 'astro';
|
|
3
|
-
export default function
|
|
3
|
+
export default function markdocIntegration(userMarkdocConfig?: ReadonlyMarkdocConfig): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -3,16 +3,27 @@ import fs from "node:fs";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import {
|
|
5
5
|
getAstroConfigPath,
|
|
6
|
+
isValidUrl,
|
|
6
7
|
MarkdocError,
|
|
7
8
|
parseFrontmatter,
|
|
8
9
|
prependForwardSlash
|
|
9
10
|
} from "./utils.js";
|
|
10
|
-
|
|
11
|
+
import { emitESMImage } from "astro/assets";
|
|
12
|
+
function markdocIntegration(userMarkdocConfig = {}) {
|
|
11
13
|
return {
|
|
12
14
|
name: "@astrojs/markdoc",
|
|
13
15
|
hooks: {
|
|
14
16
|
"astro:config:setup": async (params) => {
|
|
15
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
updateConfig,
|
|
19
|
+
config: astroConfig,
|
|
20
|
+
addContentEntryType
|
|
21
|
+
} = params;
|
|
22
|
+
updateConfig({
|
|
23
|
+
vite: {
|
|
24
|
+
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })]
|
|
25
|
+
}
|
|
26
|
+
});
|
|
16
27
|
function getEntryInfo({ fileUrl, contents }) {
|
|
17
28
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
|
18
29
|
return {
|
|
@@ -25,42 +36,85 @@ function markdoc(markdocConfig = {}) {
|
|
|
25
36
|
addContentEntryType({
|
|
26
37
|
extensions: [".mdoc"],
|
|
27
38
|
getEntryInfo,
|
|
39
|
+
async getRenderModule({ entry }) {
|
|
40
|
+
var _a;
|
|
41
|
+
validateRenderProperties(userMarkdocConfig, astroConfig);
|
|
42
|
+
const ast = Markdoc.parse(entry.body);
|
|
43
|
+
const pluginContext = this;
|
|
44
|
+
const markdocConfig = {
|
|
45
|
+
...userMarkdocConfig,
|
|
46
|
+
variables: {
|
|
47
|
+
...userMarkdocConfig.variables,
|
|
48
|
+
entry
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if ((_a = astroConfig.experimental) == null ? void 0 : _a.assets) {
|
|
52
|
+
await emitOptimizedImages(ast.children, {
|
|
53
|
+
astroConfig,
|
|
54
|
+
pluginContext,
|
|
55
|
+
filePath: entry._internal.filePath
|
|
56
|
+
});
|
|
57
|
+
markdocConfig.nodes ??= {};
|
|
58
|
+
markdocConfig.nodes.image = {
|
|
59
|
+
...Markdoc.nodes.image,
|
|
60
|
+
transform(node, config) {
|
|
61
|
+
const attributes = node.transformAttributes(config);
|
|
62
|
+
const children = node.transformChildren(config);
|
|
63
|
+
if (node.type === "image" && "__optimizedSrc" in node.attributes) {
|
|
64
|
+
const { __optimizedSrc, ...rest } = node.attributes;
|
|
65
|
+
return new Markdoc.Tag("Image", { ...rest, src: __optimizedSrc }, children);
|
|
66
|
+
} else {
|
|
67
|
+
return new Markdoc.Tag("img", attributes, children);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const content = Markdoc.transform(ast, markdocConfig);
|
|
73
|
+
return {
|
|
74
|
+
code: `import { jsx as h } from 'astro/jsx-runtime';
|
|
75
|
+
import { Renderer } from '@astrojs/markdoc/components';
|
|
76
|
+
const transformedContent = ${JSON.stringify(
|
|
77
|
+
content
|
|
78
|
+
)};
|
|
79
|
+
export async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }
|
|
80
|
+
Content[Symbol.for('astro.needsHeadRendering')] = true;`
|
|
81
|
+
};
|
|
82
|
+
},
|
|
28
83
|
contentModuleTypes: await fs.promises.readFile(
|
|
29
84
|
new URL("../template/content-module-types.d.ts", import.meta.url),
|
|
30
85
|
"utf-8"
|
|
31
86
|
)
|
|
32
87
|
});
|
|
33
|
-
const viteConfig = {
|
|
34
|
-
plugins: [
|
|
35
|
-
{
|
|
36
|
-
name: "@astrojs/markdoc",
|
|
37
|
-
async transform(code, id) {
|
|
38
|
-
if (!id.endsWith(".mdoc"))
|
|
39
|
-
return;
|
|
40
|
-
validateRenderProperties(markdocConfig, config);
|
|
41
|
-
const body = getEntryInfo({
|
|
42
|
-
// Can't use `pathToFileUrl` - Vite IDs are not plain file paths
|
|
43
|
-
fileUrl: new URL(prependForwardSlash(id), "file://"),
|
|
44
|
-
contents: code
|
|
45
|
-
}).body;
|
|
46
|
-
const ast = Markdoc.parse(body);
|
|
47
|
-
const content = Markdoc.transform(ast, markdocConfig);
|
|
48
|
-
return `import { jsx as h } from 'astro/jsx-runtime';
|
|
49
|
-
import { Renderer } from '@astrojs/markdoc/components';
|
|
50
|
-
const transformedContent = ${JSON.stringify(
|
|
51
|
-
content
|
|
52
|
-
)};
|
|
53
|
-
export async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }
|
|
54
|
-
Content[Symbol.for('astro.needsHeadRendering')] = true;`;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
};
|
|
59
|
-
updateConfig({ vite: viteConfig });
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
};
|
|
63
91
|
}
|
|
92
|
+
async function emitOptimizedImages(nodeChildren, ctx) {
|
|
93
|
+
for (const node of nodeChildren) {
|
|
94
|
+
if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
|
|
95
|
+
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
|
|
96
|
+
if ((resolved == null ? void 0 : resolved.id) && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
|
|
97
|
+
const src = await emitESMImage(
|
|
98
|
+
resolved.id,
|
|
99
|
+
ctx.pluginContext.meta.watchMode,
|
|
100
|
+
ctx.pluginContext.emitFile,
|
|
101
|
+
{ config: ctx.astroConfig }
|
|
102
|
+
);
|
|
103
|
+
node.attributes.__optimizedSrc = src;
|
|
104
|
+
} else {
|
|
105
|
+
throw new MarkdocError({
|
|
106
|
+
message: `Could not resolve image ${JSON.stringify(
|
|
107
|
+
node.attributes.src
|
|
108
|
+
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await emitOptimizedImages(node.children, ctx);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function shouldOptimizeImage(src) {
|
|
116
|
+
return !isValidUrl(src) && !src.startsWith("/");
|
|
117
|
+
}
|
|
64
118
|
function validateRenderProperties(markdocConfig, astroConfig) {
|
|
65
119
|
const tags = markdocConfig.tags ?? {};
|
|
66
120
|
const nodes = markdocConfig.nodes ?? {};
|
|
@@ -100,6 +154,30 @@ function validateRenderProperty({
|
|
|
100
154
|
function isCapitalized(str) {
|
|
101
155
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
102
156
|
}
|
|
157
|
+
function safeAssetsVirtualModulePlugin({
|
|
158
|
+
astroConfig
|
|
159
|
+
}) {
|
|
160
|
+
const virtualModuleId = "astro:markdoc-assets";
|
|
161
|
+
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
162
|
+
return {
|
|
163
|
+
name: "astro:markdoc-safe-assets-virtual-module",
|
|
164
|
+
resolveId(id) {
|
|
165
|
+
if (id === virtualModuleId) {
|
|
166
|
+
return resolvedVirtualModuleId;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
load(id) {
|
|
170
|
+
var _a;
|
|
171
|
+
if (id !== resolvedVirtualModuleId)
|
|
172
|
+
return;
|
|
173
|
+
if ((_a = astroConfig.experimental) == null ? void 0 : _a.assets) {
|
|
174
|
+
return `export { Image } from 'astro:assets';`;
|
|
175
|
+
} else {
|
|
176
|
+
return `export const Image = () => { throw new Error('Cannot use the Image component without the \`experimental.assets\` flag.'); }`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
103
181
|
export {
|
|
104
|
-
|
|
182
|
+
markdocIntegration as default
|
|
105
183
|
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -47,4 +47,5 @@ export declare function getAstroConfigPath(fs: typeof fsMod, root: string): stri
|
|
|
47
47
|
export declare function prependForwardSlash(str: string): string;
|
|
48
48
|
export declare function validateComponentsProp(components: Record<string, AstroInstance['default']>): void;
|
|
49
49
|
export declare function isCapitalized(str: string): boolean;
|
|
50
|
+
export declare function isValidUrl(str: string): boolean;
|
|
50
51
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -84,10 +84,19 @@ const componentsPropValidator = z.record(
|
|
|
84
84
|
function isCapitalized(str) {
|
|
85
85
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
86
86
|
}
|
|
87
|
+
function isValidUrl(str) {
|
|
88
|
+
try {
|
|
89
|
+
new URL(str);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
87
95
|
export {
|
|
88
96
|
MarkdocError,
|
|
89
97
|
getAstroConfigPath,
|
|
90
98
|
isCapitalized,
|
|
99
|
+
isValidUrl,
|
|
91
100
|
parseFrontmatter,
|
|
92
101
|
prependForwardSlash,
|
|
93
102
|
validateComponentsProp
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/markdoc",
|
|
3
3
|
"description": "Add support for Markdoc pages in your Astro site",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "withastro",
|
|
@@ -26,19 +26,22 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@markdoc/markdoc": "^0.2.2",
|
|
28
28
|
"gray-matter": "^4.0.3",
|
|
29
|
-
"stringify-attributes": "^3.0.0",
|
|
30
29
|
"zod": "^3.17.3"
|
|
31
30
|
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"astro": "2.1.7"
|
|
33
|
+
},
|
|
32
34
|
"devDependencies": {
|
|
35
|
+
"astro": "2.1.7",
|
|
33
36
|
"@types/chai": "^4.3.1",
|
|
34
37
|
"@types/html-escaper": "^3.0.0",
|
|
35
38
|
"@types/mocha": "^9.1.1",
|
|
36
|
-
"astro": "2.1.4",
|
|
37
39
|
"astro-scripts": "0.0.14",
|
|
38
40
|
"chai": "^4.3.6",
|
|
39
41
|
"devalue": "^4.2.0",
|
|
40
42
|
"linkedom": "^0.14.12",
|
|
41
43
|
"mocha": "^9.2.2",
|
|
44
|
+
"rollup": "^3.20.1",
|
|
42
45
|
"vite": "^4.0.3"
|
|
43
46
|
},
|
|
44
47
|
"engines": {
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Config as ReadonlyMarkdocConfig,
|
|
3
|
+
ConfigType as MarkdocConfig,
|
|
4
|
+
Node,
|
|
5
|
+
} from '@markdoc/markdoc';
|
|
2
6
|
import Markdoc from '@markdoc/markdoc';
|
|
3
7
|
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
|
4
8
|
import fs from 'node:fs';
|
|
5
9
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import type
|
|
10
|
+
import type * as rollup from 'rollup';
|
|
7
11
|
import {
|
|
8
12
|
getAstroConfigPath,
|
|
13
|
+
isValidUrl,
|
|
9
14
|
MarkdocError,
|
|
10
15
|
parseFrontmatter,
|
|
11
16
|
prependForwardSlash,
|
|
12
17
|
} from './utils.js';
|
|
18
|
+
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
|
19
|
+
import { emitESMImage } from 'astro/assets';
|
|
20
|
+
import type { Plugin as VitePlugin } from 'vite';
|
|
13
21
|
|
|
14
22
|
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
15
23
|
// `contentEntryType` is not a public API
|
|
@@ -17,12 +25,24 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
|
17
25
|
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
|
18
26
|
};
|
|
19
27
|
|
|
20
|
-
export default function
|
|
28
|
+
export default function markdocIntegration(
|
|
29
|
+
userMarkdocConfig: ReadonlyMarkdocConfig = {}
|
|
30
|
+
): AstroIntegration {
|
|
21
31
|
return {
|
|
22
32
|
name: '@astrojs/markdoc',
|
|
23
33
|
hooks: {
|
|
24
34
|
'astro:config:setup': async (params) => {
|
|
25
|
-
const {
|
|
35
|
+
const {
|
|
36
|
+
updateConfig,
|
|
37
|
+
config: astroConfig,
|
|
38
|
+
addContentEntryType,
|
|
39
|
+
} = params as SetupHookParams;
|
|
40
|
+
|
|
41
|
+
updateConfig({
|
|
42
|
+
vite: {
|
|
43
|
+
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
26
46
|
|
|
27
47
|
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
|
28
48
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
|
@@ -36,42 +56,108 @@ export default function markdoc(markdocConfig: Config = {}): AstroIntegration {
|
|
|
36
56
|
addContentEntryType({
|
|
37
57
|
extensions: ['.mdoc'],
|
|
38
58
|
getEntryInfo,
|
|
59
|
+
async getRenderModule({ entry }) {
|
|
60
|
+
validateRenderProperties(userMarkdocConfig, astroConfig);
|
|
61
|
+
const ast = Markdoc.parse(entry.body);
|
|
62
|
+
const pluginContext = this;
|
|
63
|
+
const markdocConfig: MarkdocConfig = {
|
|
64
|
+
...userMarkdocConfig,
|
|
65
|
+
variables: {
|
|
66
|
+
...userMarkdocConfig.variables,
|
|
67
|
+
entry,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (astroConfig.experimental?.assets) {
|
|
72
|
+
await emitOptimizedImages(ast.children, {
|
|
73
|
+
astroConfig,
|
|
74
|
+
pluginContext,
|
|
75
|
+
filePath: entry._internal.filePath,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
markdocConfig.nodes ??= {};
|
|
79
|
+
markdocConfig.nodes.image = {
|
|
80
|
+
...Markdoc.nodes.image,
|
|
81
|
+
transform(node, config) {
|
|
82
|
+
const attributes = node.transformAttributes(config);
|
|
83
|
+
const children = node.transformChildren(config);
|
|
84
|
+
|
|
85
|
+
if (node.type === 'image' && '__optimizedSrc' in node.attributes) {
|
|
86
|
+
const { __optimizedSrc, ...rest } = node.attributes;
|
|
87
|
+
return new Markdoc.Tag('Image', { ...rest, src: __optimizedSrc }, children);
|
|
88
|
+
} else {
|
|
89
|
+
return new Markdoc.Tag('img', attributes, children);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content = Markdoc.transform(ast, markdocConfig);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
|
|
99
|
+
content
|
|
100
|
+
)};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
39
103
|
contentModuleTypes: await fs.promises.readFile(
|
|
40
104
|
new URL('../template/content-module-types.d.ts', import.meta.url),
|
|
41
105
|
'utf-8'
|
|
42
106
|
),
|
|
43
107
|
});
|
|
44
|
-
|
|
45
|
-
const viteConfig: InlineConfig = {
|
|
46
|
-
plugins: [
|
|
47
|
-
{
|
|
48
|
-
name: '@astrojs/markdoc',
|
|
49
|
-
async transform(code, id) {
|
|
50
|
-
if (!id.endsWith('.mdoc')) return;
|
|
51
|
-
|
|
52
|
-
validateRenderProperties(markdocConfig, config);
|
|
53
|
-
const body = getEntryInfo({
|
|
54
|
-
// Can't use `pathToFileUrl` - Vite IDs are not plain file paths
|
|
55
|
-
fileUrl: new URL(prependForwardSlash(id), 'file://'),
|
|
56
|
-
contents: code,
|
|
57
|
-
}).body;
|
|
58
|
-
const ast = Markdoc.parse(body);
|
|
59
|
-
const content = Markdoc.transform(ast, markdocConfig);
|
|
60
|
-
|
|
61
|
-
return `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
|
|
62
|
-
content
|
|
63
|
-
)};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`;
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
updateConfig({ vite: viteConfig });
|
|
69
108
|
},
|
|
70
109
|
},
|
|
71
110
|
};
|
|
72
111
|
}
|
|
73
112
|
|
|
74
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Emits optimized images, and appends the generated `src` to each AST node
|
|
115
|
+
* via the `__optimizedSrc` attribute.
|
|
116
|
+
*/
|
|
117
|
+
async function emitOptimizedImages(
|
|
118
|
+
nodeChildren: Node[],
|
|
119
|
+
ctx: {
|
|
120
|
+
pluginContext: rollup.PluginContext;
|
|
121
|
+
filePath: string;
|
|
122
|
+
astroConfig: AstroConfig;
|
|
123
|
+
}
|
|
124
|
+
) {
|
|
125
|
+
for (const node of nodeChildren) {
|
|
126
|
+
if (
|
|
127
|
+
node.type === 'image' &&
|
|
128
|
+
typeof node.attributes.src === 'string' &&
|
|
129
|
+
shouldOptimizeImage(node.attributes.src)
|
|
130
|
+
) {
|
|
131
|
+
// Attempt to resolve source with Vite.
|
|
132
|
+
// This handles relative paths and configured aliases
|
|
133
|
+
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
|
|
134
|
+
|
|
135
|
+
if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) {
|
|
136
|
+
const src = await emitESMImage(
|
|
137
|
+
resolved.id,
|
|
138
|
+
ctx.pluginContext.meta.watchMode,
|
|
139
|
+
ctx.pluginContext.emitFile,
|
|
140
|
+
{ config: ctx.astroConfig }
|
|
141
|
+
);
|
|
142
|
+
node.attributes.__optimizedSrc = src;
|
|
143
|
+
} else {
|
|
144
|
+
throw new MarkdocError({
|
|
145
|
+
message: `Could not resolve image ${JSON.stringify(
|
|
146
|
+
node.attributes.src
|
|
147
|
+
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
await emitOptimizedImages(node.children, ctx);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function shouldOptimizeImage(src: string) {
|
|
156
|
+
// Optimize anything that is NOT external or an absolute path to `public/`
|
|
157
|
+
return !isValidUrl(src) && !src.startsWith('/');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateRenderProperties(markdocConfig: ReadonlyMarkdocConfig, astroConfig: AstroConfig) {
|
|
75
161
|
const tags = markdocConfig.tags ?? {};
|
|
76
162
|
const nodes = markdocConfig.nodes ?? {};
|
|
77
163
|
|
|
@@ -120,3 +206,37 @@ function validateRenderProperty({
|
|
|
120
206
|
function isCapitalized(str: string) {
|
|
121
207
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
122
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* TODO: remove when `experimental.assets` is baselined.
|
|
212
|
+
*
|
|
213
|
+
* `astro:assets` will fail to resolve if the `experimental.assets` flag is not enabled.
|
|
214
|
+
* This ensures a fallback for the Markdoc renderer to safely import at the top level.
|
|
215
|
+
* @see ../components/TreeNode.ts
|
|
216
|
+
*/
|
|
217
|
+
function safeAssetsVirtualModulePlugin({
|
|
218
|
+
astroConfig,
|
|
219
|
+
}: {
|
|
220
|
+
astroConfig: Pick<AstroConfig, 'experimental'>;
|
|
221
|
+
}): VitePlugin {
|
|
222
|
+
const virtualModuleId = 'astro:markdoc-assets';
|
|
223
|
+
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
name: 'astro:markdoc-safe-assets-virtual-module',
|
|
227
|
+
resolveId(id) {
|
|
228
|
+
if (id === virtualModuleId) {
|
|
229
|
+
return resolvedVirtualModuleId;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
load(id) {
|
|
233
|
+
if (id !== resolvedVirtualModuleId) return;
|
|
234
|
+
|
|
235
|
+
if (astroConfig.experimental?.assets) {
|
|
236
|
+
return `export { Image } from 'astro:assets';`;
|
|
237
|
+
} else {
|
|
238
|
+
return `export const Image = () => { throw new Error('Cannot use the Image component without the \`experimental.assets\` flag.'); }`;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -145,3 +145,12 @@ const componentsPropValidator = z.record(
|
|
|
145
145
|
export function isCapitalized(str: string) {
|
|
146
146
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
export function isValidUrl(str: string): boolean {
|
|
150
|
+
try {
|
|
151
|
+
new URL(str);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parseHTML } from 'linkedom';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { loadFixture } from '../../../astro/test/test-utils.js';
|
|
4
|
+
import markdoc from '../dist/index.js';
|
|
5
|
+
|
|
6
|
+
const root = new URL('./fixtures/entry-prop/', import.meta.url);
|
|
7
|
+
|
|
8
|
+
describe('Markdoc - Entry prop', () => {
|
|
9
|
+
let baseFixture;
|
|
10
|
+
|
|
11
|
+
before(async () => {
|
|
12
|
+
baseFixture = await loadFixture({
|
|
13
|
+
root,
|
|
14
|
+
integrations: [markdoc()],
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('dev', () => {
|
|
19
|
+
let devServer;
|
|
20
|
+
|
|
21
|
+
before(async () => {
|
|
22
|
+
devServer = await baseFixture.startDevServer();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
after(async () => {
|
|
26
|
+
await devServer.stop();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('has expected entry properties', async () => {
|
|
30
|
+
const res = await baseFixture.fetch('/');
|
|
31
|
+
const html = await res.text();
|
|
32
|
+
const { document } = parseHTML(html);
|
|
33
|
+
expect(document.querySelector('h1')?.textContent).to.equal('Processed by schema: Test entry');
|
|
34
|
+
expect(document.getElementById('id')?.textContent?.trim()).to.equal('id: entry.mdoc');
|
|
35
|
+
expect(document.getElementById('slug')?.textContent?.trim()).to.equal('slug: entry');
|
|
36
|
+
expect(document.getElementById('collection')?.textContent?.trim()).to.equal(
|
|
37
|
+
'collection: blog'
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('build', () => {
|
|
43
|
+
before(async () => {
|
|
44
|
+
await baseFixture.build();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('has expected entry properties', async () => {
|
|
48
|
+
const html = await baseFixture.readFile('/index.html');
|
|
49
|
+
const { document } = parseHTML(html);
|
|
50
|
+
expect(document.querySelector('h1')?.textContent).to.equal('Processed by schema: Test entry');
|
|
51
|
+
expect(document.getElementById('id')?.textContent?.trim()).to.equal('id: entry.mdoc');
|
|
52
|
+
expect(document.getElementById('slug')?.textContent?.trim()).to.equal('slug: entry');
|
|
53
|
+
expect(document.getElementById('collection')?.textContent?.trim()).to.equal(
|
|
54
|
+
'collection: blog'
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
|
6
|
+
esac
|
|
7
|
+
|
|
8
|
+
if [ -z "$NODE_PATH" ]; then
|
|
9
|
+
export NODE_PATH="/home/runner/work/astro/astro/node_modules/.pnpm/node_modules"
|
|
10
|
+
else
|
|
11
|
+
export NODE_PATH="$NODE_PATH:/home/runner/work/astro/astro/node_modules/.pnpm/node_modules"
|
|
12
|
+
fi
|
|
13
|
+
if [ -x "$basedir/node" ]; then
|
|
14
|
+
exec "$basedir/node" "$basedir/../../../../../../../astro/astro.js" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec node "$basedir/../../../../../../../astro/astro.js" "$@"
|
|
17
|
+
fi
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getEntryBySlug } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
const entry = await getEntryBySlug('blog', 'entry');
|
|
5
|
+
const { Content } = await entry.render();
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
12
|
+
<meta name="viewport" content="width=device-width" />
|
|
13
|
+
<meta name="generator" content={Astro.generator} />
|
|
14
|
+
<title>Astro</title>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<Content />
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
|
6
|
+
esac
|
|
7
|
+
|
|
8
|
+
if [ -z "$NODE_PATH" ]; then
|
|
9
|
+
export NODE_PATH="/home/runner/work/astro/astro/node_modules/.pnpm/node_modules"
|
|
10
|
+
else
|
|
11
|
+
export NODE_PATH="$NODE_PATH:/home/runner/work/astro/astro/node_modules/.pnpm/node_modules"
|
|
12
|
+
fi
|
|
13
|
+
if [ -x "$basedir/node" ]; then
|
|
14
|
+
exec "$basedir/node" "$basedir/../../../../../../../astro/astro.js" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec node "$basedir/../../../../../../../astro/astro.js" "$@"
|
|
17
|
+
fi
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getEntryBySlug } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
const intro = await getEntryBySlug('docs', 'intro');
|
|
5
|
+
const { Content } = await intro.render();
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
12
|
+
<meta name="viewport" content="width=device-width" />
|
|
13
|
+
<meta name="generator" content={Astro.generator} />
|
|
14
|
+
<title>Astro</title>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<Content />
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
|
2
|
+
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
3
|
+
<style>
|
|
4
|
+
path { fill: #000; }
|
|
5
|
+
@media (prefers-color-scheme: dark) {
|
|
6
|
+
path { fill: #FFF; }
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parseHTML } from 'linkedom';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { loadFixture } from '../../../astro/test/test-utils.js';
|
|
4
|
+
|
|
5
|
+
const root = new URL('./fixtures/image-assets/', import.meta.url);
|
|
6
|
+
|
|
7
|
+
describe('Markdoc - Image assets', () => {
|
|
8
|
+
let baseFixture;
|
|
9
|
+
|
|
10
|
+
before(async () => {
|
|
11
|
+
baseFixture = await loadFixture({
|
|
12
|
+
root,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('dev', () => {
|
|
17
|
+
let devServer;
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
devServer = await baseFixture.startDevServer();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(async () => {
|
|
24
|
+
await devServer.stop();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses public/ image paths unchanged', async () => {
|
|
28
|
+
const res = await baseFixture.fetch('/');
|
|
29
|
+
const html = await res.text();
|
|
30
|
+
const { document } = parseHTML(html);
|
|
31
|
+
expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('transforms relative image paths to optimized path', async () => {
|
|
35
|
+
const res = await baseFixture.fetch('/');
|
|
36
|
+
const html = await res.text();
|
|
37
|
+
const { document } = parseHTML(html);
|
|
38
|
+
expect(document.querySelector('#relative > img')?.src).to.equal(
|
|
39
|
+
'/_image?href=%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&f=webp'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('transforms aliased image paths to optimized path', async () => {
|
|
44
|
+
const res = await baseFixture.fetch('/');
|
|
45
|
+
const html = await res.text();
|
|
46
|
+
const { document } = parseHTML(html);
|
|
47
|
+
expect(document.querySelector('#alias > img')?.src).to.equal(
|
|
48
|
+
'/_image?href=%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&f=webp'
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('build', () => {
|
|
54
|
+
before(async () => {
|
|
55
|
+
await baseFixture.build();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses public/ image paths unchanged', async () => {
|
|
59
|
+
const html = await baseFixture.readFile('/index.html');
|
|
60
|
+
const { document } = parseHTML(html);
|
|
61
|
+
expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('transforms relative image paths to optimized path', async () => {
|
|
65
|
+
const html = await baseFixture.readFile('/index.html');
|
|
66
|
+
const { document } = parseHTML(html);
|
|
67
|
+
expect(document.querySelector('#relative > img')?.src).to.match(/^\/_astro\/oar.*\.webp$/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('transforms aliased image paths to optimized path', async () => {
|
|
71
|
+
const html = await baseFixture.readFile('/index.html');
|
|
72
|
+
const { document } = parseHTML(html);
|
|
73
|
+
expect(document.querySelector('#alias > img')?.src).to.match(/^\/_astro\/cityscape.*\.webp$/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import stringifyAttributes from 'stringify-attributes';
|
|
3
|
-
import type { AstroNode } from './astroNode';
|
|
4
|
-
|
|
5
|
-
type Props = {
|
|
6
|
-
node: AstroNode;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const Node = (Astro.props as Props).node;
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
typeof Node === 'string' ? (
|
|
14
|
-
<Fragment set:text={Node} />
|
|
15
|
-
) : 'component' in Node ? (
|
|
16
|
-
<Node.component {...Node.props}>
|
|
17
|
-
{Node.children.map((child) => (
|
|
18
|
-
<Astro.self node={child} />
|
|
19
|
-
))}
|
|
20
|
-
</Node.component>
|
|
21
|
-
) : (
|
|
22
|
-
<Fragment>
|
|
23
|
-
<Fragment set:html={`<${Node.tag} ${stringifyAttributes(Node.attributes)}>`} />
|
|
24
|
-
{Node.children.map((child) => (
|
|
25
|
-
<Astro.self node={child} />
|
|
26
|
-
))}
|
|
27
|
-
<Fragment set:html={`</${Node.tag}>`} />
|
|
28
|
-
</Fragment>
|
|
29
|
-
)
|
|
30
|
-
}
|
package/components/astroNode.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { AstroInstance } from 'astro';
|
|
2
|
-
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
|
3
|
-
import Markdoc from '@markdoc/markdoc';
|
|
4
|
-
import { MarkdocError, isCapitalized } from '../dist/utils.js';
|
|
5
|
-
|
|
6
|
-
export type AstroNode =
|
|
7
|
-
| string
|
|
8
|
-
| {
|
|
9
|
-
component: AstroInstance['default'];
|
|
10
|
-
props: Record<string, any>;
|
|
11
|
-
children: AstroNode[];
|
|
12
|
-
}
|
|
13
|
-
| {
|
|
14
|
-
tag: string;
|
|
15
|
-
attributes: Record<string, any>;
|
|
16
|
-
children: AstroNode[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function createAstroNode(
|
|
20
|
-
node: RenderableTreeNode,
|
|
21
|
-
components: Record<string, AstroInstance['default']> = {}
|
|
22
|
-
): AstroNode {
|
|
23
|
-
if (typeof node === 'string' || typeof node === 'number') {
|
|
24
|
-
return String(node);
|
|
25
|
-
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (node.name in components) {
|
|
30
|
-
const component = components[node.name];
|
|
31
|
-
const props = node.attributes;
|
|
32
|
-
const children = node.children.map((child) => createAstroNode(child, components));
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
component,
|
|
36
|
-
props,
|
|
37
|
-
children,
|
|
38
|
-
};
|
|
39
|
-
} else if (isCapitalized(node.name)) {
|
|
40
|
-
throw new MarkdocError({
|
|
41
|
-
message: `Unable to render ${JSON.stringify(node.name)}.`,
|
|
42
|
-
hint: 'Did you add this to the "components" prop on your <Content /> component?',
|
|
43
|
-
});
|
|
44
|
-
} else {
|
|
45
|
-
return {
|
|
46
|
-
tag: node.name,
|
|
47
|
-
attributes: node.attributes,
|
|
48
|
-
children: node.children.map((child) => createAstroNode(child, components)),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|