Files
MetonaEditor/README.md
T
thzxx 4d6767482e
CI / test-parser (push) Successful in 9m29s
CI / test-core (push) Successful in 9m36s
CI / test-rest (push) Successful in 9m29s
CI / e2e (push) Successful in 9m44s
CI / verify (18.x) (push) Successful in 9m50s
CI / verify (20.x) (push) Successful in 9m50s
CI / verify (24.x) (push) Successful in 9m45s
feat: v0.4.1 — Zen 专注模式宽度可配置 + 5 项修复 + 841 测试
2026-08-09 20:17:25 +08:00

825 lines
24 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
> TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库
[![version](https://img.shields.io/badge/version-0.4.1-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-841%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 扩展,99% 行覆盖率,parseTokens / renderTokens 分离 APIregisterBlockHandler 自定义块语法
- **插件系统 v2** — 拓扑排序依赖管理,6 个预设插件开箱即用,安装 / 卸载生命周期
- **三模式视图** — edit / split / preview,拖拽分隔条调整比例,双向滚动同步
- **主题系统** — light / dark / warm / autoCSS 变量可定制,实例级隔离,主题继承,外部跟随
- **国际化** — zh-CN / en-US / ja / ko / fr / de 六种语言完整翻译,实例级语言隔离,远程加载翻译包
- **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式(宽度可配置,默认 960px)、右键上下文菜单
- **内置语法高亮** — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 16 个语言标识,规则缓存零重建开销,`MeEditor.highlight` 即插即用
- **性能** — 字数 / 词数 / 行数差异增量统计,击键零全量扫描;大纲 O(n) 线性构建,长文档即时响应
- **安全** — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),属性级注入防护(含脚注 id),白名单裸标签透传,sanitize 钩子
- **工程化** — 841 个单元测试 + Playwright 真实浏览器冒烟测试,CI 并行 4 job,`sideEffects: false` 便于 tree-shaking
- **桌面端优先** — 纯电脑端设计,无移动端冗余代码
- **引用链接** — 支持 `[text][ref]` / `![alt][ref]` 引用式链接和图片,含 title 属性
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
- **正则搜索** — 查找替换面板支持正则表达式模式
- **分隔条记忆** — 分屏比例自动保存到 localStorage
---
## 目录
- [快速开始](#快速开始)
- [配置项](#配置项)
- [工具栏](#工具栏)
- [实例 API](#实例-api)
- [静态 API](#静态-api)
- [事件系统](#事件系统)
- [Markdown 解析器](#markdown-解析器)
- [插件系统](#插件系统)
- [语法高亮](#语法高亮)
- [主题系统](#主题系统)
- [国际化](#国际化)
- [构建与测试](#构建与测试)
- [License](#license)
---
## 快速开始
### 方式一:npm 安装(推荐)
```bash
npm install @metona-team/metona-editor
```
```typescript
// ES Module / TypeScript
import MeEditor from '@metona-team/metona-editor';
const editor = MeEditor.create('#editor', {
value: '# Hello World',
mode: 'split',
theme: 'auto',
plugins: ['autoSave', 'searchReplace'],
});
```
```javascript
// CommonJS
const MeEditor = require('@metona-team/metona-editor');
const editor = MeEditor.create('#editor', { mode: 'split' });
```
### 方式二:CDN 引入
```html
<!-- jsDelivr CDN (推荐) -->
<script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.4.1/dist/metona-editor.min.js"></script>
<!-- unpkg CDN -->
<script src="https://unpkg.com/@metona-team/metona-editor@0.4.1/dist/metona-editor.min.js"></script>
<!-- Gitea 源 -->
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
<div id="editor"></div>
<script>
const editor = MeEditor.create('#editor', { mode: 'split' });
</script>
```
### 方式三:本地文件
```bash
# 下载产物文件到项目中
# dist/metona-editor.js → UMD (浏览器)
# dist/metona-editor.min.js → UMD 压缩
# dist/metona-editor.mjs → ES Module
# dist/metona-editor.cjs → CommonJS
```
```html
<!-- 浏览器直接引入本地文件 -->
<script src="./dist/metona-editor.min.js"></script>
```
```javascript
// Node.js ESM
import MeEditor from './dist/metona-editor.mjs';
// Node.js CJS
const MeEditor = require('./dist/metona-editor.cjs');
```
---
## 配置项
```typescript
MeEditor.create(container, {
// 内容
value: '', // 初始 Markdown 文本
placeholder: '', // 占位符
id: '', // 实例 id(默认自动生成,用于分隔条/草稿等存储键隔离)
// 视图
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 专注模式
zenMaxWidth: 960, // Zen 内容区最大宽度:数字为 px / 字符串原样作为 CSS 值 / false 不限制
wordWrap: true, // 自动换行
maxLength: 0, // 最大字符数(0=不限)
floatingToolbar: true, // 选中文本浮动格式栏
// 主题与语言
theme: 'auto', // 'light' | 'dark' | 'warm' | 'auto'
locale: 'zh-CN', // 'zh-CN' | 'en-US' | 'ja' | 'ko' | 'fr' | 'de'
// 渲染钩子
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: '<svg>...</svg>',
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
editor.replaceAll(search: string, replace: string, caseSensitive?: boolean): number
editor.replaceAllRegex(pattern: RegExp, replace: string): number
editor.lineCount(): number
editor.getLine(line: number): string
editor.copyAsMarkdown(): this // 复制源码到剪贴板
editor.copyAsHTML(): this // 复制渲染 HTML 到剪贴板
```
### 光标与选区
```typescript
editor.getSelectedText(): string
editor.getCursorPosition(): { line: number; column: number }
editor.setCursorPosition(line: number, column?: number): this
editor.scrollToLine(line: number): this
editor.selectLine(line: number): this
editor.selectAll(): this
```
### 浮动工具栏
```typescript
editor.toggleFloatingToolbar(): this // 开关选中文本格式栏
editor.isFloatingToolbar(): boolean
```
### 命令执行
```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
// formatTable(光标所在表格按列宽对齐)
// 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.setZenMaxWidth(width: number | string | false): this // 运行时调整专注模式宽度
editor.getZenMaxWidth(): number | string | false
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; // ms0 = 不自动消失
animation?: 'fade' | 'slide' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom';
}): this
```
### Copy API
```typescript
editor.copyAsMarkdown(): this // 复制 Markdown 源码到剪贴板
editor.copyAsHTML(): this // 复制渲染后的 HTML 到剪贴板
```
### 销毁
```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';
// 内置语法高亮
import { highlight, registerLanguage, getSupportedLanguages } 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 })` |
| `beforeChange` | 内容变更前 | `(oldValue, newValue, editor)` |
| `afterChange` | 内容变更后 | `(newValue, editor)` |
| `copy` | 复制到剪贴板 | `({ type })` |
| `selectionChange` | 选区变化 | `({ start, end, text })` |
| `cursorMove` | 光标移动 | `({ line, column })` |
### 全局钩子
| 钩子 | 触发时机 |
|------|---------|
| `beforeCreate` | 实例构造初始化前 |
| `afterCreate` | 实例构造初始化完成 |
| `beforeRender` | 每次渲染前(全局) |
| `afterRender` | 每次渲染后(全局) |
| `beforeDestroy` | destroy 前 |
| `afterDestroy` | destroy 后 |
| `beforeChange` | 内容变更前(全局) |
| `afterChange` | 内容变更后(全局) |
---
## Markdown 解析器
### 支持语法
| 语法 | 示例 | 输出 |
|------|------|------|
| ATX 标题 | `# H1` ~ `###### H6` | `<h1>` ~ `<h6>` |
| Setext 标题 | `H1\n===` | `<h1>` / `<h2>` |
| 粗体 | `**bold**` `__bold__` | `<strong>` |
| 粗斜体 | `***text***` `___text___` | `<em><strong>` |
| 斜体 | `*italic*` `_italic_` | `<em>` |
| 删除线 | `~~text~~` | `<del>` |
| 下划线 | `<u>text</u>`(裸标签透传) | `<u>` |
| 高亮 | `==text==` | `<mark>` |
| 上标 | `x^2^` | `<sup>` |
| 下标 | `H~2~O` | `<sub>` |
| 行内代码 | `` `code` `` | `<code>` |
| 代码块 | ` ```lang ` | `<pre><code class="language-lang">` |
| 缩进代码块 | ` code` / `\tcode` | `<pre><code>` |
| 引用 | `> quote` | `<blockquote>` |
| 无序列表 | `- / * / + item` | `<ul><li>` |
| 有序列表 | `1. item` | `<ol><li>` |
| 嵌套列表 | 多级缩进 | 多级 `<ul>/<ol>` |
| 任务列表 | `- [x] done` | `<li class="me-task-item">` |
| 水平线 | `---` `***` `___` | `<hr>` |
| 表格 | `\| a \| b \|` | `<table>` + 列对齐 |
| 链接 | `[text](url 'title')` | `<a>` |
| 图片 | `![alt](url)` | `<img>` |
| 引用链接 | `[text][ref]` + `[ref]: url` | `<a>` |
| 引用图片 | `![alt][ref]` + `[ref]: url` | `<img>` |
| 表格格式化 | `exec('formatTable')` | 光标所在表格按列宽对齐 |
| 自动链接 | `<https://...>` | `<a>` |
| 数学公式 | `$E=mc^2$` `$$\int$$` | `<span>` / `<div>` |
| 脚注 | `text[^1]` | `<sup>` + 底部定义 |
| 定义列表 | `Term\n: def` | `<dl><dt><dd>` |
| Emoji | `:smile:` `:rocket:` | 😊 🚀(150+ |
| 反斜杠转义 | `\*` `\_` `\\` | 取消标点特殊含义 |
| HTML 注释 | `<!-- note -->` | 透传 |
| 实体引用保护 | `&amp;` `&#169;` | 不被二次转义 |
### 链接内格式
```typescript
parseMarkdown('[**粗体链接**](https://example.com)')
// => <a href="..."><strong>粗体链接</strong></a>
```
### 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:`
- 属性级注入防护:`href` / `src` / `alt` / `title` / 脚注 id / `language-*` 引号与换行均被转义
- 白名单裸标签透传:仅 `<u>` / `</u>`(无属性),带属性的标签仍被转义
- `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 文件,支持 PDF(打印) | `editor.use('exportTool'); editor.exportMarkdown()` |
| `searchReplace` | Ctrl+F 查找 / Ctrl+H 替换,支持正则、大小写、全字匹配 | `editor.use('searchReplace')` |
| `imagePaste` | 粘贴剪贴板图片转 base64,可限尺寸 | `editor.use('imagePaste', { maxSizeKB: 500 })` |
| `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') // 预设副本
```
---
## 语法高亮
v0.2.5 起内置零依赖轻量高亮器,支持 js / ts / tsx / jsx / python / bash / css / html / json / yaml / markdown / java / go / rust(含 shell / md 别名共 16 个语言标识)。
```typescript
import MeEditor from '@metona-team/metona-editor';
// 作为 highlight 钩子(代码块自动高亮)
MeEditor.create('#editor', { highlight: MeEditor.highlight });
// 独立调用
const html = MeEditor.highlight(code, 'typescript');
// 注册自定义语言
MeEditor.registerLanguage('myLang', { keywords: ['kw'], builtins: [] });
```
高亮类名:`me-hl-keyword` / `me-hl-string` / `me-hl-comment` / `me-hl-number` / `me-hl-builtin` / `me-hl-function`,内置浅色 / 深色自适应配色,也可用 CSS 覆盖定制。
---
## 主题系统
### 内置主题
```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);
// 手动断开所有外部跟随订阅(实例 destroy 时也会自动断开)
editor.getThemeContext().dispose();
```
---
## 国际化
### 切换语言
```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
# 浏览器冒烟测试(Playwright + Chromium,需先构建)
npm run build && npm run test:e2e
# 性能基准(先构建再运行)
npm run build && npm run bench
# 代码格式化
npm run format
```
### 构建产物
```
dist/
├── metona-editor.js UMD(浏览器直接引入)
├── metona-editor.min.js UMD 压缩版(CDN
├── metona-editor.mjs ES Module
├── metona-editor.cjs CommonJS
└── metona-editor.d.ts TypeScript 类型声明
```
### 源码结构
```
src/
├── index.ts 入口 / 全局API
├── core.ts MarkdownEditor 类(构造 / 事件 / 历史 / 模式)
├── commands.ts 编辑命令实现(包裹 / 前缀 / 插入 / 表格格式化)
├── floating-toolbar.ts 选中文本浮动格式栏
├── context-menu.ts 右键上下文菜单
├── outline.ts 大纲面板
├── parser.ts 自研 Markdown 解析器
├── highlight.ts 内置轻量语法高亮器
├── 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 + jsdom841 用例) |
| 浏览器冒烟 | Playwright + Chromium22 项断言) |
| 类型生成 | rollup-plugin-dts |
| 零运行时依赖 | ✅ |
---
## License
[MIT](./LICENSE) © MetonaTeam