🚧 Optimized HTTP language parser

This commit is contained in:
2025-11-01 17:42:22 +08:00
parent 8ac78e39f1
commit 93c85b800b
24 changed files with 1945 additions and 582 deletions

View File

@@ -26,20 +26,20 @@ try {
// 运行 lezer-generator
console.log('⚙️ building parser...');
execSync('npx lezer-generator codeblock.grammar -o parser.js', {
execSync('npx lezer-generator codeblock.grammar -o parser.ts --typeScript', {
cwd: __dirname,
stdio: 'inherit'
});
// 检查生成的文件
const parserFile = path.join(__dirname, 'parser.js');
const termsFile = path.join(__dirname, 'parser.terms.js');
const parserFile = path.join(__dirname, 'parser.ts');
const termsFile = path.join(__dirname, 'parser.terms.ts');
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
console.log('✅ parser file successfully generated');
console.log('📦 parser files:');
console.log(' - parser.js');
console.log(' - parser.terms.js');
console.log(' - parser.ts');
console.log(' - parser.terms.ts');
} else {
throw new Error('failed to generate parser');
}

View File

@@ -3,7 +3,7 @@
* 提供多语言代码块支持
*/
import { parser } from "./parser.js";
import { parser } from "./parser";
import { configureNesting } from "./nested-parser";
import {

View File

@@ -4,7 +4,7 @@
*/
import { ExternalTokenizer } from "@lezer/lr";
import { BlockContent } from "./parser.terms.js";
import { BlockContent } from "./parser.terms";
import { LANGUAGES } from "./languages";
const EOF = -1;

View File

@@ -24,7 +24,7 @@ export {
} from './nested-parser';
// 解析器术语
export * from './parser.terms.js';
export * from './parser.terms';
// 外部标记器
export {
@@ -34,4 +34,4 @@ export {
// 解析器
export {
parser
} from './parser.js';
} from './parser';

View File

@@ -86,7 +86,7 @@ export class LanguageInfo {
* 支持的语言列表
*/
export const LANGUAGES: LanguageInfo[] = [
new LanguageInfo("text", "Plain Text", null),
new LanguageInfo("text", "Text", null),
new LanguageInfo("json", "JSON", jsonLanguage.parser, ["json"], {
parser: "json",
plugins: [babelPrettierPlugin, prettierPluginEstree]

View File

@@ -4,7 +4,7 @@
*/
import { parseMixed } from "@lezer/common";
import { BlockContent, BlockLanguage } from "./parser.terms.js";
import { BlockContent, BlockLanguage } from "./parser.terms";
import { languageMapping } from "./languages";
/**

View File

@@ -4,7 +4,7 @@
import {Extension} from '@codemirror/state';
import {httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter';
import {httpRunButtonGutter, httpRunButtonTheme, httpRequestsField} from './widgets/run-gutter';
/**
* 创建 HTTP Client 扩展
@@ -16,7 +16,10 @@ export function createHttpClientExtension(): Extension[] {
// HTTP 语言解析器
// extensions.push(httpLanguage);
// 运行按钮 Gutte
// StateField缓存 HTTP 请求解析结果
extensions.push(httpRequestsField);
// 运行按钮 Gutter
extensions.push(httpRunButtonGutter);
extensions.push(httpRunButtonTheme);

View File

@@ -2,8 +2,8 @@
/**
* HTTP Grammar Parser Builder
* 编译 Lezer grammar 文件为 JavaScript parser
* 使用命令行方式编译
* 编译 Lezer grammar 文件为 TypeScript parser
* 使用 --typeScript 选项生成 .ts 文件
*/
import { execSync } from 'child_process';
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
console.log('🚀 开始编译 HTTP grammar parser...');
console.log('🚀 开始编译 HTTP grammar parser (TypeScript)...');
try {
// 检查语法文件是否存在
@@ -24,22 +24,22 @@ try {
console.log('📄 语法文件:', grammarFile);
// 运行 lezer-generator
console.log('⚙️ 编译 parser...');
execSync('npx lezer-generator http.grammar -o http.grammar.js', {
// 运行 lezer-generator with TypeScript output
console.log('⚙️ 编译 parser (生成 TypeScript)...');
execSync('npx lezer-generator http.grammar -o http.parser.ts --typeScript', {
cwd: __dirname,
stdio: 'inherit'
});
// 检查生成的文件
const parserFile = path.join(__dirname, 'http.grammar.js');
const termsFile = path.join(__dirname, 'http.grammar.terms.js');
const parserFile = path.join(__dirname, 'http.parser.ts');
const termsFile = path.join(__dirname, 'http.parser.terms.ts');
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
console.log('✅ Parser 文件成功生成!');
console.log('📦 生成的文件:');
console.log(' - http.grammar.js');
console.log(' - http.grammar.terms.js');
console.log(' - http.parser.ts');
console.log(' - http.parser.terms.ts');
} else {
throw new Error('Parser 生成失败');
}
@@ -50,4 +50,3 @@ try {
console.error('❌ 编译失败:', error.message);
process.exit(1);
}

View File

@@ -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,
});

View File

@@ -1,6 +1,7 @@
import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
import { parser } from './http.grammar.js';
import { httpHighlighting } from './http-highlight';
import { CompletionContext } from '@codemirror/autocomplete';
import { parser } from './http.parser';
import { httpHighlighting } from './http.highlight';
/**
* HTTP Client 语言定义
@@ -12,18 +13,19 @@ const httpParserWithMetadata = parser.configure({
// 应用语法高亮
httpHighlighting,
// 折叠规则:允许折叠多行 Body、Variables、Headers 等
// 折叠规则:允许折叠块结构
foldNodeProp.add({
BodyStatement: foldInside,
VariablesStatement: foldInside,
RequestStatement: foldInside,
Block: foldInside,
AtRule: foldInside,
Document: foldInside,
}),
// 缩进规则
indentNodeProp.add({
BodyStatement: () => 2,
HeaderStatement: () => 0,
VariableDeclaration: () => 0,
Block: () => 2,
Declaration: () => 0,
AtRule: () => 0,
}),
],
});
@@ -32,8 +34,6 @@ const httpParserWithMetadata = parser.configure({
export const httpLanguage = LRLanguage.define({
parser: httpParserWithMetadata,
languageData: {
// 注释配置
commentTokens: { line: '#' },
//自动闭合括号
closeBrackets: { brackets: ['(', '[', '{', '"', "'"] },
@@ -45,7 +45,7 @@ export const httpLanguage = LRLanguage.define({
/**
* HTTP Client 语言支持
* 包含语法高亮、折叠、缩进等完整功能
* 包含语法高亮、折叠、缩进、自动补全等完整功能
*/
export function http() {
return new LanguageSupport(httpLanguage, [
@@ -58,7 +58,7 @@ export function http() {
/**
* HTTP Client 自动补全
*/
function httpCompletion(context: any) {
function httpCompletion(context: CompletionContext) {
const word = context.matchBefore(/\w*/);
if (!word || (word.from === word.to && !context.explicit)) {
return null;
@@ -76,49 +76,29 @@ function httpCompletion(context: any) {
{ label: 'HEAD', type: 'keyword', detail: 'HTTP Method' },
{ label: 'OPTIONS', type: 'keyword', detail: 'HTTP Method' },
// 关键字
{ label: 'HEADER', type: 'keyword', detail: 'Header Statement' },
{ label: 'BODY', type: 'keyword', detail: 'Body Statement' },
{ label: 'VARIABLES', type: 'keyword', detail: 'Variables Statement' },
// Body 类型
{ label: 'TEXT', type: 'keyword', detail: 'Body Type' },
{ label: 'JSON', type: 'keyword', detail: 'Body Type' },
{ label: 'XML', type: 'keyword', detail: 'Body Type' },
{ label: 'FORM', type: 'keyword', detail: 'Body Type' },
{ label: 'URLENCODED', type: 'keyword', detail: 'Body Type' },
{ label: 'GRAPHQL', type: 'keyword', detail: 'Body Type' },
{ label: 'BINARY', type: 'keyword', detail: 'Body Type' },
// HTTP 版本
{ label: 'HTTP/1.0', type: 'constant', detail: 'HTTP Version' },
{ label: 'HTTP/1.1', type: 'constant', detail: 'HTTP Version' },
{ label: 'HTTP/2.0', type: 'constant', detail: 'HTTP Version' },
// @ 规则
{ label: '@json', type: 'keyword', detail: 'Body Type' },
{ label: '@formdata', type: 'keyword', detail: 'Body Type' },
{ label: '@urlencoded', type: 'keyword', detail: 'Body Type' },
{ label: '@text', type: 'keyword', detail: 'Body Type' },
{ label: '@res', type: 'keyword', detail: 'Response' },
// 常用 Headers
{ label: 'Content-Type', type: 'property', detail: 'Header Name' },
{ label: 'Authorization', type: 'property', detail: 'Header Name' },
{ label: 'Accept', type: 'property', detail: 'Header Name' },
{ label: 'User-Agent', type: 'property', detail: 'Header Name' },
{ label: 'Cookie', type: 'property', detail: 'Header Name' },
{ label: 'content-type', type: 'property', detail: 'Header' },
{ label: 'authorization', type: 'property', detail: 'Header' },
{ label: 'accept', type: 'property', detail: 'Header' },
{ label: 'user-agent', type: 'property', detail: 'Header' },
{ label: 'host', type: 'property', detail: 'Header' },
// 常用 Content-Type
{ label: 'application/json', type: 'constant', detail: 'Content Type' },
{ label: 'application/xml', type: 'constant', detail: 'Content Type' },
{ label: 'text/html', type: 'constant', detail: 'Content Type' },
{ label: 'text/plain', type: 'constant', detail: 'Content Type' },
{ label: 'multipart/form-data', type: 'constant', detail: 'Content Type' },
{ label: 'application/x-www-form-urlencoded', type: 'constant', detail: 'Content Type' },
{ label: '"application/json"', type: 'constant', detail: 'Content Type' },
{ label: '"text/plain"', type: 'constant', detail: 'Content Type' },
{ label: '"multipart/form-data"', type: 'constant', detail: 'Content Type' },
{ label: '"application/x-www-form-urlencoded"', type: 'constant', detail: 'Content Type' },
// 特殊标记
{ label: '@timestamp', type: 'keyword', detail: 'Timestamp' },
{ label: '@file', type: 'keyword', detail: 'File Reference' },
// 内置函数
{ label: '$timestamp()', type: 'function', detail: 'Current Timestamp' },
{ label: '$uuid()', type: 'function', detail: 'Generate UUID' },
{ label: '$randomInt()', type: 'function', detail: 'Random Integer' },
{ label: '$hash()', type: 'function', detail: 'Hash Function' },
// 布尔值
{ label: 'true', type: 'constant', detail: 'Boolean' },
{ label: 'false', type: 'constant', detail: 'Boolean' },
],
};
}
@@ -127,4 +107,3 @@ function httpCompletion(context: any) {
* 导出语言定义和高亮配置
*/
export { httpHighlighting };

View File

@@ -1,267 +1,214 @@
@precedence {
member,
call
// HTTP Client Grammar
//
// 语法规则:
// 1. HTTP 头部属性:逗号可选
// host: "example.com"
// content-type: "application/json"
//
// 2. 请求体格式:
// @json - JSON 格式(属性必须用逗号分隔)
// @formdata - 表单数据(属性必须用逗号分隔)
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
// @text - 纯文本内容(使用 content 字段)
// @res - 响应数据(属性必须用逗号分隔)
//
// 3. 注释:
// # 单行注释
//
// 示例 1 - JSON 请求:
// POST "http://api.example.com/users" {
// content-type: "application/json"
//
// @json {
// name: "张三",
// age: 25,
// email: "zhangsan@example.com"
// }
// }
//
// 示例 2 - FormData 请求:
// POST "http://api.example.com/upload" {
// content-type: "multipart/form-data"
//
// @formdata {
// file: "avatar.png",
// username: "zhangsan"
// }
// }
//
// 示例 3 - URLEncoded 请求:
// POST "http://api.example.com/login" {
// content-type: "application/x-www-form-urlencoded"
//
// @urlencoded {
// username: "admin",
// password: "123456"
// }
// }
//
// 示例 4 - 纯文本请求:
// POST "http://api.example.com/webhook" {
// content-type: "text/plain"
//
// @text {
// content: "纯文本内容"
// }
// }
@skip { whitespace | LineComment }
@top Document { item* }
item {
RequestStatement |
AtRule
}
@top Document { statement* }
statement {
VariableDeclaration |
ResponseLine |
RequestLine |
HeaderStatement |
BodyStatement |
VariablesStatement
}
// ==================== 变量定义 ====================
VariableDeclaration {
"@" VariableName "=" VariableValue ";"
}
VariableName { word }
VariableValue[isolate] { anyContent }
// ==================== 请求行 ====================
RequestLine {
Method Url HttpVersion? ";"
// HTTP 请求 - URL 必须是字符串
RequestStatement {
Method Url Block
}
Method {
@specialize[@name="GET"]<word, "GET"> |
@specialize[@name="POST"]<word, "POST"> |
@specialize[@name="PUT"]<word, "PUT"> |
@specialize[@name="DELETE"]<word, "DELETE"> |
@specialize[@name="PATCH"]<word, "PATCH"> |
@specialize[@name="HEAD"]<word, "HEAD"> |
@specialize[@name="OPTIONS"]<word, "OPTIONS"> |
@specialize[@name="CONNECT"]<word, "CONNECT"> |
@specialize[@name="TRACE"]<word, "TRACE">
@specialize[@name=GET]<identifier, "GET"> |
@specialize[@name=POST]<identifier, "POST"> |
@specialize[@name=PUT]<identifier, "PUT"> |
@specialize[@name=DELETE]<identifier, "DELETE"> |
@specialize[@name=PATCH]<identifier, "PATCH"> |
@specialize[@name=HEAD]<identifier, "HEAD"> |
@specialize[@name=OPTIONS]<identifier, "OPTIONS"> |
@specialize[@name=CONNECT]<identifier, "CONNECT"> |
@specialize[@name=TRACE]<identifier, "TRACE">
}
Url { urlPart+ }
// URL 必须是字符串
Url { StringLiteral }
urlPart { urlContent | TemplateExpression }
HttpVersion { httpVersionToken }
// ==================== Header 语句 ====================
HeaderStatement {
HeaderKeyword HeaderName colon HeaderValue ";"
// @ 规则(支持多种请求体格式)
AtRule {
JsonRule |
FormDataRule |
UrlEncodedRule |
TextRule |
ResponseRule
}
colon { ":" }
HeaderKeyword { @specialize[@name="HEADER"]<word, "HEADER"> }
HeaderName { word }
HeaderValue { headerValuePart* }
headerValuePart { headerValueContent | TemplateExpression }
// ==================== Body 语句 ====================
BodyStatement {
BodyKeyword BodyType BodyContent ";"
// @json 块JSON 格式请求体(属性必须用逗号分隔)
JsonRule {
@specialize[@name=JsonKeyword]<AtKeyword, "@json">
JsonBlock
}
BodyKeyword { @specialize[@name="BODY"]<word, "BODY"> }
BodyType {
@specialize[@name="JSON"]<word, "JSON"> |
@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">
// @formdata 块:表单数据格式(属性必须用逗号分隔)
FormDataRule {
@specialize[@name=FormDataKeyword]<AtKeyword, "@formdata">
JsonBlock
}
BodyContent { bodyContentPart* }
bodyContentPart { bodyText | TemplateExpression | FileReference }
FileReference {
"@file" FilePath
// @urlencoded 块URL 编码格式(属性必须用逗号分隔)
UrlEncodedRule {
@specialize[@name=UrlEncodedKeyword]<AtKeyword, "@urlencoded">
JsonBlock
}
FilePath { filePathContent }
// ==================== Variables 语句 ====================
VariablesStatement {
VariablesKeyword VariablesContent ";"
// @text 块:纯文本请求体(使用 content 字段)
TextRule {
@specialize[@name=TextKeyword]<AtKeyword, "@text">
JsonBlock
}
VariablesKeyword { @specialize[@name="VARIABLES"]<word, "VARIABLES"> }
VariablesContent[isolate] { variablesContent }
// ==================== 响应 ====================
// 响应行 - 固定格式RESPONSE <状态码> <状态文本> <大小> <时间戳>;
ResponseLine {
ResponseKeyword StatusCode StatusText Size Timestamp ";"
// @res 块:响应数据(属性必须用逗号分隔)
ResponseRule {
@specialize[@name=ResKeyword]<AtKeyword, "@res">
JsonBlock
}
ResponseKeyword { @specialize[@name="RESPONSE"]<word, "RESPONSE"> }
StatusCode { Number }
StatusText { word+ }
Size { Number sizeUnit }
// 时间戳格式YYYY-MM-DD HH:MM:SS 或 ISO8601 格式
Timestamp { timestampContent }
// ==================== 模板表达式 ====================
TemplateExpression {
"{{" templateContent "}}"
// 普通块结构(属性逗号可选)
Block {
"{" blockContent "}"
}
templateContent {
VariableName |
MemberExpression |
FunctionCall
blockContent {
(Property | AtRule)*
}
MemberExpression {
VariableName !member ("." PropertyName)+
// HTTP 属性(逗号可选)
Property {
PropertyName { identifier }
":" value ","?
}
PropertyName { word }
FunctionCall {
"$" FunctionName !call "(" argumentList? ")"
// JSON 块结构(属性必须用逗号分隔)
JsonBlock {
"{" jsonBlockContent? "}"
}
FunctionName { word }
argumentList {
argument ("," argument)*
jsonBlockContent {
JsonProperty ("," JsonProperty)* ","?
}
argument { Number | String | word }
// JSON 属性
JsonProperty {
PropertyName { identifier }
":" jsonValue
}
// ==================== Tokens ====================
//
NumberLiteral {
numberLiteralInner Unit?
}
@skip { spaces | newline | LineComment }
// HTTP 属性值(支持块嵌套)
value {
StringLiteral |
NumberLiteral |
Block |
identifier
}
// JSON 属性值(支持嵌套对象)
jsonValue {
StringLiteral |
NumberLiteral |
JsonBlock |
identifier
}
// Tokens
@tokens {
// 空白字符
spaces[@export] { $[ \t]+ }
// 单行注释(# 开头到行尾)
LineComment { "#" ![\n]* }
newline[@export] { $[\r\n] }
AtKeyword { "@" "-"? @asciiLetter (@asciiLetter | @digit | "-")* }
// 注释
LineComment[@export,isolate] { "#" ![\n]* }
// 标识符
identifierChar { @asciiLetter | $[_$] }
word { identifierChar (identifierChar | @digit | $[-])* }
// 数字
Number {
@digit+ ("." @digit+)?
// 标识符(属性名,支持连字符)
identifier {
(@asciiLetter | $[_$])
(@asciiLetter | @digit | $[-_$])*
}
// 字符串
String {
'"' stringContentDouble* '"' |
"'" stringContentSingle* "'"
// 单位(必须跟在数字后面,所以不单独匹配)
Unit { @asciiLetter+ }
whitespace { @whitespace+ }
@precedence { identifier, Unit }
numberLiteralInner {
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
(("e" | "E") ("+" | "-")? @digit+)?
}
stringContentDouble { ![\\\n"]+ | "\\" _ }
stringContentSingle { ![\\\n']+ | "\\" _ }
// URL 内容 - 排除空格、换行、特殊前缀字符
// 允许 # (用于锚点),但不允许以 # 开头(避免和注释冲突)
urlContent { ![ \t\n\r@{};]+ }
// HTTP 版本
httpVersionToken { "HTTP/" @digit+ "." @digit+ }
// Header 值内容 - 排除所有 skip tokens 和特殊字符
headerValueContent { ![ \t\n\r#{};]+ }
// Body 文本内容 - 排除空格、换行、分号
// 允许大括号、#、@、$ 等因为JSON/XML/GraphQL/email等需要它们
// {{ 和 @file 是独立token有更高优先级
bodyText { ![ \t\n\r;]+ }
// 任意内容(直到分号) - 只排除换行和分号
// 允许空格、#、数字等所有字符
anyContent { ![\n\r;]+ }
// 文件路径内容 - 排除空格、换行、分号
filePathContent { ![ \t\n\r;]+ }
// Variables 内容 - 只排除分号,允许所有字符包括换行
variablesContent { ![;]+ }
// 时间戳内容 - 日期时间格式,包含日期时间字符
timestampContent { timestampChar+ }
timestampChar { @digit | $[-:TZ.+] }
// Duration 单位
durationUnit { "ms" | "s" | "m" }
// Size 单位
sizeUnit { "B" | "KB" | "MB" | "GB" }
// 优先级声明
@precedence { urlContent, LineComment }
@precedence { "{{", bodyText }
@precedence { "@file", bodyText }
@precedence { "$", bodyText }
@precedence { bodyText, LineComment }
@precedence { anyContent, LineComment }
@precedence { anyContent, spaces }
@precedence { filePathContent, LineComment }
@precedence { variablesContent, newline }
@precedence { variablesContent, LineComment }
@precedence { variablesContent, spaces }
@precedence { "$", word }
@precedence { spaces, newline, word }
@precedence { httpVersionToken, urlContent, word }
@precedence { "{{", bodyText }
@precedence { word, bodyText }
@precedence { Number, word }
// 符号
"(" ")" "[" "]" "{" "}"
"." "," ":" "=" "@" "$" ";" "{{" "}}"
// 组合 tokens
"@file"
StringLiteral[isolate] {
"\"" (!["\n\\] | "\\" _)* "\"" |
"'" (!['\n\\] | "\\" _)* "'"
}
":" ","
"{" "}"
}
@external propSource httpHighlighting from "./http.highlight"
@detectDelim

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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
})

View File

@@ -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';

View File

@@ -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' });
});
});
});

View File

@@ -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;
}
/**
* 解析 PropertyHTTP 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);
}

View File

@@ -1,27 +1,115 @@
import { EditorView, GutterMarker, gutter } from '@codemirror/view';
import { StateField } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import { getNoteBlockFromPos } from '../../codeblock/state';
import { blockState } from '../../codeblock/state';
import type { SyntaxNode } from '@lezer/common';
import { parseHttpRequest, type HttpRequest } from '../parser/request-parser';
// ==================== 常量定义 ====================
/**
* 语法树节点类型常量
*/
const NODE_TYPES = {
REQUEST_STATEMENT: 'RequestStatement',
METHOD: 'Method',
URL: 'Url',
BLOCK: 'Block',
} as const;
/** 支持的 HTTP 方法(小写) - 使用 Set 以提高查找性能 */
const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'connect', 'trace']);
/**
* 有效的 HTTP 方法列表
*/
const VALID_HTTP_METHODS = new Set([
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
'HEAD',
'OPTIONS',
'CONNECT',
'TRACE'
]);
/** 匹配 ### Request 标记的正则表达式 */
const REQUEST_MARKER_REGEX = /^###\s+Request(?:\s|$)/i;
/**
* HTTP 请求缓存信息
*/
interface CachedHttpRequest {
lineNumber: number; // 行号(用于快速查找)
position: number; // 字符位置(用于解析)
request: HttpRequest; // 完整的解析结果
}
/** 匹配 ### Response 标记的正则表达式 */
const RESPONSE_MARKER_REGEX = /^###\s+Response/i;
/**
* 预解析所有 HTTP 块中的请求
* 只在文档改变时调用,结果缓存在 StateField 中
*
* 优化:一次遍历完成验证和解析,避免重复工作
*/
function parseHttpRequests(state: any): Map<number, CachedHttpRequest> {
const requestsMap = new Map<number, CachedHttpRequest>();
const blocks = state.field(blockState, false);
/** 匹配 HTTP 方法的正则表达式 */
const HTTP_METHOD_REGEX = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+/i;
if (!blocks) return requestsMap;
/** HTTP 方法在行首的最大偏移位置(字符数) */
const MAX_METHOD_POSITION_OFFSET = 20;
const tree = syntaxTree(state);
/** 向上查找 ### Request 标记的最大行数 */
const MAX_REQUEST_MARKER_DISTANCE = 10;
// 只遍历 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;
}
/**
* StateField缓存所有 HTTP 请求的完整解析结果
* 只在文档改变时重新解析
*/
const httpRequestsField = StateField.define<Map<number, CachedHttpRequest>>({
create(state) {
return parseHttpRequests(state);
},
update(requests, transaction) {
// 只有文档改变或缓存为空时才重新解析
if (transaction.docChanged || requests.size === 0) {
return parseHttpRequests(transaction.state);
}
return requests;
}
});
// ==================== 运行按钮 Marker ====================
@@ -29,7 +117,10 @@ const MAX_REQUEST_MARKER_DISTANCE = 10;
* 运行按钮 Gutter Marker
*/
class RunButtonMarker extends GutterMarker {
constructor(private readonly linePosition: number) {
constructor(
private readonly lineNumber: number,
private readonly cachedRequest: HttpRequest
) {
super();
}
@@ -49,95 +140,20 @@ class RunButtonMarker extends GutterMarker {
return button;
}
private async executeRequest(view: EditorView) {
console.log(`\n============ 执行 HTTP 请求 ============`);
console.log(`位置: ${this.linePosition}`);
console.log('行号:', this.lineNumber);
// 直接使用缓存的解析结果,无需重新解析!
console.log('解析结果:', JSON.stringify(this.cachedRequest, null, 2));
// TODO: 调用后端 API 执行请求
// const response = await executeHttpRequest(this.cachedRequest);
// renderResponse(response);
}
}
/**
* 使用语法树检查一行是否是 HTTP 请求行(更可靠)
* 必须符合规则:前面有 ### Request然后才是 GET/POST 等请求行
*/
function isRequestLineInSyntaxTree(view: EditorView, lineFrom: number, lineTo: number): boolean {
const tree = syntaxTree(view.state);
let hasHttpMethod = false;
// 遍历该行的语法树节点
tree.iterate({
from: lineFrom,
to: lineTo,
enter: (node: SyntaxNode) => {
// HTTP 解析器将 HTTP 方法GET、POST 等)标记为 "keyword"
// 并且该节点应该在行首附近
if (node.name === 'keyword' &&
node.from >= lineFrom &&
node.from < lineFrom + MAX_METHOD_POSITION_OFFSET) {
const text = view.state.sliceDoc(node.from, node.to);
if (HTTP_METHODS.has(text.toLowerCase())) {
// 检查前面是否有 ### Request 标记
if (hasPrecedingRequestMarker(view, lineFrom)) {
hasHttpMethod = true;
}
}
}
}
});
return hasHttpMethod;
}
/**
* 检查前面是否有 ### Request 标记
* 只要包含 "### Request",后面可以跟任何描述文字
*/
function hasPrecedingRequestMarker(view: EditorView, lineFrom: number): boolean {
const currentLineNum = view.state.doc.lineAt(lineFrom).number;
// 向上查找前面的几行(最多往上找指定行数)
for (let i = currentLineNum - 1;
i >= Math.max(1, currentLineNum - MAX_REQUEST_MARKER_DISTANCE);
i--) {
const line = view.state.doc.line(i);
const lineText = view.state.sliceDoc(line.from, line.to).trim();
if (REQUEST_MARKER_REGEX.test(lineText)) {
return true;
}
// 如果遇到 ### Response停止查找
if (RESPONSE_MARKER_REGEX.test(lineText)) {
return false;
}
// 如果是空行,继续往上找
if (lineText === '') {
continue;
}
// 如果遇到另一个请求方法,停止查找
if (HTTP_METHOD_REGEX.test(lineText)) {
return false;
}
}
return false;
}
/**
* 检查位置是否在 HTTP 块内
*/
function isInHttpBlock(view: EditorView, pos: number): boolean {
try {
const block = getNoteBlockFromPos(view.state, pos);
return block?.language.name === 'http' || block?.language.name === 'rest';
} catch {
return false;
}
}
/**
* 创建运行按钮 Gutter
@@ -145,24 +161,21 @@ function isInHttpBlock(view: EditorView, pos: number): boolean {
export const httpRunButtonGutter = gutter({
class: 'cm-http-gutter',
// 为每一行决定是否显示 marker
lineMarker(view, line) {
const linePos = line.from;
// O(1) 查找:从缓存中获取请求
const requestsMap = view.state.field(httpRequestsField, false);
if (!requestsMap) return null;
// 第一步:检查是否在 HTTP 块内
if (!isInHttpBlock(view, linePos)) {
const lineNumber = view.state.doc.lineAt(line.from).number;
const cached = requestsMap.get(lineNumber);
if (!cached) {
return null;
}
// 第二步:使用语法树检查是否是请求行
if (!isRequestLineInSyntaxTree(view, line.from, line.to)) {
return null;
}
// 创建运行按钮
return new RunButtonMarker(linePos);
// 创建并返回运行按钮,传递缓存的解析结果
return new RunButtonMarker(cached.lineNumber, cached.request);
},
});
export const httpRunButtonTheme = EditorView.baseTheme({
@@ -197,3 +210,5 @@ export const httpRunButtonTheme = EditorView.baseTheme({
},
});
// 导出 StateField 供扩展系统使用
export { httpRequestsField };