@astrojs/mdx 0.13.0 → 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 +37 -0
- package/dist/index.js +5 -23
- package/dist/plugins.js +35 -4
- 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 +47 -5
- 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,42 @@
|
|
|
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
|
+
|
|
3
40
|
## 0.13.0
|
|
4
41
|
|
|
5
42
|
### Minor 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
|
}) {
|
|
@@ -94,6 +97,27 @@ export const _internal = { injectedFrontmatter: ${JSON.stringify(
|
|
|
94
97
|
tree.children = exportNodes.concat(tree.children);
|
|
95
98
|
};
|
|
96
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
|
+
}
|
|
97
121
|
const DEFAULT_REMARK_PLUGINS = [remarkGfm, remarkSmartypants];
|
|
98
122
|
const DEFAULT_REHYPE_PLUGINS = [];
|
|
99
123
|
async function getRemarkPlugins(mdxOptions, config) {
|
|
@@ -121,11 +145,13 @@ async function getRemarkPlugins(mdxOptions, config) {
|
|
|
121
145
|
remarkPlugins.push(remarkPrism);
|
|
122
146
|
}
|
|
123
147
|
remarkPlugins = [...remarkPlugins, ...mdxOptions.remarkPlugins ?? []];
|
|
148
|
+
if (config.experimental.contentCollections) {
|
|
149
|
+
remarkPlugins.push(toRemarkContentRelImageError(config));
|
|
150
|
+
}
|
|
124
151
|
return remarkPlugins;
|
|
125
152
|
}
|
|
126
153
|
function getRehypePlugins(mdxOptions, config) {
|
|
127
154
|
let rehypePlugins = [
|
|
128
|
-
rehypeCollectHeadings,
|
|
129
155
|
rehypeMetaString,
|
|
130
156
|
[rehypeRaw, { passThrough: nodeTypes }]
|
|
131
157
|
];
|
|
@@ -143,7 +169,12 @@ function getRehypePlugins(mdxOptions, config) {
|
|
|
143
169
|
];
|
|
144
170
|
break;
|
|
145
171
|
}
|
|
146
|
-
rehypePlugins = [
|
|
172
|
+
rehypePlugins = [
|
|
173
|
+
...rehypePlugins,
|
|
174
|
+
...mdxOptions.rehypePlugins ?? [],
|
|
175
|
+
rehypeHeadingIds,
|
|
176
|
+
rehypeInjectHeadingsExport
|
|
177
|
+
];
|
|
147
178
|
return rehypePlugins;
|
|
148
179
|
}
|
|
149
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,
|
|
@@ -112,6 +116,34 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
|
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
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
|
+
|
|
115
147
|
const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkSmartypants];
|
|
116
148
|
const DEFAULT_REHYPE_PLUGINS: PluggableList = [];
|
|
117
149
|
|
|
@@ -145,6 +177,11 @@ export async function getRemarkPlugins(
|
|
|
145
177
|
}
|
|
146
178
|
|
|
147
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
|
+
}
|
|
148
185
|
return remarkPlugins;
|
|
149
186
|
}
|
|
150
187
|
|
|
@@ -153,8 +190,6 @@ export function getRehypePlugins(
|
|
|
153
190
|
config: AstroConfig
|
|
154
191
|
): MdxRollupPluginOptions['rehypePlugins'] {
|
|
155
192
|
let rehypePlugins: PluggableList = [
|
|
156
|
-
// getHeadings() is guaranteed by TS, so we can't allow user to override
|
|
157
|
-
rehypeCollectHeadings,
|
|
158
193
|
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
|
|
159
194
|
rehypeMetaString,
|
|
160
195
|
// rehypeRaw allows custom syntax highlighters to work without added config
|
|
@@ -175,7 +210,14 @@ export function getRehypePlugins(
|
|
|
175
210
|
break;
|
|
176
211
|
}
|
|
177
212
|
|
|
178
|
-
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
|
+
];
|
|
179
221
|
return rehypePlugins;
|
|
180
222
|
}
|
|
181
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
|
+
});
|