33 Commits

Author SHA1 Message Date
28072c7f90 📝 Update document 2025-11-18 18:46:27 +08:00
991a89147e 🎨 Optimize code 2025-11-17 23:14:58 +08:00
a08c0d8448 🎨 Modify code block logic 2025-11-17 22:11:16 +08:00
59db8dd177 Added Monocraft font 2025-11-16 22:04:02 +08:00
29693f1baf 💄 Modify some styles 2025-11-16 21:23:59 +08:00
5d6f157ae1 🎨 Update initial code block definition
Some checks failed
Build and Release Voidraft / prepare (push) Failing after 2s
Build and Release Voidraft / build (push) Has been cancelled
Build and Release Voidraft / release (push) Has been cancelled
2025-11-16 17:49:19 +08:00
afda3d5301 Merge remote-tracking branch 'github/dependabot/npm_and_yarn/frontend/js-yaml-4.1.1' 2025-11-16 15:44:58 +08:00
5d4ba757aa Optimize HTTP client 2025-11-16 15:41:16 +08:00
d12d58b15a 🐛 Fixed markdown preview issue 2025-11-16 15:22:49 +08:00
627c3dc71f 🐛 Fixed markdown preview issue 2025-11-16 15:16:49 +08:00
26c7a3241c 🐛 Fixed module path issues 2025-11-16 02:42:59 +08:00
46c5e3dd1a Added markdown and mermaid preview 2025-11-16 02:42:01 +08:00
dependabot[bot]
92a6c6bfdb ⬆️ Bump js-yaml from 4.1.0 to 4.1.1 in /frontend
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 18:38:56 +00:00
031aa49f9f Added markdown and mermaid preview 2025-11-16 02:37:30 +08:00
1d7aee4cea 🐛 Fixed data migration issues 2025-11-14 20:16:16 +08:00
dec3ef5ef4 Added cursor protection extension 2025-11-13 21:00:35 +08:00
d42f913250 Added editor cursor state persistence 2025-11-13 20:44:47 +08:00
bae4e663fb ⬆️ Upgrade dependencies 2025-11-13 20:02:22 +08:00
a17e060d16 ⬆️ Upgrade dependencies 2025-11-13 19:36:11 +08:00
71946965eb 🐛 Fixed database constraint issues 2025-11-08 17:35:29 +08:00
d4cd22d234 🚀 Update build and release workflows 2025-11-08 17:17:07 +08:00
05f2f7d46d 🚀 Update build and release workflows 2025-11-08 17:05:31 +08:00
9deb2744a9 🚀 Update build and release workflows 2025-11-08 16:24:22 +08:00
6fac7c42d6 🚀 Update build and release workflows 2025-11-08 16:03:26 +08:00
3393bc84e3 🚀 Add build and release workflows 2025-11-08 15:50:30 +08:00
286b0159d7 🎨 Optimize update services
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-11-08 00:00:08 +08:00
cc98e556c6 ♻️ Optimize code
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-11-07 22:34:12 +08:00
5902f482d9 ♻️ Refactor configuration change notification service 2025-11-07 00:35:11 +08:00
551e7e2cfd Optimize hotkey service 2025-11-06 22:42:44 +08:00
e0179b5838 🎨 Optimize hotkey service 2025-11-06 00:08:26 +08:00
df79267e16 Optimize multi-window services 2025-11-05 22:07:43 +08:00
1f0254822f 🎨 Optimize multi-window services 2025-11-05 00:10:26 +08:00
e9b6fef3cd Added mermaid language support 2025-11-04 22:58:36 +08:00
277 changed files with 22925 additions and 4977 deletions

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

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

View File

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

293
build/COMMANDS.md Normal file
View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

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

View File

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

View File

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

View File

