扩展 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的功能:renderer
、tokenizer
、hooks
、walkTokens
和extensions
。
-
renderer
、tokenizer
和hooks
选项是包含函数的对象,这些函数将被合并到内置的renderer
和tokenizer
中。 -
walkTokens
选项是一个函数,它将在渲染之前遍历每个token并进行任何最终的调整。 -
extensions
选项是一个对象数组,可以包含在默认解析逻辑之前执行的附加自定义renderer
和tokenizer
步骤。
Marked Pipeline
在构建自定义扩展之前,了解Marked将Markdown转换为HTML所使用的组件是非常重要的:
- 用户为Marked提供要翻译的输入字符串。
- 分词器将输入文本字符串的片段输入到每个分词器中,并从输出中生成一系列token的嵌套树结构。
- 每个分词器接收一段Markdown文本,如果它匹配特定模式,就生成一个包含任何相关信息的token对象。
walkTokens
函数将遍历树中的每个token,并对token内容进行任何最终的调整。- 解析器遍历token树,将每个token输入到适当的
renderer
中,并将它们的输出连接成最终的HTML结果。 - 每个
renderer
接收一个token,并操纵其内容以生成一段HTML。
Marked提供方法直接覆盖任何现有token类型的renderer
和tokenizer
,以及插入附加的自定义renderer
和tokenizer
函数以处理完全自定义的语法。例如,使用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>