13 Commits

Author SHA1 Message Date
4b0f39d747 Merge branch 'master' into dev 2025-11-21 23:37:36 +08:00
096cc1da94 🎨 Optimize hyperlink extension 2025-11-21 23:35:42 +08:00
2d3200ad97 ♻️ Refactor context menu 2025-11-21 22:30:47 +08:00
4e82e2f6f7 ♻️ Refactor the Markdown preview theme application logic 2025-11-21 20:20:06 +08:00
339ed53c2e ♻️ Refactor theme module 2025-11-21 00:03:03 +08:00
fc7c162e2f ♻️ Refactor theme module 2025-11-20 23:07:12 +08:00
dependabot[bot]
24f1549730 ⬆️ Bump golang.org/x/crypto from 0.44.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 03:04:41 +00:00
5584a46ca2 ♻️ Refactor theme module 2025-11-20 00:39:00 +08:00
4471441d6f ♻️ Refactor some code 2025-11-19 20:54:58 +08:00
991a89147e 🎨 Optimize code 2025-11-17 23:14:58 +08:00
a08c0d8448 🎨 Modify code block logic 2025-11-17 22:11:16 +08:00
59db8dd177 Added Monocraft font 2025-11-16 22:04:02 +08:00
29693f1baf 💄 Modify some styles 2025-11-16 21:23:59 +08:00
128 changed files with 4479 additions and 5005 deletions

View File

