26 Commits

Author SHA1 Message Date
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
689b0d5d14 🚀 Fixed resource path issues 2025-11-04 01:21:10 +08:00
a058e62595 🚀 Fixed resource path issues 2025-11-04 01:07:28 +08:00
8571fc0f5c 🚀 Fixed resource path issues 2025-11-04 00:57:49 +08:00
4dad0a86b3 🚀 Improve deployment scripts 2025-11-04 00:36:32 +08:00
3168b7ff43 🚀 Modify deployment command 2025-11-04 00:27:34 +08:00
d002a5be5a 🚀 Adjust base path 2025-11-04 00:16:39 +08:00
24a550463c 🚀 Remove dead links 2025-11-03 23:58:27 +08:00
14ae3e80c4 Beautify style and add automatic deployment scripts 2025-11-03 23:38:15 +08:00
e4d3969e95 📝 Build document directory structure 2025-11-03 22:49:38 +08:00
0b16d1d4ac Merge branch 'master' into docs 2025-11-03 22:18:06 +08:00
300514531d Improve HTTP client function 2025-11-03 22:15:45 +08:00
6a4780b002 📝 Replace official website framework 2025-11-02 22:48:33 +08:00
197 changed files with 14676 additions and 6076 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 }}

67
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
#
name: Deploy VitePress site to Pages
on:
# 在针对 `main` 分支的推送上运行。如果你
# 使用 `master` 分支作为默认分支,请将其更改为 `master`
push:
branches: [master]
# 允许你从 Actions 选项卡手动运行此工作流程
workflow_dispatch:
# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
concurrency:
group: pages
cancel-in-progress: false
jobs:
# 构建工作
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 如果未启用 lastUpdated则不需要
# - uses: pnpm/action-setup@v3 # 如果使用 pnpm请取消此区域注释
# with:
# version: 9
# - uses: oven-sh/setup-bun@v1 # 如果使用 Bun请取消注释
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: cd frontend && npm ci
- name: Build with VitePress
run: cd frontend && npm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: frontend/docs/.vitepress/dist
# 部署工作
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ frontend/node_modules
build/linux/appimage/build build/linux/appimage/build
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
.idea .idea
frontend/docs/.vitepress/cache/
frontend/docs/.vitepress/dist/

View File

@@ -12,25 +12,13 @@ vars:
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
tasks: 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: build:
summary: Builds the application summary: Builds the application
deps: [version]
cmds: cmds:
- task: "{{OS}}:build" - task: "{{OS}}:build"
package: package:
summary: Packages a production build of the application summary: Packages a production build of the application
deps: [version]
cmds: cmds:
- task: "{{OS}}:package" - 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/)

View File

@@ -1 +0,0 @@
voidraft.landaiqing.cn

View File

@@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>voidraft - Changelog</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/changelog.css">
<link rel="icon" href="img/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="theme-dark">
<div class="container">
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title" data-en="voidraft Changelog" data-zh="voidraft 更新日志">voidraft Changelog</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
</button>
<button id="lang-toggle" class="btn btn-secondary">
<i class="fas fa-language"></i> 中/EN
</button>
</div>
</div>
<div class="card-content">
<!-- 导航区域 -->
<div class="nav-links">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> <span data-en="Home" data-zh="首页">Home</span>
</a>
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary">
<i class="fab fa-github"></i> <span data-en="Source Code" data-zh="源代码">Source Code</span>
</a>
</div>
<!-- 加载中提示 -->
<div id="loading" class="loading-container">
<div class="loading-spinner"></div>
<p data-en="Loading releases..." data-zh="正在加载版本信息...">Loading releases...</p>
</div>
<!-- 更新日志内容 -->
<div id="changelog" class="changelog-container">
<!-- 通过JavaScript动态填充内容 -->
</div>
<!-- 错误信息 -->
<div id="error-message" class="error-container" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<p data-en="Failed to load release information. Please try again later."
data-zh="加载版本信息失败,请稍后再试。">Failed to load release information. Please try again later.</p>
</div>
</div>
<!-- 页脚 -->
<footer class="footer">
<p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 voidraft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
</div>
</footer>
</div>
</div>
<script src="js/script.js"></script>
<script src="js/changelog.js"></script>
</body>
</html>

View File

