diff --git a/.changeset/new-coats-cheer.md b/.changeset/new-coats-cheer.md new file mode 100644 index 000000000..12b4c0797 --- /dev/null +++ b/.changeset/new-coats-cheer.md @@ -0,0 +1,5 @@ +--- +'@astrojs/mdx': minor +--- + +Add remarkPlugins and rehypePlugins to config, with the same default plugins as our standard Markdown parser diff --git a/packages/integrations/mdx/README.md b/packages/integrations/mdx/README.md index ed3e5ec86..541529f9f 100644 --- a/packages/integrations/mdx/README.md +++ b/packages/integrations/mdx/README.md @@ -80,7 +80,65 @@ Also check our [Astro Integration Documentation][astro-integration] for more on ## 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. +
+ 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: + +```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], + })], +} +``` + +
+ +
+ rehypePlugins + +**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], + })], +} +``` +
## Examples diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 9ea034634..51036b0e1 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -31,7 +31,9 @@ }, "dependencies": { "@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": { "@types/chai": "^4.3.1", @@ -41,7 +43,8 @@ "astro-scripts": "workspace:*", "chai": "^4.3.6", "linkedom": "^0.14.12", - "mocha": "^9.2.2" + "mocha": "^9.2.2", + "remark-toc": "^8.0.1" }, "engines": { "node": "^14.18.0 || >=16.12.0" diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index 140e86632..af63c4ad2 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -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 { parse as parseESM } from 'es-module-lexer'; +import remarkGfm from 'remark-gfm'; +import remarkSmartypants from 'remark-smartypants'; import { getFileInfo } from './utils.js'; -export default function mdx(): AstroIntegration { +type WithExtends = T | { extends: T }; + +type MdxOptions = { + remarkPlugins?: WithExtends; + rehypePlugins?: WithExtends; +} + +const DEFAULT_REMARK_PLUGINS = [remarkGfm, remarkSmartypants]; + +function handleExtends(config: WithExtends, defaults: T[] = []): T[] | undefined { + if (Array.isArray(config)) return config; + + return [...defaults, ...(config?.extends ?? [])]; +} + +export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration { return { name: '@astrojs/mdx', hooks: { @@ -15,6 +32,9 @@ export default function mdx(): AstroIntegration { { enforce: 'pre', ...mdxPlugin({ + remarkPlugins: handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS), + rehypePlugins: handleExtends(mdxOptions.rehypePlugins), + // place these after so the user can't override jsx: true, jsxImportSource: 'astro', // Note: disable `.md` support diff --git a/packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-gfm.mdx b/packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-gfm.mdx new file mode 100644 index 000000000..bbb0e7399 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-gfm.mdx @@ -0,0 +1,3 @@ +# GitHub-flavored Markdown test + +This should auto-gen a link: https://example.com diff --git a/packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-toc.mdx b/packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-toc.mdx new file mode 100644 index 000000000..fe9cac3ee --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/with-toc.mdx @@ -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 diff --git a/packages/integrations/mdx/test/mdx-remark-plugins.test.js b/packages/integrations/mdx/test/mdx-remark-plugins.test.js new file mode 100644 index 000000000..545df3174 --- /dev/null +++ b/packages/integrations/mdx/test/mdx-remark-plugins.test.js @@ -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; + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38df67c29..e323b72d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2101,9 +2101,14 @@ importers: es-module-lexer: ^0.10.5 linkedom: ^0.14.12 mocha: ^9.2.2 + remark-gfm: ^3.0.1 + remark-smartypants: ^2.0.0 + remark-toc: ^8.0.1 dependencies: '@mdx-js/rollup': 2.1.2 es-module-lexer: 0.10.5 + remark-gfm: 3.0.1 + remark-smartypants: 2.0.0 devDependencies: '@types/chai': 4.3.1 '@types/mocha': 9.1.1 @@ -2113,6 +2118,7 @@ importers: chai: 4.3.6 linkedom: 0.14.12 mocha: 9.2.2 + remark-toc: 8.0.1 packages/integrations/netlify: specifiers: @@ -7995,6 +8001,10 @@ packages: '@types/node': 18.0.3 dev: true + /@types/extend/3.0.1: + resolution: {integrity: sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==} + dev: true + /@types/github-slugger/1.3.0: resolution: {integrity: sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==} dev: true @@ -12029,7 +12039,19 @@ packages: /mdast-util-to-string/3.1.0: 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: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} @@ -14029,6 +14051,14 @@ packages: unist-util-visit: 4.1.0 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: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -15326,6 +15356,13 @@ packages: unist-util-is: 3.0.0 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: resolution: {integrity: sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==} dependencies: @@ -15338,6 +15375,14 @@ packages: unist-util-visit-parents: 2.1.2 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: resolution: {integrity: sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==} dependencies: