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:
2026-07-24 17:16:55 +08:00
parent 0ce7494c7a
commit b9e98a4ff4
17 changed files with 216 additions and 24 deletions
+65 -1
View File
@@ -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(/^(&gt;)\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 || '';