@@ -1170,7 +1170,7 @@ export class Theme {
this["type"] = ("" as ThemeType); this["type"] = ("" as ThemeType);
} }
if (!("colors" in $$source)) { if (!("colors" in $$source)) {
this["colors"] = (new ThemeColorConfig()); this["colors"] = ({} as ThemeColorConfig);
} }
if (!("isDefault" in $$source)) { if (!("isDefault" in $$source)) {
this["isDefault"] = false; this["isDefault"] = false;
@@ -1199,303 +1199,9 @@ export class Theme {
} }
/** /**
* ThemeColorConfig 主题颜色配置(与前端 ThemeColors 接口保持一致) * ThemeColorConfig 使用与前端 ThemeColors 相同的结构,存储任意主题键值
*/ */
export class ThemeColorConfig { export type ThemeColorConfig = { [_: string]: any };
/**
* 主题基本信息
* 主题名称
*/
"name": string;
/**
* 是否为深色主题
*/
"dark": boolean;
/**
* 基础色调
* 主背景色
*/
"background": string;
/**
* 次要背景色(用于代码块交替背景)
*/
"backgroundSecondary": string;
/**
* 面板背景
*/
"surface": string;
/**
* 下拉菜单背景
*/
"dropdownBackground": string;
/**
* 下拉菜单边框
*/
"dropdownBorder": string;
/**
* 文本颜色
* 主文本色
*/
"foreground": string;
/**
* 次要文本色
*/
"foregroundSecondary": string;
/**
* 注释色
*/
"comment": string;
/**
* 语法高亮色 - 核心
* 关键字
*/
"keyword": string;
/**
* 字符串
*/
"string": string;
/**
* 函数名
*/
"function": string;
/**
* 数字
*/
"number": string;
/**
* 操作符
*/
"operator": string;
/**
* 变量
*/
"variable": string;
/**
* 类型
*/
"type": string;
/**
* 语法高亮色 - 扩展
* 常量
*/
"constant": string;
/**
* 存储类型(如 static, const
*/
"storage": string;
/**
* 参数
*/
"parameter": string;
/**
* 类名
*/
"class": string;
/**
* 标题Markdown等
*/
"heading": string;
/**
* 无效内容/错误
*/
"invalid": string;
/**
* 正则表达式
*/
"regexp": string;
/**
* 界面元素
* 光标
*/
"cursor": string;
/**
* 选中背景
*/
"selection": string;
/**
* 失焦选中背景
*/
"selectionBlur": string;
/**
* 当前行高亮
*/
"activeLine": string;
/**
* 行号
*/
"lineNumber": string;
/**
* 活动行号颜色
*/
"activeLineNumber": string;
/**
* 边框和分割线
* 边框色
*/
"borderColor": string;
/**
* 浅色边框
*/
"borderLight": string;
/**
* 搜索和匹配
* 搜索匹配
*/
"searchMatch": string;
/**
* 匹配括号
*/
"matchingBracket": string;
/** Creates a new ThemeColorConfig instance. */
constructor($$source: Partial<ThemeColorConfig> = {}) {
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("dark" in $$source)) {
this["dark"] = false;
}
if (!("background" in $$source)) {
this["background"] = "";
}
if (!("backgroundSecondary" in $$source)) {
this["backgroundSecondary"] = "";
}
if (!("surface" in $$source)) {
this["surface"] = "";
}
if (!("dropdownBackground" in $$source)) {
this["dropdownBackground"] = "";
}
if (!("dropdownBorder" in $$source)) {
this["dropdownBorder"] = "";
}
if (!("foreground" in $$source)) {
this["foreground"] = "";
}
if (!("foregroundSecondary" in $$source)) {
this["foregroundSecondary"] = "";
}
if (!("comment" in $$source)) {
this["comment"] = "";
}
if (!("keyword" in $$source)) {
this["keyword"] = "";
}
if (!("string" in $$source)) {
this["string"] = "";
}
if (!("function" in $$source)) {
this["function"] = "";
}
if (!("number" in $$source)) {
this["number"] = "";
}
if (!("operator" in $$source)) {
this["operator"] = "";
}
if (!("variable" in $$source)) {
this["variable"] = "";
}
if (!("type" in $$source)) {
this["type"] = "";
}
if (!("constant" in $$source)) {
this["constant"] = "";
}
if (!("storage" in $$source)) {
this["storage"] = "";
}
if (!("parameter" in $$source)) {
this["parameter"] = "";
}
if (!("class" in $$source)) {
this["class"] = "";
}
if (!("heading" in $$source)) {
this["heading"] = "";
}
if (!("invalid" in $$source)) {
this["invalid"] = "";
}
if (!("regexp" in $$source)) {
this["regexp"] = "";
}
if (!("cursor" in $$source)) {
this["cursor"] = "";
}
if (!("selection" in $$source)) {
this["selection"] = "";
}
if (!("selectionBlur" in $$source)) {
this["selectionBlur"] = "";
}
if (!("activeLine" in $$source)) {
this["activeLine"] = "";
}
if (!("lineNumber" in $$source)) {
this["lineNumber"] = "";
}
if (!("activeLineNumber" in $$source)) {
this["activeLineNumber"] = "";
}
if (!("borderColor" in $$source)) {
this["borderColor"] = "";
}
if (!("borderLight" in $$source)) {
this["borderLight"] = "";
}
if (!("searchMatch" in $$source)) {
this["searchMatch"] = "";
}
if (!("matchingBracket" in $$source)) {
this["matchingBracket"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new ThemeColorConfig instance from a string or object.
*/
static createFrom($$source: any = {}): ThemeColorConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
}
}
/** /**
* ThemeType 主题类型枚举 * ThemeType 主题类型枚举
@@ -1636,6 +1342,11 @@ var $$createType6 = (function $$initCreateType6(...args): any {
}); });
const $$createType7 = $Create.Map($Create.Any, $Create.Any); const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType8 = HotkeyCombo.createFrom; const $$createType8 = HotkeyCombo.createFrom;
const $$createType9 = ThemeColorConfig.createFrom; var $$createType9 = (function $$initCreateType9(...args): any {
if ($$createType9 === $$initCreateType9) {
$$createType9 = $$createType7;
}
return $$createType9(...args);
});
const $$createType10 = GithubConfig.createFrom; const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom; const $$createType11 = GiteaConfig.createFrom;

View File

@@ -18,23 +18,10 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
/** /**
* GetAllThemes 获取所有主题 * GetThemeByName 通过名称获取主题覆盖,若不存在则返回 nil
*/ */
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } { export function GetThemeByName(name: string): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2425053076) as any; let $resultPromise = $Call.ByID(1938954770, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetThemeByID 根据ID或名称获取主题
* 如果 id > 0按ID查询如果 id = 0按名称查询
*/
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(127385338, id, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
}) as any; }) as any;
@@ -43,10 +30,10 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
} }
/** /**
* ResetTheme 重置主题为预设配置 * ResetTheme 删除指定主题的覆盖配置
*/ */
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } { export function ResetTheme(name: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, id, name) as any; let $resultPromise = $Call.ByID(1806334457, name) as any;
return $resultPromise; return $resultPromise;
} }
@@ -59,7 +46,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
} }
/** /**
* ServiceStartup 服务启动时初始化 * ServiceStartup 服务启动
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2915959937, options) as any; let $resultPromise = $Call.ByID(2915959937, options) as any;
@@ -67,14 +54,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
} }
/** /**
* UpdateTheme 更新主题 * UpdateTheme 保存或更新主题覆盖
*/ */
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } { export function UpdateTheme(name: string, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, id, colors) as any; let $resultPromise = $Call.ByID(70189749, name, colors) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.Theme.createFrom; const $$createType0 = models$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -1,254 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
鸿蒙字体压缩工具
使用 fonttools 库压缩 TTF 字体文件,减小文件大小
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
from typing import List, Tuple
def check_dependencies():
"""检查必要的依赖是否已安装"""
missing_packages = []
# 检查 fonttools
try:
import fontTools
except ImportError:
missing_packages.append('fonttools')
# 检查 brotli
try:
import brotli
except ImportError:
missing_packages.append('brotli')
# 检查 pyftsubset 命令是否可用
try:
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
if result.returncode != 0:
missing_packages.append('fonttools[subset]')
except FileNotFoundError:
if 'fonttools' not in missing_packages:
missing_packages.append('fonttools[subset]')
if missing_packages:
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
print("请运行以下命令安装:")
print(f"pip install {' '.join(missing_packages)}")
return False
return True
def get_file_size(file_path: str) -> int:
"""获取文件大小(字节)"""
return os.path.getsize(file_path)
def format_file_size(size_bytes: int) -> str:
"""格式化文件大小显示"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.2f} KB"
else:
return f"{size_bytes / (1024 * 1024):.2f} MB"
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
"""
压缩单个字体文件
Args:
input_path: 输入字体文件路径
output_path: 输出字体文件路径
compression_level: 压缩级别 ("basic", "medium", "aggressive")
Returns:
bool: 压缩是否成功
"""
try:
# 基础压缩参数
base_args = [
"pyftsubset", input_path,
"--output-file=" + output_path,
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
]
# 根据压缩级别设置不同的参数
if compression_level == "basic":
# 基础压缩:保留常用字符和功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
"--layout-features=*", # 保留所有布局特性
"--glyph-names", # 保留字形名称
"--symbol-cmap", # 保留符号映射
"--legacy-cmap", # 保留传统字符映射
"--notdef-glyph", # 保留 .notdef 字形
"--recommended-glyphs", # 保留推荐字形
"--name-IDs=*", # 保留所有名称ID
"--name-legacy", # 保留传统名称
]
elif compression_level == "medium":
# 中等压缩:移除一些不常用的功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
"--layout-features=kern,liga,clig", # 只保留关键布局特性
"--no-glyph-names", # 移除字形名称
"--notdef-glyph",
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
]
else: # aggressive
# 激进压缩:最大程度减小文件大小
args = base_args + [
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
"--no-layout-features", # 移除所有布局特性
"--no-glyph-names", # 移除字形名称
"--no-symbol-cmap", # 移除符号映射
"--no-legacy-cmap", # 移除传统映射
"--notdef-glyph",
"--name-IDs=1,2", # 只保留最基本的名称
"--desubroutinize", # 去子程序化可能减小CFF字体大小
]
# 执行压缩命令
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode == 0:
return True
else:
print(f"压缩失败: {result.stderr}")
return False
except Exception as e:
print(f"压缩过程中出现错误: {str(e)}")
return False
def find_font_files(directory: str) -> List[str]:
"""查找目录中的所有字体文件"""
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
font_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if any(file.lower().endswith(ext) for ext in font_extensions):
font_files.append(os.path.join(root, file))
return font_files
def compress_fonts_batch(font_directory: str, compression_level: str = "basic"):
"""
批量压缩字体文件
Args:
font_directory: 字体文件目录
compression_level: 压缩级别
"""
if not os.path.exists(font_directory):
print(f"错误: 目录 {font_directory} 不存在")
return
# 查找所有字体文件
font_files = find_font_files(font_directory)
if not font_files:
print("未找到字体文件")
return
print(f"找到 {len(font_files)} 个字体文件")
print(f"压缩级别: {compression_level}")
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
print("-" * 60)
total_original_size = 0
total_compressed_size = 0
successful_compressions = 0
for i, font_file in enumerate(font_files, 1):
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
# 获取原始文件大小
original_size = get_file_size(font_file)
total_original_size += original_size
# 生成输出文件名(保持原文件名,只改变扩展名)
file_dir = os.path.dirname(font_file)
base_name = os.path.splitext(os.path.basename(font_file))[0]
output_file = os.path.join(file_dir, f"{base_name}.woff2")
# 压缩字体
if compress_font(font_file, output_file, compression_level):
if os.path.exists(output_file):
compressed_size = get_file_size(output_file)
total_compressed_size += compressed_size
successful_compressions += 1
# 计算压缩率
compression_ratio = (1 - compressed_size / original_size) * 100
print(f" ✓ 成功: {format_file_size(original_size)}{format_file_size(compressed_size)} "
f"(压缩 {compression_ratio:.1f}%)")
else:
print(f" ✗ 失败: 输出文件未生成")
else:
print(f" ✗ 失败: 压缩过程出错")
print()
# 显示总结
print("=" * 60)
print("压缩完成!")
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
if successful_compressions > 0:
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
print(f"总大小: {format_file_size(total_original_size)}{format_file_size(total_compressed_size)}")
print(f"总压缩率: {total_compression_ratio:.1f}%")
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
def main():
"""主函数"""
print("鸿蒙字体压缩工具")
print("=" * 60)
# 检查依赖
if not check_dependencies():
return
# 获取当前脚本所在目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 设置默认字体目录
font_directory = current_dir
print(f"字体目录: {font_directory}")
# 让用户选择压缩级别
print("\n请选择压缩级别:")
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
print("2. 中等压缩 (平衡文件大小和功能)")
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
while True:
choice = input("\n请输入选择 (1-3): ").strip()
if choice == "1":
compression_level = "basic"
break
elif choice == "2":
compression_level = "medium"
break
elif choice == "3":
compression_level = "aggressive"
break
else:
print("无效选择,请输入 1、2 或 3")
# 开始批量压缩
compress_fonts_batch(font_directory, compression_level=compression_level)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,179 @@
# 字体压缩工具使用指南
## 📖 简介
`font_compressor.py` 是一个通用的字体压缩工具,可以:
- ✅ 将 TTF、OTF、WOFF 字体文件转换为 WOFF2 格式
- ✅ 支持相对路径和绝对路径
- ✅ 自动生成 CSS 字体定义文件
- ✅ 智能识别字体字重和样式
- ✅ 批量处理整个目录(包括子目录)
## 🚀 前置要求
安装 Python 依赖包:
```bash
pip install fonttools brotli
```
## 📝 使用方法
### 基础用法
```bash
# 进入 fonts 目录
cd frontend/src/assets/fonts
# 交互式模式处理当前目录
python font_compressor.py
# 处理相对路径的 Monocraft 目录
python font_compressor.py Monocraft
# 处理相对路径并指定压缩级别
python font_compressor.py Monocraft -l basic
```
### 生成 CSS 文件
```bash
# 压缩 Monocraft 字体并生成 CSS 文件
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
# 压缩 Hack 字体并生成 CSS
python font_compressor.py Hack -l basic -c ../styles/hack_fonts_new.css
# 压缩 OpenSans 字体并生成 CSS
python font_compressor.py OpenSans -l medium -c ../styles/opensans_fonts.css
```
### 高级用法
```bash
# 使用绝对路径
python font_compressor.py E:\Go_WorkSpace\voidraft\frontend\src\assets\fonts\Monocraft -l basic -c monocraft.css
# 不同压缩级别
python font_compressor.py Monocraft -l basic # 基础压缩,保留所有功能
python font_compressor.py Monocraft -l medium # 中等压缩,平衡大小和功能
python font_compressor.py Monocraft -l aggressive # 激进压缩,最小文件
```
## ⚙️ 命令行参数
| 参数 | 说明 | 示例 |
|------|------|------|
| `directory` | 字体目录(相对/绝对路径) | `Monocraft``/path/to/fonts` |
| `-l, --level` | 压缩级别 (basic/medium/aggressive) | `-l basic` |
| `-c, --css` | CSS 输出文件路径 | `-c monocraft.css` |
| `--version` | 显示版本信息 | `--version` |
| `-h, --help` | 显示帮助信息 | `-h` |
## 📊 压缩级别说明
### basic基础 - 推荐
- 保留大部分字体功能
- 适合网页使用
- 压缩率约 30-40%
### medium中等
- 移除一些不常用的功能
- 平衡文件大小和功能
- 压缩率约 40-50%
### aggressive激进
- 最大程度减小文件大小
- 可能影响高级排版功能
- 压缩率约 50-60%
## 📁 输出结果
### 字体文件
压缩后的 `.woff2` 文件会保存在原文件相同的目录下,例如:
- `Monocraft/ttf/Monocraft-Bold.ttf``Monocraft/ttf/Monocraft-Bold.woff2`
- `Hack/hack-regular.ttf``Hack/hack-regular.woff2`
### CSS 文件
生成的 CSS 文件会包含:
- 自动识别的字体家族名称
- 正确的字重和样式设置
- 使用相对路径的字体引用
- 按字重排序的 `@font-face` 定义
生成的 CSS 示例:
```css
/* 自动生成的字体文件 */
/* 由 font_compressor.py 生成 */
/* Monocraft 字体家族 */
/* Monocraft Light */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Monocraft Bold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
```
## 🎯 实际使用示例
### 示例 1: 压缩 Monocraft 字体
```bash
cd frontend/src/assets/fonts
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
```
这将:
1. 扫描 `Monocraft/ttf``Monocraft/otf` 目录
2. 将所有字体文件转换为 WOFF2
3.`frontend/src/assets/styles/monocraft_fonts.css` 生成 CSS 文件
### 示例 2: 批量处理多个字体目录
```bash
cd frontend/src/assets/fonts
# 压缩 Monocraft
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
# 压缩 OpenSans
python font_compressor.py OpenSans -l basic -c ../styles/opensans_fonts.css
# 压缩 Hack已有 CSS只需生成新版本对比
python font_compressor.py Hack -l basic -c ../styles/hack_fonts_new.css
```
## 🔍 字体信息自动识别
工具会自动从文件名识别:
- **字重**Thin(100), Light(300), Regular(400), Medium(500), SemiBold(600), Bold(700), Black(900)
- **样式**normal, italic
- **字体家族**:自动去除字重和样式后缀
支持的命名格式:
- `FontName-Bold.ttf`
- `FontName_Bold.otf`
- `FontName-BoldItalic.ttf`
- `FontName_SemiBold_Italic.woff`
## 📞 获取帮助
```bash
python font_compressor.py --help
```

View File

@@ -0,0 +1,494 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用字体压缩工具
使用 fonttools 库将字体文件转换为 WOFF2 格式,减小文件大小
支持 TTF、OTF、WOFF 等格式的字体文件
"""
import os
import sys
import subprocess
import shutil
import argparse
import re
from pathlib import Path
from typing import List, Tuple, Dict, Optional
def check_dependencies():
"""检查必要的依赖是否已安装"""
missing_packages = []
# 检查 fonttools
try:
import fontTools
except ImportError:
missing_packages.append('fonttools')
# 检查 brotli
try:
import brotli
except ImportError:
missing_packages.append('brotli')
# 检查 pyftsubset 命令是否可用
try:
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
if result.returncode != 0:
missing_packages.append('fonttools[subset]')
except FileNotFoundError:
if 'fonttools' not in missing_packages:
missing_packages.append('fonttools[subset]')
if missing_packages:
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
print("请运行以下命令安装:")
print(f"pip install {' '.join(missing_packages)}")
return False
return True
def get_file_size(file_path: str) -> int:
"""获取文件大小(字节)"""
return os.path.getsize(file_path)
def format_file_size(size_bytes: int) -> str:
"""格式化文件大小显示"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.2f} KB"
else:
return f"{size_bytes / (1024 * 1024):.2f} MB"
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
"""
压缩单个字体文件
Args:
input_path: 输入字体文件路径
output_path: 输出字体文件路径
compression_level: 压缩级别 ("basic", "medium", "aggressive")
Returns:
bool: 压缩是否成功
"""
try:
# 基础压缩参数
base_args = [
"pyftsubset", input_path,
"--output-file=" + output_path,
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
]
# 根据压缩级别设置不同的参数
if compression_level == "basic":
# 基础压缩:保留常用字符和功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
"--layout-features=*", # 保留所有布局特性
"--glyph-names", # 保留字形名称
"--symbol-cmap", # 保留符号映射
"--legacy-cmap", # 保留传统字符映射
"--notdef-glyph", # 保留 .notdef 字形
"--recommended-glyphs", # 保留推荐字形
"--name-IDs=*", # 保留所有名称ID
"--name-legacy", # 保留传统名称
]
elif compression_level == "medium":
# 中等压缩:移除一些不常用的功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
"--layout-features=kern,liga,clig", # 只保留关键布局特性
"--no-glyph-names", # 移除字形名称
"--notdef-glyph",
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
]
else: # aggressive
# 激进压缩:最大程度减小文件大小
args = base_args + [
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
"--no-layout-features", # 移除所有布局特性
"--no-glyph-names", # 移除字形名称
"--no-symbol-cmap", # 移除符号映射
"--no-legacy-cmap", # 移除传统映射
"--notdef-glyph",
"--name-IDs=1,2", # 只保留最基本的名称
"--desubroutinize", # 去子程序化可能减小CFF字体大小
]
# 执行压缩命令
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode == 0:
return True
else:
print(f"压缩失败: {result.stderr}")
return False
except Exception as e:
print(f"压缩过程中出现错误: {str(e)}")
return False
def find_font_files(directory: str, exclude_woff2: bool = False) -> List[str]:
"""查找目录中的所有字体文件"""
if exclude_woff2:
font_extensions = ['.ttf', '.otf', '.woff']
else:
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
font_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if any(file.lower().endswith(ext) for ext in font_extensions):
font_files.append(os.path.join(root, file))
return font_files
def parse_font_info(filename: str) -> Dict[str, any]:
"""
从字体文件名解析字体信息(字重、样式等)
Args:
filename: 字体文件名(不含路径)
Returns:
包含字体信息的字典
"""
# 移除扩展名
name_without_ext = os.path.splitext(filename)[0]
# 字重映射
weight_mapping = {
'thin': (100, 'Thin'),
'extralight': (200, 'ExtraLight'),
'light': (300, 'Light'),
'regular': (400, 'Regular'),
'normal': (400, 'Regular'),
'medium': (500, 'Medium'),
'semibold': (600, 'SemiBold'),
'bold': (700, 'Bold'),
'extrabold': (800, 'ExtraBold'),
'black': (900, 'Black'),
'heavy': (900, 'Heavy'),
}
# 默认值
font_weight = 400
font_style = 'normal'
weight_name = 'Regular'
# 检查是否为斜体
if re.search(r'italic', name_without_ext, re.IGNORECASE):
font_style = 'italic'
# 检查字重
name_lower = name_without_ext.lower()
for weight_key, (weight_value, weight_label) in weight_mapping.items():
if weight_key in name_lower:
font_weight = weight_value
weight_name = weight_label
break
# 提取字体家族名称(移除字重和样式后缀)
family_name = name_without_ext
for weight_key, (_, weight_label) in weight_mapping.items():
family_name = re.sub(r'[-_]?' + weight_label, '', family_name, flags=re.IGNORECASE)
family_name = re.sub(r'[-_]?italic', '', family_name, flags=re.IGNORECASE)
family_name = family_name.strip('-_')
return {
'family': family_name,
'weight': font_weight,
'style': font_style,
'weight_name': weight_name,
'full_name': name_without_ext
}
def generate_css(font_files: List[str], output_css_path: str, css_base_path: str):
"""
生成CSS字体文件
Args:
font_files: 字体文件路径列表woff2文件
output_css_path: 输出CSS文件路径
css_base_path: CSS文件相对于字体文件的基础路径
"""
# 按字体家族分组
font_groups: Dict[str, List[Dict]] = {}
for font_file in font_files:
if not font_file.endswith('.woff2'):
continue
filename = os.path.basename(font_file)
font_info = parse_font_info(filename)
# 计算相对路径
font_dir = os.path.dirname(font_file)
css_dir = os.path.dirname(output_css_path)
try:
# 计算从CSS文件到字体文件的相对路径
rel_path = os.path.relpath(font_file, css_dir)
# 统一使用正斜杠适用于Web
rel_path = rel_path.replace('\\', '/')
except ValueError:
# 如果在不同驱动器上,使用绝对路径
rel_path = font_file.replace('\\', '/')
font_info['path'] = rel_path
family = font_info['family']
if family not in font_groups:
font_groups[family] = []
font_groups[family].append(font_info)
# 生成CSS内容
css_lines = ['/* 自动生成的字体文件 */', '/* 由 font_compressor.py 生成 */', '']
for family, fonts in sorted(font_groups.items()):
css_lines.append(f'/* {family} 字体家族 */')
css_lines.append('')
# 按字重排序
fonts.sort(key=lambda x: (x['weight'], x['style']))
for font in fonts:
css_lines.append(f"/* {family} {font['weight_name']}{' Italic' if font['style'] == 'italic' else ''} */")
css_lines.append('@font-face {')
css_lines.append(f" font-family: '{family}';")
css_lines.append(f" src: url('{font['path']}') format('woff2');")
css_lines.append(f" font-weight: {font['weight']};")
css_lines.append(f" font-style: {font['style']};")
css_lines.append(' font-display: swap;')
css_lines.append('}')
css_lines.append('')
# 写入CSS文件
with open(output_css_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(css_lines))
print(f"[OK] CSS文件已生成: {output_css_path}")
print(f" 包含 {sum(len(fonts) for fonts in font_groups.values())} 个字体定义")
print(f" 字体家族: {', '.join(sorted(font_groups.keys()))}")
def compress_fonts_batch(font_directory: str, compression_level: str = "basic") -> List[str]:
"""
批量压缩字体文件
Args:
font_directory: 字体文件目录
compression_level: 压缩级别
Returns:
生成的woff2文件路径列表
"""
if not os.path.exists(font_directory):
print(f"错误: 目录 {font_directory} 不存在")
return []
# 查找所有字体文件排除已经是woff2的
font_files = find_font_files(font_directory, exclude_woff2=True)
if not font_files:
print("未找到字体文件")
return []
print(f"找到 {len(font_files)} 个字体文件")
print(f"压缩级别: {compression_level}")
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
print("-" * 60)
total_original_size = 0
total_compressed_size = 0
successful_compressions = 0
generated_woff2_files = []
for i, font_file in enumerate(font_files, 1):
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
# 获取原始文件大小
original_size = get_file_size(font_file)
total_original_size += original_size
# 生成输出文件名(保持原文件名,只改变扩展名)
file_dir = os.path.dirname(font_file)
base_name = os.path.splitext(os.path.basename(font_file))[0]
output_file = os.path.join(file_dir, f"{base_name}.woff2")
# 压缩字体
if compress_font(font_file, output_file, compression_level):
if os.path.exists(output_file):
compressed_size = get_file_size(output_file)
total_compressed_size += compressed_size
successful_compressions += 1
generated_woff2_files.append(output_file)
# 计算压缩率
compression_ratio = (1 - compressed_size / original_size) * 100
print(f" [OK] 成功: {format_file_size(original_size)} -> {format_file_size(compressed_size)} "
f"(压缩 {compression_ratio:.1f}%)")
else:
print(f" [失败] 输出文件未生成")
else:
print(f" [失败] 压缩过程出错")
print()
# 显示总结
print("=" * 60)
print("压缩完成!")
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
if successful_compressions > 0:
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
print(f"总大小: {format_file_size(total_original_size)}{format_file_size(total_compressed_size)}")
print(f"总压缩率: {total_compression_ratio:.1f}%")
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
return generated_woff2_files
def main():
"""主函数"""
# 解析命令行参数
parser = argparse.ArgumentParser(
description='通用字体压缩工具 - 将字体文件转换为 WOFF2 格式并生成CSS',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
使用示例:
%(prog)s # 交互式模式,处理当前目录
%(prog)s Monocraft # 处理相对路径目录
%(prog)s Monocraft -l basic # 使用基础压缩级别
%(prog)s Monocraft -l basic -c monocraft.css # 压缩并生成CSS文件
%(prog)s /path/to/fonts -l medium -c fonts.css # 使用绝对路径
压缩级别说明:
basic - 基础压缩:保留大部分功能,适合网页使用
medium - 中等压缩:平衡文件大小和功能
aggressive - 激进压缩:最小文件大小,可能影响显示效果
CSS生成说明:
使用 -c/--css 选项生成CSS文件自动使用相对路径引用字体文件
'''
)
parser.add_argument(
'directory',
nargs='?',
default=None,
help='字体文件目录路径(支持相对/绝对路径,默认为当前脚本所在目录)'
)
parser.add_argument(
'-l', '--level',
choices=['basic', 'medium', 'aggressive'],
default=None,
help='压缩级别basic基础、medium中等、aggressive激进'
)
parser.add_argument(
'-c', '--css',
default=None,
help='生成CSS文件路径相对于脚本位置或绝对路径'
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s 2.0'
)
args = parser.parse_args()
print("=" * 60)
print("通用字体压缩工具 v2.0")
print("=" * 60)
# 检查依赖
if not check_dependencies():
return
# 获取脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
# 确定字体目录
if args.directory:
# 支持相对路径和绝对路径
if os.path.isabs(args.directory):
font_directory = args.directory
else:
font_directory = os.path.join(script_dir, args.directory)
font_directory = os.path.abspath(font_directory)
else:
# 默认使用当前脚本所在目录
font_directory = script_dir
# 检查目录是否存在
if not os.path.exists(font_directory):
print(f"\n错误: 目录不存在: {font_directory}")
sys.exit(1)
print(f"\n字体目录: {font_directory}")
# 确定压缩级别
compression_level = args.level
if compression_level is None:
# 交互式选择压缩级别
print("\n请选择压缩级别:")
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
print("2. 中等压缩 (平衡文件大小和功能)")
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
while True:
choice = input("\n请输入选择 (1-3): ").strip()
if choice == "1":
compression_level = "basic"
break
elif choice == "2":
compression_level = "medium"
break
elif choice == "3":
compression_level = "aggressive"
break
else:
print("无效选择,请输入 1、2 或 3")
# 开始批量压缩
print()
generated_files = compress_fonts_batch(font_directory, compression_level=compression_level)
# 生成CSS文件
if args.css and generated_files:
print()
print("=" * 60)
print("生成CSS文件...")
print("=" * 60)
# 确定CSS输出路径
if os.path.isabs(args.css):
css_path = args.css
else:
css_path = os.path.join(script_dir, args.css)
css_path = os.path.abspath(css_path)
# 确保输出目录存在
css_dir = os.path.dirname(css_path)
if css_dir and not os.path.exists(css_dir):
os.makedirs(css_dir)
# 生成CSS
generate_css(generated_files, css_path, script_dir)
elif args.css and not generated_files:
print("\n警告: 没有成功生成WOFF2文件跳过CSS生成")
print()
print("=" * 60)
print("全部完成!")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,9 @@
/* 导入所有CSS文件 */ /* 导入所有CSS文件 */
@import 'normalize.css'; @import 'normalize.css';
@import 'variables.css';
@import "harmony_fonts.css"; @import "harmony_fonts.css";
@import 'scrollbar.css';
@import 'hack_fonts.css'; @import 'hack_fonts.css';
@import 'opensans_fonts.css'; @import 'opensans_fonts.css';
@import "monocraft_fonts.css";
@import 'variables.css';
@import 'scrollbar.css';
@import 'styles.css';

View File

@@ -0,0 +1,202 @@
/* 自动生成的字体文件 */
/* 由 font_compressor.py 生成 */
/* Monocraft 字体家族 */
/* Monocraft ExtraLight Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-ExtraLight-Italic.woff2') format('woff2');
font-weight: 200;
font-style: italic;
font-display: swap;
}
/* Monocraft ExtraLight Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-ExtraLight-Italic.woff2') format('woff2');
font-weight: 200;
font-style: italic;
font-display: swap;
}
/* Monocraft ExtraLight */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
/* Monocraft ExtraLight */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
/* Monocraft Light Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Light-Italic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
}
/* Monocraft Light Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Light-Italic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
}
/* Monocraft Light */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Monocraft Light */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Monocraft Regular Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Monocraft Regular Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Monocraft SemiBold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-SemiBold-Italic.woff2') format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
}
/* Monocraft SemiBold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-SemiBold-Italic.woff2') format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
}
/* Monocraft SemiBold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Monocraft SemiBold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Monocraft Bold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Bold-Italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* Monocraft Bold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Bold-Italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* Monocraft Bold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Monocraft Bold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Monocraft Black Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Black-Italic.woff2') format('woff2');
font-weight: 900;
font-style: italic;
font-display: swap;
}
/* Monocraft Black Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Black-Italic.woff2') format('woff2');
font-weight: 900;
font-style: italic;
font-display: swap;
}
/* Monocraft Black */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* Monocraft Black */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}

View File

@@ -0,0 +1,3 @@
body {
background-color: var(--bg-primary);
}

View File

@@ -1,255 +1,148 @@
:root { :root {
/* 编辑器区域 */ --voidraft-font-mono: "HarmonyOS", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
--text-primary: #9BB586; /* 内容区域字体颜色 */
/* 深色主题颜色变量 */
--dark-toolbar-bg: #2d2d2d;
--dark-toolbar-border: #404040;
--dark-toolbar-text: #ffffff;
--dark-toolbar-text-secondary: #cccccc;
--dark-toolbar-button-hover: #404040;
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--dark-bg-secondary: #0E1217;
--dark-text-secondary: #a0aec0;
--dark-text-muted: #666;
--dark-border-color: #2d3748;
--dark-settings-bg: #2a2a2a;
--dark-settings-card-bg: #333333;
--dark-settings-text: #ffffff;
--dark-settings-text-secondary: #cccccc;
--dark-settings-border: #444444;
--dark-settings-input-bg: #3a3a3a;
--dark-settings-input-border: #555555;
--dark-settings-hover: #404040;
--dark-scrollbar-track: #2a2a2a;
--dark-scrollbar-thumb: #555555;
--dark-scrollbar-thumb-hover: #666666;
--dark-selection-bg: rgba(181, 206, 168, 0.1);
--dark-selection-text: #b5cea8;
--dark-danger-color: #ff6b6b;
--dark-bg-primary: #1a1a1a;
--dark-bg-hover: #2a2a2a;
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
--dark-loading-color: #fff;
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--dark-loading-done-color: #6f6;
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* 浅色主题颜色变量 */
--light-toolbar-bg: #f8f9fa;
--light-toolbar-border: #e9ecef;
--light-toolbar-text: #212529;
--light-toolbar-text-secondary: #495057;
--light-toolbar-button-hover: #e9ecef;
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--light-bg-secondary: #f7fef7;
--light-text-secondary: #374151;
--light-text-muted: #6b7280;
--light-border-color: #e5e7eb;
--light-settings-bg: #ffffff;
--light-settings-card-bg: #f8f9fa;
--light-settings-text: #212529;
--light-settings-text-secondary: #6c757d;
--light-settings-border: #dee2e6;
--light-settings-input-bg: #ffffff;
--light-settings-input-border: #ced4da;
--light-settings-hover: #e9ecef;
--light-scrollbar-track: #f1f3f4;
--light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8;
--light-selection-bg: rgba(59, 130, 246, 0.15);
--light-selection-text: #2563eb;
--light-danger-color: #dc3545;
--light-bg-primary: #ffffff;
--light-bg-hover: #f1f3f4;
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--light-loading-color: #1a3c1a;
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--light-loading-done-color: #008800;
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* 默认使用深色主题 */
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
color-scheme: light dark;
} }
/* 监听系统深色主题 */ /* 默认/暗色主题 */
@media (prefers-color-scheme: dark) { :root,
:root[data-theme="dark"],
:root[data-theme="auto"] { :root[data-theme="auto"] {
--toolbar-bg: var(--dark-toolbar-bg); color-scheme: dark;
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text); --text-primary: #ffffff;
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover); --toolbar-bg: #2d2d2d;
--toolbar-separator: var(--dark-toolbar-button-hover); --toolbar-border: #404040;
--tab-active-line: var(--dark-tab-active-line); --toolbar-text: #ffffff;
--bg-secondary: var(--dark-bg-secondary); --toolbar-text-secondary: #cccccc;
--text-secondary: var(--dark-text-secondary); --toolbar-button-hover: #404040;
--text-muted: var(--dark-text-muted); --toolbar-separator: #404040;
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg); --tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--settings-card-bg: var(--dark-settings-card-bg); --bg-secondary: #0e1217;
--settings-text: var(--dark-settings-text); --bg-primary: #1a1a1a;
--settings-text-secondary: var(--dark-settings-text-secondary); --bg-hover: #2a2a2a;
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg); --text-secondary: #a0aec0;
--settings-input-border: var(--dark-settings-input-border); --text-muted: #666666;
--settings-hover: var(--dark-settings-hover); --text-danger: #ff6b6b;
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb); --border-color: #2d3748;
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg); --settings-bg: #2a2a2a;
--selection-text: var(--dark-selection-text); --settings-card-bg: #333333;
--text-danger: var(--dark-danger-color); --settings-text: #ffffff;
--bg-primary: var(--dark-bg-primary); --settings-text-secondary: #cccccc;
--bg-hover: var(--dark-bg-hover); --settings-border: #444444;
--voidraft-bg-gradient: var(--dark-loading-bg-gradient); --settings-input-bg: #3a3a3a;
--voidraft-loading-color: var(--dark-loading-color); --settings-input-border: #555555;
--voidraft-loading-glow: var(--dark-loading-glow); --settings-hover: #404040;
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay); --scrollbar-track: #2a2a2a;
} --scrollbar-thumb: #555555;
--scrollbar-thumb-hover: #666666;
--selection-bg: rgba(181, 206, 168, 0.1);
--selection-text: #b5cea8;
--voidraft-bg-gradient: radial-gradient(#222922, #000500);
--voidraft-loading-color: #ffffff;
--voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--voidraft-loading-done-color: #66ff66;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
} }
/* 监听系统浅色主题 */ /* 色主题 */
:root[data-theme="light"] {
color-scheme: light;
--text-primary: #000000;
--toolbar-bg: #f8f9fa;
--toolbar-border: #e9ecef;
--toolbar-text: #212529;
--toolbar-text-secondary: #495057;
--toolbar-button-hover: #e9ecef;
--toolbar-separator: #e9ecef;
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--bg-secondary: #f7fef7;
--bg-primary: #ffffff;
--bg-hover: #f1f3f4;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-danger: #dc3545;
--border-color: #e5e7eb;
--settings-bg: #ffffff;
--settings-card-bg: #f8f9fa;
--settings-text: #212529;
--settings-text-secondary: #6c757d;
--settings-border: #dee2e6;
--settings-input-bg: #ffffff;
--settings-input-border: #ced4da;
--settings-hover: #e9ecef;
--scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
}
/* 跟随系统的浅色偏好 */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root[data-theme="auto"] { :root[data-theme="auto"] {
--toolbar-bg: var(--light-toolbar-bg); color-scheme: light;
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text);
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover);
--toolbar-separator: var(--light-toolbar-button-hover);
--tab-active-line: var(--light-tab-active-line);
--bg-secondary: var(--light-bg-secondary);
--text-secondary: var(--light-text-secondary);
--text-muted: var(--light-text-muted);
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg);
--settings-card-bg: var(--light-settings-card-bg);
--settings-text: var(--light-settings-text);
--settings-text-secondary: var(--light-settings-text-secondary);
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg);
--settings-input-border: var(--light-settings-input-border);
--settings-hover: var(--light-settings-hover);
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
}
/* 手动选择浅色主题 */ --text-primary: #000000;
:root[data-theme="light"] {
--toolbar-bg: var(--light-toolbar-bg);
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text);
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover);
--toolbar-separator: var(--light-toolbar-button-hover);
--tab-active-line: var(--light-tab-active-line);
--bg-secondary: var(--light-bg-secondary);
--text-secondary: var(--light-text-secondary);
--text-muted: var(--light-text-muted);
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg);
--settings-card-bg: var(--light-settings-card-bg);
--settings-text: var(--light-settings-text);
--settings-text-secondary: var(--light-settings-text-secondary);
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg);
--settings-input-border: var(--light-settings-input-border);
--settings-hover: var(--light-settings-hover);
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
/* 手动选择深色主题 */ --toolbar-bg: #f8f9fa;
:root[data-theme="dark"] { --toolbar-border: #e9ecef;
--toolbar-bg: var(--dark-toolbar-bg); --toolbar-text: #212529;
--toolbar-border: var(--dark-toolbar-border); --toolbar-text-secondary: #495057;
--toolbar-text: var(--dark-toolbar-text); --toolbar-button-hover: #e9ecef;
--toolbar-text-secondary: var(--dark-toolbar-text-secondary); --toolbar-separator: #e9ecef;
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover); --tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--tab-active-line: var(--dark-tab-active-line); --bg-secondary: #f7fef7;
--bg-secondary: var(--dark-bg-secondary); --bg-primary: #ffffff;
--text-secondary: var(--dark-text-secondary); --bg-hover: #f1f3f4;
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color); --text-secondary: #374151;
--settings-bg: var(--dark-settings-bg); --text-muted: #6b7280;
--settings-card-bg: var(--dark-settings-card-bg); --text-danger: #dc3545;
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary); --border-color: #e5e7eb;
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg); --settings-bg: #ffffff;
--settings-input-border: var(--dark-settings-input-border); --settings-card-bg: #f8f9fa;
--settings-hover: var(--dark-settings-hover); --settings-text: #212529;
--scrollbar-track: var(--dark-scrollbar-track); --settings-text-secondary: #6c757d;
--scrollbar-thumb: var(--dark-scrollbar-thumb); --settings-border: #dee2e6;
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover); --settings-input-bg: #ffffff;
--selection-bg: var(--dark-selection-bg); --settings-input-border: #ced4da;
--selection-text: var(--dark-selection-text); --settings-hover: #e9ecef;
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary); --scrollbar-track: #f1f3f4;
--bg-hover: var(--dark-bg-hover); --scrollbar-thumb: #c1c1c1;
--voidraft-bg-gradient: var(--dark-loading-bg-gradient); --scrollbar-thumb-hover: #a8a8a8;
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow); --selection-bg: rgba(59, 130, 246, 0.15);
--voidraft-loading-done-color: var(--dark-loading-done-color); --selection-text: #2563eb;
--voidraft-loading-overlay: var(--dark-loading-overlay);
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
}
} }

View File

@@ -13,6 +13,10 @@ export const FONT_OPTIONS = [
label: 'Open Sans', label: 'Open Sans',
value: '"Open Sans"' value: '"Open Sans"'
}, },
{
label: 'Monocraft',
value: 'Monocraft'
},
// Common system fonts // Common system fonts
{ {
label: 'Arial', label: 'Arial',

View File

@@ -142,7 +142,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-family: var(--voidraft-mono-font, monospace),serif; font-family: var(--voidraft-font-mono),serif;
} }
.loading-word { .loading-word {

View File

@@ -147,7 +147,7 @@ onUnmounted(() => {
padding: 8px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-primary);
transition: all 0.15s ease; transition: all 0.15s ease;
gap: 8px; gap: 8px;
@@ -165,7 +165,7 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
width: 12px; width: 12px;
height: 12px; height: 12px;
color: var(--text-muted); color: var(--text-primary);
transition: color 0.15s ease; transition: color 0.15s ease;
.menu-item:hover & { .menu-item:hover & {

View File

@@ -161,53 +161,6 @@ export default {
customThemeColors: 'Custom Theme Colors', customThemeColors: 'Custom Theme Colors',
resetToDefault: 'Reset to Default', resetToDefault: 'Reset to Default',
colorValue: 'Color Value', colorValue: 'Color Value',
themeColors: {
basic: 'Basic Colors',
text: 'Text Colors',
syntax: 'Syntax Highlighting',
interface: 'Interface Elements',
border: 'Borders & Dividers',
search: 'Search & Matching',
// Base Colors
background: 'Main Background',
backgroundSecondary: 'Secondary Background',
surface: 'Panel Background',
dropdownBackground: 'Dropdown Background',
dropdownBorder: 'Dropdown Border',
// Text Colors
foreground: 'Primary Text',
foregroundSecondary: 'Secondary Text',
comment: 'Comments',
// Syntax Highlighting - Core
keyword: 'Keywords',
string: 'Strings',
function: 'Functions',
number: 'Numbers',
operator: 'Operators',
variable: 'Variables',
type: 'Types',
// Syntax Highlighting - Extended
constant: 'Constants',
storage: 'Storage Type',
parameter: 'Parameters',
class: 'Class Names',
heading: 'Headings',
invalid: 'Invalid/Error',
regexp: 'Regular Expressions',
// Interface Elements
cursor: 'Cursor',
selection: 'Selection Background',
selectionBlur: 'Unfocused Selection',
activeLine: 'Active Line Highlight',
lineNumber: 'Line Numbers',
activeLineNumber: 'Active Line Number',
// Borders & Dividers
borderColor: 'Border Color',
borderLight: 'Light Border',
// Search & Matching
searchMatch: 'Search Match',
matchingBracket: 'Matching Bracket'
},
lineHeight: 'Line Height', lineHeight: 'Line Height',
tabSettings: 'Tab Settings', tabSettings: 'Tab Settings',
tabSize: 'Tab Size', tabSize: 'Tab Size',

View File

@@ -202,54 +202,6 @@ export default {
customThemeColors: '自定义主题颜色', customThemeColors: '自定义主题颜色',
resetToDefault: '重置为默认', resetToDefault: '重置为默认',
colorValue: '颜色值', colorValue: '颜色值',
themeColors: {
basic: '基础色调',
text: '文本颜色',
syntax: '语法高亮',
interface: '界面元素',
border: '边框分割线',
search: '搜索匹配',
// 基础色调
background: '主背景色',
backgroundSecondary: '次要背景色',
surface: '面板背景',
dropdownBackground: '下拉菜单背景',
dropdownBorder: '下拉菜单边框',
// 文本颜色
foreground: '主文本色',
foregroundSecondary: '次要文本色',
comment: '注释色',
// 语法高亮 - 核心
keyword: '关键字',
string: '字符串',
function: '函数名',
number: '数字',
operator: '操作符',
variable: '变量',
type: '类型',
// 语法高亮 - 扩展
constant: '常量',
storage: '存储类型',
parameter: '参数',
class: '类名',
heading: '标题',
invalid: '无效内容',
regexp: '正则表达式',
// 界面元素
cursor: '光标',
selection: '选中背景',
selectionBlur: '失焦选中背景',
activeLine: '当前行高亮',
lineNumber: '行号',
activeLineNumber: '活动行号',
// 边框和分割线
borderColor: '边框色',
borderLight: '浅色边框',
// 搜索和匹配
searchMatch: '搜索匹配',
matchingBracket: '匹配括号'
},
hotkeyPreview: '预览:', hotkeyPreview: '预览:',
none: '无', none: '无',
backup: { backup: {

View File

@@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import { import {
createDynamicExtensions, createDynamicExtensions,
@@ -29,7 +30,7 @@ import {generateContentHash} from "@/common/utils/hashUtils";
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
import {EDITOR_CONFIG} from '@/common/constant/editor'; import {EDITOR_CONFIG} from '@/common/constant/editor';
import {createHttpClientExtension} from "@/views/editor/extensions/httpclient"; import {createHttpClientExtension} from "@/views/editor/extensions/httpclient";
import {markdownPreviewExtension} from "@/views/editor/extensions/markdownPreview"; import {markdownPreviewExtension, updateMarkdownPreviewTheme} from "@/views/editor/extensions/markdownPreview";
import {createDebounce} from '@/common/utils/debounce'; import {createDebounce} from '@/common/utils/debounce';
export interface DocumentStats { export interface DocumentStats {
@@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => {
fontWeight: configStore.config.editing.fontWeight fontWeight: configStore.config.editing.fontWeight
}); });
const wheelZoomExtension = createWheelZoomExtension(
() => configStore.increaseFontSize(),
() => configStore.decreaseFontSize()
);
// 统计扩展 // 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats); const statsExtension = createStatsUpdateExtension(updateDocumentStats);
@@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => {
themeExtension, themeExtension,
...tabExtensions, ...tabExtensions,
fontExtension, fontExtension,
wheelZoomExtension,
statsExtension, statsExtension,
contentChangeExtension, contentChangeExtension,
codeBlockExtension, codeBlockExtension,
@@ -635,6 +642,13 @@ export const useEditorStore = defineStore('editor', () => {
}); });
}; };
// 应用 Markdown 预览主题
const applyPreviewThemeSettings = () => {
editorCache.values().forEach(instance => {
updateMarkdownPreviewTheme(instance.view);
});
};
// 应用Tab设置 // 应用Tab设置
const applyTabSettings = () => { const applyTabSettings = () => {
editorCache.values().forEach(instance => { editorCache.values().forEach(instance => {
@@ -707,12 +721,15 @@ export const useEditorStore = defineStore('editor', () => {
// 更新前端编辑器扩展 - 应用于所有实例 // 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager(); const manager = getExtensionManager();
if (manager) { if (manager) {
// 使用立即更新模式,跳过防抖 // 直接更新前端扩展至所有视图
manager.updateExtensionImmediate(id, enabled, config || {}); manager.updateExtension(id, enabled, config);
} }
// 重新加载扩展配置 // 重新加载扩展配置
await extensionStore.loadExtensions(); await extensionStore.loadExtensions();
if (manager) {
manager.initExtensions(extensionStore.extensions);
}
await applyKeymapSettings(); await applyKeymapSettings();
}; };
@@ -773,6 +790,7 @@ export const useEditorStore = defineStore('editor', () => {
// 配置更新方法 // 配置更新方法
applyFontSettings, applyFontSettings,
applyThemeSettings, applyThemeSettings,
applyPreviewThemeSettings,
applyTabSettings, applyTabSettings,
applyKeymapSettings, applyKeymapSettings,

View File

@@ -1,191 +1,157 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import {SystemThemeType, ThemeType, Theme, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models'; import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
import { ThemeService } from '@/../bindings/voidraft/internal/services'; import { ThemeService } from '@/../bindings/voidraft/internal/services';
import { useConfigStore } from './configStore'; import { useConfigStore } from './configStore';
import { useEditorStore } from './editorStore'; import { useEditorStore } from './editorStore';
import type { ThemeColors } from '@/views/editor/theme/types'; import type { ThemeColors } from '@/views/editor/theme/types';
import { cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap } from '@/views/editor/theme/presets';
type ThemeOption = { name: string; type: ThemeType };
const resolveThemeName = (name?: string) =>
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
const createThemeOptions = (type: ThemeType): ThemeOption[] =>
themePresetList
.filter(preset => preset.type === type)
.map(preset => ({ name: preset.name, type: preset.type }));
const darkThemeOptions = createThemeOptions(ThemeType.ThemeTypeDark);
const lightThemeOptions = createThemeOptions(ThemeType.ThemeTypeLight);
const cloneColors = (colors: ThemeColorConfig): ThemeColors =>
JSON.parse(JSON.stringify(colors)) as ThemeColors;
const getPresetColors = (name: string): ThemeColors => {
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
const colors = cloneThemeColors(preset.colors);
colors.themeName = name;
return colors;
};
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
const safeName = resolveThemeName(themeName);
try {
const theme = await ThemeService.GetThemeByName(safeName);
if (theme?.colors) {
const colors = cloneColors(theme.colors);
colors.themeName = safeName;
return colors;
}
} catch (error) {
console.error('Failed to load theme override:', error);
}
return getPresetColors(safeName);
};
/**
* 主题管理 Store
* 职责:管理主题状态、颜色配置和预设主题列表
*/
export const useThemeStore = defineStore('theme', () => { export const useThemeStore = defineStore('theme', () => {
const configStore = useConfigStore(); const configStore = useConfigStore();
// 所有主题列表
const allThemes = ref<Theme[]>([]);
// 当前主题的颜色配置
const currentColors = ref<ThemeColors | null>(null); const currentColors = ref<ThemeColors | null>(null);
// 计算属性:当前系统主题模式 const currentTheme = computed(
const currentTheme = computed(() => () => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
); );
// 计算属性:当前是否为深色模式 const isDarkMode = computed(
const isDarkMode = computed(() => () =>
currentTheme.value === SystemThemeType.SystemThemeDark || currentTheme.value === SystemThemeType.SystemThemeDark ||
(currentTheme.value === SystemThemeType.SystemThemeAuto && (currentTheme.value === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches) window.matchMedia('(prefers-color-scheme: dark)').matches)
); );
// 计算属性:根据类型获取主题列表 const availableThemes = computed<ThemeOption[]>(() =>
const darkThemes = computed(() => isDarkMode.value ? darkThemeOptions : lightThemeOptions
allThemes.value.filter(t => t.type === ThemeType.ThemeTypeDark)
); );
const lightThemes = computed(() =>
allThemes.value.filter(t => t.type === ThemeType.ThemeTypeLight)
);
// 计算属性:当前可用的主题列表
const availableThemes = computed(() =>
isDarkMode.value ? darkThemes.value : lightThemes.value
);
// 应用主题到 DOM
const applyThemeToDOM = (theme: SystemThemeType) => { const applyThemeToDOM = (theme: SystemThemeType) => {
const themeMap = { const themeMap = {
[SystemThemeType.SystemThemeAuto]: 'auto', [SystemThemeType.SystemThemeAuto]: 'auto',
[SystemThemeType.SystemThemeDark]: 'dark', [SystemThemeType.SystemThemeDark]: 'dark',
[SystemThemeType.SystemThemeLight]: 'light' [SystemThemeType.SystemThemeLight]: 'light',
}; };
document.documentElement.setAttribute('data-theme', themeMap[theme]); document.documentElement.setAttribute('data-theme', themeMap[theme]);
}; };
// 从数据库加载所有主题 const loadThemeColors = async (themeName?: string) => {
const loadAllThemes = async () => { const targetName = resolveThemeName(
try { themeName || configStore.config?.appearance?.currentTheme
const themes = await ThemeService.GetAllThemes(); );
allThemes.value = (themes || []).filter((t): t is Theme => t !== null); currentColors.value = await fetchThemeColors(targetName);
return allThemes.value;
} catch (error) {
console.error('Failed to load themes from database:', error);
allThemes.value = [];
return [];
}
}; };
// 初始化主题颜色
const initializeThemeColors = async () => {
// 加载所有主题
await loadAllThemes();
// 从配置获取当前主题名称并加载
const currentThemeName = configStore.config?.appearance?.currentTheme || 'default-dark';
const theme = allThemes.value.find(t => t.name === currentThemeName);
if (!theme) {
console.error(`Theme not found: ${currentThemeName}`);
return;
}
// 直接设置当前主题颜色
currentColors.value = theme.colors as ThemeColors;
};
// 初始化主题
const initializeTheme = async () => { const initializeTheme = async () => {
const theme = currentTheme.value; applyThemeToDOM(currentTheme.value);
applyThemeToDOM(theme); await loadThemeColors();
await initializeThemeColors();
}; };
// 设置系统主题模式(深色/浅色/自动)
const setTheme = async (theme: SystemThemeType) => { const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme); await configStore.setSystemTheme(theme);
applyThemeToDOM(theme); applyThemeToDOM(theme);
refreshEditorTheme(); refreshEditorTheme();
}; };
// 切换到指定的预设主题
const switchToTheme = async (themeName: string) => { const switchToTheme = async (themeName: string) => {
const theme = allThemes.value.find(t => t.name === themeName); if (!themePresetMap[themeName]) {
if (!theme) {
console.error('Theme not found:', themeName); console.error('Theme not found:', themeName);
return false; return false;
} }
// 直接设置当前主题颜色 await loadThemeColors(themeName);
currentColors.value = theme.colors as ThemeColors;
// 持久化到配置
await configStore.setCurrentTheme(themeName); await configStore.setCurrentTheme(themeName);
// 刷新编辑器
refreshEditorTheme(); refreshEditorTheme();
return true; return true;
}; };
// 更新当前主题的颜色配置
const updateCurrentColors = (colors: Partial<ThemeColors>) => { const updateCurrentColors = (colors: Partial<ThemeColors>) => {
if (!currentColors.value) return; if (!currentColors.value) return;
Object.assign(currentColors.value, colors); Object.assign(currentColors.value, colors);
}; };
// 保存当前主题颜色到数据库
const saveCurrentTheme = async () => { const saveCurrentTheme = async () => {
if (!currentColors.value) { if (!currentColors.value) {
throw new Error('No theme selected'); throw new Error('No theme selected');
} }
const theme = allThemes.value.find(t => t.name === currentColors.value!.name); const themeName = resolveThemeName(currentColors.value.themeName);
if (!theme) { currentColors.value.themeName = themeName;
throw new Error('Theme not found');
}
await ThemeService.UpdateTheme(theme.id, currentColors.value as ThemeColorConfig); await ThemeService.UpdateTheme(themeName, currentColors.value as unknown as ThemeColorConfig);
await loadThemeColors(themeName);
refreshEditorTheme();
return true; return true;
}; };
// 重置当前主题为预设配置
const resetCurrentTheme = async () => { const resetCurrentTheme = async () => {
if (!currentColors.value) { if (!currentColors.value) {
throw new Error('No theme selected'); throw new Error('No theme selected');
} }
// 调用后端重置 const themeName = resolveThemeName(currentColors.value.themeName);
await ThemeService.ResetTheme(0, currentColors.value.name); await ThemeService.ResetTheme(themeName);
// 重新加载所有主题
await loadAllThemes();
const updatedTheme = allThemes.value.find(t => t.name === currentColors.value!.name);
if (updatedTheme) {
currentColors.value = updatedTheme.colors as ThemeColors;
}
await loadThemeColors(themeName);
refreshEditorTheme(); refreshEditorTheme();
return true; return true;
}; };
// 刷新编辑器主题
const refreshEditorTheme = () => { const refreshEditorTheme = () => {
applyThemeToDOM(currentTheme.value); applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore(); const editorStore = useEditorStore();
editorStore?.applyThemeSettings(); editorStore?.applyThemeSettings();
editorStore?.applyPreviewThemeSettings();
}; };
return { return {
// 状态
allThemes,
darkThemes,
lightThemes,
availableThemes, availableThemes,
currentTheme, currentTheme,
currentColors, currentColors,
isDarkMode, isDarkMode,
// 方法
setTheme, setTheme,
switchToTheme, switchToTheme,
initializeTheme, initializeTheme,
loadAllThemes,
updateCurrentColors, updateCurrentColors,
saveCurrentTheme, saveCurrentTheme,
resetCurrentTheme, resetCurrentTheme,

View File

@@ -3,11 +3,12 @@ import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import { useEditorStore } from '@/stores/editorStore'; import { useEditorStore } from '@/stores/editorStore';
import { useDocumentStore } from '@/stores/documentStore'; import { useDocumentStore } from '@/stores/documentStore';
import { useConfigStore } from '@/stores/configStore'; import { useConfigStore } from '@/stores/configStore';
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
import Toolbar from '@/components/toolbar/Toolbar.vue'; import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useWindowStore} from "@/stores/windowStore"; import { useWindowStore } from '@/stores/windowStore';
import LoadingScreen from '@/components/loading/LoadingScreen.vue'; import LoadingScreen from '@/components/loading/LoadingScreen.vue';
import {useTabStore} from "@/stores/tabStore"; import { useTabStore } from '@/stores/tabStore';
import ContextMenu from './contextMenu/ContextMenu.vue';
import { contextMenuManager } from './contextMenu/manager';
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
@@ -19,47 +20,31 @@ const editorElement = ref<HTMLElement | null>(null);
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation); const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
// 创建滚轮缩放处理器
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
);
onMounted(async () => { onMounted(async () => {
if (!editorElement.value) return; if (!editorElement.value) return;
// 从URL查询参数中获取documentId
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined; const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
// 初始化文档存储优先使用URL参数中的文档ID
await documentStore.initialize(urlDocumentId); await documentStore.initialize(urlDocumentId);
// 设置编辑器容器
editorStore.setEditorContainer(editorElement.value); editorStore.setEditorContainer(editorElement.value);
await tabStore.initializeTab(); await tabStore.initializeTab();
// 添加滚轮事件监听
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 移除滚轮事件监听 contextMenuManager.destroy();
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', wheelHandler);
}
editorStore.clearAllEditors();
}); });
</script> </script>
<template> <template>
<div class="editor-container"> <div class="editor-container">
<div ref="editorElement" class="editor"></div>
<Toolbar/>
<transition name="loading-fade"> <transition name="loading-fade">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT" /> <LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT" />
</transition> </transition>
<div ref="editorElement" class="editor"></div>
<Toolbar />
<ContextMenu :portal-target="editorElement" />
</div> </div>
</template> </template>
@@ -76,6 +61,7 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
position: relative;
} }
} }
@@ -88,7 +74,6 @@ onBeforeUnmount(() => {
overflow: auto; overflow: auto;
} }
// 加载动画过渡效果
.loading-fade-enter-active, .loading-fade-enter-active,
.loading-fade-leave-active { .loading-fade-leave-active {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;

View File

@@ -1,33 +1,47 @@
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view'; import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
import type {Text} from '@codemirror/state';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
/** /**
* 内容变化监听插件 - 集成文档和编辑器管理
*/ */
export function createContentChangePlugin() { export function createContentChangePlugin() {
return ViewPlugin.fromClass( return ViewPlugin.fromClass(
class ContentChangePlugin { class ContentChangePlugin {
private editorStore = useEditorStore(); private readonly editorStore = useEditorStore();
private lastContent = ''; private lastDoc: Text;
private rafId: number | null = null;
private pendingNotification = false;
constructor(private view: EditorView) { constructor(private view: EditorView) {
this.lastContent = view.state.doc.toString(); this.lastDoc = view.state.doc;
} }
update(update: ViewUpdate) { update(update: ViewUpdate) {
if (!update.docChanged) return; if (!update.docChanged || update.state.doc === this.lastDoc) {
return;
const newContent = this.view.state.doc.toString(); }
if (newContent === this.lastContent) return;
this.lastContent = newContent;
this.editorStore.onContentChange();
this.lastDoc = update.state.doc;
this.scheduleNotification();
} }
destroy() { destroy() {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.pendingNotification = false;
}
private scheduleNotification() {
if (this.pendingNotification) return;
this.pendingNotification = true;
this.rafId = requestAnimationFrame(() => {
this.pendingNotification = false;
this.rafId = null;
this.editorStore.onContentChange();
});
} }
} }
); );

View File

@@ -1,22 +1,40 @@
// 处理滚轮缩放字体的事件处理函数 import {EditorView} from '@codemirror/view';
export const createWheelZoomHandler = ( import type {Extension} from '@codemirror/state';
increaseFontSize: () => void,
decreaseFontSize: () => void type FontAdjuster = () => Promise<void> | void;
) => {
return (event: WheelEvent) => { const runAdjuster = (adjuster: FontAdjuster) => {
// 检查是否按住了Ctrl键 try {
if (event.ctrlKey) { const result = adjuster();
// 阻止默认行为(防止页面缩放) if (result && typeof (result as Promise<void>).then === 'function') {
(result as Promise<void>).catch((error) => {
console.error('Failed to adjust font size:', error);
});
}
} catch (error) {
console.error('Failed to adjust font size:', error);
}
};
export const createWheelZoomExtension = (
increaseFontSize: FontAdjuster,
decreaseFontSize: FontAdjuster
): Extension => {
return EditorView.domEventHandlers({
wheel(event) {
if (!event.ctrlKey) {
return false;
}
event.preventDefault(); event.preventDefault();
// 根据滚轮方向增大或减小字体
if (event.deltaY < 0) { if (event.deltaY < 0) {
// 向上滚动,增大字体 runAdjuster(increaseFontSize);
increaseFontSize(); } else if (event.deltaY > 0) {
} else { runAdjuster(decreaseFontSize);
// 向下滚动,减小字体
decreaseFontSize();
} }
return true;
} }
}; });
}; };

View File

@@ -0,0 +1,180 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { contextMenuManager } from './manager';
import type { RenderMenuItem } from './menuSchema';
const props = defineProps<{
portalTarget?: HTMLElement | null;
}>();
const menuState = contextMenuManager.useState();
const menuRef = ref<HTMLDivElement | null>(null);
const adjustedPosition = ref({ x: 0, y: 0 });
const isVisible = computed(() => menuState.value.visible);
const items = computed(() => menuState.value.items);
const position = computed(() => menuState.value.position);
const teleportTarget = computed<HTMLElement | string>(() => props.portalTarget ?? 'body');
watch(
position,
(newPosition) => {
adjustedPosition.value = { ...newPosition };
if (isVisible.value) {
nextTick(adjustMenuWithinViewport);
}
},
{ deep: true }
);
watch(isVisible, (visible) => {
if (visible) {
nextTick(adjustMenuWithinViewport);
}
});
const menuStyle = computed(() => ({
left: `${adjustedPosition.value.x}px`,
top: `${adjustedPosition.value.y}px`
}));
async function adjustMenuWithinViewport() {
await nextTick();
const menuEl = menuRef.value;
if (!menuEl) return;
const rect = menuEl.getBoundingClientRect();
let nextX = adjustedPosition.value.x;
let nextY = adjustedPosition.value.y;
if (rect.right > window.innerWidth) {
nextX = Math.max(0, window.innerWidth - rect.width - 8);
}
if (rect.bottom > window.innerHeight) {
nextY = Math.max(0, window.innerHeight - rect.height - 8);
}
adjustedPosition.value = { x: nextX, y: nextY };
}
function handleItemClick(item: RenderMenuItem) {
if (item.type !== "action" || item.disabled) {
return;
}
contextMenuManager.runCommand(item);
}
function handleOverlayMouseDown() {
contextMenuManager.hide();
}
function stopPropagation(event: MouseEvent) {
event.stopPropagation();
}
</script>
<template>
<Teleport :to="teleportTarget">
<template v-if="isVisible">
<div class="cm-context-overlay" @mousedown="handleOverlayMouseDown" />
<div
ref="menuRef"
class="cm-context-menu show"
:style="menuStyle"
role="menu"
@contextmenu.prevent
@mousedown="stopPropagation"
>
<template v-for="item in items" :key="item.id">
<div v-if="item.type === 'separator'" class="cm-context-menu-divider" />
<div
v-else
class="cm-context-menu-item"
:class="{ 'is-disabled': item.disabled }"
role="menuitem"
:aria-disabled="item.disabled ? 'true' : 'false'"
@click="handleItemClick(item)"
>
<div class="cm-context-menu-item-label">
<span>{{ item.label }}</span>
</div>
<span v-if="item.shortcut" class="cm-context-menu-item-shortcut">
{{ item.shortcut }}
</span>
</div>
</template>
</div>
</template>
</Teleport>
</template>
<style scoped lang="scss">
.cm-context-overlay {
position: absolute;
inset: 0;
z-index: 9000;
background: transparent;
}
.cm-context-menu {
position: fixed;
min-width: 180px;
max-width: 320px;
padding: 4px 0;
border-radius: 3px;
background-color: var(--settings-card-bg, #1c1c1e);
color: var(--settings-text, #f6f6f6);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
z-index: 10000;
opacity: 0;
transform: scale(0.96);
transform-origin: top left;
transition: opacity 0.12s ease, transform 0.12s ease;
}
.cm-context-menu.show {
opacity: 1;
transform: scale(1);
}
.cm-context-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 14px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease;
white-space: nowrap;
}
.cm-context-menu-item:hover {
background-color: var(--toolbar-button-hover);
color: var(--toolbar-text, #ffffff);
}
.cm-context-menu-item.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cm-context-menu-item-label {
display: flex;
align-items: center;
gap: 8px;
}
.cm-context-menu-item-shortcut {
font-size: 12px;
opacity: 0.65;
}
.cm-context-menu-divider {
height: 1px;
margin: 4px 0;
border: none;
background-color: rgba(255, 255, 255, 0.08);
}
</style>

View File

@@ -1,156 +0,0 @@
/**
* 编辑器上下文菜单样式
* 支持系统主题自动适配
*/
.cm-context-menu {
position: fixed;
background-color: var(--settings-card-bg);
color: var(--settings-text);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 0;
/* 优化阴影效果,只在右下角显示自然的阴影 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12);
min-width: 200px;
max-width: 320px;
z-index: 9999;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
opacity: 0;
transform: scale(0.95);
transition: opacity 0.15s ease-out, transform 0.15s ease-out;
overflow: visible; /* 确保子菜单可以显示在外部 */
}
.cm-context-menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
transition: all 0.1s ease;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cm-context-menu-item:hover {
background-color: var(--toolbar-button-hover);
color: var(--toolbar-text);
}
.cm-context-menu-item-label {
display: flex;
align-items: center;
gap: 8px;
}
.cm-context-menu-item-shortcut {
opacity: 0.7;
font-size: 12px;
padding: 2px 4px;
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text-secondary);
margin-left: 16px;
}
.cm-context-menu-item-ripple {
position: absolute;
border-radius: 50%;
background-color: var(--selection-bg);
width: 100px;
height: 100px;
opacity: 0.5;
transform: scale(0);
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
/* 菜单分组标题样式 */
.cm-context-menu-group-title {
padding: 6px 12px;
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
user-select: none;
}
/* 菜单分隔线样式 */
.cm-context-menu-divider {
height: 1px;
background-color: var(--border-color);
margin: 4px 0;
}
/* 子菜单样式 */
.cm-context-submenu-container {
position: relative;
}
.cm-context-menu-item-with-submenu {
position: relative;
}
.cm-context-menu-item-with-submenu::after {
content: "";
position: absolute;
right: 12px;
font-size: 16px;
opacity: 0.7;
}
.cm-context-submenu {
position: fixed; /* 改为fixed定位避免受父元素影响 */
min-width: 180px;
opacity: 0;
pointer-events: none;
transform: translateX(10px);
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 10000;
border-radius: 6px;
background-color: var(--settings-card-bg);
color: var(--settings-text);
border: 1px solid var(--border-color);
padding: 4px 0;
/* 子菜单也使用相同的阴影效果 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.cm-context-menu-item-with-submenu:hover .cm-context-submenu {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
/* 深色主题下的特殊样式 */
:root[data-theme="dark"] .cm-context-menu {
/* 深色主题下阴影更深,但仍然只在右下角 */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
}
:root[data-theme="dark"] .cm-context-submenu {
/* 深色主题下子菜单阴影 */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
}
:root[data-theme="dark"] .cm-context-menu-divider {
background-color: var(--dark-border-color);
opacity: 0.6;
}
/* 动画相关类 */
.cm-context-menu.show {
opacity: 1;
transform: scale(1);
}
.cm-context-menu.hide {
opacity: 0;
}

View File

@@ -1,585 +0,0 @@
/**
* 上下文菜单视图实现
*/
import { EditorView } from "@codemirror/view";
import { MenuItem } from "../contextMenu";
import "./contextMenu.css";
// 为Window对象添加cmSubmenus属性
declare global {
interface Window {
cmSubmenus?: Map<string, HTMLElement>;
}
}
/**
* 菜单项元素池用于复用DOM元素
*/
class MenuItemPool {
private pool: HTMLElement[] = [];
private maxPoolSize = 50; // 最大池大小
/**
* 获取或创建菜单项元素
*/
get(): HTMLElement {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
const menuItem = document.createElement("div");
menuItem.className = "cm-context-menu-item";
return menuItem;
}
/**
* 回收菜单项元素
*/
release(element: HTMLElement): void {
if (this.pool.length < this.maxPoolSize) {
// 清理元素状态
element.className = "cm-context-menu-item";
element.innerHTML = "";
element.style.cssText = "";
// 移除所有事件监听器(通过克隆节点)
const cleanElement = element.cloneNode(false) as HTMLElement;
this.pool.push(cleanElement);
}
}
/**
* 清空池
*/
clear(): void {
this.pool.length = 0;
}
}
/**
* 上下文菜单管理器
*/
class ContextMenuManager {
private static instance: ContextMenuManager;
private menuElement: HTMLElement | null = null;
private submenuPool: Map<string, HTMLElement> = new Map();
private menuItemPool = new MenuItemPool();
private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
private keyDownHandler: ((e: KeyboardEvent) => void) | null = null;
private currentView: EditorView | null = null;
private activeSubmenus: Set<HTMLElement> = new Set();
private ripplePool: HTMLElement[] = [];
// 事件委托处理器
private menuClickHandler: ((e: MouseEvent) => void) | null = null;
private menuMouseHandler: ((e: MouseEvent) => void) | null = null;
private constructor() {
this.initializeEventHandlers();
}
/**
* 获取单例实例
*/
static getInstance(): ContextMenuManager {
if (!ContextMenuManager.instance) {
ContextMenuManager.instance = new ContextMenuManager();
}
return ContextMenuManager.instance;
}
/**
* 初始化事件处理器
*/
private initializeEventHandlers(): void {
// 点击事件委托
this.menuClickHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('.cm-context-menu-item') as HTMLElement;
if (menuItem && menuItem.dataset.command) {
e.preventDefault();
e.stopPropagation();
// 添加点击动画
this.addRippleEffect(menuItem, e);
// 执行命令
const commandName = menuItem.dataset.command;
const command = this.getCommandByName(commandName);
if (command && this.currentView) {
command(this.currentView);
}
// 隐藏菜单
this.hide();
}
};
// 鼠标事件委托
this.menuMouseHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('.cm-context-menu-item') as HTMLElement;
if (!menuItem) return;
if (e.type === 'mouseenter') {
this.handleMenuItemMouseEnter(menuItem);
} else if (e.type === 'mouseleave') {
this.handleMenuItemMouseLeave(menuItem, e);
}
};
// 键盘事件处理器
this.keyDownHandler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
this.hide();
}
};
// 点击外部关闭处理器
this.clickOutsideHandler = (e: MouseEvent) => {
if (this.menuElement && !this.isClickInsideMenu(e.target as Node)) {
this.hide();
}
};
}
/**
* 获取或创建主菜单元素
*/
private getOrCreateMenuElement(): HTMLElement {
if (!this.menuElement) {
this.menuElement = document.createElement("div");
this.menuElement.className = "cm-context-menu";
this.menuElement.style.display = "none";
document.body.appendChild(this.menuElement);
// 阻止菜单内右键点击冒泡
this.menuElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
// 添加事件委托
this.menuElement.addEventListener('click', this.menuClickHandler!);
this.menuElement.addEventListener('mouseenter', this.menuMouseHandler!, true);
this.menuElement.addEventListener('mouseleave', this.menuMouseHandler!, true);
}
return this.menuElement;
}
/**
* 创建或获取子菜单元素
*/
private getOrCreateSubmenu(id: string): HTMLElement {
if (!this.submenuPool.has(id)) {
const submenu = document.createElement("div");
submenu.className = "cm-context-menu cm-context-submenu";
submenu.style.display = "none";
document.body.appendChild(submenu);
this.submenuPool.set(id, submenu);
// 阻止子菜单点击事件冒泡
submenu.addEventListener('click', (e) => {
e.stopPropagation();
});
// 添加事件委托
submenu.addEventListener('click', this.menuClickHandler!);
submenu.addEventListener('mouseenter', this.menuMouseHandler!, true);
submenu.addEventListener('mouseleave', this.menuMouseHandler!, true);
}
return this.submenuPool.get(id)!;
}
/**
* 创建菜单项DOM元素
*/
private createMenuItemElement(item: MenuItem): HTMLElement {
const menuItem = this.menuItemPool.get();
// 如果有子菜单,添加相应类
if (item.submenu && item.submenu.length > 0) {
menuItem.classList.add("cm-context-menu-item-with-submenu");
}
// 创建内容容器
const contentContainer = document.createElement("div");
contentContainer.className = "cm-context-menu-item-label";
// 标签文本
const label = document.createElement("span");
label.textContent = item.label;
contentContainer.appendChild(label);
menuItem.appendChild(contentContainer);
// 快捷键提示(如果有)
if (item.shortcut) {
const shortcut = document.createElement("span");
shortcut.className = "cm-context-menu-item-shortcut";
shortcut.textContent = item.shortcut;
menuItem.appendChild(shortcut);
}
// 存储命令信息用于事件委托
if (item.command) {
menuItem.dataset.command = this.registerCommand(item.command);
}
// 处理子菜单
if (item.submenu && item.submenu.length > 0) {
const submenuId = `submenu-${item.label.replace(/\s+/g, '-').toLowerCase()}`;
menuItem.dataset.submenuId = submenuId;
const submenu = this.getOrCreateSubmenu(submenuId);
this.populateSubmenu(submenu, item.submenu);
// 记录子菜单
if (!window.cmSubmenus) {
window.cmSubmenus = new Map();
}
window.cmSubmenus.set(submenuId, submenu);
}
return menuItem;
}
/**
* 填充子菜单内容
*/
private populateSubmenu(submenu: HTMLElement, items: MenuItem[]): void {
// 清空现有内容
while (submenu.firstChild) {
submenu.removeChild(submenu.firstChild);
}
// 添加子菜单项
items.forEach(item => {
const subMenuItemElement = this.createMenuItemElement(item);
submenu.appendChild(subMenuItemElement);
});
// 初始状态设置为隐藏
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.display = 'block';
}
/**
* 命令注册和管理
*/
private commands: Map<string, (view: EditorView) => void> = new Map();
private commandCounter = 0;
private registerCommand(command: (view: EditorView) => void): string {
const commandId = `cmd_${this.commandCounter++}`;
this.commands.set(commandId, command);
return commandId;
}
private getCommandByName(commandId: string): ((view: EditorView) => void) | undefined {
return this.commands.get(commandId);
}
/**
* 处理菜单项鼠标进入事件
*/
private handleMenuItemMouseEnter(menuItem: HTMLElement): void {
const submenuId = menuItem.dataset.submenuId;
if (!submenuId) return;
const submenu = this.submenuPool.get(submenuId);
if (!submenu) return;
const rect = menuItem.getBoundingClientRect();
// 计算子菜单位置
submenu.style.left = `${rect.right}px`;
submenu.style.top = `${rect.top}px`;
// 检查子菜单是否会超出屏幕
requestAnimationFrame(() => {
const submenuRect = submenu.getBoundingClientRect();
if (submenuRect.right > window.innerWidth) {
submenu.style.left = `${rect.left - submenuRect.width}px`;
}
if (submenuRect.bottom > window.innerHeight) {
const newTop = rect.top - (submenuRect.bottom - window.innerHeight);
submenu.style.top = `${Math.max(0, newTop)}px`;
}
});
// 显示子菜单
submenu.style.opacity = '1';
submenu.style.pointerEvents = 'auto';
submenu.style.visibility = 'visible';
submenu.style.transform = 'translateX(0)';
this.activeSubmenus.add(submenu);
}
/**
* 处理菜单项鼠标离开事件
*/
private handleMenuItemMouseLeave(menuItem: HTMLElement, e: MouseEvent): void {
const submenuId = menuItem.dataset.submenuId;
if (!submenuId) return;
const submenu = this.submenuPool.get(submenuId);
if (!submenu) return;
// 检查是否移动到子菜单上
const toElement = e.relatedTarget as HTMLElement;
if (submenu.contains(toElement)) {
return;
}
this.hideSubmenu(submenu);
}
/**
* 隐藏子菜单
*/
private hideSubmenu(submenu: HTMLElement): void {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.transform = 'translateX(10px)';
setTimeout(() => {
if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
this.activeSubmenus.delete(submenu);
}
/**
* 添加点击波纹效果
*/
private addRippleEffect(menuItem: HTMLElement, e: MouseEvent): void {
let ripple: HTMLElement;
if (this.ripplePool.length > 0) {
ripple = this.ripplePool.pop()!;
} else {
ripple = document.createElement("div");
ripple.className = "cm-context-menu-item-ripple";
}
// 计算相对位置
const rect = menuItem.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
ripple.style.left = (x - 50) + "px";
ripple.style.top = (y - 50) + "px";
ripple.style.transform = "scale(0)";
ripple.style.opacity = "1";
menuItem.appendChild(ripple);
// 执行动画
requestAnimationFrame(() => {
ripple.style.transform = "scale(1)";
ripple.style.opacity = "0";
setTimeout(() => {
if (ripple.parentNode === menuItem) {
menuItem.removeChild(ripple);
this.ripplePool.push(ripple);
}
}, 300);
});
}
/**
* 检查点击是否在菜单内
*/
private isClickInsideMenu(target: Node): boolean {
if (this.menuElement && this.menuElement.contains(target)) {
return true;
}
// 检查是否在子菜单内
for (const submenu of this.activeSubmenus) {
if (submenu.contains(target)) {
return true;
}
}
return false;
}
/**
* 定位菜单元素
*/
private positionMenu(menu: HTMLElement, clientX: number, clientY: number): void {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let left = clientX;
let top = clientY;
requestAnimationFrame(() => {
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
if (left + menuWidth > windowWidth) {
left = windowWidth - menuWidth - 5;
}
if (top + menuHeight > windowHeight) {
top = windowHeight - menuHeight - 5;
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
});
}
/**
* 显示上下文菜单
*/
show(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
this.currentView = view;
// 获取或创建菜单元素
const menu = this.getOrCreateMenuElement();
// 隐藏所有子菜单
this.hideAllSubmenus();
// 清空现有菜单项并回收到池中
while (menu.firstChild) {
const child = menu.firstChild as HTMLElement;
if (child.classList.contains('cm-context-menu-item')) {
this.menuItemPool.release(child);
}
menu.removeChild(child);
}
// 清空命令注册
this.commands.clear();
this.commandCounter = 0;
// 添加主菜单项
items.forEach(item => {
const menuItemElement = this.createMenuItemElement(item);
menu.appendChild(menuItemElement);
});
// 显示菜单
menu.style.display = "block";
// 定位菜单
this.positionMenu(menu, clientX, clientY);
// 添加全局事件监听器
document.addEventListener("click", this.clickOutsideHandler!, true);
document.addEventListener("keydown", this.keyDownHandler!);
// 触发显示动画
requestAnimationFrame(() => {
if (menu) {
menu.classList.add("show");
}
});
}
/**
* 隐藏所有子菜单
*/
private hideAllSubmenus(): void {
this.activeSubmenus.forEach(submenu => {
this.hideSubmenu(submenu);
});
this.activeSubmenus.clear();
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.transform = 'translateX(10px)';
});
}
}
/**
* 隐藏上下文菜单
*/
hide(): void {
// 隐藏所有子菜单
this.hideAllSubmenus();
if (this.menuElement) {
// 添加淡出动画
this.menuElement.classList.remove("show");
this.menuElement.classList.add("hide");
// 等待动画完成后隐藏
setTimeout(() => {
if (this.menuElement) {
this.menuElement.style.display = "none";
this.menuElement.classList.remove("hide");
}
}, 150);
}
// 移除全局事件监听器
if (this.clickOutsideHandler) {
document.removeEventListener("click", this.clickOutsideHandler, true);
}
if (this.keyDownHandler) {
document.removeEventListener("keydown", this.keyDownHandler);
}
this.currentView = null;
}
/**
* 销毁管理器
*/
destroy(): void {
this.hide();
if (this.menuElement) {
document.body.removeChild(this.menuElement);
this.menuElement = null;
}
this.submenuPool.forEach(submenu => {
if (submenu.parentNode) {
document.body.removeChild(submenu);
}
});
this.submenuPool.clear();
this.menuItemPool.clear();
this.commands.clear();
this.activeSubmenus.clear();
this.ripplePool.length = 0;
if (window.cmSubmenus) {
window.cmSubmenus.clear();
}
}
}
// 获取单例实例
const contextMenuManager = ContextMenuManager.getInstance();
/**
* 显示上下文菜单
*/
export function showContextMenu(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
contextMenuManager.show(view, clientX, clientY, items);
}

View File

@@ -1,174 +1,141 @@
/** import { EditorView } from '@codemirror/view';
* 编辑器上下文菜单实现 import { Extension } from '@codemirror/state';
* 提供基本的复制、剪切、粘贴等操作,支持动态快捷键显示 import { copyCommand, cutCommand, pasteCommand } from '../extensions/codeblock/copyPaste';
*/ import { KeyBindingCommand } from '@/../bindings/voidraft/internal/models/models';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { EditorView } from "@codemirror/view"; import { undo, redo } from '@codemirror/commands';
import { Extension } from "@codemirror/state"; import i18n from '@/i18n';
import { copyCommand, cutCommand, pasteCommand } from "../extensions/codeblock/copyPaste"; import { useSystemStore } from '@/stores/systemStore';
import { KeyBindingCommand } from "@/../bindings/voidraft/internal/models/models"; import { showContextMenu } from './manager';
import { useKeybindingStore } from "@/stores/keybindingStore";
import { import {
undo, redo buildRegisteredMenu,
} from "@codemirror/commands"; createMenuContext,
import i18n from "@/i18n"; registerMenuNodes
import {useSystemStore} from "@/stores/systemStore"; } from './menuSchema';
import type { MenuSchemaNode } from './menuSchema';
/**
* 菜单项类型定义
*/
export interface MenuItem {
/** 菜单项显示文本 */
label: string;
/** 点击时执行的命令 (如果有子菜单可以为null) */
command?: (view: EditorView) => boolean;
/** 快捷键提示文本 (可选) */
shortcut?: string;
/** 子菜单项 (可选) */
submenu?: MenuItem[];
}
// 导入相关功能
import { showContextMenu } from "./contextMenuView";
/**
* 获取翻译文本
* @param key 翻译键
* @returns 翻译后的文本
*/
function t(key: string): string { function t(key: string): string {
return i18n.global.t(key); return i18n.global.t(key);
} }
/**
* 获取快捷键显示文本 function formatKeyBinding(keyBinding: string): string {
* @param command 命令ID const systemStore = useSystemStore();
* @returns 快捷键显示文本 const isMac = systemStore.isMacOS;
*/
function getShortcutText(command: KeyBindingCommand): string { return keyBinding
.replace("Mod", isMac ? "Cmd" : "Ctrl")
.replace("Shift", "Shift")
.replace("Alt", isMac ? "Option" : "Alt")
.replace("Ctrl", isMac ? "Ctrl" : "Ctrl")
.replace(/-/g, " + ");
}
const shortcutCache = new Map<KeyBindingCommand, string>();
function getShortcutText(command?: KeyBindingCommand): string {
if (command === undefined) {
return "";
}
const cached = shortcutCache.get(command);
if (cached !== undefined) {
return cached;
}
try { try {
const keybindingStore = useKeybindingStore(); const keybindingStore = useKeybindingStore();
const binding = keybindingStore.keyBindings.find(
// 如果找到该命令的快捷键配置 (kb) => kb.command === command && kb.enabled
const binding = keybindingStore.keyBindings.find(kb =>
kb.command === command && kb.enabled
); );
if (binding && binding.key) { if (binding?.key) {
// 格式化快捷键显示 const formatted = formatKeyBinding(binding.key);
return formatKeyBinding(binding.key); shortcutCache.set(command, formatted);
return formatted;
} }
} catch (error) { } catch (error) {
console.warn("An error occurred while getting the shortcut:", error); console.warn("An error occurred while getting the shortcut:", error);
} }
shortcutCache.set(command, "");
return ""; return "";
} }
/**
* 格式化快捷键显示
* @param keyBinding 快捷键字符串
* @returns 格式化后的显示文本
*/
function formatKeyBinding(keyBinding: string): string {
// 获取系统信息
const systemStore = useSystemStore();
const isMac = systemStore.isMacOS;
// 替换修饰键名称为更友好的显示 function getBuiltinMenuNodes(): MenuSchemaNode[] {
return keyBinding
.replace("Mod", isMac ? "⌘" : "Ctrl")
.replace("Shift", isMac ? "⇧" : "Shift")
.replace("Alt", isMac ? "⌥" : "Alt")
.replace("Ctrl", isMac ? "⌃" : "Ctrl")
.replace(/-/g, " + ");
}
/**
* 创建编辑菜单项
*/
function createEditItems(): MenuItem[] {
return [ return [
{ {
label: t("keybindings.commands.blockCopy"), id: "copy",
labelKey: "keybindings.commands.blockCopy",
command: copyCommand, command: copyCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockCopyCommand) shortcutCommand: KeyBindingCommand.BlockCopyCommand,
enabled: (context) => context.hasSelection
}, },
{ {
label: t("keybindings.commands.blockCut"), id: "cut",
labelKey: "keybindings.commands.blockCut",
command: cutCommand, command: cutCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockCutCommand) shortcutCommand: KeyBindingCommand.BlockCutCommand,
visible: (context) => context.isEditable,
enabled: (context) => context.hasSelection && context.isEditable
}, },
{ {
label: t("keybindings.commands.blockPaste"), id: "paste",
labelKey: "keybindings.commands.blockPaste",
command: pasteCommand, command: pasteCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockPasteCommand) shortcutCommand: KeyBindingCommand.BlockPasteCommand,
} visible: (context) => context.isEditable
];
}
/**
* 创建历史操作菜单项
*/
function createHistoryItems(): MenuItem[] {
return [
{
label: t("keybindings.commands.historyUndo"),
command: undo,
shortcut: getShortcutText(KeyBindingCommand.HistoryUndoCommand)
}, },
{ {
label: t("keybindings.commands.historyRedo"), id: "undo",
labelKey: "keybindings.commands.historyUndo",
command: undo,
shortcutCommand: KeyBindingCommand.HistoryUndoCommand,
visible: (context) => context.isEditable
},
{
id: "redo",
labelKey: "keybindings.commands.historyRedo",
command: redo, command: redo,
shortcut: getShortcutText(KeyBindingCommand.HistoryRedoCommand) shortcutCommand: KeyBindingCommand.HistoryRedoCommand,
visible: (context) => context.isEditable
} }
]; ];
} }
let builtinMenuRegistered = false;
/** function ensureBuiltinMenuRegistered(): void {
* 创建主菜单项 if (builtinMenuRegistered) return;
*/ registerMenuNodes(getBuiltinMenuNodes());
function createMainMenuItems(): MenuItem[] { builtinMenuRegistered = true;
// 基本编辑操作放在主菜单
const basicItems = createEditItems();
// 历史操作放在主菜单
const historyItems = createHistoryItems();
// 构建主菜单
return [
...basicItems,
...historyItems
];
} }
/**
* 创建编辑器上下文菜单
*/
export function createEditorContextMenu(): Extension { export function createEditorContextMenu(): Extension {
// 为编辑器添加右键事件处理 ensureBuiltinMenuRegistered();
return EditorView.domEventHandlers({ return EditorView.domEventHandlers({
contextmenu: (event, view) => { contextmenu: (event, view) => {
// 阻止默认右键菜单
event.preventDefault(); event.preventDefault();
// 获取菜单项 const context = createMenuContext(view, event as MouseEvent);
const menuItems = createMainMenuItems(); const menuItems = buildRegisteredMenu(context, {
translate: t,
formatShortcut: getShortcutText
});
if (menuItems.length === 0) {
return false;
}
// 显示上下文菜单
showContextMenu(view, event.clientX, event.clientY, menuItems); showContextMenu(view, event.clientX, event.clientY, menuItems);
return true; return true;
} }
}); });
} }
/**
* 默认导出
*/
export default createEditorContextMenu; export default createEditorContextMenu;

View File

@@ -0,0 +1,88 @@
import type { EditorView } from '@codemirror/view';
import { readonly, shallowRef, type ShallowRef } from 'vue';
import type { RenderMenuItem } from './menuSchema';
interface MenuPosition {
x: number;
y: number;
}
interface ContextMenuState {
visible: boolean;
position: MenuPosition;
items: RenderMenuItem[];
view: EditorView | null;
}
class ContextMenuManager {
private state: ShallowRef<ContextMenuState> = shallowRef({
visible: false,
position: { x: 0, y: 0 },
items: [] as RenderMenuItem[],
view: null as EditorView | null
});
useState() {
return readonly(this.state);
}
show(view: EditorView, clientX: number, clientY: number, items: RenderMenuItem[]): void {
this.state.value = {
visible: true,
position: { x: clientX, y: clientY },
items,
view
};
}
hide(): void {
if (!this.state.value.visible) {
return;
}
const previousPosition = this.state.value.position;
const view = this.state.value.view;
this.state.value = {
visible: false,
position: previousPosition,
items: [],
view: null
};
if (view) {
view.focus();
}
}
runCommand(item: RenderMenuItem): void {
if (item.type !== "action" || item.disabled) {
return;
}
const { view } = this.state.value;
if (item.command && view) {
item.command(view);
}
this.hide();
}
destroy(): void {
this.state.value = {
visible: false,
position: { x: 0, y: 0 },
items: [],
view: null
};
}
}
export const contextMenuManager = new ContextMenuManager();
export function showContextMenu(
view: EditorView,
clientX: number,
clientY: number,
items: RenderMenuItem[]
): void {
contextMenuManager.show(view, clientX, clientY, items);
}

View File

@@ -0,0 +1,102 @@
import type { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import type { KeyBindingCommand } from '@/../bindings/voidraft/internal/models/models';
export interface MenuContext {
view: EditorView;
event: MouseEvent;
hasSelection: boolean;
selectionText: string;
isEditable: boolean;
}
export type MenuSchemaNode =
| {
id: string;
type?: "action";
labelKey: string;
command?: (view: EditorView) => boolean;
shortcutCommand?: KeyBindingCommand;
visible?: (context: MenuContext) => boolean;
enabled?: (context: MenuContext) => boolean;
}
| {
id: string;
type: "separator";
visible?: (context: MenuContext) => boolean;
};
export interface RenderMenuItem {
id: string;
type: "action" | "separator";
label?: string;
shortcut?: string;
disabled?: boolean;
command?: (view: EditorView) => boolean;
}
interface MenuBuildOptions {
translate: (key: string) => string;
formatShortcut: (command?: KeyBindingCommand) => string;
}
const menuRegistry: MenuSchemaNode[] = [];
export function createMenuContext(view: EditorView, event: MouseEvent): MenuContext {
const { state } = view;
const hasSelection = state.selection.ranges.some((range) => !range.empty);
const selectionText = hasSelection
? state.sliceDoc(state.selection.main.from, state.selection.main.to)
: "";
const isEditable = !state.facet(EditorState.readOnly);
return {
view,
event,
hasSelection,
selectionText,
isEditable
};
}
export function registerMenuNodes(nodes: MenuSchemaNode[]): void {
menuRegistry.push(...nodes);
}
export function buildRegisteredMenu(
context: MenuContext,
options: MenuBuildOptions
): RenderMenuItem[] {
return menuRegistry
.map((node) => convertNode(node, context, options))
.filter((item): item is RenderMenuItem => Boolean(item));
}
function convertNode(
node: MenuSchemaNode,
context: MenuContext,
options: MenuBuildOptions
): RenderMenuItem | null {
if (node.visible && !node.visible(context)) {
return null;
}
if (node.type === "separator") {
return {
id: node.id,
type: "separator"
};
}
const disabled = node.enabled ? !node.enabled(context) : false;
const shortcut = options.formatShortcut(node.shortcutCommand);
return {
id: node.id,
type: "action",
label: options.translate(node.labelKey),
shortcut: shortcut || undefined,
disabled,
command: node.command
};
}

View File

@@ -0,0 +1,56 @@
import { Annotation, Transaction } from "@codemirror/state";
/**
* 统一的 CodeBlock 注解,用于标记内部触发的事务。
*/
export const codeBlockEvent = Annotation.define<string>();
export const LANGUAGE_CHANGE = "codeblock-language-change";
export const ADD_NEW_BLOCK = "codeblock-add-new-block";
export const MOVE_BLOCK = "codeblock-move-block";
export const DELETE_BLOCK = "codeblock-delete-block";
export const CURRENCIES_LOADED = "codeblock-currencies-loaded";
export const CONTENT_EDIT = "codeblock-content-edit";
/**
* 统一管理的 userEvent 常量。
*/
export const USER_EVENTS = {
INPUT: "input",
DELETE: "delete",
MOVE: "move",
SELECT: "select",
DELETE_LINE: "delete.line",
DELETE_CUT: "delete.cut",
INPUT_PASTE: "input.paste",
MOVE_LINE: "move.line",
MOVE_CHARACTER: "move.character",
SELECT_BLOCK_BOUNDARY: "select.block-boundary",
INPUT_REPLACE: "input.replace",
INPUT_REPLACE_ALL: "input.replace.all",
} as const;
/**
* 判断事务列表中是否包含指定注解。
*/
export function transactionsHasAnnotation(
transactions: readonly Transaction[],
annotation: string
) {
return transactions.some(
tr => tr.annotation(codeBlockEvent) === annotation
);
}
/**
* 判断事务列表中是否包含任一注解。
*/
export function transactionsHasAnnotationsAny(
transactions: readonly Transaction[],
annotations: readonly string[]
) {
return transactions.some(tr => {
const value = tr.annotation(codeBlockEvent);
return value ? annotations.includes(value) : false;
});
}

View File

@@ -2,11 +2,12 @@
* Block 命令 * Block 命令
*/ */
import { EditorSelection } from "@codemirror/state"; import { EditorSelection, Transaction } from "@codemirror/state";
import { Command } from "@codemirror/view"; import { Command } from "@codemirror/view";
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./state"; import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./state";
import { Block, EditorOptions, DELIMITER_REGEX } from "./types"; import { Block, EditorOptions, DELIMITER_REGEX } from "./types";
import { formatBlockContent } from "./formatCode"; import { formatBlockContent } from "./formatCode";
import { codeBlockEvent, LANGUAGE_CHANGE, ADD_NEW_BLOCK, MOVE_BLOCK, DELETE_BLOCK, CURRENCIES_LOADED, USER_EVENTS } from "./annotation";
/** /**
* 获取块分隔符 * 获取块分隔符
@@ -32,7 +33,7 @@ export const insertNewBlockAtCursor = (options: EditorOptions): Command => ({ st
dispatch(state.replaceSelection(delimText), { dispatch(state.replaceSelection(delimText), {
scrollIntoView: true, scrollIntoView: true,
userEvent: "input", userEvent: USER_EVENTS.INPUT,
}); });
return true; return true;
@@ -55,9 +56,10 @@ export const addNewBlockBeforeCurrent = (options: EditorOptions): Command => ({
insert: delimText, insert: delimText,
}, },
selection: EditorSelection.cursor(block.delimiter.from + delimText.length), selection: EditorSelection.cursor(block.delimiter.from + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, { }, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "input", userEvent: USER_EVENTS.INPUT,
})); }));
return true; return true;
@@ -79,10 +81,11 @@ export const addNewBlockAfterCurrent = (options: EditorOptions): Command => ({ s
from: block.content.to, from: block.content.to,
insert: delimText, insert: delimText,
}, },
selection: EditorSelection.cursor(block.content.to + delimText.length) selection: EditorSelection.cursor(block.content.to + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, { }, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "input", userEvent: USER_EVENTS.INPUT,
})); }));
return true; return true;
@@ -105,9 +108,10 @@ export const addNewBlockBeforeFirst = (options: EditorOptions): Command => ({ st
insert: delimText, insert: delimText,
}, },
selection: EditorSelection.cursor(delimText.length), selection: EditorSelection.cursor(delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, { }, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "input", userEvent: USER_EVENTS.INPUT,
})); }));
return true; return true;
@@ -129,10 +133,11 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
from: block.content.to, from: block.content.to,
insert: delimText, insert: delimText,
}, },
selection: EditorSelection.cursor(block.content.to + delimText.length) selection: EditorSelection.cursor(block.content.to + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, { }, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "input", userEvent: USER_EVENTS.INPUT,
})); }));
return true; return true;
@@ -144,11 +149,6 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) { export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) {
if (state.readOnly) return false; if (state.readOnly) return false;
const currentDelimiter = state.doc.sliceString(block.delimiter.from, block.delimiter.to);
// 重置正则表达式的 lastIndex
DELIMITER_REGEX.lastIndex = 0;
if (currentDelimiter.match(DELIMITER_REGEX)) {
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`; const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
dispatch({ dispatch({
@@ -157,12 +157,10 @@ export function changeLanguageTo(state: any, dispatch: any, block: Block, langua
to: block.delimiter.to, to: block.delimiter.to,
insert: newDelimiter, insert: newDelimiter,
}, },
annotations: [codeBlockEvent.of(LANGUAGE_CHANGE)],
}); });
return true; return true;
} else {
return false;
}
} }
/** /**
@@ -189,7 +187,7 @@ function updateSel(sel: EditorSelection, by: (range: any) => any): EditorSelecti
} }
function setSel(state: any, selection: EditorSelection) { function setSel(state: any, selection: EditorSelection) {
return state.update({ selection, scrollIntoView: true, userEvent: "select" }); return state.update({ selection, scrollIntoView: true, userEvent: USER_EVENTS.SELECT });
} }
function extendSel(state: any, dispatch: any, how: (range: any) => any) { function extendSel(state: any, dispatch: any, how: (range: any) => any) {
@@ -303,10 +301,11 @@ export const deleteBlock = (_options: EditorOptions): Command => ({ state, dispa
to: block.range.to, to: block.range.to,
insert: "" insert: ""
}, },
selection: EditorSelection.cursor(newCursorPos) selection: EditorSelection.cursor(newCursorPos),
annotations: [codeBlockEvent.of(DELETE_BLOCK)]
}, { }, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "delete" userEvent: USER_EVENTS.DELETE
})); }));
return true; return true;
@@ -366,10 +365,11 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
dispatch(state.update({ dispatch(state.update({
changes, changes,
selection: EditorSelection.cursor(newCursorPos) selection: EditorSelection.cursor(newCursorPos),
annotations: [codeBlockEvent.of(MOVE_BLOCK)]
}, { }, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "move" userEvent: USER_EVENTS.MOVE
})); }));
return true; return true;
@@ -381,3 +381,20 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
export const formatCurrentBlock: Command = (view) => { export const formatCurrentBlock: Command = (view) => {
return formatBlockContent(view); return formatBlockContent(view);
}; };
/**
* 触发一次货币数据刷新,让数学块重新计算
*/
export function triggerCurrenciesLoaded({ state, dispatch }: { state: any; dispatch: any }) {
if (!dispatch || state.readOnly) {
return false;
}
dispatch(state.update({
changes: { from: 0, to: 0, insert: "" },
annotations: [
codeBlockEvent.of(CURRENCIES_LOADED),
Transaction.addToHistory.of(false)
],
}));
return true;
}

View File

@@ -7,6 +7,7 @@ import { EditorState, EditorSelection } from "@codemirror/state";
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { Command } from "@codemirror/view"; import { Command } from "@codemirror/view";
import { LANGUAGES } from "./lang-parser/languages"; import { LANGUAGES } from "./lang-parser/languages";
import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation";
/** /**
* 构建块分隔符正则表达式 * 构建块分隔符正则表达式
@@ -89,7 +90,8 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({
view.dispatch({ view.dispatch({
changes: ranges, changes: ranges,
scrollIntoView: true, scrollIntoView: true,
userEvent: "delete.cut" userEvent: USER_EVENTS.DELETE_CUT,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
}); });
} }
} }
@@ -111,7 +113,8 @@ const copyCut = (view: EditorView, cut: boolean): boolean => {
view.dispatch({ view.dispatch({
changes: ranges, changes: ranges,
scrollIntoView: true, scrollIntoView: true,
userEvent: "delete.cut" userEvent: USER_EVENTS.DELETE_CUT,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
}); });
} }
@@ -142,8 +145,9 @@ function doPaste(view: EditorView, input: string) {
} }
view.dispatch(changes, { view.dispatch(changes, {
userEvent: "input.paste", userEvent: USER_EVENTS.INPUT_PASTE,
scrollIntoView: true scrollIntoView: true,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
}); });
} }

View File

@@ -7,6 +7,7 @@ import { EditorView } from '@codemirror/view';
import { EditorSelection } from '@codemirror/state'; import { EditorSelection } from '@codemirror/state';
import { blockState } from './state'; import { blockState } from './state';
import { Block } from './types'; import { Block } from './types';
import { USER_EVENTS } from './annotation';
/** /**
* 二分查找:找到包含指定位置的块 * 二分查找:找到包含指定位置的块
@@ -136,7 +137,7 @@ export function createCursorProtection() {
view.dispatch({ view.dispatch({
selection: EditorSelection.cursor(adjustedPos), selection: EditorSelection.cursor(adjustedPos),
scrollIntoView: true, scrollIntoView: true,
userEvent: 'select' userEvent: USER_EVENTS.SELECT
}); });
// 阻止默认行为 // 阻止默认行为
@@ -148,4 +149,3 @@ export function createCursorProtection() {
} }
}); });
} }

View File

@@ -3,8 +3,9 @@
*/ */
import { ViewPlugin, EditorView, Decoration, WidgetType, layer, RectangleMarker } from "@codemirror/view"; import { ViewPlugin, EditorView, Decoration, WidgetType, layer, RectangleMarker } from "@codemirror/view";
import { StateField, RangeSetBuilder, EditorState } from "@codemirror/state"; import { StateField, RangeSetBuilder, EditorState, Transaction } from "@codemirror/state";
import { blockState } from "./state"; import { blockState } from "./state";
import { codeBlockEvent, USER_EVENTS } from "./annotation";
/** /**
* 块开始装饰组件 * 块开始装饰组件
@@ -180,10 +181,11 @@ const blockLayer = layer({
*/ */
const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any) => { const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any) => {
const protect: number[] = []; const protect: number[] = [];
const internalEvent = tr.annotation(codeBlockEvent);
// 获取块状态并获取第一个块的分隔符大小 // 获取块状态并获取第一个块的分隔符大小
const blocks = tr.startState.field(blockState); const blocks = tr.startState.field(blockState);
if (blocks && blocks.length > 0) { if (!internalEvent && blocks && blocks.length > 0) {
const firstBlock = blocks[0]; const firstBlock = blocks[0];
const firstBlockDelimiterSize = firstBlock.delimiter.to; const firstBlockDelimiterSize = firstBlock.delimiter.to;
@@ -194,23 +196,27 @@ const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any)
} }
// 如果是搜索替换操作,保护所有块分隔符 // 如果是搜索替换操作,保护所有块分隔符
if (tr.annotations.some((a: any) => a.value === "input.replace" || a.value === "input.replace.all")) { const userEvent = tr.annotation(Transaction.userEvent);
blocks.forEach((block: any) => { if (userEvent && (userEvent === USER_EVENTS.INPUT_REPLACE || userEvent === USER_EVENTS.INPUT_REPLACE_ALL)) {
blocks?.forEach((block: any) => {
if (block.delimiter) { if (block.delimiter) {
protect.push(block.delimiter.from, block.delimiter.to); protect.push(block.delimiter.from, block.delimiter.to);
} }
}); });
} }
// 返回保护范围数组,如果没有需要保护的范围则返回 false // 返回保护范围数组;若无需保护则返回 true 放行事务
return protect.length > 0 ? protect : false; return protect.length > 0 ? protect : true;
}); })
/** /**
* 防止选择在第一个块之前 * 防止选择在第一个块之前
* 使用 transactionFilter 来确保选择不会在第一个块之前 * 使用 transactionFilter 来确保选择不会在第一个块之前
*/ */
const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: any) => { const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: any) => {
if (tr.annotation(codeBlockEvent)) {
return tr;
}
// 获取块状态并获取第一个块的分隔符大小 // 获取块状态并获取第一个块的分隔符大小
const blocks = tr.startState.field(blockState); const blocks = tr.startState.field(blockState);
if (!blocks || blocks.length === 0) { if (!blocks || blocks.length === 0) {

View File

@@ -6,6 +6,8 @@
import { EditorSelection, SelectionRange } from "@codemirror/state"; import { EditorSelection, SelectionRange } from "@codemirror/state";
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { getNoteBlockFromPos } from "./state"; import { getNoteBlockFromPos } from "./state";
import { codeBlockEvent, CONTENT_EDIT } from "./annotation";
import { USER_EVENTS } from "./annotation";
interface LineBlock { interface LineBlock {
from: number; from: number;
@@ -87,7 +89,8 @@ export const deleteLine = (view: EditorView): boolean => {
changes, changes,
selection, selection,
scrollIntoView: true, scrollIntoView: true,
userEvent: "delete.line" userEvent: USER_EVENTS.DELETE_LINE,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
}); });
return true; return true;
@@ -127,7 +130,8 @@ export const deleteLineCommand = ({ state, dispatch }: { state: any; dispatch: a
changes, changes,
selection, selection,
scrollIntoView: true, scrollIntoView: true,
userEvent: "delete.line" userEvent: USER_EVENTS.DELETE_LINE,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
})); }));
return true; return true;

View File

@@ -4,6 +4,7 @@ import * as prettier from "prettier/standalone";
import { getActiveNoteBlock } from "./state"; import { getActiveNoteBlock } from "./state";
import { getLanguage } from "./lang-parser/languages"; import { getLanguage } from "./lang-parser/languages";
import { SupportedLanguage } from "./types"; import { SupportedLanguage } from "./types";
import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation";
export const formatBlockContent = (view) => { export const formatBlockContent = (view) => {
if (!view || view.state.readOnly) if (!view || view.state.readOnly)
@@ -87,7 +88,8 @@ export const formatBlockContent = (view) => {
}, },
selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)), selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
scrollIntoView: true, scrollIntoView: true,
userEvent: "input" userEvent: USER_EVENTS.INPUT,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
}); });
return true; return true;

View File

@@ -6,6 +6,12 @@
import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view"; import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
import { RangeSetBuilder } from "@codemirror/state"; import { RangeSetBuilder } from "@codemirror/state";
import { getNoteBlockFromPos } from "./state"; import { getNoteBlockFromPos } from "./state";
import { transactionsHasAnnotation, CURRENCIES_LOADED } from "./annotation";
type MathParserEntry = {
parser: any;
prev?: any;
};
// 声明全局math对象 // 声明全局math对象
declare global { declare global {
interface Window { interface Window {
@@ -62,8 +68,7 @@ class MathResult extends WidgetType {
/** /**
* 数学装饰函数 * 数学装饰函数
*/ */
function mathDeco(view: any): any { function mathDeco(view: any, parserCache: WeakMap<any, MathParserEntry>): any {
const mathParsers = new WeakMap();
const builder = new RangeSetBuilder(); const builder = new RangeSetBuilder();
for (const { from, to } of view.visibleRanges) { for (const { from, to } of view.visibleRanges) {
@@ -72,12 +77,17 @@ function mathDeco(view: any): any {
const block = getNoteBlockFromPos(view.state, pos); const block = getNoteBlockFromPos(view.state, pos);
if (block && block.language.name === "math") { if (block && block.language.name === "math") {
// get math.js parser and cache it for this block let entry = parserCache.get(block);
let { parser, prev } = mathParsers.get(block) || {}; let parser = entry?.parser;
if (!parser) { if (!parser) {
if (line.from > block.content.from) {
pos = block.content.from;
continue;
}
if (typeof window.math !== 'undefined') { if (typeof window.math !== 'undefined') {
parser = window.math.parser(); parser = window.math.parser();
mathParsers.set(block, { parser, prev }); entry = { parser, prev: undefined };
parserCache.set(block, entry);
} }
} }
@@ -85,10 +95,15 @@ function mathDeco(view: any): any {
let result: any; let result: any;
try { try {
if (parser) { if (parser) {
parser.set("prev", prev); if (entry && line.from === block.content.from && typeof parser.clear === "function") {
parser.clear();
entry.prev = undefined;
}
const prevValue = entry?.prev;
parser.set("prev", prevValue);
result = parser.evaluate(line.text); result = parser.evaluate(line.text);
if (result !== undefined) { if (entry && result !== undefined) {
mathParsers.set(block, { parser, prev: result }); entry.prev = result;
} }
} }
} catch (e) { } catch (e) {
@@ -97,7 +112,7 @@ function mathDeco(view: any): any {
// if we got a result from math.js, add the result decoration // if we got a result from math.js, add the result decoration
if (result !== undefined) { if (result !== undefined) {
const format = parser?.get("format"); const format = parser?.get?.("format");
let resultWidget: MathResult | undefined; let resultWidget: MathResult | undefined;
if (typeof(result) === "string") { if (typeof(result) === "string") {
@@ -142,15 +157,25 @@ function mathDeco(view: any): any {
*/ */
export const mathBlock = ViewPlugin.fromClass(class { export const mathBlock = ViewPlugin.fromClass(class {
decorations: any; decorations: any;
mathParsers: WeakMap<any, MathParserEntry>;
constructor(view: any) { constructor(view: any) {
this.decorations = mathDeco(view); this.mathParsers = new WeakMap();
this.decorations = mathDeco(view, this.mathParsers);
} }
update(update: any) { update(update: any) {
// If the document changed, the viewport changed, update the decorations const hasCurrencyUpdate = transactionsHasAnnotation(update.transactions, CURRENCIES_LOADED);
if (update.docChanged || update.viewportChanged) { if (update.docChanged || hasCurrencyUpdate) {
this.decorations = mathDeco(update.view); // 文档结构或汇率变化时重置解析缓存
this.mathParsers = new WeakMap();
}
if (
update.docChanged ||
update.viewportChanged ||
hasCurrencyUpdate
) {
this.decorations = mathDeco(update.view, this.mathParsers);
} }
} }
}, { }, {

View File

@@ -6,6 +6,8 @@
import { EditorSelection, SelectionRange } from "@codemirror/state"; import { EditorSelection, SelectionRange } from "@codemirror/state";
import { blockState } from "./state"; import { blockState } from "./state";
import { LANGUAGES } from "./lang-parser/languages"; import { LANGUAGES } from "./lang-parser/languages";
import { codeBlockEvent, CONTENT_EDIT } from "./annotation";
import { USER_EVENTS } from "./annotation";
interface LineBlock { interface LineBlock {
from: number; from: number;
@@ -131,7 +133,8 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
changes, changes,
scrollIntoView: true, scrollIntoView: true,
selection: EditorSelection.create(ranges, state.selection.mainIndex), selection: EditorSelection.create(ranges, state.selection.mainIndex),
userEvent: "move.line" userEvent: USER_EVENTS.MOVE_LINE,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
})); }));
return true; return true;

View File

@@ -3,7 +3,8 @@
*/ */
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { syntaxTree, syntaxTreeAvailable } from '@codemirror/language'; import { syntaxTree, ensureSyntaxTree } from '@codemirror/language';
import type { Tree } from '@lezer/common';
import { Block as BlockNode, BlockDelimiter, BlockContent, BlockLanguage } from './lang-parser/parser.terms.js'; import { Block as BlockNode, BlockDelimiter, BlockContent, BlockLanguage } from './lang-parser/parser.terms.js';
import { import {
SupportedLanguage, SupportedLanguage,
@@ -15,51 +16,47 @@ import {
} from './types'; } from './types';
import { LANGUAGES } from './lang-parser/languages'; import { LANGUAGES } from './lang-parser/languages';
const DEFAULT_LANGUAGE = (LANGUAGES[0]?.token || 'text') as string;
/** /**
* 从语法树解析代码块 * 从语法树解析代码块
*/ */
export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null { export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null {
if (!syntaxTreeAvailable(state)) { const tree = syntaxTree(state);
if (!tree) {
return null; return null;
} }
return collectBlocksFromTree(tree, state);
}
const tree = syntaxTree(state); function collectBlocksFromTree(tree: Tree, state: EditorState): Block[] | null {
const blocks: Block[] = []; const blocks: Block[] = [];
const doc = state.doc; const doc = state.doc;
// 遍历语法树中的所有块
tree.iterate({ tree.iterate({
enter(node) { enter(node) {
if (node.type.id === BlockNode) { if (node.type.id === BlockNode) {
// 查找块的分隔符和内容
let delimiter: { from: number; to: number } | null = null; let delimiter: { from: number; to: number } | null = null;
let content: { from: number; to: number } | null = null; let content: { from: number; to: number } | null = null;
let language = 'text'; let language: string = DEFAULT_LANGUAGE;
let auto = false; let auto = false;
// 遍历块的子节点
const blockNode = node.node; const blockNode = node.node;
blockNode.firstChild?.cursor().iterate(child => { blockNode.firstChild?.cursor().iterate(child => {
if (child.type.id === BlockDelimiter) { if (child.type.id === BlockDelimiter) {
delimiter = { from: child.from, to: child.to }; delimiter = { from: child.from, to: child.to };
// 解析整个分隔符文本来获取语言和自动检测标记
const delimiterText = doc.sliceString(child.from, child.to); const delimiterText = doc.sliceString(child.from, child.to);
// 使用正则表达式解析分隔符
const match = delimiterText.match(/∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/); const match = delimiterText.match(/∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/);
if (match) { if (match) {
language = match[1] || 'text'; language = match[1] || DEFAULT_LANGUAGE;
auto = match[2] === '-a'; auto = match[2] === '-a';
} else { } else {
// 回退到逐个解析子节点
child.node.firstChild?.cursor().iterate(langChild => { child.node.firstChild?.cursor().iterate(langChild => {
if (langChild.type.id === BlockLanguage) { if (langChild.type.id === BlockLanguage) {
const langText = doc.sliceString(langChild.from, langChild.to); const langText = doc.sliceString(langChild.from, langChild.to);
language = langText || 'text'; language = langText || DEFAULT_LANGUAGE;
} }
// 检查是否有自动检测标记 if (doc.sliceString(langChild.from, langChild.to) === AUTO_DETECT_SUFFIX) {
if (doc.sliceString(langChild.from, langChild.to) === '-a') {
auto = true; auto = true;
} }
}); });
@@ -88,7 +85,6 @@ export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null {
}); });
if (blocks.length > 0) { if (blocks.length > 0) {
// 设置第一个块分隔符的大小
firstBlockDelimiterSize = blocks[0].delimiter.to; firstBlockDelimiterSize = blocks[0].delimiter.to;
return blocks; return blocks;
} }
@@ -107,183 +103,52 @@ export function getBlocksFromString(state: EditorState): Block[] {
const doc = state.doc; const doc = state.doc;
if (doc.length === 0) { if (doc.length === 0) {
// 如果文档为空,创建一个默认的文本块 return [createPlainTextBlock(0, 0)];
return [{
language: {
name: 'text',
auto: false,
},
content: {
from: 0,
to: 0,
},
delimiter: {
from: 0,
to: 0,
},
range: {
from: 0,
to: 0,
},
}];
} }
const content = doc.sliceString(0, doc.length); const content = doc.sliceString(0, doc.length);
const delim = "\n∞∞∞"; const delimiter = DELIMITER_PREFIX;
let pos = 0; const suffixLength = DELIMITER_SUFFIX.length;
// 检查文档是否以分隔符开始(不带前导换行符) let pos = content.indexOf(delimiter);
if (content.startsWith("∞∞∞")) {
// 文档直接以分隔符开始,调整为标准格式
pos = 0;
} else if (content.startsWith("\n∞∞∞")) {
// 文档以换行符+分隔符开始这是标准格式从位置0开始解析
pos = 0;
} else {
// 如果文档不以分隔符开始,查找第一个分隔符
const firstDelimPos = content.indexOf(delim);
if (firstDelimPos === -1) { if (pos === -1) {
// 如果没有找到分隔符,整个文档作为一个文本块
firstBlockDelimiterSize = 0; firstBlockDelimiterSize = 0;
return [{ return [createPlainTextBlock(0, doc.length)];
language: {
name: 'text',
auto: false,
},
content: {
from: 0,
to: doc.length,
},
delimiter: {
from: 0,
to: 0,
},
range: {
from: 0,
to: doc.length,
},
}];
} }
// 创建第一个块(分隔符之前的内容) if (pos > 0) {
blocks.push(createPlainTextBlock(0, pos));
}
while (pos !== -1 && pos < doc.length) {
const blockStart = pos;
const langStart = blockStart + delimiter.length;
const delimiterEnd = content.indexOf(DELIMITER_SUFFIX, langStart);
if (delimiterEnd === -1) break;
const delimiterText = content.slice(blockStart, delimiterEnd + suffixLength);
const delimiterInfo = parseDelimiter(delimiterText);
if (!delimiterInfo) break;
const contentStart = delimiterEnd + suffixLength;
const nextDelimiter = content.indexOf(delimiter, contentStart);
const contentEnd = nextDelimiter === -1 ? doc.length : nextDelimiter;
blocks.push({ blocks.push({
language: { language: { name: delimiterInfo.language, auto: delimiterInfo.auto },
name: 'text', content: { from: contentStart, to: contentEnd },
auto: false, delimiter: { from: blockStart, to: delimiterEnd + suffixLength },
}, range: { from: blockStart, to: contentEnd },
content: {
from: 0,
to: firstDelimPos,
},
delimiter: {
from: 0,
to: 0,
},
range: {
from: 0,
to: firstDelimPos,
},
}); });
pos = firstDelimPos; pos = nextDelimiter;
firstBlockDelimiterSize = 0;
} }
while (pos < doc.length) {
let blockStart: number;
if (pos === 0 && content.startsWith("∞∞∞")) {
// 处理文档开头直接是分隔符的情况(不带前导换行符)
blockStart = 0;
} else if (pos === 0 && content.startsWith("\n∞∞∞")) {
// 处理文档开头是换行符+分隔符的情况(标准格式)
blockStart = 0;
} else {
blockStart = content.indexOf(delim, pos);
if (blockStart !== pos) {
// 如果在当前位置没有找到分隔符,可能是文档结尾
break;
}
}
// 确定语言开始位置
let langStart: number;
if (pos === 0 && content.startsWith("∞∞∞")) {
// 文档直接以分隔符开始,跳过 ∞∞∞
langStart = blockStart + 3;
} else {
// 标准情况,跳过 \n∞∞∞
langStart = blockStart + delim.length;
}
const delimiterEnd = content.indexOf("\n", langStart);
if (delimiterEnd < 0) {
console.error("Error parsing blocks. Delimiter didn't end with newline");
break;
}
const langFull = content.substring(langStart, delimiterEnd);
let auto = false;
let lang = langFull;
if (langFull.endsWith("-a")) {
auto = true;
lang = langFull.substring(0, langFull.length - 2);
}
const contentFrom = delimiterEnd + 1;
let blockEnd = content.indexOf(delim, contentFrom);
if (blockEnd < 0) {
blockEnd = doc.length;
}
const block: Block = {
language: {
name: lang || 'text',
auto: auto,
},
content: {
from: contentFrom,
to: blockEnd,
},
delimiter: {
from: blockStart,
to: delimiterEnd + 1,
},
range: {
from: blockStart,
to: blockEnd,
},
};
blocks.push(block);
pos = blockEnd;
}
// 如果没有找到任何块,创建一个默认块
if (blocks.length === 0) { if (blocks.length === 0) {
blocks.push({ blocks.push(createPlainTextBlock(0, doc.length));
language: {
name: 'text',
auto: false,
},
content: {
from: 0,
to: doc.length,
},
delimiter: {
from: 0,
to: 0,
},
range: {
from: 0,
to: doc.length,
},
});
firstBlockDelimiterSize = 0; firstBlockDelimiterSize = 0;
} else { } else {
// 设置第一个块分隔符的大小
firstBlockDelimiterSize = blocks[0].delimiter.to; firstBlockDelimiterSize = blocks[0].delimiter.to;
} }
@@ -294,13 +159,19 @@ export function getBlocksFromString(state: EditorState): Block[] {
* 获取文档中的所有块 * 获取文档中的所有块
*/ */
export function getBlocks(state: EditorState): Block[] { export function getBlocks(state: EditorState): Block[] {
// 优先使用语法树解析 let blocks = getBlocksFromSyntaxTree(state);
const syntaxTreeBlocks = getBlocksFromSyntaxTree(state); if (blocks) {
if (syntaxTreeBlocks) { return blocks;
return syntaxTreeBlocks; }
const ensuredTree = ensureSyntaxTree(state, state.doc.length, 200);
if (ensuredTree) {
blocks = collectBlocksFromTree(ensuredTree, state);
if (blocks) {
return blocks;
}
} }
// 如果语法树不可用,回退到字符串解析
return getBlocksFromString(state); return getBlocksFromString(state);
} }
@@ -396,10 +267,21 @@ export function parseDelimiter(delimiterText: string): { language: SupportedLang
const validLanguage = LANGUAGES.some(lang => lang.token === languageName) const validLanguage = LANGUAGES.some(lang => lang.token === languageName)
? languageName as SupportedLanguage ? languageName as SupportedLanguage
: 'text'; : DEFAULT_LANGUAGE as SupportedLanguage;
return { return {
language: validLanguage, language: validLanguage,
auto: isAuto auto: isAuto
}; };
} }
function createPlainTextBlock(from: number, to: number): Block {
return {
language: { name: DEFAULT_LANGUAGE, auto: false },
content: { from, to },
delimiter: { from: 0, to: 0 },
range: { from, to },
};
}

View File

@@ -7,6 +7,7 @@ import { StateField, StateEffect, RangeSetBuilder, EditorSelection, EditorState,
import { selectAll as defaultSelectAll } from "@codemirror/commands"; import { selectAll as defaultSelectAll } from "@codemirror/commands";
import { Command } from "@codemirror/view"; import { Command } from "@codemirror/view";
import { getActiveNoteBlock, blockState } from "./state"; import { getActiveNoteBlock, blockState } from "./state";
import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation";
/** /**
* 当用户按下 Ctrl+A 时,我们希望首先选择整个块。但如果整个块已经被选中, * 当用户按下 Ctrl+A 时,我们希望首先选择整个块。但如果整个块已经被选中,
@@ -115,7 +116,8 @@ export const selectAll: Command = ({ state, dispatch }) => {
// 选择当前块的所有内容 // 选择当前块的所有内容
dispatch(state.update({ dispatch(state.update({
selection: { anchor: block.content.from, head: block.content.to }, selection: { anchor: block.content.from, head: block.content.to },
userEvent: "select" userEvent: USER_EVENTS.SELECT,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
})); }));
return true; return true;
@@ -127,7 +129,7 @@ export const selectAll: Command = ({ state, dispatch }) => {
*/ */
export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) => { export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) => {
// 只处理选择变化的事务,并且忽略我们自己生成的事务 // 只处理选择变化的事务,并且忽略我们自己生成的事务
if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === "select.block-boundary") { if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === USER_EVENTS.SELECT_BLOCK_BOUNDARY) {
return tr; return tr;
} }
@@ -181,7 +183,7 @@ export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) =>
return { return {
...tr, ...tr,
selection: EditorSelection.create(correctedRanges, tr.selection.mainIndex), selection: EditorSelection.create(correctedRanges, tr.selection.mainIndex),
annotations: tr.annotations.concat(Transaction.userEvent.of("select.block-boundary")) annotations: tr.annotations.concat(Transaction.userEvent.of(USER_EVENTS.SELECT_BLOCK_BOUNDARY))
}; };
} }
} catch (error) { } catch (error) {

View File

@@ -5,6 +5,7 @@
import { EditorSelection, findClusterBreak } from "@codemirror/state"; import { EditorSelection, findClusterBreak } from "@codemirror/state";
import { getNoteBlockFromPos } from "./state"; import { getNoteBlockFromPos } from "./state";
import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation";
/** /**
* 交换光标前后的字符 * 交换光标前后的字符
@@ -46,7 +47,8 @@ export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any
dispatch(state.update(changes, { dispatch(state.update(changes, {
scrollIntoView: true, scrollIntoView: true,
userEvent: "move.character" userEvent: USER_EVENTS.MOVE_CHARACTER,
annotations: [codeBlockEvent.of(CONTENT_EDIT)],
})); }));
return true; return true;

View File

@@ -9,9 +9,8 @@ import {
} from '@codemirror/view'; } from '@codemirror/view';
import { Extension, Range } from '@codemirror/state'; import { Extension, Range } from '@codemirror/state';
import * as runtime from "@wailsio/runtime"; import * as runtime from "@wailsio/runtime";
import { getNoteBlockFromPos } from '../codeblock/state';
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`; const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi; const defaultRegexp = /\b(([a-zA-Z][\w+\-.]*):\/\/[^\s/$.?#].[^\s]*)\b/gi;
export interface HyperLinkState { export interface HyperLinkState {
at: number; at: number;
@@ -54,18 +53,8 @@ function hyperLinkDecorations(view: EditorView, anchor?: HyperLinkExtensionOptio
const from = match.index; const from = match.index;
const to = from + match[0].length; const to = from + match[0].length;
// 检查当前位置是否在 HTTP 代码块中
const block = getNoteBlockFromPos(view.state, from);
if (block && block.language.name === 'http') {
// 如果在 HTTP 代码块中,跳过超链接装饰
continue;
}
const linkMark = Decoration.mark({ const linkMark = Decoration.mark({
class: 'cm-hyper-link-text', class: 'cm-hyper-link-text'
attributes: {
'data-url': match[0]
}
}); });
widgets.push(linkMark.range(from, to)); widgets.push(linkMark.range(from, to));
@@ -91,14 +80,7 @@ const linkDecorator = (
) => ) =>
new MatchDecorator({ new MatchDecorator({
regexp: regexp || defaultRegexp, regexp: regexp || defaultRegexp,
decorate: (add, from, to, match, view) => { decorate: (add, from, to, match, _view) => {
// 检查当前位置是否在 HTTP 代码块中
const block = getNoteBlockFromPos(view.state, from);
if (block && block.language.name === 'http') {
// 如果在 HTTP 代码块中,跳过超链接装饰
return;
}
const url = match[0]; const url = match[0];
let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url, match.input, from, to) : url; let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url, match.input, from, to) : url;
if (matchData && matchData[url]) { if (matchData && matchData[url]) {
@@ -109,10 +91,7 @@ const linkDecorator = (
const linkIcon = new HyperLinkIcon({ at: start, url: urlStr, anchor }); const linkIcon = new HyperLinkIcon({ at: start, url: urlStr, anchor });
add(from, to, Decoration.mark({ add(from, to, Decoration.mark({
class: 'cm-hyper-link-text cm-hyper-link-underline', class: 'cm-hyper-link-text cm-hyper-link-underline'
attributes: {
'data-url': urlStr
}
})); }));
add(start, end, Decoration.widget({ widget: linkIcon, side: 1 })); add(start, end, Decoration.widget({ widget: linkIcon, side: 1 }));
}, },
@@ -158,7 +137,7 @@ export function hyperLinkExtension({ regexp, match, handle, anchor, showIcon = t
export const hyperLinkStyle = EditorView.baseTheme({ export const hyperLinkStyle = EditorView.baseTheme({
'.cm-hyper-link-text': { '.cm-hyper-link-text': {
color: '#0969da', color: '#0969da',
cursor: 'pointer', cursor: 'text',
transition: 'color 0.2s ease', transition: 'color 0.2s ease',
textDecoration: 'underline', textDecoration: 'underline',
textDecorationColor: '#0969da', textDecorationColor: '#0969da',
@@ -216,17 +195,12 @@ export const hyperLinkStyle = EditorView.baseTheme({
}); });
export const hyperLinkClickHandler = EditorView.domEventHandlers({ export const hyperLinkClickHandler = EditorView.domEventHandlers({
click: (event, view) => { click: (event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement | null;
let urlElement = target; const iconElement = target?.closest?.('.cm-hyper-link-icon') as (HTMLElement | null);
while (urlElement && !urlElement.hasAttribute('data-url')) { if (iconElement && iconElement.hasAttribute('data-url')) {
urlElement = urlElement.parentElement as HTMLElement; const url = iconElement.getAttribute('data-url');
if (!urlElement || urlElement === document.body) break;
}
if (urlElement && urlElement.hasAttribute('data-url')) {
const url = urlElement.getAttribute('data-url');
if (url) { if (url) {
runtime.Browser.OpenURL(url); runtime.Browser.OpenURL(url);
event.preventDefault(); event.preventDefault();

View File

@@ -2,6 +2,7 @@
* Markdown 预览扩展主入口 * Markdown 预览扩展主入口
*/ */
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { Compartment } from "@codemirror/state";
import { useThemeStore } from "@/stores/themeStore"; import { useThemeStore } from "@/stores/themeStore";
import { usePanelStore } from "@/stores/panelStore"; import { usePanelStore } from "@/stores/panelStore";
import { useDocumentStore } from "@/stores/documentStore"; import { useDocumentStore } from "@/stores/documentStore";
@@ -52,11 +53,30 @@ export function toggleMarkdownPreview(view: EditorView): boolean {
/** /**
* 导出 Markdown 预览扩展 * 导出 Markdown 预览扩展
*/ */
export function markdownPreviewExtension() { const previewThemeCompartment = new Compartment();
const buildPreviewTheme = () => {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const colors = themeStore.currentColors; const colors = themeStore.currentColors;
return colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({});
};
const theme = colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({}); export function markdownPreviewExtension() {
return [
return [previewPanelState, previewPanelPlugin, theme]; previewPanelState,
previewPanelPlugin,
previewThemeCompartment.of(buildPreviewTheme())
];
}
export function updateMarkdownPreviewTheme(view: EditorView): void {
if (!view?.dispatch) return;
try {
view.dispatch({
effects: previewThemeCompartment.reconfigure(buildPreviewTheme())
});
} catch (error) {
console.error("Failed to update markdown preview theme", error);
}
} }

View File

@@ -22,7 +22,7 @@ export class MarkdownPreviewPanel {
private readonly resizeHandle: HTMLDivElement; private readonly resizeHandle: HTMLDivElement;
private readonly content: HTMLDivElement; private readonly content: HTMLDivElement;
private view: EditorView; private view: EditorView;
private themeUnwatch?: () => void; private themeUnwatchers: Array<() => void> = [];
private lastRenderedContent: string = ""; private lastRenderedContent: string = "";
private readonly debouncedUpdate: ReturnType<typeof createDebounce>; private readonly debouncedUpdate: ReturnType<typeof createDebounce>;
private isDestroyed: boolean = false; // 标记面板是否已销毁 private isDestroyed: boolean = false; // 标记面板是否已销毁
@@ -38,11 +38,22 @@ export class MarkdownPreviewPanel {
// 监听主题变化 // 监听主题变化
const themeStore = useThemeStore(); const themeStore = useThemeStore();
this.themeUnwatch = watch(() => themeStore.isDarkMode, (isDark) => { this.themeUnwatchers.push(
watch(() => themeStore.isDarkMode, (isDark) => {
const newTheme = isDark ? "dark" : "default"; const newTheme = isDark ? "dark" : "default";
updateMermaidTheme(newTheme); updateMermaidTheme(newTheme);
this.lastRenderedContent = ""; // 清空缓存,强制重新渲染 this.resetPreviewContent();
}); })
);
this.themeUnwatchers.push(
watch(
() => themeStore.currentColors,
() => {
this.resetPreviewContent();
},
{ deep: true }
)
);
// 创建 DOM 结构 // 创建 DOM 结构
this.dom = document.createElement("div"); this.dom = document.createElement("div");
@@ -315,6 +326,16 @@ export class MarkdownPreviewPanel {
}); });
} }
private resetPreviewContent(): void {
if (this.isDestroyed) {
return;
}
this.md = createMarkdownRenderer();
this.lastRenderedContent = "";
this.updateContentInternal();
}
/** /**
* 响应编辑器更新 * 响应编辑器更新
*/ */
@@ -342,6 +363,11 @@ export class MarkdownPreviewPanel {
// 清空缓存 // 清空缓存
this.lastRenderedContent = ""; this.lastRenderedContent = "";
if (this.themeUnwatchers.length) {
this.themeUnwatchers.forEach(unwatch => unwatch());
this.themeUnwatchers = [];
}
} }
/** /**

View File

@@ -78,6 +78,34 @@ export function createMarkdownPreviewTheme(colors: ThemeColors) {
} }
}, },
// 面板动画效果
'.cm-panels.cm-panels-top': {
borderBottom: '2px solid black'
},
'.cm-panels.cm-panels-bottom': {
animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
},
'@keyframes panelSlideUp': {
from: {
transform: 'translateY(100%)',
opacity: '0'
},
to: {
transform: 'translateY(0)',
opacity: '1'
}
},
'@keyframes panelSlideDown': {
from: {
transform: 'translateY(0)',
opacity: '1'
},
to: {
transform: 'translateY(100%)',
opacity: '0'
}
},
// 内容区域 // 内容区域
".cm-preview-content": { ".cm-preview-content": {
flex: 1, flex: 1,

View File

@@ -1,7 +1,7 @@
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore'; import { useExtensionStore } from '@/stores/extensionStore';
import { KeymapManager } from './keymapManager'; import { Manager } from './manager';
/** /**
* 异步创建快捷键扩展 * 异步创建快捷键扩展
@@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds); return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
}; };
/** /**
@@ -37,10 +37,10 @@ export const updateKeymapExtension = (view: any): void => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds); Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
}; };
// 导出相关模块 // 导出相关模块
export { KeymapManager } from './keymapManager'; export { Manager } from './manager';
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands'; export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'; export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';

View File

@@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands';
* *
* CodeMirror快捷键扩展 * CodeMirror快捷键扩展
*/ */
export class KeymapManager { export class Manager {
private static compartment = new Compartment(); private static compartment = new Compartment();
/** /**

View File

@@ -1,299 +0,0 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types'
import {createDebounce} from '@/common/utils/debounce';
/**
* 扩展管理器
* 负责管理所有动态扩展的注册、启用、禁用和配置更新
* 采用统一配置,多视图同步的设计模式
*/
export class ExtensionManager {
// 统一的扩展状态存储
private extensionStates = new Map<ExtensionID, ExtensionState>();
// 编辑器视图管理
private viewsMap = new Map<number, EditorViewInfo>();
private activeViewId: number | null = null;
// 注册的扩展工厂
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
// 防抖处理
private debouncedUpdateFunctions = new Map<ExtensionID, {
debouncedFn: (enabled: boolean, config: any) => void;
cancel: () => void;
flush: () => void;
}>();
/**
* 注册扩展工厂
* @param id 扩展ID
* @param factory 扩展工厂
*/
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
this.extensionFactories.set(id, factory);
// 创建初始状态
if (!this.extensionStates.has(id)) {
const compartment = new Compartment();
const defaultConfig = factory.getDefaultConfig();
this.extensionStates.set(id, {
id,
factory,
config: defaultConfig,
enabled: false,
compartment,
extension: [] // 默认为空扩展(禁用状态)
});
}
// 为每个扩展创建防抖函数
if (!this.debouncedUpdateFunctions.has(id)) {
const { debouncedFn, cancel, flush } = createDebounce(
(enabled: boolean, config: any) => {
this.updateExtensionImmediate(id, enabled, config);
},
{ delay: 300 }
);
this.debouncedUpdateFunctions.set(id, {
debouncedFn,
cancel,
flush
});
}
}
/**
* 获取所有注册的扩展ID列表
*/
getRegisteredExtensions(): ExtensionID[] {
return Array.from(this.extensionFactories.keys());
}
/**
* 检查扩展是否已注册
* @param id 扩展ID
*/
isExtensionRegistered(id: ExtensionID): boolean {
return this.extensionFactories.has(id);
}
/**
* 从后端配置初始化扩展状态
* @param extensionConfigs 后端扩展配置列表
*/
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const factory = this.extensionFactories.get(config.id);
if (!factory) continue;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config.config)) {
continue;
}
try {
// 创建扩展实例
const extension = config.enabled ? factory.create(config.config) : [];
// 如果状态已存在则更新,否则创建新状态
if (this.extensionStates.has(config.id)) {
const state = this.extensionStates.get(config.id)!;
state.config = config.config;
state.enabled = config.enabled;
state.extension = extension;
} else {
const compartment = new Compartment();
this.extensionStates.set(config.id, {
id: config.id,
factory,
config: config.config,
enabled: config.enabled,
compartment,
extension
});
}
} catch (error) {
console.error(`Failed to initialize extension ${config.id}:`, error);
}
}
}
/**
* 获取初始扩展配置数组(用于创建编辑器)
* @returns CodeMirror扩展数组
*/
getInitialExtensions(): Extension[] {
const extensions: Extension[] = [];
// 为每个注册的扩展添加compartment
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension));
}
return extensions;
}
/**
* 设置编辑器视图
* @param view 编辑器视图实例
* @param documentId 文档ID
*/
setView(view: EditorView, documentId: number): void {
// 保存视图信息
this.viewsMap.set(documentId, {
view,
documentId,
registered: true
});
// 设置当前活动视图
this.activeViewId = documentId;
}
/**
* 获取当前活动视图
*/
private getActiveView(): EditorView | null {
if (this.activeViewId === null) return null;
const viewInfo = this.viewsMap.get(this.activeViewId);
return viewInfo ? viewInfo.view : null;
}
/**
* 更新单个扩展配置并应用到所有视图(带防抖功能)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
const debouncedUpdate = this.debouncedUpdateFunctions.get(id);
if (debouncedUpdate) {
debouncedUpdate.debouncedFn(enabled, config);
} else {
// 如果没有防抖函数,直接执行
this.updateExtensionImmediate(id, enabled, config);
}
}
/**
* 立即更新扩展(无防抖)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
// 获取扩展状态
const state = this.extensionStates.get(id);
if (!state) return;
// 获取工厂
const factory = state.factory;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config)) {
return;
}
try {
// 创建新的扩展实例
const extension = enabled ? factory.create(config) : [];
// 更新内部状态
state.config = config;
state.enabled = enabled;
state.extension = extension;
// 应用到所有视图
this.applyExtensionToAllViews(id);
} catch (error) {
console.error(`Failed to update extension ${id}:`, error);
}
}
/**
* 将指定扩展的当前状态应用到所有视图
* @param id 扩展ID
*/
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
// 遍历所有视图并应用更改
for (const viewInfo of this.viewsMap.values()) {
try {
if (!viewInfo.registered) continue;
viewInfo.view.dispatch({
effects: state.compartment.reconfigure(state.extension)
});
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error);
}
}
}
/**
* 获取扩展当前状态
* @param id 扩展ID
*/
getExtensionState(id: ExtensionID): {
enabled: boolean
config: any
} | null {
const state = this.extensionStates.get(id);
if (!state) return null;
return {
enabled: state.enabled,
config: state.config
};
}
/**
* 重置扩展到默认配置
* @param id 扩展ID
*/
resetExtensionToDefault(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
const defaultConfig = state.factory.getDefaultConfig();
this.updateExtension(id, true, defaultConfig);
}
/**
* 从管理器中移除视图
* @param documentId 文档ID
*/
removeView(documentId: number): void {
if (this.activeViewId === documentId) {
this.activeViewId = null;
}
this.viewsMap.delete(documentId);
}
/**
* 销毁管理器
*/
destroy(): void {
// 清除所有防抖函数
for (const { cancel } of this.debouncedUpdateFunctions.values()) {
cancel();
}
this.debouncedUpdateFunctions.clear();
this.viewsMap.clear();
this.activeViewId = null;
this.extensionFactories.clear();
this.extensionStates.clear();
}
}

View File

@@ -1,301 +1,152 @@
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n'; import i18n from '@/i18n';
import {ExtensionFactory} from './types' import {ExtensionDefinition} from './types';
// 导入现有扩展的创建函数
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'; import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
import {color} from '../extensions/colorSelector'; import {color} from '../extensions/colorSelector';
import {hyperLink} from '../extensions/hyperlink'; import {hyperLink} from '../extensions/hyperlink';
import {minimap} from '../extensions/minimap'; import {minimap} from '../extensions/minimap';
import {vscodeSearch} from '../extensions/vscodeSearch'; import {vscodeSearch} from '../extensions/vscodeSearch';
import {createCheckboxExtension} from '../extensions/checkbox'; import {createCheckboxExtension} from '../extensions/checkbox';
import {createTranslatorExtension} from '../extensions/translator'; import {createTranslatorExtension} from '../extensions/translator';
import {foldingOnIndent} from '../extensions/fold/foldExtension'; import {foldingOnIndent} from '../extensions/fold/foldExtension';
/** type ExtensionEntry = {
* 彩虹括号扩展工厂 definition: ExtensionDefinition
*/ displayNameKey: string
export const rainbowBracketsFactory: ExtensionFactory = { descriptionKey: string
create(_config: any) {
return rainbowBracketsExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
}; };
/** type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
* 文本高亮扩展工厂
*/ const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
export const textHighlightFactory: ExtensionFactory = { create,
create(config: any) { defaultConfig
return createTextHighlighter({
backgroundColor: config.backgroundColor || '#FFD700',
opacity: config.opacity || 0.3
}); });
},
getDefaultConfig() {
return {
backgroundColor: '#FFD700', // 金黄色
opacity: 0.3 // 透明度
};
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1));
}
};
/** const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
* 小地图扩展工厂
*/
export const minimapFactory: ExtensionFactory = {
create(config: any) {
const options = {
displayText: config.displayText || 'characters',
showOverlay: config.showOverlay || 'always',
autohide: config.autohide || false
};
return minimap(options);
},
getDefaultConfig() {
return {
displayText: 'characters',
showOverlay: 'always',
autohide: false
};
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.displayText || typeof config.displayText === 'string') &&
(!config.showOverlay || typeof config.showOverlay === 'string') &&
(!config.autohide || typeof config.autohide === 'boolean');
}
};
/**
* 超链接扩展工厂
*/
export const hyperlinkFactory: ExtensionFactory = {
create(_config: any) {
return hyperLink;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 颜色选择器扩展工厂
*/
export const colorSelectorFactory: ExtensionFactory = {
create(_config: any) {
return color;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 搜索扩展工厂
*/
export const searchFactory: ExtensionFactory = {
create(_config: any) {
return vscodeSearch;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
export const foldFactory: ExtensionFactory = {
create(_config: any) {
return foldingOnIndent;
},
getDefaultConfig(): any {
return {};
},
validateConfig(config: any): boolean {
return typeof config === 'object';
}
};
/**
* 选择框扩展工厂
*/
export const checkboxFactory: ExtensionFactory = {
create(_config: any) {
return createCheckboxExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 翻译扩展工厂
*/
export const translatorFactory: ExtensionFactory = {
create(config: any) {
return createTranslatorExtension({
minSelectionLength: config.minSelectionLength || 2,
maxTranslationLength: config.maxTranslationLength || 5000,
});
},
getDefaultConfig() {
return {
minSelectionLength: 2,
maxTranslationLength: 5000,
};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 所有扩展的统一配置
* 排除$zero值以避免TypeScript类型错误
*/
const EXTENSION_CONFIGS = {
// 编辑增强扩展
[ExtensionID.ExtensionRainbowBrackets]: { [ExtensionID.ExtensionRainbowBrackets]: {
factory: rainbowBracketsFactory, definition: defineExtension(() => rainbowBracketsExtension()),
displayNameKey: 'extensions.rainbowBrackets.name', displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description' descriptionKey: 'extensions.rainbowBrackets.description'
}, },
[ExtensionID.ExtensionHyperlink]: { [ExtensionID.ExtensionHyperlink]: {
factory: hyperlinkFactory, definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name', displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description' descriptionKey: 'extensions.hyperlink.description'
}, },
[ExtensionID.ExtensionColorSelector]: { [ExtensionID.ExtensionColorSelector]: {
factory: colorSelectorFactory, definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name', displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description' descriptionKey: 'extensions.colorSelector.description'
}, },
[ExtensionID.ExtensionTranslator]: { [ExtensionID.ExtensionTranslator]: {
factory: translatorFactory, definition: defineExtension((config: any) => createTranslatorExtension({
minSelectionLength: config?.minSelectionLength ?? 2,
maxTranslationLength: config?.maxTranslationLength ?? 5000
}), {
minSelectionLength: 2,
maxTranslationLength: 5000
}),
displayNameKey: 'extensions.translator.name', displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description' descriptionKey: 'extensions.translator.description'
}, },
// UI增强扩展
[ExtensionID.ExtensionMinimap]: { [ExtensionID.ExtensionMinimap]: {
factory: minimapFactory, definition: defineExtension((config: any) => minimap({
displayText: config?.displayText ?? 'characters',
showOverlay: config?.showOverlay ?? 'always',
autohide: config?.autohide ?? false
}), {
displayText: 'characters',
showOverlay: 'always',
autohide: false
}),
displayNameKey: 'extensions.minimap.name', displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description' descriptionKey: 'extensions.minimap.description'
}, },
// 工具扩展
[ExtensionID.ExtensionSearch]: { [ExtensionID.ExtensionSearch]: {
factory: searchFactory, definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name', displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description' descriptionKey: 'extensions.search.description'
}, },
[ExtensionID.ExtensionFold]: { [ExtensionID.ExtensionFold]: {
factory: foldFactory, definition: defineExtension(() => foldingOnIndent),
displayNameKey: 'extensions.fold.name', displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description' descriptionKey: 'extensions.fold.description'
}, },
[ExtensionID.ExtensionTextHighlight]: { [ExtensionID.ExtensionTextHighlight]: {
factory: textHighlightFactory, definition: defineExtension((config: any) => createTextHighlighter({
backgroundColor: config?.backgroundColor ?? '#FFD700',
opacity: config?.opacity ?? 0.3
}), {
backgroundColor: '#FFD700',
opacity: 0.3
}),
displayNameKey: 'extensions.textHighlight.name', displayNameKey: 'extensions.textHighlight.name',
descriptionKey: 'extensions.textHighlight.description' descriptionKey: 'extensions.textHighlight.description'
}, },
[ExtensionID.ExtensionCheckbox]: { [ExtensionID.ExtensionCheckbox]: {
factory: checkboxFactory, definition: defineExtension(() => createCheckboxExtension()),
displayNameKey: 'extensions.checkbox.name', displayNameKey: 'extensions.checkbox.name',
descriptionKey: 'extensions.checkbox.description' descriptionKey: 'extensions.checkbox.description'
} }
} as const;
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
if (!isRegisteredExtension(id)) {
return undefined;
}
return EXTENSION_REGISTRY[id];
}; };
/** export function registerAllExtensions(manager: Manager): void {
* 注册所有扩展工厂到管理器 (Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
* @param manager 扩展管理器实例 manager.registerExtension(id, entry.definition);
*/
export function registerAllExtensions(manager: ExtensionManager): void {
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
manager.registerExtension(id as ExtensionID, config.factory);
}); });
} }
/**
* 获取扩展工厂的显示名称
* @param id 扩展ID
* @returns 显示名称
*/
export function getExtensionDisplayName(id: ExtensionID): string { export function getExtensionDisplayName(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id; return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
} }
/**
* 获取扩展工厂的描述
* @param id 扩展ID
* @returns 描述
*/
export function getExtensionDescription(id: ExtensionID): string { export function getExtensionDescription(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''; return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
} }
/** function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
* 获取扩展工厂实例 return getRegistryEntry(id)?.definition;
* @param id 扩展ID
* @returns 扩展工厂实例
*/
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
} }
/**
* 获取扩展的默认配置
* @param id 扩展ID
* @returns 默认配置对象
*/
export function getExtensionDefaultConfig(id: ExtensionID): any { export function getExtensionDefaultConfig(id: ExtensionID): any {
const factory = getExtensionFactory(id); const definition = getExtensionDefinition(id);
return factory?.getDefaultConfig() || {}; if (!definition) return {};
return cloneConfig(definition.defaultConfig);
} }
/**
* 检查扩展是否有配置项
* @param id 扩展ID
* @returns 是否有配置项
*/
export function hasExtensionConfig(id: ExtensionID): boolean { export function hasExtensionConfig(id: ExtensionID): boolean {
const defaultConfig = getExtensionDefaultConfig(id); return Object.keys(getExtensionDefaultConfig(id)).length > 0;
return Object.keys(defaultConfig).length > 0;
} }
/**
* 获取所有可用扩展的ID列表
* @returns 扩展ID数组
*/
export function getAllExtensionIds(): ExtensionID[] { export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]; return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
} }
const cloneConfig = (config: any) => {
if (Array.isArray(config)) {
return config.map(cloneConfig);
}
if (config && typeof config === 'object') {
return Object.keys(config).reduce((acc, key) => {
acc[key] = cloneConfig(config[key]);
return acc;
}, {} as Record<string, any>);
}
return config;
};

View File

@@ -1,13 +1,13 @@
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {useExtensionStore} from '@/stores/extensionStore'; import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {registerAllExtensions} from './extensions'; import {registerAllExtensions} from './extensions';
/** /**
* 全局扩展管理器实例 * 全局扩展管理器实例
*/ */
const extensionManager = new ExtensionManager(); const extensionManager = new Manager();
/** /**
* 异步创建动态扩展 * 异步创建动态扩展
@@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
} }
// 初始化扩展管理器配置 // 初始化扩展管理器配置
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions); extensionManager.initExtensions(extensionStore.extensions);
// 获取初始扩展配置 // 获取初始扩展配置
return extensionManager.getInitialExtensions(); return extensionManager.getInitialExtensions();
@@ -36,7 +36,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
* 获取扩展管理器实例 * 获取扩展管理器实例
* @returns 扩展管理器 * @returns 扩展管理器
*/ */
export const getExtensionManager = (): ExtensionManager => { export const getExtensionManager = (): Manager => {
return extensionManager; return extensionManager;
}; };
@@ -58,5 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => {
}; };
// 导出相关模块 // 导出相关模块
export {ExtensionManager} from './extensionManager'; export {Manager} from './manager';
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions'; export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';

View File

@@ -0,0 +1,135 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionDefinition, ExtensionState} from './types';
/**
* 扩展管理器
* 负责注册、初始化与同步所有动态扩展
*/
export class Manager {
private extensionStates = new Map<ExtensionID, ExtensionState>();
private views = new Map<number, EditorView>();
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(id);
if (existingState) {
existingState.definition = definition;
if (existingState.config === undefined) {
existingState.config = this.cloneConfig(definition.defaultConfig ?? {});
}
} else {
const compartment = new Compartment();
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
this.extensionStates.set(id, {
id,
definition,
config: defaultConfig,
enabled: false,
compartment,
extension: []
});
}
}
initExtensions(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const state = this.extensionStates.get(config.id);
if (!state) continue;
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
this.commitExtensionState(state, config.enabled, resolvedConfig);
}
}
getInitialExtensions(): Extension[] {
const extensions: Extension[] = [];
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension));
}
return extensions;
}
setView(view: EditorView, documentId: number): void {
this.views.set(documentId, view);
this.applyAllExtensionsToView(view);
}
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
const state = this.extensionStates.get(id);
if (!state) return;
const resolvedConfig = this.resolveConfig(state, config);
this.commitExtensionState(state, enabled, resolvedConfig);
}
removeView(documentId: number): void {
this.views.delete(documentId);
}
destroy(): void {
this.views.clear();
this.extensionStates.clear();
}
private resolveConfig(state: ExtensionState, config?: any): any {
if (config !== undefined) {
return this.cloneConfig(config);
}
if (state.config !== undefined) {
return this.cloneConfig(state.config);
}
return this.cloneConfig(state.definition.defaultConfig ?? {});
}
private commitExtensionState(state: ExtensionState, enabled: boolean, config: any): void {
try {
const runtimeExtension = enabled ? state.definition.create(config) : [];
state.enabled = enabled;
state.config = config;
state.extension = runtimeExtension;
this.applyExtensionToAllViews(state.id);
} catch (error) {
console.error(`Failed to update extension ${state.id}:`, error);
}
}
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
for (const [documentId, view] of this.views.entries()) {
try {
view.dispatch({effects: state.compartment.reconfigure(state.extension)});
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${documentId}:`, error);
}
}
}
private applyAllExtensionsToView(view: EditorView): void {
const effects: any[] = [];
for (const state of this.extensionStates.values()) {
effects.push(state.compartment.reconfigure(state.extension));
}
if (effects.length === 0) return;
try {
view.dispatch({effects});
} catch (error) {
console.error('Failed to register extensions on view:', error);
}
}
private cloneConfig<T>(config: T): T {
if (Array.isArray(config)) {
return config.map(item => this.cloneConfig(item)) as unknown as T;
}
if (config && typeof config === 'object') {
return Object.keys(config as Record<string, any>).reduce((acc, key) => {
(acc as any)[key] = this.cloneConfig((config as Record<string, any>)[key]);
return acc;
}, {} as Record<string, any>) as T;
}
return config;
}
}

View File

@@ -1,49 +1 @@
import {Compartment, Extension} from '@codemirror/state'; import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
/**
* 扩展工厂接口
* 每个扩展需要实现此接口来创建和配置扩展
*/
export interface ExtensionFactory {
/**
* 创建扩展实例
* @param config 扩展配置
* @returns CodeMirror扩展
*/
create(config: any): Extension
/**
* 获取默认配置
* @returns 默认配置对象
*/
getDefaultConfig(): any
/**
* 验证配置
* @param config 配置对象
* @returns 是否有效
*/
validateConfig?(config: any): boolean
}
/**
* 扩展状态
*/
export interface ExtensionState {
id: ExtensionID
factory: ExtensionFactory
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}
/**
* 视图信息
*/
export interface EditorViewInfo {
view: EditorView
documentId: number
registered: boolean
}

View File

@@ -4,6 +4,8 @@ import {tags} from '@lezer/highlight';
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import type {ThemeColors} from './types'; import type {ThemeColors} from './types';
const MONO_FONT_FALLBACK = 'var(--voidraft-font-mono, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace)';
/** /**
* 创建通用主题 * 创建通用主题
* @param colors 主题颜色配置 * @param colors 主题颜色配置
@@ -12,28 +14,15 @@ import type {ThemeColors} from './types';
export function createBaseTheme(colors: ThemeColors): Extension { export function createBaseTheme(colors: ThemeColors): Extension {
// 编辑器主题样式 // 编辑器主题样式
const theme = EditorView.theme({ const theme = EditorView.theme({
'&': { '&': {
color: colors.foreground,
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
// 确保编辑器容器背景一致
'.cm-editor': { '.cm-editor': {
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
transition: 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标 // 光标
'.cm-cursor, .cm-dropCursor': { '.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor, borderLeftColor: colors.cursor,
@@ -42,19 +31,11 @@ export function createBaseTheme(colors: ThemeColors): Extension {
marginTop: '-2px', marginTop: '-2px',
}, },
// 选择 // 选择背景
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection, backgroundColor: colors.selection,
}, },
'.cm-content ::selection': {
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
},
// 当前行高亮 // 当前行高亮
'.cm-activeLine': { '.cm-activeLine': {
@@ -66,7 +47,6 @@ export function createBaseTheme(colors: ThemeColors): Extension {
backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)', backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)',
color: colors.lineNumber, color: colors.lineNumber,
border: 'none', border: 'none',
borderRight: colors.dark ? 'none' : `1px solid ${colors.borderLight}`,
padding: '0 2px 0 4px', padding: '0 2px 0 4px',
userSelect: 'none', userSelect: 'none',
}, },
@@ -75,105 +55,11 @@ export function createBaseTheme(colors: ThemeColors): Extension {
color: colors.activeLineNumber, color: colors.activeLineNumber,
}, },
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: colors.comment,
},
// 面板
'.cm-panels': {
// backgroundColor: colors.dropdownBackground,
// color: colors.foreground
},
'.cm-panels.cm-panels-top': {
borderBottom: '2px solid black'
},
'.cm-panels.cm-panels-bottom': {
animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
},
'@keyframes panelSlideUp': {
from: {
transform: 'translateY(100%)',
opacity: '0'
},
to: {
transform: 'translateY(0)',
opacity: '1'
}
},
'@keyframes panelSlideDown': {
from: {
transform: 'translateY(0)',
opacity: '1'
},
to: {
transform: 'translateY(100%)',
opacity: '0'
}
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.searchMatch,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: colors.dark ? '#50606D' : '#e6f3ff',
},
// 括号匹配 // 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`, outline: `0.5px solid ${colors.matchingBracket}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: colors.dark ? '0.5px solid #bc8f8f' : '0.5px solid #d73a49',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: colors.dark ? 'none' : `1px solid ${colors.dropdownBorder}`,
backgroundColor: colors.surface,
color: colors.foreground,
boxShadow: colors.dark ? 'none' : '0 2px 8px rgba(0,0,0,0.1)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
}, },
// 代码块层(自定义) // 代码块层(自定义)
@@ -195,8 +81,19 @@ export function createBaseTheme(colors: ThemeColors): Extension {
background: colors.backgroundSecondary, background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.borderColor}`, borderTop: `1px solid ${colors.borderColor}`,
}, },
'.code-block-empty-selected': {
backgroundColor: colors.selection,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
position: 'relative',
},
'.code-block-start.first': {
height: '0px',
},
// 数学计算结果(自定义) // 数学计算结果
'.code-blocks-math-result': { '.code-blocks-math-result': {
paddingLeft: "12px", paddingLeft: "12px",
position: "relative", position: "relative",
@@ -223,91 +120,116 @@ export function createBaseTheme(colors: ThemeColors): Extension {
'.code-blocks-math-result-copied.fade-out': { '.code-blocks-math-result-copied.fade-out': {
opacity: 0, opacity: 0,
}, },
// 代码块开始标记(自定义)
'.code-block-start': {
height: '12px',
position: 'relative',
},
'.code-block-start.first': {
height: '0px',
},
}, {dark: colors.dark}); }, {dark: colors.dark});
// 语法高亮样式
const highlightStyle = HighlightStyle.define([ const highlightStyle = HighlightStyle.define([
// 关键字 {tag: tags.comment, color: colors.comment, fontStyle: 'italic'},
{tag: tags.lineComment, color: colors.lineComment, fontStyle: 'italic'},
{tag: tags.blockComment, color: colors.blockComment, fontStyle: 'italic'},
{tag: tags.docComment, color: colors.docComment, fontStyle: 'italic'},
{tag: tags.name, color: colors.name},
{tag: tags.variableName, color: colors.variableName},
{tag: tags.typeName, color: colors.typeName},
{tag: tags.tagName, color: colors.tagName},
{tag: tags.propertyName, color: colors.propertyName},
{tag: tags.attributeName, color: colors.attributeName},
{tag: tags.className, color: colors.className},
{tag: tags.labelName, color: colors.labelName},
{tag: tags.namespace, color: colors.namespace},
{tag: tags.macroName, color: colors.macroName},
{tag: tags.literal, color: colors.literal},
{tag: tags.string, color: colors.string},
{tag: tags.docString, color: colors.docString},
{tag: tags.character, color: colors.character},
{tag: tags.attributeValue, color: colors.attributeValue},
{tag: tags.number, color: colors.number},
{tag: tags.integer, color: colors.integer},
{tag: tags.float, color: colors.float},
{tag: tags.bool, color: colors.bool},
{tag: tags.regexp, color: colors.regexp},
{tag: tags.escape, color: colors.escape},
{tag: tags.color, color: colors.color},
{tag: tags.url, color: colors.url},
{tag: tags.keyword, color: colors.keyword}, {tag: tags.keyword, color: colors.keyword},
{tag: tags.self, color: colors.self},
{tag: tags.null, color: colors.null},
{tag: tags.atom, color: colors.atom},
{tag: tags.unit, color: colors.unit},
{tag: tags.modifier, color: colors.modifier},
{tag: tags.operatorKeyword, color: colors.operatorKeyword},
{tag: tags.controlKeyword, color: colors.controlKeyword},
{tag: tags.definitionKeyword, color: colors.definitionKeyword},
{tag: tags.moduleKeyword, color: colors.moduleKeyword},
// 操作符 {tag: tags.operator, color: colors.operator},
{tag: [tags.operator, tags.operatorKeyword], color: colors.operator}, {tag: tags.derefOperator, color: colors.derefOperator},
{tag: tags.arithmeticOperator, color: colors.arithmeticOperator},
{tag: tags.logicOperator, color: colors.logicOperator},
{tag: tags.bitwiseOperator, color: colors.bitwiseOperator},
{tag: tags.compareOperator, color: colors.compareOperator},
{tag: tags.updateOperator, color: colors.updateOperator},
{tag: tags.definitionOperator, color: colors.definitionOperator},
{tag: tags.typeOperator, color: colors.typeOperator},
{tag: tags.controlOperator, color: colors.controlOperator},
// 名称、变量 {tag: tags.punctuation, color: colors.punctuation},
{tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: colors.variable}, {tag: tags.separator, color: colors.separator},
{tag: [tags.variableName], color: colors.variable}, {tag: tags.bracket, color: colors.bracket},
{tag: [tags.labelName], color: colors.operator}, {tag: tags.angleBracket, color: colors.angleBracket},
{tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: colors.variable}, {tag: tags.squareBracket, color: colors.squareBracket},
{tag: tags.paren, color: colors.paren},
{tag: tags.brace, color: colors.brace},
// 函数 {tag: tags.content, color: colors.content},
{tag: [tags.function(tags.variableName)], color: colors.function}, {tag: tags.heading, color: colors.heading, fontWeight: 'bold'},
{tag: [tags.propertyName], color: colors.function}, {tag: tags.heading1, color: colors.heading1, fontWeight: 'bold', fontSize: '1.4em'},
{tag: tags.heading2, color: colors.heading2, fontWeight: 'bold', fontSize: '1.3em'},
{tag: tags.heading3, color: colors.heading3, fontWeight: 'bold', fontSize: '1.2em'},
{tag: tags.heading4, color: colors.heading4, fontWeight: 'bold', fontSize: '1.1em'},
{tag: tags.heading5, color: colors.heading5, fontWeight: 'bold'},
{tag: tags.heading6, color: colors.heading6, fontWeight: 'bold'},
{tag: tags.contentSeparator, color: colors.contentSeparator},
{tag: tags.list, color: colors.list},
{tag: tags.quote, color: colors.quote, fontStyle: 'italic'},
{tag: tags.emphasis, color: colors.emphasis, fontStyle: 'italic'},
{tag: tags.strong, color: colors.strong, fontWeight: 'bold'},
{tag: tags.link, color: colors.link, textDecoration: 'underline'},
{tag: tags.monospace, color: colors.monospace, fontFamily: MONO_FONT_FALLBACK},
{tag: tags.strikethrough, color: colors.strikethrough, textDecoration: 'line-through'},
// 类型、类 {tag: tags.inserted, color: colors.inserted},
{tag: [tags.typeName], color: colors.type}, {tag: tags.deleted, color: colors.deleted},
{tag: [tags.className], color: colors.class}, {tag: tags.changed, color: colors.changed},
// 常量 {tag: tags.meta, color: colors.meta, fontStyle: 'italic'},
{tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.constant}, {tag: tags.documentMeta, color: colors.documentMeta},
{tag: tags.annotation, color: colors.annotation},
{tag: tags.processingInstruction, color: colors.processingInstruction},
// 字符串 {tag: tags.definition(tags.variableName), color: colors.definition},
{tag: [tags.processingInstruction, tags.string, tags.inserted], color: colors.string}, {tag: tags.definition(tags.propertyName), color: colors.definition},
{tag: [tags.special(tags.string)], color: colors.string}, {tag: tags.definition(tags.name), color: colors.definition},
{tag: [tags.quote], color: colors.comment}, {tag: tags.constant(tags.variableName), color: colors.constant},
{tag: tags.constant(tags.propertyName), color: colors.constant},
{tag: tags.constant(tags.name), color: colors.constant},
{tag: tags.function(tags.variableName), color: colors.function},
{tag: tags.function(tags.propertyName), color: colors.function},
{tag: tags.function(tags.name), color: colors.function},
{tag: tags.standard(tags.variableName), color: colors.standard},
{tag: tags.standard(tags.name), color: colors.standard},
{tag: tags.local(tags.variableName), color: colors.local},
{tag: tags.local(tags.name), color: colors.local},
{tag: tags.special(tags.variableName), color: colors.special},
{tag: tags.special(tags.name), color: colors.special},
{tag: tags.special(tags.string), color: colors.special},
// 数字
{
tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace],
color: colors.number
},
// 正则表达式
{tag: [tags.url, tags.escape, tags.regexp, tags.link], color: colors.regexp},
// 注释
{tag: [tags.meta, tags.comment], color: colors.comment, fontStyle: 'italic'},
// 分隔符、括号
{tag: [tags.definition(tags.name), tags.separator], color: colors.variable},
{tag: [tags.brace], color: colors.variable},
{tag: [tags.squareBracket], color: colors.dark ? '#bf616a' : colors.keyword},
{tag: [tags.angleBracket], color: colors.dark ? '#d08770' : colors.operator},
{tag: [tags.attributeName], color: colors.variable},
// 标签
{tag: [tags.tagName], color: colors.number},
// 注解
{tag: [tags.annotation], color: colors.invalid},
// 特殊样式
{tag: tags.strong, fontWeight: 'bold'},
{tag: tags.emphasis, fontStyle: 'italic'},
{tag: tags.strikethrough, textDecoration: 'line-through'},
{tag: tags.link, color: colors.variable, textDecoration: 'underline'},
// 标题
{tag: tags.heading, fontWeight: 'bold', color: colors.heading},
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
// 无效内容
{tag: tags.invalid, color: colors.invalid}, {tag: tags.invalid, color: colors.invalid},
]); ]);
return [ return [
theme, theme,
syntaxHighlighting(highlightStyle), syntaxHighlighting(highlightStyle),
]; ];
} }

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'aura', themeName: 'aura',
dark: true, dark: true,
// 基础色调
background: '#21202e', background: '#21202e',
backgroundSecondary: '#2B2A3BFF', backgroundSecondary: '#2b2a3b',
surface: '#21202e',
dropdownBackground: '#21202e',
dropdownBorder: '#3b334b',
// 文本颜色
foreground: '#edecee', foreground: '#edecee',
foregroundSecondary: '#edecee',
comment: '#6d6d6d',
// 语法高亮色 - 核心
keyword: '#a277ff',
string: '#61ffca',
function: '#ffca85',
number: '#61ffca',
operator: '#a277ff',
variable: '#edecee',
type: '#82e2ff',
// 语法高亮色 - 扩展
constant: '#61ffca',
storage: '#a277ff',
parameter: '#edecee',
class: '#82e2ff',
heading: '#a277ff',
invalid: '#ff6767',
regexp: '#61ffca',
// 界面元素
cursor: '#a277ff', cursor: '#a277ff',
selection: '#3d375e7f', selection: '#3d375e7f',
selectionBlur: '#3d375e7f',
activeLine: '#4d4b6622', activeLine: '#4d4b6622',
lineNumber: '#a394f033', lineNumber: '#a394f033',
activeLineNumber: '#cdccce', activeLineNumber: '#cdccce',
diffInserted: '#61ffca',
// 边框和分割线 diffDeleted: '#ff6767',
diffChanged: '#ffca85',
borderColor: '#3b334b', borderColor: '#3b334b',
borderLight: '#edecee19',
// 搜索和匹配
searchMatch: '#61ffca',
matchingBracket: '#a394f033', matchingBracket: '#a394f033',
}
// 使用通用主题工厂函数创建 Aura 主题 comment: '#6d6d6d',
export const aura: Extension = createBaseTheme(config) lineComment: '#5c5c5c',
blockComment: '#5a5a5a',
docComment: '#747474',
name: '#edecee',
variableName: '#edecee',
typeName: '#82e2ff',
tagName: '#7cd4ff',
propertyName: '#d2d1f9',
attributeName: '#f6d1ff',
className: '#95dbff',
labelName: '#ffc285',
namespace: '#6fd0ff',
macroName: '#ffca85',
literal: '#82e2ff',
string: '#61ffca',
docString: '#61ffca',
character: '#73ffd7',
attributeValue: '#ffe3c4',
number: '#82e2ff',
integer: '#82e2ff',
float: '#82e2ff',
bool: '#ffd18b',
regexp: '#61ffca',
escape: '#4ff7c6',
color: '#ffc57c',
url: '#7cd4ff',
keyword: '#a277ff',
self: '#c89eff',
null: '#f69aff',
atom: '#61ffca',
unit: '#61ffca',
modifier: '#c094ff',
operatorKeyword: '#b98dff',
controlKeyword: '#c17aff',
definitionKeyword: '#bd8eff',
moduleKeyword: '#cfa2ff',
operator: '#a277ff',
derefOperator: '#c59bff',
arithmeticOperator: '#c78df5',
logicOperator: '#c088ff',
bitwiseOperator: '#ce8cff',
compareOperator: '#c786ff',
updateOperator: '#bb7cff',
definitionOperator: '#b070ff',
typeOperator: '#b98aff',
controlOperator: '#a867ff',
punctuation: '#d1a6ff',
separator: '#ceb1ff',
bracket: '#adabff',
angleBracket: '#ffc3ff',
squareBracket: '#ff9ddd',
paren: '#f39ddf',
brace: '#f589d6',
content: '#edecee',
heading: '#a277ff',
heading1: '#caa0ff',
heading2: '#c192ff',
heading3: '#b684ff',
heading4: '#aa76ff',
heading5: '#9f68ff',
heading6: '#955aff',
contentSeparator: '#a277ff',
list: '#c0c0c0',
quote: '#9280a3',
emphasis: '#edecee',
strong: '#f4f3f5',
link: '#79d3ff',
monospace: '#d5d0d8',
strikethrough: '#b9b3c0',
inserted: '#61ffca',
deleted: '#ff6767',
changed: '#ffca85',
invalid: '#ff6767',
meta: '#807d8c',
documentMeta: '#7b7886',
annotation: '#7df5d9',
processingInstruction: '#7b7490',
definition: '#d0cfe4',
constant: '#61ffca',
function: '#ffca85',
standard: '#c1c0cf',
local: '#c9c8d7',
special: '#ffd9a8',
};
export const aura: Extension = createBaseTheme(config);

View File

@@ -1,63 +1,114 @@
import {createBaseTheme} from '../base'; import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types'; import type {ThemeColors} from '../types';
// 默认深色主题颜色
export const defaultDarkColors: ThemeColors = { export const defaultDarkColors: ThemeColors = {
// 主题信息 themeName: 'default-dark',
name: 'default-dark',
dark: true, dark: true,
// 基础色调 // 基础色调
background: '#252B37', // 主背景色 background: '#252B37',
backgroundSecondary: '#213644', // 次要背景色 backgroundSecondary: '#213644',
surface: '#474747', // 面板背景
dropdownBackground: '#252B37', // 下拉菜单背景
dropdownBorder: '#ffffff19', // 下拉菜单边框
// 文本 // 文本与界面
foreground: '#9BB586', // 主文本色 foreground: '#ffffff',
foregroundSecondary: '#9c9c9c', // 次要文本色 cursor: '#ffffff',
comment: '#6272a4', // 注释色 selection: '#0865a9',
activeLine: '#ffffff0a',
lineNumber: '#ffffff26',
activeLineNumber: '#ffffff99',
diffInserted: '#64d189',
diffDeleted: '#ff6b6b',
diffChanged: '#ffb86c',
borderColor: '#1e222a',
matchingBracket: '#ffffff19',
// 语法高亮色 - 核心 // 语法标签色值
keyword: '#ff79c6', // 关键字 comment: '#6272a4',
string: '#f1fa8c', // 字符串 lineComment: '#5c6b99',
function: '#50fa7b', // 函数名 blockComment: '#596492',
number: '#bd93f9', // 数字 docComment: '#6e7bb5',
operator: '#ff79c6', // 操作符 name: '#dfe8ce',
variable: '#8fbcbb', // 变量 variableName: '#8fbcbb',
type: '#8be9fd', // 类型 typeName: '#8be9fd',
tagName: '#77d7f4',
// 语法高亮色 - 扩展 propertyName: '#c9e3b0',
constant: '#bd93f9', // 常量 attributeName: '#e1c8ff',
storage: '#ff79c6', // 存储类型 className: '#a5e0ff',
parameter: '#8fbcbb', // 参数 labelName: '#f7b267',
class: '#8be9fd', // 类名 namespace: '#5cd1ff',
heading: '#ff79c6', // 标题 macroName: '#ffcf8b',
invalid: '#d30102', // 无效内容 literal: '#c3b5ff',
regexp: '#f1fa8c', // 正则表达式 string: '#f1fa8c',
docString: '#e9f28a',
// 界面元素 character: '#ffd684',
cursor: '#ffffff', // 光标 attributeValue: '#ffe099',
selection: '#0865a9', // 选中背景 number: '#bd93f9',
selectionBlur: '#225377', // 失焦选中背景 integer: '#c6a5ff',
activeLine: '#ffffff0a', // 当前行高亮 float: '#b68afd',
lineNumber: '#ffffff26', // 行号 bool: '#7dd4cc',
activeLineNumber: '#ffffff99', // 活动行号 regexp: '#9cf0f1',
escape: '#85dedd',
// 边框和分割线 color: '#ffd38d',
borderColor: '#1e222a', // 边框色 url: '#8de0ff',
borderLight: '#ffffff19', // 浅色边框 keyword: '#ff79c6',
self: '#ff94d6',
// 搜索和匹配 null: '#ff9fe2',
searchMatch: '#8fbcbb', // 搜索匹配 atom: '#cba6ff',
matchingBracket: '#ffffff19', // 匹配括号 unit: '#a8dbd2',
modifier: '#f78cc8',
operatorKeyword: '#ff84cf',
controlKeyword: '#ff6fb6',
definitionKeyword: '#ff92d6',
moduleKeyword: '#ff8aca',
operator: '#ff79c6',
derefOperator: '#ff9bd6',
arithmeticOperator: '#ff7fc4',
logicOperator: '#ff9fcf',
bitwiseOperator: '#ff6fb8',
compareOperator: '#ff85c7',
updateOperator: '#ff76bd',
definitionOperator: '#ff6db7',
typeOperator: '#ff9bdd',
controlOperator: '#ff69ad',
punctuation: '#f5a6d9',
separator: '#f0a3d7',
bracket: '#cda0ff',
angleBracket: '#ffc0f1',
squareBracket: '#ff8db5',
paren: '#ff9ec8',
brace: '#fe7ab1',
content: '#dfeed0',
heading: '#ff9b6b',
heading1: '#ffb75f',
heading2: '#ffad57',
heading3: '#ffa14e',
heading4: '#ff9447',
heading5: '#ff8842',
heading6: '#ff7b3c',
contentSeparator: '#ff79c6',
list: '#acd1a2',
quote: '#7c8fb5',
emphasis: '#d9f7c1',
strong: '#fdf1c1',
link: '#6ac8ff',
monospace: '#d1dbc0',
strikethrough: '#b7c3a5',
inserted: '#64d189',
deleted: '#ff6b6b',
changed: '#ffb86c',
invalid: '#d30102',
meta: '#7285bb',
documentMeta: '#6a7caa',
annotation: '#9bf0ff',
processingInstruction: '#7685bd',
definition: '#9ec9c3',
constant: '#bd93f9',
function: '#50fa7b',
standard: '#8ab0a8',
local: '#92c7bb',
special: '#f4d67a',
}; };
// 创建深色主题
export function createDarkTheme(colors: ThemeColors = defaultDarkColors) { export function createDarkTheme(colors: ThemeColors = defaultDarkColors) {
return createBaseTheme({...colors, dark: true}); return createBaseTheme({...colors, dark: true});
} }
// 默认深色主题
export const defaultDark = createDarkTheme(defaultDarkColors);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'dracula', themeName: 'dracula',
dark: true, dark: true,
// 基础色调 background: '#282a36',
background: '#282A36', backgroundSecondary: '#323543',
backgroundSecondary: '#323543FF',
surface: '#282A36',
dropdownBackground: '#282A36',
dropdownBorder: '#191A21',
// 文本颜色 foreground: '#f8f8f2',
foreground: '#F8F8F2', cursor: '#f8f8f2',
foregroundSecondary: '#F8F8F2', selection: '#44475a',
comment: '#6272A4',
// 语法高亮色 - 核心
keyword: '#FF79C6',
string: '#F1FA8C',
function: '#50FA7B',
number: '#BD93F9',
operator: '#FF79C6',
variable: '#F8F8F2',
type: '#8BE9FD',
// 语法高亮色 - 扩展
constant: '#BD93F9',
storage: '#FF79C6',
parameter: '#F8F8F2',
class: '#8BE9FD',
heading: '#BD93F9',
invalid: '#FF5555',
regexp: '#F1FA8C',
// 界面元素
cursor: '#F8F8F2',
selection: '#44475A',
selectionBlur: '#44475A',
activeLine: '#53576c22', activeLine: '#53576c22',
lineNumber: '#6272A4', lineNumber: '#6272a4',
activeLineNumber: '#F8F8F2', activeLineNumber: '#f8f8f2',
diffInserted: '#50fa7b',
diffDeleted: '#ff5555',
diffChanged: '#f1fa8c',
borderColor: '#191a21',
matchingBracket: '#44475a',
// 边框和分割线 comment: '#6272a4',
borderColor: '#191A21', lineComment: '#55608c',
borderLight: '#F8F8F219', blockComment: '#4f597f',
docComment: '#7c89bd',
name: '#f8f8f2',
variableName: '#f8f8f2',
typeName: '#8be9fd',
tagName: '#7de5ff',
propertyName: '#dcdce5',
attributeName: '#fcb5ff',
className: '#9cecff',
labelName: '#ffb86c',
namespace: '#6deeff',
macroName: '#50fa7b',
literal: '#bd93f9',
string: '#f1fa8c',
docString: '#f5ffa9',
character: '#ffec99',
attributeValue: '#ffcf99',
number: '#bd93f9',
integer: '#cfa6ff',
float: '#b48cff',
bool: '#ffb38b',
regexp: '#f1fa8c',
escape: '#f7ffae',
color: '#ffcf99',
url: '#8ae8ff',
keyword: '#ff79c6',
self: '#ff9dd7',
null: '#ff8fb0',
atom: '#bd93f9',
unit: '#bd93f9',
modifier: '#ff90d4',
operatorKeyword: '#ff8bd2',
controlKeyword: '#ff7dc1',
definitionKeyword: '#ff91d1',
moduleKeyword: '#ffacd9',
operator: '#ff79c6',
derefOperator: '#ff91d1',
arithmeticOperator: '#ff88c5',
logicOperator: '#ff8bcf',
bitwiseOperator: '#ff74ba',
compareOperator: '#ff86c6',
updateOperator: '#ff7cbf',
definitionOperator: '#ff6aae',
typeOperator: '#ff98d9',
controlOperator: '#ff6aa6',
punctuation: '#f4ade4',
separator: '#f3a6dc',
bracket: '#cfaefc',
angleBracket: '#ffcff1',
squareBracket: '#ff9fcc',
paren: '#ffb1d8',
brace: '#ff90c1',
content: '#f8f8f2',
heading: '#bd93f9',
heading1: '#d2b3ff',
heading2: '#c7a8ff',
heading3: '#bb9dff',
heading4: '#af92ff',
heading5: '#a387ff',
heading6: '#977cff',
contentSeparator: '#ff79c6',
list: '#c8cbd1',
quote: '#7b86a7',
emphasis: '#f8f8f2',
strong: '#ffffff',
link: '#8be9fd',
monospace: '#dadfde',
strikethrough: '#c2c8d1',
inserted: '#50fa7b',
deleted: '#ff5555',
changed: '#f1fa8c',
invalid: '#ff5555',
meta: '#8791bb',
documentMeta: '#7b84aa',
annotation: '#a7f7d4',
processingInstruction: '#6c7699',
definition: '#d6d9f2',
constant: '#bd93f9',
function: '#50fa7b',
standard: '#bac4d8',
local: '#c3c8da',
special: '#ffd6a5',
};
// 搜索和匹配 export const dracula: Extension = createBaseTheme(config);
searchMatch: '#50FA7B',
matchingBracket: '#44475A',
}
// 使用通用主题工厂函数创建 Dracula 主题
export const dracula: Extension = createBaseTheme(config)

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'github-dark', themeName: 'github-dark',
dark: true, dark: true,
// 基础色调
background: '#24292e', background: '#24292e',
backgroundSecondary: '#2E343BFF', backgroundSecondary: '#2e343b',
surface: '#24292e',
dropdownBackground: '#24292e',
dropdownBorder: '#1b1f23',
// 文本颜色
foreground: '#d1d5da', foreground: '#d1d5da',
foregroundSecondary: '#d1d5da',
comment: '#6a737d',
// 语法高亮色 - 核心
keyword: '#f97583',
string: '#9ecbff',
function: '#79b8ff',
number: '#79b8ff',
operator: '#f97583',
variable: '#ffab70',
type: '#79b8ff',
// 语法高亮色 - 扩展
constant: '#79b8ff',
storage: '#f97583',
parameter: '#e1e4e8',
class: '#b392f0',
heading: '#79b8ff',
invalid: '#f97583',
regexp: '#9ecbff',
// 界面元素
cursor: '#c8e1ff', cursor: '#c8e1ff',
selection: '#3392FF44', selection: '#3392ff44',
selectionBlur: '#3392FF44',
activeLine: '#4d566022', activeLine: '#4d566022',
lineNumber: '#444d56', lineNumber: '#444d56',
activeLineNumber: '#e1e4e8', activeLineNumber: '#e1e4e8',
diffInserted: '#2ea043',
// 边框和分割线 diffDeleted: '#d73a49',
diffChanged: '#c69026',
borderColor: '#1b1f23', borderColor: '#1b1f23',
borderLight: '#d1d5da19', matchingBracket: '#17e5e650',
// 搜索和匹配 comment: '#6a737d',
searchMatch: '#79b8ff', lineComment: '#596068',
matchingBracket: '#17E5E650', blockComment: '#4f555c',
} docComment: '#7c858f',
name: '#d1d5da',
variableName: '#ffab70',
typeName: '#79b8ff',
tagName: '#8dd1ff',
propertyName: '#d9dee5',
attributeName: '#c0a7ff',
className: '#b392f0',
labelName: '#ffab70',
namespace: '#84c5ff',
macroName: '#79b8ff',
literal: '#79b8ff',
string: '#9ecbff',
docString: '#aed3ff',
character: '#ffe4b2',
attributeValue: '#ffcf9a',
number: '#79b8ff',
integer: '#6fb1ff',
float: '#62a7ff',
bool: '#ffa657',
regexp: '#9ecbff',
escape: '#8bc2ff',
color: '#ffc27c',
url: '#68b7ff',
keyword: '#f97583',
self: '#ffa5b1',
null: '#ff8b76',
atom: '#79b8ff',
unit: '#79b8ff',
modifier: '#ff9a8c',
operatorKeyword: '#ff8c80',
controlKeyword: '#ff7f73',
definitionKeyword: '#ff9aa1',
moduleKeyword: '#ffb1ae',
operator: '#f97583',
derefOperator: '#ff8a7d',
arithmeticOperator: '#ff7c6a',
logicOperator: '#ff8172',
bitwiseOperator: '#ff6958',
compareOperator: '#ff7c6c',
updateOperator: '#ff6d5e',
definitionOperator: '#ff5d54',
typeOperator: '#ff8ca5',
controlOperator: '#ff5b4f',
punctuation: '#d6a3c5',
separator: '#d2a9c9',
bracket: '#98a6c8',
angleBracket: '#c3d5ff',
squareBracket: '#b6c4e4',
paren: '#b0bace',
brace: '#a1aabf',
content: '#d1d5da',
heading: '#79b8ff',
heading1: '#9ac7ff',
heading2: '#8fbfff',
heading3: '#85b7ff',
heading4: '#7bafff',
heading5: '#70a7ff',
heading6: '#669eff',
contentSeparator: '#f97583',
list: '#b8bfc7',
quote: '#7d848c',
emphasis: '#d1d5da',
strong: '#f5f7f9',
link: '#79b8ff',
monospace: '#cfd6df',
strikethrough: '#acb4bd',
inserted: '#2ea043',
deleted: '#d73a49',
changed: '#c69026',
invalid: '#f97583',
meta: '#8591a1',
documentMeta: '#7b8593',
annotation: '#90d6ff',
processingInstruction: '#6a7380',
definition: '#cdd4de',
constant: '#79b8ff',
function: '#79b8ff',
standard: '#bac4d1',
local: '#c5ccd7',
special: '#ffd9a6',
};
// 使用通用主题工厂函数创建 GitHub Dark 主题 export const githubDark: Extension = createBaseTheme(config);
export const githubDark: Extension = createBaseTheme(config)

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'material-dark', themeName: 'material-dark',
dark: true, dark: true,
// 基础色调
background: '#263238', background: '#263238',
backgroundSecondary: '#2D3E46FF', backgroundSecondary: '#2d3e46',
surface: '#263238',
dropdownBackground: '#263238',
dropdownBorder: '#FFFFFF10',
// 文本颜色 foreground: '#eeffff',
foreground: '#EEFFFF', cursor: '#ffcc00',
foregroundSecondary: '#EEFFFF', selection: '#80cbc420',
comment: '#546E7A',
// 语法高亮色 - 核心
keyword: '#C792EA',
string: '#C3E88D',
function: '#82AAFF',
number: '#F78C6C',
operator: '#C792EA',
variable: '#EEFFFF',
type: '#B2CCD6',
// 语法高亮色 - 扩展
constant: '#F78C6C',
storage: '#C792EA',
parameter: '#EEFFFF',
class: '#FFCB6B',
heading: '#C3E88D',
invalid: '#FF5370',
regexp: '#89DDFF',
// 界面元素
cursor: '#FFCC00',
selection: '#80CBC420',
selectionBlur: '#80CBC420',
activeLine: '#4c616c22', activeLine: '#4c616c22',
lineNumber: '#37474F', lineNumber: '#37474f',
activeLineNumber: '#607a86', activeLineNumber: '#607a86',
diffInserted: '#c3e88d',
// 边框和分割线 diffDeleted: '#ff5370',
borderColor: '#FFFFFF10', diffChanged: '#ffcb6b',
borderLight: '#EEFFFF19', borderColor: '#ffffff10',
// 搜索和匹配
searchMatch: '#82AAFF',
matchingBracket: '#263238', matchingBracket: '#263238',
}
// 使用通用主题工厂函数创建 Material Dark 主题 comment: '#546e7a',
export const materialDark: Extension = createBaseTheme(config) lineComment: '#4b606a',
blockComment: '#455962',
docComment: '#6c8795',
name: '#eeffff',
variableName: '#eeffff',
typeName: '#b2ccd6',
tagName: '#9ad4f5',
propertyName: '#e0f2ff',
attributeName: '#ffdcdc',
className: '#ffcb6b',
labelName: '#ffd17a',
namespace: '#8ad2e7',
macroName: '#82aaff',
literal: '#f78c6c',
string: '#c3e88d',
docString: '#d3f8a8',
character: '#ffe8c0',
attributeValue: '#ffd99f',
number: '#f78c6c',
integer: '#ff996e',
float: '#ffad80',
bool: '#ffd37d',
regexp: '#89ddff',
escape: '#66d9ff',
color: '#ffd492',
url: '#72d1ff',
keyword: '#c792ea',
self: '#d29ef2',
null: '#ff8aad',
atom: '#f78c6c',
unit: '#f78c6c',
modifier: '#dca8f0',
operatorKeyword: '#ca8de3',
controlKeyword: '#c280e1',
definitionKeyword: '#ce95ea',
moduleKeyword: '#d8a8f0',
operator: '#c792ea',
derefOperator: '#d79ef4',
arithmeticOperator: '#d28aec',
logicOperator: '#cd84e3',
bitwiseOperator: '#c77cdf',
compareOperator: '#cc8fe5',
updateOperator: '#c47ad9',
definitionOperator: '#bb6fd0',
typeOperator: '#cfa2ed',
controlOperator: '#b767cf',
punctuation: '#d9b4ff',
separator: '#d5aef6',
bracket: '#9fb6c5',
angleBracket: '#c4ddff',
squareBracket: '#a7c5dd',
paren: '#adc3d4',
brace: '#92aabd',
content: '#eeffff',
heading: '#c3e88d',
heading1: '#aeea9c',
heading2: '#a0dd92',
heading3: '#92d087',
heading4: '#85c37d',
heading5: '#78b673',
heading6: '#6aa969',
contentSeparator: '#c792ea',
list: '#b7cad4',
quote: '#758892',
emphasis: '#eeffff',
strong: '#f8ffff',
link: '#89ddff',
monospace: '#d7e4ec',
strikethrough: '#b4c4cc',
inserted: '#c3e88d',
deleted: '#ff5370',
changed: '#ffcb6b',
invalid: '#ff5370',
meta: '#6d8795',
documentMeta: '#648292',
annotation: '#73e0ff',
processingInstruction: '#617480',
definition: '#d0dae4',
constant: '#f78c6c',
function: '#82aaff',
standard: '#bacdd8',
local: '#c3d3dc',
special: '#ffd8a6',
};
export const materialDark: Extension = createBaseTheme(config);

View File

@@ -1,76 +1,123 @@
import {Extension} from "@codemirror/state" import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors const chalky = '#e5c07b';
const coral = '#e06c75';
const chalky = "#e5c07b", const cyan = '#56b6c2';
coral = "#e06c75", const ivory = '#abb2bf';
cyan = "#56b6c2", const stone = '#7d8799';
invalid = "#ffffff", const malibu = '#61afef';
ivory = "#abb2bf", const sage = '#98c379';
stone = "#7d8799", // Brightened compared to original to increase contrast const whiskey = '#d19a66';
malibu = "#61afef", const violet = '#c678dd';
sage = "#98c379", const darkBackground = '#21252b';
whiskey = "#d19a66", const highlightBackground = '#313949ff';
violet = "#c678dd", const background = '#282c34';
darkBackground = "#21252b", const selection = '#3e4451';
highlightBackground = "#313949FF",
background = "#282c34",
tooltipBackground = "#353a42",
selection = "#3E4451",
cursor = "#528bff"
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'one-dark', themeName: 'one-dark',
dark: true, dark: true,
// 基础色调 background,
background: background,
backgroundSecondary: highlightBackground, backgroundSecondary: highlightBackground,
surface: tooltipBackground,
dropdownBackground: darkBackground,
dropdownBorder: stone,
// 文本颜色
foreground: ivory, foreground: ivory,
foregroundSecondary: stone, cursor: '#528bff',
comment: stone, selection,
// 语法高亮色 - 核心
keyword: violet,
string: sage,
function: malibu,
number: chalky,
operator: cyan,
variable: coral,
type: chalky,
// 语法高亮色 - 扩展
constant: whiskey,
storage: violet,
parameter: coral,
class: chalky,
heading: coral,
invalid: invalid,
regexp: cyan,
// 界面元素
cursor: cursor,
selection: selection,
selectionBlur: selection,
activeLine: '#6699ff0b', activeLine: '#6699ff0b',
lineNumber: stone, lineNumber: stone,
activeLineNumber: ivory, activeLineNumber: ivory,
diffInserted: sage,
// 边框和分割线 diffDeleted: coral,
diffChanged: whiskey,
borderColor: darkBackground, borderColor: darkBackground,
borderLight: ivory + '19',
// 搜索和匹配
searchMatch: malibu,
matchingBracket: '#bad0f847', matchingBracket: '#bad0f847',
}
// 使用通用主题工厂函数创建 One Dark 主题 comment: stone,
export const oneDark: Extension = createBaseTheme(config) lineComment: '#6c7484',
blockComment: '#606775',
docComment: '#8b92a0',
name: ivory,
variableName: coral,
typeName: chalky,
tagName: '#e4c78f',
propertyName: '#d7dee8',
attributeName: '#efb8c2',
className: chalky,
labelName: '#f7b267',
namespace: '#88c0ff',
macroName: malibu,
literal: chalky,
string: sage,
docString: '#b3d899',
character: '#d9f59c',
attributeValue: '#f0c390',
number: chalky,
integer: '#f2c78d',
float: '#f1ba6a',
bool: '#f28f6a',
regexp: cyan,
escape: '#7fd5e9',
color: whiskey,
url: '#7dc7ff',
keyword: violet,
self: '#d98ae8',
null: '#ef8fa8',
atom: whiskey,
unit: '#fbd38a',
modifier: '#d391f2',
operatorKeyword: '#78c3d6',
controlKeyword: '#bf6edb',
definitionKeyword: '#d383e6',
moduleKeyword: '#a6c1ff',
operator: cyan,
derefOperator: '#72c1d3',
arithmeticOperator: '#6ab4ce',
logicOperator: '#6ccad7',
bitwiseOperator: '#4fa8c2',
compareOperator: '#64b9cc',
updateOperator: '#4299b8',
definitionOperator: '#398daf',
typeOperator: '#3fc4e2',
controlOperator: '#3f96b0',
punctuation: '#8eaac2',
separator: '#7a96b1',
bracket: '#b3bcc7',
angleBracket: '#cfd5dd',
squareBracket: '#96a2ae',
paren: '#7f8c97',
brace: '#9aa5af',
content: ivory,
heading: coral,
heading1: '#ffb19d',
heading2: '#ffa188',
heading3: '#ff9173',
heading4: '#ff825e',
heading5: '#ff7249',
heading6: '#ff6234',
contentSeparator: cyan,
list: '#9da7b4',
quote: '#8b94a4',
emphasis: ivory,
strong: '#f4f6f8',
link: malibu,
monospace: '#c2cad1',
strikethrough: '#9ea5b1',
inserted: sage,
deleted: coral,
changed: whiskey,
invalid: '#ffffff',
meta: '#96a1b4',
documentMeta: '#8a95a6',
annotation: '#84d0ff',
processingInstruction: '#7c889c',
definition: '#c9cfd8',
constant: whiskey,
function: malibu,
standard: '#aeb7c5',
local: '#b9c2ce',
special: '#f4d67a',
};
export const oneDark: Extension = createBaseTheme(config);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'solarized-dark', themeName: 'solarized-dark',
dark: true, dark: true,
// 基础色调 background: '#002b36',
background: '#002B36', backgroundSecondary: '#003643',
backgroundSecondary: '#003643FF',
surface: '#002B36',
dropdownBackground: '#002B36',
dropdownBorder: '#2AA19899',
// 文本颜色 foreground: '#fdf6e3',
foreground: '#93A1A1', cursor: '#d30102',
foregroundSecondary: '#93A1A1',
comment: '#586E75',
// 语法高亮色 - 核心
keyword: '#859900',
string: '#2AA198',
function: '#268BD2',
number: '#D33682',
operator: '#859900',
variable: '#268BD2',
type: '#CB4B16',
// 语法高亮色 - 扩展
constant: '#CB4B16',
storage: '#93A1A1',
parameter: '#268BD2',
class: '#CB4B16',
heading: '#268BD2',
invalid: '#DC322F',
regexp: '#DC322F',
// 界面元素
cursor: '#D30102',
selection: '#274642', selection: '#274642',
selectionBlur: '#274642',
activeLine: '#005b7022', activeLine: '#005b7022',
lineNumber: '#93A1A1', lineNumber: '#93a1a1',
activeLineNumber: '#949494', activeLineNumber: '#949494',
diffInserted: '#859900',
// 边框和分割线 diffDeleted: '#dc322f',
diffChanged: '#b58900',
borderColor: '#073642', borderColor: '#073642',
borderLight: '#93A1A119',
// 搜索和匹配
searchMatch: '#2AA198',
matchingBracket: '#073642', matchingBracket: '#073642',
}
// 使用通用主题工厂函数创建 Solarized Dark 主题 comment: '#586e75',
export const solarizedDark: Extension = createBaseTheme(config) lineComment: '#4f646a',
blockComment: '#46595e',
docComment: '#7c8f94',
name: '#fdf6e3',
variableName: '#b58900',
typeName: '#2aa198',
tagName: '#2ab7a5',
propertyName: '#d7c8a1',
attributeName: '#f1c795',
className: '#b58900',
labelName: '#d7991f',
namespace: '#3ca8a0',
macroName: '#268bd2',
literal: '#d33682',
string: '#859900',
docString: '#9cc200',
character: '#b3dd00',
attributeValue: '#e1c272',
number: '#d33682',
integer: '#c0478a',
float: '#b03a79',
bool: '#ffcc4d',
regexp: '#2aa198',
escape: '#35bcb1',
color: '#d19100',
url: '#268bd2',
keyword: '#cb4b16',
self: '#e2572f',
null: '#ff6845',
atom: '#d33682',
unit: '#ad8100',
modifier: '#d96d22',
operatorKeyword: '#bc5822',
controlKeyword: '#c14a17',
definitionKeyword: '#de5c29',
moduleKeyword: '#d4975b',
operator: '#6c71c4',
derefOperator: '#8a78d8',
arithmeticOperator: '#7d6fd0',
logicOperator: '#8376d5',
bitwiseOperator: '#6a5dc3',
compareOperator: '#8171cd',
updateOperator: '#5d54b4',
definitionOperator: '#5f56b8',
typeOperator: '#379e9d',
controlOperator: '#5950a7',
punctuation: '#b1a6d2',
separator: '#a090c1',
bracket: '#cac5dc',
angleBracket: '#e4e1ee',
squareBracket: '#bdb6cf',
paren: '#aba5c0',
brace: '#c2bcd5',
content: '#fdf6e3',
heading: '#cb4b16',
heading1: '#e06c2c',
heading2: '#d95d1b',
heading3: '#c1500f',
heading4: '#b3450a',
heading5: '#a33805',
heading6: '#932c00',
contentSeparator: '#6c71c4',
list: '#c3b79f',
quote: '#8b968f',
emphasis: '#fdf6e3',
strong: '#fefaf0',
link: '#268bd2',
monospace: '#d6cfbd',
strikethrough: '#c4bba5',
inserted: '#859900',
deleted: '#dc322f',
changed: '#b58900',
invalid: '#dc322f',
meta: '#687b84',
documentMeta: '#5f7179',
annotation: '#2bb0cf',
processingInstruction: '#5a6b71',
definition: '#dacfb9',
constant: '#d33682',
function: '#268bd2',
standard: '#9bb1b0',
local: '#b4c3bb',
special: '#b58900',
};
export const solarizedDark: Extension = createBaseTheme(config);

Some files were not shown because too many files have changed in this diff Show More