跳到主要内容

扩展 Marked

为了支持单一职责和开闭原则,我们尽量使扩展Marked变得相对容易。如果您想要添加自定义功能,这里就是开始的地方。

使用marked.use()

使用marked.use(extension)是扩展Marked的推荐方式。扩展对象可以包含Marked中可用的任何选项:

import { marked } from 'marked';

marked.use({
pedantic: false,
gfm: true,
breaks: false
});

您也可以一次性提供多个扩展对象。

marked.use(myExtension, extension2, extension3);

// 等同于:

marked.use(myExtension);
marked.use(extension2);
marked.use(extension3);

所有选项将覆盖之前设置的选项,但以下选项将合并到现有框架中,并且可以用来更改或扩展Marked的功能:renderertokenizerhookswalkTokensextensions

  • renderertokenizerhooks选项是包含函数的对象,这些函数将被合并到内置的renderertokenizer中。

  • walkTokens选项是一个函数,它将在渲染之前遍历每个token并进行任何最终的调整。

  • extensions选项是一个对象数组,可以包含在默认解析逻辑之前执行的附加自定义renderertokenizer步骤。

Marked Pipeline

在构建自定义扩展之前,了解Marked将Markdown转换为HTML所使用的组件是非常重要的:

  • 用户为Marked提供要翻译的输入字符串。
  • 分词器将输入文本字符串的片段输入到每个分词器中,并从输出中生成一系列token的嵌套树结构。
  • 每个分词器接收一段Markdown文本,如果它匹配特定模式,就生成一个包含任何相关信息的token对象。
  • walkTokens函数将遍历树中的每个token,并对token内容进行任何最终的调整。
  • 解析器遍历token树,将每个token输入到适当的renderer中,并将它们的输出连接成最终的HTML结果。
  • 每个renderer接收一个token,并操纵其内容以生成一段HTML。

Marked提供方法直接覆盖任何现有token类型的renderertokenizer,以及插入附加的自定义renderertokenizer函数以处理完全自定义的语法。例如,使用marked.use({renderer})将修改renderer,而使用 marked.use({extensions: [{renderer}]}) 将添加一个新的renderer。请参阅自定义扩展示例以了解如何执行此操作。

渲染器: renderer

🚨 Marked v13已将renderer更改为接受tokens。要启用这些新renderer函数,请将useNewRenderer: true添加到扩展中。请参阅v13发行说明中的示例 🚨

renderer定义了给定token的HTML输出。如果您在传递给marked.use()的选项对象中提供renderer,则该对象中的任何函数都将覆盖默认处理该token类型的方式。

调用marked.use()多次以覆盖相同函数将给最后一个分配的版本提供优先权。可以返回false以回退到序列中的上一个覆盖,或者如果所有覆盖都返回false,则恢复默认行为。返回任何其他值(包括空值)将防止回退行为。

示例:通过添加类似于GitHub上的嵌入锚点标签来覆盖默认标题token的输出。

// 创建引用实例
import { marked } from 'marked';

// 覆盖函数
const renderer = {
heading(text, depth) {
const escapedText = text.toLowerCase().replace(/[^w]+/g, '-');

return `
<h${depth}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${depth}>`;
}
};

marked.use({ renderer });

// 运行marked
console.log(marked.parse('# heading+'));

输出

<h1>
<a name="heading-" class="anchor" href="#heading-">
<span class="header-link"></span>
</a>
heading+
</h1>

注意:以下方式调用marked.use()将避免覆盖标题token的输出,但在过程中创建一个新的标题renderer。

marked.use({
extensions: [{
name: 'heading',
renderer(token) {
return /* ... */;
}
}]
});

块 renderer 方法

space(token: Tokens.Space): string
code(token: Tokens.Code): string
blockquote(token: Tokens.Blockquote): string
html(token: Tokens.HTML | Tokens.Tag): string
heading(token: Tokens.Heading): string
hr(token: Tokens.Hr): string
list(token: Tokens.List): string
listitem(token: Tokens.ListItem): string
checkbox(token: Tokens.Checkbox): string
paragraph(token: Tokens.Paragraph): string
table(token: Tokens.Table): string
tablerow(token: Tokens.TableRow): string
tablecell(token: Tokens.TableCell): string