@@ -1,347 +0,0 @@
/* 更新日志页面样式 */
.nav-links {
margin-bottom: 30px;
display: flex;
gap: 15px;
}
.loading-container {
text-align: center;
padding: 40px 0;
background-color: transparent;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.theme-dark .loading-spinner {
border-color: rgba(255, 255, 255, 0.1);
border-left-color: var(--primary-color);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
text-align: center;
color: var(--error-color);
padding: 20px;
border: 2px dashed var(--error-color);
margin: 20px 0;
border-radius: 4px;
background-color: rgba(var(--card-bg-rgb), 0.7);
}
.error-container i {
font-size: 24px;
margin-bottom: 10px;
}
/* 更新日志容器 */
.changelog-container {
display: none;
position: relative;
z-index: 1;
}
.release {
margin-bottom: 40px;
border-left: 4px solid var(--primary-color);
padding-left: 20px;
background-color: rgba(var(--card-bg-rgb), 0.5);
padding: 15px 20px;
border-radius: 4px;
}
.release-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.release-version {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
}
.release-date {
color: var(--text-color);
opacity: 0.7;
font-size: 14px;
}
.release-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
background-color: var(--primary-color);
color: #000;
}
.release-badge.pre-release {
background-color: var(--warning-color);
}
.release-description {
margin-bottom: 20px;
line-height: 1.6;
}
.release-assets {
background-color: rgba(var(--light-bg-rgb), 0.7);
padding: 15px;
border-radius: 4px;
margin-top: 15px;
}
.release-assets-title {
font-size: 16px;
margin-bottom: 10px;
}
.asset-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.asset-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.asset-item:last-child {
border-bottom: none;
}
.asset-icon {
margin-right: 10px;
color: var(--accent-color);
}
.asset-name {
flex-grow: 1;
}
.asset-size {
font-size: 12px;
color: var(--text-color);
opacity: 0.7;
}
/* 资源下载按钮 */
.download-btn {
margin-left: 10px;
padding: 3px 10px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 12px;
transition: all 0.2s ease;
display: inline-block;
text-align: center;
}
.download-btn:hover {
background-color: var(--secondary-color);
}
.markdown-content {
line-height: 1.8;
overflow-wrap: break-word;
background-color: transparent;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin: 10px 0;
}
.markdown-content li {
margin-bottom: 8px;
}
.markdown-content li:last-child {
margin-bottom: 0;
}
.markdown-content hr {
border: none;
border-top: 2px dashed var(--border-color);
margin: 20px 0;
}
.markdown-content br {
display: block;
content: "";
margin-top: 10px;
}
.markdown-content code {
font-family: 'IBM Plex Mono', monospace;
background-color: rgba(128, 128, 128, 0.1);
padding: 2px 4px;
border-radius: 3px;
font-size: 90%;
}
.markdown-content pre {
background-color: rgba(128, 128, 128, 0.1);
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin: 15px 0;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
}
.markdown-content a {
color: var(--primary-color);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.data-source {
padding: 10px 15px;
margin-bottom: 20px;
background-color: rgba(var(--light-bg-rgb), 0.7);
border-radius: 4px;
font-size: 14px;
text-align: right;
opacity: 0.7;
}
.data-source a {
color: var(--primary-color);
text-decoration: none;
font-weight: bold;
}
.data-source a:hover {
text-decoration: underline;
}
/* Markdown内容样式增强 */
.markdown-content blockquote {
border-left: 4px solid var(--primary-color);
padding: 10px 15px;
margin: 15px 0;
background-color: rgba(var(--light-bg-rgb), 0.5);
border-radius: 0 4px 4px 0;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin: 10px 0;
}
/* 移动设备响应式优化 */
@media (max-width: 768px) {
.release-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.release-assets {
padding: 12px 8px;
}
.asset-item {
flex-wrap: wrap;
padding: 12px 0;
position: relative;
}
.asset-name {
width: 100%;
margin-bottom: 8px;
word-break: break-all;
}
.asset-size {
margin-left: 25px;
}
.download-btn {
margin-left: 10px;
padding: 5px 12px;
}
}
@media (max-width: 480px) {
.release {
padding-left: 12px;
}
.asset-item {
flex-direction: column;
align-items: flex-start;
}
.asset-icon {
margin-bottom: 5px;
}
.asset-size {
margin-left: 0;
margin-top: 5px;
}
.download-btn {
margin-left: 0;
margin-top: 10px;
width: 100%;
text-align: center;
padding: 8px;
}
.markdown-content pre {
padding: 10px;
margin: 10px 0;
}
}
/* 确保日志页面页脚样式一致 */
.footer {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.footer-text {
margin: 0 0 15px 0;
}

View File

@@ -1,45 +0,0 @@
/* cyrillic-ext */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -1,27 +0,0 @@
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -1,717 +0,0 @@
@import url('./space-mono-font.css');
@import url('./ibm-plex-mono-font.css');
/* 浅色主题 */
:root {
--bg-color: #fefefe;
--text-color: #000000;
--primary-color: #F08080;
--primary-color-rgb: 240, 128, 128;
--secondary-color: #ff006e;
--accent-color: #073B4C;
--card-bg: #ffffff;
--card-bg-rgb: 255, 255, 255;
--border-color: #000000;
--light-bg: #f0f0f0;
--light-bg-rgb: 240, 240, 240;
--shadow-color: rgba(240, 128, 128, 0.5);
--success-color: #27c93f;
--warning-color: #FFD166;
--error-color: #ff006e;
--info-color: #118ab2;
--code-bg: #ffffff;
--code-bg-rgb: 255, 255, 255;
--preview-header-bg: #f0f0f0;
--preview-header-bg-rgb: 240, 240, 240;
--grid-color-1: rgba(0, 0, 0, 0.08);
--grid-color-2: rgba(0, 0, 0, 0.05);
--header-title-color: #000000;
}
/* 暗色主题变量 */
.theme-dark {
--bg-color: #121212;
--text-color: #ffffff;
--primary-color: #F08080;
--primary-color-rgb: 240, 128, 128;
--secondary-color: #ff006e;
--accent-color: #118ab2;
--card-bg: #1e1e1e;
--card-bg-rgb: 30, 30, 30;
--border-color: #ffffff;
--light-bg: #2a2a2a;
--light-bg-rgb: 42, 42, 42;
--shadow-color: rgba(240, 128, 128, 0.5);
--success-color: #27c93f;
--warning-color: #FFD166;
--error-color: #ff006e;
--info-color: #118ab2;
--code-bg: #1e1e1e;
--code-bg-rgb: 30, 30, 30;
--preview-header-bg: #252526;
--preview-header-bg-rgb: 37, 37, 38;
--grid-color-1: rgba(255, 255, 255, 0.08);
--grid-color-2: rgba(255, 255, 255, 0.05);
--header-title-color: #000000;
}
/* 主题切换和语言切换的过渡效果 */
.theme-transition,
.theme-transition *,
.lang-transition,
.lang-transition * {
transition: all 0.3s ease !important;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes gridMove {
0% {
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
}
100% {
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
}
}
body {
background-color: var(--bg-color);
background-image:
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
background-position: center;
animation: gridMove 40s linear infinite;
font-family: 'Space Mono', monospace;
color: var(--text-color);
line-height: 1.6;
padding: 20px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 卡片容器 */
.card {
background-color: var(--card-bg);
background-image:
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
background-position: center;
border: 4px solid var(--border-color);
box-shadow: 12px 12px 0 var(--shadow-color);
margin-bottom: 40px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
z-index: 10;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 16px 16px 0 var(--shadow-color);
}
/* 卡片头部 */
.card-header {
background-color: rgba(var(--primary-color-rgb), 0.9);
border-bottom: 4px solid var(--border-color);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.card-title {
font-size: 24px;
font-weight: bold;
margin: 0;
color: var(--header-title-color);
}
.card-controls {
display: flex;
gap: 10px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: var(--secondary-color);
color: #fff;
text-decoration: none;
font-weight: bold;
border: 3px solid var(--border-color);
box-shadow: 4px 4px 0 var(--shadow-color);
transition: all 0.2s ease;
cursor: pointer;
font-family: 'Space Mono', monospace;
font-size: 14px;
}
.btn:hover {
background: var(--card-bg);
color: var(--primary-color);
border: 3px solid var(--primary-color);
box-shadow: none;
}
.btn-secondary {
background: var(--light-bg);
color: var(--text-color);
}
.btn-secondary:hover {
background: var(--card-bg);
color: var(--primary-color);
border: 3px solid var(--primary-color);
}
/* 卡片内容 */
.card-content {
padding: 30px;
position: relative;
z-index: 1;
background-color: rgba(var(--card-bg-rgb), 0.5);
}
/* Logo区域 */
.logo-container {
text-align: center;
margin-bottom: 40px;
}
.logo-frame {
width: 150px;
height: 150px;
background: var(--card-bg);
border: 4px solid var(--border-color);
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.logo-image {
width: 130px;
height: 130px;
object-fit: contain;
border: 2px solid var(--border-color);
}
.logo-text {
font-size: 32px;
font-weight: bold;
margin: 0;
}
.tagline {
font-size: 16px;
margin: 10px 0 0;
color: var(--accent-color);
}
/* 介绍区域 */
.intro-box {
border: 2px dashed var(--border-color);
padding: 20px;
background-color: rgba(var(--light-bg-rgb), 0.7);
margin-bottom: 30px;
text-align: center;
}
.intro-text {
font-size: 16px;
margin-bottom: 0;
}
/* 按钮组 */
.button-group {
display: flex;
justify-content: center;
gap: 20px;
margin: 30px 0;
}
/* 特性网格 */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
margin: 40px 0;
}
/* 特性卡片 */
.feature-card {
background-color: rgba(var(--card-bg-rgb), 0.8);
border: 3px solid var(--border-color);
box-shadow: 5px 5px 0 var(--shadow-color);
padding: 20px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card:hover {
transform: translateY(-3px);
box-shadow: 7px 7px 0 var(--shadow-color);
}
.feature-icon {
font-size: 24px;
margin-bottom: 15px;
color: var(--secondary-color);
}
.feature-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.feature-desc {
font-size: 14px;
}
/* 预览区域 */
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 30px 0;
}
@media (max-width: 768px) {
.preview-container {
grid-template-columns: 1fr;
}
}
/* 预览窗口 */
.preview-window {
border: 3px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
margin: 10px;
flex: 1;
min-width: 300px;
background-color: rgba(var(--card-bg-rgb), 0.7);
display: flex;
flex-direction: column;
box-shadow: 5px 5px 0 var(--shadow-color);
}
/* 预览头部 */
.preview-header {
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
padding: 10px;
display: flex;
align-items: center;
border-bottom: 2px solid var(--border-color);
}
.preview-controls {
display: flex;
gap: 6px;
margin-right: 15px;
}
.preview-btn {
width: 12px;
height: 12px;
border-radius: 50%;
border: 0.5px solid rgba(0, 0, 0, 0.1);
}
.preview-btn:nth-child(1) {
background-color: #ff5f56;
}
.preview-btn:nth-child(2) {
background-color: #ffbd2e;
}
.preview-btn:nth-child(3) {
background-color: #27c93f;
}
.preview-title {
font-size: 13px;
opacity: 0.8;
color: var(--text-color);
font-weight: normal;
}
/* 预览内容 */
.preview-content {
padding: 15px;
flex-grow: 1;
overflow: auto;
background-color: rgba(var(--code-bg-rgb), 0.5);
}
/* 代码块容器 */
.code-block-wrapper {
background-color: rgba(var(--code-bg-rgb), 0.8);
border: 2px solid var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
/* 块头部 */
.block-header {
background-color: rgba(var(--light-bg-rgb), 0.8);
padding: 8px 12px;
border-bottom: 2px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.block-language {
color: rgba(128, 128, 128, 0.8);
font-family: 'IBM Plex Mono', monospace;
display: flex;
align-items: center;
}
.block-language::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-right: 5px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.code-block {
font-family: 'IBM Plex Mono', monospace;
font-size: 13px;
line-height: 1.6;
margin: 0;
white-space: pre;
tab-size: 4;
-moz-tab-size: 4;
padding: 10px;
}
.theme-dark .code-block-wrapper {
border-color: rgba(255, 255, 255, 0.15);
}
.theme-dark .block-header {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.15);
}
.theme-dark .block-language {
color: rgba(255, 255, 255, 0.6);
}
.theme-dark .block-language::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
}
.theme-dark .code-block {
color: #d4d4d4;
}
/* 代码高亮 */
.theme-dark .keyword { color: #c586c0; }
.theme-dark .function { color: #dcdcaa; }
.theme-dark .variable { color: #9cdcfe; }
.theme-dark .string { color: #ce9178; }
.theme-dark .comment { color: #6a9955; }
.theme-dark .class { color: #4ec9b0; }
.theme-dark .parameter { color: #9cdcfe; }
.theme-dark .built-in { color: #4ec9b0; }
/* 浅色主题代码高亮 */
.keyword { color: #af00db; }
.function { color: #795e26; }
.variable { color: #001080; }
.string { color: #a31515; }
.comment { color: #008000; }
.class { color: #267f99; }
.parameter { color: #001080; }
.built-in { color: #267f99; }
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border: none;
transition: opacity 0.3s ease;
}
.theme-dark .light-theme-img {
display: none !important;
}
.theme-dark .dark-theme-img {
display: block;
}
body:not(.theme-dark) .dark-theme-img {
display: none !important;
}
body:not(.theme-dark) .light-theme-img {
display: block !important;
}
/* 技术栈列表 */
.tech-list {
list-style: none;
padding: 0;
margin: 0;
}
/* 技术栈列表 */
.tech-item {
padding: 15px;
margin-bottom: 15px;
border: 2px solid var(--border-color);
background-color: rgba(var(--light-bg-rgb), 0.7);
display: flex;
align-items: center;
}
.tech-icon {
margin-right: 15px;
color: var(--secondary-color);
font-size: 20px;
width: 30px;
text-align: center;
}
.tech-name {
font-weight: bold;
margin-right: 10px;
}
.tech-desc {
font-size: 14px;
color: var(--accent-color);
}
/* 页脚 */
.footer {
border-top: 2px solid var(--border-color);
padding: 20px 0;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background-color: transparent;
position: relative;
z-index: 1;
}
.footer-text {
margin: 0 0 15px 0;
font-size: 14px;
opacity: 0.7;
}
.footer-links {
display: flex;
gap: 15px;
justify-content: center;
}
.footer-link {
color: var(--secondary-color);
text-decoration: none;
font-size: 14px;
transition: color 0.3s;
}
.footer-link:hover {
color: var(--primary-color);
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.button-group {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
text-align: center;
}
.features-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.card-header {
flex-direction: column;
gap: 15px;
}
.card-controls {
width: 100%;
}
.logo-frame {
width: 120px;
height: 120px;
}
.logo-image {
width: 100px;
height: 100px;
}
}
/* 针对移动设备的响应式优化 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 10px;
}
.card {
margin-bottom: 30px;
}
.card-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.card-controls {
width: 100%;
justify-content: center;
}
.button-group {
flex-wrap: wrap;
gap: 15px;
}
/* 预览区域优化 */
.preview-content {
max-width: 100%;
overflow-x: auto;
}
.code-block {
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
line-height: 1.4;
}
.block-header {
padding: 6px 10px;
}
/* 日志界面导航链接优化 */
.nav-links {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.nav-links .btn {
width: 100%;
text-align: center;
}
}
@media (max-width: 480px) {
/* 特性卡片优化 */
.features-grid {
grid-template-columns: 1fr;
gap: 20px;
}
/* 预览窗口优化 */
.preview-container {
flex-direction: column;
}
.preview-window {
margin-bottom: 20px;
width: 100%;
}
/* 技术栈列表小屏幕优化 */
.tech-item {
flex-wrap: wrap;
}
.tech-desc {
width: 100%;
padding-left: 40px; /* 图标宽度+右边距 */
margin-top: 5px;
}
/* 日志界面资源列表项优化 */
.asset-item {
flex-wrap: wrap;
padding: 15px 0;
}
.asset-name {
width: 100%;
word-break: break-all;
margin-bottom: 10px;
}
.asset-size {
order: 2;
margin-top: 10px;
}
.download-btn {
order: 3;
margin-left: 0;
margin-top: 10px;
width: 100%;
text-align: center;
padding: 8px;
}
/* 页脚链接优化 */
.footer {
flex-direction: column;
text-align: center;
}
.footer-links {
margin-top: 15px;
justify-content: center;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,256 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>voidraft - An elegant text snippet recording tool designed for developers.</title>
<meta name="description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, voidraft">
<meta name="author" content="voidraft Team">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
<!-- Internationalization / hreflang -->
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://landaiqing.github.io/voidraft/">
<meta property="og:title" content="voidraft - An elegant text snippet recording tool designed for developers">
<meta property="og:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<meta property="og:site_name" content="voidraft">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
<meta property="twitter:title" content="voidraft - An elegant text snippet recording tool designed for developers">
<meta property="twitter:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<link rel="stylesheet" href="./css/styles.css">
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "voidraft",
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
"url": "https://landaiqing.github.io/voidraft/",
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
"author": {
"@type": "Organization",
"name": "voidraft"
},
"operatingSystem": ["Windows", "macOS", "Linux"],
"applicationCategory": "DeveloperApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
"softwareVersion": "Latest",
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
"codeRepository": "https://github.com/landaiqing/voidraft"
}
</script>
</head>
<body class="theme-dark">
<div class="container">
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title">voidraft</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
</button>
<button id="lang-toggle" class="btn btn-secondary">
<i class="fas fa-language"></i> 中/EN
</button>
</div>
</div>
<div class="card-content">
<!-- Logo和介绍 -->
<div class="logo-container">
<div class="logo-frame">
<img src="img/logo.png" alt="voidraft Logo" class="logo-image">
</div>
<h2 class="logo-text" data-en="voidraft" data-zh="voidraft">voidraft</h2>
<p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p>
</div>
<div class="intro-box">
<p class="intro-text" data-en="Designed for developers to record, organize, and manage various text snippets anytime, anywhere." data-zh="专为开发者打造,随时随地记录、整理和管理各种文本片段。">Designed for developers to record, organize, and manage various text snippets anytime, anywhere.</p>
</div>
<div class="button-group">
<a href="https://github.com/landaiqing/voidraft/releases" class="btn" data-en="Download" data-zh="下载">
<i class="fas fa-download"></i> Download
</a>
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary" data-en="Source Code" data-zh="源代码">
<i class="fab fa-github"></i> Source Code
</a>
<a href="changelog.html" class="btn btn-secondary" data-en="Changelog" data-zh="更新日志">
<i class="fas fa-history"></i> Changelog
</a>
</div>
<!-- 特性部分 -->
<h2 data-en="Core Features" data-zh="核心特性">Core Features</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-code"></i>
</div>
<h3 class="feature-title" data-en="Developer-Friendly" data-zh="开发者友好">Developer-Friendly</h3>
<p class="feature-desc" data-en="Multi-language code blocks with syntax highlighting for 30+ programming languages" data-zh="多语言代码块支持为30+种编程语言提供语法高亮">Multi-language code blocks with syntax highlighting for 30+ programming languages</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-magic"></i>
</div>
<h3 class="feature-title" data-en="Code Formatting" data-zh="代码格式化">Code Formatting</h3>
<p class="feature-desc" data-en="Built-in Prettier support for one-click code beautification" data-zh="内置Prettier支持一键美化代码">Built-in Prettier support for one-click code beautification</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-palette"></i>
</div>
<h3 class="feature-title" data-en="Custom Themes" data-zh="自定义主题">Custom Themes</h3>
<p class="feature-desc" data-en="Dark/Light themes with full customization options" data-zh="深色/浅色主题,支持完全自定义">Dark/Light themes with full customization options</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-clone"></i>
</div>
<h3 class="feature-title" data-en="Multi-Window" data-zh="多窗口支持">Multi-Window</h3>
<p class="feature-desc" data-en="Edit multiple documents simultaneously" data-zh="同时编辑多个文档">Edit multiple documents simultaneously</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-layer-group"></i>
</div>
<h3 class="feature-title" data-en="Block Editing" data-zh="块状编辑">Block Editing</h3>
<p class="feature-desc" data-en="Split content into independent code blocks with different language settings" data-zh="将内容分割为独立的代码块,每个块可设置不同语言">Split content into independent code blocks with different language settings</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-puzzle-piece"></i>
</div>
<h3 class="feature-title" data-en="Extensions" data-zh="丰富扩展">Extensions</h3>
<p class="feature-desc" data-en="Rainbow brackets, VSCode-style search, color picker, translation tool, and more" data-zh="彩虹括号、VSCode风格搜索、颜色选择器、翻译工具等多种扩展">Rainbow brackets, VSCode-style search, color picker, translation tool, and more</p>
</div>
</div>
<!-- 预览部分 -->
<h2 data-en="Preview" data-zh="预览">Preview</h2>
<div class="preview-container">
<div class="preview-window">
<div class="preview-header">
<div class="preview-controls">
<span class="preview-btn"></span>
<span class="preview-btn"></span>
<span class="preview-btn"></span>
</div>
<div class="preview-title">voidraft</div>
</div>
<div class="preview-content">
<div class="code-block-wrapper">
<div class="block-header">
<div class="block-language">javascript</div>
</div>
<pre class="code-block">
<span class="keyword">function</span> <span class="function">createDocument</span>() {
<span class="keyword">const</span> <span class="variable">doc</span> = <span class="keyword">new</span> <span class="class">Document</span>();
<span class="variable">doc</span>.<span class="function">addCodeBlock</span>(<span class="string">'javascript'</span>, <span class="string">`
<span class="keyword">function</span> <span class="function">greeting</span>(<span class="parameter">name</span>) {
<span class="keyword">return</span> <span class="string">`Hello, </span>${<span class="parameter">name</span>}<span class="string">!`</span>;
}
<span class="built-in">console</span>.<span class="function">log</span>(<span class="function">greeting</span>(<span class="string">'World'</span>));
`</span>);
<span class="keyword">return</span> <span class="variable">doc</span>;
}</pre>
</div>
<div class="code-block-wrapper" style="margin-top: 10px;">
<div class="block-header">
<div class="block-language">text</div>
</div>
<pre class="code-block">
<span class="comment">// voidraft - An elegant text snippet recording tool</span>
<span class="comment">// Multi-language support | Code formatting | Custom themes</span>
<span class="comment">// A modern text editor designed for developers</span></pre>
</div>
</div>
</div>
<div class="preview-window">
<img src="img/screenshot-dark.png" alt="voidraft 界面预览" class="preview-image dark-theme-img">
<img src="img/screenshot-light.png" alt="voidraft 界面预览" class="preview-image light-theme-img" style="display: none;">
</div>
</div>
<!-- 技术栈部分 -->
<h2 data-en="Technical Stack" data-zh="技术栈">Technical Stack</h2>
<ul class="tech-list">
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-desktop"></i></div>
<span class="tech-name">Wails3</span>
<span class="tech-desc" data-en="Cross-platform desktop application framework" data-zh="跨平台桌面应用框架">Cross-platform desktop application framework</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-cogs"></i></div>
<span class="tech-name">Go 1.21+</span>
<span class="tech-desc" data-en="Fast and efficient backend language" data-zh="快速高效的后端语言">Fast and efficient backend language</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fab fa-vuejs"></i></div>
<span class="tech-name">Vue 3 + TypeScript</span>
<span class="tech-desc" data-en="Modern frontend framework" data-zh="现代化前端框架">Modern frontend framework</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-edit"></i></div>
<span class="tech-name">CodeMirror 6</span>
<span class="tech-desc" data-en="Modern code editor with extension support" data-zh="支持扩展的现代化代码编辑器">Modern code editor with extension support</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-database"></i></div>
<span class="tech-name">SQLite</span>
<span class="tech-desc" data-en="Lightweight database for document storage" data-zh="轻量级文档存储数据库">Lightweight database for document storage</span>
</li>
</ul>
</div>
<!-- 页脚 -->
<footer class="footer">
<p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2025 voidraft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
</div>
</footer>
</div>
</div>
<script src="js/script.js"></script>
</body>
</html>

View File

@@ -1,705 +0,0 @@
/**
* voidraft - Changelog Script
* 从GitHub API获取发布信息支持Gitea备用源
*/
/**
* 仓库配置类
*/
class RepositoryConfig {
constructor() {
this.repos = {
github: {
owner: 'landaiqing',
name: 'voidraft',
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
},
gitea: {
owner: 'landaiqing',
name: 'voidraft',
domain: 'git.landaiqing.cn',
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
}
};
}
/**
* 获取仓库配置
* @param {string} source - 'github' 或 'gitea'
*/
getRepo(source) {
return this.repos[source];
}
/**
* 获取所有仓库配置
*/
getAllRepos() {
return this.repos;
}
}
/**
* 国际化消息管理类
*/
class I18nMessages {
constructor() {
this.messages = {
loading: {
en: 'Loading releases...',
zh: '正在加载版本信息...'
},
noReleases: {
en: 'No release information found',
zh: '没有找到版本发布信息'
},
fetchError: {
en: 'Failed to load release information. Please try again later.',
zh: '无法获取版本信息,请稍后再试'
},
githubApiError: {
en: 'GitHub API returned an error status: ',
zh: 'GitHub API返回错误状态: '
},
giteaApiError: {
en: 'Gitea API returned an error status: ',
zh: 'Gitea API返回错误状态: '
},
dataSource: {
en: 'Data source: ',
zh: '数据来源: '
},
downloads: {
en: 'Downloads',
zh: '下载资源'
},
download: {
en: 'Download',
zh: '下载'
},
preRelease: {
en: 'Pre-release',
zh: '预发布'
}
};
}
/**
* 获取消息
* @param {string} key - 消息键
* @param {string} lang - 语言代码
*/
getMessage(key, lang = 'en') {
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
}
/**
* 获取当前语言
*/
getCurrentLang() {
return window.currentLang || 'en';
}
}
/**
* API客户端类
*/
class APIClient {
constructor(repositoryConfig, i18nMessages) {
this.repositoryConfig = repositoryConfig;
this.i18nMessages = i18nMessages;
}
/**
* 从指定源获取发布信息
* @param {string} source - 'github' 或 'gitea'
*/
async fetchReleases(source) {
const repo = this.repositoryConfig.getRepo(source);
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
const options = {
headers: { 'Accept': 'application/json' }
};
if (source === 'github') {
return this.fetchFromGitHub(repo, options, errorMessageKey);
} else {
return this.fetchFromGitea(repo, options, errorMessageKey);
}
}
/**
* 从GitHub获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitHub(repo, options, errorMessageKey) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
options.signal = controller.signal;
options.headers['Accept'] = 'application/vnd.github.v3+json';
try {
const response = await fetch(repo.apiUrl, options);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* 从Gitea获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitea(repo, options, errorMessageKey) {
const response = await fetch(repo.apiUrl, options);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
}
}
/**
* UI管理类
*/
class UIManager {
constructor(i18nMessages) {
this.i18nMessages = i18nMessages;
this.elements = {
loading: document.getElementById('loading'),
changelog: document.getElementById('changelog'),
error: document.getElementById('error-message')
};
}
/**
* 显示加载状态
*/
showLoading() {
this.elements.loading.style.display = 'block';
this.elements.error.style.display = 'none';
this.elements.changelog.innerHTML = '';
}
/**
* 隐藏加载状态
*/
hideLoading() {
this.elements.loading.style.display = 'none';
}
/**
* 显示错误消息
* @param {string} message - 错误消息
*/
showError(message) {
const errorMessageElement = this.elements.error.querySelector('p');
if (errorMessageElement) {
errorMessageElement.textContent = message;
} else {
this.elements.error.textContent = message;
}
this.elements.error.style.display = 'block';
this.hideLoading();
}
/**
* 显示发布信息
* @param {Array} releases - 发布信息数组
* @param {string} source - 数据源
*/
displayReleases(releases, source) {
this.hideLoading();
// 清除现有内容
this.elements.changelog.innerHTML = '';
// 创建数据源元素
const sourceElement = this.createSourceElement(source);
this.elements.changelog.appendChild(sourceElement);
// 创建发布信息元素
releases.forEach(release => {
const releaseElement = this.createReleaseElement(release, source);
this.elements.changelog.appendChild(releaseElement);
});
this.elements.changelog.style.display = 'block';
}
/**
* 创建数据源元素
* @param {string} source - 数据源
*/
createSourceElement(source) {
const sourceElement = document.createElement('div');
sourceElement.className = 'data-source';
// 创建带有国际化支持的源标签
const sourceLabel = document.createElement('span');
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
// 创建链接
const sourceLink = document.createElement('a');
const repositoryConfig = new RepositoryConfig();
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
sourceLink.target = '_blank';
// 组装元素
sourceElement.appendChild(sourceLabel);
sourceElement.appendChild(sourceLink);
return sourceElement;
}
/**
* 创建发布信息元素
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
createReleaseElement(release, source) {
const releaseElement = document.createElement('div');
releaseElement.className = 'release';
// 格式化发布日期
const releaseDate = new Date(release.published_at || release.created_at);
const formattedDate = DateFormatter.formatDate(releaseDate);
// 创建头部
const headerElement = this.createReleaseHeader(release, formattedDate);
releaseElement.appendChild(headerElement);
// 添加发布说明
if (release.body) {
const descriptionElement = document.createElement('div');
descriptionElement.className = 'release-description markdown-content';
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
releaseElement.appendChild(descriptionElement);
}
// 添加下载资源
const assets = AssetManager.getAssetsFromRelease(release, source);
if (assets && assets.length > 0) {
const assetsElement = this.createAssetsElement(assets);
releaseElement.appendChild(assetsElement);
}
return releaseElement;
}
/**
* 创建发布信息头部
*/
createReleaseHeader(release, formattedDate) {
const headerElement = document.createElement('div');
headerElement.className = 'release-header';
// 版本元素
const versionElement = document.createElement('div');
versionElement.className = 'release-version';
// 版本文本
const versionText = document.createElement('span');
versionText.textContent = release.name || release.tag_name;
versionElement.appendChild(versionText);
// 预发布标记
if (release.prerelease) {
const preReleaseTag = document.createElement('span');
preReleaseTag.className = 'release-badge pre-release';
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
versionElement.appendChild(preReleaseTag);
}
// 日期元素
const dateElement = document.createElement('div');
dateElement.className = 'release-date';
dateElement.textContent = formattedDate;
headerElement.appendChild(versionElement);
headerElement.appendChild(dateElement);
return headerElement;
}
/**
* 创建资源文件元素
* @param {Array} assets - 资源文件数组
*/
createAssetsElement(assets) {
const assetsElement = document.createElement('div');
assetsElement.className = 'release-assets';
// 资源标题
const assetsTitle = document.createElement('div');
assetsTitle.className = 'release-assets-title';
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
// 资源列表
const assetList = document.createElement('ul');
assetList.className = 'asset-list';
// 添加每个资源
assets.forEach(asset => {
const assetItem = this.createAssetItem(asset);
assetList.appendChild(assetItem);
});
assetsElement.appendChild(assetsTitle);
assetsElement.appendChild(assetList);
return assetsElement;
}
/**
* 创建资源文件项
* @param {Object} asset - 资源文件对象
*/
createAssetItem(asset) {
const assetItem = document.createElement('li');
assetItem.className = 'asset-item';
// 文件图标
const iconElement = document.createElement('i');
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
// 文件名
const nameElement = document.createElement('span');
nameElement.className = 'asset-name';
nameElement.textContent = asset.name;
// 文件大小
const sizeElement = document.createElement('span');
sizeElement.className = 'asset-size';
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
// 下载链接
const downloadLink = document.createElement('a');
downloadLink.className = 'download-btn';
downloadLink.href = asset.browser_download_url;
downloadLink.target = '_blank';
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
// 组装资源项
assetItem.appendChild(iconElement);
assetItem.appendChild(nameElement);
assetItem.appendChild(sizeElement);
assetItem.appendChild(downloadLink);
return assetItem;
}
}
/**
* 资源管理器类
*/
class AssetManager {
/**
* 从发布信息中获取资源文件
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
static getAssetsFromRelease(release, source) {
let assets = [];
if (source === 'github') {
assets = release.assets || [];
} else { // Gitea
assets = release.assets || [];
// 检查Gitea特定的资源结构
if (!assets.length && release.attachments) {
assets = release.attachments.map(attachment => ({
name: attachment.name,
size: attachment.size,
browser_download_url: attachment.browser_download_url
}));
}
}
return assets;
}
}
/**
* 文件图标助手类
*/
class FileIconHelper {
/**
* 根据文件扩展名获取图标
* @param {string} filename - 文件名
*/
static getFileIcon(filename) {
const extension = filename.split('.').pop().toLowerCase();
const iconMap = {
'exe': 'download',
'msi': 'download',
'dmg': 'download',
'pkg': 'download',
'deb': 'download',
'rpm': 'download',
'tar': 'file-archive',
'gz': 'file-archive',
'zip': 'file-archive',
'7z': 'file-archive',
'rar': 'file-archive',
'pdf': 'file-pdf',
'txt': 'file-alt',
'md': 'file-alt',
'json': 'file-code',
'xml': 'file-code',
'yml': 'file-code',
'yaml': 'file-code'
};
return iconMap[extension] || 'file';
}
}
/**
* 文件大小格式化器类
*/
class FileSizeFormatter {
/**
* 格式化文件大小
* @param {number} bytes - 字节数
*/
static formatFileSize(bytes) {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
}
/**
* 日期格式化器类
*/
class DateFormatter {
/**
* 格式化日期
* @param {Date} date - 日期对象
*/
static formatDate(date) {
const options = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
const lang = window.currentLang || 'en';
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
return date.toLocaleDateString(locale, options);
}
}
/**
* Markdown解析器类
*/
class MarkdownParser {
/**
* 简单的Markdown解析
* @param {string} markdown - Markdown文本
*/
static parseMarkdown(markdown) {
if (!markdown) return '';
// 预处理:保留原始换行符,用特殊标记替换
const preservedLineBreaks = '___LINE_BREAK___';
markdown = markdown.replace(/\n/g, preservedLineBreaks);
// 引用块 - > text
markdown = markdown.replace(/&gt;\s*(.*?)(?=&gt;|$)/g, '<blockquote>$1</blockquote>');
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
// 链接 - [text](url)
markdown = markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 标题 - # Heading
markdown = markdown.replace(/^### (.*?)(?=___LINE_BREAK___|$)/gm, '<h3>$1</h3>');
markdown = markdown.replace(/^## (.*?)(?=___LINE_BREAK___|$)/gm, '<h2>$1</h2>');
markdown = markdown.replace(/^# (.*?)(?=___LINE_BREAK___|$)/gm, '<h1>$1</h1>');
// 粗体 - **text**
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 斜体 - *text*
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
// 代码块 - ```code```
markdown = markdown.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// 行内代码 - `code`
markdown = markdown.replace(/`([^`]+)`/g, '<code>$1</code>');
// 处理列表项
// 先将每个列表项转换为HTML
markdown = markdown.replace(/- (.*?)(?=___LINE_BREAK___- |___LINE_BREAK___$|$)/g, '<li>$1</li>');
markdown = markdown.replace(/\* (.*?)(?=___LINE_BREAK___\* |___LINE_BREAK___$|$)/g, '<li>$1</li>');
markdown = markdown.replace(/\d+\. (.*?)(?=___LINE_BREAK___\d+\. |___LINE_BREAK___$|$)/g, '<li>$1</li>');
// 然后将连续的列表项包装在ul或ol中
const listItemRegex = /<li>.*?<\/li>/g;
const listItems = markdown.match(listItemRegex) || [];
if (listItems.length > 0) {
// 将连续的列表项组合在一起
let lastIndex = 0;
let result = '';
let inList = false;
listItems.forEach(item => {
const itemIndex = markdown.indexOf(item, lastIndex);
// 添加列表项之前的内容
if (itemIndex > lastIndex) {
result += markdown.substring(lastIndex, itemIndex);
}
// 如果不在列表中,开始一个新列表
if (!inList) {
result += '<ul>';
inList = true;
}
// 添加列表项
result += item;
// 更新lastIndex
lastIndex = itemIndex + item.length;
// 检查下一个内容是否是列表项
const nextItemIndex = markdown.indexOf('<li>', lastIndex);
if (nextItemIndex === -1 || nextItemIndex > lastIndex + 20) { // 如果下一个列表项不紧邻
result += '</ul>';
inList = false;
}
});
// 添加剩余内容
if (lastIndex < markdown.length) {
result += markdown.substring(lastIndex);
}
markdown = result;
}
// 处理水平分隔线
markdown = markdown.replace(/---/g, '<hr>');
// 恢复换行符
markdown = markdown.replace(/___LINE_BREAK___/g, '<br>');
// 处理段落
markdown = markdown.replace(/<br><br>/g, '</p><p>');
// 包装在段落标签中
if (!markdown.startsWith('<p>')) {
markdown = `<p>${markdown}</p>`;
}
return markdown;
}
}
/**
* 更新日志主应用类
*/
class ChangelogApp {
constructor() {
this.repositoryConfig = new RepositoryConfig();
this.i18nMessages = new I18nMessages();
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
this.uiManager = new UIManager(this.i18nMessages);
this.init();
}
/**
* 初始化应用
*/
init() {
this.uiManager.showLoading();
// 首先尝试GitHub API
this.apiClient.fetchReleases('github')
.then(releases => {
this.uiManager.displayReleases(releases, 'github');
})
.catch(() => {
// GitHub失败时尝试Gitea
return this.apiClient.fetchReleases('gitea')
.then(releases => {
this.uiManager.displayReleases(releases, 'gitea');
});
})
.catch(error => {
console.error('获取发布信息失败:', error);
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
});
// 监听语言变化事件
document.addEventListener('languageChanged', () => this.updateUI());
}
/**
* 更新UI元素当语言变化时
*/
updateUI() {
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
const currentLang = this.i18nMessages.getCurrentLang();
elementsToUpdate.forEach(element => {
const text = element.getAttribute(`data-${currentLang}`);
if (text) {
element.textContent = text;
}
});
}
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
new ChangelogApp();
});

View File

@@ -1,443 +0,0 @@
/**
* voidraft - Website Script
*/
/**
* 主题管理类
*/
class ThemeManager {
constructor() {
this.themeToggle = document.getElementById('theme-toggle');
this.currentTheme = this.getInitialTheme();
this.init();
}
/**
* 获取初始主题
*/
getInitialTheme() {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
}
/**
* 初始化主题管理器
*/
init() {
if (!this.themeToggle) return;
this.setTheme(this.currentTheme);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
}
/**
* 切换主题
*/
toggleTheme() {
document.body.classList.add('theme-transition');
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
this.saveTheme(newTheme);
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
}
/**
* 设置主题
* @param {string} theme - 'dark' 或 'light'
*/
setTheme(theme) {
this.currentTheme = theme;
const isDark = theme === 'dark';
document.body.classList.toggle('theme-dark', isDark);
document.body.classList.toggle('theme-light', !isDark);
this.updateToggleIcon(isDark);
}
/**
* 更新切换按钮图标
* @param {boolean} isDark - 是否为暗色主题
*/
updateToggleIcon(isDark) {
if (this.themeToggle) {
const icon = this.themeToggle.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
}
}
/**
* 保存主题到本地存储
* @param {string} theme - 主题名称
*/
saveTheme(theme) {
localStorage.setItem('theme', theme);
}
}
/**
* 语言管理类
*/
class LanguageManager {
constructor() {
this.langToggle = document.getElementById('lang-toggle');
this.currentLang = this.getInitialLanguage();
this.init();
}
/**
* 获取初始语言
*/
getInitialLanguage() {
const urlParams = new URLSearchParams(window.location.search);
const urlLang = urlParams.get('lang');
const savedLang = localStorage.getItem('lang');
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
return urlLang || savedLang || browserLang;
}
/**
* 初始化语言管理器
*/
init() {
if (!this.langToggle) return;
window.currentLang = this.currentLang;
this.setLanguage(this.currentLang);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.langToggle.addEventListener('click', () => {
this.toggleLanguage();
});
}
/**
* 切换语言
*/
toggleLanguage() {
document.body.classList.add('lang-transition');
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
this.setLanguage(newLang);
this.saveLanguage(newLang);
this.updateURL(newLang);
this.notifyLanguageChange(newLang);
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
}
/**
* 设置页面语言
* @param {string} lang - 'zh' 或 'en'
*/
setLanguage(lang) {
this.currentLang = lang;
window.currentLang = lang;
this.updatePageElements(lang);
this.updateHTMLLang(lang);
this.updateToggleButton(lang);
}
/**
* 更新页面元素文本
* @param {string} lang - 语言代码
*/
updatePageElements(lang) {
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
el.textContent = el.getAttribute(`data-${lang}`);
});
}
/**
* 更新HTML语言属性
* @param {string} lang - 语言代码
*/
updateHTMLLang(lang) {
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
}
/**
* 更新切换按钮文本
* @param {string} lang - 语言代码
*/
updateToggleButton(lang) {
if (this.langToggle) {
const text = lang === 'zh' ? 'EN/中' : '中/EN';
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
}
}
/**
* 保存语言到本地存储
* @param {string} lang - 语言代码
*/
saveLanguage(lang) {
localStorage.setItem('lang', lang);
}
/**
* 更新URL参数
* @param {string} lang - 语言代码
*/
updateURL(lang) {
const newUrl = new URL(window.location);
if (lang === 'zh') {
newUrl.searchParams.set('lang', 'zh');
} else {
newUrl.searchParams.delete('lang');
}
window.history.replaceState({}, '', newUrl);
}
/**
* 通知语言变更
* @param {string} lang - 语言代码
*/
notifyLanguageChange(lang) {
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
}
/**
* 获取当前语言
*/
getCurrentLanguage() {
return this.currentLang;
}
}
/**
* SEO管理类
*/
class SEOManager {
constructor(languageManager) {
this.languageManager = languageManager;
this.metaTexts = {
en: {
description: 'voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
title: 'voidraft - An elegant text snippet recording tool designed for developers.',
ogTitle: 'voidraft - An elegant text snippet recording tool designed for developers'
},
zh: {
description: 'voidraft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'voidraft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'voidraft - 专为开发者打造的优雅文本片段记录工具'
}
};
this.init();
}
/**
* 初始化SEO管理器
*/
init() {
this.bindEvents();
this.updateMetaTags(this.languageManager.getCurrentLanguage());
}
/**
* 绑定事件
*/
bindEvents() {
window.addEventListener('languageChanged', (event) => {
this.updateMetaTags(event.detail.lang);
});
}
/**
* 更新SEO元标签
* @param {string} lang - 当前语言
*/
updateMetaTags(lang) {
const texts = this.metaTexts[lang];
this.updateMetaDescription(texts.description);
this.updateOpenGraphTags(texts.ogTitle, texts.description);
this.updateTwitterCardTags(texts.ogTitle, texts.description);
this.updatePageTitle(texts.title);
}
/**
* 更新meta描述
* @param {string} description - 描述文本
*/
updateMetaDescription(description) {
const metaDesc = document.querySelector('meta[name="description"]');
if (metaDesc) {
metaDesc.content = description;
}
}
/**
* 更新Open Graph标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateOpenGraphTags(title, description) {
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDesc = document.querySelector('meta[property="og:description"]');
if (ogTitle) ogTitle.content = title;
if (ogDesc) ogDesc.content = description;
}
/**
* 更新Twitter Card标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateTwitterCardTags(title, description) {
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
if (twitterTitle) twitterTitle.content = title;
if (twitterDesc) twitterDesc.content = description;
}
/**
* 更新页面标题
* @param {string} title - 标题
*/
updatePageTitle(title) {
document.title = title;
}
}
/**
* UI效果管理类
*/
class UIEffects {
constructor() {
this.init();
}
/**
* 初始化UI效果
*/
init() {
this.initCardEffects();
}
/**
* 初始化卡片悬停效果
*/
initCardEffects() {
const cards = document.querySelectorAll('.feature-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
this.animateCardHover(card, true);
});
card.addEventListener('mouseleave', () => {
this.animateCardHover(card, false);
});
});
}
/**
* 卡片悬停动画
* @param {Element} card - 卡片元素
* @param {boolean} isHover - 是否悬停
*/
animateCardHover(card, isHover) {
if (isHover) {
card.style.transform = 'translateY(-8px)';
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
} else {
card.style.transform = 'translateY(0)';
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
}
}
}
/**
* voidraft主应用类
*/
class voidraftApp {
constructor() {
this.themeManager = null;
this.languageManager = null;
this.seoManager = null;
this.uiEffects = null;
this.init();
}
/**
* 初始化应用
*/
init() {
this.initializeManagers();
this.showConsoleBranding();
}
/**
* 初始化各个管理器
*/
initializeManagers() {
this.themeManager = new ThemeManager();
this.languageManager = new LanguageManager();
this.seoManager = new SEOManager(this.languageManager);
this.uiEffects = new UIEffects();
}
/**
* 显示控制台品牌信息
*/
showConsoleBranding() {
console.log('%c voidraft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
}
/**
* 获取主题管理器
*/
getThemeManager() {
return this.themeManager;
}
/**
* 获取语言管理器
*/
getLanguageManager() {
return this.languageManager;
}
/**
* 获取SEO管理器
*/
getSEOManager() {
return this.seoManager;
}
/**
* 获取UI效果管理器
*/
getUIEffects() {
return this.uiEffects;
}
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
window.voidRaftApp = new voidraftApp();
});

View File

@@ -62,19 +62,4 @@ export class ServiceOptions {
} }
} }
export class WebviewWindow { export type Window = any;
/** 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>);
}
}

View File

@@ -10,10 +10,17 @@
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; 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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; 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 获取配置项 * Get 获取配置项
*/ */
@@ -34,22 +41,6 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
return $typingPromise; 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 执行配置迁移 * MigrateConfig 执行配置迁移
*/ */
@@ -74,6 +65,14 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
return $resultPromise; 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 设置配置项 * 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 } { export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
let $resultPromise = $Call.ByID(3264871659, callback) as any; let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
return $resultPromise; return $resultPromise;
} }
/** /**
* SetDataPathChangeCallback 设置数据路径配置变更回调 * WatchWithContext 使用 Context 注册监听器
*/ */
export function SetDataPathChangeCallback(callback: any): Promise<void> & { cancel(): void } { export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(393017412, callback) as any; let $resultPromise = $Call.ByID(1454973098, path, 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;
return $resultPromise; return $resultPromise;
} }

View File

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

View File

@@ -5,9 +5,6 @@
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime"; 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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as http$0 from "../../../net/http/models.js"; 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 // @ts-ignore: Unused imports
import * as time$0 from "../../../time/models.js"; import * as time$0 from "../../../time/models.js";
/**
* CancelFunc 取消订阅函数
* 调用此函数可以取消对配置的监听
*/
export type CancelFunc = any;
/** /**
* HttpRequest HTTP请求结构 * HttpRequest HTTP请求结构
*/ */
@@ -24,7 +27,7 @@ export class HttpRequest {
"headers": { [_: string]: string }; "headers": { [_: string]: string };
/** /**
* json, formdata, urlencoded, text * json, formdata, urlencoded, text, params, xml, html, javascript, binary
*/ */
"bodyType"?: string; "bodyType"?: string;
"body"?: any; "body"?: any;
@@ -263,6 +266,11 @@ export class OSInfo {
} }
} }
/**
* ObserverCallback 观察者回调函数
*/
export type ObserverCallback = any;
/** /**
* SelfUpdateResult 自我更新结果 * 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 // Private type creation functions
const $$createType0 = $Create.Map($Create.Any, $Create.Any); const $$createType0 = $Create.Map($Create.Any, $Create.Any);
var $$createType1 = (function $$initCreateType1(...args): any { var $$createType1 = (function $$initCreateType1(...args): any {
@@ -463,5 +415,3 @@ const $$createType3 = $Create.Map($Create.Any, $$createType2);
const $$createType4 = OSInfo.createFrom; const $$createType4 = OSInfo.createFrom;
const $$createType5 = $Create.Nullable($$createType4); const $$createType5 = $Create.Nullable($$createType4);
const $$createType6 = $Create.Map($Create.Any, $Create.Any); 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 // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; 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 处理窗口关闭事件 * HandleWindowClose 处理窗口关闭事件
*/ */

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* WindowService 窗口管理服务(专注于窗口生命周期管理) * WindowService 窗口管理服务
* @module * @module
*/ */
@@ -12,15 +12,15 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @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 $resultPromise = $Call.ByID(1464997251) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType0($result);
}) as any; }) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise; 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 } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1105193745, snapService) as any; let $resultPromise = $Call.ByID(2432987694, options) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions // Private type creation functions
const $$createType0 = $models.WindowInfo.createFrom; const $$createType0 = $Create.Array($Create.Any);
const $$createType1 = $Create.Array($$createType0);

View File

@@ -0,0 +1,130 @@
import {defineConfig} from 'vitepress'
const base = '/'
// https://vitepress.dev/reference/site-config
export default defineConfig({
base: base,
title: "voidraft",
description: "An elegant text snippet recording tool designed for developers.",
srcDir: 'src',
assetsDir: 'assets',
cacheDir: './.vitepress/cache',
outDir: './.vitepress/dist',
srcExclude: [],
ignoreDeadLinks: false,
head: [
["link", {rel: "icon", type: "image/png", href: "/icon/favicon-96x96.png", sizes: "96x96"}],
["link", {rel: "icon", type: "image/svg+xml", href: "/icon/favicon.svg"}],
["link", {rel: "shortcut icon", href: "/icon/favicon.ico"}],
["link", {rel: "apple-touch-icon", sizes: "180x180", href: "/icon/apple-touch-icon.png"}],
["meta", {name: "apple-mobile-web-app-title", content: "voidraft"}],
["link", {rel: "manifest", href: "/icon/site.webmanifest"}],
['meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}]
],
// 国际化配置
locales: {
root: {
label: 'English',
lang: 'en-US',
description: 'An elegant text snippet recording tool designed for developers.',
themeConfig: {
logo: '/icon/logo.png',
siteTitle: 'voidraft',
nav: [
{text: 'Home', link: '/'},
{text: 'Guide', link: '/guide/introduction'}
],
sidebar: {
'/guide/': [
{
text: 'Getting Started',
items: [
{text: 'Introduction', link: '/guide/introduction'},
{text: 'Installation', link: '/guide/installation'},
{text: 'Quick Start', link: '/guide/getting-started'}
]
},
{
text: 'Features',
items: [
{text: 'Overview', link: '/guide/features'}
]
}
]
},
socialLinks: [
{icon: 'github', link: 'https://github.com/landaiqing/voidraft'}
],
outline: {
label: 'On this page'
},
lastUpdated: {
text: 'Last updated'
},
docFooter: {
prev: 'Previous',
next: 'Next'
},
darkModeSwitchLabel: 'Appearance',
sidebarMenuLabel: 'Menu',
returnToTopLabel: 'Return to top',
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2025-present landaiqing'
}
}
},
zh: {
label: '简体中文',
lang: 'zh-CN',
link: '/zh/',
description: '一个为开发者设计的优雅文本片段记录工具',
themeConfig: {
logo: '/icon/logo.png',
siteTitle: 'voidraft',
nav: [
{text: '首页', link: '/zh/'},
{text: '指南', link: '/zh/guide/introduction'}
],
sidebar: {
'/zh/guide/': [
{
text: '开始使用',
items: [
{text: '简介', link: '/zh/guide/introduction'},
{text: '安装', link: '/zh/guide/installation'},
{text: '快速开始', link: '/zh/guide/getting-started'}
]
},
{
text: '功能特性',
items: [
{text: '功能概览', link: '/zh/guide/features'}
]
}
]
},
socialLinks: [
{icon: 'github', link: 'https://github.com/landaiqing/voidraft'}
],
outline: {
label: '本页目录'
},
lastUpdated: {
text: '最后更新'
},
docFooter: {
prev: '上一页',
next: '下一页'
},
darkModeSwitchLabel: '外观',
sidebarMenuLabel: '菜单',
returnToTopLabel: '返回顶部',
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2025-present landaiqing'
}
}
}
}
})

View File

@@ -0,0 +1,6 @@
@import "style/var.css";
@import "style/blur.css";
@import "style/badge.css";
@import "style/grid.css";

View File

@@ -0,0 +1,17 @@
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './index.css'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({ app, router, siteData }) {
// ...
}
} satisfies Theme

View File

@@ -0,0 +1,21 @@
/* 提示框背景颜色 */
:root {
--vp-custom-block-tip-bg: var(--vp-c-green-soft);
}
/* 提示框 */
.custom-block.tip {
border-color: var(--vp-c-green-2);
}
/* 警告框 */
.custom-block.warning {
/* border-color: #d97706; */
border-color: var(--vp-c-yellow-2);
}
/* 危险框 */
.custom-block.danger {
/* border-color: #f43f5e; */
border-color: var(--vp-c-red-2);
}

View File

@@ -0,0 +1,73 @@
/* .vitepress/theme/style/blur.css */
:root {
/* 首页导航 */
.VPNavBar {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
/* 文档页导航两侧 */
.VPNavBar:not(.home) {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
@media (min-width: 960px) {
/* 文档页导航两侧 */
.VPNavBar:not(.home) {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
/* 首页下滑后导航两侧 */
.VPNavBar:not(.has-sidebar):not(.home.top) {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
}
@media (min-width: 960px) {
/* 文档页导航中间 */
.VPNavBar:not(.home.top) .content-body {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
/* 首页下滑后导航中间 */
.VPNavBar:not(.has-sidebar):not(.home.top) .content-body {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
}
/* 分割线 */
@media (min-width: 960px) {
/* 文档页分割线 */
.VPNavBar:not(.home.top) .divider-line {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
/* 首页分割线 */
.VPNavBar:not(.has-sidebar):not(.home.top) .divider {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
}
/* 搜索框 VPNavBarSearchButton.vue */
.DocSearch-Button {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
}
/* 移动端大纲栏 */
.VPLocalNav {
background-color: rgba(255, 255, 255, 0);
backdrop-filter: blur(10px);
/* 隐藏分割线 */
/* border-bottom: 5px solid var(--vp-c-gutter); */
border-bottom: 0px;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Grid Background
* 网格背景样式 - 为文档页面添加简约的网格背景
* -------------------------------------------------------------------------- */
.VPDoc,
.VPHome {
position: relative;
}
.VPDoc::before,
.VPHome::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
}
/* 亮色模式网格 */
:root:not(.dark) .VPDoc::before,
:root:not(.dark) .VPHome::before {
background-image:
linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
background-size: 60px 60px;
}
/* 暗色模式网格 */
.dark .VPDoc::before,
.dark .VPHome::before {
background-image:
linear-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
background-size: 60px 60px;
}

View File

@@ -0,0 +1,137 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create an accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attached to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#bd34fe 30%,
#41d1ff
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#bd34fe 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}

View File

@@ -0,0 +1,163 @@
# Features
Explore the powerful features that make voidraft a great tool for developers.
## Block-Based Editing
voidraft's core feature is its block-based editing system:
- Each block can have a different programming language
- Blocks are separated by delimiters (`∞∞∞language`)
- Navigate quickly between blocks
- Format each block independently
## Syntax Highlighting
Professional syntax highlighting for 30+ languages:
- Automatic language detection
- Customizable color schemes
- Support for nested languages
- Code folding support
## HTTP Client
Built-in HTTP client for API testing:
### Request Types
- GET, POST, PUT, DELETE, PATCH
- Custom headers
- Multiple body formats: JSON, FormData, URL-encoded, XML, Text
### Request Variables
Define and reuse variables:
```http
@var {
baseUrl: "https://api.example.com",
token: "your-api-token"
}
GET "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
}
```
### Response Handling
- View formatted JSON responses
- See response time and size
- Inspect headers
- Save responses for later
## Code Formatting
Integrated Prettier support:
- Format on save (optional)
- Format selection or entire block
- Supports JavaScript, TypeScript, CSS, HTML, JSON, and more
- Customizable formatting rules
## Editor Extensions
### VSCode-Style Search
- Find and replace with regex support
- Case-sensitive and whole word options
- Search across all blocks
### Minimap
- Bird's-eye view of your document
- Quick navigation
- Customizable size and position
### Rainbow Brackets
- Color-coded bracket pairs
- Easier to match brackets
- Customizable colors
### Color Picker
- Visual color selection
- Supports hex, RGB, HSL
- Live preview
### Translation Tool
- Translate selected text
- Multiple language support
- Quick keyboard access
### Text Highlighting
- Highlight important text
- Multiple highlight colors
- Persistent highlights
## Multi-Window Support
Work efficiently with multiple windows:
- Each window is independent
- Separate documents
- Synchronized settings
- Window state persistence
## Theme Customization
Full control over editor appearance:
### Built-in Themes
- Dark mode
- Light mode
- Auto-switch based on system
### Custom Themes
- Create your own themes
- Customize every color
- Save and share themes
- Import community themes
## Auto-Update System
Stay current with automatic updates:
- Background update checks
- Notification of new versions
- One-click update
- Update history
- Support for multiple update sources (GitHub, Gitea)
## Data Backup
Secure your data with Git-based backup:
- Automatic backups
- Manual backup triggers
- Support for GitHub and Gitea
- Multiple authentication methods (SSH, Token, Password)
- Configurable backup intervals
## Keyboard Shortcuts
Extensive keyboard support:
- Customizable shortcuts
- Vim/Emacs keybindings (planned)
- Quick command palette
- Context-aware shortcuts
## Performance
Built for speed:
- Fast startup time
- Smooth scrolling
- Efficient memory usage
- Large file support
## Privacy & Security
Your data is safe:
- Local-first storage
- Optional cloud backup
- No telemetry or tracking
- Open source codebase

View File

@@ -0,0 +1,107 @@
# Getting Started
Learn the basics of using voidraft and create your first document.
## The Editor Interface
When you open voidraft, you'll see:
- **Main Editor**: The central area where you write and edit
- **Toolbar**: Quick access to common actions
- **Status Bar**: Shows current block language and other info
## Creating Code Blocks
voidraft uses a block-based editing system. Each block can have a different language:
1. Press `Ctrl+Enter` to create a new block
2. Type `∞∞∞` followed by a language name (e.g., `∞∞∞javascript`)
3. Start coding in that block
### Supported Languages
voidraft supports 30+ programming languages including:
- JavaScript, TypeScript
- Python, Go, Rust
- HTML, CSS, Sass
- SQL, YAML, JSON
- And many more...
## Basic Operations
### Navigation
- `Ctrl+Up/Down`: Move between blocks
- `Ctrl+Home/End`: Jump to first/last block
- `Ctrl+F`: Search within document
### Editing
- `Ctrl+D`: Duplicate current line
- `Ctrl+/`: Toggle comment
- `Alt+Up/Down`: Move line up/down
- `Ctrl+Shift+F`: Format code (if language supports Prettier)
### Block Management
- `Ctrl+Enter`: Create new block
- `Ctrl+Shift+Enter`: Create block above
- `Alt+Delete`: Delete current block
## Using the HTTP Client
voidraft includes a built-in HTTP client for testing APIs:
1. Create a block with HTTP language
2. Write your HTTP request:
```http
POST "https://api.example.com/users" {
content-type: "application/json"
@json {
name: "John Doe",
email: "john@example.com"
}
}
```
3. Click the run button to execute the request
4. View the response inline
## Multi-Window Support
Work on multiple documents simultaneously:
1. Go to `File > New Window` (or `Ctrl+Shift+N`)
2. Each window is independent
3. Changes are saved automatically
## Customizing Themes
Personalize your editor:
1. Open Settings (`Ctrl+,`)
2. Go to Appearance
3. Choose a theme or create your own
4. Customize colors to your preference
## Keyboard Shortcuts
Learn essential shortcuts:
| Action | Shortcut |
|--------|----------|
| New Window | `Ctrl+Shift+N` |
| Search | `Ctrl+F` |
| Replace | `Ctrl+H` |
| Format Code | `Ctrl+Shift+F` |
| Toggle Theme | `Ctrl+Shift+T` |
| Command Palette | `Ctrl+Shift+P` |
## Next Steps
Now that you know the basics:
- Explore [Features](/guide/features) in detail

View File

@@ -0,0 +1,63 @@
# Installation
This guide will help you install voidraft on your system.
## System Requirements
- **Operating System**: Windows 10 or later (macOS and Linux support planned)
- **RAM**: 4GB minimum, 8GB recommended
- **Disk Space**: 200MB free space
## Download
Visit the [releases page](https://github.com/landaiqing/voidraft/releases) and download the latest version for your platform:
- **Windows**: `voidraft-windows-amd64-installer.exe`
## Installation Steps
### Windows
1. Download the installer from the releases page
2. Run the `voidraft-windows-amd64-installer.exe` file
3. Follow the installation wizard
4. Launch voidraft from the Start menu or desktop shortcut
## First Launch
When you first launch voidraft:
1. The application will create a data directory to store your documents
2. You'll see the main editor interface with a welcome block
3. Start typing or create your first code block!
## Configuration
voidraft stores its configuration and data in:
- **Windows**: `%APPDATA%/voidraft/`
You can customize various settings including:
- Editor theme (dark/light mode)
- Code formatting preferences
- Backup settings
- Keyboard shortcuts
## Updating
voidraft includes an auto-update feature that will notify you when new versions are available. You can:
- Check for updates manually from the settings
- Enable automatic updates
- Choose your preferred update source
## Troubleshooting
If you encounter any issues during installation:
1. Make sure you have administrator privileges
2. Check that your antivirus isn't blocking the installation
3. Visit our [GitHub issues](https://github.com/landaiqing/voidraft/issues) page for help
Next: [Getting Started →](/guide/getting-started)

View File

@@ -0,0 +1,50 @@
# Introduction
Welcome to voidraft - an elegant text snippet recording tool designed specifically for developers.
## What is voidraft?
voidraft is a modern desktop application that helps developers manage text snippets, code blocks, API responses, meeting notes, and daily to-do lists. It provides a smooth and elegant editing experience with powerful features tailored for development workflows.
## Key Features
### Block-Based Editing
voidraft uses a unique block-based editing system inspired by Heynote. You can split your content into independent code blocks, each with:
- Different programming language settings
- Syntax highlighting
- Independent formatting
- Easy navigation between blocks
### Developer Tools
- **HTTP Client**: Test APIs directly within the editor
- **Code Formatting**: Built-in Prettier support for multiple languages
- **Syntax Highlighting**: Support for 30+ programming languages
- **Auto Language Detection**: Automatically recognizes code block language types
### Customization
- **Custom Themes**: Create and save your own editor themes
- **Extensions**: Rich set of editor extensions including minimap, rainbow brackets, color picker, and more
- **Multi-Window**: Work on multiple documents simultaneously
### Data Management
- **Git-Based Backup**: Automatic backup using Git repositories
- **Cloud Sync**: Sync your data across devices
- **Auto-Update**: Stay up-to-date with the latest features
## Why voidraft?
- **Developer-Focused**: Built with developers' needs in mind
- **Modern Stack**: Uses cutting-edge technologies (Wails3, Vue 3, CodeMirror 6)
- **Cross-Platform**: Works on Windows (macOS and Linux support planned)
- **Open Source**: MIT licensed, community-driven development
## Getting Started
Ready to start? Download the latest version from our [releases page](https://github.com/landaiqing/voidraft/releases) or continue reading the documentation to learn more.
Next: [Installation →](/guide/installation)

View File

@@ -0,0 +1,56 @@
---
layout: home
hero:
name: "voidraft"
text: "An elegant text snippet recording tool"
tagline: Designed for developers, built with modern technology
image:
src: /img/hero.png
alt: "voidraft"
actions:
- theme: brand
text: Get Started
link: https://github.com/landaiqing/voidraft/releases
- theme: alt
text: Documentation
link: /guide/introduction
features:
- icon: 📝
title: Block-Based Editing
details: Split your content into independent code blocks, each with different language settings. Inspired by Heynote's innovative design philosophy.
- icon: 🎨
title: Syntax Highlighting
details: Built-in support for 30+ programming languages with automatic language detection and Prettier integration for code formatting.
- icon: 🌐
title: HTTP Client
details: Integrated HTTP client with support for multiple request formats including JSON, FormData, XML, and more. Test APIs directly within the editor.
- icon: 🎯
title: Multi-Window Support
details: Work on multiple documents simultaneously with independent windows. Each window maintains its own state and configuration.
- icon: 🎭
title: Customizable Themes
details: Full theme customization support with dark/light modes. Create and save your own editor themes to match your preferences.
- icon: 🔧
title: Rich Extensions
details: VSCode-style search and replace, rainbow brackets, minimap, color picker, translation tool, text highlighting, and more.
- icon: 🔄
title: Auto-Update System
details: Built-in self-update mechanism with support for multiple update sources. Stay up-to-date with the latest features and improvements.
- icon: ☁️
title: Git-Based Backup
details: Automatic data backup using Git repositories. Supports GitHub, Gitea, with multiple authentication methods including SSH and tokens.
- icon: ⚡
title: Modern Architecture
details: Built with Wails3, Vue 3, and CodeMirror 6. Cross-platform desktop application with native performance and modern UI.
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,21 @@
{
"name": "voidraft",
"short_name": "voidraft",
"icons": [
{
"src": "/img/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/img/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,163 @@
# 功能特性
探索 voidraft 的强大功能,让它成为开发者的优秀工具。
## 块状编辑
voidraft 的核心功能是其块状编辑系统:
- 每个块可以有不同的编程语言
- 块之间由分隔符分隔(`∞∞∞语言`
- 快速在块之间导航
- 独立格式化每个块
## 语法高亮
支持 30+ 种语言的专业语法高亮:
- 自动语言检测
- 可自定义配色方案
- 支持嵌套语言
- 代码折叠支持
## HTTP 客户端
用于 API 测试的内置 HTTP 客户端:
### 请求类型
- GET、POST、PUT、DELETE、PATCH
- 自定义请求头
- 多种请求体格式JSON、FormData、URL 编码、XML、文本
### 请求变量
定义和重用变量:
```http
@var {
baseUrl: "https://api.example.com",
token: "your-api-token"
}
GET "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
}
```
### 响应处理
- 查看格式化的 JSON 响应
- 查看响应时间和大小
- 检查响应头
- 保存响应以供日后使用
## 代码格式化
集成 Prettier 支持:
- 保存时格式化(可选)
- 格式化选区或整个块
- 支持 JavaScript、TypeScript、CSS、HTML、JSON 等
- 可自定义格式化规则
## 编辑器扩展
### VSCode 风格搜索
- 查找和替换,支持正则表达式
- 区分大小写和全字匹配选项
- 跨所有块搜索
### 小地图
- 文档的鸟瞰图
- 快速导航
- 可自定义大小和位置
### 彩虹括号
- 彩色括号配对
- 更容易匹配括号
- 可自定义颜色
### 颜色选择器
- 可视化颜色选择
- 支持 hex、RGB、HSL
- 实时预览
### 翻译工具
- 翻译选定的文本
- 支持多种语言
- 快速键盘访问
### 文本高亮
- 高亮重要文本
- 多种高亮颜色
- 持久化高亮
## 多窗口支持
高效使用多个窗口:
- 每个窗口都是独立的
- 独立的文档
- 同步的设置
- 窗口状态持久化
## 主题自定义
完全控制编辑器外观:
### 内置主题
- 深色模式
- 浅色模式
- 根据系统自动切换
### 自定义主题
- 创建你自己的主题
- 自定义每种颜色
- 保存和分享主题
- 导入社区主题
## 自动更新系统
通过自动更新保持最新:
- 后台更新检查
- 新版本通知
- 一键更新
- 更新历史
- 支持多个更新源GitHub、Gitea
## 数据备份
使用基于 Git 的备份保护你的数据:
- 自动备份
- 手动触发备份
- 支持 GitHub 和 Gitea
- 多种认证方式SSH、Token、密码
- 可配置备份间隔
## 键盘快捷键
广泛的键盘支持:
- 可自定义快捷键
- Vim/Emacs 按键绑定(计划中)
- 快速命令面板
- 上下文感知快捷键
## 性能
专为速度而构建:
- 快速启动时间
- 流畅滚动
- 高效内存使用
- 支持大文件
## 隐私与安全
你的数据是安全的:
- 本地优先存储
- 可选云备份
- 无遥测或跟踪
- 开源代码库

View File

@@ -0,0 +1,107 @@
# 快速开始
学习使用 voidraft 的基础知识并创建你的第一个文档。
## 编辑器界面
当你打开 voidraft 时,你将看到:
- **主编辑器**:编写和编辑的中心区域
- **工具栏**:快速访问常用操作
- **状态栏**:显示当前块的语言和其他信息
## 创建代码块
voidraft 使用基于块的编辑系统。每个块可以有不同的语言:
1.`Ctrl+Enter` 创建新块
2. 输入 `∞∞∞` 后跟语言名称(例如 `∞∞∞javascript`
3. 在该块中开始编码
### 支持的语言
voidraft 支持 30+ 种编程语言,包括:
- JavaScript、TypeScript
- Python、Go、Rust
- HTML、CSS、Sass
- SQL、YAML、JSON
- 以及更多...
## 基本操作
### 导航
- `Ctrl+Up/Down`:在块之间移动
- `Ctrl+Home/End`:跳转到第一个/最后一个块
- `Ctrl+F`:在文档中搜索
### 编辑
- `Ctrl+D`:复制当前行
- `Ctrl+/`:切换注释
- `Alt+Up/Down`:向上/向下移动行
- `Ctrl+Shift+F`:格式化代码(如果语言支持 Prettier
### 块管理
- `Ctrl+Enter`:创建新块
- `Ctrl+Shift+Enter`:在上方创建块
- `Alt+Delete`:删除当前块
## 使用 HTTP 客户端
voidraft 包含用于测试 API 的内置 HTTP 客户端:
1. 创建一个 HTTP 语言的块
2. 编写你的 HTTP 请求:
```http
POST "https://api.example.com/users" {
content-type: "application/json"
@json {
name: "",
email: "zhangsan@example.com"
}
}
```
3. 点击运行按钮执行请求
4. 内联查看响应
## 多窗口支持
同时处理多个文档:
1. 转到 `文件 > 新建窗口`(或 `Ctrl+Shift+N`
2. 每个窗口都是独立的
3. 更改会自动保存
## 自定义主题
个性化你的编辑器:
1. 打开设置(`Ctrl+,`
2. 转到外观
3. 选择主题或创建自己的主题
4. 根据你的偏好自定义颜色
## 键盘快捷键
学习基本快捷键:
| 操作 | 快捷键 |
|-----|--------|
| 新建窗口 | `Ctrl+Shift+N` |
| 搜索 | `Ctrl+F` |
| 替换 | `Ctrl+H` |
| 格式化代码 | `Ctrl+Shift+F` |
| 切换主题 | `Ctrl+Shift+T` |
| 命令面板 | `Ctrl+Shift+P` |
## 下一步
现在你已经了解了基础知识:
- 详细探索[功能特性](/zh/guide/features)

View File

@@ -0,0 +1,63 @@
# 安装
本指南将帮助你在系统上安装 voidraft。
## 系统要求
- **操作系统**Windows 10 或更高版本macOS 和 Linux 支持计划中)
- **内存**:最低 4GB推荐 8GB
- **磁盘空间**200MB 可用空间
## 下载
访问[发布页面](https://github.com/landaiqing/voidraft/releases)并下载适合你平台的最新版本:
- **Windows**`voidraft-windows-amd64-installer.exe`
## 安装步骤
### Windows
1. 从发布页面下载安装程序
2. 运行 `voidraft-windows-amd64-installer.exe` 文件
3. 按照安装向导操作
4. 从开始菜单或桌面快捷方式启动 voidraft
## 首次启动
首次启动 voidraft 时:
1. 应用程序将创建一个数据目录来存储你的文档
2. 你将看到带有欢迎块的主编辑器界面
3. 开始输入或创建你的第一个代码块!
## 配置
voidraft 将其配置和数据存储在:
- **Windows**`%APPDATA%/voidraft/`
你可以自定义各种设置,包括:
- 编辑器主题(深色/浅色模式)
- 代码格式化偏好
- 备份设置
- 键盘快捷键
## 更新
voidraft 包含自动更新功能,会在有新版本时通知你。你可以:
- 从设置中手动检查更新
- 启用自动更新
- 选择首选的更新源
## 故障排除
如果在安装过程中遇到任何问题:
1. 确保你有管理员权限
2. 检查杀毒软件是否阻止了安装
3. 访问我们的 [GitHub issues](https://github.com/landaiqing/voidraft/issues) 页面寻求帮助
下一步:[快速开始 →](/zh/guide/getting-started)

View File

@@ -0,0 +1,50 @@
# 简介
欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具。
## 什么是 voidraft
voidraft 是一个现代化的桌面应用程序帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
## 核心特性
### 块状编辑模式
voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
- 不同的编程语言设置
- 语法高亮
- 独立格式化
- 轻松在块之间导航
### 开发者工具
- **HTTP 客户端**:直接在编辑器中测试 API
- **代码格式化**:内置 Prettier 支持多种语言
- **语法高亮**:支持 30+ 种编程语言
- **自动语言检测**:自动识别代码块语言类型
### 自定义
- **自定义主题**:创建并保存你自己的编辑器主题
- **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
- **多窗口**:同时处理多个文档
### 数据管理
- **Git 备份**:使用 Git 仓库自动备份
- **云同步**:跨设备同步你的数据
- **自动更新**:及时获取最新功能
## 为什么选择 voidraft
- **专注开发者**:考虑开发者需求而构建
- **现代技术栈**使用前沿技术Wails3、Vue 3、CodeMirror 6
- **跨平台**:支持 WindowsmacOS 和 Linux 支持计划中)
- **开源**MIT 许可证,社区驱动开发
## 开始使用
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
下一步:[安装 →](/zh/guide/installation)

View File

@@ -0,0 +1,56 @@
---
layout: home
hero:
name: "voidraft"
text: "优雅的文本片段记录工具"
tagline: 为开发者设计,用现代技术打造
image:
src: /img/hero.png
alt: "voidraft"
actions:
- theme: brand
text: 开始使用
link: https://github.com/landaiqing/voidraft/releases
- theme: alt
text: 使用文档
link: /zh/guide/introduction
features:
- icon: 📝
title: 块状编辑模式
details: 将内容分割为独立的代码块,每个块可设置不同语言。继承了 Heynote 优雅的块状编辑理念。
- icon: 🎨
title: 语法高亮
details: 内置支持 30+ 种编程语言的语法高亮,自动语言检测,集成 Prettier 代码格式化工具。
- icon: 🌐
title: HTTP 客户端
details: 集成 HTTP 客户端,支持 JSON、FormData、XML 等多种请求格式。直接在编辑器中测试 API。
- icon: 🎯
title: 多窗口支持
details: 同时编辑多个文档,每个窗口独立维护自己的状态和配置。
- icon: 🎭
title: 主题自定义
details: 完整的主题自定义支持,支持深色/浅色模式。创建并保存你自己的编辑器主题。
- icon: 🔧
title: 丰富的扩展
details: VSCode 风格搜索替换、彩虹括号、小地图、颜色选择器、翻译工具、文本高亮等实用扩展。
- icon: 🔄
title: 自动更新系统
details: 内置自我更新机制,支持多个更新源。及时获取最新功能和改进。
- icon: ☁️
title: Git 备份
details: 基于 Git 的自动数据备份。支持 GitHub、Gitea提供 SSH、Token 等多种认证方式。
- icon: ⚡
title: 现代化架构
details: 采用 Wails3、Vue 3 和 CodeMirror 6 构建。跨平台桌面应用,原生性能,现代化界面。
---

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,15 @@
"lint": "eslint", "lint": "eslint",
"lint:fix": "eslint --fix", "lint:fix": "eslint --fix",
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js", "build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js",
"test": "vitest" "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",
"app:dev": "cd .. &&wails3 dev",
"app:build": "cd .. && wails3 task build",
"app:package": "cd .. && wails3 package",
"app:generate": "cd .. && wails3 generate bindings -ts"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.19.1", "@codemirror/autocomplete": "^6.19.1",
@@ -86,6 +94,7 @@
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "^7.1.12", "vite": "^7.1.12",
"vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.6", "vitest": "^4.0.6",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.2" "vue-tsc": "^3.1.2"

View File

@@ -80,7 +80,7 @@ function animationLoop() {
// 等待一段时间后重置动画 // 等待一段时间后重置动画
resetTimeoutId = window.setTimeout(() => { resetTimeoutId = window.setTimeout(() => {
reset(); reset();
}, 750); }, 500);
} }
} }
@@ -136,7 +136,8 @@ onBeforeUnmount(() => {
left: 0; left: 0;
right: 0; right: 0;
bottom: 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; z-index: 1000;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,175 +1,49 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed, readonly, ref, shallowRef, watchEffect, onScopeDispose } from 'vue'; import { ref, onScopeDispose } from 'vue';
import type { GitBackupConfig } from '@/../bindings/voidraft/internal/models';
import { BackupService } from '@/../bindings/voidraft/internal/services'; import { BackupService } from '@/../bindings/voidraft/internal/services';
import { useConfigStore } from '@/stores/configStore'; import { useConfigStore } from '@/stores/configStore';
import { createTimerManager } from '@/common/utils/timerUtils'; 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', () => { export const useBackupStore = defineStore('backup', () => {
// === 核心状态 === const isPushing = ref(false);
const config = shallowRef<GitBackupConfig | null>(null); const message = ref<string | null>(null);
const isError = ref(false);
// 统一的备份结果状态 const timer = createTimerManager();
const backupResult = ref<BackupResult>({
status: BackupStatus.IDLE
});
// === 定时器管理 ===
const statusTimer = createTimerManager();
// 组件卸载时清理定时器
onScopeDispose(() => {
statusTimer.clear();
});
// === 外部依赖 ===
const configStore = useConfigStore(); const configStore = useConfigStore();
// === 计算属性 === onScopeDispose(() => timer.clear());
const isEnabled = computed(() => configStore.config.backup.enabled);
const isConfigured = computed(() => Boolean(configStore.config.backup.repo_url?.trim()));
// 派生状态计算属性 const pushToRemote = async () => {
const isPushing = computed(() => backupResult.value.status === BackupStatus.PUSHING); const isConfigured = Boolean(configStore.config.backup.repo_url?.trim());
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
);
// === 状态管理方法 === if (isPushing.value || !isConfigured) {
/**
* 设置备份状态
* @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) {
return; return;
} }
try { try {
setBackupStatus(BackupStatus.PUSHING); isPushing.value = true;
message.value = null;
timer.clear();
await BackupService.PushToRemote(); 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) { } 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 { return {
// 只读状态
config: readonly(config),
backupResult: readonly(backupResult),
// 计算属性
isEnabled,
isConfigured,
isPushing, isPushing,
isSuccess, message,
isError, isError,
errorMessage, pushToRemote
};
// 方法
pushToRemote,
retryBackup,
clearStatus
} as const;
}); });

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'; 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 { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models'; import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
import { useConfigStore } from './configStore'; import { useConfigStore } from './configStore';

View File

@@ -55,9 +55,11 @@ onBeforeUnmount(() => {
<template> <template>
<div class="editor-container"> <div class="editor-container">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
<div ref="editorElement" class="editor"></div> <div ref="editorElement" class="editor"></div>
<Toolbar/> <Toolbar/>
<transition name="loading-fade">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
</transition>
</div> </div>
</template> </template>
@@ -85,4 +87,15 @@ onBeforeUnmount(() => {
:deep(.cm-scroller) { :deep(.cm-scroller) {
overflow: auto; 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> </style>

View File

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

View File

@@ -24,7 +24,7 @@ import {lessLanguage} from "@codemirror/lang-less";
import {angularLanguage} from "@codemirror/lang-angular"; import {angularLanguage} from "@codemirror/lang-angular";
import { svelteLanguage } from "@replit/codemirror-lang-svelte"; import { svelteLanguage } from "@replit/codemirror-lang-svelte";
import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language"; import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language";
import { mermaidLanguage } from '@/views/editor/language/mermaid';
import {StreamLanguage} from "@codemirror/language"; import {StreamLanguage} from "@codemirror/language";
import {ruby} from "@codemirror/legacy-modes/mode/ruby"; import {ruby} from "@codemirror/legacy-modes/mode/ruby";
import {shell} from "@codemirror/legacy-modes/mode/shell"; 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("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" import {blockContent} from "./external-tokens.js"
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, 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", 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~OPVO~OUYO!TXO~O!TZO~O", 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", goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto", nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
maxTerm: 51, maxTerm: 52,
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 1, 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], tokenizers: [blockContent, 0, 1],
topRules: {"Document":[0,2]}, topRules: {"Document":[0,2]},
tokenPrec: 0 tokenPrec: 0

View File

@@ -66,6 +66,7 @@ export type SupportedLanguage =
| 'angular' | 'angular'
| 'svelte' | 'svelte'
| 'http' // HTTP Client | '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_REGEX = /^\n∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/gm;
export const DELIMITER_PREFIX = '\n∞∞∞'; export const DELIMITER_PREFIX = '\n∞∞∞';

View File

@@ -10,6 +10,11 @@
// @formdata - 表单数据(属性必须用逗号分隔) // @formdata - 表单数据(属性必须用逗号分隔)
// @urlencoded - URL 编码格式(属性必须用逗号分隔) // @urlencoded - URL 编码格式(属性必须用逗号分隔)
// @text - 纯文本内容 // @text - 纯文本内容
// @params - URL 参数(用于 GET 请求)
// @xml - XML 格式(固定 key: xml
// @html - HTML 格式(固定 key: html
// @javascript - JavaScript 格式(固定 key: javascript
// @binary - 二进制文件(固定 key: binary值格式@file 路径)
// //
// 3. 变量定义: // 3. 变量定义:
// @var { // @var {
@@ -70,7 +75,52 @@
// } // }
// } // }
// //
// 示例 5 - 带响应数据 // 示例 5 - URL 参数请求
// GET "http://api.example.com/users" {
// @params {
// page: 1,
// size: 20,
// keyword: "张三"
// }
// }
//
// 示例 6 - XML 请求:
// POST "http://api.example.com/soap" {
// content-type: "application/xml"
//
// @xml {
// xml: "<user><name>张三</name><age>25</age></user>"
// }
// }
//
// 示例 7 - HTML 请求:
// POST "http://api.example.com/render" {
// content-type: "text/html"
//
// @html {
// html: "<div><h1>标题</h1><p>内容</p></div>"
// }
// }
//
// 示例 8 - JavaScript 请求:
// POST "http://api.example.com/execute" {
// content-type: "application/javascript"
//
// @javascript {
// javascript: "function hello() { return 'Hello World'; }"
// }
// }
//
// 示例 9 - 二进制文件上传:
// POST "http://api.example.com/upload" {
// content-type: "application/octet-stream"
//
// @binary {
// binary: "@file E://Documents/avatar.png"
// }
// }
//
// 示例 10 - 带响应数据:
// POST "http://api.example.com/login" { // POST "http://api.example.com/login" {
// @json { // @json {
// username: "admin", // username: "admin",
@@ -120,14 +170,13 @@ ResponseDeclaration {
ResponseBlock ResponseBlock
} }
// 响应状态:状态码200 或 200-OK或 "error" 关键字 // 响应状态:数字或数字-标识符组合200 或 200-OK或 "error" 关键字
// 数字开头的状态码作为一个整体 token
ResponseStatus { ResponseStatus {
StatusCode | (NumberLiteral ("-" identifier)?) |
@specialize[@name=ErrorStatus]<identifier, "error"> @specialize[@name=ErrorStatus]<identifier, "error">
} }
// 响应时间:数字 + "ms" 作为一个整体 token // 响应时间:直接使用 TimeValue token
ResponseTime { ResponseTime {
TimeValue TimeValue
} }
@@ -168,7 +217,12 @@ AtRule {
(JsonRule | (JsonRule |
FormDataRule | FormDataRule |
UrlEncodedRule | UrlEncodedRule |
TextRule) ","? TextRule |
ParamsRule |
XmlRule |
HtmlRule |
JavaScriptRule |
BinaryRule) ","?
} }
// @json 块JSON 格式请求体(属性必须用逗号分隔) // @json 块JSON 格式请求体(属性必须用逗号分隔)
@@ -195,6 +249,36 @@ TextRule {
JsonBlock JsonBlock
} }
// @params 块URL 参数(用于 GET 请求,属性必须用逗号分隔)
ParamsRule {
@specialize[@name=ParamsKeyword]<AtKeyword, "@params">
JsonBlock
}
// @xml 块XML 格式请求体(固定 key: xml
XmlRule {
@specialize[@name=XmlKeyword]<AtKeyword, "@xml">
XmlBlock
}
// @html 块HTML 格式请求体(固定 key: html
HtmlRule {
@specialize[@name=HtmlKeyword]<AtKeyword, "@html">
HtmlBlock
}
// @javascript 块JavaScript 格式请求体(固定 key: javascript
JavaScriptRule {
@specialize[@name=JavaScriptKeyword]<AtKeyword, "@javascript">
JavaScriptBlock
}
// @binary 块:二进制文件(固定 key: binary值格式@file 路径)
BinaryRule {
@specialize[@name=BinaryKeyword]<AtKeyword, "@binary">
BinaryBlock
}
// 普通块结构(属性逗号可选,最多一个请求体) // 普通块结构(属性逗号可选,最多一个请求体)
Block { Block {
"{" blockContent? "}" "{" blockContent? "}"
@@ -229,6 +313,30 @@ JsonProperty {
":" jsonValue ":" jsonValue
} }
// XML 块结构(可为空 {} 或必须包含 xml: value
XmlBlock {
"{" (@specialize[@name=XmlKey]<identifier, "xml"> ":" jsonValue) "}" |
"{" "}"
}
// HTML 块结构(可为空 {} 或必须包含 html: value
HtmlBlock {
"{" (@specialize[@name=HtmlKey]<identifier, "html"> ":" jsonValue) "}" |
"{" "}"
}
// JavaScript 块结构(可为空 {} 或必须包含 javascript: value
JavaScriptBlock {
"{" (@specialize[@name=JavaScriptKey]<identifier, "javascript"> ":" jsonValue) "}" |
"{" "}"
}
// Binary 块结构(可为空 {} 或必须包含 binary: value
BinaryBlock {
"{" (@specialize[@name=BinaryKey]<identifier, "binary"> ":" jsonValue) "}" |
"{" "}"
}
// 值 // 值
NumberLiteral { NumberLiteral {
numberLiteralInner Unit? numberLiteralInner Unit?
@@ -328,19 +436,14 @@ JsonNull { @specialize[@name=Null]<identifier, "null"> }
"T" @digit @digit ":" @digit @digit ":" @digit @digit "T" @digit @digit ":" @digit @digit ":" @digit @digit
} }
// 状态码:纯数字或数字-字母组合200, 200-OK, 404-Not-Found // 时间值:数字 + ms作为一个整体 token
StatusCode { TimeValue[isolate] {
@digit+ ("-" @asciiLetter (@asciiLetter | "-")*)?
}
// 时间值:数字 + ms123ms
TimeValue {
@digit+ "ms" @digit+ "ms"
} }
whitespace { @whitespace+ } whitespace { @whitespace+ }
@precedence { Timestamp, TimeValue, StatusCode, numberLiteralInner, VariableRef, identifier, Unit } @precedence { Timestamp, TimeValue, numberLiteralInner, VariableRef, identifier, Unit }
numberLiteralInner { numberLiteralInner {
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+) ("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)

View File

@@ -0,0 +1,302 @@
import { describe, it, expect } from 'vitest';
import { parser } from './http.parser';
describe('HTTP Grammar - 固定 Key 约束测试', () => {
function parseAndCheck(content: string, expectError: boolean = false) {
const tree = parser.parse(content);
console.log('\n=== 语法树结构 ===');
let hasError = false;
tree.iterate({
enter: (node) => {
const depth = getDepth(node.node);
const indent = ' '.repeat(depth);
const text = content.slice(node.from, node.to);
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
if (node.name === '⚠') {
hasError = true;
console.log(`${indent}⚠ 错误节点 [${node.from}-${node.to}]: ${JSON.stringify(preview)}`);
} else {
console.log(`${indent}${node.name.padEnd(30 - depth * 2)} [${String(node.from).padStart(3)}-${String(node.to).padEnd(3)}]: ${JSON.stringify(preview)}`);
}
}
});
if (expectError) {
expect(hasError).toBe(true);
} else {
expect(hasError).toBe(false);
}
function getDepth(currentNode: any): number {
let depth = 0;
let node = currentNode;
while (node && node.parent) {
depth++;
node = node.parent;
}
return depth;
}
}
describe('✅ @xml - 正确使用固定 key', () => {
it('应该接受正确的 xml key', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
xml: "<user><name>张三</name></user>"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('应该拒绝错误的 key 名称', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
data: "<user><name>张三</name></user>"
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
it('应该拒绝多个属性', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
xml: "<user></user>",
other: "value"
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
});
describe('✅ @html - 正确使用固定 key', () => {
it('应该接受正确的 html key', () => {
const content = `POST "https://api.example.com/render" {
@html {
html: "<div><h1>标题</h1></div>"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('应该拒绝错误的 key 名称', () => {
const content = `POST "https://api.example.com/render" {
@html {
content: "<div><h1>标题</h1></div>"
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
});
describe('✅ @javascript - 正确使用固定 key', () => {
it('应该接受正确的 javascript key', () => {
const content = `POST "https://api.example.com/execute" {
@javascript {
javascript: "function hello() { return 'world'; }"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('应该拒绝错误的 key 名称', () => {
const content = `POST "https://api.example.com/execute" {
@javascript {
code: "function hello() { return 'world'; }"
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
});
describe('✅ @binary - 正确使用固定 key', () => {
it('应该接受正确的 binary key', () => {
const content = `POST "https://api.example.com/upload" {
@binary {
binary: "@file E://Documents/avatar.png"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('应该拒绝错误的 key 名称', () => {
const content = `POST "https://api.example.com/upload" {
@binary {
file: "@file E://Documents/avatar.png"
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
});
describe('✅ 对比:@json 和 @params 允许任意 key', () => {
it('@json 可以使用任意 key 名称', () => {
const content = `POST "https://api.example.com/api" {
@json {
name: "张三",
age: 25,
email: "test@example.com"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('@params 可以使用任意 key 名称', () => {
const content = `GET "https://api.example.com/users" {
@params {
page: 1,
size: 20,
filter: "active"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
});
describe('✅ 空块测试 - 现在支持空块', () => {
it('@xml 空块应该成功', () => {
const content = `POST "https://api.example.com/soap" {
@xml {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('@html 空块应该成功', () => {
const content = `POST "https://api.example.com/render" {
@html {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('@javascript 空块应该成功', () => {
const content = `POST "https://api.example.com/execute" {
@javascript {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
it('@binary 空块应该成功', () => {
const content = `POST "https://api.example.com/upload" {
@binary {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, false);
});
});
describe('❌ 定义了 key 但没有值应该报错', () => {
it('@xml 定义了 xml key 但没有值', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
xml:
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
it('@html 定义了 html key 但没有值', () => {
const content = `POST "https://api.example.com/render" {
@html {
html:
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
it('@javascript 定义了 javascript key 但没有值', () => {
const content = `POST "https://api.example.com/execute" {
@javascript {
javascript:
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
it('@binary 定义了 binary key 但没有值', () => {
const content = `POST "https://api.example.com/upload" {
@binary {
binary:
}
}`;
console.log('\n测试内容应该有错误:');
console.log(content);
parseAndCheck(content, true);
});
});
});

View File

@@ -0,0 +1,312 @@
import { describe, it, expect } from 'vitest';
import { parser } from './http.parser';
describe('HTTP Grammar - 新增请求格式测试', () => {
function parseAndCheck(content: string, expectError: boolean = false) {
const tree = parser.parse(content);
console.log('\n=== 语法树结构 ===');
let hasError = false;
tree.iterate({
enter: (node) => {
const depth = getDepth(node.node);
const indent = ' '.repeat(depth);
const text = content.slice(node.from, node.to);
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
if (node.name === '⚠') {
hasError = true;
console.log(`${indent}⚠ 错误节点 [${node.from}-${node.to}]: ${JSON.stringify(preview)}`);
} else {
console.log(`${indent}${node.name.padEnd(30 - depth * 2)} [${String(node.from).padStart(3)}-${String(node.to).padEnd(3)}]: ${JSON.stringify(preview)}`);
}
}
});
if (expectError) {
expect(hasError).toBe(true);
} else {
expect(hasError).toBe(false);
}
function getDepth(currentNode: any): number {
let depth = 0;
let node = currentNode;
while (node && node.parent) {
depth++;
node = node.parent;
}
return depth;
}
}
it('✅ @params - URL 参数', () => {
const content = `GET "https://api.example.com/users" {
@params {
page: 1,
size: 20,
keyword: "张三"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @xml - XML 格式请求体', () => {
const content = `POST "https://api.example.com/soap" {
content-type: "application/xml"
@xml {
xml: "<user><name>张三</name><age>25</age></user>"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @html - HTML 格式请求体', () => {
const content = `POST "https://api.example.com/render" {
content-type: "text/html"
@html {
html: "<div><h1>标题</h1><p>内容</p></div>"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @javascript - JavaScript 格式请求体', () => {
const content = `POST "https://api.example.com/execute" {
content-type: "application/javascript"
@javascript {
javascript: "function hello() { return 'Hello World'; }"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @binary - 二进制文件上传', () => {
const content = `POST "https://api.example.com/upload" {
content-type: "application/octet-stream"
@binary {
binary: "@file E://Documents/avatar.png"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ 混合使用 - @params 和响应', () => {
const content = `GET "https://api.example.com/search" {
authorization: "Bearer token123"
@params {
q: "关键词",
page: 1,
limit: 50
}
}
@response 200-OK 156ms 2025-11-03T10:30:00 {
"total": 100,
"data": []
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ 复杂 XML 内容', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ 多行 JavaScript', () => {
const content = `POST "https://api.example.com/run" {
@javascript {
javascript: "function calculate(a, b) {\\n return a + b;\\n}"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @binary 支持不同路径格式', () => {
const content = `POST "https://api.example.com/upload" {
@binary {
binary: "@file C:/Users/Documents/file.pdf"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @params 支持空值', () => {
const content = `GET "https://api.example.com/list" {
@params {
filter: "",
page: 1
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @xml 空块', () => {
const content = `POST "https://api.example.com/soap" {
content-type: "application/xml"
@xml {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @html 空块', () => {
const content = `POST "https://api.example.com/render" {
@html {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @javascript 空块', () => {
const content = `POST "https://api.example.com/execute" {
@javascript {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ @binary 空块', () => {
const content = `POST "https://api.example.com/upload" {
@binary {}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('❌ @xml 定义了 key 但没有值应该报错', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
xml:
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, true);
});
it('❌ @html 定义了 key 但没有值应该报错', () => {
const content = `POST "https://api.example.com/render" {
@html {
html:
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content, true);
});
it('✅ @xml 与其他格式混合', () => {
const content = `POST "https://api.example.com/multi" {
authorization: "Bearer token123"
@xml {
xml: "<data><item>test</item></data>"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
it('✅ 多个请求使用不同格式', () => {
const content = `POST "https://api.example.com/xml" {
@xml {
xml: "<user><name>张三</name></user>"
}
}
POST "https://api.example.com/html" {
@html {
html: "<div>内容</div>"
}
}
POST "https://api.example.com/js" {
@javascript {
javascript: "console.log('test');"
}
}
POST "https://api.example.com/binary" {
@binary {
binary: "@file C:/test.bin"
}
}`;
console.log('\n测试内容:');
console.log(content);
parseAndCheck(content);
});
});

View File

@@ -73,7 +73,7 @@ describe('HTTP Grammar - @response 响应语法', () => {
expect(hasNode(state, 'ResponseDeclaration')).toBe(true); expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
expect(hasNode(state, 'ErrorStatus')).toBe(true); expect(hasNode(state, 'ErrorStatus')).toBe(true);
expect(hasNode(state, 'TimeUnit')).toBe(true); expect(hasNode(state, 'TimeValue')).toBe(true);
}); });
it('✅ 响应与请求结合', () => { it('✅ 响应与请求结合', () => {
@@ -145,8 +145,8 @@ POST "https://api.example.com/users" {
const state = createTestState(content); const state = createTestState(content);
expect(hasNode(state, 'TimeUnit')).toBe(true); expect(hasNode(state, 'TimeValue')).toBe(true);
expect(getNodeText(state, 'TimeUnit')).toBe('ms'); expect(getNodeText(state, 'TimeValue')).toBe('12345ms');
}); });
it('✅ 响应块包含复杂 JSON', () => { it('✅ 响应块包含复杂 JSON', () => {

View File

@@ -88,7 +88,7 @@ describe('HTTP Grammar 解析测试', () => {
return depth; return depth;
} }
it('应该正确解析标准的 GET 请求(包含 @json 和 @res', () => { it('应该正确解析标准的 GET 请求(包含 @json', () => {
const code = `GET "http://127.0.0.1:80/api/create" { const code = `GET "http://127.0.0.1:80/api/create" {
host: "https://api.example.com", host: "https://api.example.com",
content-type: "application/json", content-type: "application/json",
@@ -98,17 +98,6 @@ describe('HTTP Grammar 解析测试', () => {
name : "xxx", name : "xxx",
test: "xx" test: "xx"
} }
@res {
code: 200,
status: "ok",
size: "20kb",
time: "2025-10-31 10:30:26",
data: {
xxx:"xxx"
}
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -277,7 +266,7 @@ POST "http://test2.com" {
expect(result.hasError).toBe(false); expect(result.hasError).toBe(false);
}); });
it('应该支持 @json/@res 块后面不加逗号JSON块内部必须用逗号', () => { it('应该支持 @json 块后面不加逗号JSON块内部必须用逗号', () => {
const code = `POST "http://test.com" { const code = `POST "http://test.com" {
host: "test.com" host: "test.com"
@@ -285,11 +274,6 @@ POST "http://test2.com" {
name: "xxx", name: "xxx",
test: "xx" test: "xx"
} }
@res {
code: 200,
status: "ok"
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -475,14 +459,6 @@ describe('HTTP 请求体格式测试', () => {
level: "advanced" level: "advanced"
} }
} }
@res {
code: 200,
message: "success",
data: {
id: 12345
}
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -509,12 +485,6 @@ describe('HTTP 请求体格式测试', () => {
age: 25, age: 25,
description: "用户头像上传" description: "用户头像上传"
} }
@res {
code: 200,
message: "上传成功",
url: "https://cdn.example.com/avatar.png"
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -539,12 +509,6 @@ describe('HTTP 请求体格式测试', () => {
password: "123456", password: "123456",
remember: true remember: true
} }
@res {
code: 200,
message: "登录成功",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -593,13 +557,6 @@ POST "http://api.example.com/login" {
username: "admin", username: "admin",
password: "123456" password: "123456"
} }
# 期望的响应
@res {
code: 200,
# 用户token
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -615,7 +572,7 @@ POST "http://api.example.com/login" {
expect(result.hasError).toBe(false); expect(result.hasError).toBe(false);
}); });
it('✅ 混合多种格式 - JSON + 响应', () => { it('✅ 混合多种格式 - JSON 请求', () => {
const code = `POST "http://api.example.com/login" { const code = `POST "http://api.example.com/login" {
content-type: "application/json" content-type: "application/json"
user-agent: "Mozilla/5.0" user-agent: "Mozilla/5.0"
@@ -624,16 +581,6 @@ POST "http://api.example.com/login" {
username: "admin", username: "admin",
password: "123456" password: "123456"
} }
@res {
code: 200,
message: "登录成功",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
user: {
id: 1,
name: "管理员"
}
}
}`; }`;
const tree = parseCode(code); const tree = parseCode(code);
@@ -723,3 +670,442 @@ POST "http://api.example.com/login" {
}); });
}); });
describe('HTTP 新格式测试 - params/xml/html/javascript/binary', () => {
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
};
}
it('✅ @params - URL 参数格式', () => {
const code = `GET "http://api.example.com/users" {
authorization: "Bearer token123"
@params {
page: 1,
size: 20,
keyword: "张三",
status: "active"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @params 格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @xml - XML 格式', () => {
const code = `POST "http://api.example.com/soap" {
content-type: "application/xml"
@xml {
xml: "<user><name>张三</name><age>25</age></user>"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @xml 格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @xml - 空块', () => {
const code = `POST "http://api.example.com/soap" {
@xml {}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @xml 空块格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @html - HTML 格式', () => {
const code = `POST "http://api.example.com/render" {
content-type: "text/html"
@html {
html: "<div><h1>标题</h1><p>内容</p></div>"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @html 格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @html - 空块', () => {
const code = `POST "http://api.example.com/render" {
@html {}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @html 空块格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @javascript - JavaScript 格式', () => {
const code = `POST "http://api.example.com/execute" {
content-type: "application/javascript"
@javascript {
javascript: "function hello() { return 'Hello World'; }"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @javascript 格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @javascript - 空块', () => {
const code = `POST "http://api.example.com/execute" {
@javascript {}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @javascript 空块格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @binary - 二进制文件上传', () => {
const code = `POST "http://api.example.com/upload" {
content-type: "application/octet-stream"
@binary {
binary: "@file E://Documents/avatar.png"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @binary 格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ @binary - 空块', () => {
const code = `POST "http://api.example.com/upload" {
@binary {}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @binary 空块格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ 复杂 XML - SOAP 请求', () => {
const code = `POST "http://api.example.com/soap" {
@xml {
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 复杂 XML 格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ 混合使用 - params + headers', () => {
const code = `GET "http://api.example.com/search" {
authorization: "Bearer token123"
user-agent: "Mozilla/5.0"
@params {
q: "搜索关键词",
page: 1,
limit: 50,
sort: "desc"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 混合使用格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
});
it('✅ 多个不同新格式的请求', () => {
const code = `# XML 请求
POST "http://api.example.com/xml" {
@xml {
xml: "<user><name>张三</name></user>"
}
}
# HTML 请求
POST "http://api.example.com/html" {
@html {
html: "<div>内容</div>"
}
}
# JavaScript 请求
POST "http://api.example.com/js" {
@javascript {
javascript: "console.log('test');"
}
}
# Binary 请求
POST "http://api.example.com/upload" {
@binary {
binary: "@file C:/test.bin"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 多新格式请求错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
// 统计 RequestStatement 数量
let requestCount = 0;
tree.iterate({
enter: (node: any) => {
if (node.name === 'RequestStatement') requestCount++;
}
});
expect(requestCount).toBe(4);
});
it('❌ @xml - 定义了 xml key 但没有值(应该报错)', () => {
const code = `POST "http://api.example.com/soap" {
@xml {
xml:
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
// 应该有错误
expect(result.hasError).toBe(true);
});
it('❌ @html - 定义了 html key 但没有值(应该报错)', () => {
const code = `POST "http://api.example.com/render" {
@html {
html:
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
// 应该有错误
expect(result.hasError).toBe(true);
});
it('❌ @xml - 使用错误的 key 名称(应该报错)', () => {
const code = `POST "http://api.example.com/soap" {
@xml {
data: "<user><name>张三</name></user>"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
// 应该有错误
expect(result.hasError).toBe(true);
});
it('✅ 所有格式组合测试', () => {
const code = `# 传统格式
POST "http://api.example.com/json" {
@json {
name: "test",
age: 25
}
}
POST "http://api.example.com/form" {
@formdata {
file: "test.png",
desc: "description"
}
}
POST "http://api.example.com/login" {
@urlencoded {
username: "admin",
password: "123456"
}
}
POST "http://api.example.com/text" {
@text {
content: "纯文本内容"
}
}
# 新格式
GET "http://api.example.com/search" {
@params {
q: "keyword",
page: 1
}
}
POST "http://api.example.com/xml" {
@xml {
xml: "<data>test</data>"
}
}
POST "http://api.example.com/html" {
@html {
html: "<div>test</div>"
}
}
POST "http://api.example.com/js" {
@javascript {
javascript: "alert('test');"
}
}
POST "http://api.example.com/upload" {
@binary {
binary: "@file C:/file.bin"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 所有格式组合测试错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
}
expect(result.hasError).toBe(false);
// 统计 RequestStatement 数量应该有9个
let requestCount = 0;
tree.iterate({
enter: (node: any) => {
if (node.name === 'RequestStatement') requestCount++;
}
});
expect(requestCount).toBe(9);
});
});

View File

@@ -18,12 +18,18 @@ export const httpHighlighting = styleTags({
"TRACE CONNECT": t.modifier, "TRACE CONNECT": t.modifier,
// ========== @ 规则(请求体格式和变量声明)========== // ========== @ 规则(请求体格式和变量声明)==========
// @json, @formdata, @urlencoded - 使用类型名 // @json, @formdata, @urlencoded, @params - 使用类型名
"JsonKeyword FormDataKeyword UrlEncodedKeyword": t.typeName, "JsonKeyword FormDataKeyword UrlEncodedKeyword ParamsKeyword": t.typeName,
// @text - 使用特殊类型 // @text - 使用特殊类型
"TextKeyword": t.special(t.typeName), "TextKeyword": t.special(t.typeName),
// @xml, @html, @javascript - 使用类型名
"XmlKeyword HtmlKeyword JavaScriptKeyword": t.typeName,
// @binary - 使用特殊类型
"BinaryKeyword": t.special(t.typeName),
// @var - 变量声明关键字 // @var - 变量声明关键字
"VarKeyword": t.definitionKeyword, "VarKeyword": t.definitionKeyword,
@@ -60,15 +66,16 @@ export const httpHighlighting = styleTags({
// ========== 响应相关 ========== // ========== 响应相关 ==========
// 响应状态码 - 数字颜色 // 响应状态码 - 数字颜色
"StatusCode": t.number, "ResponseStatus/NumberLiteral": t.number,
"ResponseStatus/StatusCode": t.number, "ResponseStatus/identifier": t.constant(t.variableName),
// 响应错误状态 - 关键字 // 响应错误状态 - 关键字
"ErrorStatus": t.operatorKeyword, "ErrorStatus": t.operatorKeyword,
// 响应时间 - 数字颜色 // 响应时间 - 数字和单位颜色
"TimeValue": t.number, "TimeValue": t.number,
"ResponseTime": t.number, "ResponseTime/TimeValue": t.number,
"TimeUnit": t.unit,
// 时间戳 - 字符串颜色 // 时间戳 - 字符串颜色
"Timestamp": t.string, "Timestamp": t.string,
@@ -99,6 +106,12 @@ export const httpHighlighting = styleTags({
"JsonValue/StringLiteral": t.string, "JsonValue/StringLiteral": t.string,
"JsonValue/NumberLiteral": t.number, "JsonValue/NumberLiteral": t.number,
// ========== 固定 key 名称xml、html、javascript、binary==========
"XmlKey": t.constant(t.propertyName),
"HtmlKey": t.constant(t.propertyName),
"JavaScriptKey": t.constant(t.propertyName),
"BinaryKey": t.constant(t.propertyName),
// ========== 标点符号 ========== // ========== 标点符号 ==========
// 冒号 - 分隔符 // 冒号 - 分隔符
":": t.separator, ":": t.separator,

View File

@@ -40,17 +40,34 @@ export const
UrlEncodedKeyword = 44, UrlEncodedKeyword = 44,
TextRule = 45, TextRule = 45,
TextKeyword = 46, TextKeyword = 46,
ResponseDeclaration = 47, ParamsRule = 47,
ResponseKeyword = 48, ParamsKeyword = 48,
ResponseStatus = 49, XmlRule = 49,
StatusCode = 50, XmlKeyword = 50,
ErrorStatus = 51, XmlBlock = 51,
ResponseTime = 52, XmlKey = 52,
TimeValue = 53, HtmlRule = 53,
ResponseTimestamp = 54, HtmlKeyword = 54,
Timestamp = 55, HtmlBlock = 55,
ResponseBlock = 56, HtmlKey = 56,
JsonObject = 57, JavaScriptRule = 57,
JsonMember = 58, JavaScriptKeyword = 58,
JsonValue = 59, JavaScriptBlock = 59,
JsonArray = 62 JavaScriptKey = 60,
BinaryRule = 61,
BinaryKeyword = 62,
BinaryBlock = 63,
BinaryKey = 64,
ResponseDeclaration = 65,
ResponseKeyword = 66,
ResponseStatus = 67,
ErrorStatus = 68,
ResponseTime = 69,
TimeValue = 70,
ResponseTimestamp = 71,
Timestamp = 72,
ResponseBlock = 73,
JsonObject = 74,
JsonMember = 75,
JsonValue = 76,
JsonArray = 79

View File

@@ -1,26 +1,26 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr" import {LRParser} from "@lezer/lr"
import {httpHighlighting} from "./http.highlight" import {httpHighlighting} from "./http.highlight"
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@response":96} const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@params":96, "@xml":100, "@html":108, "@javascript":116, "@binary":124, "@response":132}
const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, error:102} const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, xml:104, html:112, javascript:120, binary:128, error:136}
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: "-bQYQPOOO!aQPO'#C_OOQO'#Ct'#CtO!aQPO'#DTO!aQPO'#DVO!aQPO'#DXO!aQPO'#DZO!fQPO'#DSO#yQPO'#CsO$jQPO'#DlO$qQPO'#DgO$|QPO'#D]OOQO'#Du'#DuOOQO'#Dm'#DmQYQPOOO%UQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59n,59nOOQO'#DO'#DOO%^QPO,59_O%cQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO&QQPO'#D}OOQO,5:W,5:WO&YQPO,5:WOOQO'#Di'#DiO&_QPO'#DhO&dQPO'#D|OOQO,5:R,5:RO&lQPO,5:ROOQO'#D_'#D_O&qQPO,59wOOQO-E7k-E7kOOQO'#Cf'#CfO&vQPO'#CeO&{QPO'#DvOOQO,59O,59OO'TQPO,59OO'kQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO'rQPO,5:iO'yQPO,5:iOOQO1G/r1G/rO$OQPO,5:SO(RQPO,5:hO(^QPO,5:hOOQO1G/m1G/mOOQO'#Db'#DbO(fQPO1G/cO(kQPO,59PO)SQPO,5:bO)[QPO,5:bOOQO1G.j1G.jOOQO'#DR'#DRO)dQPO'#DQOOQO'#Do'#DoO)iQPO'#DzO)pQPO'#DzOOQO,59k,59kO)xQPO,59kOOQO,5:[,5:[O)}QPO1G0TOOQO-E7n-E7nOOQO1G/n1G/nOOQO,5:],5:]O*UQPO1G0SOOQO-E7o-E7oOOQO'#Dd'#DdO*aQPO7+$}OOQO'#Dx'#DxOOQO1G.k1G.kOOQO,5:Y,5:YO*iQPO1G/|OOQO-E7l-E7lO*qQPO,59lOOQO-E7m-E7mO+SQPO,5:fO+SQPO,5:fOOQO1G/V1G/VP$OQPO'#DpP$tQPO'#DqOOQO'#Df'#DfOOQO<<Hi<<HiP%XQPO'#DnOOQO'#D{'#D{O+[QPO1G/WO+sQPO1G0QOOQO7+$r7+$r", states: "2`QYQPOOO!pQPO'#C_OOQO'#Ct'#CtO!pQPO'#DTO!pQPO'#DVO!pQPO'#DXO!pQPO'#DZO!pQPO'#D]O!uQPO'#D_O!zQPO'#DcO#PQPO'#DgO#UQPO'#DkO#ZQPO'#DSO$}QPO'#CsO%nQPO'#D}O%uQPO'#DxO&QQPO'#DoOOQO'#EW'#EWOOQO'#EO'#EOQYQPOOO&YQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59w,59wO&bQPO'#DaOOQO,59y,59yO&jQPO'#DeOOQO,59},59}O&rQPO'#DiOOQO,5:R,5:RO&zQPO'#DmOOQO,5:V,5:VOOQO,59n,59nOOQO'#DO'#DOO'SQPO,59_O'XQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO(VQPO'#EaOOQO,5:i,5:iO(_QPO,5:iOOQO'#Dz'#DzO(dQPO'#DyO(iQPO'#E`OOQO,5:d,5:dO(qQPO,5:dO(vQQO'#CiO)RQQO'#DqOOQO'#Dq'#DqO)ZQPO,5:ZOOQO-E7|-E7|OOQO'#Cf'#CfO)`QPO'#CeO)eQPO'#EXOOQO,59O,59OO)mQPO,59OO)rQPO,59{OOQO,59{,59{O)wQPO,5:POOQO,5:P,5:PO)|QPO,5:TOOQO,5:T,5:TO*RQPO,5:XOOQO,5:X,5:XO*xQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO+PQPO,5:{O+WQPO,5:{OOQO1G0T1G0TO%SQPO,5:eO+`QPO,5:zO+kQPO,5:zOOQO1G0O1G0OO+sQPO,5:]OOQO'#Ds'#DsO+xQPO1G/uO+}QPO,59PO,fQPO,5:sO,nQPO,5:sOOQO1G.j1G.jO+}QPO1G/gO+}QPO1G/kO+}QPO1G/oO+}QPO1G/sOOQO'#DR'#DRO,vQPO'#DQOOQO'#EQ'#EQO,{QPO'#E]O-SQPO'#E]OOQO,59k,59kO-[QPO,59kOOQO,5:m,5:mO-aQPO1G0gOOQO-E8P-E8POOQO1G0P1G0POOQO,5:n,5:nO-hQPO1G0fOOQO-E8Q-E8QOOQO1G/w1G/wOOQO'#Du'#DuO-sQPO7+%aOOQO'#EZ'#EZOOQO1G.k1G.kOOQO,5:k,5:kO-{QPO1G0_OOQO-E7}-E7}O.TQPO7+%RO.YQPO7+%VO._QPO7+%ZO.dQPO7+%_O.iQPO,59lOOQO-E8O-E8OO.zQPO,5:wO.zQPO,5:wOOQO1G/V1G/VP%SQPO'#ERP%xQPO'#ESOOQO'#Dw'#DwOOQO<<H{<<H{P&]QPO'#EPOOQO<<Hm<<HmOOQO<<Hq<<HqOOQO<<Hu<<HuOOQO<<Hy<<HyOOQO'#E^'#E^O/SQPO1G/WO/zQPO1G0cOOQO7+$r7+$r",
stateData: ",U~O!hOSPOS~OTPOVYOiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QZO!_XO~OV_O~OfeOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!_vX!fvXUvX!kvX~O[fO~OVYO[oO_oOaiOcjOekO!_XO!mhO~O!^mO~P$OOUrO[pO!kpO~O!StO!TtO~OUzO!kwO~OV|O~O^!OOf]X!^]XU]Xx]Xz]X|]X!O]X!k]X~Of!PO!^!qX~O!^!RO~OZ!SO~Of!TOU!pX~OU!VO~O!V!WO~OZ!YO~Of!ZOU!jX~OU!]O~OxROzSO|TO!OUO!k!^O~OU!cO~P'YO!^!qa~P$OOf!fO!^!qa~O[pO!kpOU!pa~Of!jOU!pa~O!X!lO~OV_O[!nO_!nOaiOcjOekO!mhO~O!kwOU!ja~Of!qOU!ja~OZ!sO~OU!nX~P'YO!k!^OU!nX~OU!wO~O!^!qi~P$OO[pO!kpOU!pi~OVYO!_XO~O!kwOU!ji~OV|O[!}O_!}O!k!}O!mhO~O!k!^OU!na~Of#QOUtixtizti|ti!Oti!kti~O!k!^OU!ni~O!X!V!S!m_!k^!m~", stateData: "0[~O!yOSPOS~OTPOV_OiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QVO!SWO!WXO![YO!`ZO!d`O!p^O~OVdO~OVkO~OVmO~OVoO~OVqO~OfsOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!SvX!WvX![vX!`vX!dvX!pvX!wvXUvX!|vX~O[tO~OV_O[}O_}OawOcxOeyO!p^O#OvO~O!o{O~P%SOU!QO[!OO!|!OO~O!f!UO#O!SO~OU![O!|!XO~OU!_O!U!^O~OU!aO!Y!`O~OU!cO!^!bO~OU!eO!b!dO~OV!fO~O^!hOf]X!o]XU]Xx]Xz]X|]X!O]X!Q]X!S]X!W]X![]X!`]X!|]X~Of!iO!o#TX~O!o!kO~OZ!lO~Of!mOU#SX~OU!oO~O^!hO!h]X#R]X~O#R!pO!h!eX~O!h!qO~OZ!sO~Of!tOU!{X~OU!vO~OZ!wO~OZ!xO~OZ!yO~OZ!zO~OxROzSO|TO!OUO!QVO!SWO!WXO![YO!`ZO!|!{O~OU#QO~P*WO!o#Ta~P%SOf#TO!o#Ta~O[!OO!|!OOU#Sa~Of#XOU#Sa~O!|#ZO~O!j#[O~OVdO[#^O_#^OawOcxOeyO#OvO~O!|!XOU!{a~Of#aOU!{a~OZ#gO~OU#PX~P*WO!|!{OU#PX~OU#kO~O!o#Ti~P%SO[!OO!|!OOU#Si~OV_O!p^O~O!|!XOU!{i~OU#qO~OU#rO~OU#sO~OU#tO~OV!fO[#uO_#uO!|#uO#OvO~O!|!{OU#Pa~Of#xOUtixtizti|ti!Oti!Qti!Sti!Wti![ti!`ti!|ti~O!|!{OU#Pi~O!j!h#O_!|^_~",
goto: "'^!rPPP!sPPPP!w#Z#cPP#iPP#vP#vP#vPP!s$QPPPPPPPPP$U$X$_$g$o$yP$yP$yP$yP!sP%PPP%SP%VP%Y%]%k%sPP%]&O&U&[&j&pPPP&v&zP&}P'Q'T'W'ZT[O^Q`PQaRQbSQcTQdUR!n!YQy_V!p!Z!q!|Xx_!Z!q!|YoX!P!S!f!xQ!n!YR!}!sYoX!P!S!f!xR!n!YTWO^RgWQ}gR!}!s]!`|!a!b!u!v#P]!_|!a!b!u!v#PS[O^Q!b|R!u!aXVO^|!aRuZR!XuR!m!XR!{!mS[O^YoX!P!S!f!xR!z!mQqYV!i!T!j!yQlXU!e!P!f!xR!h!SQ^ORv^Q![yR!r![Q!a|U!t!a!v#PQ!v!bR#P!uQ!QlR!g!QQ!UqR!k!UT]O^R{_R!o!YR!d|R#O!sRsYRnX", goto: "(l#UPPP#VPPPP#Z#t#|PP$SPP$hP$hP$hPP#V$vPPPPPPPPP$z$}%T%]%e%oP%oP%oP%oP%oP%oP%uP%oP%xP%oP%{P%oP&OP#VP&RP&UP&XP&[&_&m&uPP&_'Q'W'^'l'rPPP'x'|P(PP(`(cP(f(iTaOcQePQfRQgSQhTQiUQjVZ#^!s!w!x!y!zQ!ZdV#`!t#a#pX!Yd!t#a#pY}^!i!l#T#lQ!T`Y#^!s!w!x!y!zR#u#gY}^!i!l#T#lZ#^!s!w!x!y!zT]OcRu]Q!guR#u#g]!}!f#O#P#i#j#w]!|!f#O#P#i#j#wSaOcQ#P!fR#i#OX[Oc!f#ORlWRnXRpYRrZR!V`R!r!VR#]!rR#o#]SaOcY}^!i!l#T#lR#n#]Q!P_V#W!m#X#mQz^U#S!i#T#lR#V!lQcOR!WcQ!u!ZR#b!uQ#O!fU#h#O#j#wQ#j#PR#w#iQ!jzR#U!jQ!n!PR#Y!nTbOcR!]dQ#_!sQ#c!wQ#d!xQ#e!yR#f!zR#R!fR#v#gR!R_R|^",
nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ResponseDeclaration ResponseKeyword ResponseStatus StatusCode ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray", nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ParamsRule ParamsKeyword XmlRule XmlKeyword XmlBlock XmlKey HtmlRule HtmlKeyword HtmlBlock HtmlKey JavaScriptRule JavaScriptKeyword JavaScriptBlock JavaScriptKey BinaryRule BinaryKeyword BinaryBlock BinaryKey ResponseDeclaration ResponseKeyword ResponseStatus ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray",
maxTerm: 79, maxTerm: 97,
nodeProps: [ nodeProps: [
["openedBy", 6,"{",60,"["], ["openedBy", 6,"{",77,"["],
["closedBy", 7,"}",61,"]"], ["closedBy", 7,"}",78,"]"],
["isolate", -3,12,15,55,""] ["isolate", -4,12,15,70,72,""]
], ],
propSources: [httpHighlighting], propSources: [httpHighlighting],
skippedNodes: [0,1,4], skippedNodes: [0,1,4],
repeatNodeCount: 5, repeatNodeCount: 5,
tokenData: "3b~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O(P!O!P(Y!Q![)p![!]/Y!b!c/_!c!}0V!}#O0p#P#Q0u#R#S%y#T#o0V#o#p0z#q#r3]#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!h~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!k~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&b~(SQ!O!P(Y!Q![)Y~(]P!Q![(`~(eR!m~!Q![(`!g!h(n#X#Y(n~(qR{|(z}!O(z!Q![)Q~(}P!Q![)Q~)VP!m~!Q![)Q~)_S!m~!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~~)wU!S~!m~}!O*Z!O!P(`!Q![*r!g!h(n#X#Y(n#a#b.}~*^Q!c!}*d#T#o*d~*iR!S~}!O*d!c!}*d#T#o*d~*yU!S~!m~}!O*Z!O!P(`!Q![+]!g!h(n#X#Y(n#a#b.}~+dU!S~!m~}!O*Z!O!P(`!Q![+v!g!h(n#X#Y(n#a#b.}~+}U!S~!m~}!O,a!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~,dR!Q![,m!c!}*d#T#o*d~,pP!Q![,s~,vP}!O,y~,|P!Q![-P~-SP!Q![-V~-YP!v!w-]~-`P!Q![-c~-fP!Q![-i~-lP![!]-o~-rP!Q![-u~-xP!Q![-{~.OP![!].R~.UP!Q![.X~.[P!Q![._~.dO!X~~.kU!S~!m~}!O*Z!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~/QP#g#h/T~/YO!V~~/_OZ~~/bR}!O/k!c!}/t#T#o/t~/nQ!c!}/t#T#o/t~/ySS~}!O/t!Q![/t!c!}/t#T#o/t~0^U!k~^~tu%y}!O%y!Q![%y!c!}0V#R#S%y#T#o0V~0uO!_~~0zO!^~~1PPV~#o#p1S~1VStu1c!c!}1c#R#S1c#T#o1c~1fXtu1c}!O1c!O!P1c!Q![1c![!]2R!c!}1c#R#S1c#T#o1c#q#r3V~2UUOY2RZ#q2R#q#r2h#r;'S2R;'S;=`3P<%lO2R~2kTO#q2R#q#r2z#r;'S2R;'S;=`3P<%lO2R~3PO_~~3SP;=`<%l2R~3YP#q#r2z~3bOU~", tokenData: "2h~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O)p!O!P(Y!Q![){![!].`!b!c.e!c!}/]!}#O/v#P#Q/{#R#S%y#T#o/]#o#p0Q#q#r2c#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!y~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!|~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&bP(SQ!O!P(Y!Q![)YP(]P!Q![(`P(eR#OP!Q![(`!g!h(n#X#Y(nP(qR{|(z}!O(z!Q![)QP(}P!Q![)QP)VP#OP!Q![)QP)_S#OP!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~R)uQ#RQ!O!P(Y!Q![)Y~*QT#OP!O!P(`!Q![*a!g!h(n#X#Y(n#a#b.T~*fT#OP!O!P(`!Q![*u!g!h(n#X#Y(n#a#b.T~*zT#OP!O!P(`!Q![+Z!g!h(n#X#Y(n#a#b.T~+`U#OP}!O+r!O!P(`!Q![-o!g!h(n#X#Y(n#a#b.T~+uP!Q![+x~+{P!Q![,O~,RP}!O,U~,XP!Q![,[~,_P!Q![,b~,eP!v!w,h~,kP!Q![,n~,qP!Q![,t~,wP![!],z~,}P!Q![-Q~-TP!Q![-W~-ZP![!]-^~-aP!Q![-d~-gP!Q![-j~-oO!j~~-tT#OP!O!P(`!Q![-o!g!h(n#X#Y(n#a#b.T~.WP#g#h.Z~.`O!h~~.eOZ~~.hR}!O.q!c!}.z#T#o.z~.tQ!c!}.z#T#o.z~/PSS~}!O.z!Q![.z!c!}.z#T#o.z~/dU!|~^~tu%y}!O%y!Q![%y!c!}/]#R#S%y#T#o/]~/{O!p~~0QO!o~~0VPV~#o#p0Y~0]Stu0i!c!}0i#R#S0i#T#o0i~0lXtu0i}!O0i!O!P0i!Q![0i![!]1X!c!}0i#R#S0i#T#o0i#q#r2]~1[UOY1XZ#q1X#q#r1n#r;'S1X;'S;=`2V<%lO1X~1qTO#q1X#q#r2Q#r;'S1X;'S;=`2V<%lO1X~2VO_~~2YP;=`<%l1X~2`P#q#r2Q~2hOU~",
tokenizers: [0], tokenizers: [0, 1],
topRules: {"Document":[0,2]}, topRules: {"Document":[0,2]},
specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 73, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}], specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 90, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
tokenPrec: 503 tokenPrec: 694
}) })

View File

@@ -0,0 +1,313 @@
import { describe, it, expect } from 'vitest';
import { EditorState } from '@codemirror/state';
import { httpLanguage } from '../language';
import { parseHttpRequest } from './request-parser';
/**
* 创建测试用的 EditorState
*/
function createTestState(content: string): EditorState {
return EditorState.create({
doc: content,
extensions: [httpLanguage]
});
}
describe('HTTP Request Parser - 新格式测试', () => {
describe('✅ @params - URL 参数', () => {
it('应该正确解析 params 请求', () => {
const content = `GET "https://api.example.com/users" {
@params {
page: 1,
size: 20,
keyword: "张三"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.method).toBe('GET');
expect(request?.url).toBe('https://api.example.com/users');
expect(request?.bodyType).toBe('params');
expect(request?.body).toEqual({
page: 1,
size: 20,
keyword: '张三'
});
});
});
describe('✅ @xml - XML 格式', () => {
it('应该正确解析 xml 请求', () => {
const content = `POST "https://api.example.com/soap" {
content-type: "application/xml"
@xml {
xml: "<user><name>张三</name><age>25</age></user>"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.method).toBe('POST');
expect(request?.bodyType).toBe('xml');
expect(request?.body).toEqual({
xml: '<user><name>张三</name><age>25</age></user>'
});
expect(request?.headers['content-type']).toBe('application/xml');
});
it('应该正确解析空 xml 块', () => {
const content = `POST "https://api.example.com/soap" {
@xml {}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('xml');
expect(request?.body).toEqual({});
});
});
describe('✅ @html - HTML 格式', () => {
it('应该正确解析 html 请求', () => {
const content = `POST "https://api.example.com/render" {
content-type: "text/html"
@html {
html: "<div><h1>标题</h1><p>内容</p></div>"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.method).toBe('POST');
expect(request?.bodyType).toBe('html');
expect(request?.body).toEqual({
html: '<div><h1>标题</h1><p>内容</p></div>'
});
expect(request?.headers['content-type']).toBe('text/html');
});
it('应该正确解析空 html 块', () => {
const content = `POST "https://api.example.com/render" {
@html {}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('html');
expect(request?.body).toEqual({});
});
});
describe('✅ @javascript - JavaScript 格式', () => {
it('应该正确解析 javascript 请求', () => {
const content = `POST "https://api.example.com/execute" {
content-type: "application/javascript"
@javascript {
javascript: "function hello() { return 'Hello World'; }"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.method).toBe('POST');
expect(request?.bodyType).toBe('javascript');
expect(request?.body).toEqual({
javascript: "function hello() { return 'Hello World'; }"
});
expect(request?.headers['content-type']).toBe('application/javascript');
});
it('应该正确解析空 javascript 块', () => {
const content = `POST "https://api.example.com/execute" {
@javascript {}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('javascript');
expect(request?.body).toEqual({});
});
});
describe('✅ @binary - 二进制文件', () => {
it('应该正确解析 binary 请求', () => {
const content = `POST "https://api.example.com/upload" {
content-type: "application/octet-stream"
@binary {
binary: "@file E://Documents/avatar.png"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.method).toBe('POST');
expect(request?.bodyType).toBe('binary');
expect(request?.body).toEqual({
binary: '@file E://Documents/avatar.png'
});
expect(request?.headers['content-type']).toBe('application/octet-stream');
});
it('应该正确解析空 binary 块', () => {
const content = `POST "https://api.example.com/upload" {
@binary {}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('binary');
expect(request?.body).toEqual({});
});
});
describe('✅ 混合使用场景', () => {
it('应该正确解析带 params 和 headers 的请求', () => {
const content = `GET "https://api.example.com/search" {
authorization: "Bearer token123"
@params {
q: "关键词",
page: 1,
limit: 50
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.method).toBe('GET');
expect(request?.headers['authorization']).toBe('Bearer token123');
expect(request?.bodyType).toBe('params');
expect(request?.body).toEqual({
q: '关键词',
page: 1,
limit: 50
});
});
it('应该正确解析复杂 XML 内容', () => {
const content = `POST "https://api.example.com/soap" {
@xml {
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('xml');
expect(request?.body.xml).toContain('soap:Envelope');
expect(request?.body.xml).toContain('GetUser');
});
});
describe('✅ 对比:传统格式仍然可用', () => {
it('JSON 格式仍然正常工作', () => {
const content = `POST "https://api.example.com/api" {
@json {
name: "张三",
age: 25,
email: "test@example.com"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('json');
expect(request?.body).toEqual({
name: '张三',
age: 25,
email: 'test@example.com'
});
});
it('FormData 格式仍然正常工作', () => {
const content = `POST "https://api.example.com/form" {
@formdata {
username: "admin",
password: "123456"
}
}`;
const state = createTestState(content);
const request = parseHttpRequest(state, 0);
expect(request).not.toBeNull();
expect(request?.bodyType).toBe('formdata');
expect(request?.body).toEqual({
username: 'admin',
password: '123456'
});
});
});
describe('✅ 多个请求解析', () => {
it('应该能在同一文档中解析不同格式的请求', () => {
const content = `POST "https://api.example.com/xml" {
@xml {
xml: "<user><name>张三</name></user>"
}
}
POST "https://api.example.com/html" {
@html {
html: "<div>内容</div>"
}
}
POST "https://api.example.com/js" {
@javascript {
javascript: "console.log('test');"
}
}`;
const state = createTestState(content);
// 解析第一个请求XML
const request1 = parseHttpRequest(state, 0);
expect(request1?.bodyType).toBe('xml');
expect(request1?.body.xml).toContain('张三');
// 解析第二个请求HTML- 找到第二个 POST 的位置
const secondPostIndex = content.indexOf('POST', 10);
const request2 = parseHttpRequest(state, secondPostIndex);
expect(request2?.bodyType).toBe('html');
expect(request2?.body.html).toContain('内容');
// 解析第三个请求JavaScript
const thirdPostIndex = content.indexOf('POST', secondPostIndex + 10);
const request3 = parseHttpRequest(state, thirdPostIndex);
expect(request3?.bodyType).toBe('javascript');
expect(request3?.body.javascript).toContain('console.log');
});
});
});

View File

@@ -17,7 +17,7 @@ export interface HttpRequest {
headers: Record<string, string>; headers: Record<string, string>;
/** 请求体类型 */ /** 请求体类型 */
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text'; bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text' | 'params' | 'xml' | 'html' | 'javascript' | 'binary';
/** 请求体内容 */ /** 请求体内容 */
body?: any; body?: any;
@@ -48,12 +48,30 @@ const NODE_TYPES = {
FORMDATA_RULE: 'FormDataRule', FORMDATA_RULE: 'FormDataRule',
URLENCODED_RULE: 'UrlEncodedRule', URLENCODED_RULE: 'UrlEncodedRule',
TEXT_RULE: 'TextRule', TEXT_RULE: 'TextRule',
PARAMS_RULE: 'ParamsRule',
XML_RULE: 'XmlRule',
HTML_RULE: 'HtmlRule',
JAVASCRIPT_RULE: 'JavaScriptRule',
BINARY_RULE: 'BinaryRule',
JSON_KEYWORD: 'JsonKeyword', JSON_KEYWORD: 'JsonKeyword',
FORMDATA_KEYWORD: 'FormDataKeyword', FORMDATA_KEYWORD: 'FormDataKeyword',
URLENCODED_KEYWORD: 'UrlEncodedKeyword', URLENCODED_KEYWORD: 'UrlEncodedKeyword',
TEXT_KEYWORD: 'TextKeyword', TEXT_KEYWORD: 'TextKeyword',
PARAMS_KEYWORD: 'ParamsKeyword',
XML_KEYWORD: 'XmlKeyword',
HTML_KEYWORD: 'HtmlKeyword',
JAVASCRIPT_KEYWORD: 'JavaScriptKeyword',
BINARY_KEYWORD: 'BinaryKeyword',
JSON_BLOCK: 'JsonBlock', JSON_BLOCK: 'JsonBlock',
JSON_PROPERTY: 'JsonProperty', JSON_PROPERTY: 'JsonProperty',
XML_BLOCK: 'XmlBlock',
HTML_BLOCK: 'HtmlBlock',
JAVASCRIPT_BLOCK: 'JavaScriptBlock',
BINARY_BLOCK: 'BinaryBlock',
XML_KEY: 'XmlKey',
HTML_KEY: 'HtmlKey',
JAVASCRIPT_KEY: 'JavaScriptKey',
BINARY_KEY: 'BinaryKey',
VARIABLE_REF: 'VariableRef', VARIABLE_REF: 'VariableRef',
} as const; } as const;
@@ -220,17 +238,74 @@ export class HttpRequestParser {
[NODE_TYPES.FORMDATA_RULE]: 'formdata', [NODE_TYPES.FORMDATA_RULE]: 'formdata',
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded', [NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
[NODE_TYPES.TEXT_RULE]: 'text', [NODE_TYPES.TEXT_RULE]: 'text',
[NODE_TYPES.PARAMS_RULE]: 'params',
[NODE_TYPES.XML_RULE]: 'xml',
[NODE_TYPES.HTML_RULE]: 'html',
[NODE_TYPES.JAVASCRIPT_RULE]: 'javascript',
[NODE_TYPES.BINARY_RULE]: 'binary',
}; };
const type = typeMap[node.name]; const type = typeMap[node.name];
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
// 根据不同的规则类型解析不同的块
let content: any = null;
if (node.name === NODE_TYPES.XML_RULE) {
const blockNode = node.getChild(NODE_TYPES.XML_BLOCK);
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'xml') : null;
} else if (node.name === NODE_TYPES.HTML_RULE) {
const blockNode = node.getChild(NODE_TYPES.HTML_BLOCK);
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'html') : null;
} else if (node.name === NODE_TYPES.JAVASCRIPT_RULE) {
const blockNode = node.getChild(NODE_TYPES.JAVASCRIPT_BLOCK);
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'javascript') : null;
} else if (node.name === NODE_TYPES.BINARY_RULE) {
const blockNode = node.getChild(NODE_TYPES.BINARY_BLOCK);
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'binary') : null;
} else {
// json, formdata, urlencoded, text, params 使用 JsonBlock
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
content = blockNode ? this.parseJsonBlock(blockNode) : null;
}
return { return {
type, type,
content: blockNode ? this.parseJsonBlock(blockNode) : null content
}; };
} }
/**
* 解析固定 key 的块xml, html, javascript, binary
* 格式:{ key: "value" } 或 {}(空块)
*/
private parseFixedKeyBlock(node: SyntaxNode, keyName: string): any {
// 查找固定的 key 节点
const keyNode = node.getChild(
keyName === 'xml' ? NODE_TYPES.XML_KEY :
keyName === 'html' ? NODE_TYPES.HTML_KEY :
keyName === 'javascript' ? NODE_TYPES.JAVASCRIPT_KEY :
NODE_TYPES.BINARY_KEY
);
// 如果没有 key返回空对象支持空块
if (!keyNode) {
return {};
}
// 查找值节点(冒号后面的内容)
let value: any = null;
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.name === NODE_TYPES.STRING_LITERAL ||
child.name === NODE_TYPES.VARIABLE_REF) {
value = this.parseValue(child);
break;
}
}
// 返回格式:{ xml: "value" } 或 { html: "value" } 等
return value !== null ? { [keyName]: value } : {};
}
/** /**
* 解析 JsonBlock用于 @json, @form, @urlencoded * 解析 JsonBlock用于 @json, @form, @urlencoded
*/ */

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';

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