Files
MetonaEditor/README.md
T
thzxx 9e0c1d7a7f release: v0.1.1 — bug fixes, CSS scoping, parser improvements
Bug Fixes:
- fix(core): insert() replace param was non-functional due to identical ternary branches
- fix(types): correct project URL typo (MetonaToast → MetonaEditor)

Improvements:
- feat(core): scope CSS theme variables to .me-wrapper per-instance, preventing global style pollution
- feat(themes): add optional target parameter to setThemeVariables() for element-scoped theming
- perf(parser): improve bold/strikethrough regex to support inline delimiter chars (e.g. **a*b**, ~~a~b~~)
- fix(parser): prevent *** cross-tag nesting by requiring first content char ≠ delimiter
- docs(animations): clarify module purpose as future-use animation metadata registry

Tests:
- test(core): add 2 cases for insert() replace:true / replace:false behavior
- All 409 tests passing (+2 new)

Chores:
- bump version 0.1.0 → 0.1.1 across all source files, package.json, README, and demo
2026-07-23 20:02:04 +08:00

852 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MetonaEditor
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
[![npm version](https://img.shields.io/badge/version-0.1.1-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 类型声明,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)、searchReplaceCtrl+F 查找替换)
---
## 目录
- [快速开始](#快速开始)
- [配置项](#配置项)
- [API 参考](#api-参考)
- [事件系统](#事件系统)
- [插件系统](#插件系统)
- [主题系统](#主题系统)
- [国际化](#国际化)
- [Markdown 解析器](#markdown-解析器)
- [TypeScript 支持](#typescript-支持)
- [测试](#测试)
- [构建](#构建)
- [浏览器兼容性](#浏览器兼容性)
- [License](#license)
---
## 快速开始
### 安装
```bash
npm install @metona-team/metona-editor
```
### 浏览器直接引入(UMD
> 样式由 JS 自动注入(`styles.js` 运行时动态创建 `<style>` 标签),无需单独引入 CSS 文件。
```html
<!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
```html
<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
```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) => 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
});
```
### 工具栏配置
```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'); // => '<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 提供两层事件系统:
### 实例事件
```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>...</svg>', // 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` | `<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>` |
| 图片 | `![alt](url)` | `<img>` |
| 自动链接 | `<https://...>` | `<a>` |
### XSS 防护
```javascript
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
### 代码高亮钩子
```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<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 方法、插件接口、事件回调、常量。
---
## 测试
```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 | 99.6% | 96.7% | 98.5% |
| core.js | 99.5% | 96.6% | 98.2% |
| **总体** | **96.9%** | **80.3%** | **94.6%** |
测试文件位于 [tests/](./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 + 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