🚧 Optimized HTTP language parser
This commit is contained in:
@@ -26,20 +26,20 @@ try {
|
|||||||
|
|
||||||
// 运行 lezer-generator
|
// 运行 lezer-generator
|
||||||
console.log('⚙️ building parser...');
|
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,
|
cwd: __dirname,
|
||||||
stdio: 'inherit'
|
stdio: 'inherit'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查生成的文件
|
// 检查生成的文件
|
||||||
const parserFile = path.join(__dirname, 'parser.js');
|
const parserFile = path.join(__dirname, 'parser.ts');
|
||||||
const termsFile = path.join(__dirname, 'parser.terms.js');
|
const termsFile = path.join(__dirname, 'parser.terms.ts');
|
||||||
|
|
||||||
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
||||||
console.log('✅ parser file successfully generated!');
|
console.log('✅ parser file successfully generated!');
|
||||||
console.log('📦 parser files:');
|
console.log('📦 parser files:');
|
||||||
console.log(' - parser.js');
|
console.log(' - parser.ts');
|
||||||
console.log(' - parser.terms.js');
|
console.log(' - parser.terms.ts');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('failed to generate parser');
|
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 { configureNesting } from "./nested-parser";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ExternalTokenizer } from "@lezer/lr";
|
import { ExternalTokenizer } from "@lezer/lr";
|
||||||
import { BlockContent } from "./parser.terms.js";
|
import { BlockContent } from "./parser.terms";
|
||||||
import { LANGUAGES } from "./languages";
|
import { LANGUAGES } from "./languages";
|
||||||
|
|
||||||
const EOF = -1;
|
const EOF = -1;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export {
|
|||||||
} from './nested-parser';
|
} from './nested-parser';
|
||||||
|
|
||||||
// 解析器术语
|
// 解析器术语
|
||||||
export * from './parser.terms.js';
|
export * from './parser.terms';
|
||||||
|
|
||||||
// 外部标记器
|
// 外部标记器
|
||||||
export {
|
export {
|
||||||
@@ -34,4 +34,4 @@ export {
|
|||||||
// 解析器
|
// 解析器
|
||||||
export {
|
export {
|
||||||
parser
|
parser
|
||||||
} from './parser.js';
|
} from './parser';
|
||||||
@@ -86,7 +86,7 @@ export class LanguageInfo {
|
|||||||
* 支持的语言列表
|
* 支持的语言列表
|
||||||
*/
|
*/
|
||||||
export const LANGUAGES: LanguageInfo[] = [
|
export const LANGUAGES: LanguageInfo[] = [
|
||||||
new LanguageInfo("text", "Plain Text", null),
|
new LanguageInfo("text", "Text", null),
|
||||||
new LanguageInfo("json", "JSON", jsonLanguage.parser, ["json"], {
|
new LanguageInfo("json", "JSON", jsonLanguage.parser, ["json"], {
|
||||||
parser: "json",
|
parser: "json",
|
||||||
plugins: [babelPrettierPlugin, prettierPluginEstree]
|
plugins: [babelPrettierPlugin, prettierPluginEstree]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { parseMixed } from "@lezer/common";
|
import { parseMixed } from "@lezer/common";
|
||||||
import { BlockContent, BlockLanguage } from "./parser.terms.js";
|
import { BlockContent, BlockLanguage } from "./parser.terms";
|
||||||
import { languageMapping } from "./languages";
|
import { languageMapping } from "./languages";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import {Extension} from '@codemirror/state';
|
import {Extension} from '@codemirror/state';
|
||||||
|
|
||||||
import {httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter';
|
import {httpRunButtonGutter, httpRunButtonTheme, httpRequestsField} from './widgets/run-gutter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 HTTP Client 扩展
|
* 创建 HTTP Client 扩展
|
||||||
@@ -16,7 +16,10 @@ export function createHttpClientExtension(): Extension[] {
|
|||||||
// HTTP 语言解析器
|
// HTTP 语言解析器
|
||||||
// extensions.push(httpLanguage);
|
// extensions.push(httpLanguage);
|
||||||
|
|
||||||
// 运行按钮 Gutte
|
// StateField:缓存 HTTP 请求解析结果
|
||||||
|
extensions.push(httpRequestsField);
|
||||||
|
|
||||||
|
// 运行按钮 Gutter
|
||||||
extensions.push(httpRunButtonGutter);
|
extensions.push(httpRunButtonGutter);
|
||||||
extensions.push(httpRunButtonTheme);
|
extensions.push(httpRunButtonTheme);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP Grammar Parser Builder
|
* HTTP Grammar Parser Builder
|
||||||
* 编译 Lezer grammar 文件为 JavaScript parser
|
* 编译 Lezer grammar 文件为 TypeScript parser
|
||||||
* 使用命令行方式编译
|
* 使用 --typeScript 选项生成 .ts 文件
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'url';
|
|||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
console.log('🚀 开始编译 HTTP grammar parser...');
|
console.log('🚀 开始编译 HTTP grammar parser (TypeScript)...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查语法文件是否存在
|
// 检查语法文件是否存在
|
||||||
@@ -24,22 +24,22 @@ try {
|
|||||||
|
|
||||||
console.log('📄 语法文件:', grammarFile);
|
console.log('📄 语法文件:', grammarFile);
|
||||||
|
|
||||||
// 运行 lezer-generator
|
// 运行 lezer-generator with TypeScript output
|
||||||
console.log('⚙️ 编译 parser...');
|
console.log('⚙️ 编译 parser (生成 TypeScript)...');
|
||||||
execSync('npx lezer-generator http.grammar -o http.grammar.js', {
|
execSync('npx lezer-generator http.grammar -o http.parser.ts --typeScript', {
|
||||||
cwd: __dirname,
|
cwd: __dirname,
|
||||||
stdio: 'inherit'
|
stdio: 'inherit'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查生成的文件
|
// 检查生成的文件
|
||||||
const parserFile = path.join(__dirname, 'http.grammar.js');
|
const parserFile = path.join(__dirname, 'http.parser.ts');
|
||||||
const termsFile = path.join(__dirname, 'http.grammar.terms.js');
|
const termsFile = path.join(__dirname, 'http.parser.terms.ts');
|
||||||
|
|
||||||
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
|
||||||
console.log('✅ Parser 文件成功生成!');
|
console.log('✅ Parser 文件成功生成!');
|
||||||
console.log('📦 生成的文件:');
|
console.log('📦 生成的文件:');
|
||||||
console.log(' - http.grammar.js');
|
console.log(' - http.parser.ts');
|
||||||
console.log(' - http.grammar.terms.js');
|
console.log(' - http.parser.terms.ts');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Parser 生成失败');
|
throw new Error('Parser 生成失败');
|
||||||
}
|
}
|
||||||
@@ -50,4 +50,3 @@ try {
|
|||||||
console.error('❌ 编译失败:', error.message);
|
console.error('❌ 编译失败:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
|
import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
|
||||||
import { parser } from './http.grammar.js';
|
import { CompletionContext } from '@codemirror/autocomplete';
|
||||||
import { httpHighlighting } from './http-highlight';
|
import { parser } from './http.parser';
|
||||||
|
import { httpHighlighting } from './http.highlight';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP Client 语言定义
|
* HTTP Client 语言定义
|
||||||
@@ -12,18 +13,19 @@ const httpParserWithMetadata = parser.configure({
|
|||||||
// 应用语法高亮
|
// 应用语法高亮
|
||||||
httpHighlighting,
|
httpHighlighting,
|
||||||
|
|
||||||
// 折叠规则:允许折叠多行 Body、Variables、Headers 等
|
// 折叠规则:允许折叠块结构
|
||||||
foldNodeProp.add({
|
foldNodeProp.add({
|
||||||
BodyStatement: foldInside,
|
RequestStatement: foldInside,
|
||||||
VariablesStatement: foldInside,
|
Block: foldInside,
|
||||||
|
AtRule: foldInside,
|
||||||
Document: foldInside,
|
Document: foldInside,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 缩进规则
|
// 缩进规则
|
||||||
indentNodeProp.add({
|
indentNodeProp.add({
|
||||||
BodyStatement: () => 2,
|
Block: () => 2,
|
||||||
HeaderStatement: () => 0,
|
Declaration: () => 0,
|
||||||
VariableDeclaration: () => 0,
|
AtRule: () => 0,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -32,10 +34,8 @@ const httpParserWithMetadata = parser.configure({
|
|||||||
export const httpLanguage = LRLanguage.define({
|
export const httpLanguage = LRLanguage.define({
|
||||||
parser: httpParserWithMetadata,
|
parser: httpParserWithMetadata,
|
||||||
languageData: {
|
languageData: {
|
||||||
// 注释配置
|
|
||||||
commentTokens: { line: '#' },
|
|
||||||
|
|
||||||
// 自动闭合括号
|
//自动闭合括号
|
||||||
closeBrackets: { brackets: ['(', '[', '{', '"', "'"] },
|
closeBrackets: { brackets: ['(', '[', '{', '"', "'"] },
|
||||||
|
|
||||||
// 单词字符定义
|
// 单词字符定义
|
||||||
@@ -45,7 +45,7 @@ export const httpLanguage = LRLanguage.define({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP Client 语言支持
|
* HTTP Client 语言支持
|
||||||
* 包含语法高亮、折叠、缩进等完整功能
|
* 包含语法高亮、折叠、缩进、自动补全等完整功能
|
||||||
*/
|
*/
|
||||||
export function http() {
|
export function http() {
|
||||||
return new LanguageSupport(httpLanguage, [
|
return new LanguageSupport(httpLanguage, [
|
||||||
@@ -58,7 +58,7 @@ export function http() {
|
|||||||
/**
|
/**
|
||||||
* HTTP Client 自动补全
|
* HTTP Client 自动补全
|
||||||
*/
|
*/
|
||||||
function httpCompletion(context: any) {
|
function httpCompletion(context: CompletionContext) {
|
||||||
const word = context.matchBefore(/\w*/);
|
const word = context.matchBefore(/\w*/);
|
||||||
if (!word || (word.from === word.to && !context.explicit)) {
|
if (!word || (word.from === word.to && !context.explicit)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -76,49 +76,29 @@ function httpCompletion(context: any) {
|
|||||||
{ label: 'HEAD', type: 'keyword', detail: 'HTTP Method' },
|
{ label: 'HEAD', type: 'keyword', detail: 'HTTP Method' },
|
||||||
{ label: 'OPTIONS', type: 'keyword', detail: 'HTTP Method' },
|
{ label: 'OPTIONS', type: 'keyword', detail: 'HTTP Method' },
|
||||||
|
|
||||||
// 关键字
|
// @ 规则
|
||||||
{ label: 'HEADER', type: 'keyword', detail: 'Header Statement' },
|
{ label: '@json', type: 'keyword', detail: 'Body Type' },
|
||||||
{ label: 'BODY', type: 'keyword', detail: 'Body Statement' },
|
{ label: '@formdata', type: 'keyword', detail: 'Body Type' },
|
||||||
{ label: 'VARIABLES', type: 'keyword', detail: 'Variables Statement' },
|
{ label: '@urlencoded', type: 'keyword', detail: 'Body Type' },
|
||||||
|
{ label: '@text', type: 'keyword', detail: 'Body Type' },
|
||||||
// Body 类型
|
{ label: '@res', type: 'keyword', detail: 'Response' },
|
||||||
{ 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' },
|
|
||||||
|
|
||||||
// 常用 Headers
|
// 常用 Headers
|
||||||
{ label: 'Content-Type', type: 'property', detail: 'Header Name' },
|
{ label: 'content-type', type: 'property', detail: 'Header' },
|
||||||
{ label: 'Authorization', type: 'property', detail: 'Header Name' },
|
{ label: 'authorization', type: 'property', detail: 'Header' },
|
||||||
{ label: 'Accept', type: 'property', detail: 'Header Name' },
|
{ label: 'accept', type: 'property', detail: 'Header' },
|
||||||
{ label: 'User-Agent', type: 'property', detail: 'Header Name' },
|
{ label: 'user-agent', type: 'property', detail: 'Header' },
|
||||||
{ label: 'Cookie', type: 'property', detail: 'Header Name' },
|
{ label: 'host', type: 'property', detail: 'Header' },
|
||||||
|
|
||||||
// 常用 Content-Type
|
// 常用 Content-Type
|
||||||
{ label: 'application/json', type: 'constant', detail: 'Content Type' },
|
{ label: '"application/json"', type: 'constant', detail: 'Content Type' },
|
||||||
{ label: 'application/xml', type: 'constant', detail: 'Content Type' },
|
{ label: '"text/plain"', type: 'constant', detail: 'Content Type' },
|
||||||
{ label: 'text/html', type: 'constant', detail: 'Content Type' },
|
{ label: '"multipart/form-data"', type: 'constant', detail: 'Content Type' },
|
||||||
{ label: 'text/plain', type: 'constant', detail: 'Content Type' },
|
{ label: '"application/x-www-form-urlencoded"', 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: 'true', type: 'constant', detail: 'Boolean' },
|
||||||
{ label: '@file', type: 'keyword', detail: 'File Reference' },
|
{ label: 'false', type: 'constant', detail: 'Boolean' },
|
||||||
|
|
||||||
// 内置函数
|
|
||||||
{ 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' },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -127,4 +107,3 @@ function httpCompletion(context: any) {
|
|||||||
* 导出语言定义和高亮配置
|
* 导出语言定义和高亮配置
|
||||||
*/
|
*/
|
||||||
export { httpHighlighting };
|
export { httpHighlighting };
|
||||||
|
|
||||||
|
|||||||
@@ -1,267 +1,214 @@
|
|||||||
@precedence {
|
// HTTP Client Grammar
|
||||||
member,
|
//
|
||||||
call
|
// 语法规则:
|
||||||
|
// 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* }
|
// HTTP 请求 - URL 必须是字符串
|
||||||
|
RequestStatement {
|
||||||
statement {
|
Method Url Block
|
||||||
VariableDeclaration |
|
|
||||||
ResponseLine |
|
|
||||||
RequestLine |
|
|
||||||
HeaderStatement |
|
|
||||||
BodyStatement |
|
|
||||||
VariablesStatement
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 变量定义 ====================
|
|
||||||
|
|
||||||
VariableDeclaration {
|
|
||||||
"@" VariableName "=" VariableValue ";"
|
|
||||||
}
|
|
||||||
|
|
||||||
VariableName { word }
|
|
||||||
|
|
||||||
VariableValue[isolate] { anyContent }
|
|
||||||
|
|
||||||
// ==================== 请求行 ====================
|
|
||||||
|
|
||||||
RequestLine {
|
|
||||||
Method Url HttpVersion? ";"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Method {
|
Method {
|
||||||
@specialize[@name="GET"]<word, "GET"> |
|
@specialize[@name=GET]<identifier, "GET"> |
|
||||||
@specialize[@name="POST"]<word, "POST"> |
|
@specialize[@name=POST]<identifier, "POST"> |
|
||||||
@specialize[@name="PUT"]<word, "PUT"> |
|
@specialize[@name=PUT]<identifier, "PUT"> |
|
||||||
@specialize[@name="DELETE"]<word, "DELETE"> |
|
@specialize[@name=DELETE]<identifier, "DELETE"> |
|
||||||
@specialize[@name="PATCH"]<word, "PATCH"> |
|
@specialize[@name=PATCH]<identifier, "PATCH"> |
|
||||||
@specialize[@name="HEAD"]<word, "HEAD"> |
|
@specialize[@name=HEAD]<identifier, "HEAD"> |
|
||||||
@specialize[@name="OPTIONS"]<word, "OPTIONS"> |
|
@specialize[@name=OPTIONS]<identifier, "OPTIONS"> |
|
||||||
@specialize[@name="CONNECT"]<word, "CONNECT"> |
|
@specialize[@name=CONNECT]<identifier, "CONNECT"> |
|
||||||
@specialize[@name="TRACE"]<word, "TRACE">
|
@specialize[@name=TRACE]<identifier, "TRACE">
|
||||||
}
|
}
|
||||||
|
|
||||||
Url { urlPart+ }
|
// URL 必须是字符串
|
||||||
|
Url { StringLiteral }
|
||||||
|
|
||||||
urlPart { urlContent | TemplateExpression }
|
// @ 规则(支持多种请求体格式)
|
||||||
|
AtRule {
|
||||||
HttpVersion { httpVersionToken }
|
JsonRule |
|
||||||
|
FormDataRule |
|
||||||
// ==================== Header 语句 ====================
|
UrlEncodedRule |
|
||||||
|
TextRule |
|
||||||
HeaderStatement {
|
ResponseRule
|
||||||
HeaderKeyword HeaderName colon HeaderValue ";"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
colon { ":" }
|
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
||||||
|
JsonRule {
|
||||||
HeaderKeyword { @specialize[@name="HEADER"]<word, "HEADER"> }
|
@specialize[@name=JsonKeyword]<AtKeyword, "@json">
|
||||||
|
JsonBlock
|
||||||
HeaderName { word }
|
|
||||||
|
|
||||||
HeaderValue { headerValuePart* }
|
|
||||||
|
|
||||||
headerValuePart { headerValueContent | TemplateExpression }
|
|
||||||
|
|
||||||
// ==================== Body 语句 ====================
|
|
||||||
|
|
||||||
BodyStatement {
|
|
||||||
BodyKeyword BodyType BodyContent ";"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BodyKeyword { @specialize[@name="BODY"]<word, "BODY"> }
|
// @formdata 块:表单数据格式(属性必须用逗号分隔)
|
||||||
|
FormDataRule {
|
||||||
BodyType {
|
@specialize[@name=FormDataKeyword]<AtKeyword, "@formdata">
|
||||||
@specialize[@name="JSON"]<word, "JSON"> |
|
JsonBlock
|
||||||
@specialize[@name="FORM"]<word, "FORM"> |
|
|
||||||
@specialize[@name="URLENCODED"]<word, "URLENCODED"> |
|
|
||||||
@specialize[@name="GRAPHQL"]<word, "GRAPHQL"> |
|
|
||||||
@specialize[@name="XML"]<word, "XML"> |
|
|
||||||
@specialize[@name="TEXT"]<word, "TEXT"> |
|
|
||||||
@specialize[@name="BINARY"]<word, "BINARY"> |
|
|
||||||
@specialize[@name="MULTIPART"]<word, "MULTIPART">
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BodyContent { bodyContentPart* }
|
// @urlencoded 块:URL 编码格式(属性必须用逗号分隔)
|
||||||
|
UrlEncodedRule {
|
||||||
bodyContentPart { bodyText | TemplateExpression | FileReference }
|
@specialize[@name=UrlEncodedKeyword]<AtKeyword, "@urlencoded">
|
||||||
|
JsonBlock
|
||||||
FileReference {
|
|
||||||
"@file" FilePath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FilePath { filePathContent }
|
// @text 块:纯文本请求体(使用 content 字段)
|
||||||
|
TextRule {
|
||||||
// ==================== Variables 语句 ====================
|
@specialize[@name=TextKeyword]<AtKeyword, "@text">
|
||||||
|
JsonBlock
|
||||||
VariablesStatement {
|
|
||||||
VariablesKeyword VariablesContent ";"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VariablesKeyword { @specialize[@name="VARIABLES"]<word, "VARIABLES"> }
|
// @res 块:响应数据(属性必须用逗号分隔)
|
||||||
|
ResponseRule {
|
||||||
VariablesContent[isolate] { variablesContent }
|
@specialize[@name=ResKeyword]<AtKeyword, "@res">
|
||||||
|
JsonBlock
|
||||||
// ==================== 响应 ====================
|
|
||||||
|
|
||||||
// 响应行 - 固定格式:RESPONSE <状态码> <状态文本> <大小> <时间戳>;
|
|
||||||
ResponseLine {
|
|
||||||
ResponseKeyword StatusCode StatusText Size Timestamp ";"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseKeyword { @specialize[@name="RESPONSE"]<word, "RESPONSE"> }
|
// 普通块结构(属性逗号可选)
|
||||||
|
Block {
|
||||||
StatusCode { Number }
|
"{" blockContent "}"
|
||||||
|
|
||||||
StatusText { word+ }
|
|
||||||
|
|
||||||
Size { Number sizeUnit }
|
|
||||||
|
|
||||||
// 时间戳格式:YYYY-MM-DD HH:MM:SS 或 ISO8601 格式
|
|
||||||
Timestamp { timestampContent }
|
|
||||||
|
|
||||||
// ==================== 模板表达式 ====================
|
|
||||||
|
|
||||||
TemplateExpression {
|
|
||||||
"{{" templateContent "}}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templateContent {
|
blockContent {
|
||||||
VariableName |
|
(Property | AtRule)*
|
||||||
MemberExpression |
|
|
||||||
FunctionCall
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MemberExpression {
|
// HTTP 属性(逗号可选)
|
||||||
VariableName !member ("." PropertyName)+
|
Property {
|
||||||
|
PropertyName { identifier }
|
||||||
|
":" value ","?
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyName { word }
|
// JSON 块结构(属性必须用逗号分隔)
|
||||||
|
JsonBlock {
|
||||||
FunctionCall {
|
"{" jsonBlockContent? "}"
|
||||||
"$" FunctionName !call "(" argumentList? ")"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionName { word }
|
jsonBlockContent {
|
||||||
|
JsonProperty ("," JsonProperty)* ","?
|
||||||
argumentList {
|
|
||||||
argument ("," argument)*
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
@tokens {
|
||||||
// 空白字符
|
// 单行注释(# 开头到行尾)
|
||||||
spaces[@export] { $[ \t]+ }
|
LineComment { "#" ![\n]* }
|
||||||
|
|
||||||
newline[@export] { $[\r\n] }
|
AtKeyword { "@" "-"? @asciiLetter (@asciiLetter | @digit | "-")* }
|
||||||
|
|
||||||
// 注释
|
// 标识符(属性名,支持连字符)
|
||||||
LineComment[@export,isolate] { "#" ![\n]* }
|
identifier {
|
||||||
|
(@asciiLetter | $[_$])
|
||||||
// 标识符
|
(@asciiLetter | @digit | $[-_$])*
|
||||||
identifierChar { @asciiLetter | $[_$] }
|
|
||||||
|
|
||||||
word { identifierChar (identifierChar | @digit | $[-])* }
|
|
||||||
|
|
||||||
// 数字
|
|
||||||
Number {
|
|
||||||
@digit+ ("." @digit+)?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字符串
|
// 单位(必须跟在数字后面,所以不单独匹配)
|
||||||
String {
|
Unit { @asciiLetter+ }
|
||||||
'"' stringContentDouble* '"' |
|
|
||||||
"'" stringContentSingle* "'"
|
whitespace { @whitespace+ }
|
||||||
|
|
||||||
|
@precedence { identifier, Unit }
|
||||||
|
|
||||||
|
numberLiteralInner {
|
||||||
|
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
||||||
|
(("e" | "E") ("+" | "-")? @digit+)?
|
||||||
}
|
}
|
||||||
|
|
||||||
stringContentDouble { ![\\\n"]+ | "\\" _ }
|
StringLiteral[isolate] {
|
||||||
|
"\"" (!["\n\\] | "\\" _)* "\"" |
|
||||||
stringContentSingle { ![\\\n']+ | "\\" _ }
|
"'" (!['\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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@external propSource httpHighlighting from "./http.highlight"
|
||||||
|
|
||||||
@detectDelim
|
@detectDelim
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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
|
|
||||||
@@ -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,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,
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
})
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,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<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',
|
||||||
|
} 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<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);
|
||||||
|
// 移除引号
|
||||||
|
return urlText.replace(/^["']|["']$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 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, @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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,27 +1,115 @@
|
|||||||
import { EditorView, GutterMarker, gutter } from '@codemirror/view';
|
import { EditorView, GutterMarker, gutter } from '@codemirror/view';
|
||||||
|
import { StateField } from '@codemirror/state';
|
||||||
import { syntaxTree } from '@codemirror/language';
|
import { syntaxTree } from '@codemirror/language';
|
||||||
import { getNoteBlockFromPos } from '../../codeblock/state';
|
import { blockState } from '../../codeblock/state';
|
||||||
import type { SyntaxNode } from '@lezer/common';
|
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<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);
|
||||||
|
|
||||||
|
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;
|
* StateField:缓存所有 HTTP 请求的完整解析结果
|
||||||
|
* 只在文档改变时重新解析
|
||||||
/** HTTP 方法在行首的最大偏移位置(字符数) */
|
*/
|
||||||
const MAX_METHOD_POSITION_OFFSET = 20;
|
const httpRequestsField = StateField.define<Map<number, CachedHttpRequest>>({
|
||||||
|
create(state) {
|
||||||
/** 向上查找 ### Request 标记的最大行数 */
|
return parseHttpRequests(state);
|
||||||
const MAX_REQUEST_MARKER_DISTANCE = 10;
|
},
|
||||||
|
|
||||||
|
update(requests, transaction) {
|
||||||
|
// 只有文档改变或缓存为空时才重新解析
|
||||||
|
if (transaction.docChanged || requests.size === 0) {
|
||||||
|
return parseHttpRequests(transaction.state);
|
||||||
|
}
|
||||||
|
return requests;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 运行按钮 Marker ====================
|
// ==================== 运行按钮 Marker ====================
|
||||||
|
|
||||||
@@ -29,7 +117,10 @@ const MAX_REQUEST_MARKER_DISTANCE = 10;
|
|||||||
* 运行按钮 Gutter Marker
|
* 运行按钮 Gutter Marker
|
||||||
*/
|
*/
|
||||||
class RunButtonMarker extends GutterMarker {
|
class RunButtonMarker extends GutterMarker {
|
||||||
constructor(private readonly linePosition: number) {
|
constructor(
|
||||||
|
private readonly lineNumber: number,
|
||||||
|
private readonly cachedRequest: HttpRequest
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,124 +136,46 @@ class RunButtonMarker extends GutterMarker {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.executeRequest(view);
|
this.executeRequest(view);
|
||||||
};
|
};
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async executeRequest(view: EditorView) {
|
private async executeRequest(view: EditorView) {
|
||||||
console.log(`\n============ 执行 HTTP 请求 ============`);
|
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
|
* 创建运行按钮 Gutter
|
||||||
*/
|
*/
|
||||||
export const httpRunButtonGutter = gutter({
|
export const httpRunButtonGutter = gutter({
|
||||||
class: 'cm-http-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({
|
export const httpRunButtonTheme = EditorView.baseTheme({
|
||||||
@@ -183,7 +196,7 @@ export const httpRunButtonTheme = EditorView.baseTheme({
|
|||||||
padding: '0',
|
padding: '0',
|
||||||
transition: 'color 0.15s ease',
|
transition: 'color 0.15s ease',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 悬停效果
|
// 悬停效果
|
||||||
'.cm-http-run-button:hover': {
|
'.cm-http-run-button:hover': {
|
||||||
color: '#45a049', // 深绿色
|
color: '#45a049', // 深绿色
|
||||||
@@ -197,3 +210,5 @@ export const httpRunButtonTheme = EditorView.baseTheme({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 导出 StateField 供扩展系统使用
|
||||||
|
export { httpRequestsField };
|
||||||
Reference in New Issue
Block a user