Skip to content

Commit

Permalink
feat(markdown): allow overwriting plugins (#1226)
Browse files Browse the repository at this point in the history
Co-authored-by: Yaël Guilloux <[email protected]>
  • Loading branch information
farnabaz and Tahul committed Sep 7, 2022
1 parent ae907a8 commit 35df339
Show file tree
Hide file tree
Showing 12 changed files with 608 additions and 95 deletions.
70 changes: 21 additions & 49 deletions docs/content/4.api/3.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default defineNuxtConfig({
})
```

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

## `base`

Expand Down Expand Up @@ -93,63 +92,41 @@ export default defineNuxtConfig({

## `markdown`

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

> The following explanation is valid for both `remarkPlugins` and `rehypePlugins`
To configure how the module will parse Markdown, you can:

- Add a new plugin to the defaults:
To configure how the module will parse Markdown, you can use `markdown.remarkPlugins` and `markdown.rehypePlugins` in your `nuxt.config.ts` file:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
content: {
markdown: {
remarkPlugins: ['remark-emoji']
}
}
})
```

- Override the default plugins:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
content: {
markdown: {
remarkPlugins: () => ['remark-emoji']
}
}
})
```

- Use local plugins:
// Object syntax can be used to override default options
remarkPlugins: {
// Override remark-emoji options
'remark-emoji': {
emoticon: true
},

// Disable remark-gfm
'remark-gfm': false,

// Add remark-oembed
'remark-oembed': {
// Options
}
},

```ts [nuxt.config.ts]
export default defineNuxtConfig({
content: {
markdown: {
remarkPlugins: [
'~/plugins/my-custom-remark-plugin.js'
// Array syntax can be used to add plugins
rehypePlugins: [
'rehype-figure'
]
}
}
})
```

- Provide options directly in the definition:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
content: {
markdown: {
remarkPlugins: [
['remark-emoji', { emoticon: true }]
]
}
}
})
```
> [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.
> When adding a new plugin, make sure to install it in your dependencies.
Expand Down Expand Up @@ -234,8 +211,3 @@ List of locale codes. This codes will be used to detect contents locale.
- Default: `undefined`{lang=ts}

Default locale for top level contents. Module will use first locale code from `locales` array if this option is not defined.

[default-settings]: #defaults

[remark]: https://github.com/remarkjs/remark
[rehype]: https://github.com/rehypejs/rehype
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
"jiti": "^1.13.0",
"lint-staged": "^13.0.1",
"nuxt": "^3.0.0-rc.3",
"rehype-figure": "^1.0.1",
"remark-oembed": "^1.2.2",
"vitest": "^0.14.1"
}
}
5 changes: 3 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
PROSE_TAGS,
useContentMounts
} from './utils'
import type { MarkdownPlugin } from './runtime/types'

