Cherry Markdown语法基础:抽象语法类设计与实现
引言:现代Markdown引擎的架构挑战
你是否曾思考过,一个现代化的Markdown编辑器如何高效解析数百种语法规则,同时保持优秀的性能和扩展性?Cherry Markdown通过其精妙的抽象语法类设计,为这个问题提供了优雅的解决方案。
本文将深入解析Cherry Markdown的核心语法类架构,揭示其如何通过面向对象设计和多态机制实现高效的Markdown解析。读完本文,你将掌握:
- Cherry Markdown的三层语法抽象架构
- 语法类的生命周期管理和缓存机制
- 正则表达式规则的定义与编译策略
- 段落级与句子级语法的差异与协作
- 自定义语法扩展的最佳实践
语法类体系架构
Cherry Markdown采用分层架构设计,将语法处理分为三个核心层次:
核心基类:SyntaxBase
SyntaxBase是所有语法类的基类,定义了统一的接口和生命周期方法:
export default class SyntaxBase {
static HOOK_NAME = 'default';
static HOOK_TYPE = HOOKS_TYPE_LIST.DEFAULT;
constructor(editorConfig) {
this.RULE = this.rule(editorConfig);
}
// 生命周期方法
beforeMakeHtml(str) { return str; }
makeHtml(str) { return str; }
afterMakeHtml(str) { return str; }
// 规则测试
test(str) {
return this.RULE.reg ? this.RULE.reg.test(str) : false;
}
// 规则定义抽象方法
rule(editorConfig) {
return { begin: '', end: '', content: '', reg: new RegExp('') };
}
}
段落级语法基类:ParagraphBase
ParagraphBase继承自SyntaxBase,专门处理段落级语法元素,引入了缓存机制来优化性能:
export default class ParagraphBase extends SyntaxBase {
static HOOK_TYPE = HOOKS_TYPE_LIST.PAR;
static IN_PARAGRAPH_CACHE_KEY_PREFIX = '!';
constructor({ needCache, defaultCache } = { needCache: false }) {
super({});
this.needCache = !!needCache;
if (needCache) {
this.cache = defaultCache || {};
this.cacheKey = `~~C${cacheCounter}`;
cacheCounter += 1;
}
}
// 缓存管理方法
pushCache(str, sign = '', lineCount = 0) {
const $sign = sign || this.$engine.hash(str);
const key = `${this.cacheKey}I${$sign}_L${lineCount}$`;
this.cache[$sign] = { content: str, key };
return key;
}
popCache(sign) {
return this.cache[sign].content || '';
}
restoreCache(html) {
const regex = new RegExp(
`${this.cacheKey}I((?:${ParagraphBase.IN_PARAGRAPH_CACHE_KEY_PREFIX_REGEX})?\\w+)\\$`,
'g'
);
return html.replace(regex, (match, cacheSign) =>
this.popCache(cacheSign.replace(/_L\d+$/, ''))
);
}
}
语法规则定义与编译
Cherry Markdown采用结构化的规则定义方式,支持多种语法风格:
规则定义结构
// ATX标题语法规则定义
const atx = {
begin: '(?:^|\\n)(\\n*)(?:\\h*(#{1,6}))', // 匹配行首和标题级别
content: '(.+?)', // 匹配标题内容
end: '(?:\\h*#*\\h*)?(?=$|\\n)', // 匹配行尾
reg: compileRegExp(atx, 'g', true) // 编译后的正则表达式
};
// Setext标题语法规则
const setext = {
begin: '(?:^|\\n)(\\n*)',
content: [
'(?:\\h*',
'(.+)', // 标题文本
')\\n',
'(?:\\h*',
'([=]+|[-]+)', // 下划线级别
')'
].join(''),
end: '(?=$|\\n)',
reg: compileRegExp(setext, 'g', true)
};
正则表达式编译策略
Cherry Markdown使用专门的编译函数处理规则定义:
function compileRegExp(rule, flags = 'g', namedGroup = false) {
let pattern = rule.begin + rule.content + rule.end;
if (namedGroup) {
// 支持命名捕获组
pattern = pattern.replace(/\(\?<(\w+)>/g, '(?<$1>');
}
return new RegExp(pattern, flags);
}
语法处理生命周期
每个语法类都遵循严格的生命周期管理:
三阶段处理流程
具体实现示例:标题语法
以Header类为例,展示完整的三阶段处理:
export default class Header extends ParagraphBase {
static HOOK_NAME = 'header';
// 阶段1: 预处理和缓存
beforeMakeHtml(str) {
let $str = str;
if (this.test($str, ATX_HEADER)) {
$str = $str.replace(this.RULE[ATX_HEADER].reg, (match, lines, level, text) => {
if (text.trim() === '') return match;
return this.getCacheWithSpace(this.pushCache(match), match, true);
});
}
return $str;
}
// 阶段2: 实际转换
makeHtml(str, sentenceMakeFunc) {
let $str = this.restoreCache(str);
if (this.test($str, ATX_HEADER)) {
$str = $str.replace(this.RULE[ATX_HEADER].reg, (match, lines, level, text) => {
const lineCount = calculateLinesOfParagraph(lines, this.getLineCount(match));
const $text = text.replace(/\s+#+\s*$/, '');
const { html: result, sign } = this.$wrapHeader($text, level.length, lineCount, sentenceMakeFunc);
return this.getCacheWithSpace(this.pushCache(result, sign, lineCount), match, true);
});
}
return $str;
}
// 阶段3: 后处理
afterMakeHtml(html) {
const $html = super.afterMakeHtml(html);
this.headerIDCache = []; // 清理ID缓存
this.headerIDCounter = {};
return $html;
}
}
高级特性:ID生成与锚点处理
Cherry Markdown提供了智能的标题ID生成机制:
ID生成算法
$generateId(headerText, toLowerCase = true) {
const len = headerText.length;
let id = '';
for (let i = 0; i < len; i++) {
const c = headerText.charAt(i);
if (alphabetic.test(c)) {
id += toLowerCase ? c.toLowerCase() : c;
} else if (numeric.test(c)) {
id += c;
} else if (toDashChars.test(c)) {
id += id.length < 1 || id.charAt(id.length - 1) !== '-' ? '-' : '';
} else if (c.charCodeAt(0) > 255) {
try { id += encodeURIComponent(c); } catch (error) {}
}
}
return id;
}
// 重复ID处理
generateIDNoDup(headerText) {
let newId = this.$generateId(headerText, true);
const idIndex = this.headerIDCache.indexOf(newId);
if (idIndex !== -1) {
this.headerIDCounter[idIndex] += 1;
newId += `-${this.headerIDCounter[idIndex] + 1}`;
} else {
const newIndex = this.headerIDCache.push(newId);
this.headerIDCounter[newIndex - 1] = 1;
}
return newId;
}
锚点包装逻辑
$wrapHeader(text, level, dataLines, sentenceMakeFunc) {
const processedText = sentenceMakeFunc(text.trim());
let { html } = processedText;
// 自定义ID提取
const customIDRegex = /\s+\{#([A-Za-z0-9-]+)\}$/;
const idMatch = html.match(customIDRegex);
let anchorID;
if (idMatch !== null) {
html = html.substring(0, idMatch.index);
[, anchorID] = idMatch;
}
const headerTextRaw = this.$parseTitleText(html);
if (!anchorID) {
anchorID = this.generateIDNoDup(headerTextRaw.replace(/~fn#([0-9]+)#/g, ''));
}
const safeAnchorID = `safe_${anchorID}`;
const sign = this.$engine.hash(`${level}-${processedText.sign}-${anchorID}-${dataLines}`);
return {
html: `<h${level} id="${safeAnchorID}" data-sign="${sign}" data-lines="${dataLines}">
${this.$getAnchor(anchorID)}${html}
</h${level}>`,
sign: `${sign}`
};
}
性能优化策略
缓存机制对比
| 缓存类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 段落缓存 | 块级元素(标题、代码块等) | 减少重复解析,提升性能 | 增加内存使用 |
| 行内缓存 | 句子级元素(加粗、斜体等) | 精细控制,内存友好 | 解析开销较大 |
| 签名缓存 | 唯一内容标识 | 避免重复处理相同内容 | 哈希计算开销 |
换行处理策略
Cherry Markdown支持两种换行模式:
initBrReg(classicBr = false) {
this.classicBr = classicBr;
// 经典模式:两个以上换行才分割段落
// 现代模式:一个换行转<br>,两个换行分割段落
}
$cleanParagraph(str) {
const trimedPar = str.replace(/^\n+/, '').replace(/\n+$/, '');
if (this.classicBr) {
return trimedPar;
}
return this.joinRawHtml(trimedPar).replace(/\n/g, '<br>').replace(/\r/g, '\n');
}
自定义语法扩展指南
基于Cherry Markdown的抽象类体系,扩展自定义语法变得非常简单:
基本扩展步骤
- 选择基类:根据语法特性选择
SyntaxBase或ParagraphBase - 定义规则:实现
rule()方法返回正则表达式规则 - 实现转换:重写
makeHtml()方法处理语法转换 - 注册钩子:使用
Cherry.useHook()注册自定义语法
示例:自定义警告框语法
import ParagraphBase from '@/core/ParagraphBase';
export default class AlertBlock extends ParagraphBase {
static HOOK_NAME = 'alert';
rule() {
return {
begin: '(?:^|\\n)(\\n*)!!!\\s*(\\w+)\\s*\\n',
content: '([\\s\\S]+?)',
end: '\\n!!!\\s*\\n',
reg: new RegExp(this.begin + this.content + this.end, 'g')
};
}
makeHtml(str, sentenceMakeFunc) {
return str.replace(this.RULE.reg, (match, lines, type, content) => {
const processedContent = sentenceMakeFunc(content.trim());
return `<div class="alert alert-${type}">${processedContent.html}</div>`;
});
}
}
// 注册自定义语法
Cherry.useHook(AlertBlock);
总结与最佳实践
Cherry Markdown的抽象语法类设计体现了现代前端架构的多个重要原则:
设计原则总结
- 单一职责原则:每个语法类只负责一种语法元素的处理
- 开闭原则:通过继承和钩子机制支持扩展,避免修改现有代码
- 依赖倒置原则:高层模块不依赖低层模块,都依赖抽象接口
- 接口隔离原则:细粒度的生命周期接口,避免冗余实现
性能优化建议
- 合理使用缓存:对重复内容使用签名缓存,对复杂解析使用结果缓存
- 避免过度嵌套:控制正则表达式的复杂度,避免回溯爆炸
- 批量处理:在
afterMakeHtml阶段进行批量后处理操作 - 内存管理:及时清理不再使用的缓存内容
扩展性考虑
- 插件化架构:所有语法都以插件形式存在,支持动态加载和卸载
- 配置驱动:通过配置对象控制语法特性和行为
- 类型安全:完整的TypeScript类型定义,提高开发体验
Cherry Markdown的语法类体系不仅提供了强大的Markdown解析能力,更为开发者提供了清晰的扩展路径和性能优化空间。通过深入理解这一架构,你可以更好地定制和优化自己的Markdown处理需求,构建出更加强大和高效的文档处理应用。
无论是构建下一代文档编辑器、开发技术博客平台,还是实现企业级内容管理系统,Cherry Markdown的抽象语法类设计都将为你提供坚实的技术基础。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