内联 renderer 方法

strong(token: Tokens.Strong): string
em(token: Tokens.Em): string
codespan(token: Tokens.Codespan): string
br(token: Tokens.Br): string
del(token: Tokens.Del): string
link(token: Tokens.Link): string
image(token: Tokens.Image): string
text(token: Tokens.Text | Tokens.Escape | Tokens.Tag): string

The Tokens.* properties can be found here.

分词器: tokenizer

分词器定义了如何将Markdown文本转换为tokens。如果您向Marked选项中提供分词器对象,它将合并到内置的分词器中,并且其中的任何函数都将覆盖对默认token类型的处理。

调用marked.use()多次以覆盖相同函数将给最后一个分配的版本提供优先权。可以返回false以回退到序列中的上一个覆盖,或者如果所有覆盖都返回false,则恢复默认行为。返回任何其他值(包括空值)将防止回退行为。

示例:覆盖默认codespan分词器以包括LaTeX。

// 创建引用实例
import { marked } from 'marked';

// 覆盖函数
const tokenizer = {
codespan(src) {
const match = src.match(/^$+([^$n]+?)$+/);
if (match) {
return {
type: 'codespan',
raw: match[0],
text: match[1].trim()
};
}

// return false to use original codespan tokenizer
return false;
}
};

marked.use({ tokenizer });

// 运行marked
console.log(marked.parse('$ latex code $nn` other code `'));

输出

<p><code>latex code</code></p>
<p><code>other code</code></p>

注意:这并不完全支持LaTeX,请参阅问题#1948。

块 tokenizer 方法

space(src: string): Tokens.Space
code(src: string): Tokens.Code
fences(src: string): Tokens.Code
heading(src: string): Tokens.Heading
hr(src: string): Tokens.Hr
blockquote(src: string): Tokens.Blockquote
list(src: string): Tokens.List
html(src: string): Tokens.HTML
def(src: string): Tokens.Def
table(src: string): Tokens.Table
lheading(src: string): Tokens.Heading
paragraph(src: string): Tokens.Paragraph
text(src: string): Tokens.Text

内联 tokenizer 方法

escape(src: string): Tokens.Escape
tag(src: string): Tokens.Tag
link(src: string): Tokens.Link | Tokens.Image
reflink(src: string, links: object): Tokens.Link | Tokens.Image | Tokens.Text
emStrong(src: string, maskedSrc: string, prevChar: string): Tokens.Em | Tokens.Strong
codespan(src: string): Tokens.Codespan
br(src: string): Tokens.Br
del(src: string): Tokens.Del
autolink(src: string): Tokens.Link
url(src: string): Tokens.Link
inlineText(src: string): Tokens.Text

The Tokens.* properties can be found here.

Walk Tokens: walkTokens

walkTokens函数会对每个token进行调用。在移动到兄弟token之前会先调用子token。每个token都是通过引用传递的,因此当传递给parser时更新将被保留。当启用异步模式时,返回值将被等待。否则,返回值将被忽略。

marked.use()可以多次以不同的walkTokens函数调用。每个函数将以最后分配的函数开始的顺序被调用。

示例:将标题token覆盖为从h2开始。

import { marked } from 'marked';

// 覆盖函数
const walkTokens = (token) => {
if (token.type === 'heading') {
token.depth += 1;
}
};

marked.use({ walkTokens });

// 运行marked
console.log(marked.parse('# heading 2nn## heading 3'));

输出

<h2 id="heading-2">heading 2</h2>
<h3 id="heading-3">heading 3</h3>

钩子: hooks

Hooks是一些钩子方法,它们将集成到marked的某些部分。以下可用的hooks如下:

