From 93c85b800bd4c9d6609fd60103ec092cc930d82e Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sat, 1 Nov 2025 17:42:22 +0800 Subject: [PATCH] :construction: Optimized HTTP language parser --- .../codeblock/lang-parser/build-parser.js | 10 +- .../codeblock/lang-parser/codeblock-lang.ts | 2 +- .../codeblock/lang-parser/external-tokens.ts | 2 +- .../extensions/codeblock/lang-parser/index.ts | 4 +- .../codeblock/lang-parser/languages.ts | 2 +- .../codeblock/lang-parser/nested-parser.ts | 2 +- .../{parser.terms.js => parser.terms.ts} | 0 .../lang-parser/{parser.js => parser.ts} | 0 .../editor/extensions/httpclient/index.ts | 7 +- .../httpclient/language/build-parser.js | 21 +- .../httpclient/language/http-highlight.ts | 75 -- .../httpclient/language/http-language.ts | 83 +- .../httpclient/language/http.grammar | 397 +++++----- .../httpclient/language/http.grammar.js | 23 - .../httpclient/language/http.grammar.terms.js | 60 -- .../httpclient/language/http.grammar.test.ts | 725 ++++++++++++++++++ .../httpclient/language/http.highlight.ts | 67 ++ .../httpclient/language/http.parser.terms.ts | 35 + .../httpclient/language/http.parser.ts | 26 + .../extensions/httpclient/language/index.ts | 29 +- .../httpclient/parser/request-parser.test.ts | 346 +++++++++ .../httpclient/parser/request-parser.ts | 355 +++++++++ .../editor/extensions/httpclient/types.ts | 1 - .../httpclient/widgets/run-gutter.ts | 255 +++--- 24 files changed, 1945 insertions(+), 582 deletions(-) rename frontend/src/views/editor/extensions/codeblock/lang-parser/{parser.terms.js => parser.terms.ts} (100%) rename frontend/src/views/editor/extensions/codeblock/lang-parser/{parser.js => parser.ts} (100%) delete mode 100644 frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts delete mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.grammar.js delete mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.grammar.test.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.highlight.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.parser.terms.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.parser.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/parser/request-parser.test.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/parser/request-parser.ts delete mode 100644 frontend/src/views/editor/extensions/httpclient/types.ts diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/build-parser.js b/frontend/src/views/editor/extensions/codeblock/lang-parser/build-parser.js index 6828daf..3f655c4 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/build-parser.js +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/build-parser.js @@ -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'); } diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock-lang.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock-lang.ts index 5eca835..b91bf64 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock-lang.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock-lang.ts @@ -3,7 +3,7 @@ * 提供多语言代码块支持 */ -import { parser } from "./parser.js"; +import { parser } from "./parser"; import { configureNesting } from "./nested-parser"; import { diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/external-tokens.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/external-tokens.ts index 6139216..809fe42 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/external-tokens.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/external-tokens.ts @@ -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; diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/index.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/index.ts index 1fedf96..5264513 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/index.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/index.ts @@ -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'; \ No newline at end of file +} from './parser'; \ No newline at end of file diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index 807ef1e..ba8b72f 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -86,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] diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/nested-parser.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/nested-parser.ts index 8bf0c2a..a281f44 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/nested-parser.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/nested-parser.ts @@ -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"; /** diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.terms.js b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.terms.ts similarity index 100% rename from frontend/src/views/editor/extensions/codeblock/lang-parser/parser.terms.js rename to frontend/src/views/editor/extensions/codeblock/lang-parser/parser.terms.ts diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.js b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts similarity index 100% rename from frontend/src/views/editor/extensions/codeblock/lang-parser/parser.js rename to frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts diff --git a/frontend/src/views/editor/extensions/httpclient/index.ts b/frontend/src/views/editor/extensions/httpclient/index.ts index 733fcfa..307822d 100644 --- a/frontend/src/views/editor/extensions/httpclient/index.ts +++ b/frontend/src/views/editor/extensions/httpclient/index.ts @@ -4,7 +4,7 @@ import {Extension} from '@codemirror/state'; -import {httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter'; +import {httpRunButtonGutter, httpRunButtonTheme, httpRequestsField} from './widgets/run-gutter'; /** * 创建 HTTP Client 扩展 @@ -16,7 +16,10 @@ export function createHttpClientExtension(): Extension[] { // HTTP 语言解析器 // extensions.push(httpLanguage); - // 运行按钮 Gutte + // StateField:缓存 HTTP 请求解析结果 + extensions.push(httpRequestsField); + + // 运行按钮 Gutter extensions.push(httpRunButtonGutter); extensions.push(httpRunButtonTheme); diff --git a/frontend/src/views/editor/extensions/httpclient/language/build-parser.js b/frontend/src/views/editor/extensions/httpclient/language/build-parser.js index f3878bf..52e8176 100644 --- a/frontend/src/views/editor/extensions/httpclient/language/build-parser.js +++ b/frontend/src/views/editor/extensions/httpclient/language/build-parser.js @@ -2,8 +2,8 @@ /** * HTTP Grammar Parser Builder - * 编译 Lezer grammar 文件为 JavaScript parser - * 使用命令行方式编译 + * 编译 Lezer grammar 文件为 TypeScript parser + * 使用 --typeScript 选项生成 .ts 文件 */ import { execSync } from 'child_process'; @@ -13,7 +13,7 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -console.log('🚀 开始编译 HTTP grammar parser...'); +console.log('🚀 开始编译 HTTP grammar parser (TypeScript)...'); try { // 检查语法文件是否存在 @@ -24,22 +24,22 @@ try { console.log('📄 语法文件:', grammarFile); - // 运行 lezer-generator - console.log('⚙️ 编译 parser...'); - execSync('npx lezer-generator http.grammar -o http.grammar.js', { + // 运行 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.grammar.js'); - const termsFile = path.join(__dirname, 'http.grammar.terms.js'); + 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.grammar.js'); - console.log(' - http.grammar.terms.js'); + console.log(' - http.parser.ts'); + console.log(' - http.parser.terms.ts'); } else { throw new Error('Parser 生成失败'); } @@ -50,4 +50,3 @@ try { console.error('❌ 编译失败:', error.message); process.exit(1); } - diff --git a/frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts b/frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts deleted file mode 100644 index 4176be3..0000000 --- a/frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { styleTags, tags as t } from '@lezer/highlight'; - -/** - * HTTP Client 语法高亮配置 - */ -export const httpHighlighting = styleTags({ - // 注释 - LineComment: t.lineComment, - - // HTTP 方法关键字 - "GET POST PUT DELETE PATCH HEAD OPTIONS": t.keyword, - - // 关键字 - "HEADER BODY VARIABLES RESPONSE": t.keyword, - - // Body 类型 - "TEXT JSON XML FORM URLENCODED GRAPHQL BINARY": t.keyword, - - // 变量名 - VariableName: t.variableName, - VariableValue: t.string, - - // URL 和版本 - Url: t.url, - UrlText: t.url, - HttpVersion: t.literal, - - // Header - HeaderName: t.propertyName, - HeaderValue: t.string, - - // Body 内容 - BodyContent: t.content, - - // Variables 内容 - VariablesContent: t.content, - - // 响应 - StatusCode: t.number, - StatusText: t.string, - Duration: t.literal, - Size: t.literal, - Timestamp: t.literal, - - // 文件引用 - FilePath: t.string, - - // 模板表达式 - "{{ }}": t.special(t.brace), - "TemplateExpression/VariableName": t.variableName, - - // 成员访问 - "MemberExpression/VariableName": t.variableName, - "MemberExpression/PropertyName": t.propertyName, - PropertyName: t.propertyName, - - // 函数调用 - FunctionName: t.function(t.variableName), - - // 基础类型 - Number: t.number, - String: t.string, - - // 符号 - ": Spread": t.punctuation, - "( )": t.paren, - "[ ]": t.squareBracket, - "{ }": t.brace, - ".": t.derefOperator, - ", ;": t.separator, - "@": t.meta, - "$": t.meta, - "=": t.definitionOperator, -}); - diff --git a/frontend/src/views/editor/extensions/httpclient/language/http-language.ts b/frontend/src/views/editor/extensions/httpclient/language/http-language.ts index af77aa2..d913b26 100644 --- a/frontend/src/views/editor/extensions/httpclient/language/http-language.ts +++ b/frontend/src/views/editor/extensions/httpclient/language/http-language.ts @@ -1,6 +1,7 @@ import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language'; -import { parser } from './http.grammar.js'; -import { httpHighlighting } from './http-highlight'; +import { CompletionContext } from '@codemirror/autocomplete'; +import { parser } from './http.parser'; +import { httpHighlighting } from './http.highlight'; /** * HTTP Client 语言定义 @@ -12,18 +13,19 @@ const httpParserWithMetadata = parser.configure({ // 应用语法高亮 httpHighlighting, - // 折叠规则:允许折叠多行 Body、Variables、Headers 等 + // 折叠规则:允许折叠块结构 foldNodeProp.add({ - BodyStatement: foldInside, - VariablesStatement: foldInside, + RequestStatement: foldInside, + Block: foldInside, + AtRule: foldInside, Document: foldInside, }), // 缩进规则 indentNodeProp.add({ - BodyStatement: () => 2, - HeaderStatement: () => 0, - VariableDeclaration: () => 0, + Block: () => 2, + Declaration: () => 0, + AtRule: () => 0, }), ], }); @@ -32,10 +34,8 @@ const httpParserWithMetadata = parser.configure({ export const httpLanguage = LRLanguage.define({ parser: httpParserWithMetadata, languageData: { - // 注释配置 - commentTokens: { line: '#' }, - // 自动闭合括号 + //自动闭合括号 closeBrackets: { brackets: ['(', '[', '{', '"', "'"] }, // 单词字符定义 @@ -45,7 +45,7 @@ export const httpLanguage = LRLanguage.define({ /** * HTTP Client 语言支持 - * 包含语法高亮、折叠、缩进等完整功能 + * 包含语法高亮、折叠、缩进、自动补全等完整功能 */ export function http() { return new LanguageSupport(httpLanguage, [ @@ -58,7 +58,7 @@ export function http() { /** * HTTP Client 自动补全 */ -function httpCompletion(context: any) { +function httpCompletion(context: CompletionContext) { const word = context.matchBefore(/\w*/); if (!word || (word.from === word.to && !context.explicit)) { return null; @@ -76,49 +76,29 @@ function httpCompletion(context: any) { { label: 'HEAD', type: 'keyword', detail: 'HTTP Method' }, { label: 'OPTIONS', type: 'keyword', detail: 'HTTP Method' }, - // 关键字 - { label: 'HEADER', type: 'keyword', detail: 'Header Statement' }, - { label: 'BODY', type: 'keyword', detail: 'Body Statement' }, - { label: 'VARIABLES', type: 'keyword', detail: 'Variables Statement' }, - - // Body 类型 - { label: 'TEXT', type: 'keyword', detail: 'Body Type' }, - { label: 'JSON', type: 'keyword', detail: 'Body Type' }, - { label: 'XML', type: 'keyword', detail: 'Body Type' }, - { label: 'FORM', type: 'keyword', detail: 'Body Type' }, - { label: 'URLENCODED', type: 'keyword', detail: 'Body Type' }, - { label: 'GRAPHQL', type: 'keyword', detail: 'Body Type' }, - { label: 'BINARY', type: 'keyword', detail: 'Body Type' }, - - // HTTP 版本 - { label: 'HTTP/1.0', type: 'constant', detail: 'HTTP Version' }, - { label: 'HTTP/1.1', type: 'constant', detail: 'HTTP Version' }, - { label: 'HTTP/2.0', type: 'constant', detail: 'HTTP Version' }, + // @ 规则 + { label: '@json', type: 'keyword', detail: 'Body Type' }, + { label: '@formdata', type: 'keyword', detail: 'Body Type' }, + { label: '@urlencoded', type: 'keyword', detail: 'Body Type' }, + { label: '@text', type: 'keyword', detail: 'Body Type' }, + { label: '@res', type: 'keyword', detail: 'Response' }, // 常用 Headers - { label: 'Content-Type', type: 'property', detail: 'Header Name' }, - { label: 'Authorization', type: 'property', detail: 'Header Name' }, - { label: 'Accept', type: 'property', detail: 'Header Name' }, - { label: 'User-Agent', type: 'property', detail: 'Header Name' }, - { label: 'Cookie', type: 'property', detail: 'Header Name' }, + { label: 'content-type', type: 'property', detail: 'Header' }, + { label: 'authorization', type: 'property', detail: 'Header' }, + { label: 'accept', type: 'property', detail: 'Header' }, + { label: 'user-agent', type: 'property', detail: 'Header' }, + { label: 'host', type: 'property', detail: 'Header' }, // 常用 Content-Type - { label: 'application/json', type: 'constant', detail: 'Content Type' }, - { label: 'application/xml', type: 'constant', detail: 'Content Type' }, - { label: 'text/html', type: 'constant', detail: 'Content Type' }, - { label: 'text/plain', type: 'constant', detail: 'Content Type' }, - { label: 'multipart/form-data', type: 'constant', detail: 'Content Type' }, - { label: 'application/x-www-form-urlencoded', type: 'constant', detail: 'Content Type' }, + { label: '"application/json"', type: 'constant', detail: 'Content Type' }, + { label: '"text/plain"', type: 'constant', detail: 'Content Type' }, + { label: '"multipart/form-data"', type: 'constant', detail: 'Content Type' }, + { label: '"application/x-www-form-urlencoded"', type: 'constant', detail: 'Content Type' }, - // 特殊标记 - { label: '@timestamp', type: 'keyword', detail: 'Timestamp' }, - { label: '@file', type: 'keyword', detail: 'File Reference' }, - - // 内置函数 - { label: '$timestamp()', type: 'function', detail: 'Current Timestamp' }, - { label: '$uuid()', type: 'function', detail: 'Generate UUID' }, - { label: '$randomInt()', type: 'function', detail: 'Random Integer' }, - { label: '$hash()', type: 'function', detail: 'Hash Function' }, + // 布尔值 + { label: 'true', type: 'constant', detail: 'Boolean' }, + { label: 'false', type: 'constant', detail: 'Boolean' }, ], }; } @@ -127,4 +107,3 @@ function httpCompletion(context: any) { * 导出语言定义和高亮配置 */ export { httpHighlighting }; - diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar b/frontend/src/views/editor/extensions/httpclient/language/http.grammar index 10b9948..7a89106 100644 --- a/frontend/src/views/editor/extensions/httpclient/language/http.grammar +++ b/frontend/src/views/editor/extensions/httpclient/language/http.grammar @@ -1,267 +1,214 @@ -@precedence { - member, - call +// HTTP Client Grammar +// +// 语法规则: +// 1. HTTP 头部属性:逗号可选 +// host: "example.com" +// content-type: "application/json" +// +// 2. 请求体格式: +// @json - JSON 格式(属性必须用逗号分隔) +// @formdata - 表单数据(属性必须用逗号分隔) +// @urlencoded - URL 编码格式(属性必须用逗号分隔) +// @text - 纯文本内容(使用 content 字段) +// @res - 响应数据(属性必须用逗号分隔) +// +// 3. 注释: +// # 单行注释 +// +// 示例 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: "纯文本内容" +// } +// } + +@skip { whitespace | LineComment } + +@top Document { item* } + +item { + RequestStatement | + AtRule } -@top Document { statement* } - -statement { - VariableDeclaration | - ResponseLine | - RequestLine | - HeaderStatement | - BodyStatement | - VariablesStatement -} - -// ==================== 变量定义 ==================== - -VariableDeclaration { - "@" VariableName "=" VariableValue ";" -} - -VariableName { word } - -VariableValue[isolate] { anyContent } - -// ==================== 请求行 ==================== - -RequestLine { - Method Url HttpVersion? ";" +// HTTP 请求 - URL 必须是字符串 +RequestStatement { + Method Url Block } Method { - @specialize[@name="GET"] | - @specialize[@name="POST"] | - @specialize[@name="PUT"] | - @specialize[@name="DELETE"] | - @specialize[@name="PATCH"] | - @specialize[@name="HEAD"] | - @specialize[@name="OPTIONS"] | - @specialize[@name="CONNECT"] | - @specialize[@name="TRACE"] + @specialize[@name=GET] | + @specialize[@name=POST] | + @specialize[@name=PUT] | + @specialize[@name=DELETE] | + @specialize[@name=PATCH] | + @specialize[@name=HEAD] | + @specialize[@name=OPTIONS] | + @specialize[@name=CONNECT] | + @specialize[@name=TRACE] } -Url { urlPart+ } +// URL 必须是字符串 +Url { StringLiteral } -urlPart { urlContent | TemplateExpression } - -HttpVersion { httpVersionToken } - -// ==================== Header 语句 ==================== - -HeaderStatement { - HeaderKeyword HeaderName colon HeaderValue ";" +// @ 规则(支持多种请求体格式) +AtRule { + JsonRule | + FormDataRule | + UrlEncodedRule | + TextRule | + ResponseRule } -colon { ":" } - -HeaderKeyword { @specialize[@name="HEADER"] } - -HeaderName { word } - -HeaderValue { headerValuePart* } - -headerValuePart { headerValueContent | TemplateExpression } - -// ==================== Body 语句 ==================== - -BodyStatement { - BodyKeyword BodyType BodyContent ";" +// @json 块:JSON 格式请求体(属性必须用逗号分隔) +JsonRule { + @specialize[@name=JsonKeyword] + JsonBlock } -BodyKeyword { @specialize[@name="BODY"] } - -BodyType { - @specialize[@name="JSON"] | - @specialize[@name="FORM"] | - @specialize[@name="URLENCODED"] | - @specialize[@name="GRAPHQL"] | - @specialize[@name="XML"] | - @specialize[@name="TEXT"] | - @specialize[@name="BINARY"] | - @specialize[@name="MULTIPART"] +// @formdata 块:表单数据格式(属性必须用逗号分隔) +FormDataRule { + @specialize[@name=FormDataKeyword] + JsonBlock } -BodyContent { bodyContentPart* } - -bodyContentPart { bodyText | TemplateExpression | FileReference } - -FileReference { - "@file" FilePath +// @urlencoded 块:URL 编码格式(属性必须用逗号分隔) +UrlEncodedRule { + @specialize[@name=UrlEncodedKeyword] + JsonBlock } -FilePath { filePathContent } - -// ==================== Variables 语句 ==================== - -VariablesStatement { - VariablesKeyword VariablesContent ";" +// @text 块:纯文本请求体(使用 content 字段) +TextRule { + @specialize[@name=TextKeyword] + JsonBlock } -VariablesKeyword { @specialize[@name="VARIABLES"] } - -VariablesContent[isolate] { variablesContent } - -// ==================== 响应 ==================== - -// 响应行 - 固定格式:RESPONSE <状态码> <状态文本> <大小> <时间戳>; -ResponseLine { - ResponseKeyword StatusCode StatusText Size Timestamp ";" +// @res 块:响应数据(属性必须用逗号分隔) +ResponseRule { + @specialize[@name=ResKeyword] + JsonBlock } -ResponseKeyword { @specialize[@name="RESPONSE"] } - -StatusCode { Number } - -StatusText { word+ } - -Size { Number sizeUnit } - -// 时间戳格式:YYYY-MM-DD HH:MM:SS 或 ISO8601 格式 -Timestamp { timestampContent } - -// ==================== 模板表达式 ==================== - -TemplateExpression { - "{{" templateContent "}}" +// 普通块结构(属性逗号可选) +Block { + "{" blockContent "}" } -templateContent { - VariableName | - MemberExpression | - FunctionCall +blockContent { + (Property | AtRule)* } -MemberExpression { - VariableName !member ("." PropertyName)+ +// HTTP 属性(逗号可选) +Property { + PropertyName { identifier } + ":" value ","? } -PropertyName { word } - -FunctionCall { - "$" FunctionName !call "(" argumentList? ")" +// JSON 块结构(属性必须用逗号分隔) +JsonBlock { + "{" jsonBlockContent? "}" } -FunctionName { word } - -argumentList { - argument ("," argument)* +jsonBlockContent { + JsonProperty ("," JsonProperty)* ","? } -argument { Number | String | word } +// JSON 属性 +JsonProperty { + PropertyName { identifier } + ":" jsonValue +} -// ==================== Tokens ==================== +// 值 +NumberLiteral { + numberLiteralInner Unit? +} -@skip { spaces | newline | LineComment } +// HTTP 属性值(支持块嵌套) +value { + StringLiteral | + NumberLiteral | + Block | + identifier +} +// JSON 属性值(支持嵌套对象) +jsonValue { + StringLiteral | + NumberLiteral | + JsonBlock | + identifier +} + +// Tokens @tokens { - // 空白字符 - spaces[@export] { $[ \t]+ } + // 单行注释(# 开头到行尾) + LineComment { "#" ![\n]* } - newline[@export] { $[\r\n] } + AtKeyword { "@" "-"? @asciiLetter (@asciiLetter | @digit | "-")* } - // 注释 - LineComment[@export,isolate] { "#" ![\n]* } - - // 标识符 - identifierChar { @asciiLetter | $[_$] } - - word { identifierChar (identifierChar | @digit | $[-])* } - - // 数字 - Number { - @digit+ ("." @digit+)? + // 标识符(属性名,支持连字符) + identifier { + (@asciiLetter | $[_$]) + (@asciiLetter | @digit | $[-_$])* } - // 字符串 - String { - '"' stringContentDouble* '"' | - "'" stringContentSingle* "'" + // 单位(必须跟在数字后面,所以不单独匹配) + Unit { @asciiLetter+ } + + whitespace { @whitespace+ } + + @precedence { identifier, Unit } + + numberLiteralInner { + ("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+) + (("e" | "E") ("+" | "-")? @digit+)? } - - stringContentDouble { ![\\\n"]+ | "\\" _ } - - stringContentSingle { ![\\\n']+ | "\\" _ } - - // URL 内容 - 排除空格、换行、特殊前缀字符 - // 允许 # (用于锚点),但不允许以 # 开头(避免和注释冲突) - urlContent { ![ \t\n\r@{};]+ } - - // HTTP 版本 - httpVersionToken { "HTTP/" @digit+ "." @digit+ } - - // Header 值内容 - 排除所有 skip tokens 和特殊字符 - headerValueContent { ![ \t\n\r#{};]+ } - - // Body 文本内容 - 排除空格、换行、分号 - // 允许大括号、#、@、$ 等,因为JSON/XML/GraphQL/email等需要它们 - // {{ 和 @file 是独立token,有更高优先级 - bodyText { ![ \t\n\r;]+ } - - // 任意内容(直到分号) - 只排除换行和分号 - // 允许空格、#、数字等所有字符 - anyContent { ![\n\r;]+ } - - // 文件路径内容 - 排除空格、换行、分号 - filePathContent { ![ \t\n\r;]+ } - - // Variables 内容 - 只排除分号,允许所有字符包括换行 - variablesContent { ![;]+ } - - // 时间戳内容 - 日期时间格式,包含日期时间字符 - timestampContent { timestampChar+ } - - timestampChar { @digit | $[-:TZ.+] } - - // Duration 单位 - durationUnit { "ms" | "s" | "m" } - - // Size 单位 - sizeUnit { "B" | "KB" | "MB" | "GB" } - - // 优先级声明 - @precedence { urlContent, LineComment } - - @precedence { "{{", bodyText } - - @precedence { "@file", bodyText } - - @precedence { "$", bodyText } - - @precedence { bodyText, LineComment } - - @precedence { anyContent, LineComment } - - @precedence { anyContent, spaces } - - @precedence { filePathContent, LineComment } - - @precedence { variablesContent, newline } - - @precedence { variablesContent, LineComment } - - @precedence { variablesContent, spaces } - - @precedence { "$", word } - - @precedence { spaces, newline, word } - - @precedence { httpVersionToken, urlContent, word } - - @precedence { "{{", bodyText } - - @precedence { word, bodyText } - - @precedence { Number, word } - - // 符号 - "(" ")" "[" "]" "{" "}" - - "." "," ":" "=" "@" "$" ";" "{{" "}}" - - // 组合 tokens - "@file" + + StringLiteral[isolate] { + "\"" (!["\n\\] | "\\" _)* "\"" | + "'" (!['\n\\] | "\\" _)* "'" + } + + ":" "," + + "{" "}" } +@external propSource httpHighlighting from "./http.highlight" + @detectDelim diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.js b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.js deleted file mode 100644 index ffc2d9c..0000000 --- a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.js +++ /dev/null @@ -1,23 +0,0 @@ -// This file was generated by lezer-generator. You probably shouldn't edit it. -import {LRParser} from "@lezer/lr" -const spec_word = {__proto__:null,RESPONSE:22, GET:38, POST:40, PUT:42, DELETE:44, PATCH:46, HEAD:48, OPTIONS:50, CONNECT:52, TRACE:54, HEADER:90, BODY:102, JSON:106, FORM:108, URLENCODED:110, GRAPHQL:112, XML:114, TEXT:116, BINARY:118, MULTIPART:120, VARIABLES:134} -export const parser = LRParser.deserialize({ - version: 14, - states: ",lQ]QPOOO!ZQPO'#C`OOQO'#Cf'#CfO!`QPO'#CeOOQO'#Cn'#CnO!eQQO'#CmOOQO'#DY'#DYOOQO'#D`'#D`O!mQPO'#D_OOQO'#Dp'#DpO#XQSO'#DoOOQO'#EO'#EOOOQO'#Ds'#DsQ]QPOOO#^QPO'#DXOOQO'#Cb'#CbO#cQPO,58zOOQO'#Ch'#ChO#hQPO,59PO#mQPO'#C{OOQO'#ET'#ETOOQO'#Du'#DuO#uQQO'#CxO$TQPO,59XOOQO'#Db'#DbO$]QWO,59yOOQO'#Dr'#DrO$kQPO,5:ZOOQO-E7q-E7qOOQO'#D['#D[O$pQPO,59sO$uQ`O1G.fOOQO'#Dt'#DtO$zQPO'#CjO%SQPO1G.kO%XQPO'#EVO%aQPO'#DROOQO'#EV'#EVO%fQPO,59gOOQO-E7s-E7sOOQO'#DW'#DWOOQO1G.s1G.sO%kQPO1G.sO%pQpO'#DlOOQO'#E]'#E]OOQO'#Dy'#DyO%uQWO'#DkO&TQPO1G/eOOQO1G/u1G/uO&YQ!bO1G/_OOQO'#Cd'#CdO&eQPO7+$QOOQO-E7r-E7rO&jQ#tO'#CkO&oQ#tO7+$VO&tQPO'#DvO&yQPO,59hOOQO'#DS'#DSO'RQPO,59mOOQO1G/R1G/ROOQO7+$_7+$_OOQO'#Dn'#DnOOQO,5:W,5:WOOQO-E7w-E7wOOQO7+%P7+%POOQO'#EZ'#EZOOQO'#Dx'#DxO'WQ!bO'#D^O'cQPO7+$yOOQO<a;=`<%l0z<%lO;O!_>dXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%l;O<%lO0z!_?SP;=`<%l;O!a?dc!xQ#SS#Rp!t`#QWOX7wXY.oYZ/oZ]7w]^/o^p7wpq.oq!]7w!]!^0z!^!b7w!b!c;O!c#o7w#o#p;O#p#q7w#q#r;O#r;'S7w;'S;=`@o;=`<%l0z<%lO7w!a@rXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%l7w<%lO0z!aAbP;=`<%l7w#qAre#O!b#SS#Rp!t`#QWOXAeXY.oYZ&mZ]Ae]^/o^pAepq.oqrAersCTst;Ot!]Ae!]!^0z!^#OAe#O#PDi#P#oAe#o#p;O#p#qAe#q#r;O#r;'SAe;'S;=`Fq<%lOAe#qCdaxP#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#qDvc#O!b#SS#Rp!t`#QWOXAeXY.oYZ/oZ]Ae]^/o^pAepq.oqsAest;Ot!]Ae!]!^0z!^#oAe#o#p;O#p#qAe#q#r;O#r;'SAe;'S;=`FR;=`<%l0z<%lOAe#qFUXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%lAe<%lO0z#qFtP;=`<%lAe#sGWe#O!b!xQ#SS#Rp!t`#QWOX,wXY.oYZ/oZ],w]^/o^p,wpq.oqs,wst7wt!],w!]!^0z!^!b,w!b!cAe!c#o,w#o#p;O#p#q,w#q#r;O#r;'S,w;'S;=`Hi;=`<%l0z<%lO,w#sHlXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%l,w<%lO0z#sI[P;=`<%l,w~Inb!xQ#SS#Rp!t`#QWP~OXI_XYJvYZ&mZ]I_]^Kl^pI_pqJvq!]I_!]!^LY!^!bI_!b!cL}!c#oI_#o#pL}#p#qI_#q#rL}#r;'SI_;'S;=`NW<%lOI_~KPX#SS!t`P~OYJvYZ&mZ]Jv]^Kl^!]Jv!]!^LY!^;'SJv;'S;=`Lw<%lOJv~KsV#SSP~OYKlYZ&mZ!]Kl!]!^LY!^;'SKl;'S;=`Lq<%lOKl~L_SP~OYLYZ;'SLY;'S;=`Lk<%lOLY~LnP;=`<%lLY~LtP;=`<%lKl~LzP;=`<%lJv~M[[#SS#Rp!t`#QWP~OXL}XYJvYZ&mZ]L}]^Kl^pL}pqJvq!]L}!]!^LY!^;'SL};'S;=`NQ<%lOL}~NTP;=`<%lL}~NZP;=`<%lI_#sNql#O!b!xQ#SS#RptP!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!!zl#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!%Rh#O!b!xQ#SS#Rp!t`#QWOX!$rXY!&mYZ&mZ]!$r]^!'m^p!$rpq!&mqs!$rst!,|tw!$rwx6Zx!]!$r!]!^!(e!^!b!$r!b!c!4U!c#O!$r#O#P!8V#P#o!$r#o#p!.o#p#q!$r#q#r!.o#r;'S!$r;'S;=`!:g<%lO!$re!&t]#SS!t`OY!&mYZ&mZ]!&m]^!'m^w!&mwx3{x!]!&m!]!^!(e!^#O!&m#O#P!+a#P;'S!&m;'S;=`!,v<%lO!&mT!'rZ#SSOY!'mYZ&mZw!'mwx0gx!]!'m!]!^!(e!^#O!'m#O#P!*S#P;'S!'m;'S;=`!+Z<%lO!'mP!(hWOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|<%lO!(eP!)TRO;'S!(e;'S;=`!)^;=`O!(eP!)aXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!(e<%lO!(eP!*PP;=`<%l!(eT!*XU#SSO!]!'m!]!^!(e!^;'S!'m;'S;=`!*k;=`<%l!(e<%lO!'mT!*nXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!'m<%lO!(eT!+^P;=`<%l!'me!+hY#SS!t`OY!&mYZ!'mZ]!&m]^!'m^!]!&m!]!^!(e!^;'S!&m;'S;=`!,W;=`<%l!(e<%lO!&me!,ZXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!&m<%lO!(ee!,yP;=`<%l!&m!a!-Zf!xQ#SS#Rp!t`#QWOX!,|XY!&mYZ&mZ]!,|]^!'m^p!,|pq!&mqw!,|wx9jx!]!,|!]!^!(e!^!b!,|!b!c!.o!c#O!,|#O#P!1v#P#o!,|#o#p!.o#p#q!,|#q#r!.o#r;'S!,|;'S;=`!4O<%lO!,|!_!.z`#SS#Rp!t`#QWOX!.oXY!&mYZ&mZ]!.o]^!'m^p!.opq!&mqw!.owx<]x!]!.o!]!^!(e!^#O!.o#O#P!/|#P;'S!.o;'S;=`!1p<%lO!.o!_!0X]#SS#Rp!t`#QWOX!.oXY!&mYZ!'mZ]!.o]^!'m^p!.opq!&mq!]!.o!]!^!(e!^;'S!.o;'S;=`!1Q;=`<%l!(e<%lO!.o!_!1TXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!.o<%lO!(e!_!1sP;=`<%l!.o!a!2Tc!xQ#SS#Rp!t`#QWOX!,|XY!&mYZ!'mZ]!,|]^!'m^p!,|pq!&mq!]!,|!]!^!(e!^!b!,|!b!c!.o!c#o!,|#o#p!.o#p#q!,|#q#r!.o#r;'S!,|;'S;=`!3`;=`<%l!(e<%lO!,|!a!3cXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!,|<%lO!(e!a!4RP;=`<%l!,|#q!4cf#O!b#SS#Rp!t`#QWOX!4UXY!&mYZ&mZ]!4U]^!'m^p!4Upq!&mqs!4Ust!.otw!4UwxCTx!]!4U!]!^!(e!^#O!4U#O#P!5w#P#o!4U#o#p!.o#p#q!4U#q#r!.o#r;'S!4U;'S;=`!8P<%lO!4U#q!6Uc#O!b#SS#Rp!t`#QWOX!4UXY!&mYZ!'mZ]!4U]^!'m^p!4Upq!&mqs!4Ust!.ot!]!4U!]!^!(e!^#o!4U#o#p!.o#p#q!4U#q#r!.o#r;'S!4U;'S;=`!7a;=`<%l!(e<%lO!4U#q!7dXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!4U<%lO!(e#q!8SP;=`<%l!4U#s!8fe#O!b!xQ#SS#Rp!t`#QWOX!$rXY!&mYZ!'mZ]!$r]^!'m^p!$rpq!&mqs!$rst!,|t!]!$r!]!^!(e!^!b!$r!b!c!4U!c#o!$r#o#p!.o#p#q!$r#q#r!.o#r;'S!$r;'S;=`!9w;=`<%l!(e<%lO!$r#s!9zXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!$r<%lO!(e#s!:jP;=`<%l!$r#s!;OcwP#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!Yn#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!=w![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!@icyP#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!BXnqP#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!=w![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!Djn#O!b!v#t]P!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!Fh!P!Q$b!Q![!DV![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!Fyn#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!Hw![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!I[n#O!b!v#t]P!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!Hw![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!Kmn!PP#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!=w![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b~!MpOR~#s!NRcVP#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#q# mcTP#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#Y)x#Y#Z#!x#Z#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p##Vc#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#])x#]#^#$b#^#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p#$oc#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#`)x#`#a#%z#a#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p#&Xc#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#X)x#X#Y#'d#Y#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p#'sa#O!b!aW#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x&i#)]l#O!b!u#t!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i#+fn#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!d!!i!d!e#(x!e!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#-un#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!v!!i!v!w#/s!w!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#0Un#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!v!!i!v!w#2S!w!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#2en#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!r!!i!r!s#4c!s!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#4tm#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!P$b!P!Q#6o!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#7Oe#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!Q$b!Q![#8a![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#8pg#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!O$b!O!P#:X!P!Q$b!Q![#8a![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#:he#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!Q$b!Q![#;y![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#<[e#O!b!|R!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!Q$b!Q![#;y![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i#>Qr#O!b!v#t!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu{$b{|!=w|}$b}!O#=m!O!P!=w!P!Q$b!Q![#=m![!]!=w!^!b$b!b!c)x!c!v!!i!v!w#=m!w!|!!i!|!}#=m!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#r#@g]#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^#o(n#o#p#A`#p;'S(n;'S;=`)l<%lO(n#r#AmZn!l#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^;'S(n;'S;=`)l<%lO(n!_#Bk]#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^#q(n#q#r#Cd#r;'S(n;'S;=`)l<%lO(n!_#CqZmP#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^;'S(n;'S;=`)l<%lO(n", - tokenizers: [0, 1, 2, 3, 4, 5, 6, 7], - topRules: {"Document":[0,2]}, - specialized: [{term: 81, get: (value) => spec_word[value] || -1}], - tokenPrec: 351 -}) diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js deleted file mode 100644 index 7f9f96d..0000000 --- a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js +++ /dev/null @@ -1,60 +0,0 @@ -// This file was generated by lezer-generator. You probably shouldn't edit it. -export const - spaces = 78, - newline = 79, - LineComment = 1, - Document = 2, - VariableDeclaration = 4, - VariableName = 6, - VariableValue = 8, - ResponseLine = 9, - ResponseKeyword = 10, - RESPONSE = 11, - StatusCode = 12, - Number = 13, - StatusText = 14, - Size = 15, - Timestamp = 16, - RequestLine = 17, - Method = 18, - GET = 19, - POST = 20, - PUT = 21, - DELETE = 22, - PATCH = 23, - HEAD = 24, - OPTIONS = 25, - CONNECT = 26, - TRACE = 27, - Url = 28, - TemplateExpression = 31, - MemberExpression = 32, - PropertyName = 34, - FunctionCall = 37, - FunctionName = 38, - String = 40, - HttpVersion = 42, - HeaderStatement = 43, - HeaderKeyword = 44, - HEADER = 45, - HeaderName = 46, - HeaderValue = 48, - BodyStatement = 49, - BodyKeyword = 50, - BODY = 51, - BodyType = 52, - JSON = 53, - FORM = 54, - URLENCODED = 55, - GRAPHQL = 56, - XML = 57, - TEXT = 58, - BINARY = 59, - MULTIPART = 60, - BodyContent = 61, - FileReference = 62, - FilePath = 64, - VariablesStatement = 65, - VariablesKeyword = 66, - VARIABLES = 67, - VariablesContent = 68 diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.test.ts b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.test.ts new file mode 100644 index 0000000..29d1a9b --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.test.ts @@ -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); + }); +}); + diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.highlight.ts b/frontend/src/views/editor/extensions/httpclient/language/http.highlight.ts new file mode 100644 index 0000000..037d9d3 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.highlight.ts @@ -0,0 +1,67 @@ +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), + + // @res - 使用命名空间(紫色系) + "ResKeyword": t.namespace, + + // @ 符号本身 - 使用元标记 + "AtKeyword": t.meta, + + // ========== URL(特殊处理)========== + // URL 节点 - 使用链接颜色 + "Url": t.link, + + // ========== 属性和值 ========== + // 属性名 - 使用定义名称 + "PropertyName": t.definition(t.attributeName), + + // 普通标识符值 - 使用常量名 + "identifier": t.constant(t.variableName), + + // ========== 字面量 ========== + // 数字 - 数字颜色 + "NumberLiteral": t.number, + + // 字符串 - 字符串颜色 + "StringLiteral": t.string, + + // 单位 - 单位颜色 + "Unit": t.unit, + + // ========== 注释 ========== + // # 单行注释 - 行注释颜色 + "LineComment": t.lineComment, + + // ========== 标点符号 ========== + // 冒号 - 分隔符 + ":": t.separator, + + // 逗号 - 分隔符 + ",": t.separator, + + // 花括号 - 大括号 + "{ }": t.brace, +}) diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.parser.terms.ts b/frontend/src/views/editor/extensions/httpclient/language/http.parser.terms.ts new file mode 100644 index 0000000..a1c3321 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.parser.terms.ts @@ -0,0 +1,35 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + LineComment = 1, + Document = 2, + RequestStatement = 3, + Method = 4, + GET = 5, + POST = 6, + PUT = 7, + DELETE = 8, + PATCH = 9, + HEAD = 10, + OPTIONS = 11, + CONNECT = 12, + TRACE = 13, + Url = 14, + StringLiteral = 15, + Block = 18, + Property = 19, + NumberLiteral = 22, + Unit = 23, + AtRule = 25, + JsonRule = 26, + AtKeyword = 27, + JsonKeyword = 28, + JsonBlock = 29, + JsonProperty = 30, + FormDataRule = 32, + FormDataKeyword = 33, + UrlEncodedRule = 34, + UrlEncodedKeyword = 35, + TextRule = 36, + TextKeyword = 37, + ResponseRule = 38, + ResKeyword = 39 diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.parser.ts b/frontend/src/views/editor/extensions/httpclient/language/http.parser.ts new file mode 100644 index 0000000..9a536e1 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.parser.ts @@ -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_identifier = {__proto__:null,GET:10, POST:12, PUT:14, DELETE:16, PATCH:18, HEAD:20, OPTIONS:22, CONNECT:24, TRACE:26} +const spec_AtKeyword = {__proto__:null,"@json":56, "@formdata":66, "@urlencoded":70, "@text":74, "@res":78} +export const parser = LRParser.deserialize({ + version: 14, + states: "'nQYQPOOOOQO'#C`'#C`O!WQPO'#CvO!WQPO'#C|O!WQPO'#DOO!WQPO'#DQO!WQPO'#DSOOQO'#Cu'#CuO!]QPO'#C_OOQO'#D['#D[OOQO'#DU'#DUQYQPOOO!bQPO'#CyOOQO,59b,59bOOQO,59h,59hOOQO,59j,59jOOQO,59l,59lOOQO,59n,59nOOQO'#Cj'#CjO!jQPO,58yOOQO-E7S-E7SOOQO'#C{'#C{O!oQPO'#CzO!tQPO'#DaOOQO,59e,59eO!|QPO,59eO#gQPO'#CnOOQO1G.e1G.eO#nQPO,59fO#|QPO,59{O$UQPO,59{OOQO1G/P1G/POOQO'#Cp'#CpO$^QPO'#CoOOQO'#DV'#DVO$cQPO'#D^O$jQPO,59YO$oQPO'#CrOOQO'#Db'#DbOOQO1G/Q1G/QOOQO,59r,59rO%^QPO1G/gOOQO-E7U-E7UO%fQPO,59ZOOQO-E7T-E7TOOQO1G.t1G.tOOQO,59^,59^P!eQPO'#DWOOQO'#D_'#D_O%tQPO1G.uOOQO7+$a7+$a", + stateData: "&c~O}OSPOS~OTPOUPOVPOWPOXPOYPOZPO[PO]POlQOqROsSOuTOwUO~Oa[O~O_bO~O`hO!PeO~OajO~OelO~OhmO`!TX~O`oO~OlQOqROsSOuTOwUO!PpO~O`!QP~P#RO_vOa[O!PvO!SuO~O!PeO`!Ta~OhyO`!Ta~Oe{O~O`!QX~P#RO`}O~Og!OO`fXhfXlfXqfXsfXufXwfX!PfX~O!PeO`!Ti~O_!QOajO!P!QO!SuO~Oh!SO`cilciqcisciuciwci!Pci~O!Pg~", + goto: "$k!VPPP!W![PPPPPPPPP!`PPP!c!i!mP!qPP!w#PPP#V#i#q#PP#PP#PP#PP#w#}$TPPP$ZP$_$bP$e$hTXOZTWOZRcWQkcR!Q{TrjsTqjsQvlR!Q{SXOZTrjsXVOZjsQ]QQ^RQ_SQ`TQaURvlQg[Vxmy!PXf[my!PQZORdZQsjR|sQngRznTYOZRtjR!R{Ri[Rwl", + nodeNames: "⚠ LineComment Document RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url StringLiteral } { Block Property PropertyName : NumberLiteral Unit , AtRule JsonRule AtKeyword JsonKeyword JsonBlock JsonProperty PropertyName FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ResponseRule ResKeyword", + maxTerm: 52, + nodeProps: [ + ["isolate", 15,""], + ["openedBy", 16,"{"], + ["closedBy", 17,"}"] + ], + propSources: [httpHighlighting], + skippedNodes: [0,1,27], + repeatNodeCount: 3, + tokenData: "+[~RjX^!spq!srs#hst%[tu%swx&[{|'y|})e}!O'y!O!P(S!Q![)S![!])j!b!c)o!c!}*g#R#S%s#T#o*g#o#p+Q#q#r+V#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~!xY}~X^!spq!s#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~#kWOY#hZr#hrs$Ts#O#h#O#P$Y#P;'S#h;'S;=`%U<%lO#h~$YO_~~$]RO;'S#h;'S;=`$f;=`O#h~$iXOY#hZr#hrs$Ts#O#h#O#P$Y#P;'S#h;'S;=`%U;=`<%l#h<%lO#h~%XP;=`<%l#h~%aSP~OY%[Z;'S%[;'S;=`%m<%lO%[~%pP;=`<%l%[~%xU!P~tu%s}!O%s!Q![%s!c!}%s#R#S%s#T#o%s~&_WOY&[Zw&[wx$Tx#O&[#O#P&w#P;'S&[;'S;=`'s<%lO&[~&zRO;'S&[;'S;=`'T;=`O&[~'WXOY&[Zw&[wx$Tx#O&[#O#P&w#P;'S&[;'S;=`'s;=`<%l&[<%lO&[~'vP;=`<%l&[~'|Q!O!P(S!Q![)S~(VP!Q![(Y~(_R!S~!Q![(Y!g!h(h#X#Y(h~(kR{|(t}!O(t!Q![(z~(wP!Q![(z~)PP!S~!Q![(z~)XS!S~!O!P(Y!Q![)S!g!h(h#X#Y(h~)jOh~~)oOe~~)rR}!O){!c!}*U#T#o*U~*OQ!c!}*U#T#o*U~*ZSk~}!O*U!Q![*U!c!}*U#T#o*U~*nU!P~g~tu%s}!O%s!Q![%s!c!}*g#R#S%s#T#o*g~+VOa~~+[O`~", + tokenizers: [0], + topRules: {"Document":[0,2]}, + specialized: [{term: 47, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1},{term: 27, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1}], + tokenPrec: 246 +}) diff --git a/frontend/src/views/editor/extensions/httpclient/language/index.ts b/frontend/src/views/editor/extensions/httpclient/language/index.ts index 6f0e7f5..ccb9355 100644 --- a/frontend/src/views/editor/extensions/httpclient/language/index.ts +++ b/frontend/src/views/editor/extensions/httpclient/language/index.ts @@ -1,3 +1,28 @@ -export { http, httpLanguage, httpHighlighting } from './http-language'; -export { parser } from './http.grammar.js'; +/** + * HTTP Client 语言支持 + * + * 基于 CSS 语法结构的简化 HTTP 请求语言 + * + * 语法示例: + * ```http + * POST http://127.0.0.1:80/api/create { + * host: "https://api.example.com"; + * content-type: "application/json"; + * + * @json { + * name: "xxx"; + * } + * + * @res { + * code: 200; + * status: "ok"; + * } + * } + * ``` + */ +export { http, httpLanguage, httpHighlighting } from './http-language'; +export { parser } from './http.parser'; + +// 类型定义 +export type { LRLanguage } from '@codemirror/language'; diff --git a/frontend/src/views/editor/extensions/httpclient/parser/request-parser.test.ts b/frontend/src/views/editor/extensions/httpclient/parser/request-parser.test.ts new file mode 100644 index 0000000..a1f0873 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/parser/request-parser.test.ts @@ -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' }); + }); + }); +}); + diff --git a/frontend/src/views/editor/extensions/httpclient/parser/request-parser.ts b/frontend/src/views/editor/extensions/httpclient/parser/request-parser.ts new file mode 100644 index 0000000..6055997 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/parser/request-parser.ts @@ -0,0 +1,355 @@ +import { EditorState } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import type { SyntaxNode } from '@lezer/common'; + +/** + * HTTP 请求模型 + */ +export interface HttpRequest { + /** 请求方法 */ + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; + + /** 请求 URL */ + url: string; + + /** 请求头 */ + headers: Record; + + /** 请求体类型 */ + 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', +} as const; + +/** + * HTTP 请求解析器 + */ +export class HttpRequestParser { + constructor(private state: EditorState) {} + + /** + * 解析指定位置的 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 = {}; + 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); + // 移除引号 + return urlText.replace(/^["']|["']$/g, ''); + } + + /** + * 解析 Block 节点(包含 headers 和 body) + */ + private parseBlock( + node: SyntaxNode, + headers: Record, + 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 = { + [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, @formdata, @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) + 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.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; + } + } + + return { name, value }; + } + + /** + * 解析值节点(字符串、数字、标识符、嵌套块) + */ + private parseValue(node: SyntaxNode): any { + if (node.name === NODE_TYPES.STRING_LITERAL) { + const text = this.getNodeText(node); + // 移除引号 + return text.replace(/^["']|["']$/g, ''); + } else if (node.name === NODE_TYPES.NUMBER_LITERAL) { + const text = this.getNodeText(node); + return parseFloat(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 请求 + */ +export function parseHttpRequest(state: EditorState, pos: number): HttpRequest | null { + const parser = new HttpRequestParser(state); + return parser.parseRequestAt(pos); +} + diff --git a/frontend/src/views/editor/extensions/httpclient/types.ts b/frontend/src/views/editor/extensions/httpclient/types.ts deleted file mode 100644 index 8b13789..0000000 --- a/frontend/src/views/editor/extensions/httpclient/types.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts b/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts index 41ebe4d..7c20624 100644 --- a/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts +++ b/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts @@ -1,27 +1,115 @@ import { EditorView, GutterMarker, gutter } from '@codemirror/view'; +import { StateField } from '@codemirror/state'; import { syntaxTree } from '@codemirror/language'; -import { getNoteBlockFromPos } from '../../codeblock/state'; +import { blockState } from '../../codeblock/state'; import type { SyntaxNode } from '@lezer/common'; +import { parseHttpRequest, type HttpRequest } from '../parser/request-parser'; -// ==================== 常量定义 ==================== +/** + * 语法树节点类型常量 + */ +const NODE_TYPES = { + REQUEST_STATEMENT: 'RequestStatement', + METHOD: 'Method', + URL: 'Url', + BLOCK: 'Block', +} as const; -/** 支持的 HTTP 方法(小写) - 使用 Set 以提高查找性能 */ -const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'connect', 'trace']); +/** + * 有效的 HTTP 方法列表 + */ +const VALID_HTTP_METHODS = new Set([ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + 'HEAD', + 'OPTIONS', + 'CONNECT', + 'TRACE' +]); -/** 匹配 ### Request 标记的正则表达式 */ -const REQUEST_MARKER_REGEX = /^###\s+Request(?:\s|$)/i; +/** + * HTTP 请求缓存信息 + */ +interface CachedHttpRequest { + lineNumber: number; // 行号(用于快速查找) + position: number; // 字符位置(用于解析) + request: HttpRequest; // 完整的解析结果 +} -/** 匹配 ### Response 标记的正则表达式 */ -const RESPONSE_MARKER_REGEX = /^###\s+Response/i; +/** + * 预解析所有 HTTP 块中的请求 + * 只在文档改变时调用,结果缓存在 StateField 中 + * + * 优化:一次遍历完成验证和解析,避免重复工作 + */ +function parseHttpRequests(state: any): Map { + const requestsMap = new Map(); + 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); + + 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; +} -/** 匹配 HTTP 方法的正则表达式 */ -const HTTP_METHOD_REGEX = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+/i; - -/** HTTP 方法在行首的最大偏移位置(字符数) */ -const MAX_METHOD_POSITION_OFFSET = 20; - -/** 向上查找 ### Request 标记的最大行数 */ -const MAX_REQUEST_MARKER_DISTANCE = 10; +/** + * StateField:缓存所有 HTTP 请求的完整解析结果 + * 只在文档改变时重新解析 + */ +const httpRequestsField = StateField.define>({ + create(state) { + return parseHttpRequests(state); + }, + + update(requests, transaction) { + // 只有文档改变或缓存为空时才重新解析 + if (transaction.docChanged || requests.size === 0) { + return parseHttpRequests(transaction.state); + } + return requests; + } +}); // ==================== 运行按钮 Marker ==================== @@ -29,7 +117,10 @@ const MAX_REQUEST_MARKER_DISTANCE = 10; * 运行按钮 Gutter Marker */ class RunButtonMarker extends GutterMarker { - constructor(private readonly linePosition: number) { + constructor( + private readonly lineNumber: number, + private readonly cachedRequest: HttpRequest + ) { super(); } @@ -45,124 +136,46 @@ class RunButtonMarker extends GutterMarker { e.stopPropagation(); this.executeRequest(view); }; - + return button; } - - private async executeRequest(view: EditorView) { console.log(`\n============ 执行 HTTP 请求 ============`); - console.log(`位置: ${this.linePosition}`); - + console.log('行号:', this.lineNumber); + + // 直接使用缓存的解析结果,无需重新解析! + console.log('解析结果:', JSON.stringify(this.cachedRequest, null, 2)); + + // TODO: 调用后端 API 执行请求 + // const response = await executeHttpRequest(this.cachedRequest); + // renderResponse(response); } } -/** - * 使用语法树检查一行是否是 HTTP 请求行(更可靠) - * 必须符合规则:前面有 ### Request,然后才是 GET/POST 等请求行 - */ -function isRequestLineInSyntaxTree(view: EditorView, lineFrom: number, lineTo: number): boolean { - const tree = syntaxTree(view.state); - let hasHttpMethod = false; - - // 遍历该行的语法树节点 - tree.iterate({ - from: lineFrom, - to: lineTo, - enter: (node: SyntaxNode) => { - // HTTP 解析器将 HTTP 方法(GET、POST 等)标记为 "keyword" - // 并且该节点应该在行首附近 - if (node.name === 'keyword' && - node.from >= lineFrom && - node.from < lineFrom + MAX_METHOD_POSITION_OFFSET) { - const text = view.state.sliceDoc(node.from, node.to); - if (HTTP_METHODS.has(text.toLowerCase())) { - // 检查前面是否有 ### Request 标记 - if (hasPrecedingRequestMarker(view, lineFrom)) { - hasHttpMethod = true; - } - } - } - } - }); - - return hasHttpMethod; -} -/** - * 检查前面是否有 ### Request 标记 - * 只要包含 "### Request",后面可以跟任何描述文字 - */ -function hasPrecedingRequestMarker(view: EditorView, lineFrom: number): boolean { - const currentLineNum = view.state.doc.lineAt(lineFrom).number; - - // 向上查找前面的几行(最多往上找指定行数) - for (let i = currentLineNum - 1; - i >= Math.max(1, currentLineNum - MAX_REQUEST_MARKER_DISTANCE); - i--) { - const line = view.state.doc.line(i); - const lineText = view.state.sliceDoc(line.from, line.to).trim(); - - if (REQUEST_MARKER_REGEX.test(lineText)) { - return true; - } - - // 如果遇到 ### Response,停止查找 - if (RESPONSE_MARKER_REGEX.test(lineText)) { - return false; - } - - // 如果是空行,继续往上找 - if (lineText === '') { - continue; - } - - // 如果遇到另一个请求方法,停止查找 - if (HTTP_METHOD_REGEX.test(lineText)) { - return false; - } - } - - return false; -} - -/** - * 检查位置是否在 HTTP 块内 - */ -function isInHttpBlock(view: EditorView, pos: number): boolean { - try { - const block = getNoteBlockFromPos(view.state, pos); - return block?.language.name === 'http' || block?.language.name === 'rest'; - } catch { - return false; - } -} /** * 创建运行按钮 Gutter */ export const httpRunButtonGutter = gutter({ class: 'cm-http-gutter', - - // 为每一行决定是否显示 marker - lineMarker(view, line) { - const linePos = line.from; - - // 第一步:检查是否在 HTTP 块内 - if (!isInHttpBlock(view, linePos)) { - return null; - } - - // 第二步:使用语法树检查是否是请求行 - if (!isRequestLineInSyntaxTree(view, line.from, line.to)) { - return null; - } - - // 创建运行按钮 - return new RunButtonMarker(linePos); - }, + 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.lineNumber, cached.request); + }, }); export const httpRunButtonTheme = EditorView.baseTheme({ @@ -183,7 +196,7 @@ export const httpRunButtonTheme = EditorView.baseTheme({ padding: '0', transition: 'color 0.15s ease', }, - + // 悬停效果 '.cm-http-run-button:hover': { color: '#45a049', // 深绿色 @@ -197,3 +210,5 @@ export const httpRunButtonTheme = EditorView.baseTheme({ }, }); +// 导出 StateField 供扩展系统使用 +export { httpRequestsField }; \ No newline at end of file