安装与引入
-# npm 安装 npm install @metona-team/metona-editor -# ES Module +// TypeScript / ES Module import MeEditor from '@metona-team/metona-editor'; const editor = MeEditor.create('#editor', { value: '# Hello World', mode: 'split', }); -# 浏览器 UMD +// 浏览器 UMD <script src="dist/metona-editor.js"></script> <script> const editor = MeEditor.create('#editor'); </script>
全部配置项
-所有配置均为可选,以下是默认值:
- +所有配置均为可选。
MeEditor.create(container, { // 内容 - value: '', // 初始 Markdown 文本 - placeholder: '', // 占位符 + value: '', placeholder: '', // 视图 - mode: 'split', // 'edit' | 'split' | 'preview' - height: 400, // 数字为 px,字符串原样使用 - toolbar: DEFAULT_TOOLBAR, // 工具栏配置,false 隐藏 - wordCount: true, // 字数统计状态栏 + mode: 'split', height: 400, + toolbar: DEFAULT_TOOLBAR, wordCount: true, + lineNumbers: true, outline: false, // 行为 - autofocus: false, // 自动聚焦 - spellcheck: false, // 拼写检查 - readOnly: false, // 只读模式 - historyLimit: 100, // 历史栈上限 - historyDebounce: 400, // 历史防抖延迟(ms) - syncScroll: true, // 分屏模式同步滚动 - tabSize: 2, // 制表符空格数(0 表示 \t) + autofocus: false, spellcheck: false, + readOnly: false, tabSize: 2, + historyLimit: 100, historyDebounce: 400, + syncScroll: true, autoBrackets: true, + zenMode: false, wordWrap: true, - // 主题与国际化 - theme: 'auto', // 'light' | 'dark' | 'auto' | 'warm' | 自定义 - locale: 'zh-CN', // 'zh-CN' | 'en-US' | 自定义 + // 主题 / 语言 + theme: 'auto', locale: 'zh-CN', - // 自定义渲染 - render: null, // (md, env) => html - highlight: null, // (code, lang) => html - sanitize: null, // (html) => safeHtml + // 钩子 + render: null, highlight: null, + sanitize: null, // 外观 - className: '', // 容器额外 class - style: {}, // 内联样式 + className: '', style: {}, - // 插件与回调 - plugins: [], // 实例级插件数组 - onChange: null, // (value, editor) => void - onInput: null, // (value, editor) => void - onFocus: null, // (editor) => void - onBlur: null, // (editor) => void - onSave: null, // (value, editor) => void - onModeChange: null, // (mode, editor) => void - onFullscreen: null, // (fullscreen, editor) => void - onCreate: null, // (editor) => void - onDestroy: null, // (editor) => void + // 插件 / 回调 + plugins: [], + onChange: null, onInput: null, + onFocus: null, onBlur: null, + onSave: null, onModeChange: null, + onFullscreen: null, onCreate: null, + onDestroy: null, onLinkClick: null, });
工具栏配置
- -// 默认工具栏(完整按钮) +工具栏
+// 默认完整工具栏 MeEditor.create(container, { toolbar: true }); -// 隐藏工具栏 +// 隐藏 MeEditor.create(container, { toolbar: false }); -// 自定义工具栏 — '|' 为分隔符 -MeEditor.create(container, { - toolbar: ['bold', 'italic', '|', 'h1', 'h2', '|', 'undo', 'redo'], -});- +// 自定义 | 为分隔符 +MeEditor.create(container, { toolbar: ['bold', 'italic', '|', 'undo', 'redo'] });
可用动作:
bolditalicstrikethroughunderlinecode @@ -312,346 +185,244 @@ MeEditor.create
实例 API
- + +实例 API
内容操作
-editor.getValue(); // 获取 Markdown 文本 -editor.setValue(md, { silent }); // 设置内容,silent 不触发 change -editor.getHTML(); // 获取渲染后的 HTML -editor.refresh(); // 强制刷新预览(内容未变时) -editor.insert(text, { replace }); // 光标处插入 / 替换选区 -editor.wrap(before, after); // 选区包裹 -editor.focus(); // 聚焦 -editor.blur(); // 失焦+
editor.getValue(); // => string +editor.setValue(md, { silent }); // => this +editor.getHTML(); // => string +editor.refresh(); // 强制重渲染 +editor.insert(text, { replace }); // => this +editor.wrap(before, after); // => this +editor.focus(); blur();
命令执行
-editor.exec(action, ...args); // 执行命令,返回 this 链式调用 - -editor.exec('bold').exec('h1'); // 链式调用- +
命令执行
+editor.exec(action) // => this, 支持链式
| action | 效果 | 快捷键 |
|---|---|---|
bold | 加粗 **text** | Ctrl+B |
italic | 斜体 *text* | Ctrl+I |
strikethrough | 删除线 ~~text~~ | — |
underline | 下划线 <u>text</u> | Ctrl+U |
code | 行内代码 `text` | Ctrl+E |
h1/h2/h3 | 标题 # / ## / ### | Ctrl+1/2/3 |
quote | 引用 > | Ctrl+Q |
ul/ol | 无序/有序列表 | — |
indent/outdent | 缩进/反缩进 | Tab/Shift+Tab |
hr | 水平线 --- | — |
link | 插入链接 [text](url) | Ctrl+K |
image | 插入图片  | — |
bold | **text** | Ctrl+B |
italic | *text* | Ctrl+I |
strikethrough | ~~text~~ | — |
underline | <u>text</u> | Ctrl+U |
code | `text` | Ctrl+E |
h1/h2/h3 | # / ## / ### | Ctrl+1/2/3 |
quote | > text | Ctrl+Q |
ul/ol | - / 1. 列表 | — |
link | [text](url) | Ctrl+K |
image |  | — |
table | 插入表格 | — |
undo/redo | 撤销/重做 | Ctrl+Z / Ctrl+Y |
hr | ---水平线 | — |
indent/outdent | 缩进/反缩进 | Tab / Shift+Tab |
undo/redo | 撤销/重做 | Ctrl+Z / Y |
edit/split/preview | 切换模式 | — |
fullscreen | 切换全屏 | — |
fullscreen | 全屏 | — |
zen | Zen 模式 | — |
wordwrap | 自动换行 | — |
历史栈
-editor.undo(); // 撤销 -editor.redo(); // 重做 -editor.canUndo(); // 是否可撤销 -editor.canRedo(); // 是否可重做+
历史栈
+editor.undo(); editor.redo(); +editor.canUndo(); editor.canRedo();
模式与全屏
-editor.setMode('split'); // 设置模式:edit | split | preview -editor.getMode(); // 获取当前模式 -editor.toggleFullscreen(); // 切换全屏 -editor.exitFullscreen(); // 退出全屏 -editor.isFullscreen(); // 是否全屏+
模式 / 全屏 / Zen
+editor.setMode('split'); editor.getMode(); +editor.toggleFullscreen(); isFullscreen(); exitFullscreen(); +editor.toggleZen(); isZen(); +editor.toggleWordWrap(); setWordWrap(true); isWordWrap();
统计与状态
+统计与状态
editor.getStats(); -// => { characters, words, chineseChars, englishWords, lines, readingTime } - +// => { characters, words, chineseChars, englishWords, lines, readingTime } editor.getStatus(); -// => { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }+// => { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }
启用/禁用/只读
-editor.enable(); // 启用 -editor.disable(); // 禁用 -editor.isDisabled(); // 是否禁用 -editor.setReadOnly(true); // 只读模式 -editor.isReadOnly(); // 是否只读+
启用 / 禁用 / 只读
+editor.enable(); disable(); isDisabled(); +editor.setReadOnly(true); isReadOnly();
事件系统
- +事件系统
| 事件名 | 触发时机 | 回调参数 |
|---|---|---|
input | textarea 原生 input | (value, editor) |
| 事件 | 触发时机 | 参数 |
input | textarea 输入 | (value, editor) |
change | 内容变化 | (value, editor) |
focus | 聚焦 | (editor) |
blur | 失焦 | (editor) |
save | Ctrl+S | (value, editor) |
focus / blur | 聚焦/失焦 | (editor) |
save | Ctrl+S | (editor) |
modeChange | 模式切换 | (mode, editor) |
fullscreen | 全屏切换 | (fullscreen, editor) |
themeChange | 主题变更 | ({ theme, resolved, config }) |
localeChange | 语言变更 | ({ locale, direction }) |
zenChange | Zen 切换 | (zen, editor) |
destroy | 销毁 | () |
autosave | autoSave 保存 | ({ key, value }) |
beforeRender | 渲染前 | (editor) |
afterRender | 渲染后 | (editor) |
beforeRender / afterRender | 渲染前后 | (editor) |
autosave | autoSave 触发 | ({ key, value }) |
linkClick | 预览链接点击 | ({ href, text }) |
全局钩子(所有实例共享)
+全局钩子
| 钩子 | 触发时机 |
|---|---|
beforeCreate | 构造函数初始化前 |
afterCreate | 构造函数初始化完成 |
beforeRender | 每次渲染前(全局) |
afterRender | 每次渲染后(全局) |
beforeDestroy | destroy 前 |
afterDestroy | destroy 后 |
beforeCreate / afterCreate | 实例构造前后 |
beforeRender / afterRender | 全局渲染前后 |
beforeDestroy / afterDestroy | 销毁前后 |
插件管理
-editor.use(plugin, options); // 安装插件 -editor.getPlugins(); // 获取已安装插件列表 -editor.addToolbarButton(config); // 追加工具栏按钮+
插件管理
+editor.use(plugin, options); // 安装 +editor.unuse(name); // 卸载 +editor.getPlugins(); // 已安装列表 +editor.addToolbarButton(config); // 追加按钮
生命周期
-editor.destroy(); // 销毁实例 -editor.isDestroyed(); // 是否已销毁+
快捷键管理
+editor.registerShortcut('Ctrl+Shift+T', handler, 'desc'); +editor.unregisterShortcut('Ctrl+Shift+T'); +editor.getShortcuts();
静态 API
+生命周期 / Toast
+editor.destroy(); isDestroyed(); +editor.toast(msg, { type, duration, animation });+
静态 API
import MeEditor from '@metona-team/metona-editor'; // 工厂函数 MeEditor.create(container, options); // 全局默认插件 -MeEditor.use(presetPlugins.autoSave, { delay: 2000 }); -MeEditor.use('searchReplace'); +MeEditor.use('autoSave', { delay: 2000 }); MeEditor.use(customPlugin); -// 全局事件钩子 -MeEditor.on('beforeCreate', (editor) => {}); +// 全局钩子 +MeEditor.on('beforeCreate', (editor) => {}); MeEditor.off('beforeCreate', handler); -// 主题与语言 +// 全局主题 / 语言 MeEditor.setTheme('dark'); MeEditor.setLocale('en-US'); -// 状态查询 -MeEditor.getStatus(); // => { version, theme, locale, globalPlugins, presetPlugins } - -// 销毁全局资源 -MeEditor.destroy(); - -// 内置解析器 -import { parseMarkdown, safeUrl, slugify } from '@metona-team/metona-editor'; -parseMarkdown('# Hello'); // => '<h1 id="hello">Hello</h1>' -safeUrl('javascript:alert(1)'); // => '' -slugify('Hello World'); // => 'hello-world'+// 解析器独立使用 +import { parseMarkdown, parseTokens, renderTokens } from '@metona-team/metona-editor';
解析器语法参考
- + +解析器语法参考
| 语法 | 示例 | 输出 |
|---|---|---|
| 标题 | # H1 ~ ###### H6 | <h1> ~ <h6> |
| 段落 | 纯文本 | <p> |
| 粗体 | **bold** __bold__ | <strong> |
| 粗斜体 v0.1.15 | ***text*** ___text___ | <em><strong> |
| 斜体 | *italic* _italic_ | <em> |
| 删除线 | ~~text~~ | <del> |
| 高亮标记 | ==text== | <mark> |
| 上标 | x^2^ | <sup> |
| 下标 | H~2~O | <sub> |
| 行内代码 | `code` | <code> |
| 代码块 | ```lang | <pre><code> |
| 引用 | > quote | <blockquote> |
| 无序列表 | - item | <ul><li> |
| 有序列表 | 1. item | <ol><li> |
| 任务列表 | - [x] done | <li class="me-task-item"> |
| 水平线 | --- *** ___ | <hr> |
| 表格 | | a | b | | <table>(支持 :---: 列对齐) |
| 链接 v0.1.15 | [**text**](url) | 链接文本内支持行内格式 |
| 图片 |  | <img> |
| 自动链接 | <https://...> | <a> |
| Emoji | :smile: :rocket: | 😊 🚀(150+ 常用) |
| 反斜杠转义 v0.1.15 | \* \_ \\ | 取消标点特殊含义 |
| 标题 | # H1 ~ ###### H6 | <h1> ~ <h6> |
| Setext 标题 | H1\n=== | <h1> / <h2> |
| 粗体 | **bold** __bold__ | <strong> |
| 粗斜体 v0.1.15 | ***text*** ___text___ | <em><strong> |
| 斜体 | *italic* _italic_ | <em> |
| 删除线 / 高亮 | ~~del~~ ==mark== | <del> / <mark> |
| 上标 / 下标 | x^2^ H~2~O | <sup> / <sub> |
| 行内代码 | `code` | <code> |
| 代码块 | ```lang | <pre><code> |
| 缩进代码块 | code | <pre><code> |
| 引用 | > quote | <blockquote> |
| 无序列表 | - * + item | <ul><li> |
| 有序列表 | 1. item | <ol><li> |
| 嵌套列表 | 多级缩进 | 多级 <ul>/<ol> |
| 任务列表 | - [x] done | <li class="me-task-item"> |
| 水平线 | --- *** ___ | <hr> |
| 表格 | | a | b | | <table> + 列对齐 |
| 链接 v0.1.15 | [**t**](url) | 文本内支持行内格式 |
| 图片 |  | <img> |
| 自动链接 | <https://...> | <a> |
| 数学公式 | $E=mc^2$ | <span> / <div> |
| 脚注 | text[^1] | <sup> + 底部 |
| 定义列表 | Term\n: def | <dl><dt><dd> |
| Emoji | :smile: :rocket: | 😊 🚀(150+) |
| 反斜杠转义 v0.1.15 | \* \_ | 取消标点特殊含义 |
XSS 防护
-所有文本经 HTML 转义。URL 自动过滤 javascript: vbscript: file: 及非图片 data: 协议。
XSS 防护
+所有文本经 escapeHTML 转义,URL 过滤 javascript: / vbscript: / file: / 非图片 data:,提供 sanitize 钩子。
解析器 API v0.1.15 新增
- -parseTokens / renderTokens(解析与渲染分离)
-将 Markdown 解析为中间 Token 数组,可在渲染前变换,再输出 HTML。
+解析器 API
+parseTokens / renderTokens
import { parseTokens, renderTokens } from '@metona-team/metona-editor'; -// 解析为 Token 数组 const { tokens, footnotes } = parseTokens('# Hello\n\n**world**'); -// tokens = [ -// { type: 'heading', level: 1, text: 'Hello' }, -// { type: 'paragraph', text: '**world**' } -// ] - -// 你可以在渲染前变换 token 数组 -tokens.unshift({ type: 'hr' }); - -// 渲染为 HTML +tokens.unshift({ type: 'hr' }); // 渲染前变换 const html = renderTokens(tokens, env, footnotes);-
registerBlockHandler(注册自定义块级语法)
-无需替换整个解析器,按优先级注册自定义块级语法处理器。
+registerBlockHandler
import { registerBlockHandler } from '@metona-team/metona-editor'; - registerBlockHandler({ - name: 'customAlert', - priority: 8.5, // 越小越先尝试 - test: (line) => line.match(/^:::(\w+)/), - parse: (lines, i, match) => { - // 返回 { token: {...}, newIndex } - return { token: { type: 'alert', alertType: match[1] }, newIndex: j }; - }, + name: 'custom', priority: 8.5, + test: (line) => line.match(/^:::/), + parse: (lines, i, match) => ({ token: { type: 'alert' }, newIndex: j }) });- -
TIP:如果自定义 token 的 type 不被 renderTokens 识别,可以在渲染前通过 tokens.map() 转换为已知 type,或自行实现渲染逻辑。
插件约定
- + +插件约定
const myPlugin = { - name: 'myPlugin', - description: '我的自定义插件', - - install(editor, options) { - // this 指向插件对象本身 - this._timer = null; - editor.on('change', this._onChange); - }, - - destroy(editor) { - if (this._timer) clearTimeout(this._timer); - editor.off('change', this._onChange); - }, + name: 'myPlugin', description: '我的插件', + install(editor) { editor.on('change', this._onChange); }, + destroy(editor) { editor.off('change', this._onChange); }, };
预设插件
- -autoSave — 自动保存草稿
-editor.use(presetPlugins.autoSave, { - key: 'me-draft-' + editor.id, // localStorage key - delay: 1000, // 防抖延迟(ms) -}); -editor.restoreDraft(); // 恢复草稿 -editor.clearDraft(); // 清除草稿 -editor.getDraftKey(); // 获取存储 key- -
exportTool — 导出文件
-editor.use(presetPlugins.exportTool); -editor.exportMarkdown('my-doc.md'); -editor.exportHTML('my-doc.html', { - title: '文档标题', - css: 'body { font-family: sans-serif; }', - lang: 'zh-CN', -});- -
searchReplace — 查找替换
-editor.use(presetPlugins.searchReplace); -// Ctrl+F → 查找 Ctrl+H → 替换 -// Enter → 下一个 Shift+Enter → 上一个 Escape → 关闭- -
imagePaste — 粘贴图片转 base64
-editor.use(presetPlugins.imagePaste); -// 在编辑区 Ctrl+V 粘贴剪贴板图片 -// 自动插入 +
预设插件
+autoSave
+editor.use('autoSave', { key: 'draft-key', delay: 1000 }); +editor.restoreDraft(); editor.clearDraft();+
exportTool
+editor.use('exportTool'); +editor.exportMarkdown('doc.md'); editor.exportHTML('doc.html');+
searchReplace
+editor.use('searchReplace'); +// Ctrl+F 查找 / Ctrl+H 替换+
imagePaste
+editor.use('imagePaste'); // 粘贴图片自动转 base64+
shortcutHelp
+editor.use('shortcutHelp'); // 按 ? 弹出快捷键面板+
fileSystem
+editor.use('fileSystem'); +editor.openFile(); editor.saveFile(); editor.saveFileAs();
主题系统
+ +主题系统
+// 实例级 +editor.setTheme('dark'); editor.getTheme(); -// 全局切换主题 -MeEditor.setTheme('light'); // 亮色 -MeEditor.setTheme('dark'); // 暗色 -MeEditor.setTheme('warm'); // 暖色 -MeEditor.setTheme('auto'); // 自动(默认) - -// 实例级主题(v0.1.5,每个编辑器独立) -editor.setTheme('dark'); -editor.getTheme(); // 'dark' -editor.on('themeChange', ({ theme }) => {}); - -// 注册自定义主题(v0.1.5: 支持继承) +// 注册自定义 import { themeUtils } from '@metona-team/metona-editor'; -themeUtils.registerTheme('ocean', { - extends: 'dark', // 继承 dark 主题 - accent: '#00ddff', -}); +themeUtils.registerTheme('ocean', { extends: 'dark', accent: '#00ddff' }); -// 外部主题跟随(v0.1.5) -themeUtils.followExternalTheme( - { element: document.body, attr: 'data-theme' }, - (theme) => editor.setTheme(theme) -); - -// 从父容器继承主题 -themeUtils.adoptFromParent(editor.container, - (theme) => editor.setTheme(theme) -); +// 外部跟随 +editor.getThemeContext().adopt(); // 从父容器继承 // CSS 变量导出 -themeUtils.exportCSSVars(); // { '--md-bg': '#fff', ... } -themeUtils.getCSSVariable('accent'); // '#3b82f6'- -CSS 变量限定在
+themeUtils.exportCSSVars(); themeUtils.getCSSVariable('accent');.me-wrapper元素上,不污染全局。支持@media print打印样式。v0.1.5 新增实例级主题隔离、外部跟随、主题继承。
国际化
+ +国际化
+// 切换 +editor.setLocale('en-US'); MeEditor.setLocale('zh-CN'); -// 切换语言 -MeEditor.setLocale('en-US'); -MeEditor.setLocale('zh-CN'); - -// 翻译函数 +// 添加翻译 import { i18nUtils } from '@metona-team/metona-editor'; -i18nUtils.t('bold'); // => '粗体'(zh-CN) -i18nUtils.t('bold'); // => 'Bold'(en-US) +i18nUtils.addTranslations('ja', { bold: '太字' }); -// 添加语言 -i18nUtils.addTranslations('ja', { bold: '太字', italic: '斜体' }); +// 远程加载 +await i18nUtils.loadRemote('/locales/ja.json', 'ja'); // 格式化 -i18nUtils.formatNumber(1234567); // => '1,234,567' -i18nUtils.formatCurrency(99.99, 'USD'); // => '$99.99' -i18nUtils.formatDate('2024-01-15'); // => '1/15/2024'+i18nUtils.formatNumber(1234567); +i18nUtils.formatDate('2024-01-15');