基于 marked.js 的扩展机制,创建一个自定义的块级容器扩展,让内容渲染为
标签而非默认的
标签
概述
marked.js 默认会将 markdown 中的普通文本块渲染为 <p> 标签,本文将介绍如何通过 marked.js 的扩展机制,实现自动将非空文本块渲染为 <div> 标签 ,且遇到空白行时自动分隔生成新的 <div> ,同时保留 markdown 内联语法(如加粗、斜体、链接等)的解析能力,无需手动编写自定义标记(如 ::div)。
实现目标
- 普通文本行不再生成
<p>标签,而是自动生成<div>标签; - 连续的非空行合并为一个
<div>; - 空白行作为分隔符,空白行后的非空行生成新的
<div>; - 保留内联语法(加粗 、斜体 等)的正常解析;
- 不影响 marked.js 其他原生特性(如代码块、列表、标题等)。
实现思路
- 扩展类型 :定义
block级扩展(块级扩展),优先级高于 marked.js 默认的<p>标签解析; - 匹配规则:通过正则匹配「连续的非空文本行」,直到空白行或文本结束;
- Token 处理:解析匹配到的文本块生成自定义 Token,并处理其中的内联语法 Token;
- 渲染逻辑 :将自定义 Token 渲染为
<div>标签,同时解析内联 Token 保证语法生效。
核心扩展代码
javascript
// 1. 定义自定义 div 块级扩展
const autoDivBlock = {
name: 'autoDiv', // 扩展唯一名称
level: 'block', // 块级扩展(覆盖默认<p>标签的块解析)
/**
* 标记解析中断点:优先匹配非空行开头,触发当前扩展
* @param {string} src - 待解析的 markdown 源码
* @returns {number} 匹配到的位置索引,-1 表示不匹配
*/
start(src) {
// 匹配非空、非换行的字符开头(排除空白行)
const match = src.match(/^[^\n\s]/);
return match ? match.index : -1;
},
/**
* Token 生成器:解析 markdown 源码,生成自定义 Token
* @param {string} src - 待解析的 markdown 源码
* @param {Array} tokens - 已生成的 Token 列表
* @returns {Object|null} 自定义 Token 对象
*/
tokenizer(src, tokens) {
// 正则规则说明:
// ^ 锚定文本开头
// ([\s\S]*?) 非贪婪匹配所有内容(包括换行),捕获连续非空行
// (?=\n\n|$) 正向预查:直到空白行(\n\n)或文本结束($)停止
const rule = /^([\s\S]*?)(?=\n\n|$)/;
const match = rule.exec(src);
if (match) {
// 过滤纯空白的匹配,避免生成空的<div>
const content = match[1].trim();
if (!content) return null;
// 生成自定义 Token
const token = {
type: 'autoDiv', // 与扩展 name 对应
raw: match[0], // 原始匹配文本(用于消耗源码,避免重复解析)
text: content, // 提取 div 内的有效内容
tokens: [] // 存储内联 Token(如加粗、斜体等)
};
// 解析内容中的内联语法,生成子 Token
this.lexer.inline(token.text, token.tokens);
return token;
}
},
/**
* 渲染器:将自定义 Token 转换为 HTML 字符串
* @param {Object} token - 自定义 Token 对象
* @returns {string} 渲染后的 HTML 字符串
*/
renderer(token) {
// 渲染为<div>,可自定义 class 方便样式控制
return `<div class="auto-generated-div">${this.parser.parseInline(token.tokens)}</div>`;
}
};
// 2. 注册扩展并配置 marked.js
marked.use({
extensions: [autoDivBlock], // 注册自定义扩展
gfm: true, // 保留 GFM 特性(如表格、删除线等)
breaks: true // 保留换行符解析
});
// 3. 测试示例
const testMarkdown = `
第一行内容
第二行内容(和第一行同属一个div)
第三行内容(空白行分隔,新div)
**第四行加粗内容**(和第三行同属一个div)
(空白行后无内容,不生成div)
第五行内容(又一个新div)
`;
// 解析 markdown 并输出结果
const resultHtml = marked.parse(testMarkdown);
console.log('渲染后的 HTML:\n', resultHtml);
关键代码解释
1. start 函数
javascript
start(src) {
const match = src.match(/^[^\n\s]/);
return match ? match.index : -1;
}
- 作用:告诉 marked.js 「在什么位置停止并检查当前扩展的匹配规则」;
- 逻辑:匹配「非空、非换行的字符开头」,确保只要有非空行就优先触发当前扩展,覆盖默认的
<p>标签解析; - 返回值:匹配到的索引(触发扩展)或
-1(不触发,交给其他扩展/默认解析)。
2. tokenizer 函数
- 正则规则 :
/^([\s\S]*?)(?=\n\n|$)/是核心,实现「捕获连续非空行,空白行分隔」的逻辑:([\s\S]*?):非贪婪匹配任意字符(包括换行),保证只匹配到下一个分隔符为止;(?=\n\n|$):正向预查,匹配「空白行(\n\n)」或「文本结束($)」,不消耗字符,仅作为分隔标记。
- Token 处理 :
this.lexer.inline(token.text, token.tokens)解析文本中的内联语法(如加粗 、斜体 ),生成子 Token 存储在token.tokens中,保证内联语法正常渲染。
3. renderer 函数
javascript
renderer(token) {
return `<div class="auto-generated-div">${this.parser.parseInline(token.tokens)}</div>`;
}
- 作用:将自定义 Token 转换为最终的 HTML;
this.parser.parseInline(token.tokens):将存储的内联子 Token 解析为对应的 HTML(如**加粗**→<strong>加粗</strong>);- 自定义
auto-generated-div类:方便后续通过 CSS 控制<div>样式。
测试示例与输出
输入的 Markdown 文本
markdown
第一行内容
第二行内容(和第一行同属一个div)
第三行内容(空白行分隔,新div)
**第四行加粗内容**(和第三行同属一个div)
(空白行后无内容,不生成div)
第五行内容(又一个新div)
渲染后的 HTML 输出(格式化后)
html
<div class="auto-generated-div">第一行内容
第二行内容(和第一行同属一个div)</div>
<div class="auto-generated-div">第三行内容(空白行分隔,新div)
<strong>第四行加粗内容</strong>(和第三行同属一个div)</div>
<div class="auto-generated-div">第五行内容(又一个新div)</div>
核心特性总结
- 自动替换标签 :普通文本块不再生成
<p>,而是自动生成<div>,无需手动标记; - 空白行分隔 :连续非空行合并为一个
<div>,空白行触发新<div>生成; - 保留内联语法:支持加粗、斜体、链接等内联 markdown 语法的正常解析;
- 兼容性:不影响 marked.js 原生特性(如代码块、列表、标题等)的解析;
- 可扩展性 :可通过修改
renderer函数自定义<div>的 class、style 等属性。
扩展与优化建议
-
自定义样式 :通过
auto-generated-div类为<div>添加样式,例如:css.auto-generated-div { margin: 8px 0; line-height: 1.6; padding: 4px 0; } -
排除特殊块 :若需要保留代码块、列表等原生块级标签的解析,可在
start函数中增加排除逻辑; -
自定义分隔规则 :若需修改分隔逻辑(如多个空白行才分隔),可调整
tokenizer中的正则规则(如(?=\n{3,}|$)表示至少3个换行符才分隔)。