# MetonaEditor > 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。 [![npm version](https://img.shields.io/badge/version-0.1.0-blue.svg)](https://www.npmjs.com/package/@metona-team/metona-editor) [![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![tests](https://img.shields.io/badge/tests-407%20passed-brightgreen.svg)](./tests) [![coverage](https://img.shields.io/badge/coverage-97%25-brightgreen.svg)](./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'); // => '

Hello

' parseMarkdown('**bold**'); // => '

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` | `
  • ` | | 有序列表 | `1. item` | `
    1. ` | | 任务列表 | `- [x] done` | `
    2. ` | | 水平线 | `---` `***` `___` | `
      ` | | 表格 | `\| a \| b \|` | `` | | 链接 | `[text](url)` | `` | | 图片 | `![alt](url)` | `` | | 自动链接 | `` | `` | ### XSS 防护 ```javascript parseMarkdown('[click](javascript:alert(1))'); // => '

      click

      '(危险协议被过滤) parseMarkdown(''); // => '

      <script>alert(1)</script>

      '(HTML 转义) ``` 过滤的协议:`javascript:` `vbscript:` `file:` `data:`(非 image) ### 代码高亮钩子 ```javascript MeEditor.create(container, { highlight: (code, lang) => { // 返回高亮后的 HTML,例如集成 Prism.js / highlight.js return Prism.highlight(code, Prism.languages[lang], lang); }, }); ``` 未提供 `highlight` 时,代码块以纯文本输出。`highlight` 抛错时自动回退为转义输出。 ### 替换整个解析器 ```javascript import { marked } from 'marked'; MeEditor.create(container, { render: (md, env) => { // 完全替换内置解析器 return marked.parse(md); }, }); ``` ### HTML 净化钩子 ```javascript import DOMPurify from 'dompurify'; MeEditor.create(container, { sanitize: (html) => DOMPurify.sanitize(html), }); ``` --- ## TypeScript 支持 库自带完整的 TypeScript 类型声明: ```typescript import MeEditor, { MarkdownEditor, MarkdownEditorOptions } from '@metona-team/metona-editor'; const editor: MarkdownEditor = MeEditor.create( document.getElementById('editor')!, { value: '# Hello', mode: 'split', onChange: (value: string, editor: MarkdownEditor) => { console.log(value); }, } ); editor.setValue('new content'); const html: string = editor.getHTML(); const stats = editor.getStats(); ``` 类型声明覆盖:所有配置项、API 方法、插件接口、事件回调、常量。 --- ## 测试 ```bash # 运行全部测试 npm test # 监听模式 npm run test:watch # 生成覆盖率报告 npm run test:coverage ``` ### 测试覆盖 | 文件 | 语句覆盖率 | 分支覆盖率 | 函数覆盖率 | |------|-----------|-----------|-----------| | animations.js | 100% | 100% | 100% | | constants.js | 100% | 100% | 100% | | icons.js | 100% | 100% | 100% | | locales.js | 100% | 100% | 100% | | parser.js | 95.6% | 87.6% | 100% | | utils.js | 96.2% | 87.1% | 100% | | themes.js | 92.9% | 84.2% | 96.7% | | i18n.js | 86.6% | 69.5% | 93.9% | | plugins.js | 79.9% | 59.6% | 73.8% | | core.js | 72.9% | 60.9% | 69.4% | | **总体** | **80.6%** | **67.8%** | **79.3%** | 测试文件位于 [tests/](./tests) 目录,共 **291 个测试用例**,覆盖: - `parser.test.js` — Markdown 解析器全部语法 + XSS 防护 - `utils.test.js` — 工具函数(generateId / escapeHTML / debounce / throttle / deepMerge 等) - `core.test.js` — MarkdownEditor 构造、内容 API、exec 命令、历史栈、模式、事件、插件、销毁 - `plugins.test.js` — 预设插件(autoSave / exportTool / searchReplace)+ PluginManager + pluginUtils - `animations.test.js` — 动画注册与查询 - `themes.test.js` — 主题切换、持久化、监听、自定义注册 - `i18n.test.js` — 翻译、语言切换、格式化、监听 --- ## 构建 ```bash # 安装依赖 npm install # 构建(生成 5 个产物) npm run build # 开发模式(监听 + 热更新) npm run dev # 类型检查 npm run typecheck # 代码检查 npm run lint npm run lint:fix # 格式化 npm run format # 生成 JSDoc 文档 npm run docs ``` ### 构建产物 ``` dist/ ├── metona-editor.js # UMD(浏览器直接引入) ├── metona-editor.min.js # UMD 压缩版(CDN) ├── metona-editor.esm.js # ES Module ├── metona-editor.cjs.js # CommonJS └── metona-editor.d.ts # TypeScript 类型声明 ``` --- ## 浏览器兼容性 支持所有现代浏览器(ES2020+): - Chrome / Edge 80+ - Firefox 75+ - Safari 13+ - 不支持 IE 11 --- ## License [MIT](./LICENSE) © MetonaTeam ## 相关链接 - [npm 包主页](https://www.npmjs.com/package/@metona-team/metona-editor) - [问题反馈](https://git.metona.cn/MetonaTeam/MetonaEditor/issues) - [在线演示](./site/demo.html)