- README.md: full API reference, syntax table, plugin/theme/i18n docs - site/index.html: v0.2.0 hero, live demo, feature cards, API overview - site/docs.html: sidebar nav, full config, instance API tables, parser reference - site/demo.html: all-syntax demo with TS code samples
18 KiB
18 KiB
MetonaEditor
TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库
特性
- TypeScript 源码 — 全模块 TypeScript 严格模式,完整类型导出,IDE 智能提示
- 零运行时依赖 — 打包后单文件,无任何第三方库,UMD / ESM / CJS 三种格式
- 自研解析器 — CommonMark + GFM 扩展,96% 语句覆盖率,parseTokens / renderTokens 分离 API,registerBlockHandler 自定义块语法
- 插件系统 v2 — 拓扑排序依赖管理,6 个预设插件开箱即用,安装 / 卸载生命周期
- 三模式视图 — edit / split / preview,拖拽分隔条调整比例,双向滚动同步
- 主题系统 — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随
- 国际化 — zh-CN / en-US 完整翻译,实例级语言隔离,远程加载翻译包
- 编辑体验 — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式
- 安全 — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),sanitize 钩子
- 桌面端优先 — 纯电脑端设计,无移动端冗余代码
目录
快速开始
npm 安装
npm install @metona-team/metona-editor
import MeEditor from '@metona-team/metona-editor';
const editor = MeEditor.create('#editor', {
value: '# Hello World',
mode: 'split',
plugins: ['autoSave', 'searchReplace'],
});
CDN 引入
<div id="editor"></div>
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
<script>
const editor = MeEditor.create('#editor', { mode: 'split' });
</script>
配置项
MeEditor.create(container, {
// 内容
value: '', // 初始 Markdown 文本
placeholder: '', // 占位符
// 视图
mode: 'split', // 'edit' | 'split' | 'preview'
height: 400, // 数字为 px,字符串原样使用
toolbar: DEFAULT_TOOLBAR, // 工具栏按钮数组,false 隐藏
wordCount: true, // 状态栏字数统计
lineNumbers: true, // 行号装订线
outline: false, // 大纲面板
// 行为
autofocus: false, // 自动聚焦
spellcheck: false, // 拼写检查
readOnly: false, // 只读模式
tabSize: 2, // Tab 空格数(0 = \t)
historyLimit: 100, // 历史栈上限
historyDebounce: 400, // 历史栈防抖(ms)
syncScroll: true, // 分屏同步滚动
autoBrackets: true, // 括号自动闭合
zenMode: false, // Zen 专注模式
wordWrap: true, // 自动换行
maxLength: 0, // 最大字符数(0=不限)
// 主题与语言
theme: 'auto', // 'light' | 'dark' | 'warm' | 'auto'
locale: 'zh-CN', // 'zh-CN' | 'en-US'
// 渲染钩子
render: null, // (md: string, env: RenderEnv) => string
highlight: null, // (code: string, lang: string) => string
sanitize: null, // (html: string) => string
// 外观
className: '', // 容器额外 CSS class
style: {}, // 容器内联样式
// 插件
plugins: [], // 实例级插件列表
// 回调
onChange: null, // (value: string, editor: MarkdownEditor) => void
onInput: null, // (value: string, editor: MarkdownEditor) => void
onFocus: null, // (editor: MarkdownEditor) => void
onBlur: null, // (editor: MarkdownEditor) => void
onSave: null, // (value: string, editor: MarkdownEditor) => void
onModeChange: null, // (mode: EditMode, editor: MarkdownEditor) => void
onFullscreen: null, // (fullscreen: boolean, editor: MarkdownEditor) => void
onCreate: null, // (editor: MarkdownEditor) => void
onDestroy: null, // (editor: MarkdownEditor) => void
onLinkClick: null, // (href: string, text: string, editor: MarkdownEditor) => void
});
工具栏
默认工具栏
['bold','italic','strikethrough','code','|',
'h1','h2','h3','|',
'quote','ul','ol','|',
'link','image','table','hr','|',
'undo','redo','|',
'edit','split','preview','fullscreen']
自定义工具栏
// 精简版
MeEditor.create('#editor', {
toolbar: ['bold', 'italic', '|', 'h1', 'h2', '|', 'undo', 'redo'],
});
// 添加自定义按钮
editor.addToolbarButton({
action: 'timestamp',
title: '插入时间戳',
icon: '<svg>...</svg>',
onClick: (editor) => editor.insert(new Date().toISOString()),
});
实例 API
内容操作
editor.getValue(): string
editor.setValue(md: string, opts?: { silent?: boolean }): this
editor.getHTML(): string
editor.refresh(): this // 强制重渲染(内容未变时)
editor.insert(text: string, opts?: { replace?: boolean }): this
editor.wrap(before: string, after?: string): this
命令执行
editor.exec(action: string): this
// 支持 action:
// bold italic strikethrough underline code
// h1 h2 h3 quote ul ol hr
// link image table
// indent outdent undo redo
// edit split preview fullscreen
// zen wordwrap
历史栈
editor.undo(): this
editor.redo(): this
editor.canUndo(): boolean
editor.canRedo(): boolean
模式与全屏
editor.setMode(mode: 'edit' | 'split' | 'preview'): this
editor.getMode(): EditMode
editor.toggleFullscreen(): this
editor.isFullscreen(): boolean
editor.exitFullscreen(): this
主题与语言
editor.setTheme(theme: string): this
editor.getTheme(): string
editor.setLocale(locale: string): this
editor.getLocale(): string
editor.t(key: string, params?: object): string
统计与状态
editor.getStats(): { characters, words, chineseChars, englishWords, lines, readingTime }
editor.getStatus(): { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }
启用 / 禁用 / 只读
editor.enable(): this
editor.disable(): this
editor.isDisabled(): boolean
editor.setReadOnly(readOnly: boolean): this
editor.isReadOnly(): boolean
事件
editor.on(name: string, fn: (...args: any[]) => void): () => void
editor.off(name: string, fn: (...args: any[]) => void): this
插件
editor.use(plugin: string | PluginObject, options?: object): this
editor.unuse(name: string): this
editor.getPlugins(): PluginObject[]
editor.addToolbarButton(config: ToolbarButtonConfig): this
快捷键
editor.registerShortcut(combo: string, handler: Function | string, description?: string): this
editor.unregisterShortcut(combo: string): this
editor.getShortcuts(): Shortcut[]
工具栏管理
editor.configureToolbar(tools: string[]): this
editor.removeToolbarButton(action: string): this
Zen / Word Wrap
editor.toggleZen(): this
editor.isZen(): boolean
editor.toggleWordWrap(): this
editor.setWordWrap(on: boolean): this
editor.isWordWrap(): boolean
Toast
editor.toast(message: string, opts?: {
type?: 'success' | 'error' | 'warning' | 'info';
duration?: number;
animation?: 'fade' | 'slide' | 'scale';
}): this
销毁
editor.destroy(): void
editor.isDestroyed(): boolean
静态 API
import MeEditor from '@metona-team/metona-editor';
// 工厂函数
MeEditor.create(container, options)
// 全局默认插件
MeEditor.use(plugin, options)
MeEditor.use('autoSave', { delay: 2000 })
// 全局事件钩子
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, parseTokens, renderTokens, safeUrl, slugify, registerBlockHandler } from '@metona-team/metona-editor';
事件系统
实例事件
| 事件 | 触发时机 | 参数 |
|---|---|---|
input |
textarea 原生 input | (value, editor) |
change |
内容变化 | (value, editor) |
focus |
聚焦 | (editor) |
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) |
linkClick |
预览区链接点击 | ({ href, text }) |
fileOpened |
fileSystem 打开文件 | ({ name, handle }) |
fileSaved |
fileSystem 保存文件 | ({ handle }) |
全局钩子
| 钩子 | 触发时机 |
|---|---|
beforeCreate |
实例构造初始化前 |
afterCreate |
实例构造初始化完成 |
beforeRender |
每次渲染前(全局) |
afterRender |
每次渲染后(全局) |
beforeDestroy |
destroy 前 |
afterDestroy |
destroy 后 |
Markdown 解析器
支持语法
| 语法 | 示例 | 输出 |
|---|---|---|
| ATX 标题 | # H1 ~ ###### H6 |
<h1> ~ <h6> |
| Setext 标题 | H1\n=== |
<h1> / <h2> |
| 粗体 | **bold** __bold__ |
<strong> |
| 粗斜体 | ***text*** ___text___ |
<em><strong> |
| 斜体 | *italic* _italic_ |
<em> |
| 删除线 | ~~text~~ |
<del> |
| 高亮 | ==text== |
<mark> |
| 上标 | x^2^ |
<sup> |
| 下标 | H~2~O |
<sub> |
| 行内代码 | `code` |
<code> |
| 代码块 | ```lang |
<pre><code class="language-lang"> |
| 缩进代码块 | code / \tcode |
<pre><code> |
| 引用 | > quote |
<blockquote> |
| 无序列表 | - / * / + item |
<ul><li> |
| 有序列表 | 1. item |
<ol><li> |
| 嵌套列表 | 多级缩进 | 多级 <ul>/<ol> |
| 任务列表 | - [x] done |
<li class="me-task-item"> |
| 水平线 | --- *** ___ |
<hr> |
| 表格 | | a | b | |
<table> + 列对齐 |
| 链接 | [text](url 'title') |
<a> |
| 图片 |  |
<img> |
| 自动链接 | <https://...> |
<a> |
| 数学公式 | $E=mc^2$ $$\int$$ |
<span> / <div> |
| 脚注 | text[^1] |
<sup> + 底部定义 |
| 定义列表 | Term\n: def |
<dl><dt><dd> |
| Emoji | :smile: :rocket: |
😊 🚀(150+) |
| 反斜杠转义 | \* \_ \\ |
取消标点特殊含义 |
| HTML 注释 | <!-- note --> |
透传 |
| 实体引用保护 | & © |
不被二次转义 |
链接内格式
parseMarkdown('[**粗体链接**](https://example.com)')
// => <a href="..."><strong>粗体链接</strong></a>
parseTokens / renderTokens
import { parseTokens, renderTokens } from '@metona-team/metona-editor';
const { tokens, footnotes } = parseTokens('# Hello\n\n**world**');
// tokens: [{ type: 'heading', level: 1, text: 'Hello' }, ...]
// 在渲染前变换 token
tokens.unshift({ type: 'hr' });
const html = renderTokens(tokens, {}, footnotes);
registerBlockHandler
import { registerBlockHandler } from '@metona-team/metona-editor';
registerBlockHandler({
name: 'customAlert',
priority: 8.5, // 越小越先尝试
test: (line) => line.match(/^:::(\\w+)/),
parse: (lines, i, match) => ({
token: { type: 'alert', alertType: match[1] },
newIndex: i + 2,
}),
});
XSS 安全
- 所有文本经
escapeHTML转义 - 链接 URL 过滤
javascript:/vbscript:/file:/ 非图片data: data:image限制最大 500KBsanitize钩子供外部净化(如 DOMPurify)highlight钩子异常自动回退为纯文本
插件系统
自定义插件
const myPlugin = {
name: 'myPlugin',
version: '1.0.0',
description: 'My custom plugin',
install(editor) {
// 安装逻辑
editor.on('change', this._onChange);
},
destroy(editor) {
// 清理逻辑
editor.off('change', this._onChange);
},
};
editor.use(myPlugin, { optionA: 'value' });
editor.unuse('myPlugin');
预设插件
| 插件 | 说明 | 用法 |
|---|---|---|
autoSave |
localStorage 自动保存草稿 | editor.use('autoSave', { delay: 1000 }) |
exportTool |
导出 .md / .html 文件 | editor.use('exportTool'); editor.exportMarkdown() |
searchReplace |
Ctrl+F 查找 / Ctrl+H 替换 | editor.use('searchReplace') |
imagePaste |
粘贴剪贴板图片转 base64 | editor.use('imagePaste') |
shortcutHelp |
按 ? 弹出快捷键面板 | editor.use('shortcutHelp') |
fileSystem |
File System Access API 读写磁盘 | editor.use('fileSystem'); editor.openFile() |
PluginManager & pluginUtils
import { PluginManager, pluginUtils } from '@metona-team/metona-editor';
const pm = new PluginManager();
pm.register('my', myPlugin);
pm.has('my'); // true
pm.destroy();
pluginUtils.validatePlugin(plugin) // { valid, errors }
pluginUtils.createPlugin({ ... }) // 工厂函数
pluginUtils.getPreset('autoSave') // 预设副本
主题系统
内置主题
editor.setTheme('light') // 亮色
editor.setTheme('dark') // 暗色
editor.setTheme('warm') // 暖色
editor.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', {
extends: 'dark', // 继承 dark 主题
bg: '#0a1628',
accent: '#38bdf8',
});
themeUtils.switchTheme('ocean');
外部主题跟随
// 从父容器继承
editor.getThemeContext().adopt();
// 跟随外部元素
editor.getThemeContext().syncWithElement(document.body);
国际化
切换语言
editor.setLocale('en-US') // 实例级
MeEditor.setLocale('zh-CN') // 全局
添加语言
import { i18nUtils } from '@metona-team/metona-editor';
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') // 本地化日期
快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+B |
粗体 |
Ctrl+I |
斜体 |
Ctrl+U |
下划线 |
Ctrl+E |
行内代码 |
Ctrl+K |
插入链接 |
Ctrl+1/2/3 |
标题 H1/H2/H3 |
Ctrl+Q |
引用 |
Ctrl+Z |
撤销 |
Ctrl+Y / Ctrl+Shift+Z |
重做 |
Ctrl+S |
保存 |
Ctrl+F |
查找 |
Ctrl+H |
查找替换 |
Tab / Shift+Tab |
缩进 / 反缩进 |
? |
快捷键帮助 |
Enter |
智能 Enter(列表/引用延续) |
构建与测试
# 安装依赖
npm install
# 开发模式(热更新 + 示例站点)
npm run dev
# 构建产物(dist/)
npm run build
# 类型检查
npm run typecheck
# 运行测试
npm test
# 测试覆盖率
npm run test -- --coverage
# 代码格式化
npm run format
构建产物
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 类型声明
源码结构
src/
├── index.ts 入口 / 全局API
├── core.ts MarkdownEditor 类
├── parser.ts 自研 Markdown 解析器
├── plugins.ts 插件系统 & 6 个预设
├── themes.ts 主题系统
├── i18n.ts 国际化
├── styles.ts CSS-in-JS
├── constants.ts 常量 / 类型定义
├── utils.ts 工具函数
├── animations.ts 动画元数据
├── icons.ts 工具栏 SVG 图标
└── locales.ts 中英文翻译数据
技术栈
| 项目 | 技术 |
|---|---|
| 语言 | TypeScript 5 (strict) |
| 构建 | Rollup 3 |
| 测试 | Jest 29 + jsdom |
| 类型生成 | rollup-plugin-dts |
| 零运行时依赖 | ✅ |
License
MIT © MetonaTeam