@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.
@@ -1,5 +1,5 @@
1
- @astrojs/mdx:build: cache hit, replaying output 72ab36e70338807d
2
- @astrojs/mdx:build: 
3
- @astrojs/mdx:build: > @astrojs/mdx@0.13.0 build /home/runner/work/astro/astro/packages/integrations/mdx
4
- @astrojs/mdx:build: > astro-scripts build "src/**/*.ts" && tsc
5
- @astrojs/mdx:build: 
1
+ @astrojs/mdx:build: cache hit, replaying output 93db87e92a57f3ab
2
+ @astrojs/mdx:build: 
3
+ @astrojs/mdx:build: > @astrojs/mdx@0.14.0 build /home/runner/work/astro/astro/packages/integrations/mdx
4
+ @astrojs/mdx:build: > astro-scripts build "src/**/*.ts" && tsc
5
+ @astrojs/mdx:build: 
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, handleExtendsNotSupported, parseFrontmatter } from "./utils.js";
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
- handleExtendsNotSupported(mdxOptions.remarkPlugins);
25
- handleExtendsNotSupported(mdxOptions.rehypePlugins);
26
- if (mdxOptions.extendPlugins === "markdown" && (((_a = config.markdown.rehypePlugins) == null ? void 0 : _a.length) || ((_b = config.markdown.remarkPlugins) == null ? void 0 : _b.length))) {
27
- console.info(
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 rehypeCollectHeadings from "./rehype-collect-headings.js";
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 = [...rehypePlugins, ...mdxOptions.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
- export interface MarkdownHeading {
2
- depth: number;
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 rehypeCollectHeadings() {
5
- const slugger = new Slugger();
6
- return function(tree) {
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
- rehypeCollectHeadings as default
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 handleExtendsNotSupported(pluginConfig: any): void;
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 handleExtendsNotSupported(pluginConfig) {
63
- if (typeof pluginConfig === "object" && pluginConfig !== null && pluginConfig.hasOwnProperty("extends")) {
64
- throw new Error(
65
- `[MDX] The "extends" plugin option is no longer supported! Astro now extends your project's \`markdown\` plugin configuration by default. To customize this behavior, see the \`extendPlugins\` option instead: https://docs.astro.build/en/guides/integrations-guide/mdx/#extendplugins`
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
- handleExtendsNotSupported,
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.13.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.7.0",
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, handleExtendsNotSupported, parseFrontmatter } from './utils.js';
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
- handleExtendsNotSupported(mdxOptions.remarkPlugins);
49
- handleExtendsNotSupported(mdxOptions.rehypePlugins);
50
-
51
- // TODO: remove for 1.0. Shipping to ease migration to new minor
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 rehypeCollectHeadings from './rehype-collect-headings.js';
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 = [...rehypePlugins, ...(mdxOptions.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 Slugger from 'github-slugger';
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 interface MarkdownHeading {
6
- depth: number;
7
- slug: string;
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
- // TODO: remove for 1.0
87
- export function handleExtendsNotSupported(pluginConfig: any) {
88
- if (
89
- typeof pluginConfig === 'object' &&
90
- pluginConfig !== null &&
91
- (pluginConfig as any).hasOwnProperty('extends')
92
- ) {
93
- throw new Error(
94
- `[MDX] The "extends" plugin option is no longer supported! Astro now extends your project's \`markdown\` plugin configuration by default. To customize this behavior, see the \`extendPlugins\` option instead: https://docs.astro.build/en/guides/integrations-guide/mdx/#extendplugins`
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
+ });