@@ -14,14 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* OnDataPathChanged handles data path changes
*/
export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3652863491) as any;
return $resultPromise;
}
/**
* RegisterModel 注册模型与表的映射关系
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,13 +93,32 @@ export default defineConfig({
items: [
{text: '简介', link: '/zh/guide/introduction'},
{text: '安装', link: '/zh/guide/installation'},
{text: '快速开始', link: '/zh/guide/getting-started'}
{text: '快速开始', link: '/zh/guide/getting-started'},
{text: '界面总览', link: '/zh/guide/ui-overview'},
{text: '块语法与结构', link: '/zh/guide/block-syntax'}
]
},
{
text: '功能特性',
text: '编辑与效率',
items: [
{text: '功能概览', link: '/zh/guide/features'}
{text: '键盘快捷键', link: '/zh/guide/keyboard-shortcuts'},
{text: '多窗口与标签页', link: '/zh/guide/multiwindow-tabs'},
{text: '扩展与插件', link: '/zh/guide/extensions'},
{text: 'HTTP 客户端', link: '/zh/guide/http-client'}
]
},
{
text: '个性化与数据',
items: [
{text: '设置与配置', link: '/zh/guide/settings'},
{text: '主题与外观', link: '/zh/guide/themes'},
{text: '备份与更新', link: '/zh/guide/backup-update'}
]
},
{
text: '问题处理',
items: [
{text: '常见问题与故障排查', link: '/zh/guide/troubleshooting'}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,60 @@
# 备份与更新
![备份更新占位](/img/placeholder-backup.png)
> 替换为备份设置、推送状态、更新提示的截图。
## Git 备份
`BackupService``dataPath` 转化为 Git 仓库,并提供自动/手动推送。
### 初始化
1. 在设置 > 备份中开启「启用备份」。
2. 填写远程仓库 URLHTTPS 或 SSH
3. 选择认证方式:
- **Token**:适用于 GitHub/Gitea https 仓库。
- **SSH Key**:指定私钥路径和 passphrase。
- **用户名/密码**:适合自建 HTTP 仓库。
4. 点击“测试连接”(按钮在计划中,可先在终端测试)。
### 自动备份
- 勾选 “自动备份” + 设置 `BackupInterval`(分钟)。
- 服务会创建 ticker 定时 `git add -> commit -> push`
- Commit 消息形如 `Auto backup <timestamp>`,包含 `voidraft.db`, `extensions.json`, `config.json`, `voidraft_data.bin`
### 手动推送
- 打开工具栏消息中心或设置页点击“立即推送”。
- `backupStore.pushToRemote` 会显示状态气泡(成功/失败提示 3~5 秒)。
### 常见问题
| 提示 | 解决 |
| --- | --- |
| `repository not found` | 检查 Repo URL 与权限,必要时创建空仓库 |
| `authentication required` | 选择正确认证方式,确认 token scope需 repo 权限) |
| `auto backup stopped` | 查看日志,可能是网络不通或凭据失效;修改配置后服务会自动重启 |
## 自动更新
`SelfUpdateService` 负责检测、下载、应用新版。
### 检查更新
- 启动时若勾选 “自动更新” 会自动检查。
- 也可在设置 > 更新点击 “检查更新” 或在工具栏更新图标处触发。
- 服务优先访问 `primarySource`,失败时回退 `backupSource`
### 下载与应用
1. 检测到更新后,界面提示版本号与变更信息(从 Release Notes 获取)。
2. 点击 “下载并安装” 后,后台执行下载,完成后提示“准备重启”。
3. 选择 “立即重启” 将调用 `RestartApplication`,自动重新打开上次的文档。
4. 若启用了 “更新前备份”,在下载前会触发一次 Git push。
### 失败处理
| 场景 | 建议 |
| --- | --- |
| 下载失败 | 检查网络/代理,切换至备用源 |
| 校验失败 | 删除 `%LOCALAPPDATA%/voidraft/update-cache` 再重试 |
| 应用后无法启动 | 从 Git 备份回滚数据,下载旧版本安装包覆盖 |
## 发布渠道
- 官方 GitHub Releases`https://github.com/landaiqing/voidraft/releases`
- 自建 Gitea`https://git.landaiqing.cn/landaiqing/voidraft`
- 可在设置中替换 Owner/Repo/BaseURL以指向企业私有镜像。
> 建议将备份仓库设为私有,并在更新前后验证数据完整性。若要接入 S3/OSS 等备份方式,可关注 roadmap 或自行扩展。

View File

@@ -0,0 +1,78 @@
# 块语法与结构
![块语法占位](/img/placeholder-block-flow.png)
> 替换为展示分隔符(`∞∞∞language`)、块内容、语言标签的截图。
## 结构定义
每个块都由 **分隔符 + 内容** 组成:
```
∞∞∞language[-a]\n
<内容>
```
- `language``lang-parser/languages.ts` 中的 token例如 `text``javascript``python``md``http``math``sql`
- `-a`:可选自动检测后缀,表示忽略显式语言,由 `lang-detect/autodetect.ts` 根据内容猜测。
- 内容允许空行;块之间无需额外空格。
`parser.ts` 会将块解析为:
```ts
{
language: { name: "javascript", auto: false },
delimiter: { from, to },
content: { from, to },
range: { from, to }
}
```
这些字段供格式化、HTTP 运行、块操作等扩展示例使用。
## 快捷命令
| 命令 | 默认快捷键 | 说明 |
| --- | --- | --- |
| blockAddAfterCurrent | `Ctrl+Enter` | 插入新块(下方) |
| blockAddBeforeCurrent | `Ctrl+Shift+Enter` | 插入新块(上方) |
| blockGotoPrevious/Next | `Alt+↑ / Alt+↓` | 在块之间跳转 |
| blockSelectAll | `Ctrl+Shift+A` | 选中当前块(含分隔符) |
| blockDelete | `Alt+Delete` | 删除整个块 |
| blockMoveUp/Down | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | 重排块顺序 |
| blockFormat | `Ctrl+Shift+F` | 针对当前块执行 Prettier |
## 语言与能力矩阵
| 语言 | 适配特性 |
| --- | --- |
| `text`/`note` | 基础文本,没有特殊扩展 |
| `md` | Markdown 预览、checkbox、高亮 |
| `javascript`/`typescript`/`json` | Prettier 格式化、彩虹括号、折叠、颜色选择 |
| `go`/`rust`/`python`/`java` | 高亮、折叠、自动缩进、语法跳转 |
| `http` | HTTP DSL、变量、响应插入详见 [HTTP 客户端](/zh/guide/http-client)|
| `math` | `mathBlock` 运行器,支持 `prev` 引用上一次结果 |
| `sql`/`yaml`/`toml` | 语法高亮、格式化(由 Prettier/插件支持) |
> 若需要不在列表中的语言,可先使用 `text` 块输入,再在工具栏搜索语言名;也可以在 `lang-parser/languages.ts` 中添加条目。
## 自动检测策略
- 当分隔符以 `∞∞∞text-a` 写成时,`AUTO_DETECT_SUFFIX` 生效,`lang-detect` 会基于内容统计 + Levenshtein 距离预测语言。
- 自动结果会写入块状态,但不会覆盖分隔符原文,因此可通过工具栏明确指定。
## 特殊块
### HTTP 块
- 语法位于 `extensions/httpclient/language`,支持 `@var/@json/@form/@multipart` 等指令。
- 运行器会在块尾生成 `### Response`包含状态码、耗时、headers、body。
### 数学块
- 语言设为 `math`,逐行计算。
- `prev` 变量表示上一行结果,可完成链式运算。
- 结果挂件可点击复制,或显示格式化/定点值。
### Markdown 块
- 工具栏中的 Preview 按钮会调用 `toggleMarkdownPreview`,右侧打开面板。
- 预览状态按文档隔离,不会影响其他文档。
## 分隔符校验
- `parser.ts` 暴露 `isValidDelimiter` 方法,格式错误时分隔符会以红色底纹标记。
- 复制/剪切操作会自动扩选到整个块,确保分隔符完整。
## 维护建议
- 保持每个逻辑主题占用一个块,并用 `md` 块写标题。
- 大量多语言内容时,可用 `text` + 自动检测,待语言确定后再改分隔符。
- 若文档出现“无法解析块”提示,可运行 `格式化文档` 或在命令面板触发“重建语法树”。

View File

@@ -0,0 +1,42 @@
# 扩展与插件
![扩展占位](/img/placeholder-extensions.png)
> 替换为展示扩展设置面板或功能合集(小地图、搜索、翻译)的截图。
voidraft 的扩展系统由 `internal/models/extensions.go` + 前端 `ExtensionManager` 驱动。扩展配置存储在 `%USERPROFILE%/.voidraft/data/extensions.json`,可在设置页面勾选启用或调整参数。
## 核心扩展
| 扩展 ID | 功能 | 关键文件 |
| --- | --- | --- |
| `editor` | 基础 CodeMirror 行为、光标保护、滚轮缩放 | `frontend/src/views/editor/basic/*` |
| `codeblock` | 块解析、拖拽、复制、格式化、数学、HTTP DSL | `extensions/codeblock` |
| `vscodeSearch` | VSCode 风格搜索替换面板 | `extensions/vscodeSearch` |
| `markdownPreview` | Markdown 实时预览 | `extensions/markdownPreview` |
## 编辑增强
- **Rainbow Brackets (`rainbowBrackets`)**:彩虹色括号匹配。
- **Fold (`fold`)**:代码折叠/展开,支持 `Ctrl+Alt+[`/`]`
- **Hyperlink (`hyperlink`)**:识别 URL/邮箱,`Ctrl+Click` 打开。
- **Color Selector (`colorSelector`)**:悬浮配色器,支持 HEX/RGB/HSL。
- **Checkbox (`checkbox`)**Markdown 任务列表交互式勾选。
- **Text Highlight (`textHighlight`)**`Mod+Shift+H` 快速标记重点,可自定义颜色/透明度。
## 工具扩展
- **Translator (`translator`)**:选区翻译;配置项包括默认翻译器、最短/最长字符数。后端集成 Bing/Google/Youdao/DeepL/TartuNLP。
- **Minimap (`minimap`)**:右侧迷你地图,支持悬浮/常驻、显示字符/块,突出当前选区。
- **Search (`search`)**:补充 VSCode 风格搜索,暴露命令给快捷键系统。
- **HTTP Client (`httpclient`)**DSL + 运行器,详见 [HTTP 客户端](/zh/guide/http-client)。
## 未来扩展(欢迎参与)
- Vim / Emacs 键位层(正在计划)。
- 自定义命令面板Command Palette
- 代码片段/模板库扩展。
- AI 助手(文生代码/注释)。
## 开发者指南概述
1. **注册扩展**:在 `extensionManager.registerFactory` 中添加自定义扩展工厂。
2. **配置项**:在 `extensions.json` 中声明默认配置,并在设置面板暴露 UIVue 组件)。
3. **热更新**:调用 `manager.updateExtensionImmediate(id, enabled, config)` 实时切换,无需刷新窗口。
4. **后端交互**:通过 `ExtensionService.UpdateExtensionState` 将配置写入 SQLite。
> 如果需要编写自用扩展,可 fork 项目在 `frontend/src/views/editor/extensions` 中添加文件,再通过 PR 贡献给社区。

View File

@@ -1,163 +1,86 @@
# 功能特性
探索 voidraft 的强大功能,让它成为开发者的优秀工具。
![扩展能力占位](/img/placeholder-extensions.png)
> 替换为展示彩虹括号、小地图、搜索工具条等扩展组合的截图。
## 块状编辑
## 1. 编辑体验
### 块状编辑全流程
- `∞∞∞language[-a]` 语法由 `codeblock/lang-parser` 解析,支持自动检测、分隔符校验、块范围缓存。
- `blockState` 暴露 API`getActiveBlock/getFirstBlock/getLastBlock`供格式化、重排、复制、HTTP 执行等插件共享。
- `mathBlock` 可在块尾展示计算结果,点击可复制;`CURRENCIES_LOADED` 注解在汇率更新时刷新缓存。
voidraft 的核心功能是其块状编辑系统:
### 语言支持
- 内建 30+ 语言模版(`lang-parser/languages.ts`),覆盖 JS/TS/HTML/CSS/Go/Rust/Python/SQL/YAML/HTTP/Markdown/Plain/Text/Math。
- 语言切换下拉实时更新分隔符;支持自定义别名(例如 `∞∞∞shell`)。
- 每个块可以有不同的编程语言
- 块之间由分隔符分隔(`∞∞∞语言`
- 快速在块之间导航
- 独立格式化每个块
### 语法高亮与主题
- `rainbowBracket``fold``hyperlink``colorSelector` 等扩展组合提供接近 VSCode 的体验。
- `ThemeService` 预置 12+ 暗/亮主题,可在设置中克隆、修改 JSON 色板,并立即生效。
## 语法高亮
### 文本统计与滚轮缩放
- `statsExtension` 实时统计行数、字符数和选区,展示在状态栏。
- `wheelZoomExtension``Ctrl + 鼠标滚轮` 调整字体大小,同时同步 `configStore`
支持 30+ 种语言的专业语法高亮:
## 2. 高效工具箱
### VSCode 式搜索替换
- `extensions/vscodeSearch` 提供悬浮面板,支持大小写/整词/正则、向上/向下跳转、批量替换。
- 对应快捷键:`Ctrl+F``Ctrl+H``Alt+Enter`(替换全部)。
- 自动语言检测
- 可自定义配色方案
- 支持嵌套语言
- 代码折叠支持
### Markdown 预览
- `panelStore` 为每个文档维护预览状态,保证不同文档互不影响。
- 选中 Markdown 块后点击工具栏预览按钮即可在右侧展开实时渲染面板。
## HTTP 客户端
### HTTP 客户端
- Request DSL + 运行器在 [专章](/zh/guide/http-client) 详细说明。
- 支持变量、响应插入、多种请求体、定制 header、复制 cURL。
用于 API 测试的内置 HTTP 客户端:
### 翻译助手
- `translator` 扩展监听选区,符合长度阈值后显示按钮;由 `TranslationService` 调用 Bing/Google/Youdao/DeepL/TartuNLP。
- 支持语种缓存、复制译文、切换译文方向。
### 请求类型
- GET、POST、PUT、DELETE、PATCH
- 自定义请求头
- 多种请求体格式JSON、FormData、URL 编码、XML、文本
### 颜色与高亮
- `colorSelector` 识别 `#fff/rgba/hsl`、打开取色器;`textHighlight``Mod+Shift+H` 标记重要行。
### 请求变量
定义和重用变量:
## 3. 复杂布局能力
### 多窗口
- `WindowService` 允许为任意文档创建独立 WebViewURL 自动携带 `?documentId=`
- `WindowSnapService` 根据主窗口位置吸附子窗口(上下左右+四角),并缓存尺寸、位置。
- 支持全局热键(默认 `Alt+X`)一键显示或隐藏所有窗口。
```http
@var {
baseUrl: "https://api.example.com",
token: "your-api-token"
}
### 标签页
- `tabStore` 通过 `enableTabs` 控制;支持拖拽排序、关闭其他/左侧/右侧标签。
- 与多窗口互斥:当文档被新窗口接管后会从标签栏移除,避免重复。
GET "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
}
```
### 系统托盘与置顶
- `TrayService` 控制关闭时隐藏到托盘或直接退出。
- 工具栏提供图钉按钮,可即时切换 `AlwaysOnTop`(支持临时置顶和永久置顶)。
### 响应处理
- 查看格式化的 JSON 响应
- 查看响应时间和大小
- 检查响应头
- 保存响应以供日后使用
## 4. 数据守护
### SQLite + 自动迁移
- `DatabaseService` 启动时执行 PRAGMA + 表结构校验,缺失字段自动 `ALTER TABLE`
- 默认生成 `documents/extensions/key_bindings/themes` 等表,支持软删除与锁定。
## 代码格式化
### Git 备份
- `BackupService``dataPath` 初始化为 Git 仓库,支持 Token/SSHKey/用户名密码三种方式。
- 自动任务按分钟运行(`BackupInterval`),包括 add/commit/push也可从 UI 触发一次性 push。
集成 Prettier 支持:
### 配置快照
- 所有设置存于 `config.json`,包含 `metadata.version/lastUpdated`,方便手工回滚。
- `ConfigService.Watch` 为窗口吸附、托盘、热键等服务提供实时响应。
- 保存时格式化(可选)
- 格式化选区或整个块
- 支持 JavaScript、TypeScript、CSS、HTML、JSON 等
- 可自定义格式化规则
### 自动更新
- `SelfUpdateService` 先检查主源Gitea失败再回退到 GitHub下载完成后可一键「重启并更新」。
- 更新前可选自动触发 Git 备份(`backupBeforeUpdate`)。
## 编辑器扩展
## 5. 自动化与集成
- **启动时动作**:可开启开机自启(`StartupService`)、默认最小化至托盘。
- **HTTP 运行挂钩**`response-inserter` 可在响应块尾部插入 `// @timestamp` 等自定义标记。
- **Math/汇率**`mathBlock` 可引用上一次结果 (`prev`),配合 `CURRENCIES_LOADED` 注解支撑货币换算。
- **系统信息**`SystemService` 暴露内存、GC、Goroutine 数量,可在调试面板查看。
### VSCode 风格搜索
- 查找和替换,支持正则表达式
- 区分大小写和全字匹配选项
- 跨所有块搜索
### 小地图
- 文档的鸟瞰图
- 快速导航
- 可自定义大小和位置
### 彩虹括号
- 彩色括号配对
- 更容易匹配括号
- 可自定义颜色
### 颜色选择器
- 可视化颜色选择
- 支持 hex、RGB、HSL
- 实时预览
### 翻译工具
- 翻译选定的文本
- 支持多种语言
- 快速键盘访问
### 文本高亮
- 高亮重要文本
- 多种高亮颜色
- 持久化高亮
## 多窗口支持
高效使用多个窗口:
- 每个窗口都是独立的
- 独立的文档
- 同步的设置
- 窗口状态持久化
## 主题自定义
完全控制编辑器外观:
### 内置主题
- 深色模式
- 浅色模式
- 根据系统自动切换
### 自定义主题
- 创建你自己的主题
- 自定义每种颜色
- 保存和分享主题
- 导入社区主题
## 自动更新系统
通过自动更新保持最新:
- 后台更新检查
- 新版本通知
- 一键更新
- 更新历史
- 支持多个更新源GitHub、Gitea
## 数据备份
使用基于 Git 的备份保护你的数据:
- 自动备份
- 手动触发备份
- 支持 GitHub 和 Gitea
- 多种认证方式SSH、Token、密码
- 可配置备份间隔
## 键盘快捷键
广泛的键盘支持:
- 可自定义快捷键
- Vim/Emacs 按键绑定(计划中)
- 快速命令面板
- 上下文感知快捷键
## 性能
专为速度而构建:
- 快速启动时间
- 流畅滚动
- 高效内存使用
- 支持大文件
## 隐私与安全
你的数据是安全的:
- 本地优先存储
- 可选云备份
- 无遥测或跟踪
- 开源代码库
## 6. 可配置的快捷键
- 详见 [键盘快捷键](/zh/guide/keyboard-shortcuts)。默认绑定定义在 `internal/models/key_bindings.go`,前端设置页可逐项修改、禁用。
## 7. 文档 & 帮助
- 文档站以 VitePress 构建(`frontend/docs`),内置中英双语导航,可一键部署到 GitHub Pages。
- `README` 与本文档同步介绍核心功能;建议将常用工作流截图补充到每个「图片占位」中。

View File

@@ -1,107 +1,82 @@
# 快速开始
学习使用 voidraft 的基础知识并创建你的第一个文档。
![块结构流程占位](/img/placeholder-block-flow.png)
> 替换为展示块分隔符、语言标签、内容的截图,帮助读者直观理解 `∞∞∞language` 结构。
## 编辑器界面
## 5 分钟上手流程
1. **启动应用**:等待加载动画结束,默认会打开 `default` 文档。
2. **新建文档**:点击工具栏的文档列表按钮,输入标题后创建;也可在设置里开启标签页,以便同时挂载多个文档。
3. **创建首个块**
- 在空白处输入 `∞∞∞javascript` 并回车。
- 输入代码或文本,`CodeBlockExtension` 会自动匹配语法高亮。
4. **格式化与预览**
- 选中块后点击工具栏的「Format」或使用 `Ctrl+Shift+F`
- 如果块语言是 `md`可点击「Preview」按钮开启 Markdown 侧栏。
5. **运行 HTTP 请求**:创建 `∞∞∞http` 块,填写请求,再点击行号旁的 Run 按钮即可获取响应。
6. **打开第二窗口**:在文档列表中右键文档 -> “在新窗口中打开”。`WindowService` 会创建无边框窗口并自动贴靠主窗口。
当你打开 voidraft 时,你将看到:
## 界面导览
- **主编辑区**CodeMirror 视图,支持鼠标滚轮 + `Ctrl` 缩放(`wheelZoomExtension`)。
- **右侧小地图**`extensions/minimap` 提供鸟瞰和选区同步。
- **底部状态**`editorStore.documentStats` 实时展示行数/字符/选区。
- **工具栏**`Toolbar.vue`包含文档切换、块语言下拉、窗口置顶、格式化、Markdown 预览、更新提示、进入设置等。
- **主编辑器**:编写和编辑的中心区域
- **工具栏**:快速访问常用操作
- **状态栏**:显示当前块的语言和其他信息
## 块的基本操作
| 操作 | 快捷键 | 说明 |
| --- | --- | --- |
| 新建块(下方) | `Ctrl+Enter` | 在当前块后插入 `∞∞∞text-a` 分隔符 |
| 新建块(上方) | `Ctrl+Shift+Enter` | 在当前块前插入 |
| 跳到上/下一个块 | `Alt+Up / Alt+Down` | 通过 `blockGotoPrevious/Next` 命令 |
| 删除块 | `Alt+Delete` | 仅删除块内容,不影响其他块 |
| 块排序 | `Ctrl+Shift+↑/↓` | `moveLines` 结合块范围移动 |
| 复制块 | `Ctrl+C`(光标在块上即可) | `copyPaste.ts` 自动扩展选区至整个块 |
## 创建代码块
## 自动语言与格式化
- 当分隔符写成 `∞∞∞text-a` 时,会触发语言自动检测(`lang-detect/autodetect.ts`),常用于粘贴未知代码。
- `formatCode.ts` 调用 Prettier自动选择 parser若语言不支持会提示不可格式化。
- 块语言可在工具栏下拉中修改,列表由 `lang-parser/languages.ts` 提供。
voidraft 使用基于块的编辑系统。每个块可以有不同的语言:
## Markdown / 待办
1. 使用 `∞∞∞md` 分隔符。
2. 在块内写 Markdown点击工具栏预览按钮。
3. 勾选/取消 Checkbox`extensions/checkbox`)即可同步更新文本。
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 请求:
## 翻译与文本标注
- 选中文本后会浮现翻译入口(`translator` 扩展),点击即可在块内查看结果、复制、切换目标语言。
- `textHighlight` 扩展提供 `Mod+Shift+H` 高亮当前选区,颜色可在扩展设置中调整。
## HTTP 客户端概览
```http
POST "https://api.example.com/users" {
http
@var {
baseUrl: "https://api.example.com",
token: "{{secrets.token}}"
}
POST "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
content-type: "application/json"
@json {
name: "",
email: "zhangsan@example.com"
name: "voidraft",
role: "developer"
}
}
```
- `parser/request-parser.ts` 会将变量与请求体解析为结构化对象。
- 点击 gutter Run 获取响应,`response-inserter.ts` 会将结果写入 `### Response` 区块。
3. 点击运行按钮执行请求
4. 内联查看响应
## 自动保存与版本安全
- `editorStore` 为每个文档维护 `autoSaveTimer`,默认 2000 ms可在设置 > 编辑 调整。
- `documentStates` 记录每个文档的光标位置,切换文档或重启应用都会恢复。
- 若开启 Git 备份,可在工具栏或设置中查看最近一次 `push` 是否成功。
## 多窗口支持
同时处理多个文档:
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)
## 最佳实践
- 使用 Markdown 块为每组代码加标题/注释,便于导航。
- 重要文档启用“锁定”以避免被删除(文档右键菜单)。
- 多窗口 + 吸附用于常驻参考资料,标签页用于在一个窗口内快速切换。
- 善用「窗口置顶」图钉,让 voidraft 叠放在 VSCode/浏览器之上。
接下来:
- [界面总览](/zh/guide/ui-overview)
- [块语法与结构](/zh/guide/block-syntax)

View File

@@ -0,0 +1,72 @@
# HTTP 客户端
![HTTP 客户端占位](/img/placeholder-http.png)
> 替换为 HTTP 块 + 响应卡片的截图。
voidraft 将 HTTP 测试写成块(`∞∞∞http`),语法与 JetBrains Http Client 类似。解析与执行由 `frontend/src/views/editor/extensions/httpclient` 完成。
## 基本语法
```http
http
GET "https://api.example.com/users" {
accept: "application/json"
}
```
- 方法 + URL 必须用双引号包裹。
- Header 以 `key: "value"` 格式编写。
- 请求体使用内联指令(见下文)。
## 变量与环境
```http
@var {
baseUrl: "https://api.example.com",
token: "{{secrets.token}}"
}
GET "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
}
```
- `@var` 块使用 JSON 语法。
- 变量在任意请求中以 `{{name}}` 引用。
- `variable-resolver.ts` 支持嵌套、默认值、外部 secrets 映射。
## 请求体助手
| 指令 | 示例 | 说明 |
| --- | --- | --- |
| `@json` | `@json { "name": "voidraft" }` | 自动 `content-type: application/json` 并格式化 |
| `@form` | `@form { username: "demo" }` | 转为 `application/x-www-form-urlencoded` |
| `@multipart` | `@multipart { file: @"C:\tmp\a.txt" }` | 读取文件、多段表单 |
| `@text` | `@text <raw body>` | 自由文本 |
## 运行与响应
1. 将光标置于 HTTP 块内。
2. 点击行号左侧的 Run三角形图标或按下自定义快捷键。
3. 运行结果会插入到块尾的 `### Response` 中,包含:
- 状态行 + 响应时间 + 体积。
- Headers可折叠
- 响应体(自动格式化 JSON / XML / HTML / Text
- 复制、另存为、再次发送等快捷按钮。
## 多请求文档
- 每个 `∞∞∞http` 块被视为独立请求。
- `request-parser.ts` 会解析同一块内的多个请求(以 `###` 分隔)。
- 使用 Markdown 块写注释或分组标题。
## 变量注入顺序
1. 块内 `@var`
2. 文档级变量(计划中)。
3. 环境变量(`EnvironmentService` 预留)。
## 调试技巧
- 运行器会在控制台打印完整请求信息,可通过 `wails3 dev` 查看。
- 如果响应过大,可右键响应块选择“折叠正文”或“导出到文件”。
- 网络错误会在响应卡片顶部以红条展示,内容来自 `HttpClientService`
- 需要代理时确保系统代理已设置voidraft 会自动继承。
## 与其他功能配合
- 运行结果可直接与 Markdown/代码块混排,形成 API 使用手册。
- 配合 Git 备份可版本化 API 调试记录。
- 可将响应复制到其他块(例如 JSON → Prettier 之后用于 mock
> 欢迎在 Issue 中提交你希望支持的额外 DSL 指令(例如 GraphQL、WebSocket、gRPC

View File

@@ -1,63 +1,77 @@
# 安装
本指南将帮助你在系统上安装 voidraft。
![安装流程占位](/img/placeholder-settings.png)
> 替换为安装向导或设置页截图,展示关键开关(置顶、数据目录、自动更新等)。
## 系统要求
## 系统要求2025.11
| 项目 | 最低配置 | 推荐配置 |
| --- | --- | --- |
| 操作系统 | Windows 10 19045 / Windows 11 21H2 | Windows 11 23H2macOS/Linux 版本开发中) |
| CPU | x86_64 双核 | 4 核以上 |
| 内存 | 4 GB | ≥ 8 GB |
| 磁盘空间 | 200 MB含 SQLite 数据) | 1 GB 以上以保存附件/备份 |
| 运行环境 | Go 1.21+, Node.js 18+(仅开发者编译时需要) | 同左 + pnpm 8 用于前端 |
- **操作系统**Windows 10 或更高版本macOS 和 Linux 支持计划中)
- **内存**:最低 4GB推荐 8GB
- **磁盘空间**200MB 可用空间
## 获取发行版
1. 打开 [GitHub Releases](https://github.com/landaiqing/voidraft/releases) 或自建 Gitea 镜像。
2. 下载 `voidraft-windows-amd64-installer.exe`(安装版)或 `voidraft-portable.zip`(绿色版)。
3. (可选)验证 SHA256
```powershell
Get-FileHash .\voidraft-windows-amd64-installer.exe -Algorithm SHA256
```
4. 双击安装包,按向导完成安装;或解压绿色版至任意目录并创建快捷方式。
## 下载
## 首次启动流程
1. 启动后将创建数据目录:`%USERPROFILE%\.voidraft\data`(含 `voidraft.db`、`config.json`、`extensions.json`)。
2. 默认会生成 `default` 文档和一段示例块 `∞∞∞text-a`。
3. 若检测到旧版本数据,`ConfigMigrationService` 会自动迁移字段;`DataMigrationService` 确保表结构一致。
4. 首次运行建议立刻打开「设置 > 备份」配置远程 Git 仓库。
访问[发布页面](https://github.com/landaiqing/voidraft/releases)并下载适合你平台的最新版本:
## 开发者手动构建
```bash
# 克隆项目
git clone https://github.com/landaiqing/voidraft.git
cd voidraft
- **Windows**`voidraft-windows-amd64-installer.exe`
# 安装前端依赖
cd frontend
npm install
npm run build
cd ..
## 安装步骤
# 构建/运行桌面应用
wails3 dev # 启动调试
wails3 package # 生成安装包(输出位于 bin/
```
> 若遇到 `wails3` 未找到,请先执行 `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`。
### Windows
## 数据目录与可执行文件
| 类型 | 默认位置 | 说明 |
| --- | --- | --- |
| 安装目录 | `C:\Program Files\voidraft` | 包含主程序与嵌入式前端资源 |
| 数据目录 | `C:\Users\<you>\.voidraft\data` | 可在设置 > 通用修改 `dataPath`,修改后需重启 |
| 备份仓库 | `dataPath/.git` | `BackupService` 初始化或使用现有仓库 |
| 日志 | `%LOCALAPPDATA%/voidraft/logs/*.log` | 通过 Wails `application.Log` 输出 |
1. 从发布页面下载安装程序
2. 运行 `voidraft-windows-amd64-installer.exe` 文件
3. 按照安装向导操作
4. 从开始菜单或桌面快捷方式启动 voidraft
## 常用 CLI 检查
```powershell
# 查看版本
& "C:\Program Files\voidraft\voidraft.exe" --version
## 首次启动
# 清理缓存(若前端异常)
Remove-Item "$env:APPDATA\voidraft\Cache" -Recurse -Force
```
首次启动 voidraft 时:
## 防火墙与代理
- voidraft 仅在使用 HTTP 客户端、更新检测、REST 翻译器时发起网络请求。
- 若处于企业代理,请在系统代理中放行 `voidraft.exe` 或设置环境变量 `HTTP(S)_PROXY`HTTP 客户端会继承系统代理。
1. 应用程序将创建一个数据目录来存储你的文档
2. 你将看到带有欢迎块的主编辑器界面
3. 开始输入或创建你的第一个代码块!
## 配置
voidraft 将其配置和数据存储在:
- **Windows**`%APPDATA%/voidraft/`
你可以自定义各种设置,包括:
- 编辑器主题(深色/浅色模式)
- 代码格式化偏好
- 备份设置
- 键盘快捷键
## 更新
voidraft 包含自动更新功能,会在有新版本时通知你。你可以:
- 从设置中手动检查更新
- 启用自动更新
- 选择首选的更新源
## 故障排除
如果在安装过程中遇到任何问题:
1. 确保你有管理员权限
2. 检查杀毒软件是否阻止了安装
3. 访问我们的 [GitHub issues](https://github.com/landaiqing/voidraft/issues) 页面寻求帮助
下一步:[快速开始 →](/zh/guide/getting-started)
## 常见安装问题
| 症状 | 处理方案 |
| --- | --- |
| 安装向导被安全策略阻止 | 使用签名哈希进行白名单设置或改用便携版 |
| 启动后白屏 | 删除 `%APPDATA%/voidraft/Cache`,确保显卡驱动支持 WebView2 |
| `wails3 dev` 报错缺少 WebView2 | 安装 [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) |
| 便携版无法写入 | 检查解压目录是否具有写权限,或在设置内切换 `dataPath` 至可写分区 |
> 继续阅读:[快速开始](/zh/guide/getting-started)

View File

@@ -1,50 +1,73 @@
# 简介
欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具
> voidraft 是一款面向开发者的「块式工作台」,用 CodeMirror 6 打造 Heynote 风格的体验,并结合 Wails3 + Go 后端提供系统托盘、全局热键、自动备份等桌面级能力
## 什么是 voidraft
![主界面总览占位](/img/placeholder-main-ui.png)
> 将 `/img/placeholder-main-ui.png` 替换为真实的应用主界面截图,演示数据面板、工具栏和右侧小地图。
voidraft 是一个现代化的桌面应用程序帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
## 产品定位
- **核心诉求**:在一处快速记录代码/配置/API 响应/待办清单,并能随时重排、格式化、运行或搜索。
- **目标用户**需要跨项目管理零碎文本的开发者、DevOps、测试或产品技术写作者。
- **设计理念**:所有内容都拆成可重排的块(`∞∞∞language`);每个块拥有独立语言、格式化器与扩展;多窗口/多标签保证同一份数据的不同视角。
## 核心特性
## 面向场景
1. **临时代码/脚本草稿**:支持 30+ 语言高亮、Prettier 格式化、彩虹括号、文本高亮。
2. **API 调试台**HTTP 块内置运行器、变量解析、响应插入;请求和响应始终和文档共存。
3. **会议 & 需求记录**Markdown 块 + Checkbox 扩展 + 颜色标注快速整理想法。
4. **翻译与研究**:选中文本即可调 Bing/Google/DeepL/TartuNLP/有道翻译,结果内联呈现。
5. **多窗口资料墙**重要文档可弹出独立无边框窗口依附Snap在主窗口侧边。
### 块状编辑模式
## 核心概念
### 块式编辑器
- 解析器位于 `frontend/src/views/editor/extensions/codeblock`,依赖自研 Lezer 语法树确保 `∞∞∞` 分隔符稳定。
- 块结构(语言、是否自动检测、正文范围)存入 `blockState`供格式化、移动、复制、HTTP 执行等扩展复用。
- `math` 块使用 `math.js` 运行器,`http` 块调用 request parser + gutter run widget。
voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
- 不同的编程语言设置
- 语法高亮
- 独立格式化
- 轻松在块之间导航
### 扩展驱动
- 后端通过 `internal/models/extensions.go` 定义扩展 ID/配置,`ExtensionService` 负责持久化。
- 前端 `ExtensionManager` 根据扩展配置动态拼装 CodeMirror Extension pipeline小地图、VSCode Search、Translator、Color Picker 等)。
- 所有扩展都可在设置页热切换,立即同步到当前与所有已打开的编辑器实例。
### 开发者工具
### 数据与安全
- SQLite 数据保存在 `%USERPROFILE%/.voidraft/data/voidraft.db`(可在设置中自定义 dataPath
- `DatabaseService` 自动迁移表结构,`DocumentService` 提供软删除/锁定机制避免误删默认草稿。
- `BackupService` 基于 go-gitSSH/Token/用户名密码)把 dataPath git 化,可按分钟全量提交、推送到 GitHub/Gitea 等。
- `SelfUpdateService` 同时轮询 GitHub/Gitea Release支持自动下载 + 一键重启。
- **HTTP 客户端**:直接在编辑器中测试 API
- **代码格式化**:内置 Prettier 支持多种语言
- **语法高亮**:支持 30+ 种编程语言
- **自动语言检测**:自动识别代码块语言类型
## 系统架构概览
| 层级 | 说明 | 关键路径 |
| --- | --- | --- |
| 桌面容器 | Wails3 + Go 1.21,负责窗口、托盘、热键、服务注入 | `main.go`, `internal/services` |
| 后端服务 | Config/Document/Extension/Theme/Backup/Window/Hotkey/Translation 等 | `internal/services/*.go` |
| 数据模型 | Document、Theme、KeyBinding、GitBackup、Config | `internal/models` |
| 前端应用 | Vue 3 + Vite + Pinia + vue-router | `frontend/src` |
| 编辑器内核 | CodeMirror 6 扩展及自研块解析、HTTP DSL、Markdown 预览 | `frontend/src/views/editor` |
| 文档站点 | VitePress多语言导航 | `frontend/docs` |
### 自定义
## 模块速览
- **文档存储**`DocumentService` 支持创建/重命名/软删除/恢复、多窗口并发打开同一文档。
- **编辑器实例管理**`editorStore` 使用 LRU 缓存 + 自动保存计时器,确保在多文档切换时保留光标位置、未保存内容。
- **HTTP 客户端**`extensions/httpclient` 包括 Lezer 语法、变量解析、响应插入与运行 gutter支持 JSON/FormData/GraphQL 等多体格式。
- **Markdown 预览**`panelStore` 管理逐文档的预览状态,可随块实时刷新。
- **多窗口/吸附**`WindowService` + `WindowSnapService` 根据主窗口位置智能吸附子窗口、自动记忆尺寸。
- **全局热键**`HotkeyService` 监听系统级组合键,切换窗口显隐(默认 Alt+X可配置
- **系统托盘**`systray.SetupSystemTray` 注入显示/隐藏、退出、开机启动等操作。
- **翻译生态**`TranslationService` 聚合 Bing/Google/Youdao/DeepL/TartuNLP前端 `translator` 扩展提供 Tooltip + 复制。
- **主题与外观**`ThemeService` 预置 12+ 主题,可重置/克隆;前端 `createThemeExtension` 即时应用。
- **自定义主题**:创建并保存你自己的编辑器主题
- **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
- **多窗口**:同时处理多个文档
## 数据流(从键盘到持久化)
1. 用户按键 -> CodeMirror extensions 更新文档。
2. `contentChangeExtension` 记录脏状态并刷新 `documentStats`(行数、字符数、选区字符数)。
3. 触发自动保存计时器(默认 2s -> `DocumentService.UpdateDocumentContent` 写入 SQLite。
4. 若开启 Git 自动备份,每次 Commit 会序列化数据库 + 附带 `voidraft_data.bin`
5. 配置变更Pinia store通过 `ConfigService.Set` 传回 Go并触发观察者如 WindowSnap/Hotkey/Backup
### 数据管理
- **Git 备份**:使用 Git 仓库自动备份
- **云同步**:跨设备同步你的数据
- **自动更新**:及时获取最新功能
## 为什么选择 voidraft
- **专注开发者**:考虑开发者需求而构建
- **现代技术栈**使用前沿技术Wails3、Vue 3、CodeMirror 6
- **跨平台**:支持 WindowsmacOS 和 Linux 支持计划中)
- **开源**MIT 许可证,社区驱动开发
## 开始使用
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
下一步:[安装 →](/zh/guide/installation)
## 版本节奏与路线图
- ✅ 当前实现多窗口、标签页、HTTP 客户端、Markdown Preview、数学块、彩虹括号、翻译、Git 备份、自动更新。
- 🚧 进行中自定义扩展导入、键位模版、Linux/macOS 原生打包。
- 🗺️ 规划中:剪贴板历史、团队同步、云端模板市场。
## 下一步
- [安装 voidraft](/zh/guide/installation)
- [界面总览](/zh/guide/ui-overview)
- [快速开始](/zh/guide/getting-started)

View File

@@ -0,0 +1,74 @@
# 键盘快捷键
![快捷键占位](/img/placeholder-shortcuts.png)
> 替换为展示快捷键设置界面或常用快捷键速查表的截图。
快捷键定义源自 `internal/models/key_bindings.go`,在设置 > 键位 中可以启用/禁用或改写。下表列出常用组合:
## 块管理
| 功能 | 默认快捷键 | 备注 |
| --- | --- | --- |
| 新建块(下方) | `Ctrl+Enter` | `blockAddAfterCurrent` |
| 新建块(上方) | `Ctrl+Shift+Enter` | `blockAddBeforeCurrent` |
| 跳到上/下一个块 | `Alt+↑ / Alt+↓` | `blockGotoPrevious/Next` |
| 选择当前块 | `Ctrl+Shift+A` | `blockSelectAll` |
| 删除块 | `Alt+Delete` | `blockDelete` |
| 块上移/下移 | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | `blockMoveUp/Down` |
| 复制块 | `Ctrl+C`(块获得焦点) | `blockCopy` |
| 剪切块 | `Ctrl+X` | `blockCut` |
| 粘贴块 | `Ctrl+V` | `blockPaste` |
## 行与文本编辑
| 功能 | 快捷键 |
| --- | --- |
| 行复制(上/下) | `Shift+Alt+↑ / Shift+Alt+↓` |
| 行移动(上/下) | `Alt+↑ / Alt+↓`(在块内部) |
| 插入空行 | `Ctrl+Enter`(块尾后仍可插入) |
| 选择整行 | `Alt+L` |
| 语法级跳转 | `Ctrl+Alt+Left / Ctrl+Alt+Right` |
| 匹配括号 | `Shift+Ctrl+\` |
| 注释/块注释 | `Ctrl+/` / `Shift+Alt+A` |
| Tab 缩进/反向缩进 | `Ctrl+]` / `Ctrl+[` |
| 删除单词(向前/向后) | `Ctrl+Backspace` / `Ctrl+Delete` |
## 搜索与替换
| 功能 | 快捷键 | 描述 |
| --- | --- | --- |
| 打开搜索 | `Ctrl+F` | `showSearch` |
| 打开替换 | `Ctrl+H` | `searchShowReplace` |
| 切换大小写/整词/正则 | `Alt+C / Alt+W / Alt+R` | `searchToggleCase/Word/Regex` |
| 替换全部 | `Alt+Enter` | `searchReplaceAll` |
## Markdown/预览/格式化
| 功能 | 快捷键 |
| --- | --- |
| 格式化块 | `Ctrl+Shift+F` |
| 打开 Markdown 预览 | 工具栏按钮(建议映射到 `Ctrl+Shift+M` |
| 高亮文本 | `Mod+Shift+H` |
## 窗口与系统
| 功能 | 快捷键 |
| --- | --- |
| 新建窗口 | `Ctrl+Shift+N`(命令面板) |
| 全局显示/隐藏所有窗口 | 默认 `Alt+X`(可在设置 > 通用 > 全局热键中修改) |
| 打开设置 | `Ctrl+,` |
| 切换主题 | `Ctrl+Shift+T`(可自定义) |
## HTTP 客户端
| 功能 | 快捷键 |
| --- | --- |
| 运行请求 | 点击行号旁 Run 或自定义 `Ctrl+Alt+R` |
| 复制响应正文 | `Ctrl+Alt+C`(响应块聚焦时) |
## 翻译工具
| 功能 | 快捷键 |
| --- | --- |
| 显示翻译浮层 | 选中 ≥ `minSelectionLength` 的文本后按 `Ctrl+'`(可自定义) |
| 复制译文 | 在浮层中按 `Ctrl+C` |
## 自定义与导出
1. 打开设置 > 键位,列表会加载来自 `ExtensionService.GetAllKeyBindings()` 的数据。
2. 可单独禁用某个绑定或录入新组合;存储在 `%USERPROFILE%/.voidraft/data/key_bindings.json`
3. 需要与系统级快捷键冲突时,可勾选“忽略系统修饰键”。
> 建议将以上表格打印贴在工作区,或在文档中保留常用组合,方便新同事查阅。

View File

@@ -0,0 +1,67 @@
# 多窗口与标签页
![多窗口占位](/img/placeholder-multiwindow.png)
> 替换为展示主窗口 + 侧边浮窗(子窗口)或标签页齐开的截图。
## 多窗口工作流
- `WindowService.OpenDocumentWindow` 会根据文档 ID 创建新 WebView 窗口URL 自动附加 `?documentId=<id>`
- `windowStore` 通过查询字符串判断当前是否为子窗口(非主窗口)。
- 子窗口具备:
- 独立的 CodeMirror 实例与扩展栈。
- 与主窗口共享的 Document/Config Store因此编辑内容实时同步SQLite 数据库为唯一来源)。
- `WindowSnapService` 提供吸附:拖动靠近主窗口边缘时自动贴靠;支持上下左右以及四个角。
- 关闭时自动注销吸附状态,避免悬挂引用。
### 操作步骤
1. 打开文档列表(工具栏图标或 `Ctrl+Shift+O`)。
2. 右键目标文档 → 选择 “在新窗口中打开”。
3. 若文档已在标签页打开,会自动从标签栏移除,防止重复。
4. 关闭窗口:
- 点击自定义标题栏关闭按钮。
- 系统托盘菜单选择退出。
### 使用建议
- 将参考资料或检查清单放在子窗口中,配合“窗口置顶”保持常驻。
- 通过 Windows Snap + voidraft Snap 组合,可快速排版 2-4 个窗口。
- 若想在多窗口之间同步滚动,可尝试启用“共享视图状态”扩展(计划中)。
## 标签页模式
- 在设置 > 通用中开启“启用标签页”(`config.general.enableTabs`)。
- `tabStore` 维护 `tabsMap` + `tabOrder`,支持:
- 拖拽排序(拖动标签即可)。
- 关闭单个/其他/左侧/右侧标签。
- 检测当前文档是否已存在标签。
- 标签栏位于主窗口顶部,紧贴工具栏下方。
### 常用操作
| 操作 | 方法 |
| --- | --- |
| 关闭标签 | 点击标签上的叉号或中键 |
| 关闭其他 | 右键标签 → “关闭其他标签” |
| 关闭右侧/左侧 | 右键标签 → 选择对应菜单 |
| 固定标签(计划中) | 将在后续版本中提供 pin 功能 |
### Tabs vs 窗口
| 项目 | 标签页 | 新窗口 |
| --- | --- | --- |
| UI 占用 | 集成在一个窗口中 | 独立操作系统窗口 |
| 跨屏 | 不方便 | 可拖到其他显示器 |
| 独立置顶 | 不可单独置顶 | 每个窗口可单独置顶 |
| 推荐场景 | 同一背景的多个文档 | 跨项目/跨显示器对比 |
## 系统托盘与热键
- 勾选 “启用系统托盘” 后,关闭窗口默认隐藏至托盘。
- 全局热键(默认 `Alt+X`)由 `HotkeyService` 控制:
- 若 main window 可见 → 隐藏所有 voidraft 窗口。
- 若 main window 隐藏 → Show + Restore + Focus。
- `TrayService` 还提供“最小化到托盘”“显示主窗口”等菜单项。
## 常见问题
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 新窗口无法打开 | 文档 ID 不存在或被锁定 | 在文档列表确认状态,必要时解锁 |
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
| 关闭窗口直接退出应用 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
| 标签页切换慢 | 同时开启标签 + 多窗口导致资源占用 | 关闭暂不需要的窗口或减少标签 |
> 如果需要更复杂的布局(如平铺窗口、快捷布局),欢迎在 Issue 中提出建议。

View File

@@ -0,0 +1,71 @@
# 设置与配置
![设置占位](/img/placeholder-settings.png)
> 替换为设置页截图,突出通用/编辑/外观/更新/备份等分栏。
所有设置都映射到 `internal/models/config.go`,持久化文件位于 `%USERPROFILE%/.voidraft/data/config.json`。前端 `configStore` 负责与后端 `ConfigService` 同步。
## 通用General
| 选项 | 说明 | 后端键 |
| --- | --- | --- |
| 窗口置顶 (`alwaysOnTop`) | 永久置顶主窗口 | `general.alwaysOnTop` |
| 数据目录 (`dataPath`) | SQLite + 备份所在目录,修改后需重启 | `general.dataPath` |
| 系统托盘 (`enableSystemTray`) | 关闭窗口后隐藏到托盘而非退出 | `general.enableSystemTray` |
| 开机自启 (`startAtLogin`) | 调用 `StartupService` 注册 | `general.startAtLogin` |
| 窗口吸附 (`enableWindowSnap`) | `WindowSnapService` 是否启用 | `general.enableWindowSnap` |
| 全局热键 (`enableGlobalHotkey` + `globalHotkey`) | 默认 Alt+X控制显隐 | `general.globalHotkey` |
| 标签页 (`enableTabs`) | 启用多标签界面 | `general.enableTabs` |
| 加载动画 (`enableLoadingAnimation`) | 切换文档时显示动画 | `general.enableLoadingAnimation` |
## 编辑Editing
| 选项 | 说明 |
| --- | --- |
| Font Size/Family/Weight/Line Height | 立即作用于所有编辑器实例 |
| Tab Size/Tab Type/Enable Tab Indent | 映射 `tabExtension` 行为 |
| Auto Save Delay | ms影响 `editorStore` 自动保存周期 |
## 外观Appearance
| 选项 | 说明 |
| --- | --- |
| Language | UI 语言(`zh-CN`/`en-US` |
| System Theme | 深色/浅色/跟随系统 |
| Current Theme | 选择预设或自定义主题(详见 [主题与外观](/zh/guide/themes) |
## 更新Updates
| 选项 | 说明 |
| --- | --- |
| Auto Update | 启动时自动检查更新 |
| Primary/Backup Source | `github``gitea`,对应 `UpdatesConfig` |
| Backup Before Update | 下载更新前执行 Git 备份 |
| Update Timeout | HTTP 请求超时 |
| GitHub/Gitea 仓库 | owner/repo/baseURL可指向自建镜像 |
## 备份Backup
| 选项 | 说明 |
| --- | --- |
| Enabled | 开关 Git 备份 |
| Repo URL | 远程仓库地址HTTPS 或 SSH |
| Auth Method | `token` / `ssh_key` / `user_pass` |
| Username/Password/Token/SSH Key Path | 根据认证方式填写 |
| Backup Interval | 自动备份间隔(分钟) |
| Auto Backup | 是否按间隔自动推送 |
## 键位Key Bindings
- 列表由 `ExtensionService.GetAllKeyBindings()` 提供。可搜索命令 ID 或组合。
- 允许将命令禁用(关闭开关)或录入新组合。
- 更改立即影响所有编辑器实例。
## 扩展Extensions
- 显示 `ExtensionSettings` 中的所有扩展。
- 每项可开关并展示 JSON 配置(背景色、最小选区、最小化提示等)。
- 修改后调用 `ExtensionService.UpdateExtensionState` 并通知 `ExtensionManager` 热更新。
## 配置文件备份
- 每次修改配置都会更新 `metadata.lastUpdated`,可用 Git 备份追踪历史。
- 若出现配置损坏,可删除 `config.json`,应用会写入 `NewDefaultAppConfig`
## 导入/导出(建议)
- 目前可手动复制 `config.json`/`extensions.json`/`key_bindings.json`
- 计划提供 UI 层面的导入导出按钮,便于跨设备同步。
> 修改高级选项(如 dataPath后建议重启以确保后台服务数据库、备份、窗口吸附等读取到最新配置。

View File

@@ -0,0 +1,44 @@
# 主题与外观
![主题占位](/img/placeholder-themes.png)
> 替换为主题切换界面或自定义主题编辑器的截图。
voidraft 的主题由后端 `ThemeService` 管理,存储在 `themes` 表。前端通过 `themeStore` + `createThemeExtension` 应用色板。
## 预设主题
| 名称 | 类型 | 说明 |
| --- | --- | --- |
| default-dark | Dark | 默认暗色,适合低光环境 |
| default-light | Light | 默认亮色 |
| dracula | Dark | 高对比度紫色系 |
| aura | Dark | 柔和霓虹风 |
| github-dark / github-light | Dark/Light | 与 GitHub 主题接近 |
| material-dark / light | Dark/Light | Material Design 色板 |
| one-dark | Dark | VSCode 经典主题 |
| solarized-dark / light | Dark/Light | Solarized 配色 |
| tokyo-night / storm / day | Dark/Light | Tokyo Night 三件套 |
## 自定义主题
1. 打开设置 > 外观,选择「创建主题」。
2. 颜色字段对应 `ThemeColorConfig`,包含 `editor.background`, `editor.foreground`, `gutter`, `selection`, `bracket`, `keyword`, `string`, `comment`, `accent` 等。
3. 保存后立即写入数据库,可通过 `Reset` 按钮恢复为预设值。
4. 前端 `themeExtension` 会向 CodeMirror 注入新的 `EditorView.theme`
## 动态切换
- 切换主题会立即影响所有已打开的编辑器实例;`updateEditorTheme` 逐个更新 `EditorView`
- `SystemTheme` 设为 `auto`voidraft 会监听操作系统深浅模式并自动切换到 `default-dark``default-light`
## 字体与行高
- 字体配置来自设置 > 编辑,`createFontExtensionFromBackend` 会同步 `fontFamily/fontSize/fontWeight/lineHeight`
- 可在通用设置中的“滚轮缩放”手势下临时调整字号。
## 小地图/装饰色
- `minimap` 扩展读取主题中的 `accent` 颜色,用于高亮当前视区。
- `textHighlight` 扩展的默认背景色可在扩展设置中配置。
## 截图建议
- 展示暗/亮主题对比。
- 展示主题编辑对话框,标出关键字段。
- 展示自定义主题应用后的编辑器界面。
> 如果希望导入 VSCode `.json` 主题,可将颜色映射到 `ThemeColorConfig` 后写入数据库,或等待官方导入工具上线。

View File

@@ -0,0 +1,53 @@
# 常见问题与故障排查
![故障排查占位](/img/placeholder-troubleshooting.png)
> 替换为错误提示或日志查看界面的截图。
## 安装与启动
| 问题 | 可能原因 | 解决步骤 |
| --- | --- | --- |
| 启动白屏 | WebView2 缺失或缓存损坏 | 安装 WebView2 Runtime删除 `%APPDATA%/voidraft/Cache` 后重启 |
| 双击无反应 | 被安全策略拦截 | 以管理员运行或使用便携版;验证 SHA256 后加入白名单 |
| `wails3 dev` 报错 | Go/Node 版本不符或缺少 WebView2 | 确保 Go 1.21+、Node 18+,安装 WebView2 |
## 编辑器
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 格式化按钮灰色 | 当前块语言无对应 Prettier parser | 更换语言或安装支持的语言扩展(未来版本) |
| 块解析错误 | 分隔符格式不正确 | 确认 `∞∞∞language` 后跟换行;可用自动重建语法树命令 |
| 翻译浮层不出现 | 选区过短或超过最大长度 | 在设置 > 扩展 > translator 中调整阈值 |
| 小地图不同步 | 编辑器实例未刷新 | 切换文档或重开应用,检查扩展是否被禁用 |
## 窗口与多实例
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
| 全局热键无效 | 与系统/其他软件冲突 | 在设置 > 通用改用非系统占用组合,比如 `Ctrl+Alt+Space` |
| 关闭窗口直接退出 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
## HTTP 客户端
| 问题 | 可能原因 | 解决 |
| --- | --- | --- |
| 发送失败,提示 `proxy` | 系统代理配置异常 | 在系统代理或环境变量中设置 HTTP(S)_PROXY或关闭代理再试 |
| 响应乱码 | 服务器未声明编码 | 手动在请求头中加 `accept-charset: utf-8`,或在响应视图切换编码(计划) |
| 变量未替换 | 变量名拼写或作用域错误 | 确认 `@var` 定义位置,使用 `{{name}}` 语法 |
## 数据与备份
| 问题 | 可能原因 | 解决 |
| --- | --- | --- |
| 自动备份停在 “未初始化” | Repo URL/认证缺失 | 补全备份配置或关闭自动备份 |
| Push 失败 | Token 权限不足或网络问题 | 为 Token 开启 `repo` scope检查代理稍后再试 |
| 数据目录迁移后文件缺失 | 未重启或权限不足 | 修改 `dataPath` 后重启应用;确保目标目录可写 |
## 更新
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 检查更新超时 | 主源不可达 | 切换到备用源或关闭代理重试 |
| 下载完成但未重启 | 权限或文件被占用 | 以管理员运行,关闭杀毒软件后重试 |
## 收集日志
- Wails 日志:`%LOCALAPPDATA%/voidraft/logs/*.log`
- 终端调试:运行 `wails3 dev` 并观察控制台输出。
- 若提交 Issue请附上系统版本、voidraft 版本、日志片段、复现步骤。
> 仍未解决?请到 GitHub Issues 提交反馈,并尽可能附上截图和日志。

View File

@@ -0,0 +1,46 @@
# 界面总览
![界面截图占位](/img/placeholder-main-ui.png)
> 替换为包含顶部工具栏、块区域、右侧小地图、底部状态栏的完整截图。
voidraft 的主窗口由四个区域组成:
| 区域 | 位置 | 作用 | 相关代码 |
| --- | --- | --- | --- |
| 工具栏 | 顶部浮层 | 文档切换、块语言选择、格式化、Markdown 预览、窗口置顶、更新提示、进入设置 | `frontend/src/components/toolbar/Toolbar.vue` |
| 编辑器主体 | 中央 | CodeMirror 6 视图承载块编辑、HTTP 运行器、翻译按钮等 | `frontend/src/views/editor/Editor.vue` + `extensions` |
| 导航辅助 | 右侧 | 小地图、滚动条、块徽标、HTTP 运行按钮 | `extensions/minimap`, `codeblock/decorations.ts` |
| 底部状态 | 左下角 | 行数、字符数、选区统计、文档脏状态 | `editorStore.documentStats` |
## 工具栏详解
| 项 | 说明 | 快捷入口 |
| --- | --- | --- |
| 文档切换器 | 展开后列出全部文档,支持搜索、创建、在新窗口打开 | 同步 `DocumentService.ListAllDocumentsMeta` |
| 块语言下拉 | 当前块语言,列表取自 `lang-parser/languages.ts`,支持搜索 | 鼠标选择或输入语言 token |
| Pin窗口置顶 | 临时 / 永久置顶切换,调用 `SystemService.SetWindowOnTop``config.general.alwaysOnTop` | Alt+Space自定义 |
| Format / Preview | 对当前块执行 Prettier 或打开 Markdown 预览 | `Ctrl+Shift+F` / 工具栏按钮 |
| 更新提示 | 轮询 `SelfUpdateService`,有更新时显示小点,可直接“检查/下载/重启” | 设置 > 更新 |
| 设置入口 | 跳转到 Vue Router 的 `/settings` 页面 | `Ctrl+,` |
## 多文档视图
- **标签页(可选)**:在设置 > 通用中启用“标签页模式”,`tabStore` 将当前文档加入 tab bar支持拖拽、批量关闭。
- **多窗口**:以文档列表右键「在新窗口中打开」或命令面板为入口。`WindowService` 会根据文档 ID 命名窗口,`WindowSnapService` 自动吸附。
- **系统托盘**:关闭窗口时默认最小化到托盘,可在托盘图标中重新唤醒或彻底退出。
## 面板与浮层
- **Markdown 预览**:针对选中的 Markdown 块,面板会贴在右侧,支持实时滚动同步、关闭动画。
- **HTTP 响应**:运行后在块底部自动插入 `### Response`,可展开查看头部/体/耗时。
- **翻译浮层**:选中文本后自动出现按钮,点击后显示结果卡片,附带复制、语种切换。
## 快捷状态
- **底部统计**
- `Ln`:当前块内行号。
- `Ch`:字符数。
- `Sel`:选区字符数。
- **右上角加载动画**:当编辑器实例加载或切换文档时显示,遵循 `enableLoadingAnimation` 设置。
## 建议截图
1. 默认深色主题 + 多块示例。
2. 打开 Markdown 预览 + 小地图。
3. 展示 HTTP 块运行按钮与响应卡片。
4. 展示标签页或多窗口。

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,15 @@
"lint": "eslint",
"lint:fix": "eslint --fix",
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js",
"build:mermaid-parser": "node src/views/editor/language/mermaid/build-parsers.js",
"test": "vitest",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"app:dev": "cd .. &&wails3 dev",
"app:build": "cd .. && wails3 task build",
"app:package": "cd .. && wails3 package",
"app:generate": "cd .. && wails3 generate bindings -ts"
},
"dependencies": {
"@codemirror/autocomplete": "^6.19.1",
@@ -42,56 +47,64 @@
"@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.1",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.6",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.2",
"@lezer/lr": "^1.4.3",
"@mdit/plugin-katex": "^0.23.2",
"@mdit/plugin-tasklist": "^0.22.2",
"@prettier/plugin-xml": "^3.4.2",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@toml-tools/lexer": "^1.0.0",
"@toml-tools/parser": "^1.0.0",
"@types/markdown-it": "^14.1.2",
"codemirror": "^6.0.2",
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
"colors-named-hex": "^1.0.2",
"groovy-beautify": "^0.0.17",
"highlight.js": "^11.11.1",
"hsl-matcher": "^1.2.4",
"java-parser": "^3.0.1",
"jsox": "^1.2.123",
"linguist-languages": "^9.1.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.12.1",
"npm": "^11.6.2",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.93.3",
"vue": "^3.5.22",
"sass": "^1.94.0",
"vue": "^3.5.24",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@eslint/js": "^9.39.0",
"@eslint/js": "^9.39.1",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.9.2",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest",
"cross-env": "^10.1.0",
"eslint": "^9.39.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.5.1",
"globals": "^16.5.0",
"happy-dom": "^20.0.10",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "^8.46.4",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.1.12",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.6",
"vitest": "^4.0.8",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.2"
"vue-tsc": "^3.1.3"
},
"overrides": {
"vite": "npm:rolldown-vite@latest"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,10 @@ export const FONT_OPTIONS = [
label: 'Open Sans',
value: '"Open Sans"'
},
{
label: 'Monocraft',
value: 'Monocraft'
},
// Common system fonts
{
label: 'Arial',
@@ -46,7 +50,7 @@ export const FONT_OPTIONS = [
label: 'System UI',
value: 'system-ui'
},
// Chinese fonts
{
label: 'Microsoft YaHei',
@@ -56,7 +60,7 @@ export const FONT_OPTIONS = [
label: 'PingFang SC',
value: '"PingFang SC"'
},
// Popular programming fonts
{
label: 'JetBrains Mono',

View File

@@ -0,0 +1,159 @@
// Enclose abbreviations in <abbr> tags
//
import MarkdownIt, {StateBlock, StateCore, Token} from 'markdown-it';
/**
* 环境接口,包含缩写定义
*/
interface AbbrEnv {
abbreviations?: { [key: string]: string };
}
/**
* markdown-it-abbr 插件
* 用于支持缩写语法
*/
export default function abbr_plugin(md: MarkdownIt): void {
const escapeRE = md.utils.escapeRE;
const arrayReplaceAt = md.utils.arrayReplaceAt;
// ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on;
// you can check character classes here:
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
const OTHER_CHARS = ' \r\n$+<=>^`|~';
const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source;
const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source;
function abbr_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
let labelEnd: number;
let pos = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
if (pos + 2 >= max) { return false; }
if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false; }
if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false; }
const labelStart = pos;
for (; pos < max; pos++) {
const ch = state.src.charCodeAt(pos);
if (ch === 0x5B /* [ */) {
return false;
} else if (ch === 0x5D /* ] */) {
labelEnd = pos;
break;
} else if (ch === 0x5C /* \ */) {
pos++;
}
}
if (labelEnd! < 0 || state.src.charCodeAt(labelEnd! + 1) !== 0x3A/* : */) {
return false;
}
if (silent) { return true; }
const label = state.src.slice(labelStart, labelEnd!).replace(/\\(.)/g, '$1');
const title = state.src.slice(labelEnd! + 2, max).trim();
if (label.length === 0) { return false; }
if (title.length === 0) { return false; }
const env = state.env as AbbrEnv;
if (!env.abbreviations) { env.abbreviations = {}; }
// prepend ':' to avoid conflict with Object.prototype members
if (typeof env.abbreviations[':' + label] === 'undefined') {
env.abbreviations[':' + label] = title;
}
state.line = startLine + 1;
return true;
}
function abbr_replace(state: StateCore): void {
const blockTokens = state.tokens;
const env = state.env as AbbrEnv;
if (!env.abbreviations) { return; }
const regSimple = new RegExp('(?:' +
Object.keys(env.abbreviations).map(function (x: string) {
return x.substr(1);
}).sort(function (a: string, b: string) {
return b.length - a.length;
}).map(escapeRE).join('|') +
')');
const regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' +
'(' + Object.keys(env.abbreviations).map(function (x: string) {
return x.substr(1);
}).sort(function (a: string, b: string) {
return b.length - a.length;
}).map(escapeRE).join('|') + ')' +
'($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])'
const reg = new RegExp(regText, 'g');
for (let j = 0, l = blockTokens.length; j < l; j++) {
if (blockTokens[j].type !== 'inline') { continue; }
let tokens = blockTokens[j].children!;
// We scan from the end, to keep position when new tags added.
for (let i = tokens.length - 1; i >= 0; i--) {
const currentToken = tokens[i];
if (currentToken.type !== 'text') { continue; }
let pos = 0;
const text = currentToken.content;
reg.lastIndex = 0;
const nodes: Token[] = [];
// fast regexp run to determine whether there are any abbreviated words
// in the current token
if (!regSimple.test(text)) { continue; }
let m: RegExpExecArray | null;
while ((m = reg.exec(text))) {
if (m.index > 0 || m[1].length > 0) {
const token = new state.Token('text', '', 0);
token.content = text.slice(pos, m.index + m[1].length);
nodes.push(token);
}
const token_o = new state.Token('abbr_open', 'abbr', 1);
token_o.attrs = [['title', env.abbreviations[':' + m[2]]]];
nodes.push(token_o);
const token_t = new state.Token('text', '', 0);
token_t.content = m[2];
nodes.push(token_t);
const token_c = new state.Token('abbr_close', 'abbr', -1);
nodes.push(token_c);
reg.lastIndex -= m[3].length;
pos = reg.lastIndex;
}
if (!nodes.length) { continue; }
if (pos < text.length) {
const token = new state.Token('text', '', 0);
token.content = text.slice(pos);
nodes.push(token);
}
// replace current node
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
}
}
}
md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: ['paragraph', 'reference'] });
md.core.ruler.after('linkify', 'abbr_replace', abbr_replace);
}

