diff --git a/src/core.js b/src/core.js index ee8f3c8..626e42b 100644 --- a/src/core.js +++ b/src/core.js @@ -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, '$&'); - // 粗体 - html = html.replace(/(\*\*|__)(.+?)\1/g, '$&'); - // 斜体 - html = html.replace(/(?$&'); - // 删除线 - html = html.replace(/~~(.+?)~~/g, '$&'); - // 行内代码 - html = html.replace(/`([^`]+)`/g, '$&'); - // 链接 - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$&'); - // 图片 + // 正则按优先级:先匹配更具体的模式,避免被泛模式吞掉 + // 1. 图片(先于链接) html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$&'); - // 列表标记 + // 2. 链接 + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$&'); + // 3. 行内代码(先于粗体/斜体,避免 `**` 被误匹配) + html = html.replace(/`([^`]+)`/g, '$&'); + // 4. 粗体 **text** / __text__ + html = html.replace(/(\*\*|__)(.+?)\1/g, '$&'); + // 5. 删除线 + html = html.replace(/~~(.+?)~~/g, '$&'); + // 6. 斜体 *text* / _text_(在粗体之后,避免 ** 被拆成两个斜体) + html = html.replace(/(?$&'); + html = html.replace(/(?$&'); + // 7. 标题(行级) + html = html.replace(/^(#{1,6})\s+(.+)$/gm, '$&'); + // 8. 列表标记 html = html.replace(/^(\s*[-*+]|\d+\.)\s/gm, '$&'); - // 引用 + // 9. 引用 html = html.replace(/^(>)\s?/gm, '$&'); - // 水平线 + // 10. 水平线 html = html.replace(/^([-*_]{3,})\s*$/gm, '$&'); 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 = [];