Skip to content

Commit a58e85b

Browse files
enhance: implement AiSON.stringify (#983)
* enhance: implement `AiSON.stringify` * Update Changelog * Run api extractor * Update Changelog
1 parent 1ced8c7 commit a58e85b

File tree

4 files changed

+128
-0
lines changed

4 files changed

+128
-0
lines changed

etc/aiscript.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ class AiScriptUserError extends AiScriptRuntimeError {
9696
export class AiSON {
9797
// (undocumented)
9898
static parse(input: string): JsValue;
99+
// (undocumented)
100+
static stringify(value: JsValue, _unused?: null, indent?: number | string): string;
99101
}
100102

101103
// @public (undocumented)

src/parser/aison.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import { nodeToJs } from '../utils/node-to-js.js';
55
import { Scanner } from './scanner.js';
66
import { parseAiSonTopLevel } from './syntaxes/aison.js';
7+
import { jsToVal } from '../interpreter/util.js';
78
import type { JsValue } from '../interpreter/util.js';
9+
import type { Value } from '../interpreter/value.js';
810

911
export class AiSON {
1012
public static parse(input: string): JsValue {
@@ -13,4 +15,52 @@ export class AiSON {
1315

1416
return nodeToJs(ast);
1517
}
18+
19+
private static stringifyWalk(value: Value, indent: string | null, currentIndent = ''): string {
20+
switch (value.type) {
21+
case 'bool': return value.value ? 'true' : 'false';
22+
case 'null': return 'null';
23+
case 'num': return value.value.toString();
24+
case 'str': return JSON.stringify(value.value);
25+
case 'arr': {
26+
if (value.value.length === 0) return '[]';
27+
const items = value.value.map(item => this.stringifyWalk(item, indent, currentIndent + (indent ?? '')));
28+
if (indent != null && indent !== '') {
29+
return `[\n${currentIndent + indent}${items.join(`,\n${currentIndent + indent}`)}\n${currentIndent}]`;
30+
} else {
31+
return `[${items.join(', ')}]`;
32+
}
33+
}
34+
case 'obj': {
35+
const keys = [...value.value.keys()];
36+
if (keys.length === 0) return '{}';
37+
const items = keys.map(key => {
38+
const val = value.value.get(key)!;
39+
return `${key}: ${this.stringifyWalk(val, indent, currentIndent + (indent ?? ''))}`;
40+
});
41+
if (indent != null && indent !== '') {
42+
return `{\n${currentIndent + indent}${items.join(`,\n${currentIndent + indent}`)}\n${currentIndent}}`;
43+
} else {
44+
return `{${items.join(', ')}}`;
45+
}
46+
}
47+
default:
48+
throw new Error(`Cannot stringify value of type: ${value.type}`);
49+
}
50+
}
51+
52+
public static stringify(value: JsValue, _unused = null, indent: number | string = 0): string {
53+
let _indent: string | null = null;
54+
if (typeof indent === 'number') {
55+
if (indent > 0) {
56+
_indent = ' '.repeat(indent);
57+
}
58+
} else if (indent.length > 0) {
59+
_indent = indent;
60+
}
61+
62+
const aisValue = jsToVal(value);
63+
64+
return this.stringifyWalk(aisValue, _indent);
65+
}
1666
}

test/aison.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,78 @@ greet()`)).toThrow();
7878
{foo: "bar"}`)).toThrow();
7979
});
8080
});
81+
82+
describe('stringify', () => {
83+
test.concurrent('str', () => {
84+
expect(AiSON.stringify('Ai-chan kawaii')).toEqual('"Ai-chan kawaii"');
85+
});
86+
87+
test.concurrent('number', () => {
88+
expect(AiSON.stringify(42)).toEqual('42');
89+
});
90+
91+
test.concurrent('bool', () => {
92+
expect(AiSON.stringify(true)).toEqual('true');
93+
});
94+
95+
test.concurrent('null', () => {
96+
expect(AiSON.stringify(null)).toEqual('null');
97+
});
98+
99+
test.concurrent('array', () => {
100+
expect(AiSON.stringify([1, 2, 3])).toEqual('[1, 2, 3]');
101+
});
102+
103+
test.concurrent('object', () => {
104+
expect(AiSON.stringify({ key: 'value' })).toEqual('{key: "value"}');
105+
});
106+
107+
test.concurrent('nested', () => {
108+
expect(AiSON.stringify([{ key: 'value' }])).toEqual('[{key: "value"}]');
109+
});
110+
111+
test.concurrent('pretty print: array', () => {
112+
expect(AiSON.stringify([1, 2, 3], null, 2)).toEqual(`[
113+
1,
114+
2,
115+
3
116+
]`);
117+
});
118+
119+
test.concurrent('pretty print: object', () => {
120+
expect(AiSON.stringify({ key: 'value', foo: 'bar' }, null, 2)).toEqual(`{
121+
key: "value",
122+
foo: "bar"
123+
}`);
124+
});
125+
126+
test.concurrent('pretty print: nested', () => {
127+
expect(AiSON.stringify({ arr: [1, 2, { key: 'value' }] }, null, 2)).toEqual(`{
128+
arr: [
129+
1,
130+
2,
131+
{
132+
key: "value"
133+
}
134+
]
135+
}`);
136+
});
137+
138+
test.concurrent('custom indent', () => {
139+
expect(AiSON.stringify({ key: 'value', foo: 'bar' }, null, '\t')).toEqual(`{
140+
\tkey: "value",
141+
\tfoo: "bar"
142+
}`);
143+
});
144+
145+
test.concurrent('no indent when indent is 0', () => {
146+
expect(AiSON.stringify({ key: 'value', foo: 'bar' }, null, 0)).toEqual('{key: "value", foo: "bar"}');
147+
});
148+
149+
test.concurrent('can parse generated aison', () => {
150+
const obj = { arr: [1, 2, { key: 'value' }] };
151+
const aison = AiSON.stringify(obj);
152+
const parsed = AiSON.parse(aison);
153+
expect(parsed).toStrictEqual(obj);
154+
});
155+
});

unreleased/aison-stringify.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `AiSON.stringify` を実装しました。`JSON.stringify`と互換性のあるかたちで引数を取りますが、`JSON.stringify`のようにAiSONに直接変換できない値を含むオブジェクトを渡すことはできません。

0 commit comments

Comments
 (0)