View File

@@ -0,0 +1,209 @@
// Process definition lists
//
import MarkdownIt, { StateBlock, Token } from 'markdown-it';
/**
* markdown-it-deflist 插件
* 用于支持定义列表语法
*/
export default function deflist_plugin(md: MarkdownIt): void {
const isSpace = md.utils.isSpace;
// Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail.
function skipMarker(state: StateBlock, line: number): number {
let start = state.bMarks[line] + state.tShift[line];
const max = state.eMarks[line];
if (start >= max) { return -1; }
// Check bullet
const marker = state.src.charCodeAt(start++);
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1; }
const pos = state.skipSpaces(start);
// require space after ":"
if (start === pos) { return -1; }
// no empty definitions, e.g. " : "
if (pos >= max) { return -1; }
return start;
}
function markTightParagraphs(state: StateBlock, idx: number): void {
const level = state.level + 2;
for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
state.tokens[i + 2].hidden = true;
state.tokens[i].hidden = true;
i += 2;
}
}
}
function deflist(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) { return false; }
return skipMarker(state, startLine) >= 0;
}
let nextLine = startLine + 1;
if (nextLine >= endLine) { return false; }
if (state.isEmpty(nextLine)) {
nextLine++;
if (nextLine >= endLine) { return false; }
}
if (state.sCount[nextLine] < state.blkIndent) { return false; }
let contentStart = skipMarker(state, nextLine);
if (contentStart < 0) { return false; }
// Start list
const listTokIdx = state.tokens.length;
let tight = true;
const token_dl_o: Token = state.push('dl_open', 'dl', 1);
const listLines: [number, number] = [startLine, 0];
token_dl_o.map = listLines;
//
// Iterate list items
//
let dtLine = startLine;
let ddLine = nextLine;
// One definition list can contain multiple DTs,
// and one DT can be followed by multiple DDs.
//
// Thus, there is two loops here, and label is
// needed to break out of the second one
//
/* eslint no-labels:0,block-scoped-var:0 */
OUTER:
for (;;) {
let prevEmptyEnd = false;
const token_dt_o: Token = state.push('dt_open', 'dt', 1);
token_dt_o.map = [dtLine, dtLine];
const token_i: Token = state.push('inline', '', 0);
token_i.map = [dtLine, dtLine];
token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim();
token_i.children = [];
state.push('dt_close', 'dt', -1);
for (;;) {
const token_dd_o: Token = state.push('dd_open', 'dd', 1);
const itemLines: [number, number] = [nextLine, 0];
token_dd_o.map = itemLines;
let pos = contentStart;
const max = state.eMarks[ddLine];
let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]);
while (pos < max) {
const ch = state.src.charCodeAt(pos);
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
pos++;
}
contentStart = pos;
const oldTight = state.tight;
const oldDDIndent = state.ddIndent;
const oldIndent = state.blkIndent;
const oldTShift = state.tShift[ddLine];
const oldSCount = state.sCount[ddLine];
const oldParentType = state.parentType;
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2;
state.tShift[ddLine] = contentStart - state.bMarks[ddLine];
state.sCount[ddLine] = offset;
state.tight = true;
state.parentType = 'deflist' as any;
state.md.block.tokenize(state, ddLine, endLine);
// If any of list item is tight, mark list as tight
if (!state.tight || prevEmptyEnd) {
tight = false;
}
// Item become loose if finish with empty line,
// but we should filter last element, because it means list finish
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1);
state.tShift[ddLine] = oldTShift;
state.sCount[ddLine] = oldSCount;
state.tight = oldTight;
state.parentType = oldParentType;
state.blkIndent = oldIndent;
state.ddIndent = oldDDIndent;
state.push('dd_close', 'dd', -1);
itemLines[1] = nextLine = state.line;
if (nextLine >= endLine) { break OUTER; }
if (state.sCount[nextLine] < state.blkIndent) { break OUTER; }
contentStart = skipMarker(state, nextLine);
if (contentStart < 0) { break; }
ddLine = nextLine;
// go to the next loop iteration:
// insert DD tag and repeat checking
}
if (nextLine >= endLine) { break; }
dtLine = nextLine;
if (state.isEmpty(dtLine)) { break; }
if (state.sCount[dtLine] < state.blkIndent) { break; }
ddLine = dtLine + 1;
if (ddLine >= endLine) { break; }
if (state.isEmpty(ddLine)) { ddLine++; }
if (ddLine >= endLine) { break; }
if (state.sCount[ddLine] < state.blkIndent) { break; }
contentStart = skipMarker(state, ddLine);
if (contentStart < 0) { break; }
// go to the next loop iteration:
// insert DT and DD tags and repeat checking
}
// Finilize list
state.push('dl_close', 'dl', -1);
listLines[1] = nextLine;
state.line = nextLine;
// mark paragraphs tight if needed
if (tight) {
markTightParagraphs(state, listTokIdx);
}
return true;
}
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: ['paragraph', 'reference', 'blockquote'] });
}

