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:
Ben Holmes 2023-01-03 17:12:47 -05:00 committed by GitHub
parent 163a9a9d0e
commit a9c2920264
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 2196 additions and 1673 deletions

View 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.

View file

@ -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

View file

@ -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

View file

@ -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);
});

View file

@ -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 projects 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`

View file

@ -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"

View file

@ -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 projects markdown plugin config ([see Markdown docs](https://docs.astro.build/en/guides/markdown-content/#configuring-markdown))
* - "astroDefaults" - inherit Astros 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

View file

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

View file

@ -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({

View file

@ -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 () => {

View file

@ -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",

View file

@ -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));

View file

@ -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 {

File diff suppressed because it is too large Load diff