From 22e867eda89de894c54b3e38a2310383367dd407 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Thu, 23 Jul 2026 16:23:07 +0800 Subject: [PATCH] Initial commit: MetonaEditor v0.1.0 --- .gitignore | 7 + LICENSE | 21 + README.md | 837 +++++ babel.config.js | 16 + build.sh | 77 + jest.config.js | 17 + package-lock.json | 7444 ++++++++++++++++++++++++++++++++++++++ package.json | 85 + rollup.config.js | 122 + serve.sh | 31 + site/demo.html | 400 ++ site/index.html | 728 ++++ src/animations.js | 70 + src/constants.js | 194 + src/core.js | 945 +++++ src/i18n.js | 391 ++ src/icons.js | 56 + src/index.js | 248 ++ src/locales.js | 168 + src/parser.js | 322 ++ src/plugins.js | 483 +++ src/styles.js | 381 ++ src/themes.js | 388 ++ src/utils.js | 138 + tests/animations.test.js | 146 + tests/core.test.js | 1227 +++++++ tests/i18n.test.js | 412 +++ tests/parser.test.js | 353 ++ tests/plugins.test.js | 869 +++++ tests/themes.test.js | 433 +++ tests/utils.test.js | 246 ++ types/index.d.ts | 366 ++ 32 files changed, 17621 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 babel.config.js create mode 100644 build.sh create mode 100644 jest.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 serve.sh create mode 100644 site/demo.html create mode 100644 site/index.html create mode 100644 src/animations.js create mode 100644 src/constants.js create mode 100644 src/core.js create mode 100644 src/i18n.js create mode 100644 src/icons.js create mode 100644 src/index.js create mode 100644 src/locales.js create mode 100644 src/parser.js create mode 100644 src/plugins.js create mode 100644 src/styles.js create mode 100644 src/themes.js create mode 100644 src/utils.js create mode 100644 tests/animations.test.js create mode 100644 tests/core.test.js create mode 100644 tests/i18n.test.js create mode 100644 tests/parser.test.js create mode 100644 tests/plugins.test.js create mode 100644 tests/themes.test.js create mode 100644 tests/utils.test.js create mode 100644 types/index.d.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d932ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +.DS_Store + +# 本地 npm 发布凭据(含 _auth,勿提交) +.npmrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd61329 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 thzxx + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9b0871 --- /dev/null +++ b/README.md @@ -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 + + +
+ + + + + + + + + +``` + +### 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'); // => 'bold
' + +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 字符串 + 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` | `` |
+| 粗体 | `**bold**` `__bold__` | `` |
+| 斜体 | `*italic*` `_italic_` | `` |
+| 删除线 | `~~text~~` | `` |
+| 行内代码 | `` `code` `` | `` |
+| 代码块 | ` ```lang ` | `` |
+| 引用 | `> quote` | `` |
+| 无序列表 | `- item` / `* item` / `+ item` | `
` |
+| 表格 | `\| a \| b \|` | `