- 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
874 lines
23 KiB
Markdown
874 lines
23 KiB
Markdown
# MetonaEditor
|
||
|
||
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
|
||
|
||
[](https://www.npmjs.com/package/@metona-team/metona-editor)
|
||
[](./LICENSE)
|
||
[](./tests)
|
||
[](./tests)
|
||
|
||
---
|
||
|
||
## 特性
|
||
|
||
- **零依赖** — 单文件打包,无任何运行时依赖,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)、searchReplace(Ctrl+F 查找替换)、imagePaste(粘贴图片转 base64)
|
||
- **性能优化** — 渲染缓存跳过重复解析,`refresh()` 强制刷新,模式切换无冗余渲染
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [快速开始](#快速开始)
|
||
- [配置项](#配置项)
|
||
- [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, // 拼写检查
|
||
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) => 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.refresh(); // 强制刷新预览(内容未变时使用)
|
||
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, readOnly, 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(); // 是否禁用
|
||
editor.setReadOnly(true); // 设置为只读
|
||
editor.isReadOnly(); // 是否只读
|
||
```
|
||
|
||
#### 销毁
|
||
|
||
```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 })` |
|
||
| `beforeRender` | 渲染前(实例级) | `(editor)` |
|
||
| `afterRender` | 渲染后(实例级) | `(editor)` |
|
||
|
||
### 静态钩子(全局)
|
||
|
||
```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 — 关闭
|
||
```
|
||
|
||
#### imagePaste — 粘贴图片转 base64
|
||
|
||
```javascript
|
||
editor.use(presetPlugins.imagePaste);
|
||
|
||
// 在编辑区 Ctrl+V 粘贴剪贴板中的图片
|
||
// 自动生成  并插入
|
||
```
|
||
|
||
### 自定义工具栏按钮
|
||
|
||
```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, imagePaste }
|
||
```
|
||
|
||
---
|
||
|
||
## 主题系统
|
||
|
||
### 内置主题
|
||
|
||
```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>` |
|
||
| 高亮标记 | `==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>` |
|
||
| 图片 | `` | `<img>` |
|
||
| 自动链接 | `<https://...>` | `<a>` |
|
||
| Emoji 短码 | `:smile:` `:rocket:` | 😊 🚀(80+ 常用) |
|
||
|
||
### XSS 防护
|
||
|
||
```javascript
|
||
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)
|
||
|
||
### 代码高亮钩子
|
||
|
||
```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) 目录,共 **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` — 翻译、语言切换、格式化、监听
|
||
|
||
---
|
||
|
||
## 构建
|
||
|
||
```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
|