@metona-team/metona-editor (0.1.1)
Installation
@metona-team:registry=https://git.metona.cn/api/packages/MetonaTeam/npm/npm install @metona-team/metona-editor@0.1.1"@metona-team/metona-editor": "0.1.1"About this package
轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。
MetonaEditor
轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
特性
- 零依赖 — 单文件打包,无任何运行时依赖,UMD/ESM/CJS 三种格式
- 现代化 — 原生 ES2020+ 实现,CSS 变量主题系统,响应式布局,无障碍支持
- 易扩展 — 完整插件系统(install/destroy 生命周期),自定义工具栏按钮,预设插件开箱即用
- 易维护 — 模块化源码,JSDoc 注释完整,TypeScript 类型声明,407 个单元测试覆盖
- 易使用 — 工厂函数
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 查找替换)
目录
快速开始
安装
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, // 拼写检查
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
});
工具栏配置
// 使用默认工具栏
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.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, 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.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 }) |
静态钩子(全局)
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 — 关闭
自定义工具栏按钮
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 }
主题系统
内置主题
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(); // 取消监听
持久化
主题会自动保存到 localStorage,key 为 metona-editor-theme。initTheme() 在库加载时自动调用。
国际化
切换语言
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();
语言会自动保存到 localStorage,key 为 metona-editor-locale。
Markdown 解析器
MetonaEditor 内置自研轻量解析器,支持 CommonMark 子集 + GFM 扩展:
支持的语法
| 语法 | 示例 | 输出 |
|---|---|---|
| 标题 | # H1 ## H2 |
<h1> ~ <h6> |
| 段落 | 纯文本 | <p> |
| 粗体 | **bold** __bold__ |
<strong> |
| 斜体 | *italic* _italic_ |
<em> |
| 删除线 | ~~text~~ |
<del> |
| 行内代码 | `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> |
| 图片 |  |
<img> |
| 自动链接 | <https://...> |
<a> |
XSS 防护
parseMarkdown('[click](javascript:alert(1))');
// => '<p><a href="">click</a></p>'(危险协议被过滤)
parseMarkdown('<script>alert(1)</script>');
// => '<p><script>alert(1)</script></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/ 目录,共 407 个测试用例,覆盖:
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 + pluginUtilsanimations.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
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @babel/core | ^7.22.0 |
| @babel/plugin-transform-modules-commonjs | ^7.22.0 |
| @babel/preset-env | ^7.22.0 |
| @rollup/plugin-commonjs | ^25.0.0 |
| @rollup/plugin-node-resolve | ^15.0.0 |
| @rollup/plugin-terser | ^0.4.0 |
| @types/jest | ^29.5.0 |
| babel-jest | ^29.5.0 |
| eslint | ^8.40.0 |
| jest | ^29.5.0 |
| jest-environment-jsdom | ^29.5.0 |
| prettier | ^2.8.0 |
| rollup | ^3.20.0 |
| rollup-plugin-dts | ^5.3.0 |
| rollup-plugin-livereload | ^2.0.5 |
| rollup-plugin-serve | ^2.0.1 |
| typescript | ^5.0.0 |
Keywords
markdown
editor
wysiwyg
ui
component
lightweight
zero-dependency
Details
Assets (1)
Versions (2)
View all
metona-editor-0.1.1.tgz
279 KiB