segment-matcher
Version:
Segment Matcher - TypeScript version with dual ESM/CJS format support
408 lines (407 loc) • 14.6 kB
JavaScript
import { PatternToken } from './pattern_token.js';
import { PatternParseError } from './errors.js';
/**
* 解析缓存
*/
const parseCache = new Map();
/**
* 优化的字符串分割函数
*/
function optimizedSplit(str, delimiter) {
const index = str.indexOf(delimiter);
if (index === -1) {
return [str];
}
return [str.substring(0, index), str.substring(index + delimiter.length)];
}
/**
* 优化的字符串修剪函数
*/
function optimizedTrim(str) {
let start = 0;
let end = str.length;
while (start < end && str[start] <= ' ')
start++;
while (end > start && str[end - 1] <= ' ')
end--;
return str.substring(start, end);
}
/**
* 模式解析器类
*
* 负责将命令模式字符串解析为令牌数组。
* 支持多种模式元素:字面量、类型化字面量、必需参数、可选参数、剩余参数等。
*
* @example
* ```typescript
* const tokens = PatternParser.parse('hello <name:text> [count:number=1]');
* // 返回解析后的令牌数组
* ```
*/
export class PatternParser {
/**
* 解析命令模式字符串
*
* 将模式字符串解析为令牌数组,支持以下模式元素:
* - 普通字面量:`hello`
* - 类型化字面量:`{text:start}`, `{face:1}`, `{image:avatar.png}`
* - 必需参数:`<name:text>`, `<count:number>`
* - 可选参数:`[message:text]`, `[count:number=1]`
* - 剩余参数:`[...rest]`, `[...rest:face]`
*
* @param pattern - 命令模式字符串
* @returns 解析后的令牌数组
*
* @throws {PatternParseError} 当模式格式错误或解析失败时抛出
*
* @example
* ```typescript
* // 基础模式
* const tokens1 = PatternParser.parse('hello <name:text>');
*
* // 复杂模式
* const tokens2 = PatternParser.parse('{text:start}<command:text>[count:number=1][...rest]');
* ```
*/
static parse(pattern) {
// 检查缓存
if (parseCache.has(pattern)) {
return parseCache.get(pattern);
}
const tokens = [];
let i = 0; // 当前解析位置
try {
while (i < pattern.length) {
const char = pattern[i];
// 根据当前字符类型选择解析方法
if (char === '{') {
// 解析类型化字面量:{type:value}
tokens.push(PatternParser.parseTypedLiteral(pattern, i));
i = PatternParser.findClosingBrace(pattern, i);
}
else if (char === '<') {
// 解析必需参数:<name:type>
tokens.push(PatternParser.parseRequiredParameter(pattern, i));
i = PatternParser.findClosingBrace(pattern, i);
}
else if (char === '[') {
// 解析可选参数:[name:type] 或 [name:type=default]
tokens.push(PatternParser.parseOptionalParameter(pattern, i));
i = PatternParser.findClosingBrace(pattern, i);
}
else {
// 解析普通字面量
const { token, newIndex } = PatternParser.parseLiteral(pattern, i);
if (token)
tokens.push(token);
i = newIndex;
}
}
// 缓存结果
parseCache.set(pattern, tokens);
return tokens;
}
catch (error) {
// 统一错误处理,确保抛出 PatternParseError
if (error instanceof PatternParseError) {
throw error;
}
throw new PatternParseError(`Failed to parse pattern: ${error instanceof Error ? error.message : 'Unknown error'}`, pattern, i);
}
}
/**
* 清除解析缓存
*
* 在内存紧张或需要强制重新解析时调用。
*/
static clearCache() {
parseCache.clear();
}
/**
* 获取缓存统计信息
*/
static getCacheStats() {
return { size: parseCache.size };
}
/**
* 解析类型化字面量
*
* 解析格式为 `{type:value}` 的类型化字面量。
* 支持各种消息段类型,如 text、face、image、at 等。
*
* @param pattern - 完整的模式字符串
* @param startIndex - 开始解析的位置('{' 的位置)
* @returns 解析后的类型化字面量令牌
*
* @example
* ```typescript
* // 文本类型化字面量
* const token1 = PatternParser.parseTypedLiteral('{text:start}', 0);
*
* // 表情类型化字面量
* const token2 = PatternParser.parseTypedLiteral('{face:1}', 0);
*
* // 图片类型化字面量
* const token3 = PatternParser.parseTypedLiteral('{image:avatar.png}', 0);
* ```
*/
static parseTypedLiteral(pattern, startIndex) {
let i = startIndex + 1; // 跳过 '{'
let content = '';
// 收集 '{' 和 '}' 之间的内容
while (i < pattern.length && pattern[i] !== '}') {
content += pattern[i];
i++;
}
// 只分割第一个冒号,避免 URL 中的冒号被错误分割
const parts = optimizedSplit(content, ':');
const type = optimizedTrim(parts[0]);
const value = parts.length > 1 ? optimizedTrim(parts[1]) : '';
// 不做类型白名单校验
return PatternToken.createTypedLiteral(type, value);
}
/**
* 解析必需参数
*
* 解析格式为 `<name:type>` 的必需参数。
* 必需参数在匹配时必须提供,否则匹配失败。
*
* @param pattern - 完整的模式字符串
* @param startIndex - 开始解析的位置('<' 的位置)
* @returns 解析后的必需参数令牌
*
* @example
* ```typescript
* // 文本参数
* const token1 = PatternParser.parseRequiredParameter('<name:text>', 0);
*
* // 数字参数
* const token2 = PatternParser.parseRequiredParameter('<count:number>', 0);
*
* // 表情参数
* const token3 = PatternParser.parseRequiredParameter('<emoji:face>', 0);
* ```
*/
static parseRequiredParameter(pattern, startIndex) {
let i = startIndex + 1; // 跳过 '<'
let content = '';
// 收集 '<' 和 '>' 之间的内容
while (i < pattern.length && pattern[i] !== '>') {
content += pattern[i];
i++;
}
// 分割参数名和类型
const parts = optimizedSplit(content, ':');
const name = optimizedTrim(parts[0]);
const segType = parts.length > 1 ? optimizedTrim(parts[1]) : 'text';
// 不做类型白名单校验
return PatternToken.createParameter(name, segType, false);
}
/**
* 解析可选参数
*
* 解析格式为 `[name:type]` 或 `[name:type=default]` 的可选参数。
* 支持默认值语法,包括字符串、数字、JSON 对象等。
* 还支持剩余参数语法 `[...rest]` 和 `[...rest:type]`。
*
* @param pattern - 完整的模式字符串
* @param startIndex - 开始解析的位置('[' 的位置)
* @returns 解析后的可选参数令牌
*
* @example
* ```typescript
* // 基础可选参数
* const token1 = PatternParser.parseOptionalParameter('[message:text]', 0);
*
* // 带默认值的可选参数
* const token2 = PatternParser.parseOptionalParameter('[count:number=1]', 0);
*
* // 带 JSON 默认值的可选参数
* const token3 = PatternParser.parseOptionalParameter('[emoji:face={"id":1}]', 0);
*
* // 剩余参数
* const token4 = PatternParser.parseOptionalParameter('[...rest]', 0);
* const token5 = PatternParser.parseOptionalParameter('[...rest:face]', 0);
* ```
*/
static parseOptionalParameter(pattern, startIndex) {
// 使用 findClosingBrace 获取完整内容
const endIndex = PatternParser.findClosingBrace(pattern, startIndex);
const content = pattern.slice(startIndex + 1, endIndex - 1); // 不包括 '[' 和 ']'
// 检查是否为剩余参数:...rest 或 ...rest:type
if (content.startsWith('...')) {
const restContent = content.slice(3); // 跳过 '...'
if (restContent.includes(':')) {
const [name, type] = optimizedSplit(restContent, ':');
return PatternToken.createRestParameter(optimizedTrim(name), optimizedTrim(type));
}
else {
return PatternToken.createRestParameter(optimizedTrim(restContent), null);
}
}
// 检查是否包含默认值(等号)
const equalIndex = content.indexOf('=');
if (equalIndex !== -1) {
// 包含默认值:[name:type=default]
const beforeEqual = content.slice(0, equalIndex);
const afterEqual = content.slice(equalIndex + 1);
const [name, type] = optimizedSplit(beforeEqual, ':');
const defaultValue = PatternParser.parseDefaultValue(afterEqual);
return PatternToken.createParameter(optimizedTrim(name), optimizedTrim(type || 'text'), true, defaultValue);
}
else {
// 不包含默认值:[name:type]
const [name, type] = optimizedSplit(content, ':');
return PatternToken.createParameter(optimizedTrim(name), optimizedTrim(type || 'text'), true);
}
}
/**
* 解析默认值
*
* 解析字符串、数字、JSON 对象等类型的默认值。
* 支持嵌套的 JSON 结构,如对象和数组。
*
* @param defaultValueStr - 默认值字符串
* @returns 解析后的默认值
*
* @example
* ```typescript
* // 字符串默认值
* PatternParser.parseDefaultValue('hello'); // 'hello'
*
* // 数字默认值
* PatternParser.parseDefaultValue('42'); // 42
*
* // JSON 对象默认值
* PatternParser.parseDefaultValue('{"id":1,"name":"test"}'); // {id: 1, name: "test"}
*
* // 数组默认值
* PatternParser.parseDefaultValue('[1,2,3]'); // [1, 2, 3]
* ```
*/
static parseDefaultValue(defaultValueStr) {
const trimmed = optimizedTrim(defaultValueStr);
// 尝试解析为 JSON
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
return JSON.parse(trimmed);
}
catch {
// JSON 解析失败,尝试修复常见问题
try {
// 修复缺少引号的键名
const fixed = trimmed.replace(/([a-zA-Z0-9_]+):/g, '"$1":');
return JSON.parse(fixed);
}
catch {
// 仍然失败,返回原始字符串
return trimmed;
}
}
}
// 尝试解析为数字
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
const num = Number(trimmed);
if (!isNaN(num)) {
return num;
}
}
// 布尔值
if (trimmed === 'true')
return true;
if (trimmed === 'false')
return false;
// 返回原始字符串
return trimmed;
}
/**
* 解析普通字面量
*
* 解析连续的普通字符作为字面量令牌。
* 遇到特殊字符('{', '<', '[')时停止解析。
*
* @param pattern - 完整的模式字符串
* @param startIndex - 开始解析的位置
* @returns 解析结果,包含令牌和新的解析位置
*
* @example
* ```typescript
* // 解析简单字面量
* const result1 = PatternParser.parseLiteral('hello <name>', 0);
* // result1.token.value === 'hello', result1.newIndex === 5
*
* // 解析包含空格的字面量
* const result2 = PatternParser.parseLiteral('hello world <name>', 0);
* // result2.token.value === 'hello world ', result2.newIndex === 12
* ```
*/
static parseLiteral(pattern, startIndex) {
let i = startIndex;
let literal = '';
// 收集连续的普通字符
while (i < pattern.length) {
const char = pattern[i];
// 遇到特殊字符时停止
if (char === '{' || char === '<' || char === '[') {
break;
}
literal += char;
i++;
}
// 如果收集到了字面量内容,创建令牌
if (literal.length > 0) {
return {
token: PatternToken.createLiteral(literal),
newIndex: i
};
}
// 没有收集到内容,返回 null
return {
token: null,
newIndex: i
};
}
/**
* 查找匹配的闭合括号
*
* 从指定的开始位置查找匹配的闭合括号。
* 支持嵌套的括号结构,如 `{[...]}`。
*
* @param pattern - 完整的模式字符串
* @param startIndex - 开始位置(开括号的位置)
* @returns 闭合括号的位置
*
* @throws {PatternParseError} 当找不到匹配的闭合括号时抛出
*
* @example
* ```typescript
* // 简单括号匹配
* PatternParser.findClosingBrace('{text:hello}', 0); // 返回 11
*
* // 嵌套括号匹配
* PatternParser.findClosingBrace('{[text:hello]}', 0); // 返回 13
* ```
*/
static findClosingBrace(pattern, startIndex) {
const openChar = pattern[startIndex];
const closeChar = openChar === '{' ? '}' : openChar === '<' ? '>' : ']';
let depth = 0;
let i = startIndex;
while (i < pattern.length) {
const char = pattern[i];
if (char === openChar) {
depth++;
}
else if (char === closeChar) {
depth--;
if (depth === 0) {
return i + 1; // 返回闭合括号后的位置
}
}
i++;
}
// 找不到匹配的闭合括号
throw new PatternParseError(`Unmatched opening brace '${openChar}' at position ${startIndex}`, pattern, startIndex);
}
}