[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:
parent
d50f46bfab
commit
19433eb4a4
8 changed files with 217 additions and 6 deletions
5
.changeset/new-coats-cheer.md
Normal file
5
.changeset/new-coats-cheer.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/mdx': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add remarkPlugins and rehypePlugins to config, with the same default plugins as our standard Markdown parser
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
3
packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-gfm.mdx
vendored
Normal file
3
packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-gfm.mdx
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# GitHub-flavored Markdown test
|
||||||
|
|
||||||
|
This should auto-gen a link: https://example.com
|
19
packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-toc.mdx
vendored
Normal file
19
packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-toc.mdx
vendored
Normal 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
|
58
packages/integrations/mdx/test/mdx-remark-plugins.test.js
Normal file
58
packages/integrations/mdx/test/mdx-remark-plugins.test.js
Normal 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;
|
||||||
|
});
|
||||||
|
});
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue