@astrojs/mdx 0.12.2 → 0.14.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 +46 -0
- package/dist/index.js +5 -23
- package/dist/plugins.js +44 -7
- 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 +4 -2
- package/src/index.ts +5 -29
- package/src/plugins.ts +55 -9
- package/src/rehype-collect-headings.ts +4 -43
- package/src/utils.ts +16 -11
- package/test/mdx-get-headings.test.js +91 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[
|
|
2
|
-
[
|
|
3
|
-
[
|
|
4
|
-
[
|
|
5
|
-
[
|
|
1
|
+
[35m@astrojs/mdx:build: [0mcache hit, replaying output [2m93db87e92a57f3ab[0m
|
|
2
|
+
[35m@astrojs/mdx:build: [0m
|
|
3
|
+
[35m@astrojs/mdx:build: [0m> @astrojs/mdx@0.14.0 build /home/runner/work/astro/astro/packages/integrations/mdx
|
|
4
|
+
[35m@astrojs/mdx:build: [0m> astro-scripts build "src/**/*.ts" && tsc
|
|
5
|
+
[35m@astrojs/mdx:build: [0m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @astrojs/mdx
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#5654](https://github.com/withastro/astro/pull/5654) [`2c65b433b`](https://github.com/withastro/astro/commit/2c65b433bf840a1bb93b0a1947df5949e33512ff) Thanks [@delucis](https://github.com/delucis)! - Run heading ID injection after user plugins
|
|
8
|
+
|
|
9
|
+
⚠️ BREAKING CHANGE ⚠️
|
|
10
|
+
|
|
11
|
+
If you are using a rehype plugin that depends on heading IDs injected by Astro, the IDs will no longer be available when your plugin runs by default.
|
|
12
|
+
|
|
13
|
+
To inject IDs before your plugins run, import and add the `rehypeHeadingIds` plugin to your `rehypePlugins` config:
|
|
14
|
+
|
|
15
|
+
```diff
|
|
16
|
+
// astro.config.mjs
|
|
17
|
+
+ import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
18
|
+
import mdx from '@astrojs/mdx';
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
integrations: [mdx()],
|
|
22
|
+
markdown: {
|
|
23
|
+
rehypePlugins: [
|
|
24
|
+
+ rehypeHeadingIds,
|
|
25
|
+
otherPluginThatReliesOnHeadingIDs,
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#5667](https://github.com/withastro/astro/pull/5667) [`a5ba4af79`](https://github.com/withastro/astro/commit/a5ba4af79930145f4edf66d45cd40ddad045cc86) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Chore: remove verbose "Now interiting Markdown plugins..." logs
|
|
34
|
+
|
|
35
|
+
- [#5648](https://github.com/withastro/astro/pull/5648) [`853081d1c`](https://github.com/withastro/astro/commit/853081d1c857d8ad8a9634c37ed8fd123d32d241) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Prevent relative image paths in `src/content/`
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [[`853081d1c`](https://github.com/withastro/astro/commit/853081d1c857d8ad8a9634c37ed8fd123d32d241), [`2c65b433b`](https://github.com/withastro/astro/commit/2c65b433bf840a1bb93b0a1947df5949e33512ff)]:
|
|
38
|
+
- @astrojs/markdown-remark@1.2.0
|
|
39
|
+
|
|
40
|
+
## 0.13.0
|
|
41
|
+
|
|
42
|
+
### Minor Changes
|
|
43
|
+
|
|
44
|
+
- [#5291](https://github.com/withastro/astro/pull/5291) [`5ec0f6ed5`](https://github.com/withastro/astro/commit/5ec0f6ed55b0a14a9663a90a03428345baf126bd) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Introduce Content Collections experimental API
|
|
45
|
+
- Organize your Markdown and MDX content into easy-to-manage collections.
|
|
46
|
+
- Add type safety to your frontmatter with schemas.
|
|
47
|
+
- Generate landing pages, static routes, and SSR endpoints from your content using the collection query APIs.
|
|
48
|
+
|
|
3
49
|
## 0.12.2
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { compile as mdxCompile } from "@mdx-js/mdx";
|
|
2
2
|
import mdxPlugin from "@mdx-js/rollup";
|
|
3
3
|
import { parse as parseESM } from "es-module-lexer";
|
|
4
|
-
import { blue, bold } from "kleur/colors";
|
|
5
4
|
import fs from "node:fs/promises";
|
|
6
5
|
import { VFile } from "vfile";
|
|
7
6
|
import {
|
|
@@ -10,7 +9,7 @@ import {
|
|
|
10
9
|
recmaInjectImportMetaEnvPlugin,
|
|
11
10
|
rehypeApplyFrontmatterExport
|
|
12
11
|
} from "./plugins.js";
|
|
13
|
-
import { getFileInfo,
|
|
12
|
+
import { getFileInfo, parseFrontmatter } from "./utils.js";
|
|
14
13
|
const RAW_CONTENT_ERROR = "MDX does not support rawContent()! If you need to read the Markdown contents to calculate values (ex. reading time), we suggest injecting frontmatter via remark plugins. Learn more on our docs: https://docs.astro.build/en/guides/integrations-guide/mdx/#inject-frontmatter-via-remark-or-rehype-plugins";
|
|
15
14
|
const COMPILED_CONTENT_ERROR = "MDX does not support compiledContent()! If you need to read the HTML contents to calculate values (ex. reading time), we suggest injecting frontmatter via rehype plugins. Learn more on our docs: https://docs.astro.build/en/guides/integrations-guide/mdx/#inject-frontmatter-via-remark-or-rehype-plugins";
|
|
16
15
|
function mdx(mdxOptions = {}) {
|
|
@@ -18,29 +17,12 @@ function mdx(mdxOptions = {}) {
|
|
|
18
17
|
name: "@astrojs/mdx",
|
|
19
18
|
hooks: {
|
|
20
19
|
"astro:config:setup": async ({ updateConfig, config, addPageExtension, command }) => {
|
|
21
|
-
var _a, _b;
|
|
22
20
|
addPageExtension(".mdx");
|
|
23
21
|
mdxOptions.extendPlugins ?? (mdxOptions.extendPlugins = "markdown");
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
blue(`[MDX] Now inheriting remark and rehype plugins from "markdown" config.`)
|
|
29
|
-
);
|
|
30
|
-
console.info(
|
|
31
|
-
`If you applied a plugin to both your Markdown and MDX configs, we suggest ${bold(
|
|
32
|
-
"removing the duplicate MDX entry."
|
|
33
|
-
)}`
|
|
34
|
-
);
|
|
35
|
-
console.info(`See "extendPlugins" option to configure this behavior.`);
|
|
36
|
-
}
|
|
37
|
-
let remarkRehypeOptions = mdxOptions.remarkRehype;
|
|
38
|
-
if (mdxOptions.extendPlugins === "markdown") {
|
|
39
|
-
remarkRehypeOptions = {
|
|
40
|
-
...config.markdown.remarkRehype,
|
|
41
|
-
...remarkRehypeOptions
|
|
42
|
-
};
|
|
43
|
-
}
|
|
22
|
+
const remarkRehypeOptions = {
|
|
23
|
+
...mdxOptions.extendPlugins === "markdown" ? config.markdown.remarkRehype : {},
|
|
24
|
+
...mdxOptions.remarkRehype
|
|
25
|
+
};
|
|
44
26
|
const mdxPluginOpts = {
|
|
45
27
|
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
|
|
46
28
|
rehypePlugins: getRehypePlugins(mdxOptions, config),
|
package/dist/plugins.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { rehypeHeadingIds } from "@astrojs/markdown-remark";
|
|
1
2
|
import { nodeTypes } from "@mdx-js/mdx";
|
|
2
3
|
import { visit as estreeVisit } from "estree-util-visit";
|
|
3
4
|
import { bold, yellow } from "kleur/colors";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
4
6
|
import rehypeRaw from "rehype-raw";
|
|
5
7
|
import remarkGfm from "remark-gfm";
|
|
6
8
|
import remarkSmartypants from "remark-smartypants";
|
|
7
|
-
import
|
|
9
|
+
import { visit } from "unist-util-visit";
|
|
10
|
+
import { rehypeInjectHeadingsExport } from "./rehype-collect-headings.js";
|
|
8
11
|
import rehypeMetaString from "./rehype-meta-string.js";
|
|
9
12
|
import remarkPrism from "./remark-prism.js";
|
|
10
13
|
import remarkShiki from "./remark-shiki.js";
|
|
11
|
-
import { jsToTreeNode } from "./utils.js";
|
|
14
|
+
import { isRelativePath, jsToTreeNode } from "./utils.js";
|
|
12
15
|
function recmaInjectImportMetaEnvPlugin({
|
|
13
16
|
importMetaEnv
|
|
14
17
|
}) {
|
|
@@ -38,19 +41,25 @@ function remarkInitializeAstroData() {
|
|
|
38
41
|
}
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
|
-
const EXPORT_NAME = "frontmatter";
|
|
42
44
|
function rehypeApplyFrontmatterExport(pageFrontmatter) {
|
|
43
45
|
return function(tree, vfile) {
|
|
44
46
|
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data);
|
|
45
47
|
const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter };
|
|
46
48
|
const exportNodes = [
|
|
47
|
-
jsToTreeNode(
|
|
49
|
+
jsToTreeNode(
|
|
50
|
+
`export const frontmatter = ${JSON.stringify(
|
|
51
|
+
frontmatter
|
|
52
|
+
)};
|
|
53
|
+
export const _internal = { injectedFrontmatter: ${JSON.stringify(
|
|
54
|
+
injectedFrontmatter
|
|
55
|
+
)} };`
|
|
56
|
+
)
|
|
48
57
|
];
|
|
49
58
|
if (frontmatter.layout) {
|
|
50
59
|
exportNodes.unshift(
|
|
51
60
|
jsToTreeNode(
|
|
52
61
|
`import { jsx as layoutJsx } from 'astro/jsx-runtime';
|
|
53
|
-
|
|
62
|
+
|
|
54
63
|
export default async function ({ children }) {
|
|
55
64
|
const Layout = (await import(${JSON.stringify(frontmatter.layout)})).default;
|
|
56
65
|
const { layout, ...content } = frontmatter;
|
|
@@ -88,6 +97,27 @@ function rehypeApplyFrontmatterExport(pageFrontmatter) {
|
|
|
88
97
|
tree.children = exportNodes.concat(tree.children);
|
|
89
98
|
};
|
|
90
99
|
}
|
|
100
|
+
function toRemarkContentRelImageError({ srcDir }) {
|
|
101
|
+
const contentDir = new URL("content/", srcDir);
|
|
102
|
+
return function remarkContentRelImageError() {
|
|
103
|
+
return (tree, vfile) => {
|
|
104
|
+
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
|
|
105
|
+
if (!isContentFile)
|
|
106
|
+
return;
|
|
107
|
+
const relImagePaths = /* @__PURE__ */ new Set();
|
|
108
|
+
visit(tree, "image", function raiseError(node) {
|
|
109
|
+
if (isRelativePath(node.url)) {
|
|
110
|
+
relImagePaths.add(node.url);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (relImagePaths.size === 0)
|
|
114
|
+
return;
|
|
115
|
+
const errorMessage = `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):
|
|
116
|
+
` + [...relImagePaths].map((path) => JSON.stringify(path)).join(",\n");
|
|
117
|
+
throw new Error(errorMessage);
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
91
121
|
const DEFAULT_REMARK_PLUGINS = [remarkGfm, remarkSmartypants];
|
|
92
122
|
const DEFAULT_REHYPE_PLUGINS = [];
|
|
93
123
|
async function getRemarkPlugins(mdxOptions, config) {
|
|
@@ -115,11 +145,13 @@ async function getRemarkPlugins(mdxOptions, config) {
|
|
|
115
145
|
remarkPlugins.push(remarkPrism);
|
|
116
146
|
}
|
|
117
147
|
remarkPlugins = [...remarkPlugins, ...mdxOptions.remarkPlugins ?? []];
|
|
148
|
+
if (config.experimental.contentCollections) {
|
|
149
|
+
remarkPlugins.push(toRemarkContentRelImageError(config));
|
|
150
|
+
}
|
|
118
151
|
return remarkPlugins;
|
|
119
152
|
}
|
|
120
153
|
function getRehypePlugins(mdxOptions, config) {
|
|
121
154
|
let rehypePlugins = [
|
|
122
|
-
rehypeCollectHeadings,
|
|
123
155
|
rehypeMetaString,
|
|
124
156
|
[rehypeRaw, { passThrough: nodeTypes }]
|
|
125
157
|
];
|
|
@@ -137,7 +169,12 @@ function getRehypePlugins(mdxOptions, config) {
|
|
|
137
169
|
];
|
|
138
170
|
break;
|
|
139
171
|
}
|
|
140
|
-
rehypePlugins = [
|
|
172
|
+
rehypePlugins = [
|
|
173
|
+
...rehypePlugins,
|
|
174
|
+
...mdxOptions.rehypePlugins ?? [],
|
|
175
|
+
rehypeHeadingIds,
|
|
176
|
+
rehypeInjectHeadingsExport
|
|
177
|
+
];
|
|
141
178
|
return rehypePlugins;
|
|
142
179
|
}
|
|
143
180
|
function markdownShouldExtendDefaultPlugins(config) {
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
slug: string;
|
|
4
|
-
text: string;
|
|
5
|
-
}
|
|
6
|
-
export default function rehypeCollectHeadings(): (tree: any) => void;
|
|
1
|
+
import { MarkdownVFile } from '@astrojs/markdown-remark';
|
|
2
|
+
export declare function rehypeInjectHeadingsExport(): (tree: any, file: MarkdownVFile) => void;
|
|
@@ -1,47 +1,12 @@
|
|
|
1
|
-
import Slugger from "github-slugger";
|
|
2
|
-
import { visit } from "unist-util-visit";
|
|
3
1
|
import { jsToTreeNode } from "./utils.js";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const headings = [];
|
|
8
|
-
visit(tree, (node) => {
|
|
9
|
-
if (node.type !== "element")
|
|
10
|
-
return;
|
|
11
|
-
const { tagName } = node;
|
|
12
|
-
if (tagName[0] !== "h")
|
|
13
|
-
return;
|
|
14
|
-
const [_, level] = tagName.match(/h([0-6])/) ?? [];
|
|
15
|
-
if (!level)
|
|
16
|
-
return;
|
|
17
|
-
const depth = Number.parseInt(level);
|
|
18
|
-
let text = "";
|
|
19
|
-
visit(node, (child, __, parent) => {
|
|
20
|
-
if (child.type === "element" || parent == null) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (child.type === "raw" && child.value.match(/^\n?<.*>\n?$/)) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if ((/* @__PURE__ */ new Set(["text", "raw", "mdxTextExpression"])).has(child.type)) {
|
|
27
|
-
text += child.value;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
node.properties = node.properties || {};
|
|
31
|
-
if (typeof node.properties.id !== "string") {
|
|
32
|
-
let slug = slugger.slug(text);
|
|
33
|
-
if (slug.endsWith("-")) {
|
|
34
|
-
slug = slug.slice(0, -1);
|
|
35
|
-
}
|
|
36
|
-
node.properties.id = slug;
|
|
37
|
-
}
|
|
38
|
-
headings.push({ depth, slug: node.properties.id, text });
|
|
39
|
-
});
|
|
2
|
+
function rehypeInjectHeadingsExport() {
|
|
3
|
+
return function(tree, file) {
|
|
4
|
+
const headings = file.data.__astroHeadings || [];
|
|
40
5
|
tree.children.unshift(
|
|
41
6
|
jsToTreeNode(`export function getHeadings() { return ${JSON.stringify(headings)} }`)
|
|
42
7
|
);
|
|
43
8
|
};
|
|
44
9
|
}
|
|
45
10
|
export {
|
|
46
|
-
|
|
11
|
+
rehypeInjectHeadingsExport
|
|
47
12
|
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export declare function getFileInfo(id: string, config: AstroConfig): FileInfo;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function parseFrontmatter(code: string, id: string): matter.GrayMatterFile<string>;
|
|
16
16
|
export declare function jsToTreeNode(jsString: string, acornOpts?: AcornOpts): MdxjsEsm;
|
|
17
|
-
export declare function
|
|
17
|
+
export declare function isRelativePath(path: string): boolean;
|
|
18
18
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -59,16 +59,23 @@ function jsToTreeNode(jsString, acornOpts = {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
function isRelativePath(path) {
|
|
63
|
+
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
|
|
64
|
+
}
|
|
65
|
+
function startsWithDotDotSlash(path) {
|
|
66
|
+
const c1 = path[0];
|
|
67
|
+
const c2 = path[1];
|
|
68
|
+
const c3 = path[2];
|
|
69
|
+
return c1 === "." && c2 === "." && c3 === "/";
|
|
70
|
+
}
|
|
71
|
+
function startsWithDotSlash(path) {
|
|
72
|
+
const c1 = path[0];
|
|
73
|
+
const c2 = path[1];
|
|
74
|
+
return c1 === "." && c2 === "/";
|
|
68
75
|
}
|
|
69
76
|
export {
|
|
70
77
|
getFileInfo,
|
|
71
|
-
|
|
78
|
+
isRelativePath,
|
|
72
79
|
jsToTreeNode,
|
|
73
80
|
parseFrontmatter
|
|
74
81
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/mdx",
|
|
3
3
|
"description": "Use MDX within Astro",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "withastro",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@astrojs/markdown-remark": "^1.2.0",
|
|
26
27
|
"@astrojs/prism": "^1.0.2",
|
|
27
28
|
"@mdx-js/mdx": "^2.1.2",
|
|
28
29
|
"@mdx-js/rollup": "^2.1.1",
|
|
@@ -44,9 +45,10 @@
|
|
|
44
45
|
"@types/chai": "^4.3.1",
|
|
45
46
|
"@types/estree": "^1.0.0",
|
|
46
47
|
"@types/github-slugger": "^1.3.0",
|
|
48
|
+
"@types/mdast": "^3.0.10",
|
|
47
49
|
"@types/mocha": "^9.1.1",
|
|
48
50
|
"@types/yargs-parser": "^21.0.0",
|
|
49
|
-
"astro": "1.
|
|
51
|
+
"astro": "1.8.0",
|
|
50
52
|
"astro-scripts": "0.0.9",
|
|
51
53
|
"chai": "^4.3.6",
|
|
52
54
|
"cheerio": "^1.0.0-rc.11",
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
|
|
3
3
|
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
|
4
4
|
import type { AstroIntegration } from 'astro';
|
|
5
5
|
import { parse as parseESM } from 'es-module-lexer';
|
|
6
|
-
import { blue, bold } from 'kleur/colors';
|
|
7
6
|
import fs from 'node:fs/promises';
|
|
8
7
|
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
|
9
8
|
import { VFile } from 'vfile';
|
|
@@ -14,7 +13,7 @@ import {
|
|
|
14
13
|
recmaInjectImportMetaEnvPlugin,
|
|
15
14
|
rehypeApplyFrontmatterExport,
|
|
16
15
|
} from './plugins.js';
|
|
17
|
-
import { getFileInfo,
|
|
16
|
+
import { getFileInfo, parseFrontmatter } from './utils.js';
|
|
18
17
|
|
|
19
18
|
const RAW_CONTENT_ERROR =
|
|
20
19
|
'MDX does not support rawContent()! If you need to read the Markdown contents to calculate values (ex. reading time), we suggest injecting frontmatter via remark plugins. Learn more on our docs: https://docs.astro.build/en/guides/integrations-guide/mdx/#inject-frontmatter-via-remark-or-rehype-plugins';
|
|
@@ -45,33 +44,10 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
|
|
45
44
|
addPageExtension('.mdx');
|
|
46
45
|
mdxOptions.extendPlugins ??= 'markdown';
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
mdxOptions.extendPlugins === 'markdown' &&
|
|
54
|
-
(config.markdown.rehypePlugins?.length || config.markdown.remarkPlugins?.length)
|
|
55
|
-
) {
|
|
56
|
-
console.info(
|
|
57
|
-
blue(`[MDX] Now inheriting remark and rehype plugins from "markdown" config.`)
|
|
58
|
-
);
|
|
59
|
-
console.info(
|
|
60
|
-
`If you applied a plugin to both your Markdown and MDX configs, we suggest ${bold(
|
|
61
|
-
'removing the duplicate MDX entry.'
|
|
62
|
-
)}`
|
|
63
|
-
);
|
|
64
|
-
console.info(`See "extendPlugins" option to configure this behavior.`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let remarkRehypeOptions = mdxOptions.remarkRehype;
|
|
68
|
-
|
|
69
|
-
if (mdxOptions.extendPlugins === 'markdown') {
|
|
70
|
-
remarkRehypeOptions = {
|
|
71
|
-
...config.markdown.remarkRehype,
|
|
72
|
-
...remarkRehypeOptions,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
47
|
+
const remarkRehypeOptions = {
|
|
48
|
+
...(mdxOptions.extendPlugins === 'markdown' ? config.markdown.remarkRehype : {}),
|
|
49
|
+
...mdxOptions.remarkRehype,
|
|
50
|
+
};
|
|
75
51
|
|
|
76
52
|
const mdxPluginOpts: MdxRollupPluginOptions = {
|
|
77
53
|
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
|
package/src/plugins.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
1
2
|
import { nodeTypes } from '@mdx-js/mdx';
|
|
2
3
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
|
3
4
|
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
|
@@ -5,16 +6,19 @@ import type { AstroConfig, MarkdownAstroData } from 'astro';
|
|
|
5
6
|
import type { Literal, MemberExpression } from 'estree';
|
|
6
7
|
import { visit as estreeVisit } from 'estree-util-visit';
|
|
7
8
|
import { bold, yellow } from 'kleur/colors';
|
|
9
|
+
import type { Image } from 'mdast';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
8
11
|
import rehypeRaw from 'rehype-raw';
|
|
9
12
|
import remarkGfm from 'remark-gfm';
|
|
10
13
|
import remarkSmartypants from 'remark-smartypants';
|
|
14
|
+
import { visit } from 'unist-util-visit';
|
|
11
15
|
import type { Data, VFile } from 'vfile';
|
|
12
16
|
import { MdxOptions } from './index.js';
|
|
13
|
-
import
|
|
17
|
+
import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
|
|
14
18
|
import rehypeMetaString from './rehype-meta-string.js';
|
|
15
19
|
import remarkPrism from './remark-prism.js';
|
|
16
20
|
import remarkShiki from './remark-shiki.js';
|
|
17
|
-
import { jsToTreeNode } from './utils.js';
|
|
21
|
+
import { isRelativePath, jsToTreeNode } from './utils.js';
|
|
18
22
|
|
|
19
23
|
export function recmaInjectImportMetaEnvPlugin({
|
|
20
24
|
importMetaEnv,
|
|
@@ -51,14 +55,18 @@ export function remarkInitializeAstroData() {
|
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
const EXPORT_NAME = 'frontmatter';
|
|
55
|
-
|
|
56
58
|
export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any>) {
|
|
57
59
|
return function (tree: any, vfile: VFile) {
|
|
58
60
|
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data);
|
|
59
61
|
const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter };
|
|
60
62
|
const exportNodes = [
|
|
61
|
-
jsToTreeNode(
|
|
63
|
+
jsToTreeNode(
|
|
64
|
+
`export const frontmatter = ${JSON.stringify(
|
|
65
|
+
frontmatter
|
|
66
|
+
)};\nexport const _internal = { injectedFrontmatter: ${JSON.stringify(
|
|
67
|
+
injectedFrontmatter
|
|
68
|
+
)} };`
|
|
69
|
+
),
|
|
62
70
|
];
|
|
63
71
|
if (frontmatter.layout) {
|
|
64
72
|
// NOTE(bholmesdev) 08-22-2022
|
|
@@ -69,7 +77,7 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
|
|
|
69
77
|
jsToTreeNode(
|
|
70
78
|
/** @see 'vite-plugin-markdown' for layout props reference */
|
|
71
79
|
`import { jsx as layoutJsx } from 'astro/jsx-runtime';
|
|
72
|
-
|
|
80
|
+
|
|
73
81
|
export default async function ({ children }) {
|
|
74
82
|
const Layout = (await import(${JSON.stringify(frontmatter.layout)})).default;
|
|
75
83
|
const { layout, ...content } = frontmatter;
|
|
@@ -108,6 +116,34 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
|
|
|
108
116
|
};
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
/**
|
|
120
|
+
* `src/content/` does not support relative image paths.
|
|
121
|
+
* This plugin throws an error if any are found
|
|
122
|
+
*/
|
|
123
|
+
function toRemarkContentRelImageError({ srcDir }: { srcDir: URL }) {
|
|
124
|
+
const contentDir = new URL('content/', srcDir);
|
|
125
|
+
return function remarkContentRelImageError() {
|
|
126
|
+
return (tree: any, vfile: VFile) => {
|
|
127
|
+
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
|
|
128
|
+
if (!isContentFile) return;
|
|
129
|
+
|
|
130
|
+
const relImagePaths = new Set<string>();
|
|
131
|
+
visit(tree, 'image', function raiseError(node: Image) {
|
|
132
|
+
if (isRelativePath(node.url)) {
|
|
133
|
+
relImagePaths.add(node.url);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (relImagePaths.size === 0) return;
|
|
137
|
+
|
|
138
|
+
const errorMessage =
|
|
139
|
+
`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` +
|
|
140
|
+
[...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');
|
|
141
|
+
|
|
142
|
+
throw new Error(errorMessage);
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
111
147
|
const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkSmartypants];
|
|
112
148
|
const DEFAULT_REHYPE_PLUGINS: PluggableList = [];
|
|
113
149
|
|
|
@@ -141,6 +177,11 @@ export async function getRemarkPlugins(
|
|
|
141
177
|
}
|
|
142
178
|
|
|
143
179
|
remarkPlugins = [...remarkPlugins, ...(mdxOptions.remarkPlugins ?? [])];
|
|
180
|
+
|
|
181
|
+
// Apply last in case user plugins resolve relative image paths
|
|
182
|
+
if (config.experimental.contentCollections) {
|
|
183
|
+
remarkPlugins.push(toRemarkContentRelImageError(config));
|
|
184
|
+
}
|
|
144
185
|
return remarkPlugins;
|
|
145
186
|
}
|
|
146
187
|
|
|
@@ -149,8 +190,6 @@ export function getRehypePlugins(
|
|
|
149
190
|
config: AstroConfig
|
|
150
191
|
): MdxRollupPluginOptions['rehypePlugins'] {
|
|
151
192
|
let rehypePlugins: PluggableList = [
|
|
152
|
-
// getHeadings() is guaranteed by TS, so we can't allow user to override
|
|
153
|
-
rehypeCollectHeadings,
|
|
154
193
|
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
|
|
155
194
|
rehypeMetaString,
|
|
156
195
|
// rehypeRaw allows custom syntax highlighters to work without added config
|
|
@@ -171,7 +210,14 @@ export function getRehypePlugins(
|
|
|
171
210
|
break;
|
|
172
211
|
}
|
|
173
212
|
|
|
174
|
-
rehypePlugins = [
|
|
213
|
+
rehypePlugins = [
|
|
214
|
+
...rehypePlugins,
|
|
215
|
+
...(mdxOptions.rehypePlugins ?? []),
|
|
216
|
+
// getHeadings() is guaranteed by TS, so this must be included.
|
|
217
|
+
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
|
|
218
|
+
rehypeHeadingIds,
|
|
219
|
+
rehypeInjectHeadingsExport,
|
|
220
|
+
];
|
|
175
221
|
return rehypePlugins;
|
|
176
222
|
}
|
|
177
223
|
|
|
@@ -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,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
|
+
});
|