thzxx 70eb71623f docs: update README for v0.1.3 — features, API, syntax, tests
- Feature list: add readOnly, imagePaste, emoji, render cache, print styles
- Test badge: 407 → 425
- Config: add readOnly, historyDebounce options
- API: add refresh(), setReadOnly(), isReadOnly()
- getStatus: add readOnly field
- Events: add beforeRender, afterRender instance events
- Plugins: add imagePaste preset documentation
- Syntax table: add highlight, superscript, subscript, emoji, table alignment
- Test coverage: update file descriptions and total count
2026-07-23 21:03:02 +08:00
2026-07-23 16:23:07 +08:00
2026-07-23 16:23:07 +08:00
2026-07-23 16:23:07 +08:00
2026-07-23 16:23:07 +08:00

MetonaEditor

轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。

npm version license tests coverage


特性

  • 零依赖 — 单文件打包,无任何运行时依赖,UMD/ESM/CJS 三种格式
  • 现代化 — 原生 ES2020+ 实现,CSS 变量主题系统,响应式布局,无障碍支持
  • 易扩展 — 完整插件系统(install/destroy 生命周期),自定义工具栏按钮,预设插件开箱即用
  • 易维护 — 模块化源码,JSDoc 注释完整,TypeScript 类型声明,425 个单元测试覆盖
  • 易使用 — 工厂函数 create() 一行接入,链式 API,中文优先文档与翻译
  • 安全 — 内置 XSS 防护(javascript:/vbscript:/file: 协议过滤),HTML 转义,safeUrl 净化
  • 内置解析器 — 自研 CommonMark 子集 + GFM 扩展(表格含列对齐、任务列表、emoji 短码、上下标),可整体替换
  • 三模式视图 — edit / split / preview 自由切换,分屏拖拽调整比例,滚动同步,模式切换保持滚动位置
  • 主题系统 — light / dark / auto / warm 四套预设,CSS 变量定制,跟随系统主题,@media print 打印样式
  • 国际化 — zh-CN / en-US 完整翻译,RTL 支持,Intl 数字/货币/日期格式化
  • 历史栈 — 撤销/重做,防抖合并(可配置延迟),可配置上限
  • 只读模式readOnly 配置 / setReadOnly() API,适用于预览/展示场景
  • 预设插件 — autoSave(草稿)、exportTool(导出 .md/.html)、searchReplaceCtrl+F 查找替换)、imagePaste(粘贴图片转 base64
  • 性能优化 — 渲染缓存跳过重复解析,refresh() 强制刷新,模式切换无冗余渲染

目录


快速开始

安装

npm install @metona-team/metona-editor

浏览器直接引入(UMD

样式由 JS 自动注入(styles.js 运行时动态创建 <style> 标签),无需单独引入 CSS 文件。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
  <div id="editor"></div>

  <!-- CDN UMD 开发版 -->
  <script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>

  <!-- CDN 压缩版(生产环境推荐) -->
  <!-- <script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.min.js"></script> -->

  <script>
    const editor = MeEditor.create('#editor', {
      value: '# Hello World',
      mode: 'split',
    });
  </script>
</body>
</html>

CDN ES Module

<script type="module">
  import MeEditor from 'https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.esm.js';

  const editor = MeEditor.create('#editor', {
    value: '# Hello World',
    mode: 'split',
  });
</script>

ES Module

import MeEditor from '@metona-team/metona-editor';

const editor = MeEditor.create('#editor', {
  value: '# Hello World',
  mode: 'split',
  onChange: (value, editor) => {
    console.log('内容变化:', value);
  },
});

直接使用类

import { MarkdownEditor } from '@metona-team/metona-editor';

const editor = new MarkdownEditor('#editor', {
  value: '# Hello World',
});

CommonJS

const MeEditor = require('@metona-team/metona-editor');
const editor = MeEditor.create('#editor', {});

配置项

所有配置项均为可选,以下是默认值:

MeEditor.create(container, {
  // 内容
  value: '',              // 初始 Markdown 文本
  placeholder: '',        // 占位符

  // 视图
  mode: 'split',          // 'edit' | 'split' | 'preview'
  height: 400,            // 数字为 px,字符串原样使用
  toolbar: DEFAULT_TOOLBAR, // 工具栏配置,false 隐藏
  wordCount: true,        // 字数统计状态栏

  // 行为
  autofocus: false,       // 自动聚焦
  spellcheck: false,      // 拼写检查
  readOnly: false,        // 只读模式
  historyLimit: 100,      // 历史栈上限
  historyDebounce: 400,   // 历史栈防抖延迟(ms
  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) => safeHtmlHTML 净化钩子

  // 外观
  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) => voidCtrl+S
  onModeChange: null,     // (mode, editor) => void
  onFullscreen: null,     // (fullscreen, editor) => void
  onCreate: null,         // (editor) => void
  onDestroy: null,        // (editor) => void
});