View File

@@ -0,0 +1,4 @@
export { default as bare } from './lib/bare';
export { default as light } from './lib/light';
export { default as full } from './lib/full';

View File

@@ -0,0 +1,26 @@
import MarkdownIt from 'markdown-it';
import emoji_html from './render';
import emoji_replace from './replace';
import normalize_opts, { EmojiOptions } from './normalize_opts';
/**
* Bare emoji 插件(不包含预定义的 emoji 数据)
*/
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
const defaults: EmojiOptions = {
defs: {},
shortcuts: {},
enabled: []
};
const opts = normalize_opts(md.utils.assign({}, defaults, options || {}) as EmojiOptions);
md.renderer.rules.emoji = emoji_html;
md.core.ruler.after(
'linkify',
'emoji',
emoji_replace(md, opts.defs, opts.shortcuts, opts.scanRE, opts.replaceRE)
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
// Generated, don't edit
import { EmojiDefs } from '../normalize_opts';
const emojies: EmojiDefs = {
"grinning": "😀",
"smiley": "😃",
"smile": "😄",
"grin": "😁",
"laughing": "😆",
"satisfied": "😆",
"sweat_smile": "😅",
"joy": "😂",
"wink": "😉",
"blush": "😊",
"innocent": "😇",
"heart_eyes": "😍",
"kissing_heart": "😘",
"kissing": "😗",
"kissing_closed_eyes": "😚",
"kissing_smiling_eyes": "😙",
"yum": "😋",
"stuck_out_tongue": "😛",
"stuck_out_tongue_winking_eye": "😜",
"stuck_out_tongue_closed_eyes": "😝",
"neutral_face": "😐",
"expressionless": "😑",
"no_mouth": "😶",
"smirk": "😏",
"unamused": "😒",
"relieved": "😌",
"pensive": "😔",
"sleepy": "😪",
"sleeping": "😴",
"mask": "😷",
"dizzy_face": "😵",
"sunglasses": "😎",
"confused": "😕",
"worried": "😟",
"open_mouth": "😮",
"hushed": "😯",
"astonished": "😲",
"flushed": "😳",
"frowning": "😦",
"anguished": "😧",
"fearful": "😨",
"cold_sweat": "😰",
"disappointed_relieved": "😥",
"cry": "😢",
"sob": "😭",
"scream": "😱",
"confounded": "😖",
"persevere": "😣",
"disappointed": "😞",
"sweat": "😓",
"weary": "😩",
"tired_face": "😫",
"rage": "😡",
"pout": "😡",
"angry": "😠",
"smiling_imp": "😈",
"smiley_cat": "😺",
"smile_cat": "😸",
"joy_cat": "😹",
"heart_eyes_cat": "😻",
"smirk_cat": "😼",
"kissing_cat": "😽",
"scream_cat": "🙀",
"crying_cat_face": "😿",
"pouting_cat": "😾",
"heart": "❤️",
"hand": "✋",
"raised_hand": "✋",
"v": "✌️",
"point_up": "☝️",
"fist_raised": "✊",
"fist": "✊",
"monkey_face": "🐵",
"cat": "🐱",
"cow": "🐮",
"mouse": "🐭",
"coffee": "☕",
"hotsprings": "♨️",
"anchor": "⚓",
"airplane": "✈️",
"hourglass": "⌛",
"watch": "⌚",
"sunny": "☀️",
"star": "⭐",
"cloud": "☁️",
"umbrella": "☔",
"zap": "⚡",
"snowflake": "❄️",
"sparkles": "✨",
"black_joker": "🃏",
"mahjong": "🀄",
"phone": "☎️",
"telephone": "☎️",
"envelope": "✉️",
"pencil2": "✏️",
"black_nib": "✒️",
"scissors": "✂️",
"wheelchair": "♿",
"warning": "⚠️",
"aries": "♈",
"taurus": "♉",
"gemini": "♊",
"cancer": "♋",
"leo": "♌",
"virgo": "♍",
"libra": "♎",
"scorpius": "♏",
"sagittarius": "♐",
"capricorn": "♑",
"aquarius": "♒",
"pisces": "♓",
"heavy_multiplication_x": "✖️",
"heavy_plus_sign": "",
"heavy_minus_sign": "",
"heavy_division_sign": "➗",
"bangbang": "‼️",
"interrobang": "⁉️",
"question": "❓",
"grey_question": "❔",
"grey_exclamation": "❕",
"exclamation": "❗",
"heavy_exclamation_mark": "❗",
"wavy_dash": "〰️",
"recycle": "♻️",
"white_check_mark": "✅",
"ballot_box_with_check": "☑️",
"heavy_check_mark": "✔️",
"x": "❌",
"negative_squared_cross_mark": "❎",
"curly_loop": "➰",
"loop": "➿",
"part_alternation_mark": "〽️",
"eight_spoked_asterisk": "✳️",
"eight_pointed_black_star": "✴️",
"sparkle": "❇️",
"copyright": "©️",
"registered": "®️",
"tm": "™️",
"information_source": "",
"m": "Ⓜ️",
"black_circle": "⚫",
"white_circle": "⚪",
"black_large_square": "⬛",
"white_large_square": "⬜",
"black_medium_square": "◼️",
"white_medium_square": "◻️",
"black_medium_small_square": "◾",
"white_medium_small_square": "◽",
"black_small_square": "▪️",
"white_small_square": "▫️"
};
export default emojies;

View File

@@ -0,0 +1,45 @@
// Emoticons -> Emoji mapping.
//
// (!) Some patterns skipped, to avoid collisions
// without increase matcher complicity. Than can change in future.
//
// Places to look for more emoticons info:
//
// - http://en.wikipedia.org/wiki/List_of_emoticons#Western
// - https://github.com/wooorm/emoticon/blob/master/Support.md
// - http://factoryjoe.com/projects/emoticons/
//
import { EmojiShortcuts } from '../normalize_opts';
const shortcuts: EmojiShortcuts = {
angry: ['>:(', '>:-('],
blush: [':")', ':-")'],
broken_heart: ['</3', '<\\3'],
// :\ and :-\ not used because of conflict with markdown escaping
confused: [':/', ':-/'], // twemoji shows question
cry: [":'(", ":'-(", ':,(', ':,-('],
frowning: [':(', ':-('],
heart: ['<3'],
imp: [']:(', ']:-('],
innocent: ['o:)', 'O:)', 'o:-)', 'O:-)', '0:)', '0:-)'],
joy: [":')", ":'-)", ':,)', ':,-)', ":'D", ":'-D", ':,D', ':,-D'],
kissing: [':*', ':-*'],
laughing: ['x-)', 'X-)'],
neutral_face: [':|', ':-|'],
open_mouth: [':o', ':-o', ':O', ':-O'],
rage: [':@', ':-@'],
smile: [':D', ':-D'],
smiley: [':)', ':-)'],
smiling_imp: [']:)', ']:-)'],
sob: [":,'(", ":,'-(", ';(', ';-('],
stuck_out_tongue: [':P', ':-P'],
sunglasses: ['8-)', 'B-)'],
sweat: [',:(', ',:-('],
sweat_smile: [',:)', ',:-)'],
unamused: [':s', ':-S', ':z', ':-Z', ':$', ':-$'],
wink: [';)', ';-)']
};
export default shortcuts;

View File

@@ -0,0 +1,21 @@
import MarkdownIt from 'markdown-it';
import emojies_defs from './data/full';
import emojies_shortcuts from './data/shortcuts';
import bare_emoji_plugin from './bare';
import { EmojiOptions } from './normalize_opts';
/**
* Full emoji 插件(包含完整的 emoji 数据)
*/
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
const defaults: EmojiOptions = {
defs: emojies_defs,
shortcuts: emojies_shortcuts,
enabled: []
};
const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions;
bare_emoji_plugin(md, opts);
}

