1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { isFrontmatterValid } from '@astrojs/markdown-remark';
import type { Root, RootContent } from 'hast';
import type { VFile } from 'vfile';
import { jsToTreeNode } from './utils.js';
// Passed metadata to help determine adding charset utf8 by default
declare module 'vfile' {
interface DataMap {
applyFrontmatterExport?: {
srcDir?: URL;
};
}
}
const exportConstPartialTrueRe = /export\s+const\s+partial\s*=\s*true/;
export function rehypeApplyFrontmatterExport() {
return function (tree: Root, vfile: VFile) {
const frontmatter = vfile.data.astro?.frontmatter;
if (!frontmatter || !isFrontmatterValid(frontmatter))
throw new Error(
// Copied from Astro core `errors-data`
// TODO: find way to import error data from core
'[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.',
);
const extraChildren: RootContent[] = [
jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
];
if (frontmatter.layout) {
extraChildren.unshift(
jsToTreeNode(
// NOTE: Use `__astro_*` import names to prevent conflicts with user code
/** @see 'vite-plugin-markdown' for layout props reference */
`\
import { jsx as __astro_layout_jsx__ } from 'astro/jsx-runtime';
import __astro_layout_component__ from ${JSON.stringify(frontmatter.layout)};
export default function ({ children }) {
const { layout, ...content } = frontmatter;
content.file = file;
content.url = url;
return __astro_layout_jsx__(__astro_layout_component__, {
file,
url,
content,
frontmatter: content,
headings: getHeadings(),
'server:root': true,
children,
});
};`,
),
);
} else if (shouldAddCharset(tree, vfile)) {
extraChildren.unshift({
type: 'mdxJsxFlowElement',
name: 'meta',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'charset',
value: 'utf-8',
},
],
children: [],
});
}
tree.children = extraChildren.concat(tree.children);
};
}
/**
* If this is a page (e.g. in src/pages), has no layout frontmatter (handled before calling this function),
* has no leading component that looks like a wrapping layout, and `partial` isn't set to true, we default to
* adding charset=utf-8 like markdown so that users don't have to worry about it for MDX pages without layouts.
*/
function shouldAddCharset(tree: Root, vfile: VFile) {
const srcDirUrl = vfile.data.applyFrontmatterExport?.srcDir;
if (!srcDirUrl) return false;
const hasConstPartialTrue = tree.children.some(
(node) => node.type === 'mdxjsEsm' && exportConstPartialTrueRe.test(node.value),
);
if (hasConstPartialTrue) return false;
// NOTE: the pages directory is a non-configurable Astro behaviour
const pagesDir = path.join(fileURLToPath(srcDirUrl), 'pages').replace(/\\/g, '/');
// `vfile.path` comes from Vite, which is a normalized path (no backslashes)
const filePath = vfile.path;
if (!filePath.startsWith(pagesDir)) return false;
const hasLeadingUnderscoreInPath = filePath
.slice(pagesDir.length)
.replace(/\\/g, '/')
.split('/')
.some((part) => part.startsWith('_'));
if (hasLeadingUnderscoreInPath) return false;
// Bail if the first content found is a wrapping layout component
for (const child of tree.children) {
if (child.type === 'element') break;
if (child.type === 'mdxJsxFlowElement') {
// If is fragment or lowercase tag name (html tags), skip and assume there's no layout
if (child.name == null) break;
if (child.name[0] === child.name[0].toLowerCase()) break;
return false;
}
}
return true;
}
|