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

This commit is contained in:
2026-08-09 20:17:25 +08:00
parent b743af8d90
commit 4d6767482e
14 changed files with 258 additions and 35 deletions
+18
View File
@@ -2,6 +2,24 @@
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
### Added
+10 -7
View File
@@ -2,9 +2,9 @@
> TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库
[![version](https://img.shields.io/badge/version-0.4.0-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor)
[![version](https://img.shields.io/badge/version-0.4.1-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor)
[![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![tests](https://img.shields.io/badge/tests-826%20passed-brightgreen)](./tests)
[![tests](https://img.shields.io/badge/tests-841%20passed-brightgreen)](./tests)
[![coverage](https://img.shields.io/badge/coverage-parser%2099%25-brightgreen)](./tests)
[![types](https://img.shields.io/badge/types-TypeScript%20strict-blue)](./tsconfig.json)
@@ -17,11 +17,11 @@
- **三模式视图** — edit / split / preview,拖拽分隔条调整比例,双向滚动同步
- **主题系统** — light / dark / warm / autoCSS 变量可定制,实例级隔离,主题继承,外部跟随
- **国际化** — zh-CN / en-US / ja / ko / fr / de 六种语言完整翻译,实例级语言隔离,远程加载翻译包
- **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式、右键上下文菜单
- **编辑体验** — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式(宽度可配置,默认 960px、右键上下文菜单
- **内置语法高亮** — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 16 个语言标识,规则缓存零重建开销,`MeEditor.highlight` 即插即用
- **性能** — 字数 / 词数 / 行数差异增量统计,击键零全量扫描;大纲 O(n) 线性构建,长文档即时响应
- **安全** — 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 属性
- **RTL 支持** — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
@@ -79,10 +79,10 @@ const editor = MeEditor.create('#editor', { mode: 'split' });
```html
<!-- 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 -->
<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 源 -->
<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, // 分屏同步滚动
autoBrackets: true, // 括号自动闭合
zenMode: false, // 启动即进入 Zen 专注模式
zenMaxWidth: 960, // Zen 内容区最大宽度:数字为 px / 字符串原样作为 CSS 值 / false 不限制
wordWrap: true, // 自动换行
maxLength: 0, // 最大字符数(0=不限)
floatingToolbar: true, // 选中文本浮动格式栏
@@ -346,6 +347,8 @@ editor.removeToolbarButton(action: string): this
```typescript
editor.toggleZen(): this
editor.isZen(): boolean
editor.setZenMaxWidth(width: number | string | false): this // 运行时调整专注模式宽度
editor.getZenMaxWidth(): number | string | false
editor.toggleWordWrap(): this
editor.setWordWrap(on: boolean): this
editor.isWordWrap(): boolean
@@ -809,7 +812,7 @@ src/
|------|------|
| 语言 | TypeScript 5 (strict) |
| 构建 | Rollup 3 |
| 测试 | Jest 29 + jsdom826 用例) |
| 测试 | Jest 29 + jsdom841 用例) |
| 浏览器冒烟 | Playwright + Chromium22 项断言) |
| 类型生成 | rollup-plugin-dts |
| 零运行时依赖 | ✅ |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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.",
"type": "module",
"main": "dist/metona-editor.cjs",
+11 -8
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"/>
<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>"/>
<title>MetonaEditor v0.4.0 — 全功能演示</title>
<title>MetonaEditor v0.4.1 — 全功能演示</title>
<style>
*,*::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}
@@ -45,11 +45,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
<body>
<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">
<span class="badge">TypeScript</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">桌面端优先</span>
</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>零依赖 tokenizerjs / 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>当前行高亮,与编辑区滚动同步</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>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>
@@ -93,7 +93,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
</div>
<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>
<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>
(function(){
var demoMd=[
'# 🚀 MetonaEditor v0.4.0 全功能演示',
'# 🚀 MetonaEditor v0.4.1 全功能演示',
'',
'> 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 验证',
'- **实例级 i18n**:状态栏 / 工具栏 / 右键菜单 / 大纲全部跟随实例语言',
'- `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.3.1 | 2026-08 | 821 | 增量统计+高亮缓存 |',
'| v0.3.0 | 2026-08 | 801 | 安全修复+缓存指纹 |',
+7 -3
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"/>
<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>"/>
<title>MetonaEditor v0.4.0 · 文档</title>
<title>MetonaEditor v0.4.1 · 文档</title>
<style>
*{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%)}
@@ -65,7 +65,7 @@ footer a{color:var(--accent);text-decoration:none}
<header class="docs-header">
<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>
<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>
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>
<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>
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>
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">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">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>
@@ -487,7 +491,7 @@ i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span
<footer>
<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>
</footer>
+8 -8
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"/>
<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>"/>
<title>MetonaEditor v0.4.0 · TypeScript · 桌面端 Markdown 编辑器</title>
<title>MetonaEditor v0.4.1 · TypeScript · 桌面端 Markdown 编辑器</title>
<style>
*{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)}
@@ -106,10 +106,10 @@ footer a:hover{text-decoration:underline}
<h1>TypeScript 重构 · 零依赖<br/><span class="grad">桌面端 Markdown 编辑器</span></h1>
<p class="tagline">全模块 TypeScript 严格模式,内置自研解析器(99% 行覆盖率),6 个预设插件,中文优先。为桌面端现代 Web 应用而生。</p>
<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">零运行时依赖</span>
<span class="badge">826 tests</span>
<span class="badge">841 tests</span>
<span class="badge">桌面端优先</span>
<span class="badge">MIT</span>
</div>
@@ -120,7 +120,7 @@ footer a:hover{text-decoration:underline}
<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">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>
</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"><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"><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>
</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-com">// 方式二:CDN 引入</span>
<span class="c-com">&lt;script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.4.0/dist/metona-editor.min.js"&gt;&lt;/script&gt;</span>
<span class="c-com">&lt;script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.4.1/dist/metona-editor.min.js"&gt;&lt;/script&gt;</span>
<span class="c-com">// 方式三:本地文件</span>
<span class="c-com">&lt;script src="./dist/metona-editor.min.js"&gt;&lt;/script&gt;</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/issues" target="_blank" rel="noopener">问题反馈</a>
</div>
<div>MetonaEditor v0.4.0 · TypeScript · MIT License · MetonaTeam</div>
<div>MetonaEditor v0.4.1 · TypeScript · MIT License · MetonaTeam</div>
</div>
</footer>
@@ -240,7 +240,7 @@ MeEditor.themes.switchTheme('dark');
MeEditor.create('#editor', {
mode: 'split', height: 480, theme: 'dark',
value: [
'# 欢迎使用 MetonaEditor v0.4.0',
'# 欢迎使用 MetonaEditor v0.4.1',
'',
'一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。',
'',
+2
View File
@@ -41,6 +41,7 @@ export interface EditorOptions {
outline?: boolean;
autoBrackets?: boolean;
zenMode?: boolean;
zenMaxWidth?: number | string | false;
wordWrap?: boolean;
maxLength?: number;
theme?: ThemeName;
@@ -119,6 +120,7 @@ export const DEFAULTS: Readonly<EditorOptions> = Object.freeze({
outline: false,
autoBrackets: true,
zenMode: false,
zenMaxWidth: 960,
wordWrap: true,
maxLength: 0,
theme: 'auto',
+25 -3
View File
@@ -135,7 +135,7 @@ export class MarkdownEditor {
this._listeners = {}; this._renderRaf = null; this._historyTimer = null;
this._fullscreen = false; this._destroyed = false; this._lastRenderedValue = null;
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._floatingToolbar = null;
this._floatingEnabled = this.config.floatingToolbar !== false;
@@ -148,6 +148,7 @@ export class MarkdownEditor {
// 实例 i18n 需在 _buildDOM 之前创建:DOM 构建与工具栏渲染会读取实例语言
this._i18nCtx = createInstanceI18n(this);
this._buildDOM();
this._applyZenWidth();
this._themeCtx = createInstanceTheme(this);
if (options.theme && typeof document !== 'undefined') {
@@ -166,9 +167,11 @@ export class MarkdownEditor {
if (this.config.floatingToolbar !== false) this._initFloatingToolbar();
this.textarea.value = this._value;
if (!this._wordWrap) this.textarea.style.whiteSpace = 'pre';
this._pushHistory();
this._render();
this._updateWordCount();
this._buildOutline();
if (this._zenMode) this._setZen(true);
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(); }); }
_render(): void {
if (this._mode === 'edit') return;
if (this._mode === 'edit') { this._renderGutter(); return; }
if (this._lastRenderedValue === this._value) return;
this._lastRenderedValue = this._value;
MarkdownEditor.trigger('beforeRender', this); this._emit('beforeRender', this);
@@ -603,7 +606,7 @@ export class MarkdownEditor {
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; }
_updateModeButtons(): void {
@@ -627,6 +630,7 @@ export class MarkdownEditor {
_setZen(on: boolean): void {
this._zenMode = !!on; if (!this.el) return;
this.el.classList.toggle('me-zen', this._zenMode);
this._applyZenWidth();
if (this._zenMode && this.toolbarEl) {
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'; };
@@ -643,6 +647,24 @@ export class MarkdownEditor {
}
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; }
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; }
+2 -2
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor — Type-safe, lightweight Markdown Editor
* @module metona-editor
* @version 0.4.0
* @version 0.4.1
*/
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 type { EditMode, ThemeName, ToolbarItem, EditorOptions } from './constants';
const VERSION = '0.4.0';
const VERSION = '0.4.1';
const globalPlugins: any[] = [];
+5 -1
View File
@@ -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;
};
},
destroy() {},
destroy(editor: EditorLike) {
if (editor) { delete (editor as any).exportMarkdown; delete (editor as any).exportHTML; delete (editor as any).exportPDF; }
},
};
const searchReplacePlugin: Plugin = {
@@ -413,6 +415,7 @@ const searchReplacePlugin: Plugin = {
}
editor._value = ta.value; if (typeof editor._pushHistory === 'function') editor._pushHistory();
if (typeof editor._render === 'function') editor._render();
if (typeof editor._updateWordCount === 'function') editor._updateWordCount();
if (typeof editor._emit === 'function') editor._emit('change', editor._value);
findNext();
};
@@ -425,6 +428,7 @@ const searchReplacePlugin: Plugin = {
ta.setSelectionRange(0,0); editor._value = ta.value;
if (typeof editor._pushHistory === 'function') editor._pushHistory();
if (typeof editor._render === 'function') editor._render();
if (typeof editor._updateWordCount === 'function') editor._updateWordCount();
if (typeof editor._emit === 'function') editor._emit('change', editor._value);
findAll();
};
+1 -1
View File
@@ -139,7 +139,7 @@ const generateCSS = (): string => {
.me-outline-l2 a{padding-left:12px}
.me-outline-l3 a{padding-left:20px;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-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}
+141
View File
@@ -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 更新', () => {
test('gutter 行数不变时跳过 DOM 更新', () => {
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 分隔条实例隔离', () => {
test('分隔条位置按实例 id 保存', () => {
localStorage.clear();
+1 -1
View File
@@ -29,7 +29,7 @@ describe('index.ts - 全局 API', () => {
test('VERSION 是字符串', () => {
expect(typeof VERSION).toBe('string');
expect(VERSION).toBe('0.4.0');
expect(VERSION).toBe('0.4.1');
});
test('api.default 是 api 本身', () => {
+26
View File
@@ -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 尺寸限制', () => {
test('超出 maxSizeKB 时 toast 警告且不插入', () => {
document.body.innerHTML = '';