签名描述
preprocess(markdown: string): string在将markdown发送到marked之前处理markdown。
postprocess(html: string): string在marked完成解析后处理html。
processAllTokens(tokens: Token[]): Token[]在walkTokens之前处理所有token。

marked.use()可以多次以不同的hooks函数调用。每个函数将以最后分配的函数开始的顺序被调用。

示例:根据front-matter设置选项

import { marked } from 'marked';
import fm from 'front-matter';

// 覆盖函数
function preprocess(markdown) {
const { attributes, body } = fm(markdown);
for (const prop in attributes) {
if (prop in this.options) {
this.options[prop] = attributes[prop];
}
}
return body;
}

marked.use({ hooks: { preprocess } });

// 运行marked
console.log(marked.parse(`
---
breaks: true
---

line1
line2
`.trim()));

输出

<p>line1<br>line2</p>

示例:使用isomorphic-dompurify清理HTML

import { marked } from 'marked';
import DOMPurify from 'isomorphic-dompurify';

// 覆盖函数
function postprocess(html) {
return DOMPurify.sanitize(html);
}

marked.use({ hooks: { postprocess } });

// 运行marked
console.log(marked.parse(`
<img src=x onerror=alert(1)//>
`));

输出

<img src="x">

自定义扩展: extensions

您可以向选项对象提供扩展数组。此数组可以包含任何数量的扩展对象,使用以下属性:

名称描述
name用于标识将由此扩展处理的token的字符串。
level用于确定何时运行扩展分词器的字符串。必须等于'block'或'inline'。
start(string src)返回下一个潜在自定义token的开始索引的函数。
tokenizer(string src, array tokens)从Markdown文本字符串中读取,并返回生成的token的函数。
renderer(object token)读取token并返回生成的HTML输出字符串的函数。
childTokens [optional]匹配任何应由walkTokens函数遍历的token参数名称的字符串数组。

示例:添加自定义语法来生成描述列表。

const descriptionList = {
name: 'descriptionList',
level: 'block', // 是否为块级或内联级分词器?
start(src) { return src.match(/:[^:n]/)?.index; }, // 提示Marked.js停止并检查匹配项
tokenizer(src, tokens) {
const rule = /^(?::[^:n]+:[^:n]*(?:n|$))+/; // 用于完整token的正则表达式,锚定到字符串开始
const match = rule.exec(src);
if (match) {
const token = { // 要生成的token
type: 'descriptionList', // 应该与上面的"name"匹配
raw: match[0], // 从源中消耗的所有文本
text: match[0].trim(), // 其他自定义属性
tokens: [] // 存储子内联token的数组
};
this.lexer.inline(token.text, token.tokens); // 将这些数据排队以进行内联token处理
return token;
}
},
renderer(token) {
return `<dl>${this.parser.parseInline(token.tokens)}n</dl>`; // parseInline将子token转换为HTML
}
};

const description = {
name: 'description',
level: 'inline', // 是否为块级或内联级分词器?
start(src) { return src.match(/:/)?.index; }, // 提示Marked.js停止并检查匹配项
tokenizer(src, tokens) {
const rule = /^:([^:n]+):([^:n]*)(?:n|$)/; // 用于完整token的正则表达式,锚定到字符串开始
const match = rule.exec(src);
if (match) {
return { // 要生成的token
type: 'description', // 应该与上面的"name"匹配
raw: match[0], // 从源中消耗的所有文本
dt: this.lexer.inlineTokens(match[1].trim()), // 其他自定义属性,包括
dd: this.lexer.inlineTokens(match[2].trim()) // 任何嵌套的内联token
};
}
},
renderer(token) {
return `n<dt>${this.parser.parseInline(token.dt)}</dt><dd>${this.parser.parseInline(token.dd)}</dd>`;
},
childTokens: ['dt', 'dd'], // 要由walkTokens遍历的任何子token
};

function walkTokens(token) { // 完成token树的后期处理
if (token.type === 'strong') {
token.text += ' walked';
token.tokens = this.Lexer.lexInline(token.text)
}
}
marked.use({ extensions: [descriptionList, description], walkTokens });

