Compare commits
12 Commits
v1.5.2
...
689b0d5d14
| Author | SHA1 | Date | |
|---|---|---|---|
| 689b0d5d14 | |||
| a058e62595 | |||
| 8571fc0f5c | |||
| 4dad0a86b3 | |||
| 3168b7ff43 | |||
| d002a5be5a | |||
| 24a550463c | |||
| 14ae3e80c4 | |||
| e4d3969e95 | |||
| 0b16d1d4ac | |||
| 300514531d | |||
| 6a4780b002 |
67
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
|
||||||
|
#
|
||||||
|
name: Deploy VitePress site to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
# 在针对 `main` 分支的推送上运行。如果你
|
||||||
|
# 使用 `master` 分支作为默认分支,请将其更改为 `master`
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
# 允许你从 Actions 选项卡手动运行此工作流程
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
|
||||||
|
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# 构建工作
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
|
||||||
|
# - uses: pnpm/action-setup@v3 # 如果使用 pnpm,请取消此区域注释
|
||||||
|
# with:
|
||||||
|
# version: 9
|
||||||
|
# - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && npm ci
|
||||||
|
- name: Build with VitePress
|
||||||
|
run: cd frontend && npm run docs:build
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: frontend/docs/.vitepress/dist
|
||||||
|
|
||||||
|
# 部署工作
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Deploy
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
4
.gitignore
vendored
@@ -4,4 +4,6 @@ frontend/dist
|
|||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
build/linux/appimage/build
|
build/linux/appimage/build
|
||||||
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||||
.idea
|
.idea
|
||||||
|
frontend/docs/.vitepress/cache/
|
||||||
|
frontend/docs/.vitepress/dist/
|
||||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 0 B |
@@ -39,7 +39,7 @@ tasks:
|
|||||||
summary: Generates Windows `.syso` file
|
summary: Generates Windows `.syso` file
|
||||||
dir: build
|
dir: build
|
||||||
cmds:
|
cmds:
|
||||||
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
- wails3 generate syso -arch {{.ARCH}} -icon windows/favicon_256x256.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||||
vars:
|
vars:
|
||||||
ARCH: '{{.ARCH | default ARCH}}'
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
voidraft.landaiqing.cn
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>voidraft - Changelog</title>
|
|
||||||
<link rel="stylesheet" href="css/styles.css">
|
|
||||||
<link rel="stylesheet" href="css/changelog.css">
|
|
||||||
<link rel="icon" href="img/favicon.ico">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
||||||
</head>
|
|
||||||
<body class="theme-dark">
|
|
||||||
<div class="container">
|
|
||||||
<!-- 主卡片 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h1 class="card-title" data-en="voidraft Changelog" data-zh="voidraft 更新日志">voidraft Changelog</h1>
|
|
||||||
<div class="card-controls">
|
|
||||||
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
|
|
||||||
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
|
|
||||||
</button>
|
|
||||||
<button id="lang-toggle" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-language"></i> 中/EN
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-content">
|
|
||||||
<!-- 导航区域 -->
|
|
||||||
<div class="nav-links">
|
|
||||||
<a href="index.html" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-home"></i> <span data-en="Home" data-zh="首页">Home</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary">
|
|
||||||
<i class="fab fa-github"></i> <span data-en="Source Code" data-zh="源代码">Source Code</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载中提示 -->
|
|
||||||
<div id="loading" class="loading-container">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p data-en="Loading releases..." data-zh="正在加载版本信息...">Loading releases...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 更新日志内容 -->
|
|
||||||
<div id="changelog" class="changelog-container">
|
|
||||||
<!-- 通过JavaScript动态填充内容 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误信息 -->
|
|
||||||
<div id="error-message" class="error-container" style="display: none;">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<p data-en="Failed to load release information. Please try again later."
|
|
||||||
data-zh="加载版本信息失败,请稍后再试。">Failed to load release information. Please try again later.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 页脚 -->
|
|
||||||
<footer class="footer">
|
|
||||||
<p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 voidraft - An elegant text snippet recording tool designed for developers</p>
|
|
||||||
<div class="footer-links">
|
|
||||||
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
|
|
||||||
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
|
|
||||||
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/script.js"></script>
|
|
||||||
<script src="js/changelog.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
/* 更新日志页面样式 */
|
|
||||||
.nav-links {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-left-color: var(--primary-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .loading-spinner {
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
border-left-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--error-color);
|
|
||||||
padding: 20px;
|
|
||||||
border: 2px dashed var(--error-color);
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: rgba(var(--card-bg-rgb), 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container i {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 更新日志容器 */
|
|
||||||
.changelog-container {
|
|
||||||
display: none;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
border-left: 4px solid var(--primary-color);
|
|
||||||
padding-left: 20px;
|
|
||||||
background-color: rgba(var(--card-bg-rgb), 0.5);
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-version {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-date {
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-left: 10px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-badge.pre-release {
|
|
||||||
background-color: var(--warning-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-description {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-assets {
|
|
||||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-assets-title {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-list {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-icon {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-name {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-size {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 资源下载按钮 */
|
|
||||||
.download-btn {
|
|
||||||
margin-left: 10px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn:hover {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content {
|
|
||||||
line-height: 1.8;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h1,
|
|
||||||
.markdown-content h2,
|
|
||||||
.markdown-content h3 {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
padding-left: 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 2px dashed var(--border-color);
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content br {
|
|
||||||
display: block;
|
|
||||||
content: "";
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content code {
|
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
|
||||||
background-color: rgba(128, 128, 128, 0.1);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre {
|
|
||||||
background-color: rgba(128, 128, 128, 0.1);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre code {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-source {
|
|
||||||
padding: 10px 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: right;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-source a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-source a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Markdown内容样式增强 */
|
|
||||||
.markdown-content blockquote {
|
|
||||||
border-left: 4px solid var(--primary-color);
|
|
||||||
padding: 10px 15px;
|
|
||||||
margin: 15px 0;
|
|
||||||
background-color: rgba(var(--light-bg-rgb), 0.5);
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
padding-left: 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动设备响应式优化 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.release-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-assets {
|
|
||||||
padding: 12px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-item {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 12px 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-name {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-size {
|
|
||||||
margin-left: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
margin-left: 10px;
|
|
||||||
padding: 5px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.release {
|
|
||||||
padding-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-icon {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-size {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保日志页面页脚样式一致 */
|
|
||||||
.footer {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-text {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'IBM Plex Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'IBM Plex Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'IBM Plex Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'IBM Plex Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'IBM Plex Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@@ -1,717 +0,0 @@
|
|||||||
@import url('./space-mono-font.css');
|
|
||||||
@import url('./ibm-plex-mono-font.css');
|
|
||||||
|
|
||||||
/* 浅色主题 */
|
|
||||||
:root {
|
|
||||||
--bg-color: #fefefe;
|
|
||||||
--text-color: #000000;
|
|
||||||
--primary-color: #F08080;
|
|
||||||
--primary-color-rgb: 240, 128, 128;
|
|
||||||
--secondary-color: #ff006e;
|
|
||||||
--accent-color: #073B4C;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--card-bg-rgb: 255, 255, 255;
|
|
||||||
--border-color: #000000;
|
|
||||||
--light-bg: #f0f0f0;
|
|
||||||
--light-bg-rgb: 240, 240, 240;
|
|
||||||
--shadow-color: rgba(240, 128, 128, 0.5);
|
|
||||||
--success-color: #27c93f;
|
|
||||||
--warning-color: #FFD166;
|
|
||||||
--error-color: #ff006e;
|
|
||||||
--info-color: #118ab2;
|
|
||||||
--code-bg: #ffffff;
|
|
||||||
--code-bg-rgb: 255, 255, 255;
|
|
||||||
--preview-header-bg: #f0f0f0;
|
|
||||||
--preview-header-bg-rgb: 240, 240, 240;
|
|
||||||
--grid-color-1: rgba(0, 0, 0, 0.08);
|
|
||||||
--grid-color-2: rgba(0, 0, 0, 0.05);
|
|
||||||
--header-title-color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 暗色主题变量 */
|
|
||||||
.theme-dark {
|
|
||||||
--bg-color: #121212;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--primary-color: #F08080;
|
|
||||||
--primary-color-rgb: 240, 128, 128;
|
|
||||||
--secondary-color: #ff006e;
|
|
||||||
--accent-color: #118ab2;
|
|
||||||
--card-bg: #1e1e1e;
|
|
||||||
--card-bg-rgb: 30, 30, 30;
|
|
||||||
--border-color: #ffffff;
|
|
||||||
--light-bg: #2a2a2a;
|
|
||||||
--light-bg-rgb: 42, 42, 42;
|
|
||||||
--shadow-color: rgba(240, 128, 128, 0.5);
|
|
||||||
--success-color: #27c93f;
|
|
||||||
--warning-color: #FFD166;
|
|
||||||
--error-color: #ff006e;
|
|
||||||
--info-color: #118ab2;
|
|
||||||
--code-bg: #1e1e1e;
|
|
||||||
--code-bg-rgb: 30, 30, 30;
|
|
||||||
--preview-header-bg: #252526;
|
|
||||||
--preview-header-bg-rgb: 37, 37, 38;
|
|
||||||
--grid-color-1: rgba(255, 255, 255, 0.08);
|
|
||||||
--grid-color-2: rgba(255, 255, 255, 0.05);
|
|
||||||
--header-title-color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主题切换和语言切换的过渡效果 */
|
|
||||||
.theme-transition,
|
|
||||||
.theme-transition *,
|
|
||||||
.lang-transition,
|
|
||||||
.lang-transition * {
|
|
||||||
transition: all 0.3s ease !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gridMove {
|
|
||||||
0% {
|
|
||||||
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
background-image:
|
|
||||||
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
|
||||||
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
|
||||||
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
|
||||||
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
|
||||||
background-position: center;
|
|
||||||
animation: gridMove 40s linear infinite;
|
|
||||||
font-family: 'Space Mono', monospace;
|
|
||||||
color: var(--text-color);
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 20px;
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片容器 */
|
|
||||||
.card {
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
background-image:
|
|
||||||
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
|
||||||
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
|
||||||
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
|
||||||
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
|
||||||
background-position: center;
|
|
||||||
border: 4px solid var(--border-color);
|
|
||||||
box-shadow: 12px 12px 0 var(--shadow-color);
|
|
||||||
margin-bottom: 40px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 16px 16px 0 var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片头部 */
|
|
||||||
.card-header {
|
|
||||||
background-color: rgba(var(--primary-color-rgb), 0.9);
|
|
||||||
border-bottom: 4px solid var(--border-color);
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--header-title-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: var(--secondary-color);
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
border: 3px solid var(--border-color);
|
|
||||||
box-shadow: 4px 4px 0 var(--shadow-color);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'Space Mono', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background: var(--card-bg);
|
|
||||||
color: var(--primary-color);
|
|
||||||
border: 3px solid var(--primary-color);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--light-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--card-bg);
|
|
||||||
color: var(--primary-color);
|
|
||||||
border: 3px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片内容 */
|
|
||||||
.card-content {
|
|
||||||
padding: 30px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
background-color: rgba(var(--card-bg-rgb), 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo区域 */
|
|
||||||
.logo-container {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-frame {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 4px solid var(--border-color);
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-image {
|
|
||||||
width: 130px;
|
|
||||||
height: 130px;
|
|
||||||
object-fit: contain;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagline {
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 10px 0 0;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 介绍区域 */
|
|
||||||
.intro-box {
|
|
||||||
border: 2px dashed var(--border-color);
|
|
||||||
padding: 20px;
|
|
||||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
|
||||||
margin-bottom: 30px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-text {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮组 */
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 特性网格 */
|
|
||||||
.features-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 30px;
|
|
||||||
margin: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 特性卡片 */
|
|
||||||
.feature-card {
|
|
||||||
background-color: rgba(var(--card-bg-rgb), 0.8);
|
|
||||||
border: 3px solid var(--border-color);
|
|
||||||
box-shadow: 5px 5px 0 var(--shadow-color);
|
|
||||||
padding: 20px;
|
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 7px 7px 0 var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览区域 */
|
|
||||||
.preview-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.preview-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览窗口 */
|
|
||||||
.preview-window {
|
|
||||||
border: 3px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 10px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 300px;
|
|
||||||
background-color: rgba(var(--card-bg-rgb), 0.7);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 5px 5px 0 var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览头部 */
|
|
||||||
.preview-header {
|
|
||||||
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-btn {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 0.5px solid rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-btn:nth-child(1) {
|
|
||||||
background-color: #ff5f56;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-btn:nth-child(2) {
|
|
||||||
background-color: #ffbd2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-btn:nth-child(3) {
|
|
||||||
background-color: #27c93f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-title {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.8;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览内容 */
|
|
||||||
.preview-content {
|
|
||||||
padding: 15px;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: rgba(var(--code-bg-rgb), 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码块容器 */
|
|
||||||
.code-block-wrapper {
|
|
||||||
background-color: rgba(var(--code-bg-rgb), 0.8);
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 块头部 */
|
|
||||||
.block-header {
|
|
||||||
background-color: rgba(var(--light-bg-rgb), 0.8);
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-language {
|
|
||||||
color: rgba(128, 128, 128, 0.8);
|
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-language::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin-right: 5px;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block {
|
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre;
|
|
||||||
tab-size: 4;
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .code-block-wrapper {
|
|
||||||
border-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .block-header {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
border-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .block-language {
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .block-language::before {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .code-block {
|
|
||||||
color: #d4d4d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码高亮 */
|
|
||||||
.theme-dark .keyword { color: #c586c0; }
|
|
||||||
.theme-dark .function { color: #dcdcaa; }
|
|
||||||
.theme-dark .variable { color: #9cdcfe; }
|
|
||||||
.theme-dark .string { color: #ce9178; }
|
|
||||||
.theme-dark .comment { color: #6a9955; }
|
|
||||||
.theme-dark .class { color: #4ec9b0; }
|
|
||||||
.theme-dark .parameter { color: #9cdcfe; }
|
|
||||||
.theme-dark .built-in { color: #4ec9b0; }
|
|
||||||
|
|
||||||
/* 浅色主题代码高亮 */
|
|
||||||
.keyword { color: #af00db; }
|
|
||||||
.function { color: #795e26; }
|
|
||||||
.variable { color: #001080; }
|
|
||||||
.string { color: #a31515; }
|
|
||||||
.comment { color: #008000; }
|
|
||||||
.class { color: #267f99; }
|
|
||||||
.parameter { color: #001080; }
|
|
||||||
.built-in { color: #267f99; }
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
border: none;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-theme-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .dark-theme-img {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.theme-dark) .dark-theme-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.theme-dark) .light-theme-img {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 技术栈列表 */
|
|
||||||
.tech-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 技术栈列表 */
|
|
||||||
.tech-item {
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-icon {
|
|
||||||
margin-right: 15px;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
font-size: 20px;
|
|
||||||
width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-name {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页脚 */
|
|
||||||
.footer {
|
|
||||||
border-top: 2px solid var(--border-color);
|
|
||||||
padding: 20px 0;
|
|
||||||
margin-top: 40px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
background-color: transparent;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-text {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.button-group {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.features-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.card-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-frame {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-image {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 针对移动设备的响应式优化 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览区域优化 */
|
|
||||||
.preview-content {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-header {
|
|
||||||
padding: 6px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 日志界面导航链接优化 */
|
|
||||||
.nav-links {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links .btn {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
/* 特性卡片优化 */
|
|
||||||
.features-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览窗口优化 */
|
|
||||||
.preview-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-window {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 技术栈列表小屏幕优化 */
|
|
||||||
.tech-item {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-desc {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 40px; /* 图标宽度+右边距 */
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 日志界面资源列表项优化 */
|
|
||||||
.asset-item {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-name {
|
|
||||||
width: 100%;
|
|
||||||
word-break: break-all;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-size {
|
|
||||||
order: 2;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
order: 3;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页脚链接优化 */
|
|
||||||
.footer {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
margin-top: 15px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB |
256
docs/index.html
@@ -1,256 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>voidraft - An elegant text snippet recording tool designed for developers.</title>
|
|
||||||
<meta name="description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
|
|
||||||
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, voidraft">
|
|
||||||
<meta name="author" content="voidraft Team">
|
|
||||||
<meta name="robots" content="index, follow">
|
|
||||||
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
|
|
||||||
|
|
||||||
<!-- Internationalization / hreflang -->
|
|
||||||
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
|
|
||||||
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
|
|
||||||
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
|
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
<meta property="og:url" content="https://landaiqing.github.io/voidraft/">
|
|
||||||
<meta property="og:title" content="voidraft - An elegant text snippet recording tool designed for developers">
|
|
||||||
<meta property="og:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
|
|
||||||
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
|
|
||||||
<meta property="og:site_name" content="voidraft">
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta property="twitter:card" content="summary_large_image">
|
|
||||||
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
|
|
||||||
<meta property="twitter:title" content="voidraft - An elegant text snippet recording tool designed for developers">
|
|
||||||
<meta property="twitter:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
|
|
||||||
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="./css/styles.css">
|
|
||||||
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
||||||
|
|
||||||
<!-- Structured Data -->
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "SoftwareApplication",
|
|
||||||
"name": "voidraft",
|
|
||||||
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
|
|
||||||
"url": "https://landaiqing.github.io/voidraft/",
|
|
||||||
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
|
|
||||||
"author": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "voidraft"
|
|
||||||
},
|
|
||||||
"operatingSystem": ["Windows", "macOS", "Linux"],
|
|
||||||
"applicationCategory": "DeveloperApplication",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
|
|
||||||
"softwareVersion": "Latest",
|
|
||||||
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
|
|
||||||
"codeRepository": "https://github.com/landaiqing/voidraft"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body class="theme-dark">
|
|
||||||
<div class="container">
|
|
||||||
<!-- 主卡片 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h1 class="card-title">voidraft</h1>
|
|
||||||
<div class="card-controls">
|
|
||||||
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
|
|
||||||
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
|
|
||||||
</button>
|
|
||||||
<button id="lang-toggle" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-language"></i> 中/EN
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-content">
|
|
||||||
<!-- Logo和介绍 -->
|
|
||||||
<div class="logo-container">
|
|
||||||
<div class="logo-frame">
|
|
||||||
<img src="img/logo.png" alt="voidraft Logo" class="logo-image">
|
|
||||||
</div>
|
|
||||||
<h2 class="logo-text" data-en="voidraft" data-zh="voidraft">voidraft</h2>
|
|
||||||
<p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="intro-box">
|
|
||||||
<p class="intro-text" data-en="Designed for developers to record, organize, and manage various text snippets anytime, anywhere." data-zh="专为开发者打造,随时随地记录、整理和管理各种文本片段。">Designed for developers to record, organize, and manage various text snippets anytime, anywhere.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<a href="https://github.com/landaiqing/voidraft/releases" class="btn" data-en="Download" data-zh="下载">
|
|
||||||
<i class="fas fa-download"></i> Download
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary" data-en="Source Code" data-zh="源代码">
|
|
||||||
<i class="fab fa-github"></i> Source Code
|
|
||||||
</a>
|
|
||||||
<a href="changelog.html" class="btn btn-secondary" data-en="Changelog" data-zh="更新日志">
|
|
||||||
<i class="fas fa-history"></i> Changelog
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 特性部分 -->
|
|
||||||
<h2 data-en="Core Features" data-zh="核心特性">Core Features</h2>
|
|
||||||
|
|
||||||
<div class="features-grid">
|
|
||||||
<div class="feature-card">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i class="fas fa-code"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="feature-title" data-en="Developer-Friendly" data-zh="开发者友好">Developer-Friendly</h3>
|
|
||||||
<p class="feature-desc" data-en="Multi-language code blocks with syntax highlighting for 30+ programming languages" data-zh="多语言代码块支持,为30+种编程语言提供语法高亮">Multi-language code blocks with syntax highlighting for 30+ programming languages</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-card">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i class="fas fa-magic"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="feature-title" data-en="Code Formatting" data-zh="代码格式化">Code Formatting</h3>
|
|
||||||
<p class="feature-desc" data-en="Built-in Prettier support for one-click code beautification" data-zh="内置Prettier支持,一键美化代码">Built-in Prettier support for one-click code beautification</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-card">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i class="fas fa-palette"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="feature-title" data-en="Custom Themes" data-zh="自定义主题">Custom Themes</h3>
|
|
||||||
<p class="feature-desc" data-en="Dark/Light themes with full customization options" data-zh="深色/浅色主题,支持完全自定义">Dark/Light themes with full customization options</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-card">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i class="fas fa-clone"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="feature-title" data-en="Multi-Window" data-zh="多窗口支持">Multi-Window</h3>
|
|
||||||
<p class="feature-desc" data-en="Edit multiple documents simultaneously" data-zh="同时编辑多个文档">Edit multiple documents simultaneously</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-card">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i class="fas fa-layer-group"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="feature-title" data-en="Block Editing" data-zh="块状编辑">Block Editing</h3>
|
|
||||||
<p class="feature-desc" data-en="Split content into independent code blocks with different language settings" data-zh="将内容分割为独立的代码块,每个块可设置不同语言">Split content into independent code blocks with different language settings</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-card">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i class="fas fa-puzzle-piece"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="feature-title" data-en="Extensions" data-zh="丰富扩展">Extensions</h3>
|
|
||||||
<p class="feature-desc" data-en="Rainbow brackets, VSCode-style search, color picker, translation tool, and more" data-zh="彩虹括号、VSCode风格搜索、颜色选择器、翻译工具等多种扩展">Rainbow brackets, VSCode-style search, color picker, translation tool, and more</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 预览部分 -->
|
|
||||||
<h2 data-en="Preview" data-zh="预览">Preview</h2>
|
|
||||||
|
|
||||||
<div class="preview-container">
|
|
||||||
<div class="preview-window">
|
|
||||||
<div class="preview-header">
|
|
||||||
<div class="preview-controls">
|
|
||||||
<span class="preview-btn"></span>
|
|
||||||
<span class="preview-btn"></span>
|
|
||||||
<span class="preview-btn"></span>
|
|
||||||
</div>
|
|
||||||
<div class="preview-title">voidraft</div>
|
|
||||||
</div>
|
|
||||||
<div class="preview-content">
|
|
||||||
<div class="code-block-wrapper">
|
|
||||||
<div class="block-header">
|
|
||||||
<div class="block-language">javascript</div>
|
|
||||||
</div>
|
|
||||||
<pre class="code-block">
|
|
||||||
<span class="keyword">function</span> <span class="function">createDocument</span>() {
|
|
||||||
<span class="keyword">const</span> <span class="variable">doc</span> = <span class="keyword">new</span> <span class="class">Document</span>();
|
|
||||||
|
|
||||||
<span class="variable">doc</span>.<span class="function">addCodeBlock</span>(<span class="string">'javascript'</span>, <span class="string">`
|
|
||||||
<span class="keyword">function</span> <span class="function">greeting</span>(<span class="parameter">name</span>) {
|
|
||||||
<span class="keyword">return</span> <span class="string">`Hello, </span>${<span class="parameter">name</span>}<span class="string">!`</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="built-in">console</span>.<span class="function">log</span>(<span class="function">greeting</span>(<span class="string">'World'</span>));
|
|
||||||
`</span>);
|
|
||||||
|
|
||||||
<span class="keyword">return</span> <span class="variable">doc</span>;
|
|
||||||
}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="code-block-wrapper" style="margin-top: 10px;">
|
|
||||||
<div class="block-header">
|
|
||||||
<div class="block-language">text</div>
|
|
||||||
</div>
|
|
||||||
<pre class="code-block">
|
|
||||||
<span class="comment">// voidraft - An elegant text snippet recording tool</span>
|
|
||||||
<span class="comment">// Multi-language support | Code formatting | Custom themes</span>
|
|
||||||
<span class="comment">// A modern text editor designed for developers</span></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="preview-window">
|
|
||||||
<img src="img/screenshot-dark.png" alt="voidraft 界面预览" class="preview-image dark-theme-img">
|
|
||||||
<img src="img/screenshot-light.png" alt="voidraft 界面预览" class="preview-image light-theme-img" style="display: none;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 技术栈部分 -->
|
|
||||||
<h2 data-en="Technical Stack" data-zh="技术栈">Technical Stack</h2>
|
|
||||||
|
|
||||||
<ul class="tech-list">
|
|
||||||
<li class="tech-item">
|
|
||||||
<div class="tech-icon"><i class="fas fa-desktop"></i></div>
|
|
||||||
<span class="tech-name">Wails3</span>
|
|
||||||
<span class="tech-desc" data-en="Cross-platform desktop application framework" data-zh="跨平台桌面应用框架">Cross-platform desktop application framework</span>
|
|
||||||
</li>
|
|
||||||
<li class="tech-item">
|
|
||||||
<div class="tech-icon"><i class="fas fa-cogs"></i></div>
|
|
||||||
<span class="tech-name">Go 1.21+</span>
|
|
||||||
<span class="tech-desc" data-en="Fast and efficient backend language" data-zh="快速高效的后端语言">Fast and efficient backend language</span>
|
|
||||||
</li>
|
|
||||||
<li class="tech-item">
|
|
||||||
<div class="tech-icon"><i class="fab fa-vuejs"></i></div>
|
|
||||||
<span class="tech-name">Vue 3 + TypeScript</span>
|
|
||||||
<span class="tech-desc" data-en="Modern frontend framework" data-zh="现代化前端框架">Modern frontend framework</span>
|
|
||||||
</li>
|
|
||||||
<li class="tech-item">
|
|
||||||
<div class="tech-icon"><i class="fas fa-edit"></i></div>
|
|
||||||
<span class="tech-name">CodeMirror 6</span>
|
|
||||||
<span class="tech-desc" data-en="Modern code editor with extension support" data-zh="支持扩展的现代化代码编辑器">Modern code editor with extension support</span>
|
|
||||||
</li>
|
|
||||||
<li class="tech-item">
|
|
||||||
<div class="tech-icon"><i class="fas fa-database"></i></div>
|
|
||||||
<span class="tech-name">SQLite</span>
|
|
||||||
<span class="tech-desc" data-en="Lightweight database for document storage" data-zh="轻量级文档存储数据库">Lightweight database for document storage</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 页脚 -->
|
|
||||||
<footer class="footer">
|
|
||||||
<p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2025 voidraft - An elegant text snippet recording tool designed for developers</p>
|
|
||||||
<div class="footer-links">
|
|
||||||
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
|
|
||||||
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
|
|
||||||
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,705 +0,0 @@
|
|||||||
/**
|
|
||||||
* voidraft - Changelog Script
|
|
||||||
* 从GitHub API获取发布信息,支持Gitea备用源
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 仓库配置类
|
|
||||||
*/
|
|
||||||
class RepositoryConfig {
|
|
||||||
constructor() {
|
|
||||||
this.repos = {
|
|
||||||
github: {
|
|
||||||
owner: 'landaiqing',
|
|
||||||
name: 'voidraft',
|
|
||||||
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
|
|
||||||
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
|
|
||||||
},
|
|
||||||
gitea: {
|
|
||||||
owner: 'landaiqing',
|
|
||||||
name: 'voidraft',
|
|
||||||
domain: 'git.landaiqing.cn',
|
|
||||||
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
|
|
||||||
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取仓库配置
|
|
||||||
* @param {string} source - 'github' 或 'gitea'
|
|
||||||
*/
|
|
||||||
getRepo(source) {
|
|
||||||
return this.repos[source];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有仓库配置
|
|
||||||
*/
|
|
||||||
getAllRepos() {
|
|
||||||
return this.repos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 国际化消息管理类
|
|
||||||
*/
|
|
||||||
class I18nMessages {
|
|
||||||
constructor() {
|
|
||||||
this.messages = {
|
|
||||||
loading: {
|
|
||||||
en: 'Loading releases...',
|
|
||||||
zh: '正在加载版本信息...'
|
|
||||||
},
|
|
||||||
noReleases: {
|
|
||||||
en: 'No release information found',
|
|
||||||
zh: '没有找到版本发布信息'
|
|
||||||
},
|
|
||||||
fetchError: {
|
|
||||||
en: 'Failed to load release information. Please try again later.',
|
|
||||||
zh: '无法获取版本信息,请稍后再试'
|
|
||||||
},
|
|
||||||
githubApiError: {
|
|
||||||
en: 'GitHub API returned an error status: ',
|
|
||||||
zh: 'GitHub API返回错误状态: '
|
|
||||||
},
|
|
||||||
giteaApiError: {
|
|
||||||
en: 'Gitea API returned an error status: ',
|
|
||||||
zh: 'Gitea API返回错误状态: '
|
|
||||||
},
|
|
||||||
dataSource: {
|
|
||||||
en: 'Data source: ',
|
|
||||||
zh: '数据来源: '
|
|
||||||
},
|
|
||||||
downloads: {
|
|
||||||
en: 'Downloads',
|
|
||||||
zh: '下载资源'
|
|
||||||
},
|
|
||||||
download: {
|
|
||||||
en: 'Download',
|
|
||||||
zh: '下载'
|
|
||||||
},
|
|
||||||
preRelease: {
|
|
||||||
en: 'Pre-release',
|
|
||||||
zh: '预发布'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取消息
|
|
||||||
* @param {string} key - 消息键
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
getMessage(key, lang = 'en') {
|
|
||||||
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前语言
|
|
||||||
*/
|
|
||||||
getCurrentLang() {
|
|
||||||
return window.currentLang || 'en';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API客户端类
|
|
||||||
*/
|
|
||||||
class APIClient {
|
|
||||||
constructor(repositoryConfig, i18nMessages) {
|
|
||||||
this.repositoryConfig = repositoryConfig;
|
|
||||||
this.i18nMessages = i18nMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从指定源获取发布信息
|
|
||||||
* @param {string} source - 'github' 或 'gitea'
|
|
||||||
*/
|
|
||||||
async fetchReleases(source) {
|
|
||||||
const repo = this.repositoryConfig.getRepo(source);
|
|
||||||
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
headers: { 'Accept': 'application/json' }
|
|
||||||
};
|
|
||||||
|
|
||||||
if (source === 'github') {
|
|
||||||
return this.fetchFromGitHub(repo, options, errorMessageKey);
|
|
||||||
} else {
|
|
||||||
return this.fetchFromGitea(repo, options, errorMessageKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从GitHub获取数据
|
|
||||||
* @param {Object} repo - 仓库配置
|
|
||||||
* @param {Object} options - 请求选项
|
|
||||||
* @param {string} errorMessageKey - 错误消息键
|
|
||||||
*/
|
|
||||||
async fetchFromGitHub(repo, options, errorMessageKey) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
||||||
|
|
||||||
options.signal = controller.signal;
|
|
||||||
options.headers['Accept'] = 'application/vnd.github.v3+json';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(repo.apiUrl, options);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const releases = await response.json();
|
|
||||||
|
|
||||||
if (!releases || releases.length === 0) {
|
|
||||||
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return releases;
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Gitea获取数据
|
|
||||||
* @param {Object} repo - 仓库配置
|
|
||||||
* @param {Object} options - 请求选项
|
|
||||||
* @param {string} errorMessageKey - 错误消息键
|
|
||||||
*/
|
|
||||||
async fetchFromGitea(repo, options, errorMessageKey) {
|
|
||||||
const response = await fetch(repo.apiUrl, options);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const releases = await response.json();
|
|
||||||
|
|
||||||
if (!releases || releases.length === 0) {
|
|
||||||
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return releases;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI管理类
|
|
||||||
*/
|
|
||||||
class UIManager {
|
|
||||||
constructor(i18nMessages) {
|
|
||||||
this.i18nMessages = i18nMessages;
|
|
||||||
this.elements = {
|
|
||||||
loading: document.getElementById('loading'),
|
|
||||||
changelog: document.getElementById('changelog'),
|
|
||||||
error: document.getElementById('error-message')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示加载状态
|
|
||||||
*/
|
|
||||||
showLoading() {
|
|
||||||
this.elements.loading.style.display = 'block';
|
|
||||||
this.elements.error.style.display = 'none';
|
|
||||||
this.elements.changelog.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隐藏加载状态
|
|
||||||
*/
|
|
||||||
hideLoading() {
|
|
||||||
this.elements.loading.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示错误消息
|
|
||||||
* @param {string} message - 错误消息
|
|
||||||
*/
|
|
||||||
showError(message) {
|
|
||||||
const errorMessageElement = this.elements.error.querySelector('p');
|
|
||||||
if (errorMessageElement) {
|
|
||||||
errorMessageElement.textContent = message;
|
|
||||||
} else {
|
|
||||||
this.elements.error.textContent = message;
|
|
||||||
}
|
|
||||||
this.elements.error.style.display = 'block';
|
|
||||||
this.hideLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示发布信息
|
|
||||||
* @param {Array} releases - 发布信息数组
|
|
||||||
* @param {string} source - 数据源
|
|
||||||
*/
|
|
||||||
displayReleases(releases, source) {
|
|
||||||
this.hideLoading();
|
|
||||||
|
|
||||||
// 清除现有内容
|
|
||||||
this.elements.changelog.innerHTML = '';
|
|
||||||
|
|
||||||
// 创建数据源元素
|
|
||||||
const sourceElement = this.createSourceElement(source);
|
|
||||||
this.elements.changelog.appendChild(sourceElement);
|
|
||||||
|
|
||||||
// 创建发布信息元素
|
|
||||||
releases.forEach(release => {
|
|
||||||
const releaseElement = this.createReleaseElement(release, source);
|
|
||||||
this.elements.changelog.appendChild(releaseElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.elements.changelog.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建数据源元素
|
|
||||||
* @param {string} source - 数据源
|
|
||||||
*/
|
|
||||||
createSourceElement(source) {
|
|
||||||
const sourceElement = document.createElement('div');
|
|
||||||
sourceElement.className = 'data-source';
|
|
||||||
|
|
||||||
// 创建带有国际化支持的源标签
|
|
||||||
const sourceLabel = document.createElement('span');
|
|
||||||
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
|
|
||||||
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
|
|
||||||
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
|
|
||||||
|
|
||||||
// 创建链接
|
|
||||||
const sourceLink = document.createElement('a');
|
|
||||||
const repositoryConfig = new RepositoryConfig();
|
|
||||||
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
|
|
||||||
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
|
|
||||||
sourceLink.target = '_blank';
|
|
||||||
|
|
||||||
// 组装元素
|
|
||||||
sourceElement.appendChild(sourceLabel);
|
|
||||||
sourceElement.appendChild(sourceLink);
|
|
||||||
|
|
||||||
return sourceElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建发布信息元素
|
|
||||||
* @param {Object} release - 发布信息对象
|
|
||||||
* @param {string} source - 数据源
|
|
||||||
*/
|
|
||||||
createReleaseElement(release, source) {
|
|
||||||
const releaseElement = document.createElement('div');
|
|
||||||
releaseElement.className = 'release';
|
|
||||||
|
|
||||||
// 格式化发布日期
|
|
||||||
const releaseDate = new Date(release.published_at || release.created_at);
|
|
||||||
const formattedDate = DateFormatter.formatDate(releaseDate);
|
|
||||||
|
|
||||||
// 创建头部
|
|
||||||
const headerElement = this.createReleaseHeader(release, formattedDate);
|
|
||||||
releaseElement.appendChild(headerElement);
|
|
||||||
|
|
||||||
// 添加发布说明
|
|
||||||
if (release.body) {
|
|
||||||
const descriptionElement = document.createElement('div');
|
|
||||||
descriptionElement.className = 'release-description markdown-content';
|
|
||||||
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
|
|
||||||
releaseElement.appendChild(descriptionElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载资源
|
|
||||||
const assets = AssetManager.getAssetsFromRelease(release, source);
|
|
||||||
if (assets && assets.length > 0) {
|
|
||||||
const assetsElement = this.createAssetsElement(assets);
|
|
||||||
releaseElement.appendChild(assetsElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return releaseElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建发布信息头部
|
|
||||||
*/
|
|
||||||
createReleaseHeader(release, formattedDate) {
|
|
||||||
const headerElement = document.createElement('div');
|
|
||||||
headerElement.className = 'release-header';
|
|
||||||
|
|
||||||
// 版本元素
|
|
||||||
const versionElement = document.createElement('div');
|
|
||||||
versionElement.className = 'release-version';
|
|
||||||
|
|
||||||
// 版本文本
|
|
||||||
const versionText = document.createElement('span');
|
|
||||||
versionText.textContent = release.name || release.tag_name;
|
|
||||||
versionElement.appendChild(versionText);
|
|
||||||
|
|
||||||
// 预发布标记
|
|
||||||
if (release.prerelease) {
|
|
||||||
const preReleaseTag = document.createElement('span');
|
|
||||||
preReleaseTag.className = 'release-badge pre-release';
|
|
||||||
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
|
|
||||||
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
|
|
||||||
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
|
|
||||||
versionElement.appendChild(preReleaseTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 日期元素
|
|
||||||
const dateElement = document.createElement('div');
|
|
||||||
dateElement.className = 'release-date';
|
|
||||||
dateElement.textContent = formattedDate;
|
|
||||||
|
|
||||||
headerElement.appendChild(versionElement);
|
|
||||||
headerElement.appendChild(dateElement);
|
|
||||||
|
|
||||||
return headerElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建资源文件元素
|
|
||||||
* @param {Array} assets - 资源文件数组
|
|
||||||
*/
|
|
||||||
createAssetsElement(assets) {
|
|
||||||
const assetsElement = document.createElement('div');
|
|
||||||
assetsElement.className = 'release-assets';
|
|
||||||
|
|
||||||
// 资源标题
|
|
||||||
const assetsTitle = document.createElement('div');
|
|
||||||
assetsTitle.className = 'release-assets-title';
|
|
||||||
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
|
|
||||||
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
|
|
||||||
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
|
|
||||||
|
|
||||||
// 资源列表
|
|
||||||
const assetList = document.createElement('ul');
|
|
||||||
assetList.className = 'asset-list';
|
|
||||||
|
|
||||||
// 添加每个资源
|
|
||||||
assets.forEach(asset => {
|
|
||||||
const assetItem = this.createAssetItem(asset);
|
|
||||||
assetList.appendChild(assetItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
assetsElement.appendChild(assetsTitle);
|
|
||||||
assetsElement.appendChild(assetList);
|
|
||||||
|
|
||||||
return assetsElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建资源文件项
|
|
||||||
* @param {Object} asset - 资源文件对象
|
|
||||||
*/
|
|
||||||
createAssetItem(asset) {
|
|
||||||
const assetItem = document.createElement('li');
|
|
||||||
assetItem.className = 'asset-item';
|
|
||||||
|
|
||||||
// 文件图标
|
|
||||||
const iconElement = document.createElement('i');
|
|
||||||
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
|
|
||||||
|
|
||||||
// 文件名
|
|
||||||
const nameElement = document.createElement('span');
|
|
||||||
nameElement.className = 'asset-name';
|
|
||||||
nameElement.textContent = asset.name;
|
|
||||||
|
|
||||||
// 文件大小
|
|
||||||
const sizeElement = document.createElement('span');
|
|
||||||
sizeElement.className = 'asset-size';
|
|
||||||
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
|
|
||||||
|
|
||||||
// 下载链接
|
|
||||||
const downloadLink = document.createElement('a');
|
|
||||||
downloadLink.className = 'download-btn';
|
|
||||||
downloadLink.href = asset.browser_download_url;
|
|
||||||
downloadLink.target = '_blank';
|
|
||||||
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
|
|
||||||
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
|
|
||||||
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
|
|
||||||
|
|
||||||
// 组装资源项
|
|
||||||
assetItem.appendChild(iconElement);
|
|
||||||
assetItem.appendChild(nameElement);
|
|
||||||
assetItem.appendChild(sizeElement);
|
|
||||||
assetItem.appendChild(downloadLink);
|
|
||||||
|
|
||||||
return assetItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资源管理器类
|
|
||||||
*/
|
|
||||||
class AssetManager {
|
|
||||||
/**
|
|
||||||
* 从发布信息中获取资源文件
|
|
||||||
* @param {Object} release - 发布信息对象
|
|
||||||
* @param {string} source - 数据源
|
|
||||||
*/
|
|
||||||
static getAssetsFromRelease(release, source) {
|
|
||||||
let assets = [];
|
|
||||||
|
|
||||||
if (source === 'github') {
|
|
||||||
assets = release.assets || [];
|
|
||||||
} else { // Gitea
|
|
||||||
assets = release.assets || [];
|
|
||||||
// 检查Gitea特定的资源结构
|
|
||||||
if (!assets.length && release.attachments) {
|
|
||||||
assets = release.attachments.map(attachment => ({
|
|
||||||
name: attachment.name,
|
|
||||||
size: attachment.size,
|
|
||||||
browser_download_url: attachment.browser_download_url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件图标助手类
|
|
||||||
*/
|
|
||||||
class FileIconHelper {
|
|
||||||
/**
|
|
||||||
* 根据文件扩展名获取图标
|
|
||||||
* @param {string} filename - 文件名
|
|
||||||
*/
|
|
||||||
static getFileIcon(filename) {
|
|
||||||
const extension = filename.split('.').pop().toLowerCase();
|
|
||||||
|
|
||||||
const iconMap = {
|
|
||||||
'exe': 'download',
|
|
||||||
'msi': 'download',
|
|
||||||
'dmg': 'download',
|
|
||||||
'pkg': 'download',
|
|
||||||
'deb': 'download',
|
|
||||||
'rpm': 'download',
|
|
||||||
'tar': 'file-archive',
|
|
||||||
'gz': 'file-archive',
|
|
||||||
'zip': 'file-archive',
|
|
||||||
'7z': 'file-archive',
|
|
||||||
'rar': 'file-archive',
|
|
||||||
'pdf': 'file-pdf',
|
|
||||||
'txt': 'file-alt',
|
|
||||||
'md': 'file-alt',
|
|
||||||
'json': 'file-code',
|
|
||||||
'xml': 'file-code',
|
|
||||||
'yml': 'file-code',
|
|
||||||
'yaml': 'file-code'
|
|
||||||
};
|
|
||||||
|
|
||||||
return iconMap[extension] || 'file';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件大小格式化器类
|
|
||||||
*/
|
|
||||||
class FileSizeFormatter {
|
|
||||||
/**
|
|
||||||
* 格式化文件大小
|
|
||||||
* @param {number} bytes - 字节数
|
|
||||||
*/
|
|
||||||
static formatFileSize(bytes) {
|
|
||||||
if (!bytes) return '';
|
|
||||||
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
|
|
||||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日期格式化器类
|
|
||||||
*/
|
|
||||||
class DateFormatter {
|
|
||||||
/**
|
|
||||||
* 格式化日期
|
|
||||||
* @param {Date} date - 日期对象
|
|
||||||
*/
|
|
||||||
static formatDate(date) {
|
|
||||||
const options = {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
};
|
|
||||||
|
|
||||||
const lang = window.currentLang || 'en';
|
|
||||||
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
|
|
||||||
|
|
||||||
return date.toLocaleDateString(locale, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Markdown解析器类
|
|
||||||
*/
|
|
||||||
class MarkdownParser {
|
|
||||||
/**
|
|
||||||
* 简单的Markdown解析
|
|
||||||
* @param {string} markdown - Markdown文本
|
|
||||||
*/
|
|
||||||
static parseMarkdown(markdown) {
|
|
||||||
if (!markdown) return '';
|
|
||||||
|
|
||||||
// 预处理:保留原始换行符,用特殊标记替换
|
|
||||||
const preservedLineBreaks = '___LINE_BREAK___';
|
|
||||||
markdown = markdown.replace(/\n/g, preservedLineBreaks);
|
|
||||||
|
|
||||||
// 引用块 - > text
|
|
||||||
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
|
|
||||||
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
|
|
||||||
|
|
||||||
// 链接 - [text](url)
|
|
||||||
markdown = markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
||||||
|
|
||||||
// 标题 - # Heading
|
|
||||||
markdown = markdown.replace(/^### (.*?)(?=___LINE_BREAK___|$)/gm, '<h3>$1</h3>');
|
|
||||||
markdown = markdown.replace(/^## (.*?)(?=___LINE_BREAK___|$)/gm, '<h2>$1</h2>');
|
|
||||||
markdown = markdown.replace(/^# (.*?)(?=___LINE_BREAK___|$)/gm, '<h1>$1</h1>');
|
|
||||||
|
|
||||||
// 粗体 - **text**
|
|
||||||
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
||||||
|
|
||||||
// 斜体 - *text*
|
|
||||||
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
||||||
|
|
||||||
// 代码块 - ```code```
|
|
||||||
markdown = markdown.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
||||||
|
|
||||||
// 行内代码 - `code`
|
|
||||||
markdown = markdown.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
||||||
|
|
||||||
// 处理列表项
|
|
||||||
// 先将每个列表项转换为HTML
|
|
||||||
markdown = markdown.replace(/- (.*?)(?=___LINE_BREAK___- |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
|
||||||
markdown = markdown.replace(/\* (.*?)(?=___LINE_BREAK___\* |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
|
||||||
markdown = markdown.replace(/\d+\. (.*?)(?=___LINE_BREAK___\d+\. |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
|
||||||
|
|
||||||
// 然后将连续的列表项包装在ul或ol中
|
|
||||||
const listItemRegex = /<li>.*?<\/li>/g;
|
|
||||||
const listItems = markdown.match(listItemRegex) || [];
|
|
||||||
|
|
||||||
if (listItems.length > 0) {
|
|
||||||
// 将连续的列表项组合在一起
|
|
||||||
let lastIndex = 0;
|
|
||||||
let result = '';
|
|
||||||
let inList = false;
|
|
||||||
|
|
||||||
listItems.forEach(item => {
|
|
||||||
const itemIndex = markdown.indexOf(item, lastIndex);
|
|
||||||
|
|
||||||
// 添加列表项之前的内容
|
|
||||||
if (itemIndex > lastIndex) {
|
|
||||||
result += markdown.substring(lastIndex, itemIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不在列表中,开始一个新列表
|
|
||||||
if (!inList) {
|
|
||||||
result += '<ul>';
|
|
||||||
inList = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加列表项
|
|
||||||
result += item;
|
|
||||||
|
|
||||||
// 更新lastIndex
|
|
||||||
lastIndex = itemIndex + item.length;
|
|
||||||
|
|
||||||
// 检查下一个内容是否是列表项
|
|
||||||
const nextItemIndex = markdown.indexOf('<li>', lastIndex);
|
|
||||||
if (nextItemIndex === -1 || nextItemIndex > lastIndex + 20) { // 如果下一个列表项不紧邻
|
|
||||||
result += '</ul>';
|
|
||||||
inList = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加剩余内容
|
|
||||||
if (lastIndex < markdown.length) {
|
|
||||||
result += markdown.substring(lastIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
markdown = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理水平分隔线
|
|
||||||
markdown = markdown.replace(/---/g, '<hr>');
|
|
||||||
|
|
||||||
// 恢复换行符
|
|
||||||
markdown = markdown.replace(/___LINE_BREAK___/g, '<br>');
|
|
||||||
|
|
||||||
// 处理段落
|
|
||||||
markdown = markdown.replace(/<br><br>/g, '</p><p>');
|
|
||||||
|
|
||||||
// 包装在段落标签中
|
|
||||||
if (!markdown.startsWith('<p>')) {
|
|
||||||
markdown = `<p>${markdown}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return markdown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新日志主应用类
|
|
||||||
*/
|
|
||||||
class ChangelogApp {
|
|
||||||
constructor() {
|
|
||||||
this.repositoryConfig = new RepositoryConfig();
|
|
||||||
this.i18nMessages = new I18nMessages();
|
|
||||||
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
|
|
||||||
this.uiManager = new UIManager(this.i18nMessages);
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化应用
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
this.uiManager.showLoading();
|
|
||||||
|
|
||||||
// 首先尝试GitHub API
|
|
||||||
this.apiClient.fetchReleases('github')
|
|
||||||
.then(releases => {
|
|
||||||
this.uiManager.displayReleases(releases, 'github');
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// GitHub失败时尝试Gitea
|
|
||||||
return this.apiClient.fetchReleases('gitea')
|
|
||||||
.then(releases => {
|
|
||||||
this.uiManager.displayReleases(releases, 'gitea');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取发布信息失败:', error);
|
|
||||||
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听语言变化事件
|
|
||||||
document.addEventListener('languageChanged', () => this.updateUI());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新UI元素(当语言变化时)
|
|
||||||
*/
|
|
||||||
updateUI() {
|
|
||||||
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
|
|
||||||
const currentLang = this.i18nMessages.getCurrentLang();
|
|
||||||
|
|
||||||
elementsToUpdate.forEach(element => {
|
|
||||||
const text = element.getAttribute(`data-${currentLang}`);
|
|
||||||
if (text) {
|
|
||||||
element.textContent = text;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当DOM加载完成时初始化应用
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
new ChangelogApp();
|
|
||||||
});
|
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
/**
|
|
||||||
* voidraft - Website Script
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主题管理类
|
|
||||||
*/
|
|
||||||
class ThemeManager {
|
|
||||||
constructor() {
|
|
||||||
this.themeToggle = document.getElementById('theme-toggle');
|
|
||||||
this.currentTheme = this.getInitialTheme();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取初始主题
|
|
||||||
*/
|
|
||||||
getInitialTheme() {
|
|
||||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
const savedTheme = localStorage.getItem('theme');
|
|
||||||
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化主题管理器
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
if (!this.themeToggle) return;
|
|
||||||
|
|
||||||
this.setTheme(this.currentTheme);
|
|
||||||
this.bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定事件
|
|
||||||
*/
|
|
||||||
bindEvents() {
|
|
||||||
this.themeToggle.addEventListener('click', () => {
|
|
||||||
this.toggleTheme();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换主题
|
|
||||||
*/
|
|
||||||
toggleTheme() {
|
|
||||||
document.body.classList.add('theme-transition');
|
|
||||||
|
|
||||||
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
|
||||||
this.setTheme(newTheme);
|
|
||||||
this.saveTheme(newTheme);
|
|
||||||
|
|
||||||
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置主题
|
|
||||||
* @param {string} theme - 'dark' 或 'light'
|
|
||||||
*/
|
|
||||||
setTheme(theme) {
|
|
||||||
this.currentTheme = theme;
|
|
||||||
const isDark = theme === 'dark';
|
|
||||||
|
|
||||||
document.body.classList.toggle('theme-dark', isDark);
|
|
||||||
document.body.classList.toggle('theme-light', !isDark);
|
|
||||||
|
|
||||||
this.updateToggleIcon(isDark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新切换按钮图标
|
|
||||||
* @param {boolean} isDark - 是否为暗色主题
|
|
||||||
*/
|
|
||||||
updateToggleIcon(isDark) {
|
|
||||||
if (this.themeToggle) {
|
|
||||||
const icon = this.themeToggle.querySelector('i');
|
|
||||||
if (icon) {
|
|
||||||
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存主题到本地存储
|
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
*/
|
|
||||||
saveTheme(theme) {
|
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语言管理类
|
|
||||||
*/
|
|
||||||
class LanguageManager {
|
|
||||||
constructor() {
|
|
||||||
this.langToggle = document.getElementById('lang-toggle');
|
|
||||||
this.currentLang = this.getInitialLanguage();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取初始语言
|
|
||||||
*/
|
|
||||||
getInitialLanguage() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const urlLang = urlParams.get('lang');
|
|
||||||
const savedLang = localStorage.getItem('lang');
|
|
||||||
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
||||||
return urlLang || savedLang || browserLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化语言管理器
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
if (!this.langToggle) return;
|
|
||||||
|
|
||||||
window.currentLang = this.currentLang;
|
|
||||||
this.setLanguage(this.currentLang);
|
|
||||||
this.bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定事件
|
|
||||||
*/
|
|
||||||
bindEvents() {
|
|
||||||
this.langToggle.addEventListener('click', () => {
|
|
||||||
this.toggleLanguage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换语言
|
|
||||||
*/
|
|
||||||
toggleLanguage() {
|
|
||||||
document.body.classList.add('lang-transition');
|
|
||||||
|
|
||||||
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
|
|
||||||
this.setLanguage(newLang);
|
|
||||||
this.saveLanguage(newLang);
|
|
||||||
this.updateURL(newLang);
|
|
||||||
this.notifyLanguageChange(newLang);
|
|
||||||
|
|
||||||
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置页面语言
|
|
||||||
* @param {string} lang - 'zh' 或 'en'
|
|
||||||
*/
|
|
||||||
setLanguage(lang) {
|
|
||||||
this.currentLang = lang;
|
|
||||||
window.currentLang = lang;
|
|
||||||
|
|
||||||
this.updatePageElements(lang);
|
|
||||||
this.updateHTMLLang(lang);
|
|
||||||
this.updateToggleButton(lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新页面元素文本
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
updatePageElements(lang) {
|
|
||||||
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
|
|
||||||
el.textContent = el.getAttribute(`data-${lang}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新HTML语言属性
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
updateHTMLLang(lang) {
|
|
||||||
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新切换按钮文本
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
updateToggleButton(lang) {
|
|
||||||
if (this.langToggle) {
|
|
||||||
const text = lang === 'zh' ? 'EN/中' : '中/EN';
|
|
||||||
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存语言到本地存储
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
saveLanguage(lang) {
|
|
||||||
localStorage.setItem('lang', lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新URL参数
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
updateURL(lang) {
|
|
||||||
const newUrl = new URL(window.location);
|
|
||||||
if (lang === 'zh') {
|
|
||||||
newUrl.searchParams.set('lang', 'zh');
|
|
||||||
} else {
|
|
||||||
newUrl.searchParams.delete('lang');
|
|
||||||
}
|
|
||||||
window.history.replaceState({}, '', newUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知语言变更
|
|
||||||
* @param {string} lang - 语言代码
|
|
||||||
*/
|
|
||||||
notifyLanguageChange(lang) {
|
|
||||||
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前语言
|
|
||||||
*/
|
|
||||||
getCurrentLanguage() {
|
|
||||||
return this.currentLang;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SEO管理类
|
|
||||||
*/
|
|
||||||
class SEOManager {
|
|
||||||
constructor(languageManager) {
|
|
||||||
this.languageManager = languageManager;
|
|
||||||
this.metaTexts = {
|
|
||||||
en: {
|
|
||||||
description: 'voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
|
|
||||||
title: 'voidraft - An elegant text snippet recording tool designed for developers.',
|
|
||||||
ogTitle: 'voidraft - An elegant text snippet recording tool designed for developers'
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
description: 'voidraft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
|
|
||||||
title: 'voidraft - 专为开发者打造的优雅文本片段记录工具',
|
|
||||||
ogTitle: 'voidraft - 专为开发者打造的优雅文本片段记录工具'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化SEO管理器
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
this.bindEvents();
|
|
||||||
this.updateMetaTags(this.languageManager.getCurrentLanguage());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定事件
|
|
||||||
*/
|
|
||||||
bindEvents() {
|
|
||||||
window.addEventListener('languageChanged', (event) => {
|
|
||||||
this.updateMetaTags(event.detail.lang);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新SEO元标签
|
|
||||||
* @param {string} lang - 当前语言
|
|
||||||
*/
|
|
||||||
updateMetaTags(lang) {
|
|
||||||
const texts = this.metaTexts[lang];
|
|
||||||
|
|
||||||
this.updateMetaDescription(texts.description);
|
|
||||||
this.updateOpenGraphTags(texts.ogTitle, texts.description);
|
|
||||||
this.updateTwitterCardTags(texts.ogTitle, texts.description);
|
|
||||||
this.updatePageTitle(texts.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新meta描述
|
|
||||||
* @param {string} description - 描述文本
|
|
||||||
*/
|
|
||||||
updateMetaDescription(description) {
|
|
||||||
const metaDesc = document.querySelector('meta[name="description"]');
|
|
||||||
if (metaDesc) {
|
|
||||||
metaDesc.content = description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新Open Graph标签
|
|
||||||
* @param {string} title - 标题
|
|
||||||
* @param {string} description - 描述
|
|
||||||
*/
|
|
||||||
updateOpenGraphTags(title, description) {
|
|
||||||
const ogTitle = document.querySelector('meta[property="og:title"]');
|
|
||||||
const ogDesc = document.querySelector('meta[property="og:description"]');
|
|
||||||
|
|
||||||
if (ogTitle) ogTitle.content = title;
|
|
||||||
if (ogDesc) ogDesc.content = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新Twitter Card标签
|
|
||||||
* @param {string} title - 标题
|
|
||||||
* @param {string} description - 描述
|
|
||||||
*/
|
|
||||||
updateTwitterCardTags(title, description) {
|
|
||||||
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
|
|
||||||
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
|
|
||||||
|
|
||||||
if (twitterTitle) twitterTitle.content = title;
|
|
||||||
if (twitterDesc) twitterDesc.content = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新页面标题
|
|
||||||
* @param {string} title - 标题
|
|
||||||
*/
|
|
||||||
updatePageTitle(title) {
|
|
||||||
document.title = title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI效果管理类
|
|
||||||
*/
|
|
||||||
class UIEffects {
|
|
||||||
constructor() {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化UI效果
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
this.initCardEffects();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化卡片悬停效果
|
|
||||||
*/
|
|
||||||
initCardEffects() {
|
|
||||||
const cards = document.querySelectorAll('.feature-card');
|
|
||||||
|
|
||||||
cards.forEach(card => {
|
|
||||||
card.addEventListener('mouseenter', () => {
|
|
||||||
this.animateCardHover(card, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('mouseleave', () => {
|
|
||||||
this.animateCardHover(card, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 卡片悬停动画
|
|
||||||
* @param {Element} card - 卡片元素
|
|
||||||
* @param {boolean} isHover - 是否悬停
|
|
||||||
*/
|
|
||||||
animateCardHover(card, isHover) {
|
|
||||||
if (isHover) {
|
|
||||||
card.style.transform = 'translateY(-8px)';
|
|
||||||
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
|
|
||||||
} else {
|
|
||||||
card.style.transform = 'translateY(0)';
|
|
||||||
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* voidraft主应用类
|
|
||||||
*/
|
|
||||||
class voidraftApp {
|
|
||||||
constructor() {
|
|
||||||
this.themeManager = null;
|
|
||||||
this.languageManager = null;
|
|
||||||
this.seoManager = null;
|
|
||||||
this.uiEffects = null;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化应用
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
this.initializeManagers();
|
|
||||||
this.showConsoleBranding();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化各个管理器
|
|
||||||
*/
|
|
||||||
initializeManagers() {
|
|
||||||
this.themeManager = new ThemeManager();
|
|
||||||
this.languageManager = new LanguageManager();
|
|
||||||
this.seoManager = new SEOManager(this.languageManager);
|
|
||||||
this.uiEffects = new UIEffects();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示控制台品牌信息
|
|
||||||
*/
|
|
||||||
showConsoleBranding() {
|
|
||||||
console.log('%c voidraft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
|
|
||||||
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取主题管理器
|
|
||||||
*/
|
|
||||||
getThemeManager() {
|
|
||||||
return this.themeManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取语言管理器
|
|
||||||
*/
|
|
||||||
getLanguageManager() {
|
|
||||||
return this.languageManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取SEO管理器
|
|
||||||
*/
|
|
||||||
getSEOManager() {
|
|
||||||
return this.seoManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取UI效果管理器
|
|
||||||
*/
|
|
||||||
getUIEffects() {
|
|
||||||
return this.uiEffects;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当DOM加载完成时初始化应用
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
window.voidRaftApp = new voidraftApp();
|
|
||||||
});
|
|
||||||
@@ -24,7 +24,7 @@ export class HttpRequest {
|
|||||||
"headers": { [_: string]: string };
|
"headers": { [_: string]: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* json, formdata, urlencoded, text
|
* json, formdata, urlencoded, text, params, xml, html, javascript, binary
|
||||||
*/
|
*/
|
||||||
"bodyType"?: string;
|
"bodyType"?: string;
|
||||||
"body"?: any;
|
"body"?: any;
|
||||||
|
|||||||
130
frontend/docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import {defineConfig} from 'vitepress'
|
||||||
|
const base = '/'
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default defineConfig({
|
||||||
|
base: base,
|
||||||
|
title: "voidraft",
|
||||||
|
description: "An elegant text snippet recording tool designed for developers.",
|
||||||
|
srcDir: 'src',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
cacheDir: './.vitepress/cache',
|
||||||
|
outDir: './.vitepress/dist',
|
||||||
|
srcExclude: [],
|
||||||
|
ignoreDeadLinks: false,
|
||||||
|
head: [
|
||||||
|
["link", {rel: "icon", type: "image/png", href: "/icon/favicon-96x96.png", sizes: "96x96"}],
|
||||||
|
["link", {rel: "icon", type: "image/svg+xml", href: "/icon/favicon.svg"}],
|
||||||
|
["link", {rel: "shortcut icon", href: "/icon/favicon.ico"}],
|
||||||
|
["link", {rel: "apple-touch-icon", sizes: "180x180", href: "/icon/apple-touch-icon.png"}],
|
||||||
|
["meta", {name: "apple-mobile-web-app-title", content: "voidraft"}],
|
||||||
|
["link", {rel: "manifest", href: "/icon/site.webmanifest"}],
|
||||||
|
['meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}]
|
||||||
|
],
|
||||||
|
|
||||||
|
// 国际化配置
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en-US',
|
||||||
|
description: 'An elegant text snippet recording tool designed for developers.',
|
||||||
|
themeConfig: {
|
||||||
|
logo: '/icon/logo.png',
|
||||||
|
siteTitle: 'voidraft',
|
||||||
|
nav: [
|
||||||
|
{text: 'Home', link: '/'},
|
||||||
|
{text: 'Guide', link: '/guide/introduction'}
|
||||||
|
],
|
||||||
|
sidebar: {
|
||||||
|
'/guide/': [
|
||||||
|
{
|
||||||
|
text: 'Getting Started',
|
||||||
|
items: [
|
||||||
|
{text: 'Introduction', link: '/guide/introduction'},
|
||||||
|
{text: 'Installation', link: '/guide/installation'},
|
||||||
|
{text: 'Quick Start', link: '/guide/getting-started'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Features',
|
||||||
|
items: [
|
||||||
|
{text: 'Overview', link: '/guide/features'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
socialLinks: [
|
||||||
|
{icon: 'github', link: 'https://github.com/landaiqing/voidraft'}
|
||||||
|
],
|
||||||
|
outline: {
|
||||||
|
label: 'On this page'
|
||||||
|
},
|
||||||
|
lastUpdated: {
|
||||||
|
text: 'Last updated'
|
||||||
|
},
|
||||||
|
docFooter: {
|
||||||
|
prev: 'Previous',
|
||||||
|
next: 'Next'
|
||||||
|
},
|
||||||
|
darkModeSwitchLabel: 'Appearance',
|
||||||
|
sidebarMenuLabel: 'Menu',
|
||||||
|
returnToTopLabel: 'Return to top',
|
||||||
|
footer: {
|
||||||
|
message: 'Released under the MIT License.',
|
||||||
|
copyright: 'Copyright © 2025-present landaiqing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
label: '简体中文',
|
||||||
|
lang: 'zh-CN',
|
||||||
|
link: '/zh/',
|
||||||
|
description: '一个为开发者设计的优雅文本片段记录工具',
|
||||||
|
themeConfig: {
|
||||||
|
logo: '/icon/logo.png',
|
||||||
|
siteTitle: 'voidraft',
|
||||||
|
nav: [
|
||||||
|
{text: '首页', link: '/zh/'},
|
||||||
|
{text: '指南', link: '/zh/guide/introduction'}
|
||||||
|
],
|
||||||
|
sidebar: {
|
||||||
|
'/zh/guide/': [
|
||||||
|
{
|
||||||
|
text: '开始使用',
|
||||||
|
items: [
|
||||||
|
{text: '简介', link: '/zh/guide/introduction'},
|
||||||
|
{text: '安装', link: '/zh/guide/installation'},
|
||||||
|
{text: '快速开始', link: '/zh/guide/getting-started'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '功能特性',
|
||||||
|
items: [
|
||||||
|
{text: '功能概览', link: '/zh/guide/features'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
socialLinks: [
|
||||||
|
{icon: 'github', link: 'https://github.com/landaiqing/voidraft'}
|
||||||
|
],
|
||||||
|
outline: {
|
||||||
|
label: '本页目录'
|
||||||
|
},
|
||||||
|
lastUpdated: {
|
||||||
|
text: '最后更新'
|
||||||
|
},
|
||||||
|
docFooter: {
|
||||||
|
prev: '上一页',
|
||||||
|
next: '下一页'
|
||||||
|
},
|
||||||
|
darkModeSwitchLabel: '外观',
|
||||||
|
sidebarMenuLabel: '菜单',
|
||||||
|
returnToTopLabel: '返回顶部',
|
||||||
|
footer: {
|
||||||
|
message: 'Released under the MIT License.',
|
||||||
|
copyright: 'Copyright © 2025-present landaiqing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
6
frontend/docs/.vitepress/theme/index.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@import "style/var.css";
|
||||||
|
@import "style/blur.css";
|
||||||
|
@import "style/badge.css";
|
||||||
|
@import "style/grid.css";
|
||||||
|
|
||||||
|
|
||||||
17
frontend/docs/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import { h } from 'vue'
|
||||||
|
import type { Theme } from 'vitepress'
|
||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
Layout: () => {
|
||||||
|
return h(DefaultTheme.Layout, null, {
|
||||||
|
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enhanceApp({ app, router, siteData }) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
} satisfies Theme
|
||||||
21
frontend/docs/.vitepress/theme/style/badge.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* 提示框背景颜色 */
|
||||||
|
:root {
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-green-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提示框 */
|
||||||
|
.custom-block.tip {
|
||||||
|
border-color: var(--vp-c-green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 警告框 */
|
||||||
|
.custom-block.warning {
|
||||||
|
/* border-color: #d97706; */
|
||||||
|
border-color: var(--vp-c-yellow-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险框 */
|
||||||
|
.custom-block.danger {
|
||||||
|
/* border-color: #f43f5e; */
|
||||||
|
border-color: var(--vp-c-red-2);
|
||||||
|
}
|
||||||
73
frontend/docs/.vitepress/theme/style/blur.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/* .vitepress/theme/style/blur.css */
|
||||||
|
:root {
|
||||||
|
/* 首页导航 */
|
||||||
|
.VPNavBar {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文档页导航两侧 */
|
||||||
|
.VPNavBar:not(.home) {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
/* 文档页导航两侧 */
|
||||||
|
.VPNavBar:not(.home) {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页下滑后导航两侧 */
|
||||||
|
.VPNavBar:not(.has-sidebar):not(.home.top) {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
/* 文档页导航中间 */
|
||||||
|
.VPNavBar:not(.home.top) .content-body {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页下滑后导航中间 */
|
||||||
|
.VPNavBar:not(.has-sidebar):not(.home.top) .content-body {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线 */
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
/* 文档页分割线 */
|
||||||
|
.VPNavBar:not(.home.top) .divider-line {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页分割线 */
|
||||||
|
.VPNavBar:not(.has-sidebar):not(.home.top) .divider {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索框 VPNavBarSearchButton.vue */
|
||||||
|
.DocSearch-Button {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端大纲栏 */
|
||||||
|
.VPLocalNav {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
/* 隐藏分割线 */
|
||||||
|
/* border-bottom: 5px solid var(--vp-c-gutter); */
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/docs/.vitepress/theme/style/grid.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Grid Background
|
||||||
|
* 网格背景样式 - 为文档页面添加简约的网格背景
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.VPDoc,
|
||||||
|
.VPHome {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDoc::before,
|
||||||
|
.VPHome::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 亮色模式网格 */
|
||||||
|
:root:not(.dark) .VPDoc::before,
|
||||||
|
:root:not(.dark) .VPHome::before {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||||
|
background-size: 60px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式网格 */
|
||||||
|
.dark .VPDoc::before,
|
||||||
|
.dark .VPHome::before {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
||||||
|
background-size: 60px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
137
frontend/docs/.vitepress/theme/style/var.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Customize default theme styling by overriding CSS variables:
|
||||||
|
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
*
|
||||||
|
* Each colors have exact same color scale system with 3 levels of solid
|
||||||
|
* colors with different brightness, and 1 soft color.
|
||||||
|
*
|
||||||
|
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||||
|
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||||
|
*
|
||||||
|
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||||
|
*
|
||||||
|
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||||
|
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||||
|
* top of it.
|
||||||
|
*
|
||||||
|
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||||
|
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||||
|
* on top of it.
|
||||||
|
*
|
||||||
|
* The soft color must be semi transparent alpha channel. This is crucial
|
||||||
|
* because it allows adding multiple "soft" colors on top of each other
|
||||||
|
* to create an accent, such as when having inline code block inside
|
||||||
|
* custom containers.
|
||||||
|
*
|
||||||
|
* - `default`: The color used purely for subtle indication without any
|
||||||
|
* special meanings attached to it such as bg color for menu hover state.
|
||||||
|
*
|
||||||
|
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||||
|
* brand theme, etc.
|
||||||
|
*
|
||||||
|
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||||
|
* brand color for this by default.
|
||||||
|
*
|
||||||
|
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||||
|
* container, badges, etc.
|
||||||
|
*
|
||||||
|
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||||
|
* in custom container, badges, etc.
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
:root {
|
||||||
|
--vp-c-default-1: var(--vp-c-gray-1);
|
||||||
|
--vp-c-default-2: var(--vp-c-gray-2);
|
||||||
|
--vp-c-default-3: var(--vp-c-gray-3);
|
||||||
|
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||||
|
|
||||||
|
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||||
|
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||||
|
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||||
|
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||||
|
|
||||||
|
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||||
|
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||||
|
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||||
|
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||||
|
|
||||||
|
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||||
|
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||||
|
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||||
|
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||||
|
|
||||||
|
--vp-c-danger-1: var(--vp-c-red-1);
|
||||||
|
--vp-c-danger-2: var(--vp-c-red-2);
|
||||||
|
--vp-c-danger-3: var(--vp-c-red-3);
|
||||||
|
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Button
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-button-brand-border: transparent;
|
||||||
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||||
|
--vp-button-brand-hover-border: transparent;
|
||||||
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||||
|
--vp-button-brand-active-border: transparent;
|
||||||
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Home
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||||
|
120deg,
|
||||||
|
#bd34fe 30%,
|
||||||
|
#41d1ff
|
||||||
|
);
|
||||||
|
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
#bd34fe 50%,
|
||||||
|
#47caff 50%
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-filter: blur(44px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(68px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Custom Block
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-custom-block-tip-border: transparent;
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||||
|
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Algolia
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.DocSearch {
|
||||||
|
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||||
|
}
|
||||||
163
frontend/docs/src/guide/features.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Features
|
||||||
|
|
||||||
|
Explore the powerful features that make voidraft a great tool for developers.
|
||||||
|
|
||||||
|
## Block-Based Editing
|
||||||
|
|
||||||
|
voidraft's core feature is its block-based editing system:
|
||||||
|
|
||||||
|
- Each block can have a different programming language
|
||||||
|
- Blocks are separated by delimiters (`∞∞∞language`)
|
||||||
|
- Navigate quickly between blocks
|
||||||
|
- Format each block independently
|
||||||
|
|
||||||
|
## Syntax Highlighting
|
||||||
|
|
||||||
|
Professional syntax highlighting for 30+ languages:
|
||||||
|
|
||||||
|
- Automatic language detection
|
||||||
|
- Customizable color schemes
|
||||||
|
- Support for nested languages
|
||||||
|
- Code folding support
|
||||||
|
|
||||||
|
## HTTP Client
|
||||||
|
|
||||||
|
Built-in HTTP client for API testing:
|
||||||
|
|
||||||
|
### Request Types
|
||||||
|
- GET, POST, PUT, DELETE, PATCH
|
||||||
|
- Custom headers
|
||||||
|
- Multiple body formats: JSON, FormData, URL-encoded, XML, Text
|
||||||
|
|
||||||
|
### Request Variables
|
||||||
|
Define and reuse variables:
|
||||||
|
|
||||||
|
```http
|
||||||
|
@var {
|
||||||
|
baseUrl: "https://api.example.com",
|
||||||
|
token: "your-api-token"
|
||||||
|
}
|
||||||
|
|
||||||
|
GET "{{baseUrl}}/users" {
|
||||||
|
authorization: "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Handling
|
||||||
|
- View formatted JSON responses
|
||||||
|
- See response time and size
|
||||||
|
- Inspect headers
|
||||||
|
- Save responses for later
|
||||||
|
|
||||||
|
## Code Formatting
|
||||||
|
|
||||||
|
Integrated Prettier support:
|
||||||
|
|
||||||
|
- Format on save (optional)
|
||||||
|
- Format selection or entire block
|
||||||
|
- Supports JavaScript, TypeScript, CSS, HTML, JSON, and more
|
||||||
|
- Customizable formatting rules
|
||||||
|
|
||||||
|
## Editor Extensions
|
||||||
|
|
||||||
|
### VSCode-Style Search
|
||||||
|
- Find and replace with regex support
|
||||||
|
- Case-sensitive and whole word options
|
||||||
|
- Search across all blocks
|
||||||
|
|
||||||
|
### Minimap
|
||||||
|
- Bird's-eye view of your document
|
||||||
|
- Quick navigation
|
||||||
|
- Customizable size and position
|
||||||
|
|
||||||
|
### Rainbow Brackets
|
||||||
|
- Color-coded bracket pairs
|
||||||
|
- Easier to match brackets
|
||||||
|
- Customizable colors
|
||||||
|
|
||||||
|
### Color Picker
|
||||||
|
- Visual color selection
|
||||||
|
- Supports hex, RGB, HSL
|
||||||
|
- Live preview
|
||||||
|
|
||||||
|
### Translation Tool
|
||||||
|
- Translate selected text
|
||||||
|
- Multiple language support
|
||||||
|
- Quick keyboard access
|
||||||
|
|
||||||
|
### Text Highlighting
|
||||||
|
- Highlight important text
|
||||||
|
- Multiple highlight colors
|
||||||
|
- Persistent highlights
|
||||||
|
|
||||||
|
## Multi-Window Support
|
||||||
|
|
||||||
|
Work efficiently with multiple windows:
|
||||||
|
|
||||||
|
- Each window is independent
|
||||||
|
- Separate documents
|
||||||
|
- Synchronized settings
|
||||||
|
- Window state persistence
|
||||||
|
|
||||||
|
## Theme Customization
|
||||||
|
|
||||||
|
Full control over editor appearance:
|
||||||
|
|
||||||
|
### Built-in Themes
|
||||||
|
- Dark mode
|
||||||
|
- Light mode
|
||||||
|
- Auto-switch based on system
|
||||||
|
|
||||||
|
### Custom Themes
|
||||||
|
- Create your own themes
|
||||||
|
- Customize every color
|
||||||
|
- Save and share themes
|
||||||
|
- Import community themes
|
||||||
|
|
||||||
|
## Auto-Update System
|
||||||
|
|
||||||
|
Stay current with automatic updates:
|
||||||
|
|
||||||
|
- Background update checks
|
||||||
|
- Notification of new versions
|
||||||
|
- One-click update
|
||||||
|
- Update history
|
||||||
|
- Support for multiple update sources (GitHub, Gitea)
|
||||||
|
|
||||||
|
## Data Backup
|
||||||
|
|
||||||
|
Secure your data with Git-based backup:
|
||||||
|
|
||||||
|
- Automatic backups
|
||||||
|
- Manual backup triggers
|
||||||
|
- Support for GitHub and Gitea
|
||||||
|
- Multiple authentication methods (SSH, Token, Password)
|
||||||
|
- Configurable backup intervals
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
Extensive keyboard support:
|
||||||
|
|
||||||
|
- Customizable shortcuts
|
||||||
|
- Vim/Emacs keybindings (planned)
|
||||||
|
- Quick command palette
|
||||||
|
- Context-aware shortcuts
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
Built for speed:
|
||||||
|
|
||||||
|
- Fast startup time
|
||||||
|
- Smooth scrolling
|
||||||
|
- Efficient memory usage
|
||||||
|
- Large file support
|
||||||
|
|
||||||
|
## Privacy & Security
|
||||||
|
|
||||||
|
Your data is safe:
|
||||||
|
|
||||||
|
- Local-first storage
|
||||||
|
- Optional cloud backup
|
||||||
|
- No telemetry or tracking
|
||||||
|
- Open source codebase
|
||||||
|
|
||||||
107
frontend/docs/src/guide/getting-started.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
Learn the basics of using voidraft and create your first document.
|
||||||
|
|
||||||
|
## The Editor Interface
|
||||||
|
|
||||||
|
When you open voidraft, you'll see:
|
||||||
|
|
||||||
|
- **Main Editor**: The central area where you write and edit
|
||||||
|
- **Toolbar**: Quick access to common actions
|
||||||
|
- **Status Bar**: Shows current block language and other info
|
||||||
|
|
||||||
|
## Creating Code Blocks
|
||||||
|
|
||||||
|
voidraft uses a block-based editing system. Each block can have a different language:
|
||||||
|
|
||||||
|
1. Press `Ctrl+Enter` to create a new block
|
||||||
|
2. Type `∞∞∞` followed by a language name (e.g., `∞∞∞javascript`)
|
||||||
|
3. Start coding in that block
|
||||||
|
|
||||||
|
### Supported Languages
|
||||||
|
|
||||||
|
voidraft supports 30+ programming languages including:
|
||||||
|
- JavaScript, TypeScript
|
||||||
|
- Python, Go, Rust
|
||||||
|
- HTML, CSS, Sass
|
||||||
|
- SQL, YAML, JSON
|
||||||
|
- And many more...
|
||||||
|
|
||||||
|
## Basic Operations
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
- `Ctrl+Up/Down`: Move between blocks
|
||||||
|
- `Ctrl+Home/End`: Jump to first/last block
|
||||||
|
- `Ctrl+F`: Search within document
|
||||||
|
|
||||||
|
### Editing
|
||||||
|
|
||||||
|
- `Ctrl+D`: Duplicate current line
|
||||||
|
- `Ctrl+/`: Toggle comment
|
||||||
|
- `Alt+Up/Down`: Move line up/down
|
||||||
|
- `Ctrl+Shift+F`: Format code (if language supports Prettier)
|
||||||
|
|
||||||
|
### Block Management
|
||||||
|
|
||||||
|
- `Ctrl+Enter`: Create new block
|
||||||
|
- `Ctrl+Shift+Enter`: Create block above
|
||||||
|
- `Alt+Delete`: Delete current block
|
||||||
|
|
||||||
|
## Using the HTTP Client
|
||||||
|
|
||||||
|
voidraft includes a built-in HTTP client for testing APIs:
|
||||||
|
|
||||||
|
1. Create a block with HTTP language
|
||||||
|
2. Write your HTTP request:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST "https://api.example.com/users" {
|
||||||
|
content-type: "application/json"
|
||||||
|
|
||||||
|
@json {
|
||||||
|
name: "John Doe",
|
||||||
|
email: "john@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Click the run button to execute the request
|
||||||
|
4. View the response inline
|
||||||
|
|
||||||
|
## Multi-Window Support
|
||||||
|
|
||||||
|
Work on multiple documents simultaneously:
|
||||||
|
|
||||||
|
1. Go to `File > New Window` (or `Ctrl+Shift+N`)
|
||||||
|
2. Each window is independent
|
||||||
|
3. Changes are saved automatically
|
||||||
|
|
||||||
|
## Customizing Themes
|
||||||
|
|
||||||
|
Personalize your editor:
|
||||||
|
|
||||||
|
1. Open Settings (`Ctrl+,`)
|
||||||
|
2. Go to Appearance
|
||||||
|
3. Choose a theme or create your own
|
||||||
|
4. Customize colors to your preference
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
Learn essential shortcuts:
|
||||||
|
|
||||||
|
| Action | Shortcut |
|
||||||
|
|--------|----------|
|
||||||
|
| New Window | `Ctrl+Shift+N` |
|
||||||
|
| Search | `Ctrl+F` |
|
||||||
|
| Replace | `Ctrl+H` |
|
||||||
|
| Format Code | `Ctrl+Shift+F` |
|
||||||
|
| Toggle Theme | `Ctrl+Shift+T` |
|
||||||
|
| Command Palette | `Ctrl+Shift+P` |
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you know the basics:
|
||||||
|
|
||||||
|
- Explore [Features](/guide/features) in detail
|
||||||
|
|
||||||
63
frontend/docs/src/guide/installation.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
This guide will help you install voidraft on your system.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- **Operating System**: Windows 10 or later (macOS and Linux support planned)
|
||||||
|
- **RAM**: 4GB minimum, 8GB recommended
|
||||||
|
- **Disk Space**: 200MB free space
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
Visit the [releases page](https://github.com/landaiqing/voidraft/releases) and download the latest version for your platform:
|
||||||
|
|
||||||
|
- **Windows**: `voidraft-windows-amd64-installer.exe`
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. Download the installer from the releases page
|
||||||
|
2. Run the `voidraft-windows-amd64-installer.exe` file
|
||||||
|
3. Follow the installation wizard
|
||||||
|
4. Launch voidraft from the Start menu or desktop shortcut
|
||||||
|
|
||||||
|
## First Launch
|
||||||
|
|
||||||
|
When you first launch voidraft:
|
||||||
|
|
||||||
|
1. The application will create a data directory to store your documents
|
||||||
|
2. You'll see the main editor interface with a welcome block
|
||||||
|
3. Start typing or create your first code block!
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
voidraft stores its configuration and data in:
|
||||||
|
|
||||||
|
- **Windows**: `%APPDATA%/voidraft/`
|
||||||
|
|
||||||
|
You can customize various settings including:
|
||||||
|
- Editor theme (dark/light mode)
|
||||||
|
- Code formatting preferences
|
||||||
|
- Backup settings
|
||||||
|
- Keyboard shortcuts
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
voidraft includes an auto-update feature that will notify you when new versions are available. You can:
|
||||||
|
|
||||||
|
- Check for updates manually from the settings
|
||||||
|
- Enable automatic updates
|
||||||
|
- Choose your preferred update source
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If you encounter any issues during installation:
|
||||||
|
|
||||||
|
1. Make sure you have administrator privileges
|
||||||
|
2. Check that your antivirus isn't blocking the installation
|
||||||
|
3. Visit our [GitHub issues](https://github.com/landaiqing/voidraft/issues) page for help
|
||||||
|
|
||||||
|
Next: [Getting Started →](/guide/getting-started)
|
||||||
|
|
||||||
50
frontend/docs/src/guide/introduction.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
Welcome to voidraft - an elegant text snippet recording tool designed specifically for developers.
|
||||||
|
|
||||||
|
## What is voidraft?
|
||||||
|
|
||||||
|
voidraft is a modern desktop application that helps developers manage text snippets, code blocks, API responses, meeting notes, and daily to-do lists. It provides a smooth and elegant editing experience with powerful features tailored for development workflows.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Block-Based Editing
|
||||||
|
|
||||||
|
voidraft uses a unique block-based editing system inspired by Heynote. You can split your content into independent code blocks, each with:
|
||||||
|
- Different programming language settings
|
||||||
|
- Syntax highlighting
|
||||||
|
- Independent formatting
|
||||||
|
- Easy navigation between blocks
|
||||||
|
|
||||||
|
### Developer Tools
|
||||||
|
|
||||||
|
- **HTTP Client**: Test APIs directly within the editor
|
||||||
|
- **Code Formatting**: Built-in Prettier support for multiple languages
|
||||||
|
- **Syntax Highlighting**: Support for 30+ programming languages
|
||||||
|
- **Auto Language Detection**: Automatically recognizes code block language types
|
||||||
|
|
||||||
|
### Customization
|
||||||
|
|
||||||
|
- **Custom Themes**: Create and save your own editor themes
|
||||||
|
- **Extensions**: Rich set of editor extensions including minimap, rainbow brackets, color picker, and more
|
||||||
|
- **Multi-Window**: Work on multiple documents simultaneously
|
||||||
|
|
||||||
|
### Data Management
|
||||||
|
|
||||||
|
- **Git-Based Backup**: Automatic backup using Git repositories
|
||||||
|
- **Cloud Sync**: Sync your data across devices
|
||||||
|
- **Auto-Update**: Stay up-to-date with the latest features
|
||||||
|
|
||||||
|
## Why voidraft?
|
||||||
|
|
||||||
|
- **Developer-Focused**: Built with developers' needs in mind
|
||||||
|
- **Modern Stack**: Uses cutting-edge technologies (Wails3, Vue 3, CodeMirror 6)
|
||||||
|
- **Cross-Platform**: Works on Windows (macOS and Linux support planned)
|
||||||
|
- **Open Source**: MIT licensed, community-driven development
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Ready to start? Download the latest version from our [releases page](https://github.com/landaiqing/voidraft/releases) or continue reading the documentation to learn more.
|
||||||
|
|
||||||
|
Next: [Installation →](/guide/installation)
|
||||||
|
|
||||||
56
frontend/docs/src/index.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "voidraft"
|
||||||
|
text: "An elegant text snippet recording tool"
|
||||||
|
tagline: Designed for developers, built with modern technology
|
||||||
|
image:
|
||||||
|
src: /img/hero.png
|
||||||
|
alt: "voidraft"
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Get Started
|
||||||
|
link: https://github.com/landaiqing/voidraft/releases
|
||||||
|
- theme: alt
|
||||||
|
text: Documentation
|
||||||
|
link: /guide/introduction
|
||||||
|
|
||||||
|
features:
|
||||||
|
- icon: 📝
|
||||||
|
title: Block-Based Editing
|
||||||
|
details: Split your content into independent code blocks, each with different language settings. Inspired by Heynote's innovative design philosophy.
|
||||||
|
|
||||||
|
- icon: 🎨
|
||||||
|
title: Syntax Highlighting
|
||||||
|
details: Built-in support for 30+ programming languages with automatic language detection and Prettier integration for code formatting.
|
||||||
|
|
||||||
|
- icon: 🌐
|
||||||
|
title: HTTP Client
|
||||||
|
details: Integrated HTTP client with support for multiple request formats including JSON, FormData, XML, and more. Test APIs directly within the editor.
|
||||||
|
|
||||||
|
- icon: 🎯
|
||||||
|
title: Multi-Window Support
|
||||||
|
details: Work on multiple documents simultaneously with independent windows. Each window maintains its own state and configuration.
|
||||||
|
|
||||||
|
- icon: 🎭
|
||||||
|
title: Customizable Themes
|
||||||
|
details: Full theme customization support with dark/light modes. Create and save your own editor themes to match your preferences.
|
||||||
|
|
||||||
|
- icon: 🔧
|
||||||
|
title: Rich Extensions
|
||||||
|
details: VSCode-style search and replace, rainbow brackets, minimap, color picker, translation tool, text highlighting, and more.
|
||||||
|
|
||||||
|
- icon: 🔄
|
||||||
|
title: Auto-Update System
|
||||||
|
details: Built-in self-update mechanism with support for multiple update sources. Stay up-to-date with the latest features and improvements.
|
||||||
|
|
||||||
|
- icon: ☁️
|
||||||
|
title: Git-Based Backup
|
||||||
|
details: Automatic data backup using Git repositories. Supports GitHub, Gitea, with multiple authentication methods including SSH and tokens.
|
||||||
|
|
||||||
|
- icon: ⚡
|
||||||
|
title: Modern Architecture
|
||||||
|
details: Built with Wails3, Vue 3, and CodeMirror 6. Cross-platform desktop application with native performance and modern UI.
|
||||||
|
|
||||||
|
---
|
||||||
BIN
frontend/docs/src/public/icon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/docs/src/public/icon/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
frontend/docs/src/public/icon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
3
frontend/docs/src/public/icon/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
21
frontend/docs/src/public/icon/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "voidraft",
|
||||||
|
"short_name": "voidraft",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/img/web-app-manifest-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
BIN
frontend/docs/src/public/icon/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/docs/src/public/icon/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
frontend/docs/src/public/img/hero.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
163
frontend/docs/src/zh/guide/features.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 功能特性
|
||||||
|
|
||||||
|
探索 voidraft 的强大功能,让它成为开发者的优秀工具。
|
||||||
|
|
||||||
|
## 块状编辑
|
||||||
|
|
||||||
|
voidraft 的核心功能是其块状编辑系统:
|
||||||
|
|
||||||
|
- 每个块可以有不同的编程语言
|
||||||
|
- 块之间由分隔符分隔(`∞∞∞语言`)
|
||||||
|
- 快速在块之间导航
|
||||||
|
- 独立格式化每个块
|
||||||
|
|
||||||
|
## 语法高亮
|
||||||
|
|
||||||
|
支持 30+ 种语言的专业语法高亮:
|
||||||
|
|
||||||
|
- 自动语言检测
|
||||||
|
- 可自定义配色方案
|
||||||
|
- 支持嵌套语言
|
||||||
|
- 代码折叠支持
|
||||||
|
|
||||||
|
## HTTP 客户端
|
||||||
|
|
||||||
|
用于 API 测试的内置 HTTP 客户端:
|
||||||
|
|
||||||
|
### 请求类型
|
||||||
|
- GET、POST、PUT、DELETE、PATCH
|
||||||
|
- 自定义请求头
|
||||||
|
- 多种请求体格式:JSON、FormData、URL 编码、XML、文本
|
||||||
|
|
||||||
|
### 请求变量
|
||||||
|
定义和重用变量:
|
||||||
|
|
||||||
|
```http
|
||||||
|
@var {
|
||||||
|
baseUrl: "https://api.example.com",
|
||||||
|
token: "your-api-token"
|
||||||
|
}
|
||||||
|
|
||||||
|
GET "{{baseUrl}}/users" {
|
||||||
|
authorization: "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应处理
|
||||||
|
- 查看格式化的 JSON 响应
|
||||||
|
- 查看响应时间和大小
|
||||||
|
- 检查响应头
|
||||||
|
- 保存响应以供日后使用
|
||||||
|
|
||||||
|
## 代码格式化
|
||||||
|
|
||||||
|
集成 Prettier 支持:
|
||||||
|
|
||||||
|
- 保存时格式化(可选)
|
||||||
|
- 格式化选区或整个块
|
||||||
|
- 支持 JavaScript、TypeScript、CSS、HTML、JSON 等
|
||||||
|
- 可自定义格式化规则
|
||||||
|
|
||||||
|
## 编辑器扩展
|
||||||
|
|
||||||
|
### VSCode 风格搜索
|
||||||
|
- 查找和替换,支持正则表达式
|
||||||
|
- 区分大小写和全字匹配选项
|
||||||
|
- 跨所有块搜索
|
||||||
|
|
||||||
|
### 小地图
|
||||||
|
- 文档的鸟瞰图
|
||||||
|
- 快速导航
|
||||||
|
- 可自定义大小和位置
|
||||||
|
|
||||||
|
### 彩虹括号
|
||||||
|
- 彩色括号配对
|
||||||
|
- 更容易匹配括号
|
||||||
|
- 可自定义颜色
|
||||||
|
|
||||||
|
### 颜色选择器
|
||||||
|
- 可视化颜色选择
|
||||||
|
- 支持 hex、RGB、HSL
|
||||||
|
- 实时预览
|
||||||
|
|
||||||
|
### 翻译工具
|
||||||
|
- 翻译选定的文本
|
||||||
|
- 支持多种语言
|
||||||
|
- 快速键盘访问
|
||||||
|
|
||||||
|
### 文本高亮
|
||||||
|
- 高亮重要文本
|
||||||
|
- 多种高亮颜色
|
||||||
|
- 持久化高亮
|
||||||
|
|
||||||
|
## 多窗口支持
|
||||||
|
|
||||||
|
高效使用多个窗口:
|
||||||
|
|
||||||
|
- 每个窗口都是独立的
|
||||||
|
- 独立的文档
|
||||||
|
- 同步的设置
|
||||||
|
- 窗口状态持久化
|
||||||
|
|
||||||
|
## 主题自定义
|
||||||
|
|
||||||
|
完全控制编辑器外观:
|
||||||
|
|
||||||
|
### 内置主题
|
||||||
|
- 深色模式
|
||||||
|
- 浅色模式
|
||||||
|
- 根据系统自动切换
|
||||||
|
|
||||||
|
### 自定义主题
|
||||||
|
- 创建你自己的主题
|
||||||
|
- 自定义每种颜色
|
||||||
|
- 保存和分享主题
|
||||||
|
- 导入社区主题
|
||||||
|
|
||||||
|
## 自动更新系统
|
||||||
|
|
||||||
|
通过自动更新保持最新:
|
||||||
|
|
||||||
|
- 后台更新检查
|
||||||
|
- 新版本通知
|
||||||
|
- 一键更新
|
||||||
|
- 更新历史
|
||||||
|
- 支持多个更新源(GitHub、Gitea)
|
||||||
|
|
||||||
|
## 数据备份
|
||||||
|
|
||||||
|
使用基于 Git 的备份保护你的数据:
|
||||||
|
|
||||||
|
- 自动备份
|
||||||
|
- 手动触发备份
|
||||||
|
- 支持 GitHub 和 Gitea
|
||||||
|
- 多种认证方式(SSH、Token、密码)
|
||||||
|
- 可配置备份间隔
|
||||||
|
|
||||||
|
## 键盘快捷键
|
||||||
|
|
||||||
|
广泛的键盘支持:
|
||||||
|
|
||||||
|
- 可自定义快捷键
|
||||||
|
- Vim/Emacs 按键绑定(计划中)
|
||||||
|
- 快速命令面板
|
||||||
|
- 上下文感知快捷键
|
||||||
|
|
||||||
|
## 性能
|
||||||
|
|
||||||
|
专为速度而构建:
|
||||||
|
|
||||||
|
- 快速启动时间
|
||||||
|
- 流畅滚动
|
||||||
|
- 高效内存使用
|
||||||
|
- 支持大文件
|
||||||
|
|
||||||
|
## 隐私与安全
|
||||||
|
|
||||||
|
你的数据是安全的:
|
||||||
|
|
||||||
|
- 本地优先存储
|
||||||
|
- 可选云备份
|
||||||
|
- 无遥测或跟踪
|
||||||
|
- 开源代码库
|
||||||
|
|
||||||
107
frontend/docs/src/zh/guide/getting-started.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# 快速开始
|
||||||
|
|
||||||
|
学习使用 voidraft 的基础知识并创建你的第一个文档。
|
||||||
|
|
||||||
|
## 编辑器界面
|
||||||
|
|
||||||
|
当你打开 voidraft 时,你将看到:
|
||||||
|
|
||||||
|
- **主编辑器**:编写和编辑的中心区域
|
||||||
|
- **工具栏**:快速访问常用操作
|
||||||
|
- **状态栏**:显示当前块的语言和其他信息
|
||||||
|
|
||||||
|
## 创建代码块
|
||||||
|
|
||||||
|
voidraft 使用基于块的编辑系统。每个块可以有不同的语言:
|
||||||
|
|
||||||
|
1. 按 `Ctrl+Enter` 创建新块
|
||||||
|
2. 输入 `∞∞∞` 后跟语言名称(例如 `∞∞∞javascript`)
|
||||||
|
3. 在该块中开始编码
|
||||||
|
|
||||||
|
### 支持的语言
|
||||||
|
|
||||||
|
voidraft 支持 30+ 种编程语言,包括:
|
||||||
|
- JavaScript、TypeScript
|
||||||
|
- Python、Go、Rust
|
||||||
|
- HTML、CSS、Sass
|
||||||
|
- SQL、YAML、JSON
|
||||||
|
- 以及更多...
|
||||||
|
|
||||||
|
## 基本操作
|
||||||
|
|
||||||
|
### 导航
|
||||||
|
|
||||||
|
- `Ctrl+Up/Down`:在块之间移动
|
||||||
|
- `Ctrl+Home/End`:跳转到第一个/最后一个块
|
||||||
|
- `Ctrl+F`:在文档中搜索
|
||||||
|
|
||||||
|
### 编辑
|
||||||
|
|
||||||
|
- `Ctrl+D`:复制当前行
|
||||||
|
- `Ctrl+/`:切换注释
|
||||||
|
- `Alt+Up/Down`:向上/向下移动行
|
||||||
|
- `Ctrl+Shift+F`:格式化代码(如果语言支持 Prettier)
|
||||||
|
|
||||||
|
### 块管理
|
||||||
|
|
||||||
|
- `Ctrl+Enter`:创建新块
|
||||||
|
- `Ctrl+Shift+Enter`:在上方创建块
|
||||||
|
- `Alt+Delete`:删除当前块
|
||||||
|
|
||||||
|
## 使用 HTTP 客户端
|
||||||
|
|
||||||
|
voidraft 包含用于测试 API 的内置 HTTP 客户端:
|
||||||
|
|
||||||
|
1. 创建一个 HTTP 语言的块
|
||||||
|
2. 编写你的 HTTP 请求:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST "https://api.example.com/users" {
|
||||||
|
content-type: "application/json"
|
||||||
|
|
||||||
|
@json {
|
||||||
|
name: "张三",
|
||||||
|
email: "zhangsan@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 点击运行按钮执行请求
|
||||||
|
4. 内联查看响应
|
||||||
|
|
||||||
|
## 多窗口支持
|
||||||
|
|
||||||
|
同时处理多个文档:
|
||||||
|
|
||||||
|
1. 转到 `文件 > 新建窗口`(或 `Ctrl+Shift+N`)
|
||||||
|
2. 每个窗口都是独立的
|
||||||
|
3. 更改会自动保存
|
||||||
|
|
||||||
|
## 自定义主题
|
||||||
|
|
||||||
|
个性化你的编辑器:
|
||||||
|
|
||||||
|
1. 打开设置(`Ctrl+,`)
|
||||||
|
2. 转到外观
|
||||||
|
3. 选择主题或创建自己的主题
|
||||||
|
4. 根据你的偏好自定义颜色
|
||||||
|
|
||||||
|
## 键盘快捷键
|
||||||
|
|
||||||
|
学习基本快捷键:
|
||||||
|
|
||||||
|
| 操作 | 快捷键 |
|
||||||
|
|-----|--------|
|
||||||
|
| 新建窗口 | `Ctrl+Shift+N` |
|
||||||
|
| 搜索 | `Ctrl+F` |
|
||||||
|
| 替换 | `Ctrl+H` |
|
||||||
|
| 格式化代码 | `Ctrl+Shift+F` |
|
||||||
|
| 切换主题 | `Ctrl+Shift+T` |
|
||||||
|
| 命令面板 | `Ctrl+Shift+P` |
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
现在你已经了解了基础知识:
|
||||||
|
|
||||||
|
- 详细探索[功能特性](/zh/guide/features)
|
||||||
|
|
||||||
63
frontend/docs/src/zh/guide/installation.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 安装
|
||||||
|
|
||||||
|
本指南将帮助你在系统上安装 voidraft。
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
- **操作系统**:Windows 10 或更高版本(macOS 和 Linux 支持计划中)
|
||||||
|
- **内存**:最低 4GB,推荐 8GB
|
||||||
|
- **磁盘空间**:200MB 可用空间
|
||||||
|
|
||||||
|
## 下载
|
||||||
|
|
||||||
|
访问[发布页面](https://github.com/landaiqing/voidraft/releases)并下载适合你平台的最新版本:
|
||||||
|
|
||||||
|
- **Windows**:`voidraft-windows-amd64-installer.exe`
|
||||||
|
|
||||||
|
## 安装步骤
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. 从发布页面下载安装程序
|
||||||
|
2. 运行 `voidraft-windows-amd64-installer.exe` 文件
|
||||||
|
3. 按照安装向导操作
|
||||||
|
4. 从开始菜单或桌面快捷方式启动 voidraft
|
||||||
|
|
||||||
|
## 首次启动
|
||||||
|
|
||||||
|
首次启动 voidraft 时:
|
||||||
|
|
||||||
|
1. 应用程序将创建一个数据目录来存储你的文档
|
||||||
|
2. 你将看到带有欢迎块的主编辑器界面
|
||||||
|
3. 开始输入或创建你的第一个代码块!
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
voidraft 将其配置和数据存储在:
|
||||||
|
|
||||||
|
- **Windows**:`%APPDATA%/voidraft/`
|
||||||
|
|
||||||
|
你可以自定义各种设置,包括:
|
||||||
|
- 编辑器主题(深色/浅色模式)
|
||||||
|
- 代码格式化偏好
|
||||||
|
- 备份设置
|
||||||
|
- 键盘快捷键
|
||||||
|
|
||||||
|
## 更新
|
||||||
|
|
||||||
|
voidraft 包含自动更新功能,会在有新版本时通知你。你可以:
|
||||||
|
|
||||||
|
- 从设置中手动检查更新
|
||||||
|
- 启用自动更新
|
||||||
|
- 选择首选的更新源
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
如果在安装过程中遇到任何问题:
|
||||||
|
|
||||||
|
1. 确保你有管理员权限
|
||||||
|
2. 检查杀毒软件是否阻止了安装
|
||||||
|
3. 访问我们的 [GitHub issues](https://github.com/landaiqing/voidraft/issues) 页面寻求帮助
|
||||||
|
|
||||||
|
下一步:[快速开始 →](/zh/guide/getting-started)
|
||||||
|
|
||||||
50
frontend/docs/src/zh/guide/introduction.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 简介
|
||||||
|
|
||||||
|
欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具。
|
||||||
|
|
||||||
|
## 什么是 voidraft?
|
||||||
|
|
||||||
|
voidraft 是一个现代化的桌面应用程序,帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
### 块状编辑模式
|
||||||
|
|
||||||
|
voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
|
||||||
|
- 不同的编程语言设置
|
||||||
|
- 语法高亮
|
||||||
|
- 独立格式化
|
||||||
|
- 轻松在块之间导航
|
||||||
|
|
||||||
|
### 开发者工具
|
||||||
|
|
||||||
|
- **HTTP 客户端**:直接在编辑器中测试 API
|
||||||
|
- **代码格式化**:内置 Prettier 支持多种语言
|
||||||
|
- **语法高亮**:支持 30+ 种编程语言
|
||||||
|
- **自动语言检测**:自动识别代码块语言类型
|
||||||
|
|
||||||
|
### 自定义
|
||||||
|
|
||||||
|
- **自定义主题**:创建并保存你自己的编辑器主题
|
||||||
|
- **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
|
||||||
|
- **多窗口**:同时处理多个文档
|
||||||
|
|
||||||
|
### 数据管理
|
||||||
|
|
||||||
|
- **Git 备份**:使用 Git 仓库自动备份
|
||||||
|
- **云同步**:跨设备同步你的数据
|
||||||
|
- **自动更新**:及时获取最新功能
|
||||||
|
|
||||||
|
## 为什么选择 voidraft?
|
||||||
|
|
||||||
|
- **专注开发者**:考虑开发者需求而构建
|
||||||
|
- **现代技术栈**:使用前沿技术(Wails3、Vue 3、CodeMirror 6)
|
||||||
|
- **跨平台**:支持 Windows(macOS 和 Linux 支持计划中)
|
||||||
|
- **开源**:MIT 许可证,社区驱动开发
|
||||||
|
|
||||||
|
## 开始使用
|
||||||
|
|
||||||
|
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
|
||||||
|
|
||||||
|
下一步:[安装 →](/zh/guide/installation)
|
||||||
|
|
||||||
56
frontend/docs/src/zh/index.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "voidraft"
|
||||||
|
text: "优雅的文本片段记录工具"
|
||||||
|
tagline: 为开发者设计,用现代技术打造
|
||||||
|
image:
|
||||||
|
src: /img/hero.png
|
||||||
|
alt: "voidraft"
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: 开始使用
|
||||||
|
link: https://github.com/landaiqing/voidraft/releases
|
||||||
|
- theme: alt
|
||||||
|
text: 使用文档
|
||||||
|
link: /zh/guide/introduction
|
||||||
|
|
||||||
|
features:
|
||||||
|
- icon: 📝
|
||||||
|
title: 块状编辑模式
|
||||||
|
details: 将内容分割为独立的代码块,每个块可设置不同语言。继承了 Heynote 优雅的块状编辑理念。
|
||||||
|
|
||||||
|
- icon: 🎨
|
||||||
|
title: 语法高亮
|
||||||
|
details: 内置支持 30+ 种编程语言的语法高亮,自动语言检测,集成 Prettier 代码格式化工具。
|
||||||
|
|
||||||
|
- icon: 🌐
|
||||||
|
title: HTTP 客户端
|
||||||
|
details: 集成 HTTP 客户端,支持 JSON、FormData、XML 等多种请求格式。直接在编辑器中测试 API。
|
||||||
|
|
||||||
|
- icon: 🎯
|
||||||
|
title: 多窗口支持
|
||||||
|
details: 同时编辑多个文档,每个窗口独立维护自己的状态和配置。
|
||||||
|
|
||||||
|
- icon: 🎭
|
||||||
|
title: 主题自定义
|
||||||
|
details: 完整的主题自定义支持,支持深色/浅色模式。创建并保存你自己的编辑器主题。
|
||||||
|
|
||||||
|
- icon: 🔧
|
||||||
|
title: 丰富的扩展
|
||||||
|
details: VSCode 风格搜索替换、彩虹括号、小地图、颜色选择器、翻译工具、文本高亮等实用扩展。
|
||||||
|
|
||||||
|
- icon: 🔄
|
||||||
|
title: 自动更新系统
|
||||||
|
details: 内置自我更新机制,支持多个更新源。及时获取最新功能和改进。
|
||||||
|
|
||||||
|
- icon: ☁️
|
||||||
|
title: Git 备份
|
||||||
|
details: 基于 Git 的自动数据备份。支持 GitHub、Gitea,提供 SSH、Token 等多种认证方式。
|
||||||
|
|
||||||
|
- icon: ⚡
|
||||||
|
title: 现代化架构
|
||||||
|
details: 采用 Wails3、Vue 3 和 CodeMirror 6 构建。跨平台桌面应用,原生性能,现代化界面。
|
||||||
|
|
||||||
|
---
|
||||||
875
frontend/package-lock.json
generated
@@ -11,7 +11,10 @@
|
|||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"lint:fix": "eslint --fix",
|
"lint:fix": "eslint --fix",
|
||||||
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js",
|
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js",
|
||||||
"test": "vitest"
|
"test": "vitest",
|
||||||
|
"docs:dev": "vitepress dev docs",
|
||||||
|
"docs:build": "vitepress build docs",
|
||||||
|
"docs:preview": "vitepress preview docs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.19.1",
|
"@codemirror/autocomplete": "^6.19.1",
|
||||||
@@ -86,8 +89,9 @@
|
|||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.1.12",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
|
"vitepress": "^2.0.0-alpha.12",
|
||||||
"vitest": "^4.0.6",
|
"vitest": "^4.0.6",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.1.2"
|
"vue-tsc": "^3.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,11 @@
|
|||||||
// @formdata - 表单数据(属性必须用逗号分隔)
|
// @formdata - 表单数据(属性必须用逗号分隔)
|
||||||
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
|
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
|
||||||
// @text - 纯文本内容
|
// @text - 纯文本内容
|
||||||
|
// @params - URL 参数(用于 GET 请求)
|
||||||
|
// @xml - XML 格式(固定 key: xml)
|
||||||
|
// @html - HTML 格式(固定 key: html)
|
||||||
|
// @javascript - JavaScript 格式(固定 key: javascript)
|
||||||
|
// @binary - 二进制文件(固定 key: binary,值格式:@file 路径)
|
||||||
//
|
//
|
||||||
// 3. 变量定义:
|
// 3. 变量定义:
|
||||||
// @var {
|
// @var {
|
||||||
@@ -70,7 +75,52 @@
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// 示例 5 - 带响应数据:
|
// 示例 5 - URL 参数请求:
|
||||||
|
// GET "http://api.example.com/users" {
|
||||||
|
// @params {
|
||||||
|
// page: 1,
|
||||||
|
// size: 20,
|
||||||
|
// keyword: "张三"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 6 - XML 请求:
|
||||||
|
// POST "http://api.example.com/soap" {
|
||||||
|
// content-type: "application/xml"
|
||||||
|
//
|
||||||
|
// @xml {
|
||||||
|
// xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 7 - HTML 请求:
|
||||||
|
// POST "http://api.example.com/render" {
|
||||||
|
// content-type: "text/html"
|
||||||
|
//
|
||||||
|
// @html {
|
||||||
|
// html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 8 - JavaScript 请求:
|
||||||
|
// POST "http://api.example.com/execute" {
|
||||||
|
// content-type: "application/javascript"
|
||||||
|
//
|
||||||
|
// @javascript {
|
||||||
|
// javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 9 - 二进制文件上传:
|
||||||
|
// POST "http://api.example.com/upload" {
|
||||||
|
// content-type: "application/octet-stream"
|
||||||
|
//
|
||||||
|
// @binary {
|
||||||
|
// binary: "@file E://Documents/avatar.png"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 10 - 带响应数据:
|
||||||
// POST "http://api.example.com/login" {
|
// POST "http://api.example.com/login" {
|
||||||
// @json {
|
// @json {
|
||||||
// username: "admin",
|
// username: "admin",
|
||||||
@@ -120,14 +170,13 @@ ResponseDeclaration {
|
|||||||
ResponseBlock
|
ResponseBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应状态:状态码(200 或 200-OK)或 "error" 关键字
|
// 响应状态:数字或数字-标识符组合(200 或 200-OK)或 "error" 关键字
|
||||||
// 数字开头的状态码作为一个整体 token
|
|
||||||
ResponseStatus {
|
ResponseStatus {
|
||||||
StatusCode |
|
(NumberLiteral ("-" identifier)?) |
|
||||||
@specialize[@name=ErrorStatus]<identifier, "error">
|
@specialize[@name=ErrorStatus]<identifier, "error">
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应时间:数字 + "ms" 作为一个整体 token
|
// 响应时间:直接使用 TimeValue token
|
||||||
ResponseTime {
|
ResponseTime {
|
||||||
TimeValue
|
TimeValue
|
||||||
}
|
}
|
||||||
@@ -168,7 +217,12 @@ AtRule {
|
|||||||
(JsonRule |
|
(JsonRule |
|
||||||
FormDataRule |
|
FormDataRule |
|
||||||
UrlEncodedRule |
|
UrlEncodedRule |
|
||||||
TextRule) ","?
|
TextRule |
|
||||||
|
ParamsRule |
|
||||||
|
XmlRule |
|
||||||
|
HtmlRule |
|
||||||
|
JavaScriptRule |
|
||||||
|
BinaryRule) ","?
|
||||||
}
|
}
|
||||||
|
|
||||||
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
||||||
@@ -195,6 +249,36 @@ TextRule {
|
|||||||
JsonBlock
|
JsonBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @params 块:URL 参数(用于 GET 请求,属性必须用逗号分隔)
|
||||||
|
ParamsRule {
|
||||||
|
@specialize[@name=ParamsKeyword]<AtKeyword, "@params">
|
||||||
|
JsonBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @xml 块:XML 格式请求体(固定 key: xml)
|
||||||
|
XmlRule {
|
||||||
|
@specialize[@name=XmlKeyword]<AtKeyword, "@xml">
|
||||||
|
XmlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @html 块:HTML 格式请求体(固定 key: html)
|
||||||
|
HtmlRule {
|
||||||
|
@specialize[@name=HtmlKeyword]<AtKeyword, "@html">
|
||||||
|
HtmlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @javascript 块:JavaScript 格式请求体(固定 key: javascript)
|
||||||
|
JavaScriptRule {
|
||||||
|
@specialize[@name=JavaScriptKeyword]<AtKeyword, "@javascript">
|
||||||
|
JavaScriptBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @binary 块:二进制文件(固定 key: binary,值格式:@file 路径)
|
||||||
|
BinaryRule {
|
||||||
|
@specialize[@name=BinaryKeyword]<AtKeyword, "@binary">
|
||||||
|
BinaryBlock
|
||||||
|
}
|
||||||
|
|
||||||
// 普通块结构(属性逗号可选,最多一个请求体)
|
// 普通块结构(属性逗号可选,最多一个请求体)
|
||||||
Block {
|
Block {
|
||||||
"{" blockContent? "}"
|
"{" blockContent? "}"
|
||||||
@@ -229,6 +313,30 @@ JsonProperty {
|
|||||||
":" jsonValue
|
":" jsonValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XML 块结构(可为空 {} 或必须包含 xml: value)
|
||||||
|
XmlBlock {
|
||||||
|
"{" (@specialize[@name=XmlKey]<identifier, "xml"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML 块结构(可为空 {} 或必须包含 html: value)
|
||||||
|
HtmlBlock {
|
||||||
|
"{" (@specialize[@name=HtmlKey]<identifier, "html"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// JavaScript 块结构(可为空 {} 或必须包含 javascript: value)
|
||||||
|
JavaScriptBlock {
|
||||||
|
"{" (@specialize[@name=JavaScriptKey]<identifier, "javascript"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary 块结构(可为空 {} 或必须包含 binary: value)
|
||||||
|
BinaryBlock {
|
||||||
|
"{" (@specialize[@name=BinaryKey]<identifier, "binary"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
// 值
|
// 值
|
||||||
NumberLiteral {
|
NumberLiteral {
|
||||||
numberLiteralInner Unit?
|
numberLiteralInner Unit?
|
||||||
@@ -328,19 +436,14 @@ JsonNull { @specialize[@name=Null]<identifier, "null"> }
|
|||||||
"T" @digit @digit ":" @digit @digit ":" @digit @digit
|
"T" @digit @digit ":" @digit @digit ":" @digit @digit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态码:纯数字或数字-字母组合(200, 200-OK, 404-Not-Found)
|
// 时间值:数字 + ms,作为一个整体 token
|
||||||
StatusCode {
|
TimeValue[isolate] {
|
||||||
@digit+ ("-" @asciiLetter (@asciiLetter | "-")*)?
|
|
||||||
}
|
|
||||||
|
|
||||||
// 时间值:数字 + ms(123ms)
|
|
||||||
TimeValue {
|
|
||||||
@digit+ "ms"
|
@digit+ "ms"
|
||||||
}
|
}
|
||||||
|
|
||||||
whitespace { @whitespace+ }
|
whitespace { @whitespace+ }
|
||||||
|
|
||||||
@precedence { Timestamp, TimeValue, StatusCode, numberLiteralInner, VariableRef, identifier, Unit }
|
@precedence { Timestamp, TimeValue, numberLiteralInner, VariableRef, identifier, Unit }
|
||||||
|
|
||||||
numberLiteralInner {
|
numberLiteralInner {
|
||||||
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parser } from './http.parser';
|
||||||
|
|
||||||
|
describe('HTTP Grammar - 固定 Key 约束测试', () => {
|
||||||
|
|
||||||
|
function parseAndCheck(content: string, expectError: boolean = false) {
|
||||||
|
const tree = parser.parse(content);
|
||||||
|
|
||||||
|
console.log('\n=== 语法树结构 ===');
|
||||||
|
let hasError = false;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node) => {
|
||||||
|
const depth = getDepth(node.node);
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
const text = content.slice(node.from, node.to);
|
||||||
|
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
|
||||||
|
|
||||||
|
if (node.name === '⚠') {
|
||||||
|
hasError = true;
|
||||||
|
console.log(`${indent}⚠ 错误节点 [${node.from}-${node.to}]: ${JSON.stringify(preview)}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${indent}${node.name.padEnd(30 - depth * 2)} [${String(node.from).padStart(3)}-${String(node.to).padEnd(3)}]: ${JSON.stringify(preview)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expectError) {
|
||||||
|
expect(hasError).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(hasError).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDepth(currentNode: any): number {
|
||||||
|
let depth = 0;
|
||||||
|
let node = currentNode;
|
||||||
|
while (node && node.parent) {
|
||||||
|
depth++;
|
||||||
|
node = node.parent;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('✅ @xml - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 xml key', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
data: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝多个属性', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user></user>",
|
||||||
|
other: "value"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @html - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 html key', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
content: "<div><h1>标题</h1></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @javascript - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 javascript key', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'world'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {
|
||||||
|
code: "function hello() { return 'world'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @binary - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 binary key', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
file: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 对比:@json 和 @params 允许任意 key', () => {
|
||||||
|
it('@json 可以使用任意 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/api" {
|
||||||
|
@json {
|
||||||
|
name: "张三",
|
||||||
|
age: 25,
|
||||||
|
email: "test@example.com"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@params 可以使用任意 key 名称', () => {
|
||||||
|
const content = `GET "https://api.example.com/users" {
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
filter: "active"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 空块测试 - 现在支持空块', () => {
|
||||||
|
it('@xml 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@html 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@javascript 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@binary 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('❌ 定义了 key 但没有值应该报错', () => {
|
||||||
|
it('@xml 定义了 xml key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@html 定义了 html key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@javascript 定义了 javascript key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {
|
||||||
|
javascript:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@binary 定义了 binary key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parser } from './http.parser';
|
||||||
|
|
||||||
|
describe('HTTP Grammar - 新增请求格式测试', () => {
|
||||||
|
|
||||||
|
function parseAndCheck(content: string, expectError: boolean = false) {
|
||||||
|
const tree = parser.parse(content);
|
||||||
|
|
||||||
|
console.log('\n=== 语法树结构 ===');
|
||||||
|
let hasError = false;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node) => {
|
||||||
|
const depth = getDepth(node.node);
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
const text = content.slice(node.from, node.to);
|
||||||
|
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
|
||||||
|
|
||||||
|
if (node.name === '⚠') {
|
||||||
|
hasError = true;
|
||||||
|
console.log(`${indent}⚠ 错误节点 [${node.from}-${node.to}]: ${JSON.stringify(preview)}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${indent}${node.name.padEnd(30 - depth * 2)} [${String(node.from).padStart(3)}-${String(node.to).padEnd(3)}]: ${JSON.stringify(preview)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expectError) {
|
||||||
|
expect(hasError).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(hasError).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDepth(currentNode: any): number {
|
||||||
|
let depth = 0;
|
||||||
|
let node = currentNode;
|
||||||
|
while (node && node.parent) {
|
||||||
|
depth++;
|
||||||
|
node = node.parent;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('✅ @params - URL 参数', () => {
|
||||||
|
const content = `GET "https://api.example.com/users" {
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: "张三"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml - XML 格式请求体', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html - HTML 格式请求体', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
content-type: "text/html"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript - JavaScript 格式请求体', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
content-type: "application/javascript"
|
||||||
|
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary - 二进制文件上传', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
content-type: "application/octet-stream"
|
||||||
|
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 混合使用 - @params 和响应', () => {
|
||||||
|
const content = `GET "https://api.example.com/search" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
q: "关键词",
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@response 200-OK 156ms 2025-11-03T10:30:00 {
|
||||||
|
"total": 100,
|
||||||
|
"data": []
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 复杂 XML 内容', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 多行 JavaScript', () => {
|
||||||
|
const content = `POST "https://api.example.com/run" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "function calculate(a, b) {\\n return a + b;\\n}"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary 支持不同路径格式', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/Users/Documents/file.pdf"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @params 支持空值', () => {
|
||||||
|
const content = `GET "https://api.example.com/list" {
|
||||||
|
@params {
|
||||||
|
filter: "",
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @xml 定义了 key 但没有值应该报错', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @html 定义了 key 但没有值应该报错', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml 与其他格式混合', () => {
|
||||||
|
const content = `POST "https://api.example.com/multi" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<data><item>test</item></data>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 多个请求使用不同格式', () => {
|
||||||
|
const content = `POST "https://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>内容</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "console.log('test');"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/binary" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/test.bin"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ describe('HTTP Grammar - @response 响应语法', () => {
|
|||||||
|
|
||||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||||
expect(hasNode(state, 'ErrorStatus')).toBe(true);
|
expect(hasNode(state, 'ErrorStatus')).toBe(true);
|
||||||
expect(hasNode(state, 'TimeUnit')).toBe(true);
|
expect(hasNode(state, 'TimeValue')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('✅ 响应与请求结合', () => {
|
it('✅ 响应与请求结合', () => {
|
||||||
@@ -145,8 +145,8 @@ POST "https://api.example.com/users" {
|
|||||||
|
|
||||||
const state = createTestState(content);
|
const state = createTestState(content);
|
||||||
|
|
||||||
expect(hasNode(state, 'TimeUnit')).toBe(true);
|
expect(hasNode(state, 'TimeValue')).toBe(true);
|
||||||
expect(getNodeText(state, 'TimeUnit')).toBe('ms');
|
expect(getNodeText(state, 'TimeValue')).toBe('12345ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('✅ 响应块包含复杂 JSON', () => {
|
it('✅ 响应块包含复杂 JSON', () => {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ describe('HTTP Grammar 解析测试', () => {
|
|||||||
return depth;
|
return depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('应该正确解析标准的 GET 请求(包含 @json 和 @res)', () => {
|
it('应该正确解析标准的 GET 请求(包含 @json)', () => {
|
||||||
const code = `GET "http://127.0.0.1:80/api/create" {
|
const code = `GET "http://127.0.0.1:80/api/create" {
|
||||||
host: "https://api.example.com",
|
host: "https://api.example.com",
|
||||||
content-type: "application/json",
|
content-type: "application/json",
|
||||||
@@ -98,17 +98,6 @@ describe('HTTP Grammar 解析测试', () => {
|
|||||||
name : "xxx",
|
name : "xxx",
|
||||||
test: "xx"
|
test: "xx"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
status: "ok",
|
|
||||||
size: "20kb",
|
|
||||||
time: "2025-10-31 10:30:26",
|
|
||||||
data: {
|
|
||||||
xxx:"xxx"
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -277,7 +266,7 @@ POST "http://test2.com" {
|
|||||||
expect(result.hasError).toBe(false);
|
expect(result.hasError).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该支持 @json/@res 块后面不加逗号(JSON块内部必须用逗号)', () => {
|
it('应该支持 @json 块后面不加逗号(JSON块内部必须用逗号)', () => {
|
||||||
const code = `POST "http://test.com" {
|
const code = `POST "http://test.com" {
|
||||||
host: "test.com"
|
host: "test.com"
|
||||||
|
|
||||||
@@ -285,11 +274,6 @@ POST "http://test2.com" {
|
|||||||
name: "xxx",
|
name: "xxx",
|
||||||
test: "xx"
|
test: "xx"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
status: "ok"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -475,14 +459,6 @@ describe('HTTP 请求体格式测试', () => {
|
|||||||
level: "advanced"
|
level: "advanced"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "success",
|
|
||||||
data: {
|
|
||||||
id: 12345
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -509,12 +485,6 @@ describe('HTTP 请求体格式测试', () => {
|
|||||||
age: 25,
|
age: 25,
|
||||||
description: "用户头像上传"
|
description: "用户头像上传"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "上传成功",
|
|
||||||
url: "https://cdn.example.com/avatar.png"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -539,12 +509,6 @@ describe('HTTP 请求体格式测试', () => {
|
|||||||
password: "123456",
|
password: "123456",
|
||||||
remember: true
|
remember: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "登录成功",
|
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -593,13 +557,6 @@ POST "http://api.example.com/login" {
|
|||||||
username: "admin",
|
username: "admin",
|
||||||
password: "123456"
|
password: "123456"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 期望的响应
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
# 用户token
|
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -615,7 +572,7 @@ POST "http://api.example.com/login" {
|
|||||||
expect(result.hasError).toBe(false);
|
expect(result.hasError).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('✅ 混合多种格式 - JSON + 响应', () => {
|
it('✅ 混合多种格式 - JSON 请求', () => {
|
||||||
const code = `POST "http://api.example.com/login" {
|
const code = `POST "http://api.example.com/login" {
|
||||||
content-type: "application/json"
|
content-type: "application/json"
|
||||||
user-agent: "Mozilla/5.0"
|
user-agent: "Mozilla/5.0"
|
||||||
@@ -624,16 +581,6 @@ POST "http://api.example.com/login" {
|
|||||||
username: "admin",
|
username: "admin",
|
||||||
password: "123456"
|
password: "123456"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "登录成功",
|
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
name: "管理员"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -723,3 +670,442 @@ POST "http://api.example.com/login" {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('HTTP 新格式测试 - params/xml/html/javascript/binary', () => {
|
||||||
|
|
||||||
|
function parseCode(code: string) {
|
||||||
|
const tree = parser.parse(code);
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
|
||||||
|
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
|
||||||
|
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node: any) => {
|
||||||
|
if (node.name === '⚠') {
|
||||||
|
errors.push({
|
||||||
|
name: node.name,
|
||||||
|
from: node.from,
|
||||||
|
to: node.to,
|
||||||
|
text: tree.toString().substring(node.from, node.to)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasError: errors.length > 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('✅ @params - URL 参数格式', () => {
|
||||||
|
const code = `GET "http://api.example.com/users" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: "张三",
|
||||||
|
status: "active"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @params 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml - XML 格式', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @xml 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @xml 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html - HTML 格式', () => {
|
||||||
|
const code = `POST "http://api.example.com/render" {
|
||||||
|
content-type: "text/html"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @html 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @html 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript - JavaScript 格式', () => {
|
||||||
|
const code = `POST "http://api.example.com/execute" {
|
||||||
|
content-type: "application/javascript"
|
||||||
|
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @javascript 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @javascript 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary - 二进制文件上传', () => {
|
||||||
|
const code = `POST "http://api.example.com/upload" {
|
||||||
|
content-type: "application/octet-stream"
|
||||||
|
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @binary 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @binary 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 复杂 XML - SOAP 请求', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 复杂 XML 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 混合使用 - params + headers', () => {
|
||||||
|
const code = `GET "http://api.example.com/search" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
user-agent: "Mozilla/5.0"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
q: "搜索关键词",
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
sort: "desc"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 混合使用格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 多个不同新格式的请求', () => {
|
||||||
|
const code = `# XML 请求
|
||||||
|
POST "http://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML 请求
|
||||||
|
POST "http://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>内容</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# JavaScript 请求
|
||||||
|
POST "http://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "console.log('test');"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Binary 请求
|
||||||
|
POST "http://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/test.bin"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 多新格式请求错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
|
||||||
|
// 统计 RequestStatement 数量
|
||||||
|
let requestCount = 0;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node: any) => {
|
||||||
|
if (node.name === 'RequestStatement') requestCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requestCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @xml - 定义了 xml key 但没有值(应该报错)', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
// 应该有错误
|
||||||
|
expect(result.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @html - 定义了 html key 但没有值(应该报错)', () => {
|
||||||
|
const code = `POST "http://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
// 应该有错误
|
||||||
|
expect(result.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @xml - 使用错误的 key 名称(应该报错)', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
data: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
// 应该有错误
|
||||||
|
expect(result.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 所有格式组合测试', () => {
|
||||||
|
const code = `# 传统格式
|
||||||
|
POST "http://api.example.com/json" {
|
||||||
|
@json {
|
||||||
|
name: "test",
|
||||||
|
age: 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/form" {
|
||||||
|
@formdata {
|
||||||
|
file: "test.png",
|
||||||
|
desc: "description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/login" {
|
||||||
|
@urlencoded {
|
||||||
|
username: "admin",
|
||||||
|
password: "123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/text" {
|
||||||
|
@text {
|
||||||
|
content: "纯文本内容"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 新格式
|
||||||
|
GET "http://api.example.com/search" {
|
||||||
|
@params {
|
||||||
|
q: "keyword",
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<data>test</data>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>test</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "alert('test');"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/file.bin"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 所有格式组合测试错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
|
||||||
|
// 统计 RequestStatement 数量(应该有9个)
|
||||||
|
let requestCount = 0;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node: any) => {
|
||||||
|
if (node.name === 'RequestStatement') requestCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requestCount).toBe(9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,18 @@ export const httpHighlighting = styleTags({
|
|||||||
"TRACE CONNECT": t.modifier,
|
"TRACE CONNECT": t.modifier,
|
||||||
|
|
||||||
// ========== @ 规则(请求体格式和变量声明)==========
|
// ========== @ 规则(请求体格式和变量声明)==========
|
||||||
// @json, @formdata, @urlencoded - 使用类型名
|
// @json, @formdata, @urlencoded, @params - 使用类型名
|
||||||
"JsonKeyword FormDataKeyword UrlEncodedKeyword": t.typeName,
|
"JsonKeyword FormDataKeyword UrlEncodedKeyword ParamsKeyword": t.typeName,
|
||||||
|
|
||||||
// @text - 使用特殊类型
|
// @text - 使用特殊类型
|
||||||
"TextKeyword": t.special(t.typeName),
|
"TextKeyword": t.special(t.typeName),
|
||||||
|
|
||||||
|
// @xml, @html, @javascript - 使用类型名
|
||||||
|
"XmlKeyword HtmlKeyword JavaScriptKeyword": t.typeName,
|
||||||
|
|
||||||
|
// @binary - 使用特殊类型
|
||||||
|
"BinaryKeyword": t.special(t.typeName),
|
||||||
|
|
||||||
// @var - 变量声明关键字
|
// @var - 变量声明关键字
|
||||||
"VarKeyword": t.definitionKeyword,
|
"VarKeyword": t.definitionKeyword,
|
||||||
|
|
||||||
@@ -60,15 +66,16 @@ export const httpHighlighting = styleTags({
|
|||||||
|
|
||||||
// ========== 响应相关 ==========
|
// ========== 响应相关 ==========
|
||||||
// 响应状态码 - 数字颜色
|
// 响应状态码 - 数字颜色
|
||||||
"StatusCode": t.number,
|
"ResponseStatus/NumberLiteral": t.number,
|
||||||
"ResponseStatus/StatusCode": t.number,
|
"ResponseStatus/identifier": t.constant(t.variableName),
|
||||||
|
|
||||||
// 响应错误状态 - 关键字
|
// 响应错误状态 - 关键字
|
||||||
"ErrorStatus": t.operatorKeyword,
|
"ErrorStatus": t.operatorKeyword,
|
||||||
|
|
||||||
// 响应时间 - 数字颜色
|
// 响应时间 - 数字和单位颜色
|
||||||
"TimeValue": t.number,
|
"TimeValue": t.number,
|
||||||
"ResponseTime": t.number,
|
"ResponseTime/TimeValue": t.number,
|
||||||
|
"TimeUnit": t.unit,
|
||||||
|
|
||||||
// 时间戳 - 字符串颜色
|
// 时间戳 - 字符串颜色
|
||||||
"Timestamp": t.string,
|
"Timestamp": t.string,
|
||||||
@@ -99,6 +106,12 @@ export const httpHighlighting = styleTags({
|
|||||||
"JsonValue/StringLiteral": t.string,
|
"JsonValue/StringLiteral": t.string,
|
||||||
"JsonValue/NumberLiteral": t.number,
|
"JsonValue/NumberLiteral": t.number,
|
||||||
|
|
||||||
|
// ========== 固定 key 名称(xml、html、javascript、binary)==========
|
||||||
|
"XmlKey": t.constant(t.propertyName),
|
||||||
|
"HtmlKey": t.constant(t.propertyName),
|
||||||
|
"JavaScriptKey": t.constant(t.propertyName),
|
||||||
|
"BinaryKey": t.constant(t.propertyName),
|
||||||
|
|
||||||
// ========== 标点符号 ==========
|
// ========== 标点符号 ==========
|
||||||
// 冒号 - 分隔符
|
// 冒号 - 分隔符
|
||||||
":": t.separator,
|
":": t.separator,
|
||||||
|
|||||||
@@ -40,17 +40,34 @@ export const
|
|||||||
UrlEncodedKeyword = 44,
|
UrlEncodedKeyword = 44,
|
||||||
TextRule = 45,
|
TextRule = 45,
|
||||||
TextKeyword = 46,
|
TextKeyword = 46,
|
||||||
ResponseDeclaration = 47,
|
ParamsRule = 47,
|
||||||
ResponseKeyword = 48,
|
ParamsKeyword = 48,
|
||||||
ResponseStatus = 49,
|
XmlRule = 49,
|
||||||
StatusCode = 50,
|
XmlKeyword = 50,
|
||||||
ErrorStatus = 51,
|
XmlBlock = 51,
|
||||||
ResponseTime = 52,
|
XmlKey = 52,
|
||||||
TimeValue = 53,
|
HtmlRule = 53,
|
||||||
ResponseTimestamp = 54,
|
HtmlKeyword = 54,
|
||||||
Timestamp = 55,
|
HtmlBlock = 55,
|
||||||
ResponseBlock = 56,
|
HtmlKey = 56,
|
||||||
JsonObject = 57,
|
JavaScriptRule = 57,
|
||||||
JsonMember = 58,
|
JavaScriptKeyword = 58,
|
||||||
JsonValue = 59,
|
JavaScriptBlock = 59,
|
||||||
JsonArray = 62
|
JavaScriptKey = 60,
|
||||||
|
BinaryRule = 61,
|
||||||
|
BinaryKeyword = 62,
|
||||||
|
BinaryBlock = 63,
|
||||||
|
BinaryKey = 64,
|
||||||
|
ResponseDeclaration = 65,
|
||||||
|
ResponseKeyword = 66,
|
||||||
|
ResponseStatus = 67,
|
||||||
|
ErrorStatus = 68,
|
||||||
|
ResponseTime = 69,
|
||||||
|
TimeValue = 70,
|
||||||
|
ResponseTimestamp = 71,
|
||||||
|
Timestamp = 72,
|
||||||
|
ResponseBlock = 73,
|
||||||
|
JsonObject = 74,
|
||||||
|
JsonMember = 75,
|
||||||
|
JsonValue = 76,
|
||||||
|
JsonArray = 79
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import {LRParser} from "@lezer/lr"
|
import {LRParser} from "@lezer/lr"
|
||||||
import {httpHighlighting} from "./http.highlight"
|
import {httpHighlighting} from "./http.highlight"
|
||||||
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@response":96}
|
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@params":96, "@xml":100, "@html":108, "@javascript":116, "@binary":124, "@response":132}
|
||||||
const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, error:102}
|
const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, xml:104, html:112, javascript:120, binary:128, error:136}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "-bQYQPOOO!aQPO'#C_OOQO'#Ct'#CtO!aQPO'#DTO!aQPO'#DVO!aQPO'#DXO!aQPO'#DZO!fQPO'#DSO#yQPO'#CsO$jQPO'#DlO$qQPO'#DgO$|QPO'#D]OOQO'#Du'#DuOOQO'#Dm'#DmQYQPOOO%UQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59n,59nOOQO'#DO'#DOO%^QPO,59_O%cQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO&QQPO'#D}OOQO,5:W,5:WO&YQPO,5:WOOQO'#Di'#DiO&_QPO'#DhO&dQPO'#D|OOQO,5:R,5:RO&lQPO,5:ROOQO'#D_'#D_O&qQPO,59wOOQO-E7k-E7kOOQO'#Cf'#CfO&vQPO'#CeO&{QPO'#DvOOQO,59O,59OO'TQPO,59OO'kQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO'rQPO,5:iO'yQPO,5:iOOQO1G/r1G/rO$OQPO,5:SO(RQPO,5:hO(^QPO,5:hOOQO1G/m1G/mOOQO'#Db'#DbO(fQPO1G/cO(kQPO,59PO)SQPO,5:bO)[QPO,5:bOOQO1G.j1G.jOOQO'#DR'#DRO)dQPO'#DQOOQO'#Do'#DoO)iQPO'#DzO)pQPO'#DzOOQO,59k,59kO)xQPO,59kOOQO,5:[,5:[O)}QPO1G0TOOQO-E7n-E7nOOQO1G/n1G/nOOQO,5:],5:]O*UQPO1G0SOOQO-E7o-E7oOOQO'#Dd'#DdO*aQPO7+$}OOQO'#Dx'#DxOOQO1G.k1G.kOOQO,5:Y,5:YO*iQPO1G/|OOQO-E7l-E7lO*qQPO,59lOOQO-E7m-E7mO+SQPO,5:fO+SQPO,5:fOOQO1G/V1G/VP$OQPO'#DpP$tQPO'#DqOOQO'#Df'#DfOOQO<<Hi<<HiP%XQPO'#DnOOQO'#D{'#D{O+[QPO1G/WO+sQPO1G0QOOQO7+$r7+$r",
|
states: "2`QYQPOOO!pQPO'#C_OOQO'#Ct'#CtO!pQPO'#DTO!pQPO'#DVO!pQPO'#DXO!pQPO'#DZO!pQPO'#D]O!uQPO'#D_O!zQPO'#DcO#PQPO'#DgO#UQPO'#DkO#ZQPO'#DSO$}QPO'#CsO%nQPO'#D}O%uQPO'#DxO&QQPO'#DoOOQO'#EW'#EWOOQO'#EO'#EOQYQPOOO&YQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59w,59wO&bQPO'#DaOOQO,59y,59yO&jQPO'#DeOOQO,59},59}O&rQPO'#DiOOQO,5:R,5:RO&zQPO'#DmOOQO,5:V,5:VOOQO,59n,59nOOQO'#DO'#DOO'SQPO,59_O'XQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO(VQPO'#EaOOQO,5:i,5:iO(_QPO,5:iOOQO'#Dz'#DzO(dQPO'#DyO(iQPO'#E`OOQO,5:d,5:dO(qQPO,5:dO(vQQO'#CiO)RQQO'#DqOOQO'#Dq'#DqO)ZQPO,5:ZOOQO-E7|-E7|OOQO'#Cf'#CfO)`QPO'#CeO)eQPO'#EXOOQO,59O,59OO)mQPO,59OO)rQPO,59{OOQO,59{,59{O)wQPO,5:POOQO,5:P,5:PO)|QPO,5:TOOQO,5:T,5:TO*RQPO,5:XOOQO,5:X,5:XO*xQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO+PQPO,5:{O+WQPO,5:{OOQO1G0T1G0TO%SQPO,5:eO+`QPO,5:zO+kQPO,5:zOOQO1G0O1G0OO+sQPO,5:]OOQO'#Ds'#DsO+xQPO1G/uO+}QPO,59PO,fQPO,5:sO,nQPO,5:sOOQO1G.j1G.jO+}QPO1G/gO+}QPO1G/kO+}QPO1G/oO+}QPO1G/sOOQO'#DR'#DRO,vQPO'#DQOOQO'#EQ'#EQO,{QPO'#E]O-SQPO'#E]OOQO,59k,59kO-[QPO,59kOOQO,5:m,5:mO-aQPO1G0gOOQO-E8P-E8POOQO1G0P1G0POOQO,5:n,5:nO-hQPO1G0fOOQO-E8Q-E8QOOQO1G/w1G/wOOQO'#Du'#DuO-sQPO7+%aOOQO'#EZ'#EZOOQO1G.k1G.kOOQO,5:k,5:kO-{QPO1G0_OOQO-E7}-E7}O.TQPO7+%RO.YQPO7+%VO._QPO7+%ZO.dQPO7+%_O.iQPO,59lOOQO-E8O-E8OO.zQPO,5:wO.zQPO,5:wOOQO1G/V1G/VP%SQPO'#ERP%xQPO'#ESOOQO'#Dw'#DwOOQO<<H{<<H{P&]QPO'#EPOOQO<<Hm<<HmOOQO<<Hq<<HqOOQO<<Hu<<HuOOQO<<Hy<<HyOOQO'#E^'#E^O/SQPO1G/WO/zQPO1G0cOOQO7+$r7+$r",
|
||||||
stateData: ",U~O!hOSPOS~OTPOVYOiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QZO!_XO~OV_O~OfeOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!_vX!fvXUvX!kvX~O[fO~OVYO[oO_oOaiOcjOekO!_XO!mhO~O!^mO~P$OOUrO[pO!kpO~O!StO!TtO~OUzO!kwO~OV|O~O^!OOf]X!^]XU]Xx]Xz]X|]X!O]X!k]X~Of!PO!^!qX~O!^!RO~OZ!SO~Of!TOU!pX~OU!VO~O!V!WO~OZ!YO~Of!ZOU!jX~OU!]O~OxROzSO|TO!OUO!k!^O~OU!cO~P'YO!^!qa~P$OOf!fO!^!qa~O[pO!kpOU!pa~Of!jOU!pa~O!X!lO~OV_O[!nO_!nOaiOcjOekO!mhO~O!kwOU!ja~Of!qOU!ja~OZ!sO~OU!nX~P'YO!k!^OU!nX~OU!wO~O!^!qi~P$OO[pO!kpOU!pi~OVYO!_XO~O!kwOU!ji~OV|O[!}O_!}O!k!}O!mhO~O!k!^OU!na~Of#QOUtixtizti|ti!Oti!kti~O!k!^OU!ni~O!X!V!S!m_!k^!m~",
|
stateData: "0[~O!yOSPOS~OTPOV_OiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QVO!SWO!WXO![YO!`ZO!d`O!p^O~OVdO~OVkO~OVmO~OVoO~OVqO~OfsOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!SvX!WvX![vX!`vX!dvX!pvX!wvXUvX!|vX~O[tO~OV_O[}O_}OawOcxOeyO!p^O#OvO~O!o{O~P%SOU!QO[!OO!|!OO~O!f!UO#O!SO~OU![O!|!XO~OU!_O!U!^O~OU!aO!Y!`O~OU!cO!^!bO~OU!eO!b!dO~OV!fO~O^!hOf]X!o]XU]Xx]Xz]X|]X!O]X!Q]X!S]X!W]X![]X!`]X!|]X~Of!iO!o#TX~O!o!kO~OZ!lO~Of!mOU#SX~OU!oO~O^!hO!h]X#R]X~O#R!pO!h!eX~O!h!qO~OZ!sO~Of!tOU!{X~OU!vO~OZ!wO~OZ!xO~OZ!yO~OZ!zO~OxROzSO|TO!OUO!QVO!SWO!WXO![YO!`ZO!|!{O~OU#QO~P*WO!o#Ta~P%SOf#TO!o#Ta~O[!OO!|!OOU#Sa~Of#XOU#Sa~O!|#ZO~O!j#[O~OVdO[#^O_#^OawOcxOeyO#OvO~O!|!XOU!{a~Of#aOU!{a~OZ#gO~OU#PX~P*WO!|!{OU#PX~OU#kO~O!o#Ti~P%SO[!OO!|!OOU#Si~OV_O!p^O~O!|!XOU!{i~OU#qO~OU#rO~OU#sO~OU#tO~OV!fO[#uO_#uO!|#uO#OvO~O!|!{OU#Pa~Of#xOUtixtizti|ti!Oti!Qti!Sti!Wti![ti!`ti!|ti~O!|!{OU#Pi~O!j!h#O_!|^_~",
|
||||||
goto: "'^!rPPP!sPPPP!w#Z#cPP#iPP#vP#vP#vPP!s$QPPPPPPPPP$U$X$_$g$o$yP$yP$yP$yP!sP%PPP%SP%VP%Y%]%k%sPP%]&O&U&[&j&pPPP&v&zP&}P'Q'T'W'ZT[O^Q`PQaRQbSQcTQdUR!n!YQy_V!p!Z!q!|Xx_!Z!q!|YoX!P!S!f!xQ!n!YR!}!sYoX!P!S!f!xR!n!YTWO^RgWQ}gR!}!s]!`|!a!b!u!v#P]!_|!a!b!u!v#PS[O^Q!b|R!u!aXVO^|!aRuZR!XuR!m!XR!{!mS[O^YoX!P!S!f!xR!z!mQqYV!i!T!j!yQlXU!e!P!f!xR!h!SQ^ORv^Q![yR!r![Q!a|U!t!a!v#PQ!v!bR#P!uQ!QlR!g!QQ!UqR!k!UT]O^R{_R!o!YR!d|R#O!sRsYRnX",
|
goto: "(l#UPPP#VPPPP#Z#t#|PP$SPP$hP$hP$hPP#V$vPPPPPPPPP$z$}%T%]%e%oP%oP%oP%oP%oP%oP%uP%oP%xP%oP%{P%oP&OP#VP&RP&UP&XP&[&_&m&uPP&_'Q'W'^'l'rPPP'x'|P(PP(`(cP(f(iTaOcQePQfRQgSQhTQiUQjVZ#^!s!w!x!y!zQ!ZdV#`!t#a#pX!Yd!t#a#pY}^!i!l#T#lQ!T`Y#^!s!w!x!y!zR#u#gY}^!i!l#T#lZ#^!s!w!x!y!zT]OcRu]Q!guR#u#g]!}!f#O#P#i#j#w]!|!f#O#P#i#j#wSaOcQ#P!fR#i#OX[Oc!f#ORlWRnXRpYRrZR!V`R!r!VR#]!rR#o#]SaOcY}^!i!l#T#lR#n#]Q!P_V#W!m#X#mQz^U#S!i#T#lR#V!lQcOR!WcQ!u!ZR#b!uQ#O!fU#h#O#j#wQ#j#PR#w#iQ!jzR#U!jQ!n!PR#Y!nTbOcR!]dQ#_!sQ#c!wQ#d!xQ#e!yR#f!zR#R!fR#v#gR!R_R|^",
|
||||||
nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ResponseDeclaration ResponseKeyword ResponseStatus StatusCode ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray",
|
nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ParamsRule ParamsKeyword XmlRule XmlKeyword XmlBlock XmlKey HtmlRule HtmlKeyword HtmlBlock HtmlKey JavaScriptRule JavaScriptKeyword JavaScriptBlock JavaScriptKey BinaryRule BinaryKeyword BinaryBlock BinaryKey ResponseDeclaration ResponseKeyword ResponseStatus ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray",
|
||||||
maxTerm: 79,
|
maxTerm: 97,
|
||||||
nodeProps: [
|
nodeProps: [
|
||||||
["openedBy", 6,"{",60,"["],
|
["openedBy", 6,"{",77,"["],
|
||||||
["closedBy", 7,"}",61,"]"],
|
["closedBy", 7,"}",78,"]"],
|
||||||
["isolate", -3,12,15,55,""]
|
["isolate", -4,12,15,70,72,""]
|
||||||
],
|
],
|
||||||
propSources: [httpHighlighting],
|
propSources: [httpHighlighting],
|
||||||
skippedNodes: [0,1,4],
|
skippedNodes: [0,1,4],
|
||||||
repeatNodeCount: 5,
|
repeatNodeCount: 5,
|
||||||
tokenData: "3b~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O(P!O!P(Y!Q![)p![!]/Y!b!c/_!c!}0V!}#O0p#P#Q0u#R#S%y#T#o0V#o#p0z#q#r3]#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!h~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!k~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&b~(SQ!O!P(Y!Q![)Y~(]P!Q![(`~(eR!m~!Q![(`!g!h(n#X#Y(n~(qR{|(z}!O(z!Q![)Q~(}P!Q![)Q~)VP!m~!Q![)Q~)_S!m~!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~~)wU!S~!m~}!O*Z!O!P(`!Q![*r!g!h(n#X#Y(n#a#b.}~*^Q!c!}*d#T#o*d~*iR!S~}!O*d!c!}*d#T#o*d~*yU!S~!m~}!O*Z!O!P(`!Q![+]!g!h(n#X#Y(n#a#b.}~+dU!S~!m~}!O*Z!O!P(`!Q![+v!g!h(n#X#Y(n#a#b.}~+}U!S~!m~}!O,a!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~,dR!Q![,m!c!}*d#T#o*d~,pP!Q![,s~,vP}!O,y~,|P!Q![-P~-SP!Q![-V~-YP!v!w-]~-`P!Q![-c~-fP!Q![-i~-lP![!]-o~-rP!Q![-u~-xP!Q![-{~.OP![!].R~.UP!Q![.X~.[P!Q![._~.dO!X~~.kU!S~!m~}!O*Z!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~/QP#g#h/T~/YO!V~~/_OZ~~/bR}!O/k!c!}/t#T#o/t~/nQ!c!}/t#T#o/t~/ySS~}!O/t!Q![/t!c!}/t#T#o/t~0^U!k~^~tu%y}!O%y!Q![%y!c!}0V#R#S%y#T#o0V~0uO!_~~0zO!^~~1PPV~#o#p1S~1VStu1c!c!}1c#R#S1c#T#o1c~1fXtu1c}!O1c!O!P1c!Q![1c![!]2R!c!}1c#R#S1c#T#o1c#q#r3V~2UUOY2RZ#q2R#q#r2h#r;'S2R;'S;=`3P<%lO2R~2kTO#q2R#q#r2z#r;'S2R;'S;=`3P<%lO2R~3PO_~~3SP;=`<%l2R~3YP#q#r2z~3bOU~",
|
tokenData: "2h~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O)p!O!P(Y!Q![){![!].`!b!c.e!c!}/]!}#O/v#P#Q/{#R#S%y#T#o/]#o#p0Q#q#r2c#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!y~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!|~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&bP(SQ!O!P(Y!Q![)YP(]P!Q![(`P(eR#OP!Q![(`!g!h(n#X#Y(nP(qR{|(z}!O(z!Q![)QP(}P!Q![)QP)VP#OP!Q![)QP)_S#OP!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~R)uQ#RQ!O!P(Y!Q![)Y~*QT#OP!O!P(`!Q![*a!g!h(n#X#Y(n#a#b.T~*fT#OP!O!P(`!Q![*u!g!h(n#X#Y(n#a#b.T~*zT#OP!O!P(`!Q![+Z!g!h(n#X#Y(n#a#b.T~+`U#OP}!O+r!O!P(`!Q![-o!g!h(n#X#Y(n#a#b.T~+uP!Q![+x~+{P!Q![,O~,RP}!O,U~,XP!Q![,[~,_P!Q![,b~,eP!v!w,h~,kP!Q![,n~,qP!Q![,t~,wP![!],z~,}P!Q![-Q~-TP!Q![-W~-ZP![!]-^~-aP!Q![-d~-gP!Q![-j~-oO!j~~-tT#OP!O!P(`!Q![-o!g!h(n#X#Y(n#a#b.T~.WP#g#h.Z~.`O!h~~.eOZ~~.hR}!O.q!c!}.z#T#o.z~.tQ!c!}.z#T#o.z~/PSS~}!O.z!Q![.z!c!}.z#T#o.z~/dU!|~^~tu%y}!O%y!Q![%y!c!}/]#R#S%y#T#o/]~/{O!p~~0QO!o~~0VPV~#o#p0Y~0]Stu0i!c!}0i#R#S0i#T#o0i~0lXtu0i}!O0i!O!P0i!Q![0i![!]1X!c!}0i#R#S0i#T#o0i#q#r2]~1[UOY1XZ#q1X#q#r1n#r;'S1X;'S;=`2V<%lO1X~1qTO#q1X#q#r2Q#r;'S1X;'S;=`2V<%lO1X~2VO_~~2YP;=`<%l1X~2`P#q#r2Q~2hOU~",
|
||||||
tokenizers: [0],
|
tokenizers: [0, 1],
|
||||||
topRules: {"Document":[0,2]},
|
topRules: {"Document":[0,2]},
|
||||||
specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 73, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
|
specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 90, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
|
||||||
tokenPrec: 503
|
tokenPrec: 694
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { httpLanguage } from '../language';
|
||||||
|
import { parseHttpRequest } from './request-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试用的 EditorState
|
||||||
|
*/
|
||||||
|
function createTestState(content: string): EditorState {
|
||||||
|
return EditorState.create({
|
||||||
|
doc: content,
|
||||||
|
extensions: [httpLanguage]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HTTP Request Parser - 新格式测试', () => {
|
||||||
|
|
||||||
|
describe('✅ @params - URL 参数', () => {
|
||||||
|
it('应该正确解析 params 请求', () => {
|
||||||
|
const content = `GET "https://api.example.com/users" {
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: "张三"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('GET');
|
||||||
|
expect(request?.url).toBe('https://api.example.com/users');
|
||||||
|
expect(request?.bodyType).toBe('params');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: '张三'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @xml - XML 格式', () => {
|
||||||
|
it('应该正确解析 xml 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('xml');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
xml: '<user><name>张三</name><age>25</age></user>'
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('application/xml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 xml 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('xml');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @html - HTML 格式', () => {
|
||||||
|
it('应该正确解析 html 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
content-type: "text/html"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('html');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
html: '<div><h1>标题</h1><p>内容</p></div>'
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 html 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('html');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @javascript - JavaScript 格式', () => {
|
||||||
|
it('应该正确解析 javascript 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
content-type: "application/javascript"
|
||||||
|
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('javascript');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('application/javascript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 javascript 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('javascript');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @binary - 二进制文件', () => {
|
||||||
|
it('应该正确解析 binary 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
content-type: "application/octet-stream"
|
||||||
|
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('binary');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
binary: '@file E://Documents/avatar.png'
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('application/octet-stream');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 binary 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('binary');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 混合使用场景', () => {
|
||||||
|
it('应该正确解析带 params 和 headers 的请求', () => {
|
||||||
|
const content = `GET "https://api.example.com/search" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
q: "关键词",
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('GET');
|
||||||
|
expect(request?.headers['authorization']).toBe('Bearer token123');
|
||||||
|
expect(request?.bodyType).toBe('params');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
q: '关键词',
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析复杂 XML 内容', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('xml');
|
||||||
|
expect(request?.body.xml).toContain('soap:Envelope');
|
||||||
|
expect(request?.body.xml).toContain('GetUser');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 对比:传统格式仍然可用', () => {
|
||||||
|
it('JSON 格式仍然正常工作', () => {
|
||||||
|
const content = `POST "https://api.example.com/api" {
|
||||||
|
@json {
|
||||||
|
name: "张三",
|
||||||
|
age: 25,
|
||||||
|
email: "test@example.com"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('json');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
name: '张三',
|
||||||
|
age: 25,
|
||||||
|
email: 'test@example.com'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('FormData 格式仍然正常工作', () => {
|
||||||
|
const content = `POST "https://api.example.com/form" {
|
||||||
|
@formdata {
|
||||||
|
username: "admin",
|
||||||
|
password: "123456"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('formdata');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
username: 'admin',
|
||||||
|
password: '123456'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 多个请求解析', () => {
|
||||||
|
it('应该能在同一文档中解析不同格式的请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>内容</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "console.log('test');"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
|
||||||
|
// 解析第一个请求(XML)
|
||||||
|
const request1 = parseHttpRequest(state, 0);
|
||||||
|
expect(request1?.bodyType).toBe('xml');
|
||||||
|
expect(request1?.body.xml).toContain('张三');
|
||||||
|
|
||||||
|
// 解析第二个请求(HTML)- 找到第二个 POST 的位置
|
||||||
|
const secondPostIndex = content.indexOf('POST', 10);
|
||||||
|
const request2 = parseHttpRequest(state, secondPostIndex);
|
||||||
|
expect(request2?.bodyType).toBe('html');
|
||||||
|
expect(request2?.body.html).toContain('内容');
|
||||||
|
|
||||||
|
// 解析第三个请求(JavaScript)
|
||||||
|
const thirdPostIndex = content.indexOf('POST', secondPostIndex + 10);
|
||||||
|
const request3 = parseHttpRequest(state, thirdPostIndex);
|
||||||
|
expect(request3?.bodyType).toBe('javascript');
|
||||||
|
expect(request3?.body.javascript).toContain('console.log');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export interface HttpRequest {
|
|||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
|
|
||||||
/** 请求体类型 */
|
/** 请求体类型 */
|
||||||
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text';
|
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text' | 'params' | 'xml' | 'html' | 'javascript' | 'binary';
|
||||||
|
|
||||||
/** 请求体内容 */
|
/** 请求体内容 */
|
||||||
body?: any;
|
body?: any;
|
||||||
@@ -48,12 +48,30 @@ const NODE_TYPES = {
|
|||||||
FORMDATA_RULE: 'FormDataRule',
|
FORMDATA_RULE: 'FormDataRule',
|
||||||
URLENCODED_RULE: 'UrlEncodedRule',
|
URLENCODED_RULE: 'UrlEncodedRule',
|
||||||
TEXT_RULE: 'TextRule',
|
TEXT_RULE: 'TextRule',
|
||||||
|
PARAMS_RULE: 'ParamsRule',
|
||||||
|
XML_RULE: 'XmlRule',
|
||||||
|
HTML_RULE: 'HtmlRule',
|
||||||
|
JAVASCRIPT_RULE: 'JavaScriptRule',
|
||||||
|
BINARY_RULE: 'BinaryRule',
|
||||||
JSON_KEYWORD: 'JsonKeyword',
|
JSON_KEYWORD: 'JsonKeyword',
|
||||||
FORMDATA_KEYWORD: 'FormDataKeyword',
|
FORMDATA_KEYWORD: 'FormDataKeyword',
|
||||||
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
||||||
TEXT_KEYWORD: 'TextKeyword',
|
TEXT_KEYWORD: 'TextKeyword',
|
||||||
|
PARAMS_KEYWORD: 'ParamsKeyword',
|
||||||
|
XML_KEYWORD: 'XmlKeyword',
|
||||||
|
HTML_KEYWORD: 'HtmlKeyword',
|
||||||
|
JAVASCRIPT_KEYWORD: 'JavaScriptKeyword',
|
||||||
|
BINARY_KEYWORD: 'BinaryKeyword',
|
||||||
JSON_BLOCK: 'JsonBlock',
|
JSON_BLOCK: 'JsonBlock',
|
||||||
JSON_PROPERTY: 'JsonProperty',
|
JSON_PROPERTY: 'JsonProperty',
|
||||||
|
XML_BLOCK: 'XmlBlock',
|
||||||
|
HTML_BLOCK: 'HtmlBlock',
|
||||||
|
JAVASCRIPT_BLOCK: 'JavaScriptBlock',
|
||||||
|
BINARY_BLOCK: 'BinaryBlock',
|
||||||
|
XML_KEY: 'XmlKey',
|
||||||
|
HTML_KEY: 'HtmlKey',
|
||||||
|
JAVASCRIPT_KEY: 'JavaScriptKey',
|
||||||
|
BINARY_KEY: 'BinaryKey',
|
||||||
VARIABLE_REF: 'VariableRef',
|
VARIABLE_REF: 'VariableRef',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -220,16 +238,73 @@ export class HttpRequestParser {
|
|||||||
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
||||||
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
||||||
[NODE_TYPES.TEXT_RULE]: 'text',
|
[NODE_TYPES.TEXT_RULE]: 'text',
|
||||||
|
[NODE_TYPES.PARAMS_RULE]: 'params',
|
||||||
|
[NODE_TYPES.XML_RULE]: 'xml',
|
||||||
|
[NODE_TYPES.HTML_RULE]: 'html',
|
||||||
|
[NODE_TYPES.JAVASCRIPT_RULE]: 'javascript',
|
||||||
|
[NODE_TYPES.BINARY_RULE]: 'binary',
|
||||||
};
|
};
|
||||||
|
|
||||||
const type = typeMap[node.name];
|
const type = typeMap[node.name];
|
||||||
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
|
||||||
|
// 根据不同的规则类型解析不同的块
|
||||||
|
let content: any = null;
|
||||||
|
|
||||||
|
if (node.name === NODE_TYPES.XML_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.XML_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'xml') : null;
|
||||||
|
} else if (node.name === NODE_TYPES.HTML_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.HTML_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'html') : null;
|
||||||
|
} else if (node.name === NODE_TYPES.JAVASCRIPT_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.JAVASCRIPT_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'javascript') : null;
|
||||||
|
} else if (node.name === NODE_TYPES.BINARY_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.BINARY_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'binary') : null;
|
||||||
|
} else {
|
||||||
|
// json, formdata, urlencoded, text, params 使用 JsonBlock
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
||||||
|
content = blockNode ? this.parseJsonBlock(blockNode) : null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
content: blockNode ? this.parseJsonBlock(blockNode) : null
|
content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析固定 key 的块(xml, html, javascript, binary)
|
||||||
|
* 格式:{ key: "value" } 或 {}(空块)
|
||||||
|
*/
|
||||||
|
private parseFixedKeyBlock(node: SyntaxNode, keyName: string): any {
|
||||||
|
// 查找固定的 key 节点
|
||||||
|
const keyNode = node.getChild(
|
||||||
|
keyName === 'xml' ? NODE_TYPES.XML_KEY :
|
||||||
|
keyName === 'html' ? NODE_TYPES.HTML_KEY :
|
||||||
|
keyName === 'javascript' ? NODE_TYPES.JAVASCRIPT_KEY :
|
||||||
|
NODE_TYPES.BINARY_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果没有 key,返回空对象(支持空块)
|
||||||
|
if (!keyNode) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找值节点(冒号后面的内容)
|
||||||
|
let value: any = null;
|
||||||
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||||
|
if (child.name === NODE_TYPES.STRING_LITERAL ||
|
||||||
|
child.name === NODE_TYPES.VARIABLE_REF) {
|
||||||
|
value = this.parseValue(child);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回格式:{ xml: "value" } 或 { html: "value" } 等
|
||||||
|
return value !== null ? { [keyName]: value } : {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 JsonBlock(用于 @json, @form, @urlencoded)
|
* 解析 JsonBlock(用于 @json, @form, @urlencoded)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type HttpRequest struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
BodyType string `json:"bodyType,omitempty"` // json, formdata, urlencoded, text
|
BodyType string `json:"bodyType,omitempty"` // json, formdata, urlencoded, text, params, xml, html, javascript, binary
|
||||||
Body interface{} `json:"body,omitempty"`
|
Body interface{} `json:"body,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRe
|
|||||||
case "json":
|
case "json":
|
||||||
req.SetHeader("Content-Type", "application/json")
|
req.SetHeader("Content-Type", "application/json")
|
||||||
req.SetBody(request.Body)
|
req.SetBody(request.Body)
|
||||||
|
|
||||||
case "formdata":
|
case "formdata":
|
||||||
if formData, ok := request.Body.(map[string]interface{}); ok {
|
if formData, ok := request.Body.(map[string]interface{}); ok {
|
||||||
for key, value := range formData {
|
for key, value := range formData {
|
||||||
@@ -104,6 +105,7 @@ func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "urlencoded":
|
case "urlencoded":
|
||||||
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||||
if formData, ok := request.Body.(map[string]interface{}); ok {
|
if formData, ok := request.Body.(map[string]interface{}); ok {
|
||||||
@@ -113,9 +115,79 @@ func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRe
|
|||||||
}
|
}
|
||||||
req.SetBody(values.Encode())
|
req.SetBody(values.Encode())
|
||||||
}
|
}
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
req.SetHeader("Content-Type", "text/plain")
|
req.SetHeader("Content-Type", "text/plain")
|
||||||
req.SetBody(fmt.Sprintf("%v", request.Body))
|
req.SetBody(fmt.Sprintf("%v", request.Body))
|
||||||
|
|
||||||
|
case "params":
|
||||||
|
// URL 参数:添加到查询字符串中
|
||||||
|
if params, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
for key, value := range params {
|
||||||
|
req.SetQueryParam(key, fmt.Sprintf("%v", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "xml":
|
||||||
|
// XML 格式:从 Body 中提取 xml 字段
|
||||||
|
req.SetHeader("Content-Type", "application/xml")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if xmlContent, exists := bodyMap["xml"]; exists {
|
||||||
|
req.SetBody(fmt.Sprintf("%v", xmlContent))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("xml body type requires 'xml' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("xml body must be an object with 'xml' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "html":
|
||||||
|
// HTML 格式:从 Body 中提取 html 字段
|
||||||
|
req.SetHeader("Content-Type", "text/html")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if htmlContent, exists := bodyMap["html"]; exists {
|
||||||
|
req.SetBody(fmt.Sprintf("%v", htmlContent))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("html body type requires 'html' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("html body must be an object with 'html' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "javascript":
|
||||||
|
// JavaScript 格式:从 Body 中提取 javascript 字段
|
||||||
|
req.SetHeader("Content-Type", "application/javascript")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if jsContent, exists := bodyMap["javascript"]; exists {
|
||||||
|
req.SetBody(fmt.Sprintf("%v", jsContent))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("javascript body type requires 'javascript' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("javascript body must be an object with 'javascript' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "binary":
|
||||||
|
// Binary 格式:从 Body 中提取 binary 字段(文件路径)
|
||||||
|
req.SetHeader("Content-Type", "application/octet-stream")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if binaryContent, exists := bodyMap["binary"]; exists {
|
||||||
|
binaryStr := fmt.Sprintf("%v", binaryContent)
|
||||||
|
// 检查是否是文件类型,使用 @file 关键词
|
||||||
|
if strings.HasPrefix(binaryStr, "@file ") {
|
||||||
|
// 提取文件路径(去掉 @file 前缀)
|
||||||
|
filePath := strings.TrimSpace(strings.TrimPrefix(binaryStr, "@file "))
|
||||||
|
req.SetFile("file", filePath)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("binary body requires '@file path/to/file' format")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("binary body type requires 'binary' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("binary body must be an object with 'binary' field")
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported body type: %s", request.BodyType)
|
return fmt.Errorf("unsupported body type: %s", request.BodyType)
|
||||||
}
|
}
|
||||||
|
|||||||
212
internal/services/httpclient_service_test.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHttpClientService_SetRequestBody(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHttpClientService(logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request *HttpRequest
|
||||||
|
expectError bool
|
||||||
|
errorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "JSON 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/json",
|
||||||
|
BodyType: "json",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"name": "张三",
|
||||||
|
"age": 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "XML 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/xml",
|
||||||
|
BodyType: "xml",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"xml": "<user><name>张三</name></user>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "XML 请求 - 缺少 xml 字段",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/xml",
|
||||||
|
BodyType: "xml",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"data": "<user><name>张三</name></user>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "xml body type requires 'xml' field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTML 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/html",
|
||||||
|
BodyType: "html",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"html": "<div><h1>标题</h1></div>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JavaScript 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/js",
|
||||||
|
BodyType: "javascript",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"javascript": "function hello() { return 'world'; }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Binary 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/upload",
|
||||||
|
BodyType: "binary",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"binary": "@file C:/test.bin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Binary 请求 - 错误格式(缺少 @file)",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/upload",
|
||||||
|
BodyType: "binary",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"binary": "C:/test.bin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "binary body requires '@file path/to/file' format",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Params 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://api.example.com/users",
|
||||||
|
BodyType: "params",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"page": 1,
|
||||||
|
"size": 20,
|
||||||
|
"query": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FormData 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/form",
|
||||||
|
BodyType: "formdata",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "123456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UrlEncoded 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/login",
|
||||||
|
BodyType: "urlencoded",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "123456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/text",
|
||||||
|
BodyType: "text",
|
||||||
|
Body: "纯文本内容",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "不支持的 Body 类型",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/unknown",
|
||||||
|
BodyType: "unknown",
|
||||||
|
Body: "test",
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "unsupported body type: unknown",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := service.client.R().SetContext(context.Background())
|
||||||
|
err := service.setRequestBody(req, tt.request)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errorMsg != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHttpClientService_FormatBytes(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHttpClientService(logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bytes int64
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"负数", -1, "0 B"},
|
||||||
|
{"零字节", 0, "0 B"},
|
||||||
|
{"小于1KB", 500, "500 B"},
|
||||||
|
{"1KB", 1024, "1.0 KB"},
|
||||||
|
{"1MB", 1024 * 1024, "1.0 MB"},
|
||||||
|
{"1.5MB", 1024*1024 + 512*1024, "1.5 MB"},
|
||||||
|
{"1GB", 1024 * 1024 * 1024, "1.0 GB"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := service.formatBytes(tt.bytes)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||