View File

@@ -0,0 +1,21 @@
import MarkdownIt from 'markdown-it';
import emojies_defs from './data/light';
import emojies_shortcuts from './data/shortcuts';
import bare_emoji_plugin from './bare';
import { EmojiOptions } from './normalize_opts';
/**
* Light emoji 插件(包含常用的 emoji 数据)
*/
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
const defaults: EmojiOptions = {
defs: emojies_defs,
shortcuts: emojies_shortcuts,
enabled: []
};
const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions;
bare_emoji_plugin(md, opts);
}

View File

@@ -0,0 +1,95 @@
/**
* Emoji 定义类型
*/
export interface EmojiDefs {
[key: string]: string;
}
/**
* Emoji 快捷方式类型
*/
export interface EmojiShortcuts {
[key: string]: string | string[];
}
/**
* 输入选项接口
*/
export interface EmojiOptions {
defs: EmojiDefs;
shortcuts: EmojiShortcuts;
enabled: string[];
}
/**
* 标准化后的选项接口
*/
export interface NormalizedEmojiOptions {
defs: EmojiDefs;
shortcuts: { [key: string]: string };
scanRE: RegExp;
replaceRE: RegExp;
}
/**
* 转义正则表达式特殊字符
*/
function quoteRE(str: string): string {
return str.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
}
/**
* 将输入选项转换为更可用的格式并编译搜索正则表达式
*/
export default function normalize_opts(options: EmojiOptions): NormalizedEmojiOptions {
let emojies = options.defs;
// Filter emojies by whitelist, if needed
if (options.enabled.length) {
emojies = Object.keys(emojies).reduce((acc: EmojiDefs, key: string) => {
if (options.enabled.indexOf(key) >= 0) acc[key] = emojies[key];
return acc;
}, {});
}
// Flatten shortcuts to simple object: { alias: emoji_name }
const shortcuts = Object.keys(options.shortcuts).reduce((acc: { [key: string]: string }, key: string) => {
// Skip aliases for filtered emojies, to reduce regexp
if (!emojies[key]) return acc;
if (Array.isArray(options.shortcuts[key])) {
(options.shortcuts[key] as string[]).forEach((alias: string) => { acc[alias] = key; });
return acc;
}
acc[options.shortcuts[key] as string] = key;
return acc;
}, {});
const keys = Object.keys(emojies);
let names: string;
// If no definitions are given, return empty regex to avoid replacements with 'undefined'.
if (keys.length === 0) {
names = '^$';
} else {
// Compile regexp
names = keys
.map((name: string) => { return `:${name}:`; })
.concat(Object.keys(shortcuts))
.sort()
.reverse()
.map((name: string) => { return quoteRE(name); })
.join('|');
}
const scanRE = RegExp(names);
const replaceRE = RegExp(names, 'g');
return {
defs: emojies,
shortcuts,
scanRE,
replaceRE
};
}

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