Markdown and MDX configuration rework (#5684)
* feat: change extendDefaults -> gfm * deps: remove smartypants from md/remark * tests: update markdown plugin tests * fix: borked lockfile * feat: allow all Markdown options in MDX config, with extend * deps: remove smartypants from MDX * chore: remove unused `mode` property * chore: remark rehype types * chore: dead code * fix: order of default config properties * refactor: move md defaults to remark * fix: RemarkRehype type * fix: apply defaults based on MD defaults * chore: update plugin tests * chore: add syntaxHighlight test * refactor: remove drafts from config defaults * docs: new MDX config options * chore: add changeset * edit: test both extends for syntax highlight * refactor: remove MDX config deep merge * docs: update README and changeset * edit: avoid -> disable Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * edit: `drafts` clarification Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * edit: remove "scare quotes" Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * docs: MDX config options redraft * docs: add migration * chore: changeset heading levels * refactor: githubFlavoredMarkdown -> gfm * chore: remove unused imports Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
163a9a9d0e
commit
a9c2920264
14 changed files with 2196 additions and 1673 deletions
63
.changeset/shaggy-keys-turn.md
Normal file
63
.changeset/shaggy-keys-turn.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
'astro': major
|
||||
'@astrojs/markdown-remark': major
|
||||
'@astrojs/mdx': minor
|
||||
---
|
||||
|
||||
Refine Markdown and MDX configuration options for ease-of-use.
|
||||
|
||||
#### Markdown
|
||||
|
||||
- **Remove `remark-smartypants`** from Astro's default Markdown plugins.
|
||||
- **Replace the `extendDefaultPlugins` option** with a simplified `gfm` boolean. This is enabled by default, and can be disabled to remove GitHub-Flavored Markdown.
|
||||
- Ensure GitHub-Flavored Markdown is applied whether or not custom `remarkPlugins` or `rehypePlugins` are configured. If you want to apply custom plugins _and_ remove GFM, manually set `gfm: false` in your config.
|
||||
|
||||
#### MDX
|
||||
|
||||
- Support _all_ Markdown configuration options (except `drafts`) from your MDX integration config. This includes `syntaxHighlighting` and `shikiConfig` options to further customize the MDX renderer.
|
||||
- Simplify `extendDefaults` to an `extendMarkdownConfig` option. MDX options will default to their equivalent in your Markdown config. By setting `extendMarkdownConfig` to false, you can "eject" to set your own syntax highlighting, plugins, and more.
|
||||
|
||||
#### Migration
|
||||
|
||||
To preserve your existing Markdown and MDX setup, you may need some configuration changes:
|
||||
|
||||
##### Smartypants manual installation
|
||||
|
||||
[Smartypants](https://github.com/silvenon/remark-smartypants) has been removed from Astro's default setup. If you rely on this plugin, [install `remark-smartypants`](https://github.com/silvenon/remark-smartypants#installing) and apply to your `astro.config.*`:
|
||||
|
||||
```diff
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
+ import smartypants from 'remark-smartypants';
|
||||
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
+ remarkPlugins: [smartypants],
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
##### Migrate `extendDefaultPlugins` to `gfm`
|
||||
|
||||
You may have disabled Astro's built-in plugins (GitHub-Flavored Markdown and Smartypants) with the `extendDefaultPlugins` option. Since Smartypants has been removed, this has been renamed to `gfm`.
|
||||
|
||||
```diff
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
- extendDefaultPlugins: false,
|
||||
+ gfm: false,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Additionally, applying remark and rehype plugins **no longer disables** `gfm`. You will need to opt-out manually by setting `gfm` to `false`.
|
||||
|
||||
##### Migrate MDX's `extendPlugins` to `extendMarkdownConfig`
|
||||
|
||||
You may have used the `extendPlugins` option to manage plugin defaults in MDX. This has been replaced by 2 flags:
|
||||
- `extendMarkdownConfig` (`true` by default) to toggle Markdown config inheritance. This replaces the `extendPlugins: 'markdown'` option.
|
||||
- `gfm` (`true` by default) to toggle GitHub-Flavored Markdown in MDX. This replaces the `extendPlugins: 'defaults'` option.
|
|
@ -734,10 +734,6 @@ export interface AstroUserConfig {
|
|||
* @description
|
||||
* Pass [remark plugins](https://github.com/remarkjs/remark) to customize how your Markdown is built. You can import and apply the plugin function (recommended), or pass the plugin name as a string.
|
||||
*
|
||||
* :::caution
|
||||
* Providing a list of plugins will **remove** our default plugins. To preserve these defaults, see the [`extendDefaultPlugins`](#markdownextenddefaultplugins) flag.
|
||||
* :::
|
||||
*
|
||||
* ```js
|
||||
* import remarkToc from 'remark-toc';
|
||||
* {
|
||||
|
@ -755,10 +751,6 @@ export interface AstroUserConfig {
|
|||
* @description
|
||||
* Pass [rehype plugins](https://github.com/remarkjs/remark-rehype) to customize how your Markdown's output HTML is processed. You can import and apply the plugin function (recommended), or pass the plugin name as a string.
|
||||
*
|
||||
* :::caution
|
||||
* Providing a list of plugins will **remove** our default plugins. To preserve these defaults, see the [`extendDefaultPlugins`](#markdownextenddefaultplugins) flag.
|
||||
* :::
|
||||
*
|
||||
* ```js
|
||||
* import rehypeMinifyHtml from 'rehype-minify';
|
||||
* {
|
||||
|
@ -771,23 +763,21 @@ export interface AstroUserConfig {
|
|||
rehypePlugins?: RehypePlugins;
|
||||
/**
|
||||
* @docs
|
||||
* @name markdown.extendDefaultPlugins
|
||||
* @name markdown.gfm
|
||||
* @type {boolean}
|
||||
* @default `false`
|
||||
* @default `true`
|
||||
* @description
|
||||
* Astro applies the [GitHub-flavored Markdown](https://github.com/remarkjs/remark-gfm) and [Smartypants](https://github.com/silvenon/remark-smartypants) plugins by default. When adding your own remark or rehype plugins, you can preserve these defaults by setting the `extendDefaultPlugins` flag to `true`:
|
||||
* Astro uses [GitHub-flavored Markdown](https://github.com/remarkjs/remark-gfm) by default. To disable this, set the `gfm` flag to `false`:
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* markdown: {
|
||||
* extendDefaultPlugins: true,
|
||||
* remarkPlugins: [exampleRemarkPlugin],
|
||||
* rehypePlugins: [exampleRehypePlugin],
|
||||
* gfm: false,
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
extendDefaultPlugins?: boolean;
|
||||
gfm?: boolean;
|
||||
/**
|
||||
* @docs
|
||||
* @name markdown.remarkRehype
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { RehypePlugin, RemarkPlugin, RemarkRehype } from '@astrojs/markdown-remark';
|
||||
import { markdownConfigDefaults } from '@astrojs/markdown-remark';
|
||||
import type * as Postcss from 'postcss';
|
||||
import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki';
|
||||
import type { AstroUserConfig, ViteUserConfig } from '../../@types/astro';
|
||||
|
@ -33,15 +34,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
integrations: [],
|
||||
markdown: {
|
||||
drafts: false,
|
||||
syntaxHighlight: 'shiki',
|
||||
shikiConfig: {
|
||||
langs: [],
|
||||
theme: 'github-dark',
|
||||
wrap: false,
|
||||
},
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
remarkRehype: {},
|
||||
...markdownConfigDefaults,
|
||||
},
|
||||
vite: {},
|
||||
legacy: {
|
||||
|
@ -184,7 +177,7 @@ export const AstroConfigSchema = z.object({
|
|||
.custom<RemarkRehype>((data) => data instanceof Object && !Array.isArray(data))
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.markdown.remarkRehype),
|
||||
extendDefaultPlugins: z.boolean().default(false),
|
||||
gfm: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.gfm),
|
||||
})
|
||||
.default({}),
|
||||
vite: z
|
||||
|
|
|
@ -46,29 +46,51 @@ describe('Astro Markdown plugins', () => {
|
|||
expect($('#hello-world').hasClass('title')).to.equal(true);
|
||||
});
|
||||
|
||||
for (const extendDefaultPlugins of [true, false]) {
|
||||
it(`Handles default plugins when extendDefaultPlugins = ${extendDefaultPlugins}`, async () => {
|
||||
// Asserts Astro 1.0 behavior is removed. Test can be removed in Astro 3.0.
|
||||
it('Still applies GFM when user plugins are provided', async () => {
|
||||
const fixture = await buildFixture({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
|
||||
},
|
||||
});
|
||||
const html = await fixture.readFile('/with-gfm/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: GFM autolink applied correctly
|
||||
expect($('a[href="https://example.com"]')).to.have.lengthOf(1);
|
||||
|
||||
// test 2: remark plugins still applied
|
||||
expect(html).to.include('Remark plugin applied!');
|
||||
|
||||
// test 3: rehype plugins still applied
|
||||
expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
|
||||
expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
|
||||
});
|
||||
|
||||
for (const gfm of [true, false]) {
|
||||
it(`Handles GFM when gfm = ${gfm}`, async () => {
|
||||
const fixture = await buildFixture({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
|
||||
extendDefaultPlugins,
|
||||
gfm,
|
||||
},
|
||||
});
|
||||
const html = await fixture.readFile('/with-gfm/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: GFM autolink applied correctly
|
||||
if (extendDefaultPlugins === true) {
|
||||
if (gfm === true) {
|
||||
expect($('a[href="https://example.com"]')).to.have.lengthOf(1);
|
||||
} else {
|
||||
expect($('a[href="https://example.com"]')).to.have.lengthOf(0);
|
||||
}
|
||||
|
||||
// test 2: (sanity check) remark plugins still applied
|
||||
// test 2: remark plugins still applied
|
||||
expect(html).to.include('Remark plugin applied!');
|
||||
|
||||
// test 3: (sanity check) rehype plugins still applied
|
||||
// test 3: rehype plugins still applied
|
||||
expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
|
||||
expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
|
||||
});
|
||||
|
|
|
@ -78,116 +78,100 @@ Visit the [MDX docs](https://mdxjs.com/docs/what-is-mdx/) to learn about using s
|
|||
|
||||
Once the MDX integration is installed, no configuration is necessary to use `.mdx` files in your Astro project.
|
||||
|
||||
You can extend how your MDX is rendered by adding remark, rehype and recma plugins.
|
||||
You can configure how your MDX is rendered with the following options:
|
||||
|
||||
- [`extendPlugins`](#extendplugins)
|
||||
- [`remarkRehype`](#remarkrehype)
|
||||
- [`remarkPlugins`](#remarkplugins)
|
||||
- [`rehypePlugins`](#rehypeplugins)
|
||||
- [Options inherited from Markdown config](#options-inherited-from-markdown-config)
|
||||
- [`extendMarkdownConfig`](#extendmarkdownconfig)
|
||||
- [`recmaPlugins`](#recmaplugins)
|
||||
|
||||
### `extendPlugins`
|
||||
### Options inherited from Markdown config
|
||||
|
||||
You can customize how MDX files inherit your project’s existing Markdown configuration using the `extendPlugins` option.
|
||||
All [`markdown` configuration options](https://docs.astro.build/en/reference/configuration-reference/#markdown-options) except `drafts` can be configured separately in the MDX integration. This includes remark and rehype plugins, syntax highlighting, and more. Options will default to those in your Markdown config ([see the `extendMarkdownConfig` option](#extendmarkdownconfig) to modify this).
|
||||
|
||||
#### `markdown` (default)
|
||||
:::note
|
||||
There is no separate MDX configuration for [including pages marked as draft in the build](https://docs.astro.build/en/reference/configuration-reference/#markdowndrafts). This Markdown setting will be respected by both Markdown and MDX files and cannot be overriden for MDX files specifically.
|
||||
:::
|
||||
|
||||
Astro's MDX files will inherit all [`markdown` options](https://docs.astro.build/en/reference/configuration-reference/#markdown-options) in your Astro configuration file, which includes the [GitHub-Flavored Markdown](https://github.com/remarkjs/remark-gfm) and [Smartypants](https://github.com/silvenon/remark-smartypants) plugins by default.
|
||||
|
||||
Any additional plugins you apply in your MDX config will be applied *after* your configured Markdown plugins.
|
||||
|
||||
#### `astroDefaults`
|
||||
|
||||
Astro's MDX files will apply only [Astro's default plugins](/en/reference/configuration-reference/#markdownextenddefaultplugins), without inheriting the rest of 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) to your MDX files, while ignoring any `markdown.remarkPlugins` configuration:
|
||||
|
||||
```js "extendPlugins: 'astroDefaults'"
|
||||
```ts
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import remarkToc from 'remark-toc';
|
||||
import rehypeMinifyHtml from 'rehype-minify-html';
|
||||
|
||||
export default {
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
mdx({
|
||||
syntaxHighlight: 'shiki',
|
||||
shikiConfig: { theme: 'dracula' },
|
||||
remarkPlugins: [remarkToc],
|
||||
rehypePlugins: [rehypeMinifyHtml],
|
||||
remarkRehype: { footnoteLabel: 'Footnotes' },
|
||||
gfm: false,
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
:::caution
|
||||
MDX does not support passing remark and rehype plugins as a string. You should install, import, and apply the plugin function instead.
|
||||
:::
|
||||
|
||||
📚 See the [Markdown Options reference](https://docs.astro.build/en/reference/configuration-reference/#markdown-options) for a complete list of options.
|
||||
|
||||
### `extendMarkdownConfig`
|
||||
|
||||
- **Type:** `boolean`
|
||||
- **Default:** `true`
|
||||
|
||||
MDX will extend [your project's existing Markdown configuration](https://docs.astro.build/en/reference/configuration-reference/#markdown-options) by default. To override individual options, you can specify their equivalent in your MDX configuration.
|
||||
|
||||
For example, say you need to disable GitHub-Flavored Markdown and apply a different set of remark plugins for MDX files. You can apply these options like so, with `extendMarkdownConfig` enabled by default:
|
||||
|
||||
```ts
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
remarkPlugins: [/** ignored */]
|
||||
syntaxHighlight: 'prism',
|
||||
remarkPlugins: [remarkPlugin1],
|
||||
gfm: true,
|
||||
},
|
||||
integrations: [mdx({
|
||||
remarkPlugins: [remarkToc],
|
||||
// Astro defaults applied
|
||||
extendPlugins: 'astroDefaults',
|
||||
})],
|
||||
}
|
||||
integrations: [
|
||||
mdx({
|
||||
// `syntaxHighlight` inherited from Markdown
|
||||
|
||||
// Markdown `remarkPlugins` ignored,
|
||||
// only `remarkPlugin2` applied.
|
||||
remarkPlugins: [remarkPlugin2],
|
||||
// `gfm` overridden to `false`
|
||||
gfm: false,
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
#### `false`
|
||||
You may also need to disable `markdown` config extension in MDX. For this, set `extendMarkdownConfig` to `false`:
|
||||
|
||||
Astro's MDX files will not inherit any [`markdown` options](https://docs.astro.build/en/reference/configuration-reference/#markdown-options), nor will any Astro Markdown defaults be applied:
|
||||
|
||||
```js "extendPlugins: false"
|
||||
```ts
|
||||
// astro.config.mjs
|
||||
import remarkToc from 'remark-toc';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
export default {
|
||||
integrations: [mdx({
|
||||
remarkPlugins: [remarkToc],
|
||||
// Astro defaults not applied
|
||||
extendPlugins: false,
|
||||
})],
|
||||
}
|
||||
```
|
||||
|
||||
### `remarkRehype`
|
||||
|
||||
Markdown content is transformed into HTML through remark-rehype which has [a number of options](https://github.com/remarkjs/remark-rehype#options).
|
||||
|
||||
You can set remark-rehype options in your config file:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
export default {
|
||||
integrations: [mdx({
|
||||
remarkRehype: {
|
||||
footnoteLabel: 'Catatan kaki',
|
||||
footnoteBackLabel: 'Kembali ke konten',
|
||||
},
|
||||
})],
|
||||
};
|
||||
```
|
||||
This inherits the configuration of [`markdown.remarkRehype`](https://docs.astro.build/en/reference/configuration-reference/#markdownremarkrehype). This behavior can be changed by configuring `extendPlugins`.
|
||||
|
||||
### `remarkPlugins`
|
||||
|
||||
Browse [awesome-remark](https://github.com/remarkjs/awesome-remark) for a full curated list of [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) to extend your Markdown's capabilities.
|
||||
|
||||
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({
|
||||
remarkPlugins: [remarkToc],
|
||||
})],
|
||||
}
|
||||
```
|
||||
|
||||
### `rehypePlugins`
|
||||
|
||||
Browse [awesome-rehype](https://github.com/rehypejs/awesome-rehype) for a full curated list of [Rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) to transform the HTML that your Markdown generates.
|
||||
|
||||
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-accessible-emojis`](https://www.npmjs.com/package/rehype-accessible-emojis) 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 rehypeAccessibleEmojis from 'rehype-accessible-emojis';
|
||||
|
||||
export default {
|
||||
integrations: [mdx({
|
||||
rehypePlugins: [rehypeAccessibleEmojis],
|
||||
})],
|
||||
}
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkPlugin1],
|
||||
},
|
||||
integrations: [
|
||||
mdx({
|
||||
// Markdown config now ignored
|
||||
extendMarkdownConfig: false,
|
||||
// No `remarkPlugins` applied
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### `recmaPlugins`
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
"rehype-raw": "^6.1.1",
|
||||
"remark-frontmatter": "^4.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-smartypants": "^2.0.0",
|
||||
"shiki": "^0.11.1",
|
||||
"unist-util-visit": "^4.1.0",
|
||||
"vfile": "^5.3.2"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/internal.js';
|
||||
import { markdownConfigDefaults } from '@astrojs/markdown-remark';
|
||||
import { compile as mdxCompile } from '@mdx-js/mdx';
|
||||
import { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||||
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
||||
|
@ -17,44 +18,41 @@ const RAW_CONTENT_ERROR =
|
|||
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';
|
||||
|
||||
export type MdxOptions = {
|
||||
remarkPlugins?: PluggableList;
|
||||
rehypePlugins?: PluggableList;
|
||||
recmaPlugins?: 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;
|
||||
remarkRehype?: RemarkRehypeOptions;
|
||||
export type MdxOptions = Omit<typeof markdownConfigDefaults, 'remarkPlugins' | 'rehypePlugins'> & {
|
||||
extendMarkdownConfig: boolean;
|
||||
recmaPlugins: PluggableList;
|
||||
// Markdown allows strings as remark and rehype plugins.
|
||||
// This is not supported by the MDX compiler, so override types here.
|
||||
remarkPlugins: PluggableList;
|
||||
rehypePlugins: PluggableList;
|
||||
remarkRehype: RemarkRehypeOptions;
|
||||
};
|
||||
|
||||
export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
||||
export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroIntegration {
|
||||
return {
|
||||
name: '@astrojs/mdx',
|
||||
hooks: {
|
||||
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => {
|
||||
addPageExtension('.mdx');
|
||||
mdxOptions.extendPlugins ??= 'markdown';
|
||||
|
||||
const remarkRehypeOptions = {
|
||||
...(mdxOptions.extendPlugins === 'markdown' ? config.markdown.remarkRehype : {}),
|
||||
...mdxOptions.remarkRehype,
|
||||
};
|
||||
const extendMarkdownConfig =
|
||||
partialMdxOptions.extendMarkdownConfig ?? defaultOptions.extendMarkdownConfig;
|
||||
|
||||
const mdxOptions = applyDefaultOptions({
|
||||
options: partialMdxOptions,
|
||||
defaults: extendMarkdownConfig ? config.markdown : defaultOptions,
|
||||
});
|
||||
|
||||
const mdxPluginOpts: MdxRollupPluginOptions = {
|
||||
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
|
||||
rehypePlugins: getRehypePlugins(mdxOptions, config),
|
||||
rehypePlugins: getRehypePlugins(mdxOptions),
|
||||
recmaPlugins: mdxOptions.recmaPlugins,
|
||||
remarkRehypeOptions: mdxOptions.remarkRehype,
|
||||
jsx: true,
|
||||
jsxImportSource: 'astro',
|
||||
// Note: disable `.md` (and other alternative extensions for markdown files like `.markdown`) support
|
||||
format: 'mdx',
|
||||
mdExtensions: [],
|
||||
remarkRehypeOptions,
|
||||
};
|
||||
|
||||
let importMetaEnv: Record<string, any> = {
|
||||
|
@ -166,6 +164,34 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
|||
};
|
||||
}
|
||||
|
||||
const defaultOptions: MdxOptions = {
|
||||
...markdownConfigDefaults,
|
||||
extendMarkdownConfig: true,
|
||||
recmaPlugins: [],
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
remarkRehype: {},
|
||||
};
|
||||
|
||||
function applyDefaultOptions({
|
||||
options,
|
||||
defaults,
|
||||
}: {
|
||||
options: Partial<MdxOptions>;
|
||||
defaults: MdxOptions;
|
||||
}): MdxOptions {
|
||||
return {
|
||||
syntaxHighlight: options.syntaxHighlight ?? defaults.syntaxHighlight,
|
||||
extendMarkdownConfig: options.extendMarkdownConfig ?? defaults.extendMarkdownConfig,
|
||||
recmaPlugins: options.recmaPlugins ?? defaults.recmaPlugins,
|
||||
remarkRehype: options.remarkRehype ?? defaults.remarkRehype,
|
||||
gfm: options.gfm ?? defaults.gfm,
|
||||
remarkPlugins: options.remarkPlugins ?? defaults.remarkPlugins,
|
||||
rehypePlugins: options.rehypePlugins ?? defaults.rehypePlugins,
|
||||
shikiConfig: options.shikiConfig ?? defaults.shikiConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Converts the first dot in `import.meta.env` to its Unicode escape sequence,
|
||||
// which prevents Vite from replacing strings like `import.meta.env.SITE`
|
||||
// in our JS representation of loaded Markdown files
|
||||
|
|
|
@ -14,7 +14,6 @@ import type { Image } from 'mdast';
|
|||
import { pathToFileURL } from 'node:url';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkSmartypants from 'remark-smartypants';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { VFile } from 'vfile';
|
||||
import { MdxOptions } from './index.js';
|
||||
|
@ -140,36 +139,22 @@ function toRemarkContentRelImageError({ srcDir }: { srcDir: URL }) {
|
|||
};
|
||||
}
|
||||
|
||||
const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkSmartypants];
|
||||
const DEFAULT_REHYPE_PLUGINS: PluggableList = [];
|
||||
|
||||
export async function getRemarkPlugins(
|
||||
mdxOptions: MdxOptions,
|
||||
config: AstroConfig
|
||||
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
|
||||
let remarkPlugins: PluggableList = [];
|
||||
switch (mdxOptions.extendPlugins) {
|
||||
case false:
|
||||
break;
|
||||
case 'astroDefaults':
|
||||
remarkPlugins = [...remarkPlugins, ...DEFAULT_REMARK_PLUGINS];
|
||||
break;
|
||||
default:
|
||||
remarkPlugins = [
|
||||
...remarkPlugins,
|
||||
...(markdownShouldExtendDefaultPlugins(config) ? DEFAULT_REMARK_PLUGINS : []),
|
||||
...ignoreStringPlugins(config.markdown.remarkPlugins ?? []),
|
||||
];
|
||||
break;
|
||||
if (mdxOptions.syntaxHighlight === 'shiki') {
|
||||
remarkPlugins.push([await remarkShiki(mdxOptions.shikiConfig)]);
|
||||
}
|
||||
if (config.markdown.syntaxHighlight === 'shiki') {
|
||||
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
|
||||
}
|
||||
if (config.markdown.syntaxHighlight === 'prism') {
|
||||
if (mdxOptions.syntaxHighlight === 'prism') {
|
||||
remarkPlugins.push(remarkPrism);
|
||||
}
|
||||
if (mdxOptions.gfm) {
|
||||
remarkPlugins.push(remarkGfm);
|
||||
}
|
||||
|
||||
remarkPlugins = [...remarkPlugins, ...(mdxOptions.remarkPlugins ?? [])];
|
||||
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
|
||||
|
||||
// Apply last in case user plugins resolve relative image paths
|
||||
if (config.experimental.contentCollections) {
|
||||
|
@ -178,34 +163,17 @@ export async function getRemarkPlugins(
|
|||
return remarkPlugins;
|
||||
}
|
||||
|
||||
export function getRehypePlugins(
|
||||
mdxOptions: MdxOptions,
|
||||
config: AstroConfig
|
||||
): MdxRollupPluginOptions['rehypePlugins'] {
|
||||
export function getRehypePlugins(mdxOptions: MdxOptions): MdxRollupPluginOptions['rehypePlugins'] {
|
||||
let rehypePlugins: PluggableList = [
|
||||
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
|
||||
rehypeMetaString,
|
||||
// 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,
|
||||
...(markdownShouldExtendDefaultPlugins(config) ? DEFAULT_REHYPE_PLUGINS : []),
|
||||
...ignoreStringPlugins(config.markdown.rehypePlugins ?? []),
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
rehypePlugins = [
|
||||
...rehypePlugins,
|
||||
...(mdxOptions.rehypePlugins ?? []),
|
||||
...ignoreStringPlugins(mdxOptions.rehypePlugins),
|
||||
// getHeadings() is guaranteed by TS, so this must be included.
|
||||
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
|
||||
rehypeHeadingIds,
|
||||
|
@ -216,13 +184,6 @@ export function getRehypePlugins(
|
|||
return rehypePlugins;
|
||||
}
|
||||
|
||||
function markdownShouldExtendDefaultPlugins(config: AstroConfig): boolean {
|
||||
return (
|
||||
config.markdown.extendDefaultPlugins ||
|
||||
(config.markdown.remarkPlugins.length === 0 && config.markdown.rehypePlugins.length === 0)
|
||||
);
|
||||
}
|
||||
|
||||
function ignoreStringPlugins(plugins: any[]) {
|
||||
let validPlugins: PluggableList = [];
|
||||
let hasInvalidPlugin = false;
|
||||
|
|
|
@ -80,91 +80,57 @@ describe('MDX plugins', () => {
|
|||
expect(selectTocLink(document)).to.be.null;
|
||||
});
|
||||
|
||||
it('respects "extendDefaultPlugins" when extending markdown', async () => {
|
||||
const fixture = await buildFixture({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
rehypePlugins: [rehypeExamplePlugin],
|
||||
extendDefaultPlugins: true,
|
||||
},
|
||||
integrations: [mdx()],
|
||||
for (const extendMarkdownConfig of [true, false]) {
|
||||
describe(`extendMarkdownConfig = ${extendMarkdownConfig}`, () => {
|
||||
let fixture;
|
||||
before(async () => {
|
||||
fixture = await buildFixture({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkToc],
|
||||
gfm: false,
|
||||
},
|
||||
integrations: [
|
||||
mdx({
|
||||
extendMarkdownConfig,
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
rehypePlugins: [rehypeExamplePlugin],
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Handles MDX plugins', async () => {
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
expect(selectRemarkExample(document, 'MDX remark plugins not applied.')).to.not.be.null;
|
||||
expect(selectRehypeExample(document, 'MDX rehype plugins not applied.')).to.not.be.null;
|
||||
});
|
||||
|
||||
it('Handles Markdown plugins', async () => {
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
expect(
|
||||
selectTocLink(
|
||||
document,
|
||||
'`remarkToc` plugin applied unexpectedly. Should override Markdown config.'
|
||||
)
|
||||
).to.be.null;
|
||||
});
|
||||
|
||||
it('Handles gfm', async () => {
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
if (extendMarkdownConfig === true) {
|
||||
expect(selectGfmLink(document), 'Does not respect `markdown.gfm` option.').to.be.null;
|
||||
} else {
|
||||
expect(selectGfmLink(document), 'Respects `markdown.gfm` unexpectedly.').to.not.be.null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
expect(selectRemarkExample(document)).to.not.be.null;
|
||||
expect(selectRehypeExample(document)).to.not.be.null;
|
||||
expect(selectGfmLink(document)).to.not.be.null;
|
||||
});
|
||||
|
||||
it('extends markdown config with extendPlugins: "markdown"', async () => {
|
||||
const fixture = await buildFixture({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
rehypePlugins: [rehypeExamplePlugin],
|
||||
},
|
||||
integrations: [
|
||||
mdx({
|
||||
extendPlugins: 'markdown',
|
||||
remarkPlugins: [remarkToc],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
expect(selectRemarkExample(document)).to.not.be.null;
|
||||
expect(selectRehypeExample(document)).to.not.be.null;
|
||||
expect(selectTocLink(document)).to.not.be.null;
|
||||
});
|
||||
|
||||
it('extends default plugins with extendPlugins: "astroDefaults"', async () => {
|
||||
const fixture = await buildFixture({
|
||||
markdown: {
|
||||
// should NOT be applied to MDX
|
||||
remarkPlugins: [remarkToc],
|
||||
},
|
||||
integrations: [
|
||||
mdx({
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
rehypePlugins: [rehypeExamplePlugin],
|
||||
extendPlugins: 'astroDefaults',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
expect(selectGfmLink(document)).to.not.be.null;
|
||||
// remark and rehype plugins still respected
|
||||
expect(selectRemarkExample(document)).to.not.be.null;
|
||||
expect(selectRehypeExample(document)).to.not.be.null;
|
||||
// Does NOT inherit TOC from markdown config
|
||||
expect(selectTocLink(document)).to.be.null;
|
||||
});
|
||||
|
||||
it('does not extend default plugins with extendPlugins: false', async () => {
|
||||
const fixture = await buildFixture({
|
||||
markdown: {
|
||||
remarkPlugins: [remarkExamplePlugin],
|
||||
},
|
||||
integrations: [
|
||||
mdx({
|
||||
remarkPlugins: [],
|
||||
extendPlugins: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const html = await fixture.readFile(FILE);
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
expect(selectGfmLink(document)).to.be.null;
|
||||
expect(selectRemarkExample(document)).to.be.null;
|
||||
});
|
||||
}
|
||||
|
||||
it('supports custom recma plugins', async () => {
|
||||
const fixture = await buildFixture({
|
||||
|
|
|
@ -67,6 +67,32 @@ describe('MDX syntax highlighting', () => {
|
|||
const prismCodeBlock = document.querySelector('pre.language-astro');
|
||||
expect(prismCodeBlock).to.not.be.null;
|
||||
});
|
||||
|
||||
for (const extendMarkdownConfig of [true, false]) {
|
||||
it(`respects syntaxHighlight when extendMarkdownConfig = ${extendMarkdownConfig}`, async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: FIXTURE_ROOT,
|
||||
markdown: {
|
||||
syntaxHighlight: 'shiki',
|
||||
},
|
||||
integrations: [
|
||||
mdx({
|
||||
extendMarkdownConfig,
|
||||
syntaxHighlight: 'prism',
|
||||
}),
|
||||
],
|
||||
});
|
||||
await fixture.build();
|
||||
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
const shikiCodeBlock = document.querySelector('pre.astro-code');
|
||||
expect(shikiCodeBlock, 'Markdown config syntaxHighlight used unexpectedly').to.be.null;
|
||||
const prismCodeBlock = document.querySelector('pre.language-astro');
|
||||
expect(prismCodeBlock).to.not.be.null;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('supports custom highlighter - shiki-twoslash', async () => {
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
"remark-gfm": "^3.0.1",
|
||||
"remark-parse": "^10.0.1",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"remark-smartypants": "^2.0.0",
|
||||
"shiki": "^0.11.1",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-map": "^3.1.1",
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import type { MarkdownRenderingOptions, MarkdownRenderingResult, MarkdownVFile } from './types';
|
||||
import type {
|
||||
AstroMarkdownOptions,
|
||||
MarkdownRenderingOptions,
|
||||
MarkdownRenderingResult,
|
||||
MarkdownVFile,
|
||||
} from './types';
|
||||
|
||||
import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
|
||||
import { loadPlugins } from './load-plugins.js';
|
||||
|
@ -20,14 +25,25 @@ import rehypeRaw from 'rehype-raw';
|
|||
import rehypeStringify from 'rehype-stringify';
|
||||
import markdown from 'remark-parse';
|
||||
import markdownToHtml from 'remark-rehype';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { unified } from 'unified';
|
||||
import { VFile } from 'vfile';
|
||||
|
||||
export { rehypeHeadingIds } from './rehype-collect-headings.js';
|
||||
export * from './types.js';
|
||||
|
||||
export const DEFAULT_REMARK_PLUGINS = ['remark-gfm', 'remark-smartypants'];
|
||||
export const DEFAULT_REHYPE_PLUGINS = [];
|
||||
export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = {
|
||||
syntaxHighlight: 'shiki',
|
||||
shikiConfig: {
|
||||
langs: [],
|
||||
theme: 'github-dark',
|
||||
wrap: false,
|
||||
},
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
remarkRehype: {},
|
||||
gfm: true,
|
||||
};
|
||||
|
||||
/** Shared utility for rendering markdown */
|
||||
export async function renderMarkdown(
|
||||
|
@ -36,12 +52,12 @@ export async function renderMarkdown(
|
|||
): Promise<MarkdownRenderingResult> {
|
||||
let {
|
||||
fileURL,
|
||||
syntaxHighlight = 'shiki',
|
||||
shikiConfig = {},
|
||||
remarkPlugins = [],
|
||||
rehypePlugins = [],
|
||||
remarkRehype = {},
|
||||
extendDefaultPlugins = false,
|
||||
syntaxHighlight = markdownConfigDefaults.syntaxHighlight,
|
||||
shikiConfig = markdownConfigDefaults.shikiConfig,
|
||||
remarkPlugins = markdownConfigDefaults.remarkPlugins,
|
||||
rehypePlugins = markdownConfigDefaults.rehypePlugins,
|
||||
remarkRehype = markdownConfigDefaults.remarkRehype,
|
||||
gfm = markdownConfigDefaults.gfm,
|
||||
isAstroFlavoredMd = false,
|
||||
isExperimentalContentCollections = false,
|
||||
contentDir,
|
||||
|
@ -55,9 +71,8 @@ export async function renderMarkdown(
|
|||
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
||||
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
||||
|
||||
if (extendDefaultPlugins || (remarkPlugins.length === 0 && rehypePlugins.length === 0)) {
|
||||
remarkPlugins = [...DEFAULT_REMARK_PLUGINS, ...remarkPlugins];
|
||||
rehypePlugins = [...DEFAULT_REHYPE_PLUGINS, ...rehypePlugins];
|
||||
if (gfm) {
|
||||
parser.use(remarkGfm);
|
||||
}
|
||||
|
||||
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
||||
|
|
|
@ -30,8 +30,9 @@ export type RehypePlugin<PluginParameters extends any[] = any[]> = unified.Plugi
|
|||
export type RehypePlugins = (string | [string, any] | RehypePlugin | [RehypePlugin, any])[];
|
||||
|
||||
export type RemarkRehype = Omit<RemarkRehypeOptions, 'handlers' | 'unknownHandler'> & {
|
||||
handlers: typeof Handlers;
|
||||
} & { handler: typeof Handler };
|
||||
handlers?: typeof Handlers;
|
||||
handler?: typeof Handler;
|
||||
};
|
||||
|
||||
export interface ShikiConfig {
|
||||
langs?: ILanguageRegistration[];
|
||||
|
@ -40,14 +41,13 @@ export interface ShikiConfig {
|
|||
}
|
||||
|
||||
export interface AstroMarkdownOptions {
|
||||
mode?: 'md' | 'mdx';
|
||||
drafts?: boolean;
|
||||
syntaxHighlight?: 'shiki' | 'prism' | false;
|
||||
shikiConfig?: ShikiConfig;
|
||||
remarkPlugins?: RemarkPlugins;
|
||||
rehypePlugins?: RehypePlugins;
|
||||
remarkRehype?: RemarkRehype;
|
||||
extendDefaultPlugins?: boolean;
|
||||
gfm?: boolean;
|
||||
}
|
||||
|
||||
export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
||||
|
|
3233
pnpm-lock.yaml
3233
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue