# MetonaEditor > TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库 [![version](https://img.shields.io/badge/version-0.2.0-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor) [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE) [![tests](https://img.shields.io/badge/tests-610%20passed-brightgreen)](./tests) [![coverage](https://img.shields.io/badge/coverage-parser%2099%25-brightgreen)](./tests) [![types](https://img.shields.io/badge/types-TypeScript%20strict-blue)](./tsconfig.json) ## 特性 - **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 钩子 - **桌面端优先** — 纯电脑端设计,无移动端冗余代码 --- ## 目录 - [快速开始](#快速开始) - [配置项](#配置项) - [工具栏](#工具栏) - [实例 API](#实例-api) - [静态 API](#静态-api) - [事件系统](#事件系统) - [Markdown 解析器](#markdown-解析器) - [插件系统](#插件系统) - [主题系统](#主题系统) - [国际化](#国际化) - [构建与测试](#构建与测试) - [License](#license) --- ## 快速开始 ### npm 安装 ```bash npm install @metona-team/metona-editor ``` ```typescript import MeEditor from '@metona-team/metona-editor'; const editor = MeEditor.create('#editor', { value: '# Hello World', mode: 'split', plugins: ['autoSave', 'searchReplace'], }); ``` ### CDN 引入 ```html
``` --- ## 配置项 ```typescript 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 }); ``` --- ## 工具栏 ### 默认工具栏 ```typescript ['bold','italic','strikethrough','code','|', 'h1','h2','h3','|', 'quote','ul','ol','|', 'link','image','table','hr','|', 'undo','redo','|', 'edit','split','preview','fullscreen'] ``` ### 自定义工具栏 ```typescript // 精简版 MeEditor.create('#editor', { toolbar: ['bold', 'italic', '|', 'h1', 'h2', '|', 'undo', 'redo'], }); // 添加自定义按钮 editor.addToolbarButton({ action: 'timestamp', title: '插入时间戳', icon: '...', onClick: (editor) => editor.insert(new Date().toISOString()), }); ``` --- ## 实例 API ### 内容操作 ```typescript 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 ``` ### 命令执行 ```typescript 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 ``` ### 历史栈 ```typescript editor.undo(): this editor.redo(): this editor.canUndo(): boolean editor.canRedo(): boolean ``` ### 模式与全屏 ```typescript editor.setMode(mode: 'edit' | 'split' | 'preview'): this editor.getMode(): EditMode editor.toggleFullscreen(): this editor.isFullscreen(): boolean editor.exitFullscreen(): this ``` ### 主题与语言 ```typescript editor.setTheme(theme: string): this editor.getTheme(): string editor.setLocale(locale: string): this editor.getLocale(): string editor.t(key: string, params?: object): string ``` ### 统计与状态 ```typescript editor.getStats(): { characters, words, chineseChars, englishWords, lines, readingTime } editor.getStatus(): { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins } ``` ### 启用 / 禁用 / 只读 ```typescript editor.enable(): this editor.disable(): this editor.isDisabled(): boolean editor.setReadOnly(readOnly: boolean): this editor.isReadOnly(): boolean ``` ### 事件 ```typescript editor.on(name: string, fn: (...args: any[]) => void): () => void editor.off(name: string, fn: (...args: any[]) => void): this ``` ### 插件 ```typescript editor.use(plugin: string | PluginObject, options?: object): this editor.unuse(name: string): this editor.getPlugins(): PluginObject[] editor.addToolbarButton(config: ToolbarButtonConfig): this ``` ### 快捷键 ```typescript editor.registerShortcut(combo: string, handler: Function | string, description?: string): this editor.unregisterShortcut(combo: string): this editor.getShortcuts(): Shortcut[] ``` ### 工具栏管理 ```typescript editor.configureToolbar(tools: string[]): this editor.removeToolbarButton(action: string): this ``` ### Zen / Word Wrap ```typescript editor.toggleZen(): this editor.isZen(): boolean editor.toggleWordWrap(): this editor.setWordWrap(on: boolean): this editor.isWordWrap(): boolean ``` ### Toast ```typescript editor.toast(message: string, opts?: { type?: 'success' | 'error' | 'warning' | 'info'; duration?: number; animation?: 'fade' | 'slide' | 'scale'; }): this ``` ### 销毁 ```typescript editor.destroy(): void editor.isDestroyed(): boolean ``` --- ## 静态 API ```typescript 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` | `

