Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MDX] Extend Markdown plugin config, with customization options #4504

Merged
merged 16 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ten-walls-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/mdx': minor
---

Introduce new `extendPlugins` configuration option. This defaults to inheriting all remark and rehype plugins from your `markdown` config, with options to use either Astro's defaults or no inheritance at all.
81 changes: 64 additions & 17 deletions packages/integrations/mdx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,55 +306,102 @@ export default {

### remarkPlugins

**Default plugins:** [remark-gfm](https://github.com/remarkjs/remark-gfm), [remark-smartypants](https://github.com/silvenon/remark-smartypants)

[Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) allow you to extend your Markdown with new capabilities. This includes [auto-generating a table of contents](https://github.com/remarkjs/remark-toc), [applying accessible emoji labels](https://github.com/florianeckerstorfer/remark-a11y-emoji), and more. We encourage you to browse [awesome-remark](https://github.com/remarkjs/awesome-remark) for a full curated list!

We apply [GitHub-flavored Markdown](https://github.com/remarkjs/remark-gfm) and [Smartypants](https://github.com/silvenon/remark-smartypants) by default. This brings some niceties like auto-generating clickable links from text (ex. `https://example.com`) and formatting quotes for readability. When applying your own plugins, you can choose to preserve or remove these defaults.

To apply plugins _while preserving_ Astro's default plugins, use a nested `extends` object like so:
This example applies the [`remark-toc`](https://github.com/remarkjs/remark-toc) plugin to `.mdx` files. To customize plugin inheritance from your Markdown config or Astro's defaults, [see the `extendPlugins` option](#extendPlugins).

```js
// astro.config.mjs
import remarkToc from 'remark-toc';

export default {
integrations: [mdx({
// apply remark-toc alongside GitHub-flavored markdown and Smartypants
remarkPlugins: { extends: [remarkToc] },
remarkPlugins: [remarkToc],
})],
}
```

To apply plugins _without_ Astro's defaults, you can apply a plain array:
### rehypePlugins

[Rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) allow you to transform the HTML that your Markdown generates. We encourage you to browse [awesome-rehype](https://github.com/rehypejs/awesome-rehype) for a full curated list of plugins!

We apply our own (non-removable) [`collect-headings`](https://github.com/withastro/astro/blob/main/packages/integrations/mdx/src/rehype-collect-headings.ts) plugin. This applies IDs to all headings (i.e. `h1 -> h6`) in your MDX files to [link to headings via anchor tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#linking_to_an_element_on_the_same_page).

This example applies the [`rehype-minify`](https://github.com/rehypejs/rehype-minify) plugin to `.mdx` files. To customize plugin inheritance from your Markdown config or Astro's defaults, [see the `extendPlugins` option](#extendPlugins).

```js
// astro.config.mjs
import remarkToc from 'remark-toc';
import rehypeMinifyHtml from 'rehype-minify';

export default {
integrations: [mdx({
// apply remark-toc alone, removing other defaults
remarkPlugins: [remarkToc],
rehypePlugins: [rehypeMinifyHtml],
})],
}
```

### rehypePlugins
### extendPlugins

[Rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) allow you to transform the HTML that your Markdown generates. We recommend checking the [Remark plugin](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) catalog first _before_ considering rehype plugins, since most users want to transform their Markdown syntax instead. If HTML transforms are what you need, we encourage you to browse [awesome-rehype](https://github.com/rehypejs/awesome-rehype) for a full curated list of plugins!
**Type:** `'markdown' | 'astroDefaults' | false`

We apply our own (non-overridable) [`collect-headings`](https://github.com/withastro/astro/blob/main/packages/integrations/mdx/src/rehype-collect-headings.ts) plugin. This applies IDs to all headings (i.e. `h1 -> h6`) in your MDX files to [link to headings via anchor tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#linking_to_an_element_on_the_same_page).
**Default:** `'markdown'`

To apply additional rehype plugins, pass an array to the `rehypePlugins` option like so:
#### `markdown` (default)
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

By default, Astro inherits all [remark](#remarkPlugins) and [rehype](#rehypePlugins) plugins from [the `markdown` option in your Astro config](https://docs.astro.build/en/guides/markdown-content/#markdown-plugins). This also respects the [`markdown.extendDefaultPlugins`](https://docs.astro.build/en/reference/configuration-reference/#markdownextenddefaultplugins) option to extend Astro's defaults. Any additional plugins you apply in your MDX config will be applied _after_ your configured Markdown plugins.

This example applies [`remark-toc`](https://github.com/remarkjs/remark-toc) to Markdown _and_ MDX, and [`rehype-minify`](https://github.com/rehypejs/rehype-minify) to MDX alone:

```js
// astro.config.mjs
import rehypeMinifyHtml from 'rehype-minify';
import remarkToc from 'remark-toc';
import rehypeMinify from 'rehype-minify';

export default {
markdown: {
// Applied to .md and .mdx files
remarkPlugins: [remarkToc],
},
integrations: [mdx({
rehypePlugins: [rehypeMinifyHtml],
// Applied to .mdx files only
rehypePlugins: [rehypeMinify],
})],
}
```

#### `astroDefaults`

You may _only_ want to extend [Astro's default plugins](https://docs.astro.build/en/reference/configuration-reference/#markdownextenddefaultplugins) without inheriting your Markdown config. This example will apply the default [GitHub-Flavored Markdown](https://github.com/remarkjs/remark-gfm) and [Smartypants](https://github.com/silvenon/remark-smartypants) plugins alongside [`remark-toc`](https://github.com/remarkjs/remark-toc):

```js "extendPlugins: 'astroDefaults'"
// astro.config.mjs
import remarkToc from 'remark-toc';

export default {
markdown: {
remarkPlugins: [/** ignored */]
},
integrations: [mdx({
remarkPlugins: [remarkToc],
// Astro defaults applied
extendPlugins: 'astroDefaults',
})],
}
```

#### `false`

If you don't want to extend any plugins, set `extendPlugins` to `false`:

```js "extendPlugins: false"
// astro.config.mjs
import remarkToc from 'remark-toc';

export default {
integrations: [mdx({
remarkPlugins: [remarkToc],
// Astro defaults not applied
extendPlugins: false,
})],
}
```
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"es-module-lexer": "^0.10.5",
"github-slugger": "^1.4.0",
"gray-matter": "^4.0.3",
"kleur": "^4.1.4",
"rehype-raw": "^6.1.1",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
Expand Down
94 changes: 32 additions & 62 deletions packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,52 @@
import { compile as mdxCompile, nodeTypes } from '@mdx-js/mdx';
import { compile as mdxCompile } from '@mdx-js/mdx';
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import type { AstroConfig, AstroIntegration } from 'astro';
import type { AstroIntegration } from 'astro';
import { parse as parseESM } from 'es-module-lexer';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import { VFile } from 'vfile';
import type { Plugin as VitePlugin } from 'vite';
import { rehypeApplyFrontmatterExport, remarkInitializeAstroData } from './astro-data-utils.js';
import rehypeCollectHeadings from './rehype-collect-headings.js';
import remarkPrism from './remark-prism.js';
import remarkShiki from './remark-shiki.js';
import { getFileInfo, parseFrontmatter } from './utils.js';

type WithExtends<T> = T | { extends: T };

type MdxOptions = {
remarkPlugins?: WithExtends<MdxRollupPluginOptions['remarkPlugins']>;
rehypePlugins?: WithExtends<MdxRollupPluginOptions['rehypePlugins']>;
};

const DEFAULT_REMARK_PLUGINS: MdxRollupPluginOptions['remarkPlugins'] = [
remarkGfm,
remarkSmartypants,
];
const DEFAULT_REHYPE_PLUGINS: MdxRollupPluginOptions['rehypePlugins'] = [];
import { bold, blue } from 'kleur/colors';
import { rehypeApplyFrontmatterExport } from './astro-data-utils.js';
import {
getFileInfo,
parseFrontmatter,
handleExtendsNotSupported,
getRehypePlugins,
getRemarkPlugins,
} from './utils.js';
import type { MdxOptions } from './utils.js';

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';

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';

function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] = []): T[] {
if (Array.isArray(config)) return config;

return [...defaults, ...(config?.extends ?? [])];
}

async function getRemarkPlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
let remarkPlugins = [
// Initialize vfile.data.astroExports before all plugins are run
remarkInitializeAstroData,
...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
];
if (config.markdown.syntaxHighlight === 'shiki') {
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
}
if (config.markdown.syntaxHighlight === 'prism') {
remarkPlugins.push(remarkPrism);
}
return remarkPlugins;
}

function getRehypePlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): MdxRollupPluginOptions['rehypePlugins'] {
let rehypePlugins = [
[rehypeRaw, { passThrough: nodeTypes }] as any,
...handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS),
];

// getHeadings() is guaranteed by TS, so we can't allow user to override
rehypePlugins.unshift(rehypeCollectHeadings);

return rehypePlugins;
}

export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
return {
name: '@astrojs/mdx',
hooks: {
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => {
addPageExtension('.mdx');
mdxOptions.extendPlugins ??= 'markdown';

handleExtendsNotSupported(mdxOptions.remarkPlugins);
handleExtendsNotSupported(mdxOptions.rehypePlugins);

// TODO: remove for 1.0. Shipping to ease migration to new minor
if (
mdxOptions.extendPlugins === 'markdown' &&
(config.markdown.rehypePlugins?.length || config.markdown.remarkPlugins?.length)
) {
console.log(
blue(`[MDX] Now inheriting remark and rehype plugins from "markdown" config.`)
);
console.log(
`If you applied a plugin to both your Markdown and MDX configs, we suggest ${bold(
'removing the duplicate MDX entry.'
)}`
);
console.log(`See "extendPlugins" option to configure this behavior.`);
}

const mdxPluginOpts: MdxRollupPluginOptions = {
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
Expand Down
127 changes: 125 additions & 2 deletions packages/integrations/mdx/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import type { Options as AcornOpts } from 'acorn';
import { parse } from 'acorn';
import type { AstroConfig, SSRError } from 'astro';
import type { MdxjsEsm } from 'mdast-util-mdx';

import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import { bold, yellow } from 'kleur/colors';
import { nodeTypes } from '@mdx-js/mdx';
import { parse } from 'acorn';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import { remarkInitializeAstroData } from './astro-data-utils.js';
import rehypeCollectHeadings from './rehype-collect-headings.js';
import remarkPrism from './remark-prism.js';
import remarkShiki from './remark-shiki.js';
import matter from 'gray-matter';

export type MdxOptions = {
remarkPlugins?: PluggableList;
rehypePlugins?: PluggableList;
/**
* Choose which remark and rehype plugins to inherit, if any.
*
* - "markdown" (default) - inherit your project’s markdown plugin config ([see Markdown docs](https://docs.astro.build/en/guides/markdown-content/#configuring-markdown))
* - "astroDefaults" - inherit Astro’s default plugins only ([see defaults](https://docs.astro.build/en/reference/configuration-reference/#markdownextenddefaultplugins))
* - false - do not inherit any plugins
*/
extendPlugins?: 'markdown' | 'astroDefaults' | false;
};

function appendForwardSlash(path: string) {
return path.endsWith('/') ? path : path + '/';
}
Expand All @@ -14,6 +37,9 @@ interface FileInfo {
fileUrl: string;
}

const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkSmartypants];
const DEFAULT_REHYPE_PLUGINS: PluggableList = [];

/** @see 'vite-plugin-utils' for source */
export function getFileInfo(id: string, config: AstroConfig): FileInfo {
const sitePathname = appendForwardSlash(
Expand Down Expand Up @@ -83,3 +109,100 @@ export function jsToTreeNode(
},
};
}

export async function getRemarkPlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
let remarkPlugins: PluggableList = [
// Set "vfile.data.astro" for plugins to inject frontmatter
remarkInitializeAstroData,
];
switch (mdxOptions.extendPlugins) {
case false:
break;
case 'astroDefaults':
remarkPlugins = [...remarkPlugins, ...DEFAULT_REMARK_PLUGINS];
break;
default:
remarkPlugins = [
...remarkPlugins,
...(config.markdown.extendDefaultPlugins ? DEFAULT_REMARK_PLUGINS : []),
...ignoreStringPlugins(config.markdown.remarkPlugins ?? []),
];
break;
}
if (config.markdown.syntaxHighlight === 'shiki') {
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
}
if (config.markdown.syntaxHighlight === 'prism') {
remarkPlugins.push(remarkPrism);
}

remarkPlugins = [...remarkPlugins, ...(mdxOptions.remarkPlugins ?? [])];
return remarkPlugins;
}

export function getRehypePlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): MdxRollupPluginOptions['rehypePlugins'] {
let rehypePlugins: PluggableList = [
// getHeadings() is guaranteed by TS, so we can't allow user to override
rehypeCollectHeadings,
// rehypeRaw allows custom syntax highlighters to work without added config
[rehypeRaw, { passThrough: nodeTypes }] as any,
];
switch (mdxOptions.extendPlugins) {
case false:
break;
case 'astroDefaults':
rehypePlugins = [...rehypePlugins, ...DEFAULT_REHYPE_PLUGINS];
break;
default:
rehypePlugins = [
...rehypePlugins,
...(config.markdown.extendDefaultPlugins ? DEFAULT_REHYPE_PLUGINS : []),
...ignoreStringPlugins(config.markdown.rehypePlugins ?? []),
];
break;
}

rehypePlugins = [...rehypePlugins, ...(mdxOptions.rehypePlugins ?? [])];
return rehypePlugins;
}

function ignoreStringPlugins(plugins: any[]) {
let validPlugins: PluggableList = [];
let hasInvalidPlugin = false;
for (const plugin of plugins) {
if (typeof plugin === 'string') {
console.warn(yellow(`[MDX] ${bold(plugin)} not applied.`));
hasInvalidPlugin = true;
} else if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
console.warn(yellow(`[MDX] ${bold(plugin[0])} not applied.`));
hasInvalidPlugin = true;
} else {
validPlugins.push(plugin);
}
}
if (hasInvalidPlugin) {
console.warn(
`To inherit Markdown plugins in MDX, please use explicit imports in your config instead of "strings." See Markdown docs: https://docs.astro.build/en/guides/markdown-content/#markdown-plugins`
);
}
return validPlugins;
}

// TODO: remove for 1.0
export function handleExtendsNotSupported(pluginConfig: any) {
if (
typeof pluginConfig === 'object' &&
pluginConfig !== null &&
(pluginConfig as any).hasOwnProperty('extends')
) {
throw new Error(
`[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`
);
}
}
Loading