Skip to content

Commit ede65e8

Browse files
farnabazTahul
andauthored
feat(markdown): allow overwriting plugins (#1226)
Co-authored-by: Yaël Guilloux <[email protected]>
1 parent 46c3957 commit ede65e8

File tree

12 files changed

+608
-95
lines changed

12 files changed

+608
-95
lines changed

docs/content/4.api/3.configuration.md

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export default defineNuxtConfig({
1616
})
1717
```
1818

19-
Before diving into the individual attributes, have a [look at the default settings][default-settings] of the module.
2019

2120
## `base`
2221

@@ -93,63 +92,41 @@ export default defineNuxtConfig({
9392

9493
## `markdown`
9594

96-
This module uses [remark][remark] and [rehype][rehype] under the hood to compile markdown files into JSON AST that will be stored into the body variable.
95+
This module uses [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/remarkjs/remark-rehype) under the hood to compile markdown files into JSON AST that will be stored into the body variable.
9796

98-
> The following explanation is valid for both `remarkPlugins` and `rehypePlugins`
99-
100-
To configure how the module will parse Markdown, you can:
101-
102-
- Add a new plugin to the defaults:
97+
To configure how the module will parse Markdown, you can use `markdown.remarkPlugins` and `markdown.rehypePlugins` in your `nuxt.config.ts` file:
10398

10499
```ts [nuxt.config.ts]
105100
export default defineNuxtConfig({
106101
content: {
107102
markdown: {
108-
remarkPlugins: ['remark-emoji']
109-
}
110-
}
111-
})
112-
```
113-
114-
- Override the default plugins:
115103

116-
```ts [nuxt.config.ts]
117-
export default defineNuxtConfig({
118-
content: {
119-
markdown: {
120-
remarkPlugins: () => ['remark-emoji']
121-
}
122-
}
123-
})
124-
```
125-
126-
- Use local plugins:
104+
// Object syntax can be used to override default options
105+
remarkPlugins: {
106+
// Override remark-emoji options
107+
'remark-emoji': {
108+
emoticon: true
109+
},
110+
111+
// Disable remark-gfm
112+
'remark-gfm': false,
113+
114+
// Add remark-oembed
115+
'remark-oembed': {
116+
// Options
117+
}
118+
},
127119

128-
```ts [nuxt.config.ts]
129-
export default defineNuxtConfig({
130-
content: {
131-
markdown: {
132-
remarkPlugins: [
133-
'~/plugins/my-custom-remark-plugin.js'
120+
// Array syntax can be used to add plugins
121+
rehypePlugins: [
122+
'rehype-figure'
134123
]
135124
}
136125
}
137126
})
138127
```
139128

140-
- Provide options directly in the definition:
141-
142-
```ts [nuxt.config.ts]
143-
export default defineNuxtConfig({
144-
content: {
145-
markdown: {
146-
remarkPlugins: [
147-
['remark-emoji', { emoticon: true }]
148-
]
149-
}
150-
}
151-
})
152-
```
129+
> [Here](https://github.com/nuxt/content/tree/main/src/runtime/markdown-parser/index.ts#L23) is a list of plugins @nuxt/content is using by default.
153130
154131
> When adding a new plugin, make sure to install it in your dependencies.
155132
@@ -234,8 +211,3 @@ List of locale codes. This codes will be used to detect contents locale.
234211
- Default: `undefined`{lang=ts}
235212

236213
Default locale for top level contents. Module will use first locale code from `locales` array if this option is not defined.
237-
238-
[default-settings]: #defaults
239-
240-
[remark]: https://github.com/remarkjs/remark
241-
[rehype]: https://github.com/rehypejs/rehype

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@
9999
"jiti": "^1.13.0",
100100
"lint-staged": "^13.0.1",
101101
"nuxt": "^3.0.0-rc.3",
102+
"rehype-figure": "^1.0.1",
103+
"remark-oembed": "^1.2.2",
102104
"vitest": "^0.14.1"
103105
}
104106
}

src/module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
PROSE_TAGS,
2727
useContentMounts
2828
} from './utils'
29+
import type { MarkdownPlugin } from './runtime/types'
2930

3031
export type MountOptions = {
3132
name: string
@@ -101,14 +102,14 @@ export interface ModuleOptions {
101102
*
102103
* @default []
103104
*/
104-
remarkPlugins?: Array<string | [string, any]>
105+
remarkPlugins?: Array<string | [string, MarkdownPlugin]> | Record<string, false | MarkdownPlugin>
105106
/**
106107
* Register custom remark plugin to provide new feature into your markdown contents.
107108
* Checkout: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md
108109
*
109110
* @default []
110111
*/
111-
rehypePlugins?: Array<string | [string, any]>
112+
rehypePlugins?: Array<string | [string, MarkdownPlugin]> | Record<string, false | MarkdownPlugin>
112113
}
113114
/**
114115
* Content module uses `shiki` to highlight code blocks.

src/runtime/markdown-parser/content.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@ import type { Processor } from 'unified'
22
import { unified } from 'unified'
33
import remarkParse from 'remark-parse'
44
import remark2rehype from 'remark-rehype'
5-
import { MarkdownOptions, MarkdownRoot } from '../types'
5+
import { MarkdownOptions, MarkdownPlugin, MarkdownRoot } from '../types'
66
import remarkMDC from './remark-mdc'
77
import handlers from './handler'
88
import compiler from './compiler'
99
import { flattenNodeText } from './utils/ast'
1010
import { nodeTextContent } from './utils/node'
1111

12-
const usePlugins = (plugins: any[], stream: Processor) =>
13-
plugins.reduce((stream, plugin) => stream.use(plugin[0] || plugin, plugin[1] || undefined), stream)
12+
const usePlugins = (plugins: Record<string, false | MarkdownPlugin>, stream: Processor) => {
13+
for (const plugin of Object.values(plugins)) {
14+
if (plugin) {
15+
const { instance, ...options } = plugin
16+
stream.use(instance, options)
17+
}
18+
}
19+
}
1420

1521
/**
1622
* Generate text excerpt summary

src/runtime/markdown-parser/index.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,35 @@ export const useDefaultOptions = (): MarkdownOptions => ({
2020
searchDepth: 2
2121
},
2222
tags: {},
23-
remarkPlugins: [
24-
remarkEmoji,
25-
remarkSqueezeParagraphs,
26-
remarkGfm
27-
],
28-
rehypePlugins: [
29-
rehypeSlug,
30-
rehypeExternalLinks,
31-
rehypeSortAttributeValues,
32-
rehypeSortAttributes,
33-
[rehypeRaw, { passThrough: ['element'] }]
34-
]
23+
remarkPlugins: {
24+
'remark-emoji': {
25+
instance: remarkEmoji
26+
},
27+
'remark-squeeze-paragraphs': {
28+
instance: remarkSqueezeParagraphs
29+
},
30+
'remark-gfm': {
31+
instance: remarkGfm
32+
}
33+
},
34+
rehypePlugins: {
35+
'rehype-slug': {
36+
instance: rehypeSlug
37+
},
38+
'rehype-external-links': {
39+
instance: rehypeExternalLinks
40+
},
41+
'rehype-sort-attribute-values': {
42+
instance: rehypeSortAttributeValues
43+
},
44+
'rehype-sort-attributes': {
45+
instance: rehypeSortAttributes
46+
},
47+
'rehype-raw': {
48+
instance: rehypeRaw,
49+
passThrough: ['element']
50+
}
51+
}
3552
})
3653

3754
export async function parse (file: string, userOptions: Partial<MarkdownOptions> = {}) {
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import { parse } from '../../markdown-parser'
2-
import type { MarkdownOptions } from '../../types'
2+
import type { MarkdownOptions, MarkdownPlugin } from '../../types'
33
import { MarkdownParsedContent } from '../../types'
44
import { useRuntimeConfig } from '#imports'
55

6-
const importPlugin = async (p: [string, any]) => ([
7-
await import(p[0]).then(res => res.default || res),
8-
typeof p[1] === 'object' ? { ...p[1] } : p[1]
9-
])
10-
116
export default {
127
name: 'markdown',
138
extensions: ['.md'],
149
parse: async (_id, content) => {
1510
const config: MarkdownOptions = { ...useRuntimeConfig().content?.markdown || {} }
16-
config.rehypePlugins = await Promise.all((config.rehypePlugins || []).map(importPlugin))
17-
config.remarkPlugins = await Promise.all((config.remarkPlugins || []).map(importPlugin))
11+
config.rehypePlugins = await importPlugins(config.rehypePlugins)
12+
config.remarkPlugins = await importPlugins(config.remarkPlugins)
1813

1914
const parsed = await parse(content, config)
2015

@@ -26,3 +21,18 @@ export default {
2621
}
2722
}
2823
}
24+
25+
async function importPlugins (plugins: Record<string, false | MarkdownPlugin> = {}) {
26+
const resolvedPlugins = {}
27+
for (const [name, plugin] of Object.entries(plugins)) {
28+
if (plugin) {
29+
resolvedPlugins[name] = {
30+
instance: await import(name).then(m => m.default || m),
31+
...plugin
32+
}
33+
} else {
34+
resolvedPlugins[name] = false
35+
}
36+
}
37+
return resolvedPlugins
38+
}

src/runtime/types.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export interface MarkdownRoot {
8080
props?: Record<string, any>
8181
}
8282

83+
export interface MarkdownPlugin extends Record<string, any> {}
84+
8385
export interface MarkdownOptions {
8486
/**
8587
* Enable/Disable MDC components.
@@ -93,8 +95,8 @@ export interface MarkdownOptions {
9395
searchDepth: number
9496
}
9597
tags: Record<string, string>
96-
remarkPlugins: Array<any | [any, any]>
97-
rehypePlugins: Array<any | [any, any]>
98+
remarkPlugins: Record<string, false | (MarkdownPlugin & { instance: any })>
99+
rehypePlugins: Record<string, false | (MarkdownPlugin & { instance: any })>
98100
}
99101

100102
export interface TocLink {

src/utils.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type { Nuxt } from '@nuxt/schema'
44
import fsDriver from 'unstorage/drivers/fs'
55
import httpDriver from 'unstorage/drivers/http'
66
import { WebSocketServer } from 'ws'
7-
import { useLogger } from '@nuxt/kit'
87
import type { ModuleOptions, MountOptions } from './module'
8+
import type { MarkdownPlugin } from './runtime/types'
99

1010
/**
1111
* Internal version that represents cache format.
@@ -119,20 +119,20 @@ export function createWebSocket () {
119119
}
120120

121121
export function processMarkdownOptions (options: ModuleOptions['markdown']) {
122-
options.rehypePlugins = (options.rehypePlugins || []).map(resolveMarkdownPlugin).filter(Boolean)
123-
options.remarkPlugins = (options.remarkPlugins || []).map(resolveMarkdownPlugin).filter(Boolean)
124-
125-
return options
126-
127-
function resolveMarkdownPlugin (plugin: string | [string, any]): [string, any] {
128-
if (typeof plugin === 'string') { plugin = [plugin, {}] }
129-
130-
if (!Array.isArray(plugin)) {
131-
useLogger('@nuxt/content').warn('Plugin silently ignored:', (plugin as any).name || plugin)
132-
return
133-
}
122+
return {
123+
...options,
124+
remarkPlugins: resolveMarkdownPlugins(options.remarkPlugins),
125+
rehypePlugins: resolveMarkdownPlugins(options.rehypePlugins)
126+
}
127+
}
134128

135-
// TODO: Add support for local custom plugins
136-
return plugin
129+
function resolveMarkdownPlugins (plugins): Record<string, false | MarkdownPlugin> {
130+
if (Array.isArray(plugins)) {
131+
return Object.values(plugins).reduce((plugins, plugin) => {
132+
const [name, pluginOptions] = Array.isArray(plugin) ? plugin : [plugin, {}]
133+
plugins[name] = pluginOptions
134+
return plugins
135+
}, {})
137136
}
137+
return plugins || {}
138138
}

test/basic.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { testCSVParser } from './features/parser-csv'
1212
import { testRegex } from './features/regex'
1313
import { testMarkdownParserExcerpt } from './features/parser-markdown-excerpt'
1414
import { testParserHooks } from './features/parser-hooks'
15+
import { testModuleOption } from './features/module-options'
1516
import { testContentQuery } from './features/content-query'
1617

1718
describe('fixtures:basic', async () => {
@@ -126,4 +127,6 @@ describe('fixtures:basic', async () => {
126127
testRegex()
127128

128129
testParserHooks()
130+
131+
testModuleOption()
129132
})

test/features/module-options.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, test, expect } from 'vitest'
2+
import { $fetch } from '@nuxt/test-utils'
3+
4+
export const testModuleOption = () => {
5+
describe('module options', () => {
6+
test('overwrite `remark-emoji` options: enable emoticon', async () => {
7+
const parsed = await $fetch('/api/parse', {
8+
method: 'POST',
9+
body: {
10+
id: 'content:index.md',
11+
content: [
12+
'# Hello :-)'
13+
].join('\n')
14+
}
15+
})
16+
expect(parsed.body.children[0].children[0].value).toContain('😃')
17+
})
18+
19+
test('disable `remark-gfm`', async () => {
20+
const parsed = await $fetch('/api/parse', {
21+
method: 'POST',
22+
body: {
23+
id: 'content:index.md',
24+
content: [
25+
'~one~'
26+
].join('\n')
27+
}
28+
})
29+
expect(parsed.body.children[0].children[0].value).toBe('~one~')
30+
})
31+
32+
test('add `remark-oembed`', async () => {
33+
const parsed = await $fetch('/api/parse', {
34+
method: 'POST',
35+
body: {
36+
id: 'content:index.md',
37+
content: [
38+
'https://www.youtube.com/watch?v=aoLhACqJCUg'
39+
].join('\n')
40+
}
41+
})
42+
expect(parsed.body.children[0].props.className).toContain('remark-oembed-you-tube')
43+
})
44+
45+
test('add `rehype-figure`', async () => {
46+
const parsed = await $fetch('/api/parse', {
47+
method: 'POST',
48+
body: {
49+
id: 'content:index.md',
50+
content: [
51+
'![Alt](https://nuxtjs.org/design-kit/colored-logo.svg)'
52+
].join('\n')
53+
}
54+
})
55+
expect(parsed.body.children[0].props.className).toContain('rehype-figure')
56+
expect(parsed.body.children[0].tag).toContain('figure')
57+
})
58+
})
59+
}

0 commit comments

Comments
 (0)