@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.
@@ -1,5 +1,5 @@
1
- @astrojs/mdx:build: cache hit, replaying output 7b0a39f7d517504d
2
- @astrojs/mdx:build: 
3
- @astrojs/mdx:build: > @astrojs/mdx@0.12.2 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,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, 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
  }) {
@@ -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(`export const ${EXPORT_NAME} = ${JSON.stringify(frontmatter)};`)
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 = [...rehypePlugins, ...mdxOptions.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
- 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.12.2",
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.6.15",
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,
@@ -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(`export const ${EXPORT_NAME} = ${JSON.stringify(frontmatter)};`),
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 = [...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
+ ];
175
221
  return rehypePlugins;
176
222
  }
177
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
+ });