[MDX] Support remark and rehype plugins, with defaults (#3977)

* feaet: allow remark and rehype plugin config

* deps: add remark-gfm, remark-smartypants

* feat: add gfm and smartypants by default

* test: add GFM and remark plugin tests

* feat: preserve default plugins with "extends"

* docs: add remarkPlugins

* docs: add rehypePlugins

* chore: changeset

* fix: remove skip from mdx tests

* chore: dup hyperlink flavor text

* chore: authGen -> autoGen

* nit: markdown -> Markdown

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* nit: markdown -> Markdown 1

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* nit: markdown -> Markdown 2

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* nit: markdown -> Markdown 3

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* nit: markdown -> Markdown 4

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
Ben Holmes 2022-07-20 14:14:23 -04:00 committed by GitHub
parent d50f46bfab
commit 19433eb4a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 217 additions and 6 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/mdx': minor
---
Add remarkPlugins and rehypePlugins to config, with the same default plugins as our standard Markdown parser

View file

@ -80,7 +80,65 @@ Also check our [Astro Integration Documentation][astro-integration] for more on
## Configuration ## Configuration
There are currently no configuration options for the `@astrojs/mdx` integration. Please [open an issue](https://github.com/withastro/astro/issues/new/choose) if you have a compelling use case to share. <details>
<summary><strong>remarkPlugins</strong></summary>
**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:
```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] },
})],
}
```
To apply plugins _without_ Astro's defaults, you can apply a plain array:
```js
// astro.config.mjs
import remarkToc from 'remark-toc';
export default {
integrations: [mdx({
// apply remark-toc alone, removing other defaults
remarkPlugins: [remarkToc],
})],
}
```
</details>
<details>
<summary><strong>rehypePlugins</strong></summary>
**Default plugins:** none
[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!
To apply rehype plugins, use the `rehypePlugins` configuration option like so:
```js
// astro.config.mjs
import rehypeMinifyHtml from 'rehype-minify';
export default {
integrations: [mdx({
rehypePlugins: [rehypeMinifyHtml],
})],
}
```
</details>
## Examples ## Examples

View file

@ -31,7 +31,9 @@
}, },
"dependencies": { "dependencies": {
"@mdx-js/rollup": "^2.1.1", "@mdx-js/rollup": "^2.1.1",
"es-module-lexer": "^0.10.5" "es-module-lexer": "^0.10.5",
"remark-gfm": "^3.0.1",
"remark-smartypants": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.3.1", "@types/chai": "^4.3.1",
@ -41,7 +43,8 @@
"astro-scripts": "workspace:*", "astro-scripts": "workspace:*",
"chai": "^4.3.6", "chai": "^4.3.6",
"linkedom": "^0.14.12", "linkedom": "^0.14.12",
"mocha": "^9.2.2" "mocha": "^9.2.2",
"remark-toc": "^8.0.1"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.12.0" "node": "^14.18.0 || >=16.12.0"

View file

@ -1,9 +1,26 @@
import mdxPlugin from '@mdx-js/rollup'; import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import type { AstroIntegration } from 'astro'; import type { AstroIntegration } from 'astro';
import { parse as parseESM } from 'es-module-lexer'; import { parse as parseESM } from 'es-module-lexer';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import { getFileInfo } from './utils.js'; import { getFileInfo } from './utils.js';
export default function mdx(): AstroIntegration { type WithExtends<T> = T | { extends: T };
type MdxOptions = {
remarkPlugins?: WithExtends<MdxRollupPluginOptions['remarkPlugins']>;
rehypePlugins?: WithExtends<MdxRollupPluginOptions['rehypePlugins']>;
}
const DEFAULT_REMARK_PLUGINS = [remarkGfm, remarkSmartypants];
function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] = []): T[] | undefined {
if (Array.isArray(config)) return config;
return [...defaults, ...(config?.extends ?? [])];
}
export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
return { return {
name: '@astrojs/mdx', name: '@astrojs/mdx',
hooks: { hooks: {
@ -15,6 +32,9 @@ export default function mdx(): AstroIntegration {
{ {
enforce: 'pre', enforce: 'pre',
...mdxPlugin({ ...mdxPlugin({
remarkPlugins: handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
rehypePlugins: handleExtends(mdxOptions.rehypePlugins),
// place these after so the user can't override
jsx: true, jsx: true,
jsxImportSource: 'astro', jsxImportSource: 'astro',
// Note: disable `.md` support // Note: disable `.md` support

View file

@ -0,0 +1,3 @@
# GitHub-flavored Markdown test
This should auto-gen a link: https://example.com

View file

@ -0,0 +1,19 @@
# TOC test
## Table of contents
## Section 1
Some text!
### Subsection 1
Some subsection test!
### Subsection 2
Oh cool, more text!
## Section 2
And section 2, with a hyperlink to check GFM is preserved: https://handle-me-gfm.com

View file

@ -0,0 +1,58 @@
import mdx from '@astrojs/mdx';
import { expect } from 'chai';
import { parseHTML } from 'linkedom';
import { loadFixture } from '../../../astro/test/test-utils.js';
import remarkToc from 'remark-toc';
const FIXTURE_ROOT = new URL('./fixtures/mdx-remark-plugins/', import.meta.url);
describe('MDX remark plugins', () => {
it('supports custom remark plugins - TOC', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx({
remarkPlugins: [remarkToc],
})],
});
await fixture.build();
const html = await fixture.readFile('/with-toc/index.html');
const { document } = parseHTML(html);
const tocLink1 = document.querySelector('ul a[href="#section-1"]');
expect(tocLink1).to.not.be.null;
});
it('applies GitHub-flavored markdown by default', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx()],
});
await fixture.build();
const html = await fixture.readFile('/with-gfm/index.html');
const { document } = parseHTML(html);
const autoGenLink = document.querySelector('a[href="https://example.com"]');
expect(autoGenLink).to.not.be.null;
});
it('preserves default GitHub-flavored markdown with "extends"', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx({
remarkPlugins: { extends: [remarkToc] },
})],
});
await fixture.build();
const html = await fixture.readFile('/with-toc/index.html');
const { document } = parseHTML(html);
const tocLink1 = document.querySelector('ul a[href="#section-1"]');
expect(tocLink1).to.not.be.null;
const autoGenLink = document.querySelector('a[href="https://handle-me-gfm.com"]');
expect(autoGenLink).to.not.be.null;
});
});

View file

@ -2101,9 +2101,14 @@ importers:
es-module-lexer: ^0.10.5 es-module-lexer: ^0.10.5
linkedom: ^0.14.12 linkedom: ^0.14.12
mocha: ^9.2.2 mocha: ^9.2.2
remark-gfm: ^3.0.1
remark-smartypants: ^2.0.0
remark-toc: ^8.0.1
dependencies: dependencies:
'@mdx-js/rollup': 2.1.2 '@mdx-js/rollup': 2.1.2
es-module-lexer: 0.10.5 es-module-lexer: 0.10.5
remark-gfm: 3.0.1
remark-smartypants: 2.0.0
devDependencies: devDependencies:
'@types/chai': 4.3.1 '@types/chai': 4.3.1
'@types/mocha': 9.1.1 '@types/mocha': 9.1.1
@ -2113,6 +2118,7 @@ importers:
chai: 4.3.6 chai: 4.3.6
linkedom: 0.14.12 linkedom: 0.14.12
mocha: 9.2.2 mocha: 9.2.2
remark-toc: 8.0.1
packages/integrations/netlify: packages/integrations/netlify:
specifiers: specifiers:
@ -7995,6 +8001,10 @@ packages:
'@types/node': 18.0.3 '@types/node': 18.0.3
dev: true dev: true
/@types/extend/3.0.1:
resolution: {integrity: sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==}
dev: true
/@types/github-slugger/1.3.0: /@types/github-slugger/1.3.0:
resolution: {integrity: sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==} resolution: {integrity: sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==}
dev: true dev: true
@ -12029,7 +12039,19 @@ packages:
/mdast-util-to-string/3.1.0: /mdast-util-to-string/3.1.0:
resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==} resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==}
dev: false
/mdast-util-toc/6.1.0:
resolution: {integrity: sha512-0PuqZELXZl4ms1sF7Lqigrqik4Ll3UhbI+jdTrfw7pZ9QPawgl7LD4GQ8MkU7bT/EwiVqChNTbifa2jLLKo76A==}
dependencies:
'@types/extend': 3.0.1
'@types/github-slugger': 1.3.0
'@types/mdast': 3.0.10
extend: 3.0.2
github-slugger: 1.4.0
mdast-util-to-string: 3.1.0
unist-util-is: 5.1.1
unist-util-visit: 3.1.0
dev: true
/mdurl/1.0.1: /mdurl/1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
@ -14029,6 +14051,14 @@ packages:
unist-util-visit: 4.1.0 unist-util-visit: 4.1.0
dev: false dev: false
/remark-toc/8.0.1:
resolution: {integrity: sha512-7he2VOm/cy13zilnOTZcyAoyoolV26ULlon6XyCFU+vG54Z/LWJnwphj/xKIDLOt66QmJUgTyUvLVHi2aAElyg==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-toc: 6.1.0
unified: 10.1.2
dev: true
/require-directory/2.1.1: /require-directory/2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -15326,6 +15356,13 @@ packages:
unist-util-is: 3.0.0 unist-util-is: 3.0.0
dev: true dev: true
/unist-util-visit-parents/4.1.1:
resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==}
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.1.1
dev: true
/unist-util-visit-parents/5.1.0: /unist-util-visit-parents/5.1.0:
resolution: {integrity: sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==} resolution: {integrity: sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==}
dependencies: dependencies:
@ -15338,6 +15375,14 @@ packages:
unist-util-visit-parents: 2.1.2 unist-util-visit-parents: 2.1.2
dev: true dev: true
/unist-util-visit/3.1.0:
resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==}
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.1.1
unist-util-visit-parents: 4.1.1
dev: true
/unist-util-visit/4.1.0: /unist-util-visit/4.1.0:
resolution: {integrity: sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==} resolution: {integrity: sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==}
dependencies: dependencies: