16 Commits

Author SHA1 Message Date
bae4e663fb ⬆️ Upgrade dependencies 2025-11-13 20:02:22 +08:00
a17e060d16 ⬆️ Upgrade dependencies 2025-11-13 19:36:11 +08:00
71946965eb 🐛 Fixed database constraint issues 2025-11-08 17:35:29 +08:00
d4cd22d234 🚀 Update build and release workflows 2025-11-08 17:17:07 +08:00
05f2f7d46d 🚀 Update build and release workflows 2025-11-08 17:05:31 +08:00
9deb2744a9 🚀 Update build and release workflows 2025-11-08 16:24:22 +08:00
6fac7c42d6 🚀 Update build and release workflows 2025-11-08 16:03:26 +08:00
3393bc84e3 🚀 Add build and release workflows 2025-11-08 15:50:30 +08:00
286b0159d7 🎨 Optimize update services
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-11-08 00:00:08 +08:00
cc98e556c6 ♻️ Optimize code
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-11-07 22:34:12 +08:00
5902f482d9 ♻️ Refactor configuration change notification service 2025-11-07 00:35:11 +08:00
551e7e2cfd Optimize hotkey service 2025-11-06 22:42:44 +08:00
e0179b5838 🎨 Optimize hotkey service 2025-11-06 00:08:26 +08:00
df79267e16 Optimize multi-window services 2025-11-05 22:07:43 +08:00
1f0254822f 🎨 Optimize multi-window services 2025-11-05 00:10:26 +08:00
e9b6fef3cd Added mermaid language support 2025-11-04 22:58:36 +08:00
141 changed files with 10760 additions and 3622 deletions

325
.github/workflows/build-release.yml vendored Normal file
View File

@@ -0,0 +1,325 @@
name: Build and Release Voidraft
on:
# 推送标签时触发(用于正式发布)
push:
tags:
- 'v*' # 触发条件:推送 v 开头的标签,如 v1.0.0
branches:
- main # 仅当标签在 main 分支时触发
# 手动触发(用于测试)
workflow_dispatch:
inputs:
release:
description: '是否创建 Release测试时选 false'
required: true
type: boolean
default: false
platforms:
description: '要构建的平台用逗号分隔windows,linux,macos-intel,macos-arm'
required: true
type: string
default: 'windows,linux'
env:
NODE_OPTIONS: "--max-old-space-size=4096" # 防止 Node.js 内存溢出
jobs:
# 准备构建配置
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
should_release: ${{ steps.check-release.outputs.should_release }}
steps:
- name: 确定构建平台
id: set-matrix
run: |
# 如果是手动触发,根据输入决定平台
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PLATFORMS="${{ github.event.inputs.platforms }}"
else
# 标签触发,构建所有平台
PLATFORMS="windows,linux,macos-intel,macos-arm"
fi
echo "构建平台: $PLATFORMS"
# 构建矩阵 JSON
MATRIX='{"include":[]}'
if [[ "$PLATFORMS" == *"windows"* ]]; then
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"windows-latest","os":"windows","arch":"amd64","platform_dir":"windows","id":"windows"}]')
fi
if [[ "$PLATFORMS" == *"linux"* ]]; then
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"ubuntu-22.04","os":"linux","arch":"amd64","platform_dir":"linux","id":"linux"}]')
fi
if [[ "$PLATFORMS" == *"macos-intel"* ]]; then
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"macos-latest","os":"darwin","arch":"amd64","platform_dir":"darwin","id":"macos-intel"}]')
fi
if [[ "$PLATFORMS" == *"macos-arm"* ]]; then
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"macos-latest","os":"darwin","arch":"arm64","platform_dir":"darwin","id":"macos-arm"}]')
fi
# 使用 -c 确保输出紧凑的单行 JSON符合 GitHub Actions 输出格式
echo "matrix=$(echo "$MATRIX" | jq -c .)" >> $GITHUB_OUTPUT
# 输出美化的 JSON 用于日志查看
echo "生成的矩阵:"
echo "$MATRIX" | jq .
- name: 检查是否创建 Release
id: check-release
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# 手动触发,根据输入决定
echo "should_release=${{ github.event.inputs.release }}" >> $GITHUB_OUTPUT
else
# 标签触发,自动创建 Release
echo "should_release=true" >> $GITHUB_OUTPUT
fi
build:
needs: prepare
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
runs-on: ${{ matrix.platform }}
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
submodules: recursive
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
- name: 设置 Node.js 环境
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
# Linux 平台依赖
- name: 安装 Linux 依赖
if: matrix.os == 'linux'
run: |
sudo apt-get update
# Wails 3 + 项目特定依赖
sudo apt-get install -y \
build-essential \
pkg-config \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libx11-dev \
rpm \
fuse \
file
# 依赖说明:
# - build-essential: C/C++ 编译工具链
# - pkg-config: 包配置工具
# - libgtk-3-dev: GTK3 GUI 框架
# - libwebkit2gtk-4.1-dev: WebKit2GTK 4.1 (Wails 3 要求)
# - libayatana-appindicator3-dev: 系统托盘支持
# - librsvg2-dev: SVG 图标支持
# - patchelf: 修改 ELF 二进制文件
# - libx11-dev: X11 库 (热键服务依赖)
# - rpm: RPM 打包工具
# - fuse: AppImage 运行依赖
# - file: 文件类型检测工具
# Windows 平台依赖
- name: 设置 Windows 构建环境
if: matrix.os == 'windows'
run: |
# 安装 NSIS (用于创建安装程序)
choco install nsis -y
# 将 NSIS 添加到 PATH
echo "C:\Program Files (x86)\NSIS" >> $GITHUB_PATH
shell: bash
# macOS 平台依赖
- name: 设置 macOS 构建环境
if: matrix.os == 'darwin'
run: |
# Xcode 命令行工具通常已安装
xcode-select --install 2>/dev/null || true
# 安装 Wails CLI
- name: 安装 Wails CLI
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
# 安装前端依赖
- name: 安装前端依赖
working-directory: frontend
run: npm ci
# 构建前端
- name: 构建前端
working-directory: frontend
run: npm run build
# 使用 Wails Task 构建和打包应用
- name: 构建和打包 Wails 应用
run: wails3 task ${{ matrix.id }}:package PRODUCTION=true ARCH=${{ matrix.arch }}
env:
CGO_ENABLED: 1
APP_NAME: voidraft
BIN_DIR: bin
ROOT_DIR: .
shell: bash
# 整理构建产物
- name: 整理构建产物
id: organize_artifacts
run: |
echo "=== 构建产物列表 ==="
ls -lhR bin/ || echo "bin 目录不存在"
# 创建输出目录
mkdir -p artifacts
# 根据平台复制产物
if [ "${{ matrix.os }}" = "windows" ]; then
echo "Windows 平台:查找 NSIS 安装程序"
find . -name "*.exe" -type f
cp bin/*.exe artifacts/ 2>/dev/null || echo "未找到 .exe 文件"
elif [ "${{ matrix.os }}" = "linux" ]; then
echo "Linux 平台:查找 AppImage, deb, rpm, archlinux 包"
find bin -type f
cp bin/*.AppImage artifacts/ 2>/dev/null || echo "未找到 AppImage"
cp bin/*.deb artifacts/ 2>/dev/null || echo "未找到 deb"
cp bin/*.rpm artifacts/ 2>/dev/null || echo "未找到 rpm"
cp bin/*.pkg.tar.zst artifacts/ 2>/dev/null || echo "未找到 archlinux 包"
elif [ "${{ matrix.os }}" = "darwin" ]; then
echo "macOS 平台:查找 .app bundle"
find bin -name "*.app" -type d
# macOS: .app bundle打包成 zip
if [ -d "bin/voidraft.app" ]; then
cd bin
zip -r ../artifacts/voidraft-darwin-${{ matrix.arch }}.app.zip voidraft.app
cd ..
else
echo "未找到 .app bundle"
fi
fi
echo "=== 最终产物 ==="
ls -lh artifacts/ || echo "artifacts 目录为空"
shell: bash
# 上传构建产物到 Artifacts
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: voidraft-${{ matrix.id }}
path: artifacts/*
if-no-files-found: warn
# 创建 GitHub Release 并上传所有构建产物
release:
needs: [prepare, build]
if: ${{ needs.prepare.outputs.should_release == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 下载所有构建产物
uses: actions/download-artifact@v4
with:
path: artifacts
- name: 显示下载的文件
run: |
echo "下载的构建产物:"
ls -R artifacts/
- name: 准备 Release 文件
run: |
mkdir -p release
find artifacts -type f -exec cp {} release/ \;
ls -lh release/
- name: 生成 Release 说明
id: release_notes
run: |
# 获取版本号
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.sha }}"
VERSION_NAME="测试构建 ${VERSION:0:7}"
else
VERSION="${{ github.ref_name }}"
VERSION_NAME="${VERSION}"
fi
cat > release_notes.md << EOF
## Voidraft ${VERSION_NAME}
### 📦 下载
根据你的操作系统选择对应的版本:
- **Windows (64位)**: \`voidraft-windows-amd64.exe\`
- **Linux (64位)**: \`voidraft-linux-amd64\`
- **macOS (Intel)**: \`voidraft-darwin-amd64.zip\`
- **macOS (Apple Silicon)**: \`voidraft-darwin-arm64.zip\`
### 📝 更新内容
请查看 [提交历史](../../commits/${{ github.ref_name }}) 了解本次更新的详细内容。
### 💡 使用说明
#### Windows
1. 下载 `voidraft-windows-amd64.exe`
2. 直接运行即可
#### Linux
1. 下载 `voidraft-linux-amd64`
2. 添加执行权限:`chmod +x voidraft-linux-amd64`
3. 运行:`./voidraft-linux-amd64`
#### macOS
1. 下载对应架构的 zip 文件
2. 解压后运行
3. 如果提示无法打开,请在 系统偏好设置 > 安全性与隐私 中允许运行
---
构建时间: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
EOF
- name: 创建 GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release/*
body_path: release_notes.md
draft: false
prerelease: ${{ github.event_name == 'workflow_dispatch' }}
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('test-{0}', github.sha) || github.ref_name }}
name: ${{ github.event_name == 'workflow_dispatch' && format('测试构建 {0}', github.sha) || github.ref_name }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -12,25 +12,13 @@ vars:
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
tasks:
version:
summary: Generate version information
cmds:
- '{{if eq OS "windows"}}cmd /c ".\scripts\version.bat"{{else}}bash ./scripts/version.sh{{end}}'
sources:
- scripts/version.bat
- scripts/version.sh
generates:
- version.txt
build:
summary: Builds the application
deps: [version]
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
deps: [version]
cmds:
- task: "{{OS}}:package"

293
build/COMMANDS.md Normal file
View File

@@ -0,0 +1,293 @@
# Wails 3 命令参考表
本文档列出了 Voidraft 项目中使用的所有 Wails 3 命令和参数。
## 📋 命令总览
| 类别 | 命令 | 用途 |
|------|------|------|
| 生成工具 | `wails3 generate` | 生成项目所需的各种资源文件 |
| 打包工具 | `wails3 tool package` | 使用 nfpm 打包应用程序 |
| 任务执行 | `wails3 task` | 执行 Taskfile.yml 中定义的任务 |
---
## 🔧 详细命令参数
### 1. `wails3 generate bindings`
**生成 TypeScript 绑定文件**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-f` | 构建标志 | `''` (空字符串) |
| `-clean` | 清理旧绑定 | `true` |
| `-ts` | 生成 TypeScript | 无需值 |
**使用位置:** `build/Taskfile.yml:53`
**完整命令:**
```bash
wails3 generate bindings -f '' -clean=true -ts
```
---
### 2. `wails3 generate icons`
**从单个图片生成多平台图标**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-input` | 输入图片路径 | `appicon.png` |
| `-macfilename` | macOS 图标输出路径 | `darwin/icons.icns` |
| `-windowsfilename` | Windows 图标输出路径 | `windows/icons.ico` |
**使用位置:** `build/Taskfile.yml:64`
**完整命令:**
```bash
wails3 generate icons \
-input appicon.png \
-macfilename darwin/icons.icns \
-windowsfilename windows/icons.ico
```
---
### 3. `wails3 generate syso`
**生成 Windows .syso 资源文件**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-arch` | 目标架构 | `amd64` / `arm64` |
| `-icon` | 图标文件 | `windows/icon.ico` |
| `-manifest` | 清单文件 | `windows/wails.exe.manifest` |
| `-info` | 应用信息 JSON | `windows/info.json` |
| `-out` | 输出文件路径 | `../wails_windows_amd64.syso` |
**使用位置:** `build/windows/Taskfile.yml:42`
**完整命令:**
```bash
wails3 generate syso \
-arch amd64 \
-icon windows/icon.ico \
-manifest windows/wails.exe.manifest \
-info windows/info.json \
-out ../wails_windows_amd64.syso
```
---
### 4. `wails3 generate webview2bootstrapper`
**生成 Windows WebView2 引导程序**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-dir` | 输出目录 | `build/windows/nsis` |
**使用位置:** `build/windows/Taskfile.yml:55`
**完整命令:**
```bash
wails3 generate webview2bootstrapper -dir "build/windows/nsis"
```
**说明:** 下载 Microsoft Edge WebView2 运行时安装程序,用于 NSIS 打包。
---
### 5. `wails3 generate .desktop`
**生成 Linux .desktop 桌面文件**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 应用名称 | `voidraft` |
| `-exec` | 可执行文件名 | `voidraft` |
| `-icon` | 图标名称 | `appicon` |
| `-outputfile` | 输出文件路径 | `build/linux/voidraft.desktop` |
| `-categories` | 应用分类 | `Development;` |
**使用位置:** `build/linux/Taskfile.yml:107`
**完整命令:**
```bash
wails3 generate .desktop \
-name "voidraft" \
-exec "voidraft" \
-icon "appicon" \
-outputfile build/linux/voidraft.desktop \
-categories "Development;"
```
---
### 6. `wails3 generate appimage`
**生成 Linux AppImage 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-binary` | 二进制文件名 | `voidraft` |
| `-icon` | 图标文件路径 | `../../appicon.png` |
| `-desktopfile` | .desktop 文件路径 | `../voidraft.desktop` |
| `-outputdir` | 输出目录 | `../../../bin` |
| `-builddir` | 构建临时目录 | `build/linux/appimage/build` |
**使用位置:** `build/linux/Taskfile.yml:49`
**完整命令:**
```bash
wails3 generate appimage \
-binary voidraft \
-icon ../../appicon.png \
-desktopfile ../voidraft.desktop \
-outputdir ../../../bin \
-builddir build/linux/appimage/build
```
**说明:** 自动下载 linuxdeploy 工具并创建 AppImage。
---
### 7. `wails3 tool package` (deb)
**创建 Debian/Ubuntu .deb 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 包名称 | `voidraft` |
| `-format` | 包格式 | `deb` |
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
| `-out` | 输出目录 | `./bin` |
**使用位置:** `build/linux/Taskfile.yml:90`
**完整命令:**
```bash
wails3 tool package \
-name voidraft \
-format deb \
-config ./build/linux/nfpm/nfpm.yaml \
-out ./bin
```
---
### 8. `wails3 tool package` (rpm)
**创建 RedHat/CentOS/Fedora .rpm 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 包名称 | `voidraft` |
| `-format` | 包格式 | `rpm` |
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
| `-out` | 输出目录 | `./bin` |
**使用位置:** `build/linux/Taskfile.yml:95`
**完整命令:**
```bash
wails3 tool package \
-name voidraft \
-format rpm \
-config ./build/linux/nfpm/nfpm.yaml \
-out ./bin
```
---
### 9. `wails3 tool package` (archlinux)
**创建 Arch Linux .pkg.tar.zst 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 包名称 | `voidraft` |
| `-format` | 包格式 | `archlinux` |
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
| `-out` | 输出目录 | `./bin` |
**使用位置:** `build/linux/Taskfile.yml:100`
**完整命令:**
```bash
wails3 tool package \
-name voidraft \
-format archlinux \
-config ./build/linux/nfpm/nfpm.yaml \
-out ./bin
```
---
### 10. `wails3 task`
**执行 Taskfile.yml 中定义的任务**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `[taskname]` | 任务名称 | `build`, `package`, `run` |
| `[VAR=value]` | 变量赋值 | `PRODUCTION=true`, `ARCH=amd64` |
**常用任务:**
| 任务 | 说明 | 命令 |
|------|------|------|
| `build` | 构建应用 | `wails3 task build PRODUCTION=true` |
| `package` | 打包应用 | `wails3 task package` |
| `run` | 运行应用 | `wails3 task run` |
**使用位置:** `build/config.yml`
---
## 📊 平台对应命令表
| 平台 | 主要命令 | 产物 |
|------|----------|------|
| **Windows** | `generate syso`<br>`generate webview2bootstrapper` | `.syso` 资源文件<br>NSIS 安装程序 |
| **Linux** | `generate .desktop`<br>`generate appimage`<br>`tool package -format deb/rpm/archlinux` | `.desktop` 文件<br>`.AppImage`<br>`.deb` / `.rpm` / `.pkg.tar.zst` |
| **macOS** | `generate icons` | `.icns` 图标<br>`.app` 应用包 |
| **通用** | `generate bindings`<br>`generate icons` | TypeScript 绑定<br>多平台图标 |
---
## 🚀 快速参考
### 完整构建流程
```bash
# 1. 生成绑定和图标
wails3 task common:generate:bindings
wails3 task common:generate:icons
# 2. 构建前端
cd frontend
npm install
npm run build
cd ..
# 3. 构建应用(各平台)
cd build/windows && wails3 task build PRODUCTION=true # Windows
cd build/linux && wails3 task build PRODUCTION=true # Linux
cd build/darwin && wails3 task build PRODUCTION=true # macOS
# 4. 打包应用(各平台)
cd build/windows && wails3 task package # NSIS 安装程序
cd build/linux && wails3 task package # AppImage + deb + rpm + archlinux
cd build/darwin && wails3 task package # .app bundle
```
---
## 📝 注意事项
1. **变量传递:** Task 命令支持通过 `VAR=value` 格式传递变量
2. **路径问题:** 相对路径基于 Taskfile.yml 所在目录
3. **依赖顺序:** 某些任务有依赖关系(通过 `deps:` 定义)
4. **环境变量:** 使用 `env:` 定义的环境变量会自动设置
---
## 🔗 相关文档
- [Wails 3 官方文档](https://v3alpha.wails.io/)
- [Taskfile 语法](https://taskfile.dev/)
- [nfpm 打包工具](https://nfpm.goreleaser.com/)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -39,7 +39,7 @@ tasks:
summary: Generates Windows `.syso` file
dir: build
cmds:
- wails3 generate syso -arch {{.ARCH}} -icon windows/favicon_256x256.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
vars:
ARCH: '{{.ARCH | default ARCH}}'

View File

@@ -62,19 +62,4 @@ export class ServiceOptions {
}
}
export class WebviewWindow {
/** Creates a new WebviewWindow instance. */
constructor($$source: Partial<WebviewWindow> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new WebviewWindow instance from a string or object.
*/
static createFrom($$source: any = {}): WebviewWindow {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new WebviewWindow($$parsedSource as Partial<WebviewWindow>);
}
}
export type Window = any;

View File

@@ -10,10 +10,17 @@
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* Get 获取配置项
*/
@@ -34,22 +41,6 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
return $typingPromise;
}
/**
* GetConfigDir 获取配置目录
*/
export function GetConfigDir(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2275626561) as any;
return $resultPromise;
}
/**
* GetSettingsPath 获取设置文件路径
*/
export function GetSettingsPath(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2175583370) as any;
return $resultPromise;
}
/**
* MigrateConfig 执行配置迁移
*/
@@ -74,6 +65,14 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
return $resultPromise;
}
/**
* ServiceStartup initializes the service when the application starts
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3311949428, options) as any;
return $resultPromise;
}
/**
* Set 设置配置项
*/
@@ -83,34 +82,18 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
}
/**
* SetBackupConfigChangeCallback 设置备份配置变更回调
* Watch 注册配置变更监听器
*/
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3264871659, callback) as any;
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
return $resultPromise;
}
/**
* SetDataPathChangeCallback 设置数据路径配置变更回调
* WatchWithContext 使用 Context 注册监听器
*/
export function SetDataPathChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(393017412, callback) as any;
return $resultPromise;
}
/**
* SetHotkeyChangeCallback 设置热键配置变更回调
*/
export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(283872321, callback) as any;
return $resultPromise;
}
/**
* SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
*/
export function SetWindowSnapConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2324961653, callback) as any;
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
return $resultPromise;
}

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* HotkeyService Windows全局热键服务
* HotkeyService 全局热键服务
* @module
*/
@@ -48,8 +48,8 @@ export function IsRegistered(): Promise<boolean> & { cancel(): void } {
/**
* RegisterHotkey 注册全局热键
*/
export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1103945691, hotkey) as any;
export function RegisterHotkey(combo: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1103945691, combo) as any;
return $resultPromise;
}
@@ -62,7 +62,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
}
/**
* ServiceStartup initializes the service when the application starts
* ServiceStartup 服务启动时初始化
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3079990808, options) as any;
@@ -80,8 +80,8 @@ export function UnregisterHotkey(): Promise<void> & { cancel(): void } {
/**
* UpdateHotkey 更新热键配置
*/
export function UpdateHotkey(enable: boolean, hotkey: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(823285555, enable, hotkey) as any;
export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(823285555, enable, combo) as any;
return $resultPromise;
}

View File

@@ -5,9 +5,6 @@
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as http$0 from "../../../net/http/models.js";
@@ -15,6 +12,12 @@ import * as http$0 from "../../../net/http/models.js";
// @ts-ignore: Unused imports
import * as time$0 from "../../../time/models.js";
/**
* CancelFunc 取消订阅函数
* 调用此函数可以取消对配置的监听
*/
export type CancelFunc = any;
/**
* HttpRequest HTTP请求结构
*/
@@ -263,6 +266,11 @@ export class OSInfo {
}
}
/**
* ObserverCallback 观察者回调函数
*/
export type ObserverCallback = any;
/**
* SelfUpdateResult 自我更新结果
*/
@@ -394,62 +402,6 @@ export class SystemInfo {
}
}
/**
* WindowInfo 窗口信息
*/
export class WindowInfo {
"Window": application$0.WebviewWindow | null;
"DocumentID": number;
"Title": string;
/** Creates a new WindowInfo instance. */
constructor($$source: Partial<WindowInfo> = {}) {
if (!("Window" in $$source)) {
this["Window"] = null;
}
if (!("DocumentID" in $$source)) {
this["DocumentID"] = 0;
}
if (!("Title" in $$source)) {
this["Title"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new WindowInfo instance from a string or object.
*/
static createFrom($$source: any = {}): WindowInfo {
const $$createField0_0 = $$createType8;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("Window" in $$parsedSource) {
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
}
return new WindowInfo($$parsedSource as Partial<WindowInfo>);
}
}
/**
* WindowSnapService 窗口吸附服务
*/
export class WindowSnapService {
/** Creates a new WindowSnapService instance. */
constructor($$source: Partial<WindowSnapService> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new WindowSnapService instance from a string or object.
*/
static createFrom($$source: any = {}): WindowSnapService {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
}
}
// Private type creation functions
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
var $$createType1 = (function $$initCreateType1(...args): any {
@@ -463,5 +415,3 @@ const $$createType3 = $Create.Map($Create.Any, $$createType2);
const $$createType4 = OSInfo.createFrom;
const $$createType5 = $Create.Nullable($$createType4);
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
const $$createType7 = application$0.WebviewWindow.createFrom;
const $$createType8 = $Create.Nullable($$createType7);

View File

@@ -10,6 +10,14 @@
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* AutoShowHide 自动显示/隐藏主窗口
*/
export function AutoShowHide(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4044219428) as any;
return $resultPromise;
}
/**
* HandleWindowClose 处理窗口关闭事件
*/

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* WindowService 窗口管理服务(专注于窗口生命周期管理)
* WindowService 窗口管理服务
* @module
*/
@@ -12,15 +12,15 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* GetOpenWindows 获取所有打开的窗口信息
* GetOpenWindows 获取所有打开的文档窗口
*/
export function GetOpenWindows(): Promise<$models.WindowInfo[]> & { cancel(): void } {
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1464997251) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
@@ -51,13 +51,12 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
}
/**
* SetWindowSnapService 设置窗口吸附服务引用
* ServiceStartup 服务启动时初始化
*/
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2432987694, options) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $models.WindowInfo.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType0 = $Create.Array($Create.Any);

View File

@@ -33,13 +33,13 @@
"@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.1",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.6",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.2",
"@lezer/lr": "^1.4.3",
"@prettier/plugin-xml": "^3.4.2",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@toml-tools/lexer": "^1.0.0",
@@ -54,36 +54,36 @@
"jsox": "^1.2.123",
"linguist-languages": "^9.1.0",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.93.3",
"vue": "^3.5.22",
"sass": "^1.94.0",
"vue": "^3.5.24",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@eslint/js": "^9.39.0",
"@eslint/js": "^9.39.1",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.9.2",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest",
"cross-env": "^10.1.0",
"eslint": "^9.39.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.5.1",
"globals": "^16.5.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "^8.46.4",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.1.12",
"vite": "^7.2.2",
"vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.6",
"vitest": "^4.0.8",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.2"
"vue-tsc": "^3.1.3"
}
},
"node_modules/@babel/helper-string-parser": {
@@ -96,21 +96,21 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.4"
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -120,13 +120,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -539,9 +539,9 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.1.tgz",
"integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==",
"version": "6.9.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.2.tgz",
"integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -1154,9 +1154,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.0",
"resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.0.tgz",
"integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==",
"version": "9.39.1",
"resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1483,9 +1483,9 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"version": "1.4.3",
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.3.tgz",
"integrity": "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
@@ -2560,17 +2560,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
"integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz",
"integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/type-utils": "8.46.2",
"@typescript-eslint/utils": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2",
"@typescript-eslint/scope-manager": "8.46.4",
"@typescript-eslint/type-utils": "8.46.4",
"@typescript-eslint/utils": "8.46.4",
"@typescript-eslint/visitor-keys": "8.46.4",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -2584,7 +2584,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.46.2",
"@typescript-eslint/parser": "^8.46.4",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -2600,16 +2600,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.46.2.tgz",
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.46.4.tgz",
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2",
"@typescript-eslint/scope-manager": "8.46.4",
"@typescript-eslint/types": "8.46.4",
"@typescript-eslint/typescript-estree": "8.46.4",
"@typescript-eslint/visitor-keys": "8.46.4",
"debug": "^4.3.4"
},
"engines": {
@@ -2625,14 +2625,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.46.4.tgz",
"integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.46.2",
"@typescript-eslint/types": "^8.46.2",
"@typescript-eslint/tsconfig-utils": "^8.46.4",
"@typescript-eslint/types": "^8.46.4",
"debug": "^4.3.4"
},
"engines": {
@@ -2647,14 +2647,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz",
"integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2"
"@typescript-eslint/types": "8.46.4",
"@typescript-eslint/visitor-keys": "8.46.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2665,9 +2665,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz",
"integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2682,15 +2682,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
"integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz",
"integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2",
"@typescript-eslint/utils": "8.46.2",
"@typescript-eslint/types": "8.46.4",
"@typescript-eslint/typescript-estree": "8.46.4",
"@typescript-eslint/utils": "8.46.4",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -2707,9 +2707,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.46.2.tgz",
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.46.4.tgz",
"integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2721,16 +2721,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz",
"integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.46.2",
"@typescript-eslint/tsconfig-utils": "8.46.2",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2",
"@typescript-eslint/project-service": "8.46.4",
"@typescript-eslint/tsconfig-utils": "8.46.4",
"@typescript-eslint/types": "8.46.4",
"@typescript-eslint/visitor-keys": "8.46.4",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2776,16 +2776,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.46.2.tgz",
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.46.4.tgz",
"integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2"
"@typescript-eslint/scope-manager": "8.46.4",
"@typescript-eslint/types": "8.46.4",
"@typescript-eslint/typescript-estree": "8.46.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2800,13 +2800,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz",
"integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/types": "8.46.4",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -2842,17 +2842,17 @@
}
},
"node_modules/@vitest/expect": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.6.tgz",
"integrity": "sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.8.tgz",
"integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.0.6",
"@vitest/utils": "4.0.6",
"chai": "^6.0.1",
"@vitest/spy": "4.0.8",
"@vitest/utils": "4.0.8",
"chai": "^6.2.0",
"tinyrainbow": "^3.0.3"
},
"funding": {
@@ -2860,15 +2860,15 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.6.tgz",
"integrity": "sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.8.tgz",
"integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.0.6",
"@vitest/spy": "4.0.8",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.19"
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -2897,9 +2897,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.6.tgz",
"integrity": "sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.8.tgz",
"integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2910,13 +2910,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.6.tgz",
"integrity": "sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.8.tgz",
"integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.0.6",
"@vitest/utils": "4.0.8",
"pathe": "^2.0.3"
},
"funding": {
@@ -2924,14 +2924,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.6.tgz",
"integrity": "sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.8.tgz",
"integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.6",
"magic-string": "^0.30.19",
"@vitest/pretty-format": "4.0.8",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
@@ -2939,9 +2939,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.6.tgz",
"integrity": "sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.8.tgz",
"integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2949,13 +2949,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.6.tgz",
"integrity": "sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.8.tgz",
"integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.6",
"@vitest/pretty-format": "4.0.8",
"tinyrainbow": "^3.0.3"
},
"funding": {
@@ -2992,71 +2992,71 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.24.tgz",
"integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.4",
"@vue/shared": "3.5.22",
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.24",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz",
"integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.22",
"@vue/shared": "3.5.22"
"@vue/compiler-core": "3.5.24",
"@vue/shared": "3.5.24"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz",
"integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.4",
"@vue/compiler-core": "3.5.22",
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-ssr": "3.5.22",
"@vue/shared": "3.5.22",
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.24",
"@vue/compiler-dom": "3.5.24",
"@vue/compiler-ssr": "3.5.24",
"@vue/shared": "3.5.24",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.19",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz",
"integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/shared": "3.5.22"
"@vue/compiler-dom": "3.5.24",
"@vue/shared": "3.5.24"
}
},
"node_modules/@vue/devtools-api": {
"version": "7.7.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.5.tgz",
"integrity": "sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==",
"version": "7.7.8",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.8.tgz",
"integrity": "sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.5"
"@vue/devtools-kit": "^7.7.8"
}
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.5.tgz",
"integrity": "sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==",
"version": "7.7.8",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.8.tgz",
"integrity": "sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.5",
"@vue/devtools-shared": "^7.7.8",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
@@ -3066,18 +3066,18 @@
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.5.tgz",
"integrity": "sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==",
"version": "7.7.8",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.8.tgz",
"integrity": "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.2.tgz",
"integrity": "sha512-PyFDCqpdfYUT+oMLqcc61oHfJlC6yjhybaefwQjRdkchIihToOEpJ2Wu/Ebq2yrnJdd1EsaAvZaXVAqcxtnDxQ==",
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.3.tgz",
"integrity": "sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3112,53 +3112,53 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.22.tgz",
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.24.tgz",
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.22"
"@vue/shared": "3.5.24"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.22",
"@vue/shared": "3.5.22"
"@vue/reactivity": "3.5.24",
"@vue/shared": "3.5.24"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.22",
"@vue/runtime-core": "3.5.22",
"@vue/shared": "3.5.22",
"@vue/reactivity": "3.5.24",
"@vue/runtime-core": "3.5.24",
"@vue/shared": "3.5.24",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.22",
"@vue/shared": "3.5.22"
"@vue/compiler-ssr": "3.5.24",
"@vue/shared": "3.5.24"
},
"peerDependencies": {
"vue": "3.5.22"
"vue": "3.5.24"
}
},
"node_modules/@vue/shared": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.24.tgz",
"integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
@@ -3335,9 +3335,9 @@
}
},
"node_modules/alien-signals": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.0.5.tgz",
"integrity": "sha512-+2bRQFO1f9GLeIabDQWJlluL1NspZlLjpjaSSwwpl+9Tz5tS/3KrceHdwjNvIMEbYWSpoqtOPuXLTSoPgvIEWw==",
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.0.tgz",
"integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==",
"dev": true,
"license": "MIT"
},
@@ -3799,9 +3799,9 @@
}
},
"node_modules/chai": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.0.tgz",
"integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==",
"version": "6.2.1",
"resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.1.tgz",
"integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4513,9 +4513,9 @@
}
},
"node_modules/eslint": {
"version": "9.39.0",
"resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.0.tgz",
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
"version": "9.39.1",
"resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4525,7 +4525,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.0",
"@eslint/js": "9.39.1",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -5646,9 +5646,9 @@
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -6358,19 +6358,19 @@
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
"typescript": ">=4.5.0",
"vue": "^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
@@ -6910,9 +6910,9 @@
}
},
"node_modules/sass": {
"version": "1.93.3",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.93.3.tgz",
"integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
"version": "1.94.0",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.94.0.tgz",
"integrity": "sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
@@ -7162,9 +7162,9 @@
"license": "MIT"
},
"node_modules/std-env": {
"version": "3.9.0",
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz",
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
"version": "3.10.0",
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"devOptional": true,
"license": "MIT"
},
@@ -7488,16 +7488,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.46.2",
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
"integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
"version": "8.46.4",
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.46.4.tgz",
"integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2",
"@typescript-eslint/utils": "8.46.2"
"@typescript-eslint/eslint-plugin": "8.46.4",
"@typescript-eslint/parser": "8.46.4",
"@typescript-eslint/typescript-estree": "8.46.4",
"@typescript-eslint/utils": "8.46.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7917,9 +7917,9 @@
}
},
"node_modules/vite": {
"version": "7.1.12",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"version": "7.2.2",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8129,26 +8129,26 @@
"license": "MIT"
},
"node_modules/vitest": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.6.tgz",
"integrity": "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==",
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.8.tgz",
"integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.0.6",
"@vitest/mocker": "4.0.6",
"@vitest/pretty-format": "4.0.6",
"@vitest/runner": "4.0.6",
"@vitest/snapshot": "4.0.6",
"@vitest/spy": "4.0.6",
"@vitest/utils": "4.0.6",
"@vitest/expect": "4.0.8",
"@vitest/mocker": "4.0.8",
"@vitest/pretty-format": "4.0.8",
"@vitest/runner": "4.0.8",
"@vitest/snapshot": "4.0.8",
"@vitest/spy": "4.0.8",
"@vitest/utils": "4.0.8",
"debug": "^4.4.3",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
"magic-string": "^0.30.19",
"magic-string": "^0.30.21",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^3.9.0",
"std-env": "^3.10.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.15",
@@ -8169,10 +8169,10 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.0.6",
"@vitest/browser-preview": "4.0.6",
"@vitest/browser-webdriverio": "4.0.6",
"@vitest/ui": "4.0.6",
"@vitest/browser-playwright": "4.0.8",
"@vitest/browser-preview": "4.0.8",
"@vitest/browser-webdriverio": "4.0.8",
"@vitest/ui": "4.0.8",
"happy-dom": "*",
"jsdom": "*"
},
@@ -8234,16 +8234,16 @@
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.22.tgz",
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
"version": "3.5.24",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.24.tgz",
"integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-sfc": "3.5.22",
"@vue/runtime-dom": "3.5.22",
"@vue/server-renderer": "3.5.22",
"@vue/shared": "3.5.22"
"@vue/compiler-dom": "3.5.24",
"@vue/compiler-sfc": "3.5.24",
"@vue/runtime-dom": "3.5.24",
"@vue/server-renderer": "3.5.24",
"@vue/shared": "3.5.24"
},
"peerDependencies": {
"typescript": "*"
@@ -8339,14 +8339,14 @@
"license": "MIT"
},
"node_modules/vue-tsc": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.2.tgz",
"integrity": "sha512-3fd4DY0rFczs5f+VB3OhcLU83V6+3Puj2yLBe0Ak65k7ERk+STVNKaOAi0EBo6Lc15UiJB6LzU6Mxy4+h/pKew==",
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.3.tgz",
"integrity": "sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "2.4.23",
"@vue/language-core": "3.1.2"
"@vue/language-core": "3.1.3"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"

View File

@@ -11,10 +11,15 @@
"lint": "eslint",
"lint:fix": "eslint --fix",
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js",
"build:mermaid-parser": "node src/views/editor/language/mermaid/build-parsers.js",
"test": "vitest",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"app:dev": "cd .. &&wails3 dev",
"app:build": "cd .. && wails3 task build",
"app:package": "cd .. && wails3 package",
"app:generate": "cd .. && wails3 generate bindings -ts"
},
"dependencies": {
"@codemirror/autocomplete": "^6.19.1",
@@ -42,13 +47,13 @@
"@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.1",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.6",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.2",
"@lezer/lr": "^1.4.3",
"@prettier/plugin-xml": "^3.4.2",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@toml-tools/lexer": "^1.0.0",
@@ -63,35 +68,35 @@
"jsox": "^1.2.123",
"linguist-languages": "^9.1.0",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.93.3",
"vue": "^3.5.22",
"sass": "^1.94.0",
"vue": "^3.5.24",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@eslint/js": "^9.39.0",
"@eslint/js": "^9.39.1",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.9.2",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest",
"cross-env": "^10.1.0",
"eslint": "^9.39.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.5.1",
"globals": "^16.5.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "^8.46.4",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.1.12",
"vite": "^7.2.2",
"vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.6",
"vitest": "^4.0.8",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.2"
"vue-tsc": "^3.1.3"
}
}

View File

@@ -80,7 +80,7 @@ function animationLoop() {
// 等待一段时间后重置动画
resetTimeoutId = window.setTimeout(() => {
reset();
}, 750);
}, 500);
}
}
@@ -136,7 +136,8 @@ onBeforeUnmount(() => {
left: 0;
right: 0;
bottom: 0;
background: var(--voidraft-bg-gradient, radial-gradient(#222922, #000500));
//background: var(--voidraft-bg-gradient, rgba(0, 5, 0, 0.15));
//backdrop-filter: blur(2px);
z-index: 1000;
display: flex;
align-items: center;

View File

@@ -1,175 +1,49 @@
import { defineStore } from 'pinia';
import { computed, readonly, ref, shallowRef, watchEffect, onScopeDispose } from 'vue';
import type { GitBackupConfig } from '@/../bindings/voidraft/internal/models';
import { ref, onScopeDispose } from 'vue';
import { BackupService } from '@/../bindings/voidraft/internal/services';
import { useConfigStore } from '@/stores/configStore';
import { createTimerManager } from '@/common/utils/timerUtils';
// 备份状态枚举
export enum BackupStatus {
IDLE = 'idle',
PUSHING = 'pushing',
SUCCESS = 'success',
ERROR = 'error'
}
// 备份操作结果类型
export interface BackupResult {
status: BackupStatus;
message?: string;
timestamp?: number;
}
// 类型守卫函数
const isBackupError = (error: unknown): error is Error => {
return error instanceof Error;
};
// 工具类型:提取错误消息
type ErrorMessage<T> = T extends Error ? string : string;
export const useBackupStore = defineStore('backup', () => {
// === 核心状态 ===
const config = shallowRef<GitBackupConfig | null>(null);
const isPushing = ref(false);
const message = ref<string | null>(null);
const isError = ref(false);
// 统一的备份结果状态
const backupResult = ref<BackupResult>({
status: BackupStatus.IDLE
});
// === 定时器管理 ===
const statusTimer = createTimerManager();
// 组件卸载时清理定时器
onScopeDispose(() => {
statusTimer.clear();
});
// === 外部依赖 ===
const timer = createTimerManager();
const configStore = useConfigStore();
// === 计算属性 ===
const isEnabled = computed(() => configStore.config.backup.enabled);
const isConfigured = computed(() => Boolean(configStore.config.backup.repo_url?.trim()));
onScopeDispose(() => timer.clear());
// 派生状态计算属性
const isPushing = computed(() => backupResult.value.status === BackupStatus.PUSHING);
const isSuccess = computed(() => backupResult.value.status === BackupStatus.SUCCESS);
const isError = computed(() => backupResult.value.status === BackupStatus.ERROR);
const errorMessage = computed(() =>
backupResult.value.status === BackupStatus.ERROR ? backupResult.value.message : null
);
const pushToRemote = async () => {
const isConfigured = Boolean(configStore.config.backup.repo_url?.trim());
// === 状态管理方法 ===
/**
* 设置备份状态
* @param status 备份状态
* @param message 可选消息
* @param autoHide 是否自动隐藏(毫秒)
*/
const setBackupStatus = <T extends BackupStatus>(
status: T,
message?: T extends BackupStatus.ERROR ? string : string,
autoHide?: number
): void => {
statusTimer.clear();
backupResult.value = {
status,
message,
timestamp: Date.now()
};
// 自动隐藏逻辑
if (autoHide && (status === BackupStatus.SUCCESS || status === BackupStatus.ERROR)) {
statusTimer.set(() => {
if (backupResult.value.status === status) {
backupResult.value = { status: BackupStatus.IDLE };
}
}, autoHide);
}
};
/**
* 清除当前状态
*/
const clearStatus = (): void => {
statusTimer.clear();
backupResult.value = { status: BackupStatus.IDLE };
};
/**
* 处理错误的通用方法
*/
const handleError = (error: unknown): void => {
const message: ErrorMessage<typeof error> = isBackupError(error)
? error.message
: 'Backup operation failed';
setBackupStatus(BackupStatus.ERROR, message, 5000);
};
// === 业务逻辑方法 ===
/**
* 推送到远程仓库
* 使用现代 async/await 和错误处理
*/
const pushToRemote = async (): Promise<void> => {
// 前置条件检查
if (isPushing.value || !isConfigured.value) {
if (isPushing.value || !isConfigured) {
return;
}
try {
setBackupStatus(BackupStatus.PUSHING);
isPushing.value = true;
message.value = null;
timer.clear();
await BackupService.PushToRemote();
setBackupStatus(BackupStatus.SUCCESS, 'Backup completed successfully', 3000);
isError.value = false;
message.value = 'push successful';
timer.set(() => { message.value = null; }, 3000);
} catch (error) {
handleError(error);
isError.value = true;
message.value = error instanceof Error ? error.message : 'backup operation failed';
timer.set(() => { message.value = null; }, 5000);
} finally {
isPushing.value = false;
}
};
/**
* 重试备份操作
*/
const retryBackup = async (): Promise<void> => {
if (isError.value) {
await pushToRemote();
}
};
// === 响应式副作用 ===
// 监听配置变化,自动清除错误状态
watchEffect(() => {
if (isEnabled.value && isConfigured.value && isError.value) {
// 配置修复后清除错误状态
clearStatus();
}
});
// === 返回的 API ===
return {
// 只读状态
config: readonly(config),
backupResult: readonly(backupResult),
// 计算属性
isEnabled,
isConfigured,
isPushing,
isSuccess,
message,
isError,
errorMessage,
// 方法
pushToRemote,
retryBackup,
clearStatus
} as const;
pushToRemote
};
});

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { computed, readonly, ref, shallowRef, onScopeDispose } from 'vue';
import { computed, readonly, ref, onScopeDispose } from 'vue';
import { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
import { useConfigStore } from './configStore';

View File

@@ -55,9 +55,11 @@ onBeforeUnmount(() => {
<template>
<div class="editor-container">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
<div ref="editorElement" class="editor"></div>
<Toolbar/>
<transition name="loading-fade">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
</transition>
</div>
</template>
@@ -85,4 +87,15 @@ onBeforeUnmount(() => {
:deep(.cm-scroller) {
overflow: auto;
}
// 加载动画过渡效果
.loading-fade-enter-active,
.loading-fade-leave-active {
transition: opacity 0.3s ease;
}
.loading-fade-enter-from,
.loading-fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -18,7 +18,7 @@ BlockLanguage {
"go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" |
"ps1" | "dart" | "scala" | "math" | "dockerfile" | "lua" | "vue" | "lezer" |
"liquid" | "wast" | "sass" | "less" | "angular" | "svelte" |
"http"
"http" | "mermaid"
}
@tokens {

View File

@@ -24,7 +24,7 @@ import {lessLanguage} from "@codemirror/lang-less";
import {angularLanguage} from "@codemirror/lang-angular";
import { svelteLanguage } from "@replit/codemirror-lang-svelte";
import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language";
import { mermaidLanguage } from '@/views/editor/language/mermaid';
import {StreamLanguage} from "@codemirror/language";
import {ruby} from "@codemirror/legacy-modes/mode/ruby";
import {shell} from "@codemirror/legacy-modes/mode/shell";
@@ -226,6 +226,7 @@ export const LANGUAGES: LanguageInfo[] = [
}
}),
new LanguageInfo("http", "Http", httpLanguage.parser, ["http"]),
new LanguageInfo("mermaid", "Mermaid", mermaidLanguage.parser, ["mermaid"]),
];

View File

@@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr"
import {blockContent} from "./external-tokens.js"
export const parser = LRParser.deserialize({
version: 14,
states: "!jQQOQOOOVOQO'#C`O#xOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#}OSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$VOSO1G.fOOOP7+$Q7+$Q",
stateData: "$[~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO~OPVO~OUYO!TXO~O!TZO~O",
states: "!jQQOQOOOVOQO'#C`O#{OPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO$QOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$YOSO1G.fOOOP7+$Q7+$Q",
stateData: "$_~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO!TTO~OPVO~OUYO!UXO~O!UZO~O",
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
maxTerm: 51,
maxTerm: 52,
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "3u~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_)R#_#`*Q#`#a*]#a#b,Y#d#e,y#f#g-r#g#h.V#h#i0|#j#k2R#k#l2d#l#m2{#m#n3^R!fP!TQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eQ#a#b(k#h#i(v~(nP#`#a(q~(vO]~~(yP#d#e(|~)RO!S~~)UQ#T#U)[#g#h)m~)_P#j#k)b~)eP#T#U)h~)mO`~~)rPo~#c#d)u~)xP#b#c){~*QOZ~~*TP#h#i*W~*]Or~~*`R#X#Y*i#]#^+`#i#j+}~*lQ#g#h*r#n#o*}~*uP#g#h*x~*}O!P~~+QP#X#Y+T~+WP#f#g+Z~+`O{~~+cP#e#f+f~+iP#i#j+l~+oP#]#^+r~+uP#W#X+x~+}O|~~,QP#T#U,T~,YOy~~,]Q#T#U,c#W#X,t~,fP#h#i,i~,lP#[#],o~,tOw~~,yO_~~,|R#[#]-V#g#h-b#m#n-m~-YP#d#e-]~-bOa~~-eP!R!S-h~-mOt~~-rO[~~-uQ#U#V-{#g#h.Q~.QOg~~.VOe~~.YU#T#U.l#V#W.}#[#]/f#e#f/k#j#k/v#k#l0e~.oP#g#h.r~.uP#g#h.x~.}O!O~~/QP#T#U/T~/WP#`#a/Z~/^P#T#U/a~/fOv~~/kOh~~/nP#`#a/q~/vO^~~/yP#X#Y/|~0PP#`#a0S~0VP#h#i0Y~0]P#X#Y0`~0eO!R~~0hP#]#^0k~0nP#Y#Z0q~0tP#h#i0w~0|Oq~~1PR#X#Y1Y#c#d1k#g#h1|~1]P#l#m1`~1cP#h#i1f~1kOY~~1nP#a#b1q~1tP#`#a1w~1|Oj~~2ROp~~2UP#i#j2X~2[P#X#Y2_~2dOz~~2gP#T#U2j~2mP#g#h2p~2sP#h#i2v~2{O}~~3OP#a#b3R~3UP#`#a3X~3^Oc~~3aP#T#U3d~3gP#a#b3j~3mP#`#a3p~3uOi~",
tokenData: "4m~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_)R#_#`*Q#`#a*]#a#b,Y#d#e-q#f#g.j#g#h.}#h#i1t#j#k2y#k#l3[#l#m3s#m#n4UR!fP!UQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eQ#a#b(k#h#i(v~(nP#`#a(q~(vO]~~(yP#d#e(|~)RO!S~~)UQ#T#U)[#g#h)m~)_P#j#k)b~)eP#T#U)h~)mO`~~)rPo~#c#d)u~)xP#b#c){~*QOZ~~*TP#h#i*W~*]Or~~*`R#X#Y*i#]#^+`#i#j+}~*lQ#g#h*r#n#o*}~*uP#g#h*x~*}O!P~~+QP#X#Y+T~+WP#f#g+Z~+`O{~~+cP#e#f+f~+iP#i#j+l~+oP#]#^+r~+uP#W#X+x~+}O|~~,QP#T#U,T~,YOy~~,]R#T#U,f#W#X,w#X#Y,|~,iP#h#i,l~,oP#[#],r~,wOw~~,|O_~~-PP#f#g-S~-VP#a#b-Y~-]P#T#U-`~-cP#]#^-f~-iP#W#X-l~-qO!T~~-tR#[#]-}#g#h.Y#m#n.e~.QP#d#e.T~.YOa~~.]P!R!S.`~.eOt~~.jO[~~.mQ#U#V.s#g#h.x~.xOg~~.}Oe~~/QU#T#U/d#V#W/u#[#]0^#e#f0c#j#k0n#k#l1]~/gP#g#h/j~/mP#g#h/p~/uO!O~~/xP#T#U/{~0OP#`#a0R~0UP#T#U0X~0^Ov~~0cOh~~0fP#`#a0i~0nO^~~0qP#X#Y0t~0wP#`#a0z~0}P#h#i1Q~1TP#X#Y1W~1]O!R~~1`P#]#^1c~1fP#Y#Z1i~1lP#h#i1o~1tOq~~1wR#X#Y2Q#c#d2c#g#h2t~2TP#l#m2W~2ZP#h#i2^~2cOY~~2fP#a#b2i~2lP#`#a2o~2tOj~~2yOp~~2|P#i#j3P~3SP#X#Y3V~3[Oz~~3_P#T#U3b~3eP#g#h3h~3kP#h#i3n~3sO}~~3vP#a#b3y~3|P#`#a4P~4UOc~~4XP#T#U4[~4_P#a#b4b~4eP#`#a4h~4mOi~",
tokenizers: [blockContent, 0, 1],
topRules: {"Document":[0,2]},
tokenPrec: 0

View File

@@ -66,6 +66,7 @@ export type SupportedLanguage =
| 'angular'
| 'svelte'
| 'http' // HTTP Client
| 'mermaid'
/**
* 创建块的选项
@@ -85,7 +86,6 @@ export interface EditorOptions {
}
// 分隔符格式常量
export const DELIMITER_REGEX = /^\n∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/gm;
export const DELIMITER_PREFIX = '\n∞∞∞';

View File

@@ -0,0 +1,57 @@
import { buildParserFile } from '@lezer/generator';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { readFileSync, writeFileSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const parsersDir = join(__dirname, 'parsers');
const parserTypes = [
'mermaid',
'mindmap',
'pie',
'flowchart',
'sequence',
'journey',
'requirement',
'gantt'
];
console.log('开始构建 Mermaid 语法解析器...\n');
for (const type of parserTypes) {
try {
const grammarPath = join(parsersDir, type, `${type}.grammar`);
const outputPath = join(parsersDir, type, `${type}.parser.grammar.ts`);
console.log(`正在处理: ${type}`);
console.log(` 读取: ${grammarPath}`);
const grammar = readFileSync(grammarPath, 'utf-8');
const result = buildParserFile(grammar, {
fileName: `${type}.grammar`,
typeScript: true,
warn: (message) => console.warn(` 警告: ${message}`)
});
writeFileSync(outputPath, result.parser);
console.log(` ✓ 生成: ${outputPath}`);
// 生成 terms 文件
if (result.terms) {
const termsPath = join(parsersDir, type, `${type}.grammar.terms.ts`);
writeFileSync(termsPath, result.terms);
console.log(` ✓ 生成: ${termsPath}`);
}
console.log('');
} catch (error) {
console.error(` ✗ 错误: ${type} - ${error.message}\n`);
process.exit(1);
}
}
console.log('✓ 所有解析器构建完成!');

View File

@@ -0,0 +1,62 @@
import { foldService } from '@codemirror/language';
const countLeadingSpaces = (str: string) => {
let count = 0;
for (let i = 0; i < str.length; i++) {
if (str[i] === ' ') {
count++;
} else if (str[i] === '\t') {
count += 4;
} else {
break;
}
}
return count;
};
const isEmptyLine = (text: string) => {
return /^[ \t]*$/.test(text);
};
export const foldByIndent = () => {
return foldService.of((state, lineStart, lineEnd) => {
const line = state.doc.lineAt(lineStart);
const lineCount = state.doc.lines;
let indents = countLeadingSpaces(line.text);
let foldStart = lineStart;
let foldEnd = lineEnd;
let nextLine = line;
while (nextLine.number < lineCount) {
nextLine = state.doc.line(nextLine.number + 1);
if (nextLine.text === '' || isEmptyLine(nextLine.text)) continue;
let nextIndents = countLeadingSpaces(nextLine.text);
if (nextIndents > indents && !isEmptyLine(nextLine.text)) {
foldEnd = nextLine.to;
} else {
break;
}
}
if (
state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number
) {
return null;
}
foldStart = line.to;
const lineAtFoldStart = state.doc.lineAt(foldStart);
if (lineAtFoldStart.text === '' || isEmptyLine(lineAtFoldStart.text)) {
return null;
}
return { from: foldStart, to: foldEnd };
});
};

View File

@@ -0,0 +1,45 @@
export {
mermaidLanguage,
mindmapLanguage,
pieLanguage,
flowchartLanguage,
sequenceLanguage,
journeyLanguage,
requirementLanguage,
ganttLanguage,
} from './language-definitions';
export {
mermaidLanguageDescription,
mindmapLanguageDescription,
pieLanguageDescription,
flowchartLanguageDescription,
sequenceLanguageDescription,
journeyLanguageDescription,
requirementLanguageDescription,
ganttLanguageDescription,
} from './language-descriptions';
export {
mermaid,
mindmap,
pie,
flowchart,
sequence,
journey,
requirement,
gantt,
} from './language-support';
export {
mermaidTags,
mindmapTags,
pieTags,
flowchartTags,
sequenceTags,
journeyTags,
requirementTags,
ganttTags,
} from './tags';
export { foldByIndent } from './extensions';

View File

@@ -0,0 +1,74 @@
import { LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common';
import {
mermaidParser,
mindmapParser,
pieParser,
flowchartParser,
sequenceParser,
journeyParser,
requirementParser,
ganttParser,
} from '../parsers';
import { DiagramType, MermaidLanguageType } from '../types';
export const mermaidLanguage = LRLanguage.define({
name: MermaidLanguageType.Mermaid,
parser: mermaidParser.configure({
wrap: parseMixed((node) => {
switch (node.name) {
case DiagramType.Mindmap:
return { parser: mindmapParser };
case DiagramType.Pie:
return { parser: pieParser };
case DiagramType.Flowchart:
return { parser: flowchartParser };
case DiagramType.Sequence:
return { parser: sequenceParser };
case DiagramType.Journey:
return { parser: journeyParser };
case DiagramType.Requirement:
return { parser: requirementParser };
case DiagramType.Gantt:
return { parser: ganttParser };
default:
return null;
}
}),
}),
});
export const mindmapLanguage = LRLanguage.define({
name: MermaidLanguageType.Mindmap,
parser: mindmapParser,
});
export const pieLanguage = LRLanguage.define({
name: MermaidLanguageType.Pie,
parser: pieParser,
});
export const flowchartLanguage = LRLanguage.define({
name: MermaidLanguageType.Flowchart,
parser: flowchartParser,
});
export const sequenceLanguage = LRLanguage.define({
name: MermaidLanguageType.Sequence,
parser: sequenceParser,
});
export const journeyLanguage = LRLanguage.define({
name: MermaidLanguageType.Journey,
parser: journeyParser,
});
export const requirementLanguage = LRLanguage.define({
name: MermaidLanguageType.Requirement,
parser: requirementParser,
});
export const ganttLanguage = LRLanguage.define({
name: MermaidLanguageType.Gantt,
parser: ganttParser,
});

View File

@@ -0,0 +1,71 @@
import { LanguageDescription } from '@codemirror/language';
import {
mermaid,
mindmap,
pie,
flowchart,
sequence,
journey,
requirement,
gantt,
} from '../language-support';
import { MermaidDescriptionName, MermaidAlias } from '../types';
export const mermaidLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Mermaid,
load: async () => {
return mermaid();
},
});
export const mindmapLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Mindmap,
load: async () => {
return mindmap();
},
});
export const pieLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Pie,
load: async () => {
return pie();
},
});
export const flowchartLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Flowchart,
alias: [MermaidAlias.Graph],
load: async () => {
return flowchart();
},
});
export const sequenceLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Sequence,
alias: [MermaidAlias.Sequence],
load: async () => {
return sequence();
},
});
export const journeyLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Journey,
load: async () => {
return journey();
},
});
export const requirementLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Requirement,
alias: [MermaidAlias.Requirement],
load: async () => {
return requirement();
},
});
export const ganttLanguageDescription = LanguageDescription.of({
name: MermaidDescriptionName.Gantt,
load: async () => {
return gantt();
},
});

View File

@@ -0,0 +1,43 @@
import { LanguageSupport } from '@codemirror/language';
import {
mermaidLanguage,
mindmapLanguage,
pieLanguage,
flowchartLanguage,
sequenceLanguage,
journeyLanguage,
requirementLanguage,
ganttLanguage,
} from '../language-definitions';
export function mermaid() {
return new LanguageSupport(mermaidLanguage);
}
export function mindmap() {
return new LanguageSupport(mindmapLanguage);
}
export function pie() {
return new LanguageSupport(pieLanguage);
}
export function flowchart() {
return new LanguageSupport(flowchartLanguage);
}
export function sequence() {
return new LanguageSupport(sequenceLanguage);
}
export function journey() {
return new LanguageSupport(journeyLanguage);
}
export function requirement() {
return new LanguageSupport(requirementLanguage);
}
export function gantt() {
return new LanguageSupport(ganttLanguage);
}

View File

@@ -0,0 +1,170 @@
@top FlowchartDiagram { document+ }
@skip { spaces | LineComment }
@skip {} {
String {
singleQuote (stringContentSingle)* (singleQuote) |
doubleQuote (stringContentDouble)* (doubleQuote) |
backTick (stringContentBackTick)* (backTick)
}
}
document {
(
DiagramName |
DiagramName Orientation |
DiagramName Orientation newlines* subDocument (newlines* subDocument semicolon?)*
) newlines*
}
subDocument {
NodeId |
Node |
Link |
NodeEdge |
ampersand |
Keyword |
emptyParentheses |
colon |
tripleColon |
String |
StyleKeyword NodeId StyleText
}
NodeId {
identifier | Orientation
}
text {
NodeText | String
}
edgeText {
NodeEdgeText | String
}
emptyParentheses { "()" }
Node {
"(" text ")" |
"[" text "]" |
"|" text "|" |
"([" text "])" |
"[(" text "])" |
"[[" text "]]" |
"[(" text ")]" |
"((" text "))" |
">" text "]" |
"{" text "}" |
"{{" text "}}" |
"(((" text ")))"
}
NodeEdge {
(DoubleHyphen | DoubleEqual) edgeText Link
}
Link {
linkType1 |
linkType2 |
linkType3 |
linkType4 |
linkType5 |
linkType6
}
DiagramName {
kw<"flowchart"> |
kw<"graph">
}
Orientation {
kw<"TB"> |
kw<"TD"> |
kw<"BT"> |
kw<"RL"> |
kw<"LR">
}
StyleKeyword {
kw<"style"> |
kw<"linkStyle"> |
kw<"class"> |
kw<"classDef">
}
Keyword {
kw<"subgraph"> |
kw<"end"> |
kw<"direction"> |
kw<"click"> |
kw<"call"> |
kw<"href"> |
kw<"_self"> |
kw<"_blank"> |
kw<"_parent"> |
kw<"_to">
}
kw<term> { @specialize<identifier, term> }
@external tokens nodeEdgeText from "./tokens" { NodeEdgeText }
@external tokens nodeText from "./tokens" { NodeText }
@external tokens styleText from "./tokens" { StyleText }
/*
Single character tokens will need to go inside the @tokens rule to specify precedence over "char".
Longer tokens always beat shorter tokens, which is why "flowchart" takes priority over multiple "char" tokens.
The @specialize rule also helps makes "DiagramName" have higher priority for "flowchart" even though it overlaps with "char" and "identifier" tokens
*/
@tokens {
char { @asciiLetter | @digit | $[!"\#$%&'*+\.`?\\_\/\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC] }
identifier { char+ }
newlines { $[\n]+ }
spaces { @whitespace+ }
stringContentSingle { ![']+ }
stringContentDouble { !["]+ }
stringContentBackTick { ![`]+ }
LineComment { "%%" ![\n]* }
DoubleHyphen { "--" }
DoubleEqual { "==" }
linkType1 { ("<-" | "x-" | "o-") ("->" | "-x" | "-o")}
linkType2 { ("<=" | "x=" | "o=") ("=>" | "=x" | "=o")}
linkType3 { ("<-" | "x-" | "o-")? "-"+ ("->" | "-x" | "-o")?}
linkType4 { ("<=" | "x=" | "o=")? "="+ ("=>" | "=x" | "=o")?}
linkType5 { ("<-" | "x-" | "o-" | "-")? "."+ ("->" | "-x" | "-o" | "-")?}
linkType6 { "~~~" | "---" | "===" }
ampersand { "&" }
semicolon { ";" }
singleQuote { "'" }
doubleQuote { '"' }
backTick { "`" }
tripleColon[@name=":::"] { ":::" }
colon[@name=":"] { ":" }
@precedence {
newlines,
spaces,
LineComment,
linkType1,
linkType2,
linkType3,
linkType4,
linkType5,
linkType6,
DoubleHyphen,
DoubleEqual,
ampersand,
semicolon,
singleQuote,
doubleQuote,
backTick,
tripleColon,
colon,
identifier
}
}
@external propSource flowchartHighlighting from "./highlight"

View File

@@ -0,0 +1,3 @@
export declare const NodeText: number;
export declare const NodeEdgeText: number;
export declare const StyleText: number;

View File

@@ -0,0 +1,20 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
NodeEdgeText = 1,
NodeText = 2,
StyleText = 3,
LineComment = 4,
FlowchartDiagram = 5,
DiagramName = 6,
Orientation = 7,
NodeId = 8,
Node = 9,
String = 10,
Link = 11,
NodeEdge = 12,
DoubleHyphen = 13,
DoubleEqual = 14,
Keyword = 15,
colon = 16,
tripleColon = 17,
StyleKeyword = 18

View File

@@ -0,0 +1,217 @@
import { describe, it, expect } from 'vitest';
import { parser } from './flowchart.parser.grammar';
/**
* Flowchart Grammar 测试
*
* 测试目标:验证标准的 Mermaid Flowchart 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Flowchart Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 flowchart 声明TB 方向)', () => {
const code = `flowchart TB
A
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析基础的 graph 声明LR 方向)', () => {
const code = `graph LR
A
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带方框节点的流程图', () => {
const code = `flowchart TD
A[Christmas]
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带箭头连接的节点', () => {
const code = `flowchart TD
A --> B
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带标签的连接线', () => {
const code = `flowchart TD
A -- text --> B
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析不同形状的节点(圆形)', () => {
const code = `flowchart TD
A((Circle))
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析不同形状的节点(菱形)', () => {
const code = `flowchart TD
A{Diamond}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析完整的流程图示例', () => {
const code = `flowchart TD
A[Start] --> B{Is it?}
B -->|Yes| C[OK]
B -->|No| D[End]
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
import { styleTags, tags as t } from '@lezer/highlight';
import { flowchartTags } from '../../tags';
export const flowchartHighlighting = styleTags({
'( )': t.paren,
'[ ]': t.squareBracket,
'{ }': t.brace,
'<': t.angleBracket,
DiagramName: flowchartTags.diagramName,
DoubleEqual: flowchartTags.link,
DoubleHyphen: flowchartTags.link,
Keyword: flowchartTags.keyword,
LineComment: flowchartTags.lineComment,
Link: flowchartTags.link,
NodeEdge: flowchartTags.nodeEdge,
NodeEdgeText: flowchartTags.nodeEdgeText,
NodeId: flowchartTags.nodeId,
NodeText: flowchartTags.nodeText,
Number: flowchartTags.number,
Orientation: flowchartTags.orientation,
String: flowchartTags.string,
});

View File

@@ -0,0 +1,55 @@
import { ExternalTokenizer } from '@lezer/lr';
import { NodeText, NodeEdgeText, StyleText } from './flowchart.grammar.terms';
const skipCodePoints = [-1, 9, 13, 32, 34, 39, 96];
const startBracketCodePoints = [40, 62, 91, 123, 124];
const endBracketCodePoints = [41, 93, 124, 125];
const hyphen = 45;
const equal = 61;
const dot = 46;
export const nodeText = new ExternalTokenizer((input) => {
if (
skipCodePoints.includes(input.next) ||
startBracketCodePoints.includes(input.next)
)
return;
while (!endBracketCodePoints.includes(input.next) && input.next !== -1) {
input.advance();
}
input.acceptToken(NodeText);
});
export const nodeEdgeText = new ExternalTokenizer((input) => {
if (
skipCodePoints.includes(input.next) ||
startBracketCodePoints.includes(input.next) ||
input.next === hyphen ||
input.next === equal ||
input.next === dot
)
return;
while (
input.next !== hyphen &&
input.next !== equal &&
input.next !== dot &&
input.next !== -1
) {
input.advance();
}
input.acceptToken(NodeEdgeText);
});
export const styleText = new ExternalTokenizer((input) => {
if (input.next === 10 || input.next === -1) return;
while (input.next !== 10 && input.next !== -1) {
input.advance();
}
input.acceptToken(StyleText);
});

View File

@@ -0,0 +1,60 @@
@top GanttDiagram {
document
}
@skip { spaces }
document {
DiagramName newlines? |
DiagramName newlines (subDocument newlines?)+
}
subDocument {
Title ImportantText |
Section ImportantText |
DateFormat Text |
AxisFormat Text |
Excludes Text |
TickInterval Text |
TodayMarker Text |
Weekday Text |
Text |
InclusiveEndDates |
LineComment
}
ImportantText {
text
}
Text {
text
}
DiagramName { kw<"gantt"> }
kw<term> { @specialize<identifier, term> }
@external tokens textToken from "./tokens" {
AxisFormat[group=Keyword],
DateFormat[group=Keyword],
Excludes[group=Keyword],
InclusiveEndDates[group=Keyword],
TickInterval[group=Keyword],
Title[group=Keyword],
TodayMarker[group=Keyword],
Weekday[group=Keyword],
Section,
text
}
@tokens {
identifier { @asciiLetter+ }
spaces { @whitespace+ }
newlines { $[\n]+ }
LineComment { "%%" ![\n]* }
@precedence { newlines, spaces }
}
@external propSource ganttHighlighting from "./highlight"

View File

@@ -0,0 +1,10 @@
export declare const AxisFormat: number;
export declare const DateFormat: number;
export declare const Excludes: number;
export declare const InclusiveEndDates: number;
export declare const Section: number;
export declare const TickInterval: number;
export declare const Title: number;
export declare const TodayMarker: number;
export declare const Weekday: number;
export declare const text: number;

View File

@@ -0,0 +1,17 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
AxisFormat = 1,
DateFormat = 2,
Excludes = 3,
InclusiveEndDates = 4,
TickInterval = 5,
Title = 6,
TodayMarker = 7,
Weekday = 8,
Section = 9,
text = 17,
GanttDiagram = 10,
DiagramName = 11,
ImportantText = 12,
Text = 13,
LineComment = 14

View File

@@ -0,0 +1,273 @@
import { describe, it, expect } from 'vitest';
import { parser } from './gantt.parser.grammar';
/**
* Gantt Diagram Grammar 测试
*
* 测试目标:验证标准的 Mermaid Gantt Diagram 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Gantt Diagram Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 gantt 声明', () => {
const code = `gantt
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带标题的 gantt 图', () => {
const code = `gantt
title A Gantt Diagram
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带日期格式的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带章节的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
section Section
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带任务的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
title Adding GANTT diagram functionality to mermaid
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带活动任务的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
section A section
Active task :active, des2, 2014-01-09, 3d
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 axisFormat 的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
axisFormat %m-%d
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 excludes 的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
excludes weekends
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 todayMarker 的 gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
todayMarker off
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析完整的 gantt 图示例', () => {
const code = `gantt
dateFormat YYYY-MM-DD
title Adding GANTT diagram functionality to mermaid
excludes weekends
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task : des3, after des2, 5d
Future task2 : des4, after des3, 5d
section Critical tasks
Completed task in the critical line :crit, done, 2014-01-06,24h
Implement parser and jison :crit, done, after des1, 2d
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,24 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {textToken} from "./tokens"
import {ganttHighlighting} from "./highlight"
const spec_identifier = {__proto__:null,gantt:44}
export const parser = LRParser.deserialize({
version: 14,
states: "!|OVQQOOO[QQO'#CpQOQQOOOOQO'#Cg'#CgO!XQRO,59[OOQP'#Ci'#CiO!`QRO'#CtO!SQRO'#CtOOQP'#Ct'#CtO!eQRO'#CkO#`QRO1G.vOOQP'#Ch'#ChOOQP,59`,59`OOQP,59V,59VOOQP-E6i-E6i",
stateData: "#j~OcOS~OfRO~OgSO`dX~OPVOQVORVOSWOTVOUUOVVOWVOXUO^WOaTO~O`da~PdOaZO~Og]OP_XQ_XR_XS_XT_XU_XV_XW_XX_X^_X`_Xa_X~O`di~PdOgc~",
goto: "!UiPPPPPPPPPPPjmpPwPPPP}PPP!QRPOR[USWSYR[VQYSR^YRQOTXSY",
nodeNames: "⚠ AxisFormat DateFormat Excludes InclusiveEndDates TickInterval Title TodayMarker Weekday Section GanttDiagram DiagramName ImportantText Text LineComment",
maxTerm: 24,
nodeProps: [
["group", -8,1,2,3,4,5,6,7,8,"Keyword"]
],
propSources: [ganttHighlighting],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "$l~R_XY!QYZ!uZ^!Qpq!Quv#r!c!}$a#T#o$a#y#z!Q$f$g!Q#BY#BZ!Q$IS$I_!Q$I|$JO!Q$JT$JU!Q$KV$KW!Q&FU&FV!Q~!VYc~X^!Qpq!Q#y#z!Q$f$g!Q#BY#BZ!Q$IS$I_!Q$I|$JO!Q$JT$JU!Q$KV$KW!Q&FU&FV!Q~!|[g~c~XY!QYZ!uZ^!Qpq!Q#y#z!Q$f$g!Q#BY#BZ!Q$IS$I_!Q$I|$JO!Q$JT$JU!Q$KV$KW!Q&FU&FV!Q~#uPuv#x~#}S^~OY#xZ;'S#x;'S;=`$Z<%lO#x~$^P;=`<%l#x~$fQe~!c!}$a#T#o$a",
tokenizers: [textToken, 0],
topRules: {"GanttDiagram":[0,10]},
specialized: [{term: 21, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
tokenPrec: 115
})

View File

@@ -0,0 +1,9 @@
import { styleTags } from '@lezer/highlight';
import { ganttTags } from '../../tags';
export const ganttHighlighting = styleTags({
'DiagramName Section': ganttTags.diagramName,
Keyword: ganttTags.keyword,
ImportantText: ganttTags.string,
LineComment: ganttTags.lineComment,
});

View File

@@ -0,0 +1,59 @@
import { ExternalTokenizer } from '@lezer/lr';
import {
AxisFormat,
DateFormat,
Excludes,
InclusiveEndDates,
Section,
TickInterval,
Title,
TodayMarker,
Weekday,
text,
} from './gantt.grammar.terms';
const keywordMap: { [key: string]: number } = {
axisFormat: AxisFormat,
dateFormat: DateFormat,
excludes: Excludes,
inclusiveEndDates: InclusiveEndDates,
section: Section,
tickInterval: TickInterval,
title: Title,
todayMarker: TodayMarker,
weekday: Weekday,
};
const keywords = Object.keys(keywordMap);
export const textToken = new ExternalTokenizer((input) => {
if (input.next === 32 || input.next === 10 || input.next === -1) return;
if (input.next === 37 && input.peek(1) === 37) {
return;
}
let tokens = '';
while (input.next !== 10 && input.next !== -1) {
tokens += String.fromCodePoint(input.next);
input.advance();
}
const activeKeyword = keywords.filter((keyword) => {
if (keyword === tokens) {
return tokens.startsWith(keyword);
}
return tokens.startsWith(keyword + ' ');
});
if (activeKeyword.length > 0) {
input.acceptToken(
keywordMap[activeKeyword[0]],
activeKeyword[0].length - tokens.length
);
return;
}
input.acceptToken(text);
});

View File

@@ -0,0 +1,8 @@
export { parser as mermaidParser } from './mermaid/mermaid.parser.grammar';
export { parser as mindmapParser } from './mindmap/mindmap.parser.grammar';
export { parser as pieParser } from './pie/pie.parser.grammar';
export { parser as flowchartParser } from './flowchart/flowchart.parser.grammar';
export { parser as sequenceParser } from './sequence/sequence.parser.grammar';
export { parser as journeyParser } from './journey/journey.parser.grammar';
export { parser as requirementParser } from './requirement/requirement.parser.grammar';
export { parser as ganttParser } from './gantt/gantt.parser.grammar';

View File

@@ -0,0 +1,11 @@
import { styleTags } from '@lezer/highlight';
import { journeyTags } from '../../tags';
export const journeyHighlighting = styleTags({
DiagramName: journeyTags.diagramName,
'Text TaskName': journeyTags.text,
Actor: journeyTags.actor,
Keyword: journeyTags.keyword,
LineComment: journeyTags.lineComment,
Score: journeyTags.score,
});

View File

@@ -0,0 +1,59 @@
@top JourneyDiagram {
document
}
@skip { spaces }
document {
DiagramName newlines* (
() |
subDocument newlines* |
subDocument (newlines+ subDocument)+ newlines*
)
}
subDocument {
LineComment |
Keyword Text |
Task
}
Task {
TaskName ":" Score (":" Actor ("," Actor)*)?
}
Text {
text1
}
TaskName {
text2
}
Score {
text2
}
Actor {
text3
}
DiagramName { kw<"journey"> }
kw<term> { @specialize<identifier, term> }
@external tokens keywordTokens from "./tokens" { Keyword }
@external tokens textTokens1 from "./tokens" { text1 }
@external tokens textTokens2 from "./tokens" { text2 }
@external tokens textTokens3 from "./tokens" { text3 }
@tokens {
spaces { @whitespace+ }
newlines { $[\n]+ }
LineComment { "%%" ![\n]* }
identifier { @asciiLetter+ }
@precedence { newlines, spaces }
}
@external propSource journeyHighlighting from "./highlight"

View File

@@ -0,0 +1,5 @@
export declare const Keyword: number;
export declare const text1: number;
export declare const text2: number;
export declare const text3: number;

View File

@@ -0,0 +1,14 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Keyword = 1,
text1 = 14,
text2 = 15,
text3 = 16,
JourneyDiagram = 2,
DiagramName = 3,
LineComment = 4,
Text = 5,
Task = 6,
TaskName = 7,
Score = 8,
Actor = 9

View File

@@ -0,0 +1,234 @@
import { describe, it, expect } from 'vitest';
import { parser } from './journey.parser.grammar';
/**
* Journey Diagram Grammar 测试
*
* 测试目标:验证标准的 Mermaid Journey Diagram 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Journey Diagram Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 journey 声明', () => {
const code = `journey
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带标题的 journey 图', () => {
const code = `journey
title My working day
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带章节的 journey 图', () => {
const code = `journey
title My working day
section Go to work
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带任务和分数的 journey 图', () => {
const code = `journey
title My working day
section Go to work
Make tea: 5
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带任务、分数和参与者的 journey 图', () => {
const code = `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带多个参与者的任务', () => {
const code = `journey
title My working day
section Go to work
Make tea: 5: Me, Cat
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析多个章节的 journey 图', () => {
const code = `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
section Work
Do work: 1: Me, Cat
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析完整的 journey 图示例', () => {
const code = `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 5: Me
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,21 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {keywordTokens, textTokens1, textTokens2, textTokens3} from "./tokens"
import {journeyHighlighting} from "./highlight"
const spec_identifier = {__proto__:null,journey:42}
export const parser = LRParser.deserialize({
version: 14,
states: "%^OVQ`OOO[QeO'#CoQOQ`OOOOQT'#C_'#C_OOQT'#Cf'#CfOmQeO,59ZOOQO'#Cc'#CcO!OQ`O'#CbOOQO'#Cs'#CsO!TQbO'#CsOvQ`O,59ZOOQT-E6d-E6dO!YQ`O1G.uO!bQdO,58|OOQO'#Ca'#CaOOQO,59_,59_O!gQeO1G.uO!YQ`O1G.uO!xQeO7+$aO#RQ`O7+$aOOQO'#Cd'#CdO#ZQ`O1G.hOOQO,59S,59SOOQO-E6f-E6fO#fQeO<<G{O#wQhO7+$SP#|QeO'#CfOOQO'#Ce'#CeO$[Q`O<<GnO#wQhO'#CgO$gQ`OAN=YOOQO,59R,59ROOQO-E6e-E6e",
stateData: "$u~ObOS~OeRO~OPXOSWO_UOfSO]cX~OPXOSWO_UOfSO]ca~Oh]O~O^^O~OfSO]ci~O_dO~OPXOSWO_UOfSO]ci~OPXOSWO_UOfSO]cq~OhiO]UifUi~OPXOSWO_UOfSO]cy~O`kO~OPXOSWO_UOfSO~OimO]UyfUy~OimO]U!RfU!R~Ofb~",
goto: "#_hPPPiPlow!P!S!Y!n!tPPPPPP#OPPP#RRPOR_X]WPT`bhj]VPT`bhjRe]QliRomQTPYZT`bhjQ`YSb[aRhcQnlRpnQaYQc[TgacRQOQYPQ[TXf`bhj",
nodeNames: "⚠ Keyword JourneyDiagram DiagramName LineComment Text Task TaskName Score Actor",
maxTerm: 25,
propSources: [journeyHighlighting],
skippedNodes: [0],
repeatNodeCount: 3,
tokenData: "$|~RaXY!WYZ!{Z^!Wpq!Wuv#x|}$g![!]$l!c!}$q#T#o$q#y#z!W$f$g!W#BY#BZ!W$IS$I_!W$I|$JO!W$JT$JU!W$KV$KW!W&FU&FV!W~!]Yb~X^!Wpq!W#y#z!W$f$g!W#BY#BZ!W$IS$I_!W$I|$JO!W$JT$JU!W$KV$KW!W&FU&FV!W~#S[f~b~XY!WYZ!{Z^!Wpq!W#y#z!W$f$g!W#BY#BZ!W$IS$I_!W$I|$JO!W$JT$JU!W$KV$KW!W&FU&FV!W~#{Puv$O~$TSS~OY$OZ;'S$O;'S;=`$a<%lO$O~$dP;=`<%l$O~$lOi~~$qOh~~$vQd~!c!}$q#T#o$q",
tokenizers: [keywordTokens, textTokens1, textTokens2, textTokens3, 0],
topRules: {"JourneyDiagram":[0,2]},
specialized: [{term: 20, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
tokenPrec: 172
})

View File

@@ -0,0 +1,69 @@
import { ExternalTokenizer } from '@lezer/lr';
import { Keyword, text1, text2, text3 } from './journey.grammar.terms';
import type { InputStream } from '@lezer/lr';
const skipCodePoints = [-1, 9, 10, 13, 32];
const keywords = ['title', 'section'];
const isComment = (input: InputStream) => {
return input.peek(0) === 37 && input.peek(1) === 37;
};
const shouldSkip = (input: InputStream) => {
return skipCodePoints.includes(input.next) || isComment(input);
};
export const keywordTokens = new ExternalTokenizer((input) => {
if (shouldSkip(input)) return;
let tokens = '';
while (!skipCodePoints.includes(input.next)) {
tokens += String.fromCodePoint(input.next);
input.advance();
}
const activeKeyword = keywords.filter((keyword) => {
if (keyword === tokens) {
return tokens.toLowerCase().startsWith(keyword);
}
return tokens.toLowerCase().startsWith(keyword + ' '); // ensure the keyword isn't used as a token unless there's a space at the end e.g. titleStuff
});
if (activeKeyword.length > 0) {
input.acceptToken(Keyword, activeKeyword[0].length - tokens.length);
return;
}
});
export const textTokens1 = new ExternalTokenizer((input) => {
if (shouldSkip(input)) return;
while (input.next !== 10 && input.next !== -1) {
input.advance();
}
input.acceptToken(text1);
});
export const textTokens2 = new ExternalTokenizer((input) => {
if (shouldSkip(input)) return;
while (input.next !== 58 && input.next !== 10 && input.next !== -1) {
input.advance();
}
input.acceptToken(text2);
});
export const textTokens3 = new ExternalTokenizer((input) => {
if (shouldSkip(input)) return;
while (input.next !== 44 && input.next !== 10 && input.next !== -1) {
input.advance();
}
input.acceptToken(text3);
});

View File

@@ -0,0 +1,29 @@
// See this link for entire Pie chart syntax in Mermaid: https://mermaid.js.org/syntax/pie.html#syntax
@top MermaidDiagram {
preDiagramLine* (
PieDiagram |
MindmapDiagram |
FlowchartDiagram |
SequenceDiagram |
JourneyDiagram |
RequirementDiagram |
GanttDiagram
)
}
@skip { space }
@tokens {
space { $[ \t\r]+ }
}
@external tokens diagramText from "./tokens" {
preDiagramLine,
PieDiagram,
MindmapDiagram,
FlowchartDiagram,
SequenceDiagram,
JourneyDiagram,
RequirementDiagram,
GanttDiagram
}

View File

@@ -0,0 +1,8 @@
export declare const preDiagramLine: number;
export declare const MindmapDiagram: number;
export declare const PieDiagram: number;
export declare const FlowchartDiagram: number;
export declare const SequenceDiagram: number;
export declare const JourneyDiagram: number;
export declare const RequirementDiagram: number;
export declare const GanttDiagram: number;

View File

@@ -0,0 +1,11 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
preDiagramLine = 11,
PieDiagram = 1,
MindmapDiagram = 2,
FlowchartDiagram = 3,
SequenceDiagram = 4,
JourneyDiagram = 5,
RequirementDiagram = 6,
GanttDiagram = 7,
MermaidDiagram = 8

View File

@@ -0,0 +1,283 @@
import { describe, it, expect } from 'vitest';
import { parser } from './mermaid.parser.grammar';
/**
* Mermaid Grammar 测试
*
* 测试目标:验证标准的 Mermaid 综合语法是否能正确解析,不应该出现错误节点(⚠)
* 这个测试涵盖所有类型的 Mermaid 图表
*/
describe('Mermaid Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析 Pie 图', () => {
const code = `pie title Pets
"Dogs" : 386
"Cats" : 85
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 Mindmap 图', () => {
const code = `mindmap
root((mindmap))
Origins
Long history
Research
On effectiveness
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 Flowchart 图', () => {
const code = `flowchart TD
A[Start] --> B{Is it?}
B -->|Yes| C[OK]
B -->|No| D[End]
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 Sequence 图', () => {
const code = `sequenceDiagram
Alice->>John: Hello John
John-->>Alice: Great!
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 Journey 图', () => {
const code = `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 Requirement 图', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
text: the test text
risk: high
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 Gantt 图', () => {
const code = `gantt
dateFormat YYYY-MM-DD
title Adding GANTT diagram
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带注释的 flowchart 图', () => {
const code = `%% This is a comment
flowchart TD
A[Start] --> B[End]
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带空行的 sequence 图', () => {
const code = `
sequenceDiagram
participant Alice
Alice->>John: Hello
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析 graph 类型的流程图', () => {
const code = `graph LR
A[Square Rect] -- Link text --> B((Circle))
A --> C(Round Rect)
B --> D{Rhombus}
C --> D
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,17 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {diagramText} from "./tokens"
export const parser = LRParser.deserialize({
version: 14,
states: "nOVQROOOOQQ'#Ce'#CeOVQROOQOQPOOOOQQ-E6c-E6c",
stateData: "q~O]OS~OPROQRORROSROTROUROVROZPO~O",
goto: "aYPPPPPPPPPZQQORSQ",
nodeNames: "⚠ PieDiagram MindmapDiagram FlowchartDiagram SequenceDiagram JourneyDiagram RequirementDiagram GanttDiagram MermaidDiagram",
maxTerm: 13,
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "j~RRXY[]^[pq[~aR]~XY[]^[pq[",
tokenizers: [0, diagramText],
topRules: {"MermaidDiagram":[0,8]},
tokenPrec: 0
})

View File

@@ -0,0 +1,52 @@
import { ExternalTokenizer } from '@lezer/lr';
import {
preDiagramLine,
MindmapDiagram,
PieDiagram,
FlowchartDiagram,
SequenceDiagram,
JourneyDiagram,
RequirementDiagram,
GanttDiagram,
} from './mermaid.grammar.terms';
const skipCodePoints = [-1, 9, 13, 32];
const diagramMap: Record<string, number> = {
mindmap: MindmapDiagram,
pie: PieDiagram,
flowchart: FlowchartDiagram,
graph: FlowchartDiagram,
sequenceDiagram: SequenceDiagram,
journey: JourneyDiagram,
requirementDiagram: RequirementDiagram,
gantt: GanttDiagram,
};
const diagrams = Object.keys(diagramMap);
export const diagramText = new ExternalTokenizer((input) => {
if (skipCodePoints.includes(input.next)) return;
let tokens = '';
while (input.next != 10 && input.next !== -1) {
tokens += String.fromCodePoint(input.next);
input.advance();
}
input.advance();
const activeDiagram = diagrams.filter((diagram) => {
return tokens.startsWith(diagram);
});
if (activeDiagram.length > 0) {
while (input.next !== -1) {
input.advance();
}
input.acceptToken(diagramMap[activeDiagram[0]]);
} else {
input.acceptToken(preDiagramLine);
}
});

View File

@@ -0,0 +1,11 @@
import { styleTags } from '@lezer/highlight';
import { mindmapTags } from '../../tags';
export const mindmapHighlighting = styleTags({
DiagramName: mindmapTags.diagramName,
LineText1: mindmapTags.lineText1,
LineText2: mindmapTags.lineText2,
LineText3: mindmapTags.lineText3,
LineText4: mindmapTags.lineText4,
LineText5: mindmapTags.lineText5,
});

View File

@@ -0,0 +1,92 @@
@top MindmapDiagram {
newline+ |
DiagramName Line*
}
@skip { spaces | newlineEmpty }
lineText {
LineText1 |
LineText2 |
LineText3 |
LineText4 |
LineText5
}
ShapedText {
square |
roundedSquare |
circle |
bang |
cloud |
hexagon
}
square {
"[" lineText "]"
}
roundedSquare {
"(" lineText ")"
}
circle {
"((" lineText "))"
}
bang {
"))" lineText "(("
}
cloud {
")" lineText "("
}
hexagon {
"{{" lineText "}}"
}
IconLine {
"::" Icon "(" lineText ")"
}
ClassLine {
":::" lineText
}
Line {
newline |
newline indent (
lineText |
IconLine |
ClassLine |
ShapedText |
lineText ShapedText
)
}
DiagramName { kw<"mindmap"> }
Icon { kw<"icon"> }
kw<term> { @specialize<word, term> }
@context trackIndent from "./tokens.js"
@external tokens indentation from "./tokens" { indent }
@external tokens lineTextType from "./tokens" {
LineText1,
LineText2,
LineText3,
LineText4,
LineText5
}
@tokens {
spaces { ($[ \t\f] | "\\" $[\n\r])+ }
word { @asciiLetter+ }
}
@external tokens newlines from "./tokens" { newline, newlineEmpty }
@external propSource mindmapHighlighting from "./highlight"

View File

@@ -0,0 +1,8 @@
export declare const newline: number;
export declare const newlineEmpty: number;
export declare const indent: number;
export declare const LineText1: number;
export declare const LineText2: number;
export declare const LineText3: number;
export declare const LineText4: number;
export declare const LineText5: number;

View File

@@ -0,0 +1,17 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
indent = 16,
LineText1 = 1,
LineText2 = 2,
LineText3 = 3,
LineText4 = 4,
LineText5 = 5,
newline = 17,
newlineEmpty = 18,
MindmapDiagram = 6,
DiagramName = 7,
Line = 8,
IconLine = 9,
Icon = 10,
ClassLine = 11,
ShapedText = 12

View File

@@ -0,0 +1,239 @@
import { describe, it, expect } from 'vitest';
import { parser } from './mindmap.parser.grammar';
/**
* Mindmap Grammar 测试
*
* 测试目标:验证标准的 Mermaid Mindmap 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Mindmap Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 mindmap 声明', () => {
const code = `mindmap
Root
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带子节点的 mindmap', () => {
const code = `mindmap
Root
A
B
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析多层级的 mindmap', () => {
const code = `mindmap
Root
A
A1
A2
B
B1
B2
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带方括号形状的节点', () => {
const code = `mindmap
Root
[Square node]
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带圆括号形状的节点', () => {
const code = `mindmap
Root
(Rounded node)
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带双圆括号形状的节点(圆形)', () => {
const code = `mindmap
Root
((Circle node))
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带花括号形状的节点(六边形)', () => {
const code = `mindmap
Root
{{Hexagon node}}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析完整的 mindmap 示例', () => {
const code = `mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,23 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {indentation, lineTextType, newlines} from "./tokens"
import {trackIndent} from "./tokens.js"
import {mindmapHighlighting} from "./highlight"
const spec_word = {__proto__:null,mindmap:44, icon:50}
export const parser = LRParser.deserialize({
version: 14,
states: "&fOYQ[OOOOQW'#Ci'#CiQbQ[OOQgQ[OOOOQW'#Cc'#CcOOQW-E6g-E6gOlQ]O'#CdOOQW'#Cj'#CjQgQ[OOO!]Q^O,59OOOQW-E6h-E6hOOQW'#Cs'#CsO!vQ[O'#CeO!{Q^O'#CgO!{Q^O'#CyO!{Q^O'#C|O!{Q^O'#C}O!{Q^O'#DQO!{Q^O'#DRO!{Q^O'#DSOOQW'#Ch'#ChO#^Q[O1G.jOOQW1G.j1G.jO#hQ[O,59POOQW'#Cf'#CfOOQW,59R,59RO#mQ[O,59eO#rQ[O,59hO#wQ[O,59iO#|Q[O,59lO$RQ[O,59mO$WQ[O,59nOOQW7+$U7+$UO!{Q^O1G.kOOQW1G/P1G/POOQW1G/S1G/SOOQW1G/T1G/TOOQW1G/W1G/WOOQW1G/X1G/XOOQW1G/Y1G/YO$]Q[O7+$VOOQW<<Gq<<Gq",
stateData: "$b~OdOSbOS~OaPOfSO~OaPO~OaUO~O`XO_WXaWX~Oj_OkbOn^Or`OsaOwcO~OPZOQZORZOSZOTZOh[Ol]O~PwOihO~OPZOQZORZOSZOTZO~O_WiaWi~PwOjqO~OorO~OksO~OstO~OruO~OjvO~OxwO~OkyO~O",
goto: "#YwPPPPPPPx{!P!S!P!V!]!cPPPPPPPP!iPPPPP#UPP#U#UPP#U#U#URROTVRWRfXRg[QfXRpeQQORTQQWRRYWQeXQi]Qj^Qk_Ql`QmaQnbQocRxqTdXe",
nodeNames: "⚠ LineText1 LineText2 LineText3 LineText4 LineText5 MindmapDiagram DiagramName Line IconLine Icon ClassLine ShapedText",
maxTerm: 40,
context: trackIndent,
propSources: [mindmapHighlighting],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: "$b~R]XYz[]zpqzxy!fyz!s![!]#Q!c!}#e!}#O#p#O#P!]#P#Q#u#T#o#e#o#p#z#q#r$V~!PSd~XYz[]zpqz#O#P!]~!`QYZz]^z~!kPj~xy!n~!sOr~~!xPk~yz!{~#QOs~~#TP![!]#W~#]Ph~![!]#`~#eOl~~#jQe~!c!}#e#T#o#e~#uOn~~#zOo~~#}P#o#p$Q~$VOw~~$YP#q#r$]~$bOx~",
tokenizers: [indentation, lineTextType, 0, newlines],
topRules: {"MindmapDiagram":[0,6]},
specialized: [{term: 21, get: (value: keyof typeof spec_word) => spec_word[value] || -1}],
tokenPrec: 0
})

View File

@@ -0,0 +1,140 @@
import { ExternalTokenizer, ContextTracker } from '@lezer/lr';
import {
newline as newlineToken,
newlineEmpty,
indent,
LineText1,
LineText2,
LineText3,
LineText4,
LineText5,
} from './mindmap.grammar.terms';
import type { InputStream } from '@lezer/lr';
type InputStreamWithRead = InputStream & {
read: (inputPosition: number, stackPosition: number) => string;
};
const LineTextTokens = [LineText1, LineText2, LineText3, LineText4, LineText5];
const newline = 10,
carriageReturn = 13,
space = 32,
tab = 9,
hash = 35,
colon = 58,
parenL = 40,
parenR = 41,
bracketL = 91,
bracketR = 93,
braceL = 123,
braceR = 125;
export const newlines = new ExternalTokenizer(
(input, _stack) => {
if (input.next < 0) return;
else {
input.advance();
let spaces = 0;
while ((input.next as number) == space || (input.next as number) == tab) {
input.advance();
spaces++;
}
let empty =
input.next == newline ||
input.next == carriageReturn ||
input.next == hash;
input.acceptToken(empty ? newlineEmpty : newlineToken, -spaces);
}
},
{ contextual: true, fallback: true }
);
export const lineTextType = new ExternalTokenizer((input, stack) => {
let chars = 0;
while (input.next > -1 && input.next !== newline) {
if (input.next === colon) return;
if (
input.next === parenL ||
input.next === bracketL ||
input.next === braceL
) {
if (chars > 0) {
input.acceptToken(stack.context.lineType);
return;
} else return;
}
if (
(input.next === parenR ||
input.next === bracketR ||
input.next === braceR) &&
chars > 0
) {
input.acceptToken(stack.context.lineType);
return;
}
input.advance();
chars++;
}
input.acceptToken(stack.context.lineType);
});
const tabDepth = (depth: number) => {
return 4 - (depth % 4);
};
export const indentation = new ExternalTokenizer((input, _stack) => {
let prev = input.peek(-1);
if (prev == newline || prev == carriageReturn) {
let depth = 0;
let chars = 0;
while (true) {
if (input.next == space) depth++;
else if (input.next == tab) depth += tabDepth(depth);
else break;
input.advance();
chars++;
}
if (
input.next != newline &&
input.next != carriageReturn &&
input.next != hash
) {
input.acceptToken(indent);
}
}
});
const indentTracker = {
lineType: LineText1,
};
const countIndent = (space: string) => {
let depth = 0;
for (let i = 0; i < space.length; i++)
depth += space.charCodeAt(i) == tab ? tabDepth(depth) : 1;
return depth;
};
const getLineType = (depth: number) => {
return LineTextTokens[depth % 5];
};
export const trackIndent = new ContextTracker({
start: indentTracker,
shift(context, term, stack, input: InputStreamWithRead) {
if (term === indent) {
const depth = countIndent(input.read(input.pos, stack.pos));
context.lineType = getLineType(depth);
}
return context;
},
});

View File

@@ -0,0 +1,12 @@
import { styleTags } from '@lezer/highlight';
import { pieTags } from '../../tags';
export const pieHighlighting = styleTags({
DiagramName: pieTags.diagramName,
LineComment: pieTags.lineComment,
Number: pieTags.number,
ShowData: pieTags.showData,
String: pieTags.string,
Title: pieTags.title,
TitleText: pieTags.titleText,
});

View File

@@ -0,0 +1,44 @@
@top PieDiagram { document+ }
@skip { spaces | LineComment }
document {
DiagramName ShowData? (
() |
Title |
Title TitleText |
Title TitleText kvPair+ |
Title kvPair+ |
kvPair+
)
}
@skip {} {
String {
'"' (stringContentDouble)* '"'
}
}
kvPair {
String ":" Number
}
DiagramName { kw<"pie"> }
ShowData { kw<"showData"> }
Title { kw<"title"> }
kw<term> { @specialize<identifier, term> }
@external tokens titleText from "./tokens" { TitleText }
@tokens {
identifier { @asciiLetter+ }
stringContentDouble { !["]+ }
spaces { @whitespace+ }
Number { @digit+ ("." @digit+)? }
LineComment { "%%" ![\n]* }
}
@external propSource pieHighlighting from "./highlight"

View File

@@ -0,0 +1,2 @@
export declare const LineComment: number;
export declare const TitleText: number;

View File

@@ -0,0 +1,10 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
TitleText = 1,
LineComment = 2,
PieDiagram = 3,
DiagramName = 4,
ShowData = 5,
Title = 6,
String = 7,
Number = 8

View File

@@ -0,0 +1,204 @@
import { describe, it, expect } from 'vitest';
import { parser } from './pie.parser.grammar';
/**
* Pie Chart Grammar 测试
*
* 测试目标:验证标准的 Mermaid Pie Chart 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Pie Chart Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 pie 声明', () => {
const code = `pie
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 showData 的 pie 图', () => {
const code = `pie showData
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带标题的 pie 图', () => {
const code = `pie title My Pie Chart
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带数据的 pie 图', () => {
const code = `pie
"Dogs" : 386
"Cats" : 85
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带标题和数据的完整 pie 图', () => {
const code = `pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带小数值的数据', () => {
const code = `pie
"A" : 10.5
"B" : 20.75
"C" : 30.25
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 showData 和完整数据的 pie 图', () => {
const code = `pie showData
title Key elements in Product X
"Calcium" : 42.96
"Potassium" : 50.05
"Magnesium" : 10.01
"Iron" : 5
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,21 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {titleText} from "./tokens"
import {pieHighlighting} from "./highlight"
const spec_identifier = {__proto__:null,pie:34, showData:36, title:38}
export const parser = LRParser.deserialize({
version: 14,
states: "$nOYQQOOO_QQO'#CkOOQO'#Ce'#CeQYQQOOOOQO'#C`'#C`OpOSO'#CcOxQQO'#CpOOQO'#Cf'#CfO}QQO,59VO!YQRO,59VO!hQQO,59VOOQO'#Ca'#CaOOQP'#Cb'#CbOOQO-E6c-E6cOOOO'#Cg'#CgO!vOSO,58}OOQO,58},58}O#OQQO,59[OOQO-E6d-E6dO#TQQO1G.qO#TQQO1G.qO#`QRO1G.qOOOO-E6e-E6eOOQO1G.i1G.iOOQO1G.v1G.vO#nQQO7+$]O#nQQO7+$]O#yQQO<<Gw",
stateData: "$U~O^OSQOS~OaSO~ObZOc[OeTO[_Xa_X~Oe`Of^O~OgaO~OeTO[_aa_a~OPdOeTO[_aa_a~Oc[OeTO[_aa_a~OegOf^O~OWhO~OeTO[_ia_i~OPjOeTO[_ia_i~OeTO[_qa_q~OeTO[_ya_y~O",
goto: "#RePPPPfjmsP!P!V!kPPP!qPPPP!uTPORRYPQXPReYeUPWXYcdeijkQROR]RQWPWbWcikScXYSideRkjQ_TRf_TQOReVPWXYcdeijk",
nodeNames: "⚠ TitleText LineComment PieDiagram DiagramName ShowData Title String Number",
maxTerm: 23,
propSources: [pieHighlighting],
skippedNodes: [0,2],
repeatNodeCount: 3,
tokenData: "*V~RrOX#]X^#t^p#]pq#tqr#]rs%gsu#]uv%lv!Q#]!Q!['`![!])R!]!c#]!c!})f!}#T#]#T#o)f#o#y#]#y#z#t#z$f#]$f$g#t$g#BY#]#BY#BZ#t#BZ$IS#]$IS$I_#t$I_$I|#]$I|$JO#t$JO$JT#]$JT$JU#t$JU$KV#]$KV$KW#t$KW&FU#]&FU&FV#t&FV;'S#];'S;=`#n<%lO#]Q#bSfQOr#]s;'S#];'S;=`#n<%lO#]Q#qP;=`<%l#]R#{h^PfQOX#]X^#t^p#]pq#tqr#]s#y#]#y#z#t#z$f#]$f$g#t$g#BY#]#BY#BZ#t#BZ$IS#]$IS$I_#t$I_$I|#]$I|$JO#t$JO$JT#]$JT$JU#t$JU$KV#]$KV$KW#t$KW&FU#]&FU&FV#t&FV;'S#];'S;=`#n<%lO#]~%lOe~R%qUfQOr#]su#]uv&Tv;'S#];'S;=`#n<%lO#]R&[VQPfQOY&TYZ#]Zr&Trs&qs;'S&T;'S;=`'Y<%lO&TP&vSQPOY&qZ;'S&q;'S;=`'S<%lO&qP'VP;=`<%l&qR']P;=`<%l&TR'gWWPfQOr#]s!O#]!O!P(P!P!Q#]!Q!['`![;'S#];'S;=`#n<%lO#]R(UUfQOr#]s!Q#]!Q![(h![;'S#];'S;=`#n<%lO#]R(oUWPfQOr#]s!Q#]!Q![(h![;'S#];'S;=`#n<%lO#]R)YSgPfQOr#]s;'S#];'S;=`#n<%lO#]R)mW`PfQOr#]s!c#]!c!})f!}#T#]#T#o)f#o;'S#];'S;=`#n<%lO#]",
tokenizers: [titleText, 0, 1],
topRules: {"PieDiagram":[0,3]},
specialized: [{term: 16, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
tokenPrec: 0
})

View File

@@ -0,0 +1,17 @@
import { ExternalTokenizer } from '@lezer/lr';
import { TitleText } from './pie.grammar.terms';
export const titleText = new ExternalTokenizer((input) => {
if (input.next === 10) {
input.acceptToken(TitleText);
return;
}
if (input.next === -1) return;
while (input.next !== 10 && input.next !== -1) {
input.advance();
}
input.acceptToken(TitleText);
});

View File

@@ -0,0 +1,13 @@
import { styleTags } from '@lezer/highlight';
import { requirementTags } from '../../tags';
export const requirementHighlighting = styleTags({
'DiagramName SubDiagramType': requirementTags.diagramName,
LineComment: requirementTags.lineComment,
IDNumber: requirementTags.number,
'UnquotedString RelationshipStart': requirementTags.unquotedString,
QuotedString: requirementTags.quotedString,
PropKeyword: requirementTags.unquotedString,
Keyword: requirementTags.keyword,
'ForwardArrow BackArrow Hyphen': requirementTags.arrow,
});

View File

@@ -0,0 +1,151 @@
@top RequirementDiagram {
document
}
@skip { spaces | LineComment }
@skip {} {
UnquotedString { unquotedString }
QuotedString {
'"' (stringContent)* '"'
}
}
document {
DiagramName newlines? |
DiagramName newlines ((subDiagram | RelationshipLine ) newlines?)+
}
subDiagram {
SubDiagramType subDiagramName "{" newlines "}" |
SubDiagramType subDiagramName "{" newlines subDiagramLine+ "}"
}
subDiagramName {
UnquotedString | QuotedString
}
subDiagramLine {
(
idLine |
textLine |
riskLine |
verifyMethodLine |
typeLine |
docRefLine
) newlines?
}
idLine {
ID ":" IDNumber
}
textLine {
Text ":" textContent
}
riskLine {
Risk ":" RiskType
}
verifyMethodLine {
VerifyMethod ":" VerifyMethodType
}
typeLine {
Type ":" textContent
}
docRefLine {
DocRef ":" textContent
}
textContent {
UnquotedString | QuotedString
}
RelationshipLine {
relationshipStart Hyphen RelationshipType ForwardArrow relationshipEnd |
relationshipStart BackArrow RelationshipType Hyphen relationshipEnd
}
relationshipStart {
RelationshipStart | QuotedString
}
relationshipEnd {
UnquotedString | QuotedString
}
DiagramName { diagramKw<"requirementDiagram"> }
SubDiagramType {
diagramKw<"requirement"> | diagramKw<"Requirement"> |
diagramKw<"functionalRequirement"> | diagramKw<"FunctionalRequirement"> |
diagramKw<"performanceRequirement"> | diagramKw<"PerformanceRequirement"> |
diagramKw<"interfaceRequirement"> | diagramKw<"InterfaceRequirement"> |
diagramKw<"physicalRequirement"> | diagramKw<"PhysicalRequirement"> |
diagramKw<"designConstraint"> | diagramKw<"DesignConstraint"> |
diagramKw<"element"> | diagramKw<"Element">
}
ID { propKw<"id"> | propKw<"Id"> | propKw<"ID"> }
Text { propKw<"text"> | propKw<"Text"> }
Risk { propKw<"risk"> | propKw<"Risk"> }
VerifyMethod { propKw<"verifymethod"> | propKw<"verifyMethod"> | propKw<"VerifyMethod"> }
Type { propKw<"type"> | propKw<"Type"> }
DocRef { propKw<"docRef"> | propKw<"DocRef"> }
RiskType {
kw<"low"> | kw<"Low"> |
kw<"medium"> | kw<"Medium"> |
kw<"high"> | kw<"High">
}
VerifyMethodType {
kw<"analysis"> | kw<"Analysis"> |
kw<"demonstration"> | kw<"Demonstration"> |
kw<"inspection"> | kw<"Inspection"> |
kw<"test"> | kw<"Test">
}
RelationshipType {
kw<"contains"> | kw<"Contains"> |
kw<"copies"> | kw<"Copies"> |
kw<"derives"> | kw<"Derives"> |
kw<"satisfies"> | kw<"Satisfies"> |
kw<"verifies"> | kw<"Verifies"> |
kw<"refines"> | kw<"Refines"> |
kw<"traces"> | kw<"Traces">
}
diagramKw<term> { @specialize<word, term> }
propKw<term> { @specialize[@name=PropKeyword]<word, term> }
kw<term> { @specialize[@name=Keyword]<word, term> }
@external tokens relationshipStart from "./tokens" { RelationshipStart }
@tokens {
word { @asciiLetter+ }
spaces { @whitespace+ }
newlines { $[\n]+ }
LineComment { "%%" ![\n]* }
unquotedString { word ![\r\n\{\<\>\-\=]* }
IDNumber { @digit+ ("." @digit+)* }
stringContent { !["]+ }
ForwardArrow { "->"}
BackArrow { "<-"}
Hyphen { "-" }
@precedence { newlines, spaces }
@precedence { ForwardArrow, Hyphen }
@precedence { BackArrow, Hyphen }
}
@external propSource requirementHighlighting from "./highlight"

View File

@@ -0,0 +1 @@
export declare const RelationshipStart: number;

View File

@@ -0,0 +1,25 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
RelationshipStart = 1,
LineComment = 2,
RequirementDiagram = 3,
DiagramName = 4,
SubDiagramType = 5,
UnquotedString = 6,
QuotedString = 7,
ID = 8,
PropKeyword = 44,
IDNumber = 12,
Text = 13,
Risk = 16,
RiskType = 19,
Keyword = 61,
VerifyMethod = 26,
VerifyMethodType = 30,
Type = 39,
DocRef = 42,
RelationshipLine = 45,
Hyphen = 46,
RelationshipType = 47,
ForwardArrow = 62,
BackArrow = 63

View File

@@ -0,0 +1,330 @@
import { describe, it, expect } from 'vitest';
import { parser } from './requirement.parser.grammar';
/**
* Requirement Diagram Grammar 测试
*
* 测试目标:验证标准的 Mermaid Requirement Diagram 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Requirement Diagram Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 requirementDiagram 声明', () => {
const code = `requirementDiagram
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析空的需求定义', () => {
const code = `requirementDiagram
requirement test_req {
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 ID 的需求', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带文本的需求', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
text: the test text
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带风险级别的需求', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
text: the test text
risk: high
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带验证方法的需求', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
text: the test text
risk: high
verifymethod: test
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析不同类型的需求functionalRequirement', () => {
const code = `requirementDiagram
functionalRequirement test_req {
id: 1.1
text: the test text
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析元素定义', () => {
const code = `requirementDiagram
element test_entity {
type: simulation
}
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析关系contains', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
}
element test_entity {
type: simulation
}
test_entity - contains -> test_req
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析反向关系', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
}
element test_entity {
type: simulation
}
test_req <- satisfies - test_entity
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析完整的需求图示例', () => {
const code = `requirementDiagram
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
functionalRequirement test_req2 {
id: 1.1
text: the second test text.
risk: low
verifymethod: inspection
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req2
test_req - traces -> test_req2
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,21 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {relationshipStart} from "./tokens"
import {requirementHighlighting} from "./highlight"
const spec_word = {__proto__:null,requirementDiagram:144, requirement:150, Requirement:152, functionalRequirement:154, FunctionalRequirement:156, performanceRequirement:158, PerformanceRequirement:160, interfaceRequirement:162, InterfaceRequirement:164, physicalRequirement:166, PhysicalRequirement:168, designConstraint:170, DesignConstraint:172, element:174, Element:176, id:18, Id:20, ID:22, text:28, Text:30, risk:34, Risk:36, low:40, Low:42, medium:44, Medium:46, high:48, High:50, verifymethod:54, verifyMethod:56, VerifyMethod:58, analysis:62, Analysis:64, demonstration:66, Demonstration:68, inspection:70, Inspection:72, test:74, Test:76, type:80, Type:82, docRef:86, DocRef:88, contains:96, Contains:98, copies:100, Copies:102, derives:104, Derives:106, satisfies:108, Satisfies:110, verifies:112, Verifies:114, refines:116, Refines:118, traces:120, Traces:122}
export const parser = LRParser.deserialize({
version: 14,
states: ")`OYQQOOO_QQO'#DtQOQQOOOOQO'#C`'#C`O!kQRO,5:`O!rOSO'#CcOOQO'#Ef'#EfO!zQQO'#DZO#SQRO'#DnO$^QRO1G/zOOQO'#Ca'#CaO$eQWO'#DxOOOO'#Do'#DoO$mOSO,58}OOQP,58},58}O$uQQO,59uO$uQQO,59uOOQP,5:Y,5:YOOQP-E7l-E7lOOQP'#Cb'#CbOOQP'#Eg'#EgO%sQQO,5:dOOOO-E7m-E7mOOQP1G.i1G.iO%xQQO1G/aOOQO'#D]'#D]O%}QQO1G/aO&SQQO1G0OO$eQWO7+${O'VQQO7+%jOOQP<<Hg<<HgO'^QQO'#E_O'cQQO'#EbO'hQQO'#EcO'mQQO'#E^OOQO'#Dp'#DpO(qQQO<<IUOOQO'#Cd'#CdOOQO'#Ci'#CiOOQO'#Cl'#ClOOQO'#Cv'#CvOOQO'#DT'#DTOOQO'#DW'#DWO(xQQO'#EaO(}QQO'#EdO)SQQO'#EeOOQP<<IU<<IUO)XQQO,5:yO)^QQO,5:|O)rQQO,5:}OOQO,5:x,5:xOOQO-E7n-E7nOOQPAN>pAN>pO$eQWO,5:{O$eQWO,5;OO$eQWO,5;POOQO1G0e1G0eOOQO1G0h1G0hOOQO'#Co'#CoOOQO1G0i1G0iOOQO'#Cz'#CzOOQO1G0g1G0gOOQO1G0j1G0jOOQO1G0k1G0k",
stateData: "*e~O!gOSQOS~O!jRO~O!kSO!e!hX~OPUO!mYO!nYO!oYO!pYO!qYO!rYO!sYO!tYO!uYO!vYO!wYO!xYO!yYO!zYO!|TO~O!e!ha~PgO!|^O!}[O~O!O_O!a`O~O!kaOP!bX!e!bX!m!bX!n!bX!o!bX!p!bX!q!bX!r!bX!s!bX!t!bX!u!bX!v!bX!w!bX!x!bX!y!bX!z!bX!|!bX~O!e!hi~PgO!{cO!|TO~O!|gO!}[O~O!QiO!RiO!SiO!TiO!UiO!ViO!WiO!XiO!YiO!ZiO![iO!]iO!^iO!_iO~O#OkO~O!`lO~O!OlO~O!kmO~OXuOYuOZuO^vO_vOawObwOkxOlxOmxOxyOyyO{zO|zO~O#P!OO~P&XO#S!PO~O#S!QO~O#S!RO~O!k!SOX#QXY#QXZ#QX^#QX_#QXa#QXb#QXk#QXl#QXm#QXx#QXy#QX{#QX|#QX#P#QX~O#P!UO~P&XO#S!VO~O#S!WO~O#S!XO~O[!YO~Od![Oe![Of![Og![Oh![Oi![O~Oo!^Op!^Oq!^Or!^Os!^Ot!^Ou!^Ov!^O~O!k!a!g!`!O!`~",
goto: "%r#[PPPP#]#`#d#k#vPPPP#zPP$OPP$SPPPPPP$VPPP$ZPPPPPPPP$^PP$bPP$fP$jPPPPPPPPPPPPPPPP$p$v$|PPP%SPPP$fPPPPPPPPPPPPPPPPPPP%V%ZP%Z%Z%Z%Z%Z%_%cRPOTZSXZdZl!V!W!XSUSXZdZl!V!W!XTomtT{mtTpmtR!Z!QTqmtR!]!RT|mtT}mtTWSXQh_Rj`QXSRbXQ]TRf]QtmR!TtRQOTsmtTrmtTVSXQeZQnlQ!_!VQ!`!WR!a!X",
nodeNames: "⚠ RelationshipStart LineComment RequirementDiagram DiagramName SubDiagramType UnquotedString QuotedString ID PropKeyword PropKeyword PropKeyword IDNumber Text PropKeyword PropKeyword Risk PropKeyword PropKeyword RiskType Keyword Keyword Keyword Keyword Keyword Keyword VerifyMethod PropKeyword PropKeyword PropKeyword VerifyMethodType Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Type PropKeyword PropKeyword DocRef PropKeyword PropKeyword RelationshipLine Hyphen RelationshipType Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword Keyword ForwardArrow BackArrow",
maxTerm: 103,
propSources: [requirementHighlighting],
skippedNodes: [0,2],
repeatNodeCount: 3,
tokenData: "1g~R{OX#xXY$aYZ&SZ^$a^p#xpq$aqr#xrs'}su#xuv(Sv}#x}!O)v!O!Q#x!Q![*t![!]+|!]!^#x!^!_,a!_!c#x!c!}-]!}#T#x#T#o-]#o#p0o#p#q#x#q#r1S#r#y#x#y#z$a#z$f#x$f$g$a$g#BY#x#BY#BZ$a#BZ$IS#x$IS$I_$a$I_$I|#x$I|$JO$a$JO$JT#x$JT$JU$a$JU$KV#x$KV$KW$a$KW&FU#x&FU&FV$a&FV;'S#x;'S;=`$Z<%lO#xQ#}S!}QOr#xs;'S#x;'S;=`$Z<%lO#xQ$^P;=`<%l#xV$hh!}Q!gTOX#xX^$a^p#xpq$aqr#xs#y#x#y#z$a#z$f#x$f$g$a$g#BY#x#BY#BZ$a#BZ$IS#x$IS$I_$a$I_$I|#x$I|$JO$a$JO$JT#x$JT$JU$a$JU$KV#x$KV$KW$a$KW&FU#x&FU&FV$a&FV;'S#x;'S;=`$Z<%lO#xV&]j!}Q!kP!gTOX#xXY$aYZ&SZ^$a^p#xpq$aqr#xs#y#x#y#z$a#z$f#x$f$g$a$g#BY#x#BY#BZ$a#BZ$IS#x$IS$I_$a$I_$I|#x$I|$JO$a$JO$JT#x$JT$JU$a$JU$KV#x$KV$KW$a$KW&FU#x&FU&FV$a&FV;'S#x;'S;=`$Z<%lO#x~(SO!|~V(XU!}QOr#xsu#xuv(kv;'S#x;'S;=`$Z<%lO#xV(rVQT!}QOY(kYZ#xZr(krs)Xs;'S(k;'S;=`)p<%lO(kT)^SQTOY)XZ;'S)X;'S;=`)j<%lO)XT)mP;=`<%l)XV)sP;=`<%l(kR)}U!}Q!OPOr#xs!`#x!`!a*a!a;'S#x;'S;=`$Z<%lO#xR*hS!}Q!`POr#xs;'S#x;'S;=`$Z<%lO#xR*{W[P!}QOr#xs!O#x!O!P+e!P!Q#x!Q![*t![;'S#x;'S;=`$Z<%lO#xR+jU!}QOr#xs!Q#x!Q![*t![;'S#x;'S;=`$Z<%lO#xR,TS#SP!}QOr#xs;'S#x;'S;=`$Z<%lO#xR,fU!}QOr#xs}#x}!O,x!O;'S#x;'S;=`$Z<%lO#xR-PS!}Q!aPOr#xs;'S#x;'S;=`$Z<%lO#xV-fb!}Q!{S!iPOY.nYZ#xZ].n]^#x^r.nrs/ts}.n}!O#x!O!^.n!^!a#x!a!c.n!c!}-]!}#T.n#T#o-]#o#p#x#p;'S.n;'S;=`0i<%lO.nU.u_!}Q!{SOY.nYZ#xZ].n]^#x^r.nrs/ts}.n}!O#x!O!^.n!^!a#x!a#o.n#o#p#x#p;'S.n;'S;=`0i<%lO.nS/yW!{SOY/tZ]/t^}/t!O!^/t!a#o/t#p;'S/t;'S;=`0c<%lO/tS0fP;=`<%l/tU0lP;=`<%l.nR0vS#OP!}QOr#xs;'S#x;'S;=`$Z<%lO#xR1ZS#PP!}QOr#xs;'S#x;'S;=`$Z<%lO#x",
tokenizers: [relationshipStart, 0, 1, 2],
topRules: {"RequirementDiagram":[0,3]},
specialized: [{term: 71, get: (value: keyof typeof spec_word) => spec_word[value] || -1}],
tokenPrec: 428
})

View File

@@ -0,0 +1,24 @@
import { ExternalTokenizer } from '@lezer/lr';
import { RelationshipStart } from './requirement.grammar.terms';
const notAllowedCodePoints = [-1, 45, 60, 62, 10, 13, 123, 61];
export const relationshipStart = new ExternalTokenizer((input) => {
if (notAllowedCodePoints.includes(input.next) || input.next === 32) return;
let peek;
let tokens = '';
let count = 0;
do {
peek = input.peek(count);
if (peek === -1) return;
tokens += String.fromCodePoint(peek);
count++;
} while (!notAllowedCodePoints.includes(peek));
if (peek === 45 || peek === 60) {
tokens = tokens.slice(0, -1).trim();
input.acceptToken(RelationshipStart, tokens.length);
}
});

View File

@@ -0,0 +1,14 @@
import { styleTags } from '@lezer/highlight';
import { sequenceTags } from '../../tags';
export const sequenceHighlighting = styleTags({
DiagramName: sequenceTags.diagramName,
NodeText: sequenceTags.nodeText,
Keyword1: sequenceTags.keyword1,
Keyword2: sequenceTags.keyword2,
LineComment: sequenceTags.lineComment,
'Arrow ArrowSuffix': sequenceTags.arrow,
Position: sequenceTags.position,
MessageText1: sequenceTags.messageText1,
MessageText2: sequenceTags.messageText2,
});

View File

@@ -0,0 +1,114 @@
@top SequenceDiagram {
document
}
@skip { spaces }
document {
DiagramName newlines? |
DiagramName newlines subDocument newlines? |
DiagramName newlines subDocument (newlines subDocument)+ newlines?
}
subDocument {
LineComment |
NodeText Arrow ArrowSuffix? newlines? NodeText ":" MessageText1 |
(Create | Destroy)? (Participant | Actor)? NodeText (As NodeText)? |
(Activate | Deactivate) NodeText |
Note Position NodeText ("," NodeText)? ":" MessageText1 |
Keyword MessageText2 |
Autonumber |
End |
Link NodeText ":" MessageText1
}
MessageText1 {
messageText
}
MessageText2 {
messageText
}
ArrowSuffix {
"+" | "-"
}
Link[group=Keyword1] {
link | links
}
Keyword[group=Keyword1] {
alt |
and |
box |
break |
critical |
else |
loop |
opt |
option |
par |
rect
}
DiagramName { kw<"sequenceDiagram"> }
kw<term> { @specialize<identifier, term> }
@external tokens messageTextToken from "./tokens" { messageText }
@external tokens textTokens from "./tokens" {
Activate[group=Keyword1],
Autonumber[group=Keyword1],
Create[group=Keyword1],
Deactivate[group=Keyword1],
Destroy[group=Keyword1],
End[group=Keyword1],
Note[group=Keyword1],
Actor[group=Keyword2],
As[group=Keyword2],
Participant[group=Keyword2],
NodeText,
Position,
alt,
and,
box,
break,
critical,
else,
link,
links
loop,
opt,
option,
par,
rect
}
@tokens {
spaces { @whitespace+ }
newlines { $[\n]+ }
LineComment { "%%" ![\n]* }
identifierChar { @asciiLetter | $[$\u{a1}-\u{10ffff}] }
word { identifierChar (identifierChar | @digit)* }
identifier { word }
Arrow {
"->" |
"-->" |
"->>" |
"-->>" |
"-x" |
"--x" |
"-)" |
"--)"
}
@precedence {
newlines,
spaces,
Arrow,
identifier
}
}
@external propSource sequenceHighlighting from "./highlight"

View File

@@ -0,0 +1,26 @@
export declare const _break: number;
export declare const _else: number;
export declare const Activate: number;
export declare const Actor: number;
export declare const alt: number;
export declare const and: number;
export declare const As: number;
export declare const Autonumber: number;
export declare const box: number;
export declare const Create: number;
export declare const critical: number;
export declare const Deactivate: number;
export declare const Destroy: number;
export declare const End: number;
export declare const link: number;
export declare const links: number;
export declare const loop: number;
export declare const messageText: number;
export declare const NodeText: number;
export declare const Note: number;
export declare const opt: number;
export declare const option: number;
export declare const par: number;
export declare const Participant: number;
export declare const Position: number;
export declare const rect: number;

View File

@@ -0,0 +1,37 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
messageText = 24,
Activate = 1,
Autonumber = 2,
Create = 3,
Deactivate = 4,
Destroy = 5,
End = 6,
Note = 7,
Actor = 8,
As = 9,
Participant = 10,
NodeText = 11,
Position = 12,
alt = 25,
and = 26,
box = 27,
_break = 28,
critical = 29,
_else = 30,
link = 31,
links = 32,
loop = 33,
opt = 34,
option = 35,
par = 36,
rect = 37,
SequenceDiagram = 13,
DiagramName = 14,
LineComment = 15,
Arrow = 16,
ArrowSuffix = 17,
MessageText1 = 18,
Keyword = 19,
MessageText2 = 20,
Link = 21

View File

@@ -0,0 +1,270 @@
import { describe, it, expect } from 'vitest';
import { parser } from './sequence.parser.grammar';
/**
* Sequence Diagram Grammar 测试
*
* 测试目标:验证标准的 Mermaid Sequence Diagram 语法是否能正确解析,不应该出现错误节点(⚠)
*/
describe('Sequence Diagram Grammar 解析测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false; // 限制深度
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
/**
* 获取节点深度
*/
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('应该正确解析基础的 sequenceDiagram 声明', () => {
const code = `sequenceDiagram
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 participant 的序列图', () => {
const code = `sequenceDiagram
participant Alice
participant Bob
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析简单的消息传递', () => {
const code = `sequenceDiagram
Alice->John: Hello John, how are you?
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带虚线箭头的消息', () => {
const code = `sequenceDiagram
Alice-->John: Hello
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带异步箭头的消息', () => {
const code = `sequenceDiagram
Alice->>John: Hello
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带激活/停用的序列图', () => {
const code = `sequenceDiagram
Alice->>John: Hello
activate John
John-->>Alice: Hi there!
deactivate John
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 note 的序列图', () => {
const code = `sequenceDiagram
Alice->John: Hello John
Note over Alice,John: A typical interaction
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 alt/else 的序列图', () => {
const code = `sequenceDiagram
Alice->John: Hello John
alt is sick
John-->Alice: Not so good
else is well
John-->Alice: Feeling fresh
end
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析带 loop 的序列图', () => {
const code = `sequenceDiagram
Alice->John: Hello John
loop Every minute
John-->Alice: Great!
end
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
it('应该正确解析完整的序列图示例', () => {
const code = `sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop HealthCheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('语法树:');
console.log(printTree(tree, code));
console.log('错误节点:', result.errors);
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -0,0 +1,3 @@
import { LRParser } from '@lezer/lr';
export declare const parser: LRParser;

View File

@@ -0,0 +1,24 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {messageTextToken, textTokens} from "./tokens"
import {sequenceHighlighting} from "./highlight"
const spec_identifier = {__proto__:null,sequenceDiagram:84}
export const parser = LRParser.deserialize({
version: 14,
states: "'nOVQSOOO[QSO'#DUQOQSOOOOQO'#Cj'#CjO#QQUO,59pOOQP'#Co'#CoOOQQ'#Cq'#CqOOQO'#DY'#DYO#XQUO'#DYO#gQUO'#DYO#lQUO'#DYO#wQUO'#DYO#|QUO'#DYO$RQTO'#DYO$WQUO'#DYO$]QSO1G/[O$eQYO,59tO$sQUO,59tO$xQUO,59tO%TQUO,59tOOQO,59t,59tO%YQUO,59tOOQO'#Cp'#CpO%_QSO,59tO%dQUO7+$vO%kQSO7+$vOOQQ'#Cm'#CmO%sQSO1G/`O%xQUO1G/`O%}QUO1G/`OOQO1G/`1G/`O&VQUO1G/`O&[QUO1G/`O&gQSO1G/`O&oQTO1G/`OOQO,59^,59^O&tQUO<<HbOOQO-E6p-E6pO&oQTO7+$zO&{QSO7+$zO'QQUO7+$zOOQO7+$z7+$zO'VQUO7+$zOOQO'#Cn'#CnPdQUO'#CrOOQO<<Hf<<HfO&oQTO<<HfO'[QSO<<HfOOQOAN>QAN>QO&oQTOAN>QOOQOG23lG23l",
stateData: "'g~OwOS~OzRO~O{SOgxX~OPZOQVORYOSZOTYOUVOV[OWXOYXOZWO_VOiTOjTOkTOlTOmTOnTOoUOpUOqTOrTOsTOtTOuTO~Ogxa~PdOXaO``Og|X{|X~OZbO~OWcOYcOZbO~OZdO~O[eO~OhfO~OZgO~O{hOgxi~OZkO{lO}jO!OjO~OZnO~OXoOg|a{|a~OZpO~OZqO~O!PrO~Ogxq~PdO{tOgxq~O!PvO~OZwO~OZwO{xO~OZyO~OXzOg|i{|i~O!PvO!QxO~Oh{O~Ogxy~PdO!P!OO~OZ!PO~OZ}O~O!P!RO~O{w`y`~",
goto: "#S}PPPPPPPPPPPPPP!OPP!R!U!b!h!k!qPPPPPPPPPPPPPPPPP!wPPP!zRPORm`QyrQ}vQ!Q!OR!S!RX]Sht|Rd]X^Sht|Qi_RuiRQOQ_SVsht|",
nodeNames: "⚠ Activate Autonumber Create Deactivate Destroy End Note Actor As Participant NodeText Position SequenceDiagram DiagramName LineComment Arrow ArrowSuffix MessageText1 Keyword MessageText2 Link",
maxTerm: 48,
nodeProps: [
["group", -9,1,2,3,4,5,6,7,19,21,"Keyword1",-3,8,9,10,"Keyword2"]
],
propSources: [sequenceHighlighting],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "(x~RmXY!|YZ#qZ^!|pq!|tu$nuv%`{|%}|}&S}!O&X![!]'T!c!}$n#T#o$n#y#z!|$f$g!|$g#BY$n#BY#BZ'Y#BZ$IS$n$IS$I_'Y$I_$I|$n$I|$JO'Y$JO$JT$n$JT$JU'Y$JU$KV$n$KV$KW'Y$KW&FU$n&FU&FV'Y&FV;'S$n;'S;=`%Y<%lO$n~#RYw~X^!|pq!|#y#z!|$f$g!|#BY#BZ!|$IS$I_!|$I|$JO!|$JT$JU!|$KV$KW!|&FU&FV!|~#x[{~w~XY!|YZ#qZ^!|pq!|#y#z!|$f$g!|#BY#BZ!|$IS$I_!|$I|$JO!|$JT$JU!|$KV$KW!|&FU&FV!|~$sVy~tu$n!Q![$n!c!}$n#T#o$n$g;'S$n;'S;=`%Y<%lO$n~%]P;=`<%l$n~%cPuv%f~%kS_~OY%fZ;'S%f;'S;=`%w<%lO%f~%zP;=`<%l%f~&SO}~~&XO!Q~R&^S!OQyz&j}!O&o!`!a&{#l#m&jP&oO`PP&rRyz&j!`!a&{#l#m&jP'QP`P!`!a&j~'YO!P~~'agw~y~X^!|pq!|tu$n!Q![$n!c!}$n#T#o$n#y#z!|$f$g!|$g#BY$n#BY#BZ'Y#BZ$IS$n$IS$I_'Y$I_$I|$n$I|$JO'Y$JO$JT$n$JT$JU'Y$JU$KV$n$KV$KW'Y$KW&FU$n&FU&FV'Y&FV;'S$n;'S;=`%Y<%lO$n",
tokenizers: [messageTextToken, textTokens, 0, 1],
topRules: {"SequenceDiagram":[0,13]},
specialized: [{term: 41, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
tokenPrec: 293
})

View File

@@ -0,0 +1,129 @@
import { ExternalTokenizer } from '@lezer/lr';
import {
_break,
_else,
Activate,
Actor,
alt,
and,
As,
Autonumber,
box,
Create,
critical,
Deactivate,
Destroy,
End,
link,
links,
loop,
messageText,
NodeText,
Note,
opt,
option,
par,
Participant,
Position,
rect,
} from './sequence.grammar.terms';
const skipCodePoints = [-1, 9, 10, 13, 32, 37];
const arrowSuffixCodePoints = [43, 45];
const notAllowedCodePoints = [44, 58, 62];
const notAllowed2Chars = ['->', '-x', '-)', ' -', ' '];
const notAllowed3Chars = ['-->', '->>', '--x', '--)', ' as'];
const keywordMap: { [key: string]: number } = {
'left of': Position,
'right of': Position,
activate: Activate,
actor: Actor,
alt: alt,
and: and,
as: As,
autonumber: Autonumber,
box: box,
break: _break,
create: Create,
critical: critical,
deactivate: Deactivate,
destroy: Destroy,
else: _else,
end: End,
link: link,
links: links,
loop: loop,
note: Note,
opt: opt,
option: option,
over: Position,
par: par,
participant: Participant,
rect: rect,
};
const keywords = Object.keys(keywordMap);
export const messageTextToken = new ExternalTokenizer((input) => {
if (skipCodePoints.includes(input.next)) return;
while (input.next !== 10 && input.next !== -1) {
input.advance();
}
input.acceptToken(messageText);
});
export const textTokens = new ExternalTokenizer((input) => {
if (
skipCodePoints.includes(input.next) ||
arrowSuffixCodePoints.includes(input.next)
)
return;
const isArrowNext = () => {
if (input.peek(0) === -1 || input.peek(1) === -1 || input.peek(2) === -1)
return false;
let result =
String.fromCodePoint(input.peek(0)) + String.fromCodePoint(input.peek(1));
if (notAllowed2Chars.includes(result)) return true;
result += String.fromCodePoint(input.peek(2));
if (notAllowed3Chars.includes(result)) return true;
return false;
};
let tokens = '';
while (
!notAllowedCodePoints.includes(input.next) &&
!isArrowNext() &&
input.next !== 10 &&
input.next !== -1
) {
tokens += String.fromCodePoint(input.next);
input.advance();
}
const activeKeyword = keywords.filter((keyword) => {
if (keyword === tokens) {
return tokens.toLowerCase().startsWith(keyword);
}
return tokens.toLowerCase().startsWith(keyword + ' ');
});
if (activeKeyword.length > 0) {
input.acceptToken(
keywordMap[activeKeyword[0]],
activeKeyword[0].length - tokens.length
);
return;
}
input.acceptToken(NodeText);
});

View File

@@ -0,0 +1,76 @@
import { Tag, tags as t } from '@lezer/highlight';
export const mermaidTags = {
diagramName: Tag.define(t.typeName),
};
export const mindmapTags = {
diagramName: Tag.define(mermaidTags.diagramName),
lineText1: Tag.define(),
lineText2: Tag.define(),
lineText3: Tag.define(),
lineText4: Tag.define(),
lineText5: Tag.define(),
};
export const pieTags = {
diagramName: Tag.define(mermaidTags.diagramName),
lineComment: Tag.define(t.lineComment),
number: Tag.define(t.number),
showData: Tag.define(t.keyword),
string: Tag.define(t.string),
title: Tag.define(t.keyword),
titleText: Tag.define(t.string),
};
export const flowchartTags = {
diagramName: Tag.define(mermaidTags.diagramName),
keyword: Tag.define(t.keyword),
lineComment: Tag.define(t.lineComment),
link: Tag.define(t.contentSeparator),
nodeEdge: Tag.define(t.contentSeparator),
nodeEdgeText: Tag.define(t.string),
nodeId: Tag.define(t.variableName),
nodeText: Tag.define(t.string),
number: Tag.define(t.number),
orientation: Tag.define(t.modifier),
string: Tag.define(t.string),
};
export const sequenceTags = {
diagramName: Tag.define(mermaidTags.diagramName),
arrow: Tag.define(t.contentSeparator),
keyword1: Tag.define(t.keyword),
keyword2: Tag.define(t.controlKeyword),
lineComment: Tag.define(t.lineComment),
messageText1: Tag.define(t.string),
messageText2: Tag.define(t.content),
nodeText: Tag.define(t.variableName),
position: Tag.define(t.modifier),
};
export const journeyTags = {
diagramName: Tag.define(mermaidTags.diagramName),
actor: Tag.define(t.variableName),
keyword: Tag.define(t.keyword),
lineComment: Tag.define(t.lineComment),
score: Tag.define(t.number),
text: Tag.define(t.string),
};
export const requirementTags = {
diagramName: Tag.define(mermaidTags.diagramName),
arrow: Tag.define(t.contentSeparator),
keyword: Tag.define(t.keyword),
lineComment: Tag.define(t.lineComment),
number: Tag.define(t.number),
quotedString: Tag.define(t.string),
unquotedString: Tag.define(t.content),
};
export const ganttTags = {
diagramName: Tag.define(mermaidTags.diagramName),
keyword: Tag.define(t.keyword),
lineComment: Tag.define(t.lineComment),
string: Tag.define(t.string),
};

View File

@@ -0,0 +1,38 @@
export enum DiagramType {
Mermaid = 'MermaidDiagram',
Mindmap = 'MindmapDiagram',
Pie = 'PieDiagram',
Flowchart = 'FlowchartDiagram',
Sequence = 'SequenceDiagram',
Journey = 'JourneyDiagram',
Requirement = 'RequirementDiagram',
Gantt = 'GanttDiagram',
}
export enum MermaidDescriptionName {
Mermaid = 'mermaid',
Mindmap = 'mindmap',
Pie = 'pie',
Flowchart = 'flowchart',
Sequence = 'sequenceDiagram',
Journey = 'journey',
Requirement = 'requirementDiagram',
Gantt = 'gantt',
}
export enum MermaidLanguageType {
Mermaid = 'mermaid',
Mindmap = 'mindmap',
Pie = 'pie',
Flowchart = 'flowchart',
Sequence = 'sequence',
Journey = 'journey',
Requirement = 'requirement',
Gantt = 'gantt',
}
export enum MermaidAlias {
Graph = 'graph',
Sequence = 'sequence',
Requirement = 'requirement',
}

View File

@@ -2,6 +2,7 @@
defineProps<{
title: string;
description?: string;
descriptionType?: 'default' | 'success' | 'error';
}>();
</script>
@@ -9,7 +10,16 @@ defineProps<{
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">{{ title }}</div>
<div v-if="description" class="setting-description">{{ description }}</div>
<div
v-if="description"
class="setting-description"
:class="{
'description-success': descriptionType === 'success',
'description-error': descriptionType === 'error'
}"
>
{{ description }}
</div>
</div>
<div class="setting-control">
<slot></slot>
@@ -48,6 +58,14 @@ defineProps<{
font-size: 11px;
color: var(--settings-text-secondary);
line-height: 1.4;
&.description-success {
color: #4caf50;
}
&.description-error {
color: #f44336;
}
}
}

View File

@@ -2,7 +2,7 @@
import {useConfigStore} from '@/stores/configStore';
import {useBackupStore} from '@/stores/backupStore';
import {useI18n} from 'vue-i18n';
import {computed, onUnmounted} from 'vue';
import {computed} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
@@ -13,18 +13,12 @@ const {t} = useI18n();
const configStore = useConfigStore();
const backupStore = useBackupStore();
onUnmounted(() => {
backupStore.clearStatus();
});
// 认证方式选项
const authMethodOptions = computed(() => [
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
{value: AuthMethod.UserPass, label: t('settings.backup.authMethods.userPass')}
]);
// 备份间隔选项(分钟)
const backupIntervalOptions = computed(() => [
{value: 5, label: t('settings.backup.intervals.5min')},
{value: 10, label: t('settings.backup.intervals.10min')},
@@ -33,124 +27,11 @@ const backupIntervalOptions = computed(() => [
{value: 60, label: t('settings.backup.intervals.1hour')}
]);
// 计算属性 - 启用备份
const enableBackup = computed({
get: () => configStore.config.backup.enabled,
set: (value: boolean) => configStore.setEnableBackup(value)
});
// 计算属性 - 自动备份
const autoBackup = computed({
get: () => configStore.config.backup.auto_backup,
set: (value: boolean) => configStore.setAutoBackup(value)
});
// 仓库URL
const repoUrl = computed({
get: () => configStore.config.backup.repo_url,
set: (value: string) => configStore.setRepoUrl(value)
});
// 认证方式
const authMethod = computed({
get: () => configStore.config.backup.auth_method,
set: (value: AuthMethod) => configStore.setAuthMethod(value)
});
// 备份间隔
const backupInterval = computed({
get: () => configStore.config.backup.backup_interval,
set: (value: number) => configStore.setBackupInterval(value)
});
// 用户名
const username = computed({
get: () => configStore.config.backup.username,
set: (value: string) => configStore.setUsername(value)
});
// 密码
const password = computed({
get: () => configStore.config.backup.password,
set: (value: string) => configStore.setPassword(value)
});
// 访问令牌
const token = computed({
get: () => configStore.config.backup.token,
set: (value: string) => configStore.setToken(value)
});
// SSH密钥路径
const sshKeyPath = computed({
get: () => configStore.config.backup.ssh_key_path,
set: (value: string) => configStore.setSshKeyPath(value)
});
// SSH密钥密码
const sshKeyPassphrase = computed({
get: () => configStore.config.backup.ssh_key_passphrase,
set: (value: string) => configStore.setSshKeyPassphrase(value)
});
// 处理输入变化
const handleRepoUrlChange = (event: Event) => {
const target = event.target as HTMLInputElement;
repoUrl.value = target.value;
};
const handleUsernameChange = (event: Event) => {
const target = event.target as HTMLInputElement;
username.value = target.value;
};
const handlePasswordChange = (event: Event) => {
const target = event.target as HTMLInputElement;
password.value = target.value;
};
const handleTokenChange = (event: Event) => {
const target = event.target as HTMLInputElement;
token.value = target.value;
};
const handleSshKeyPassphraseChange = (event: Event) => {
const target = event.target as HTMLInputElement;
sshKeyPassphrase.value = target.value;
};
const handleAuthMethodChange = (event: Event) => {
const target = event.target as HTMLSelectElement;
authMethod.value = target.value as AuthMethod;
};
const handleBackupIntervalChange = (event: Event) => {
const target = event.target as HTMLSelectElement;
backupInterval.value = parseInt(target.value);
};
// 推送到远程
const pushToRemote = async () => {
await backupStore.pushToRemote();
};
// 重试备份
const retryBackup = async () => {
await backupStore.retryBackup();
};
// 选择SSH密钥文件
const selectSshKeyFile = async () => {
// 使用DialogService选择文件
const selectedPath = await DialogService.SelectFile();
// 检查用户是否取消了选择或路径为空
if (!selectedPath.trim()) {
return;
if (selectedPath.trim()) {
configStore.setSshKeyPath(selectedPath.trim());
}
// 更新SSH密钥路径
sshKeyPath.value = selectedPath.trim();
};
</script>
@@ -158,34 +39,35 @@ const selectSshKeyFile = async () => {
<div class="settings-page">
<!-- 基本设置 -->
<SettingSection :title="t('settings.backup.basicSettings')">
<SettingItem
:title="t('settings.backup.enableBackup')"
>
<ToggleSwitch v-model="enableBackup"/>
<SettingItem :title="t('settings.backup.enableBackup')">
<ToggleSwitch
:modelValue="configStore.config.backup.enabled"
@update:modelValue="configStore.setEnableBackup"
/>
</SettingItem>
<SettingItem
:title="t('settings.backup.autoBackup')"
:class="{ 'disabled-setting': !enableBackup }"
:class="{ 'disabled-setting': !configStore.config.backup.enabled }"
>
<ToggleSwitch v-model="autoBackup" :disabled="!enableBackup"/>
<ToggleSwitch
:modelValue="configStore.config.backup.auto_backup"
@update:modelValue="configStore.setAutoBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
<SettingItem
:title="t('settings.backup.backupInterval')"
:class="{ 'disabled-setting': !enableBackup || !autoBackup }"
:class="{ 'disabled-setting': !configStore.config.backup.enabled || !configStore.config.backup.auto_backup }"
>
<select
class="backup-interval-select"
:value="backupInterval"
@change="handleBackupIntervalChange"
:disabled="!enableBackup || !autoBackup"
>
<option
v-for="option in backupIntervalOptions"
:key="option.value"
:value="option.value"
:value="configStore.config.backup.backup_interval"
@change="(e) => configStore.setBackupInterval(Number((e.target as HTMLSelectElement).value))"
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.auto_backup"
>
<option v-for="option in backupIntervalOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
@@ -194,53 +76,43 @@ const selectSshKeyFile = async () => {
<!-- 仓库配置 -->
<SettingSection :title="t('settings.backup.repositoryConfig')">
<SettingItem
:title="t('settings.backup.repoUrl')"
>
<SettingItem :title="t('settings.backup.repoUrl')">
<input
type="text"
class="repo-url-input"
:value="repoUrl"
@input="handleRepoUrlChange"
:value="configStore.config.backup.repo_url"
@input="(e) => configStore.setRepoUrl((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.repoUrlPlaceholder')"
:disabled="!enableBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</SettingSection>
<!-- 认证配置 -->
<SettingSection :title="t('settings.backup.authConfig')">
<SettingItem
:title="t('settings.backup.authMethod')"
>
<SettingItem :title="t('settings.backup.authMethod')">
<select
class="auth-method-select"
:value="authMethod"
@change="handleAuthMethodChange"
:disabled="!enableBackup"
>
<option
v-for="option in authMethodOptions"
:key="option.value"
:value="option.value"
:value="configStore.config.backup.auth_method"
@change="(e) => configStore.setAuthMethod((e.target as HTMLSelectElement).value as AuthMethod)"
:disabled="!configStore.config.backup.enabled"
>
<option v-for="option in authMethodOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</SettingItem>
<!-- 用户名密码认证 -->
<template v-if="authMethod === AuthMethod.UserPass">
<template v-if="configStore.config.backup.auth_method === AuthMethod.UserPass">
<SettingItem :title="t('settings.backup.username')">
<input
type="text"
class="username-input"
:value="username"
@input="handleUsernameChange"
:value="configStore.config.backup.username"
@input="(e) => configStore.setUsername((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.usernamePlaceholder')"
:disabled="!enableBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
@@ -248,56 +120,50 @@ const selectSshKeyFile = async () => {
<input
type="password"
class="password-input"
:value="password"
@input="handlePasswordChange"
:value="configStore.config.backup.password"
@input="(e) => configStore.setPassword((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.passwordPlaceholder')"
:disabled="!enableBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</template>
<!-- 访问令牌认证 -->
<template v-if="authMethod === AuthMethod.Token">
<SettingItem
:title="t('settings.backup.token')"
>
<template v-if="configStore.config.backup.auth_method === AuthMethod.Token">
<SettingItem :title="t('settings.backup.token')">
<input
type="password"
class="token-input"
:value="token"
@input="handleTokenChange"
:value="configStore.config.backup.token"
@input="(e) => configStore.setToken((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.tokenPlaceholder')"
:disabled="!enableBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</template>
<!-- SSH密钥认证 -->
<template v-if="authMethod === AuthMethod.SSHKey">
<SettingItem
:title="t('settings.backup.sshKeyPath')"
>
<template v-if="configStore.config.backup.auth_method === AuthMethod.SSHKey">
<SettingItem :title="t('settings.backup.sshKeyPath')">
<input
type="text"
class="ssh-key-path-input"
:value="sshKeyPath"
:value="configStore.config.backup.ssh_key_path"
:placeholder="t('settings.backup.sshKeyPathPlaceholder')"
:disabled="!enableBackup"
:disabled="!configStore.config.backup.enabled"
readonly
@click="enableBackup && selectSshKeyFile()"
@click="configStore.config.backup.enabled && selectSshKeyFile()"
/>
</SettingItem>
<SettingItem
:title="t('settings.backup.sshKeyPassphrase')"
>
<SettingItem :title="t('settings.backup.sshKeyPassphrase')">
<input
type="password"
class="ssh-passphrase-input"
:value="sshKeyPassphrase"
@input="handleSshKeyPassphraseChange"
:value="configStore.config.backup.ssh_key_passphrase"
@input="(e) => configStore.setSshKeyPassphrase((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.sshKeyPassphrasePlaceholder')"
:disabled="!enableBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</template>
@@ -307,34 +173,19 @@ const selectSshKeyFile = async () => {
<SettingSection :title="t('settings.backup.backupOperations')">
<SettingItem
:title="t('settings.backup.pushToRemote')"
:description="backupStore.message || undefined"
:descriptionType="backupStore.message ? (backupStore.isError ? 'error' : 'success') : 'default'"
>
<div class="backup-operation-container">
<div class="backup-status-icons">
<span v-if="backupStore.isSuccess" class="success-icon"></span>
<span v-if="backupStore.isError" class="error-icon"></span>
</div>
<button
class="push-button"
@click="() => pushToRemote()"
:disabled="!enableBackup || !repoUrl || backupStore.isPushing"
@click="backupStore.pushToRemote"
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.repo_url || backupStore.isPushing"
:class="{ 'backing-up': backupStore.isPushing }"
>
<span v-if="backupStore.isPushing" class="loading-spinner"></span>
{{ backupStore.isPushing ? t('settings.backup.pushing') : t('settings.backup.actions.push') }}
</button>
<button
v-if="backupStore.isError"
class="retry-button"
@click="() => retryBackup()"
:disabled="backupStore.isPushing"
>
{{ t('settings.backup.actions.retry') }}
</button>
</div>
</SettingItem>
<div v-if="backupStore.errorMessage" class="error-message-row">
{{ backupStore.errorMessage }}
</div>
</SettingSection>
</div>
</template>
@@ -405,38 +256,8 @@ const selectSshKeyFile = async () => {
}
}
// 备份操作容器
.backup-operation-container {
display: flex;
align-items: center;
gap: 12px;
}
// 备份状态图标
.backup-status-icons {
width: 24px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
}
// 成功和错误图标
.success-icon {
color: #4caf50;
font-size: 18px;
font-weight: bold;
}
.error-icon {
color: #f44336;
font-size: 18px;
font-weight: bold;
}
// 按钮样式
.push-button,
.retry-button {
.push-button {
padding: 8px 16px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
@@ -480,30 +301,6 @@ const selectSshKeyFile = async () => {
}
}
.retry-button {
background-color: #ff9800;
border-color: #ff9800;
color: white;
&:hover:not(:disabled) {
background-color: #f57c00;
border-color: #f57c00;
}
}
// 错误信息行样式
.error-message-row {
color: #f44336;
font-size: 11px;
line-height: 1.4;
word-wrap: break-word;
margin-top: 8px;
padding: 8px 16px;
background-color: rgba(244, 67, 54, 0.1);
border-left: 3px solid #f44336;
border-radius: 4px;
}
// 禁用状态
.disabled-setting {
opacity: 0.5;

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { computed, onUnmounted } from 'vue';
import { computed } from 'vue';
import { useConfigStore } from '@/stores/configStore';
import { useUpdateStore } from '@/stores/updateStore';
import SettingSection from '../components/SettingSection.vue';
@@ -12,11 +12,6 @@ const { t } = useI18n();
const configStore = useConfigStore();
const updateStore = useUpdateStore();
// 清理状态
onUnmounted(() => {
updateStore.clearStatus();
});
// 初始化Remarkable实例并配置
const md = new Remarkable({
html: true, // 允许HTML

20
go.mod
View File

@@ -10,11 +10,11 @@ require (
github.com/knadh/koanf/providers/structs v1.0.0
github.com/knadh/koanf/v2 v2.3.0
github.com/stretchr/testify v1.11.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.36
golang.org/x/net v0.46.0
golang.org/x/sys v0.37.0
golang.org/x/text v0.30.0
modernc.org/sqlite v1.39.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.40
golang.org/x/net v0.47.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0
modernc.org/sqlite v1.40.0
resty.dev/v3 v3.0.0-beta.3
)
@@ -29,11 +29,11 @@ require (
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -77,10 +77,10 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/image v0.32.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/image v0.33.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect

48
go.sum
View File

@@ -25,8 +25,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -34,8 +34,8 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -166,8 +166,8 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.36 h1:GQ8vSrFgafITwMd/p4k+WBjG9K/anma9Pk2eJ/5CLsI=
github.com/wailsapp/wails/v3 v3.0.0-alpha.36/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
github.com/wailsapp/wails/v3 v3.0.0-alpha.40 h1:LY0hngVwihlSXveshL5LM8ivjLTHAN6VDjOSF6szI9k=
github.com/wailsapp/wails/v3 v3.0.0-alpha.40/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -176,12 +176,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -189,13 +189,13 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -207,16 +207,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -256,8 +256,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -0,0 +1,817 @@
# Hotkey - 跨平台全局热键库
跨平台 Go 语言全局热键库,支持 Windows、Linux (X11) 和 macOS 操作系统。
## ✨ 特性
- **跨平台支持**Windows、Linux (X11)、macOS 统一 API
- **线程安全**:所有公共 API 使用互斥锁保护
- **标准化错误**:提供统一的错误类型,便于错误处理
- **资源管理**支持手动和自动finalizer资源清理
- **独立实现**:除系统库外无第三方 Go 依赖
- **状态查询**:提供 `IsRegistered()``IsClosed()` 方法
## 📦 安装
```bash
go get -u voidraft/internal/common/hotkey
```
### 平台特定依赖
#### Linux
需要安装 X11 开发库:
```bash
# Debian/Ubuntu
sudo apt install -y libx11-dev
# CentOS/RHEL
sudo yum install -y libX11-devel
# Arch Linux
sudo pacman -S libx11
```
**无界面环境(云服务器等)**
```bash
# 安装虚拟显示服务器
sudo apt install -y xvfb
# 启动虚拟显示
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
export DISPLAY=:99.0
```
#### macOS
**GUI 应用**Wails、Fyne、Cocoa 等):框架管理主事件循环,直接使用即可。
**CLI 应用**:需要使用 `darwin.Init()` 启动 NSApplication 主事件循环,参见下文"macOS CLI 应用示例"。
#### Windows
无额外依赖,开箱即用。
## 📖 使用指南
### 基本用法
```go
package main
import (
"fmt"
"voidraft/internal/common/hotkey"
)
func main() {
// 创建热键Ctrl+Shift+S
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
// 注册热键
if err := hk.Register(); err != nil {
panic(err)
}
defer hk.Close() // 确保资源清理
fmt.Println("热键已注册,按 Ctrl+Shift+S 触发...")
// 监听热键事件
for {
select {
case <-hk.Keydown():
fmt.Println("热键按下!")
case <-hk.Keyup():
fmt.Println("热键释放!")
}
}
}
```
### 完整示例:带错误处理
```go
package main
import (
"errors"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"voidraft/internal/common/hotkey"
)
func main() {
// 创建热键
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModAlt}, hotkey.KeyQ)
// 注册热键,处理可能的错误
if err := hk.Register(); err != nil {
switch {
case errors.Is(err, hotkey.ErrHotkeyConflict):
log.Fatal("热键冲突:该组合键已被其他程序占用")
case errors.Is(err, hotkey.ErrPlatformUnavailable):
log.Fatal("平台不支持:", err)
case errors.Is(err, hotkey.ErrAlreadyRegistered):
log.Fatal("热键已经注册")
default:
log.Fatal("注册热键失败:", err)
}
}
defer hk.Close()
fmt.Println("热键 Ctrl+Alt+Q 已注册")
fmt.Println("按下热键触发,或按 Ctrl+C 退出...")
// 优雅退出
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// 事件循环
for {
select {
case <-hk.Keydown():
fmt.Println("[事件] 热键按下")
// 执行你的业务逻辑
case <-hk.Keyup():
fmt.Println("[事件] 热键释放")
case <-sigChan:
fmt.Println("\n正在退出...")
return
}
}
}
```
### 防抖处理(应用层)
如果热键按住会持续触发,建议在应用层添加防抖:
```go
package main
import (
"fmt"
"log"
"time"
"voidraft/internal/common/hotkey"
)
func main() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyD)
if err := hk.Register(); err != nil {
log.Fatal(err)
}
defer hk.Close()
// 防抖参数
var lastTrigger time.Time
debounceInterval := 800 * time.Millisecond // 推荐 800ms
for {
select {
case <-hk.Keydown():
now := time.Now()
// 检查距离上次触发的时间
if !lastTrigger.IsZero() && now.Sub(lastTrigger) < debounceInterval {
fmt.Println("触发被忽略(防抖)")
continue
}
lastTrigger = now
fmt.Println("热键触发!")
// 执行你的业务逻辑
}
}
}
```
### 动态修改热键
```go
// 注销旧热键
if err := hk.Unregister(); err != nil {
log.Printf("注销失败:%v", err)
}
// 修改热键组合
hk = hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyF1)
// 重新注册
if err := hk.Register(); err != nil {
log.Printf("注册失败:%v", err)
}
// 重要:重新获取通道引用!
keydownChan := hk.Keydown()
keyupChan := hk.Keyup()
```
### 状态检查
```go
// 检查热键是否已注册
if hk.IsRegistered() {
fmt.Println("热键已注册")
}
// 检查热键是否已关闭
if hk.IsClosed() {
fmt.Println("热键已关闭,无法再使用")
}
```
### 资源管理最佳实践
```go
func registerHotkey() error {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyH)
// 使用 defer 确保资源释放
defer hk.Close()
if err := hk.Register(); err != nil {
return err
}
// ... 使用热键 ...
return nil
}
// Close() 是幂等的,多次调用是安全的
hk.Close()
hk.Close() // 不会 panic
```
## 🔑 支持的修饰键
### 所有平台通用
```go
hotkey.ModCtrl // Ctrl 键
hotkey.ModShift // Shift 键
hotkey.ModAlt // Alt 键Linux: 通常映射到 Mod1
```
### Linux 额外支持
```go
hotkey.Mod1 // 通常是 Alt
hotkey.Mod2 // 通常是 Num Lock
hotkey.Mod3 // (较少使用)
hotkey.Mod4 // 通常是 Super/Windows 键
hotkey.Mod5 // (较少使用)
```
### 组合使用
```go
// Ctrl+Shift+Alt+S
hk := hotkey.New(
[]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift, hotkey.ModAlt},
hotkey.KeyS,
)
```
## ⌨️ 支持的按键
### 字母键
```go
hotkey.KeyA - hotkey.KeyZ // A-Z
```
### 数字键
```go
hotkey.Key0 - hotkey.Key9 // 0-9
```
### 功能键
```go
hotkey.KeyF1 - hotkey.KeyF20 // F1-F20
```
### 特殊键
```go
hotkey.KeySpace // 空格
hotkey.KeyReturn // 回车
hotkey.KeyEscape // ESC
hotkey.KeyDelete // Delete
hotkey.KeyTab // Tab
hotkey.KeyLeft // 左箭头
hotkey.KeyRight // 右箭头
hotkey.KeyUp // 上箭头
hotkey.KeyDown // 下箭头
```
### 自定义键码
如果需要的键未预定义,可以直接使用键码:
```go
// 使用自定义键码 0x15
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.Key(0x15))
```
## ⚠️ 错误类型
```go
// 检查特定错误类型
if errors.Is(err, hotkey.ErrAlreadyRegistered) {
// 热键已经注册
}
if errors.Is(err, hotkey.ErrNotRegistered) {
// 热键未注册
}
if errors.Is(err, hotkey.ErrClosed) {
// 热键已关闭,无法再使用
}
if errors.Is(err, hotkey.ErrHotkeyConflict) {
// 热键冲突,已被其他程序占用
}
if errors.Is(err, hotkey.ErrPlatformUnavailable) {
// 平台不可用(如 Linux 无 X11
}
if errors.Is(err, hotkey.ErrFailedToRegister) {
// 注册失败(其他原因)
}
if errors.Is(err, hotkey.ErrFailedToUnregister) {
// 注销失败
}
```
## 🎯 平台特定注意事项
### Linux
#### 1. AutoRepeat 行为
Linux 的 X11 会在按键持续按下时重复发送 `KeyPress` 事件。如果你的应用对此敏感,需要做防抖处理:
```go
var lastTrigger time.Time
debounceInterval := 500 * time.Millisecond
for {
select {
case <-hk.Keydown():
now := time.Now()
if now.Sub(lastTrigger) < debounceInterval {
continue // 忽略重复触发
}
lastTrigger = now
// 处理热键事件
fmt.Println("热键触发!")
}
}
```
#### 2. 键位映射差异
不同的 Linux 发行版和桌面环境可能有不同的键位映射。建议:
- 使用标准的 `ModCtrl``ModShift``ModAlt`
- 避免依赖 `Mod2``Mod3``Mod5`(映射不一致)
- `Mod4` 通常是 Super/Windows 键,但也可能不同
#### 3. Wayland 支持
当前版本仅支持 X11。在 Wayland 环境下:
- 需要运行在 XWayland 兼容层
- 或设置 `GDK_BACKEND=x11` 环境变量
- 原生 Wayland 支持正在开发中
#### 4. Display 连接复用
本库已优化 Linux 实现,在热键注册期间保持 X11 Display 连接:
```
注册时: XOpenDisplay → XGrabKey → 保持连接
事件循环: 使用相同连接 → XNextEvent
注销时: XUngrabKey → XCloseDisplay
```
这大幅降低了资源开销和延迟。
### Windows
#### 1. 热键按下事件
Windows 使用 `RegisterHotKey` API 注册系统级热键,通过 `WM_HOTKEY` 消息接收按键事件。
**实现细节**
- 使用 `PeekMessageW` (Unicode) 轮询消息队列
- 10ms 轮询间隔CPU 占用约 0.3-0.5%
- `isKeyDown` 状态标志防止重复触发
- 按下事件延迟通常 < 10ms
#### 2. 热键释放事件
Windows `RegisterHotKey` API 不提供键释放通知,本库使用 `GetAsyncKeyState` 轮询检测:
```go
// 检测键释放(每 10ms 检查一次)
if isKeyDown && GetAsyncKeyState(key) == 0 {
keyupIn <- struct{}{}
isKeyDown = false
}
```
**特性**
- 释放检测延迟:通常 10-20ms
- 仅在按键按下后激活检测
- 依赖于 `GetAsyncKeyState` 的精度
#### 3. 持续按住行为
Windows 在持续按住热键时会重复发送 `WM_HOTKEY` 消息。本库通过 `isKeyDown` 标志防止同一次按住重复触发 Keydown 事件。
如果需要防止快速连续按键,建议在应用层添加防抖:
```go
var lastTrigger time.Time
debounceInterval := 100 * time.Millisecond
for {
<-hk.Keydown()
now := time.Now()
if now.Sub(lastTrigger) < debounceInterval {
continue // 忽略
}
lastTrigger = now
// 处理事件...
}
```
#### 4. 系统保留热键
某些热键被 Windows 系统保留,无法注册:
- `Win+L`:锁定屏幕
- `Ctrl+Alt+Del`:安全选项
- `Alt+Tab`:切换窗口
- `Alt+F4`:关闭窗口
- `Win+D`:显示桌面
尝试注册这些热键会返回 `ErrFailedToRegister`
#### 5. 热键冲突
如果热键已被其他应用注册,`RegisterHotKey` 会失败。常见冲突来源:
- 游戏快捷键
- 输入法快捷键
- 快捷键管理工具AutoHotkey 等)
- 其他全局热键应用
返回错误为 `ErrFailedToRegister`Windows 不区分冲突和其他失败)。
#### 6. 线程模型
- 热键注册和消息循环运行在同一个 OS 线程上
- 使用 `runtime.LockOSThread()` 确保线程亲和性
- 不要求是主线程(与 macOS 不同)
- `ph.funcs` channel 用于在事件循环线程中执行注册/注销操作
---
### macOS
#### 1. 主事件循环要求
macOS 的 Carbon 事件 API 通过 GCDGrand Central Dispatch调度到主队列执行。
**GUI 应用Wails、Cocoa 等)**
- ✅ 框架已自动管理主事件循环
- ✅ 热键功能开箱即用,无需额外配置
**纯 CLI 应用**
- ⚠️ 需要手动启动 macOS 运行循环
- 参见下文"macOS 纯 CLI 应用示例"
#### 2. 权限问题
macOS 可能需要辅助功能权限。如果热键无法注册,请检查:
```
系统偏好设置 → 安全性与隐私 → 隐私 → 辅助功能
```
将你的应用添加到允许列表。
#### 3. Carbon vs Cocoa
当前实现使用 Carbon API稳定且兼容性好。未来版本可能会迁移到更现代的 Cocoa API。
#### 4. macOS 纯 CLI 应用示例
如果你的应用**不是 GUI 应用**,需要启动主事件循环。
**最简单的方法:使用 darwin.Init()(推荐)**
```go
//go:build darwin
package main
import (
"fmt"
"voidraft/internal/common/hotkey"
"voidraft/internal/common/hotkey/darwin"
)
func main() {
// 使用 darwin.Init 启动主事件循环
darwin.Init(run)
}
func run() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCmd, hotkey.ModShift}, hotkey.KeyA)
if err := hk.Register(); err != nil {
fmt.Printf("注册失败: %v\n", err)
return
}
defer hk.Close()
fmt.Println("热键已注册: Cmd+Shift+A")
for {
<-hk.Keydown()
fmt.Println("热键触发!")
}
}
```
**高级用法:使用 darwin.Call() 在主线程执行操作**
```go
darwin.Init(func() {
hk := hotkey.New(...)
hk.Register()
for {
<-hk.Keydown()
// 在主线程执行 macOS API 调用
darwin.Call(func() {
// 例如调用 Cocoa/AppKit API
fmt.Println("这段代码在主线程执行")
})
}
})
```
**注意**
- GUI 应用无需调用 `darwin.Init()`,框架已处理
- `darwin.Call()` 用于需要主线程的特定 macOS API
- 热键注册本身已通过 `dispatch_get_main_queue()` 自动调度到主线程
## 🔬 架构设计
### 目录结构
```
internal/common/hotkey/
├── hotkey.go # 统一的公共 API
├── hotkey_windows.go # Windows 平台适配器
├── hotkey_darwin.go # macOS 平台适配器
├── hotkey_linux.go # Linux 平台适配器
├── hotkey_nocgo.go # 非 CGO 平台占位符
├── windows/
│ ├── hotkey.go # Windows 核心实现
│ └── mainthread.go # Windows 线程管理
├── darwin/
│ ├── hotkey.go # macOS 核心实现
│ ├── hotkey.m # Objective-C/Carbon 代码
│ └── mainthread.go # macOS 线程管理
└── linux/
├── hotkey.go # Linux 核心实现
├── hotkey.c # X11 C 代码
└── mainthread.go # Linux 线程管理
```
### 设计原则
1. **平台隔离**:每个平台的实现完全独立,通过构建标签分离
2. **统一接口**:所有平台提供相同的 Go API
3. **资源安全**:自动资源管理,防止泄漏
4. **并发安全**:所有公共方法都是线程安全的
5. **错误透明**:标准化错误类型,便于处理
### 事件流程
```
用户按键
操作系统捕获
平台特定 APIWin32/X11/Carbon
C/Objective-C 回调
Go channel类型转换
用户应用代码
```
### 并发模型
```
主 Goroutine 事件 Goroutine 转换 Goroutine
│ │ │
Register() ────启动──────→ eventLoop() │
│ │ │
│ 等待 OS 事件 │
│ │ │
│ ├────发送────→ 类型转换 │
│ │ │ │
│ │ └─→ Keydown()/Keyup()
│ │ │
Unregister() ──停止信号──→ 退出循环 │
│ │ │
└──────等待清理─────────────┴──────────────────────────┘
```
## 📊 性能特性
### 资源占用
- **内存**:每个热键约 1-2 KB包括 goroutines、channels、CGO handles
- **Goroutines**:每个热键 3 个
- 1 个事件循环 goroutine
- 2 个通道转换 goroutine (interface{} → Event)
- **CPU**
- **Windows**10ms 轮询,约 0.3-0.5% CPU单核空闲时
- **Linux**:事件驱动 (`XNextEvent` 阻塞),几乎无 CPU 占用
- **macOS**:事件驱动 (GCD 调度),几乎无 CPU 占用
- **线程**
- 每个热键 1 个 OS 线程(通过 `runtime.LockOSThread()` 锁定)
### 延迟
**按下事件 (Keydown)**
- Windows: < 10ms取决于轮询间隔
- Linux: < 10msX11 事件延迟)
- macOS: < 5msCarbon 事件延迟)
**释放事件 (Keyup)**
- Windows: 10-20ms`GetAsyncKeyState` 轮询检测)
- Linux: < 15msX11 KeyRelease 事件)
- macOS: < 10msCarbon 事件延迟)
### 使用建议
1. **资源管理**
- 使用 `defer hk.Close()` 确保资源释放
- 不需要时及时调用 `Unregister()``Close()`
- 避免频繁创建/销毁热键对象
2. **事件处理**
- 热键事件处理应快速返回,避免阻塞 channel
- 复杂逻辑应在新 goroutine 中处理
- 考虑应用层防抖(特别是 Linux AutoRepeat
3. **错误处理**
- 始终检查 `Register()` 返回的错误
- 使用 `errors.Is()` 判断错误类型
- 处理热键冲突场景(提供备用方案或用户提示)
4. **平台差异**
- Windows Keyup 事件有轻微延迟(正常现象)
- Linux 可能需要防抖处理 AutoRepeat
- macOS CLI 应用需要启动主事件循环
## 🐛 故障排查
### Linux: "Failed to initialize the X11 display"
**问题**:无法连接到 X11 显示服务器
**解决方案**
```bash
# 检查 DISPLAY 环境变量
echo $DISPLAY
# 如果为空,设置它
export DISPLAY=:0
# 或使用虚拟显示
Xvfb :99 -screen 0 1024x768x24 &
export DISPLAY=:99
```
### macOS: 热键不触发
**问题**:注册成功但热键无响应
**解决方案**
1. 检查辅助功能权限(见上文)
2. 如果是纯 CLI 应用,确保启动了主运行循环(见上文示例)
3. 检查其他应用是否占用了该热键
### Windows: ErrFailedToRegister
**问题**:热键注册失败
**可能原因**
1. 热键已被其他应用占用AutoHotkey、游戏、输入法等
2. 尝试注册系统保留热键Win+L、Ctrl+Alt+Del 等)
3. 热键 ID 冲突(极少见)
**解决方案**
1. 检查任务管理器,关闭可能冲突的应用
2. 使用不同的热键组合
3. 在应用中提供热键自定义功能,让用户选择可用组合
4. 提供友好的错误提示,说明热键被占用
**调试方法**
```go
if err := hk.Register(); err != nil {
if errors.Is(err, hotkey.ErrFailedToRegister) {
log.Printf("热键注册失败: %v", err)
log.Printf("提示:该热键可能已被其他应用使用")
// 尝试备用热键...
}
}
```
### Windows: Keyup 事件延迟
**问题**:释放事件比预期晚 10-20ms
**原因**Windows API 限制,`RegisterHotKey` 不提供释放通知,需要轮询检测。
**这是正常行为**
- 10ms 轮询间隔导致的固有延迟
- 对大多数应用场景影响很小
- 如果需要精确的释放时机,考虑使用底层键盘钩子(复杂度更高)
### 所有平台: Keyup 事件丢失
**问题**:只收到 Keydown没有 Keyup
**可能原因**
1. 在接收 Keyup 前调用了 `Unregister()`
2. 通道缓冲区满(不太可能,使用了无缓冲通道)
3. Windows 上正常(有轻微延迟)
**解决方案**
```go
// 确保在处理完事件后再注销
for {
select {
case <-hk.Keydown():
// 处理...
case <-hk.Keyup():
// 处理...
// 现在可以安全注销
hk.Unregister()
return
}
}
```
## 🤝 贡献
欢迎贡献!请遵循以下原则:
1. **保持简洁**:不要过度优化
2. **平台一致**:确保 API 在所有平台表现一致
3. **测试充分**:在所有支持的平台测试
4. **文档完善**:更新相关文档
## 📄 许可证
本项目是 Voidraft 的一部分,遵循项目主许可证。
## 🙏 致谢
本库的设计和实现参考了多个开源项目的经验:
- `golang.design/x/hotkey` - 基础架构设计
- `github.com/robotn/gohook` - 跨平台事件处理思路
特别感谢所有为跨平台 Go 开发做出贡献的开发者们!
---
**如有问题或建议,欢迎提交 Issue** 🚀

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