@astrojs/mdx 0.13.0 → 0.15.0-beta.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 +142 -0
- package/README.md +70 -86
- package/dist/index.d.ts +8 -14
- package/dist/index.js +39 -37
- package/dist/plugins.d.ts +2 -3
- package/dist/plugins.js +58 -89
- package/dist/rehype-collect-headings.d.ts +2 -6
- package/dist/rehype-collect-headings.js +4 -39
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +14 -7
- package/package.json +5 -4
- package/src/index.ts +55 -56
- package/src/plugins.ts +73 -110
- package/src/rehype-collect-headings.ts +4 -43
- package/src/utils.ts +16 -11
- package/test/fixtures/mdx-frontmatter-injection/astro.config.mjs +2 -2
- package/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs +7 -0
- package/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx +1 -0
- package/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx +1 -0
- package/test/mdx-frontmatter-injection.test.js +4 -7
- package/test/mdx-get-headings.test.js +91 -0
- package/test/mdx-plugins.test.js +50 -84
- package/test/mdx-syntax-highlighting.test.js +26 -0
- package/test/fixtures/mdx-frontmatter-injection/src/pages/with-overrides.mdx +0 -7
package/src/plugins.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
+
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
2
|
+
import {
|
|
3
|
+
InvalidAstroDataError,
|
|
4
|
+
safelyGetAstroData,
|
|
5
|
+
} from '@astrojs/markdown-remark/dist/internal.js';
|
|
1
6
|
import { nodeTypes } from '@mdx-js/mdx';
|
|
2
7
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
|
3
8
|
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
|
4
|
-
import type { AstroConfig
|
|
9
|
+
import type { AstroConfig } from 'astro';
|
|
5
10
|
import type { Literal, MemberExpression } from 'estree';
|
|
6
11
|
import { visit as estreeVisit } from 'estree-util-visit';
|
|
7
12
|
import { bold, yellow } from 'kleur/colors';
|
|
13
|
+
import type { Image } from 'mdast';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
8
15
|
import rehypeRaw from 'rehype-raw';
|
|
9
16
|
import remarkGfm from 'remark-gfm';
|
|
10
|
-
import
|
|
11
|
-
import type {
|
|
17
|
+
import { visit } from 'unist-util-visit';
|
|
18
|
+
import type { VFile } from 'vfile';
|
|
12
19
|
import { MdxOptions } from './index.js';
|
|
13
|
-
import
|
|
20
|
+
import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
|
|
14
21
|
import rehypeMetaString from './rehype-meta-string.js';
|
|
15
22
|
import remarkPrism from './remark-prism.js';
|
|
16
23
|
import remarkShiki from './remark-shiki.js';
|
|
17
|
-
import { jsToTreeNode } from './utils.js';
|
|
24
|
+
import { isRelativePath, jsToTreeNode } from './utils.js';
|
|
18
25
|
|
|
19
26
|
export function recmaInjectImportMetaEnvPlugin({
|
|
20
27
|
importMetaEnv,
|
|
@@ -43,26 +50,18 @@ export function recmaInjectImportMetaEnvPlugin({
|
|
|
43
50
|
};
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
export function
|
|
53
|
+
export function rehypeApplyFrontmatterExport() {
|
|
47
54
|
return function (tree: any, vfile: VFile) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data);
|
|
57
|
-
const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter };
|
|
55
|
+
const astroData = safelyGetAstroData(vfile.data);
|
|
56
|
+
if (astroData instanceof InvalidAstroDataError)
|
|
57
|
+
throw new Error(
|
|
58
|
+
// Copied from Astro core `errors-data`
|
|
59
|
+
// TODO: find way to import error data from core
|
|
60
|
+
'[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`.'
|
|
61
|
+
);
|
|
62
|
+
const { frontmatter } = astroData;
|
|
58
63
|
const exportNodes = [
|
|
59
|
-
jsToTreeNode(
|
|
60
|
-
`export const frontmatter = ${JSON.stringify(
|
|
61
|
-
frontmatter
|
|
62
|
-
)};\nexport const _internal = { injectedFrontmatter: ${JSON.stringify(
|
|
63
|
-
injectedFrontmatter
|
|
64
|
-
)} };`
|
|
65
|
-
),
|
|
64
|
+
jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
|
|
66
65
|
];
|
|
67
66
|
if (frontmatter.layout) {
|
|
68
67
|
// NOTE(bholmesdev) 08-22-2022
|
|
@@ -112,80 +111,79 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
|
|
|
112
111
|
};
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
/**
|
|
115
|
+
* `src/content/` does not support relative image paths.
|
|
116
|
+
* This plugin throws an error if any are found
|
|
117
|
+
*/
|
|
118
|
+
function toRemarkContentRelImageError({ srcDir }: { srcDir: URL }) {
|
|
119
|
+
const contentDir = new URL('content/', srcDir);
|
|
120
|
+
return function remarkContentRelImageError() {
|
|
121
|
+
return (tree: any, vfile: VFile) => {
|
|
122
|
+
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
|
|
123
|
+
if (!isContentFile) return;
|
|
124
|
+
|
|
125
|
+
const relImagePaths = new Set<string>();
|
|
126
|
+
visit(tree, 'image', function raiseError(node: Image) {
|
|
127
|
+
if (isRelativePath(node.url)) {
|
|
128
|
+
relImagePaths.add(node.url);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (relImagePaths.size === 0) return;
|
|
132
|
+
|
|
133
|
+
const errorMessage =
|
|
134
|
+
`Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files):\n` +
|
|
135
|
+
[...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');
|
|
136
|
+
|
|
137
|
+
throw new Error(errorMessage);
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}
|
|
117
141
|
|
|
118
142
|
export async function getRemarkPlugins(
|
|
119
143
|
mdxOptions: MdxOptions,
|
|
120
144
|
config: AstroConfig
|
|
121
145
|
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
|
|
122
|
-
let remarkPlugins: PluggableList = [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
];
|
|
126
|
-
switch (mdxOptions.extendPlugins) {
|
|
127
|
-
case false:
|
|
128
|
-
break;
|
|
129
|
-
case 'astroDefaults':
|
|
130
|
-
remarkPlugins = [...remarkPlugins, ...DEFAULT_REMARK_PLUGINS];
|
|
131
|
-
break;
|
|
132
|
-
default:
|
|
133
|
-
remarkPlugins = [
|
|
134
|
-
...remarkPlugins,
|
|
135
|
-
...(markdownShouldExtendDefaultPlugins(config) ? DEFAULT_REMARK_PLUGINS : []),
|
|
136
|
-
...ignoreStringPlugins(config.markdown.remarkPlugins ?? []),
|
|
137
|
-
];
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
if (config.markdown.syntaxHighlight === 'shiki') {
|
|
141
|
-
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
|
|
146
|
+
let remarkPlugins: PluggableList = [];
|
|
147
|
+
if (mdxOptions.syntaxHighlight === 'shiki') {
|
|
148
|
+
remarkPlugins.push([await remarkShiki(mdxOptions.shikiConfig)]);
|
|
142
149
|
}
|
|
143
|
-
if (
|
|
150
|
+
if (mdxOptions.syntaxHighlight === 'prism') {
|
|
144
151
|
remarkPlugins.push(remarkPrism);
|
|
145
152
|
}
|
|
153
|
+
if (mdxOptions.gfm) {
|
|
154
|
+
remarkPlugins.push(remarkGfm);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
|
|
146
158
|
|
|
147
|
-
|
|
159
|
+
// Apply last in case user plugins resolve relative image paths
|
|
160
|
+
if (config.experimental.contentCollections) {
|
|
161
|
+
remarkPlugins.push(toRemarkContentRelImageError(config));
|
|
162
|
+
}
|
|
148
163
|
return remarkPlugins;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
export function getRehypePlugins(
|
|
152
|
-
mdxOptions: MdxOptions,
|
|
153
|
-
config: AstroConfig
|
|
154
|
-
): MdxRollupPluginOptions['rehypePlugins'] {
|
|
166
|
+
export function getRehypePlugins(mdxOptions: MdxOptions): MdxRollupPluginOptions['rehypePlugins'] {
|
|
155
167
|
let rehypePlugins: PluggableList = [
|
|
156
|
-
// getHeadings() is guaranteed by TS, so we can't allow user to override
|
|
157
|
-
rehypeCollectHeadings,
|
|
158
168
|
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
|
|
159
169
|
rehypeMetaString,
|
|
160
170
|
// rehypeRaw allows custom syntax highlighters to work without added config
|
|
161
171
|
[rehypeRaw, { passThrough: nodeTypes }] as any,
|
|
162
172
|
];
|
|
163
|
-
switch (mdxOptions.extendPlugins) {
|
|
164
|
-
case false:
|
|
165
|
-
break;
|
|
166
|
-
case 'astroDefaults':
|
|
167
|
-
rehypePlugins = [...rehypePlugins, ...DEFAULT_REHYPE_PLUGINS];
|
|
168
|
-
break;
|
|
169
|
-
default:
|
|
170
|
-
rehypePlugins = [
|
|
171
|
-
...rehypePlugins,
|
|
172
|
-
...(markdownShouldExtendDefaultPlugins(config) ? DEFAULT_REHYPE_PLUGINS : []),
|
|
173
|
-
...ignoreStringPlugins(config.markdown.rehypePlugins ?? []),
|
|
174
|
-
];
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
173
|
|
|
178
|
-
rehypePlugins = [
|
|
174
|
+
rehypePlugins = [
|
|
175
|
+
...rehypePlugins,
|
|
176
|
+
...ignoreStringPlugins(mdxOptions.rehypePlugins),
|
|
177
|
+
// getHeadings() is guaranteed by TS, so this must be included.
|
|
178
|
+
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
|
|
179
|
+
rehypeHeadingIds,
|
|
180
|
+
rehypeInjectHeadingsExport,
|
|
181
|
+
// computed from `astro.data.frontmatter` in VFile data
|
|
182
|
+
rehypeApplyFrontmatterExport,
|
|
183
|
+
];
|
|
179
184
|
return rehypePlugins;
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
function markdownShouldExtendDefaultPlugins(config: AstroConfig): boolean {
|
|
183
|
-
return (
|
|
184
|
-
config.markdown.extendDefaultPlugins ||
|
|
185
|
-
(config.markdown.remarkPlugins.length === 0 && config.markdown.rehypePlugins.length === 0)
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
187
|
function ignoreStringPlugins(plugins: any[]) {
|
|
190
188
|
let validPlugins: PluggableList = [];
|
|
191
189
|
let hasInvalidPlugin = false;
|
|
@@ -208,41 +206,6 @@ function ignoreStringPlugins(plugins: any[]) {
|
|
|
208
206
|
return validPlugins;
|
|
209
207
|
}
|
|
210
208
|
|
|
211
|
-
/**
|
|
212
|
-
* Copied from markdown utils
|
|
213
|
-
* @see "vite-plugin-utils"
|
|
214
|
-
*/
|
|
215
|
-
function isValidAstroData(obj: unknown): obj is MarkdownAstroData {
|
|
216
|
-
if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) {
|
|
217
|
-
const { frontmatter } = obj as any;
|
|
218
|
-
try {
|
|
219
|
-
// ensure frontmatter is JSON-serializable
|
|
220
|
-
JSON.stringify(frontmatter);
|
|
221
|
-
} catch {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
return typeof frontmatter === 'object' && frontmatter !== null;
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Copied from markdown utils
|
|
231
|
-
* @see "vite-plugin-utils"
|
|
232
|
-
*/
|
|
233
|
-
function safelyGetAstroData(vfileData: Data): MarkdownAstroData {
|
|
234
|
-
const { astro } = vfileData;
|
|
235
|
-
|
|
236
|
-
if (!astro) return { frontmatter: {} };
|
|
237
|
-
if (!isValidAstroData(astro)) {
|
|
238
|
-
throw Error(
|
|
239
|
-
`[MDX] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return astro;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
209
|
/**
|
|
247
210
|
* Check if estree entry is "import.meta.env.VARIABLE"
|
|
248
211
|
* If it is, return the variable name (i.e. "VARIABLE")
|
|
@@ -1,48 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { visit } from 'unist-util-visit';
|
|
1
|
+
import { MarkdownHeading, MarkdownVFile } from '@astrojs/markdown-remark';
|
|
3
2
|
import { jsToTreeNode } from './utils.js';
|
|
4
3
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
text: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export default function rehypeCollectHeadings() {
|
|
12
|
-
const slugger = new Slugger();
|
|
13
|
-
return function (tree: any) {
|
|
14
|
-
const headings: MarkdownHeading[] = [];
|
|
15
|
-
visit(tree, (node) => {
|
|
16
|
-
if (node.type !== 'element') return;
|
|
17
|
-
const { tagName } = node;
|
|
18
|
-
if (tagName[0] !== 'h') return;
|
|
19
|
-
const [_, level] = tagName.match(/h([0-6])/) ?? [];
|
|
20
|
-
if (!level) return;
|
|
21
|
-
const depth = Number.parseInt(level);
|
|
22
|
-
|
|
23
|
-
let text = '';
|
|
24
|
-
visit(node, (child, __, parent) => {
|
|
25
|
-
if (child.type === 'element' || parent == null) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (child.type === 'raw' && child.value.match(/^\n?<.*>\n?$/)) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (new Set(['text', 'raw', 'mdxTextExpression']).has(child.type)) {
|
|
32
|
-
text += child.value;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
node.properties = node.properties || {};
|
|
37
|
-
if (typeof node.properties.id !== 'string') {
|
|
38
|
-
let slug = slugger.slug(text);
|
|
39
|
-
if (slug.endsWith('-')) {
|
|
40
|
-
slug = slug.slice(0, -1);
|
|
41
|
-
}
|
|
42
|
-
node.properties.id = slug;
|
|
43
|
-
}
|
|
44
|
-
headings.push({ depth, slug: node.properties.id, text });
|
|
45
|
-
});
|
|
4
|
+
export function rehypeInjectHeadingsExport() {
|
|
5
|
+
return function (tree: any, file: MarkdownVFile) {
|
|
6
|
+
const headings: MarkdownHeading[] = file.data.__astroHeadings || [];
|
|
46
7
|
tree.children.unshift(
|
|
47
8
|
jsToTreeNode(`export function getHeadings() { return ${JSON.stringify(headings)} }`)
|
|
48
9
|
);
|
package/src/utils.ts
CHANGED
|
@@ -83,15 +83,20 @@ export function jsToTreeNode(
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
export function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
// Following utils taken from `packages/astro/src/core/path.ts`:
|
|
87
|
+
export function isRelativePath(path: string) {
|
|
88
|
+
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function startsWithDotDotSlash(path: string) {
|
|
92
|
+
const c1 = path[0];
|
|
93
|
+
const c2 = path[1];
|
|
94
|
+
const c3 = path[2];
|
|
95
|
+
return c1 === '.' && c2 === '.' && c3 === '/';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startsWithDotSlash(path: string) {
|
|
99
|
+
const c1 = path[0];
|
|
100
|
+
const c2 = path[1];
|
|
101
|
+
return c1 === '.' && c2 === '/';
|
|
97
102
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { defineConfig } from 'astro/config';
|
|
2
2
|
import mdx from '@astrojs/mdx';
|
|
3
|
-
import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs';
|
|
3
|
+
import { rehypeReadingTime, remarkDescription, remarkTitle } from './src/markdown-plugins.mjs';
|
|
4
4
|
|
|
5
5
|
// https://astro.build/config
|
|
6
6
|
export default defineConfig({
|
|
7
7
|
site: 'https://astro.build/',
|
|
8
8
|
integrations: [mdx({
|
|
9
|
-
remarkPlugins: [remarkTitle],
|
|
9
|
+
remarkPlugins: [remarkTitle, remarkDescription],
|
|
10
10
|
rehypePlugins: [rehypeReadingTime],
|
|
11
11
|
})],
|
|
12
12
|
});
|
|
@@ -18,3 +18,10 @@ export function remarkTitle() {
|
|
|
18
18
|
});
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
export function remarkDescription() {
|
|
23
|
+
return function (tree, vfile) {
|
|
24
|
+
const { frontmatter } = vfile.data.astro;
|
|
25
|
+
frontmatter.description = `Processed by remarkDescription plugin: ${frontmatter.description}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -33,14 +33,11 @@ describe('MDX frontmatter injection', () => {
|
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('
|
|
36
|
+
it('allow user frontmatter mutation', async () => {
|
|
37
37
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
|
|
42
|
-
expect(titles).to.contain('Overridden title');
|
|
43
|
-
expect(readingTimes).to.contain('1000 min read');
|
|
38
|
+
const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description);
|
|
39
|
+
expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 1 description');
|
|
40
|
+
expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 2 description');
|
|
44
41
|
});
|
|
45
42
|
|
|
46
43
|
it('passes injected frontmatter to layouts', async () => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
1
2
|
import mdx from '@astrojs/mdx';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
2
4
|
|
|
3
5
|
import { expect } from 'chai';
|
|
4
6
|
import { parseHTML } from 'linkedom';
|
|
@@ -58,3 +60,92 @@ describe('MDX getHeadings', () => {
|
|
|
58
60
|
);
|
|
59
61
|
});
|
|
60
62
|
});
|
|
63
|
+
|
|
64
|
+
describe('MDX heading IDs can be customized by user plugins', () => {
|
|
65
|
+
let fixture;
|
|
66
|
+
|
|
67
|
+
before(async () => {
|
|
68
|
+
fixture = await loadFixture({
|
|
69
|
+
root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
|
|
70
|
+
integrations: [mdx()],
|
|
71
|
+
markdown: {
|
|
72
|
+
rehypePlugins: [
|
|
73
|
+
() => (tree) => {
|
|
74
|
+
let count = 0;
|
|
75
|
+
visit(tree, 'element', (node, index, parent) => {
|
|
76
|
+
if (!/^h\d$/.test(node.tagName)) return;
|
|
77
|
+
if (!node.properties?.id) {
|
|
78
|
+
node.properties = { ...node.properties, id: String(count++) };
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await fixture.build();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('adds user-specified IDs to HTML output', async () => {
|
|
90
|
+
const html = await fixture.readFile('/test/index.html');
|
|
91
|
+
const { document } = parseHTML(html);
|
|
92
|
+
|
|
93
|
+
const h1 = document.querySelector('h1');
|
|
94
|
+
expect(h1?.textContent).to.equal('Heading test');
|
|
95
|
+
expect(h1?.getAttribute('id')).to.equal('0');
|
|
96
|
+
|
|
97
|
+
const headingIDs = document.querySelectorAll('h1,h2,h3').map((el) => el.id);
|
|
98
|
+
expect(JSON.stringify(headingIDs)).to.equal(
|
|
99
|
+
JSON.stringify(Array.from({ length: headingIDs.length }, (_, idx) => String(idx)))
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('generates correct getHeadings() export', async () => {
|
|
104
|
+
const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
|
|
105
|
+
expect(JSON.stringify(headingsByPage['./test.mdx'])).to.equal(
|
|
106
|
+
JSON.stringify([
|
|
107
|
+
{ depth: 1, slug: '0', text: 'Heading test' },
|
|
108
|
+
{ depth: 2, slug: '1', text: 'Section 1' },
|
|
109
|
+
{ depth: 3, slug: '2', text: 'Subsection 1' },
|
|
110
|
+
{ depth: 3, slug: '3', text: 'Subsection 2' },
|
|
111
|
+
{ depth: 2, slug: '4', text: 'Section 2' },
|
|
112
|
+
])
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('MDX heading IDs can be injected before user plugins', () => {
|
|
118
|
+
let fixture;
|
|
119
|
+
|
|
120
|
+
before(async () => {
|
|
121
|
+
fixture = await loadFixture({
|
|
122
|
+
root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
|
|
123
|
+
integrations: [
|
|
124
|
+
mdx({
|
|
125
|
+
rehypePlugins: [
|
|
126
|
+
rehypeHeadingIds,
|
|
127
|
+
() => (tree) => {
|
|
128
|
+
visit(tree, 'element', (node, index, parent) => {
|
|
129
|
+
if (!/^h\d$/.test(node.tagName)) return;
|
|
130
|
+
if (node.properties?.id) {
|
|
131
|
+
node.children.push({ type: 'text', value: ' ' + node.properties.id });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await fixture.build();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('adds user-specified IDs to HTML output', async () => {
|
|
144
|
+
const html = await fixture.readFile('/test/index.html');
|
|
145
|
+
const { document } = parseHTML(html);
|
|
146
|
+
|
|
147
|
+
const h1 = document.querySelector('h1');
|
|
148
|
+
expect(h1?.textContent).to.equal('Heading test heading-test');
|
|
149
|
+
expect(h1?.id).to.equal('heading-test');
|
|
150
|
+
});
|
|
151
|
+
});
|
package/test/mdx-plugins.test.js
CHANGED
|
@@ -80,91 +80,57 @@ describe('MDX plugins', () => {
|
|
|
80
80
|
expect(selectTocLink(document)).to.be.null;
|
|
81
81
|
});
|
|
82
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
rehypePlugins: [rehypeExamplePlugin],
|
|
133
|
-
extendPlugins: 'astroDefaults',
|
|
134
|
-
}),
|
|
135
|
-
],
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const html = await fixture.readFile(FILE);
|
|
139
|
-
const { document } = parseHTML(html);
|
|
140
|
-
|
|
141
|
-
expect(selectGfmLink(document)).to.not.be.null;
|
|
142
|
-
// remark and rehype plugins still respected
|
|
143
|
-
expect(selectRemarkExample(document)).to.not.be.null;
|
|
144
|
-
expect(selectRehypeExample(document)).to.not.be.null;
|
|
145
|
-
// Does NOT inherit TOC from markdown config
|
|
146
|
-
expect(selectTocLink(document)).to.be.null;
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('does not extend default plugins with extendPlugins: false', async () => {
|
|
150
|
-
const fixture = await buildFixture({
|
|
151
|
-
markdown: {
|
|
152
|
-
remarkPlugins: [remarkExamplePlugin],
|
|
153
|
-
},
|
|
154
|
-
integrations: [
|
|
155
|
-
mdx({
|
|
156
|
-
remarkPlugins: [],
|
|
157
|
-
extendPlugins: false,
|
|
158
|
-
}),
|
|
159
|
-
],
|
|
83
|
+
for (const extendMarkdownConfig of [true, false]) {
|
|
84
|
+
describe(`extendMarkdownConfig = ${extendMarkdownConfig}`, () => {
|
|
85
|
+
let fixture;
|
|
86
|
+
before(async () => {
|
|
87
|
+
fixture = await buildFixture({
|
|
88
|
+
markdown: {
|
|
89
|
+
remarkPlugins: [remarkToc],
|
|
90
|
+
gfm: false,
|
|
91
|
+
},
|
|
92
|
+
integrations: [
|
|
93
|
+
mdx({
|
|
94
|
+
extendMarkdownConfig,
|
|
95
|
+
remarkPlugins: [remarkExamplePlugin],
|
|
96
|
+
rehypePlugins: [rehypeExamplePlugin],
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('Handles MDX plugins', async () => {
|
|
103
|
+
const html = await fixture.readFile(FILE);
|
|
104
|
+
const { document } = parseHTML(html);
|
|
105
|
+
|
|
106
|
+
expect(selectRemarkExample(document, 'MDX remark plugins not applied.')).to.not.be.null;
|
|
107
|
+
expect(selectRehypeExample(document, 'MDX rehype plugins not applied.')).to.not.be.null;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('Handles Markdown plugins', async () => {
|
|
111
|
+
const html = await fixture.readFile(FILE);
|
|
112
|
+
const { document } = parseHTML(html);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
selectTocLink(
|
|
116
|
+
document,
|
|
117
|
+
'`remarkToc` plugin applied unexpectedly. Should override Markdown config.'
|
|
118
|
+
)
|
|
119
|
+
).to.be.null;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('Handles gfm', async () => {
|
|
123
|
+
const html = await fixture.readFile(FILE);
|
|
124
|
+
const { document } = parseHTML(html);
|
|
125
|
+
|
|
126
|
+
if (extendMarkdownConfig === true) {
|
|
127
|
+
expect(selectGfmLink(document), 'Does not respect `markdown.gfm` option.').to.be.null;
|
|
128
|
+
} else {
|
|
129
|
+
expect(selectGfmLink(document), 'Respects `markdown.gfm` unexpectedly.').to.not.be.null;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
160
132
|
});
|
|
161
|
-
|
|
162
|
-
const html = await fixture.readFile(FILE);
|
|
163
|
-
const { document } = parseHTML(html);
|
|
164
|
-
|
|
165
|
-
expect(selectGfmLink(document)).to.be.null;
|
|
166
|
-
expect(selectRemarkExample(document)).to.be.null;
|
|
167
|
-
});
|
|
133
|
+
}
|
|
168
134
|
|
|
169
135
|
it('supports custom recma plugins', async () => {
|
|
170
136
|
const fixture = await buildFixture({
|
|
@@ -67,6 +67,32 @@ describe('MDX syntax highlighting', () => {
|
|
|
67
67
|
const prismCodeBlock = document.querySelector('pre.language-astro');
|
|
68
68
|
expect(prismCodeBlock).to.not.be.null;
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
for (const extendMarkdownConfig of [true, false]) {
|
|
72
|
+
it(`respects syntaxHighlight when extendMarkdownConfig = ${extendMarkdownConfig}`, async () => {
|
|
73
|
+
const fixture = await loadFixture({
|
|
74
|
+
root: FIXTURE_ROOT,
|
|
75
|
+
markdown: {
|
|
76
|
+
syntaxHighlight: 'shiki',
|
|
77
|
+
},
|
|
78
|
+
integrations: [
|
|
79
|
+
mdx({
|
|
80
|
+
extendMarkdownConfig,
|
|
81
|
+
syntaxHighlight: 'prism',
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
await fixture.build();
|
|
86
|
+
|
|
87
|
+
const html = await fixture.readFile('/index.html');
|
|
88
|
+
const { document } = parseHTML(html);
|
|
89
|
+
|
|
90
|
+
const shikiCodeBlock = document.querySelector('pre.astro-code');
|
|
91
|
+
expect(shikiCodeBlock, 'Markdown config syntaxHighlight used unexpectedly').to.be.null;
|
|
92
|
+
const prismCodeBlock = document.querySelector('pre.language-astro');
|
|
93
|
+
expect(prismCodeBlock).to.not.be.null;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
70
96
|
});
|
|
71
97
|
|
|
72
98
|
it('supports custom highlighter - shiki-twoslash', async () => {
|