` ~ `

` | | Setext 标题 | `H1\n===` | `

` / `

` | | 粗体 | `**bold**` `__bold__` | `` | | 粗斜体 | `***text***` `___text___` | `` | | 斜体 | `*italic*` `_italic_` | `` | | 删除线 | `~~text~~` | `` | | 高亮 | `==text==` | `` | | 上标 | `x^2^` | `` | | 下标 | `H~2~O` | `` | | 行内代码 | `` `code` `` | `` | | 代码块 | ` ```lang ` | `
` |
| 缩进代码块 | `    code` / `\tcode` | `
` |
| 引用 | `> quote` | `
` | | 无序列表 | `- / * / + item` | `
  • ` | | 有序列表 | `1. item` | `
    1. ` | | 嵌套列表 | 多级缩进 | 多级 `
        /
          ` | | 任务列表 | `- [x] done` | `
        1. ` | | 水平线 | `---` `***` `___` | `
          ` | | 表格 | `\| a \| b \|` | `` + 列对齐 | | 链接 | `[text](url 'title')` | `` | | 图片 | `![alt](url)` | `` | | 自动链接 | `` | `` | | 数学公式 | `$E=mc^2$` `$$\int$$` | `` / `
          ` | | 脚注 | `text[^1]` | `` + 底部定义 | | 定义列表 | `Term\n: def` | `
          ` | | Emoji | `:smile:` `:rocket:` | 😊 🚀(150+) | | 反斜杠转义 | `\*` `\_` `\\` | 取消标点特殊含义 | | HTML 注释 | `` | 透传 | | 实体引用保护 | `&` `©` | 不被二次转义 | ### 链接内格式 ```typescript parseMarkdown('[**粗体链接**](https://example.com)') // => 粗体链接 ``` ### parseTokens / renderTokens ```typescript 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 ```typescript 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` 限制最大 500KB - `sanitize` 钩子供外部净化(如 DOMPurify) - `highlight` 钩子异常自动回退为纯文本 --- ## 插件系统 ### 自定义插件 ```typescript 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 ```typescript 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') // 预设副本 ``` --- ## 主题系统 ### 内置主题 ```typescript editor.setTheme('light') // 亮色 editor.setTheme('dark') // 暗色 editor.setTheme('warm') // 暖色 editor.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; } ``` ### 自定义主题 ```typescript import { themeUtils } from '@metona-team/metona-editor'; themeUtils.registerTheme('ocean', { extends: 'dark', // 继承 dark 主题 bg: '#0a1628', accent: '#38bdf8', }); themeUtils.switchTheme('ocean'); ``` ### 外部主题跟随 ```typescript // 从父容器继承 editor.getThemeContext().adopt(); // 跟随外部元素 editor.getThemeContext().syncWithElement(document.body); ``` --- ## 国际化 ### 切换语言 ```typescript editor.setLocale('en-US') // 实例级 MeEditor.setLocale('zh-CN') // 全局 ``` ### 添加语言 ```typescript import { i18nUtils } from '@metona-team/metona-editor'; i18nUtils.addTranslations('ja', { bold: '太字', italic: '斜体', }); ``` ### 远程加载翻译包 ```typescript await i18nUtils.loadRemote('/locales/ja.json', 'ja'); ``` ### 格式化工具 ```typescript 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(列表/引用延续) | --- ## 构建与测试 ```bash # 安装依赖 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](./LICENSE) © MetonaTeam