扩展
本文翻译自 https://marked.js.org/using-pro,详细讲解了如何扩展 Marked.js 的功能,包含代码注释和实际案例。
扩展原则
为遵循单一职责原则和开闭原则,我们设计了灵活的扩展机制,让用户能够在不修改核心代码的情况下添加自定义功能。
marked.use()
推荐使用 marked.use(extension)
扩展 Marked。参数 extension
可包含任何 Marked 支持的可选配置项:
import { marked } from 'marked';
// 覆盖默认配置
marked.use({
pedantic: false, // 关闭严格模式
gfm: true, // 启用 GitHub Flavored Markdown
breaks: false // 禁用自动换行转换
});
同时支持传入多个配置对象:
marked.use(extension1, extension2, extension3);
合并规则
以下属性会被合并而非覆盖现有配置:
属性 | 描述 |
---|---|
renderer | 渲染器函数 |
tokenizer | 分词器函数 |
hooks | 生命周期钩子 |
walkTokens | 令牌后处理函数 |
extensions | 自定义扩展集合 |
重要提示:扩展应当仅在全局作用域添加一次。在重复调用的函数或 Svelte 等框架组件中添加扩展会导致递归错误。
Marked 处理流程
Markdown → HTML 的转换流程:
输入 → 分词器 → 令牌树 → 后处理 → 解析器 → HTML
- 分词器将 Markdown 分割为令牌对象
- walkTokens 遍历令牌树执行后处理
- 解析器将令牌转换为 HTML
渲染器扩展 (Renderer)
通过提供 renderer
配置覆盖默认渲染逻辑:
// 为标题添加锚点链接(类似 GitHub 风格)
const renderer = {
heading({ depth, text }) {
// 生成 URL 安全的锚点 ID
const anchorId = text.toLowerCase().replace(/[^\w]+/g, '-');
return `
<h${depth}>
<a name="${anchorId}" class="anchor" href="#${anchorId}">
<span class="header-link"></span>
</a>
${text}
</h${depth}>`;
}
};
marked.use({ 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
行内渲染方法
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
令牌类型定义详见 https://marked.js.org/using-pro#tokens
分词器扩展 (Tokenizer)
通过 tokenizer
配置自定义分词逻辑:
// 在行内代码中支持 LaTeX 语法
const tokenizer = {
codespan(src) {
const match = src.match(/^\$+([^\$\n]+?)\$+/);
if (match) {
return {
type: 'codespan',
raw: match[0], // 原始匹配文本
text: match[1].trim() // 提取的 LaTeX 内容
};
}
return false; // 回退默认处理
}
};
marked.use({ 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
行内分词方法
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
WalkTokens 后处理
遍历令牌树并修改令牌内容:
// 所有标题级别+1 (h1 → h2)
const walkTokens = (token) => {
if (token.type === 'heading') {
token.depth += 1;
}
};
marked.use({ walkTokens });
钩子函数 (Hooks)
生命周期钩子允许接入处理流程关键点:
钩子 | 描述 |
---|---|
preprocess(markdown) | Markdown 预处理 |
postprocess(html) | HTML 后处理 |
processAllTokens(tokens) | 令牌全局处理 |
provideLexer() | 提供自定义分词器 |
provideParser() | 提供自定义解析器 |
示例:使用 Front Matter 设置选项
import fm from 'front-matter';
marked.use({
hooks: {
preprocess(markdown) {
// 解析 YAML front matter
const { attributes } = fm(markdown);
// 将 front matter 属性合并到配置
Object.assign(this.options, attributes);
return markdown;
}
}
});
示例:HTML 消毒处理
import DOMPurify from 'isomorphic-dompurify';
marked.use({
hooks: {
postprocess(html) {
return DOMPurify.sanitize(html); // 过滤危险 HTML
}
}
});
自定义扩展 (Extensions)
extensions
数组支持添加完整自定义语法:
const descriptionList = {
name: 'descriptionList',
level: 'block', // 块级扩展
start(src) { return src.match(/:[^:\n]/)?.index },
tokenizer(src) {
// 匹配 :: 分隔的描述列表
const rule = /^(:[^:\n]+:[^:\n]*(?:\n|$))+/;
const match = rule.exec(src);
if (match) {
return {
type: 'descriptionList',
raw: match[0],
tokens: [] // 存放子令牌
};
}
},
renderer(token) {
return `<dl>${this.parser.parseInline(token.tokens)}</dl>`;
}
};
marked.use({ extensions: [descriptionList] });
扩展参数详解
参数 | 必填 | 描述 |
---|---|---|
name | ✓ | 扩展标识符 |
level | ✓ | block /inline 层级 |
start() | ✓ | 检测扩展起始位置 |
tokenizer() | ✓ | 生成自定义令牌 |
renderer() | ✓ | 令牌渲染逻辑 |
childTokens | 需要遍历的子令牌字段 |
推荐使用 https://github.com/markedjs/marked-extension-template 创建扩展包
异步处理
启用 async: true
后,Marked 将返回 Promise:
// 验证链接是否有效
marked.use({
async: true,
walkTokens: async (token) => {
if (token.type === 'link') {
try {
await fetch(token.href);
} catch {
token.title = 'invalid'; // 标记失效链接
}
}
}
});
const html = await marked.parse(markdown);
直接访问 Lexer 和 Parser
// 手动执行分词和解析
const tokens = marked.lexer(markdown, options);
const html = marked.parser(tokens, options);
// 查看内置分词规则
console.log(marked.Lexer.rules.block);
console.log(marked.Lexer.rules.inline);
总结
扩展方式
- 使用
marked.use()
进行全局配置 - 支持覆盖渲染器(
renderer
)和分词器(tokenizer
) - 通过
extensions
添加全新语法支持
- 使用
处理流程
- 分层处理(块级/行内)
- 令牌树机制实现复杂嵌套
- 钩子函数介入关键生命周期
最佳实践
- 扩展应在全局作用域添加
- 优先使用官方扩展模板
- 异步操作需启用
async
选项
高级访问
- 可直接调用
lexer()
和parser()
- 支持查看和修改内置分词规则
- 可直接调用
通过灵活运用这些扩展机制,开发者可以为 Marked.js 添加各种自定义语法和处理逻辑,满足特定场景的 Markdown 处理需求。