Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions src/parser/aison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}
75 changes: 75 additions & 0 deletions test/aison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
1 change: 1 addition & 0 deletions unreleased/aison-stringify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `AiSON.stringify` を実装しました。`JSON.stringify`と互換性のあるかたちで引数を取りますが、`JSON.stringify`のようにAiSONに直接変換できない値を含むオブジェクトを渡すことはできません。
Loading