481 lines
14 KiB
TypeScript
481 lines
14 KiB
TypeScript
import { EditorState } from '@codemirror/state';
|
||
import { syntaxTree } from '@codemirror/language';
|
||
import type { SyntaxNode } from '@lezer/common';
|
||
import { VariableResolver } from './variable-resolver';
|
||
|
||
/**
|
||
* HTTP 请求模型
|
||
*/
|
||
export interface HttpRequest {
|
||
/** 请求方法 */
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||
|
||
/** 请求 URL */
|
||
url: string;
|
||
|
||
/** 请求头 */
|
||
headers: Record<string, string>;
|
||
|
||
/** 请求体类型 */
|
||
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text' | 'params' | 'xml' | 'html' | 'javascript' | 'binary';
|
||
|
||
/** 请求体内容 */
|
||
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',
|
||
PARAMS_RULE: 'ParamsRule',
|
||
XML_RULE: 'XmlRule',
|
||
HTML_RULE: 'HtmlRule',
|
||
JAVASCRIPT_RULE: 'JavaScriptRule',
|
||
BINARY_RULE: 'BinaryRule',
|
||
JSON_KEYWORD: 'JsonKeyword',
|
||
FORMDATA_KEYWORD: 'FormDataKeyword',
|
||
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
||
TEXT_KEYWORD: 'TextKeyword',
|
||
PARAMS_KEYWORD: 'ParamsKeyword',
|
||
XML_KEYWORD: 'XmlKeyword',
|
||
HTML_KEYWORD: 'HtmlKeyword',
|
||
JAVASCRIPT_KEYWORD: 'JavaScriptKeyword',
|
||
BINARY_KEYWORD: 'BinaryKeyword',
|
||
JSON_BLOCK: 'JsonBlock',
|
||
JSON_PROPERTY: 'JsonProperty',
|
||
XML_BLOCK: 'XmlBlock',
|
||
HTML_BLOCK: 'HtmlBlock',
|
||
JAVASCRIPT_BLOCK: 'JavaScriptBlock',
|
||
BINARY_BLOCK: 'BinaryBlock',
|
||
XML_KEY: 'XmlKey',
|
||
HTML_KEY: 'HtmlKey',
|
||
JAVASCRIPT_KEY: 'JavaScriptKey',
|
||
BINARY_KEY: 'BinaryKey',
|
||
VARIABLE_REF: 'VariableRef',
|
||
} as const;
|
||
|
||
/**
|
||
* HTTP 请求解析器
|
||
*/
|
||
export class HttpRequestParser {
|
||
private variableResolver: VariableResolver | null = null;
|
||
|
||
/**
|
||
* 构造函数
|
||
* @param state EditorState
|
||
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
|
||
*/
|
||
constructor(
|
||
private state: EditorState,
|
||
private blockRange?: { from: number; to: number }
|
||
) {
|
||
|
||
}
|
||
|
||
/**
|
||
* 获取或创建变量解析器(懒加载)
|
||
*/
|
||
private getVariableResolver(): VariableResolver {
|
||
if (!this.variableResolver) {
|
||
this.variableResolver = new VariableResolver(this.state, this.blockRange);
|
||
}
|
||
return this.variableResolver;
|
||
}
|
||
|
||
/**
|
||
* 解析指定位置的 HTTP 请求
|
||
* @param pos 光标位置或请求起始位置
|
||
* @returns 解析后的 HTTP 请求对象,如果解析失败返回 null
|
||
*/
|
||
parseRequestAt(pos: number): HttpRequest | null {
|
||
const tree = syntaxTree(this.state);
|
||
|
||
// 查找包含该位置的 RequestStatement 节点
|
||
const requestNode = this.findRequestNode(tree, pos);
|
||
if (!requestNode) {
|
||
return null;
|
||
}
|
||
|
||
return this.parseRequest(requestNode);
|
||
}
|
||
|
||
/**
|
||
* 查找包含指定位置的 RequestStatement 节点
|
||
*/
|
||
private findRequestNode(tree: any, pos: number): SyntaxNode | null {
|
||
let foundNode: SyntaxNode | null = null;
|
||
|
||
tree.iterate({
|
||
enter: (node: any) => {
|
||
if (node.name === NODE_TYPES.REQUEST_STATEMENT) {
|
||
if (node.from <= pos && pos <= node.to) {
|
||
foundNode = node.node;
|
||
return false; // 停止迭代
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
return foundNode;
|
||
}
|
||
|
||
/**
|
||
* 解析 RequestStatement 节点
|
||
*/
|
||
private parseRequest(node: SyntaxNode): HttpRequest | null {
|
||
// 使用 Lezer API 直接获取子节点
|
||
const methodNode = node.getChild(NODE_TYPES.METHOD);
|
||
const urlNode = node.getChild(NODE_TYPES.URL);
|
||
const blockNode = node.getChild(NODE_TYPES.BLOCK);
|
||
|
||
// 验证必需节点
|
||
if (!methodNode || !urlNode || !blockNode) {
|
||
return null;
|
||
}
|
||
|
||
const method = this.getNodeText(methodNode).toUpperCase();
|
||
const url = this.parseUrl(urlNode);
|
||
|
||
// 验证 URL 非空
|
||
if (!url) {
|
||
return null;
|
||
}
|
||
|
||
const headers: Record<string, string> = {};
|
||
let bodyType: HttpRequest['bodyType'] = undefined;
|
||
let body: any = undefined;
|
||
|
||
// 解析 Block
|
||
this.parseBlock(blockNode, headers, (type, content) => {
|
||
bodyType = type;
|
||
body = content;
|
||
});
|
||
|
||
const line = this.state.doc.lineAt(node.from);
|
||
|
||
return {
|
||
method: method as HttpRequest['method'],
|
||
url,
|
||
headers,
|
||
bodyType,
|
||
body,
|
||
position: {
|
||
from: node.from,
|
||
to: node.to,
|
||
line: line.number,
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 解析 URL 节点
|
||
*/
|
||
private parseUrl(node: SyntaxNode): string {
|
||
const urlText = this.getNodeText(node);
|
||
// 移除引号
|
||
const url = urlText.replace(/^["']|["']$/g, '');
|
||
// 替换变量
|
||
return this.getVariableResolver().replaceVariables(url);
|
||
}
|
||
|
||
/**
|
||
* 解析 Block 节点(包含 headers 和 body)
|
||
*/
|
||
private parseBlock(
|
||
node: SyntaxNode,
|
||
headers: Record<string, string>,
|
||
onBody: (type: HttpRequest['bodyType'], content: any) => void
|
||
): void {
|
||
// 遍历 Block 的子节点
|
||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||
if (child.name === NODE_TYPES.PROPERTY) {
|
||
// HTTP Header 属性
|
||
const { name, value } = this.parseProperty(child);
|
||
if (name && value !== null) {
|
||
headers[name] = value;
|
||
}
|
||
} else if (child.name === NODE_TYPES.AT_RULE) {
|
||
// AtRule 节点,直接获取第一个子节点(JsonRule, FormDataRule等)
|
||
const ruleChild = child.firstChild;
|
||
if (ruleChild) {
|
||
const { type, content } = this.parseBodyRule(ruleChild);
|
||
if (type) { // 只有有效的类型才处理
|
||
onBody(type, content);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析请求体规则
|
||
*/
|
||
private parseBodyRule(node: SyntaxNode): { type: HttpRequest['bodyType']; content: any } {
|
||
// 类型映射表
|
||
const typeMap: Record<string, HttpRequest['bodyType']> = {
|
||
[NODE_TYPES.JSON_RULE]: 'json',
|
||
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
||
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
||
[NODE_TYPES.TEXT_RULE]: 'text',
|
||
[NODE_TYPES.PARAMS_RULE]: 'params',
|
||
[NODE_TYPES.XML_RULE]: 'xml',
|
||
[NODE_TYPES.HTML_RULE]: 'html',
|
||
[NODE_TYPES.JAVASCRIPT_RULE]: 'javascript',
|
||
[NODE_TYPES.BINARY_RULE]: 'binary',
|
||
};
|
||
|
||
const type = typeMap[node.name];
|
||
|
||
// 根据不同的规则类型解析不同的块
|
||
let content: any = null;
|
||
|
||
if (node.name === NODE_TYPES.XML_RULE) {
|
||
const blockNode = node.getChild(NODE_TYPES.XML_BLOCK);
|
||
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'xml') : null;
|
||
} else if (node.name === NODE_TYPES.HTML_RULE) {
|
||
const blockNode = node.getChild(NODE_TYPES.HTML_BLOCK);
|
||
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'html') : null;
|
||
} else if (node.name === NODE_TYPES.JAVASCRIPT_RULE) {
|
||
const blockNode = node.getChild(NODE_TYPES.JAVASCRIPT_BLOCK);
|
||
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'javascript') : null;
|
||
} else if (node.name === NODE_TYPES.BINARY_RULE) {
|
||
const blockNode = node.getChild(NODE_TYPES.BINARY_BLOCK);
|
||
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'binary') : null;
|
||
} else {
|
||
// json, formdata, urlencoded, text, params 使用 JsonBlock
|
||
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
||
content = blockNode ? this.parseJsonBlock(blockNode) : null;
|
||
}
|
||
|
||
return {
|
||
type,
|
||
content
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 解析固定 key 的块(xml, html, javascript, binary)
|
||
* 格式:{ key: "value" } 或 {}(空块)
|
||
*/
|
||
private parseFixedKeyBlock(node: SyntaxNode, keyName: string): any {
|
||
// 查找固定的 key 节点
|
||
const keyNode = node.getChild(
|
||
keyName === 'xml' ? NODE_TYPES.XML_KEY :
|
||
keyName === 'html' ? NODE_TYPES.HTML_KEY :
|
||
keyName === 'javascript' ? NODE_TYPES.JAVASCRIPT_KEY :
|
||
NODE_TYPES.BINARY_KEY
|
||
);
|
||
|
||
// 如果没有 key,返回空对象(支持空块)
|
||
if (!keyNode) {
|
||
return {};
|
||
}
|
||
|
||
// 查找值节点(冒号后面的内容)
|
||
let value: any = null;
|
||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||
if (child.name === NODE_TYPES.STRING_LITERAL ||
|
||
child.name === NODE_TYPES.VARIABLE_REF) {
|
||
value = this.parseValue(child);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 返回格式:{ xml: "value" } 或 { html: "value" } 等
|
||
return value !== null ? { [keyName]: value } : {};
|
||
}
|
||
|
||
/**
|
||
* 解析 JsonBlock(用于 @json, @form, @urlencoded)
|
||
*/
|
||
private parseJsonBlock(node: SyntaxNode): any {
|
||
const result: any = {};
|
||
|
||
// 遍历 JsonProperty
|
||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||
if (child.name === NODE_TYPES.JSON_PROPERTY) {
|
||
const { name, value } = this.parseJsonProperty(child);
|
||
if (name && value !== null) {
|
||
result[name] = value;
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 解析 JsonProperty
|
||
*/
|
||
private parseJsonProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||
// 使用 API 获取属性名
|
||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||
if (!nameNode) {
|
||
return { name: null, value: null };
|
||
}
|
||
|
||
const name = this.getNodeText(nameNode);
|
||
|
||
// 尝试获取值节点(String, Number, JsonBlock, VariableRef)
|
||
let value: any = null;
|
||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||
if (child.name === NODE_TYPES.STRING_LITERAL ||
|
||
child.name === NODE_TYPES.NUMBER_LITERAL ||
|
||
child.name === NODE_TYPES.JSON_BLOCK ||
|
||
child.name === NODE_TYPES.VARIABLE_REF ||
|
||
child.name === NODE_TYPES.IDENTIFIER) {
|
||
value = this.parseValue(child);
|
||
return { name, value };
|
||
}
|
||
}
|
||
|
||
// 回退:从文本中提取值(用于 true/false 等标识符)
|
||
const fullText = this.getNodeText(node);
|
||
const colonIndex = fullText.indexOf(':');
|
||
if (colonIndex !== -1) {
|
||
const valueText = fullText.substring(colonIndex + 1).trim().replace(/,$/, '').trim();
|
||
value = this.parseValueFromText(valueText);
|
||
}
|
||
|
||
return { name, value };
|
||
}
|
||
|
||
/**
|
||
* 从文本解析值
|
||
*/
|
||
private parseValueFromText(text: string): any {
|
||
// 布尔值
|
||
if (text === 'true') return true;
|
||
if (text === 'false') return false;
|
||
if (text === 'null') return null;
|
||
|
||
// 数字
|
||
if (/^-?\d+(\.\d+)?$/.test(text)) {
|
||
return parseFloat(text);
|
||
}
|
||
|
||
// 字符串(带引号)
|
||
if ((text.startsWith('"') && text.endsWith('"')) ||
|
||
(text.startsWith("'") && text.endsWith("'"))) {
|
||
return text.slice(1, -1);
|
||
}
|
||
|
||
// 其他标识符
|
||
return text;
|
||
}
|
||
|
||
/**
|
||
* 解析 Property(HTTP Header)
|
||
*/
|
||
private parseProperty(node: SyntaxNode): { name: string | null; value: any } {
|
||
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
|
||
if (!nameNode) {
|
||
return { name: null, value: null };
|
||
}
|
||
|
||
const name = this.getNodeText(nameNode);
|
||
let value: any = null;
|
||
|
||
// 查找值节点(跳过冒号和逗号)
|
||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||
if (child.name !== NODE_TYPES.PROPERTY_NAME &&
|
||
child.name !== ':' &&
|
||
child.name !== ',') {
|
||
value = this.parseValue(child);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// HTTP Header 的值必须转换为字符串
|
||
if (value !== null && value !== undefined) {
|
||
value = String(value);
|
||
}
|
||
|
||
return { name, value };
|
||
}
|
||
|
||
/**
|
||
* 解析值节点(字符串、数字、标识符、嵌套块、变量引用)
|
||
*/
|
||
private parseValue(node: SyntaxNode): any {
|
||
if (node.name === NODE_TYPES.STRING_LITERAL) {
|
||
const text = this.getNodeText(node);
|
||
// 移除引号
|
||
const value = text.replace(/^["']|["']$/g, '');
|
||
// 替换字符串中的变量
|
||
return this.getVariableResolver().replaceVariables(value);
|
||
} else if (node.name === NODE_TYPES.NUMBER_LITERAL) {
|
||
const text = this.getNodeText(node);
|
||
return parseFloat(text);
|
||
} else if (node.name === NODE_TYPES.VARIABLE_REF) {
|
||
// 处理变量引用节点
|
||
const text = this.getNodeText(node);
|
||
const resolver = this.getVariableResolver();
|
||
const ref = resolver.parseVariableRef(text);
|
||
if (ref) {
|
||
return resolver.resolveVariable(ref.name, ref.defaultValue);
|
||
}
|
||
return text;
|
||
} else if (node.name === NODE_TYPES.IDENTIFIER) {
|
||
const text = this.getNodeText(node);
|
||
// 处理布尔值
|
||
if (text === 'true') return true;
|
||
if (text === 'false') return false;
|
||
// 处理 null
|
||
if (text === 'null') return null;
|
||
// 其他标识符作为字符串
|
||
return text;
|
||
} else if (node.name === NODE_TYPES.JSON_BLOCK) {
|
||
// 嵌套对象
|
||
return this.parseJsonBlock(node);
|
||
} else {
|
||
// 未知类型,返回原始文本
|
||
return this.getNodeText(node);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取节点的文本内容
|
||
*/
|
||
private getNodeText(node: SyntaxNode): string {
|
||
return this.state.doc.sliceString(node.from, node.to);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 便捷函数:解析指定位置的 HTTP 请求
|
||
* @param state EditorState
|
||
* @param pos 光标位置
|
||
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
|
||
*/
|
||
export function parseHttpRequest(
|
||
state: EditorState,
|
||
pos: number,
|
||
blockRange?: { from: number; to: number }
|
||
): HttpRequest | null {
|
||
const parser = new HttpRequestParser(state, blockRange);
|
||
return parser.parseRequestAt(pos);
|
||
}
|
||
|