Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5688304817 | |||
| 4380ad010c | |||
| 4fa6bb42e3 | |||
| 7aa3a7e37f | |||
| 94306497a9 | |||
| 93c85b800b | |||
| 8ac78e39f1 | |||
| 61a23fe7f2 | |||
| 87fea58102 | |||
|
|
edeac01bee |
4
frontend/bindings/net/http/index.ts
Normal file
4
frontend/bindings/net/http/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
14
frontend/bindings/net/http/models.ts
Normal file
14
frontend/bindings/net/http/models.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* A Header represents the key-value pairs in an HTTP header.
|
||||
*
|
||||
* The keys should be in canonical form, as returned by
|
||||
* [CanonicalHeaderKey].
|
||||
*/
|
||||
export type Header = { [_: string]: string[] };
|
||||
4
frontend/bindings/time/index.ts
Normal file
4
frontend/bindings/time/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
51
frontend/bindings/time/models.ts
Normal file
51
frontend/bindings/time/models.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* A Time represents an instant in time with nanosecond precision.
|
||||
*
|
||||
* Programs using times should typically store and pass them as values,
|
||||
* not pointers. That is, time variables and struct fields should be of
|
||||
* type [time.Time], not *time.Time.
|
||||
*
|
||||
* A Time value can be used by multiple goroutines simultaneously except
|
||||
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
|
||||
* [Time.UnmarshalText] are not concurrency-safe.
|
||||
*
|
||||
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
|
||||
* The [Time.Sub] method subtracts two instants, producing a [Duration].
|
||||
* The [Time.Add] method adds a Time and a Duration, producing a Time.
|
||||
*
|
||||
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
|
||||
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
|
||||
* a simple way of detecting a time that has not been initialized explicitly.
|
||||
*
|
||||
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
|
||||
* Time with a specific Location. Changing the Location of a Time value with
|
||||
* these methods does not change the actual instant it represents, only the time
|
||||
* zone in which to interpret it.
|
||||
*
|
||||
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
|
||||
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
|
||||
* but not the location name. They therefore lose information about Daylight Saving Time.
|
||||
*
|
||||
* In addition to the required “wall clock” reading, a Time may contain an optional
|
||||
* reading of the current process's monotonic clock, to provide additional precision
|
||||
* for comparison or subtraction.
|
||||
* See the “Monotonic Clocks” section in the package documentation for details.
|
||||
*
|
||||
* Note that the Go == operator compares not just the time instant but also the
|
||||
* Location and the monotonic clock reading. Therefore, Time values should not
|
||||
* be used as map or database keys without first guaranteeing that the
|
||||
* identical Location has been set for all values, which can be achieved
|
||||
* through use of the UTC or Local method, and that the monotonic clock reading
|
||||
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
|
||||
* to t == u, since t.Equal uses the most accurate comparison available and
|
||||
* correctly handles the case when only one of its arguments has a monotonic
|
||||
* clock reading.
|
||||
*/
|
||||
export type Time = any;
|
||||
@@ -0,0 +1,31 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* HttpClientService HTTP客户端服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* ExecuteRequest 执行HTTP请求
|
||||
*/
|
||||
export function ExecuteRequest(request: $models.HttpRequest | null): Promise<$models.HttpResponse | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3143343977, request) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.HttpResponse.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
@@ -8,6 +8,7 @@ import * as DialogService from "./dialogservice.js";
|
||||
import * as DocumentService from "./documentservice.js";
|
||||
import * as ExtensionService from "./extensionservice.js";
|
||||
import * as HotkeyService from "./hotkeyservice.js";
|
||||
import * as HttpClientService from "./httpclientservice.js";
|
||||
import * as KeyBindingService from "./keybindingservice.js";
|
||||
import * as MigrationService from "./migrationservice.js";
|
||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||
@@ -18,7 +19,6 @@ import * as ThemeService from "./themeservice.js";
|
||||
import * as TranslationService from "./translationservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as WindowService from "./windowservice.js";
|
||||
import * as WindowSnapService from "./windowsnapservice.js";
|
||||
export {
|
||||
BackupService,
|
||||
ConfigService,
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
DocumentService,
|
||||
ExtensionService,
|
||||
HotkeyService,
|
||||
HttpClientService,
|
||||
KeyBindingService,
|
||||
MigrationService,
|
||||
SelfUpdateService,
|
||||
@@ -36,8 +37,7 @@ export {
|
||||
ThemeService,
|
||||
TranslationService,
|
||||
TrayService,
|
||||
WindowService,
|
||||
WindowSnapService
|
||||
WindowService
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
||||
|
||||
@@ -8,6 +8,114 @@ import {Create as $Create} from "@wailsio/runtime";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as http$0 from "../../../net/http/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* HttpRequest HTTP请求结构
|
||||
*/
|
||||
export class HttpRequest {
|
||||
"method": string;
|
||||
"url": string;
|
||||
"headers": { [_: string]: string };
|
||||
|
||||
/**
|
||||
* json, formdata, urlencoded, text
|
||||
*/
|
||||
"bodyType"?: string;
|
||||
"body"?: any;
|
||||
|
||||
/** Creates a new HttpRequest instance. */
|
||||
constructor($$source: Partial<HttpRequest> = {}) {
|
||||
if (!("method" in $$source)) {
|
||||
this["method"] = "";
|
||||
}
|
||||
if (!("url" in $$source)) {
|
||||
this["url"] = "";
|
||||
}
|
||||
if (!("headers" in $$source)) {
|
||||
this["headers"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new HttpRequest instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): HttpRequest {
|
||||
const $$createField2_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("headers" in $$parsedSource) {
|
||||
$$parsedSource["headers"] = $$createField2_0($$parsedSource["headers"]);
|
||||
}
|
||||
return new HttpRequest($$parsedSource as Partial<HttpRequest>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpResponse HTTP响应结构
|
||||
*/
|
||||
export class HttpResponse {
|
||||
/**
|
||||
* 使用resp.Status()返回完整状态如"200 OK"
|
||||
*/
|
||||
"status": string;
|
||||
|
||||
/**
|
||||
* 响应时间(毫秒)
|
||||
*/
|
||||
"time": number;
|
||||
|
||||
/**
|
||||
* 请求大小
|
||||
*/
|
||||
"requestSize": string;
|
||||
"body": any;
|
||||
"headers": http$0.Header;
|
||||
"timestamp": time$0.Time;
|
||||
"error"?: any;
|
||||
|
||||
/** Creates a new HttpResponse instance. */
|
||||
constructor($$source: Partial<HttpResponse> = {}) {
|
||||
if (!("status" in $$source)) {
|
||||
this["status"] = "";
|
||||
}
|
||||
if (!("time" in $$source)) {
|
||||
this["time"] = 0;
|
||||
}
|
||||
if (!("requestSize" in $$source)) {
|
||||
this["requestSize"] = "";
|
||||
}
|
||||
if (!("body" in $$source)) {
|
||||
this["body"] = null;
|
||||
}
|
||||
if (!("headers" in $$source)) {
|
||||
this["headers"] = ({} as http$0.Header);
|
||||
}
|
||||
if (!("timestamp" in $$source)) {
|
||||
this["timestamp"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new HttpResponse instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): HttpResponse {
|
||||
const $$createField4_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("headers" in $$parsedSource) {
|
||||
$$parsedSource["headers"] = $$createField4_0($$parsedSource["headers"]);
|
||||
}
|
||||
return new HttpResponse($$parsedSource as Partial<HttpResponse>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MemoryStats 内存统计信息
|
||||
@@ -273,8 +381,8 @@ export class SystemInfo {
|
||||
* Creates a new SystemInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SystemInfo {
|
||||
const $$createField3_0 = $$createType1;
|
||||
const $$createField4_0 = $$createType2;
|
||||
const $$createField3_0 = $$createType5;
|
||||
const $$createField4_0 = $$createType6;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("osInfo" in $$parsedSource) {
|
||||
$$parsedSource["osInfo"] = $$createField3_0($$parsedSource["osInfo"]);
|
||||
@@ -313,7 +421,7 @@ export class WindowInfo {
|
||||
* Creates a new WindowInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowInfo {
|
||||
const $$createField0_0 = $$createType4;
|
||||
const $$createField0_0 = $$createType8;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("Window" in $$parsedSource) {
|
||||
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||
@@ -343,8 +451,17 @@ export class WindowSnapService {
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = OSInfo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType3 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType4 = $Create.Nullable($$createType3);
|
||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||
var $$createType1 = (function $$initCreateType1(...args): any {
|
||||
if ($$createType1 === $$initCreateType1) {
|
||||
$$createType1 = $$createType3;
|
||||
}
|
||||
return $$createType1(...args);
|
||||
});
|
||||
const $$createType2 = $Create.Array($Create.Any);
|
||||
const $$createType3 = $Create.Map($Create.Any, $$createType2);
|
||||
const $$createType4 = OSInfo.createFrom;
|
||||
const $$createType5 = $Create.Nullable($$createType4);
|
||||
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType7 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType8 = $Create.Nullable($$createType7);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* WindowSnapService 窗口吸附服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* Cleanup 清理资源
|
||||
*/
|
||||
export function Cleanup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2155505498) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
|
||||
*/
|
||||
export function GetCurrentThreshold(): Promise<number> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3176419026) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* OnWindowSnapConfigChanged 处理窗口吸附配置变更
|
||||
*/
|
||||
export function OnWindowSnapConfigChanged(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3794787039, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterWindow 注册需要吸附管理的窗口
|
||||
*/
|
||||
export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1000222723, documentID, window, title) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 实现服务关闭接口
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1172710495) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2456823262, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetSnapEnabled 设置是否启用窗口吸附
|
||||
*/
|
||||
export function SetSnapEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2280126835, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnregisterWindow 取消注册窗口
|
||||
*/
|
||||
export function UnregisterWindow(documentID: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2844230768, documentID) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
5
frontend/components.d.ts
vendored
5
frontend/components.d.ts
vendored
@@ -1,8 +1,11 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
|
||||
788
frontend/package-lock.json
generated
788
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,12 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js"
|
||||
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.19.0",
|
||||
"@codemirror/commands": "^6.9.0",
|
||||
"@codemirror/autocomplete": "^6.19.1",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
@@ -26,7 +27,7 @@
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-lezer": "^6.0.2",
|
||||
"@codemirror/lang-liquid": "^6.3.0",
|
||||
"@codemirror/lang-markdown": "^6.4.0",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
@@ -36,14 +37,14 @@
|
||||
"@codemirror/lang-wast": "^6.0.2",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.0",
|
||||
"@codemirror/lint": "^6.9.1",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
@@ -57,35 +58,36 @@
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
"jsox": "^1.2.123",
|
||||
"linguist-languages": "^9.0.0",
|
||||
"linguist-languages": "^9.1.0",
|
||||
"php-parser": "^3.2.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.93.2",
|
||||
"sass": "^1.93.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.10",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitest": "^4.0.6",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.1"
|
||||
"vue-tsc": "^3.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {computed, ref} from 'vue';
|
||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useTabStore} from "@/stores/tabStore";
|
||||
|
||||
export const useDocumentStore = defineStore('document', () => {
|
||||
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
|
||||
@@ -72,6 +73,11 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
await OpenDocumentWindow(docId);
|
||||
const tabStore = useTabStore();
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
||||
tabStore.closeTab(docId);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to open document in new window:', error);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {AsyncManager} from '@/common/utils/asyncManager';
|
||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
|
||||
import {EDITOR_CONFIG} from '@/common/constant/editor';
|
||||
import {createHttpClientExtension} from "@/views/editor/extensions/httpclient";
|
||||
|
||||
export interface DocumentStats {
|
||||
lines: number;
|
||||
@@ -154,6 +155,8 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
enableAutoDetection: true
|
||||
});
|
||||
|
||||
const httpExtension = createHttpClientExtension();
|
||||
|
||||
// 再次检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
@@ -185,7 +188,8 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
statsExtension,
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
...dynamicExtensions
|
||||
...dynamicExtensions,
|
||||
...httpExtension
|
||||
];
|
||||
|
||||
// 创建编辑器状态
|
||||
|
||||
@@ -26,20 +26,20 @@ try {
|
||||
|
||||
// 运行 lezer-generator
|
||||
console.log('⚙️ building parser...');
|
||||
execSync('npx lezer-generator codeblock.grammar -o parser.js', {
|
||||
execSync('npx lezer-generator codeblock.grammar -o parser.ts --typeScript', {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// 检查生成的文件
|
||||
const parserFile = path.join(__dirname, 'parser.js');
|
||||
const termsFile = path.join(__dirname, 'parser.terms.js');
|
||||
const parserFile = path.join(__dirname, 'parser.ts');
|
||||
const termsFile = path.join(__dirname, 'parser.terms.ts');
|
||||
|
||||
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
||||
console.log('✅ parser file successfully generated!');
|
||||
console.log('📦 parser files:');
|
||||
console.log(' - parser.js');
|
||||
console.log(' - parser.terms.js');
|
||||
console.log(' - parser.ts');
|
||||
console.log(' - parser.terms.ts');
|
||||
} else {
|
||||
throw new Error('failed to generate parser');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 提供多语言代码块支持
|
||||
*/
|
||||
|
||||
import { parser } from "./parser.js";
|
||||
import { parser } from "./parser";
|
||||
import { configureNesting } from "./nested-parser";
|
||||
|
||||
import {
|
||||
|
||||
@@ -17,7 +17,8 @@ BlockLanguage {
|
||||
"css" | "xml" | "cpp" | "rs" | "cs" | "rb" | "sh" | "yaml" | "toml" |
|
||||
"go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" |
|
||||
"ps1" | "dart" | "scala" | "math" | "dockerfile" | "lua" | "vue" | "lezer" |
|
||||
"liquid" | "wast" | "sass" | "less" | "angular" | "svelte"
|
||||
"liquid" | "wast" | "sass" | "less" | "angular" | "svelte" |
|
||||
"http"
|
||||
}
|
||||
|
||||
@tokens {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ExternalTokenizer } from "@lezer/lr";
|
||||
import { BlockContent } from "./parser.terms.js";
|
||||
import { BlockContent } from "./parser.terms";
|
||||
import { LANGUAGES } from "./languages";
|
||||
|
||||
const EOF = -1;
|
||||
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
} from './nested-parser';
|
||||
|
||||
// 解析器术语
|
||||
export * from './parser.terms.js';
|
||||
export * from './parser.terms';
|
||||
|
||||
// 外部标记器
|
||||
export {
|
||||
@@ -34,4 +34,4 @@ export {
|
||||
// 解析器
|
||||
export {
|
||||
parser
|
||||
} from './parser.js';
|
||||
} from './parser';
|
||||
@@ -23,6 +23,7 @@ import {sassLanguage} from "@codemirror/lang-sass";
|
||||
import {lessLanguage} from "@codemirror/lang-less";
|
||||
import {angularLanguage} from "@codemirror/lang-angular";
|
||||
import { svelteLanguage } from "@replit/codemirror-lang-svelte";
|
||||
import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language";
|
||||
|
||||
import {StreamLanguage} from "@codemirror/language";
|
||||
import {ruby} from "@codemirror/legacy-modes/mode/ruby";
|
||||
@@ -85,7 +86,7 @@ export class LanguageInfo {
|
||||
* 支持的语言列表
|
||||
*/
|
||||
export const LANGUAGES: LanguageInfo[] = [
|
||||
new LanguageInfo("text", "Plain Text", null),
|
||||
new LanguageInfo("text", "Text", null),
|
||||
new LanguageInfo("json", "JSON", jsonLanguage.parser, ["json"], {
|
||||
parser: "json",
|
||||
plugins: [babelPrettierPlugin, prettierPluginEstree]
|
||||
@@ -224,6 +225,7 @@ export const LANGUAGES: LanguageInfo[] = [
|
||||
filename: "index.svelte"
|
||||
}
|
||||
}),
|
||||
new LanguageInfo("http", "Http", httpLanguage.parser, ["http"]),
|
||||
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { parseMixed } from "@lezer/common";
|
||||
import { BlockContent, BlockLanguage } from "./parser.terms.js";
|
||||
import { BlockContent, BlockLanguage } from "./parser.terms";
|
||||
import { languageMapping } from "./languages";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {blockContent} from "./external-tokens.js"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!jQQOQOOOVOQO'#C`O#uOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#zOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$SOSO1G.fOOOP7+$Q7+$Q",
|
||||
stateData: "$X~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO~OPVO~OUYO!SXO~O!SZO~O",
|
||||
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
|
||||
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
|
||||
maxTerm: 50,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "3g~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_(s#_#`)r#`#a)}#a#b+z#d#e,k#f#g-d#g#h-w#h#i0n#j#k1s#k#l2U#l#m2m#m#n3OR!fP!SQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eP#a#b(h~(kP#`#a(n~(sO]~~(vQ#T#U(|#g#h)_~)PP#j#k)S~)VP#T#U)Y~)_O`~~)dPo~#c#d)g~)jP#b#c)m~)rOZ~~)uP#h#i)x~)}Or~~*QR#X#Y*Z#]#^+Q#i#j+o~*^Q#g#h*d#n#o*o~*gP#g#h*j~*oO!P~~*rP#X#Y*u~*xP#f#g*{~+QO{~~+TP#e#f+W~+ZP#i#j+^~+aP#]#^+d~+gP#W#X+j~+oO|~~+rP#T#U+u~+zOy~~+}Q#T#U,T#W#X,f~,WP#h#i,Z~,^P#[#],a~,fOw~~,kO_~~,nR#[#],w#g#h-S#m#n-_~,zP#d#e,}~-SOa~~-VP!R!S-Y~-_Ot~~-dO[~~-gQ#U#V-m#g#h-r~-rOg~~-wOe~~-zU#T#U.^#V#W.o#[#]/W#e#f/]#j#k/h#k#l0V~.aP#g#h.d~.gP#g#h.j~.oO!O~~.rP#T#U.u~.xP#`#a.{~/OP#T#U/R~/WOv~~/]Oh~~/`P#`#a/c~/hO^~~/kP#X#Y/n~/qP#`#a/t~/wP#h#i/z~/}P#X#Y0Q~0VO!R~~0YP#]#^0]~0`P#Y#Z0c~0fP#h#i0i~0nOq~~0qR#X#Y0z#c#d1]#g#h1n~0}P#l#m1Q~1TP#h#i1W~1]OY~~1`P#a#b1c~1fP#`#a1i~1nOj~~1sOp~~1vP#i#j1y~1|P#X#Y2P~2UOz~~2XP#T#U2[~2_P#g#h2b~2eP#h#i2h~2mO}~~2pP#a#b2s~2vP#`#a2y~3OOc~~3RP#T#U3U~3XP#a#b3[~3_P#`#a3b~3gOi~",
|
||||
tokenizers: [blockContent, 0, 1],
|
||||
topRules: {"Document":[0,2]},
|
||||
tokenPrec: 0
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {blockContent} from "./external-tokens.js"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!jQQOQOOOVOQO'#C`O#xOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#}OSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$VOSO1G.fOOOP7+$Q7+$Q",
|
||||
stateData: "$[~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO~OPVO~OUYO!TXO~O!TZO~O",
|
||||
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
|
||||
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
|
||||
maxTerm: 51,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "3u~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_)R#_#`*Q#`#a*]#a#b,Y#d#e,y#f#g-r#g#h.V#h#i0|#j#k2R#k#l2d#l#m2{#m#n3^R!fP!TQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eQ#a#b(k#h#i(v~(nP#`#a(q~(vO]~~(yP#d#e(|~)RO!S~~)UQ#T#U)[#g#h)m~)_P#j#k)b~)eP#T#U)h~)mO`~~)rPo~#c#d)u~)xP#b#c){~*QOZ~~*TP#h#i*W~*]Or~~*`R#X#Y*i#]#^+`#i#j+}~*lQ#g#h*r#n#o*}~*uP#g#h*x~*}O!P~~+QP#X#Y+T~+WP#f#g+Z~+`O{~~+cP#e#f+f~+iP#i#j+l~+oP#]#^+r~+uP#W#X+x~+}O|~~,QP#T#U,T~,YOy~~,]Q#T#U,c#W#X,t~,fP#h#i,i~,lP#[#],o~,tOw~~,yO_~~,|R#[#]-V#g#h-b#m#n-m~-YP#d#e-]~-bOa~~-eP!R!S-h~-mOt~~-rO[~~-uQ#U#V-{#g#h.Q~.QOg~~.VOe~~.YU#T#U.l#V#W.}#[#]/f#e#f/k#j#k/v#k#l0e~.oP#g#h.r~.uP#g#h.x~.}O!O~~/QP#T#U/T~/WP#`#a/Z~/^P#T#U/a~/fOv~~/kOh~~/nP#`#a/q~/vO^~~/yP#X#Y/|~0PP#`#a0S~0VP#h#i0Y~0]P#X#Y0`~0eO!R~~0hP#]#^0k~0nP#Y#Z0q~0tP#h#i0w~0|Oq~~1PR#X#Y1Y#c#d1k#g#h1|~1]P#l#m1`~1cP#h#i1f~1kOY~~1nP#a#b1q~1tP#`#a1w~1|Oj~~2ROp~~2UP#i#j2X~2[P#X#Y2_~2dOz~~2gP#T#U2j~2mP#g#h2p~2sP#h#i2v~2{O}~~3OP#a#b3R~3UP#`#a3X~3^Oc~~3aP#T#U3d~3gP#a#b3j~3mP#`#a3p~3uOi~",
|
||||
tokenizers: [blockContent, 0, 1],
|
||||
topRules: {"Document":[0,2]},
|
||||
tokenPrec: 0
|
||||
})
|
||||
@@ -27,6 +27,11 @@ export const blockState = StateField.define<Block[]>({
|
||||
* 获取当前活动的块
|
||||
*/
|
||||
export function getActiveNoteBlock(state: EditorState): Block | undefined {
|
||||
// 检查 blockState 字段是否存在
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 找到光标所在的块
|
||||
const range = state.selection.asSingle().ranges[0];
|
||||
return state.field(blockState).find(block =>
|
||||
@@ -38,6 +43,9 @@ export function getActiveNoteBlock(state: EditorState): Block | undefined {
|
||||
* 获取第一个块
|
||||
*/
|
||||
export function getFirstNoteBlock(state: EditorState): Block | undefined {
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
return state.field(blockState)[0];
|
||||
}
|
||||
|
||||
@@ -45,6 +53,9 @@ export function getFirstNoteBlock(state: EditorState): Block | undefined {
|
||||
* 获取最后一个块
|
||||
*/
|
||||
export function getLastNoteBlock(state: EditorState): Block | undefined {
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
const blocks = state.field(blockState);
|
||||
return blocks[blocks.length - 1];
|
||||
}
|
||||
@@ -53,6 +64,9 @@ export function getLastNoteBlock(state: EditorState): Block | undefined {
|
||||
* 根据位置获取块
|
||||
*/
|
||||
export function getNoteBlockFromPos(state: EditorState, pos: number): Block | undefined {
|
||||
if (!state.field(blockState, false)) {
|
||||
return undefined;
|
||||
}
|
||||
return state.field(blockState).find(block =>
|
||||
block.range.from <= pos && block.range.to >= pos
|
||||
);
|
||||
|
||||
@@ -65,6 +65,7 @@ export type SupportedLanguage =
|
||||
| 'less'
|
||||
| 'angular'
|
||||
| 'svelte'
|
||||
| 'http' // HTTP Client
|
||||
|
||||
/**
|
||||
* 创建块的选项
|
||||
|
||||
20
frontend/src/views/editor/extensions/httpclient/index.ts
Normal file
20
frontend/src/views/editor/extensions/httpclient/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* HTTP Client 扩展
|
||||
*/
|
||||
|
||||
import {Extension} from '@codemirror/state';
|
||||
|
||||
import {httpRequestsField, httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter';
|
||||
|
||||
/**
|
||||
* 创建 HTTP Client 扩展
|
||||
*/
|
||||
export function createHttpClientExtension(): Extension[] {
|
||||
return [
|
||||
httpRequestsField,
|
||||
httpRunButtonGutter,
|
||||
httpRunButtonTheme,
|
||||
] as Extension[];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* HTTP Grammar Parser Builder
|
||||
* 编译 Lezer grammar 文件为 TypeScript parser
|
||||
* 使用 --typeScript 选项生成 .ts 文件
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
console.log('🚀 开始编译 HTTP grammar parser (TypeScript)...');
|
||||
|
||||
try {
|
||||
// 检查语法文件是否存在
|
||||
const grammarFile = path.join(__dirname, 'http.grammar');
|
||||
if (!fs.existsSync(grammarFile)) {
|
||||
throw new Error('语法文件 http.grammar 未找到');
|
||||
}
|
||||
|
||||
console.log('📄 语法文件:', grammarFile);
|
||||
|
||||
// 运行 lezer-generator with TypeScript output
|
||||
console.log('⚙️ 编译 parser (生成 TypeScript)...');
|
||||
execSync('npx lezer-generator http.grammar -o http.parser.ts --typeScript', {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// 检查生成的文件
|
||||
const parserFile = path.join(__dirname, 'http.parser.ts');
|
||||
const termsFile = path.join(__dirname, 'http.parser.terms.ts');
|
||||
|
||||
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
||||
console.log('✅ Parser 文件成功生成!');
|
||||
console.log('📦 生成的文件:');
|
||||
console.log(' - http.parser.ts');
|
||||
console.log(' - http.parser.terms.ts');
|
||||
} else {
|
||||
throw new Error('Parser 生成失败');
|
||||
}
|
||||
|
||||
console.log('🎉 编译成功!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 编译失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
|
||||
import { parser } from './http.parser';
|
||||
import { httpHighlighting } from './http.highlight';
|
||||
|
||||
/**
|
||||
* HTTP Client 语言定义
|
||||
*/
|
||||
|
||||
// 配置折叠规则和高亮
|
||||
const httpParserWithMetadata = parser.configure({
|
||||
props: [
|
||||
// 应用语法高亮
|
||||
httpHighlighting,
|
||||
|
||||
// 折叠规则:允许折叠块结构
|
||||
foldNodeProp.add({
|
||||
RequestStatement: foldInside,
|
||||
Block: foldInside,
|
||||
AtRule: foldInside,
|
||||
Document: foldInside,
|
||||
}),
|
||||
|
||||
// 缩进规则
|
||||
indentNodeProp.add({
|
||||
Block: () => 2,
|
||||
Declaration: () => 0,
|
||||
AtRule: () => 0,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 创建 LR 语言实例
|
||||
export const httpLanguage = LRLanguage.define({
|
||||
parser: httpParserWithMetadata,
|
||||
languageData: {
|
||||
//自动闭合括号
|
||||
closeBrackets: { brackets: ['(', '[', '{', '"', "'"] },
|
||||
// 单词字符定义
|
||||
wordChars: '-_',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* HTTP Client 语言支持
|
||||
*/
|
||||
export function http() {
|
||||
return new LanguageSupport(httpLanguage);
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// HTTP Client Grammar
|
||||
//
|
||||
// 语法规则:
|
||||
// 1. HTTP 头部属性:逗号可选
|
||||
// host: "example.com"
|
||||
// content-type: "application/json"
|
||||
//
|
||||
// 2. 请求体格式:
|
||||
// @json - JSON 格式(属性必须用逗号分隔)
|
||||
// @formdata - 表单数据(属性必须用逗号分隔)
|
||||
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
|
||||
// @text - 纯文本内容
|
||||
//
|
||||
// 3. 变量定义:
|
||||
// @var {
|
||||
// baseUrl: "https://api.example.com",
|
||||
// version: "v1",
|
||||
// timeout: 30000
|
||||
// }
|
||||
//
|
||||
// 4. 变量引用:
|
||||
// {{variableName}} - 简单引用
|
||||
// {{variableName:default}} - 带默认值引用
|
||||
//
|
||||
// 5. 响应数据:
|
||||
// 使用独立的 JSON 块
|
||||
// # Response 200 OK 234ms
|
||||
// { "code": 200, "message": "success" }
|
||||
//
|
||||
// 6. 注释:
|
||||
// # 单行注释
|
||||
//
|
||||
// 示例 1 - JSON 请求:
|
||||
// POST "http://api.example.com/users" {
|
||||
// content-type: "application/json"
|
||||
//
|
||||
// @json {
|
||||
// name: "张三",
|
||||
// age: 25,
|
||||
// email: "zhangsan@example.com"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 2 - FormData 请求:
|
||||
// POST "http://api.example.com/upload" {
|
||||
// content-type: "multipart/form-data"
|
||||
//
|
||||
// @formdata {
|
||||
// file: "avatar.png",
|
||||
// username: "zhangsan"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 3 - URLEncoded 请求:
|
||||
// POST "http://api.example.com/login" {
|
||||
// content-type: "application/x-www-form-urlencoded"
|
||||
//
|
||||
// @urlencoded {
|
||||
// username: "admin",
|
||||
// password: "123456"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 4 - 纯文本请求:
|
||||
// POST "http://api.example.com/webhook" {
|
||||
// content-type: "text/plain"
|
||||
//
|
||||
// @text {
|
||||
// content: "纯文本内容"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 示例 5 - 带响应数据:
|
||||
// POST "http://api.example.com/login" {
|
||||
// @json {
|
||||
// username: "admin",
|
||||
// password: "123456"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Response 200 OK 234ms 2025-10-11 10:30:25
|
||||
// {
|
||||
// "code": 200,
|
||||
// "message": "登录成功",
|
||||
// "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
||||
// "data": {
|
||||
// "userId": 1001,
|
||||
// "username": "admin"
|
||||
// }
|
||||
// }
|
||||
|
||||
@skip { whitespace | LineComment }
|
||||
|
||||
@top Document { item* }
|
||||
|
||||
item {
|
||||
VarDeclaration |
|
||||
RequestStatement |
|
||||
ResponseDeclaration |
|
||||
AtRule |
|
||||
JsonObject |
|
||||
JsonArray
|
||||
}
|
||||
|
||||
// 变量声明
|
||||
VarDeclaration {
|
||||
@specialize[@name=VarKeyword]<AtKeyword, "@var">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// 响应声明
|
||||
// 格式:@response <status> <time>ms <timestamp> { <json> }
|
||||
// 示例:@response 200 123ms 2025-10-31T10:30:31 { "data": "..." }
|
||||
// 错误:@response error 0ms 2025-10-31T10:30:31 { "error": "..." }
|
||||
ResponseDeclaration {
|
||||
@specialize[@name=ResponseKeyword]<AtKeyword, "@response">
|
||||
ResponseStatus
|
||||
ResponseTime
|
||||
ResponseTimestamp
|
||||
ResponseBlock
|
||||
}
|
||||
|
||||
// 响应状态:状态码(200 或 200-OK)或 "error" 关键字
|
||||
// 数字开头的状态码作为一个整体 token
|
||||
ResponseStatus {
|
||||
StatusCode |
|
||||
@specialize[@name=ErrorStatus]<identifier, "error">
|
||||
}
|
||||
|
||||
// 响应时间:数字 + "ms" 作为一个整体 token
|
||||
ResponseTime {
|
||||
TimeValue
|
||||
}
|
||||
|
||||
// 响应时间戳:ISO 8601 格式字符串
|
||||
// 格式:2025-10-31T10:30:31
|
||||
ResponseTimestamp {
|
||||
Timestamp
|
||||
}
|
||||
|
||||
// 响应块:标准 JSON 对象或数组(支持带引号的 key)
|
||||
ResponseBlock {
|
||||
JsonObject | JsonArray
|
||||
}
|
||||
|
||||
// HTTP 请求 - URL 必须是字符串
|
||||
RequestStatement {
|
||||
Method Url Block
|
||||
}
|
||||
|
||||
Method {
|
||||
@specialize[@name=GET]<identifier, "GET"> |
|
||||
@specialize[@name=POST]<identifier, "POST"> |
|
||||
@specialize[@name=PUT]<identifier, "PUT"> |
|
||||
@specialize[@name=DELETE]<identifier, "DELETE"> |
|
||||
@specialize[@name=PATCH]<identifier, "PATCH"> |
|
||||
@specialize[@name=HEAD]<identifier, "HEAD"> |
|
||||
@specialize[@name=OPTIONS]<identifier, "OPTIONS"> |
|
||||
@specialize[@name=CONNECT]<identifier, "CONNECT"> |
|
||||
@specialize[@name=TRACE]<identifier, "TRACE">
|
||||
}
|
||||
|
||||
// URL 必须是字符串
|
||||
Url { StringLiteral }
|
||||
|
||||
// @ 规则(支持多种请求体格式,后面可选逗号)
|
||||
AtRule {
|
||||
(JsonRule |
|
||||
FormDataRule |
|
||||
UrlEncodedRule |
|
||||
TextRule) ","?
|
||||
}
|
||||
|
||||
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
||||
JsonRule {
|
||||
@specialize[@name=JsonKeyword]<AtKeyword, "@json">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// @formdata 块:表单数据格式(属性必须用逗号分隔)
|
||||
FormDataRule {
|
||||
@specialize[@name=FormDataKeyword]<AtKeyword, "@formdata">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// @urlencoded 块:URL 编码格式(属性必须用逗号分隔)
|
||||
UrlEncodedRule {
|
||||
@specialize[@name=UrlEncodedKeyword]<AtKeyword, "@urlencoded">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// @text 块:纯文本请求体(使用 content 字段)
|
||||
TextRule {
|
||||
@specialize[@name=TextKeyword]<AtKeyword, "@text">
|
||||
JsonBlock
|
||||
}
|
||||
|
||||
// 普通块结构(属性逗号可选,最多一个请求体)
|
||||
Block {
|
||||
"{" blockContent? "}"
|
||||
}
|
||||
|
||||
// 块内容:
|
||||
// - 选项1: 只有属性
|
||||
// - 选项2: 属性 + 请求体
|
||||
// - 选项3: 属性 + 请求体 + 属性
|
||||
blockContent {
|
||||
Property+ | Property* AtRule Property*
|
||||
}
|
||||
|
||||
// HTTP 属性(逗号可选)
|
||||
Property {
|
||||
PropertyName { identifier }
|
||||
":" value ","?
|
||||
}
|
||||
|
||||
// JSON 块结构(属性必须用逗号分隔)
|
||||
JsonBlock {
|
||||
"{" jsonBlockContent? "}"
|
||||
}
|
||||
|
||||
jsonBlockContent {
|
||||
JsonProperty ("," JsonProperty)* ","?
|
||||
}
|
||||
|
||||
// JSON 属性
|
||||
JsonProperty {
|
||||
PropertyName { identifier }
|
||||
":" jsonValue
|
||||
}
|
||||
|
||||
// 值
|
||||
NumberLiteral {
|
||||
numberLiteralInner Unit?
|
||||
}
|
||||
|
||||
// HTTP 属性值(支持块嵌套和变量引用)
|
||||
value {
|
||||
StringLiteral |
|
||||
NumberLiteral |
|
||||
VariableRef |
|
||||
Block |
|
||||
identifier
|
||||
}
|
||||
|
||||
// JSON 属性值(严格的 JSON 语法:字符串必须用引号,支持变量引用)
|
||||
jsonValue {
|
||||
StringLiteral |
|
||||
NumberLiteral |
|
||||
VariableRef |
|
||||
JsonBlock |
|
||||
JsonTrue |
|
||||
JsonFalse |
|
||||
JsonNull
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 独立 JSON 语法(用于响应数据)
|
||||
// ===============================
|
||||
|
||||
// JSON 对象(独立的 JSON 块,不需要 @ 前缀)
|
||||
JsonObject {
|
||||
"{" jsonObjectContent? "}"
|
||||
}
|
||||
|
||||
jsonObjectContent {
|
||||
JsonMember ("," JsonMember)* ","?
|
||||
}
|
||||
|
||||
// JSON 成员(支持字符串键名和标识符键名)
|
||||
JsonMember {
|
||||
(StringLiteral | identifier) ":" JsonValue
|
||||
}
|
||||
|
||||
// JSON 数组
|
||||
JsonArray {
|
||||
"[" jsonArrayContent? "]"
|
||||
}
|
||||
|
||||
jsonArrayContent {
|
||||
JsonValue ("," JsonValue)* ","?
|
||||
}
|
||||
|
||||
// JSON 值(完整的 JSON 值类型,支持变量引用)
|
||||
JsonValue {
|
||||
StringLiteral |
|
||||
NumberLiteral |
|
||||
VariableRef |
|
||||
JsonObject |
|
||||
JsonArray |
|
||||
JsonTrue |
|
||||
JsonFalse |
|
||||
JsonNull
|
||||
}
|
||||
|
||||
// JSON 字面量
|
||||
JsonTrue { @specialize[@name=True]<identifier, "true"> }
|
||||
JsonFalse { @specialize[@name=False]<identifier, "false"> }
|
||||
JsonNull { @specialize[@name=Null]<identifier, "null"> }
|
||||
|
||||
// Tokens
|
||||
@tokens {
|
||||
// 单行注释(# 开头到行尾)
|
||||
LineComment { "#" ![\n]* }
|
||||
|
||||
AtKeyword { "@" "-"? @asciiLetter (@asciiLetter | @digit | "-")* }
|
||||
|
||||
// 变量引用: {{variableName}} 或 {{variableName:default}} 或 {{obj.nested.path}}
|
||||
VariableRef[isolate] {
|
||||
"{{"
|
||||
(@asciiLetter | $[_$]) (@asciiLetter | @digit | $[-_$] | ".")*
|
||||
(":" (![\n}] | "}" ![}])*)?
|
||||
"}}"
|
||||
}
|
||||
|
||||
// 标识符(属性名,支持连字符)
|
||||
identifier {
|
||||
(@asciiLetter | $[_$])
|
||||
(@asciiLetter | @digit | $[-_$])*
|
||||
}
|
||||
|
||||
// 单位(必须跟在数字后面,所以不单独匹配)
|
||||
Unit { @asciiLetter+ }
|
||||
|
||||
// 时间戳:ISO 8601 格式(YYYY-MM-DDTHH:MM:SS)
|
||||
Timestamp[isolate] {
|
||||
@digit @digit @digit @digit "-" @digit @digit "-" @digit @digit
|
||||
"T" @digit @digit ":" @digit @digit ":" @digit @digit
|
||||
}
|
||||
|
||||
// 状态码:纯数字或数字-字母组合(200, 200-OK, 404-Not-Found)
|
||||
StatusCode {
|
||||
@digit+ ("-" @asciiLetter (@asciiLetter | "-")*)?
|
||||
}
|
||||
|
||||
// 时间值:数字 + ms(123ms)
|
||||
TimeValue {
|
||||
@digit+ "ms"
|
||||
}
|
||||
|
||||
whitespace { @whitespace+ }
|
||||
|
||||
@precedence { Timestamp, TimeValue, StatusCode, numberLiteralInner, VariableRef, identifier, Unit }
|
||||
|
||||
numberLiteralInner {
|
||||
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
||||
(("e" | "E") ("+" | "-")? @digit+)?
|
||||
}
|
||||
|
||||
StringLiteral[isolate] {
|
||||
"\"" (!["\n\\] | "\\" _)* "\"" |
|
||||
"'" (!['\n\\] | "\\" _)* "'"
|
||||
}
|
||||
|
||||
":" ","
|
||||
|
||||
"{" "}"
|
||||
|
||||
"[" "]"
|
||||
}
|
||||
|
||||
@external propSource httpHighlighting from "./http.highlight"
|
||||
|
||||
@detectDelim
|
||||
@@ -0,0 +1,263 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { httpLanguage } from './index';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
/**
|
||||
* 创建测试用的 EditorState
|
||||
*/
|
||||
function createTestState(content: string): EditorState {
|
||||
return EditorState.create({
|
||||
doc: content,
|
||||
extensions: [httpLanguage]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否存在
|
||||
*/
|
||||
function hasNode(state: EditorState, nodeName: string): boolean {
|
||||
const tree = syntaxTree(state);
|
||||
let found = false;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === nodeName) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点文本
|
||||
*/
|
||||
function getNodeText(state: EditorState, nodeName: string): string | null {
|
||||
const tree = syntaxTree(state);
|
||||
let text: string | null = null;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === nodeName) {
|
||||
text = state.doc.sliceString(node.from, node.to);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
describe('HTTP Grammar - @response 响应语法', () => {
|
||||
|
||||
it('✅ 成功响应 - 完整格式', () => {
|
||||
const content = `@response 200 123ms 2025-10-31T10:30:31 {
|
||||
"message": "success",
|
||||
"data": [1, 2, 3]
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseStatus')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseTime')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseTimestamp')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseBlock')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 错误响应 - error 关键字', () => {
|
||||
const content = `@response error 0ms 2025-10-31T10:30:31 {
|
||||
"error": "Network timeout"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'ErrorStatus')).toBe(true);
|
||||
expect(hasNode(state, 'TimeUnit')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 响应与请求结合', () => {
|
||||
const content = `GET "https://api.example.com/users" {}
|
||||
|
||||
@response 200 123ms 2025-10-31T10:30:31 {
|
||||
"users": [
|
||||
{ "id": 1, "name": "Alice" },
|
||||
{ "id": 2, "name": "Bob" }
|
||||
]
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'RequestStatement')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 多个请求和响应', () => {
|
||||
const content = `GET "https://api.example.com/users" {}
|
||||
@response 200 100ms 2025-10-31T10:30:31 {
|
||||
"users": []
|
||||
}
|
||||
|
||||
POST "https://api.example.com/users" {
|
||||
@json { "name": "Alice" }
|
||||
}
|
||||
@response 201 50ms 2025-10-31T10:30:32 {
|
||||
"id": 1,
|
||||
"name": "Alice"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
// 统计 ResponseDeclaration 数量
|
||||
let responseCount = 0;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === 'ResponseDeclaration') {
|
||||
responseCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(responseCount).toBe(2);
|
||||
});
|
||||
|
||||
it('✅ 响应状态码类型', () => {
|
||||
const testCases = [
|
||||
{ status: '200', shouldParse: true },
|
||||
{ status: '201', shouldParse: true },
|
||||
{ status: '404', shouldParse: true },
|
||||
{ status: '500', shouldParse: true },
|
||||
{ status: 'error', shouldParse: true }
|
||||
];
|
||||
|
||||
testCases.forEach(({ status, shouldParse }) => {
|
||||
const content = `@response ${status} 0ms 2025-10-31T10:30:31 { "data": null }`;
|
||||
const state = createTestState(content);
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(shouldParse);
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 响应时间单位', () => {
|
||||
const content = `@response 200 12345ms 2025-10-31T10:30:31 {
|
||||
"data": "test"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'TimeUnit')).toBe(true);
|
||||
expect(getNodeText(state, 'TimeUnit')).toBe('ms');
|
||||
});
|
||||
|
||||
it('✅ 响应块包含复杂 JSON', () => {
|
||||
const content = `@response 200 150ms 2025-10-31T10:30:31 {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Bob",
|
||||
"email": "bob@example.com",
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'JsonObject')).toBe(true);
|
||||
expect(hasNode(state, 'JsonArray')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 空响应体', () => {
|
||||
const content = `@response 204 50ms 2025-10-31T10:30:31 {}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseBlock')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 响应体为数组', () => {
|
||||
const content = `@response 200 80ms 2025-10-31T10:30:31 [
|
||||
{ "id": 1, "name": "Alice" },
|
||||
{ "id": 2, "name": "Bob" }
|
||||
]`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'JsonArray')).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 时间戳格式', () => {
|
||||
const testCases = [
|
||||
'2025-10-31T10:30:31',
|
||||
'2025-01-01T00:00:00',
|
||||
'2025-12-31T23:59:59'
|
||||
];
|
||||
|
||||
testCases.forEach(timestamp => {
|
||||
const content = `@response 200 100ms ${timestamp} { "data": null }`;
|
||||
const state = createTestState(content);
|
||||
expect(hasNode(state, 'ResponseTimestamp')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('❌ 缺少必填字段应该有错误', () => {
|
||||
const invalidCases = [
|
||||
'@response 200 { "data": null }', // 缺少时间和时间戳
|
||||
'@response 200 100ms { "data": null }', // 缺少时间戳
|
||||
];
|
||||
|
||||
invalidCases.forEach(content => {
|
||||
const state = createTestState(content);
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
// 检查是否有错误节点
|
||||
let hasError = false;
|
||||
tree.iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === '⚠') {
|
||||
hasError = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 与变量结合', () => {
|
||||
const content = `@var {
|
||||
apiUrl: "https://api.example.com"
|
||||
}
|
||||
|
||||
GET "https://api.example.com/users" {}
|
||||
|
||||
@response 200 123ms 2025-10-31T10:30:31 {
|
||||
"users": []
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
expect(hasNode(state, 'VarDeclaration')).toBe(true);
|
||||
expect(hasNode(state, 'RequestStatement')).toBe(true);
|
||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,725 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parser } from './http.parser';
|
||||
|
||||
/**
|
||||
* HTTP Grammar 测试
|
||||
*
|
||||
* 测试目标:验证标准的 HTTP 请求语法是否能正确解析,不应该出现错误节点(⚠)
|
||||
*/
|
||||
describe('HTTP Grammar 解析测试', () => {
|
||||
|
||||
/**
|
||||
* 辅助函数:解析代码并返回语法树
|
||||
*/
|
||||
function parseCode(code: string) {
|
||||
const tree = parser.parse(code);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:检查语法树中是否有错误节点
|
||||
*/
|
||||
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
|
||||
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === '⚠') {
|
||||
errors.push({
|
||||
name: node.name,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
text: tree.toString().substring(node.from, node.to)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasError: errors.length > 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:打印语法树结构(用于调试)
|
||||
*/
|
||||
function printTree(tree: any, code: string, maxDepth = 5) {
|
||||
const lines: string[] = [];
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
const depth = getNodeDepth(tree, node);
|
||||
if (depth > maxDepth) return false; // 限制深度
|
||||
|
||||
const indent = ' '.repeat(depth);
|
||||
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
|
||||
const displayText = text.length === 30 ? text + '...' : text;
|
||||
|
||||
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点深度
|
||||
*/
|
||||
function getNodeDepth(tree: any, targetNode: any): number {
|
||||
let depth = 0;
|
||||
let cursor = tree.cursor();
|
||||
|
||||
function traverse(node: any, currentDepth: number): boolean {
|
||||
if (node.from === targetNode.from && node.to === targetNode.to && node.name === targetNode.name) {
|
||||
depth = currentDepth;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 简化:假设深度就是节点的层级
|
||||
let current = targetNode;
|
||||
while (current.parent) {
|
||||
depth++;
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
it('应该正确解析标准的 GET 请求(包含 @json 和 @res)', () => {
|
||||
const code = `GET "http://127.0.0.1:80/api/create" {
|
||||
host: "https://api.example.com",
|
||||
content-type: "application/json",
|
||||
user-agent: 'Mozilla/5.0',
|
||||
|
||||
@json {
|
||||
name : "xxx",
|
||||
test: "xx"
|
||||
}
|
||||
|
||||
@res {
|
||||
code: 200,
|
||||
status: "ok",
|
||||
size: "20kb",
|
||||
time: "2025-10-31 10:30:26",
|
||||
data: {
|
||||
xxx:"xxx"
|
||||
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
// 如果有错误,打印详细信息
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 发现错误节点:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该正确解析简单的 POST 请求', () => {
|
||||
const code = `POST "http://127.0.0.1/api" {
|
||||
host: "example.com",
|
||||
content-type: "application/json"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 发现错误节点:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确解析带嵌套块的请求', () => {
|
||||
const code = `POST "http://test.com" {
|
||||
@json {
|
||||
user: {
|
||||
name: "test",
|
||||
age: 25
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 发现错误节点:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确识别 RequestStatement 节点', () => {
|
||||
const code = `GET "http://test.com" {
|
||||
host: "test.com"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
let hasRequestStatement = false;
|
||||
let hasMethod = false;
|
||||
let hasUrl = false;
|
||||
let hasBlock = false;
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === 'RequestStatement') hasRequestStatement = true;
|
||||
if (node.name === 'Method' || node.name === 'GET') hasMethod = true;
|
||||
if (node.name === 'Url') hasUrl = true;
|
||||
if (node.name === 'Block') hasBlock = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasRequestStatement).toBe(true);
|
||||
expect(hasMethod).toBe(true);
|
||||
expect(hasUrl).toBe(true);
|
||||
expect(hasBlock).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确解析多个连续的请求', () => {
|
||||
const code = `GET "http://test1.com" {
|
||||
host: "test1.com"
|
||||
}
|
||||
|
||||
POST "http://test2.com" {
|
||||
host: "test2.com"
|
||||
}`;
|
||||
|
||||
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(2);
|
||||
});
|
||||
|
||||
it('错误语法:方法名拼写错误(应该产生错误)', () => {
|
||||
const code = `Gef "http://test.com" {
|
||||
host: "test.com"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
// 这个应该有错误
|
||||
expect(result.hasError).toBe(true);
|
||||
});
|
||||
|
||||
it('错误语法:花括号不匹配(应该产生错误)', () => {
|
||||
const code = `GET "http://test.com" {
|
||||
host: "test.com"`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
// 这个应该有错误
|
||||
expect(result.hasError).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持属性后面不加逗号', () => {
|
||||
const code = `GET "http://test.com" {
|
||||
host: "test.com"
|
||||
content-type: "application/json"
|
||||
user-agent: "Mozilla/5.0"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 发现错误节点:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('应该支持 @json/@res 块后面不加逗号(JSON块内部必须用逗号)', () => {
|
||||
const code = `POST "http://test.com" {
|
||||
host: "test.com"
|
||||
|
||||
@json {
|
||||
name: "xxx",
|
||||
test: "xx"
|
||||
}
|
||||
|
||||
@res {
|
||||
code: 200,
|
||||
status: "ok"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 发现错误节点:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('应该支持混合使用逗号(有些有逗号,有些没有)', () => {
|
||||
const code = `POST "http://test.com" {
|
||||
host: "test.com",
|
||||
content-type: "application/json"
|
||||
user-agent: "Mozilla/5.0",
|
||||
|
||||
@json {
|
||||
name: "xxx",
|
||||
test: "xx"
|
||||
}
|
||||
}`;
|
||||
|
||||
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('用户提供的真实示例(HTTP 属性不用逗号,JSON 块内必须用逗号)', () => {
|
||||
const code = `GET "http://127.0.0.1:80/api/create" {
|
||||
host: "https://api.example.com"
|
||||
content-type: "application/json"
|
||||
user-agent: 'Mozilla/5.0'
|
||||
|
||||
@json {
|
||||
name: "xxx",
|
||||
test: "xx"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 发现错误节点:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('JSON 块内缺少逗号应该报错', () => {
|
||||
const code = `POST "http://test.com" {
|
||||
@json {
|
||||
name: "xxx"
|
||||
test: "xx"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
// JSON 块内缺少逗号,应该有错误
|
||||
expect(result.hasError).toBe(true);
|
||||
});
|
||||
|
||||
it('支持 @formdata 块(必须使用逗号)', () => {
|
||||
const code = `POST "http://test.com" {
|
||||
@formdata {
|
||||
file: "test.png",
|
||||
description: "test file"
|
||||
}
|
||||
}`;
|
||||
|
||||
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('支持 JSON 嵌套对象', () => {
|
||||
const code = `POST "http://test.com" {
|
||||
@json {
|
||||
user: {
|
||||
name: "test",
|
||||
age: 25
|
||||
},
|
||||
settings: {
|
||||
theme: "dark"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP 请求体格式测试', () => {
|
||||
|
||||
/**
|
||||
* 辅助函数:解析代码并返回语法树
|
||||
*/
|
||||
function parseCode(code: string) {
|
||||
const tree = parser.parse(code);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:检查语法树中是否有错误节点
|
||||
*/
|
||||
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
|
||||
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === '⚠') {
|
||||
errors.push({
|
||||
name: node.name,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
text: tree.toString().substring(node.from, node.to)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasError: errors.length > 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
it('✅ @json - JSON 格式请求体', () => {
|
||||
const code = `POST "http://api.example.com/users" {
|
||||
content-type: "application/json"
|
||||
authorization: "Bearer token123"
|
||||
|
||||
@json {
|
||||
name: "张三",
|
||||
age: 25,
|
||||
email: "zhangsan@example.com",
|
||||
address: {
|
||||
city: "北京",
|
||||
street: "长安街"
|
||||
},
|
||||
tags: {
|
||||
skill: "TypeScript",
|
||||
level: "advanced"
|
||||
}
|
||||
}
|
||||
|
||||
@res {
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
id: 12345
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ @json 格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ @formdata - 表单数据格式', () => {
|
||||
const code = `POST "http://api.example.com/upload" {
|
||||
content-type: "multipart/form-data"
|
||||
|
||||
@formdata {
|
||||
file: "avatar.png",
|
||||
username: "zhangsan",
|
||||
email: "zhangsan@example.com",
|
||||
age: 25,
|
||||
description: "用户头像上传"
|
||||
}
|
||||
|
||||
@res {
|
||||
code: 200,
|
||||
message: "上传成功",
|
||||
url: "https://cdn.example.com/avatar.png"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ @formdata 格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ @urlencoded - URL 编码格式', () => {
|
||||
const code = `POST "http://api.example.com/login" {
|
||||
content-type: "application/x-www-form-urlencoded"
|
||||
|
||||
@urlencoded {
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
remember: true
|
||||
}
|
||||
|
||||
@res {
|
||||
code: 200,
|
||||
message: "登录成功",
|
||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ @urlencoded 格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ @text - 纯文本请求体', () => {
|
||||
const code = `POST "http://api.example.com/webhook" {
|
||||
content-type: "text/plain"
|
||||
|
||||
@text {
|
||||
content: "这是一段纯文本内容,可以包含多行\\n支持中文和特殊字符!@#$%"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ @text 格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ # 单行注释', () => {
|
||||
const code = `# 这是一个用户登录接口
|
||||
POST "http://api.example.com/login" {
|
||||
# 添加认证头
|
||||
content-type: "application/json"
|
||||
|
||||
# 登录参数
|
||||
@json {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
|
||||
# 期望的响应
|
||||
@res {
|
||||
code: 200,
|
||||
# 用户token
|
||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
}
|
||||
}`;
|
||||
|
||||
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('✅ 混合多种格式 - JSON + 响应', () => {
|
||||
const code = `POST "http://api.example.com/login" {
|
||||
content-type: "application/json"
|
||||
user-agent: "Mozilla/5.0"
|
||||
|
||||
@json {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
|
||||
@res {
|
||||
code: 200,
|
||||
message: "登录成功",
|
||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
||||
user: {
|
||||
id: 1,
|
||||
name: "管理员"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
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('✅ 复杂嵌套 JSON', () => {
|
||||
const code = `POST "http://api.example.com/complex" {
|
||||
@json {
|
||||
user: {
|
||||
profile: {
|
||||
name: "张三",
|
||||
contact: {
|
||||
email: "zhangsan@example.com",
|
||||
phone: "13800138000"
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
theme: "dark",
|
||||
language: "zh-CN"
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
version: "1.0",
|
||||
timestamp: 1234567890
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
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 = `# JSON 请求
|
||||
POST "http://api.example.com/json" {
|
||||
@json {
|
||||
name: "test"
|
||||
}
|
||||
}
|
||||
|
||||
# FormData 请求
|
||||
POST "http://api.example.com/form" {
|
||||
@formdata {
|
||||
file: "test.txt",
|
||||
description: "test"
|
||||
}
|
||||
}
|
||||
|
||||
# URLEncoded 请求
|
||||
POST "http://api.example.com/login" {
|
||||
@urlencoded {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
}`;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parser } from './http.parser';
|
||||
|
||||
/**
|
||||
* HTTP 变量功能测试
|
||||
*
|
||||
* 测试变量定义 @var 和变量引用 {{variableName}} 语法
|
||||
*/
|
||||
describe('HTTP 变量功能测试', () => {
|
||||
|
||||
/**
|
||||
* 辅助函数:解析代码并返回语法树
|
||||
*/
|
||||
function parseCode(code: string) {
|
||||
const tree = parser.parse(code);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:检查语法树中是否有错误节点
|
||||
*/
|
||||
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
|
||||
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === '⚠') {
|
||||
errors.push({
|
||||
name: node.name,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
text: tree.toString().substring(node.from, node.to)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasError: errors.length > 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:打印语法树结构(用于调试)
|
||||
*/
|
||||
function printTree(tree: any, code: string, maxDepth = 5) {
|
||||
const lines: string[] = [];
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
const depth = getNodeDepth(tree, node);
|
||||
if (depth > maxDepth) return false;
|
||||
|
||||
const indent = ' '.repeat(depth);
|
||||
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
|
||||
const displayText = text.length === 30 ? text + '...' : text;
|
||||
|
||||
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getNodeDepth(tree: any, targetNode: any): number {
|
||||
let depth = 0;
|
||||
let current = targetNode;
|
||||
while (current.parent) {
|
||||
depth++;
|
||||
current = current.parent;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
it('✅ @var - 变量声明', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ @var 变量声明格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
|
||||
// 验证是否有 VarDeclaration 节点
|
||||
let hasVarDeclaration = false;
|
||||
let hasVarKeyword = false;
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === 'VarDeclaration') hasVarDeclaration = true;
|
||||
if (node.name === 'VarKeyword') hasVarKeyword = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasVarDeclaration).toBe(true);
|
||||
expect(hasVarKeyword).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 变量引用 - 在属性值中使用', () => {
|
||||
const code = `GET "http://example.com" {
|
||||
timeout: {{timeout}}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 变量引用格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
|
||||
// 验证是否有 VariableRef 节点
|
||||
let hasVariableRef = false;
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === 'VariableRef') hasVariableRef = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasVariableRef).toBe(true);
|
||||
});
|
||||
|
||||
it('✅ 变量引用 - 带默认值 {{variableName:default}}', () => {
|
||||
const code = `GET "http://example.com" {
|
||||
authorization: "Bearer {{token:default-token}}"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 带默认值的变量引用格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 完整示例 - 变量定义和使用(用户提供的示例)', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/{{version}}/users" {
|
||||
timeout: {{timeout}},
|
||||
authorization: "Bearer {{token:default-token}}"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 完整示例格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量在 JSON 请求体中使用', () => {
|
||||
const code = `@var {
|
||||
userId: "12345",
|
||||
userName: "张三"
|
||||
}
|
||||
|
||||
POST "http://api.example.com/users" {
|
||||
@json {
|
||||
id: {{userId}},
|
||||
name: {{userName}},
|
||||
email: "{{userName}}@example.com"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ JSON 中使用变量格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量在多个请求中使用', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
token: "Bearer abc123"
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/users" {
|
||||
authorization: {{token}}
|
||||
}
|
||||
|
||||
POST "{{baseUrl}}/users" {
|
||||
authorization: {{token}},
|
||||
|
||||
@json {
|
||||
name: "test"
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 多请求中使用变量格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ URL 中包含多个变量', () => {
|
||||
const code = `GET "{{protocol}}://{{host}}:{{port}}/{{path}}/{{resource}}" {
|
||||
host: "example.com"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ URL 多变量格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量名包含下划线和数字', () => {
|
||||
const code = `@var {
|
||||
api_base_url_v2: "https://api.example.com",
|
||||
timeout_30s: 30000,
|
||||
user_id_123: "123"
|
||||
}
|
||||
|
||||
GET "{{api_base_url_v2}}/users/{{user_id_123}}" {
|
||||
timeout: {{timeout_30s}}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 变量名包含下划线和数字格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 变量默认值包含特殊字符', () => {
|
||||
const code = `GET "http://example.com" {
|
||||
authorization: "Bearer {{token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0}}"
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 变量默认值特殊字符格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('✅ 混合变量和普通值', () => {
|
||||
const code = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1"
|
||||
}
|
||||
|
||||
POST "{{baseUrl}}/{{version}}/users" {
|
||||
content-type: "application/json",
|
||||
authorization: "Bearer {{token:default}}",
|
||||
user-agent: "Mozilla/5.0",
|
||||
|
||||
@json {
|
||||
name: "张三",
|
||||
age: 25,
|
||||
apiUrl: {{baseUrl}},
|
||||
apiVersion: {{version}}
|
||||
}
|
||||
}`;
|
||||
|
||||
const tree = parseCode(code);
|
||||
const result = hasErrorNodes(tree);
|
||||
|
||||
if (result.hasError) {
|
||||
console.log('\n❌ 混合变量和普通值格式错误:');
|
||||
result.errors.forEach(err => {
|
||||
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||
});
|
||||
console.log('\n完整语法树:');
|
||||
console.log(printTree(tree, code));
|
||||
}
|
||||
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { styleTags, tags as t } from "@lezer/highlight"
|
||||
|
||||
/**
|
||||
* HTTP Client 语法高亮配置
|
||||
*/
|
||||
export const httpHighlighting = styleTags({
|
||||
// ========== HTTP 方法(使用不同的强调程度)==========
|
||||
// 查询方法 - 使用普通关键字
|
||||
"GET HEAD OPTIONS": t.keyword,
|
||||
|
||||
// 修改方法 - 使用控制关键字
|
||||
"POST PUT PATCH": t.controlKeyword,
|
||||
|
||||
// 删除方法 - 使用操作符
|
||||
"DELETE": t.operatorKeyword,
|
||||
|
||||
// 其他方法 - 使用修饰关键字
|
||||
"TRACE CONNECT": t.modifier,
|
||||
|
||||
// ========== @ 规则(请求体格式和变量声明)==========
|
||||
// @json, @formdata, @urlencoded - 使用类型名
|
||||
"JsonKeyword FormDataKeyword UrlEncodedKeyword": t.typeName,
|
||||
|
||||
// @text - 使用特殊类型
|
||||
"TextKeyword": t.special(t.typeName),
|
||||
|
||||
// @var - 变量声明关键字
|
||||
"VarKeyword": t.definitionKeyword,
|
||||
|
||||
// @response - 响应关键字
|
||||
"ResponseKeyword": t.keyword,
|
||||
|
||||
// @ 符号本身 - 使用元标记
|
||||
"AtKeyword": t.meta,
|
||||
|
||||
// ========== 变量引用 ==========
|
||||
// {{variableName}} - 使用特殊变量名
|
||||
"VariableRef": t.special(t.definitionKeyword),
|
||||
|
||||
// ========== URL(特殊处理)==========
|
||||
// URL 节点 - 使用链接颜色
|
||||
"Url": t.link,
|
||||
|
||||
// ========== 属性和值 ==========
|
||||
// 属性名 - 使用定义名称
|
||||
"PropertyName": t.definition(t.attributeName),
|
||||
|
||||
// 普通标识符值 - 使用常量名
|
||||
"identifier": t.constant(t.variableName),
|
||||
|
||||
// ========== 字面量 ==========
|
||||
// 数字 - 数字颜色
|
||||
"NumberLiteral": t.number,
|
||||
|
||||
// 字符串 - 字符串颜色
|
||||
"StringLiteral": t.string,
|
||||
|
||||
// 单位 - 单位颜色
|
||||
"Unit": t.unit,
|
||||
|
||||
// ========== 响应相关 ==========
|
||||
// 响应状态码 - 数字颜色
|
||||
"StatusCode": t.number,
|
||||
"ResponseStatus/StatusCode": t.number,
|
||||
|
||||
// 响应错误状态 - 关键字
|
||||
"ErrorStatus": t.operatorKeyword,
|
||||
|
||||
// 响应时间 - 数字颜色
|
||||
"TimeValue": t.number,
|
||||
"ResponseTime": t.number,
|
||||
|
||||
// 时间戳 - 字符串颜色
|
||||
"Timestamp": t.string,
|
||||
"ResponseTimestamp": t.string,
|
||||
|
||||
// ========== 注释 ==========
|
||||
// # 单行注释 - 行注释颜色
|
||||
"LineComment": t.lineComment,
|
||||
|
||||
// ========== JSON 语法(独立 JSON 块)==========
|
||||
// JSON 对象和数组
|
||||
"JsonObject": t.brace,
|
||||
"JsonArray": t.squareBracket,
|
||||
|
||||
// JSON 属性名 - 使用属性名颜色
|
||||
"JsonProperty/PropertyName": t.propertyName,
|
||||
"JsonProperty/StringLiteral": t.propertyName,
|
||||
|
||||
// JSON 成员(属性名)- 使用属性名颜色(适用于独立 JSON 对象)
|
||||
"JsonMember/StringLiteral": t.propertyName,
|
||||
"JsonMember/identifier": t.propertyName,
|
||||
|
||||
// JSON 字面量值
|
||||
"True False": t.bool,
|
||||
"Null": t.null,
|
||||
|
||||
// JSON 值(确保字符串和数字正确高亮)
|
||||
"JsonValue/StringLiteral": t.string,
|
||||
"JsonValue/NumberLiteral": t.number,
|
||||
|
||||
// ========== 标点符号 ==========
|
||||
// 冒号 - 分隔符
|
||||
":": t.separator,
|
||||
|
||||
// 逗号 - 分隔符
|
||||
",": t.separator,
|
||||
|
||||
// 花括号 - 大括号
|
||||
"{ }": t.brace,
|
||||
|
||||
// 方括号 - 方括号
|
||||
"[ ]": t.squareBracket,
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
LineComment = 1,
|
||||
Document = 2,
|
||||
VarDeclaration = 3,
|
||||
AtKeyword = 4,
|
||||
VarKeyword = 5,
|
||||
JsonBlock = 8,
|
||||
JsonProperty = 9,
|
||||
StringLiteral = 12,
|
||||
NumberLiteral = 13,
|
||||
Unit = 14,
|
||||
VariableRef = 15,
|
||||
JsonTrue = 16,
|
||||
True = 17,
|
||||
JsonFalse = 18,
|
||||
False = 19,
|
||||
JsonNull = 20,
|
||||
Null = 21,
|
||||
RequestStatement = 23,
|
||||
Method = 24,
|
||||
GET = 25,
|
||||
POST = 26,
|
||||
PUT = 27,
|
||||
DELETE = 28,
|
||||
PATCH = 29,
|
||||
HEAD = 30,
|
||||
OPTIONS = 31,
|
||||
CONNECT = 32,
|
||||
TRACE = 33,
|
||||
Url = 34,
|
||||
Block = 35,
|
||||
Property = 36,
|
||||
AtRule = 38,
|
||||
JsonRule = 39,
|
||||
JsonKeyword = 40,
|
||||
FormDataRule = 41,
|
||||
FormDataKeyword = 42,
|
||||
UrlEncodedRule = 43,
|
||||
UrlEncodedKeyword = 44,
|
||||
TextRule = 45,
|
||||
TextKeyword = 46,
|
||||
ResponseDeclaration = 47,
|
||||
ResponseKeyword = 48,
|
||||
ResponseStatus = 49,
|
||||
StatusCode = 50,
|
||||
ErrorStatus = 51,
|
||||
ResponseTime = 52,
|
||||
TimeValue = 53,
|
||||
ResponseTimestamp = 54,
|
||||
Timestamp = 55,
|
||||
ResponseBlock = 56,
|
||||
JsonObject = 57,
|
||||
JsonMember = 58,
|
||||
JsonValue = 59,
|
||||
JsonArray = 62
|
||||
@@ -0,0 +1,26 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {httpHighlighting} from "./http.highlight"
|
||||
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@response":96}
|
||||
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}
|
||||
export const parser = LRParser.deserialize({
|
||||
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",
|
||||
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~",
|
||||
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",
|
||||
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",
|
||||
maxTerm: 79,
|
||||
nodeProps: [
|
||||
["openedBy", 6,"{",60,"["],
|
||||
["closedBy", 7,"}",61,"]"],
|
||||
["isolate", -3,12,15,55,""]
|
||||
],
|
||||
propSources: [httpHighlighting],
|
||||
skippedNodes: [0,1,4],
|
||||
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~",
|
||||
tokenizers: [0],
|
||||
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}],
|
||||
tokenPrec: 503
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export { http, httpLanguage } from './http-language';
|
||||
export { parser } from './http.parser';
|
||||
export type { LRLanguage } from '@codemirror/language';
|
||||
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { HttpRequestParser } from './request-parser';
|
||||
import { http } from '../language';
|
||||
|
||||
/**
|
||||
* 创建测试用的编辑器状态
|
||||
*/
|
||||
function createTestState(code: string): EditorState {
|
||||
return EditorState.create({
|
||||
doc: code,
|
||||
extensions: [http()],
|
||||
});
|
||||
}
|
||||
|
||||
describe('HttpRequestParser', () => {
|
||||
describe('基本请求解析', () => {
|
||||
it('应该解析简单的 GET 请求', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
content-type: "application/json"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('GET');
|
||||
expect(request?.url).toBe('http://api.example.com/users');
|
||||
expect(request?.headers).toEqual({
|
||||
host: 'api.example.com',
|
||||
'content-type': 'application/json',
|
||||
});
|
||||
expect(request?.bodyType).toBeUndefined();
|
||||
expect(request?.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该解析 POST 请求(带 JSON 请求体)', () => {
|
||||
const code = `POST "http://api.example.com/users" {
|
||||
content-type: "application/json"
|
||||
|
||||
@json {
|
||||
name: "张三",
|
||||
age: 25,
|
||||
email: "zhangsan@example.com"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('POST');
|
||||
expect(request?.url).toBe('http://api.example.com/users');
|
||||
expect(request?.headers).toEqual({
|
||||
'content-type': 'application/json',
|
||||
});
|
||||
expect(request?.bodyType).toBe('json');
|
||||
expect(request?.body).toEqual({
|
||||
name: '张三',
|
||||
age: 25,
|
||||
email: 'zhangsan@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 PUT 请求', () => {
|
||||
const code = `PUT "http://api.example.com/users/123" {
|
||||
authorization: "Bearer token123"
|
||||
|
||||
@json {
|
||||
name: "李四"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('PUT');
|
||||
expect(request?.url).toBe('http://api.example.com/users/123');
|
||||
});
|
||||
|
||||
it('应该解析 DELETE 请求', () => {
|
||||
const code = `DELETE "http://api.example.com/users/123" {
|
||||
authorization: "Bearer token123"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.method).toBe('DELETE');
|
||||
expect(request?.url).toBe('http://api.example.com/users/123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('请求体类型解析', () => {
|
||||
it('应该解析 @json 请求体', () => {
|
||||
const code = `POST "http://api.example.com/test" {
|
||||
@json {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('json');
|
||||
expect(request?.body).toEqual({
|
||||
username: 'admin',
|
||||
password: '123456',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 @formdata 请求体', () => {
|
||||
const code = `POST "http://api.example.com/upload" {
|
||||
@formdata {
|
||||
file: "avatar.png",
|
||||
description: "用户头像"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('formdata');
|
||||
expect(request?.body).toEqual({
|
||||
file: 'avatar.png',
|
||||
description: '用户头像',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 @urlencoded 请求体', () => {
|
||||
const code = `POST "http://api.example.com/login" {
|
||||
@urlencoded {
|
||||
username: "admin",
|
||||
password: "123456"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('urlencoded');
|
||||
expect(request?.body).toEqual({
|
||||
username: 'admin',
|
||||
password: '123456',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析 @text 请求体', () => {
|
||||
const code = `POST "http://api.example.com/webhook" {
|
||||
@text {
|
||||
content: "纯文本内容"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.bodyType).toBe('text');
|
||||
expect(request?.body).toEqual({
|
||||
content: '纯文本内容',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('复杂数据类型', () => {
|
||||
it('应该解析嵌套对象', () => {
|
||||
const code = `POST "http://api.example.com/users" {
|
||||
@json {
|
||||
user: {
|
||||
name: "张三",
|
||||
age: 25
|
||||
},
|
||||
settings: {
|
||||
theme: "dark"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.body).toEqual({
|
||||
user: {
|
||||
name: '张三',
|
||||
age: 25,
|
||||
},
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析布尔值', () => {
|
||||
const code = `POST "http://api.example.com/test" {
|
||||
@json {
|
||||
enabled: true,
|
||||
disabled: false
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.body).toEqual({
|
||||
enabled: true,
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('应该解析数字', () => {
|
||||
const code = `POST "http://api.example.com/test" {
|
||||
@json {
|
||||
count: 100,
|
||||
price: 19.99
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.body).toEqual({
|
||||
count: 100,
|
||||
price: 19.99,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headers 解析', () => {
|
||||
it('应该解析多个 headers', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
authorization: "Bearer token123"
|
||||
content-type: "application/json"
|
||||
user-agent: "Mozilla/5.0"
|
||||
accept: "application/json"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.headers).toEqual({
|
||||
host: 'api.example.com',
|
||||
authorization: 'Bearer token123',
|
||||
'content-type': 'application/json',
|
||||
'user-agent': 'Mozilla/5.0',
|
||||
accept: 'application/json',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持单引号字符串', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
user-agent: 'Mozilla/5.0'
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.headers['user-agent']).toBe('Mozilla/5.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('位置信息', () => {
|
||||
it('应该记录请求的位置信息', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request?.position).toBeDefined();
|
||||
expect(request?.position.line).toBe(1);
|
||||
expect(request?.position.from).toBe(0);
|
||||
expect(request?.position.to).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('解析不完整的请求应该返回 null', () => {
|
||||
const code = `GET {
|
||||
host: "test.com"
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(0);
|
||||
|
||||
expect(request).toBeNull();
|
||||
});
|
||||
|
||||
it('解析无效位置应该返回 null', () => {
|
||||
const code = `GET "http://test.com" { }`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
const request = parser.parseRequestAt(1000); // 超出范围
|
||||
|
||||
expect(request).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('多个请求', () => {
|
||||
it('应该正确解析指定位置的请求', () => {
|
||||
const code = `GET "http://api.example.com/users" {
|
||||
host: "api.example.com"
|
||||
}
|
||||
|
||||
POST "http://api.example.com/users" {
|
||||
@json {
|
||||
name: "test"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(code);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
// 解析第一个请求
|
||||
const request1 = parser.parseRequestAt(0);
|
||||
expect(request1?.method).toBe('GET');
|
||||
|
||||
// 解析第二个请求(大概在 60+ 字符位置)
|
||||
const request2 = parser.parseRequestAt(70);
|
||||
expect(request2?.method).toBe('POST');
|
||||
expect(request2?.body).toEqual({ name: 'test' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
import { VariableResolver } from './variable-resolver';
|
||||
|
||||
/**
|
||||
* HTTP 请求模型
|
||||
*/
|
||||
export interface HttpRequest {
|
||||
/** 请求方法 */
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/** 请求 URL */
|
||||
url: string;
|
||||
|
||||
/** 请求头 */
|
||||
headers: Record<string, string>;
|
||||
|
||||
/** 请求体类型 */
|
||||
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text';
|
||||
|
||||
/** 请求体内容 */
|
||||
body?: any;
|
||||
|
||||
/** 原始文本位置信息(用于调试) */
|
||||
position: {
|
||||
from: number;
|
||||
to: number;
|
||||
line: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型常量
|
||||
*/
|
||||
const NODE_TYPES = {
|
||||
REQUEST_STATEMENT: 'RequestStatement',
|
||||
METHOD: 'Method',
|
||||
URL: 'Url',
|
||||
BLOCK: 'Block',
|
||||
PROPERTY: 'Property',
|
||||
PROPERTY_NAME: 'PropertyName',
|
||||
STRING_LITERAL: 'StringLiteral',
|
||||
NUMBER_LITERAL: 'NumberLiteral',
|
||||
IDENTIFIER: 'identifier',
|
||||
AT_RULE: 'AtRule',
|
||||
JSON_RULE: 'JsonRule',
|
||||
FORMDATA_RULE: 'FormDataRule',
|
||||
URLENCODED_RULE: 'UrlEncodedRule',
|
||||
TEXT_RULE: 'TextRule',
|
||||
JSON_KEYWORD: 'JsonKeyword',
|
||||
FORMDATA_KEYWORD: 'FormDataKeyword',
|
||||
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
||||
TEXT_KEYWORD: 'TextKeyword',
|
||||
JSON_BLOCK: 'JsonBlock',
|
||||
JSON_PROPERTY: 'JsonProperty',
|
||||
VARIABLE_REF: 'VariableRef',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTTP 请求解析器
|
||||
*/
|
||||
export class HttpRequestParser {
|
||||
private variableResolver: VariableResolver | null = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param state EditorState
|
||||
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
|
||||
*/
|
||||
constructor(
|
||||
private state: EditorState,
|
||||
private blockRange?: { from: number; to: number }
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建变量解析器(懒加载)
|
||||
*/
|
||||
private getVariableResolver(): VariableResolver {
|
||||
if (!this.variableResolver) {
|
||||
this.variableResolver = new VariableResolver(this.state, this.blockRange);
|
||||
}
|
||||
return this.variableResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定位置的 HTTP 请求
|
||||
* @param pos 光标位置或请求起始位置
|
||||
* @returns 解析后的 HTTP 请求对象,如果解析失败返回 null
|
||||
*/
|
||||
parseRequestAt(pos: number): HttpRequest | null {
|
||||
const tree = syntaxTree(this.state);
|
||||
|
||||
// 查找包含该位置的 RequestStatement 节点
|
||||
const requestNode = this.findRequestNode(tree, pos);
|
||||
if (!requestNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.parseRequest(requestNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找包含指定位置的 RequestStatement 节点
|
||||
*/
|
||||
private findRequestNode(tree: any, pos: number): SyntaxNode | null {
|
||||
let foundNode: SyntaxNode | null = null;
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
if (node.name === NODE_TYPES.REQUEST_STATEMENT) {
|
||||
if (node.from <= pos && pos <= node.to) {
|
||||
foundNode = node.node;
|
||||
return false; // 停止迭代
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return foundNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 RequestStatement 节点
|
||||
*/
|
||||
private parseRequest(node: SyntaxNode): HttpRequest | null {
|
||||
// 使用 Lezer API 直接获取子节点
|
||||
const methodNode = node.getChild(NODE_TYPES.METHOD);
|
||||
const urlNode = node.getChild(NODE_TYPES.URL);
|
||||
const blockNode = node.getChild(NODE_TYPES.BLOCK);
|
||||
|
||||
// 验证必需节点
|
||||
if (!methodNode || !urlNode || !blockNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const method = this.getNodeText(methodNode).toUpperCase();
|
||||
const url = this.parseUrl(urlNode);
|
||||
|
||||
// 验证 URL 非空
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
let bodyType: HttpRequest['bodyType'] = undefined;
|
||||
let body: any = undefined;
|
||||
|
||||
// 解析 Block
|
||||
this.parseBlock(blockNode, headers, (type, content) => {
|
||||
bodyType = type;
|
||||
body = content;
|
||||
});
|
||||
|
||||
const line = this.state.doc.lineAt(node.from);
|
||||
|
||||
return {
|
||||
method: method as HttpRequest['method'],
|
||||
url,
|
||||
headers,
|
||||
bodyType,
|
||||
body,
|
||||
position: {
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
line: line.number,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 URL 节点
|
||||
*/
|
||||
private parseUrl(node: SyntaxNode): string {
|
||||
const urlText = this.getNodeText(node);
|
||||
// 移除引号
|
||||
const url = urlText.replace(/^["']|["']$/g, '');
|
||||
// 替换变量
|
||||
return this.getVariableResolver().replaceVariables(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Block 节点(包含 headers 和 body)
|
||||
*/
|
||||
private parseBlock(
|
||||
node: SyntaxNode,
|
||||
headers: Record<string, string>,
|
||||
onBody: (type: HttpRequest['bodyType'], content: any) => void
|
||||
): void {
|
||||
// 遍历 Block 的子节点
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name === NODE_TYPES.PROPERTY) {
|
||||
// HTTP Header 属性
|
||||
const { name, value } = this.parseProperty(child);
|
||||
if (name && value !== null) {
|
||||
headers[name] = value;
|
||||
}
|
||||
} else if (child.name === NODE_TYPES.AT_RULE) {
|
||||
// AtRule 节点,直接获取第一个子节点(JsonRule, FormDataRule等)
|
||||
const ruleChild = child.firstChild;
|
||||
if (ruleChild) {
|
||||
const { type, content } = this.parseBodyRule(ruleChild);
|
||||
if (type) { // 只有有效的类型才处理
|
||||
onBody(type, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析请求体规则
|
||||
*/
|
||||
private parseBodyRule(node: SyntaxNode): { type: HttpRequest['bodyType']; content: any } {
|
||||
// 类型映射表
|
||||
const typeMap: Record<string, HttpRequest['bodyType']> = {
|
||||
[NODE_TYPES.JSON_RULE]: 'json',
|
||||
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
||||
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
||||
[NODE_TYPES.TEXT_RULE]: 'text',
|
||||
};
|
||||
|
||||
const type = typeMap[node.name];
|
||||
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
||||
|
||||
return {
|
||||
type,
|
||||
content: blockNode ? this.parseJsonBlock(blockNode) : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JsonBlock(用于 @json, @form, @urlencoded)
|
||||
*/
|
||||
private parseJsonBlock(node: SyntaxNode): any {
|
||||
const result: any = {};
|
||||
|
||||
// 遍历 JsonProperty
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name === NODE_TYPES.JSON_PROPERTY) {
|
||||
const { name, value } = this.parseJsonProperty(child);
|
||||
if (name && value !== null) {
|
||||
result[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JsonProperty
|
||||
*/
|
||||
private parseJsonProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||||
// 使用 API 获取属性名
|
||||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||||
if (!nameNode) {
|
||||
return { name: null, value: null };
|
||||
}
|
||||
|
||||
const name = this.getNodeText(nameNode);
|
||||
|
||||
// 尝试获取值节点(String, Number, JsonBlock, VariableRef)
|
||||
let value: any = null;
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name === NODE_TYPES.STRING_LITERAL ||
|
||||
child.name === NODE_TYPES.NUMBER_LITERAL ||
|
||||
child.name === NODE_TYPES.JSON_BLOCK ||
|
||||
child.name === NODE_TYPES.VARIABLE_REF ||
|
||||
child.name === NODE_TYPES.IDENTIFIER) {
|
||||
value = this.parseValue(child);
|
||||
return { name, value };
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:从文本中提取值(用于 true/false 等标识符)
|
||||
const fullText = this.getNodeText(node);
|
||||
const colonIndex = fullText.indexOf(':');
|
||||
if (colonIndex !== -1) {
|
||||
const valueText = fullText.substring(colonIndex + 1).trim().replace(/,$/, '').trim();
|
||||
value = this.parseValueFromText(valueText);
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本解析值
|
||||
*/
|
||||
private parseValueFromText(text: string): any {
|
||||
// 布尔值
|
||||
if (text === 'true') return true;
|
||||
if (text === 'false') return false;
|
||||
if (text === 'null') return null;
|
||||
|
||||
// 数字
|
||||
if (/^-?\d+(\.\d+)?$/.test(text)) {
|
||||
return parseFloat(text);
|
||||
}
|
||||
|
||||
// 字符串(带引号)
|
||||
if ((text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
// 其他标识符
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Property(HTTP Header)
|
||||
*/
|
||||
private parseProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||||
if (!nameNode) {
|
||||
return { name: null, value: null };
|
||||
}
|
||||
|
||||
const name = this.getNodeText(nameNode);
|
||||
let value: any = null;
|
||||
|
||||
// 查找值节点(跳过冒号和逗号)
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.name !== NODE_TYPES.PROPERTY_NAME &&
|
||||
child.name !== ':' &&
|
||||
child.name !== ',') {
|
||||
value = this.parseValue(child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP Header 的值必须转换为字符串
|
||||
if (value !== null && value !== undefined) {
|
||||
value = String(value);
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析值节点(字符串、数字、标识符、嵌套块、变量引用)
|
||||
*/
|
||||
private parseValue(node: SyntaxNode): any {
|
||||
if (node.name === NODE_TYPES.STRING_LITERAL) {
|
||||
const text = this.getNodeText(node);
|
||||
// 移除引号
|
||||
const value = text.replace(/^["']|["']$/g, '');
|
||||
// 替换字符串中的变量
|
||||
return this.getVariableResolver().replaceVariables(value);
|
||||
} else if (node.name === NODE_TYPES.NUMBER_LITERAL) {
|
||||
const text = this.getNodeText(node);
|
||||
return parseFloat(text);
|
||||
} else if (node.name === NODE_TYPES.VARIABLE_REF) {
|
||||
// 处理变量引用节点
|
||||
const text = this.getNodeText(node);
|
||||
const resolver = this.getVariableResolver();
|
||||
const ref = resolver.parseVariableRef(text);
|
||||
if (ref) {
|
||||
return resolver.resolveVariable(ref.name, ref.defaultValue);
|
||||
}
|
||||
return text;
|
||||
} else if (node.name === NODE_TYPES.IDENTIFIER) {
|
||||
const text = this.getNodeText(node);
|
||||
// 处理布尔值
|
||||
if (text === 'true') return true;
|
||||
if (text === 'false') return false;
|
||||
// 处理 null
|
||||
if (text === 'null') return null;
|
||||
// 其他标识符作为字符串
|
||||
return text;
|
||||
} else if (node.name === NODE_TYPES.JSON_BLOCK) {
|
||||
// 嵌套对象
|
||||
return this.parseJsonBlock(node);
|
||||
} else {
|
||||
// 未知类型,返回原始文本
|
||||
return this.getNodeText(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的文本内容
|
||||
*/
|
||||
private getNodeText(node: SyntaxNode): string {
|
||||
return this.state.doc.sliceString(node.from, node.to);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷函数:解析指定位置的 HTTP 请求
|
||||
* @param state EditorState
|
||||
* @param pos 光标位置
|
||||
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
|
||||
*/
|
||||
export function parseHttpRequest(
|
||||
state: EditorState,
|
||||
pos: number,
|
||||
blockRange?: { from: number; to: number }
|
||||
): HttpRequest | null {
|
||||
const parser = new HttpRequestParser(state, blockRange);
|
||||
return parser.parseRequestAt(pos);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState, ChangeSpec } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
import { getNoteBlockFromPos } from '../../codeblock/state';
|
||||
|
||||
/**
|
||||
* 响应数据模型
|
||||
*/
|
||||
export interface HttpResponse {
|
||||
/** 状态码和状态文本,如"200 OK" */
|
||||
status: string;
|
||||
|
||||
/** 响应时间(毫秒) */
|
||||
time: number;
|
||||
|
||||
/** 请求大小 */
|
||||
requestSize?: string;
|
||||
|
||||
/** 响应体 */
|
||||
body: any;
|
||||
|
||||
/** 响应头 */
|
||||
headers?: { [_: string]: string[] };
|
||||
|
||||
/** 时间戳 */
|
||||
timestamp?: Date;
|
||||
|
||||
/** 错误信息 */
|
||||
error?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 响应插入器
|
||||
*/
|
||||
export class HttpResponseInserter {
|
||||
constructor(private view: EditorView) {}
|
||||
|
||||
/**
|
||||
* 插入HTTP响应
|
||||
* @param requestPos 请求的起始位置
|
||||
* @param response 响应数据
|
||||
*/
|
||||
insertResponse(requestPos: number, response: HttpResponse): void {
|
||||
const state = this.view.state;
|
||||
|
||||
// 获取当前代码块
|
||||
const blockInfo = getNoteBlockFromPos(state, requestPos);
|
||||
if (!blockInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockFrom = blockInfo.range.from;
|
||||
const blockTo = blockInfo.range.to;
|
||||
|
||||
// 查找请求节点和旧响应
|
||||
const context = this.findRequestAndResponse(state, requestPos, blockFrom, blockTo);
|
||||
|
||||
if (!context.requestNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成新响应文本
|
||||
const responseText = this.formatResponse(response);
|
||||
|
||||
// 确定插入位置
|
||||
let insertFrom: number;
|
||||
let insertTo: number;
|
||||
|
||||
if (context.oldResponseNode) {
|
||||
// 替换旧响应
|
||||
insertFrom = context.oldResponseNode.from;
|
||||
insertTo = context.oldResponseNode.to;
|
||||
} else {
|
||||
// 在请求后插入新响应
|
||||
// 使用 requestNode.to - 1 定位到请求的最后一个字符所在行
|
||||
const lastCharPos = Math.max(context.requestNode.from, context.requestNode.to - 1);
|
||||
const requestEndLine = state.doc.lineAt(lastCharPos);
|
||||
insertFrom = requestEndLine.to;
|
||||
insertTo = insertFrom;
|
||||
}
|
||||
|
||||
const changes: ChangeSpec = {
|
||||
from: insertFrom,
|
||||
to: insertTo,
|
||||
insert: context.oldResponseNode ? responseText : `\n\n${responseText}`
|
||||
};
|
||||
|
||||
this.view.dispatch({
|
||||
changes,
|
||||
userEvent: 'http.response.insert',
|
||||
selection: { anchor: requestPos },
|
||||
scrollIntoView: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找请求节点和旧响应节点(使用 tree.iterate)
|
||||
*/
|
||||
private findRequestAndResponse(
|
||||
state: EditorState,
|
||||
requestPos: number,
|
||||
blockFrom: number,
|
||||
blockTo: number
|
||||
): {
|
||||
requestNode: SyntaxNode | null;
|
||||
oldResponseNode: { node: SyntaxNode; from: number; to: number } | null;
|
||||
} {
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
let requestNode: SyntaxNode | null = null;
|
||||
let requestNodeTo = -1;
|
||||
let oldResponseNode: { node: SyntaxNode; from: number; to: number } | null = null;
|
||||
let nextRequestFrom = -1;
|
||||
|
||||
// 遍历查找:请求节点、旧响应、下一个请求
|
||||
tree.iterate({
|
||||
from: blockFrom,
|
||||
to: blockTo,
|
||||
enter: (node) => {
|
||||
// 1. 找到包含 requestPos 的 RequestStatement
|
||||
if (node.name === 'RequestStatement' &&
|
||||
node.from <= requestPos &&
|
||||
node.to >= requestPos) {
|
||||
requestNode = node.node;
|
||||
requestNodeTo = node.to;
|
||||
}
|
||||
|
||||
// 2. 找到请求后的第一个 ResponseDeclaration
|
||||
if (requestNode && !oldResponseNode &&
|
||||
node.name === 'ResponseDeclaration' &&
|
||||
node.from >= requestNodeTo) {
|
||||
oldResponseNode = {
|
||||
node: node.node,
|
||||
from: node.from,
|
||||
to: node.to
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 记录下一个请求的起始位置(用于确定响应范围)
|
||||
if (requestNode && nextRequestFrom === -1 &&
|
||||
node.name === 'RequestStatement' &&
|
||||
node.from > requestNodeTo) {
|
||||
nextRequestFrom = node.from;
|
||||
}
|
||||
|
||||
// 4. 早期退出优化:如果已找到请求节点,且满足以下任一条件,则停止遍历
|
||||
// - 找到了旧响应节点
|
||||
// - 找到了下一个请求(说明当前请求没有响应)
|
||||
if (requestNode !== null) {
|
||||
if (oldResponseNode !== null || nextRequestFrom !== -1) {
|
||||
return false; // 停止遍历
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果找到了下一个请求,且旧响应超出范围,则清除旧响应
|
||||
if (oldResponseNode && nextRequestFrom !== -1) {
|
||||
// TypeScript 类型收窄问题,使用非空断言
|
||||
if ((oldResponseNode as { from: number; to: number; node: SyntaxNode }).from >= nextRequestFrom) {
|
||||
oldResponseNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { requestNode, oldResponseNode };
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化响应数据(新格式:@response)
|
||||
* 格式:@response <status> <time>ms <timestamp> { <json> }
|
||||
* 状态格式:200 或 200-OK(支持完整状态文本)
|
||||
* 错误:@response error 0ms <timestamp> { "error": "..." }
|
||||
*/
|
||||
private formatResponse(response: HttpResponse): string {
|
||||
// 时间戳格式:ISO 8601(YYYY-MM-DDTHH:MM:SS)
|
||||
const timestamp = response.timestamp || new Date();
|
||||
const timestampStr = this.formatTimestampISO(timestamp);
|
||||
|
||||
// 格式化响应体为 JSON
|
||||
let bodyJson: string;
|
||||
if (response.error) {
|
||||
// 错误响应
|
||||
bodyJson = JSON.stringify({ error: String(response.error) }, null, 2);
|
||||
} else if (typeof response.body === 'string') {
|
||||
// 尝试解析 JSON 字符串
|
||||
try {
|
||||
const parsed = JSON.parse(response.body);
|
||||
bodyJson = JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
// 如果不是 JSON,包装为对象
|
||||
bodyJson = JSON.stringify({ data: response.body }, null, 2);
|
||||
}
|
||||
} else if (response.body === null || response.body === undefined) {
|
||||
// 空响应
|
||||
bodyJson = '{}';
|
||||
} else {
|
||||
// 对象或数组
|
||||
bodyJson = JSON.stringify(response.body, null, 2);
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
if (response.error) {
|
||||
// 错误格式:@response error 0ms <timestamp> { ... }
|
||||
return `@response error 0ms ${timestampStr} ${bodyJson}`;
|
||||
} else {
|
||||
// 成功格式:@response <status> <time>ms <timestamp> { ... }
|
||||
// 支持完整状态:200-OK 或 200
|
||||
const statusDisplay = this.formatStatus(response.status);
|
||||
return `@response ${statusDisplay} ${response.time}ms ${timestampStr} ${bodyJson}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化状态码显示
|
||||
* 输入:"200 OK" 或 "404 Not Found" 或 "200"
|
||||
* 输出:"200-OK" 或 "404-Not-Found" 或 "200"
|
||||
*/
|
||||
private formatStatus(status: string): string {
|
||||
// 提取状态码和状态文本
|
||||
const parts = status.trim().split(/\s+/);
|
||||
|
||||
if (parts.length === 1) {
|
||||
// 只有状态码:200
|
||||
return parts[0];
|
||||
} else {
|
||||
// 有状态码和文本:200 OK -> 200-OK
|
||||
const code = parts[0];
|
||||
const text = parts.slice(1).join('-');
|
||||
return `${code}-${text}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳为 ISO 8601 格式(YYYY-MM-DDTHH:MM:SS)
|
||||
*/
|
||||
private formatTimestampISO(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷函数:插入响应数据
|
||||
*/
|
||||
export function insertHttpResponse(view: EditorView, requestPos: number, response: HttpResponse): void {
|
||||
const inserter = new HttpResponseInserter(view);
|
||||
inserter.insertResponse(requestPos, response);
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { httpLanguage } from '../language/http-language';
|
||||
import { VariableResolver } from './variable-resolver';
|
||||
import { HttpRequestParser } from './request-parser';
|
||||
|
||||
/**
|
||||
* 创建测试用的 EditorState
|
||||
*/
|
||||
function createTestState(content: string): EditorState {
|
||||
return EditorState.create({
|
||||
doc: content,
|
||||
extensions: [httpLanguage],
|
||||
});
|
||||
}
|
||||
|
||||
describe('VariableResolver 测试', () => {
|
||||
|
||||
it('✅ 解析 @var 声明(无块范围)', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const resolver = new VariableResolver(state);
|
||||
const variables = resolver.getAllVariables();
|
||||
|
||||
expect(variables.get('baseUrl')).toBe('https://api.example.com');
|
||||
expect(variables.get('version')).toBe('v1');
|
||||
expect(variables.get('timeout')).toBe(30000);
|
||||
});
|
||||
|
||||
it('✅ 解析 @var 声明(指定块范围)', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
// 指定块范围(整个内容)
|
||||
const blockRange = { from: 0, to: content.length };
|
||||
const resolver = new VariableResolver(state, blockRange);
|
||||
const variables = resolver.getAllVariables();
|
||||
|
||||
expect(variables.get('baseUrl')).toBe('https://api.example.com');
|
||||
expect(variables.get('version')).toBe('v1');
|
||||
expect(variables.get('timeout')).toBe(30000);
|
||||
});
|
||||
|
||||
it('✅ 解析变量引用', () => {
|
||||
const resolver = new VariableResolver(createTestState(''));
|
||||
|
||||
const ref1 = resolver.parseVariableRef('{{baseUrl}}');
|
||||
expect(ref1).toEqual({
|
||||
name: 'baseUrl',
|
||||
defaultValue: undefined,
|
||||
raw: '{{baseUrl}}',
|
||||
});
|
||||
|
||||
const ref2 = resolver.parseVariableRef('{{token:default-token}}');
|
||||
expect(ref2).toEqual({
|
||||
name: 'token',
|
||||
defaultValue: 'default-token',
|
||||
raw: '{{token:default-token}}',
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 替换字符串中的变量', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const resolver = new VariableResolver(state);
|
||||
|
||||
const result = resolver.replaceVariables('{{baseUrl}}/{{version}}/users');
|
||||
expect(result).toBe('https://api.example.com/v1/users');
|
||||
});
|
||||
|
||||
it('✅ 使用默认值', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const resolver = new VariableResolver(state);
|
||||
|
||||
const result = resolver.replaceVariables('{{baseUrl}}/{{version:v1}}/users');
|
||||
expect(result).toBe('https://api.example.com/v1/users');
|
||||
});
|
||||
|
||||
it('✅ 变量未定义且无默认值,保持原样', () => {
|
||||
const state = createTestState('');
|
||||
const resolver = new VariableResolver(state);
|
||||
|
||||
const result = resolver.replaceVariables('{{undefined}}/users');
|
||||
expect(result).toBe('{{undefined}}/users');
|
||||
});
|
||||
|
||||
it('✅ 嵌套对象变量 - 路径访问', () => {
|
||||
const content = `@var {
|
||||
config: {
|
||||
nested: {
|
||||
deep: {
|
||||
value: 123
|
||||
}
|
||||
}
|
||||
},
|
||||
simple: "test"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const resolver = new VariableResolver(state);
|
||||
|
||||
// 测试简单变量
|
||||
expect(resolver.resolveVariable('simple')).toBe('test');
|
||||
|
||||
// 测试嵌套路径访问
|
||||
expect(resolver.resolveVariable('config.nested.deep.value')).toBe(123);
|
||||
|
||||
// 测试部分路径访问
|
||||
const nestedObj = resolver.resolveVariable('config.nested');
|
||||
expect(nestedObj).toEqual({ deep: { value: 123 } });
|
||||
|
||||
// 测试不存在的路径
|
||||
expect(resolver.resolveVariable('config.notExist', 'default')).toBe('default');
|
||||
|
||||
// 测试字符串替换
|
||||
const result = resolver.replaceVariables('Value is {{config.nested.deep.value}}');
|
||||
expect(result).toBe('Value is 123');
|
||||
});
|
||||
|
||||
it('✅ 嵌套对象变量 - 整个对象引用', () => {
|
||||
const content = `@var {
|
||||
config: {
|
||||
host: "example.com",
|
||||
port: 8080
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const resolver = new VariableResolver(state);
|
||||
|
||||
// 引用整个对象
|
||||
const configObj = resolver.resolveVariable('config');
|
||||
expect(configObj).toEqual({
|
||||
host: 'example.com',
|
||||
port: 8080
|
||||
});
|
||||
|
||||
// 字符串中引用对象会转换为 JSON
|
||||
const result = resolver.replaceVariables('Config: {{config}}');
|
||||
expect(result).toBe('Config: {"host":"example.com","port":8080}');
|
||||
});
|
||||
|
||||
it('✅ 块范围限制 - 只解析块内的变量', () => {
|
||||
// 模拟多块文档
|
||||
const content = `@var {
|
||||
block1Var: "value1"
|
||||
}
|
||||
|
||||
GET "http://example.com" {}
|
||||
|
||||
--- 块分隔 ---
|
||||
|
||||
@var {
|
||||
block2Var: "value2"
|
||||
}
|
||||
|
||||
POST "http://example.com" {}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
|
||||
// 第一个块:只包含 block1Var
|
||||
const block1Range = { from: 0, to: 60 };
|
||||
const resolver1 = new VariableResolver(state, block1Range);
|
||||
expect(resolver1.getAllVariables().get('block1Var')).toBe('value1');
|
||||
expect(resolver1.getAllVariables().get('block2Var')).toBeUndefined();
|
||||
|
||||
// 第二个块:只包含 block2Var
|
||||
const block2Start = content.indexOf('@var {', 60);
|
||||
const block2Range = { from: block2Start, to: content.length };
|
||||
const resolver2 = new VariableResolver(state, block2Range);
|
||||
expect(resolver2.getAllVariables().get('block1Var')).toBeUndefined();
|
||||
expect(resolver2.getAllVariables().get('block2Var')).toBe('value2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HttpRequestParser 集成变量测试', () => {
|
||||
|
||||
it('✅ 解析带变量的 URL', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1"
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/{{version}}/users" {
|
||||
host: "example.com"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
// 查找 GET 请求的位置
|
||||
const getPos = content.indexOf('GET');
|
||||
const request = parser.parseRequestAt(getPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.url).toBe('https://api.example.com/v1/users');
|
||||
});
|
||||
|
||||
it('✅ 解析 HTTP 头中的变量', () => {
|
||||
const content = `@var {
|
||||
token: "Bearer abc123",
|
||||
timeout: 5000
|
||||
}
|
||||
|
||||
GET "http://example.com" {
|
||||
authorization: {{token}},
|
||||
timeout: {{timeout}}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
const getPos = content.indexOf('GET');
|
||||
const request = parser.parseRequestAt(getPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.headers.authorization).toBe('Bearer abc123');
|
||||
// HTTP Header 值会被转换为字符串
|
||||
expect(request?.headers.timeout).toBe('5000');
|
||||
});
|
||||
|
||||
it('✅ 解析 JSON 请求体中的变量', () => {
|
||||
const content = `@var {
|
||||
userId: "12345",
|
||||
userName: "张三"
|
||||
}
|
||||
|
||||
POST "http://api.example.com/users" {
|
||||
@json {
|
||||
id: {{userId}},
|
||||
name: {{userName}}
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
const postPos = content.indexOf('POST');
|
||||
const request = parser.parseRequestAt(postPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.body).toEqual({
|
||||
id: '12345',
|
||||
name: '张三',
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 字符串中嵌入变量', () => {
|
||||
const content = `@var {
|
||||
userName: "张三"
|
||||
}
|
||||
|
||||
POST "http://api.example.com/users" {
|
||||
@json {
|
||||
name: {{userName}},
|
||||
email: "{{userName}}@example.com"
|
||||
}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
const postPos = content.indexOf('POST');
|
||||
const request = parser.parseRequestAt(postPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.body).toEqual({
|
||||
name: '张三',
|
||||
email: '张三@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('✅ 使用变量默认值', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com"
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/users" {
|
||||
timeout: {{timeout:30000}},
|
||||
authorization: "Bearer {{token:default-token}}"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
const getPos = content.indexOf('GET');
|
||||
const request = parser.parseRequestAt(getPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
// HTTP Header 值会被转换为字符串
|
||||
expect(request?.headers.timeout).toBe('30000');
|
||||
expect(request?.headers.authorization).toBe('Bearer default-token');
|
||||
});
|
||||
|
||||
it('✅ 嵌套变量在 HTTP 请求中使用', () => {
|
||||
const content = `@var {
|
||||
api: {
|
||||
base: "https://api.example.com",
|
||||
version: "v1",
|
||||
endpoints: {
|
||||
users: "/users",
|
||||
posts: "/posts"
|
||||
}
|
||||
},
|
||||
config: {
|
||||
timeout: 5000
|
||||
}
|
||||
}
|
||||
|
||||
GET "{{api.base}}/{{api.version}}{{api.endpoints.users}}" {
|
||||
timeout: {{config.timeout}}
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
const getPos = content.indexOf('GET');
|
||||
const request = parser.parseRequestAt(getPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.url).toBe('https://api.example.com/v1/users');
|
||||
expect(request?.headers.timeout).toBe('5000');
|
||||
});
|
||||
|
||||
it('✅ 完整示例 - 用户提供的场景', () => {
|
||||
const content = `@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
version: "v1",
|
||||
timeout: 30000
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/{{version}}/users" {
|
||||
timeout: {{timeout}},
|
||||
authorization: "Bearer {{token:default-token}}"
|
||||
}`;
|
||||
|
||||
const state = createTestState(content);
|
||||
const parser = new HttpRequestParser(state);
|
||||
|
||||
const getPos = content.indexOf('GET');
|
||||
const request = parser.parseRequestAt(getPos);
|
||||
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.url).toBe('https://api.example.com/v1/users');
|
||||
// HTTP Header 值会被转换为字符串(Go 后端要求)
|
||||
expect(request?.headers.timeout).toBe('30000');
|
||||
expect(request?.headers.authorization).toBe('Bearer default-token');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
|
||||
/**
|
||||
* 变量引用模型
|
||||
*/
|
||||
export interface VariableReference {
|
||||
/** 变量名 */
|
||||
name: string;
|
||||
/** 默认值(可选) */
|
||||
defaultValue?: string;
|
||||
/** 原始文本 */
|
||||
raw: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型常量
|
||||
*/
|
||||
const NODE_TYPES = {
|
||||
VAR_DECLARATION: 'VarDeclaration',
|
||||
VAR_KEYWORD: 'VarKeyword',
|
||||
JSON_BLOCK: 'JsonBlock',
|
||||
JSON_PROPERTY: 'JsonProperty',
|
||||
PROPERTY_NAME: 'PropertyName',
|
||||
STRING_LITERAL: 'StringLiteral',
|
||||
NUMBER_LITERAL: 'NumberLiteral',
|
||||
IDENTIFIER: 'identifier',
|
||||
VARIABLE_REF: 'VariableRef',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 变量解析器
|
||||
* 负责解析 @var 块和变量引用(块级作用域)
|
||||
*/
|
||||
export class VariableResolver {
|
||||
/** 变量存储 */
|
||||
private variables: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param state EditorState
|
||||
* @param blockRange 块的范围(可选),如果提供则只解析该范围内的变量
|
||||
*/
|
||||
constructor(
|
||||
private state: EditorState,
|
||||
private blockRange?: { from: number; to: number }
|
||||
) {
|
||||
// 初始化时解析变量定义
|
||||
this.parseBlockVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析块范围内的 @var 声明
|
||||
* 如果提供了 blockRange,只解析该范围内的变量
|
||||
* 否则解析整个文档(向后兼容)
|
||||
*/
|
||||
private parseBlockVariables(): void {
|
||||
const tree = syntaxTree(this.state);
|
||||
|
||||
tree.iterate({
|
||||
enter: (node: any) => {
|
||||
// 如果指定了块范围,检查节点是否在范围内
|
||||
if (this.blockRange) {
|
||||
// 节点完全在块范围外,跳过
|
||||
if (node.to < this.blockRange.from || node.from > this.blockRange.to) {
|
||||
return false; // 停止遍历此分支
|
||||
}
|
||||
}
|
||||
|
||||
if (node.name === NODE_TYPES.VAR_DECLARATION) {
|
||||
this.parseVarDeclaration(node.node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个 @var 声明
|
||||
*/
|
||||
private parseVarDeclaration(node: SyntaxNode): void {
|
||||
// 获取 JsonBlock 节点
|
||||
const jsonBlockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
||||
if (!jsonBlockNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 JsonBlock 中的所有属性
|
||||
const variables = this.parseJsonBlock(jsonBlockNode);
|
||||
|
||||
// 存储变量
|
||||
for (const [name, value] of Object.entries(variables)) {
|
||||
this.variables.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JsonBlock
|
||||
*/
|
||||
private parseJsonBlock(node: SyntaxNode): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
// 遍历所有 JsonProperty 子节点
|
||||
let child = node.firstChild;
|
||||
while (child) {
|
||||
if (child.name === NODE_TYPES.JSON_PROPERTY) {
|
||||
const { name, value } = this.parseJsonProperty(child);
|
||||
if (name) {
|
||||
result[name] = value;
|
||||
}
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JsonProperty
|
||||
*/
|
||||
private parseJsonProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||||
// 获取属性名节点
|
||||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||||
if (!nameNode) {
|
||||
return { name: null, value: null };
|
||||
}
|
||||
|
||||
const name = this.getNodeText(nameNode);
|
||||
|
||||
// 获取值节点
|
||||
let valueNode = nameNode.nextSibling;
|
||||
|
||||
// 跳过冒号
|
||||
while (valueNode && valueNode.name === ':') {
|
||||
valueNode = valueNode.nextSibling;
|
||||
}
|
||||
|
||||
if (!valueNode) {
|
||||
return { name, value: null };
|
||||
}
|
||||
|
||||
const value = this.parseValue(valueNode);
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析值节点
|
||||
*/
|
||||
private parseValue(node: SyntaxNode): any {
|
||||
switch (node.name) {
|
||||
case NODE_TYPES.STRING_LITERAL: {
|
||||
const text = this.getNodeText(node);
|
||||
// 移除引号
|
||||
return text.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
|
||||
case NODE_TYPES.NUMBER_LITERAL: {
|
||||
const text = this.getNodeText(node);
|
||||
return parseFloat(text);
|
||||
}
|
||||
|
||||
case NODE_TYPES.IDENTIFIER: {
|
||||
const text = this.getNodeText(node);
|
||||
// 处理布尔值和 null
|
||||
if (text === 'true') return true;
|
||||
if (text === 'false') return false;
|
||||
if (text === 'null') return null;
|
||||
return text;
|
||||
}
|
||||
|
||||
case NODE_TYPES.JSON_BLOCK: {
|
||||
// 嵌套对象
|
||||
return this.parseJsonBlock(node);
|
||||
}
|
||||
|
||||
default:
|
||||
// 其他类型返回原始文本
|
||||
return this.getNodeText(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析变量引用
|
||||
* 从 {{variableName}} 或 {{variableName:default}} 或 {{obj.nested.path}} 中提取信息
|
||||
*/
|
||||
public parseVariableRef(text: string): VariableReference | null {
|
||||
// 正则匹配:
|
||||
// - {{variableName}}
|
||||
// - {{variableName:default}}
|
||||
// - {{obj.nested.path}}
|
||||
// - {{obj.nested.path:default}}
|
||||
const match = text.match(/^\{\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)(:(.*))?\}\}$/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: match[1],
|
||||
defaultValue: match[3],
|
||||
raw: text,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析字符串中的所有变量引用
|
||||
* 例如: "{{baseUrl}}/{{version}}/users" 或 "{{config.nested.value}}"
|
||||
*/
|
||||
public parseVariableRefsInString(text: string): VariableReference[] {
|
||||
const refs: VariableReference[] = [];
|
||||
// 支持路径访问:允许变量名中包含 "."
|
||||
const regex = /\{\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)(:[^}]+)?\}\}/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
refs.push({
|
||||
name: match[1],
|
||||
defaultValue: match[2] ? match[2].substring(1) : undefined, // 去掉冒号
|
||||
raw: match[0],
|
||||
});
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析变量值(支持路径访问)
|
||||
* @param name 变量名,支持路径访问如 "config.nested.deep.value"
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析后的值
|
||||
*/
|
||||
public resolveVariable(name: string, defaultValue?: string): any {
|
||||
// 检查是否包含路径访问符 "."
|
||||
if (name.includes('.')) {
|
||||
return this.resolveNestedVariable(name, defaultValue);
|
||||
}
|
||||
|
||||
// 简单变量名直接查找
|
||||
if (this.variables.has(name)) {
|
||||
return this.variables.get(name);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析嵌套变量(路径访问)
|
||||
* @param path 变量路径,如 "config.nested.deep.value"
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析后的值
|
||||
*/
|
||||
private resolveNestedVariable(path: string, defaultValue?: string): any {
|
||||
const parts = path.split('.');
|
||||
const rootName = parts[0];
|
||||
|
||||
// 获取根变量
|
||||
if (!this.variables.has(rootName)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
let current: any = this.variables.get(rootName);
|
||||
|
||||
// 遍历路径访问嵌套属性
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const key = parts[i];
|
||||
|
||||
// 检查当前值是否是对象
|
||||
if (current === null || current === undefined || typeof current !== 'object') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 访问嵌套属性
|
||||
if (!(key in current)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换字符串中的所有变量引用
|
||||
* 支持:
|
||||
* - 简单变量: "{{baseUrl}}/{{version}}/users"
|
||||
* - 路径访问: "{{config.nested.deep.value}}"
|
||||
* - 默认值: "{{timeout:30000}}"
|
||||
*/
|
||||
public replaceVariables(text: string): string {
|
||||
return text.replace(
|
||||
/\{\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)(:[^}]+)?\}\}/g,
|
||||
(match, name, defaultPart) => {
|
||||
const defaultValue = defaultPart ? defaultPart.substring(1) : undefined;
|
||||
const value = this.resolveVariable(name, defaultValue);
|
||||
|
||||
// 如果值是对象或数组,转换为 JSON 字符串
|
||||
if (value !== undefined && value !== null) {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// 如果没有找到变量,保持原样
|
||||
return match;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已定义的变量
|
||||
*/
|
||||
public getAllVariables(): Map<string, any> {
|
||||
return new Map(this.variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的文本内容
|
||||
*/
|
||||
private getNodeText(node: SyntaxNode): string {
|
||||
return this.state.doc.sliceString(node.from, node.to);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { EditorView, GutterMarker, gutter } from '@codemirror/view';
|
||||
import { StateField } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { blockState } from '../../codeblock/state';
|
||||
import { parseHttpRequest, type HttpRequest } from '../parser/request-parser';
|
||||
import { insertHttpResponse, type HttpResponse } from '../parser/response-inserter';
|
||||
import { createDebounce } from '@/common/utils/debounce';
|
||||
import { ExecuteRequest } from '@/../bindings/voidraft/internal/services/httpclientservice';
|
||||
|
||||
/**
|
||||
* 语法树节点类型常量
|
||||
*/
|
||||
const NODE_TYPES = {
|
||||
REQUEST_STATEMENT: 'RequestStatement',
|
||||
METHOD: 'Method',
|
||||
URL: 'Url',
|
||||
BLOCK: 'Block',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTTP 请求缓存信息
|
||||
*/
|
||||
interface CachedHttpRequest {
|
||||
lineNumber: number; // 行号(用于快速查找)
|
||||
position: number; // 字符位置(用于解析)
|
||||
request: HttpRequest; // 完整的解析结果
|
||||
}
|
||||
|
||||
/**
|
||||
* 预解析所有 HTTP 块中的请求
|
||||
* 只在文档改变时调用,结果缓存在 StateField 中
|
||||
*
|
||||
* 优化:一次遍历完成验证和解析,避免重复工作
|
||||
*/
|
||||
function parseHttpRequests(state: any): Map<number, CachedHttpRequest> {
|
||||
const requestsMap = new Map<number, CachedHttpRequest>();
|
||||
const blocks = state.field(blockState, false);
|
||||
|
||||
if (!blocks) return requestsMap;
|
||||
|
||||
const tree = syntaxTree(state);
|
||||
|
||||
// 只遍历 HTTP 块
|
||||
for (const block of blocks) {
|
||||
if (block.language.name !== 'http') continue;
|
||||
|
||||
// 在块范围内查找所有 RequestStatement
|
||||
tree.iterate({
|
||||
from: block.content.from,
|
||||
to: block.content.to,
|
||||
enter: (node) => {
|
||||
if (node.name === NODE_TYPES.REQUEST_STATEMENT) {
|
||||
// 检查是否包含错误节点
|
||||
let hasError = false;
|
||||
node.node.cursor().iterate((nodeRef) => {
|
||||
if (nodeRef.name === '⚠') {
|
||||
hasError = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) return;
|
||||
|
||||
// 直接解析请求
|
||||
const request = parseHttpRequest(state, node.from,{from: block.content.from, to: block.content.to});
|
||||
|
||||
if (request) {
|
||||
const line = state.doc.lineAt(request.position.from);
|
||||
requestsMap.set(line.number, {
|
||||
lineNumber: line.number,
|
||||
position: request.position.from,
|
||||
request: request,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return requestsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* StateField:缓存所有 HTTP 请求的完整解析结果
|
||||
* 只在文档改变时重新解析
|
||||
*/
|
||||
const httpRequestsField = StateField.define<Map<number, CachedHttpRequest>>({
|
||||
create(state) {
|
||||
return parseHttpRequests(state);
|
||||
},
|
||||
|
||||
update(requests, transaction) {
|
||||
// 只有文档改变或缓存为空时才重新解析
|
||||
if (transaction.docChanged || requests.size === 0) {
|
||||
return parseHttpRequests(transaction.state);
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 运行按钮 Marker ====================
|
||||
|
||||
/**
|
||||
* 运行按钮 Gutter Marker
|
||||
*/
|
||||
class RunButtonMarker extends GutterMarker {
|
||||
private isLoading = false;
|
||||
private buttonElement: HTMLButtonElement | null = null;
|
||||
private readonly debouncedExecute: ((view: EditorView) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly cachedRequest: HttpRequest
|
||||
) {
|
||||
super();
|
||||
|
||||
// 创建防抖执行函数
|
||||
const { debouncedFn } = createDebounce(
|
||||
(view: EditorView) => this.executeRequestInternal(view),
|
||||
{ delay: 500 }
|
||||
);
|
||||
this.debouncedExecute = debouncedFn;
|
||||
}
|
||||
|
||||
toDOM(view: EditorView) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'cm-http-run-button';
|
||||
button.innerHTML = '▶';
|
||||
button.title = 'Run HTTP Request';
|
||||
button.setAttribute('aria-label', 'Run HTTP Request');
|
||||
|
||||
this.buttonElement = button;
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.executeRequest(view);
|
||||
};
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private executeRequest(view: EditorView) {
|
||||
if (this.debouncedExecute) {
|
||||
this.debouncedExecute(view);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeRequestInternal(view: EditorView) {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.setLoadingState(true);
|
||||
try {
|
||||
const response = await ExecuteRequest(this.cachedRequest);
|
||||
if (!response) {
|
||||
throw new Error('No response');
|
||||
}
|
||||
|
||||
// 转换后端响应为前端格式
|
||||
const httpResponse: HttpResponse = {
|
||||
status: response.status, // 后端已返回完整状态如"200 OK"
|
||||
time: response.time,
|
||||
requestSize: response.requestSize,
|
||||
body: response.body,
|
||||
headers: response.headers,
|
||||
timestamp: response.timestamp ? new Date(response.timestamp) : new Date(),
|
||||
error: response.error
|
||||
};
|
||||
|
||||
// 插入响应数据
|
||||
insertHttpResponse(view, this.cachedRequest.position.from, httpResponse);
|
||||
} catch (error) {
|
||||
// 创建错误响应
|
||||
const errorResponse: HttpResponse = {
|
||||
status: 'Request Failed',
|
||||
time: 0,
|
||||
body: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
timestamp: new Date(),
|
||||
error: error
|
||||
};
|
||||
|
||||
// 插入错误响应
|
||||
insertHttpResponse(view, this.cachedRequest.position.from, errorResponse);
|
||||
} finally {
|
||||
this.setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
private setLoadingState(loading: boolean) {
|
||||
this.isLoading = loading;
|
||||
|
||||
if (this.buttonElement) {
|
||||
if (loading) {
|
||||
this.buttonElement.className = 'cm-http-run-button cm-http-run-button-loading';
|
||||
this.buttonElement.title = 'Request in progress...';
|
||||
this.buttonElement.disabled = true;
|
||||
} else {
|
||||
this.buttonElement.className = 'cm-http-run-button';
|
||||
this.buttonElement.title = 'Run HTTP Request';
|
||||
this.buttonElement.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建运行按钮 Gutter
|
||||
*/
|
||||
export const httpRunButtonGutter = gutter({
|
||||
class: 'cm-http-gutter',
|
||||
|
||||
lineMarker(view, line) {
|
||||
// O(1) 查找:从缓存中获取请求
|
||||
const requestsMap = view.state.field(httpRequestsField, false);
|
||||
if (!requestsMap) return null;
|
||||
|
||||
const lineNumber = view.state.doc.lineAt(line.from).number;
|
||||
const cached = requestsMap.get(lineNumber);
|
||||
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建并返回运行按钮,传递缓存的解析结果
|
||||
return new RunButtonMarker(cached.request);
|
||||
},
|
||||
});
|
||||
|
||||
export const httpRunButtonTheme = EditorView.baseTheme({
|
||||
// 运行按钮样式
|
||||
'.cm-http-run-button': {
|
||||
// width: '18px',
|
||||
// height: '18px',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#4CAF50', // 绿色三角
|
||||
// fontSize: '13px',
|
||||
// lineHeight: '16px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0',
|
||||
transition: 'color 0.15s ease, opacity 0.15s ease',
|
||||
},
|
||||
|
||||
// 悬停效果
|
||||
'.cm-http-run-button:hover': {
|
||||
color: '#45a049', // 深绿色
|
||||
// backgroundColor: 'rgba(76, 175, 80, 0.1)', // 淡绿色背景
|
||||
},
|
||||
|
||||
// 激活效果
|
||||
'.cm-http-run-button:active': {
|
||||
color: '#3d8b40',
|
||||
// backgroundColor: 'rgba(76, 175, 80, 0.2)',
|
||||
},
|
||||
|
||||
// 加载状态样式
|
||||
'.cm-http-run-button-loading': {
|
||||
color: '#999999 !important', // 灰色
|
||||
cursor: 'not-allowed !important',
|
||||
opacity: '0.6',
|
||||
},
|
||||
|
||||
// 禁用悬停效果当加载时
|
||||
'.cm-http-run-button-loading:hover': {
|
||||
color: '#999999 !important',
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 StateField 供扩展系统使用
|
||||
export { httpRequestsField };
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@codemirror/view';
|
||||
import { Extension, Range } from '@codemirror/state';
|
||||
import * as runtime from "@wailsio/runtime";
|
||||
import { getNoteBlockFromPos } from '../codeblock/state';
|
||||
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
|
||||
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi;
|
||||
|
||||
@@ -53,6 +54,13 @@ function hyperLinkDecorations(view: EditorView, anchor?: HyperLinkExtensionOptio
|
||||
const from = match.index;
|
||||
const to = from + match[0].length;
|
||||
|
||||
// 检查当前位置是否在 HTTP 代码块中
|
||||
const block = getNoteBlockFromPos(view.state, from);
|
||||
if (block && block.language.name === 'http') {
|
||||
// 如果在 HTTP 代码块中,跳过超链接装饰
|
||||
continue;
|
||||
}
|
||||
|
||||
const linkMark = Decoration.mark({
|
||||
class: 'cm-hyper-link-text',
|
||||
attributes: {
|
||||
@@ -84,6 +92,13 @@ const linkDecorator = (
|
||||
new MatchDecorator({
|
||||
regexp: regexp || defaultRegexp,
|
||||
decorate: (add, from, to, match, view) => {
|
||||
// 检查当前位置是否在 HTTP 代码块中
|
||||
const block = getNoteBlockFromPos(view.state, from);
|
||||
if (block && block.language.name === 'http') {
|
||||
// 如果在 HTTP 代码块中,跳过超链接装饰
|
||||
return;
|
||||
}
|
||||
|
||||
const url = match[0];
|
||||
let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url, match.input, from, to) : url;
|
||||
if (matchData && matchData[url]) {
|
||||
|
||||
@@ -17,16 +17,16 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
||||
plugins: [
|
||||
vue(),
|
||||
nodePolyfills({
|
||||
include: [],
|
||||
include: ['process','fs','crypto','stream','vm'],
|
||||
exclude: [],
|
||||
// Whether to polyfill specific globals.
|
||||
globals: {
|
||||
Buffer: true, // can also be 'build', 'dev', or false
|
||||
Buffer: false, // can also be 'build', 'dev', or false
|
||||
global: true,
|
||||
process: true,
|
||||
},
|
||||
// Whether to polyfill `node:` protocol imports.
|
||||
protocolImports: true,
|
||||
protocolImports: false,
|
||||
}),
|
||||
Components({
|
||||
dts: true,
|
||||
|
||||
7
go.mod
7
go.mod
@@ -15,10 +15,11 @@ require (
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/text v0.30.0
|
||||
modernc.org/sqlite v1.39.1
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
)
|
||||
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.22.0 // indirect
|
||||
code.gitea.io/sdk/gitea v0.22.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
@@ -28,7 +29,7 @@ require (
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -77,7 +78,7 @@ require (
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/image v0.32.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -1,5 +1,5 @@
|
||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
||||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
@@ -25,8 +25,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -178,8 +178,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
@@ -262,3 +262,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
|
||||
173
internal/services/httpclient_service.go
Normal file
173
internal/services/httpclient_service.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// HttpClientService HTTP客户端服务
|
||||
type HttpClientService struct {
|
||||
logger *log.LogService
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
// HttpRequest HTTP请求结构
|
||||
type HttpRequest struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
BodyType string `json:"bodyType,omitempty"` // json, formdata, urlencoded, text
|
||||
Body interface{} `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// HttpResponse HTTP响应结构
|
||||
type HttpResponse struct {
|
||||
Status string `json:"status"` // 使用resp.Status()返回完整状态如"200 OK"
|
||||
Time int64 `json:"time"` // 响应时间(毫秒)
|
||||
RequestSize string `json:"requestSize"` // 请求大小
|
||||
Body interface{} `json:"body"`
|
||||
Headers http.Header `json:"headers"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Error interface{} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewHttpClientService 创建新的HTTP客户端服务实例
|
||||
func NewHttpClientService(logger *log.LogService) *HttpClientService {
|
||||
client := resty.New().
|
||||
SetTimeout(30 * time.Second).
|
||||
SetRetryCount(0).
|
||||
EnableTrace().
|
||||
SetHeaders(map[string]string{
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "*",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
})
|
||||
|
||||
return &HttpClientService{
|
||||
logger: logger,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteRequest 执行HTTP请求
|
||||
func (hcs *HttpClientService) ExecuteRequest(ctx context.Context, request *HttpRequest) (*HttpResponse, error) {
|
||||
// 创建请求
|
||||
req := hcs.client.R().SetContext(ctx)
|
||||
|
||||
// 设置请求头
|
||||
if request.Headers != nil {
|
||||
req.SetHeaders(request.Headers)
|
||||
}
|
||||
|
||||
// 设置请求体
|
||||
if err := hcs.setRequestBody(req, request); err != nil {
|
||||
return nil, fmt.Errorf("set request body failed: %w", err)
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
resp, err := req.Execute(strings.ToUpper(request.Method), request.URL)
|
||||
|
||||
// 构建响应对象
|
||||
return hcs.buildResponse(resp, err)
|
||||
}
|
||||
|
||||
// setRequestBody 设置请求体
|
||||
func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRequest) error {
|
||||
if request.Body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch strings.ToLower(request.BodyType) {
|
||||
case "json":
|
||||
req.SetHeader("Content-Type", "application/json")
|
||||
req.SetBody(request.Body)
|
||||
case "formdata":
|
||||
if formData, ok := request.Body.(map[string]interface{}); ok {
|
||||
for key, value := range formData {
|
||||
valueStr := fmt.Sprintf("%v", value)
|
||||
// 检查是否是文件类型,使用 @file 关键词
|
||||
if strings.HasPrefix(valueStr, "@file ") {
|
||||
// 提取文件路径(去掉 @file 前缀)
|
||||
filePath := strings.TrimSpace(strings.TrimPrefix(valueStr, "@file "))
|
||||
req.SetFile(key, filePath)
|
||||
} else {
|
||||
// 普通表单字段
|
||||
req.SetFormData(map[string]string{key: valueStr})
|
||||
}
|
||||
}
|
||||
}
|
||||
case "urlencoded":
|
||||
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
if formData, ok := request.Body.(map[string]interface{}); ok {
|
||||
values := url.Values{}
|
||||
for key, value := range formData {
|
||||
values.Set(key, fmt.Sprintf("%v", value))
|
||||
}
|
||||
req.SetBody(values.Encode())
|
||||
}
|
||||
case "text":
|
||||
req.SetHeader("Content-Type", "text/plain")
|
||||
req.SetBody(fmt.Sprintf("%v", request.Body))
|
||||
default:
|
||||
return fmt.Errorf("unsupported body type: %s", request.BodyType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildResponse 构建响应对象
|
||||
func (hcs *HttpClientService) buildResponse(resp *resty.Response, err error) (*HttpResponse, error) {
|
||||
httpResp := &HttpResponse{
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// 如果有错误,设置错误状态并返回
|
||||
if err != nil {
|
||||
httpResp.Status = "Request Failed"
|
||||
httpResp.Error = err.Error()
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
// 设置基本响应信息
|
||||
httpResp.Status = resp.Status()
|
||||
httpResp.Time = resp.Duration().Milliseconds()
|
||||
httpResp.RequestSize = hcs.formatBytes(resp.Size())
|
||||
|
||||
if errorData := resp.Error(); errorData != nil {
|
||||
httpResp.Error = errorData
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
httpResp.Headers = resp.Header()
|
||||
httpResp.Body = resp.String()
|
||||
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
// formatBytes 格式化字节大小
|
||||
func (hcs *HttpClientService) formatBytes(bytes int64) string {
|
||||
if bytes < 0 {
|
||||
return "0 B"
|
||||
}
|
||||
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type ServiceManager struct {
|
||||
notificationService *notifications.NotificationService
|
||||
testService *TestService // 测试服务(仅开发环境)
|
||||
BackupService *BackupService
|
||||
httpClientService *HttpClientService // HTTP客户端服务
|
||||
logger *log.LogService
|
||||
}
|
||||
|
||||
@@ -102,6 +103,9 @@ func NewServiceManager() *ServiceManager {
|
||||
// 初始化备份服务
|
||||
backupService := NewBackupService(configService, databaseService, logger)
|
||||
|
||||
// 初始化HTTP客户端服务
|
||||
httpClientService := NewHttpClientService(logger)
|
||||
|
||||
// 初始化测试服务(开发环境使用)
|
||||
testService := NewTestService(badgeService, notificationService, logger)
|
||||
|
||||
@@ -142,7 +146,6 @@ func NewServiceManager() *ServiceManager {
|
||||
databaseService: databaseService,
|
||||
documentService: documentService,
|
||||
windowService: windowService,
|
||||
windowSnapService: windowSnapService,
|
||||
migrationService: migrationService,
|
||||
systemService: systemService,
|
||||
hotkeyService: hotkeyService,
|
||||
@@ -158,6 +161,7 @@ func NewServiceManager() *ServiceManager {
|
||||
notificationService: notificationService,
|
||||
testService: testService,
|
||||
BackupService: backupService,
|
||||
httpClientService: httpClientService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -169,7 +173,6 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
||||
application.NewService(sm.databaseService),
|
||||
application.NewService(sm.documentService),
|
||||
application.NewService(sm.windowService),
|
||||
application.NewService(sm.windowSnapService),
|
||||
application.NewService(sm.keyBindingService),
|
||||
application.NewService(sm.extensionService),
|
||||
application.NewService(sm.migrationService),
|
||||
@@ -185,6 +188,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
||||
application.NewService(sm.notificationService),
|
||||
application.NewService(sm.testService),
|
||||
application.NewService(sm.BackupService),
|
||||
application.NewService(sm.httpClientService),
|
||||
}
|
||||
return services
|
||||
}
|
||||
@@ -273,3 +277,8 @@ func (sm *ServiceManager) GetNotificationService() *notifications.NotificationSe
|
||||
func (sm *ServiceManager) GetSystemService() *SystemService {
|
||||
return sm.systemService
|
||||
}
|
||||
|
||||
// GetHttpClientService 获取HTTP客户端服务
|
||||
func (sm *ServiceManager) GetHttpClientService() *HttpClientService {
|
||||
return sm.httpClientService
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
VERSION=1.5.1
|
||||
VERSION=1.5.2
|
||||
|
||||
Reference in New Issue
Block a user