diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 7ece385f..ea310ee5 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -96,6 +96,8 @@ class AiScriptUserError extends AiScriptRuntimeError { export class AiSON { // (undocumented) static parse(input: string): JsValue; + // (undocumented) + static stringify(value: JsValue, _unused?: null, indent?: number | string): string; } // @public (undocumented) diff --git a/src/parser/aison.ts b/src/parser/aison.ts index 14b6967e..7a7698a7 100644 --- a/src/parser/aison.ts +++ b/src/parser/aison.ts @@ -4,7 +4,9 @@ import { nodeToJs } from '../utils/node-to-js.js'; import { Scanner } from './scanner.js'; import { parseAiSonTopLevel } from './syntaxes/aison.js'; +import { jsToVal } from '../interpreter/util.js'; import type { JsValue } from '../interpreter/util.js'; +import type { Value } from '../interpreter/value.js'; export class AiSON { public static parse(input: string): JsValue { @@ -13,4 +15,52 @@ export class AiSON { return nodeToJs(ast); } + + private static stringifyWalk(value: Value, indent: string | null, currentIndent = ''): string { + switch (value.type) { + case 'bool': return value.value ? 'true' : 'false'; + case 'null': return 'null'; + case 'num': return value.value.toString(); + case 'str': return JSON.stringify(value.value); + case 'arr': { + if (value.value.length === 0) return '[]'; + const items = value.value.map(item => this.stringifyWalk(item, indent, currentIndent + (indent ?? ''))); + if (indent != null && indent !== '') { + return `[\n${currentIndent + indent}${items.join(`,\n${currentIndent + indent}`)}\n${currentIndent}]`; + } else { + return `[${items.join(', ')}]`; + } + } + case 'obj': { + const keys = [...value.value.keys()]; + if (keys.length === 0) return '{}'; + const items = keys.map(key => { + const val = value.value.get(key)!; + return `${key}: ${this.stringifyWalk(val, indent, currentIndent + (indent ?? ''))}`; + }); + if (indent != null && indent !== '') { + return `{\n${currentIndent + indent}${items.join(`,\n${currentIndent + indent}`)}\n${currentIndent}}`; + } else { + return `{${items.join(', ')}}`; + } + } + default: + throw new Error(`Cannot stringify value of type: ${value.type}`); + } + } + + public static stringify(value: JsValue, _unused = null, indent: number | string = 0): string { + let _indent: string | null = null; + if (typeof indent === 'number') { + if (indent > 0) { + _indent = ' '.repeat(indent); + } + } else if (indent.length > 0) { + _indent = indent; + } + + const aisValue = jsToVal(value); + + return this.stringifyWalk(aisValue, _indent); + } } diff --git a/test/aison.ts b/test/aison.ts index 97337530..f4d71aff 100644 --- a/test/aison.ts +++ b/test/aison.ts @@ -78,3 +78,78 @@ greet()`)).toThrow(); {foo: "bar"}`)).toThrow(); }); }); + +describe('stringify', () => { + test.concurrent('str', () => { + expect(AiSON.stringify('Ai-chan kawaii')).toEqual('"Ai-chan kawaii"'); + }); + + test.concurrent('number', () => { + expect(AiSON.stringify(42)).toEqual('42'); + }); + + test.concurrent('bool', () => { + expect(AiSON.stringify(true)).toEqual('true'); + }); + + test.concurrent('null', () => { + expect(AiSON.stringify(null)).toEqual('null'); + }); + + test.concurrent('array', () => { + expect(AiSON.stringify([1, 2, 3])).toEqual('[1, 2, 3]'); + }); + + test.concurrent('object', () => { + expect(AiSON.stringify({ key: 'value' })).toEqual('{key: "value"}'); + }); + + test.concurrent('nested', () => { + expect(AiSON.stringify([{ key: 'value' }])).toEqual('[{key: "value"}]'); + }); + + test.concurrent('pretty print: array', () => { + expect(AiSON.stringify([1, 2, 3], null, 2)).toEqual(`[ + 1, + 2, + 3 +]`); + }); + + test.concurrent('pretty print: object', () => { + expect(AiSON.stringify({ key: 'value', foo: 'bar' }, null, 2)).toEqual(`{ + key: "value", + foo: "bar" +}`); + }); + + test.concurrent('pretty print: nested', () => { + expect(AiSON.stringify({ arr: [1, 2, { key: 'value' }] }, null, 2)).toEqual(`{ + arr: [ + 1, + 2, + { + key: "value" + } + ] +}`); + }); + + test.concurrent('custom indent', () => { + expect(AiSON.stringify({ key: 'value', foo: 'bar' }, null, '\t')).toEqual(`{ +\tkey: "value", +\tfoo: "bar" +}`); + }); + + test.concurrent('no indent when indent is 0', () => { + expect(AiSON.stringify({ key: 'value', foo: 'bar' }, null, 0)).toEqual('{key: "value", foo: "bar"}'); + }); + + test.concurrent('can parse generated aison', () => { + const obj = { arr: [1, 2, { key: 'value' }] }; + const aison = AiSON.stringify(obj); + const parsed = AiSON.parse(aison); + expect(parsed).toStrictEqual(obj); + }); +}); diff --git a/unreleased/aison-stringify.md b/unreleased/aison-stringify.md new file mode 100644 index 00000000..05e0f763 --- /dev/null +++ b/unreleased/aison-stringify.md @@ -0,0 +1 @@ +- `AiSON.stringify` を実装しました。`JSON.stringify`と互換性のあるかたちで引数を取りますが、`JSON.stringify`のようにAiSONに直接変換できない値を含むオブジェクトを渡すことはできません。