Initial commit: MetonaEditor v0.1.0
This commit is contained in:
@@ -0,0 +1,837 @@
|
||||
# MetonaEditor
|
||||
|
||||
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
|
||||
|
||||
[](https://www.npmjs.com/package/@metona-team/metona-editor)
|
||||
[](./LICENSE)
|
||||
[](./tests)
|
||||
[](./tests)
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
- **零依赖** — 单文件打包,无任何运行时依赖,UMD/ESM/CJS 三种格式
|
||||
- **现代化** — 原生 ES2020+ 实现,CSS 变量主题系统,响应式布局,无障碍支持
|
||||
- **易扩展** — 完整插件系统(install/destroy 生命周期),自定义工具栏按钮,预设插件开箱即用
|
||||
- **易维护** — 模块化源码,JSDoc 注释完整,TypeScript 类型声明,291 个单元测试覆盖
|
||||
- **易使用** — 工厂函数 `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 查找替换)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [快速开始](#快速开始)
|
||||
- [配置项](#配置项)
|
||||
- [API 参考](#api-参考)
|
||||
- [事件系统](#事件系统)
|
||||
- [插件系统](#插件系统)
|
||||
- [主题系统](#主题系统)
|
||||
- [国际化](#国际化)
|
||||
- [Markdown 解析器](#markdown-解析器)
|
||||
- [TypeScript 支持](#typescript-支持)
|
||||
- [测试](#测试)
|
||||
- [构建](#构建)
|
||||
- [浏览器兼容性](#浏览器兼容性)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
npm install @metona-team/metona-editor
|
||||
```
|
||||
|
||||
### 浏览器直接引入(UMD)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/@metona-team/metona-editor/dist/metona-editor.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script src="https://unpkg.com/@metona-team/metona-editor/dist/metona-editor.min.js"></script>
|
||||
<script>
|
||||
const editor = MeEditor.create('#editor', {
|
||||
value: '# Hello World',
|
||||
mode: 'split',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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) => 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.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>` |
|
||||
| 图片 | `` | `<img>` |
|
||||
| 自动链接 | `<https://...>` | `<a>` |
|
||||
|
||||
### 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 | 79.9% | 59.6% | 73.8% |
|
||||
| core.js | 72.9% | 60.9% | 69.4% |
|
||||
| **总体** | **80.6%** | **67.8%** | **79.3%** |
|
||||
|
||||
测试文件位于 [tests/](./tests) 目录,共 **291 个测试用例**,覆盖:
|
||||
|
||||
- `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
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [npm 包主页](https://www.npmjs.com/package/@metona-team/metona-editor)
|
||||
- [问题反馈](https://git.metona.cn/MetonaTeam/MetonaEditor/issues)
|
||||
- [在线演示](./site/demo.html)
|
||||
Reference in New Issue
Block a user