// 等同于:

marked.use({ extensions: [descriptionList] });
marked.use({ extensions: [description] });
marked.use({ walkTokens })

console.log(marked.parse('A Description List:n'

  • ': Topic 1 : Description 1n'
  • ': Topic 2 : Description 2'));

**输出**:

```html
<p>A Description List:</p>
<dl>
<dt>Topic 1</dt><dd>Description 1</dd>
<dt><strong>Topic 2 walked</strong></dt><dd><em>Description 2</em></dd>
</dl>

异步 Marked: async

如果async选项为true,Marked将返回一个promise。async选项将告诉marked在解析token并返回HTML字符串之前等待任何walkTokens函数。

简单示例:

const walkTokens = async (token) => {
if (token.type === 'link') {
try {
await fetch(token.href);
} catch (ex) {
token.title = 'invalid';
}
}
};

marked.use({ walkTokens, async: true });

const markdown = `
[valid link](https://example.com)

[invalid link](https://invalidurl.com)
`;

const html = await marked.parse(markdown);

自定义扩展示例:

const importUrl = {
extensions: [{
name: 'importUrl',
level: 'block',
start(src) { return src.indexOf('n:'); },
tokenizer(src) {
const rule = /^:(https?://.+?):/;
const match = rule.exec(src);
if (match) {
return {
type: 'importUrl',
raw: match[0],
url: match[1],
html: '' // 将在walkTokens中替换
};
}
},
renderer(token) {
return token.html;
}
}],
async: true, // needed to tell marked to return a promise
async walkTokens(token) {
if (token.type === 'importUrl') {
const res = await fetch(token.url);
token.html = await res.text();
}
}
};

marked.use(importUrl);

const markdown = `
# example.com

:https://example.com:
`;

const html = await marked.parse(markdown);

Lexer

分词器接受Markdown字符串并调用分词器函数。

Parser

解析器接收tokens作为输入并调用renderer函数。

访问Lexer和Parser

如果您想要的话,您还可以直接访问lexer和parser。lexer和parser选项与传递给marked.setOptions()的选项相同,但它们必须是完整的选项对象,它们不会与当前或默认选项合并。

const tokens = marked.lexer(markdown, options);
console.log(marked.parser(tokens, options));
const lexer = new marked.Lexer(options);
const tokens = lexer.lex(markdown);
console.log(tokens);
console.log(lexer.tokenizer.rules.block); // block level rules used
console.log(lexer.tokenizer.rules.inline); // inline level rules used
console.log(marked.Lexer.rules.block); // all block level rules
console.log(marked.Lexer.rules.inline); // all inline level rules

注意:lexer可以用两种不同的方式使用:

marked.lexer(): 此方法将字符串标记化并返回其tokens。后续调用lexer()将忽略任何之前的调用。
new marked.Lexer().lex(): 此实例将字符串标记化并返回其tokens以及任何之前的tokens。后续调用lex()将累积tokens。

Lexer构建一个token数组,这些token将被传递给Parser。

Parser处理token数组中的每个token:

import { marked } from 'marked';

const md = `
# heading

[link][1]

[1]: #heading "heading"
`;

const tokens = marked.lexer(md);
console.log(tokens);

const html = marked.parser(tokens);
console.log(html);

输出:

[
{
type: "heading",
raw: " # headingnn",
depth: 1,
text: "heading",
tokens: [
{
type: "text",
raw: "heading",
text: "heading"
}
]
},
{
type: "paragraph",
raw: " [link][1]",
text: " [link][1]",
tokens: [
{
type: "text",
raw: " ",
text: " "
},
{
type: "link",
raw: "[link][1]",
text: "link",
href: "#heading",
title: "heading",
tokens: [
{
type: "text",
raw: "link",
text: "link"
}
]
}
]
},
{
type: "space",
raw: "nn"
},
links: {
"1": {
href: "#heading",
title: "heading"
}
}
]
<h1 id="heading">heading</h1>
<p> <a href="#heading" title="heading">link</a></p>