feat: v0.4.1 — Zen 专注模式宽度可配置 + 5 项修复 + 841 测试
CI / test-parser (push) Successful in 9m29s
CI / test-core (push) Successful in 9m36s
CI / test-rest (push) Successful in 9m29s
CI / e2e (push) Successful in 9m44s
CI / verify (18.x) (push) Successful in 9m50s
CI / verify (20.x) (push) Successful in 9m50s
CI / verify (24.x) (push) Successful in 9m45s
CI / test-parser (push) Successful in 9m29s
CI / test-core (push) Successful in 9m36s
CI / test-rest (push) Successful in 9m29s
CI / e2e (push) Successful in 9m44s
CI / verify (18.x) (push) Successful in 9m50s
CI / verify (20.x) (push) Successful in 9m50s
CI / verify (24.x) (push) Successful in 9m45s
This commit is contained in:
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
All notable changes to MetonaEditor will be documented in this file.
|
All notable changes to MetonaEditor will be documented in this file.
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-08-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`zenMaxWidth` 配置项**: Zen 专注模式内容区宽度可配置(`number` 为 px / `string` 原样作为 CSS 值 / `false` 不限制),默认由硬编码 820px 提升至 960px,经 CSS 变量 `--md-zen-max-width` 实现实例级隔离。
|
||||||
|
- **运行时 API**: `editor.setZenMaxWidth(width)` / `editor.getZenMaxWidth()` 随时调整专注模式宽度。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`wordWrap: false` 配置不生效**: 构造函数此前将 `_wordWrap` 硬编码为 `true`,忽略配置项,导致初始仍自动换行;现读取配置并在构建时应用 `white-space: pre`。
|
||||||
|
- **`outline: true` 启动时不显示大纲面板**: 构造函数此前只注册滚动跟踪、未构建面板,须等首次输入才出现;现构造后自动构建,且 `setMode()` 切换回预览类模式时同步重建。
|
||||||
|
- **edit 模式初始行号不渲染**: `_render()` 在 edit 模式直接返回导致 gutter 为空;现 edit 模式也维护行号装订线。
|
||||||
|
- **exportTool 卸载泄漏**: `unuse('exportTool')` / `destroy()` 此前不清理挂载在实例上的 `exportMarkdown` / `exportHTML` / `exportPDF` 方法;现卸载时一并删除。
|
||||||
|
- **searchReplace 替换后字数统计过期**: `replaceOne` / `replaceAll` 直接改值后未刷新状态栏统计;现调用 `_updateWordCount()`。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 回归测试 15 个(zenMaxWidth 默认/数字/字符串/false/运行时更新/启动应用、wordWrap: false 生效、outline 构造自动构建与模式切换重建、edit/preview 模式初始行号、exportTool 卸载清理),总数 826 → 841。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.4.0] - 2026-08-11
|
## [0.4.0] - 2026-08-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
- **三模式视图** — edit / split / preview,拖拽分隔条调整比例,双向滚动同步
|
- **三模式视图** — edit / split / preview,拖拽分隔条调整比例,双向滚动同步
|
||||||
- **主题系统** — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随
|
- **主题系统** — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随
|
||||||
- **国际化** — zh-CN / en-US / ja / ko / fr / de 六种语言完整翻译,实例级语言隔离,远程加载翻译包
|
- **国际化** — zh-CN / en-US / ja / ko / fr / de 六种语言完整翻译,实例级语言隔离,远程加载翻译包
|
||||||
- **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式、右键上下文菜单
|
- **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式(宽度可配置,默认 960px)、右键上下文菜单
|
||||||
- **内置语法高亮** — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 16 个语言标识,规则缓存零重建开销,`MeEditor.highlight` 即插即用
|
- **内置语法高亮** — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 16 个语言标识,规则缓存零重建开销,`MeEditor.highlight` 即插即用
|
||||||
- **性能** — 字数 / 词数 / 行数差异增量统计,击键零全量扫描;大纲 O(n) 线性构建,长文档即时响应
|
- **性能** — 字数 / 词数 / 行数差异增量统计,击键零全量扫描;大纲 O(n) 线性构建,长文档即时响应
|
||||||
- **安全** — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),属性级注入防护(含脚注 id),白名单裸标签透传,sanitize 钩子
|
- **安全** — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),属性级注入防护(含脚注 id),白名单裸标签透传,sanitize 钩子
|
||||||
- **工程化** — 826 个单元测试 + Playwright 真实浏览器冒烟测试,CI 并行 4 job,`sideEffects: false` 便于 tree-shaking
|
- **工程化** — 841 个单元测试 + Playwright 真实浏览器冒烟测试,CI 并行 4 job,`sideEffects: false` 便于 tree-shaking
|
||||||
- **桌面端优先** — 纯电脑端设计,无移动端冗余代码
|
- **桌面端优先** — 纯电脑端设计,无移动端冗余代码
|
||||||
- **引用链接** — 支持 `[text][ref]` / `![alt][ref]` 引用式链接和图片,含 title 属性
|
- **引用链接** — 支持 `[text][ref]` / `![alt][ref]` 引用式链接和图片,含 title 属性
|
||||||
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
|
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
|
||||||
@@ -79,10 +79,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.4.0/dist/metona-editor.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.4.1/dist/metona-editor.min.js"></script>
|
||||||
|
|
||||||
<!-- unpkg CDN -->
|
<!-- unpkg CDN -->
|
||||||
<script src="https://unpkg.com/@metona-team/metona-editor@0.4.0/dist/metona-editor.min.js"></script>
|
<script src="https://unpkg.com/@metona-team/metona-editor@0.4.1/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>
|
||||||
@@ -144,6 +144,7 @@ MeEditor.create(container, {
|
|||||||
syncScroll: true, // 分屏同步滚动
|
syncScroll: true, // 分屏同步滚动
|
||||||
autoBrackets: true, // 括号自动闭合
|
autoBrackets: true, // 括号自动闭合
|
||||||
zenMode: false, // 启动即进入 Zen 专注模式
|
zenMode: false, // 启动即进入 Zen 专注模式
|
||||||
|
zenMaxWidth: 960, // Zen 内容区最大宽度:数字为 px / 字符串原样作为 CSS 值 / false 不限制
|
||||||
wordWrap: true, // 自动换行
|
wordWrap: true, // 自动换行
|
||||||
maxLength: 0, // 最大字符数(0=不限)
|
maxLength: 0, // 最大字符数(0=不限)
|
||||||
floatingToolbar: true, // 选中文本浮动格式栏
|
floatingToolbar: true, // 选中文本浮动格式栏
|
||||||
@@ -346,6 +347,8 @@ editor.removeToolbarButton(action: string): this
|
|||||||
```typescript
|
```typescript
|
||||||
editor.toggleZen(): this
|
editor.toggleZen(): this
|
||||||
editor.isZen(): boolean
|
editor.isZen(): boolean
|
||||||
|
editor.setZenMaxWidth(width: number | string | false): this // 运行时调整专注模式宽度
|
||||||
|
editor.getZenMaxWidth(): number | string | false
|
||||||
editor.toggleWordWrap(): this
|
editor.toggleWordWrap(): this
|
||||||
editor.setWordWrap(on: boolean): this
|
editor.setWordWrap(on: boolean): this
|
||||||
editor.isWordWrap(): boolean
|
editor.isWordWrap(): boolean
|
||||||
@@ -809,7 +812,7 @@ src/
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 语言 | TypeScript 5 (strict) |
|
| 语言 | TypeScript 5 (strict) |
|
||||||
| 构建 | Rollup 3 |
|
| 构建 | Rollup 3 |
|
||||||
| 测试 | Jest 29 + jsdom(826 用例) |
|
| 测试 | Jest 29 + jsdom(841 用例) |
|
||||||
| 浏览器冒烟 | Playwright + Chromium(22 项断言) |
|
| 浏览器冒烟 | Playwright + Chromium(22 项断言) |
|
||||||
| 类型生成 | rollup-plugin-dts |
|
| 类型生成 | rollup-plugin-dts |
|
||||||
| 零运行时依赖 | ✅ |
|
| 零运行时依赖 | ✅ |
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@metona-team/metona-editor",
|
"name": "@metona-team/metona-editor",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Type-safe, lightweight, zero-dependency Markdown Editor. Desktop-first. React-free. Single-file bundle.",
|
"description": "Type-safe, lightweight, zero-dependency Markdown Editor. Desktop-first. React-free. Single-file bundle.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/metona-editor.cjs",
|
"main": "dist/metona-editor.cjs",
|
||||||
|
|||||||
+11
-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.4.0 — 全功能演示</title>
|
<title>MetonaEditor v0.4.1 — 全功能演示</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.4.0</h1>
|
<h1><span class="grad">Metona</span>Editor v0.4.1</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">826 tests</span>
|
<span class="badge">841 tests</span>
|
||||||
<span class="badge">6 个插件</span>
|
<span class="badge">6 个插件</span>
|
||||||
<span class="badge">桌面端优先</span>
|
<span class="badge">桌面端优先</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +85,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
|
|||||||
<div class="info-card"><div class="icon">🎨</div><h4>内置语法高亮</h4><p>零依赖 tokenizer,js / ts / python / bash 等 16 个语言标识</p></div>
|
<div class="info-card"><div class="icon">🎨</div><h4>内置语法高亮</h4><p>零依赖 tokenizer,js / ts / python / bash 等 16 个语言标识</p></div>
|
||||||
<div class="info-card"><div class="icon">📂</div><h4>磁盘文件读写</h4><p>File System Access API 打开 / 保存真实 .md 文件</p></div>
|
<div class="info-card"><div class="icon">📂</div><h4>磁盘文件读写</h4><p>File System Access API 打开 / 保存真实 .md 文件</p></div>
|
||||||
<div class="info-card"><div class="icon">🔢</div><h4>行号装订线</h4><p>当前行高亮,与编辑区滚动同步</p></div>
|
<div class="info-card"><div class="icon">🔢</div><h4>行号装订线</h4><p>当前行高亮,与编辑区滚动同步</p></div>
|
||||||
<div class="info-card"><div class="icon">🧘</div><h4>Zen 专注模式</h4><p>工具栏自动隐藏,鼠标移到顶部滑入</p></div>
|
<div class="info-card"><div class="icon">🧘</div><h4>Zen 专注模式</h4><p>工具栏自动隐藏,宽度可配置(zenMaxWidth,默认 960px)</p></div>
|
||||||
<div class="info-card"><div class="icon">⌨️</div><h4>快捷键面板</h4><p>按 ? 查看全部快捷键,Ctrl+F/H 搜索替换</p></div>
|
<div class="info-card"><div class="icon">⌨️</div><h4>快捷键面板</h4><p>按 ? 查看全部快捷键,Ctrl+F/H 搜索替换</p></div>
|
||||||
<div class="info-card"><div class="icon">📊</div><h4>Mermaid 图表</h4><p>```mermaid 代码块,加载 Mermaid.js 即可渲染</p></div>
|
<div class="info-card"><div class="icon">📊</div><h4>Mermaid 图表</h4><p>```mermaid 代码块,加载 Mermaid.js 即可渲染</p></div>
|
||||||
<div class="info-card"><div class="icon">🔌</div><h4>6 个预设插件</h4><p>autoSave / exportTool / searchReplace / imagePaste / shortcutHelp / fileSystem</p></div>
|
<div class="info-card"><div class="icon">🔌</div><h4>6 个预设插件</h4><p>autoSave / exportTool / searchReplace / imagePaste / shortcutHelp / fileSystem</p></div>
|
||||||
@@ -93,7 +93,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
MetonaEditor v0.4.0 · 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.4.1 · 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>
|
||||||
@@ -101,13 +101,15 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
|
|||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
var demoMd=[
|
var demoMd=[
|
||||||
'# 🚀 MetonaEditor v0.4.0 全功能演示',
|
'# 🚀 MetonaEditor v0.4.1 全功能演示',
|
||||||
'',
|
'',
|
||||||
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
|
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
|
||||||
'> 全模块 TypeScript 严格模式,17 个源文件,826 个测试全部通过。',
|
'> 全模块 TypeScript 严格模式,17 个源文件,841 个测试全部通过。',
|
||||||
'',
|
'',
|
||||||
'## ✨ v0.4.0 新特性',
|
'## ✨ v0.4.1 新特性',
|
||||||
'',
|
'',
|
||||||
|
'- **Zen 专注模式宽度可配置**:`zenMaxWidth` 默认 960px,支持数字 / CSS 字符串 / `false` 不限宽',
|
||||||
|
'- **修复 5 处问题**:`wordWrap: false` 生效 · outline 启动即构建 · edit 模式初始行号 · exportTool 卸载清理 · 替换后统计刷新',
|
||||||
'- **Playwright 浏览器冒烟测试**:22 项断言真实 Chromium 验证',
|
'- **Playwright 浏览器冒烟测试**:22 项断言真实 Chromium 验证',
|
||||||
'- **实例级 i18n**:状态栏 / 工具栏 / 右键菜单 / 大纲全部跟随实例语言',
|
'- **实例级 i18n**:状态栏 / 工具栏 / 右键菜单 / 大纲全部跟随实例语言',
|
||||||
'- `sideEffects: false` 优化打包 · `MeEditor.destroy()` 全面复位',
|
'- `sideEffects: false` 优化打包 · `MeEditor.destroy()` 全面复位',
|
||||||
@@ -139,6 +141,7 @@ var demoMd=[
|
|||||||
'',
|
'',
|
||||||
'| 版本 | 日期 | 测试 | 主题 |',
|
'| 版本 | 日期 | 测试 | 主题 |',
|
||||||
'| :--- | :---: | ---: | --- |',
|
'| :--- | :---: | ---: | --- |',
|
||||||
|
'| v0.4.1 | 2026-08 | 841 | Zen宽度可配置+5项修复 |',
|
||||||
'| v0.4.0 | 2026-08 | 826 | E2E冒烟+实例i18n |',
|
'| v0.4.0 | 2026-08 | 826 | E2E冒烟+实例i18n |',
|
||||||
'| v0.3.1 | 2026-08 | 821 | 增量统计+高亮缓存 |',
|
'| v0.3.1 | 2026-08 | 821 | 增量统计+高亮缓存 |',
|
||||||
'| v0.3.0 | 2026-08 | 801 | 安全修复+缓存指纹 |',
|
'| v0.3.0 | 2026-08 | 801 | 安全修复+缓存指纹 |',
|
||||||
|
|||||||
+7
-3
@@ -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.4.0 · 文档</title>
|
<title>MetonaEditor v0.4.1 · 文档</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.4.0 · TypeScript · 完整配置、API 与使用指南</p>
|
<p class="sub">MetonaEditor v0.4.1 · TypeScript · 完整配置、API 与使用指南</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -146,6 +146,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>
|
||||||
|
<span class="c-com">// Zen 内容区最大宽度(数字为 px / 字符串为 CSS 值 / false 不限宽)</span>
|
||||||
|
zenMaxWidth<span class="c-punc">:</span> <span class="c-num">960</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>
|
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>
|
floatingToolbar<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span> <span class="c-com">// 选中文本浮动格式栏</span>
|
||||||
|
|
||||||
@@ -252,6 +254,8 @@ editor<span class="c-punc">.</span><span class="c-fn">canUndo</span><span class=
|
|||||||
<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> <span class="c-com">// 或配置 zenMode: true 启动即进入</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">setZenMaxWidth</span><span class="c-punc">(</span><span class="c-num">1200</span><span class="c-punc">);</span> <span class="c-com">// 运行时调整专注模式宽度(数字 / CSS 字符串 / false)</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">getZenMaxWidth</span><span class="c-punc">();</span> <span class="c-com">// => number | string | false</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>
|
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>
|
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>
|
||||||
@@ -487,7 +491,7 @@ i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
MetonaEditor v0.4.0 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
MetonaEditor v0.4.1 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -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.4.0 · TypeScript · 桌面端 Markdown 编辑器</title>
|
<title>MetonaEditor v0.4.1 · 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.4.0</span>
|
<span class="badge accent">v0.4.1</span>
|
||||||
<span class="badge">TypeScript</span>
|
<span class="badge">TypeScript</span>
|
||||||
<span class="badge">零运行时依赖</span>
|
<span class="badge">零运行时依赖</span>
|
||||||
<span class="badge">826 tests</span>
|
<span class="badge">841 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">826</div><div class="stat-label">单元测试</div></div>
|
<div class="stat"><div class="stat-num">841</div><div class="stat-label">单元测试</div></div>
|
||||||
<div class="stat"><div class="stat-num">~42KB</div><div class="stat-label">gzip 体积</div></div>
|
<div class="stat"><div class="stat-num">~42KB</div><div class="stat-label">gzip 体积</div></div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -147,7 +147,7 @@ footer a:hover{text-decoration:underline}
|
|||||||
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/></svg></div><h3>主题 & i18n</h3><p>4 套预设主题 + CSS 变量定制 + 实例级隔离。六种语言开箱即用(中 / 英 / 日 / 韩 / 法 / 德),支持远程加载翻译包。</p></div>
|
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/></svg></div><h3>主题 & i18n</h3><p>4 套预设主题 + CSS 变量定制 + 实例级隔离。六种语言开箱即用(中 / 英 / 日 / 韩 / 法 / 德),支持远程加载翻译包。</p></div>
|
||||||
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></div><h3>内置语法高亮</h3><p>零依赖轻量高亮器,js / ts / python / bash / css 等 16 个语言标识,代码块一行配置即自动着色。</p></div>
|
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></div><h3>内置语法高亮</h3><p>零依赖轻量高亮器,js / ts / python / bash / css 等 16 个语言标识,代码块一行配置即自动着色。</p></div>
|
||||||
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div><h3>XSS 安全防护</h3><p>HTML 转义、危险协议过滤、属性级注入防护、sanitize 钩子、highlight 异常回退。多层防护,安全可靠。</p></div>
|
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div><h3>XSS 安全防护</h3><p>HTML 转义、危险协议过滤、属性级注入防护、sanitize 钩子、highlight 异常回退。多层防护,安全可靠。</p></div>
|
||||||
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div><h3>性能与工程</h3><p>字数/词数/行数差异增量统计、高亮规则缓存、大纲 O(n) 构建;826 单测 + Playwright 真实浏览器冒烟,CI 并行 4 job。</p></div>
|
<div class="feature"><div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div><h3>性能与工程</h3><p>字数/词数/行数差异增量统计、高亮规则缓存、大纲 O(n) 构建;841 单测 + Playwright 真实浏览器冒烟,CI 并行 4 job。</p></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -167,7 +167,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.4.0/dist/metona-editor.min.js"></script></span>
|
<span class="c-com"><script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.4.1/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>
|
||||||
@@ -230,7 +230,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.4.0 · TypeScript · MIT License · MetonaTeam</div>
|
<div>MetonaEditor v0.4.1 · TypeScript · MIT License · MetonaTeam</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -240,7 +240,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.4.0',
|
'# 欢迎使用 MetonaEditor v0.4.1',
|
||||||
'',
|
'',
|
||||||
'一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。',
|
'一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。',
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface EditorOptions {
|
|||||||
outline?: boolean;
|
outline?: boolean;
|
||||||
autoBrackets?: boolean;
|
autoBrackets?: boolean;
|
||||||
zenMode?: boolean;
|
zenMode?: boolean;
|
||||||
|
zenMaxWidth?: number | string | false;
|
||||||
wordWrap?: boolean;
|
wordWrap?: boolean;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
theme?: ThemeName;
|
theme?: ThemeName;
|
||||||
@@ -119,6 +120,7 @@ export const DEFAULTS: Readonly<EditorOptions> = Object.freeze({
|
|||||||
outline: false,
|
outline: false,
|
||||||
autoBrackets: true,
|
autoBrackets: true,
|
||||||
zenMode: false,
|
zenMode: false,
|
||||||
|
zenMaxWidth: 960,
|
||||||
wordWrap: true,
|
wordWrap: true,
|
||||||
maxLength: 0,
|
maxLength: 0,
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
|
|||||||
+25
-3
@@ -135,7 +135,7 @@ export class MarkdownEditor {
|
|||||||
this._listeners = {}; this._renderRaf = null; this._historyTimer = null;
|
this._listeners = {}; this._renderRaf = null; this._historyTimer = null;
|
||||||
this._fullscreen = false; this._destroyed = false; this._lastRenderedValue = null;
|
this._fullscreen = false; this._destroyed = false; this._lastRenderedValue = null;
|
||||||
this._shortcuts = []; this._contextMenuItems = []; this._customActions = {};
|
this._shortcuts = []; this._contextMenuItems = []; this._customActions = {};
|
||||||
this._outlineTimer = null; this._zenMode = this.config.zenMode === true; this._wordWrap = true; this._syncing = false;
|
this._outlineTimer = null; this._zenMode = this.config.zenMode === true; this._wordWrap = this.config.wordWrap !== false; this._syncing = false;
|
||||||
this._zenMouseHandler = null;
|
this._zenMouseHandler = null;
|
||||||
this._floatingToolbar = null;
|
this._floatingToolbar = null;
|
||||||
this._floatingEnabled = this.config.floatingToolbar !== false;
|
this._floatingEnabled = this.config.floatingToolbar !== false;
|
||||||
@@ -148,6 +148,7 @@ export class MarkdownEditor {
|
|||||||
// 实例 i18n 需在 _buildDOM 之前创建:DOM 构建与工具栏渲染会读取实例语言
|
// 实例 i18n 需在 _buildDOM 之前创建:DOM 构建与工具栏渲染会读取实例语言
|
||||||
this._i18nCtx = createInstanceI18n(this);
|
this._i18nCtx = createInstanceI18n(this);
|
||||||
this._buildDOM();
|
this._buildDOM();
|
||||||
|
this._applyZenWidth();
|
||||||
this._themeCtx = createInstanceTheme(this);
|
this._themeCtx = createInstanceTheme(this);
|
||||||
|
|
||||||
if (options.theme && typeof document !== 'undefined') {
|
if (options.theme && typeof document !== 'undefined') {
|
||||||
@@ -166,9 +167,11 @@ export class MarkdownEditor {
|
|||||||
if (this.config.floatingToolbar !== false) this._initFloatingToolbar();
|
if (this.config.floatingToolbar !== false) this._initFloatingToolbar();
|
||||||
|
|
||||||
this.textarea.value = this._value;
|
this.textarea.value = this._value;
|
||||||
|
if (!this._wordWrap) this.textarea.style.whiteSpace = 'pre';
|
||||||
this._pushHistory();
|
this._pushHistory();
|
||||||
this._render();
|
this._render();
|
||||||
this._updateWordCount();
|
this._updateWordCount();
|
||||||
|
this._buildOutline();
|
||||||
if (this._zenMode) this._setZen(true);
|
if (this._zenMode) this._setZen(true);
|
||||||
|
|
||||||
if (Array.isArray(this.config.plugins)) {
|
if (Array.isArray(this.config.plugins)) {
|
||||||
@@ -421,7 +424,7 @@ export class MarkdownEditor {
|
|||||||
_scheduleRender(): void { if (this._renderRaf) return; this._renderRaf = requestAnimationFrame(() => { this._renderRaf = null; this._render(); }); }
|
_scheduleRender(): void { if (this._renderRaf) return; this._renderRaf = requestAnimationFrame(() => { this._renderRaf = null; this._render(); }); }
|
||||||
|
|
||||||
_render(): void {
|
_render(): void {
|
||||||
if (this._mode === 'edit') return;
|
if (this._mode === 'edit') { this._renderGutter(); return; }
|
||||||
if (this._lastRenderedValue === this._value) return;
|
if (this._lastRenderedValue === this._value) return;
|
||||||
this._lastRenderedValue = this._value;
|
this._lastRenderedValue = this._value;
|
||||||
MarkdownEditor.trigger('beforeRender', this); this._emit('beforeRender', this);
|
MarkdownEditor.trigger('beforeRender', this); this._emit('beforeRender', this);
|
||||||
@@ -603,7 +606,7 @@ export class MarkdownEditor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMode(mode: EditMode): this { if (!MODES.includes(mode) || mode === this._mode) return this; const taS = this.textarea.scrollTop; const pvS = this.previewPane.scrollTop; this._mode = mode; this.bodyEl.className = `me-body me-mode-${mode}`; this._updateModeButtons(); if (mode !== 'edit') this._render(); this.textarea.scrollTop = taS; this.previewPane.scrollTop = pvS; this._emit('modeChange', mode); this._announce(`${this.t(mode) || mode} mode`); if (typeof this.config.onModeChange === 'function') { try { this.config.onModeChange(mode, this); } catch (e) { console.error(e); } } return this; }
|
setMode(mode: EditMode): this { if (!MODES.includes(mode) || mode === this._mode) return this; const taS = this.textarea.scrollTop; const pvS = this.previewPane.scrollTop; this._mode = mode; this.bodyEl.className = `me-body me-mode-${mode}`; this._updateModeButtons(); if (mode !== 'edit') { this._render(); this._buildOutline(); } this.textarea.scrollTop = taS; this.previewPane.scrollTop = pvS; this._emit('modeChange', mode); this._announce(`${this.t(mode) || mode} mode`); if (typeof this.config.onModeChange === 'function') { try { this.config.onModeChange(mode, this); } catch (e) { console.error(e); } } return this; }
|
||||||
getMode(): EditMode { return this._mode; }
|
getMode(): EditMode { return this._mode; }
|
||||||
|
|
||||||
_updateModeButtons(): void {
|
_updateModeButtons(): void {
|
||||||
@@ -627,6 +630,7 @@ export class MarkdownEditor {
|
|||||||
_setZen(on: boolean): void {
|
_setZen(on: boolean): void {
|
||||||
this._zenMode = !!on; if (!this.el) return;
|
this._zenMode = !!on; if (!this.el) return;
|
||||||
this.el.classList.toggle('me-zen', this._zenMode);
|
this.el.classList.toggle('me-zen', this._zenMode);
|
||||||
|
this._applyZenWidth();
|
||||||
if (this._zenMode && this.toolbarEl) {
|
if (this._zenMode && this.toolbarEl) {
|
||||||
if (!this._zenMouseHandler) {
|
if (!this._zenMouseHandler) {
|
||||||
this._zenMouseHandler = (e: MouseEvent) => { this.toolbarEl.style.opacity = e.clientY < 40 ? '1' : '0'; this.toolbarEl.style.pointerEvents = e.clientY < 40 ? 'auto' : 'none'; };
|
this._zenMouseHandler = (e: MouseEvent) => { this.toolbarEl.style.opacity = e.clientY < 40 ? '1' : '0'; this.toolbarEl.style.pointerEvents = e.clientY < 40 ? 'auto' : 'none'; };
|
||||||
@@ -643,6 +647,24 @@ export class MarkdownEditor {
|
|||||||
}
|
}
|
||||||
isZen(): boolean { return this._zenMode; }
|
isZen(): boolean { return this._zenMode; }
|
||||||
|
|
||||||
|
_applyZenWidth(): void {
|
||||||
|
if (!this.el) return;
|
||||||
|
const width = this.config.zenMaxWidth;
|
||||||
|
if (width == null || width === false || width === '') {
|
||||||
|
this.el.style.removeProperty('--md-zen-max-width');
|
||||||
|
} else {
|
||||||
|
this.el.style.setProperty('--md-zen-max-width', typeof width === 'number' ? `${width}px` : String(width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setZenMaxWidth(width: number | string | false): this {
|
||||||
|
this.config.zenMaxWidth = width;
|
||||||
|
this._applyZenWidth();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getZenMaxWidth(): number | string | false { return this.config.zenMaxWidth ?? 960; }
|
||||||
|
|
||||||
toggleWordWrap(): this { this._wordWrap = !this._wordWrap; if (this.textarea) this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; return this; }
|
toggleWordWrap(): this { this._wordWrap = !this._wordWrap; if (this.textarea) this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; return this; }
|
||||||
setWordWrap(on: boolean): this { this._wordWrap = !!on; if (this.textarea) this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; return this; }
|
setWordWrap(on: boolean): this { this._wordWrap = !!on; if (this.textarea) this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; return this; }
|
||||||
isWordWrap(): boolean { return this._wordWrap; }
|
isWordWrap(): boolean { return this._wordWrap; }
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor — Type-safe, lightweight Markdown Editor
|
* MetonaEditor — Type-safe, lightweight Markdown Editor
|
||||||
* @module metona-editor
|
* @module metona-editor
|
||||||
* @version 0.4.0
|
* @version 0.4.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MarkdownEditor } from './core';
|
import { MarkdownEditor } from './core';
|
||||||
@@ -14,7 +14,7 @@ import { highlight, normalizeLanguage, registerLanguage, getSupportedLanguages }
|
|||||||
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.4.0';
|
const VERSION = '0.4.1';
|
||||||
|
|
||||||
const globalPlugins: any[] = [];
|
const globalPlugins: any[] = [];
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -261,7 +261,9 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
|||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
destroy() {},
|
destroy(editor: EditorLike) {
|
||||||
|
if (editor) { delete (editor as any).exportMarkdown; delete (editor as any).exportHTML; delete (editor as any).exportPDF; }
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchReplacePlugin: Plugin = {
|
const searchReplacePlugin: Plugin = {
|
||||||
@@ -413,6 +415,7 @@ const searchReplacePlugin: Plugin = {
|
|||||||
}
|
}
|
||||||
editor._value = ta.value; if (typeof editor._pushHistory === 'function') editor._pushHistory();
|
editor._value = ta.value; if (typeof editor._pushHistory === 'function') editor._pushHistory();
|
||||||
if (typeof editor._render === 'function') editor._render();
|
if (typeof editor._render === 'function') editor._render();
|
||||||
|
if (typeof editor._updateWordCount === 'function') editor._updateWordCount();
|
||||||
if (typeof editor._emit === 'function') editor._emit('change', editor._value);
|
if (typeof editor._emit === 'function') editor._emit('change', editor._value);
|
||||||
findNext();
|
findNext();
|
||||||
};
|
};
|
||||||
@@ -425,6 +428,7 @@ const searchReplacePlugin: Plugin = {
|
|||||||
ta.setSelectionRange(0,0); editor._value = ta.value;
|
ta.setSelectionRange(0,0); editor._value = ta.value;
|
||||||
if (typeof editor._pushHistory === 'function') editor._pushHistory();
|
if (typeof editor._pushHistory === 'function') editor._pushHistory();
|
||||||
if (typeof editor._render === 'function') editor._render();
|
if (typeof editor._render === 'function') editor._render();
|
||||||
|
if (typeof editor._updateWordCount === 'function') editor._updateWordCount();
|
||||||
if (typeof editor._emit === 'function') editor._emit('change', editor._value);
|
if (typeof editor._emit === 'function') editor._emit('change', editor._value);
|
||||||
findAll();
|
findAll();
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -139,7 +139,7 @@ const generateCSS = (): string => {
|
|||||||
.me-outline-l2 a{padding-left:12px}
|
.me-outline-l2 a{padding-left:12px}
|
||||||
.me-outline-l3 a{padding-left:20px;font-size:12px}
|
.me-outline-l3 a{padding-left:20px;font-size:12px}
|
||||||
.me-outline-l4 a{padding-left:28px;font-size:12px}
|
.me-outline-l4 a{padding-left:28px;font-size:12px}
|
||||||
.me-wrapper.me-zen .me-body{max-width:820px;margin:0 auto}
|
.me-wrapper.me-zen .me-body{max-width:var(--md-zen-max-width,960px);margin:0 auto}
|
||||||
.me-wrapper.me-zen .me-textarea{font-size:15px;line-height:1.8}
|
.me-wrapper.me-zen .me-textarea{font-size:15px;line-height:1.8}
|
||||||
.me-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
.me-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
||||||
@media print{.me-wrapper{border:0!important;box-shadow:none!important}
|
@media print{.me-wrapper{border:0!important;box-shadow:none!important}
|
||||||
|
|||||||
@@ -1768,6 +1768,59 @@ describe('MarkdownEditor - v0.2.0 outline 构建', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.4.1 outline 启动初始化', () => {
|
||||||
|
test('outline: true 构造后自动构建面板(无需手动调用)', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: '# H1\n## H2', outline: true, mode: 'split' });
|
||||||
|
const panel = ed.el.querySelector('.me-outline');
|
||||||
|
expect(panel).not.toBeNull();
|
||||||
|
expect(panel!.querySelectorAll('a').length).toBe(2);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('outline: true + edit 模式构造后切换 split 自动构建面板', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: '# H1', outline: true, mode: 'edit' });
|
||||||
|
expect(ed.el.querySelector('.me-outline')).toBeNull();
|
||||||
|
ed.setMode('split');
|
||||||
|
expect(ed.el.querySelector('.me-outline')).not.toBeNull();
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('outline: false 构造后不创建面板', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: '# H1', outline: false, mode: 'split' });
|
||||||
|
expect(ed.el.querySelector('.me-outline')).toBeNull();
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.4.1 edit 模式初始行号', () => {
|
||||||
|
test('edit 模式构造后 gutter 已渲染行号', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: 'a\nb\nc', mode: 'edit' });
|
||||||
|
expect(ed.gutter.children.length).toBe(3);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preview 模式构造后 gutter 也渲染行号', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: 'a\nb\nc\nd', mode: 'preview' });
|
||||||
|
expect(ed.gutter.children.length).toBe(4);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('MarkdownEditor - v0.2.0 gutter 更新', () => {
|
describe('MarkdownEditor - v0.2.0 gutter 更新', () => {
|
||||||
test('gutter 行数不变时跳过 DOM 更新', () => {
|
test('gutter 行数不变时跳过 DOM 更新', () => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
@@ -2723,6 +2776,94 @@ describe('MarkdownEditor - v0.2.5 zenMode 初始状态', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.4.1 zenMaxWidth 专注模式宽度', () => {
|
||||||
|
test('默认 zenMaxWidth 为 960', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
expect(ed.getZenMaxWidth()).toBe(960);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('配置 zenMaxWidth 数字转为 px 并写入 CSS 变量', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { zenMaxWidth: 1200 });
|
||||||
|
expect(ed.getZenMaxWidth()).toBe(1200);
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('1200px');
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('配置 zenMaxWidth 字符串原样写入 CSS 变量', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { zenMaxWidth: 'min(1200px, 90%)' });
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('min(1200px, 90%)');
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('配置 zenMaxWidth: false 清除 CSS 变量(不限制宽度)', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { zenMaxWidth: false });
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('');
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setZenMaxWidth 运行时更新宽度且仅在 zen 时生效类', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
ed.setZenMaxWidth(1080);
|
||||||
|
expect(ed.getZenMaxWidth()).toBe(1080);
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('1080px');
|
||||||
|
ed.setZenMaxWidth(false);
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('');
|
||||||
|
ed.setZenMaxWidth('60vw');
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('60vw');
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zenMode: true 启动时应用 zenMaxWidth 变量', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { zenMode: true, zenMaxWidth: 1000 });
|
||||||
|
expect(ed.isZen()).toBe(true);
|
||||||
|
expect(ed.el.style.getPropertyValue('--md-zen-max-width')).toBe('1000px');
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.4.1 wordWrap: false 配置生效', () => {
|
||||||
|
test('wordWrap: false 初始 whiteSpace 为 pre', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { wordWrap: false });
|
||||||
|
expect(ed.isWordWrap()).toBe(false);
|
||||||
|
expect(ed.textarea.style.whiteSpace).toBe('pre');
|
||||||
|
ed.toggleWordWrap();
|
||||||
|
expect(ed.isWordWrap()).toBe(true);
|
||||||
|
expect(ed.textarea.style.whiteSpace).toBe('pre-wrap');
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('默认 wordWrap 为 true', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
expect(ed.isWordWrap()).toBe(true);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('MarkdownEditor - v0.2.5 分隔条实例隔离', () => {
|
describe('MarkdownEditor - v0.2.5 分隔条实例隔离', () => {
|
||||||
test('分隔条位置按实例 id 保存', () => {
|
test('分隔条位置按实例 id 保存', () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|||||||
+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.4.0');
|
expect(VERSION).toBe('0.4.1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('api.default 是 api 本身', () => {
|
test('api.default 是 api 本身', () => {
|
||||||
|
|||||||
@@ -1239,6 +1239,32 @@ describe('v0.2.5 exportTool exportPDF', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('v0.4.1 exportTool 卸载清理', () => {
|
||||||
|
test('unuse 后实例上的导出方法被删除', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: '# hi' });
|
||||||
|
ed.use('exportTool');
|
||||||
|
expect(typeof (ed as any).exportMarkdown).toBe('function');
|
||||||
|
ed.unuse('exportTool');
|
||||||
|
expect((ed as any).exportMarkdown).toBeUndefined();
|
||||||
|
expect((ed as any).exportHTML).toBeUndefined();
|
||||||
|
expect((ed as any).exportPDF).toBeUndefined();
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy 时实例上的导出方法被删除', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: '# hi', plugins: ['exportTool'] });
|
||||||
|
expect(typeof (ed as any).exportMarkdown).toBe('function');
|
||||||
|
ed.destroy();
|
||||||
|
expect((ed as any).exportMarkdown).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('v0.2.5 imagePaste 尺寸限制', () => {
|
describe('v0.2.5 imagePaste 尺寸限制', () => {
|
||||||
test('超出 maxSizeKB 时 toast 警告且不插入', () => {
|
test('超出 maxSizeKB 时 toast 警告且不插入', () => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user