export type MountOptions = {
name: string
Expand Down Expand Up @@ -101,14 +102,14 @@ export interface ModuleOptions {
*
* @default []
*/
remarkPlugins?: Array<string | [string, any]>
remarkPlugins?: Array<string | [string, MarkdownPlugin]> | Record<string, false | MarkdownPlugin>
/**
* Register custom remark plugin to provide new feature into your markdown contents.
* Checkout: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md
*
* @default []
*/
rehypePlugins?: Array<string | [string, any]>
rehypePlugins?: Array<string | [string, MarkdownPlugin]> | Record<string, false | MarkdownPlugin>
}
/**
* Content module uses `shiki` to highlight code blocks.
Expand Down
12 changes: 9 additions & 3 deletions src/runtime/markdown-parser/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import type { Processor } from 'unified'
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remark2rehype from 'remark-rehype'
import { MarkdownOptions, MarkdownRoot } from '../types'
import { MarkdownOptions, MarkdownPlugin, MarkdownRoot } from '../types'
import remarkMDC from './remark-mdc'
import handlers from './handler'
import compiler from './compiler'
import { flattenNodeText } from './utils/ast'
import { nodeTextContent } from './utils/node'

const usePlugins = (plugins: any[], stream: Processor) =>
plugins.reduce((stream, plugin) => stream.use(plugin[0] || plugin, plugin[1] || undefined), stream)
const usePlugins = (plugins: Record<string, false | MarkdownPlugin>, stream: Processor) => {
for (const plugin of Object.values(plugins)) {
if (plugin) {
const { instance, ...options } = plugin
stream.use(instance, options)
}
}
}

/**
* Generate text excerpt summary
Expand Down
41 changes: 29 additions & 12 deletions src/runtime/markdown-parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,35 @@ export const useDefaultOptions = (): MarkdownOptions => ({
searchDepth: 2
},
tags: {},
remarkPlugins: [
remarkEmoji,
remarkSqueezeParagraphs,
remarkGfm
],
rehypePlugins: [
rehypeSlug,
rehypeExternalLinks,
rehypeSortAttributeValues,
rehypeSortAttributes,
[rehypeRaw, { passThrough: ['element'] }]
]
remarkPlugins: {
'remark-emoji': {
instance: remarkEmoji
},
'remark-squeeze-paragraphs': {
instance: remarkSqueezeParagraphs
},
'remark-gfm': {
instance: remarkGfm
}
},
rehypePlugins: {
'rehype-slug': {
instance: rehypeSlug
},
'rehype-external-links': {
instance: rehypeExternalLinks
},
'rehype-sort-attribute-values': {
instance: rehypeSortAttributeValues
},
'rehype-sort-attributes': {
instance: rehypeSortAttributes
},
'rehype-raw': {
instance: rehypeRaw,
passThrough: ['element']
}
}
})

export async function parse (file: string, userOptions: Partial<MarkdownOptions> = {}) {
Expand Down
26 changes: 18 additions & 8 deletions src/runtime/server/transformers/markdown.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { parse } from '../../markdown-parser'
import type { MarkdownOptions } from '../../types'
import type { MarkdownOptions, MarkdownPlugin } from '../../types'
import { MarkdownParsedContent } from '../../types'
import { useRuntimeConfig } from '#imports'

const importPlugin = async (p: [string, any]) => ([
await import(p[0]).then(res => res.default || res),
typeof p[1] === 'object' ? { ...p[1] } : p[1]
])

export default {
name: 'markdown',
extensions: ['.md'],
parse: async (_id, content) => {
const config: MarkdownOptions = { ...useRuntimeConfig().content?.markdown || {} }
config.rehypePlugins = await Promise.all((config.rehypePlugins || []).map(importPlugin))
config.remarkPlugins = await Promise.all((config.remarkPlugins || []).map(importPlugin))
config.rehypePlugins = await importPlugins(config.rehypePlugins)
config.remarkPlugins = await importPlugins(config.remarkPlugins)

const parsed = await parse(content, config)

Expand All @@ -26,3 +21,18 @@ export default {
}
}
}

async function importPlugins (plugins: Record<string, false | MarkdownPlugin> = {}) {
const resolvedPlugins = {}
for (const [name, plugin] of Object.entries(plugins)) {
if (plugin) {
resolvedPlugins[name] = {
instance: await import(name).then(m => m.default || m),
...plugin
}
} else {
resolvedPlugins[name] = false
}
}
return resolvedPlugins
}
6 changes: 4 additions & 2 deletions src/runtime/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export interface MarkdownRoot {
props?: Record<string, any>
}

export interface MarkdownPlugin extends Record<string, any> {}

export interface MarkdownOptions {
/**
* Enable/Disable MDC components.
Expand All @@ -93,8 +95,8 @@ export interface MarkdownOptions {
searchDepth: number
}
tags: Record<string, string>
remarkPlugins: Array<any | [any, any]>
rehypePlugins: Array<any | [any, any]>
remarkPlugins: Record<string, false | (MarkdownPlugin & { instance: any })>
rehypePlugins: Record<string, false | (MarkdownPlugin & { instance: any })>
}

export interface TocLink {
Expand Down
30 changes: 15 additions & 15 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { Nuxt } from '@nuxt/schema'
import fsDriver from 'unstorage/drivers/fs'
import httpDriver from 'unstorage/drivers/http'
import { WebSocketServer } from 'ws'
import { useLogger } from '@nuxt/kit'
import type { ModuleOptions, MountOptions } from './module'
import type { MarkdownPlugin } from './runtime/types'

/**
* Internal version that represents cache format.
Expand Down Expand Up @@ -119,20 +119,20 @@ export function createWebSocket () {
}

export function processMarkdownOptions (options: ModuleOptions['markdown']) {
options.rehypePlugins = (options.rehypePlugins || []).map(resolveMarkdownPlugin).filter(Boolean)
options.remarkPlugins = (options.remarkPlugins || []).map(resolveMarkdownPlugin).filter(Boolean)

return options

function resolveMarkdownPlugin (plugin: string | [string, any]): [string, any] {
if (typeof plugin === 'string') { plugin = [plugin, {}] }

if (!Array.isArray(plugin)) {
useLogger('@nuxt/content').warn('Plugin silently ignored:', (plugin as any).name || plugin)
return
}
return {
...options,
remarkPlugins: resolveMarkdownPlugins(options.remarkPlugins),
rehypePlugins: resolveMarkdownPlugins(options.rehypePlugins)
}
}

// TODO: Add support for local custom plugins
return plugin
function resolveMarkdownPlugins (plugins): Record<string, false | MarkdownPlugin> {
if (Array.isArray(plugins)) {
return Object.values(plugins).reduce((plugins, plugin) => {
const [name, pluginOptions] = Array.isArray(plugin) ? plugin : [plugin, {}]
plugins[name] = pluginOptions
return plugins
}, {})
}
return plugins || {}
}
3 changes: 3 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { testCSVParser } from './features/parser-csv'
import { testRegex } from './features/regex'
import { testMarkdownParserExcerpt } from './features/parser-markdown-excerpt'
import { testParserHooks } from './features/parser-hooks'
import { testModuleOption } from './features/module-options'
import { testContentQuery } from './features/content-query'

describe('fixtures:basic', async () => {
Expand Down Expand Up @@ -126,4 +127,6 @@ describe('fixtures:basic', async () => {
testRegex()

testParserHooks()

testModuleOption()
})
59 changes: 59 additions & 0 deletions test/features/module-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, test, expect } from 'vitest'
import { $fetch } from '@nuxt/test-utils'

export const testModuleOption = () => {
describe('module options', () => {
test('overwrite `remark-emoji` options: enable emoticon', async () => {
const parsed = await $fetch('/api/parse', {
method: 'POST',
body: {
id: 'content:index.md',
content: [
'# Hello :-)'
].join('\n')
}
})
expect(parsed.body.children[0].children[0].value).toContain('😃')
})

test('disable `remark-gfm`', async () => {
const parsed = await $fetch('/api/parse', {
method: 'POST',
body: {
id: 'content:index.md',
content: [
'~one~'
].join('\n')
}
})
expect(parsed.body.children[0].children[0].value).toBe('~one~')
})

test('add `remark-oembed`', async () => {
const parsed = await $fetch('/api/parse', {
method: 'POST',
body: {
id: 'content:index.md',
content: [
'https://www.youtube.com/watch?v=aoLhACqJCUg'
].join('\n')
}
})
expect(parsed.body.children[0].props.className).toContain('remark-oembed-you-tube')
})

test('add `rehype-figure`', async () => {
const parsed = await $fetch('/api/parse', {
method: 'POST',
body: {
id: 'content:index.md',
content: [
'![Alt](https://nuxtjs.org/design-kit/colored-logo.svg)'
].join('\n')
}
})
expect(parsed.body.children[0].props.className).toContain('rehype-figure')
expect(parsed.body.children[0].tag).toContain('figure')
})
})
}
Loading

0 comments on commit 35df339

Please sign in to comment.