opt(v0.1.14): enhance syntax highlight — rAF throttle, regex order, large-doc skip

opt(core): _highlightEditor rAF throttle to avoid per-keystroke reflow
opt(core): fix regex order — image before link, code before bold, italic last
opt(core): skip highlighting for documents >50KB to prevent lag
opt(core): add toggleSyntaxHighlight() API method
fix(core): clear _hlRaf in destroy to prevent post-mortem callbacks
fix(core): initialize _hlRaf = null in constructor
This commit is contained in:
2026-07-24 17:28:06 +08:00
parent d7813d8e1b
commit 2ab4c8d499
+45 -18
View File
@@ -115,6 +115,7 @@ class MarkdownEditor {
this._zenMode = false; // v0.1.12: Zen 模式
this._wordWrap = true; // v0.1.12: 自动换行
this._syncing = false; // 滚动同步锁
this._hlRaf = null; // v0.1.14: 高亮节流
// 渲染函数:自定义覆盖内置解析器
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
@@ -1318,32 +1319,44 @@ class MarkdownEditor {
// ============ 语法高亮编辑区(v0.1.14 ============
_highlightEditor() {
if (!this.highlightLayer) return;
if (!this.highlightLayer || !this.config.syntaxHighlight) return;
// rAF 节流,避免高频更新
if (this._hlRaf) return;
this._hlRaf = requestAnimationFrame(() => {
this._hlRaf = null;
this._doHighlight();
});
}
_doHighlight() {
const text = this._value || '';
if (!text) { this.highlightLayer.innerHTML = ''; return; }
// 超过 50KB 跳过,避免大文档卡顿
if (text.length > 50000) { this.highlightLayer.innerHTML = escapeHTML(text) + '\n'; return; }
// 轻量级 tokenizer:正则匹配所有语法模式,包裹为 span
let html = escapeHTML(text);
// 标题
html = html.replace(/^(#{1,6})\s+(.+)$/gm, '<span class="mh-heading">$&</span>');
// 粗体
html = html.replace(/(\*\*|__)(.+?)\1/g, '<span class="mh-bold">$&</span>');
// 斜体
html = html.replace(/(?<!\*)\*(?!\*)(.+?)\*(?!\*)/g, '<span class="mh-italic">$&</span>');
// 删除线
html = html.replace(/~~(.+?)~~/g, '<span class="mh-strike">$&</span>');
// 行内代码
html = html.replace(/`([^`]+)`/g, '<span class="mh-code">$&</span>');
// 链接
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="mh-link">$&</span>');
// 图片
// 正则按优先级:先匹配更具体的模式,避免被泛模式吞掉
// 1. 图片(先于链接)
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<span class="mh-image">$&</span>');
// 列表标记
// 2. 链接
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="mh-link">$&</span>');
// 3. 行内代码(先于粗体/斜体,避免 `**` 被误匹配)
html = html.replace(/`([^`]+)`/g, '<span class="mh-code">$&</span>');
// 4. 粗体 **text** / __text__
html = html.replace(/(\*\*|__)(.+?)\1/g, '<span class="mh-bold">$&</span>');
// 5. 删除线
html = html.replace(/~~(.+?)~~/g, '<span class="mh-strike">$&</span>');
// 6. 斜体 *text* / _text_(在粗体之后,避免 ** 被拆成两个斜体)
html = html.replace(/(?<!\*)\*(?!\*)([^*\n]+?)\*(?!\*)/g, '<span class="mh-italic">$&</span>');
html = html.replace(/(?<!_)_(?!_)([^_\n]+?)_(?!_)/g, '<span class="mh-italic">$&</span>');
// 7. 标题(行级)
html = html.replace(/^(#{1,6})\s+(.+)$/gm, '<span class="mh-heading">$&</span>');
// 8. 列表标记
html = html.replace(/^(\s*[-*+]|\d+\.)\s/gm, '<span class="mh-list">$&</span>');
// 引用
// 9. 引用
html = html.replace(/^(&gt;)\s?/gm, '<span class="mh-quote">$&</span>');
// 水平线
// 10. 水平线
html = html.replace(/^([-*_]{3,})\s*$/gm, '<span class="mh-hr">$&</span>');
this.highlightLayer.innerHTML = html + '\n';
@@ -1351,6 +1364,19 @@ class MarkdownEditor {
if (this.textarea) this.highlightLayer.scrollTop = this.textarea.scrollTop;
}
/** 切换语法高亮(v0.1.14 */
toggleSyntaxHighlight() {
this.config.syntaxHighlight = !this.config.syntaxHighlight;
if (this.textarea) {
this.textarea.classList.toggle('me-textarea-highlight', this.config.syntaxHighlight);
}
if (!this.config.syntaxHighlight && this.highlightLayer) {
this.highlightLayer.innerHTML = '';
}
if (this.config.syntaxHighlight) this._doHighlight();
return this;
}
_updateWordCount() {
if (!this.config.wordCount || !this.statusEl) return;
const text = this._value || '';
@@ -1737,6 +1763,7 @@ class MarkdownEditor {
if (this._renderRaf) cancelAnimationFrame(this._renderRaf);
if (this._historyTimer) clearTimeout(this._historyTimer);
if (this._outlineTimer) clearTimeout(this._outlineTimer);
if (this._hlRaf) cancelAnimationFrame(this._hlRaf);
this._cleanups.forEach((fn) => { try { fn(); } catch (_) {} });
this._cleanups = [];