@astrojs/markdoc 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +47 -0
- package/README.md +110 -146
- package/components/Renderer.astro +8 -11
- package/components/TreeNode.ts +6 -23
- package/dist/config.d.ts +2 -0
- package/dist/config.js +6 -0
- package/dist/default-config.d.ts +5 -0
- package/dist/default-config.js +13 -0
- package/dist/experimental-assets-config.d.ts +2 -0
- package/dist/experimental-assets-config.js +25 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +43 -112
- package/dist/load-config.d.ts +14 -0
- package/dist/load-config.js +82 -0
- package/dist/utils.d.ts +0 -11
- package/dist/utils.js +1 -44
- package/package.json +8 -3
- package/src/config.ts +5 -0
- package/src/default-config.ts +18 -0
- package/src/experimental-assets-config.ts +29 -0
- package/src/index.ts +65 -143
- package/src/load-config.ts +102 -0
- package/src/utils.ts +0 -58
- package/template/content-module-types.d.ts +1 -3
- package/test/content-collections.test.js +24 -172
- package/test/fixtures/content-collections/package.json +0 -4
- package/test/fixtures/content-collections/src/content/blog/post-1.mdoc +7 -0
- package/test/fixtures/content-collections/src/content/blog/post-2.mdoc +7 -0
- package/test/fixtures/content-collections/src/content/blog/post-3.mdoc +7 -0
- package/test/fixtures/content-collections/src/pages/entry.json.js +1 -1
- package/test/fixtures/render-simple/astro.config.mjs +7 -0
- package/test/fixtures/render-simple/node_modules/.bin/astro +17 -0
- package/test/fixtures/render-simple/package.json +9 -0
- package/test/fixtures/{content-collections/src/pages/content-simple.astro → render-simple/src/pages/index.astro} +2 -1
- package/test/fixtures/render-with-components/astro.config.mjs +7 -0
- package/test/fixtures/render-with-components/markdoc.config.mjs +28 -0
- package/test/fixtures/render-with-components/node_modules/.bin/astro +17 -0
- package/test/fixtures/render-with-components/package.json +12 -0
- package/test/fixtures/{content-collections/src/pages/content-with-components.astro → render-with-components/src/pages/index.astro} +2 -6
- package/test/fixtures/render-with-config/astro.config.mjs +7 -0
- package/test/fixtures/render-with-config/markdoc.config.mjs +15 -0
- package/test/fixtures/render-with-config/node_modules/.bin/astro +17 -0
- package/test/fixtures/render-with-config/package.json +9 -0
- package/test/fixtures/{content-collections → render-with-config}/src/content/blog/with-config.mdoc +4 -0
- package/test/fixtures/{content-collections/src/pages/content-with-config.astro → render-with-config/src/pages/index.astro} +2 -2
- package/test/render.test.js +124 -0
- /package/test/fixtures/{content-collections → render-simple}/src/content/blog/simple.mdoc +0 -0
- /package/test/fixtures/{content-collections → render-with-components}/src/components/Code.astro +0 -0
- /package/test/fixtures/{content-collections → render-with-components}/src/components/CustomMarquee.astro +0 -0
- /package/test/fixtures/{content-collections → render-with-components}/src/content/blog/with-components.mdoc +0 -0
package/src/index.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Config as ReadonlyMarkdocConfig,
|
|
3
|
-
ConfigType as MarkdocConfig,
|
|
4
|
-
Node,
|
|
5
|
-
} from '@markdoc/markdoc';
|
|
1
|
+
import type { Node } from '@markdoc/markdoc';
|
|
6
2
|
import Markdoc from '@markdoc/markdoc';
|
|
7
3
|
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
|
8
4
|
import fs from 'node:fs';
|
|
9
5
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
getAstroConfigPath,
|
|
13
|
-
isValidUrl,
|
|
14
|
-
MarkdocError,
|
|
15
|
-
parseFrontmatter,
|
|
16
|
-
prependForwardSlash,
|
|
17
|
-
} from './utils.js';
|
|
6
|
+
import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js';
|
|
18
7
|
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
|
19
8
|
import { emitESMImage } from 'astro/assets';
|
|
20
|
-
import
|
|
9
|
+
import { bold, red } from 'kleur/colors';
|
|
10
|
+
import type * as rollup from 'rollup';
|
|
11
|
+
import { applyDefaultConfig } from './default-config.js';
|
|
12
|
+
import { loadMarkdocConfig } from './load-config.js';
|
|
21
13
|
|
|
22
14
|
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
23
15
|
// `contentEntryType` is not a public API
|
|
@@ -25,24 +17,24 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
|
25
17
|
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
|
26
18
|
};
|
|
27
19
|
|
|
28
|
-
export default function markdocIntegration(
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
export default function markdocIntegration(legacyConfig: any): AstroIntegration {
|
|
21
|
+
if (legacyConfig) {
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.log(
|
|
24
|
+
`${red(
|
|
25
|
+
bold('[Markdoc]')
|
|
26
|
+
)} 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`
|
|
27
|
+
);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
31
30
|
return {
|
|
32
31
|
name: '@astrojs/markdoc',
|
|
33
32
|
hooks: {
|
|
34
33
|
'astro:config:setup': async (params) => {
|
|
35
|
-
const {
|
|
36
|
-
updateConfig,
|
|
37
|
-
config: astroConfig,
|
|
38
|
-
addContentEntryType,
|
|
39
|
-
} = params as SetupHookParams;
|
|
34
|
+
const { config: astroConfig, addContentEntryType } = params as SetupHookParams;
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })],
|
|
44
|
-
},
|
|
45
|
-
});
|
|
36
|
+
const configLoadResult = await loadMarkdocConfig(astroConfig);
|
|
37
|
+
const userMarkdocConfig = configLoadResult?.config ?? {};
|
|
46
38
|
|
|
47
39
|
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
|
48
40
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
|
@@ -56,49 +48,63 @@ export default function markdocIntegration(
|
|
|
56
48
|
addContentEntryType({
|
|
57
49
|
extensions: ['.mdoc'],
|
|
58
50
|
getEntryInfo,
|
|
59
|
-
async getRenderModule({ entry }) {
|
|
60
|
-
validateRenderProperties(userMarkdocConfig, astroConfig);
|
|
51
|
+
async getRenderModule({ entry, viteId }) {
|
|
61
52
|
const ast = Markdoc.parse(entry.body);
|
|
62
53
|
const pluginContext = this;
|
|
63
|
-
const markdocConfig
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
const markdocConfig = applyDefaultConfig(userMarkdocConfig, { entry });
|
|
55
|
+
|
|
56
|
+
const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
|
|
57
|
+
// Ignore `variable-undefined` errors.
|
|
58
|
+
// Variables can be configured at runtime,
|
|
59
|
+
// so we cannot validate them at build time.
|
|
60
|
+
return e.error.id !== 'variable-undefined';
|
|
61
|
+
});
|
|
62
|
+
if (validationErrors.length) {
|
|
63
|
+
throw new MarkdocError({
|
|
64
|
+
message: [
|
|
65
|
+
`**${String(entry.collection)} → ${String(entry.id)}** failed to validate:`,
|
|
66
|
+
...validationErrors.map((e) => e.error.id),
|
|
67
|
+
].join('\n'),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
if (astroConfig.experimental
|
|
71
|
+
if (astroConfig.experimental.assets) {
|
|
72
72
|
await emitOptimizedImages(ast.children, {
|
|
73
73
|
astroConfig,
|
|
74
74
|
pluginContext,
|
|
75
75
|
filePath: entry._internal.filePath,
|
|
76
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
77
|
}
|
|
94
78
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
79
|
+
const code = {
|
|
80
|
+
code: `import { jsx as h } from 'astro/jsx-runtime';
|
|
81
|
+
import { applyDefaultConfig } from '@astrojs/markdoc/default-config';
|
|
82
|
+
import { Renderer } from '@astrojs/markdoc/components';
|
|
83
|
+
import * as entry from ${JSON.stringify(viteId + '?astroContent')};${
|
|
84
|
+
configLoadResult
|
|
85
|
+
? `\nimport userConfig from ${JSON.stringify(configLoadResult.fileUrl.pathname)};`
|
|
86
|
+
: ''
|
|
87
|
+
}${
|
|
88
|
+
astroConfig.experimental.assets
|
|
89
|
+
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';`
|
|
90
|
+
: ''
|
|
91
|
+
}
|
|
92
|
+
const stringifiedAst = ${JSON.stringify(
|
|
93
|
+
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
|
|
94
|
+
)};
|
|
95
|
+
export async function Content (props) {
|
|
96
|
+
const config = applyDefaultConfig(${
|
|
97
|
+
configLoadResult
|
|
98
|
+
? '{ ...userConfig, variables: { ...userConfig.variables, ...props } }'
|
|
99
|
+
: '{ variables: props }'
|
|
100
|
+
}, { entry });${
|
|
101
|
+
astroConfig.experimental.assets
|
|
102
|
+
? `\nconfig.nodes = { ...experimentalAssetsConfig.nodes, ...config.nodes };`
|
|
103
|
+
: ''
|
|
104
|
+
}
|
|
105
|
+
return h(Renderer, { stringifiedAst, config }); };`,
|
|
101
106
|
};
|
|
107
|
+
return code;
|
|
102
108
|
},
|
|
103
109
|
contentModuleTypes: await fs.promises.readFile(
|
|
104
110
|
new URL('../template/content-module-types.d.ts', import.meta.url),
|
|
@@ -156,87 +162,3 @@ function shouldOptimizeImage(src: string) {
|
|
|
156
162
|
// Optimize anything that is NOT external or an absolute path to `public/`
|
|
157
163
|
return !isValidUrl(src) && !src.startsWith('/');
|
|
158
164
|
}
|
|
159
|
-
|
|
160
|
-
function validateRenderProperties(markdocConfig: ReadonlyMarkdocConfig, astroConfig: AstroConfig) {
|
|
161
|
-
const tags = markdocConfig.tags ?? {};
|
|
162
|
-
const nodes = markdocConfig.nodes ?? {};
|
|
163
|
-
|
|
164
|
-
for (const [name, config] of Object.entries(tags)) {
|
|
165
|
-
validateRenderProperty({ type: 'tag', name, config, astroConfig });
|
|
166
|
-
}
|
|
167
|
-
for (const [name, config] of Object.entries(nodes)) {
|
|
168
|
-
validateRenderProperty({ type: 'node', name, config, astroConfig });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function validateRenderProperty({
|
|
173
|
-
name,
|
|
174
|
-
config,
|
|
175
|
-
type,
|
|
176
|
-
astroConfig,
|
|
177
|
-
}: {
|
|
178
|
-
name: string;
|
|
179
|
-
config: { render?: string };
|
|
180
|
-
type: 'node' | 'tag';
|
|
181
|
-
astroConfig: Pick<AstroConfig, 'root'>;
|
|
182
|
-
}) {
|
|
183
|
-
if (typeof config.render === 'string' && config.render.length === 0) {
|
|
184
|
-
throw new Error(
|
|
185
|
-
`Invalid ${type} configuration: ${JSON.stringify(
|
|
186
|
-
name
|
|
187
|
-
)}. The "render" property cannot be an empty string.`
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
if (typeof config.render === 'string' && !isCapitalized(config.render)) {
|
|
191
|
-
const astroConfigPath = getAstroConfigPath(fs, fileURLToPath(astroConfig.root));
|
|
192
|
-
throw new MarkdocError({
|
|
193
|
-
message: `Invalid ${type} configuration: ${JSON.stringify(
|
|
194
|
-
name
|
|
195
|
-
)}. The "render" property must reference a capitalized component name.`,
|
|
196
|
-
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',
|
|
197
|
-
location: astroConfigPath
|
|
198
|
-
? {
|
|
199
|
-
file: astroConfigPath,
|
|
200
|
-
}
|
|
201
|
-
: undefined,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function isCapitalized(str: string) {
|
|
207
|
-
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
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
|
-
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Config as MarkdocConfig } from '@markdoc/markdoc';
|
|
2
|
+
import type { AstroConfig } from 'astro';
|
|
3
|
+
import { build as esbuild } from 'esbuild';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_MARKDOC_CONFIG_FILES = [
|
|
8
|
+
'markdoc.config.js',
|
|
9
|
+
'markdoc.config.mjs',
|
|
10
|
+
'markdoc.config.mts',
|
|
11
|
+
'markdoc.config.ts',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export async function loadMarkdocConfig(astroConfig: Pick<AstroConfig, 'root'>) {
|
|
15
|
+
let markdocConfigUrl: URL | undefined;
|
|
16
|
+
for (const filename of SUPPORTED_MARKDOC_CONFIG_FILES) {
|
|
17
|
+
const filePath = new URL(filename, astroConfig.root);
|
|
18
|
+
if (!fs.existsSync(filePath)) continue;
|
|
19
|
+
|
|
20
|
+
markdocConfigUrl = filePath;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
if (!markdocConfigUrl) return;
|
|
24
|
+
|
|
25
|
+
const { code, dependencies } = await bundleConfigFile({
|
|
26
|
+
markdocConfigUrl,
|
|
27
|
+
astroConfig,
|
|
28
|
+
});
|
|
29
|
+
const config: MarkdocConfig = await loadConfigFromBundledFile(astroConfig.root, code);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
config,
|
|
33
|
+
fileUrl: markdocConfigUrl,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Forked from Vite's `bundleConfigFile` function
|
|
39
|
+
* with added handling for `.astro` imports,
|
|
40
|
+
* and removed unused Deno patches.
|
|
41
|
+
* @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961
|
|
42
|
+
*/
|
|
43
|
+
async function bundleConfigFile({
|
|
44
|
+
markdocConfigUrl,
|
|
45
|
+
astroConfig,
|
|
46
|
+
}: {
|
|
47
|
+
markdocConfigUrl: URL;
|
|
48
|
+
astroConfig: Pick<AstroConfig, 'root'>;
|
|
49
|
+
}): Promise<{ code: string; dependencies: string[] }> {
|
|
50
|
+
const result = await esbuild({
|
|
51
|
+
absWorkingDir: fileURLToPath(astroConfig.root),
|
|
52
|
+
entryPoints: [fileURLToPath(markdocConfigUrl)],
|
|
53
|
+
outfile: 'out.js',
|
|
54
|
+
write: false,
|
|
55
|
+
target: ['node16'],
|
|
56
|
+
platform: 'node',
|
|
57
|
+
packages: 'external',
|
|
58
|
+
bundle: true,
|
|
59
|
+
format: 'esm',
|
|
60
|
+
sourcemap: 'inline',
|
|
61
|
+
metafile: true,
|
|
62
|
+
plugins: [
|
|
63
|
+
{
|
|
64
|
+
name: 'stub-astro-imports',
|
|
65
|
+
setup(build) {
|
|
66
|
+
build.onResolve({ filter: /.*\.astro$/ }, () => {
|
|
67
|
+
return {
|
|
68
|
+
// Stub with an unused default export
|
|
69
|
+
path: 'data:text/javascript,export default true',
|
|
70
|
+
external: true,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
const { text } = result.outputFiles[0];
|
|
78
|
+
return {
|
|
79
|
+
code: text,
|
|
80
|
+
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Forked from Vite config loader, replacing CJS-based path concat
|
|
86
|
+
* with ESM only
|
|
87
|
+
* @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074
|
|
88
|
+
*/
|
|
89
|
+
async function loadConfigFromBundledFile(root: URL, code: string): Promise<MarkdocConfig> {
|
|
90
|
+
// Write it to disk, load it with native Node ESM, then delete the file.
|
|
91
|
+
const tmpFileUrl = new URL(`markdoc.config.timestamp-${Date.now()}.mjs`, root);
|
|
92
|
+
fs.writeFileSync(tmpFileUrl, code);
|
|
93
|
+
try {
|
|
94
|
+
return (await import(tmpFileUrl.pathname)).default;
|
|
95
|
+
} finally {
|
|
96
|
+
try {
|
|
97
|
+
fs.unlinkSync(tmpFileUrl);
|
|
98
|
+
} catch {
|
|
99
|
+
// already removed if this function is called twice simultaneously
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type { AstroInstance } from 'astro';
|
|
2
|
-
import z from 'astro/zod';
|
|
3
1
|
import matter from 'gray-matter';
|
|
4
|
-
import type fsMod from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
2
|
import type { ErrorPayload as ViteErrorPayload } from 'vite';
|
|
7
3
|
|
|
8
4
|
/**
|
|
@@ -85,28 +81,6 @@ interface ErrorProperties {
|
|
|
85
81
|
frame?: string;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
/**
|
|
89
|
-
* Matches `search` function used for resolving `astro.config` files.
|
|
90
|
-
* Used by Markdoc for error handling.
|
|
91
|
-
* @see 'astro/src/core/config/config.ts'
|
|
92
|
-
*/
|
|
93
|
-
export function getAstroConfigPath(fs: typeof fsMod, root: string): string | undefined {
|
|
94
|
-
const paths = [
|
|
95
|
-
'astro.config.mjs',
|
|
96
|
-
'astro.config.js',
|
|
97
|
-
'astro.config.ts',
|
|
98
|
-
'astro.config.mts',
|
|
99
|
-
'astro.config.cjs',
|
|
100
|
-
'astro.config.cts',
|
|
101
|
-
].map((p) => path.join(root, p));
|
|
102
|
-
|
|
103
|
-
for (const file of paths) {
|
|
104
|
-
if (fs.existsSync(file)) {
|
|
105
|
-
return file;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
84
|
/**
|
|
111
85
|
* @see 'astro/src/core/path.ts'
|
|
112
86
|
*/
|
|
@@ -114,38 +88,6 @@ export function prependForwardSlash(str: string) {
|
|
|
114
88
|
return str[0] === '/' ? str : '/' + str;
|
|
115
89
|
}
|
|
116
90
|
|
|
117
|
-
export function validateComponentsProp(components: Record<string, AstroInstance['default']>) {
|
|
118
|
-
try {
|
|
119
|
-
componentsPropValidator.parse(components);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
throw new MarkdocError({
|
|
122
|
-
message:
|
|
123
|
-
e instanceof z.ZodError
|
|
124
|
-
? e.issues[0].message
|
|
125
|
-
: 'Invalid `components` prop. Ensure you are passing an object of components to <Content />',
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const componentsPropValidator = z.record(
|
|
131
|
-
z
|
|
132
|
-
.string()
|
|
133
|
-
.min(1, 'Invalid `components` prop. Component names cannot be empty!')
|
|
134
|
-
.refine(
|
|
135
|
-
(value) => isCapitalized(value),
|
|
136
|
-
(value) => ({
|
|
137
|
-
message: `Invalid \`components\` prop: ${JSON.stringify(
|
|
138
|
-
value
|
|
139
|
-
)}. 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)`,
|
|
140
|
-
})
|
|
141
|
-
),
|
|
142
|
-
z.any()
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
export function isCapitalized(str: string) {
|
|
146
|
-
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
91
|
export function isValidUrl(str: string): boolean {
|
|
150
92
|
try {
|
|
151
93
|
new URL(str);
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
declare module 'astro:content' {
|
|
2
2
|
interface Render {
|
|
3
3
|
'.mdoc': Promise<{
|
|
4
|
-
Content(props: {
|
|
5
|
-
components?: Record<string, import('astro').AstroInstance['default']>;
|
|
6
|
-
}): import('astro').MarkdownInstance<{}>['Content'];
|
|
4
|
+
Content(props: Record<string, any>): import('astro').MarkdownInstance<{}>['Content'];
|
|
7
5
|
}>;
|
|
8
6
|
}
|
|
9
7
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseHTML } from 'linkedom';
|
|
2
1
|
import { parse as parseDevalue } from 'devalue';
|
|
3
2
|
import { expect } from 'chai';
|
|
4
3
|
import { loadFixture, fixLineEndings } from '../../../astro/test/test-utils.js';
|
|
@@ -37,70 +36,20 @@ describe('Markdoc - Content Collections', () => {
|
|
|
37
36
|
it('loads entry', async () => {
|
|
38
37
|
const res = await baseFixture.fetch('/entry.json');
|
|
39
38
|
const post = parseDevalue(await res.text());
|
|
40
|
-
expect(formatPost(post)).to.deep.equal(
|
|
39
|
+
expect(formatPost(post)).to.deep.equal(post1Entry);
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
it('loads collection', async () => {
|
|
44
43
|
const res = await baseFixture.fetch('/collection.json');
|
|
45
44
|
const posts = parseDevalue(await res.text());
|
|
46
45
|
expect(posts).to.not.be.null;
|
|
46
|
+
|
|
47
47
|
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
post1Entry,
|
|
49
|
+
post2Entry,
|
|
50
|
+
post3Entry,
|
|
51
51
|
]);
|
|
52
52
|
});
|
|
53
|
-
|
|
54
|
-
it('renders content - simple', async () => {
|
|
55
|
-
const res = await baseFixture.fetch('/content-simple');
|
|
56
|
-
const html = await res.text();
|
|
57
|
-
const { document } = parseHTML(html);
|
|
58
|
-
const h2 = document.querySelector('h2');
|
|
59
|
-
expect(h2.textContent).to.equal('Simple post');
|
|
60
|
-
const p = document.querySelector('p');
|
|
61
|
-
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('renders content - with config', async () => {
|
|
65
|
-
const fixture = await getFixtureWithConfig();
|
|
66
|
-
const server = await fixture.startDevServer();
|
|
67
|
-
|
|
68
|
-
const res = await fixture.fetch('/content-with-config');
|
|
69
|
-
const html = await res.text();
|
|
70
|
-
const { document } = parseHTML(html);
|
|
71
|
-
const h2 = document.querySelector('h2');
|
|
72
|
-
expect(h2.textContent).to.equal('Post with config');
|
|
73
|
-
const textContent = html;
|
|
74
|
-
|
|
75
|
-
expect(textContent).to.not.include('Hello');
|
|
76
|
-
expect(textContent).to.include('Hola');
|
|
77
|
-
expect(textContent).to.include(`Konnichiwa`);
|
|
78
|
-
|
|
79
|
-
await server.stop();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('renders content - with components', async () => {
|
|
83
|
-
const fixture = await getFixtureWithComponents();
|
|
84
|
-
const server = await fixture.startDevServer();
|
|
85
|
-
|
|
86
|
-
const res = await fixture.fetch('/content-with-components');
|
|
87
|
-
const html = await res.text();
|
|
88
|
-
const { document } = parseHTML(html);
|
|
89
|
-
const h2 = document.querySelector('h2');
|
|
90
|
-
expect(h2.textContent).to.equal('Post with components');
|
|
91
|
-
|
|
92
|
-
// Renders custom shortcode component
|
|
93
|
-
const marquee = document.querySelector('marquee');
|
|
94
|
-
expect(marquee).to.not.be.null;
|
|
95
|
-
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
|
96
|
-
|
|
97
|
-
// Renders Astro Code component
|
|
98
|
-
const pre = document.querySelector('pre');
|
|
99
|
-
expect(pre).to.not.be.null;
|
|
100
|
-
expect(pre.className).to.equal('astro-code');
|
|
101
|
-
|
|
102
|
-
await server.stop();
|
|
103
|
-
});
|
|
104
53
|
});
|
|
105
54
|
|
|
106
55
|
describe('build', () => {
|
|
@@ -111,7 +60,7 @@ describe('Markdoc - Content Collections', () => {
|
|
|
111
60
|
it('loads entry', async () => {
|
|
112
61
|
const res = await baseFixture.readFile('/entry.json');
|
|
113
62
|
const post = parseDevalue(res);
|
|
114
|
-
expect(formatPost(post)).to.deep.equal(
|
|
63
|
+
expect(formatPost(post)).to.deep.equal(post1Entry);
|
|
115
64
|
});
|
|
116
65
|
|
|
117
66
|
it('loads collection', async () => {
|
|
@@ -119,140 +68,43 @@ describe('Markdoc - Content Collections', () => {
|
|
|
119
68
|
const posts = parseDevalue(res);
|
|
120
69
|
expect(posts).to.not.be.null;
|
|
121
70
|
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
71
|
+
post1Entry,
|
|
72
|
+
post2Entry,
|
|
73
|
+
post3Entry,
|
|
125
74
|
]);
|
|
126
75
|
});
|
|
127
|
-
|
|
128
|
-
it('renders content - simple', async () => {
|
|
129
|
-
const html = await baseFixture.readFile('/content-simple/index.html');
|
|
130
|
-
const { document } = parseHTML(html);
|
|
131
|
-
const h2 = document.querySelector('h2');
|
|
132
|
-
expect(h2.textContent).to.equal('Simple post');
|
|
133
|
-
const p = document.querySelector('p');
|
|
134
|
-
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('renders content - with config', async () => {
|
|
138
|
-
const fixture = await getFixtureWithConfig();
|
|
139
|
-
await fixture.build();
|
|
140
|
-
|
|
141
|
-
const html = await fixture.readFile('/content-with-config/index.html');
|
|
142
|
-
const { document } = parseHTML(html);
|
|
143
|
-
const h2 = document.querySelector('h2');
|
|
144
|
-
expect(h2.textContent).to.equal('Post with config');
|
|
145
|
-
const textContent = html;
|
|
146
|
-
|
|
147
|
-
expect(textContent).to.not.include('Hello');
|
|
148
|
-
expect(textContent).to.include('Hola');
|
|
149
|
-
expect(textContent).to.include(`Konnichiwa`);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('renders content - with components', async () => {
|
|
153
|
-
const fixture = await getFixtureWithComponents();
|
|
154
|
-
await fixture.build();
|
|
155
|
-
|
|
156
|
-
const html = await fixture.readFile('/content-with-components/index.html');
|
|
157
|
-
const { document } = parseHTML(html);
|
|
158
|
-
const h2 = document.querySelector('h2');
|
|
159
|
-
expect(h2.textContent).to.equal('Post with components');
|
|
160
|
-
|
|
161
|
-
// Renders custom shortcode component
|
|
162
|
-
const marquee = document.querySelector('marquee');
|
|
163
|
-
expect(marquee).to.not.be.null;
|
|
164
|
-
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
|
165
|
-
|
|
166
|
-
// Renders Astro Code component
|
|
167
|
-
const pre = document.querySelector('pre');
|
|
168
|
-
expect(pre).to.not.be.null;
|
|
169
|
-
expect(pre.className).to.equal('astro-code');
|
|
170
|
-
});
|
|
171
76
|
});
|
|
172
77
|
});
|
|
173
78
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
integrations: [
|
|
178
|
-
markdoc({
|
|
179
|
-
variables: {
|
|
180
|
-
countries: ['ES', 'JP'],
|
|
181
|
-
},
|
|
182
|
-
functions: {
|
|
183
|
-
includes: {
|
|
184
|
-
transform(parameters) {
|
|
185
|
-
const [array, value] = Object.values(parameters);
|
|
186
|
-
return Array.isArray(array) ? array.includes(value) : false;
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
}),
|
|
191
|
-
],
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function getFixtureWithComponents() {
|
|
196
|
-
return loadFixture({
|
|
197
|
-
root,
|
|
198
|
-
integrations: [
|
|
199
|
-
markdoc({
|
|
200
|
-
nodes: {
|
|
201
|
-
fence: {
|
|
202
|
-
render: 'Code',
|
|
203
|
-
attributes: {
|
|
204
|
-
language: { type: String },
|
|
205
|
-
content: { type: String },
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
tags: {
|
|
210
|
-
mq: {
|
|
211
|
-
render: 'CustomMarquee',
|
|
212
|
-
attributes: {
|
|
213
|
-
direction: {
|
|
214
|
-
type: String,
|
|
215
|
-
default: 'left',
|
|
216
|
-
matches: ['left', 'right', 'up', 'down'],
|
|
217
|
-
errorLevel: 'critical',
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
}),
|
|
223
|
-
],
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const simplePostEntry = {
|
|
228
|
-
id: 'simple.mdoc',
|
|
229
|
-
slug: 'simple',
|
|
79
|
+
const post1Entry = {
|
|
80
|
+
id: 'post-1.mdoc',
|
|
81
|
+
slug: 'post-1',
|
|
230
82
|
collection: 'blog',
|
|
231
83
|
data: {
|
|
232
84
|
schemaWorks: true,
|
|
233
|
-
title: '
|
|
85
|
+
title: 'Post 1',
|
|
234
86
|
},
|
|
235
|
-
body: '\n##
|
|
87
|
+
body: '\n## Post 1\n\nThis is the contents of post 1.\n',
|
|
236
88
|
};
|
|
237
89
|
|
|
238
|
-
const
|
|
239
|
-
id: '
|
|
240
|
-
slug: '
|
|
90
|
+
const post2Entry = {
|
|
91
|
+
id: 'post-2.mdoc',
|
|
92
|
+
slug: 'post-2',
|
|
241
93
|
collection: 'blog',
|
|
242
94
|
data: {
|
|
243
95
|
schemaWorks: true,
|
|
244
|
-
title: 'Post
|
|
96
|
+
title: 'Post 2',
|
|
245
97
|
},
|
|
246
|
-
body: '\n## Post
|
|
98
|
+
body: '\n## Post 2\n\nThis is the contents of post 2.\n',
|
|
247
99
|
};
|
|
248
100
|
|
|
249
|
-
const
|
|
250
|
-
id: '
|
|
251
|
-
slug: '
|
|
101
|
+
const post3Entry = {
|
|
102
|
+
id: 'post-3.mdoc',
|
|
103
|
+
slug: 'post-3',
|
|
252
104
|
collection: 'blog',
|
|
253
105
|
data: {
|
|
254
106
|
schemaWorks: true,
|
|
255
|
-
title: 'Post
|
|
107
|
+
title: 'Post 3',
|
|
256
108
|
},
|
|
257
|
-
body: '\n## Post
|
|
109
|
+
body: '\n## Post 3\n\nThis is the contents of post 3.\n',
|
|
258
110
|
};
|