Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ae9e3c8e | ||
|
|
2e7baa1073 | ||
|
|
8bbbb7ff70 | ||
|
|
1dd22be0a8 | ||
|
|
20b6d03617 | ||
|
|
7bd625b9ce |
@@ -1,8 +1,8 @@
|
|||||||
# MarkLite v0.4.0 — 架构设计文档
|
# MarkLite v0.4.3 — 架构设计文档
|
||||||
|
|
||||||
## 1. 项目概述
|
## 1. 项目概述
|
||||||
|
|
||||||
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 MetonaEditor v0.1.3 编辑器(三模式视图 + 插件系统)、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
|
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 MetonaEditor v0.1.14 编辑器(三模式视图 + 插件系统)、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
|
||||||
|
|
||||||
### 1.1 核心原则
|
### 1.1 核心原则
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
|
|||||||
| 桌面框架 | Electron | v28 | 跨平台桌面应用框架 |
|
| 桌面框架 | Electron | v28 | 跨平台桌面应用框架 |
|
||||||
| 前端框架 | React | v18 | 函数组件 + Hooks |
|
| 前端框架 | React | v18 | 函数组件 + Hooks |
|
||||||
| 类型系统 | TypeScript | v5.6 | 全量类型安全 |
|
| 类型系统 | TypeScript | v5.6 | 全量类型安全 |
|
||||||
| 编辑器 | MetonaEditor | v0.1.3 | 零依赖 Markdown 编辑器,三模式视图 + 插件系统 |
|
| 编辑器 | MetonaEditor | v0.1.14 | 零依赖 Markdown 编辑器,三模式视图 + 插件系统 |
|
||||||
| 状态管理 | Zustand | v5 | 轻量级状态管理 |
|
| 状态管理 | Zustand | v5 | 轻量级状态管理 |
|
||||||
| 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 |
|
| 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 |
|
||||||
| Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线(作为 MetonaEditor render 钩子) |
|
| Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线(作为 MetonaEditor render 钩子) |
|
||||||
@@ -218,7 +218,7 @@ db.version(1).stores({
|
|||||||
| `window:confirmClose` | 无 | 请求确认关闭 |
|
| `window:confirmClose` | 无 | 请求确认关闭 |
|
||||||
| `sidebar:dirChanged` | 无 | 目录结构变化 |
|
| `sidebar:dirChanged` | 无 | 目录结构变化 |
|
||||||
|
|
||||||
## 6. 编辑器架构 — MetonaEditor v0.1.3
|
## 6. 编辑器架构 — MetonaEditor v0.1.14
|
||||||
|
|
||||||
### 6.1 三模式视图
|
### 6.1 三模式视图
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ npm run build:portable # 便携版(免安装)
|
|||||||
| zustand | ^5.0 | 状态管理 |
|
| zustand | ^5.0 | 状态管理 |
|
||||||
| dexie | ^4.0 | IndexedDB 封装 |
|
| dexie | ^4.0 | IndexedDB 封装 |
|
||||||
| nanoid | ^5.0 | 唯一 ID 生成 |
|
| nanoid | ^5.0 | 唯一 ID 生成 |
|
||||||
| @metona-team/metona-editor | ^0.1.3 | Markdown 编辑器(零依赖) |
|
| @metona-team/metona-editor | 0.1.14 | Markdown 编辑器(零依赖) |
|
||||||
| @metona-team/metona-toast | ^2.0.1 | Toast 通知组件 |
|
| @metona-team/metona-toast | ^2.0.1 | Toast 通知组件 |
|
||||||
| unified / remark / rehype | ^11.0 | Markdown 渲染管线 |
|
| unified / remark / rehype | ^11.0 | Markdown 渲染管线 |
|
||||||
| rehype-highlight | ^7.0 | 代码语法高亮 |
|
| rehype-highlight | ^7.0 | 代码语法高亮 |
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# MarkLite v0.4.0 — 开发环境配置指南
|
# MarkLite v0.4.3 — 开发环境配置指南
|
||||||
|
|
||||||
以下涵盖从 Node 版本管理、镜像源配置到 electron-builder 打包的全流程。
|
以下涵盖从 Node 版本管理、镜像源配置到 electron-builder 打包的全流程。
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript">
|
<img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript">
|
||||||
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
|
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
|
||||||
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
|
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
|
||||||
<img src="https://img.shields.io/badge/Version-v0.4.0-orange?style=flat-square" alt="Version">
|
<img src="https://img.shields.io/badge/Version-v0.4.3-orange?style=flat-square" alt="Version">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -143,7 +143,7 @@ npm run test:coverage
|
|||||||
| 桌面框架 | [Electron](https://www.electronjs.org/) v28 | 跨平台桌面应用框架 |
|
| 桌面框架 | [Electron](https://www.electronjs.org/) v28 | 跨平台桌面应用框架 |
|
||||||
| 前端框架 | [React](https://react.dev/) v18 | 函数组件 + Hooks |
|
| 前端框架 | [React](https://react.dev/) v18 | 函数组件 + Hooks |
|
||||||
| 类型系统 | [TypeScript](https://www.typescriptlang.org/) v5.6 | 全量类型安全 |
|
| 类型系统 | [TypeScript](https://www.typescriptlang.org/) v5.6 | 全量类型安全 |
|
||||||
| 编辑器 | [MetonaEditor](https://git.metona.cn/MetonaTeam/MetonaEditor) v0.1.3 | 零依赖 Markdown 编辑器,三模式视图 + 插件系统 |
|
| 编辑器 | [MetonaEditor](https://git.metona.cn/MetonaTeam/MetonaEditor) v0.1.14 | 零依赖 Markdown 编辑器,三模式视图 + 插件系统 |
|
||||||
| 状态管理 | [Zustand](https://zustand-demo.pmnd.rs/) v5 | 轻量级状态管理 |
|
| 状态管理 | [Zustand](https://zustand-demo.pmnd.rs/) v5 | 轻量级状态管理 |
|
||||||
| 持久化 | [Dexie.js](https://dexie.org/) v4 (IndexedDB) | 标签页状态 & 用户设置持久化 |
|
| 持久化 | [Dexie.js](https://dexie.org/) v4 (IndexedDB) | 标签页状态 & 用户设置持久化 |
|
||||||
| Markdown 解析 | [unified](https://unifiedjs.com/) / [remark](https://remark.js.org/) / [rehype](https://rehype.js.org/) | 插件化 Markdown 渲染管线 |
|
| Markdown 解析 | [unified](https://unifiedjs.com/) / [remark](https://remark.js.org/) / [rehype](https://rehype.js.org/) | 插件化 Markdown 渲染管线 |
|
||||||
|
|||||||
+247
-390
@@ -1,419 +1,276 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" data-md-theme="auto">
|
||||||
<head>
|
<head>
|
||||||
<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"/>
|
||||||
<title>MetonaEditor · 功能演示</title>
|
<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>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root {
|
: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}
|
||||||
--bg: #f5f6f8;
|
[data-md-theme=dark]{--app-bg:#0f1117;--app-text:#e2e8f0;--app-card:#1a1d27;--app-border:#2d3140;--header-bg:linear-gradient(135deg,#0a0d15 0%,#161923 100%)}
|
||||||
--card: #ffffff;
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--app-bg);color:var(--app-text);min-height:100vh;line-height:1.6;transition:background .3s,color .3s}
|
||||||
--text: #1f2937;
|
|
||||||
--muted: #6b7280;
|
|
||||||
--border: #e5e7eb;
|
|
||||||
--accent: #3b82f6;
|
|
||||||
--accent-soft: rgba(59, 130, 246, 0.1);
|
|
||||||
--danger: #ef4444;
|
|
||||||
--success: #10b981;
|
|
||||||
--code-bg: #1c2029;
|
|
||||||
--code-text: #e6e8eb;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.wrap { max-width: 1120px; margin: 0 auto; }
|
|
||||||
|
|
||||||
/* 顶栏 */
|
.header{background:var(--header-bg);color:var(--header-text);padding:28px 24px 20px;text-align:center;position:relative;overflow:hidden}
|
||||||
.topbar {
|
.header::before{content:'';position:absolute;top:-60px;right:-60px;width:300px;height:300px;background:radial-gradient(circle,rgba(139,92,246,.3) 0%,transparent 70%);border-radius:50%}
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
.header h1{font-size:1.85rem;font-weight:800;letter-spacing:-.5px;position:relative}
|
||||||
flex-wrap: wrap; gap: 12px;
|
.header h1 span.grad{background:linear-gradient(135deg,var(--app-accent),var(--app-accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
||||||
margin-bottom: 20px;
|
.header .badges{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:12px;position:relative}
|
||||||
}
|
.header .badge{padding:4px 12px;border-radius:20px;font-size:13px;font-weight:500;background:var(--badge-bg);color:var(--badge-text);border:1px solid rgba(255,255,255,.1)}
|
||||||
.topbar h1 { font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 12px; }
|
|
||||||
.topbar h1 a {
|
|
||||||
color: var(--accent); text-decoration: none;
|
|
||||||
font-size: 13px; font-weight: 400;
|
|
||||||
padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
.topbar h1 a:hover { border-color: var(--accent); }
|
|
||||||
.controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
||||||
.ctrl-label { font-size: 12px; color: var(--muted); margin-right: 4px; }
|
|
||||||
.ctrl-group {
|
|
||||||
display: inline-flex; gap: 0;
|
|
||||||
background: var(--card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
|
|
||||||
}
|
|
||||||
.ctrl-btn {
|
|
||||||
padding: 7px 14px; border: 0; background: transparent; cursor: pointer;
|
|
||||||
font-size: 13px; color: var(--text); transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.ctrl-btn:hover { background: #f3f4f6; }
|
|
||||||
.ctrl-btn.active { background: var(--accent); color: #fff; }
|
|
||||||
.ctrl-btn + .ctrl-btn { border-left: 1px solid var(--border); }
|
|
||||||
|
|
||||||
/* 编辑器 */
|
.toolbar-strip{display:flex;align-items:center;gap:8px;padding:8px 20px;background:var(--app-card);border-bottom:1px solid var(--app-border);flex-wrap:wrap;position:sticky;top:0;z-index:50;transition:background .3s}
|
||||||
#editor { margin-bottom: 24px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 20px -8px rgba(0,0,0,0.1); }
|
.toolbar-strip label{font-size:12px;font-weight:500;color:var(--app-text);opacity:.7;white-space:nowrap}
|
||||||
|
.toolbar-strip select{padding:4px 8px;border:1px solid var(--app-border);border-radius:6px;font-size:12px;background:var(--app-bg);color:var(--app-text);cursor:pointer}
|
||||||
|
.toolbar-strip button{padding:5px 12px;border:1px solid var(--app-border);border-radius:6px;font-size:12px;font-weight:500;background:var(--app-card);color:var(--app-text);cursor:pointer;transition:all .15s;white-space:nowrap}
|
||||||
|
.toolbar-strip button:hover{background:var(--app-accent);color:#fff;border-color:var(--app-accent)}
|
||||||
|
.toolbar-strip button.on{background:var(--app-accent);color:#fff;border-color:var(--app-accent)}
|
||||||
|
.toolbar-strip .spacer{flex:1}
|
||||||
|
|
||||||
/* 网格布局 */
|
.editor-section{max-width:1280px;margin:20px auto;padding:0 20px}
|
||||||
.grid {
|
.editor-card{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.06);background:var(--app-card);border:1px solid var(--app-border);transition:.3s}
|
||||||
display: grid;
|
.editor-card:hover{box-shadow:0 8px 36px rgba(0,0,0,.1)}
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.grid { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;max-width:1280px;margin:20px auto;padding:0 20px}
|
||||||
background: var(--card);
|
.info-card{background:var(--app-card);border:1px solid var(--app-border);border-radius:10px;padding:16px 18px;transition:.3s}
|
||||||
border: 1px solid var(--border);
|
.info-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08)}
|
||||||
border-radius: 10px;
|
.info-card .icon{font-size:22px;margin-bottom:6px}
|
||||||
padding: 16px 18px;
|
.info-card h4{font-size:13px;font-weight:650;margin-bottom:3px}
|
||||||
}
|
.info-card p{font-size:12px;color:var(--app-text);opacity:.65;margin:0}
|
||||||
.panel-title {
|
|
||||||
font-size: 13px; color: var(--muted);
|
|
||||||
margin-bottom: 10px; font-weight: 600;
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
}
|
|
||||||
.panel-title .count {
|
|
||||||
background: var(--accent-soft); color: var(--accent);
|
|
||||||
padding: 2px 8px; border-radius: 999px; font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制台 */
|
.footer{text-align:center;padding:28px 24px;font-size:13px;color:var(--app-text);opacity:.5}
|
||||||
.console {
|
.footer a{color:var(--app-accent);text-decoration:none}
|
||||||
background: var(--code-bg); color: var(--code-text);
|
@media(max-width:768px){.header h1{font-size:1.4rem}.toolbar-strip{padding:6px 10px;gap:4px}.toolbar-strip button{padding:4px 8px;font-size:11px}.info-grid{grid-template-columns:1fr 1fr}}
|
||||||
border-radius: 8px; padding: 14px 16px;
|
|
||||||
font-family: "SF Mono", "Cascadia Code", Consolas, monospace; font-size: 12.5px;
|
|
||||||
min-height: 140px; max-height: 240px; overflow: auto; white-space: pre-wrap; word-break: break-all;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.console:empty::before { content: "点击下方按钮查看输出..."; color: #6b7280; }
|
|
||||||
.console .log-time { color: #5c6370; }
|
|
||||||
.console .log-label { color: #82aaff; font-weight: 600; }
|
|
||||||
|
|
||||||
/* 事件流 */
|
|
||||||
.events {
|
|
||||||
background: var(--code-bg); color: var(--code-text);
|
|
||||||
border-radius: 8px; padding: 14px 16px;
|
|
||||||
font-family: "SF Mono", Consolas, monospace; font-size: 12px;
|
|
||||||
min-height: 140px; max-height: 240px; overflow: auto;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.events:empty::before { content: "事件流将在这里实时显示..."; color: #6b7280; }
|
|
||||||
.event-line { padding: 2px 0; }
|
|
||||||
.event-line .ev-name { color: #c3e88d; }
|
|
||||||
.event-line .ev-time { color: #5c6370; }
|
|
||||||
|
|
||||||
/* API 按钮 */
|
|
||||||
.api-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
.api-btn {
|
|
||||||
padding: 9px 12px; border: 1px solid var(--border); background: var(--card);
|
|
||||||
border-radius: 7px; cursor: pointer; font-size: 12.5px; color: var(--text);
|
|
||||||
transition: all 0.15s; text-align: center; font-family: "SF Mono", Consolas, monospace;
|
|
||||||
}
|
|
||||||
.api-btn:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
|
|
||||||
.api-btn:active { transform: translateY(0); }
|
|
||||||
.api-btn.danger:hover { border-color: var(--danger); color: var(--danger); }
|
|
||||||
.api-btn.success:hover { border-color: var(--success); color: var(--success); }
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
font-size: 12px; color: var(--muted);
|
|
||||||
margin-top: 14px; padding: 10px 14px;
|
|
||||||
background: var(--accent-soft); border-radius: 8px;
|
|
||||||
border-left: 3px solid var(--accent);
|
|
||||||
}
|
|
||||||
code { background: #f3f4f6; padding: 1px 6px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", Consolas, monospace; }
|
|
||||||
|
|
||||||
/* 状态指示 */
|
|
||||||
.status-bar {
|
|
||||||
display: flex; gap: 16px; flex-wrap: wrap;
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: var(--card); border: 1px solid var(--border);
|
|
||||||
border-radius: 8px; margin-bottom: 16px;
|
|
||||||
font-size: 12px; color: var(--muted);
|
|
||||||
}
|
|
||||||
.status-item { display: inline-flex; align-items: center; gap: 6px; }
|
|
||||||
.status-item strong { color: var(--text); font-weight: 600; }
|
|
||||||
.status-dot {
|
|
||||||
width: 8px; height: 8px; border-radius: 50%;
|
|
||||||
background: var(--success);
|
|
||||||
}
|
|
||||||
.status-dot.destroyed { background: var(--danger); }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
|
||||||
<div class="topbar">
|
<header class="header">
|
||||||
<h1>
|
<h1><span class="grad">Metona</span>Editor v0.2.4</h1>
|
||||||
MetonaEditor 功能演示
|
<div class="badges">
|
||||||
<a href="index.html">← 返回首页</a>
|
<span class="badge">TypeScript</span>
|
||||||
<a href="docs.html">📖 文档</a>
|
<span class="badge">零运行时依赖</span>
|
||||||
</h1>
|
<span class="badge">725 tests</span>
|
||||||
<div class="controls">
|
<span class="badge">6 个插件</span>
|
||||||
<span class="ctrl-label">主题</span>
|
<span class="badge">桌面端优先</span>
|
||||||
<div class="ctrl-group" id="themeCtrl">
|
|
||||||
<button class="ctrl-btn" data-theme="light">亮色</button>
|
|
||||||
<button class="ctrl-btn" data-theme="dark">暗色</button>
|
|
||||||
<button class="ctrl-btn" data-theme="warm">暖色</button>
|
|
||||||
</div>
|
|
||||||
<span class="ctrl-label">语言</span>
|
|
||||||
<div class="ctrl-group" id="localeCtrl">
|
|
||||||
<button class="ctrl-btn" data-locale="zh-CN">中文</button>
|
|
||||||
<button class="ctrl-btn" data-locale="en-US">English</button>
|
|
||||||
</div>
|
|
||||||
<span class="ctrl-label">模式</span>
|
|
||||||
<div class="ctrl-group" id="readonlyCtrl">
|
|
||||||
<button class="ctrl-btn" id="btnReadOnly">只读</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="status-bar" id="statusBar">
|
<div class="toolbar-strip" id="bar">
|
||||||
<div class="status-item">
|
<a href="index.html" style="text-decoration:none;font-size:18px;margin-right:4px" title="首页">🏠</a>
|
||||||
<span class="status-dot" id="statusDot"></span>
|
<label>主题</label>
|
||||||
<span>状态:</span><strong id="stState">运行中</strong>
|
<select id="selTheme"><option value="auto">🌓 跟随系统</option><option value="light">☀️ 亮色</option><option value="dark">🌙 暗色</option><option value="warm">🔥 暖色</option></select>
|
||||||
</div>
|
<label>语言</label>
|
||||||
<div class="status-item"><span>模式:</span><strong id="stMode">split</strong></div>
|
<select id="selLocale"><option value="zh-CN">中文</option><option value="en-US">English</option><option value="ja">日本語</option><option value="ko">한국어</option><option value="fr">Français</option><option value="de">Deutsch</option></select>
|
||||||
<div class="status-item"><span>主题:</span><strong id="stTheme">light</strong></div>
|
<label>模式</label>
|
||||||
<div class="status-item"><span>语言:</span><strong id="stLocale">zh-CN</strong></div>
|
<button data-cmd="mode" data-arg="edit">编辑</button>
|
||||||
<div class="status-item"><span>字数:</span><strong id="stWords">0</strong></div>
|
<button data-cmd="mode" data-arg="split" class="on">分屏</button>
|
||||||
<div class="status-item"><span>全屏:</span><strong id="stFs">否</strong></div>
|
<button data-cmd="mode" data-arg="preview">预览</button>
|
||||||
</div>
|
<div class="spacer"></div>
|
||||||
|
<button data-cmd="zen" title="Zen 专注模式">🧘 Zen</button>
|
||||||
<div id="editor"></div>
|
<button data-cmd="outline" title="大纲面板">📋 大纲</button>
|
||||||
|
<button data-cmd="wrap" title="切换换行">↩ 换行</button>
|
||||||
<div class="grid">
|
<button data-cmd="float" title="浮动工具栏" class="on">💬 浮动栏</button>
|
||||||
<div class="panel">
|
<button data-cmd="toast" title="Toast 通知">💬 Toast</button>
|
||||||
<div class="panel-title">
|
<button data-cmd="openFile" title="打开文件">📂 打开</button>
|
||||||
<span>API 控制台</span>
|
<button data-cmd="saveFile" title="保存文件">💾 保存</button>
|
||||||
<span class="count" id="apiCount">0 次调用</span>
|
|
||||||
</div>
|
|
||||||
<div class="console" id="console"></div>
|
|
||||||
<div class="api-grid">
|
|
||||||
<button class="api-btn" data-api="getValue">getValue()</button>
|
|
||||||
<button class="api-btn" data-api="getHTML">getHTML()</button>
|
|
||||||
<button class="api-btn" data-api="getStats">getStats()</button>
|
|
||||||
<button class="api-btn" data-api="getStatus">getStatus()</button>
|
|
||||||
<button class="api-btn" data-api="insert">insert()</button>
|
|
||||||
<button class="api-btn" data-api="exec-bold">exec('bold')</button>
|
|
||||||
<button class="api-btn" data-api="exec-h1">exec('h1')</button>
|
|
||||||
<button class="api-btn" data-api="setMode-edit">编辑模式</button>
|
|
||||||
<button class="api-btn" data-api="setMode-split">分屏模式</button>
|
|
||||||
<button class="api-btn" data-api="setMode-preview">预览模式</button>
|
|
||||||
<button class="api-btn" data-api="fullscreen">切换全屏</button>
|
|
||||||
<button class="api-btn" data-api="undo">undo()</button>
|
|
||||||
<button class="api-btn" data-api="redo">redo()</button>
|
|
||||||
<button class="api-btn success" data-api="exportMD">导出 .md</button>
|
|
||||||
<button class="api-btn success" data-api="exportHTML">导出 .html</button>
|
|
||||||
<button class="api-btn" data-api="restoreDraft">恢复草稿</button>
|
|
||||||
<button class="api-btn" data-api="clearDraft">清空草稿</button>
|
|
||||||
<button class="api-btn" data-api="refresh">refresh()</button>
|
|
||||||
<button class="api-btn danger" data-api="destroy">destroy()</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
<div class="panel-title">
|
|
||||||
<span>事件流(实时)</span>
|
|
||||||
<span class="count" id="evCount">0 个事件</span>
|
|
||||||
</div>
|
|
||||||
<div class="events" id="events"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hint">
|
|
||||||
<strong>快捷键提示:</strong>
|
|
||||||
编辑区内按 <code>Ctrl+B</code> 加粗、<code>Ctrl+I</code> 斜体、<code>Ctrl+K</code> 插入链接、
|
|
||||||
<code>Ctrl+Z</code> 撤销、<code>Ctrl+S</code> 触发保存、<code>Ctrl+F</code> 查找、<code>Ctrl+H</code> 替换、
|
|
||||||
<code>Tab</code> 缩进、<code>Esc</code> 关闭面板。
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-section">
|
||||||
|
<div class="editor-card"><div id="editor"></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-card"><div class="icon">🏗️</div><h4>TypeScript 全模块重构</h4><p>12 个 .ts 源文件,严格模式,完整类型导出</p></div>
|
||||||
|
<div class="info-card"><div class="icon">📝</div><h4>自研解析器 99% 行覆盖</h4><p>CommonMark + GFM:粗斜体、链接内格式、脚注、数学公式等</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>快捷键面板</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
<script src="../dist/metona-editor.js"></script>
|
<script src="../dist/metona-editor.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let apiCalls = 0;
|
(function(){
|
||||||
let eventCount = 0;
|
var demoMd=[
|
||||||
|
'# 🚀 MetonaEditor v0.2.4 全功能演示',
|
||||||
|
'',
|
||||||
|
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
|
||||||
|
'> 全模块 TypeScript 严格模式,12 个源文件,684 个测试全部通过。',
|
||||||
|
'',
|
||||||
|
'## ✨ v0.2.4 新特性',
|
||||||
|
'',
|
||||||
|
'- 浮动格式工具栏:选中文本自动弹出 bold/italic/code/link',
|
||||||
|
'- 新增 `getSelectedText()` `getCursorPosition()` 等 10+ API',
|
||||||
|
'- `replaceAll` / `replaceAllRegex` 批量替换',
|
||||||
|
'- `selectionChange` / `cursorMove` 事件',
|
||||||
|
'- 德语(de)翻译 · 6 种语言支持',
|
||||||
|
'',
|
||||||
|
'## 📐 文本样式',
|
||||||
|
'',
|
||||||
|
'- **粗体** / __另一种粗体__',
|
||||||
|
'- ***粗斜体*** / ___另一种粗斜体___',
|
||||||
|
'- *斜体* / _另一种斜体_',
|
||||||
|
'- ~~删除线~~ / ==高亮标记==',
|
||||||
|
'- 链接内格式: [**粗体链接**](https://example.com)',
|
||||||
|
'- 化学:H~2~O / 数学:x^2^',
|
||||||
|
'- 代码:`const x = 42;`',
|
||||||
|
'- Emoji::smile: :rocket: :fire: :rainbow: :coffee:',
|
||||||
|
'',
|
||||||
|
'## 📊 数学公式 & 图表',
|
||||||
|
'',
|
||||||
|
'行内:$E = mc^2$',
|
||||||
|
'块级:$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$',
|
||||||
|
'',
|
||||||
|
'```mermaid',
|
||||||
|
'graph TD',
|
||||||
|
' A[TypeScript] --> B[Rollup]',
|
||||||
|
' B --> C[UMD/ESM/CJS]',
|
||||||
|
' C --> D[Browser]',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'## 📋 表格与对齐',
|
||||||
|
'',
|
||||||
|
'| 版本 | 日期 | 测试 | 主题 |',
|
||||||
|
'| :--- | :---: | ---: | --- |',
|
||||||
|
'| v0.2.4 | 2026-07 | 725 | 浮动工具栏+新API |',
|
||||||
|
'| v0.2.2 | 2026-07 | 694 | 法语+表格格式化 |',
|
||||||
|
'| v0.2.1 | 2026-07 | 684 | 引用链接+右键菜单 |',
|
||||||
|
'| v0.2.0 | 2026-07 | 610 | TypeScript 重构 |',
|
||||||
|
'| v0.1.15 | 2026-07 | 583 | 解析器增强 |',
|
||||||
|
'| v0.1.14 | 2026-07 | 535 | 语法高亮+Mermaid |',
|
||||||
|
'',
|
||||||
|
'## ✅ 任务列表',
|
||||||
|
'',
|
||||||
|
'- [x] TypeScript 全模块重构',
|
||||||
|
'- [x] 解析器 99% 行覆盖率',
|
||||||
|
'- [x] parseTokens / renderTokens 分离',
|
||||||
|
'- [x] registerBlockHandler 扩展',
|
||||||
|
'- [x] 6 个预设插件完整测试',
|
||||||
|
'- [ ] VS Code 扩展',
|
||||||
|
'',
|
||||||
|
'## 🎯 嵌套列表',
|
||||||
|
'',
|
||||||
|
'1. 一级有序',
|
||||||
|
' 1. 二级有序(自动递增)',
|
||||||
|
' 2. 第二项',
|
||||||
|
'2. 回到一级',
|
||||||
|
' - 混杂无序列表',
|
||||||
|
' - 第二项',
|
||||||
|
' - [x] 三级嵌套 + 任务',
|
||||||
|
'',
|
||||||
|
'## 💻 代码块',
|
||||||
|
'',
|
||||||
|
'```ts',
|
||||||
|
'import MeEditor from "@metona-team/metona-editor";',
|
||||||
|
'',
|
||||||
|
'const editor = MeEditor.create("#editor", {',
|
||||||
|
' mode: "split",',
|
||||||
|
' theme: "dark",',
|
||||||
|
' lineNumbers: true,',
|
||||||
|
' autoBrackets: true,',
|
||||||
|
' plugins: ["autoSave", "fileSystem", "shortcutHelp"],',
|
||||||
|
' onChange: (value, editor) => {',
|
||||||
|
' console.log("changed:", value.length, "chars");',
|
||||||
|
' },',
|
||||||
|
'});',
|
||||||
|
'',
|
||||||
|
'// 链式 API',
|
||||||
|
'editor.exec("bold").exec("h1").focus();',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'## 📌 脚注',
|
||||||
|
'',
|
||||||
|
'这是一个带脚注的句子[^1],另一个脚注[^note]。',
|
||||||
|
'',
|
||||||
|
'[^1]: 脚注可以包含 **格式化** 和 `代码`。',
|
||||||
|
'[^note]: 第二个脚注的说明。',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'> **提示:** 按 `?` 查看快捷键 · 拖放文件到编辑区 · Ctrl+F 查找 · Ctrl+H 替换',
|
||||||
|
'> 在列表行尾 Enter 体验智能延续 · 选中文字后按 `(` 体验自动包裹',
|
||||||
|
'> 点击「📂 打开」加载磁盘 .md 文件 · 点击「💾 保存」写入磁盘',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
const log = (label, val) => {
|
var editor = MeEditor.create('#editor',{
|
||||||
apiCalls++;
|
value: demoMd,
|
||||||
document.getElementById('apiCount').textContent = apiCalls + ' 次调用';
|
mode: 'split', height: 620,
|
||||||
const c = document.getElementById('console');
|
lineNumbers: true, autoBrackets: true, wordCount: true,
|
||||||
const now = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
theme: 'auto', locale: 'zh-CN', outline: false,
|
||||||
const text = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
|
plugins: ['autoSave','exportTool','searchReplace','imagePaste','shortcutHelp','fileSystem'],
|
||||||
const truncated = text.length > 500 ? text.slice(0, 500) + '\n... (已截断)' : text;
|
onLinkClick: function(uri, text, ed){
|
||||||
c.innerHTML = `<span class="log-time">[${now}]</span> <span class="log-label">${label}</span>\n${escapeHtml(truncated)}`;
|
ed.toast('链接: ' + uri, { type: 'info', duration: 2000 });
|
||||||
};
|
|
||||||
|
|
||||||
const logEvent = (name, data) => {
|
|
||||||
eventCount++;
|
|
||||||
document.getElementById('evCount').textContent = eventCount + ' 个事件';
|
|
||||||
const el = document.getElementById('events');
|
|
||||||
const now = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
||||||
const dataStr = data !== undefined ? ' ' + (typeof data === 'string' ? data.slice(0, 40) : JSON.stringify(data).slice(0, 40)) : '';
|
|
||||||
const line = document.createElement('div');
|
|
||||||
line.className = 'event-line';
|
|
||||||
line.innerHTML = `<span class="ev-time">${now}</span> <span class="ev-name">${name}</span>${escapeHtml(dataStr)}`;
|
|
||||||
el.appendChild(line);
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
// 限制最多 50 条
|
|
||||||
while (el.children.length > 50) el.removeChild(el.firstChild);
|
|
||||||
};
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function updateStatus() {
|
// Control bar
|
||||||
if (editor.isDestroyed()) {
|
var bar = document.getElementById('bar');
|
||||||
document.getElementById('stState').textContent = '已销毁';
|
|
||||||
document.getElementById('statusDot').classList.add('destroyed');
|
document.getElementById('selTheme').addEventListener('change', function(){
|
||||||
return;
|
editor.setTheme(this.value);
|
||||||
}
|
});
|
||||||
const s = editor.getStatus();
|
document.getElementById('selLocale').addEventListener('change', function(){
|
||||||
const stats = editor.getStats();
|
editor.setLocale(this.value);
|
||||||
document.getElementById('stState').textContent = '运行中';
|
});
|
||||||
document.getElementById('stMode').textContent = s.mode;
|
|
||||||
document.getElementById('stTheme').textContent = s.theme;
|
bar.addEventListener('click', function(e){
|
||||||
document.getElementById('stLocale').textContent = s.locale;
|
var btn = e.target.closest('button[data-cmd]');
|
||||||
document.getElementById('stWords').textContent = stats.words;
|
if (!btn) return;
|
||||||
document.getElementById('stFs').textContent = s.fullscreen ? '是' : '否';
|
var cmd = btn.dataset.cmd;
|
||||||
|
if (cmd === 'mode') { editor.setMode(btn.dataset.arg); updateModeBtns(btn.dataset.arg); }
|
||||||
|
else if (cmd === 'zen') { editor.toggleZen(); btn.classList.toggle('on', editor.isZen()); }
|
||||||
|
else if (cmd === 'outline') { toggleOutline(btn); }
|
||||||
|
else if (cmd === 'wrap') { editor.toggleWordWrap(); btn.classList.toggle('on', !editor.isWordWrap()); }
|
||||||
|
else if (cmd === 'float') {
|
||||||
|
editor.toggleFloatingToolbar();
|
||||||
|
btn.classList.toggle('on', editor.isFloatingToolbar());
|
||||||
}
|
}
|
||||||
|
else if (cmd === 'toast') {
|
||||||
|
var types = ['success','error','warning','info'];
|
||||||
|
var msgs = { success: '✅ 操作成功', error: '❌ 操作失败', warning: '⚠️ 注意', info: 'ℹ️ 提示信息' };
|
||||||
|
editor.toast(msgs[types[Math.floor(Math.random()*4)]], { type: types[Math.floor(Math.random()*4)], duration: 2000 });
|
||||||
|
}
|
||||||
|
else if (cmd === 'openFile') { editor.openFile?.(); }
|
||||||
|
else if (cmd === 'saveFile') { editor.saveFile?.(); }
|
||||||
|
});
|
||||||
|
|
||||||
// 创建编辑器,启用全部预设插件
|
function updateModeBtns(mode){
|
||||||
const editor = MeEditor.create('#editor', {
|
bar.querySelectorAll('[data-cmd=mode]').forEach(function(b){ b.classList.toggle('on', b.dataset.arg === mode); });
|
||||||
mode: 'split',
|
}
|
||||||
height: 420,
|
|
||||||
theme: 'light',
|
|
||||||
locale: 'zh-CN',
|
|
||||||
value: [
|
|
||||||
'# 功能演示',
|
|
||||||
'',
|
|
||||||
'这里是 MetonaEditor 的完整功能演示。',
|
|
||||||
'',
|
|
||||||
'## 行内格式',
|
|
||||||
'',
|
|
||||||
'- 支持 **加粗**、*斜体*、~~删除线~~、`行内代码`',
|
|
||||||
'- 高亮 ==重点标记== 、上标 x^2^、下标 H~2~O',
|
|
||||||
'- Emoji 短码 :smile: :rocket: :fire: :tada:',
|
|
||||||
'- 支持 [链接](https://example.com) 与 ',
|
|
||||||
'',
|
|
||||||
'## 表格',
|
|
||||||
'',
|
|
||||||
'| 名称 | 值 |',
|
|
||||||
'| --- | --- |',
|
|
||||||
'| 版本 | 0.1.3 |',
|
|
||||||
'| 依赖 | 零 |',
|
|
||||||
'| 测试 | 390+ |',
|
|
||||||
'',
|
|
||||||
'## 任务列表',
|
|
||||||
'',
|
|
||||||
'- [x] 分屏预览',
|
|
||||||
'- [x] 插件系统',
|
|
||||||
'- [x] 主题与国际化',
|
|
||||||
'- [ ] 你的创意',
|
|
||||||
'',
|
|
||||||
'## 代码块',
|
|
||||||
'',
|
|
||||||
'```js',
|
|
||||||
'function hello(name) {',
|
|
||||||
' return `Hello, ${name}!`;',
|
|
||||||
'}',
|
|
||||||
'```',
|
|
||||||
'',
|
|
||||||
'> 试试顶部的主题/语言切换与下方的 API 按钮。',
|
|
||||||
'',
|
|
||||||
'右侧事件流会实时显示编辑器触发的事件。'
|
|
||||||
].join('\n'),
|
|
||||||
plugins: ['autoSave', 'exportTool', 'searchReplace', 'imagePaste'],
|
|
||||||
onChange: (val) => { logEvent('change', val); updateStatus(); },
|
|
||||||
onInput: (val) => logEvent('input', val),
|
|
||||||
onFocus: () => logEvent('focus'),
|
|
||||||
onBlur: () => logEvent('blur'),
|
|
||||||
onSave: () => logEvent('save'),
|
|
||||||
onModeChange: (mode) => { logEvent('modeChange', mode); updateStatus(); },
|
|
||||||
onFullscreen: (fs) => { logEvent('fullscreen', fs); updateStatus(); },
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听 autosave 事件(来自 autoSave 插件)
|
function toggleOutline(btn){
|
||||||
editor.on('autosave', (data) => logEvent('autosave', data));
|
editor.config.outline = !editor.config.outline;
|
||||||
|
if (editor.config.outline) { editor._buildOutline(); btn.classList.add('on'); }
|
||||||
|
else { var p = editor.el.querySelector('.me-outline'); if (p) p.remove(); btn.classList.remove('on'); }
|
||||||
|
}
|
||||||
|
|
||||||
// 主题切换
|
// Custom shortcut
|
||||||
document.getElementById('themeCtrl').addEventListener('click', (e) => {
|
editor.registerShortcut('Ctrl+Shift+T', function(){
|
||||||
const btn = e.target.closest('[data-theme]');
|
editor.toast('自定义快捷键已触发!', { type: 'info' });
|
||||||
if (!btn) return;
|
}, 'Toast 通知演示');
|
||||||
document.querySelectorAll('#themeCtrl .ctrl-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
MeEditor.setTheme(btn.dataset.theme);
|
|
||||||
editor.el.className = editor.el.className.replace(/me-theme-\w+/g, '') + ' me-theme-' + btn.dataset.theme;
|
|
||||||
updateStatus();
|
|
||||||
});
|
|
||||||
document.querySelector('#themeCtrl [data-theme="light"]').classList.add('active');
|
|
||||||
|
|
||||||
// 语言切换
|
// Events
|
||||||
document.getElementById('localeCtrl').addEventListener('click', (e) => {
|
editor.on('themeChange', function(d){ document.getElementById('selTheme').value = d.theme; });
|
||||||
const btn = e.target.closest('[data-locale]');
|
editor.on('modeChange', function(m){ updateModeBtns(m); });
|
||||||
if (!btn) return;
|
editor.on('fileOpened', function(d){ editor.toast('已打开: ' + d.name, { type: 'success' }); });
|
||||||
document.querySelectorAll('#localeCtrl .ctrl-btn').forEach(b => b.classList.remove('active'));
|
editor.on('fileSaved', function(){ editor.toast('文件已保存', { type: 'success' }); });
|
||||||
btn.classList.add('active');
|
|
||||||
MeEditor.setLocale(btn.dataset.locale);
|
|
||||||
updateStatus();
|
|
||||||
});
|
|
||||||
document.querySelector('#localeCtrl [data-locale="zh-CN"]').classList.add('active');
|
|
||||||
|
|
||||||
// 只读切换
|
// Mermaid
|
||||||
document.getElementById('btnReadOnly').addEventListener('click', () => {
|
function renderMermaid(){
|
||||||
const isRO = editor.isReadOnly();
|
if (typeof mermaid === 'undefined') return;
|
||||||
editor.setReadOnly(!isRO);
|
try { mermaid.run({ querySelector: '.me-mermaid .mermaid' }); } catch(e) {}
|
||||||
document.getElementById('btnReadOnly').classList.toggle('active', !isRO);
|
}
|
||||||
document.getElementById('btnReadOnly').textContent = !isRO ? '只读中' : '只读';
|
mermaid.initialize({ startOnLoad: false, theme: 'default' });
|
||||||
log('setReadOnly', String(!isRO));
|
editor.on('afterRender', renderMermaid);
|
||||||
updateStatus();
|
setTimeout(renderMermaid, 300);
|
||||||
});
|
})();
|
||||||
|
|
||||||
// API 控制台
|
|
||||||
document.querySelector('.api-grid').addEventListener('click', (e) => {
|
|
||||||
const btn = e.target.closest('[data-api]');
|
|
||||||
if (!btn) return;
|
|
||||||
const api = btn.dataset.api;
|
|
||||||
if (editor.isDestroyed()) { log('error', '编辑器已销毁,请刷新页面'); return; }
|
|
||||||
switch (api) {
|
|
||||||
case 'getValue': log('getValue', editor.getValue().slice(0, 200) + '...'); break;
|
|
||||||
case 'getHTML': log('getHTML', editor.getHTML()); break;
|
|
||||||
case 'getStats': log('getStats', editor.getStats()); break;
|
|
||||||
case 'getStatus': log('getStatus', editor.getStatus()); break;
|
|
||||||
case 'insert': editor.insert('\n\n## 新插入的标题\n'); log('insert', '已插入内容'); break;
|
|
||||||
case 'exec-bold': editor.exec('bold'); log('exec', "执行 'bold'"); break;
|
|
||||||
case 'exec-h1': editor.exec('h1'); log('exec', "执行 'h1'"); break;
|
|
||||||
case 'setMode-edit': editor.setMode('edit'); log('setMode', '切换到 edit'); break;
|
|
||||||
case 'setMode-split': editor.setMode('split'); log('setMode', '切换到 split'); break;
|
|
||||||
case 'setMode-preview': editor.setMode('preview'); log('setMode', '切换到 preview'); break;
|
|
||||||
case 'fullscreen': editor.toggleFullscreen(); log('fullscreen', '已切换全屏'); break;
|
|
||||||
case 'undo': editor.undo(); log('undo', '撤销'); break;
|
|
||||||
case 'redo': editor.redo(); log('redo', '重做'); break;
|
|
||||||
case 'exportMD': editor.exportMarkdown(); log('export', '已触发 .md 下载'); break;
|
|
||||||
case 'exportHTML': editor.exportHTML('demo.html', { title: 'MetonaEditor 导出' }); log('export', '已触发 .html 下载'); break;
|
|
||||||
case 'restoreDraft': editor.restoreDraft(); log('restoreDraft', '已从 localStorage 恢复草稿'); break;
|
|
||||||
case 'clearDraft': editor.clearDraft(); log('clearDraft', '已清空草稿'); break;
|
|
||||||
case 'refresh': editor.refresh(); log('refresh', '强制刷新预览'); break;
|
|
||||||
case 'destroy': editor.destroy(); log('destroy', '编辑器已销毁'); updateStatus(); break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始状态
|
|
||||||
updateStatus();
|
|
||||||
logEvent('ready', '编辑器已就绪');
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+263
-434
@@ -3,179 +3,69 @@
|
|||||||
<head>
|
<head>
|
||||||
<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"/>
|
||||||
<title>MetonaEditor · 文档</title>
|
<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>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root {
|
: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%)}
|
||||||
--bg: #0f1117;
|
html{scroll-behavior:smooth}
|
||||||
--bg-soft: #161922;
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--text);line-height:1.7;min-height:100vh}
|
||||||
--card: #1c2029;
|
.container{max-width:1000px;margin:0 auto;padding:0 24px}
|
||||||
--card-hover: #232834;
|
nav{position:sticky;top:0;z-index:100;background:rgba(15,17,23,.85);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||||
--text: #e6e8eb;
|
.nav-inner{max-width:1000px;margin:0 auto;padding:12px 24px;display:flex;align-items:center;justify-content:space-between}
|
||||||
--muted: #9ca3af;
|
.nav-brand{display:inline-flex;align-items:center;gap:10px;text-decoration:none;color:var(--text);font-weight:700;font-size:15px}
|
||||||
--accent: #3b82f6;
|
.nav-brand .logo-mark{width:26px;height:26px;border-radius:6px;background:var(--gradient);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
--accent-2: #8b5cf6;
|
.logo-mark svg{display:block}
|
||||||
--accent-3: #ec4899;
|
.nav-links{display:flex;gap:22px;align-items:center}
|
||||||
--accent-soft: rgba(59, 130, 246, 0.12);
|
.nav-links a{color:var(--muted);text-decoration:none;font-size:13.5px;transition:color .15s}
|
||||||
--border: rgba(255, 255, 255, 0.08);
|
.nav-links a:hover,.nav-links a.active{color:var(--text)}
|
||||||
--gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
|
.nav-links a.active{color:var(--accent);font-weight:600}
|
||||||
--shadow: 0 20px 50px -20px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
html { scroll-behavior: smooth; }
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.7;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.container { max-width: 1000px; margin: 0 auto; padding: 0 24px; }
|
|
||||||
|
|
||||||
/* Nav */
|
header.docs-header{text-align:center;padding:64px 24px 40px;background:radial-gradient(ellipse 60% 50% at 50% 0%,var(--accent-soft),transparent 70%),var(--bg)}
|
||||||
nav {
|
header h1{font-size:clamp(28px,4vw,44px);font-weight:800;letter-spacing:-.02em;margin-bottom:10px}
|
||||||
position: sticky; top: 0; z-index: 100;
|
header h1 .grad{background:var(--gradient);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
||||||
background: rgba(15, 17, 23, 0.85);
|
header .sub{color:var(--muted);font-size:15px}
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.nav-inner {
|
|
||||||
max-width: 1000px; margin: 0 auto; padding: 12px 24px;
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
}
|
|
||||||
.nav-brand {
|
|
||||||
display: inline-flex; align-items: center; gap: 10px;
|
|
||||||
text-decoration: none; color: var(--text);
|
|
||||||
font-weight: 700; font-size: 15px;
|
|
||||||
}
|
|
||||||
.nav-brand .logo-mark {
|
|
||||||
width: 26px; height: 26px; border-radius: 6px;
|
|
||||||
background: var(--gradient);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.logo-mark svg { display: block; }
|
|
||||||
.nav-links { display: flex; gap: 22px; align-items: center; }
|
|
||||||
.nav-links a { color: var(--muted); text-decoration: none; font-size: 13.5px; transition: color .15s; }
|
|
||||||
.nav-links a:hover, .nav-links a.active { color: var(--text); }
|
|
||||||
.nav-links a.active { color: var(--accent); font-weight: 600; }
|
|
||||||
|
|
||||||
/* Header */
|
.docs-layout{display:flex;gap:32px;padding:40px 0 80px}
|
||||||
header.docs-header {
|
.sidebar{position:sticky;top:72px;width:200px;flex-shrink:0;max-height:calc(100vh - 100px);overflow-y:auto;padding-right:16px;border-right:1px solid var(--border)}
|
||||||
text-align: center; padding: 64px 24px 40px;
|
.sidebar a{display:block;padding:6px 12px;border-radius:6px;color:var(--muted);text-decoration:none;font-size:13.5px;transition:background .15s,color .15s;margin-bottom:2px}
|
||||||
background: radial-gradient(ellipse 60% 50% at 50% 0%, var(--accent-soft), transparent 70%), var(--bg);
|
.sidebar a:hover,.sidebar a.active{background:var(--card);color:var(--text)}
|
||||||
}
|
.sidebar .toc-group{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;padding:16px 12px 6px;font-weight:600}
|
||||||
header h1 { font-size: clamp(28px, 4vw, 44px); font-weight: 800; letter-spacing: -0.02em; margin-bottom: 10px; }
|
.content{flex:1;min-width:0}
|
||||||
header h1 .grad { background: var(--gradient); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
|
.content section{margin-bottom:48px}
|
||||||
header .sub { color: var(--muted); font-size: 15px; }
|
.content h2{font-size:24px;font-weight:700;letter-spacing:-.02em;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--border)}
|
||||||
|
.content h3{font-size:18px;font-weight:600;margin:28px 0 14px}
|
||||||
|
.content h4{font-size:15px;font-weight:600;margin:20px 0 10px;color:var(--accent)}
|
||||||
|
|
||||||
/* Sidebar + Content Layout */
|
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:13.5px}
|
||||||
.docs-layout { display: flex; gap: 32px; padding: 40px 0 80px; }
|
th,td{padding:10px 14px;text-align:left;border:1px solid var(--border)}
|
||||||
.sidebar {
|
th{background:var(--card);font-weight:600;color:var(--text)}
|
||||||
position: sticky; top: 72px;
|
td{color:var(--muted)}
|
||||||
width: 200px; flex-shrink: 0;
|
tr:nth-child(even) td{background:var(--bg-soft)}
|
||||||
max-height: calc(100vh - 100px); overflow-y: auto;
|
|
||||||
padding-right: 16px;
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.sidebar a {
|
|
||||||
display: block; padding: 6px 12px; border-radius: 6px;
|
|
||||||
color: var(--muted); text-decoration: none; font-size: 13.5px;
|
|
||||||
transition: background .15s, color .15s;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
.sidebar a:hover, .sidebar a.active { background: var(--card); color: var(--text); }
|
|
||||||
.sidebar .toc-group { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; padding: 16px 12px 6px; font-weight: 600; }
|
|
||||||
.content { flex: 1; min-width: 0; }
|
|
||||||
.content section { margin-bottom: 48px; }
|
|
||||||
.content h2 {
|
|
||||||
font-size: 24px; font-weight: 700; letter-spacing: -0.02em;
|
|
||||||
margin-bottom: 16px; padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.content h3 { font-size: 18px; font-weight: 600; margin: 28px 0 14px; }
|
|
||||||
.content h4 { font-size: 15px; font-weight: 600; margin: 20px 0 10px; color: var(--accent); }
|
|
||||||
|
|
||||||
/* Tables */
|
code{font-family:"SF Mono","Cascadia Code",Consolas,monospace;font-size:13px;padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;color:var(--accent)}
|
||||||
table {
|
pre{background:var(--bg-soft);border:1px solid var(--border);border-radius:10px;padding:18px 20px;overflow-x:auto;font-family:"SF Mono","Cascadia Code",Consolas,monospace;font-size:13px;line-height:1.8;color:#e6e8eb;margin:14px 0}
|
||||||
width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13.5px;
|
.c-key{color:#c792ea}.c-str{color:#c3e88d}.c-fn{color:#82aaff}.c-com{color:#5c6370;font-style:italic}.c-kw{color:#89ddff}.c-num{color:#f78c6c}.c-punc{color:#89ddff;opacity:.7}
|
||||||
}
|
.badge{display:inline-block;padding:3px 10px;background:var(--accent-soft);color:var(--accent);border-radius:999px;font-size:12px;font-weight:600;margin-right:6px;margin-bottom:4px}
|
||||||
th, td {
|
|
||||||
padding: 10px 14px; text-align: left;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
th { background: var(--card); font-weight: 600; color: var(--text); }
|
|
||||||
td { color: var(--muted); }
|
|
||||||
tr:nth-child(even) td { background: var(--bg-soft); }
|
|
||||||
|
|
||||||
/* Code */
|
footer{border-top:1px solid var(--border);padding:36px 24px;text-align:center;color:var(--muted);font-size:13px;background:var(--bg-soft)}
|
||||||
code {
|
footer a{color:var(--accent);text-decoration:none}
|
||||||
font-family: "SF Mono", "Cascadia Code", Consolas, monospace;
|
|
||||||
font-size: 13px; padding: 2px 6px;
|
|
||||||
background: var(--card); border: 1px solid var(--border);
|
|
||||||
border-radius: 4px; color: var(--accent);
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
background: var(--bg-soft); border: 1px solid var(--border);
|
|
||||||
border-radius: 10px; padding: 18px 20px; overflow-x: auto;
|
|
||||||
font-family: "SF Mono", "Cascadia Code", Consolas, monospace;
|
|
||||||
font-size: 13px; line-height: 1.8;
|
|
||||||
color: #e6e8eb; margin: 14px 0;
|
|
||||||
}
|
|
||||||
pre .c-key { color: #c792ea; }
|
|
||||||
pre .c-str { color: #c3e88d; }
|
|
||||||
pre .c-fn { color: #82aaff; }
|
|
||||||
pre .c-com { color: #5c6370; font-style: italic; }
|
|
||||||
pre .c-kw { color: #89ddff; }
|
|
||||||
pre .c-num { color: #f78c6c; }
|
|
||||||
pre .c-punc { color: #89ddff; opacity: 0.7; }
|
|
||||||
|
|
||||||
/* Badges */
|
@media(max-width:768px){.docs-layout{flex-direction:column}.sidebar{position:static;width:100%;max-height:none;border-right:0;border-bottom:1px solid var(--border);padding-bottom:12px;display:flex;flex-wrap:wrap;gap:4px}.sidebar .toc-group{width:100%}}
|
||||||
.badge {
|
|
||||||
display: inline-block; padding: 3px 10px;
|
|
||||||
background: var(--accent-soft); color: var(--accent);
|
|
||||||
border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
||||||
margin-right: 6px; margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
footer {
|
|
||||||
border-top: 1px solid var(--border); padding: 36px 24px;
|
|
||||||
text-align: center; color: var(--muted); font-size: 13px;
|
|
||||||
background: var(--bg-soft);
|
|
||||||
}
|
|
||||||
footer a { color: var(--accent); text-decoration: none; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.docs-layout { flex-direction: column; }
|
|
||||||
.sidebar {
|
|
||||||
position: static; width: 100%; max-height: none;
|
|
||||||
border-right: 0; border-bottom: 1px solid var(--border);
|
|
||||||
padding-bottom: 12px;
|
|
||||||
display: flex; flex-wrap: wrap; gap: 4px;
|
|
||||||
}
|
|
||||||
.sidebar .toc-group { width: 100%; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-inner">
|
<div class="nav-inner">
|
||||||
<a class="nav-brand" href="index.html">
|
<a class="nav-brand" href="index.html"><span class="logo-mark"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19V6l7 8 7-8v13"/></svg></span>MetonaEditor</a>
|
||||||
<span class="logo-mark" title="MetonaEditor"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19V6l7 8 7-8v13"/></svg></span>
|
<div class="nav-links"><a href="index.html">首页</a><a href="demo.html">演示</a><a href="docs.html" class="active">文档</a></div>
|
||||||
MetonaEditor
|
|
||||||
</a>
|
|
||||||
<div class="nav-links">
|
|
||||||
<a href="index.html">首页</a>
|
|
||||||
<a href="demo.html">演示</a>
|
|
||||||
<a href="docs.html" class="active">文档</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header class="docs-header">
|
<header class="docs-header">
|
||||||
<h1><span class="grad">文档</span></h1>
|
<h1><span class="grad">API 文档</span></h1>
|
||||||
<p class="sub">MetonaEditor v0.1.3 完整 API、配置与使用指南</p>
|
<p class="sub">MetonaEditor v0.2.4 · TypeScript · 完整配置、API 与使用指南</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -190,18 +80,20 @@
|
|||||||
<a href="#api-content">内容操作</a>
|
<a href="#api-content">内容操作</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">模式与全屏</a>
|
<a href="#api-mode">模式 / 全屏 / Zen</a>
|
||||||
<a href="#api-stats">统计与状态</a>
|
<a href="#api-stats">统计与状态</a>
|
||||||
<a href="#api-readonly">启用/禁用/只读</a>
|
<a href="#api-readonly">启用 / 禁用 / 只读</a>
|
||||||
<a href="#api-events">事件系统</a>
|
<a href="#api-events">事件系统</a>
|
||||||
<a href="#api-plugin">插件管理</a>
|
<a href="#api-plugin">插件管理</a>
|
||||||
<a href="#api-lifecycle">生命周期</a>
|
<a href="#api-shortcuts">快捷键管理</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>
|
<div class="toc-group">解析器</div>
|
||||||
<a href="#syntax">解析器语法</a>
|
<a href="#parser-syntax">语法参考</a>
|
||||||
|
<a href="#parser-api">解析器 API</a>
|
||||||
<div class="toc-group">插件</div>
|
<div class="toc-group">插件</div>
|
||||||
<a href="#plugins">插件约定</a>
|
<a href="#plugins-convention">插件约定</a>
|
||||||
<a href="#preset-plugins">预设插件</a>
|
<a href="#preset-plugins">预设插件</a>
|
||||||
<div class="toc-group">主题</div>
|
<div class="toc-group">主题</div>
|
||||||
<a href="#themes">主题系统</a>
|
<a href="#themes">主题系统</a>
|
||||||
@@ -211,94 +103,77 @@
|
|||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
|
|
||||||
<!-- ======== 快速入门 ======== -->
|
<!-- 快速入门 -->
|
||||||
<section id="quickstart">
|
<section id="quickstart">
|
||||||
<h2>安装与引入</h2>
|
<h2>安装与引入</h2>
|
||||||
|
|
||||||
<pre><span class="c-com"># npm 安装</span>
|
<pre><span class="c-com"># npm 安装</span>
|
||||||
<span class="c-kw">npm</span> install @metona-team/metona-editor
|
<span class="c-kw">npm</span> install @metona-team/metona-editor
|
||||||
|
|
||||||
<span class="c-com"># ES Module</span>
|
<span class="c-com">// TypeScript / ES Module</span>
|
||||||
<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-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-kw">const</span> editor <span class="c-punc">=</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>
|
<span class="c-kw">const</span> editor <span class="c-punc">=</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>
|
||||||
<span class="c-fn">value</span><span class="c-punc">:</span> <span class="c-str">'# Hello World'</span><span class="c-punc">,</span>
|
<span class="c-fn">value</span><span class="c-punc">:</span> <span class="c-str">'# Hello World'</span><span class="c-punc">,</span>
|
||||||
<span class="c-fn">mode</span><span class="c-punc">:</span> <span class="c-str">'split'</span><span class="c-punc">,</span>
|
<span class="c-fn">mode</span><span class="c-punc">:</span> <span class="c-str">'split'</span><span class="c-punc">,</span>
|
||||||
<span class="c-punc">});</span>
|
<span class="c-punc">});</span>
|
||||||
|
|
||||||
<span class="c-com"># 浏览器 UMD</span>
|
<span class="c-com">// 浏览器 UMD</span>
|
||||||
<span class="c-com"><script src="dist/metona-editor.js"></script></span>
|
<span class="c-com"><script src="dist/metona-editor.js"></script></span>
|
||||||
<span class="c-com"><script></span>
|
<span class="c-com"><script></span>
|
||||||
<span class="c-kw">const</span> editor <span class="c-punc">=</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-kw">const</span> editor <span class="c-punc">=</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-com"></script></span></pre>
|
<span class="c-com"></script></span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 配置项 ======== -->
|
<!-- 配置 -->
|
||||||
<section id="config">
|
<section id="config">
|
||||||
<h2>全部配置项</h2>
|
<h2>全部配置项</h2>
|
||||||
<p style="color:var(--muted);margin-bottom:14px">所有配置均为可选,以下是默认值:</p>
|
<p style="color:var(--muted);margin-bottom:14px">所有配置均为可选。</p>
|
||||||
|
|
||||||
<pre>MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span>
|
<pre>MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span>
|
||||||
<span class="c-com">// 内容</span>
|
<span class="c-com">// 内容</span>
|
||||||
value<span class="c-punc">:</span> <span class="c-str">''</span><span class="c-punc">,</span> <span class="c-com">// 初始 Markdown 文本</span>
|
value<span class="c-punc">:</span> <span class="c-str">''</span><span class="c-punc">,</span> placeholder<span class="c-punc">:</span> <span class="c-str">''</span><span class="c-punc">,</span>
|
||||||
placeholder<span class="c-punc">:</span> <span class="c-str">''</span><span class="c-punc">,</span> <span class="c-com">// 占位符</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 视图</span>
|
<span class="c-com">// 视图</span>
|
||||||
mode<span class="c-punc">:</span> <span class="c-str">'split'</span><span class="c-punc">,</span> <span class="c-com">// 'edit' | 'split' | 'preview'</span>
|
mode<span class="c-punc">:</span> <span class="c-str">'split'</span><span class="c-punc">,</span> height<span class="c-punc">:</span> <span class="c-num">400</span><span class="c-punc">,</span>
|
||||||
height<span class="c-punc">:</span> <span class="c-num">400</span><span class="c-punc">,</span> <span class="c-com">// 数字为 px,字符串原样使用</span>
|
toolbar<span class="c-punc">:</span> DEFAULT_TOOLBAR<span class="c-punc">,</span> wordCount<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span>
|
||||||
toolbar<span class="c-punc">:</span> DEFAULT_TOOLBAR<span class="c-punc">,</span> <span class="c-com">// 工具栏配置,false 隐藏</span>
|
lineNumbers<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span> outline<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span>
|
||||||
wordCount<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>
|
||||||
autofocus<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> <span class="c-com">// 自动聚焦</span>
|
autofocus<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> spellcheck<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span>
|
||||||
spellcheck<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> <span class="c-com">// 拼写检查</span>
|
readOnly<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> tabSize<span class="c-punc">:</span> <span class="c-num">2</span><span class="c-punc">,</span>
|
||||||
readOnly<span class="c-punc">:</span> <span class="c-kw">false</span><span class="c-punc">,</span> <span class="c-com">// 只读模式</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>
|
||||||
historyLimit<span class="c-punc">:</span> <span class="c-num">100</span><span class="c-punc">,</span> <span class="c-com">// 历史栈上限</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>
|
||||||
historyDebounce<span class="c-punc">:</span> <span class="c-num">400</span><span class="c-punc">,</span> <span class="c-com">// 历史防抖延迟(ms)</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>
|
||||||
syncScroll<span class="c-punc">:</span> <span class="c-kw">true</span><span class="c-punc">,</span> <span class="c-com">// 分屏模式同步滚动</span>
|
|
||||||
tabSize<span class="c-punc">:</span> <span class="c-num">2</span><span class="c-punc">,</span> <span class="c-com">// 制表符空格数(0 表示 \t)</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> <span class="c-com">// 'light' | 'dark' | 'auto' | 'warm' | 自定义</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>
|
||||||
locale<span class="c-punc">:</span> <span class="c-str">'zh-CN'</span><span class="c-punc">,</span> <span class="c-com">// 'zh-CN' | 'en-US' | 自定义</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 自定义渲染</span>
|
<span class="c-com">// 钩子</span>
|
||||||
render<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (md, env) => html</span>
|
render<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> highlight<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
highlight<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (code, lang) => html</span>
|
sanitize<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
sanitize<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (html) => safeHtml</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 外观</span>
|
<span class="c-com">// 外观</span>
|
||||||
className<span class="c-punc">:</span> <span class="c-str">''</span><span class="c-punc">,</span> <span class="c-com">// 容器额外 class</span>
|
className<span class="c-punc">:</span> <span class="c-str">''</span><span class="c-punc">,</span> style<span class="c-punc">:</span> <span class="c-punc">{},</span>
|
||||||
style<span class="c-punc">:</span> <span class="c-punc">{},</span> <span class="c-com">// 内联样式</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 插件与回调</span>
|
<span class="c-com">// 插件 / 回调</span>
|
||||||
plugins<span class="c-punc">:</span> <span class="c-punc">[],</span> <span class="c-com">// 实例级插件数组</span>
|
plugins<span class="c-punc">:</span> <span class="c-punc">[],</span>
|
||||||
onChange<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (value, editor) => void</span>
|
onChange<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> onInput<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
onInput<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (value, editor) => void</span>
|
onFocus<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> onBlur<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
onFocus<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (editor) => void</span>
|
onSave<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> onModeChange<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
onBlur<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (editor) => void</span>
|
onFullscreen<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> onCreate<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
onSave<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (value, editor) => void</span>
|
onDestroy<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> onLinkClick<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span>
|
||||||
onModeChange<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (mode, editor) => void</span>
|
|
||||||
onFullscreen<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (fullscreen, editor) => void</span>
|
|
||||||
onCreate<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (editor) => void</span>
|
|
||||||
onDestroy<span class="c-punc">:</span> <span class="c-kw">null</span><span class="c-punc">,</span> <span class="c-com">// (editor) => void</span>
|
|
||||||
<span class="c-punc">});</span></pre>
|
<span class="c-punc">});</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 工具栏 ======== -->
|
<!-- 工具栏 -->
|
||||||
<section id="toolbar">
|
<section id="toolbar">
|
||||||
<h2>工具栏配置</h2>
|
<h2>工具栏</h2>
|
||||||
|
<pre><span class="c-com">// 默认完整工具栏</span>
|
||||||
<pre><span class="c-com">// 默认工具栏(完整按钮)</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span> toolbar<span class="c-punc">:</span> <span class="c-kw">true</span> <span class="c-punc">});</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span> toolbar<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>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span> toolbar<span class="c-punc">:</span> <span class="c-kw">false</span> <span class="c-punc">});</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span> toolbar<span class="c-punc">:</span> <span class="c-kw">false</span> <span class="c-punc">});</span>
|
||||||
|
|
||||||
<span class="c-com">// 自定义工具栏 — '|' 为分隔符</span>
|
<span class="c-com">// 自定义 | 为分隔符</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> <span class="c-punc">{</span> toolbar<span class="c-punc">:</span> <span class="c-punc">[</span><span class="c-str">'bold'</span><span class="c-punc">,</span> <span class="c-str">'italic'</span><span class="c-punc">,</span> <span class="c-str">'|'</span><span class="c-punc">,</span> <span class="c-str">'undo'</span><span class="c-punc">,</span> <span class="c-str">'redo'</span><span class="c-punc">]</span> <span class="c-punc">});</span></pre>
|
||||||
toolbar<span class="c-punc">:</span> <span class="c-punc">[</span><span class="c-str">'bold'</span><span class="c-punc">,</span> <span class="c-str">'italic'</span><span class="c-punc">,</span> <span class="c-str">'|'</span><span class="c-punc">,</span> <span class="c-str">'h1'</span><span class="c-punc">,</span> <span class="c-str">'h2'</span><span class="c-punc">,</span> <span class="c-str">'|'</span><span class="c-punc">,</span> <span class="c-str">'undo'</span><span class="c-punc">,</span> <span class="c-str">'redo'</span><span class="c-punc">],</span>
|
|
||||||
<span class="c-punc">});</span></pre>
|
|
||||||
|
|
||||||
<p>可用动作:</p>
|
<p>可用动作:</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="badge">bold</span><span class="badge">italic</span><span class="badge">strikethrough</span><span class="badge">underline</span><span class="badge">code</span>
|
<span class="badge">bold</span><span class="badge">italic</span><span class="badge">strikethrough</span><span class="badge">underline</span><span class="badge">code</span>
|
||||||
@@ -310,286 +185,246 @@ MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 实例 API ======== -->
|
<!-- 实例 API -->
|
||||||
<section id="api-content">
|
<section id="api-content"><h2>实例 API</h2>
|
||||||
<h2>实例 API</h2>
|
|
||||||
|
|
||||||
<h3>内容操作</h3>
|
<h3>内容操作</h3>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">getValue</span><span class="c-punc">();</span> <span class="c-com">// 获取 Markdown 文本</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">getValue</span><span class="c-punc">();</span> <span class="c-com">// => string</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">setValue</span><span class="c-punc">(</span>md<span class="c-punc">,</span> <span class="c-punc">{</span> silent <span class="c-punc">});</span> <span class="c-com">// 设置内容,silent 不触发 change</span>
|
editor<span class="c-punc">.</span><span class="c-fn">setValue</span><span class="c-punc">(</span>md<span class="c-punc">,</span> <span class="c-punc">{</span> silent <span class="c-punc">});</span> <span class="c-com">// => this</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">getHTML</span><span class="c-punc">();</span> <span class="c-com">// 获取渲染后的 HTML</span>
|
editor<span class="c-punc">.</span><span class="c-fn">getHTML</span><span class="c-punc">();</span> <span class="c-com">// => string</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">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">// 光标处插入 / 替换选区</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">// 选区包裹</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">focus</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">blur</span><span class="c-punc">();</span> <span class="c-com">// 失焦</span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-exec">
|
<section id="api-exec"><h3>命令执行</h3>
|
||||||
<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-punc">...</span>args<span class="c-punc">);</span> <span class="c-com">// 执行命令,返回 this 链式调用</span>
|
|
||||||
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">exec</span><span class="c-punc">(</span><span class="c-str">'bold'</span><span class="c-punc">).</span><span class="c-fn">exec</span><span class="c-punc">(</span><span class="c-str">'h1'</span><span class="c-punc">);</span> <span class="c-com">// 链式调用</span></pre>
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr><th>action</th><th>效果</th><th>快捷键</th></tr>
|
<tr><th>action</th><th>效果</th><th>快捷键</th></tr>
|
||||||
<tr><td><code>bold</code></td><td>加粗 **text**</td><td>Ctrl+B</td></tr>
|
<tr><td><code>bold</code></td><td>**text**</td><td>Ctrl+B</td></tr>
|
||||||
<tr><td><code>italic</code></td><td>斜体 *text*</td><td>Ctrl+I</td></tr>
|
<tr><td><code>italic</code></td><td>*text*</td><td>Ctrl+I</td></tr>
|
||||||
<tr><td><code>strikethrough</code></td><td>删除线 ~~text~~</td><td>—</td></tr>
|
<tr><td><code>strikethrough</code></td><td>~~text~~</td><td>—</td></tr>
|
||||||
<tr><td><code>underline</code></td><td>下划线 <u>text</u></td><td>Ctrl+U</td></tr>
|
<tr><td><code>underline</code></td><td><u>text</u></td><td>Ctrl+U</td></tr>
|
||||||
<tr><td><code>code</code></td><td>行内代码 `text`</td><td>Ctrl+E</td></tr>
|
<tr><td><code>code</code></td><td>`text`</td><td>Ctrl+E</td></tr>
|
||||||
<tr><td><code>h1</code>/<code>h2</code>/<code>h3</code></td><td>标题 # / ## / ###</td><td>Ctrl+1/2/3</td></tr>
|
<tr><td><code>h1</code>/<code>h2</code>/<code>h3</code></td><td># / ## / ###</td><td>Ctrl+1/2/3</td></tr>
|
||||||
<tr><td><code>quote</code></td><td>引用 ></td><td>Ctrl+Q</td></tr>
|
<tr><td><code>quote</code></td><td>> text</td><td>Ctrl+Q</td></tr>
|
||||||
<tr><td><code>ul</code>/<code>ol</code></td><td>无序/有序列表</td><td>—</td></tr>
|
<tr><td><code>ul</code>/<code>ol</code></td><td>- / 1. 列表</td><td>—</td></tr>
|
||||||
<tr><td><code>indent</code>/<code>outdent</code></td><td>缩进/反缩进</td><td>Tab/Shift+Tab</td></tr>
|
<tr><td><code>link</code></td><td>[text](url)</td><td>Ctrl+K</td></tr>
|
||||||
<tr><td><code>hr</code></td><td>水平线 ---</td><td>—</td></tr>
|
<tr><td><code>image</code></td><td></td><td>—</td></tr>
|
||||||
<tr><td><code>link</code></td><td>插入链接 [text](url)</td><td>Ctrl+K</td></tr>
|
|
||||||
<tr><td><code>image</code></td><td>插入图片 </td><td>—</td></tr>
|
|
||||||
<tr><td><code>table</code></td><td>插入表格</td><td>—</td></tr>
|
<tr><td><code>table</code></td><td>插入表格</td><td>—</td></tr>
|
||||||
<tr><td><code>undo</code>/<code>redo</code></td><td>撤销/重做</td><td>Ctrl+Z / Ctrl+Y</td></tr>
|
<tr><td><code>hr</code></td><td>---水平线</td><td>—</td></tr>
|
||||||
|
<tr><td><code>indent</code>/<code>outdent</code></td><td>缩进/反缩进</td><td>Tab / Shift+Tab</td></tr>
|
||||||
|
<tr><td><code>undo</code>/<code>redo</code></td><td>撤销/重做</td><td>Ctrl+Z / Y</td></tr>
|
||||||
<tr><td><code>edit</code>/<code>split</code>/<code>preview</code></td><td>切换模式</td><td>—</td></tr>
|
<tr><td><code>edit</code>/<code>split</code>/<code>preview</code></td><td>切换模式</td><td>—</td></tr>
|
||||||
<tr><td><code>fullscreen</code></td><td>切换全屏</td><td>—</td></tr>
|
<tr><td><code>fullscreen</code></td><td>全屏</td><td>—</td></tr>
|
||||||
|
<tr><td><code>zen</code></td><td>Zen 模式</td><td>—</td></tr>
|
||||||
|
<tr><td><code>wordwrap</code></td><td>自动换行</td><td>—</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-history">
|
<section id="api-history"><h3>历史栈</h3>
|
||||||
<h3>历史栈</h3>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">undo</span><span class="c-punc">();</span> editor<span class="c-punc">.</span><span class="c-fn">redo</span><span class="c-punc">();</span>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">undo</span><span class="c-punc">();</span> <span class="c-com">// 撤销</span>
|
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">redo</span><span class="c-punc">();</span> <span class="c-com">// 重做</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">canUndo</span><span class="c-punc">();</span> <span class="c-com">// 是否可撤销</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">canRedo</span><span class="c-punc">();</span> <span class="c-com">// 是否可重做</span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-mode">
|
<section id="api-mode"><h3>模式 / 全屏 / Zen</h3>
|
||||||
<h3>模式与全屏</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> <span class="c-com">// 设置模式:edit | split | preview</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">getMode</span><span class="c-punc">();</span> <span class="c-com">// 获取当前模式</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">toggleFullscreen</span><span class="c-punc">();</span> <span class="c-com">// 切换全屏</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">exitFullscreen</span><span class="c-punc">();</span> <span class="c-com">// 退出全屏</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">isFullscreen</span><span class="c-punc">();</span> <span class="c-com">// 是否全屏</span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-stats">
|
<section id="api-stats"><h3>统计与状态</h3>
|
||||||
<h3>统计与状态</h3>
|
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">getStats</span><span class="c-punc">();</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">getStats</span><span class="c-punc">();</span>
|
||||||
<span class="c-com">// => { characters, words, chineseChars, englishWords, lines, readingTime }</span>
|
<span class="c-com">// => { characters, words, chineseChars, englishWords, lines, readingTime }</span>
|
||||||
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">getStatus</span><span class="c-punc">();</span>
|
editor<span class="c-punc">.</span><span class="c-fn">getStatus</span><span class="c-punc">();</span>
|
||||||
<span class="c-com">// => { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }</span></pre>
|
<span class="c-com">// => { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-readonly">
|
<section id="api-readonly"><h3>启用 / 禁用 / 只读</h3>
|
||||||
<h3>启用/禁用/只读</h3>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">enable</span><span class="c-punc">();</span> <span class="c-fn">disable</span><span class="c-punc">();</span> <span class="c-fn">isDisabled</span><span class="c-punc">();</span>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">enable</span><span class="c-punc">();</span> <span class="c-com">// 启用</span>
|
editor<span class="c-punc">.</span><span class="c-fn">setReadOnly</span><span class="c-punc">(</span><span class="c-kw">true</span><span class="c-punc">);</span> <span class="c-fn">isReadOnly</span><span class="c-punc">();</span></pre>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">disable</span><span class="c-punc">();</span> <span class="c-com">// 禁用</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">isDisabled</span><span class="c-punc">();</span> <span class="c-com">// 是否禁用</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">setReadOnly</span><span class="c-punc">(</span><span class="c-kw">true</span><span class="c-punc">);</span> <span class="c-com">// 只读模式</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">isReadOnly</span><span class="c-punc">();</span> <span class="c-com">// 是否只读</span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-events">
|
<section id="api-events"><h3>事件系统</h3>
|
||||||
<h3>事件系统</h3>
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr><th>事件名</th><th>触发时机</th><th>回调参数</th></tr>
|
<tr><th>事件</th><th>触发时机</th><th>参数</th></tr>
|
||||||
<tr><td><code>input</code></td><td>textarea 原生 input</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>focus</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>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>(value, 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>
|
||||||
<tr><td><code>fullscreen</code></td><td>全屏切换</td><td><code>(fullscreen, editor)</code></td></tr>
|
<tr><td><code>fullscreen</code></td><td>全屏切换</td><td><code>(fullscreen, editor)</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>zenChange</code></td><td>Zen 切换</td><td><code>(zen, editor)</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>autosave</code></td><td>autoSave 保存</td><td><code>({ key, value })</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></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>afterRender</code></td><td>渲染后</td><td><code>(editor)</code></td></tr>
|
<tr><td><code>linkClick</code></td><td>预览链接点击</td><td><code>({ href, text })</code></td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
<h4>全局钩子</h4>
|
||||||
<h4>全局钩子(所有实例共享)</h4>
|
|
||||||
<table>
|
<table>
|
||||||
<tr><th>钩子</th><th>触发时机</th></tr>
|
<tr><th>钩子</th><th>触发时机</th></tr>
|
||||||
<tr><td><code>beforeCreate</code></td><td>构造函数初始化前</td></tr>
|
<tr><td><code>beforeCreate</code> / <code>afterCreate</code></td><td>实例构造前后</td></tr>
|
||||||
<tr><td><code>afterCreate</code></td><td>构造函数初始化完成</td></tr>
|
<tr><td><code>beforeRender</code> / <code>afterRender</code></td><td>全局渲染前后</td></tr>
|
||||||
<tr><td><code>beforeRender</code></td><td>每次渲染前(全局)</td></tr>
|
<tr><td><code>beforeDestroy</code> / <code>afterDestroy</code></td><td>销毁前后</td></tr>
|
||||||
<tr><td><code>afterRender</code></td><td>每次渲染后(全局)</td></tr>
|
|
||||||
<tr><td><code>beforeDestroy</code></td><td>destroy 前</td></tr>
|
|
||||||
<tr><td><code>afterDestroy</code></td><td>destroy 后</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-plugin">
|
<section id="api-plugin"><h3>插件管理</h3>
|
||||||
<h3>插件管理</h3>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>plugin<span class="c-punc">,</span> options<span class="c-punc">);</span> <span class="c-com">// 安装</span>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>plugin<span class="c-punc">,</span> options<span class="c-punc">);</span> <span class="c-com">// 安装插件</span>
|
editor<span class="c-punc">.</span><span class="c-fn">unuse</span><span class="c-punc">(</span>name<span class="c-punc">);</span> <span class="c-com">// 卸载</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">getPlugins</span><span class="c-punc">();</span> <span class="c-com">// 获取已安装插件列表</span>
|
editor<span class="c-punc">.</span><span class="c-fn">getPlugins</span><span class="c-punc">();</span> <span class="c-com">// 已安装列表</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">addToolbarButton</span><span class="c-punc">(</span>config<span class="c-punc">);</span> <span class="c-com">// 追加工具栏按钮</span></pre>
|
editor<span class="c-punc">.</span><span class="c-fn">addToolbarButton</span><span class="c-punc">(</span>config<span class="c-punc">);</span> <span class="c-com">// 追加按钮</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="api-lifecycle">
|
<section id="api-shortcuts"><h3>快捷键管理</h3>
|
||||||
<h3>生命周期</h3>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">registerShortcut</span><span class="c-punc">(</span><span class="c-str">'Ctrl+Shift+T'</span><span class="c-punc">,</span> handler<span class="c-punc">,</span> <span class="c-str">'desc'</span><span class="c-punc">);</span>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">destroy</span><span class="c-punc">();</span> <span class="c-com">// 销毁实例</span>
|
editor<span class="c-punc">.</span><span class="c-fn">unregisterShortcut</span><span class="c-punc">(</span><span class="c-str">'Ctrl+Shift+T'</span><span class="c-punc">);</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">isDestroyed</span><span class="c-punc">();</span> <span class="c-com">// 是否已销毁</span></pre>
|
editor<span class="c-punc">.</span><span class="c-fn">getShortcuts</span><span class="c-punc">();</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 静态 API ======== -->
|
<section id="api-destroy"><h3>生命周期 / Toast</h3>
|
||||||
<section id="static-api">
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">destroy</span><span class="c-punc">();</span> <span class="c-fn">isDestroyed</span><span class="c-punc">();</span>
|
||||||
<h2>静态 API</h2>
|
editor<span class="c-punc">.</span><span class="c-fn">toast</span><span class="c-punc">(</span>msg<span class="c-punc">,</span> <span class="c-punc">{</span> type<span class="c-punc">,</span> duration<span class="c-punc">,</span> animation <span class="c-punc">});</span></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 静态 API -->
|
||||||
|
<section id="static-api"><h2>静态 API</h2>
|
||||||
<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>
|
<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">// 工厂函数</span>
|
<span class="c-com">// 工厂函数</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> options<span class="c-punc">);</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span>container<span class="c-punc">,</span> options<span class="c-punc">);</span>
|
||||||
|
|
||||||
<span class="c-com">// 全局默认插件</span>
|
<span class="c-com">// 全局默认插件</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>presetPlugins<span class="c-punc">.</span>autoSave<span class="c-punc">,</span> <span class="c-punc">{</span> delay<span class="c-punc">:</span> <span class="c-num">2000</span> <span class="c-punc">});</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'autoSave'</span><span class="c-punc">,</span> <span class="c-punc">{</span> delay<span class="c-punc">:</span> <span class="c-num">2000</span> <span class="c-punc">});</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'searchReplace'</span><span class="c-punc">);</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>customPlugin<span class="c-punc">);</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>customPlugin<span class="c-punc">);</span>
|
||||||
|
|
||||||
<span class="c-com">// 全局事件钩子</span>
|
<span class="c-com">// 全局钩子</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">on</span><span class="c-punc">(</span><span class="c-str">'beforeCreate'</span><span class="c-punc">,</span> <span class="c-punc">(</span>editor<span class="c-punc">)</span> <span class="c-punc">=></span> <span class="c-punc">{});</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">on</span><span class="c-punc">(</span><span class="c-str">'beforeCreate'</span><span class="c-punc">,</span> <span class="c-punc">(</span>editor<span class="c-punc">)</span> <span class="c-punc">=></span> <span class="c-punc">{});</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">off</span><span class="c-punc">(</span><span class="c-str">'beforeCreate'</span><span class="c-punc">,</span> handler<span class="c-punc">);</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">off</span><span class="c-punc">(</span><span class="c-str">'beforeCreate'</span><span class="c-punc">,</span> handler<span class="c-punc">);</span>
|
||||||
|
|
||||||
<span class="c-com">// 主题与语言</span>
|
<span class="c-com">// 全局主题 / 语言</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'dark'</span><span class="c-punc">);</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'dark'</span><span class="c-punc">);</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span class="c-punc">(</span><span class="c-str">'en-US'</span><span class="c-punc">);</span>
|
MeEditor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span class="c-punc">(</span><span class="c-str">'en-US'</span><span class="c-punc">);</span>
|
||||||
|
|
||||||
<span class="c-com">// 状态查询</span>
|
<span class="c-com">// 解析器独立使用</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">getStatus</span><span class="c-punc">();</span> <span class="c-com">// => { version, theme, locale, globalPlugins, presetPlugins }</span>
|
<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-com">// 销毁全局资源</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">destroy</span><span class="c-punc">();</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 内置解析器</span>
|
|
||||||
<span class="c-kw">import</span> <span class="c-punc">{</span> parseMarkdown<span class="c-punc">,</span> safeUrl<span class="c-punc">,</span> slugify <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>
|
|
||||||
<span class="c-fn">parseMarkdown</span><span class="c-punc">(</span><span class="c-str">'# Hello'</span><span class="c-punc">);</span> <span class="c-com">// => '<h1 id="hello">Hello</h1>'</span>
|
|
||||||
<span class="c-fn">safeUrl</span><span class="c-punc">(</span><span class="c-str">'javascript:alert(1)'</span><span class="c-punc">);</span> <span class="c-com">// => ''</span>
|
|
||||||
<span class="c-fn">slugify</span><span class="c-punc">(</span><span class="c-str">'Hello World'</span><span class="c-punc">);</span> <span class="c-com">// => 'hello-world'</span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 语法参考 ======== -->
|
<!-- 解析器语法 -->
|
||||||
<section id="syntax">
|
<section id="parser-syntax"><h2>解析器语法参考</h2>
|
||||||
<h2>解析器语法参考</h2>
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr><th>语法</th><th>示例</th><th>输出</th></tr>
|
<tr><th>语法</th><th>示例</th><th>输出</th></tr>
|
||||||
<tr><td>标题</td><td><code># H1</code> ~ <code>###### H6</code></td><td><code><h1></code> ~ <code><h6></code></td></tr>
|
<tr><td>标题</td><td><code># H1</code> ~ <code>###### H6</code></td><td><h1> ~ <h6></td></tr>
|
||||||
<tr><td>段落</td><td>纯文本</td><td><code><p></code></td></tr>
|
<tr><td>Setext 标题</td><td><code>H1\n===</code></td><td><h1> / <h2></td></tr>
|
||||||
<tr><td>粗体</td><td><code>**bold**</code> <code>__bold__</code></td><td><code><strong></code></td></tr>
|
<tr><td>粗体</td><td><code>**bold**</code> <code>__bold__</code></td><td><strong></td></tr>
|
||||||
<tr><td>斜体</td><td><code>*italic*</code> <code>_italic_</code></td><td><code><em></code></td></tr>
|
<tr><td>粗斜体 <span class="badge">v0.1.15</span></td><td><code>***text***</code> <code>___text___</code></td><td><em><strong></td></tr>
|
||||||
<tr><td>删除线</td><td><code>~~text~~</code></td><td><code><del></code></td></tr>
|
<tr><td>斜体</td><td><code>*italic*</code> <code>_italic_</code></td><td><em></td></tr>
|
||||||
<tr><td>高亮标记</td><td><code>==text==</code></td><td><code><mark></code></td></tr>
|
<tr><td>删除线 / 高亮</td><td><code>~~del~~</code> <code>==mark==</code></td><td><del> / <mark></td></tr>
|
||||||
<tr><td>上标</td><td><code>x^2^</code></td><td><code><sup></code></td></tr>
|
<tr><td>上标 / 下标</td><td><code>x^2^</code> <code>H~2~O</code></td><td><sup> / <sub></td></tr>
|
||||||
<tr><td>下标</td><td><code>H~2~O</code></td><td><code><sub></code></td></tr>
|
<tr><td>行内代码</td><td><code>`code`</code></td><td><code></td></tr>
|
||||||
<tr><td>行内代码</td><td><code>`code`</code></td><td><code><code></code></td></tr>
|
<tr><td>代码块</td><td><code>```lang</code></td><td><pre><code></td></tr>
|
||||||
<tr><td>代码块</td><td><code>```lang</code></td><td><code><pre><code></code></td></tr>
|
<tr><td>缩进代码块</td><td><code> code</code></td><td><pre><code></td></tr>
|
||||||
<tr><td>引用</td><td><code>> quote</code></td><td><code><blockquote></code></td></tr>
|
<tr><td>引用</td><td><code>> quote</code></td><td><blockquote></td></tr>
|
||||||
<tr><td>无序列表</td><td><code>- item</code></td><td><code><ul><li></code></td></tr>
|
<tr><td>无序列表</td><td><code>- * + item</code></td><td><ul><li></td></tr>
|
||||||
<tr><td>有序列表</td><td><code>1. item</code></td><td><code><ol><li></code></td></tr>
|
<tr><td>有序列表</td><td><code>1. item</code></td><td><ol><li></td></tr>
|
||||||
<tr><td>任务列表</td><td><code>- [x] done</code></td><td><code><li class="me-task-item"></code></td></tr>
|
<tr><td>嵌套列表</td><td>多级缩进</td><td>多级 <ul>/<ol></td></tr>
|
||||||
<tr><td>水平线</td><td><code>---</code> <code>***</code> <code>___</code></td><td><code><hr></code></td></tr>
|
<tr><td>任务列表</td><td><code>- [x] done</code></td><td><li class="me-task-item"></td></tr>
|
||||||
<tr><td>表格</td><td><code>| a | b |</code></td><td><code><table></code>(支持 <code>:---:</code> 列对齐)</td></tr>
|
<tr><td>水平线</td><td><code>---</code> <code>***</code> <code>___</code></td><td><hr></td></tr>
|
||||||
<tr><td>链接</td><td><code>[text](url)</code></td><td><code><a></code></td></tr>
|
<tr><td>表格</td><td><code>| a | b |</code></td><td><table> + 列对齐</td></tr>
|
||||||
<tr><td>图片</td><td><code></code></td><td><code><img></code></td></tr>
|
<tr><td>链接 <span class="badge">v0.1.15</span></td><td><code>[**t**](url)</code></td><td>文本内支持行内格式</td></tr>
|
||||||
<tr><td>自动链接</td><td><code><https://...></code></td><td><code><a></code></td></tr>
|
<tr><td>图片</td><td><code></code></td><td><img></td></tr>
|
||||||
<tr><td>Emoji</td><td><code>:smile:</code> <code>:rocket:</code></td><td>😊 🚀(80+ 常用)</td></tr>
|
<tr><td>引用链接 <span class="badge">v0.2.2</span></td><td><code>[text][ref]</code> + <code>[ref]: url</code></td><td><a> 引用式</td></tr>
|
||||||
|
<tr><td>引用图片 <span class="badge">v0.2.2</span></td><td><code>![alt][ref]</code> + <code>[ref]: url</code></td><td><img> 引用式</td></tr>
|
||||||
|
<tr><td>自动链接</td><td><code><https://...></code></td><td><a></td></tr>
|
||||||
|
<tr><td>数学公式</td><td><code>$E=mc^2$</code></td><td><span> / <div></td></tr>
|
||||||
|
<tr><td>脚注</td><td><code>text[^1]</code></td><td><sup> + 底部</td></tr>
|
||||||
|
<tr><td>定义列表</td><td><code>Term\n: def</code></td><td><dl><dt><dd></td></tr>
|
||||||
|
<tr><td>Emoji</td><td><code>:smile:</code> <code>:rocket:</code></td><td>😊 🚀(150+)</td></tr>
|
||||||
|
<tr><td>反斜杠转义 <span class="badge">v0.1.15</span></td><td><code>\*</code> <code>\_</code></td><td>取消标点特殊含义</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
<h4>XSS 防护</h4>
|
||||||
<h3>XSS 防护</h3>
|
<p>所有文本经 <code>escapeHTML</code> 转义,URL 过滤 <code>javascript:</code> / <code>vbscript:</code> / <code>file:</code> / 非图片 <code>data:</code>,提供 <code>sanitize</code> 钩子。</p>
|
||||||
<p>所有文本经 HTML 转义。URL 自动过滤 <code>javascript:</code> <code>vbscript:</code> <code>file:</code> 及非图片 <code>data:</code> 协议。</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 插件 ======== -->
|
<section id="parser-api"><h2>解析器 API</h2>
|
||||||
<section id="plugins">
|
<h3>parseTokens / renderTokens</h3>
|
||||||
<h2>插件约定</h2>
|
<pre><span class="c-kw">import</span> <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>
|
||||||
|
|
||||||
|
<span class="c-kw">const</span> <span class="c-punc">{</span> tokens<span class="c-punc">,</span> footnotes <span class="c-punc">}</span> <span class="c-punc">=</span> <span class="c-fn">parseTokens</span><span class="c-punc">(</span><span class="c-str">'# Hello\n\n**world**'</span><span class="c-punc">);</span>
|
||||||
|
tokens<span class="c-punc">.</span><span class="c-fn">unshift</span><span class="c-punc">({</span> type<span class="c-punc">:</span> <span class="c-str">'hr'</span> <span class="c-punc">});</span> <span class="c-com">// 渲染前变换</span>
|
||||||
|
<span class="c-kw">const</span> html <span class="c-punc">=</span> <span class="c-fn">renderTokens</span><span class="c-punc">(</span>tokens<span class="c-punc">,</span> env<span class="c-punc">,</span> footnotes<span class="c-punc">);</span></pre>
|
||||||
|
|
||||||
|
<h3>registerBlockHandler</h3>
|
||||||
|
<pre><span class="c-kw">import</span> <span class="c-punc">{</span> registerBlockHandler <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>
|
||||||
|
<span class="c-fn">registerBlockHandler</span><span class="c-punc">({</span>
|
||||||
|
name<span class="c-punc">:</span> <span class="c-str">'custom'</span><span class="c-punc">,</span> priority<span class="c-punc">:</span> <span class="c-num">8.5</span><span class="c-punc">,</span>
|
||||||
|
test<span class="c-punc">:</span> <span class="c-punc">(</span>line<span class="c-punc">)</span> <span class="c-punc">=></span> line<span class="c-punc">.</span><span class="c-fn">match</span><span class="c-punc">(</span><span class="c-re">/^:::/</span><span class="c-punc">),</span>
|
||||||
|
parse<span class="c-punc">:</span> <span class="c-punc">(</span>lines<span class="c-punc">,</span> i<span class="c-punc">,</span> match<span class="c-punc">)</span> <span class="c-punc">=></span> <span class="c-punc">({</span> token<span class="c-punc">:</span> <span class="c-punc">{</span> type<span class="c-punc">:</span> <span class="c-str">'alert'</span> <span class="c-punc">},</span> newIndex<span class="c-punc">:</span> j <span class="c-punc">})</span>
|
||||||
|
<span class="c-punc">});</span></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 插件 -->
|
||||||
|
<section id="plugins-convention"><h2>插件约定</h2>
|
||||||
<pre><span class="c-kw">const</span> myPlugin <span class="c-punc">=</span> <span class="c-punc">{</span>
|
<pre><span class="c-kw">const</span> myPlugin <span class="c-punc">=</span> <span class="c-punc">{</span>
|
||||||
name<span class="c-punc">:</span> <span class="c-str">'myPlugin'</span><span class="c-punc">,</span>
|
name<span class="c-punc">:</span> <span class="c-str">'myPlugin'</span><span class="c-punc">,</span> description<span class="c-punc">:</span> <span class="c-str">'我的插件'</span><span class="c-punc">,</span>
|
||||||
description<span class="c-punc">:</span> <span class="c-str">'我的自定义插件'</span><span class="c-punc">,</span>
|
<span class="c-fn">install</span><span class="c-punc">(</span>editor<span class="c-punc">)</span> <span class="c-punc">{</span> editor<span class="c-punc">.</span><span class="c-fn">on</span><span class="c-punc">(</span><span class="c-str">'change'</span><span class="c-punc">,</span> <span class="c-kw">this</span><span class="c-punc">.</span>_onChange<span class="c-punc">);</span> <span class="c-punc">},</span>
|
||||||
|
<span class="c-fn">destroy</span><span class="c-punc">(</span>editor<span class="c-punc">)</span> <span class="c-punc">{</span> editor<span class="c-punc">.</span><span class="c-fn">off</span><span class="c-punc">(</span><span class="c-str">'change'</span><span class="c-punc">,</span> <span class="c-kw">this</span><span class="c-punc">.</span>_onChange<span class="c-punc">);</span> <span class="c-punc">},</span>
|
||||||
<span class="c-fn">install</span><span class="c-punc">(</span>editor<span class="c-punc">,</span> options<span class="c-punc">)</span> <span class="c-punc">{</span>
|
|
||||||
<span class="c-com">// this 指向插件对象本身</span>
|
|
||||||
<span class="c-kw">this</span><span class="c-punc">.</span>_timer <span class="c-punc">=</span> <span class="c-kw">null</span><span class="c-punc">;</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">on</span><span class="c-punc">(</span><span class="c-str">'change'</span><span class="c-punc">,</span> <span class="c-kw">this</span><span class="c-punc">.</span>_onChange<span class="c-punc">);</span>
|
|
||||||
<span class="c-punc">},</span>
|
|
||||||
|
|
||||||
<span class="c-fn">destroy</span><span class="c-punc">(</span>editor<span class="c-punc">)</span> <span class="c-punc">{</span>
|
|
||||||
<span class="c-kw">if</span> <span class="c-punc">(</span><span class="c-kw">this</span><span class="c-punc">.</span>_timer<span class="c-punc">)</span> <span class="c-fn">clearTimeout</span><span class="c-punc">(</span><span class="c-kw">this</span><span class="c-punc">.</span>_timer<span class="c-punc">);</span>
|
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">off</span><span class="c-punc">(</span><span class="c-str">'change'</span><span class="c-punc">,</span> <span class="c-kw">this</span><span class="c-punc">.</span>_onChange<span class="c-punc">);</span>
|
|
||||||
<span class="c-punc">},</span>
|
|
||||||
<span class="c-punc">};</span></pre>
|
<span class="c-punc">};</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="preset-plugins">
|
<section id="preset-plugins"><h3>预设插件</h3>
|
||||||
<h3>预设插件</h3>
|
<h4>autoSave</h4>
|
||||||
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'autoSave'</span><span class="c-punc">,</span> <span class="c-punc">{</span> key<span class="c-punc">:</span> <span class="c-str">'draft-key'</span><span class="c-punc">,</span> delay<span class="c-punc">:</span> <span class="c-num">1000</span> <span class="c-punc">});</span>
|
||||||
<h4>autoSave — 自动保存草稿</h4>
|
editor<span class="c-punc">.</span><span class="c-fn">restoreDraft</span><span class="c-punc">();</span> editor<span class="c-punc">.</span><span class="c-fn">clearDraft</span><span class="c-punc">();</span></pre>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>presetPlugins<span class="c-punc">.</span>autoSave<span class="c-punc">,</span> <span class="c-punc">{</span>
|
<h4>exportTool</h4>
|
||||||
key<span class="c-punc">:</span> <span class="c-str">'me-draft-'</span> <span class="c-punc">+</span> editor<span class="c-punc">.</span>id<span class="c-punc">,</span> <span class="c-com">// localStorage key</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'exportTool'</span><span class="c-punc">);</span>
|
||||||
delay<span class="c-punc">:</span> <span class="c-num">1000</span><span class="c-punc">,</span> <span class="c-com">// 防抖延迟(ms)</span>
|
editor<span class="c-punc">.</span><span class="c-fn">exportMarkdown</span><span class="c-punc">(</span><span class="c-str">'doc.md'</span><span class="c-punc">);</span> editor<span class="c-punc">.</span><span class="c-fn">exportHTML</span><span class="c-punc">(</span><span class="c-str">'doc.html'</span><span class="c-punc">);</span></pre>
|
||||||
<span class="c-punc">});</span>
|
<h4>searchReplace</h4>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">restoreDraft</span><span class="c-punc">();</span> <span class="c-com">// 恢复草稿</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'searchReplace'</span><span class="c-punc">);</span>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">clearDraft</span><span class="c-punc">();</span> <span class="c-com">// 清除草稿</span>
|
<span class="c-com">// Ctrl+F 查找 / Ctrl+H 替换</span></pre>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">getDraftKey</span><span class="c-punc">();</span> <span class="c-com">// 获取存储 key</span></pre>
|
<h4>imagePaste</h4>
|
||||||
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'imagePaste'</span><span class="c-punc">);</span> <span class="c-com">// 粘贴图片自动转 base64</span></pre>
|
||||||
<h4>exportTool — 导出文件</h4>
|
<h4>shortcutHelp</h4>
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>presetPlugins<span class="c-punc">.</span>exportTool<span class="c-punc">);</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'shortcutHelp'</span><span class="c-punc">);</span> <span class="c-com">// 按 ? 弹出快捷键面板</span></pre>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">exportMarkdown</span><span class="c-punc">(</span><span class="c-str">'my-doc.md'</span><span class="c-punc">);</span>
|
<h4>fileSystem</h4>
|
||||||
editor<span class="c-punc">.</span><span class="c-fn">exportHTML</span><span class="c-punc">(</span><span class="c-str">'my-doc.html'</span><span class="c-punc">,</span> <span class="c-punc">{</span>
|
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span><span class="c-str">'fileSystem'</span><span class="c-punc">);</span>
|
||||||
title<span class="c-punc">:</span> <span class="c-str">'文档标题'</span><span class="c-punc">,</span>
|
editor<span class="c-punc">.</span><span class="c-fn">openFile</span><span class="c-punc">();</span> editor<span class="c-punc">.</span><span class="c-fn">saveFile</span><span class="c-punc">();</span> editor<span class="c-punc">.</span><span class="c-fn">saveFileAs</span><span class="c-punc">();</span></pre>
|
||||||
css<span class="c-punc">:</span> <span class="c-str">'body { font-family: sans-serif; }'</span><span class="c-punc">,</span>
|
|
||||||
lang<span class="c-punc">:</span> <span class="c-str">'zh-CN'</span><span class="c-punc">,</span>
|
|
||||||
<span class="c-punc">});</span></pre>
|
|
||||||
|
|
||||||
<h4>searchReplace — 查找替换</h4>
|
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>presetPlugins<span class="c-punc">.</span>searchReplace<span class="c-punc">);</span>
|
|
||||||
<span class="c-com">// Ctrl+F → 查找 Ctrl+H → 替换</span>
|
|
||||||
<span class="c-com">// Enter → 下一个 Shift+Enter → 上一个 Escape → 关闭</span></pre>
|
|
||||||
|
|
||||||
<h4>imagePaste — 粘贴图片转 base64</h4>
|
|
||||||
<pre>editor<span class="c-punc">.</span><span class="c-fn">use</span><span class="c-punc">(</span>presetPlugins<span class="c-punc">.</span>imagePaste<span class="c-punc">);</span>
|
|
||||||
<span class="c-com">// 在编辑区 Ctrl+V 粘贴剪贴板图片</span>
|
|
||||||
<span class="c-com">// 自动插入 </span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 主题 ======== -->
|
<!-- 主题 -->
|
||||||
<section id="themes">
|
<section id="themes"><h2>主题系统</h2>
|
||||||
<h2>主题系统</h2>
|
<pre><span class="c-com">// 实例级</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'dark'</span><span class="c-punc">);</span> editor<span class="c-punc">.</span><span class="c-fn">getTheme</span><span class="c-punc">();</span>
|
||||||
|
|
||||||
<pre><span class="c-com">// 切换主题</span>
|
<span class="c-com">// 注册自定义</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'light'</span><span class="c-punc">);</span> <span class="c-com">// 亮色</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'dark'</span><span class="c-punc">);</span> <span class="c-com">// 暗色</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'warm'</span><span class="c-punc">);</span> <span class="c-com">// 暖色</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setTheme</span><span class="c-punc">(</span><span class="c-str">'auto'</span><span class="c-punc">);</span> <span class="c-com">// 自动(默认)</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 注册自定义主题</span>
|
|
||||||
<span class="c-kw">import</span> <span class="c-punc">{</span> themeUtils <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>
|
<span class="c-kw">import</span> <span class="c-punc">{</span> themeUtils <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>
|
||||||
themeUtils<span class="c-punc">.</span><span class="c-fn">registerTheme</span><span class="c-punc">(</span><span class="c-str">'ocean'</span><span class="c-punc">,</span> <span class="c-punc">{</span>
|
themeUtils<span class="c-punc">.</span><span class="c-fn">registerTheme</span><span class="c-punc">(</span><span class="c-str">'ocean'</span><span class="c-punc">,</span> <span class="c-punc">{</span> extends<span class="c-punc">:</span> <span class="c-str">'dark'</span><span class="c-punc">,</span> accent<span class="c-punc">:</span> <span class="c-str">'#00ddff'</span> <span class="c-punc">});</span>
|
||||||
bg<span class="c-punc">:</span> <span class="c-str">'#001122'</span><span class="c-punc">,</span> text<span class="c-punc">:</span> <span class="c-str">'#aabbcc'</span><span class="c-punc">,</span> accent<span class="c-punc">:</span> <span class="c-str">'#00ddff'</span><span class="c-punc">,</span>
|
|
||||||
<span class="c-punc">});</span>
|
|
||||||
themeUtils<span class="c-punc">.</span><span class="c-fn">applyTheme</span><span class="c-punc">(</span><span class="c-str">'ocean'</span><span class="c-punc">);</span></pre>
|
|
||||||
|
|
||||||
<p>CSS 变量:主题变量限定在 <code>.me-wrapper</code> 元素上,不污染全局样式。支持 <code>@media print</code> 打印样式。</p>
|
<span class="c-com">// 外部跟随</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">getThemeContext</span><span class="c-punc">().</span><span class="c-fn">adopt</span><span class="c-punc">();</span> <span class="c-com">// 从父容器继承</span>
|
||||||
|
|
||||||
|
<span class="c-com">// CSS 变量导出</span>
|
||||||
|
themeUtils<span class="c-punc">.</span><span class="c-fn">exportCSSVars</span><span class="c-punc">();</span> themeUtils<span class="c-punc">.</span><span class="c-fn">getCSSVariable</span><span class="c-punc">(</span><span class="c-str">'accent'</span><span class="c-punc">);</span></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ======== 国际化 ======== -->
|
<!-- 国际化 -->
|
||||||
<section id="i18n">
|
<section id="i18n"><h2>国际化</h2>
|
||||||
<h2>国际化</h2>
|
<pre><span class="c-com">// 切换</span>
|
||||||
|
editor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span class="c-punc">(</span><span class="c-str">'en-US'</span><span class="c-punc">);</span> MeEditor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span class="c-punc">(</span><span class="c-str">'zh-CN'</span><span class="c-punc">);</span>
|
||||||
|
|
||||||
<pre><span class="c-com">// 切换语言</span>
|
<span class="c-com">// 添加翻译</span>
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span class="c-punc">(</span><span class="c-str">'en-US'</span><span class="c-punc">);</span>
|
|
||||||
MeEditor<span class="c-punc">.</span><span class="c-fn">setLocale</span><span class="c-punc">(</span><span class="c-str">'zh-CN'</span><span class="c-punc">);</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 翻译函数</span>
|
|
||||||
<span class="c-kw">import</span> <span class="c-punc">{</span> i18nUtils <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>
|
<span class="c-kw">import</span> <span class="c-punc">{</span> i18nUtils <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>
|
||||||
i18nUtils<span class="c-punc">.</span><span class="c-fn">t</span><span class="c-punc">(</span><span class="c-str">'bold'</span><span class="c-punc">);</span> <span class="c-com">// => '粗体'(zh-CN)</span>
|
i18nUtils<span class="c-punc">.</span><span class="c-fn">addTranslations</span><span class="c-punc">(</span><span class="c-str">'ja'</span><span class="c-punc">,</span> <span class="c-punc">{</span> bold<span class="c-punc">:</span> <span class="c-str">'太字'</span> <span class="c-punc">});</span>
|
||||||
i18nUtils<span class="c-punc">.</span><span class="c-fn">t</span><span class="c-punc">(</span><span class="c-str">'bold'</span><span class="c-punc">);</span> <span class="c-com">// => 'Bold'(en-US)</span>
|
|
||||||
|
|
||||||
<span class="c-com">// 添加语言</span>
|
<span class="c-com">// 远程加载</span>
|
||||||
i18nUtils<span class="c-punc">.</span><span class="c-fn">addTranslations</span><span class="c-punc">(</span><span class="c-str">'ja'</span><span class="c-punc">,</span> <span class="c-punc">{</span> bold<span class="c-punc">:</span> <span class="c-str">'太字'</span><span class="c-punc">,</span> italic<span class="c-punc">:</span> <span class="c-str">'斜体'</span> <span class="c-punc">});</span>
|
<span class="c-kw">await</span> i18nUtils<span class="c-punc">.</span><span class="c-fn">loadRemote</span><span class="c-punc">(</span><span class="c-str">'/locales/ja.json'</span><span class="c-punc">,</span> <span class="c-str">'ja'</span><span class="c-punc">);</span>
|
||||||
|
|
||||||
<span class="c-com">// 格式化</span>
|
<span class="c-com">// 格式化</span>
|
||||||
i18nUtils<span class="c-punc">.</span><span class="c-fn">formatNumber</span><span class="c-punc">(</span><span class="c-num">1234567</span><span class="c-punc">);</span> <span class="c-com">// => '1,234,567'</span>
|
i18nUtils<span class="c-punc">.</span><span class="c-fn">formatNumber</span><span class="c-punc">(</span><span class="c-num">1234567</span><span class="c-punc">);</span>
|
||||||
i18nUtils<span class="c-punc">.</span><span class="c-fn">formatCurrency</span><span class="c-punc">(</span><span class="c-num">99.99</span><span class="c-punc">,</span> <span class="c-str">'USD'</span><span class="c-punc">);</span> <span class="c-com">// => '$99.99'</span>
|
i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span class="c-punc">(</span><span class="c-str">'2024-01-15'</span><span class="c-punc">);</span></pre>
|
||||||
i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span class="c-punc">(</span><span class="c-str">'2024-01-15'</span><span class="c-punc">);</span> <span class="c-com">// => '1/15/2024'</span></pre>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
@@ -598,24 +433,18 @@ i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
MetonaEditor v0.1.3 · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
MetonaEditor v0.2.4 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 高亮当前 section 的侧边栏链接
|
const sections = document.querySelectorAll('section[id]');
|
||||||
const sections = document.querySelectorAll('section[id]');
|
const links = document.querySelectorAll('.sidebar a');
|
||||||
const links = document.querySelectorAll('.sidebar a');
|
window.addEventListener('scroll', () => {
|
||||||
window.addEventListener('scroll', () => {
|
let current = '';
|
||||||
let current = '';
|
sections.forEach(s => { if (window.scrollY >= s.offsetTop - 100) current = s.id; });
|
||||||
sections.forEach(s => {
|
links.forEach(a => { a.classList.toggle('active', a.getAttribute('href') === '#' + current); });
|
||||||
if (window.scrollY >= s.offsetTop - 100) current = s.id;
|
});
|
||||||
});
|
|
||||||
links.forEach(a => {
|
|
||||||
a.classList.toggle('active', a.getAttribute('href') === '#' + current);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
|
|
||||||
<!-- 6. Action Toast -->
|
<!-- 6. Action Toast -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">⚡ Action Toast <span style="color:#34d399;font-size:10px;">v2.0</span></div>
|
<div class="label">⚡ Action Toast</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
|
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
|
||||||
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
|
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
|
|
||||||
<!-- 7. 分组管理 -->
|
<!-- 7. 分组管理 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">📁 分组管理 <span style="color:#34d399;font-size:10px;">v2.0</span></div>
|
<div class="label">📁 分组管理</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-p" onclick="demoGroup()">创建分组消息</button>
|
<button class="btn c-p" onclick="demoGroup()">创建分组消息</button>
|
||||||
<button class="btn c-e" onclick="dismissGroup()">关闭分组</button>
|
<button class="btn c-e" onclick="dismissGroup()">关闭分组</button>
|
||||||
@@ -209,9 +209,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 12. v2.0 新功能 -->
|
<!-- 12. 高级特性 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🆕 v2.0 新功能 <span style="color:#34d399;font-size:10px;">resetTimer · onUpdate · render · notify</span></div>
|
<div class="label">🔥 高级特性 <span style="color:#34d399;font-size:10px;">resetTimer · onUpdate · render · notify</span></div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
|
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
|
||||||
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
|
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
|
||||||
@@ -242,7 +242,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 14. 交互特性 -->
|
<!-- 14. 错误回调 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.1</span></div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
|
||||||
|
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 15. 交互特性 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">✋ 交互特性</div>
|
<div class="label">✋ 交互特性</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -254,7 +263,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 15. 国际化切换 -->
|
<!-- 16. 国际化切换 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🌍 国际化 & 格式化</div>
|
<div class="label">🌍 国际化 & 格式化</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -264,7 +273,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 16. 107种预设图标 -->
|
<!-- 17. 107种预设图标 -->
|
||||||
<div class="card" style="grid-column:1/-1;">
|
<div class="card" style="grid-column:1/-1;">
|
||||||
<div class="label">🎨 107种预设图标 — 点击任意图标发送Toast</div>
|
<div class="label">🎨 107种预设图标 — 点击任意图标发送Toast</div>
|
||||||
<div class="btn-row" id="icon-grid" style="max-height:300px;overflow-y:auto;padding:4px 0;"></div>
|
<div class="btn-row" id="icon-grid" style="max-height:300px;overflow-y:auto;padding:4px 0;"></div>
|
||||||
@@ -273,7 +282,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
MetonaToast v2.0.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
MetonaToast v0.2.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="../dist/metona-toast.js"></script>
|
<script src="../dist/metona-toast.js"></script>
|
||||||
@@ -351,7 +360,7 @@
|
|||||||
MeToast.info('自定义插件已安装(消息后追加计数)');
|
MeToast.info('自定义插件已安装(消息后追加计数)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// === v2.0 new features ===
|
// === 高级特性 ===
|
||||||
function demoResetTimer() {
|
function demoResetTimer() {
|
||||||
const t=MeToast.info('这条消息不会自动消失(resetTimer)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
const t=MeToast.info('这条消息不会自动消失(resetTimer)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
||||||
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n+' — 每次更新重置3秒'});},1000);
|
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n+' — 每次更新重置3秒'});},1000);
|
||||||
@@ -387,6 +396,25 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === onError ===
|
||||||
|
function demoOnError() {
|
||||||
|
MeToast.configure({
|
||||||
|
onError: function(info) {
|
||||||
|
MeToast.error('⚠️ 捕获错误: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MeToast.info('已启用 onError 监控(钩子/定时器异常将被捕获)', { duration: 3000 });
|
||||||
|
// 触发一个钩子错误来演示
|
||||||
|
var badFn = function() { throw new Error('demo-error'); };
|
||||||
|
Toast.on('afterShow', badFn);
|
||||||
|
MeToast.success('这条消息会触发错误', { duration: 2000 });
|
||||||
|
setTimeout(function() { Toast.off('afterShow', badFn); }, 3000);
|
||||||
|
}
|
||||||
|
function demoOnErrorReset() {
|
||||||
|
MeToast.configure({ onError: null });
|
||||||
|
MeToast.info('onError 已重置为 null');
|
||||||
|
}
|
||||||
|
|
||||||
// === i18n ===
|
// === i18n ===
|
||||||
function switchLocale(loc) {
|
function switchLocale(loc) {
|
||||||
try{MeToast.i18n.switchLocale(loc);MeToast.configure({locale:loc});MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English',{theme:'dark'});}
|
try{MeToast.i18n.switchLocale(loc);MeToast.configure({locale:loc});MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English',{theme:'dark'});}
|
||||||
|
|||||||
+12
-10
@@ -69,9 +69,9 @@
|
|||||||
<a href="#countdown">countdown()</a>
|
<a href="#countdown">countdown()</a>
|
||||||
<a href="#queue">queue()</a>
|
<a href="#queue">queue()</a>
|
||||||
<a href="#stack">stack()</a>
|
<a href="#stack">stack()</a>
|
||||||
<a href="#action">action() <span class="badge badge-new">NEW</span></a>
|
<a href="#action">action()</a>
|
||||||
<div class="section">管理方法</div>
|
<div class="section">管理方法</div>
|
||||||
<a href="#group">group() <span class="badge badge-new">NEW</span></a>
|
<a href="#group">group()</a>
|
||||||
<a href="#dismiss">dismiss()</a>
|
<a href="#dismiss">dismiss()</a>
|
||||||
<a href="#clear">clear()</a>
|
<a href="#clear">clear()</a>
|
||||||
<a href="#configure">configure()</a>
|
<a href="#configure">configure()</a>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
|
|
||||||
<h1>API 文档</h1>
|
<h1>API 文档</h1>
|
||||||
<p>MetonaToast v2.0.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
<p>MetonaToast v0.2.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||||
|
|
||||||
<!-- ===== 基础通知 ===== -->
|
<!-- ===== 基础通知 ===== -->
|
||||||
<h2 id="show">show(message, opts?)</h2>
|
<h2 id="show">show(message, opts?)</h2>
|
||||||
@@ -309,7 +309,7 @@ q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
|||||||
<span class="fn">type</span>: <span class="s">'info'</span>,
|
<span class="fn">type</span>: <span class="s">'info'</span>,
|
||||||
});</pre>
|
});</pre>
|
||||||
|
|
||||||
<h2 id="action">action(message, actions, opts?) <span class="badge badge-new">v2.0</span></h2>
|
<h2 id="action">action(message, actions, opts?)</h2>
|
||||||
<p>Action Toast:内嵌操作按钮。默认 duration=0 不自动关闭,closeButton=true。</p>
|
<p>Action Toast:内嵌操作按钮。默认 duration=0 不自动关闭,closeButton=true。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||||
@@ -331,7 +331,7 @@ q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
|||||||
]);</pre>
|
]);</pre>
|
||||||
|
|
||||||
<!-- ===== 管理方法 ===== -->
|
<!-- ===== 管理方法 ===== -->
|
||||||
<h2 id="group">group(name) <span class="badge badge-new">v2.0</span></h2>
|
<h2 id="group">group(name)</h2>
|
||||||
<p>创建 Toast 分组,返回 GroupAPI 对象。该对象所有方法自动传入 <code>group: name</code>,按组管理。</p>
|
<p>创建 Toast 分组,返回 GroupAPI 对象。该对象所有方法自动传入 <code>group: name</code>,按组管理。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||||
@@ -409,7 +409,7 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
|
|||||||
|
|
||||||
<!-- ===== 配置参考 ===== -->
|
<!-- ===== 配置参考 ===== -->
|
||||||
<h2 id="config">全部配置项</h2>
|
<h2 id="config">全部配置项</h2>
|
||||||
<p>以下配置可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts 参数。带 <span class="badge badge-new">v2.0</span> 标识的为 v2.0 新增。</p>
|
<p>以下配置可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts 参数。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||||
<tr><td>position</td><td>string</td><td>'top-right'</td><td>位置:top-left / top-center / top-right / bottom-left / bottom-center / bottom-right</td></tr>
|
<tr><td>position</td><td>string</td><td>'top-right'</td><td>位置:top-left / top-center / top-right / bottom-left / bottom-center / bottom-right</td></tr>
|
||||||
@@ -431,9 +431,10 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
|
|||||||
<tr><td>className</td><td>string</td><td>''</td><td>附加到 Toast 元素上的 CSS 类名</td></tr>
|
<tr><td>className</td><td>string</td><td>''</td><td>附加到 Toast 元素上的 CSS 类名</td></tr>
|
||||||
<tr><td>style</td><td>object</td><td>{}</td><td>附加到 Toast 元素上的内联样式对象</td></tr>
|
<tr><td>style</td><td>object</td><td>{}</td><td>附加到 Toast 元素上的内联样式对象</td></tr>
|
||||||
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码(zh-CN / en-US 等)</td></tr>
|
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码(zh-CN / en-US 等)</td></tr>
|
||||||
<tr><td>resetTimerOnUpdate <span class="badge badge-new">v2.0</span></td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</td></tr>
|
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</td></tr>
|
||||||
<tr><td>notifyWhenHidden <span class="badge badge-new">v2.0</span></td><td>boolean</td><td>false</td><td>页面不可见时自动通过 Notification API 发送系统通知</td></tr>
|
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时自动通过 Notification API 发送系统通知</td></tr>
|
||||||
<tr><td>render <span class="badge badge-new">v2.0</span></td><td>function</td><td>—</td><td>自定义渲染函数 <code>(toast) => htmlString</code>,完全接管 DOM 构建</td></tr>
|
<tr><td>render</td><td>function</td><td>—</td><td>自定义渲染函数 <code>(toast) => htmlString</code>,完全接管 DOM 构建</td></tr>
|
||||||
|
<tr><td>onError</td><td>function</td><td>—</td><td>全局错误回调 <code>({ hook, source, error, toast }) => void</code>,钩子异常或定时器错误时触发</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 id="callbacks">回调函数</h2>
|
<h2 id="callbacks">回调函数</h2>
|
||||||
@@ -442,7 +443,8 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
|
|||||||
<tr><td>onShow</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 创建并播放入场动画后</td></tr>
|
<tr><td>onShow</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 创建并播放入场动画后</td></tr>
|
||||||
<tr><td>onClose</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 被移除后(离场动画完成时)</td></tr>
|
<tr><td>onClose</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 被移除后(离场动画完成时)</td></tr>
|
||||||
<tr><td>onClick</td><td>(toast: ToastInstance) => void</td><td>Toast 被点击时(closeOnClick 为 true 时还会自动关闭)</td></tr>
|
<tr><td>onClick</td><td>(toast: ToastInstance) => void</td><td>Toast 被点击时(closeOnClick 为 true 时还会自动关闭)</td></tr>
|
||||||
<tr><td>onUpdate <span class="badge badge-new">v2.0</span></td><td>(toast: ToastInstance) => void</td><td>Toast 内容通过 update() 更新后</td></tr>
|
<tr><td>onUpdate</td><td>(toast: ToastInstance) => void</td><td>Toast 内容通过 update() 更新后</td></tr>
|
||||||
|
<tr><td>onError</td><td>({ hook, source, error, toast }) => void</td><td>钩子回调或定时器发生异常时(全局配置,适用于接入错误监控)</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 id="animations">动画列表</h2>
|
<h2 id="animations">动画列表</h2>
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marklite",
|
"name": "marklite",
|
||||||
"version": "0.4.0",
|
"version": "0.4.5",
|
||||||
"description": "Lightweight Markdown Editor for Windows",
|
"description": "Lightweight Markdown Editor for Windows",
|
||||||
"main": "./dist/main/index.js",
|
"main": "./dist/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -28,9 +28,10 @@
|
|||||||
"author": "MarkLite",
|
"author": "MarkLite",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@metona-team/metona-editor": "^0.1.3",
|
"@metona-team/metona-editor": "0.2.4",
|
||||||
|
"@metona-team/metona-toast": "0.2.1",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"@metona-team/metona-toast": "^2.0.1",
|
"mermaid": "^10.9.6",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function App() {
|
|||||||
const viewMode = useEditorStore(s => s.viewMode)
|
const viewMode = useEditorStore(s => s.viewMode)
|
||||||
const externallyModified = useEditorStore(s => s.externallyModified)
|
const externallyModified = useEditorStore(s => s.externallyModified)
|
||||||
const setExternallyModified = useEditorStore(s => s.setExternallyModified)
|
const setExternallyModified = useEditorStore(s => s.setExternallyModified)
|
||||||
const { darkMode, toggleDarkMode } = useTheme()
|
const { themeMode, cycleTheme } = useTheme()
|
||||||
const { confirm, confirmDialogProps } = useConfirm()
|
const { confirm, confirmDialogProps } = useConfirm()
|
||||||
const [showAbout, setShowAbout] = useState(false)
|
const [showAbout, setShowAbout] = useState(false)
|
||||||
const handleCloseAbout = useCallback(() => setShowAbout(false), [])
|
const handleCloseAbout = useCallback(() => setShowAbout(false), [])
|
||||||
@@ -86,7 +86,7 @@ export function App() {
|
|||||||
<div id="app" className={`mode-${viewMode}`}>
|
<div id="app" className={`mode-${viewMode}`}>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
onOpen={handleOpenFile} onSave={handleSave}
|
onOpen={handleOpenFile} onSave={handleSave}
|
||||||
darkMode={darkMode} onToggleDark={toggleDarkMode}
|
themeMode={themeMode} onCycleTheme={cycleTheme}
|
||||||
onShowAbout={() => setShowAbout(true)}
|
onShowAbout={() => setShowAbout(true)}
|
||||||
isAutoSaving={isAutoSaving}
|
isAutoSaving={isAutoSaving}
|
||||||
autoSaveEnabled={autoSaveEnabled}
|
autoSaveEnabled={autoSaveEnabled}
|
||||||
@@ -101,7 +101,7 @@ export function App() {
|
|||||||
)}
|
)}
|
||||||
{tabs.length > 0 ? (
|
{tabs.length > 0 ? (
|
||||||
<div id="content-wrapper">
|
<div id="content-wrapper">
|
||||||
<div id="editor-panel"><Editor darkMode={darkMode} /></div>
|
<div id="editor-panel"><Editor themeMode={themeMode} onAppSave={handleSave} /></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WelcomeScreen onOpen={handleOpenFile} onNew={() => createTab(null, '')} onOpenRecent={handleOpenRecent} />
|
<WelcomeScreen onOpen={handleOpenFile} onNew={() => createTab(null, '')} onOpenRecent={handleOpenRecent} />
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import MeEditor from '@metona-team/metona-editor'
|
import MeEditor from '@metona-team/metona-editor'
|
||||||
import type { MarkdownEditor, EditMode, PluginObject } from '@metona-team/metona-editor'
|
import type { MarkdownEditor } from '@metona-team/metona-editor'
|
||||||
|
import mermaid from 'mermaid'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import { setMetonaEditorGetter, useEditorStore } from '../../stores/editorStore'
|
import { setMetonaEditorGetter, useEditorStore } from '../../stores/editorStore'
|
||||||
import { settingsRepository } from '../../db/settingsRepository'
|
import { settingsRepository } from '../../db/settingsRepository'
|
||||||
import { renderMarkdownSync } from '../../lib/markdown'
|
import { renderMarkdownSync } from '../../lib/markdown'
|
||||||
|
import type { ThemeMode } from '../../types/settings'
|
||||||
|
|
||||||
|
// v0.2.4: 这些类型不再作为命名导出暴露,本地声明以保持类型安全
|
||||||
|
type EditMode = 'edit' | 'split' | 'preview'
|
||||||
|
type ThemeName = 'light' | 'dark' | 'auto' | 'warm' | string
|
||||||
|
|
||||||
|
// v0.4.5: Mermaid 初始化(全局一次性配置)
|
||||||
|
mermaid.initialize({ startOnLoad: false, theme: 'default' })
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
darkMode: boolean
|
themeMode: ThemeMode
|
||||||
|
onAppSave?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将应用 viewMode 映射到 MetonaEditor 的 mode */
|
/** 将应用 viewMode 映射到 MetonaEditor 的 mode */
|
||||||
@@ -24,26 +34,18 @@ function reverseMapMode(mode: EditMode): 'editor' | 'preview' | 'source' {
|
|||||||
return 'editor'
|
return 'editor'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 组装实例级插件列表 */
|
/** 预设插件(字符串形式,与 demo 一致) */
|
||||||
function buildPlugins(): PluginObject[] {
|
const EDITOR_PLUGINS: string[] = [
|
||||||
const plugins: PluginObject[] = []
|
'searchReplace', // Ctrl+F/Ctrl+H
|
||||||
|
'imagePaste', // Ctrl+V 粘贴图片
|
||||||
// 搜索/替换(Ctrl+F / Ctrl+H)
|
'exportTool', // 导出 MD/HTML
|
||||||
if (MeEditor.presetPlugins?.searchReplace) {
|
'shortcutHelp', // 按 ? 弹出快捷键面板
|
||||||
plugins.push(MeEditor.presetPlugins.searchReplace)
|
]
|
||||||
}
|
|
||||||
// 粘贴图片 → base64
|
|
||||||
if (MeEditor.presetPlugins?.imagePaste) {
|
|
||||||
plugins.push(MeEditor.presetPlugins.imagePaste)
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor 组件 — 基于 MetonaEditor 的 Markdown 编辑器。
|
* Editor 组件 — 基于 MetonaEditor 的 Markdown 编辑器。
|
||||||
*/
|
*/
|
||||||
export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
export const Editor = React.memo(function Editor({ themeMode, onAppSave }: EditorProps) {
|
||||||
const activeTab = useTabStore(s => s.getActiveTab())
|
const activeTab = useTabStore(s => s.getActiveTab())
|
||||||
const activeTabId = useTabStore(s => s.activeTabId)
|
const activeTabId = useTabStore(s => s.activeTabId)
|
||||||
const updateTabContent = useTabStore(s => s.updateTabContent)
|
const updateTabContent = useTabStore(s => s.updateTabContent)
|
||||||
@@ -65,7 +67,7 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
|||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
const editor = MeEditor.create(container, {
|
const config = {
|
||||||
value: activeTab?.content ?? '',
|
value: activeTab?.content ?? '',
|
||||||
mode: mapViewMode(viewMode),
|
mode: mapViewMode(viewMode),
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -78,12 +80,22 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
|||||||
'edit', 'split', 'preview', 'fullscreen'
|
'edit', 'split', 'preview', 'fullscreen'
|
||||||
],
|
],
|
||||||
locale: 'zh-CN',
|
locale: 'zh-CN',
|
||||||
theme: darkMode ? 'dark' : 'light',
|
theme: themeMode as ThemeName,
|
||||||
placeholder: '在此输入 Markdown 内容...',
|
placeholder: '在此输入 Markdown 内容...',
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
wordCount: true,
|
wordCount: true,
|
||||||
plugins: buildPlugins(),
|
autofocus: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
autoBrackets: true,
|
||||||
|
readOnly: viewMode === 'preview',
|
||||||
|
plugins: EDITOR_PLUGINS,
|
||||||
|
// v0.2.4 新增配置项
|
||||||
|
syncScroll: true,
|
||||||
|
wordWrap: true,
|
||||||
|
outline: false, // 使用自研 OutlinePanel
|
||||||
|
historyLimit: 100,
|
||||||
|
historyDebounce: 400,
|
||||||
|
|
||||||
// 使用 unified 管线渲染,保留图片路径修复能力
|
// 使用 unified 管线渲染,保留图片路径修复能力
|
||||||
render: (md: string) => {
|
render: (md: string) => {
|
||||||
@@ -106,17 +118,47 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 模式切换 → 同步到 editorStore 并持久化
|
// 模式切换 → 同步到 editorStore 并持久化
|
||||||
onModeChange: (mode: EditMode) => {
|
onModeChange: (mode: string) => {
|
||||||
const mapped = reverseMapMode(mode)
|
const mapped = reverseMapMode(mode as EditMode)
|
||||||
setViewMode(mapped)
|
setViewMode(mapped)
|
||||||
settingsRepository.save({ viewMode: mapped })
|
settingsRepository.save({ viewMode: mapped })
|
||||||
}
|
},
|
||||||
})
|
|
||||||
|
// Ctrl+S → 触发应用层保存(Electron IPC 写文件系统)
|
||||||
|
onSave: () => {
|
||||||
|
onAppSave?.()
|
||||||
|
},
|
||||||
|
|
||||||
|
// v0.2.4 新增回调: 链接点击 → 安全打开外部链接
|
||||||
|
onLinkClick: (href: string) => {
|
||||||
|
if (window.electronAPI?.openExternal) {
|
||||||
|
window.electronAPI.openExternal(href)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// v0.2.4 新增回调: 焦点事件(预留扩展点)
|
||||||
|
onFocus: () => {
|
||||||
|
// 编辑器获得焦点
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
// 编辑器失去焦点
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = MeEditor.create(container, config)
|
||||||
|
|
||||||
editorRef.current = editor
|
editorRef.current = editor
|
||||||
setMetonaEditorGetter(() => editor)
|
setMetonaEditorGetter(() => editor)
|
||||||
currentContentRef.current = activeTab?.content ?? ''
|
currentContentRef.current = activeTab?.content ?? ''
|
||||||
|
|
||||||
|
// v0.4.5: 绑定 afterRender → 触发 Mermaid 图表渲染
|
||||||
|
const renderMermaid = () => {
|
||||||
|
try { mermaid.run({ querySelector: '.me-mermaid .mermaid' }) } catch { /* 容错 */ }
|
||||||
|
}
|
||||||
|
editor.on('afterRender', renderMermaid)
|
||||||
|
// 首次渲染后延迟触发一次
|
||||||
|
setTimeout(renderMermaid, 300)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editor.destroy()
|
editor.destroy()
|
||||||
editorRef.current = null
|
editorRef.current = null
|
||||||
@@ -165,37 +207,17 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [activeTabId])
|
}, [activeTabId])
|
||||||
|
|
||||||
// ── 暗色模式同步 ──────────────────────────────────────────
|
// ── 主题同步 ──────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const themeName = darkMode ? 'dark' : 'light'
|
// 全局主题(documentElement + localStorage)
|
||||||
|
MeEditor.setTheme(themeMode)
|
||||||
// 1. 全局主题(documentElement + localStorage)
|
// v0.1.5+: 实例级主题,自动处理 wrapper CSS 变量
|
||||||
MeEditor.setTheme(themeName)
|
editorRef.current?.setTheme(themeMode)
|
||||||
|
|
||||||
// 2. 覆写当前实例 wrapper 的 inline CSS 变量
|
|
||||||
// MeEditor.setTheme 只更新 documentElement,不更新 wrapper;
|
|
||||||
// 而 create() 时写入的 inline 变量优先级最高,必须手动覆写
|
|
||||||
const wrapper = containerRef.current?.querySelector('.me-wrapper') as HTMLElement | null
|
|
||||||
if (wrapper) {
|
|
||||||
const config: Record<string, string> = MeEditor.themes.getThemeConfig(themeName) as Record<string, string>
|
|
||||||
wrapper.style.setProperty('--md-bg', config.bg)
|
|
||||||
wrapper.style.setProperty('--md-text', config.text)
|
|
||||||
wrapper.style.setProperty('--md-border', config.border)
|
|
||||||
wrapper.style.setProperty('--md-shadow', config.shadow)
|
|
||||||
if (config.hoverShadow) wrapper.style.setProperty('--md-hover-shadow', config.hoverShadow)
|
|
||||||
if (config.toolbarBg) wrapper.style.setProperty('--md-toolbar-bg', config.toolbarBg)
|
|
||||||
if (config.textareaBg) wrapper.style.setProperty('--md-textarea-bg', config.textareaBg)
|
|
||||||
if (config.previewBg) wrapper.style.setProperty('--md-preview-bg', config.previewBg)
|
|
||||||
if (config.codeBg) wrapper.style.setProperty('--md-code-bg', config.codeBg)
|
|
||||||
if (config.codeText) wrapper.style.setProperty('--md-code-text', config.codeText)
|
|
||||||
if (config.accent) wrapper.style.setProperty('--md-accent', config.accent)
|
|
||||||
if (config.muted) wrapper.style.setProperty('--md-muted', config.muted)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// 容错
|
// 容错
|
||||||
}
|
}
|
||||||
}, [darkMode])
|
}, [themeMode])
|
||||||
|
|
||||||
// ── 视图模式同步 ──────────────────────────────────────────
|
// ── 视图模式同步 ──────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -205,6 +227,8 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
|||||||
if (editor.getMode() !== targetMode) {
|
if (editor.getMode() !== targetMode) {
|
||||||
editor.setMode(targetMode)
|
editor.setMode(targetMode)
|
||||||
}
|
}
|
||||||
|
// 预览模式设为只读
|
||||||
|
editor.setReadOnly(viewMode === 'preview')
|
||||||
}, [viewMode])
|
}, [viewMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FolderOpen, Save, Moon, Sun, Info } from '../Icons'
|
import { FolderOpen, Save, Moon, Sun, Info } from '../Icons'
|
||||||
|
import type { ThemeMode } from '../../types/settings'
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
onOpen: () => void
|
onOpen: () => void
|
||||||
onSave: () => void
|
onSave: () => void
|
||||||
darkMode: boolean
|
themeMode: ThemeMode
|
||||||
onToggleDark: () => void
|
onCycleTheme: () => void
|
||||||
onShowAbout: () => void
|
onShowAbout: () => void
|
||||||
isAutoSaving: boolean
|
isAutoSaving: boolean
|
||||||
autoSaveEnabled: boolean
|
autoSaveEnabled: boolean
|
||||||
onToggleAutoSave: () => void
|
onToggleAutoSave: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THEME_LABELS: Record<ThemeMode, string> = {
|
||||||
|
light: '亮色',
|
||||||
|
dark: '暗色',
|
||||||
|
warm: '暖色',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用顶层工具栏 — 文件操作、自动保存、主题、关于。
|
* 应用顶层工具栏 — 文件操作、自动保存、主题循环、关于。
|
||||||
* 编辑器格式化和模式切换由 MetonaEditor 内置工具栏处理。
|
* 编辑器格式化和模式切换由 MetonaEditor 内置工具栏处理。
|
||||||
*/
|
*/
|
||||||
export const Toolbar = React.memo(function Toolbar({
|
export const Toolbar = React.memo(function Toolbar({
|
||||||
onOpen, onSave, darkMode, onToggleDark, onShowAbout,
|
onOpen, onSave, themeMode, onCycleTheme, onShowAbout,
|
||||||
isAutoSaving, autoSaveEnabled, onToggleAutoSave
|
isAutoSaving, autoSaveEnabled, onToggleAutoSave
|
||||||
}: ToolbarProps) {
|
}: ToolbarProps) {
|
||||||
|
const nextLabel = THEME_LABELS[themeMode] ?? '主题'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="toolbar" role="toolbar" aria-label="工具栏">
|
<div id="toolbar" role="toolbar" aria-label="工具栏">
|
||||||
<div className="toolbar-left" role="group" aria-label="文件操作">
|
<div className="toolbar-left" role="group" aria-label="文件操作">
|
||||||
@@ -42,8 +51,14 @@ export const Toolbar = React.memo(function Toolbar({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbar-right" role="group" aria-label="设置">
|
<div className="toolbar-right" role="group" aria-label="设置">
|
||||||
<button className="toolbar-btn" onClick={onToggleDark} title="切换暗色主题" aria-label={darkMode ? '切换到亮色主题' : '切换到暗色主题'}>
|
<button
|
||||||
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
|
className="toolbar-btn"
|
||||||
|
onClick={onCycleTheme}
|
||||||
|
title={`当前 ${nextLabel} — 点击切换`}
|
||||||
|
aria-label={`当前${nextLabel}主题,点击切换`}
|
||||||
|
>
|
||||||
|
{themeMode === 'dark' ? <Moon size={18} /> : <Sun size={18} />}
|
||||||
|
<span style={{ fontSize: 12, marginLeft: 2 }}>{nextLabel}</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="toolbar-btn" onClick={onShowAbout} title="关于" aria-label="关于 MarkLite">
|
<button className="toolbar-btn" onClick={onShowAbout} title="关于" aria-label="关于 MarkLite">
|
||||||
<Info size={18} />
|
<Info size={18} />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface ActiveTabRecord {
|
|||||||
|
|
||||||
export interface SettingsRecord {
|
export interface SettingsRecord {
|
||||||
id: string // 固定为 'default'
|
id: string // 固定为 'default'
|
||||||
darkMode: boolean
|
themeMode: 'light' | 'dark' | 'warm'
|
||||||
viewMode: 'editor' | 'preview' | 'source'
|
viewMode: 'editor' | 'preview' | 'source'
|
||||||
sidebarCollapsed: boolean
|
sidebarCollapsed: boolean
|
||||||
sidebarWidth: number
|
sidebarWidth: number
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const settingsRepository = {
|
|||||||
const record: SettingsRecord | undefined = await db.settings.get('default')
|
const record: SettingsRecord | undefined = await db.settings.get('default')
|
||||||
if (record) {
|
if (record) {
|
||||||
return {
|
return {
|
||||||
darkMode: record.darkMode ?? DEFAULT_SETTINGS.darkMode,
|
themeMode: record.themeMode ?? DEFAULT_SETTINGS.themeMode,
|
||||||
viewMode: record.viewMode ?? DEFAULT_SETTINGS.viewMode,
|
viewMode: record.viewMode ?? DEFAULT_SETTINGS.viewMode,
|
||||||
sidebarCollapsed: record.sidebarCollapsed ?? DEFAULT_SETTINGS.sidebarCollapsed,
|
sidebarCollapsed: record.sidebarCollapsed ?? DEFAULT_SETTINGS.sidebarCollapsed,
|
||||||
sidebarWidth: record.sidebarWidth ?? DEFAULT_SETTINGS.sidebarWidth
|
sidebarWidth: record.sidebarWidth ?? DEFAULT_SETTINGS.sidebarWidth
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import { useTabStore } from '../stores/tabStore'
|
import { useTabStore } from '../stores/tabStore'
|
||||||
import { useEditorStore } from '../stores/editorStore'
|
import { useEditorStore } from '../stores/editorStore'
|
||||||
import { recentFilesRepository } from '../db/recentFilesRepository'
|
import { recentFilesRepository } from '../db/recentFilesRepository'
|
||||||
@@ -8,12 +8,16 @@ import { showToast } from '../lib/toast'
|
|||||||
/**
|
/**
|
||||||
* AR-01: 从 App.tsx 提取的文件操作逻辑
|
* AR-01: 从 App.tsx 提取的文件操作逻辑
|
||||||
* UX-02: 添加 loading 状态指示
|
* UX-02: 添加 loading 状态指示
|
||||||
|
* v0.1.9: handleSave 添加防重入锁,避免编辑器 onSave + 全局 Ctrl+S 双重触发
|
||||||
*/
|
*/
|
||||||
export function useFileOperations() {
|
export function useFileOperations() {
|
||||||
const createTab = useTabStore(s => s.createTab)
|
const createTab = useTabStore(s => s.createTab)
|
||||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||||
const setLoading = useEditorStore(s => s.setLoading)
|
const setLoading = useEditorStore(s => s.setLoading)
|
||||||
|
|
||||||
|
// 防重入:onSave 回调 + 全局 Ctrl+S handler 可能在 300ms 内双重触发
|
||||||
|
const savingGate = useRef(false)
|
||||||
|
|
||||||
const handleOpenFile = useCallback(async (): Promise<void> => {
|
const handleOpenFile = useCallback(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (!window.electronAPI) return
|
if (!window.electronAPI) return
|
||||||
@@ -33,6 +37,9 @@ export function useFileOperations() {
|
|||||||
}, [createTab, setLoading])
|
}, [createTab, setLoading])
|
||||||
|
|
||||||
const handleSave = useCallback(async (): Promise<void> => {
|
const handleSave = useCallback(async (): Promise<void> => {
|
||||||
|
// 防重入:300ms 内忽略重复调用(编辑器 onSave + 全局 Ctrl+S 双重触发)
|
||||||
|
if (savingGate.current) return
|
||||||
|
savingGate.current = true
|
||||||
try {
|
try {
|
||||||
const tab = getActiveTab()
|
const tab = getActiveTab()
|
||||||
if (!tab || !window.electronAPI) return
|
if (!tab || !window.electronAPI) return
|
||||||
@@ -47,6 +54,9 @@ export function useFileOperations() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('保存文件失败', error)
|
logError('保存文件失败', error)
|
||||||
showToast('保存失败', 'error')
|
showToast('保存失败', 'error')
|
||||||
|
} finally {
|
||||||
|
// 300ms 后释放锁,允许下次保存
|
||||||
|
setTimeout(() => { savingGate.current = false }, 300)
|
||||||
}
|
}
|
||||||
}, [getActiveTab])
|
}, [getActiveTab])
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { useTabStore } from '../stores/tabStore'
|
|||||||
/**
|
/**
|
||||||
* 全局键盘快捷键 hook。
|
* 全局键盘快捷键 hook。
|
||||||
* MetonaEditor 内置工具栏处理格式化和模式切换(Ctrl+B/I/1/2/3),
|
* MetonaEditor 内置工具栏处理格式化和模式切换(Ctrl+B/I/1/2/3),
|
||||||
* 本 hook 仅处理应用级快捷键。
|
* v0.1.9 onSave 回调处理编辑器聚焦时的 Ctrl+S,
|
||||||
|
* 全局 handler 作为焦点外兜底(工具栏/侧边栏聚焦时仍可保存)。
|
||||||
*/
|
*/
|
||||||
export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, handleSaveAs: () => void) {
|
export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, handleSaveAs: () => void) {
|
||||||
const handleKeydown = useCallback((e: KeyboardEvent) => {
|
const handleKeydown = useCallback((e: KeyboardEvent) => {
|
||||||
const isCtrl = e.ctrlKey || e.metaKey
|
const isCtrl = e.ctrlKey || e.metaKey
|
||||||
|
|
||||||
if (isCtrl && e.key === 'o') { e.preventDefault(); handleOpenFile(); return }
|
if (isCtrl && e.key === 'o') { e.preventDefault(); handleOpenFile(); return }
|
||||||
|
// 全局兜底:编辑器未聚焦时仍可保存(编辑器聚焦时由 onSave 回调处理)
|
||||||
if (isCtrl && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); return }
|
if (isCtrl && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); return }
|
||||||
if (isCtrl && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); return }
|
if (isCtrl && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); return }
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { logError } from '../lib/errorHandler'
|
|||||||
* 替代各 hook 各自独立加载设置的模式。
|
* 替代各 hook 各自独立加载设置的模式。
|
||||||
*/
|
*/
|
||||||
export function useSettingsInit() {
|
export function useSettingsInit() {
|
||||||
const setDarkMode = useEditorStore(s => s.setDarkMode)
|
const setThemeMode = useEditorStore(s => s.setThemeMode)
|
||||||
const setViewMode = useEditorStore(s => s.setViewMode)
|
const setViewMode = useEditorStore(s => s.setViewMode)
|
||||||
const isInitialized = useRef(false)
|
const isInitialized = useRef(false)
|
||||||
|
|
||||||
@@ -20,11 +20,10 @@ export function useSettingsInit() {
|
|||||||
|
|
||||||
settingsRepository.load()
|
settingsRepository.load()
|
||||||
.then((settings) => {
|
.then((settings) => {
|
||||||
// 主题
|
// 主题:优先使用保存的设置,否则跟随系统偏好
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
const darkMode = settings.darkMode !== undefined ? settings.darkMode : prefersDark
|
const themeMode = settings.themeMode ?? (prefersDark ? 'dark' : 'light')
|
||||||
setDarkMode(darkMode)
|
setThemeMode(themeMode)
|
||||||
document.documentElement.classList.toggle('dark', darkMode)
|
|
||||||
|
|
||||||
// 视图模式
|
// 视图模式
|
||||||
setViewMode(settings.viewMode ?? 'editor')
|
setViewMode(settings.viewMode ?? 'editor')
|
||||||
@@ -42,7 +41,7 @@ export function useSettingsInit() {
|
|||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
logError('加载设置失败', error)
|
logError('加载设置失败', error)
|
||||||
})
|
})
|
||||||
}, [setDarkMode, setViewMode])
|
}, [setThemeMode, setViewMode])
|
||||||
|
|
||||||
return { isInitialized }
|
return { isInitialized }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,90 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useCallback } from 'react'
|
||||||
|
import MeEditor from '@metona-team/metona-editor'
|
||||||
import { useEditorStore } from '../stores/editorStore'
|
import { useEditorStore } from '../stores/editorStore'
|
||||||
import { settingsRepository } from '../db/settingsRepository'
|
import { settingsRepository } from '../db/settingsRepository'
|
||||||
import { syncToastTheme } from '../lib/toast'
|
import type { ThemeMode } from '../types/settings'
|
||||||
|
|
||||||
|
/** 将 hex 颜色转为带 alpha 的版本,用于背景色 */
|
||||||
|
function hexWithAlpha(hex: string, alpha: number): string {
|
||||||
|
if (!hex || !hex.startsWith('#')) return hex
|
||||||
|
const a = Math.round(alpha * 255).toString(16).padStart(2, '0')
|
||||||
|
return hex.length === 7 ? hex + a : hex.slice(0, 7) + a
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将编辑器 CSS 变量同步到应用根元素,保持颜色一致 */
|
||||||
|
function syncAppColorsToEditor(): void {
|
||||||
|
try {
|
||||||
|
const vars = MeEditor.themes.exportCSSVars()
|
||||||
|
if (!vars || typeof vars !== 'object') return
|
||||||
|
const root = document.documentElement
|
||||||
|
const set = (name: string, value: string | undefined) => {
|
||||||
|
if (value) root.style.setProperty(name, value)
|
||||||
|
}
|
||||||
|
const accent = vars['--md-accent'] ?? '#1a73e8'
|
||||||
|
// 直接映射 — 文本/边框用纯色
|
||||||
|
set('--bg', vars['--md-bg'])
|
||||||
|
set('--text', vars['--md-text'])
|
||||||
|
set('--border', vars['--md-border'])
|
||||||
|
set('--primary', accent)
|
||||||
|
set('--primary-dark', accent)
|
||||||
|
set('--text-secondary', vars['--md-muted'])
|
||||||
|
set('--text-tertiary', vars['--md-muted'])
|
||||||
|
set('--code-bg', vars['--md-code-bg'])
|
||||||
|
// 背景派生 — 用 accent 的低透明度版本
|
||||||
|
const accentBg = hexWithAlpha(accent, 0.12)
|
||||||
|
set('--primary-light', accentBg)
|
||||||
|
set('--sidebar-active', accentBg)
|
||||||
|
set('--sidebar-bg', vars['--md-bg'])
|
||||||
|
set('--sidebar-border', vars['--md-border'])
|
||||||
|
set('--sidebar-hover', hexWithAlpha(vars['--md-border'] ?? '#e1e4e8', 0.4))
|
||||||
|
set('--bg-secondary', hexWithAlpha(vars['--md-text'] ?? '#333', 0.04))
|
||||||
|
set('--bg-tertiary', hexWithAlpha(vars['--md-text'] ?? '#333', 0.08))
|
||||||
|
set('--border-light', hexWithAlpha(vars['--md-border'] ?? '#e1e4e8', 0.5))
|
||||||
|
set('--search-bg', hexWithAlpha(vars['--md-text'] ?? '#333', 0.04))
|
||||||
|
set('--search-border', vars['--md-border'])
|
||||||
|
// 阴影根据主题适配
|
||||||
|
const isDark = vars['--md-bg'] && vars['--md-bg'] !== '#ffffff' && vars['--md-bg'] !== '#fff'
|
||||||
|
root.style.setProperty('--shadow', isDark
|
||||||
|
? '0 1px 3px rgba(0,0,0,0.3)'
|
||||||
|
: '0 1px 3px rgba(0,0,0,0.08)')
|
||||||
|
root.style.setProperty('--shadow-lg', isDark
|
||||||
|
? '0 4px 12px rgba(0,0,0,0.4)'
|
||||||
|
: '0 4px 12px rgba(0,0,0,0.1)')
|
||||||
|
} catch { /* 容错 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const THEME_TO_TOAST: Record<ThemeMode, string> = {
|
||||||
|
light: 'light',
|
||||||
|
dark: 'dark',
|
||||||
|
warm: 'warm',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AR-04: 主题 hook
|
* 主题 hook — 三主题循环(亮色 → 暗色 → 暖色),
|
||||||
* 不再独立加载设置(由 useSettingsInit 统一加载)
|
* 应用颜色自动同步编辑器主题,保持一致。
|
||||||
* 仅负责主题切换时的同步和保存
|
|
||||||
*/
|
*/
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
const darkMode = useEditorStore(s => s.darkMode)
|
const themeMode = useEditorStore(s => s.themeMode)
|
||||||
const toggleDarkMode = useEditorStore(s => s.toggleDarkMode)
|
const cycleTheme = useEditorStore(s => s.cycleTheme)
|
||||||
|
|
||||||
// darkMode 变化时同步 class 并保存
|
const handleCycleTheme = useCallback(() => {
|
||||||
|
const next = cycleTheme()
|
||||||
|
// 同步编辑器全局主题
|
||||||
|
MeEditor.setTheme(next)
|
||||||
|
// 同步 Toast 主题
|
||||||
|
import('../lib/toast').then(({ MeToast }) => {
|
||||||
|
MeToast.themes.switchTheme(THEME_TO_TOAST[next] ?? 'auto')
|
||||||
|
})
|
||||||
|
// 持久化
|
||||||
|
settingsRepository.save({ themeMode: next })
|
||||||
|
}, [cycleTheme])
|
||||||
|
|
||||||
|
// themeMode 变化时同步应用颜色
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.classList.toggle('dark', darkMode)
|
// 暗色/暖色统一加 .dark class(兼容 global.css 中的 :root.dark 硬编码规则)
|
||||||
settingsRepository.save({ darkMode })
|
document.documentElement.classList.toggle('dark', themeMode === 'dark' || themeMode === 'warm')
|
||||||
syncToastTheme(darkMode)
|
syncAppColorsToEditor()
|
||||||
}, [darkMode])
|
}, [themeMode])
|
||||||
|
|
||||||
return { darkMode, toggleDarkMode }
|
return { themeMode, cycleTheme: handleCycleTheme }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import rehypeRaw from 'rehype-raw'
|
|||||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
||||||
import rehypeStringify from 'rehype-stringify'
|
import rehypeStringify from 'rehype-stringify'
|
||||||
import rehypeHighlight from 'rehype-highlight'
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
import type { Element, Root } from 'hast'
|
import type { Element, Root } from 'hast'
|
||||||
|
|
||||||
// 简单的路径解析(Electron renderer 中没有 path 模块)
|
// 简单的路径解析(Electron renderer 中没有 path 模块)
|
||||||
@@ -71,6 +72,47 @@ function rehypeFixImages(filePath: string | null): Plugin<[], Root> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义 rehype 插件:将 mermaid 代码块转为 .me-mermaid 容器
|
||||||
|
// 在 rehypeHighlight 之后运行,此时 code 元素已有 language-mermaid 类名。
|
||||||
|
// 输出结构需与 MetonaEditor 内置解析器一致,以便 mermaid.run() 统一查询。
|
||||||
|
function rehypeMermaid(): Plugin<[], Root> {
|
||||||
|
return () => (tree: Root) => {
|
||||||
|
visit(tree, 'element', (node: Element, index, parent) => {
|
||||||
|
if (node.tagName !== 'pre') return
|
||||||
|
if (parent === null || parent === undefined || index === undefined) return
|
||||||
|
|
||||||
|
const code = node.children.find(
|
||||||
|
(child): child is Element => child.type === 'element' && child.tagName === 'code'
|
||||||
|
)
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
|
const className: string[] = (code.properties?.className as string[] | undefined) ?? []
|
||||||
|
if (!className.some(c => c === 'language-mermaid' || c === 'mermaid')) return
|
||||||
|
|
||||||
|
// 提取代码文本内容
|
||||||
|
const text = (code.children ?? [])
|
||||||
|
.filter(c => c.type === 'text')
|
||||||
|
.map(c => (c as { type: 'text'; value: string }).value)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
// 替换为 mermaid 渲染容器(与 MetonaEditor 内置输出一致)
|
||||||
|
const mermaidContainer: Element = {
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'div',
|
||||||
|
properties: { className: ['me-mermaid'] },
|
||||||
|
children: [{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'div',
|
||||||
|
properties: { className: ['mermaid'] },
|
||||||
|
children: [{ type: 'text', value: text }]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children[index] = mermaidContainer
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PF-01: Markdown处理器LRU缓存
|
// PF-01: Markdown处理器LRU缓存
|
||||||
const MAX_CACHE_SIZE = 20
|
const MAX_CACHE_SIZE = 20
|
||||||
const processorCache = new Map<string, ReturnType<typeof buildProcessor>>()
|
const processorCache = new Map<string, ReturnType<typeof buildProcessor>>()
|
||||||
@@ -102,6 +144,7 @@ function buildProcessor(filePath: string | null) {
|
|||||||
strip: ['script', 'on*', 'javascript:'],
|
strip: ['script', 'on*', 'javascript:'],
|
||||||
})
|
})
|
||||||
.use(rehypeHighlight)
|
.use(rehypeHighlight)
|
||||||
|
.use(rehypeMermaid())
|
||||||
.use(rehypeStringify)
|
.use(rehypeStringify)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import MeToast from '@metona-team/metona-toast'
|
|||||||
*
|
*
|
||||||
* 替代原自研 Toast 组件(components/Toast),由 MeToast 接管全部通知渲染。
|
* 替代原自研 Toast 组件(components/Toast),由 MeToast 接管全部通知渲染。
|
||||||
* MeToast 自管理 DOM 与样式,无需在 React 树中挂载容器组件。
|
* MeToast 自管理 DOM 与样式,无需在 React 树中挂载容器组件。
|
||||||
|
*
|
||||||
|
* v0.4.4: 升级 MeToast 0.2.1,安装 keyboard / accessibility 插件。
|
||||||
*/
|
*/
|
||||||
MeToast.configure({
|
MeToast.configure({
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
@@ -17,8 +19,15 @@ MeToast.configure({
|
|||||||
showProgress: true,
|
showProgress: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
locale: 'zh-CN',
|
locale: 'zh-CN',
|
||||||
|
width: 360,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 安装内置插件
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- MeToast.use() 是插件安装方法,非 React Hook
|
||||||
|
MeToast.use('keyboard') // ESC 关闭所有 Toast
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
MeToast.use('accessibility') // 屏幕阅读器实时朗读 Toast 内容
|
||||||
|
|
||||||
/** 与原 showToast 保持兼容的类型子集 */
|
/** 与原 showToast 保持兼容的类型子集 */
|
||||||
export type ToastType = 'success' | 'error' | 'warning' | 'info'
|
export type ToastType = 'success' | 'error' | 'warning' | 'info'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe('editorStore', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useEditorStore.setState({
|
useEditorStore.setState({
|
||||||
viewMode: 'editor',
|
viewMode: 'editor',
|
||||||
darkMode: false,
|
themeMode: 'light',
|
||||||
externallyModified: null,
|
externallyModified: null,
|
||||||
loadingStates: {},
|
loadingStates: {},
|
||||||
})
|
})
|
||||||
@@ -24,29 +24,36 @@ describe('editorStore', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('setDarkMode', () => {
|
describe('themeMode', () => {
|
||||||
it('should enable dark mode', () => {
|
it('should default to light', () => {
|
||||||
useEditorStore.getState().setDarkMode(true)
|
expect(useEditorStore.getState().themeMode).toBe('light')
|
||||||
expect(useEditorStore.getState().darkMode).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should disable dark mode', () => {
|
it('should set theme mode', () => {
|
||||||
useEditorStore.setState({ darkMode: true })
|
useEditorStore.getState().setThemeMode('dark')
|
||||||
useEditorStore.getState().setDarkMode(false)
|
expect(useEditorStore.getState().themeMode).toBe('dark')
|
||||||
expect(useEditorStore.getState().darkMode).toBe(false)
|
})
|
||||||
|
|
||||||
|
it('should set warm theme', () => {
|
||||||
|
useEditorStore.getState().setThemeMode('warm')
|
||||||
|
expect(useEditorStore.getState().themeMode).toBe('warm')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('toggleDarkMode', () => {
|
describe('cycleTheme', () => {
|
||||||
it('should toggle from false to true', () => {
|
it('should cycle light → dark', () => {
|
||||||
useEditorStore.getState().toggleDarkMode()
|
expect(useEditorStore.getState().cycleTheme()).toBe('dark')
|
||||||
expect(useEditorStore.getState().darkMode).toBe(true)
|
expect(useEditorStore.getState().themeMode).toBe('dark')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle from true to false', () => {
|
it('should cycle dark → warm', () => {
|
||||||
useEditorStore.setState({ darkMode: true })
|
useEditorStore.setState({ themeMode: 'dark' })
|
||||||
useEditorStore.getState().toggleDarkMode()
|
expect(useEditorStore.getState().cycleTheme()).toBe('warm')
|
||||||
expect(useEditorStore.getState().darkMode).toBe(false)
|
})
|
||||||
|
|
||||||
|
it('should cycle warm → light', () => {
|
||||||
|
useEditorStore.setState({ themeMode: 'warm' })
|
||||||
|
expect(useEditorStore.getState().cycleTheme()).toBe('light')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { ViewMode } from '../types/settings'
|
import type { ViewMode, ThemeMode } from '../types/settings'
|
||||||
import type { MarkdownEditor } from '@metona-team/metona-editor'
|
import type { MarkdownEditor } from '@metona-team/metona-editor'
|
||||||
|
|
||||||
// Module-level getter for the MetonaEditor instance
|
// Module-level getter for the MetonaEditor instance
|
||||||
@@ -14,17 +14,19 @@ export function getMetonaEditor(): MarkdownEditor | null {
|
|||||||
return _getEditor ? _getEditor() : null
|
return _getEditor ? _getEditor() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THEME_CYCLE: ThemeMode[] = ['light', 'dark', 'warm']
|
||||||
|
|
||||||
interface EditorState {
|
interface EditorState {
|
||||||
viewMode: ViewMode
|
viewMode: ViewMode
|
||||||
darkMode: boolean
|
themeMode: ThemeMode
|
||||||
// AR-03: 外部修改检测状态,替代 DOM CustomEvent
|
// AR-03: 外部修改检测状态,替代 DOM CustomEvent
|
||||||
externallyModified: { filePath: string } | null
|
externallyModified: { filePath: string } | null
|
||||||
// UX-02: 全局加载状态
|
// UX-02: 全局加载状态
|
||||||
loadingStates: Record<string, boolean>
|
loadingStates: Record<string, boolean>
|
||||||
|
|
||||||
setViewMode: (mode: ViewMode) => void
|
setViewMode: (mode: ViewMode) => void
|
||||||
setDarkMode: (dark: boolean) => void
|
setThemeMode: (mode: ThemeMode) => void
|
||||||
toggleDarkMode: () => void
|
cycleTheme: () => ThemeMode
|
||||||
setExternallyModified: (info: { filePath: string } | null) => void
|
setExternallyModified: (info: { filePath: string } | null) => void
|
||||||
// UX-02: 加载状态管理
|
// UX-02: 加载状态管理
|
||||||
setLoading: (key: string, loading: boolean) => void
|
setLoading: (key: string, loading: boolean) => void
|
||||||
@@ -33,13 +35,19 @@ interface EditorState {
|
|||||||
|
|
||||||
export const useEditorStore = create<EditorState>((set, get) => ({
|
export const useEditorStore = create<EditorState>((set, get) => ({
|
||||||
viewMode: 'editor',
|
viewMode: 'editor',
|
||||||
darkMode: false,
|
themeMode: 'light',
|
||||||
externallyModified: null,
|
externallyModified: null,
|
||||||
loadingStates: {},
|
loadingStates: {},
|
||||||
|
|
||||||
setViewMode: (mode: ViewMode) => set({ viewMode: mode }),
|
setViewMode: (mode: ViewMode) => set({ viewMode: mode }),
|
||||||
setDarkMode: (dark: boolean) => set({ darkMode: dark }),
|
setThemeMode: (mode: ThemeMode) => set({ themeMode: mode }),
|
||||||
toggleDarkMode: () => set(state => ({ darkMode: !state.darkMode })),
|
cycleTheme: () => {
|
||||||
|
const current = get().themeMode
|
||||||
|
const idx = THEME_CYCLE.indexOf(current)
|
||||||
|
const next = THEME_CYCLE[(idx + 1) % THEME_CYCLE.length]
|
||||||
|
set({ themeMode: next })
|
||||||
|
return next
|
||||||
|
},
|
||||||
setExternallyModified: (info: { filePath: string } | null) => set({ externallyModified: info }),
|
setExternallyModified: (info: { filePath: string } | null) => set({ externallyModified: info }),
|
||||||
setLoading: (key: string, loading: boolean) => set(state => ({
|
setLoading: (key: string, loading: boolean) => set(state => ({
|
||||||
loadingStates: { ...state.loadingStates, [key]: loading }
|
loadingStates: { ...state.loadingStates, [key]: loading }
|
||||||
|
|||||||
@@ -128,6 +128,22 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FIX: Allow text selection in the preview pane.
|
||||||
|
global.css sets user-select:none on html/body to prevent
|
||||||
|
selection in UI chrome (toolbar, tabs, sidebar), but this
|
||||||
|
also blocked mouse-drag selection in the Markdown preview.
|
||||||
|
Override back to the browser default for the preview area. */
|
||||||
|
.metona-editor-wrapper .me-preview,
|
||||||
|
.metona-editor-wrapper .me-preview * {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Also restore selection in the editor textarea (MetonaEditor
|
||||||
|
explicitly sets user-select:none on it in some themes) */
|
||||||
|
.metona-editor-wrapper textarea {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tab Bar */
|
/* Tab Bar */
|
||||||
#tab-bar {
|
#tab-bar {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|||||||
@@ -23,23 +23,6 @@
|
|||||||
--sidebar-active: var(--primary-light);
|
--sidebar-active: var(--primary-light);
|
||||||
--search-bg: var(--bg-secondary);
|
--search-bg: var(--bg-secondary);
|
||||||
--search-border: var(--border);
|
--search-border: var(--border);
|
||||||
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
--font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
|
--font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark {
|
|
||||||
--primary: #8ab4f8;
|
|
||||||
--primary-light: #1a3a5c;
|
|
||||||
--primary-dark: #aecbfa;
|
|
||||||
--bg: #1e1e1e;
|
|
||||||
--bg-secondary: #252526;
|
|
||||||
--bg-tertiary: #2d2d2d;
|
|
||||||
--text: #d4d4d4;
|
|
||||||
--text-secondary: #9e9e9e;
|
|
||||||
--text-tertiary: #6e6e6e;
|
|
||||||
--border: #3e3e3e;
|
|
||||||
--border-light: #333333;
|
|
||||||
--code-bg: #2d2d2d;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
||||||
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
export type ThemeMode = 'light' | 'dark' | 'warm'
|
||||||
export type ViewMode = 'editor' | 'preview' | 'source'
|
export type ViewMode = 'editor' | 'preview' | 'source'
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
darkMode: boolean
|
themeMode: ThemeMode
|
||||||
viewMode: ViewMode
|
viewMode: ViewMode
|
||||||
sidebarCollapsed: boolean
|
sidebarCollapsed: boolean
|
||||||
sidebarWidth: number
|
sidebarWidth: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: Settings = {
|
export const DEFAULT_SETTINGS: Settings = {
|
||||||
darkMode: false,
|
themeMode: 'light',
|
||||||
viewMode: 'editor',
|
viewMode: 'editor',
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
sidebarWidth: 240
|
sidebarWidth: 240
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 共享常量 — 主进程和渲染进程共用
|
// 共享常量 — 主进程和渲染进程共用
|
||||||
export const APP_VERSION = 'v0.4.0'
|
export const APP_VERSION = 'v0.4.5'
|
||||||
export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
|
export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
|
||||||
export const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const
|
export const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const
|
||||||
export const SKIP_DIRS = new Set([
|
export const SKIP_DIRS = new Set([
|
||||||
|
|||||||
Reference in New Issue
Block a user