# MetonaEditor > 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。 [](https://www.npmjs.com/package/@metona-team/metona-editor) [](./LICENSE) [](./tests) [](./tests) --- ## 特性 - **零依赖** — 单文件打包,无任何运行时依赖,UMD/ESM/CJS 三种格式 - **现代化** — 原生 ES2020+ 实现,CSS 变量主题系统,响应式布局,无障碍支持 - **易扩展** — 完整插件系统(install/destroy 生命周期),自定义工具栏按钮,预设插件开箱即用 - **易维护** — 模块化源码,JSDoc 注释完整,TypeScript 类型声明,291 个单元测试覆盖 - **易使用** — 工厂函数 `create()` 一行接入,链式 API,中文优先文档与翻译 - **安全** — 内置 XSS 防护(`javascript:`/`vbscript:`/`file:` 协议过滤),HTML 转义,`safeUrl` 净化 - **内置解析器** — 自研 CommonMark 子集 + GFM 扩展(表格、任务列表、删除线、自动链接),可整体替换 - **三模式视图** — edit / split / preview 自由切换,分屏模式支持拖拽调整比例 - **主题系统** — light / dark / auto / warm 四套预设,CSS 变量定制,跟随系统主题 - **国际化** — zh-CN / en-US 完整翻译,RTL 支持,`Intl` 数字/货币/日期格式化 - **历史栈** — 撤销/重做,400ms 防抖合并,可配置上限 - **预设插件** — autoSave(草稿)、exportTool(导出 .md/.html)、searchReplace(Ctrl+F 查找替换) --- ## 目录 - [快速开始](#快速开始) - [配置项](#配置项) - [API 参考](#api-参考) - [事件系统](#事件系统) - [插件系统](#插件系统) - [主题系统](#主题系统) - [国际化](#国际化) - [Markdown 解析器](#markdown-解析器) - [TypeScript 支持](#typescript-支持) - [测试](#测试) - [构建](#构建) - [浏览器兼容性](#浏览器兼容性) - [License](#license) --- ## 快速开始 ### 安装 ```bash npm install @metona-team/metona-editor ``` ### 浏览器直接引入(UMD) ```html
``` ### ES Module ```javascript import MeEditor from '@metona-team/metona-editor'; const editor = MeEditor.create('#editor', { value: '# Hello World', mode: 'split', onChange: (value, editor) => { console.log('内容变化:', value); }, }); ``` ### 直接使用类 ```javascript import { MarkdownEditor } from '@metona-team/metona-editor'; const editor = new MarkdownEditor('#editor', { value: '# Hello World', }); ``` ### CommonJS ```javascript const MeEditor = require('@metona-team/metona-editor'); const editor = MeEditor.create('#editor', {}); ``` --- ## 配置项 所有配置项均为可选,以下是默认值: ```javascript MeEditor.create(container, { // 内容 value: '', // 初始 Markdown 文本 placeholder: '', // 占位符 // 视图 mode: 'split', // 'edit' | 'split' | 'preview' height: 400, // 数字为 px,字符串原样使用 toolbar: DEFAULT_TOOLBAR, // 工具栏配置,false 隐藏 wordCount: true, // 字数统计状态栏 // 行为 autofocus: false, // 自动聚焦 spellcheck: false, // 拼写检查 historyLimit: 100, // 历史栈上限 syncScroll: true, // 分屏模式同步滚动 tabSize: 2, // 制表符空格数(0 表示 \t) // 主题与国际化 theme: 'auto', // 'light' | 'dark' | 'auto' | 'warm' | 自定义 locale: 'zh-CN', // 'zh-CN' | 'en-US' | 自定义 // 自定义渲染 render: null, // (md, env) => html,覆盖内置解析器 highlight: null, // (code, lang) => html,代码高亮钩子 sanitize: null, // (html) => safeHtml,HTML 净化钩子 // 外观 className: '', // 容器额外 class style: {}, // 内联样式 // 插件 plugins: [], // 实例级插件数组 // 生命周期回调 onChange: null, // (value, editor) => void onInput: null, // (value, editor) => void onFocus: null, // (editor) => void onBlur: null, // (editor) => void onSave: null, // (editor) => void(Ctrl+S) onModeChange: null, // (mode, editor) => void onFullscreen: null, // (fullscreen, editor) => void onCreate: null, // (editor) => void onDestroy: null, // (editor) => void }); ``` ### 工具栏配置 ```javascript // 使用默认工具栏 MeEditor.create(container, { toolbar: true }); // 隐藏工具栏 MeEditor.create(container, { toolbar: false }); // 自定义工具栏('|' 为分隔符) MeEditor.create(container, { toolbar: ['bold', 'italic', '|', 'h1', 'h2', '|', 'undo', 'redo'], }); // 可用动作 // bold italic strikethrough underline code // h1 h2 h3 quote ul ol indent outdent hr // link image table // undo redo // edit split preview fullscreen ``` --- ## API 参考 ### 实例 API #### 内容操作 ```javascript editor.getValue(); // 获取 Markdown 文本 editor.setValue(md, { silent }); // 设置内容,silent 不触发 change editor.getHTML(); // 获取渲染后的 HTML editor.insert(text, { replace }); // 在光标处插入文本 editor.wrap(before, after); // 包裹选区 editor.focus(); // 聚焦编辑器 editor.blur(); // 失焦 ``` #### 命令执行 ```javascript editor.exec(action, ...args); // 执行命令,返回 this 支持链式 // 支持的 action: // bold italic strikethrough underline code // h1 h2 h3 quote ul ol indent outdent hr // link image table // undo redo // edit split preview fullscreen editor.exec('bold').exec('h1'); // 链式调用 ``` #### 历史栈 ```javascript editor.undo(); // 撤销 editor.redo(); // 重做 editor.canUndo(); // 是否可撤销 editor.canRedo(); // 是否可重做 ``` #### 模式与全屏 ```javascript editor.setMode('split'); // 设置模式:edit / split / preview editor.getMode(); // 获取当前模式 editor.toggleFullscreen(); // 切换全屏 editor.exitFullscreen(); // 退出全屏 editor.isFullscreen(); // 是否全屏 ``` #### 统计与状态 ```javascript editor.getStats(); // 返回:{ characters, words, chineseChars, englishWords, lines, readingTime } editor.getStatus(); // 返回:{ id, mode, theme, locale, fullscreen, disabled, destroyed, plugins } ``` #### 事件 ```javascript const unsub = editor.on('change', (value, editor) => {}); editor.off('change', handler); unsub(); // 取消监听(等价于 off) ``` #### 插件 ```javascript editor.use(plugin, options); // 安装插件 editor.getPlugins(); // 获取已安装插件列表 editor.addToolbarButton(config); // 追加工具栏按钮 ``` #### 启用/禁用 ```javascript editor.enable(); // 启用 editor.disable(); // 禁用 editor.isDisabled(); // 是否禁用 ``` #### 销毁 ```javascript editor.destroy(); // 销毁实例,清理 DOM 与事件 editor.isDestroyed(); // 是否已销毁 ``` ### 静态 API(顶层) ```javascript import MeEditor from '@metona-team/metona-editor'; // 工厂函数(推荐入口) MeEditor.create(container, options); // 全局默认插件(应用于所有后续创建的实例) MeEditor.use(presetPlugins.autoSave, { delay: 2000 }); MeEditor.use('searchReplace'); // 字符串形式引用预设 MeEditor.use(customPlugin); // 自定义插件对象 // 全局事件钩子(所有实例共享) MeEditor.on('beforeCreate', (editor) => {}); MeEditor.off('beforeCreate', handler); // 全局主题与国际化 MeEditor.setTheme('dark'); MeEditor.setLocale('en-US'); // 状态查询 MeEditor.getStatus(); // 返回:{ version, theme, locale, globalPlugins, presetPlugins } // 销毁所有全局资源(不销毁实例) MeEditor.destroy(); ``` ### 内置解析器(可单独使用) ```javascript import { parseMarkdown, safeUrl, slugify } from '@metona-team/metona-editor'; parseMarkdown('# Hello'); // => 'bold
' safeUrl('javascript:alert(1)'); // => ''(过滤危险协议) safeUrl('https://example.com'); // => 'https://example.com' slugify('Hello World'); // => 'hello-world' slugify('你好世界'); // => '你好世界' ``` --- ## 事件系统 MetonaEditor 提供两层事件系统: ### 实例事件 ```javascript editor.on('change', (value, editor) => { console.log('内容变化'); }); editor.on('modeChange', (mode, editor) => { console.log('模式切换:', mode); }); editor.on('destroy', () => { console.log('编辑器已销毁'); }); ``` **可用事件**: | 事件 | 触发时机 | 回调参数 | |------|---------|---------| | `input` | textarea 原生 input | `(value, editor)` | | `change` | 内容变化(input/setValue/exec) | `(value, editor)` | | `focus` | 聚焦 | `(editor)` | | `blur` | 失焦 | `(editor)` | | `save` | Ctrl+S | `(editor)` | | `modeChange` | 模式切换 | `(mode, editor)` | | `fullscreen` | 全屏切换 | `(fullscreen, editor)` | | `destroy` | 销毁 | `()` | | `autosave` | autoSave 插件触发 | `({ key, value })` | ### 静态钩子(全局) ```javascript MarkdownEditor.on('beforeCreate', (editor) => { console.log('实例即将创建'); }); MarkdownEditor.on('afterDestroy', (editor) => { console.log('实例已销毁'); }); ``` **可用钩子**: | 钩子 | 触发时机 | |------|---------| | `beforeCreate` | 构造函数初始化前 | | `afterCreate` | 构造函数初始化完成 | | `beforeRender` | 每次渲染前 | | `afterRender` | 每次渲染后 | | `beforeDestroy` | destroy 前 | | `afterDestroy` | destroy 后 | --- ## 插件系统 ### 插件约定 ```javascript const myPlugin = { name: 'myPlugin', description: '我的自定义插件', // 安装时调用,this 指向插件对象本身 install(editor, options) { // 在 this 上存放运行时状态 this._timer = null; // 暴露实例方法 editor.doSomething = () => { console.log(editor.getValue()); }; // 监听事件 editor.on('change', this._onChange); }, // 卸载时调用,清理事件与 DOM destroy(editor) { if (this._timer) clearTimeout(this._timer); if (editor && typeof editor.off === 'function') { editor.off('change', this._onChange); } }, }; ``` ### 使用插件 ```javascript // 实例级安装 editor.use(myPlugin, { option1: 'value' }); // 全局默认插件(所有新实例自动安装) MeEditor.use(myPlugin); MeEditor.use(presetPlugins.autoSave, { delay: 2000 }); ``` ### 预设插件 #### autoSave — 自动保存草稿 ```javascript editor.use(presetPlugins.autoSave, { key: 'me-draft-' + editor.id, // localStorage key delay: 1000, // 防抖延迟(ms) }); // 暴露的 API editor.getDraftKey(); // 获取存储 key editor.restoreDraft(); // 从 localStorage 恢复 editor.clearDraft(); // 清除草稿 ``` #### exportTool — 导出文件 ```javascript 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 — 查找替换 ```javascript editor.use(presetPlugins.searchReplace); // 快捷键 // Ctrl+F / Cmd+F — 打开查找面板 // Ctrl+H / Cmd+H — 打开查找替换面板 // Enter — 下一个 // Shift+Enter — 上一个 // Escape — 关闭 ``` ### 自定义工具栏按钮 ```javascript editor.addToolbarButton({ name: 'timestamp', title: '插入时间戳', icon: '', // SVG 字符串 action: () => { editor.insert(`\n${new Date().toISOString()}\n`); }, className: 'my-btn', // 可选 }); ``` ### PluginManager 与 pluginUtils ```javascript import { pluginUtils, PluginManager } from '@metona-team/metona-editor'; // 创建独立管理器 const pm = new PluginManager(); pm.register('my', myPlugin); pm.has('my'); // true pm.get('my'); // myPlugin pm.enable('my'); pm.disable('my'); pm.unregister('my'); pm.destroy(); // pluginUtils 工具集 pluginUtils.createPlugin({ name: 'x', install: () => {} }); pluginUtils.validatePlugin(plugin); // => { valid: boolean, errors: string[] } pluginUtils.getPreset('autoSave'); // => 副本 pluginUtils.getAllPresets(); // => { autoSave, exportTool, searchReplace } ``` --- ## 主题系统 ### 内置主题 ```javascript MeEditor.setTheme('light'); // 亮色 MeEditor.setTheme('dark'); // 暗色 MeEditor.setTheme('warm'); // 暖色 MeEditor.setTheme('auto'); // 跟随系统(默认) ``` ### CSS 变量定制 ```css :root { --md-bg: #ffffff; --md-text: #1f2937; --md-border: rgba(0, 0, 0, 0.08); --md-shadow: 0 10px 36px -10px rgba(0,0,0,0.18); --md-toolbar-bg: rgba(248, 249, 250, 0.92); --md-textarea-bg: #ffffff; --md-preview-bg: #ffffff; --md-code-bg: rgba(243, 244, 246, 1); --md-code-text: #1f2937; --md-accent: #3b82f6; --md-muted: #6b7280; --md-radius: 10px; --md-font: -apple-system, "Segoe UI", "PingFang SC", sans-serif; --md-mono: "SF Mono", "Consolas", monospace; } ``` ### 注册自定义主题 ```javascript import { themeUtils } from '@metona-team/metona-editor'; themeUtils.registerTheme('ocean', { bg: '#001122', text: '#aabbcc', border: '#003344', accent: '#00ddff', // 未提供字段从 light 主题继承 }); themeUtils.applyTheme('ocean'); themeUtils.hasTheme('ocean'); // true themeUtils.unregisterTheme('ocean'); // 移除(内置主题不可移除) ``` ### 主题监听 ```javascript const unsub = themeUtils.addThemeListener((theme, resolved) => { console.log('主题切换:', theme, '=>', resolved); }); unsub(); // 取消监听 ``` ### 持久化 主题会自动保存到 `localStorage`,key 为 `metona-editor-theme`。`initTheme()` 在库加载时自动调用。 --- ## 国际化 ### 切换语言 ```javascript MeEditor.setLocale('en-US'); MeEditor.setLocale('zh-CN'); ``` ### 翻译函数 ```javascript import { i18nUtils } from '@metona-team/metona-editor'; i18nUtils.t('bold'); // => '粗体'(zh-CN) i18nUtils.t('bold'); // => 'Bold'(en-US) // 插值 i18nUtils.t('greeting', { name: '世界' }); // 翻译值 '你好,{name}!' => '你好,世界!' ``` ### 添加自定义语言 ```javascript i18nUtils.addTranslations('ja', { bold: '太字', italic: '斜体', // ...其他翻译 }); MeEditor.setLocale('ja'); ``` ### 格式化 ```javascript i18nUtils.formatNumber(1234567); // => '1,234,567' i18nUtils.formatCurrency(99.99, 'USD'); // => '$99.99' i18nUtils.formatDate('2024-01-15'); // => '1/15/2024' i18nUtils.formatNumber(0.5, { style: 'percent' }); // => '50%' ``` ### 语言元信息 ```javascript i18nUtils.getSupportedLocales(); // => ['zh-CN', 'en-US', ...] i18nUtils.isLocaleSupported('zh-CN');// => true i18nUtils.getLocaleName('zh-CN'); // => '简体中文' i18nUtils.getLocaleDirection('ar'); // => 'rtl' ``` ### 语言监听 ```javascript const unsub = i18nUtils.addLocaleListener((locale) => { console.log('语言切换:', locale); }); unsub(); ``` 语言会自动保存到 `localStorage`,key 为 `metona-editor-locale`。 --- ## Markdown 解析器 MetonaEditor 内置自研轻量解析器,支持 CommonMark 子集 + GFM 扩展: ### 支持的语法 | 语法 | 示例 | 输出 | |------|------|------| | 标题 | `# H1` `## H2` | `` |
| 粗体 | `**bold**` `__bold__` | `` |
| 斜体 | `*italic*` `_italic_` | `` |
| 删除线 | `~~text~~` | `` |
| 行内代码 | `` `code` `` | `` |
| 代码块 | ` ```lang ` | `` |
| 引用 | `> quote` | `` |
| 无序列表 | `- item` / `* item` / `+ item` | `
` |
| 表格 | `\| a \| b \|` | `