feat: v0.2.5 — 内置语法高亮 + exportPDF + benchmark + CI 并行
- 新增 src/highlight.ts 零依赖轻量高亮器:13 种语言,先转义后 着色保证安全,输出 me-hl-* 类,MeEditor.highlight 即插即用 - exportTool 新增 editor.exportPDF()(隐藏 iframe + window.print), exportHTML/exportPDF 共用 HTML 构建逻辑 - imagePaste 支持 maxSizeKB 限制,超限 toast 警告 - npm run bench 性能基准:parseMarkdown 1MB ≈ 51ms - CI 测试拆分为 parser/core/rest 三个并行 job + 跨版本 verify job - docs.html/README 补齐新 API、事件、高亮文档;取消跟踪 dist 产物; 版本 0.2.4 → 0.2.5,测试 725 → 768
This commit is contained in:
+54
-15
@@ -7,37 +7,76 @@ on:
|
|||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-parser:
|
||||||
runs-on: debian-latest
|
runs-on: debian-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [18.x, 20.x, 22.x, 24.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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 }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
continue-on-error: true
|
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
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|||||||
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
All notable changes to MetonaEditor will be documented in this file.
|
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
|
## [0.2.4] - 2026-07-25
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+2
-2
@@ -104,8 +104,8 @@ npm run build
|
|||||||
# Output in dist/
|
# Output in dist/
|
||||||
# ├── metona-editor.js UMD
|
# ├── metona-editor.js UMD
|
||||||
# ├── metona-editor.min.js UMD minified
|
# ├── metona-editor.min.js UMD minified
|
||||||
# ├── metona-editor.esm.js ES Module
|
# ├── metona-editor.mjs ES Module
|
||||||
# ├── metona-editor.cjs.js CommonJS
|
# ├── metona-editor.cjs CommonJS
|
||||||
# └── metona-editor.d.ts TypeScript declarations
|
# └── metona-editor.d.ts TypeScript declarations
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
> TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库
|
> 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)
|
[](./LICENSE)
|
||||||
[](./tests)
|
[](./tests)
|
||||||
[](./tests)
|
[](./tests)
|
||||||
[](./tsconfig.json)
|
[](./tsconfig.json)
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
- **主题系统** — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随
|
- **主题系统** — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随
|
||||||
- **国际化** — zh-CN / en-US / ja / ko 完整翻译,实例级语言隔离,远程加载翻译包
|
- **国际化** — zh-CN / en-US / ja / ko 完整翻译,实例级语言隔离,远程加载翻译包
|
||||||
- **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式、右键上下文菜单
|
- **编辑体验** — 行号装订线、智能 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 属性
|
- **引用链接** — 支持 `[text][ref]` / `![alt][ref]` 引用式链接和图片,含 title 属性
|
||||||
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
|
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
|
||||||
@@ -75,10 +76,10 @@ const editor = MeEditor.create('#editor', { mode: 'split' });
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- jsDelivr CDN (推荐) -->
|
<!-- jsDelivr CDN (推荐) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.2.4/dist/metona-editor.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.2.5/dist/metona-editor.min.js"></script>
|
||||||
|
|
||||||
<!-- unpkg CDN -->
|
<!-- unpkg CDN -->
|
||||||
<script src="https://unpkg.com/@metona-team/metona-editor@0.2.4/dist/metona-editor.min.js"></script>
|
<script src="https://unpkg.com/@metona-team/metona-editor@0.2.5/dist/metona-editor.min.js"></script>
|
||||||
|
|
||||||
<!-- Gitea 源 -->
|
<!-- Gitea 源 -->
|
||||||
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
|
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
|
||||||
@@ -95,8 +96,8 @@ const editor = MeEditor.create('#editor', { mode: 'split' });
|
|||||||
# 下载产物文件到项目中
|
# 下载产物文件到项目中
|
||||||
# dist/metona-editor.js → UMD (浏览器)
|
# dist/metona-editor.js → UMD (浏览器)
|
||||||
# dist/metona-editor.min.js → UMD 压缩
|
# dist/metona-editor.min.js → UMD 压缩
|
||||||
# dist/metona-editor.esm.js → ES Module
|
# dist/metona-editor.mjs → ES Module
|
||||||
# dist/metona-editor.cjs.js → CommonJS
|
# dist/metona-editor.cjs → CommonJS
|
||||||
```
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
@@ -106,9 +107,9 @@ const editor = MeEditor.create('#editor', { mode: 'split' });
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Node.js ESM
|
// Node.js ESM
|
||||||
import MeEditor from './dist/metona-editor.esm.js';
|
import MeEditor from './dist/metona-editor.mjs';
|
||||||
// Node.js CJS
|
// 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)
|
historyDebounce: 400, // 历史栈防抖(ms)
|
||||||
syncScroll: true, // 分屏同步滚动
|
syncScroll: true, // 分屏同步滚动
|
||||||
autoBrackets: true, // 括号自动闭合
|
autoBrackets: true, // 括号自动闭合
|
||||||
zenMode: false, // Zen 专注模式
|
zenMode: false, // 启动即进入 Zen 专注模式
|
||||||
wordWrap: true, // 自动换行
|
wordWrap: true, // 自动换行
|
||||||
maxLength: 0, // 最大字符数(0=不限)
|
maxLength: 0, // 最大字符数(0=不限)
|
||||||
|
floatingToolbar: true, // 选中文本浮动格式栏
|
||||||
|
|
||||||
// 主题与语言
|
// 主题与语言
|
||||||
theme: 'auto', // 'light' | 'dark' | 'warm' | 'auto'
|
theme: 'auto', // 'light' | 'dark' | 'warm' | 'auto'
|
||||||
@@ -217,6 +219,30 @@ editor.getHTML(): string
|
|||||||
editor.refresh(): this // 强制重渲染(内容未变时)
|
editor.refresh(): this // 强制重渲染(内容未变时)
|
||||||
editor.insert(text: string, opts?: { replace?: boolean }): this
|
editor.insert(text: string, opts?: { replace?: boolean }): this
|
||||||
editor.wrap(before: string, after?: string): 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/
|
dist/
|
||||||
├── metona-editor.js UMD(浏览器直接引入)
|
├── metona-editor.js UMD(浏览器直接引入)
|
||||||
├── metona-editor.min.js UMD 压缩版(CDN)
|
├── metona-editor.min.js UMD 压缩版(CDN)
|
||||||
├── metona-editor.esm.js ES Module
|
├── metona-editor.mjs ES Module
|
||||||
├── metona-editor.cjs.js CommonJS
|
├── metona-editor.cjs CommonJS
|
||||||
└── metona-editor.d.ts TypeScript 类型声明
|
└── metona-editor.d.ts TypeScript 类型声明
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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('完成。');
|
||||||
@@ -61,15 +61,15 @@ echo "✅ 构建成功完成!"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "📁 文件结构:"
|
echo "📁 文件结构:"
|
||||||
echo " - dist/metona-editor.js (UMD格式)"
|
echo " - dist/metona-editor.js (UMD格式)"
|
||||||
echo " - dist/metona-editor.esm.js (ES Module格式)"
|
echo " - dist/metona-editor.mjs (ES Module格式)"
|
||||||
echo " - dist/metona-editor.cjs.js (CommonJS格式)"
|
echo " - dist/metona-editor.cjs (CommonJS格式)"
|
||||||
echo " - dist/metona-editor.min.js (压缩版本)"
|
echo " - dist/metona-editor.min.js (压缩版本)"
|
||||||
echo " - dist/metona-editor.d.ts (TypeScript声明)"
|
echo " - dist/metona-editor.d.ts (TypeScript声明)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🚀 使用方法:"
|
echo "🚀 使用方法:"
|
||||||
echo " 1. 浏览器: <script src='dist/metona-editor.js'></script>"
|
echo " 1. 浏览器: <script src='dist/metona-editor.js'></script>"
|
||||||
echo " 2. ES Module: import MeEditor from 'dist/metona-editor.esm.js'"
|
echo " 2. ES Module: import MeEditor from 'dist/metona-editor.mjs'"
|
||||||
echo " 3. CommonJS: const MeEditor = require('dist/metona-editor.cjs.js')"
|
echo " 3. CommonJS: const MeEditor = require('dist/metona-editor.cjs')"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📝 示例:"
|
echo "📝 示例:"
|
||||||
echo " 打开 site/index.html 查看官网"
|
echo " 打开 site/index.html 查看官网"
|
||||||
|
|||||||
+8
-8
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"/>
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"/>
|
||||||
<title>MetonaEditor v0.2.4 — 全功能演示</title>
|
<title>MetonaEditor v0.2.5 — 全功能演示</title>
|
||||||
<style>
|
<style>
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root{--app-bg:#f5f6f8;--app-text:#1a1a2e;--app-card:#fff;--app-border:#e5e7eb;--app-accent:#3b82f6;--app-accent2:#8b5cf6;--header-bg:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);--header-text:#f1f5f9;--badge-bg:rgba(255,255,255,.12);--badge-text:#e2e8f0}
|
:root{--app-bg:#f5f6f8;--app-text:#1a1a2e;--app-card:#fff;--app-border:#e5e7eb;--app-accent:#3b82f6;--app-accent2:#8b5cf6;--header-bg:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);--header-text:#f1f5f9;--badge-bg:rgba(255,255,255,.12);--badge-text:#e2e8f0}
|
||||||
@@ -45,11 +45,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1><span class="grad">Metona</span>Editor v0.2.4</h1>
|
<h1><span class="grad">Metona</span>Editor v0.2.5</h1>
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<span class="badge">TypeScript</span>
|
<span class="badge">TypeScript</span>
|
||||||
<span class="badge">零运行时依赖</span>
|
<span class="badge">零运行时依赖</span>
|
||||||
<span class="badge">725 tests</span>
|
<span class="badge">768 tests</span>
|
||||||
<span class="badge">6 个插件</span>
|
<span class="badge">6 个插件</span>
|
||||||
<span class="badge">桌面端优先</span>
|
<span class="badge">桌面端优先</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
MetonaEditor v0.2.4 · TypeScript · <a href="index.html">首页</a> · <a href="docs.html">API 文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT
|
MetonaEditor v0.2.5 · TypeScript · <a href="index.html">首页</a> · <a href="docs.html">API 文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
@@ -99,12 +99,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
|
|||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
var demoMd=[
|
var demoMd=[
|
||||||
'# 🚀 MetonaEditor v0.2.4 全功能演示',
|
'# 🚀 MetonaEditor v0.2.5 全功能演示',
|
||||||
'',
|
'',
|
||||||
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
|
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
|
||||||
'> 全模块 TypeScript 严格模式,12 个源文件,725 个测试全部通过。',
|
'> 全模块 TypeScript 严格模式,12 个源文件,768 个测试全部通过。',
|
||||||
'',
|
'',
|
||||||
'## ✨ v0.2.4 新特性',
|
'## ✨ v0.2.5 新特性',
|
||||||
'',
|
'',
|
||||||
'- 浮动格式工具栏:选中文本自动弹出 bold/italic/code/link',
|
'- 浮动格式工具栏:选中文本自动弹出 bold/italic/code/link',
|
||||||
'- 新增 `getSelectedText()` `getCursorPosition()` 等 10+ API',
|
'- 新增 `getSelectedText()` `getCursorPosition()` 等 10+ API',
|
||||||
@@ -139,7 +139,7 @@ var demoMd=[
|
|||||||
'',
|
'',
|
||||||
'| 版本 | 日期 | 测试 | 主题 |',
|
'| 版本 | 日期 | 测试 | 主题 |',
|
||||||
'| :--- | :---: | ---: | --- |',
|
'| :--- | :---: | ---: | --- |',
|
||||||
'| v0.2.4 | 2026-07 | 725 | 浮动工具栏+新API |',
|
'| v0.2.5 | 2026-08 | 768 | 浮动工具栏+新API |',
|
||||||
'| v0.2.2 | 2026-07 | 694 | 法语+表格格式化 |',
|
'| v0.2.2 | 2026-07 | 694 | 法语+表格格式化 |',
|
||||||
'| v0.2.1 | 2026-07 | 684 | 引用链接+右键菜单 |',
|
'| v0.2.1 | 2026-07 | 684 | 引用链接+右键菜单 |',
|
||||||
'| v0.2.0 | 2026-07 | 610 | TypeScript 重构 |',
|
'| v0.2.0 | 2026-07 | 610 | TypeScript 重构 |',
|
||||||
|
|||||||
+47
-6
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>"/>
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>"/>
|
||||||
<title>MetonaEditor v0.2.4 · 文档</title>
|
<title>MetonaEditor v0.2.5 · 文档</title>
|
||||||
<style>
|
<style>
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%)}
|
:root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%)}
|
||||||
@@ -65,7 +65,7 @@ footer a{color:var(--accent);text-decoration:none}
|
|||||||
|
|
||||||
<header class="docs-header">
|
<header class="docs-header">
|
||||||
<h1><span class="grad">API 文档</span></h1>
|
<h1><span class="grad">API 文档</span></h1>
|
||||||
<p class="sub">MetonaEditor v0.2.4 · TypeScript · 完整配置、API 与使用指南</p>
|
<p class="sub">MetonaEditor v0.2.5 · TypeScript · 完整配置、API 与使用指南</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -78,6 +78,7 @@ footer a{color:var(--accent);text-decoration:none}
|
|||||||
<a href="#toolbar">工具栏</a>
|
<a href="#toolbar">工具栏</a>
|
||||||
<div class="toc-group">实例 API</div>
|
<div class="toc-group">实例 API</div>
|
||||||
<a href="#api-content">内容操作</a>
|
<a href="#api-content">内容操作</a>
|
||||||
|
<a href="#api-cursor">光标与选区</a>
|
||||||
<a href="#api-exec">命令执行</a>
|
<a href="#api-exec">命令执行</a>
|
||||||
<a href="#api-history">历史栈</a>
|
<a href="#api-history">历史栈</a>
|
||||||
<a href="#api-mode">模式 / 全屏 / Zen</a>
|
<a href="#api-mode">模式 / 全屏 / Zen</a>
|
||||||
@@ -89,6 +90,8 @@ footer a{color:var(--accent);text-decoration:none}
|
|||||||
<a href="#api-destroy">生命周期</a>
|
<a href="#api-destroy">生命周期</a>
|
||||||
<div class="toc-group">静态 API</div>
|
<div class="toc-group">静态 API</div>
|
||||||
<a href="#static-api">全局函数</a>
|
<a href="#static-api">全局函数</a>
|
||||||
|
<div class="toc-group">语法高亮</div>
|
||||||
|
<a href="#highlight">内置高亮器</a>
|
||||||
<div class="toc-group">解析器</div>
|
<div class="toc-group">解析器</div>
|
||||||
<a href="#parser-syntax">语法参考</a>
|
<a href="#parser-syntax">语法参考</a>
|
||||||
<a href="#parser-api">解析器 API</a>
|
<a href="#parser-api">解析器 API</a>
|
||||||
@@ -142,6 +145,8 @@ footer a{color:var(--accent);text-decoration:none}
|
|||||||
historyLimit<span class="c-punc">:</span> <span class="c-num">100</span><span class="c-punc">,</span> historyDebounce<span class="c-punc">:</span> <span class="c-num">400</span><span class="c-punc">,</span>
|
historyLimit<span class="c-punc">:</span> <span class="c-num">100</span><span class="c-punc">,</span> historyDebounce<span class="c-punc">:</span> <span class="c-num">400</span><span class="c-punc">,</span>
|
||||||
syncScroll<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span> autoBrackets<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span>
|
syncScroll<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span> autoBrackets<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span>
|
||||||
zenMode<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> wordWrap<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span>
|
zenMode<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> wordWrap<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span>
|
||||||
|
maxLength<span class="c-punc">:</span> <span class="c-num">0</span><span class="c-punc">,</span> <span class="c-com">// 最大字符数(0=不限)</span>
|
||||||
|
floatingToolbar<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span> <span class="c-com">// 选中文本浮动格式栏</span>
|
||||||
|
|
||||||
<span class="c-com">// 主题 / 语言</span>
|
<span class="c-com">// 主题 / 语言</span>
|
||||||
theme<span class="c-punc">:</span> <span class="c-str">'auto'</span><span class="c-punc">,</span> locale<span class="c-punc">:</span> <span class="c-str">'zh-CN'</span><span class="c-punc">,</span>
|
theme<span class="c-punc">:</span> <span class="c-str">'auto'</span><span class="c-punc">,</span> locale<span class="c-punc">:</span> <span class="c-str">'zh-CN'</span><span class="c-punc">,</span>
|
||||||
@@ -194,9 +199,23 @@ editor<span class="c-punc">.</span><span class="c-fn">getHTML</span><span class=
|
|||||||
editor<span class="c-punc">.</span><span class="c-fn">refresh</span><span class="c-punc">();</span> <span class="c-com">// 强制重渲染</span>
|
editor<span class="c-punc">.</span><span class="c-fn">refresh</span><span class="c-punc">();</span> <span class="c-com">// 强制重渲染</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">insert</span><span class="c-punc">(</span>text<span class="c-punc">,</span> <span class="c-punc">{</span> replace <span class="c-punc">});</span> <span class="c-com">// => this</span>
|
editor<span class="c-punc">.</span><span class="c-fn">insert</span><span class="c-punc">(</span>text<span class="c-punc">,</span> <span class="c-punc">{</span> replace <span class="c-punc">});</span> <span class="c-com">// => this</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">wrap</span><span class="c-punc">(</span>before<span class="c-punc">,</span> after<span class="c-punc">);</span> <span class="c-com">// => this</span>
|
editor<span class="c-punc">.</span><span class="c-fn">wrap</span><span class="c-punc">(</span>before<span class="c-punc">,</span> after<span class="c-punc">);</span> <span class="c-com">// => this</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">replaceAll</span><span class="c-punc">(</span>search<span class="c-punc">,</span> replace<span class="c-punc">,</span> caseSensitive<span class="c-punc">)</span> <span class="c-com">// => 替换数</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">replaceAllRegex</span><span class="c-punc">(</span>pattern<span class="c-punc">,</span> replace<span class="c-punc">)</span> <span class="c-com">// => 替换数</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">lineCount</span><span class="c-punc">();</span> <span class="c-fn">getLine</span><span class="c-punc">(</span>n<span class="c-punc">);</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">copyAsMarkdown</span><span class="c-punc">();</span> <span class="c-fn">copyAsHTML</span><span class="c-punc">();</span> <span class="c-com">// 剪贴板</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">focus</span><span class="c-punc">();</span> <span class="c-fn">blur</span><span class="c-punc">();</span></pre>
|
editor<span class="c-punc">.</span><span class="c-fn">focus</span><span class="c-punc">();</span> <span class="c-fn">blur</span><span class="c-punc">();</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="api-cursor"><h3>光标与选区</h3>
|
||||||
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">getSelectedText</span><span class="c-punc">();</span> <span class="c-com">// => string</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">getCursorPosition</span><span class="c-punc">();</span> <span class="c-com">// => { line, column }</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">setCursorPosition</span><span class="c-punc">(</span>line<span class="c-punc">,</span> column<span class="c-punc">);</span> <span class="c-com">// => this</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">scrollToLine</span><span class="c-punc">(</span>line<span class="c-punc">);</span> <span class="c-com">// => this</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">selectLine</span><span class="c-punc">(</span>line<span class="c-punc">);</span> <span class="c-com">// => this</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">selectAll</span><span class="c-punc">();</span> <span class="c-com">// => this</span></pre>
|
||||||
|
<p>伴随事件 <code>selectionChange</code>(<code>{ start, end, text }</code>)与 <code>cursorMove</code>(<code>{ line, column }</code>)。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="api-exec"><h3>命令执行</h3>
|
<section id="api-exec"><h3>命令执行</h3>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">exec</span><span class="c-punc">(</span>action<span class="c-punc">)</span> <span class="c-com">// => this, 支持链式</span></pre>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">exec</span><span class="c-punc">(</span>action<span class="c-punc">)</span> <span class="c-com">// => this, 支持链式</span></pre>
|
||||||
<table>
|
<table>
|
||||||
@@ -227,11 +246,12 @@ editor<span class="c-punc">.</span><span class="c-fn">focus</span><span class="c
|
|||||||
editor<span class="c-punc">.</span><span class="c-fn">canUndo</span><span class="c-punc">();</span> editor<span class="c-punc">.</span><span class="c-fn">canRedo</span><span class="c-punc">();</span></pre>
|
editor<span class="c-punc">.</span><span class="c-fn">canUndo</span><span class="c-punc">();</span> editor<span class="c-punc">.</span><span class="c-fn">canRedo</span><span class="c-punc">();</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-mode"><h3>模式 / 全屏 / Zen</h3>
|
<section id="api-mode"><h3>模式 / 全屏 / Zen / 浮动工具栏</h3>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">setMode</span><span class="c-punc">(</span><span class="c-str">'split'</span><span class="c-punc">);</span> editor<span class="c-punc">.</span><span class="c-fn">getMode</span><span class="c-punc">();</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">setMode</span><span class="c-punc">(</span><span class="c-str">'split'</span><span class="c-punc">);</span> editor<span class="c-punc">.</span><span class="c-fn">getMode</span><span class="c-punc">();</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">toggleFullscreen</span><span class="c-punc">();</span> <span class="c-fn">isFullscreen</span><span class="c-punc">();</span> <span class="c-fn">exitFullscreen</span><span class="c-punc">();</span>
|
editor<span class="c-punc">.</span><span class="c-fn">toggleFullscreen</span><span class="c-punc">();</span> <span class="c-fn">isFullscreen</span><span class="c-punc">();</span> <span class="c-fn">exitFullscreen</span><span class="c-punc">();</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">toggleZen</span><span class="c-punc">();</span> <span class="c-fn">isZen</span><span class="c-punc">();</span>
|
editor<span class="c-punc">.</span><span class="c-fn">toggleZen</span><span class="c-punc">();</span> <span class="c-fn">isZen</span><span class="c-punc">();</span> <span class="c-com">// 或配置 zenMode: true 启动即进入</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">toggleWordWrap</span><span class="c-punc">();</span> <span class="c-fn">setWordWrap</span><span class="c-punc">(</span><span class="c-kw">true</span><span class="c-punc">);</span> <span class="c-fn">isWordWrap</span><span class="c-punc">();</span></pre>
|
editor<span class="c-punc">.</span><span class="c-fn">toggleWordWrap</span><span class="c-punc">();</span> <span class="c-fn">setWordWrap</span><span class="c-punc">(</span><span class="c-kw">true</span><span class="c-punc">);</span> <span class="c-fn">isWordWrap</span><span class="c-punc">();</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">toggleFloatingToolbar</span><span class="c-punc">();</span> <span class="c-fn">isFloatingToolbar</span><span class="c-punc">();</span> <span class="c-com">// 选中文本格式栏</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-stats"><h3>统计与状态</h3>
|
<section id="api-stats"><h3>统计与状态</h3>
|
||||||
@@ -251,6 +271,7 @@ editor<span class="c-punc">.</span><span class="c-fn">setReadOnly</span><span cl
|
|||||||
<tr><th>事件</th><th>触发时机</th><th>参数</th></tr>
|
<tr><th>事件</th><th>触发时机</th><th>参数</th></tr>
|
||||||
<tr><td><code>input</code></td><td>textarea 输入</td><td><code>(value, editor)</code></td></tr>
|
<tr><td><code>input</code></td><td>textarea 输入</td><td><code>(value, editor)</code></td></tr>
|
||||||
<tr><td><code>change</code></td><td>内容变化</td><td><code>(value, editor)</code></td></tr>
|
<tr><td><code>change</code></td><td>内容变化</td><td><code>(value, editor)</code></td></tr>
|
||||||
|
<tr><td><code>beforeChange</code> / <code>afterChange</code></td><td>内容变更前后</td><td><code>(oldValue, newValue, editor)</code></td></tr>
|
||||||
<tr><td><code>focus</code> / <code>blur</code></td><td>聚焦/失焦</td><td><code>(editor)</code></td></tr>
|
<tr><td><code>focus</code> / <code>blur</code></td><td>聚焦/失焦</td><td><code>(editor)</code></td></tr>
|
||||||
<tr><td><code>save</code></td><td>Ctrl+S</td><td><code>(editor)</code></td></tr>
|
<tr><td><code>save</code></td><td>Ctrl+S</td><td><code>(editor)</code></td></tr>
|
||||||
<tr><td><code>modeChange</code></td><td>模式切换</td><td><code>(mode, editor)</code></td></tr>
|
<tr><td><code>modeChange</code></td><td>模式切换</td><td><code>(mode, editor)</code></td></tr>
|
||||||
@@ -258,10 +279,14 @@ editor<span class="c-punc">.</span><span class="c-fn">setReadOnly</span><span cl
|
|||||||
<tr><td><code>themeChange</code></td><td>主题变更</td><td><code>({ theme, resolved, config })</code></td></tr>
|
<tr><td><code>themeChange</code></td><td>主题变更</td><td><code>({ theme, resolved, config })</code></td></tr>
|
||||||
<tr><td><code>localeChange</code></td><td>语言变更</td><td><code>({ locale, direction })</code></td></tr>
|
<tr><td><code>localeChange</code></td><td>语言变更</td><td><code>({ locale, direction })</code></td></tr>
|
||||||
<tr><td><code>zenChange</code></td><td>Zen 切换</td><td><code>(zen, editor)</code></td></tr>
|
<tr><td><code>zenChange</code></td><td>Zen 切换</td><td><code>(zen, editor)</code></td></tr>
|
||||||
|
<tr><td><code>selectionChange</code></td><td>选区变化</td><td><code>({ start, end, text })</code></td></tr>
|
||||||
|
<tr><td><code>cursorMove</code></td><td>光标移动</td><td><code>({ line, column })</code></td></tr>
|
||||||
<tr><td><code>destroy</code></td><td>销毁</td><td><code>()</code></td></tr>
|
<tr><td><code>destroy</code></td><td>销毁</td><td><code>()</code></td></tr>
|
||||||
<tr><td><code>beforeRender</code> / <code>afterRender</code></td><td>渲染前后</td><td><code>(editor)</code></td></tr>
|
<tr><td><code>beforeRender</code> / <code>afterRender</code></td><td>渲染前后</td><td><code>(editor)</code></td></tr>
|
||||||
<tr><td><code>autosave</code></td><td>autoSave 触发</td><td><code>({ key, value })</code></td></tr>
|
<tr><td><code>autosave</code></td><td>autoSave 触发</td><td><code>({ key, value })</code></td></tr>
|
||||||
<tr><td><code>linkClick</code></td><td>预览链接点击</td><td><code>({ href, text })</code></td></tr>
|
<tr><td><code>linkClick</code></td><td>预览链接点击</td><td><code>({ href, text })</code></td></tr>
|
||||||
|
<tr><td><code>copy</code></td><td>复制到剪贴板</td><td><code>({ type })</code></td></tr>
|
||||||
|
<tr><td><code>fileOpened</code> / <code>fileSaved</code></td><td>fileSystem 读写</td><td><code>({ name, handle })</code></td></tr>
|
||||||
</table>
|
</table>
|
||||||
<h4>全局钩子</h4>
|
<h4>全局钩子</h4>
|
||||||
<table>
|
<table>
|
||||||
@@ -313,6 +338,22 @@ MeEditor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span cl
|
|||||||
<span class="c-kw">import</span> <span class="c-punc">{</span> parseMarkdown<span class="c-punc">,</span> parseTokens<span class="c-punc">,</span> renderTokens <span class="c-punc">}</span> <span class="c-kw">from</span> <span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">;</span></pre>
|
<span class="c-kw">import</span> <span class="c-punc">{</span> parseMarkdown<span class="c-punc">,</span> parseTokens<span class="c-punc">,</span> renderTokens <span class="c-punc">}</span> <span class="c-kw">from</span> <span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">;</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 语法高亮 -->
|
||||||
|
<section id="highlight"><h2>内置语法高亮</h2>
|
||||||
|
<p>v0.2.5 起内置零依赖轻量高亮器,支持 js / ts / tsx / jsx / python / bash / css / html / json / yaml / markdown / java / go / rust。</p>
|
||||||
|
<pre><span class="c-kw">import</span> MeEditor <span class="c-kw">from</span> <span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">;</span>
|
||||||
|
|
||||||
|
<span class="c-com">// 方式一:作为 highlight 钩子(代码块自动高亮)</span>
|
||||||
|
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span><span class="c-str">'#editor'</span><span class="c-punc">,</span> <span class="c-punc">{</span> highlight<span class="c-punc">:</span> MeEditor<span class="c-punc">.</span><span class="c-fn">highlight</span> <span class="c-punc">});</span>
|
||||||
|
|
||||||
|
<span class="c-com">// 方式二:独立调用</span>
|
||||||
|
<span class="c-kw">const</span> html <span class="c-punc">=</span> MeEditor<span class="c-punc">.</span><span class="c-fn">highlight</span><span class="c-punc">(</span>code<span class="c-punc">,</span> <span class="c-str">'typescript'</span><span class="c-punc">);</span>
|
||||||
|
|
||||||
|
<span class="c-com">// 注册自定义语言</span>
|
||||||
|
MeEditor<span class="c-punc">.</span><span class="c-fn">registerLanguage</span><span class="c-punc">(</span><span class="c-str">'myLang'</span><span class="c-punc">,</span> <span class="c-punc">{</span> keywords<span class="c-punc">:</span> <span class="c-punc">[</span><span class="c-str">'kw'</span><span class="c-punc">]</span><span class="c-punc">,</span> builtins<span class="c-punc">:</span> <span class="c-punc">[]</span> <span class="c-punc">});</span></pre>
|
||||||
|
<p>高亮输出使用 <code>me-hl-keyword</code> / <code>me-hl-string</code> / <code>me-hl-comment</code> / <code>me-hl-number</code> / <code>me-hl-builtin</code> / <code>me-hl-function</code> 类,可用 CSS 变量或选择器定制颜色。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 解析器语法 -->
|
<!-- 解析器语法 -->
|
||||||
<section id="parser-syntax"><h2>解析器语法参考</h2>
|
<section id="parser-syntax"><h2>解析器语法参考</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -433,7 +474,7 @@ i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
MetonaEditor v0.2.4 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
MetonaEditor v0.2.5 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>"/>
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>"/>
|
||||||
<title>MetonaEditor v0.2.4 · TypeScript · 桌面端 Markdown 编辑器</title>
|
<title>MetonaEditor v0.2.5 · TypeScript · 桌面端 Markdown 编辑器</title>
|
||||||
<style>
|
<style>
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-3:#ec4899;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%);--shadow:0 20px 50px -20px rgba(0,0,0,.5)}
|
:root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-3:#ec4899;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%);--shadow:0 20px 50px -20px rgba(0,0,0,.5)}
|
||||||
@@ -106,10 +106,10 @@ footer a:hover{text-decoration:underline}
|
|||||||
<h1>TypeScript 重构 · 零依赖<br/><span class="grad">桌面端 Markdown 编辑器</span></h1>
|
<h1>TypeScript 重构 · 零依赖<br/><span class="grad">桌面端 Markdown 编辑器</span></h1>
|
||||||
<p class="tagline">全模块 TypeScript 严格模式,内置自研解析器(99% 行覆盖率),6 个预设插件,中文优先。为桌面端现代 Web 应用而生。</p>
|
<p class="tagline">全模块 TypeScript 严格模式,内置自研解析器(99% 行覆盖率),6 个预设插件,中文优先。为桌面端现代 Web 应用而生。</p>
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<span class="badge accent">v0.2.4</span>
|
<span class="badge accent">v0.2.5</span>
|
||||||
<span class="badge">TypeScript</span>
|
<span class="badge">TypeScript</span>
|
||||||
<span class="badge">零运行时依赖</span>
|
<span class="badge">零运行时依赖</span>
|
||||||
<span class="badge">725 tests</span>
|
<span class="badge">768 tests</span>
|
||||||
<span class="badge">桌面端优先</span>
|
<span class="badge">桌面端优先</span>
|
||||||
<span class="badge">MIT</span>
|
<span class="badge">MIT</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,7 +120,7 @@ footer a:hover{text-decoration:underline}
|
|||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat"><div class="stat-num">0</div><div class="stat-label">运行时依赖</div></div>
|
<div class="stat"><div class="stat-num">0</div><div class="stat-label">运行时依赖</div></div>
|
||||||
<div class="stat"><div class="stat-num">99%</div><div class="stat-label">解析器行覆盖</div></div>
|
<div class="stat"><div class="stat-num">99%</div><div class="stat-label">解析器行覆盖</div></div>
|
||||||
<div class="stat"><div class="stat-num">725</div><div class="stat-label">单元测试</div></div>
|
<div class="stat"><div class="stat-num">768</div><div class="stat-label">单元测试</div></div>
|
||||||
<div class="stat"><div class="stat-num">~30KB</div><div class="stat-label">gzip 体积</div></div>
|
<div class="stat"><div class="stat-num">~30KB</div><div class="stat-label">gzip 体积</div></div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -165,7 +165,7 @@ footer a:hover{text-decoration:underline}
|
|||||||
<span class="c-kw">const</span> MeEditor <span class="c-punc">=</span> <span class="c-fn">require</span><span class="c-punc">(</span><span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">);</span>
|
<span class="c-kw">const</span> MeEditor <span class="c-punc">=</span> <span class="c-fn">require</span><span class="c-punc">(</span><span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">);</span>
|
||||||
|
|
||||||
<span class="c-com">// 方式二:CDN 引入</span>
|
<span class="c-com">// 方式二:CDN 引入</span>
|
||||||
<span class="c-com"><script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.2.4/dist/metona-editor.min.js"></script></span>
|
<span class="c-com"><script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.2.5/dist/metona-editor.min.js"></script></span>
|
||||||
|
|
||||||
<span class="c-com">// 方式三:本地文件</span>
|
<span class="c-com">// 方式三:本地文件</span>
|
||||||
<span class="c-com"><script src="./dist/metona-editor.min.js"></script></span>
|
<span class="c-com"><script src="./dist/metona-editor.min.js"></script></span>
|
||||||
@@ -227,7 +227,7 @@ editor<span class="c-punc">.</span><span class="c-fn">exec</span><span class="c-
|
|||||||
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a>
|
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a>
|
||||||
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor/issues" target="_blank" rel="noopener">问题反馈</a>
|
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor/issues" target="_blank" rel="noopener">问题反馈</a>
|
||||||
</div>
|
</div>
|
||||||
<div>MetonaEditor v0.2.4 · TypeScript · MIT License · MetonaTeam</div>
|
<div>MetonaEditor v0.2.5 · TypeScript · MIT License · MetonaTeam</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ MeEditor.themes.switchTheme('dark');
|
|||||||
MeEditor.create('#editor', {
|
MeEditor.create('#editor', {
|
||||||
mode: 'split', height: 480, theme: 'dark',
|
mode: 'split', height: 480, theme: 'dark',
|
||||||
value: [
|
value: [
|
||||||
'# 欢迎使用 MetonaEditor v0.2.4',
|
'# 欢迎使用 MetonaEditor v0.2.5',
|
||||||
'',
|
'',
|
||||||
'一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。',
|
'一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。',
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* MetonaEditor Highlight — built-in lightweight syntax highlighter
|
||||||
|
* @module highlight
|
||||||
|
* @version 0.2.5
|
||||||
|
*
|
||||||
|
* Zero-dependency tokenizer for common languages. Safe by construction:
|
||||||
|
* input is HTML-escaped first, then annotated with <span> classes on the
|
||||||
|
* escaped text (no raw HTML can leak through).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { escapeHTML } from './utils';
|
||||||
|
|
||||||
|
// ============ Token rules ============
|
||||||
|
|
||||||
|
interface HighlightRule {
|
||||||
|
cls: string;
|
||||||
|
re: RegExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguageDef {
|
||||||
|
keywords: string[];
|
||||||
|
builtins: string[];
|
||||||
|
hasBlocks?: boolean; // /* */ block comments
|
||||||
|
hasHashComments?: boolean; // # comments (python/bash)
|
||||||
|
hasTemplateStrings?: boolean; // `...` (js/ts)
|
||||||
|
singleQuoteStrings?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
const buildRules = (lang: LanguageDef): HighlightRule[] => {
|
||||||
|
const rules: HighlightRule[] = [];
|
||||||
|
|
||||||
|
// Comments (highest priority; protects keywords/strings inside)
|
||||||
|
if (lang.hasHashComments) {
|
||||||
|
rules.push({ cls: 'me-hl-comment', re: /#[^\n]*/y });
|
||||||
|
} else {
|
||||||
|
rules.push({ cls: 'me-hl-comment', re: /\/\/[^\n]*/y });
|
||||||
|
}
|
||||||
|
if (lang.hasBlocks) rules.push({ cls: 'me-hl-comment', re: /\/\*[\s\S]*?\*\//y });
|
||||||
|
|
||||||
|
// Strings (double + single quotes with backslash escapes)
|
||||||
|
const strRe = lang.singleQuoteStrings === false
|
||||||
|
? /"(?:\\.|[^"\\\n])*"/y
|
||||||
|
: /"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'/y;
|
||||||
|
rules.push({ cls: 'me-hl-string', re: strRe });
|
||||||
|
if (lang.hasTemplateStrings) rules.push({ cls: 'me-hl-string', re: /`(?:\\.|[^`\\])*`/y });
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
rules.push({ cls: 'me-hl-number', re: /\b0x[\da-fA-F_]+\b|\b\d[\d_]*(?:\.\d+)?(?:e[+-]?\d+)?\b/y });
|
||||||
|
|
||||||
|
// Keywords
|
||||||
|
if (lang.keywords.length) {
|
||||||
|
rules.push({ cls: 'me-hl-keyword', re: new RegExp(`\\b(?:${lang.keywords.map(escapeRe).join('|')})\\b`, 'y') });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builtins / globals
|
||||||
|
if (lang.builtins.length) {
|
||||||
|
rules.push({ cls: 'me-hl-builtin', re: new RegExp(`\\b(?:${lang.builtins.map(escapeRe).join('|')})\\b`, 'y') });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function calls: identifier immediately followed by (
|
||||||
|
rules.push({ cls: 'me-hl-function', re: /[A-Za-z_$][\w$]*(?=\s*\()/y });
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenize = (text: string, rules: HighlightRule[]): string => {
|
||||||
|
let out = '';
|
||||||
|
let pos = 0;
|
||||||
|
const len = text.length;
|
||||||
|
while (pos < len) {
|
||||||
|
let matched = false;
|
||||||
|
for (const rule of rules) {
|
||||||
|
rule.re.lastIndex = pos;
|
||||||
|
const m = rule.re.exec(text);
|
||||||
|
if (m && m.index === pos && m[0].length > 0) {
|
||||||
|
out += `<span class="${rule.cls}">${m[0]}</span>`;
|
||||||
|
pos += m[0].length;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
out += text[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Language definitions ============
|
||||||
|
|
||||||
|
const LANGUAGES: Record<string, LanguageDef> = {
|
||||||
|
javascript: {
|
||||||
|
keywords: ['async','await','break','case','catch','class','const','continue','debugger','default','delete','do','else','export','extends','finally','for','from','function','get','if','import','in','instanceof','let','new','of','return','set','static','super','switch','this','throw','try','typeof','var','void','while','with','yield'],
|
||||||
|
builtins: ['console','document','window','globalThis','Math','JSON','Promise','Object','Array','String','Number','Boolean','Map','Set','Symbol','RegExp','Date','Error','parseInt','parseFloat','isNaN','setTimeout','setInterval','fetch','require','module','exports','process','Buffer'],
|
||||||
|
hasBlocks: true,
|
||||||
|
hasTemplateStrings: true,
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
keywords: ['abstract','any','as','async','await','boolean','break','case','catch','class','const','continue','debugger','declare','default','delete','do','else','enum','export','extends','finally','for','from','function','get','if','implements','import','in','infer','instanceof','interface','is','keyof','let','namespace','never','new','of','override','private','protected','public','readonly','return','satisfies','set','static','string','super','switch','symbol','this','throw','try','type','typeof','undefined','unique','unknown','var','void','while','with','yield'],
|
||||||
|
builtins: ['console','document','window','globalThis','Math','JSON','Promise','Object','Array','String','Number','Boolean','Map','Set','Symbol','RegExp','Date','Error','parseInt','parseFloat','setTimeout','setInterval','fetch','require','module','exports','process','Buffer'],
|
||||||
|
hasBlocks: true,
|
||||||
|
hasTemplateStrings: true,
|
||||||
|
},
|
||||||
|
jsx: {
|
||||||
|
keywords: ['async','await','break','case','catch','class','const','continue','default','do','else','export','extends','finally','for','from','function','if','import','in','instanceof','let','new','of','return','static','super','switch','this','throw','try','typeof','var','void','while','with','yield','useState','useEffect','useRef','useMemo','useCallback'],
|
||||||
|
builtins: ['console','document','window','Math','JSON','Promise','Object','Array','String','Number','Map','Set','React'],
|
||||||
|
hasBlocks: true,
|
||||||
|
hasTemplateStrings: true,
|
||||||
|
},
|
||||||
|
tsx: {
|
||||||
|
keywords: ['abstract','any','as','async','await','boolean','break','case','catch','class','const','continue','default','declare','do','else','enum','export','extends','finally','for','from','function','if','implements','import','in','infer','instanceof','interface','is','keyof','let','namespace','never','new','of','override','private','protected','public','readonly','return','static','string','super','switch','symbol','this','throw','try','type','typeof','undefined','unknown','var','void','while','with','yield','useState','useEffect','useRef'],
|
||||||
|
builtins: ['console','document','window','Math','JSON','Promise','Object','Array','String','Number','Map','Set','React'],
|
||||||
|
hasBlocks: true,
|
||||||
|
hasTemplateStrings: true,
|
||||||
|
},
|
||||||
|
python: {
|
||||||
|
keywords: ['and','as','assert','async','await','break','class','continue','def','del','elif','else','except','False','finally','for','from','global','if','import','in','is','lambda','None','nonlocal','not','or','pass','raise','return','True','try','while','with','yield'],
|
||||||
|
builtins: ['print','len','range','type','str','int','float','list','dict','set','tuple','bool','enumerate','zip','map','filter','sum','min','max','abs','round','open','input','repr','sorted','object','super','self'],
|
||||||
|
hasHashComments: true,
|
||||||
|
},
|
||||||
|
bash: {
|
||||||
|
keywords: ['if','then','else','elif','fi','for','while','until','do','done','case','esac','function','in','return','break','continue','export','local','readonly','set','unset','shift','source','alias','declare','echo','exit'],
|
||||||
|
builtins: ['echo','printf','cd','pwd','ls','mkdir','rm','cp','mv','cat','grep','sed','awk','curl','wget','npm','node','git','docker','sudo','find','xargs','tar','zip','unzip','chmod','chown','touch','head','tail','wc','sort','uniq','cut','tee','ps','kill','top','clear','history','man','which'],
|
||||||
|
hasHashComments: true,
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
shell: { hasHashComments: true, keywords: ['if','then','else','elif','fi','for','while','do','done','case','in','function','return','break','continue','export','local','echo','exit'], builtins: ['echo','cd','pwd','ls','rm','cp','mv','cat','grep','sed','awk','curl','wget','git','sudo','find','chmod','chown','touch','head','tail','wc','sort','tee','ps','kill','clear','man','which'], singleQuoteStrings: false },
|
||||||
|
css: {
|
||||||
|
keywords: ['important','inherit','initial','unset','none','auto','absolute','relative','fixed','static','sticky','flex','grid','block','inline','inline-block','hidden','visible','bold','normal','italic','solid','dashed','dotted','transparent','center','left','right','top','bottom','wrap','nowrap','repeat','cover','contain','pointer','auto'],
|
||||||
|
builtins: ['var','calc','min','max','clamp','rgb','rgba','hsl','hsla','url','linear-gradient','radial-gradient','translate','scale','rotate','repeat','minmax','fit-content','attr','counter'],
|
||||||
|
hasBlocks: true,
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
keywords: ['html','head','body','div','span','p','a','img','ul','ol','li','table','thead','tbody','tr','th','td','h1','h2','h3','h4','h5','h6','section','article','nav','header','footer','main','aside','form','input','button','select','option','textarea','label','script','style','link','meta','title','iframe','video','audio','canvas','svg','strong','em','code','pre','blockquote','br','hr','template'],
|
||||||
|
builtins: [],
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
keywords: ['true','false','null'],
|
||||||
|
builtins: [],
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
yaml: {
|
||||||
|
keywords: ['true','false','null','yes','no','on','off'],
|
||||||
|
builtins: [],
|
||||||
|
hasHashComments: true,
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
keywords: [],
|
||||||
|
builtins: [],
|
||||||
|
hasHashComments: true,
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
md: { keywords: [], builtins: [], hasHashComments: true, singleQuoteStrings: false },
|
||||||
|
java: {
|
||||||
|
keywords: ['abstract','boolean','break','byte','case','catch','char','class','const','continue','default','do','double','else','enum','extends','final','finally','float','for','goto','if','implements','import','instanceof','int','interface','long','native','new','package','private','protected','public','return','short','static','strictfp','super','switch','synchronized','this','throw','throws','transient','try','void','volatile','while','var'],
|
||||||
|
builtins: ['System','String','Integer','Double','Math','Object','Class','Exception','Thread','Arrays','List','ArrayList','Map','HashMap','Set','HashSet','Optional','Stream','Collectors','Objects'],
|
||||||
|
hasBlocks: true,
|
||||||
|
},
|
||||||
|
go: {
|
||||||
|
keywords: ['break','case','chan','const','continue','default','defer','else','fallthrough','for','func','go','goto','if','import','interface','map','package','range','return','select','struct','switch','type','var'],
|
||||||
|
builtins: ['fmt','len','cap','make','new','append','copy','panic','recover','error','string','int','int64','uint','bool','byte','rune','float64','nil','print','println','Close','Error'],
|
||||||
|
hasBlocks: true,
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
rust: {
|
||||||
|
keywords: ['as','async','await','break','const','continue','crate','dyn','else','enum','extern','false','fn','for','if','impl','in','let','loop','match','mod','move','mut','pub','ref','return','self','Self','static','struct','super','trait','true','type','unsafe','use','where','while'],
|
||||||
|
builtins: ['println','print','eprintln','format','vec','String','str','Option','Result','Some','None','Ok','Err','Vec','Box','Rc','Arc','HashMap','Iterator','match'],
|
||||||
|
hasBlocks: true,
|
||||||
|
singleQuoteStrings: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Public API ============
|
||||||
|
|
||||||
|
const aliasMap: Record<string, string> = {
|
||||||
|
js: 'javascript', ts: 'typescript', tsx: 'tsx', jsx: 'jsx',
|
||||||
|
py: 'python', sh: 'bash', shell: 'shell', zsh: 'bash',
|
||||||
|
yml: 'yaml', mjs: 'javascript', cjs: 'javascript', md: 'markdown',
|
||||||
|
htm: 'html', c: 'go', cpp: 'go', cs: 'java',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Map a code-block language hint to the canonical language name */
|
||||||
|
export const normalizeLanguage = (lang: string): string => {
|
||||||
|
const l = (lang || '').trim().toLowerCase();
|
||||||
|
if (!l) return '';
|
||||||
|
return aliasMap[l] || (LANGUAGES[l] ? l : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Highlight code with the built-in tokenizer. Falls back to escaped plain text. */
|
||||||
|
export const highlight = (code: string, lang: string): string => {
|
||||||
|
const canonical = normalizeLanguage(lang);
|
||||||
|
const def = LANGUAGES[canonical];
|
||||||
|
const escaped = escapeHTML(code);
|
||||||
|
if (!def) return escaped;
|
||||||
|
return tokenize(escaped, buildRules(def));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Register or override a language definition */
|
||||||
|
export const registerLanguage = (name: string, def: LanguageDef): void => {
|
||||||
|
LANGUAGES[name.toLowerCase()] = def;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSupportedLanguages = (): string[] => Object.keys(LANGUAGES);
|
||||||
|
|
||||||
|
export const highlightUtils = {
|
||||||
|
highlight, normalizeLanguage, registerLanguage, getSupportedLanguages,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default highlight;
|
||||||
+6
-3
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor — Type-safe, lightweight Markdown Editor
|
* MetonaEditor — Type-safe, lightweight Markdown Editor
|
||||||
* @module metona-editor
|
* @module metona-editor
|
||||||
* @version 0.2.4
|
* @version 0.2.5
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MarkdownEditor } from './core';
|
import { MarkdownEditor } from './core';
|
||||||
@@ -10,10 +10,11 @@ import { themeUtils, exportCSSVars, getCSSVariable, followExternalTheme, adoptFr
|
|||||||
import { i18nUtils, createInstanceI18n, loadRemote } from './i18n';
|
import { i18nUtils, createInstanceI18n, loadRemote } from './i18n';
|
||||||
import { pluginUtils, presetPlugins, topologicalSort, validateConfig } from './plugins';
|
import { pluginUtils, presetPlugins, topologicalSort, validateConfig } from './plugins';
|
||||||
import { animationUtils } from './animations';
|
import { animationUtils } from './animations';
|
||||||
|
import { highlight, normalizeLanguage, registerLanguage, getSupportedLanguages } from './highlight';
|
||||||
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants';
|
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants';
|
||||||
import type { EditMode, ThemeName, ToolbarItem, EditorOptions } from './constants';
|
import type { EditMode, ThemeName, ToolbarItem, EditorOptions } from './constants';
|
||||||
|
|
||||||
const VERSION = '0.2.4';
|
const VERSION = '0.2.5';
|
||||||
|
|
||||||
const globalPlugins: any[] = [];
|
const globalPlugins: any[] = [];
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ const api = {
|
|||||||
MarkdownEditor, Editor: MarkdownEditor,
|
MarkdownEditor, Editor: MarkdownEditor,
|
||||||
create, use, on, off, setTheme, setLocale, destroy, getStatus,
|
create, use, on, off, setTheme, setLocale, destroy, getStatus,
|
||||||
parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler,
|
parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler,
|
||||||
|
highlight, normalizeLanguage, registerLanguage, getSupportedLanguages,
|
||||||
themes: themeUtils, i18n: i18nUtils, animations: animationUtils, plugins: pluginUtils, presetPlugins,
|
themes: themeUtils, i18n: i18nUtils, animations: animationUtils, plugins: pluginUtils, presetPlugins,
|
||||||
topologicalSort, validateConfig, createInstanceI18n, loadRemote,
|
topologicalSort, validateConfig, createInstanceI18n, loadRemote,
|
||||||
exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme,
|
exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme,
|
||||||
@@ -70,4 +72,5 @@ const api = {
|
|||||||
if (typeof window !== 'undefined') { (window as any).MeEditor = api; }
|
if (typeof window !== 'undefined') { (window as any).MeEditor = api; }
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
export { api, api as meEditor, api as MeEditor, MarkdownEditor, MarkdownEditor as Editor, create, use, on, off, setTheme, setLocale, destroy, getStatus, parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler, themeUtils, i18nUtils, pluginUtils, presetPlugins, animationUtils, topologicalSort, validateConfig, createInstanceI18n, loadRemote, exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme, DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS, VERSION, };
|
export { api, api as meEditor, api as MeEditor, MarkdownEditor, MarkdownEditor as Editor, create, use, on, off, setTheme, setLocale, destroy, getStatus, parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler, highlight, normalizeLanguage, registerLanguage, getSupportedLanguages, themeUtils, i18nUtils, pluginUtils, presetPlugins, animationUtils, topologicalSort, validateConfig, createInstanceI18n, loadRemote, exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme, DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS, VERSION, };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* highlight.ts 单元测试
|
||||||
|
* 覆盖内置轻量语法高亮器的安全性与标记输出
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { highlight, normalizeLanguage, registerLanguage, getSupportedLanguages } from '../src/highlight';
|
||||||
|
|
||||||
|
describe('highlight - 基础', () => {
|
||||||
|
test('未知语言返回转义纯文本', () => {
|
||||||
|
const out = highlight('const x = 1;', 'nonexistent');
|
||||||
|
expect(out).toBe('const x = 1;');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('支持的语言别名归一化', () => {
|
||||||
|
expect(normalizeLanguage('js')).toBe('javascript');
|
||||||
|
expect(normalizeLanguage('ts')).toBe('typescript');
|
||||||
|
expect(normalizeLanguage('py')).toBe('python');
|
||||||
|
expect(normalizeLanguage('sh')).toBe('bash');
|
||||||
|
expect(normalizeLanguage('yml')).toBe('yaml');
|
||||||
|
expect(normalizeLanguage('Java')).toBe('java');
|
||||||
|
expect(normalizeLanguage('unknown')).toBe('');
|
||||||
|
expect(normalizeLanguage('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('关键词被标记', () => {
|
||||||
|
const out = highlight('const x = 1', 'js');
|
||||||
|
expect(out).toContain('<span class="me-hl-keyword">const</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('字符串被标记', () => {
|
||||||
|
const out = highlight('const s = "hello"', 'js');
|
||||||
|
expect(out).toContain('<span class="me-hl-string">"hello"</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('注释被标记', () => {
|
||||||
|
const out = highlight('// comment\nconst x = 1', 'js');
|
||||||
|
expect(out).toContain('<span class="me-hl-comment">// comment</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('数字被标记', () => {
|
||||||
|
const out = highlight('const n = 42', 'js');
|
||||||
|
expect(out).toContain('<span class="me-hl-number">42</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('函数调用被标记', () => {
|
||||||
|
const out = highlight('foo(bar)', 'js');
|
||||||
|
expect(out).toContain('<span class="me-hl-function">foo</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('python 井号注释与 def 关键词', () => {
|
||||||
|
const out = highlight('# note\ndef main():', 'python');
|
||||||
|
expect(out).toContain('<span class="me-hl-comment"># note</span>');
|
||||||
|
expect(out).toContain('<span class="me-hl-keyword">def</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('内置全局对象被标记', () => {
|
||||||
|
const out = highlight('console.log("x")', 'js');
|
||||||
|
expect(out).toContain('<span class="me-hl-builtin">console</span>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('highlight - 安全性', () => {
|
||||||
|
test('HTML 特殊字符被转义,不产生裸标签', () => {
|
||||||
|
const out = highlight('<script>alert(1)</script>', 'js');
|
||||||
|
expect(out).not.toContain('<script>');
|
||||||
|
expect(out).not.toContain('<script');
|
||||||
|
expect(out).toContain('<script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('字符串中的引号安全', () => {
|
||||||
|
const out = highlight('const s = "a<b>&"', 'js');
|
||||||
|
expect(out).not.toContain('a<b');
|
||||||
|
expect(out).toContain('a<b>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('恶意输入不破坏 span 结构', () => {
|
||||||
|
const out = highlight('"</span><script>x</script>"', 'js');
|
||||||
|
// 所有 span 配对(每开一个 <span 就有一个 </span>)
|
||||||
|
const opens = (out.match(/<span/g) || []).length;
|
||||||
|
const closes = (out.match(/<\/span>/g) || []).length;
|
||||||
|
expect(opens).toBe(closes);
|
||||||
|
expect(out).not.toContain('<script');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('highlight - 语言注册', () => {
|
||||||
|
test('注册自定义语言', () => {
|
||||||
|
registerLanguage('foo', { keywords: ['frobnicate'], builtins: [] });
|
||||||
|
expect(normalizeLanguage('foo')).toBe('foo');
|
||||||
|
const out = highlight('frobnicate the widget', 'foo');
|
||||||
|
expect(out).toContain('<span class="me-hl-keyword">frobnicate</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSupportedLanguages 返回语言列表', () => {
|
||||||
|
const langs = getSupportedLanguages();
|
||||||
|
expect(Array.isArray(langs)).toBe(true);
|
||||||
|
expect(langs).toContain('javascript');
|
||||||
|
expect(langs).toContain('typescript');
|
||||||
|
expect(langs).toContain('python');
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -29,7 +29,7 @@ describe('index.ts - 全局 API', () => {
|
|||||||
|
|
||||||
test('VERSION 是字符串', () => {
|
test('VERSION 是字符串', () => {
|
||||||
expect(typeof VERSION).toBe('string');
|
expect(typeof VERSION).toBe('string');
|
||||||
expect(VERSION).toBe('0.2.4');
|
expect(VERSION).toBe('0.2.5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('api.default 是 api 本身', () => {
|
test('api.default 是 api 本身', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user