工具栏配置

// 使用默认工具栏
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

内容操作

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.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');        // 链式调用

历史栈

editor.undo();                        // 撤销
editor.redo();                        // 重做
editor.canUndo();                     // 是否可撤销
editor.canRedo();                     // 是否可重做

模式与全屏

editor.setMode('split');              // 设置模式:edit / split / preview
editor.getMode();                     // 获取当前模式
editor.toggleFullscreen();            // 切换全屏
editor.exitFullscreen();              // 退出全屏
editor.isFullscreen();                // 是否全屏

统计与状态

editor.getStats();
// 返回:{ characters, words, chineseChars, englishWords, lines, readingTime }

editor.getStatus();
// 返回:{ id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }

事件

const unsub = editor.on('change', (value, editor) => {});
editor.off('change', handler);
unsub();                              // 取消监听(等价于 off

插件

editor.use(plugin, options);          // 安装插件
editor.getPlugins();                  // 获取已安装插件列表
editor.addToolbarButton(config);      // 追加工具栏按钮

启用/禁用/只读

editor.enable();                      // 启用
editor.disable();                     // 禁用
editor.isDisabled();                  // 是否禁用
editor.setReadOnly(true);             // 设置为只读
editor.isReadOnly();                  // 是否只读

销毁

editor.destroy();                     // 销毁实例,清理 DOM 与事件
editor.isDestroyed();                 // 是否已销毁

静态 API(顶层)

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();

内置解析器(可单独使用)

import { parseMarkdown, safeUrl, slugify } from '@metona-team/metona-editor';

parseMarkdown('# Hello');             // => '<h1 id="hello">Hello</h1>'
parseMarkdown('**bold**');            // => '<p><strong>bold</strong></p>'

safeUrl('javascript:alert(1)');       // => ''(过滤危险协议)
safeUrl('https://example.com');       // => 'https://example.com'

slugify('Hello World');               // => 'hello-world'
slugify('你好世界');                   // => '你好世界'

事件系统

MetonaEditor 提供两层事件系统:

实例事件

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 })
beforeRender 渲染前(实例级) (editor)
afterRender 渲染后(实例级) (editor)

静态钩子(全局)

MarkdownEditor.on('beforeCreate', (editor) => {
  console.log('实例即将创建');
});

MarkdownEditor.on('afterDestroy', (editor) => {
  console.log('实例已销毁');
});

可用钩子

钩子 触发时机
beforeCreate 构造函数初始化前
afterCreate 构造函数初始化完成
beforeRender 每次渲染前
afterRender 每次渲染后
beforeDestroy destroy 前
afterDestroy destroy 后

插件系统

插件约定

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);
    }
  },
};

使用插件

// 实例级安装
editor.use(myPlugin, { option1: 'value' });

// 全局默认插件(所有新实例自动安装)
MeEditor.use(myPlugin);
MeEditor.use(presetPlugins.autoSave, { delay: 2000 });

预设插件

autoSave — 自动保存草稿

editor.use(presetPlugins.autoSave, {
  key: 'me-draft-' + editor.id,  // localStorage key
  delay: 1000,                    // 防抖延迟(ms
});

// 暴露的 API
editor.getDraftKey();              // 获取存储 key
editor.restoreDraft();             // 从 localStorage 恢复
editor.clearDraft();               // 清除草稿

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 / Cmd+F — 打开查找面板
// Ctrl+H / Cmd+H — 打开查找替换面板
// Enter — 下一个
// Shift+Enter — 上一个
// Escape — 关闭

imagePaste — 粘贴图片转 base64

editor.use(presetPlugins.imagePaste);

// 在编辑区 Ctrl+V 粘贴剪贴板中的图片
// 自动生成 ![](data:image/png;base64,...) 并插入

自定义工具栏按钮

editor.addToolbarButton({
  name: 'timestamp',
  title: '插入时间戳',
  icon: '<svg>...</svg>',           // SVG 字符串
  action: () => {
    editor.insert(`\n${new Date().toISOString()}\n`);
  },
  className: 'my-btn',              // 可选
});

PluginManager 与 pluginUtils

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, imagePaste }

主题系统

内置主题

MeEditor.setTheme('light');         // 亮色
MeEditor.setTheme('dark');          // 暗色
MeEditor.setTheme('warm');          // 暖色
MeEditor.setTheme('auto');          // 跟随系统(默认)

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;
}

注册自定义主题

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'); // 移除(内置主题不可移除)

主题监听

