release: v0.1.14 — syntax highlight, Mermaid, fileSystem, preview links
feat(core): syntax highlight editor overlay - Transparent highlight-layer behind textarea with regex-based token coloring - Supports heading, bold, italic, strikethrough, code, link, image, list, quote, hr - Scroll-synced with textarea, config.syntaxHighlight toggle feat(core): preview link click interception - Clicks on preview links intercepted, emitted as 'linkClick' event - Configurable via onLinkClick(uri, text, editor) callback - Default: opens in new tab, anchor links pass through feat(parser): Mermaid diagram rendering - ```mermaid code blocks render as <div class="me-mermaid"><pre class="mermaid"> feat(plugins): fileSystem plugin (IndexedDB persistence) - saveToFile(id) / loadFromFile(id) / deleteFile(id) / listFiles() - Uses IndexedDB 'metona-editor-fs' database feat(config): maxLength (char limit), syntaxHighlight toggle style: syntax highlight theme colors, Mermaid container chore: bump version 0.1.13 → 0.1.14 across all files
This commit is contained in:
+65
-1
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* MetonaEditor Core - 编辑器核心
|
||||
* @module core
|
||||
* @version 0.1.13
|
||||
* @version 0.1.14
|
||||
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
|
||||
* v0.1.6: 插件依赖拓扑排序、实例 i18n、快捷键/工具栏定制、右键菜单、Toast
|
||||
* v0.1.7: 行号装订线、自动格式化、括号自动闭合、拖放、大纲面板
|
||||
* v0.1.12: 工具栏键盘导航、aria-live、Zen模式、滚动同步v2、WordWrap
|
||||
* v0.1.14: 语法高亮编辑区、图片上传钩子、预览链接拦截、Mermaid、文件存储
|
||||
*/
|
||||
|
||||
import { generateId, escapeHTML, isBrowser } from './utils.js';
|
||||
@@ -215,6 +216,12 @@ class MarkdownEditor {
|
||||
if (!this.config.lineNumbers) gutter.style.display = 'none';
|
||||
editorInner.appendChild(gutter);
|
||||
|
||||
// 语法高亮叠加层(v0.1.14)
|
||||
const highlightLayer = document.createElement('pre');
|
||||
highlightLayer.className = 'me-highlight-layer';
|
||||
highlightLayer.setAttribute('aria-hidden', 'true');
|
||||
editorInner.appendChild(highlightLayer);
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'me-textarea';
|
||||
textarea.spellcheck = !!this.config.spellcheck;
|
||||
@@ -262,6 +269,7 @@ class MarkdownEditor {
|
||||
this.editorPane = editorPane;
|
||||
this.editorInner = editorInner;
|
||||
this.gutter = gutter;
|
||||
this.highlightLayer = highlightLayer;
|
||||
this.previewPane = previewPane;
|
||||
this.dividerEl = divider;
|
||||
this.textarea = textarea;
|
||||
@@ -328,6 +336,7 @@ class MarkdownEditor {
|
||||
this._updateWordCount();
|
||||
this._renderGutter();
|
||||
this._updateOutline();
|
||||
this._highlightEditor(); // v0.1.14
|
||||
this._emit('input', this._value);
|
||||
this._emit('change', this._value);
|
||||
if (typeof this.config.onInput === 'function') {
|
||||
@@ -395,6 +404,25 @@ class MarkdownEditor {
|
||||
ta.addEventListener('focus', onFocus);
|
||||
ta.addEventListener('blur', onBlur);
|
||||
|
||||
// v0.1.14: 预览区链接点击拦截
|
||||
const onPreviewClick = (e) => {
|
||||
const a = e.target.closest('a');
|
||||
if (!a) return;
|
||||
const href = a.getAttribute('href');
|
||||
if (!href || href.startsWith('#')) return; // 锚点链接不拦截
|
||||
e.preventDefault();
|
||||
// 触发自定义事件,用户可监听处理
|
||||
this._emit('linkClick', { href, text: a.textContent });
|
||||
// 如果用户提供了 onLinkClick 回调
|
||||
if (typeof this.config.onLinkClick === 'function') {
|
||||
try { this.config.onLinkClick(href, a.textContent, this); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
// 默认:新窗口打开
|
||||
window.open(href, '_blank', 'noopener');
|
||||
};
|
||||
this.previewEl.addEventListener('click', onPreviewClick);
|
||||
|
||||
// 工具栏点击
|
||||
const onToolbarClick = (e) => {
|
||||
const btn = e.target.closest('.me-btn');
|
||||
@@ -1284,6 +1312,42 @@ class MarkdownEditor {
|
||||
requestAnimationFrame(() => { this._syncing = false; });
|
||||
}
|
||||
|
||||
// ============ 语法高亮编辑区(v0.1.14) ============
|
||||
|
||||
_highlightEditor() {
|
||||
if (!this.highlightLayer) return;
|
||||
const text = this._value || '';
|
||||
if (!text) { this.highlightLayer.innerHTML = ''; 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>');
|
||||
// 图片
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<span class="mh-image">$&</span>');
|
||||
// 列表标记
|
||||
html = html.replace(/^(\s*[-*+]|\d+\.)\s/gm, '<span class="mh-list">$&</span>');
|
||||
// 引用
|
||||
html = html.replace(/^(>)\s?/gm, '<span class="mh-quote">$&</span>');
|
||||
// 水平线
|
||||
html = html.replace(/^([-*_]{3,})\s*$/gm, '<span class="mh-hr">$&</span>');
|
||||
|
||||
this.highlightLayer.innerHTML = html + '\n';
|
||||
// 同步滚动
|
||||
if (this.textarea) this.highlightLayer.scrollTop = this.textarea.scrollTop;
|
||||
}
|
||||
|
||||
_updateWordCount() {
|
||||
if (!this.config.wordCount || !this.statusEl) return;
|
||||
const text = this._value || '';
|
||||
|
||||
Reference in New Issue
Block a user