Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3393bc84e3 | |||
| 286b0159d7 | |||
| cc98e556c6 | |||
| 5902f482d9 | |||
| 551e7e2cfd | |||
| e0179b5838 | |||
| df79267e16 | |||
| 1f0254822f | |||
| e9b6fef3cd | |||
| 689b0d5d14 | |||
| a058e62595 | |||
| 8571fc0f5c | |||
| 4dad0a86b3 | |||
| 3168b7ff43 | |||
| d002a5be5a | |||
| 24a550463c | |||
| 14ae3e80c4 | |||
| e4d3969e95 | |||
| 0b16d1d4ac | |||
| 300514531d | |||
| 6a4780b002 | |||
| 5688304817 | |||
| 4380ad010c | |||
| 4fa6bb42e3 | |||
| 7aa3a7e37f | |||
| 94306497a9 | |||
| 93c85b800b | |||
| 8ac78e39f1 | |||
| 61a23fe7f2 | |||
| 87fea58102 | |||
|
|
edeac01bee |
286
.github/workflows/build-release.yml
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
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 '.include += [{"platform":"windows-latest","os":"windows","arch":"amd64","output_name":"voidraft-windows-amd64.exe","id":"windows"}]')
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORMS" == *"linux"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"ubuntu-22.04","os":"linux","arch":"amd64","output_name":"voidraft-linux-amd64","id":"linux"}]')
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORMS" == *"macos-intel"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"macos-latest","os":"darwin","arch":"amd64","output_name":"voidraft-darwin-amd64","id":"macos-intel"}]')
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORMS" == *"macos-arm"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"macos-latest","os":"darwin","arch":"arm64","output_name":"voidraft-darwin-arm64","id":"macos-arm"}]')
|
||||
fi
|
||||
|
||||
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
|
||||
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
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.0-dev \
|
||||
pkg-config
|
||||
|
||||
# Windows 平台依赖(GitHub Actions 的 Windows runner 已包含 MinGW)
|
||||
- name: 设置 Windows 构建环境
|
||||
if: matrix.os == 'windows'
|
||||
run: |
|
||||
echo "Windows runner 已包含构建工具"
|
||||
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 应用
|
||||
- name: 构建 Wails 应用
|
||||
run: |
|
||||
wails3 build -platform ${{ matrix.os }}/${{ matrix.arch }}
|
||||
shell: bash
|
||||
|
||||
# 查找构建产物
|
||||
- name: 查找构建产物
|
||||
id: find_binary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" = "windows" ]; then
|
||||
BINARY=$(find build/bin -name "*.exe" -type f | head -n 1)
|
||||
elif [ "${{ matrix.os }}" = "darwin" ]; then
|
||||
# macOS 可能生成 .app 包或二进制文件
|
||||
BINARY=$(find build/bin -type f \( -name "*.app" -o ! -name ".*" \) | head -n 1)
|
||||
else
|
||||
BINARY=$(find build/bin -type f ! -name ".*" | head -n 1)
|
||||
fi
|
||||
echo "binary_path=$BINARY" >> $GITHUB_OUTPUT
|
||||
echo "找到的二进制文件: $BINARY"
|
||||
|
||||
# 重命名构建产物
|
||||
- name: 重命名构建产物
|
||||
shell: bash
|
||||
run: |
|
||||
BINARY_PATH="${{ steps.find_binary.outputs.binary_path }}"
|
||||
if [ -n "$BINARY_PATH" ]; then
|
||||
# 对于 macOS .app 包,打包为 zip
|
||||
if [[ "$BINARY_PATH" == *.app ]]; then
|
||||
cd "$(dirname "$BINARY_PATH")"
|
||||
zip -r "${{ matrix.output_name }}.zip" "$(basename "$BINARY_PATH")"
|
||||
echo "ARTIFACT_PATH=$(pwd)/${{ matrix.output_name }}.zip" >> $GITHUB_ENV
|
||||
else
|
||||
cp "$BINARY_PATH" "${{ matrix.output_name }}"
|
||||
echo "ARTIFACT_PATH=$(pwd)/${{ matrix.output_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
# 上传构建产物到 Artifacts
|
||||
- name: 上传构建产物
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.output_name }}
|
||||
path: ${{ env.ARTIFACT_PATH }}
|
||||
if-no-files-found: error
|
||||
|
||||
# 创建 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
@@ -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
@@ -5,3 +5,5 @@ frontend/node_modules
|
||||
build/linux/appimage/build
|
||||
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||
.idea
|
||||
frontend/docs/.vitepress/cache/
|
||||
frontend/docs/.vitepress/dist/
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 0 B |
@@ -1 +0,0 @@
|
||||
voidraft.landaiqing.cn
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB |
256
docs/index.html
@@ -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>
|
||||
@@ -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(/>\s*(.*?)(?=>|$)/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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -62,19 +62,4 @@ export class ServiceOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export class WebviewWindow {
|
||||
|
||||
/** Creates a new WebviewWindow instance. */
|
||||
constructor($$source: Partial<WebviewWindow> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WebviewWindow instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WebviewWindow {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new WebviewWindow($$parsedSource as Partial<WebviewWindow>);
|
||||
}
|
||||
}
|
||||
export type Window = any;
|
||||
|
||||
4
frontend/bindings/net/http/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
14
frontend/bindings/net/http/models.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* A Header represents the key-value pairs in an HTTP header.
|
||||
*
|
||||
* The keys should be in canonical form, as returned by
|
||||
* [CanonicalHeaderKey].
|
||||
*/
|
||||
export type Header = { [_: string]: string[] };
|
||||
4
frontend/bindings/time/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
51
frontend/bindings/time/models.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* A Time represents an instant in time with nanosecond precision.
|
||||
*
|
||||
* Programs using times should typically store and pass them as values,
|
||||
* not pointers. That is, time variables and struct fields should be of
|
||||
* type [time.Time], not *time.Time.
|
||||
*
|
||||
* A Time value can be used by multiple goroutines simultaneously except
|
||||
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
|
||||
* [Time.UnmarshalText] are not concurrency-safe.
|
||||
*
|
||||
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
|
||||
* The [Time.Sub] method subtracts two instants, producing a [Duration].
|
||||
* The [Time.Add] method adds a Time and a Duration, producing a Time.
|
||||
*
|
||||
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
|
||||
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
|
||||
* a simple way of detecting a time that has not been initialized explicitly.
|
||||
*
|
||||
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
|
||||
* Time with a specific Location. Changing the Location of a Time value with
|
||||
* these methods does not change the actual instant it represents, only the time
|
||||
* zone in which to interpret it.
|
||||
*
|
||||
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
|
||||
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
|
||||
* but not the location name. They therefore lose information about Daylight Saving Time.
|
||||
*
|
||||
* In addition to the required “wall clock” reading, a Time may contain an optional
|
||||
* reading of the current process's monotonic clock, to provide additional precision
|
||||
* for comparison or subtraction.
|
||||
* See the “Monotonic Clocks” section in the package documentation for details.
|
||||
*
|
||||
* Note that the Go == operator compares not just the time instant but also the
|
||||
* Location and the monotonic clock reading. Therefore, Time values should not
|
||||
* be used as map or database keys without first guaranteeing that the
|
||||
* identical Location has been set for all values, which can be achieved
|
||||
* through use of the UTC or Local method, and that the monotonic clock reading
|
||||
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
|
||||
* to t == u, since t.Equal uses the most accurate comparison available and
|
||||
* correctly handles the case when only one of its arguments has a monotonic
|
||||
* clock reading.
|
||||
*/
|
||||
export type Time = any;
|
||||
@@ -10,10 +10,17 @@
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* Get 获取配置项
|
||||
*/
|
||||
@@ -34,22 +41,6 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetConfigDir 获取配置目录
|
||||
*/
|
||||
export function GetConfigDir(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2275626561) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetSettingsPath 获取设置文件路径
|
||||
*/
|
||||
export function GetSettingsPath(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2175583370) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrateConfig 执行配置迁移
|
||||
*/
|
||||
@@ -74,6 +65,14 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3311949428, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 设置配置项
|
||||
*/
|
||||
@@ -83,34 +82,18 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
||||
}
|
||||
|
||||
/**
|
||||
* SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||
* Watch 注册配置变更监听器
|
||||
*/
|
||||
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3264871659, callback) as any;
|
||||
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||
* WatchWithContext 使用 Context 注册监听器
|
||||
*/
|
||||
export function SetDataPathChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(393017412, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetHotkeyChangeCallback 设置热键配置变更回调
|
||||
*/
|
||||
export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(283872321, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
|
||||
*/
|
||||
export function SetWindowSnapConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2324961653, callback) as any;
|
||||
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* HotkeyService Windows全局热键服务
|
||||
* HotkeyService 全局热键服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -48,8 +48,8 @@ export function IsRegistered(): Promise<boolean> & { cancel(): void } {
|
||||
/**
|
||||
* RegisterHotkey 注册全局热键
|
||||
*/
|
||||
export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1103945691, hotkey) as any;
|
||||
export function RegisterHotkey(combo: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1103945691, combo) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3079990808, options) as any;
|
||||
@@ -80,8 +80,8 @@ export function UnregisterHotkey(): Promise<void> & { cancel(): void } {
|
||||
/**
|
||||
* UpdateHotkey 更新热键配置
|
||||
*/
|
||||
export function UpdateHotkey(enable: boolean, hotkey: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(823285555, enable, hotkey) as any;
|
||||
export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(823285555, enable, combo) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* HttpClientService HTTP客户端服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* ExecuteRequest 执行HTTP请求
|
||||
*/
|
||||
export function ExecuteRequest(request: $models.HttpRequest | null): Promise<$models.HttpResponse | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3143343977, request) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.HttpResponse.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
@@ -8,6 +8,7 @@ import * as DialogService from "./dialogservice.js";
|
||||
import * as DocumentService from "./documentservice.js";
|
||||
import * as ExtensionService from "./extensionservice.js";
|
||||
import * as HotkeyService from "./hotkeyservice.js";
|
||||
import * as HttpClientService from "./httpclientservice.js";
|
||||
import * as KeyBindingService from "./keybindingservice.js";
|
||||
import * as MigrationService from "./migrationservice.js";
|
||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||
@@ -18,7 +19,6 @@ import * as ThemeService from "./themeservice.js";
|
||||
import * as TranslationService from "./translationservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as WindowService from "./windowservice.js";
|
||||
import * as WindowSnapService from "./windowsnapservice.js";
|
||||
export {
|
||||
BackupService,
|
||||
ConfigService,
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
DocumentService,
|
||||
ExtensionService,
|
||||
HotkeyService,
|
||||
HttpClientService,
|
||||
KeyBindingService,
|
||||
MigrationService,
|
||||
SelfUpdateService,
|
||||
@@ -36,8 +37,7 @@ export {
|
||||
ThemeService,
|
||||
TranslationService,
|
||||
TrayService,
|
||||
WindowService,
|
||||
WindowSnapService
|
||||
WindowService
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
||||
|
||||
@@ -7,7 +7,118 @@ 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";
|
||||
import * as http$0 from "../../../net/http/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* HttpRequest HTTP请求结构
|
||||
*/
|
||||
export class HttpRequest {
|
||||
"method": string;
|
||||
"url": string;
|
||||
"headers": { [_: string]: string };
|
||||
|
||||
/**
|
||||
* json, formdata, urlencoded, text, params, xml, html, javascript, binary
|
||||
*/
|
||||
"bodyType"?: string;
|
||||
"body"?: any;
|
||||
|
||||
/** Creates a new HttpRequest instance. */
|
||||
constructor($$source: Partial<HttpRequest> = {}) {
|
||||
if (!("method" in $$source)) {
|
||||
this["method"] = "";
|
||||
}
|
||||
if (!("url" in $$source)) {
|
||||
this["url"] = "";
|
||||
}
|
||||
if (!("headers" in $$source)) {
|
||||
this["headers"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new HttpRequest instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): HttpRequest {
|
||||
const $$createField2_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("headers" in $$parsedSource) {
|
||||
$$parsedSource["headers"] = $$createField2_0($$parsedSource["headers"]);
|
||||
}
|
||||
return new HttpRequest($$parsedSource as Partial<HttpRequest>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpResponse HTTP响应结构
|
||||
*/
|
||||
export class HttpResponse {
|
||||
/**
|
||||
* 使用resp.Status()返回完整状态如"200 OK"
|
||||
*/
|
||||
"status": string;
|
||||
|
||||
/**
|
||||
* 响应时间(毫秒)
|
||||
*/
|
||||
"time": number;
|
||||
|
||||
/**
|
||||
* 请求大小
|
||||
*/
|
||||
"requestSize": string;
|
||||
"body": any;
|
||||
"headers": http$0.Header;
|
||||
"timestamp": time$0.Time;
|
||||
"error"?: any;
|
||||
|
||||
/** Creates a new HttpResponse instance. */
|
||||
constructor($$source: Partial<HttpResponse> = {}) {
|
||||
if (!("status" in $$source)) {
|
||||
this["status"] = "";
|
||||
}
|
||||
if (!("time" in $$source)) {
|
||||
this["time"] = 0;
|
||||
}
|
||||
if (!("requestSize" in $$source)) {
|
||||
this["requestSize"] = "";
|
||||
}
|
||||
if (!("body" in $$source)) {
|
||||
this["body"] = null;
|
||||
}
|
||||
if (!("headers" in $$source)) {
|
||||
this["headers"] = ({} as http$0.Header);
|
||||
}
|
||||
if (!("timestamp" in $$source)) {
|
||||
this["timestamp"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new HttpResponse instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): HttpResponse {
|
||||
const $$createField4_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("headers" in $$parsedSource) {
|
||||
$$parsedSource["headers"] = $$createField4_0($$parsedSource["headers"]);
|
||||
}
|
||||
return new HttpResponse($$parsedSource as Partial<HttpResponse>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MemoryStats 内存统计信息
|
||||
@@ -155,6 +266,11 @@ export class OSInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
|
||||
/**
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
@@ -273,8 +389,8 @@ export class SystemInfo {
|
||||
* Creates a new SystemInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SystemInfo {
|
||||
const $$createField3_0 = $$createType1;
|
||||
const $$createField4_0 = $$createType2;
|
||||
const $$createField3_0 = $$createType5;
|
||||
const $$createField4_0 = $$createType6;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("osInfo" in $$parsedSource) {
|
||||
$$parsedSource["osInfo"] = $$createField3_0($$parsedSource["osInfo"]);
|
||||
@@ -286,65 +402,16 @@ 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 = $$createType4;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("Window" in $$parsedSource) {
|
||||
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||
}
|
||||
return new WindowInfo($$parsedSource as Partial<WindowInfo>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowSnapService 窗口吸附服务
|
||||
*/
|
||||
export class WindowSnapService {
|
||||
|
||||
/** Creates a new WindowSnapService instance. */
|
||||
constructor($$source: Partial<WindowSnapService> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WindowSnapService instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowSnapService {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = OSInfo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType3 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType4 = $Create.Nullable($$createType3);
|
||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||
var $$createType1 = (function $$initCreateType1(...args): any {
|
||||
if ($$createType1 === $$initCreateType1) {
|
||||
$$createType1 = $$createType3;
|
||||
}
|
||||
return $$createType1(...args);
|
||||
});
|
||||
const $$createType2 = $Create.Array($Create.Any);
|
||||
const $$createType3 = $Create.Map($Create.Any, $$createType2);
|
||||
const $$createType4 = OSInfo.createFrom;
|
||||
const $$createType5 = $Create.Nullable($$createType4);
|
||||
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* AutoShowHide 自动显示/隐藏主窗口
|
||||
*/
|
||||
export function AutoShowHide(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4044219428) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandleWindowClose 处理窗口关闭事件
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* WindowService 窗口管理服务(专注于窗口生命周期管理)
|
||||
* WindowService 窗口管理服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -12,15 +12,15 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* GetOpenWindows 获取所有打开的窗口信息
|
||||
* GetOpenWindows 获取所有打开的文档窗口
|
||||
*/
|
||||
export function GetOpenWindows(): Promise<$models.WindowInfo[]> & { cancel(): void } {
|
||||
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1464997251) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
@@ -51,13 +51,12 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* SetWindowSnapService 设置窗口吸附服务引用
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.WindowInfo.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* WindowSnapService 窗口吸附服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* Cleanup 清理资源
|
||||
*/
|
||||
export function Cleanup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2155505498) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
|
||||
*/
|
||||
export function GetCurrentThreshold(): Promise<number> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3176419026) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* OnWindowSnapConfigChanged 处理窗口吸附配置变更
|
||||
*/
|
||||
export function OnWindowSnapConfigChanged(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3794787039, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterWindow 注册需要吸附管理的窗口
|
||||
*/
|
||||
export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1000222723, documentID, window, title) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 实现服务关闭接口
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1172710495) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2456823262, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetSnapEnabled 设置是否启用窗口吸附
|
||||
*/
|
||||
export function SetSnapEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2280126835, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnregisterWindow 取消注册窗口
|
||||
*/
|
||||
export function UnregisterWindow(documentID: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2844230768, documentID) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
5
frontend/components.d.ts
vendored
@@ -1,8 +1,11 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
|
||||
130
frontend/docs/.vitepress/config.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
6
frontend/docs/.vitepress/theme/index.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@import "style/var.css";
|
||||
@import "style/blur.css";
|
||||
@import "style/badge.css";
|
||||
@import "style/grid.css";
|
||||
|
||||
|
||||
17
frontend/docs/.vitepress/theme/index.ts
Normal 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
|
||||
21
frontend/docs/.vitepress/theme/style/badge.css
Normal 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);
|
||||
}
|
||||
73
frontend/docs/.vitepress/theme/style/blur.css
Normal 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;
|
||||
}
|
||||
}
|
||||
40
frontend/docs/.vitepress/theme/style/grid.css
Normal 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;
|
||||
}
|
||||
|
||||
137
frontend/docs/.vitepress/theme/style/var.css
Normal 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;
|
||||
}
|
||||
163
frontend/docs/src/guide/features.md
Normal 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
|
||||
|
||||
107
frontend/docs/src/guide/getting-started.md
Normal 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
|
||||
|
||||
63
frontend/docs/src/guide/installation.md
Normal 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)
|
||||
|
||||
50
frontend/docs/src/guide/introduction.md
Normal 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)
|
||||
|
||||
56
frontend/docs/src/index.md
Normal 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.
|
||||
|
||||
---
|
||||
BIN
frontend/docs/src/public/icon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/docs/src/public/icon/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
frontend/docs/src/public/icon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
3
frontend/docs/src/public/icon/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
21
frontend/docs/src/public/icon/site.webmanifest
Normal 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"
|
||||
}
|
||||
BIN
frontend/docs/src/public/icon/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/docs/src/public/icon/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
frontend/docs/src/public/img/hero.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
163
frontend/docs/src/zh/guide/features.md
Normal 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 按键绑定(计划中)
|
||||
- 快速命令面板
|
||||
- 上下文感知快捷键
|
||||
|
||||
## 性能
|
||||
|
||||
专为速度而构建:
|
||||
|
||||
- 快速启动时间
|
||||
- 流畅滚动
|
||||
- 高效内存使用
|
||||
- 支持大文件
|
||||
|
||||
## 隐私与安全
|
||||
|
||||
你的数据是安全的:
|
||||
|
||||
- 本地优先存储
|
||||
- 可选云备份
|
||||
- 无遥测或跟踪
|
||||
- 开源代码库
|
||||
|
||||
107
frontend/docs/src/zh/guide/getting-started.md
Normal 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)
|
||||
|
||||
63
frontend/docs/src/zh/guide/installation.md
Normal 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)
|
||||
|
||||
50
frontend/docs/src/zh/guide/introduction.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 简介
|
||||
|
||||
欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具。
|
||||
|
||||
## 什么是 voidraft?
|
||||
|
||||
voidraft 是一个现代化的桌面应用程序,帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 块状编辑模式
|
||||
|
||||
voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
|
||||
- 不同的编程语言设置
|
||||
- 语法高亮
|
||||
- 独立格式化
|
||||
- 轻松在块之间导航
|
||||
|
||||
### 开发者工具
|
||||
|
||||
- **HTTP 客户端**:直接在编辑器中测试 API
|
||||
- **代码格式化**:内置 Prettier 支持多种语言
|
||||
- **语法高亮**:支持 30+ 种编程语言
|
||||
- **自动语言检测**:自动识别代码块语言类型
|
||||
|
||||
### 自定义
|
||||
|
||||
- **自定义主题**:创建并保存你自己的编辑器主题
|
||||
- **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
|
||||
- **多窗口**:同时处理多个文档
|
||||
|
||||
### 数据管理
|
||||
|
||||
- **Git 备份**:使用 Git 仓库自动备份
|
||||
- **云同步**:跨设备同步你的数据
|
||||
- **自动更新**:及时获取最新功能
|
||||
|
||||
## 为什么选择 voidraft?
|
||||
|
||||
- **专注开发者**:考虑开发者需求而构建
|
||||
- **现代技术栈**:使用前沿技术(Wails3、Vue 3、CodeMirror 6)
|
||||
- **跨平台**:支持 Windows(macOS 和 Linux 支持计划中)
|
||||
- **开源**:MIT 许可证,社区驱动开发
|
||||
|
||||
## 开始使用
|
||||
|
||||
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
|
||||
|
||||
下一步:[安装 →](/zh/guide/installation)
|
||||
|
||||
56
frontend/docs/src/zh/index.md
Normal 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 构建。跨平台桌面应用,原生性能,现代化界面。
|
||||
|
||||
---
|
||||
1665
frontend/package-lock.json
generated
@@ -10,11 +10,20 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint",
|
||||
"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",
|
||||
"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": {
|
||||
"@codemirror/autocomplete": "^6.19.0",
|
||||
"@codemirror/commands": "^6.9.0",
|
||||
"@codemirror/autocomplete": "^6.19.1",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
@@ -26,7 +35,7 @@
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-lezer": "^6.0.2",
|
||||
"@codemirror/lang-liquid": "^6.3.0",
|
||||
"@codemirror/lang-markdown": "^6.4.0",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
@@ -36,14 +45,14 @@
|
||||
"@codemirror/lang-wast": "^6.0.2",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.0",
|
||||
"@codemirror/lint": "^6.9.1",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
@@ -57,35 +66,37 @@
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
"jsox": "^1.2.123",
|
||||
"linguist-languages": "^9.0.0",
|
||||
"linguist-languages": "^9.1.0",
|
||||
"php-parser": "^3.2.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.93.2",
|
||||
"sass": "^1.93.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.10",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vitest": "^4.0.6",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.1"
|
||||
"vue-tsc": "^3.1.2"
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ function animationLoop() {
|
||||
// 等待一段时间后重置动画
|
||||
resetTimeoutId = window.setTimeout(() => {
|
||||
reset();
|
||||
}, 750);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,8 @@ onBeforeUnmount(() => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--voidraft-bg-gradient, radial-gradient(#222922, #000500));
|
||||
//background: var(--voidraft-bg-gradient, rgba(0, 5, 0, 0.15));
|
||||
//backdrop-filter: blur(2px);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,175 +1,49 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, readonly, ref, shallowRef, watchEffect, onScopeDispose } from 'vue';
|
||||
import type { GitBackupConfig } from '@/../bindings/voidraft/internal/models';
|
||||
import { ref, onScopeDispose } from 'vue';
|
||||
import { BackupService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||
|
||||
// 备份状态枚举
|
||||
export enum BackupStatus {
|
||||
IDLE = 'idle',
|
||||
PUSHING = 'pushing',
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
// 备份操作结果类型
|
||||
export interface BackupResult {
|
||||
status: BackupStatus;
|
||||
message?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// 类型守卫函数
|
||||
const isBackupError = (error: unknown): error is Error => {
|
||||
return error instanceof Error;
|
||||
};
|
||||
|
||||
// 工具类型:提取错误消息
|
||||
type ErrorMessage<T> = T extends Error ? string : string;
|
||||
|
||||
|
||||
export const useBackupStore = defineStore('backup', () => {
|
||||
// === 核心状态 ===
|
||||
const config = shallowRef<GitBackupConfig | null>(null);
|
||||
const isPushing = ref(false);
|
||||
const message = ref<string | null>(null);
|
||||
const isError = ref(false);
|
||||
|
||||
// 统一的备份结果状态
|
||||
const backupResult = ref<BackupResult>({
|
||||
status: BackupStatus.IDLE
|
||||
});
|
||||
|
||||
// === 定时器管理 ===
|
||||
const statusTimer = createTimerManager();
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onScopeDispose(() => {
|
||||
statusTimer.clear();
|
||||
});
|
||||
|
||||
// === 外部依赖 ===
|
||||
const timer = createTimerManager();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// === 计算属性 ===
|
||||
const isEnabled = computed(() => configStore.config.backup.enabled);
|
||||
const isConfigured = computed(() => Boolean(configStore.config.backup.repo_url?.trim()));
|
||||
onScopeDispose(() => timer.clear());
|
||||
|
||||
// 派生状态计算属性
|
||||
const isPushing = computed(() => backupResult.value.status === BackupStatus.PUSHING);
|
||||
const isSuccess = computed(() => backupResult.value.status === BackupStatus.SUCCESS);
|
||||
const isError = computed(() => backupResult.value.status === BackupStatus.ERROR);
|
||||
const errorMessage = computed(() =>
|
||||
backupResult.value.status === BackupStatus.ERROR ? backupResult.value.message : null
|
||||
);
|
||||
const pushToRemote = async () => {
|
||||
const isConfigured = Boolean(configStore.config.backup.repo_url?.trim());
|
||||
|
||||
// === 状态管理方法 ===
|
||||
|
||||
/**
|
||||
* 设置备份状态
|
||||
* @param status 备份状态
|
||||
* @param message 可选消息
|
||||
* @param autoHide 是否自动隐藏(毫秒)
|
||||
*/
|
||||
const setBackupStatus = <T extends BackupStatus>(
|
||||
status: T,
|
||||
message?: T extends BackupStatus.ERROR ? string : string,
|
||||
autoHide?: number
|
||||
): void => {
|
||||
statusTimer.clear();
|
||||
|
||||
backupResult.value = {
|
||||
status,
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 自动隐藏逻辑
|
||||
if (autoHide && (status === BackupStatus.SUCCESS || status === BackupStatus.ERROR)) {
|
||||
statusTimer.set(() => {
|
||||
if (backupResult.value.status === status) {
|
||||
backupResult.value = { status: BackupStatus.IDLE };
|
||||
}
|
||||
}, autoHide);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除当前状态
|
||||
*/
|
||||
const clearStatus = (): void => {
|
||||
statusTimer.clear();
|
||||
backupResult.value = { status: BackupStatus.IDLE };
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理错误的通用方法
|
||||
*/
|
||||
const handleError = (error: unknown): void => {
|
||||
const message: ErrorMessage<typeof error> = isBackupError(error)
|
||||
? error.message
|
||||
: 'Backup operation failed';
|
||||
|
||||
setBackupStatus(BackupStatus.ERROR, message, 5000);
|
||||
};
|
||||
|
||||
// === 业务逻辑方法 ===
|
||||
|
||||
/**
|
||||
* 推送到远程仓库
|
||||
* 使用现代 async/await 和错误处理
|
||||
*/
|
||||
const pushToRemote = async (): Promise<void> => {
|
||||
// 前置条件检查
|
||||
if (isPushing.value || !isConfigured.value) {
|
||||
if (isPushing.value || !isConfigured) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setBackupStatus(BackupStatus.PUSHING);
|
||||
isPushing.value = true;
|
||||
message.value = null;
|
||||
timer.clear();
|
||||
|
||||
await BackupService.PushToRemote();
|
||||
|
||||
setBackupStatus(BackupStatus.SUCCESS, 'Backup completed successfully', 3000);
|
||||
isError.value = false;
|
||||
message.value = 'push successful';
|
||||
timer.set(() => { message.value = null; }, 3000);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
isError.value = true;
|
||||
message.value = error instanceof Error ? error.message : 'backup operation failed';
|
||||
timer.set(() => { message.value = null; }, 5000);
|
||||
} finally {
|
||||
isPushing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重试备份操作
|
||||
*/
|
||||
const retryBackup = async (): Promise<void> => {
|
||||
if (isError.value) {
|
||||
await pushToRemote();
|
||||
}
|
||||
};
|
||||
|
||||
// === 响应式副作用 ===
|
||||
|
||||
// 监听配置变化,自动清除错误状态
|
||||
watchEffect(() => {
|
||||
if (isEnabled.value && isConfigured.value && isError.value) {
|
||||
// 配置修复后清除错误状态
|
||||
clearStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// === 返回的 API ===
|
||||
return {
|
||||
// 只读状态
|
||||
config: readonly(config),
|
||||
backupResult: readonly(backupResult),
|
||||
|
||||
// 计算属性
|
||||
isEnabled,
|
||||
isConfigured,
|
||||
isPushing,
|
||||
isSuccess,
|
||||
message,
|
||||
isError,
|
||||
errorMessage,
|
||||
|
||||
// 方法
|
||||
pushToRemote,
|
||||
retryBackup,
|
||||
clearStatus
|
||||
} as const;
|
||||
pushToRemote
|
||||
};
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {computed, ref} from 'vue';
|
||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useTabStore} from "@/stores/tabStore";
|
||||
|
||||
export const useDocumentStore = defineStore('document', () => {
|
||||
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
|
||||
@@ -72,6 +73,11 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
await OpenDocumentWindow(docId);
|
||||
const tabStore = useTabStore();
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
||||
tabStore.closeTab(docId);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to open document in new window:', error);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {AsyncManager} from '@/common/utils/asyncManager';
|
||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
|
||||
import {EDITOR_CONFIG} from '@/common/constant/editor';
|
||||
import {createHttpClientExtension} from "@/views/editor/extensions/httpclient";
|
||||
|
||||
export interface DocumentStats {
|
||||
lines: number;
|
||||
@@ -154,6 +155,8 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
enableAutoDetection: true
|
||||
});
|
||||
|
||||
const httpExtension = createHttpClientExtension();
|
||||
|
||||
// 再次检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
@@ -185,7 +188,8 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
statsExtension,
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
...dynamicExtensions
|
||||
...dynamicExtensions,
|
||||
...httpExtension
|
||||
];
|
||||
|
||||
// 创建编辑器状态
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, readonly, ref, shallowRef, onScopeDispose } from 'vue';
|
||||
import { computed, readonly, ref, onScopeDispose } from 'vue';
|
||||
import { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
||||
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
|
||||
import { useConfigStore } from './configStore';
|
||||
|
||||
@@ -55,9 +55,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
|
||||
<div ref="editorElement" class="editor"></div>
|
||||
<Toolbar/>
|
||||
<transition name="loading-fade">
|
||||
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -85,4 +87,15 @@ onBeforeUnmount(() => {
|
||||
:deep(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// 加载动画过渡效果
|
||||
.loading-fade-enter-active,
|
||||
.loading-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-fade-enter-from,
|
||||
.loading-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -26,20 +26,20 @@ try {
|
||||
|
||||
// 运行 lezer-generator
|
||||
console.log('⚙️ building parser...');
|
||||
execSync('npx lezer-generator codeblock.grammar -o parser.js', {
|
||||
execSync('npx lezer-generator codeblock.grammar -o parser.ts --typeScript', {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// 检查生成的文件
|
||||
const parserFile = path.join(__dirname, 'parser.js');
|
||||
const termsFile = path.join(__dirname, 'parser.terms.js');
|
||||
const parserFile = path.join(__dirname, 'parser.ts');
|
||||
const termsFile = path.join(__dirname, 'parser.terms.ts');
|
||||
|
||||
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
||||
console.log('✅ parser file successfully generated!');
|
||||
console.log('📦 parser files:');
|
||||
console.log(' - parser.js');
|
||||
console.log(' - parser.terms.js');
|
||||
console.log(' - parser.ts');
|
||||
console.log(' - parser.terms.ts');
|
||||
} else {
|
||||
throw new Error('failed to generate parser');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 提供多语言代码块支持
|
||||
*/
|
||||
|
||||
import { parser } from "./parser.js";
|
||||
import { parser } from "./parser";
|
||||
import { configureNesting } from "./nested-parser";
|
||||
|
||||
import {
|
||||
|
||||
@@ -17,7 +17,8 @@ BlockLanguage {
|
||||
"css" | "xml" | "cpp" | "rs" | "cs" | "rb" | "sh" | "yaml" | "toml" |
|
||||
"go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" |
|
||||
"ps1" | "dart" | "scala" | "math" | "dockerfile" | "lua" | "vue" | "lezer" |
|
||||
"liquid" | "wast" | "sass" | "less" | "angular" | "svelte"
|
||||
"liquid" | "wast" | "sass" | "less" | "angular" | "svelte" |
|
||||
"http" | "mermaid"
|
||||
}
|
||||
|
||||
@tokens {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ExternalTokenizer } from "@lezer/lr";
|
||||
import { BlockContent } from "./parser.terms.js";
|
||||
import { BlockContent } from "./parser.terms";
|
||||
import { LANGUAGES } from "./languages";
|
||||
|
||||
const EOF = -1;
|
||||
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
} from './nested-parser';
|
||||
|
||||
// 解析器术语
|
||||
export * from './parser.terms.js';
|
||||
export * from './parser.terms';
|
||||
|
||||
// 外部标记器
|
||||
export {
|
||||
@@ -34,4 +34,4 @@ export {
|
||||
// 解析器
|
||||
export {
|
||||
parser
|
||||
} from './parser.js';
|
||||
} from './parser';
|
||||
@@ -23,7 +23,8 @@ import {sassLanguage} from "@codemirror/lang-sass";
|
||||
import {lessLanguage} from "@codemirror/lang-less";
|
||||
import {angularLanguage} from "@codemirror/lang-angular";
|
||||
import { svelteLanguage } from "@replit/codemirror-lang-svelte";
|
||||
|
||||
import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language";
|
||||
import { mermaidLanguage } from '@/views/editor/language/mermaid';
|
||||
import {StreamLanguage} from "@codemirror/language";
|
||||
import {ruby} from "@codemirror/legacy-modes/mode/ruby";
|
||||
import {shell} from "@codemirror/legacy-modes/mode/shell";
|
||||
@@ -85,7 +86,7 @@ export class LanguageInfo {
|
||||
* 支持的语言列表
|
||||
*/
|
||||
export const LANGUAGES: LanguageInfo[] = [
|
||||
new LanguageInfo("text", "Plain Text", null),
|
||||
new LanguageInfo("text", "Text", null),
|
||||
new LanguageInfo("json", "JSON", jsonLanguage.parser, ["json"], {
|
||||
parser: "json",
|
||||
plugins: [babelPrettierPlugin, prettierPluginEstree]
|
||||
@@ -224,6 +225,8 @@ export const LANGUAGES: LanguageInfo[] = [
|
||||
filename: "index.svelte"
|
||||
}
|
||||
}),
|
||||
new LanguageInfo("http", "Http", httpLanguage.parser, ["http"]),
|
||||
new LanguageInfo("mermaid", "Mermaid", mermaidLanguage.parser, ["mermaid"]),
|
||||
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { parseMixed } from "@lezer/common";
|
||||
import { BlockContent, BlockLanguage } from "./parser.terms.js";
|
||||
import { BlockContent, BlockLanguage } from "./parser.terms";
|
||||
import { languageMapping } from "./languages";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {blockContent} from "./external-tokens.js"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!jQQOQOOOVOQO'#C`O#uOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#zOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$SOSO1G.fOOOP7+$Q7+$Q",
|
||||
stateData: "$X~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO~OPVO~OUYO!SXO~O!SZO~O",
|
||||
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
|
||||
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
|
||||
maxTerm: 50,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "3g~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_(s#_#`)r#`#a)}#a#b+z#d#e,k#f#g-d#g#h-w#h#i0n#j#k1s#k#l2U#l#m2m#m#n3OR!fP!SQ%&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~(eP#a#b(h~(kP#`#a(n~(sO]~~(vQ#T#U(|#g#h)_~)PP#j#k)S~)VP#T#U)Y~)_O`~~)dPo~#c#d)g~)jP#b#c)m~)rOZ~~)uP#h#i)x~)}Or~~*QR#X#Y*Z#]#^+Q#i#j+o~*^Q#g#h*d#n#o*o~*gP#g#h*j~*oO!P~~*rP#X#Y*u~*xP#f#g*{~+QO{~~+TP#e#f+W~+ZP#i#j+^~+aP#]#^+d~+gP#W#X+j~+oO|~~+rP#T#U+u~+zOy~~+}Q#T#U,T#W#X,f~,WP#h#i,Z~,^P#[#],a~,fOw~~,kO_~~,nR#[#],w#g#h-S#m#n-_~,zP#d#e,}~-SOa~~-VP!R!S-Y~-_Ot~~-dO[~~-gQ#U#V-m#g#h-r~-rOg~~-wOe~~-zU#T#U.^#V#W.o#[#]/W#e#f/]#j#k/h#k#l0V~.aP#g#h.d~.gP#g#h.j~.oO!O~~.rP#T#U.u~.xP#`#a.{~/OP#T#U/R~/WOv~~/]Oh~~/`P#`#a/c~/hO^~~/kP#X#Y/n~/qP#`#a/t~/wP#h#i/z~/}P#X#Y0Q~0VO!R~~0YP#]#^0]~0`P#Y#Z0c~0fP#h#i0i~0nOq~~0qR#X#Y0z#c#d1]#g#h1n~0}P#l#m1Q~1TP#h#i1W~1]OY~~1`P#a#b1c~1fP#`#a1i~1nOj~~1sOp~~1vP#i#j1y~1|P#X#Y2P~2UOz~~2XP#T#U2[~2_P#g#h2b~2eP#h#i2h~2mO}~~2pP#a#b2s~2vP#`#a2y~3OOc~~3RP#T#U3U~3XP#a#b3[~3_P#`#a3b~3gOi~",
|
||||
tokenizers: [blockContent, 0, 1],
|
||||
topRules: {"Document":[0,2]},
|
||||
tokenPrec: 0
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {blockContent} from "./external-tokens.js"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!jQQOQOOOVOQO'#C`O#{OPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO$QOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$YOSO1G.fOOOP7+$Q7+$Q",
|
||||
stateData: "$_~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO!TTO~OPVO~OUYO!UXO~O!UZO~O",
|
||||
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
|
||||
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
|
||||
maxTerm: 52,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "4m~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_)R#_#`*Q#`#a*]#a#b,Y#d#e-q#f#g.j#g#h.}#h#i1t#j#k2y#k#l3[#l#m3s#m#n4UR!fP!UQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eQ#a#b(k#h#i(v~(nP#`#a(q~(vO]~~(yP#d#e(|~)RO!S~~)UQ#T#U)[#g#h)m~)_P#j#k)b~)eP#T#U)h~)mO`~~)rPo~#c#d)u~)xP#b#c){~*QOZ~~*TP#h#i*W~*]Or~~*`R#X#Y*i#]#^+`#i#j+}~*lQ#g#h*r#n#o*}~*uP#g#h*x~*}O!P~~+QP#X#Y+T~+WP#f#g+Z~+`O{~~+cP#e#f+f~+iP#i#j+l~+oP#]#^+r~+uP#W#X+x~+}O|~~,QP#T#U,T~,YOy~~,]R#T#U,f#W#X,w#X#Y,|~,iP#h#i,l~,oP#[#],r~,wOw~~,|O_~~-PP#f#g-S~-VP#a#b-Y~-]P#T#U-`~-cP#]#^-f~-iP#W#X-l~-qO!T~~-tR#[#]-}#g#h.Y#m#n.e~.QP#d#e.T~.YOa~~.]P!R!S.`~.eOt~~.jO[~~.mQ#U#V.s#g#h.x~.xOg~~.}Oe~~/QU#T#U/d#V#W/u#[#]0^#e#f0c#j#k0n#k#l1]~/gP#g#h/j~/mP#g#h/p~/uO!O~~/xP#T#U/{~0OP#`#a0R~0UP#T#U0X~0^Ov~~0cOh~~0fP#`#a0i~0nO^~~0qP#X#Y0t~0wP#`#a0z~0}P#h#i1Q~1TP#X#Y1W~1]O!R~~1`P#]#^1c~1fP#Y#Z1i~1lP#h#i1o~1tOq~~1wR#X#Y2Q#c#d2c#g#h2t~2TP#l#m2W~2ZP#h#i2^~2cOY~~2fP#a#b2i~2lP#`#a2o~2tOj~~2yOp~~2|P#i#j3P~3SP#X#Y3V~3[Oz~~3_P#T#U3b~3eP#g#h3h~3kP#h#i3n~3sO}~~3vP#a#b3y~3|P#`#a4P~4UOc~~4XP#T#U4[~4_P#a#b4b~4eP#`#a4h~4mOi~",
|
||||
tokenizers: [blockContent, 0, 1],
|
||||
topRules: {"Document":[0,2]},
|
||||
tokenPrec: 0
|
||||
})
|
||||
@@ -27,6 +27,11 @@ export const blockState = StateField.define<Block[]>({
|
||||
* 获取当前活动的块
|
||||
*/
|
||||
export function getActiveNoteBlock(state: EditorState): Block | undefined {
|
||||
// 检查 blockState 字段是否存在
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 找到光标所在的块
|
||||
const range = state.selection.asSingle().ranges[0];
|
||||
return state.field(blockState).find(block =>
|
||||
@@ -38,6 +43,9 @@ export function getActiveNoteBlock(state: EditorState): Block | undefined {
|
||||
* 获取第一个块
|
||||
*/
|
||||
export function getFirstNoteBlock(state: EditorState): Block | undefined {
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
return state.field(blockState)[0];
|
||||
}
|
||||
|
||||
@@ -45,6 +53,9 @@ export function getFirstNoteBlock(state: EditorState): Block | undefined {
|
||||
* 获取最后一个块
|
||||
*/
|
||||
export function getLastNoteBlock(state: EditorState): Block | undefined {
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
const blocks = state.field(blockState);
|
||||
return blocks[blocks.length - 1];
|
||||
}
|
||||
@@ -53,6 +64,9 @@ export function getLastNoteBlock(state: EditorState): Block | undefined {
|
||||
* 根据位置获取块
|
||||
*/
|
||||
export function getNoteBlockFromPos(state: EditorState, pos: number): Block | undefined {
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
return state.field(blockState).find(block =>
|
||||
block.range.from <= pos && block.range.to >= pos
|
||||
);
|
||||
|
||||
@@ -65,6 +65,8 @@ export type SupportedLanguage =
|
||||
| 'less'
|
||||
| 'angular'
|
||||
| 'svelte'
|
||||
| 'http' // HTTP Client
|
||||
| 'mermaid'
|
||||
|
||||
/**
|
||||
* 创建块的选项
|
||||
@@ -84,7 +86,6 @@ export interface EditorOptions {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 分隔符格式常量
|
||||
export const DELIMITER_REGEX = /^\n∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/gm;
|
||||
export const DELIMITER_PREFIX = '\n∞∞∞';
|
||||
|
||||
20
frontend/src/views/editor/extensions/httpclient/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* HTTP Client 扩展
|
||||
*/
|
||||
|
||||
import {Extension} from '@codemirror/state';
|
||||
|
||||
import {httpRequestsField, httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter';
|
||||
|
||||
/**
|
||||
* 创建 HTTP Client 扩展
|
||||
*/
|
||||
export function createHttpClientExtension(): Extension[] {
|
||||
return [
|
||||
httpRequestsField,
|
||||
httpRunButtonGutter,
|
||||
httpRunButtonTheme,
|
||||
] as Extension[];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* HTTP Grammar Parser Builder
|
||||
* 编译 Lezer grammar 文件为 TypeScript parser
|
||||
* 使用 --typeScript 选项生成 .ts 文件
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
console.log('🚀 开始编译 HTTP grammar parser (TypeScript)...');
|
||||
|
||||
try {
|
||||
// 检查语法文件是否存在
|
||||
const grammarFile = path.join(__dirname, 'http.grammar');
|
||||
if (!fs.existsSync(grammarFile)) {
|
||||
throw new Error('语法文件 http.grammar 未找到');
|
||||
}
|
||||
|
||||
console.log('📄 语法文件:', grammarFile);
|
||||
|
||||
// 运行 lezer-generator with TypeScript output
|
||||
console.log('⚙️ 编译 parser (生成 TypeScript)...');
|
||||
execSync('npx lezer-generator http.grammar -o http.parser.ts --typeScript', {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// 检查生成的文件
|
||||
const parserFile = path.join(__dirname, 'http.parser.ts');
|
||||
const termsFile = path.join(__dirname, 'http.parser.terms.ts');
|
||||
|
||||
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
||||
console.log('✅ Parser 文件成功生成!');
|
||||
console.log('📦 生成的文件:');
|
||||
console.log(' - http.parser.ts');
|
||||
console.log(' - http.parser.terms.ts');
|
||||
} else {
|
||||
throw new Error('Parser 生成失败');
|
||||
}
|
||||
|
||||
console.log('🎉 编译成功!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 编译失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
|
||||
import { parser } from './http.parser';
|
||||
import { httpHighlighting } from './http.highlight';
|
||||
|
||||
/**
|
||||
* HTTP Client 语言定义
|
||||
*/
|
||||
|
||||
// 配置折叠规则和高亮
|
||||
const httpParserWithMetadata = parser.configure({
|
||||
props: [
|
||||
// 应用语法高亮
|
||||
httpHighlighting,
|
||||
|
||||
// 折叠规则:允许折叠块结构
|
||||
foldNodeProp.add({
|
||||
RequestStatement: foldInside,
|
||||
Block: foldInside,
|
||||
AtRule: foldInside,
|
||||
Document: foldInside,
|
||||
}),
|
||||
|
||||
// 缩进规则
|
||||
indentNodeProp.add({
|
||||
Block: () => 2,
|
||||
Declaration: () => 0,
|
||||
AtRule: () => 0,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 创建 LR 语言实例
|
||||
export const httpLanguage = LRLanguage.define({
|
||||
parser: httpParserWithMetadata,
|
||||
languageData: {
|
||||
//自动闭合括号
|
||||
closeBrackets: { brackets: ['(', '[', '{', '"', "'"] },
|
||||
// 单词字符定义
|
||||
wordChars: '-_',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* HTTP Client 语言支持
|
||||
*/
|
||||
export function http() {
|
||||
return new LanguageSupport(httpLanguage);
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
// HTTP Client Grammar
|
||||
//
|
||||
// 语法规则:
|
||||
// 1. HTTP 头部属性:逗号可选
|
||||
// host: "example.com"
|
||||
// content-type: "application/json"
|
||||
//
|
||||
// 2. 请求体格式:
|
||||
// @json - JSON 格式(属性必须用逗号分隔)
|
||||
// @formdata - 表单数据(属性必须用逗号分隔)
|
||||
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
|
||||
// @text - 纯文本内容
|
||||
// @params - URL 参数(用于 GET 请求)
|
||||
// @xml - XML 格式(固定 key: xml)
|
||||
// @html - HTML 格式(固定 key: html)
|
||||
// @javascript - JavaScript 格式(固定 key: javascript)
|
||||
// @binary - 二进制文件(固定 key: binary,值格式:@file 路径)
|
||||
//
|
||||
// 3. 变量定义:
|
||||
// @var {
|
||||
// baseUrl: "https://api.example.com",
|
||||
// version: "v1",
|
||||
// timeout: 30000
|
||||
// }
|
||||
//
|
||||
// 4. 变量引用:
|
||||
// {{variableName}} - 简单引用
|
||||
// {{variableName:default}} - 带默认值引用
|
||||
//
|
||||
// 5. 响应数据:
|
||||
// 使用独立的 JSON 块
|
||||
// # Response 200 OK 234ms
|
||||
// { "code": 200, "message": "success" }
|
||||
//
|
||||
// 6. 注释:
|
||||
// # 单行注释
|
||||
//
|
||||
// 示例 1 - JSON 请求:
|
||||
// POST "http://api.example.com/users" {
|
||||
// content-type: "application/json"
|
||||
//
|
||||
// @json {
|
||||
// name: "张三",
|
||||
// age: 25,
|
||||
// email: "zhangsan@example.com"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 2 - FormData 请求:
|
||||
// POST "http://api.example.com/upload" {
|
||||
// content-type: "multipart/form-data"
|
||||
//
|
||||
// @formdata {
|
||||
// file: "avatar.png",
|
||||
// username: "zhangsan"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 3 - URLEncoded 请求:
|
||||
// POST "http://api.example.com/login" {
|
||||
// content-type: "application/x-www-form-urlencoded"
|
||||
//
|
||||
// @urlencoded {
|
||||
// username: "admin",
|
||||
// password: "123456"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 4 - 纯文本请求:
|
||||
// POST "http://api.example.com/webhook" {
|
||||
// content-type: "text/plain"
|
||||
//
|
||||
// @text {
|
||||
// content: "纯文本内容"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 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" {
|
||||
// @json {
|
||||
// username: "admin",
|
||||
// password: "123456"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Response 200 OK 234ms 2025-10-11 10:30:25
|
||||
// {
|
||||
// "code": 200,
|
||||
// "message": "登录成功",
|
||||
// "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
||||
// "data": {
|
||||
// "userId": 1001,
|
||||
// "username": "admin"
|
||||
// }
|
||||
// }
|
||||
|
||||
@skip { whitespace | LineComment }
|
||||
|
||||
@top Document { item* }
|
||||
|
||||
item {
|
||||
VarDeclaration |
|
||||
RequestStatement |
|
||||
ResponseDeclaration |
|
||||
AtRule |
|
||||
JsonObject |
|
||||
JsonArray
|
||||
}
|
||||
|
||||
// 变量声明
|
||||
VarDeclaration {
|
||||
@specialize[@name=VarKeyword]<AtKeyword, "@var">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// 响应声明
|
||||
// 格式:@response <status> <time>ms <timestamp> { <json> }
|
||||
// 示例:@response 200 123ms 2025-10-31T10:30:31 { "data": "..." }
|
||||
// 错误:@response error 0ms 2025-10-31T10:30:31 { "error": "..." }
|
||||
ResponseDeclaration {
|
||||
@specialize[@name=ResponseKeyword]<AtKeyword, "@response">
|
||||
ResponseStatus
|
||||
ResponseTime
|
||||
ResponseTimestamp
|
||||
ResponseBlock
|
||||
}
|
||||
|
||||
// 响应状态:数字或数字-标识符组合(200 或 200-OK)或 "error" 关键字
|
||||
ResponseStatus {
|
||||
(NumberLiteral ("-" identifier)?) |
|
||||
@specialize[@name=ErrorStatus]<identifier, "error">
|
||||
}
|
||||
|
||||
// 响应时间:直接使用 TimeValue token
|
||||
ResponseTime {
|
||||
TimeValue
|
||||
}
|
||||
|
||||
// 响应时间戳:ISO 8601 格式字符串
|
||||
// 格式:2025-10-31T10:30:31
|
||||
ResponseTimestamp {
|
||||
Timestamp
|
||||
}
|
||||
|
||||
// 响应块:标准 JSON 对象或数组(支持带引号的 key)
|
||||
ResponseBlock {
|
||||
JsonObject | JsonArray
|
||||
}
|
||||
|
||||
// HTTP 请求 - URL 必须是字符串
|
||||
RequestStatement {
|
||||
Method Url Block
|
||||
}
|
||||
|
||||
Method {
|
||||
@specialize[@name=GET]<identifier, "GET"> |
|
||||
@specialize[@name=POST]<identifier, "POST"> |
|
||||
@specialize[@name=PUT]<identifier, "PUT"> |
|
||||
@specialize[@name=DELETE]<identifier, "DELETE"> |
|
||||
@specialize[@name=PATCH]<identifier, "PATCH"> |
|
||||
@specialize[@name=HEAD]<identifier, "HEAD"> |
|
||||
@specialize[@name=OPTIONS]<identifier, "OPTIONS"> |
|
||||
@specialize[@name=CONNECT]<identifier, "CONNECT"> |
|
||||
@specialize[@name=TRACE]<identifier, "TRACE">
|
||||
}
|
||||
|
||||
// URL 必须是字符串
|
||||
Url { StringLiteral }
|
||||
|
||||
// @ 规则(支持多种请求体格式,后面可选逗号)
|
||||
AtRule {
|
||||
(JsonRule |
|
||||
FormDataRule |
|
||||
UrlEncodedRule |
|
||||
TextRule |
|
||||
ParamsRule |
|
||||
XmlRule |
|
||||
HtmlRule |
|
||||
JavaScriptRule |
|
||||
BinaryRule) ","?
|
||||
}
|
||||
|
||||
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
||||
JsonRule {
|
||||
@specialize[@name=JsonKeyword]<AtKeyword, "@json">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// @formdata 块:表单数据格式(属性必须用逗号分隔)
|
||||
FormDataRule {
|
||||
@specialize[@name=FormDataKeyword]<AtKeyword, "@formdata">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// @urlencoded 块:URL 编码格式(属性必须用逗号分隔)
|
||||
UrlEncodedRule {
|
||||
@specialize[@name=UrlEncodedKeyword]<AtKeyword, "@urlencoded">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// @text 块:纯文本请求体(使用 content 字段)
|
||||
TextRule {
|
||||
@specialize[@name=TextKeyword]<AtKeyword, "@text">
|
||||
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 {
|
||||
"{" blockContent? "}"
|
||||
}
|
||||
|
||||
// 块内容:
|
||||
// - 选项1: 只有属性
|
||||
// - 选项2: 属性 + 请求体
|
||||
// - 选项3: 属性 + 请求体 + 属性
|
||||
blockContent {
|
||||
Property+ | Property* AtRule Property*
|
||||
}
|
||||
|
||||
// HTTP 属性(逗号可选)
|
||||
Property {
|
||||
PropertyName { identifier }
|
||||
":" value ","?
|
||||
}
|
||||
|
||||
// JSON 块结构(属性必须用逗号分隔)
|
||||
JsonBlock {
|
||||
"{" jsonBlockContent? "}"
|
||||
}
|
||||
|
||||
jsonBlockContent {
|
||||
JsonProperty ("," JsonProperty)* ","?
|
||||
}
|
||||
|
||||
// JSON 属性
|
||||
JsonProperty {
|
||||
PropertyName { identifier }
|
||||
":" 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 {
|
||||
numberLiteralInner Unit?
|
||||
}
|
||||
|
||||
// HTTP 属性值(支持块嵌套和变量引用)
|
||||
value {
|
||||
StringLiteral |
|
||||
NumberLiteral |
|
||||
VariableRef |
|
||||
Block |
|
||||
identifier
|
||||
}
|
||||
|
||||
// JSON 属性值(严格的 JSON 语法:字符串必须用引号,支持变量引用)
|
||||
jsonValue {
|
||||
StringLiteral |
|
||||
NumberLiteral |
|
||||
VariableRef |
|
||||
JsonBlock |
|
||||
JsonTrue |
|
||||
JsonFalse |
|
||||
JsonNull
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 独立 JSON 语法(用于响应数据)
|
||||
// ===============================
|
||||
|
||||
// JSON 对象(独立的 JSON 块,不需要 @ 前缀)
|
||||
JsonObject {
|
||||
"{" jsonObjectContent? "}"
|
||||
}
|
||||
|
||||
jsonObjectContent {
|
||||
JsonMember ("," JsonMember)* ","?
|
||||
}
|
||||
|
||||
// JSON 成员(支持字符串键名和标识符键名)
|
||||
JsonMember {
|
||||
(StringLiteral | identifier) ":" JsonValue
|
||||
}
|
||||
|
||||
// JSON 数组
|
||||
JsonArray {
|
||||
"[" jsonArrayContent? "]"
|
||||
}
|
||||
|
||||
jsonArrayContent {
|
||||
JsonValue ("," JsonValue)* ","?
|
||||
}
|
||||
|
||||
// JSON 值(完整的 JSON 值类型,支持变量引用)
|
||||
JsonValue {
|
||||
StringLiteral |
|
||||
NumberLiteral |
|
||||
VariableRef |
|
||||
JsonObject |
|
||||
JsonArray |
|
||||
JsonTrue |
|
||||
JsonFalse |
|
||||
JsonNull
|
||||
}
|
||||
|
||||
// JSON 字面量
|
||||
JsonTrue { @specialize[@name=True]<identifier, "true"> }
|
||||
JsonFalse { @specialize[@name=False]<identifier, "false"> }
|
||||
JsonNull { @specialize[@name=Null]<identifier, "null"> }
|
||||
|
||||
// Tokens
|
||||
@tokens {
|
||||
// 单行注释(# 开头到行尾)
|
||||
LineComment { "#" ![\n]* }
|
||||
|
||||
AtKeyword { "@" "-"? @asciiLetter (@asciiLetter | @digit | "-")* }
|
||||
|
||||
// 变量引用: {{variableName}} 或 {{variableName:default}} 或 {{obj.nested.path}}
|
||||
VariableRef[isolate] {
|
||||
"{{"
|
||||
(@asciiLetter | $[_$]) (@asciiLetter | @digit | $[-_$] | ".")*
|
||||
(":" (![\n}] | "}" ![}])*)?
|
||||
"}}"
|
||||
}
|
||||
|
||||
// 标识符(属性名,支持连字符)
|
||||
identifier {
|
||||
(@asciiLetter | $[_$])
|
||||
(@asciiLetter | @digit | $[-_$])*
|
||||
}
|
||||
|
||||
// 单位(必须跟在数字后面,所以不单独匹配)
|
||||
Unit { @asciiLetter+ }
|
||||
|
||||
// 时间戳:ISO 8601 格式(YYYY-MM-DDTHH:MM:SS)
|
||||
Timestamp[isolate] {
|
||||
@digit @digit @digit @digit "-" @digit @digit "-" @digit @digit
|
||||
"T" @digit @digit ":" @digit @digit ":" @digit @digit
|
||||
}
|
||||
|
||||
// 时间值:数字 + ms,作为一个整体 token
|
||||
TimeValue[isolate] {
|
||||
@digit+ "ms"
|
||||
}
|
||||
|
||||
whitespace { @whitespace+ }
|
||||
|
||||
@precedence { Timestamp, TimeValue, numberLiteralInner, VariableRef, identifier, Unit }
|
||||
|
||||
numberLiteralInner {
|
||||
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
||||
(("e" | "E") ("+" | "-")? @digit+)?
|
||||
}
|
||||
|
||||
StringLiteral[isolate] {
|
||||
"\"" (!["\n\\] | "\\" _)* "\"" |
|
||||
"'" (!['\n\\] | "\\" _)* "'"
|
||||
}
|
||||
|
||||
":" ","
|
||||
|
||||
"{" "}"
|
||||
|
||||
"[" "]"
|
||||
}
|
||||
|
||||
@external propSource httpHighlighting from "./http.highlight"
|
||||
|
||||
@detectDelim
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { httpLanguage } from './index';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
/**
|
||||
* 创建测试用的 EditorState
|
||||
*/
|
||||
function createTestState(content: string): EditorState {
|
||||
return EditorState.create({
|
||||
doc: content,
|
||||
extensions: [httpLanguage]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否存在
|
||||
*/
|
||||
function hasNode(state: EditorState, nodeName: string): boolean {
|
||||
const tree = syntaxTree(state);
|
||||
let found = false;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === nodeName) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点文本
|
||||
*/
|
||||
function getNodeText(state: EditorState, nodeName: string): string | null {
|
||||
const tree = syntaxTree(state);
|
||||
let text: string | null = null;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === nodeName) {
|
||||
text = state.doc.sliceString(node.from, node.to);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
describe('HTTP Grammar - @response 响应语法', () => {
|
||||
|
||||
it('✅ 成功响应 - 完整格式', () => {
|
||||
const content = `@response 200 123ms 2025-10-31T10:30:31 {
|
||||
"message": "success",
|
||||
"data": [1, 2, 3]
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseStatus')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseTime')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseTimestamp')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseBlock')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 错误响应 - error 关键字', () => {
|
||||
const content = `@response error 0ms 2025-10-31T10:30:31 {
|
||||
"error": "Network timeout"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'ErrorStatus')).toBe(true);
|
||||
expect(hasNode(state, 'TimeValue')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 响应与请求结合', () => {
|
||||
const content = `GET "https://api.example.com/users" {}
|
||||
|
||||
@response 200 123ms 2025-10-31T10:30:31 {
|
||||
"users": [
|
||||
{ "id": 1, "name": "Alice" },
|
||||
{ "id": 2, "name": "Bob" }
|
||||
]
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'RequestStatement')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 多个请求和响应', () => {
|
||||
const content = `GET "https://api.example.com/users" {}
|
||||
@response 200 100ms 2025-10-31T10:30:31 {
|
||||
"users": []
|
||||
}
|
||||
|
||||
POST "https://api.example.com/users" {
|
||||
@json { "name": "Alice" }
|
||||
}
|
||||
@response 201 50ms 2025-10-31T10:30:32 {
|
||||
"id": 1,
|
||||
"name": "Alice"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
// 统计 ResponseDeclaration 数量
|
||||
let responseCount = 0;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === 'ResponseDeclaration') {
|
||||
responseCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(responseCount).toBe(2);
|
||||
});
|
||||
|
||||
it('✅ 响应状态码类型', () => {
|
||||
const testCases = [
|
||||
{ status: '200', shouldParse: true },
|
||||
{ status: '201', shouldParse: true },
|
||||
{ status: '404', shouldParse: true },
|
||||
{ status: '500', shouldParse: true },
|
||||
{ status: 'error', shouldParse: true }
|
||||
];
|
||||
|
||||
testCases.forEach(({ status, shouldParse }) => {
|
||||
const content = `@response ${status} 0ms 2025-10-31T10:30:31 { "data": null }`;
|
||||
const state = createTestState(content);
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(shouldParse);
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 响应时间单位', () => {
|
||||
const content = `@response 200 12345ms 2025-10-31T10:30:31 {
|
||||
"data": "test"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'TimeValue')).toBe(true);
|
||||
expect(getNodeText(state, 'TimeValue')).toBe('12345ms');
|
||||
});
|
||||
|
||||
it('✅ 响应块包含复杂 JSON', () => {
|
||||
const content = `@response 200 150ms 2025-10-31T10:30:31 {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Bob",
|
||||
"email": "bob@example.com",
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'JsonObject')).toBe(true);
|
||||
expect(hasNode(state, 'JsonArray')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 空响应体', () => {
|
||||
const content = `@response 204 50ms 2025-10-31T10:30:31 {}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseBlock')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 响应体为数组', () => {
|
||||
const content = `@response 200 80ms 2025-10-31T10:30:31 [
|
||||
{ "id": 1, "name": "Alice" },
|
||||
{ "id": 2, "name": "Bob" }
|
||||
]`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'JsonArray')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 时间戳格式', () => {
|
||||
const testCases = [
|
||||
'2025-10-31T10:30:31',
|
||||
'2025-01-01T00:00:00',
|
||||
'2025-12-31T23:59:59'
|
||||
];
|
||||
|
||||
testCases.forEach(timestamp => {
|
||||
const content = `@response 200 100ms ${timestamp} { "data": null }`;
|
||||
const state = createTestState(content);
|
||||
expect(hasNode(state, 'ResponseTimestamp')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('❌ 缺少必填字段应该有错误', () => {
|
||||
const invalidCases = [
|
||||
'@response 200 { "data": null }', // 缺少时间和时间戳
|
||||
'@response 200 100ms { "data": null }', // 缺少时间戳
|
||||
];
|
||||
|
||||
invalidCases.forEach(content => {
|
||||
const state = createTestState(content);
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
// 检查是否有错误节点
|
||||
let hasError = false;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === '⚠') {
|
||||
hasError = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 与变量结合', () => {
|
||||
const content = `@var {
|
||||
apiUrl: "https://api.example.com"
|
||||
}
|
||||
|
||||
GET "https://api.example.com/users" {}
|
||||
|
||||
@response 200 123ms 2025-10-31T10:30:31 {
|
||||
"users": []
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'VarDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'RequestStatement')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parser } from './http.parser';
|
||||
|
||||
/**
|
||||
* HTTP 变量功能测试
|
||||
*
|
||||
* 测试变量定义 @var 和变量引用 {{variableName}} 语法
|
||||
*/
|
||||
describe('HTTP 变量功能测试', () => {
|
||||
|
||||
/**
|
||||
* 辅助函数:解析代码并返回语法树
|
||||
*/
|
||||
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('✅ @var - 变量声明', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ @var 变量声明格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
|
||||
// 验证是否有 VarDeclaration 节点
|
||||
let hasVarDeclaration = false;
|
||||
let hasVarKeyword = false;
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === 'VarDeclaration') hasVarDeclaration = true;
|
||||
if (node.name === 'VarKeyword') hasVarKeyword = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasVarDeclaration).toBe(true);
|
||||
expect(hasVarKeyword).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 变量引用 - 在属性值中使用', () => {
|
||||
const code = `GET "http://example.com" {
|
||||
timeout: {{timeout}}
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
|
||||
// 验证是否有 VariableRef 节点
|
||||
let hasVariableRef = false;
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === 'VariableRef') hasVariableRef = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasVariableRef).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 变量引用 - 带默认值 {{variableName:default}}', () => {
|
||||
const code = `GET "http://example.com" {
|
||||
authorization: "Bearer {{token:default-token}}"
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 完整示例 - 变量定义和使用(用户提供的示例)', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/{{version}}/users" {
|
||||
timeout: {{timeout}},
|
||||
authorization: "Bearer {{token:default-token}}"
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量在 JSON 请求体中使用', () => {
|
||||
const code = `@var {
|
||||
userId: "12345",
|
||||
userName: "张三"
|
||||
}
|
||||
|
||||
POST "http://api.example.com/users" {
|
||||
@json {
|
||||
id: {{userId}},
|
||||
name: {{userName}},
|
||||
email: "{{userName}}@example.com"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ JSON 中使用变量格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量在多个请求中使用', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
token: "Bearer abc123"
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/users" {
|
||||
authorization: {{token}}
|
||||
}
|
||||
|
||||
POST "{{baseUrl}}/users" {
|
||||
authorization: {{token}},
|
||||
|
||||
@json {
|
||||
name: "test"
|
||||
}
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ URL 中包含多个变量', () => {
|
||||
const code = `GET "{{protocol}}://{{host}}:{{port}}/{{path}}/{{resource}}" {
|
||||
host: "example.com"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ URL 多变量格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量名包含下划线和数字', () => {
|
||||
const code = `@var {
|
||||
api_base_url_v2: "https://api.example.com",
|
||||
timeout_30s: 30000,
|
||||
user_id_123: "123"
|
||||
}
|
||||
|
||||
GET "{{api_base_url_v2}}/users/{{user_id_123}}" {
|
||||
timeout: {{timeout_30s}}
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量默认值包含特殊字符', () => {
|
||||
const code = `GET "http://example.com" {
|
||||
authorization: "Bearer {{token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0}}"
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 混合变量和普通值', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1"
|
||||
}
|
||||
|
||||
POST "{{baseUrl}}/{{version}}/users" {
|
||||
content-type: "application/json",
|
||||
authorization: "Bearer {{token:default}}",
|
||||
user-agent: "Mozilla/5.0",
|
||||
|
||||
@json {
|
||||
name: "张三",
|
||||
age: 25,
|
||||
apiUrl: {{baseUrl}},
|
||||
apiVersion: {{version}}
|
||||
}
|
||||
}`;
|
||||
|
||||
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}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { styleTags, tags as t } from "@lezer/highlight"
|
||||
|
||||
/**
|
||||
* HTTP Client 语法高亮配置
|
||||
*/
|
||||
export const httpHighlighting = styleTags({
|
||||
// ========== HTTP 方法(使用不同的强调程度)==========
|
||||
// 查询方法 - 使用普通关键字
|
||||
"GET HEAD OPTIONS": t.keyword,
|
||||
|
||||
// 修改方法 - 使用控制关键字
|
||||
"POST PUT PATCH": t.controlKeyword,
|
||||
|
||||
// 删除方法 - 使用操作符
|
||||
"DELETE": t.operatorKeyword,
|
||||
|
||||
// 其他方法 - 使用修饰关键字
|
||||
"TRACE CONNECT": t.modifier,
|
||||
|
||||
// ========== @ 规则(请求体格式和变量声明)==========
|
||||
// @json, @formdata, @urlencoded, @params - 使用类型名
|
||||
"JsonKeyword FormDataKeyword UrlEncodedKeyword ParamsKeyword": t.typeName,
|
||||
|
||||
// @text - 使用特殊类型
|
||||
"TextKeyword": t.special(t.typeName),
|
||||
|
||||
// @xml, @html, @javascript - 使用类型名
|
||||
"XmlKeyword HtmlKeyword JavaScriptKeyword": t.typeName,
|
||||
|
||||
// @binary - 使用特殊类型
|
||||
"BinaryKeyword": t.special(t.typeName),
|
||||
|
||||
// @var - 变量声明关键字
|
||||
"VarKeyword": t.definitionKeyword,
|
||||
|
||||
// @response - 响应关键字
|
||||
"ResponseKeyword": t.keyword,
|
||||
|
||||
// @ 符号本身 - 使用元标记
|
||||
"AtKeyword": t.meta,
|
||||
|
||||
// ========== 变量引用 ==========
|
||||
// {{variableName}} - 使用特殊变量名
|
||||
"VariableRef": t.special(t.definitionKeyword),
|
||||
|
||||
// ========== URL(特殊处理)==========
|
||||
// URL 节点 - 使用链接颜色
|
||||
"Url": t.link,
|
||||
|
||||
// ========== 属性和值 ==========
|
||||
// 属性名 - 使用定义名称
|
||||
"PropertyName": t.definition(t.attributeName),
|
||||
|
||||
// 普通标识符值 - 使用常量名
|
||||
"identifier": t.constant(t.variableName),
|
||||
|
||||
// ========== 字面量 ==========
|
||||
// 数字 - 数字颜色
|
||||
"NumberLiteral": t.number,
|
||||
|
||||
// 字符串 - 字符串颜色
|
||||
"StringLiteral": t.string,
|
||||
|
||||
// 单位 - 单位颜色
|
||||
"Unit": t.unit,
|
||||
|
||||
// ========== 响应相关 ==========
|
||||
// 响应状态码 - 数字颜色
|
||||
"ResponseStatus/NumberLiteral": t.number,
|
||||
"ResponseStatus/identifier": t.constant(t.variableName),
|
||||
|
||||
// 响应错误状态 - 关键字
|
||||
"ErrorStatus": t.operatorKeyword,
|
||||
|
||||
// 响应时间 - 数字和单位颜色
|
||||
"TimeValue": t.number,
|
||||
"ResponseTime/TimeValue": t.number,
|
||||
"TimeUnit": t.unit,
|
||||
|
||||
// 时间戳 - 字符串颜色
|
||||
"Timestamp": t.string,
|
||||
"ResponseTimestamp": t.string,
|
||||
|
||||
// ========== 注释 ==========
|
||||
// # 单行注释 - 行注释颜色
|
||||
"LineComment": t.lineComment,
|
||||
|
||||
// ========== JSON 语法(独立 JSON 块)==========
|
||||
// JSON 对象和数组
|
||||
"JsonObject": t.brace,
|
||||
"JsonArray": t.squareBracket,
|
||||
|
||||
// JSON 属性名 - 使用属性名颜色
|
||||
"JsonProperty/PropertyName": t.propertyName,
|
||||
"JsonProperty/StringLiteral": t.propertyName,
|
||||
|
||||
// JSON 成员(属性名)- 使用属性名颜色(适用于独立 JSON 对象)
|
||||
"JsonMember/StringLiteral": t.propertyName,
|
||||
"JsonMember/identifier": t.propertyName,
|
||||
|
||||
// JSON 字面量值
|
||||
"True False": t.bool,
|
||||
"Null": t.null,
|
||||
|
||||
// JSON 值(确保字符串和数字正确高亮)
|
||||
"JsonValue/StringLiteral": t.string,
|
||||
"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,
|
||||
|
||||
// 花括号 - 大括号
|
||||
"{ }": t.brace,
|
||||
|
||||
// 方括号 - 方括号
|
||||
"[ ]": t.squareBracket,
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
LineComment = 1,
|
||||
Document = 2,
|
||||
VarDeclaration = 3,
|
||||
AtKeyword = 4,
|
||||
VarKeyword = 5,
|
||||
JsonBlock = 8,
|
||||
JsonProperty = 9,
|
||||
StringLiteral = 12,
|
||||
NumberLiteral = 13,
|
||||
Unit = 14,
|
||||
VariableRef = 15,
|
||||
JsonTrue = 16,
|
||||
True = 17,
|
||||
JsonFalse = 18,
|
||||
False = 19,
|
||||
JsonNull = 20,
|
||||
Null = 21,
|
||||
RequestStatement = 23,
|
||||
Method = 24,
|
||||
GET = 25,
|
||||
POST = 26,
|
||||
PUT = 27,
|
||||
DELETE = 28,
|
||||
PATCH = 29,
|
||||
HEAD = 30,
|
||||
OPTIONS = 31,
|
||||
CONNECT = 32,
|
||||
TRACE = 33,
|
||||
Url = 34,
|
||||
Block = 35,
|
||||
Property = 36,
|
||||
AtRule = 38,
|
||||
JsonRule = 39,
|
||||
JsonKeyword = 40,
|
||||
FormDataRule = 41,
|
||||
FormDataKeyword = 42,
|
||||
UrlEncodedRule = 43,
|
||||
UrlEncodedKeyword = 44,
|
||||
TextRule = 45,
|
||||
TextKeyword = 46,
|
||||
ParamsRule = 47,
|
||||
ParamsKeyword = 48,
|
||||
XmlRule = 49,
|
||||
XmlKeyword = 50,
|
||||
XmlBlock = 51,
|
||||
XmlKey = 52,
|
||||
HtmlRule = 53,
|
||||
HtmlKeyword = 54,
|
||||
HtmlBlock = 55,
|
||||
HtmlKey = 56,
|
||||
JavaScriptRule = 57,
|
||||
JavaScriptKeyword = 58,
|
||||
JavaScriptBlock = 59,
|
||||
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
|
||||
@@ -0,0 +1,26 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {httpHighlighting} from "./http.highlight"
|
||||
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, xml:104, html:112, javascript:120, binary:128, error:136}
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
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: "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: "(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 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: 97,
|
||||
nodeProps: [
|
||||
["openedBy", 6,"{",77,"["],
|
||||
["closedBy", 7,"}",78,"]"],
|
||||
["isolate", -4,12,15,70,72,""]
|
||||
],
|
||||
propSources: [httpHighlighting],
|
||||
skippedNodes: [0,1,4],
|
||||
repeatNodeCount: 5,
|
||||
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, 1],
|
||||
topRules: {"Document":[0,2]},
|
||||
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: 694
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export { http, httpLanguage } from './http-language';
|
||||
export { parser } from './http.parser';
|
||||
export type { LRLanguage } from '@codemirror/language';
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { HttpRequestParser } from './request-parser';
|
||||
import { http } from '../language';
|
||||
|
||||
/**
|
||||
* 创建测试用的编辑器状态
|
||||
*/
|
||||
function createTestState(code: string): EditorState {
|
||||
return EditorState.create({
|
||||
doc: code,
|
||||
extensions: [http()],
|
||||
});
|
||||
}
|
||||
|
||||
describe('HttpRequestParser', () => {
|
||||
describe('基本请求解析', () => {
|
||||
it('应该解析简单的 GET 请求', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
content-type: "application/json"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('GET');
|
||||
expect(request?.url).toBe('http://api.example.com/users');
|
||||
expect(request?.headers).toEqual({
|
||||
host: 'api.example.com',
|
||||
'content-type': 'application/json',
|
||||
});
|
||||
expect(request?.bodyType).toBeUndefined();
|
||||
expect(request?.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该解析 POST 请求(带 JSON 请求体)', () => {
|
||||
const code = `POST "http://api.example.com/users" {
|
||||
content-type: "application/json"
|
||||
|
||||
@json {
|
||||
name: "张三",
|
||||
age: 25,
|
||||
email: "zhangsan@example.com"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('POST');
|
||||
expect(request?.url).toBe('http://api.example.com/users');
|
||||
expect(request?.headers).toEqual({
|
||||
'content-type': 'application/json',
|
||||
});
|
||||
expect(request?.bodyType).toBe('json');
|
||||
expect(request?.body).toEqual({
|
||||
name: '张三',
|
||||
age: 25,
|
||||
email: 'zhangsan@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 PUT 请求', () => {
|
||||
const code = `PUT "http://api.example.com/users/123" {
|
||||
authorization: "Bearer token123"
|
||||
|
||||
@json {
|
||||
name: "李四"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('PUT');
|
||||
expect(request?.url).toBe('http://api.example.com/users/123');
|
||||
});
|
||||
|
||||
it('应该解析 DELETE 请求', () => {
|
||||
const code = `DELETE "http://api.example.com/users/123" {
|
||||
authorization: "Bearer token123"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('DELETE');
|
||||
expect(request?.url).toBe('http://api.example.com/users/123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('请求体类型解析', () => {
|
||||
it('应该解析 @json 请求体', () => {
|
||||
const code = `POST "http://api.example.com/test" {
|
||||
@json {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('json');
|
||||
expect(request?.body).toEqual({
|
||||
username: 'admin',
|
||||
password: '123456',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 @formdata 请求体', () => {
|
||||
const code = `POST "http://api.example.com/upload" {
|
||||
@formdata {
|
||||
file: "avatar.png",
|
||||
description: "用户头像"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('formdata');
|
||||
expect(request?.body).toEqual({
|
||||
file: 'avatar.png',
|
||||
description: '用户头像',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 @urlencoded 请求体', () => {
|
||||
const code = `POST "http://api.example.com/login" {
|
||||
@urlencoded {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('urlencoded');
|
||||
expect(request?.body).toEqual({
|
||||
username: 'admin',
|
||||
password: '123456',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 @text 请求体', () => {
|
||||
const code = `POST "http://api.example.com/webhook" {
|
||||
@text {
|
||||
content: "纯文本内容"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('text');
|
||||
expect(request?.body).toEqual({
|
||||
content: '纯文本内容',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('复杂数据类型', () => {
|
||||
it('应该解析嵌套对象', () => {
|
||||
const code = `POST "http://api.example.com/users" {
|
||||
@json {
|
||||
user: {
|
||||
name: "张三",
|
||||
age: 25
|
||||
},
|
||||
settings: {
|
||||
theme: "dark"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.body).toEqual({
|
||||
user: {
|
||||
name: '张三',
|
||||
age: 25,
|
||||
},
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析布尔值', () => {
|
||||
const code = `POST "http://api.example.com/test" {
|
||||
@json {
|
||||
enabled: true,
|
||||
disabled: false
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.body).toEqual({
|
||||
enabled: true,
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析数字', () => {
|
||||
const code = `POST "http://api.example.com/test" {
|
||||
@json {
|
||||
count: 100,
|
||||
price: 19.99
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.body).toEqual({
|
||||
count: 100,
|
||||
price: 19.99,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headers 解析', () => {
|
||||
it('应该解析多个 headers', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
authorization: "Bearer token123"
|
||||
content-type: "application/json"
|
||||
user-agent: "Mozilla/5.0"
|
||||
accept: "application/json"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.headers).toEqual({
|
||||
host: 'api.example.com',
|
||||
authorization: 'Bearer token123',
|
||||
'content-type': 'application/json',
|
||||
'user-agent': 'Mozilla/5.0',
|
||||
accept: 'application/json',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持单引号字符串', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
user-agent: 'Mozilla/5.0'
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.headers['user-agent']).toBe('Mozilla/5.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('位置信息', () => {
|
||||
it('应该记录请求的位置信息', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.position).toBeDefined();
|
||||
expect(request?.position.line).toBe(1);
|
||||
expect(request?.position.from).toBe(0);
|
||||
expect(request?.position.to).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('解析不完整的请求应该返回 null', () => {
|
||||
const code = `GET {
|
||||
host: "test.com"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).toBeNull();
|
||||
});
|
||||
|
||||
it('解析无效位置应该返回 null', () => {
|
||||
const code = `GET "http://test.com" { }`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(1000); // 超出范围
|
||||
|
||||
expect(request).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('多个请求', () => {
|
||||
it('应该正确解析指定位置的请求', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
}
|
||||
|
||||
POST "http://api.example.com/users" {
|
||||
@json {
|
||||
name: "test"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
// 解析第一个请求
|
||||
const request1 = parser.parseRequestAt(0);
|
||||
expect(request1?.method).toBe('GET');
|
||||
|
||||
// 解析第二个请求(大概在 60+ 字符位置)
|
||||
const request2 = parser.parseRequestAt(70);
|
||||
expect(request2?.method).toBe('POST');
|
||||
expect(request2?.body).toEqual({ name: 'test' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
import { VariableResolver } from './variable-resolver';
|
||||
|
||||
/**
|
||||
* HTTP 请求模型
|
||||
*/
|
||||
export interface HttpRequest {
|
||||
/** 请求方法 */
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/** 请求 URL */
|
||||
url: string;
|
||||
|
||||
/** 请求头 */
|
||||
headers: Record<string, string>;
|
||||
|
||||
/** 请求体类型 */
|
||||
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text' | 'params' | 'xml' | 'html' | 'javascript' | 'binary';
|
||||
|
||||
/** 请求体内容 */
|
||||
body?: any;
|
||||
|
||||
/** 原始文本位置信息(用于调试) */
|
||||
position: {
|
||||
from: number;
|
||||
to: number;
|
||||
line: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型常量
|
||||
*/
|
||||
const NODE_TYPES = {
|
||||
REQUEST_STATEMENT: 'RequestStatement',
|
||||
METHOD: 'Method',
|
||||
URL: 'Url',
|
||||
BLOCK: 'Block',
|
||||
PROPERTY: 'Property',
|
||||
PROPERTY_NAME: 'PropertyName',
|
||||
STRING_LITERAL: 'StringLiteral',
|
||||
NUMBER_LITERAL: 'NumberLiteral',
|
||||
IDENTIFIER: 'identifier',
|
||||
AT_RULE: 'AtRule',
|
||||
JSON_RULE: 'JsonRule',
|
||||
FORMDATA_RULE: 'FormDataRule',
|
||||
URLENCODED_RULE: 'UrlEncodedRule',
|
||||
TEXT_RULE: 'TextRule',
|
||||
PARAMS_RULE: 'ParamsRule',
|
||||
XML_RULE: 'XmlRule',
|
||||
HTML_RULE: 'HtmlRule',
|
||||
JAVASCRIPT_RULE: 'JavaScriptRule',
|
||||
BINARY_RULE: 'BinaryRule',
|
||||
JSON_KEYWORD: 'JsonKeyword',
|
||||
FORMDATA_KEYWORD: 'FormDataKeyword',
|
||||
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
||||
TEXT_KEYWORD: 'TextKeyword',
|
||||
PARAMS_KEYWORD: 'ParamsKeyword',
|
||||
XML_KEYWORD: 'XmlKeyword',
|
||||
HTML_KEYWORD: 'HtmlKeyword',
|
||||
JAVASCRIPT_KEYWORD: 'JavaScriptKeyword',
|
||||
BINARY_KEYWORD: 'BinaryKeyword',
|
||||
JSON_BLOCK: 'JsonBlock',
|
||||
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',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTTP 请求解析器
|
||||
*/
|
||||
export class HttpRequestParser {
|
||||
private variableResolver: VariableResolver | null = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param state EditorState
|
||||
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
|
||||
*/
|
||||
constructor(
|
||||
private state: EditorState,
|
||||
private blockRange?: { from: number; to: number }
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建变量解析器(懒加载)
|
||||
*/
|
||||
private getVariableResolver(): VariableResolver {
|
||||
if (!this.variableResolver) {
|
||||
this.variableResolver = new VariableResolver(this.state, this.blockRange);
|
||||
}
|
||||
return this.variableResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定位置的 HTTP 请求
|
||||
* @param pos 光标位置或请求起始位置
|
||||
* @returns 解析后的 HTTP 请求对象,如果解析失败返回 null
|
||||
*/
|
||||
parseRequestAt(pos: number): HttpRequest | null {
|
||||
const tree = syntaxTree(this.state);
|
||||
|
||||
// 查找包含该位置的 RequestStatement 节点
|
||||
const requestNode = this.findRequestNode(tree, pos);
|
||||
if (!requestNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.parseRequest(requestNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找包含指定位置的 RequestStatement 节点
|
||||
*/
|
||||
private findRequestNode(tree: any, pos: number): SyntaxNode | null {
|
||||
let foundNode: SyntaxNode | null = null;
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === NODE_TYPES.REQUEST_STATEMENT) {
|
||||
if (node.from <= pos && pos <= node.to) {
|
||||
foundNode = node.node;
|
||||
return false; // 停止迭代
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return foundNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 RequestStatement 节点
|
||||
*/
|
||||
private parseRequest(node: SyntaxNode): HttpRequest | null {
|
||||
// 使用 Lezer API 直接获取子节点
|
||||
const methodNode = node.getChild(NODE_TYPES.METHOD);
|
||||
const urlNode = node.getChild(NODE_TYPES.URL);
|
||||
const blockNode = node.getChild(NODE_TYPES.BLOCK);
|
||||
|
||||
// 验证必需节点
|
||||
if (!methodNode || !urlNode || !blockNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const method = this.getNodeText(methodNode).toUpperCase();
|
||||
const url = this.parseUrl(urlNode);
|
||||
|
||||
// 验证 URL 非空
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
let bodyType: HttpRequest['bodyType'] = undefined;
|
||||
let body: any = undefined;
|
||||
|
||||
// 解析 Block
|
||||
this.parseBlock(blockNode, headers, (type, content) => {
|
||||
bodyType = type;
|
||||
body = content;
|
||||
});
|
||||
|
||||
const line = this.state.doc.lineAt(node.from);
|
||||
|
||||
return {
|
||||
method: method as HttpRequest['method'],
|
||||
url,
|
||||
headers,
|
||||
bodyType,
|
||||
body,
|
||||
position: {
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
line: line.number,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 URL 节点
|
||||
*/
|
||||
private parseUrl(node: SyntaxNode): string {
|
||||
const urlText = this.getNodeText(node);
|
||||
// 移除引号
|
||||
const url = urlText.replace(/^["']|["']$/g, '');
|
||||
// 替换变量
|
||||
return this.getVariableResolver().replaceVariables(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Block 节点(包含 headers 和 body)
|
||||
*/
|
||||
private parseBlock(
|
||||
node: SyntaxNode,
|
||||
headers: Record<string, string>,
|
||||
onBody: (type: HttpRequest['bodyType'], content: any) => void
|
||||
): void {
|
||||
// 遍历 Block 的子节点
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name === NODE_TYPES.PROPERTY) {
|
||||
// HTTP Header 属性
|
||||
const { name, value } = this.parseProperty(child);
|
||||
if (name && value !== null) {
|
||||
headers[name] = value;
|
||||
}
|
||||
} else if (child.name === NODE_TYPES.AT_RULE) {
|
||||
// AtRule 节点,直接获取第一个子节点(JsonRule, FormDataRule等)
|
||||
const ruleChild = child.firstChild;
|
||||
if (ruleChild) {
|
||||
const { type, content } = this.parseBodyRule(ruleChild);
|
||||
if (type) { // 只有有效的类型才处理
|
||||
onBody(type, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析请求体规则
|
||||
*/
|
||||
private parseBodyRule(node: SyntaxNode): { type: HttpRequest['bodyType']; content: any } {
|
||||
// 类型映射表
|
||||
const typeMap: Record<string, HttpRequest['bodyType']> = {
|
||||
[NODE_TYPES.JSON_RULE]: 'json',
|
||||
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
||||
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
||||
[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];
|
||||
|
||||
// 根据不同的规则类型解析不同的块
|
||||
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 {
|
||||
type,
|
||||
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)
|
||||
*/
|
||||
private parseJsonBlock(node: SyntaxNode): any {
|
||||
const result: any = {};
|
||||
|
||||
// 遍历 JsonProperty
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name === NODE_TYPES.JSON_PROPERTY) {
|
||||
const { name, value } = this.parseJsonProperty(child);
|
||||
if (name && value !== null) {
|
||||
result[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JsonProperty
|
||||
*/
|
||||
private parseJsonProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||||
// 使用 API 获取属性名
|
||||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||||
if (!nameNode) {
|
||||
return { name: null, value: null };
|
||||
}
|
||||
|
||||
const name = this.getNodeText(nameNode);
|
||||
|
||||
// 尝试获取值节点(String, Number, JsonBlock, VariableRef)
|
||||
let value: any = null;
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name === NODE_TYPES.STRING_LITERAL ||
|
||||
child.name === NODE_TYPES.NUMBER_LITERAL ||
|
||||
child.name === NODE_TYPES.JSON_BLOCK ||
|
||||
child.name === NODE_TYPES.VARIABLE_REF ||
|
||||
child.name === NODE_TYPES.IDENTIFIER) {
|
||||
value = this.parseValue(child);
|
||||
return { name, value };
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:从文本中提取值(用于 true/false 等标识符)
|
||||
const fullText = this.getNodeText(node);
|
||||
const colonIndex = fullText.indexOf(':');
|
||||
if (colonIndex !== -1) {
|
||||
const valueText = fullText.substring(colonIndex + 1).trim().replace(/,$/, '').trim();
|
||||
value = this.parseValueFromText(valueText);
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本解析值
|
||||
*/
|
||||
private parseValueFromText(text: string): any {
|
||||
// 布尔值
|
||||
if (text === 'true') return true;
|
||||
if (text === 'false') return false;
|
||||
if (text === 'null') return null;
|
||||
|
||||
// 数字
|
||||
if (/^-?\d+(\.\d+)?$/.test(text)) {
|
||||
return parseFloat(text);
|
||||
}
|
||||
|
||||
// 字符串(带引号)
|
||||
if ((text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
// 其他标识符
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Property(HTTP Header)
|
||||
*/
|
||||
private parseProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||||
if (!nameNode) {
|
||||
return { name: null, value: null };
|
||||
}
|
||||
|
||||
const name = this.getNodeText(nameNode);
|
||||
let value: any = null;
|
||||
|
||||
// 查找值节点(跳过冒号和逗号)
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name !== NODE_TYPES.PROPERTY_NAME &&
|
||||
child.name !== ':' &&
|
||||
child.name !== ',') {
|
||||
value = this.parseValue(child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP Header 的值必须转换为字符串
|
||||
if (value !== null && value !== undefined) {
|
||||
value = String(value);
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析值节点(字符串、数字、标识符、嵌套块、变量引用)
|
||||
*/
|
||||
private parseValue(node: SyntaxNode): any {
|
||||
if (node.name === NODE_TYPES.STRING_LITERAL) {
|
||||
const text = this.getNodeText(node);
|
||||
// 移除引号
|
||||
const value = text.replace(/^["']|["']$/g, '');
|
||||
// 替换字符串中的变量
|
||||
return this.getVariableResolver().replaceVariables(value);
|
||||
} else if (node.name === NODE_TYPES.NUMBER_LITERAL) {
|
||||
const text = this.getNodeText(node);
|
||||
return parseFloat(text);
|
||||
} else if (node.name === NODE_TYPES.VARIABLE_REF) {
|
||||
// 处理变量引用节点
|
||||
const text = this.getNodeText(node);
|
||||
const resolver = this.getVariableResolver();
|
||||
const ref = resolver.parseVariableRef(text);
|
||||
if (ref) {
|
||||
return resolver.resolveVariable(ref.name, ref.defaultValue);
|
||||
}
|
||||
return text;
|
||||
} else if (node.name === NODE_TYPES.IDENTIFIER) {
|
||||
const text = this.getNodeText(node);
|
||||
// 处理布尔值
|
||||
if (text === 'true') return true;
|
||||
if (text === 'false') return false;
|
||||
// 处理 null
|
||||
if (text === 'null') return null;
|
||||
// 其他标识符作为字符串
|
||||
return text;
|
||||
} else if (node.name === NODE_TYPES.JSON_BLOCK) {
|
||||
// 嵌套对象
|
||||
return this.parseJsonBlock(node);
|
||||
} else {
|
||||
// 未知类型,返回原始文本
|
||||
return this.getNodeText(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的文本内容
|
||||
*/
|
||||
private getNodeText(node: SyntaxNode): string {
|
||||
return this.state.doc.sliceString(node.from, node.to);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷函数:解析指定位置的 HTTP 请求
|
||||
* @param state EditorState
|
||||
* @param pos 光标位置
|
||||
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
|
||||
*/
|
||||
export function parseHttpRequest(
|
||||
state: EditorState,
|
||||
pos: number,
|
||||
blockRange?: { from: number; to: number }
|
||||
): HttpRequest | null {
|
||||
const parser = new HttpRequestParser(state, blockRange);
|
||||
return parser.parseRequestAt(pos);
|
||||
}
|
||||
|
||||