diff --git a/extensions/pixelhero-config-editor/__tests__/parser.test.ts b/extensions/pixelhero-config-editor/__tests__/parser.test.ts new file mode 100644 index 00000000..13cd1d37 --- /dev/null +++ b/extensions/pixelhero-config-editor/__tests__/parser.test.ts @@ -0,0 +1,47 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import * as ts from 'typescript'; +import { parseExpression, findExportObjectLiteral } from '../src/io/parser'; + +function parse(text: string): ts.Expression { + const src = ts.createSourceFile('x.ts', `const _ = ${text};`, ts.ScriptTarget.Latest, true); + const decl = src.statements[0] as ts.VariableStatement; + return decl.declarationList.declarations[0].initializer!; +} + +test('num / str / bool', () => { + assert.deepEqual(parseExpression(parse('400')), { kind: 'num', value: 400 }); + assert.deepEqual(parseExpression(parse('"小铁卫"')), { kind: 'str', value: '小铁卫' }); + assert.deepEqual(parseExpression(parse('true')), { kind: 'bool', value: true }); +}); +test('enumRef: FacSet.HERO', () => { + assert.deepEqual(parseExpression(parse('FacSet.HERO')), { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' }); +}); +test('speed: AtkSpeedSet[AtkSpeedLv.Slow3].cd', () => { + assert.deepEqual(parseExpression(parse('AtkSpeedSet[AtkSpeedLv.Slow3].cd')), { kind: 'speed', level: 'Slow3' }); +}); +test('non-Speed two-segment access falls back to enumRef', () => { + assert.deepEqual(parseExpression(parse('Foo.bar')), { kind: 'enumRef', qualifier: 'Foo', member: 'bar' }); +}); +test('arr', () => { + assert.deepEqual(parseExpression(parse('[1,2,3]')), { kind: 'arr', items: [ + { kind: 'num', value: 1 }, { kind: 'num', value: 2 }, { kind: 'num', value: 3 }] }); +}); +test('obj with numeric + identifier keys', () => { + const v = parseExpression(parse('{6001:{uuid:6001},name:"x"}')); + assert.equal(v.kind, 'obj'); + assert.deepEqual(v.props['6001'], { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } }); + assert.deepEqual(v.props['name'], { kind: 'str', value: 'x' }); +}); +test('raw: unsupported expression preserved verbatim', () => { + const v = parseExpression(parse('a + b')); + assert.equal(v.kind, 'raw'); + assert.equal((v as any).text, 'a + b'); +}); +test('findExportObjectLiteral locates HeroInfo', () => { + const src = ts.createSourceFile('x.ts', + `export const HeroInfo: Record = { 5011:{uuid:5011} };`, ts.ScriptTarget.Latest, true); + const node = findExportObjectLiteral(src, 'HeroInfo'); + assert.ok(node); + assert.equal(node!.properties.length, 1); +}); diff --git a/extensions/pixelhero-config-editor/src/io/parser.ts b/extensions/pixelhero-config-editor/src/io/parser.ts new file mode 100644 index 00000000..0bb14221 --- /dev/null +++ b/extensions/pixelhero-config-editor/src/io/parser.ts @@ -0,0 +1,65 @@ +import * as ts from 'typescript'; +import { RecordValue } from './recordValue'; + +/** 在 SourceFile 中查找 `export const = {...}`,返回其对象字面量节点。 */ +export function findExportObjectLiteral(src: ts.SourceFile, name: string): ts.ObjectLiteralExpression | null { + let result: ts.ObjectLiteralExpression | null = null; + const visit = (node: ts.Node) => { + if (result) return; + if (ts.isVariableStatement(node)) { + for (const d of node.declarationList.declarations) { + if (d.name.getText() === name && d.initializer && ts.isObjectLiteralExpression(d.initializer)) { + result = d.initializer; + return; + } + } + } + ts.forEachChild(node, visit); + }; + visit(src); + return result; +} + +/** 识别 `AtkSpeedSet[AtkSpeedLv.].cd`,否则 null。 */ +function tryParseSpeedExpr(node: ts.PropertyAccessExpression): RecordValue | null { + if (node.name.text !== 'cd') return null; + const inner = node.expression; + if (!ts.isElementAccessExpression(inner)) return null; + if (!ts.isIdentifier(inner.expression) || inner.expression.text !== 'AtkSpeedSet') return null; + const arg = inner.argumentExpression; + if (!ts.isPropertyAccessExpression(arg)) return null; + if (!ts.isIdentifier(arg.expression) || arg.expression.text !== 'AtkSpeedLv') return null; + return { kind: 'speed', level: arg.name.text }; +} + +export function parseExpression(node: ts.Expression): RecordValue { + if (ts.isNumericLiteral(node)) return { kind: 'num', value: Number(node.text) }; + if (ts.isStringLiteral(node)) return { kind: 'str', value: node.text }; + if (node.kind === ts.SyntaxKind.TrueKeyword) return { kind: 'bool', value: true }; + if (node.kind === ts.SyntaxKind.FalseKeyword) return { kind: 'bool', value: false }; + + if (ts.isPropertyAccessExpression(node)) { + const speed = tryParseSpeedExpr(node); + if (speed) return speed; + if (ts.isIdentifier(node.expression)) { + return { kind: 'enumRef', qualifier: node.expression.text, member: node.name.text }; + } + return { kind: 'raw', text: node.getText() }; // 多级 a.b.c 等 + } + + if (ts.isArrayLiteralExpression(node)) { + return { kind: 'arr', items: node.elements.map(e => parseExpression(e)) }; + } + + if (ts.isObjectLiteralExpression(node)) { + const props: Record = {}; + for (const m of node.properties) { + if (ts.isPropertyAssignment(m)) { + props[m.name.getText()] = parseExpression(m.initializer); + } + } + return { kind: 'obj', props }; + } + + return { kind: 'raw', text: node.getText() }; // 二元/模板/调用/其他元素访问 +}