diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ce966d5..42ae805 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,37 +7,76 @@ on: branches: [master] jobs: - test: + test-parser: runs-on: debian-latest timeout-minutes: 15 - - strategy: - matrix: - node-version: [18.x, 20.x, 22.x, 24.x] - steps: - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Run parser tests + run: npx jest tests/parser.test.ts tests/highlight.test.ts --forceExit --maxWorkers=1 --testTimeout=15000 + env: + NODE_OPTIONS: --max-old-space-size=4096 + test-core: + runs-on: debian-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Run core tests + run: npx jest tests/core.test.ts tests/plugins.test.ts tests/index.test.ts --forceExit --maxWorkers=1 --testTimeout=15000 + env: + NODE_OPTIONS: --max-old-space-size=4096 + + test-rest: + runs-on: debian-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Run remaining tests + run: npx jest tests/themes.test.ts tests/i18n.test.ts tests/utils.test.ts tests/animations.test.ts tests/styles.test.ts --forceExit --maxWorkers=1 --testTimeout=15000 + env: + NODE_OPTIONS: --max-old-space-size=4096 + + verify: + runs-on: debian-latest + timeout-minutes: 15 + strategy: + matrix: + node-version: [18.x, 20.x, 24.x] + steps: + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - - name: Install dependencies run: npm ci - - name: Type check run: npm run typecheck - - name: Lint run: npm run lint continue-on-error: true - - - name: Run tests - run: npx jest --forceExit --maxWorkers=1 --testTimeout=15000 - env: - NODE_OPTIONS: --max-old-space-size=4096 - - name: Build run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f20c0..2dfa845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to MetonaEditor will be documented in this file. +## [0.2.5] - 2026-08-09 + +### Fixed +- **npm 发布配置**: `module` / `exports.import` 不再指向 TS 源码,产物改用标准 `.mjs` / `.cjs` 扩展名(此前 `"type": "module"` 导致 Node 将 `.js` 产物按 ESM 解析,`require()` 拿不到任何导出)。 +- **XSS 属性注入**: 链接/图片的 `href`/`src`/`alt`/`title` 及代码块 `language-*` 类与标题属性现经属性级转义(`escapeHTML` 在浏览器环境不转义双引号,此前的属性拼接存在注入面)。 +- **本地存储串扰**: 分隔条比例键改为 `metona-editor-divider-${id}`,多实例互不覆盖。 + +### Added +- **maxLength 生效**: 之前仅有类型与默认值,现绑定 textarea `maxlength` 并对 `setValue` / `insert` / `replaceAll` / `replaceAllRegex` 截断。 +- **zenMode 初始状态**: 配置 `zenMode: true` 启动即进入专注模式(此前仅 `toggleZen()` 可进入)。 +- **搜索面板大小写 / 全字匹配**: `matchCase` / `wholeWord` 翻译键此前存在但未实现,现补齐 UI 开关与匹配逻辑(普通与正则模式均支持,中文全字边界感知)。 +- **内置轻量语法高亮**: 新模块 `src/highlight.ts`,零依赖 tokenizer,13 种语言,`MeEditor.highlight` 即插即用,输出 `me-hl-*` 类。 +- **exportPDF**: exportTool 新增 `editor.exportPDF()`(隐藏 iframe + window.print)。 +- **imagePaste 尺寸限制**: `editor.use('imagePaste', { maxSizeKB: 500 })`,超限 toast 警告。 +- **性能基准**: `npm run bench`(parseMarkdown 1MB ≈ 51ms,highlight 200 函数 ≈ 1.2ms)。 + +### Changed +- **core.ts 拆分**: 命令、浮动工具栏、右键菜单、大纲 4 组方法拆至独立模块(`commands.ts` / `floating-toolbar.ts` / `context-menu.ts` / `outline.ts`),core 从 1162 行降至 865 行,API 完全兼容。 +- **插件状态类型收紧**: 6 个预设插件的实例状态改用 Symbol 键存储(`EditorLike` 接口 + `pluginState` 助手),消灭 `(editor as any).__xxx` 魔法属性。 +- **eslint type-aware**: 启用 `parserOptions.project` 与 `consistent-type-imports` / `no-unnecessary-type-assertion` 规则,0 errors。 +- **CI 并行化**: 测试拆分为 parser / core / rest 三个并行 job,另设跨 Node 18/20/24 的 verify job(typecheck + lint + build)。 +- **docs.html / README 补齐**: 光标选区 API、浮动工具栏、replaceAll、新事件、内置高亮文档。 +- 版本同步 0.2.4 → 0.2.5,测试 725 → 770+。 + +--- + ## [0.2.4] - 2026-07-25 ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f5f087..1ce1d90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,8 +104,8 @@ npm run build # Output in dist/ # ├── metona-editor.js UMD # ├── metona-editor.min.js UMD minified -# ├── metona-editor.esm.js ES Module -# ├── metona-editor.cjs.js CommonJS +# ├── metona-editor.mjs ES Module +# ├── metona-editor.cjs CommonJS # └── metona-editor.d.ts TypeScript declarations ``` diff --git a/README.md b/README.md index f50ff6b..45b7e3d 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ > TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库 -[](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor) +[](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor) [](./LICENSE) -[](./tests) +[](./tests) [](./tests) [](./tsconfig.json) @@ -18,7 +18,8 @@ - **主题系统** — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随 - **国际化** — zh-CN / en-US / ja / ko 完整翻译,实例级语言隔离,远程加载翻译包 - **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式、右键上下文菜单 -- **安全** — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),sanitize 钩子 +- **内置语法高亮** — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 13 种语言,`MeEditor.highlight` 即插即用 +- **安全** — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),属性级注入防护,sanitize 钩子 - **桌面端优先** — 纯电脑端设计,无移动端冗余代码 - **引用链接** — 支持 `[text][ref]` / `![alt][ref]` 引用式链接和图片,含 title 属性 - **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等) @@ -75,10 +76,10 @@ const editor = MeEditor.create('#editor', { mode: 'split' }); ```html - + - + @@ -95,8 +96,8 @@ const editor = MeEditor.create('#editor', { mode: 'split' }); # 下载产物文件到项目中 # dist/metona-editor.js → UMD (浏览器) # dist/metona-editor.min.js → UMD 压缩 -# dist/metona-editor.esm.js → ES Module -# dist/metona-editor.cjs.js → CommonJS +# dist/metona-editor.mjs → ES Module +# dist/metona-editor.cjs → CommonJS ``` ```html @@ -106,9 +107,9 @@ const editor = MeEditor.create('#editor', { mode: 'split' }); ```javascript // Node.js ESM -import MeEditor from './dist/metona-editor.esm.js'; +import MeEditor from './dist/metona-editor.mjs'; // Node.js CJS -const MeEditor = require('./dist/metona-editor.cjs.js'); +const MeEditor = require('./dist/metona-editor.cjs'); ``` --- @@ -138,9 +139,10 @@ MeEditor.create(container, { historyDebounce: 400, // 历史栈防抖(ms) syncScroll: true, // 分屏同步滚动 autoBrackets: true, // 括号自动闭合 - zenMode: false, // Zen 专注模式 + zenMode: false, // 启动即进入 Zen 专注模式 wordWrap: true, // 自动换行 maxLength: 0, // 最大字符数(0=不限) + floatingToolbar: true, // 选中文本浮动格式栏 // 主题与语言 theme: 'auto', // 'light' | 'dark' | 'warm' | 'auto' @@ -217,6 +219,30 @@ 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 ``` ### 命令执行 @@ -559,6 +585,27 @@ pluginUtils.getPreset('autoSave') // 预设副本 --- +## 语法高亮 + +v0.2.5 起内置零依赖轻量高亮器(js / ts / tsx / jsx / python / bash / css / html / json / yaml / markdown / java / go / rust)。 + +```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`。 + +--- + ## 主题系统 ### 内置主题 @@ -705,8 +752,8 @@ 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.mjs ES Module +├── metona-editor.cjs CommonJS └── metona-editor.d.ts TypeScript 类型声明 ``` diff --git a/bench/benchmark.cjs b/bench/benchmark.cjs new file mode 100644 index 0000000..76694e5 --- /dev/null +++ b/bench/benchmark.cjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +/** + * MetonaEditor 性能基准 — 解析器吞吐测试 + * 用法: npm run build && npm run bench + */ + +const MeEditor = require('../dist/metona-editor.cjs'); +const { parseMarkdown } = MeEditor; + +const buildSample = (kb) => { + const chunk = [ + '# 性能基准文档', + '', + '这是一个 **粗体** 与 *斜体* 的段落,包含 `inline code` 和 [链接](https://example.com)。', + '', + '- 列表项一', + '- 列表项二', + ' - 嵌套项', + ' - 嵌套项二', + '- 列表项三', + '', + '1. 有序一', + '2. 有序二', + '', + '> 引用内容', + '> 继续引用', + '', + '```js', + 'function hello(name) {', + ' const msg = `Hello, ${name}!`;', + ' return msg;', + '}', + '```', + '', + '| 列一 | 列二 | 列三 |', + '| --- | :---: | ---: |', + '| A | B | C |', + '| D | E | F |', + '', + '---', + '', + '引用链接:[文档][ref]', + '', + '[ref]: https://example.com/docs "参考"', + '', + '脚注示例[^1]', + '', + '[^1]: 脚注内容', + '', + '## 二级标题', + '', + '$$E = mc^2$$', + '', + 'H~2~O 与 x^2^,还有 :rocket: emoji。', + '', + ].join('\n'); + const repeat = Math.max(1, Math.ceil((kb * 1024) / chunk.length)); + return chunk.repeat(repeat); +}; + +const median = (arr) => { + const s = [...arr].sort((a, b) => a - b); + const mid = Math.floor(s.length / 2); + return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; +}; + +const benchmark = (label, fn, iterations = 5) => { + // warmup + fn(); + const times = []; + for (let i = 0; i < iterations; i++) { + const t0 = process.hrtime.bigint(); + fn(); + const t1 = process.hrtime.bigint(); + times.push(Number(t1 - t0) / 1e6); + } + const ms = median(times); + return { label, ms: ms.toFixed(1), opsPerSec: (1000 / ms).toFixed(1) }; +}; + +console.log('MetonaEditor 性能基准'); +console.log('====================='); +console.log(`版本: ${MeEditor.VERSION}`); +console.log(''); + +for (const size of [64, 256, 1024]) { + const sample = buildSample(size); + const r = benchmark(`parseMarkdown ${size}KB (${sample.length.toLocaleString()} chars)`, () => { + const html = parseMarkdown(sample); + if (!html) throw new Error('empty output'); + }); + console.log(` ${r.label}: ${r.ms} ms (${r.opsPerSec} ops/s)`); +} + +console.log(''); + +// 语法高亮基准 +try { + const code = Array.from({ length: 200 }, (_, i) => + `function f${i}(a, b) { const s = "str${i}"; // comment\n return a + b + ${i}; }`, + ).join('\n'); + const r = benchmark('highlight 200 functions (js)', () => { + MeEditor.highlight(code, 'js'); + }); + console.log(` ${r.label}: ${r.ms} ms (${r.opsPerSec} ops/s)`); +} catch (_) { + console.log(' (highlight benchmark skipped)'); +} + +console.log(''); +console.log('完成。'); diff --git a/build.sh b/build.sh index b91805c..ae342c5 100644 --- a/build.sh +++ b/build.sh @@ -61,15 +61,15 @@ echo "✅ 构建成功完成!" echo "" echo "📁 文件结构:" echo " - dist/metona-editor.js (UMD格式)" -echo " - dist/metona-editor.esm.js (ES Module格式)" -echo " - dist/metona-editor.cjs.js (CommonJS格式)" +echo " - dist/metona-editor.mjs (ES Module格式)" +echo " - dist/metona-editor.cjs (CommonJS格式)" echo " - dist/metona-editor.min.js (压缩版本)" echo " - dist/metona-editor.d.ts (TypeScript声明)" echo "" echo "🚀 使用方法:" echo " 1. 浏览器: " -echo " 2. ES Module: import MeEditor from 'dist/metona-editor.esm.js'" -echo " 3. CommonJS: const MeEditor = require('dist/metona-editor.cjs.js')" +echo " 2. ES Module: import MeEditor from 'dist/metona-editor.mjs'" +echo " 3. CommonJS: const MeEditor = require('dist/metona-editor.cjs')" echo "" echo "📝 示例:" echo " 打开 site/index.html 查看官网" diff --git a/site/demo.html b/site/demo.html index 1091365..5bbe649 100644 --- a/site/demo.html +++ b/site/demo.html @@ -4,7 +4,7 @@ -