const unsub = themeUtils.addThemeListener((theme, resolved) => {
  console.log('主题切换:', theme, '=>', resolved);
});
unsub();                             // 取消监听

持久化

主题会自动保存到 localStoragekey 为 metona-editor-themeinitTheme() 在库加载时自动调用。


国际化

切换语言

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.t('greeting', { name: '世界' });
// 翻译值 '你好,{name}' => '你好,世界!'

添加自定义语言

i18nUtils.addTranslations('ja', {
  bold: '太字',
  italic: '斜体',
  // ...其他翻译
});

MeEditor.setLocale('ja');

格式化

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%'

语言元信息

i18nUtils.getSupportedLocales();     // => ['zh-CN', 'en-US', ...]
i18nUtils.isLocaleSupported('zh-CN');// => true
i18nUtils.getLocaleName('zh-CN');    // => '简体中文'
i18nUtils.getLocaleDirection('ar');  // => 'rtl'

语言监听

const unsub = i18nUtils.addLocaleListener((locale) => {
  console.log('语言切换:', locale);
});
unsub();

语言会自动保存到 localStoragekey 为 metona-editor-locale


Markdown 解析器

MetonaEditor 内置自研轻量解析器,支持 CommonMark 子集 + GFM 扩展:

支持的语法

语法 示例 输出
标题 # H1 ## H2 <h1> ~ <h6>
段落 纯文本 <p>
粗体 **bold** __bold__ <strong>
斜体 *italic* _italic_ <em>
删除线 ~~text~~ <del>
高亮标记 ==text== <mark>
上标 x^2^ <sup>
下标 H~2~O <sub>
行内代码 `code` <code>
代码块 ```lang <pre><code class="language-lang">
引用 > quote <blockquote>
无序列表 - item / * item / + item <ul><li>
有序列表 1. item <ol><li>
任务列表 - [x] done <li class="me-task-item">
水平线 --- *** ___ <hr>
表格 | a | b | <table>(含 :---: 列对齐)
链接 [text](url) <a>
图片 ![alt](url) <img>
自动链接 <https://...> <a>
Emoji 短码 :smile: :rocket: 😊 🚀80+ 常用)

XSS 防护

parseMarkdown('[click](javascript:alert(1))');
// => '<p><a href="">click</a></p>'(危险协议被过滤)

parseMarkdown('<script>alert(1)</script>');
// => '<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>'HTML 转义)

过滤的协议:javascript: vbscript: file: data:(非 image

代码高亮钩子

MeEditor.create(container, {
  highlight: (code, lang) => {
    // 返回高亮后的 HTML,例如集成 Prism.js / highlight.js
    return Prism.highlight(code, Prism.languages[lang], lang);
  },
});

未提供 highlight 时,代码块以纯文本输出。highlight 抛错时自动回退为转义输出。

替换整个解析器

import { marked } from 'marked';

MeEditor.create(container, {
  render: (md, env) => {
    // 完全替换内置解析器
    return marked.parse(md);
  },
});

HTML 净化钩子

import DOMPurify from 'dompurify';

MeEditor.create(container, {
  sanitize: (html) => DOMPurify.sanitize(html),
});

TypeScript 支持

库自带完整的 TypeScript 类型声明:

import MeEditor, { MarkdownEditor, MarkdownEditorOptions } from '@metona-team/metona-editor';

const editor: MarkdownEditor = MeEditor.create<HTMLElement>(
  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 方法、插件接口、事件回调、常量。


测试

# 运行全部测试
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 99.6% 96.7% 98.5%
core.js 99.5% 96.6% 98.2%
总体 96.9% 80.3% 94.6%

测试文件位于 tests/ 目录,共 425 个测试用例,覆盖:

  • parser.test.js — Markdown 解析器全部语法(含新增上下标/高亮/emoji/表格对齐) + XSS 防护
  • utils.test.js — 工具函数(generateId / escapeHTML / debounce / throttle / deepMerge 等)
  • core.test.js — MarkdownEditor 构造、内容 API、exec 命令(含自定义动作)、历史栈、模式、事件、插件、销毁、readOnly
  • plugins.test.js — 预设插件(autoSave / exportTool / searchReplace / imagePaste+ PluginManager + pluginUtils
  • animations.test.js — 动画注册与查询
  • themes.test.js — 主题切换、持久化、监听、自定义注册
  • i18n.test.js — 翻译、语言切换、格式化、监听

构建

# 安装依赖
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 © MetonaTeam

S
Description
轻量、零依赖、精致美观的 Markdown Editor 库
https://editor.metona.cn
Readme MIT
586 KiB
Languages
JavaScript 72.4%
HTML 26.9%
Shell 0.7%