From 7bb5f8baccafc6969facf23f3e1165dbb6eff2ca Mon Sep 17 00:00:00 2001 From: panFD Date: Sun, 21 Jun 2026 00:05:53 +0800 Subject: [PATCH] feat(config-editor): add RecordValue serializer with tests --- .../__tests__/.gitignore | 1 + .../__tests__/recordValue.serializer.test.ts | 44 +++++++++++++++++++ .../src/io/serializer.ts | 32 ++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 extensions/pixelhero-config-editor/__tests__/.gitignore create mode 100644 extensions/pixelhero-config-editor/__tests__/recordValue.serializer.test.ts create mode 100644 extensions/pixelhero-config-editor/src/io/serializer.ts diff --git a/extensions/pixelhero-config-editor/__tests__/.gitignore b/extensions/pixelhero-config-editor/__tests__/.gitignore new file mode 100644 index 00000000..3018b3a6 --- /dev/null +++ b/extensions/pixelhero-config-editor/__tests__/.gitignore @@ -0,0 +1 @@ +.tmp/ diff --git a/extensions/pixelhero-config-editor/__tests__/recordValue.serializer.test.ts b/extensions/pixelhero-config-editor/__tests__/recordValue.serializer.test.ts new file mode 100644 index 00000000..d4763688 --- /dev/null +++ b/extensions/pixelhero-config-editor/__tests__/recordValue.serializer.test.ts @@ -0,0 +1,44 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { serializeValue, serializeEntry } from '../src/io/serializer'; +import { RecordValue } from '../src/io/recordValue'; + +test('serializeValue: num/str/bool', () => { + assert.equal(serializeValue({ kind: 'num', value: 400 }), '400'); + assert.equal(serializeValue({ kind: 'str', value: '小铁卫' }), '"小铁卫"'); + assert.equal(serializeValue({ kind: 'bool', value: true }), 'true'); +}); +test('serializeValue: enumRef', () => { + const v: RecordValue = { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' }; + assert.equal(serializeValue(v), 'FacSet.HERO'); +}); +test('serializeValue: speed', () => { + const v: RecordValue = { kind: 'speed', level: 'Slow3' }; + assert.equal(serializeValue(v), 'AtkSpeedSet[AtkSpeedLv.Slow3].cd'); +}); +test('serializeValue: arr', () => { + const v: RecordValue = { kind: 'arr', items: [{ kind: 'num', value: 1 }, { kind: 'num', value: 2 }] }; + assert.equal(serializeValue(v), '[1,2]'); +}); +test('serializeValue: obj', () => { + const v: RecordValue = { kind: 'obj', props: { a: { kind: 'num', value: 1 }, b: { kind: 'str', value: 'x' } } }; + assert.equal(serializeValue(v), '{a:1,b:"x"}'); +}); +test('serializeValue: raw passthrough', () => { + const v: RecordValue = { kind: 'raw', text: 'a + b' }; + assert.equal(serializeValue(v), 'a + b'); +}); +test('serializeEntry: all-scalar one line with trailing comma', () => { + const v: RecordValue = { kind: 'obj', props: { uuid: { kind: 'num', value: 1 }, name: { kind: 'str', value: 'x' } } }; + assert.equal(serializeEntry('1', v), '1:{uuid:1,name:"x"},'); +}); +test('serializeEntry: nested on continuation lines', () => { + const v: RecordValue = { kind: 'obj', props: { + uuid: { kind: 'num', value: 5011 }, + skills: { kind: 'obj', props: { 6001: { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } } } }, + } }; + const out = serializeEntry('5011', v); + assert.match(out, /^5011:\{uuid:5011,$/m); // 首行:键 + 标量 + assert.match(out, / skills:\{6001:\{uuid:6001\}\},$/m); // 续行 8 空格 + assert.match(out, /^ \},$/m); // 闭合 4 空格 +}); diff --git a/extensions/pixelhero-config-editor/src/io/serializer.ts b/extensions/pixelhero-config-editor/src/io/serializer.ts new file mode 100644 index 00000000..af2956b6 --- /dev/null +++ b/extensions/pixelhero-config-editor/src/io/serializer.ts @@ -0,0 +1,32 @@ +import { RecordValue, isScalar } from './recordValue'; + +export function serializeValue(v: RecordValue): string { + switch (v.kind) { + case 'num': return String(v.value); + case 'str': return JSON.stringify(v.value); + case 'bool': return String(v.value); + case 'enumRef': return `${v.qualifier}.${v.member}`; + case 'speed': return `AtkSpeedSet[AtkSpeedLv.${v.level}].cd`; + case 'arr': return `[${v.items.map(serializeValue).join(',')}]`; + case 'obj': { + const body = Object.entries(v.props).map(([k, val]) => `${k}:${serializeValue(val)}`).join(','); + return `{${body}}`; + } + case 'raw': return v.text; + } +} + +/** 序列化一条顶层记录。首行无缩进(缩进由调用处保留的 trivia 提供);嵌套字段 8 空格续行;闭合 4 空格。 */ +export function serializeEntry(key: string, obj: RecordValue): string { + if (obj.kind !== 'obj') return `${key}:${serializeValue(obj)},`; + const props = obj.props; + const keys = Object.keys(props); + const scalars = keys.filter(k => isScalar(props[k])); + const nested = keys.filter(k => !isScalar(props[k])); + const scalarTxt = scalars.map(k => `${k}:${serializeValue(props[k])}`).join(','); + if (nested.length === 0) return `${key}:{${scalarTxt}},`; + const lines = [`${key}:{${scalarTxt},`]; + for (const k of nested) lines.push(` ${k}:${serializeValue(props[k])},`); + lines.push(' },'); + return lines.join('\n'); +}