Files
MetonaEditor/README.md
T
thzxx d6a618af5d feat: v0.4.3 — 浮动工具栏定位引擎重做 + 程序化编辑统一刷新管线
- 浮动工具栏 mirror 镜像测量:隐藏镜像层复制 textarea 排版样式,选区锚点插零宽字符读像素坐标,CJK/软换行/跨行选区误差全部消除

- 实测尺寸替代魔法数字;滚动 rAF 节流重定位;滚出视口自动隐藏;a11y:role=toolbar + aria-label、visibility 移出 Tab 序列、键盘可达

- 修复死开关/监听器泄漏/销毁后 focus 崩溃/blur 焦点抖动误隐藏

- _afterProgrammaticEdit 统一 8 条程序化路径的渲染/行号/字数/大纲/事件刷新;右键菜单 label 转义、copyAsHTML Firefox 降级、paste 失败提示、Esc 退出全屏

- 测试 871 → 895,e2e 22 → 28 项断言(含真实浏览器浮动工具栏布局验证)
2026-08-20 20:43:01 +08:00

836 lines
25 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.3-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-895%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)、右键上下文菜单、浮动格式工具栏(mirror 镜像像素级定位,CJK / 软换行 / 跨行选区精确跟随,键盘可达)
- **内置语法高亮** — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 16 个语言标识,规则缓存零重建开销,`MeEditor.highlight` 即插即用
- **性能** — 字数 / 词数 / 行数差异增量统计,击键零全量扫描;大纲 O(n) 线性构建,长文档即时响应
- **安全** — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),属性级注入防护(含脚注 id),白名单裸标签透传,sanitize 钩子
- **工程化** — 895 个单元测试 + Playwright 真实浏览器冒烟测试(28 项断言),CI 并行 5 job(含 e2e),覆盖率阈值门禁,`sideEffects: false` 便于 tree-shaking
- **桌面端优先** — 纯电脑端设计,无移动端冗余代码
- **引用链接** — 支持 `[text][ref]` / `![alt][ref]` 引用式链接和图片,含 title 属性
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
- **正则搜索** — 查找替换面板支持正则表达式模式
- **分隔条记忆** — 分屏比例自动保存到 localStorage
---
## 目录
- [快速开始](#快速开始)
- [配置项](#配置项)
- [工具栏](#工具栏)
- [实例 API](#实例-api)
- [静态 API](#静态-api)
- [事件系统](#事件系统)
- [Markdown 解析器](#markdown-解析器)
- [插件系统](#插件系统)
- [语法高亮](#语法高亮)
- [主题系统](#主题系统)
- [国际化](#国际化)
- [构建与测试](#构建与测试)
- [License](#license)
---
## 快速开始
### 方式一:npm 安装(推荐)
> 包托管于 MetonaTeam 自建 Gitea 私有 npm 源(`git.metona.cn`),不在 npm 官方源发布,安装时需指定 registry。
```bash
# 直接指定私有源安装
npm install @metona-team/metona-editor --registry=https://git.metona.cn/api/packages/MetonaTeam/npm/
# 或配置项目 .npmrc(推荐,一次配置长期生效)
# .npmrc
@metona-team:registry=https://git.metona.cn/api/packages/MetonaTeam/npm/
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
<!-- Gitea 源(私有仓库需登录,公开仓库直接可访问) -->
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
<!-- 本地构建产物(下载 dist 后引入) -->
<script src="./dist/metona-editor.min.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
```
选中文本时自动浮现粗体 / 斜体 / 代码 / 链接 / 删除线按钮。定位采用 mirror 镜像测量(隐藏镜像层复制 textarea 排版样式,选区锚点插入零宽字符读像素坐标),中文混排、长行软换行、跨行选区均精确跟随;选区滚出可视区自动隐藏,滚动时节流重定位。按钮键盘可达(Tab 聚焦 + Enter 执行),隐藏态移出 Tab 序列。已知限制:RTL 布局下定位未做镜像适配。
### 命令执行
```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 / Outline
```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
editor.setOutline(on: boolean): this // 运行时开关大纲面板
editor.isOutline(): 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, clearRenderCache, getRenderCacheSize, registerBlockHandler } from '@metona-team/metona-editor';
// 内置语法高亮
import { highlight, registerLanguage, getSupportedLanguages } from '@metona-team/metona-editor';
```
---
## 事件系统
### 实例事件
| 事件 | 触发时机 | 参数 |
|------|---------|------|
| `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(列表/引用延续) |
| `Esc` | 退出全屏 |
---
## 构建与测试
```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 + jsdom895 用例,覆盖率阈值门禁) |
| 浏览器冒烟 | Playwright + Chromium28 项断言) |
| 类型生成 | rollup-plugin-dts |
| 零运行时依赖 | ✅ |
---
## License
[MIT](./LICENSE) © MetonaTeam