@jarenjs/josl 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +235 -0
- package/README.md +494 -0
- package/dist/types/cst.d.ts +78 -0
- package/dist/types/csv-machine.d.ts +104 -0
- package/dist/types/csv-stream.d.ts +102 -0
- package/dist/types/csv.d.ts +141 -0
- package/dist/types/errors.d.ts +79 -0
- package/dist/types/gbnf.d.ts +20 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/jsonx-scalar.d.ts +92 -0
- package/dist/types/jsonx-stream.d.ts +163 -0
- package/dist/types/jsonx.d.ts +31 -0
- package/dist/types/machine.d.ts +97 -0
- package/dist/types/parse.d.ts +24 -0
- package/dist/types/stream.d.ts +32 -0
- package/dist/types/stringify.d.ts +63 -0
- package/dist/types/util.d.ts +56 -0
- package/dist/types/values.d.ts +60 -0
- package/dist/types/write.d.ts +92 -0
- package/package.json +104 -0
- package/schemas/jaren-josl-data.schema.json +21 -0
- package/src/cst.js +256 -0
- package/src/csv-machine.js +908 -0
- package/src/csv-stream.js +196 -0
- package/src/csv.js +363 -0
- package/src/errors.js +103 -0
- package/src/gbnf.js +179 -0
- package/src/index.js +50 -0
- package/src/jsonx-scalar.js +326 -0
- package/src/jsonx-stream.js +806 -0
- package/src/jsonx.js +342 -0
- package/src/machine.js +1252 -0
- package/src/parse.js +37 -0
- package/src/stream.js +57 -0
- package/src/stringify.js +341 -0
- package/src/util.js +96 -0
- package/src/values.js +104 -0
- package/src/write.js +226 -0
package/src/jsonx.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
//#region JSONX
|
|
2
|
+
// JSONX is JSON extended with the JOSL first-class citizens:
|
|
3
|
+
//
|
|
4
|
+
// null (JSON already has it)
|
|
5
|
+
// bigint 123n / -9007199254740993n (unsafe ints auto-promote)
|
|
6
|
+
// regexp /pattern/flags
|
|
7
|
+
// datetimes bare RFC 3339 tokens: 1979-05-27T07:32:00Z (Date),
|
|
8
|
+
// 1979-05-27T07:32:00 (LocalDateTime), 1979-05-27
|
|
9
|
+
// (LocalDate), 07:32:00 (LocalTime)
|
|
10
|
+
// non-finite inf, -inf, nan (also Infinity/NaN, the JS spellings)
|
|
11
|
+
// separators 1_000_000, and a leading + sign
|
|
12
|
+
//
|
|
13
|
+
// `mode: 'json'` is bit-compatible strict JSON: parsing matches
|
|
14
|
+
// `JSON.parse` (minus the reviver) and stringifying delegates to
|
|
15
|
+
// `JSON.stringify`. In place of the reviver — which visits leaves
|
|
16
|
+
// bottom-up, after the fact, without telling you where you are — the
|
|
17
|
+
// parser reports document-order events with absolute paths.
|
|
18
|
+
//
|
|
19
|
+
// Scalar decoding lives in jsonx-scalar.js, shared with the incremental
|
|
20
|
+
// reader (jsonx-stream.js), so both parse every value identically.
|
|
21
|
+
|
|
22
|
+
import { JsonxSyntaxError } from './errors.js';
|
|
23
|
+
import { LocalDate, LocalTime, LocalDateTime } from './values.js';
|
|
24
|
+
import {
|
|
25
|
+
CC_TAB,
|
|
26
|
+
CC_LF,
|
|
27
|
+
CC_CR,
|
|
28
|
+
CC_SPACE,
|
|
29
|
+
CC_DQUOTE,
|
|
30
|
+
CC_PLUS,
|
|
31
|
+
CC_COMMA,
|
|
32
|
+
CC_MINUS,
|
|
33
|
+
CC_SLASH,
|
|
34
|
+
CC_COLON,
|
|
35
|
+
CC_LBRACKET,
|
|
36
|
+
CC_RBRACKET,
|
|
37
|
+
CC_LBRACE,
|
|
38
|
+
CC_RBRACE,
|
|
39
|
+
isDigitCode,
|
|
40
|
+
isAsciiLetterCode,
|
|
41
|
+
} from '@jarenjs/core/scan';
|
|
42
|
+
import { columnOf } from './util.js';
|
|
43
|
+
import { setObjectMember } from '@jarenjs/core/object';
|
|
44
|
+
import { countCharCode } from '@jarenjs/core/string';
|
|
45
|
+
import {
|
|
46
|
+
isValueEndCode,
|
|
47
|
+
decodeString,
|
|
48
|
+
matchDateTime,
|
|
49
|
+
matchNumber,
|
|
50
|
+
matchWord,
|
|
51
|
+
matchRegExp,
|
|
52
|
+
} from './jsonx-scalar.js';
|
|
53
|
+
|
|
54
|
+
class JsonxParser {
|
|
55
|
+
constructor(text, options = {}) {
|
|
56
|
+
this.text = text;
|
|
57
|
+
this.pos = 0;
|
|
58
|
+
this.mode = options.mode === 'json' ? 'json' : 'jsonx';
|
|
59
|
+
this.onEvent = options.onEvent ?? null;
|
|
60
|
+
this.path = [];
|
|
61
|
+
this.errCb = (pos, message, hint) => this.err(message, hint, pos);
|
|
62
|
+
this.endCb = (end) => {
|
|
63
|
+
this.pos = end;
|
|
64
|
+
this.checkValueEnd();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
err(message, hint, pos = this.pos) {
|
|
69
|
+
throw new JsonxSyntaxError(
|
|
70
|
+
message,
|
|
71
|
+
countCharCode(this.text, 0x0A, 0, pos) + 1,
|
|
72
|
+
columnOf(this.text, pos),
|
|
73
|
+
hint);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
extension(what, hint) {
|
|
77
|
+
if (this.mode === 'json')
|
|
78
|
+
this.err(`${what} is a JSONX extension`, hint);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
emit(type, value) {
|
|
82
|
+
if (this.onEvent !== null)
|
|
83
|
+
this.onEvent(type === 'open'
|
|
84
|
+
? { type, path: this.path.slice(), kind: value }
|
|
85
|
+
: { type, path: this.path.slice(), value });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
skipWs() {
|
|
89
|
+
const text = this.text;
|
|
90
|
+
while (this.pos < text.length) {
|
|
91
|
+
const c = text.charCodeAt(this.pos);
|
|
92
|
+
if (c !== CC_SPACE && c !== CC_TAB && c !== CC_LF && c !== CC_CR)
|
|
93
|
+
break;
|
|
94
|
+
this.pos++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
parse() {
|
|
99
|
+
this.skipWs();
|
|
100
|
+
const value = this.parseValue();
|
|
101
|
+
this.skipWs();
|
|
102
|
+
if (this.pos < this.text.length)
|
|
103
|
+
this.err('unexpected trailing characters');
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parseValue() {
|
|
108
|
+
if (this.pos >= this.text.length)
|
|
109
|
+
this.err('unexpected end of input');
|
|
110
|
+
const c = this.text.charCodeAt(this.pos);
|
|
111
|
+
if (c === CC_LBRACE)
|
|
112
|
+
return this.parseObject();
|
|
113
|
+
if (c === CC_LBRACKET)
|
|
114
|
+
return this.parseArray();
|
|
115
|
+
if (c === CC_DQUOTE)
|
|
116
|
+
return this.scalar(this.parseString());
|
|
117
|
+
if (c === CC_SLASH) {
|
|
118
|
+
this.extension('a regexp literal', 'quote the pattern as a string');
|
|
119
|
+
const [re, end] = matchRegExp(this.text, this.pos, this.errCb);
|
|
120
|
+
this.pos = end;
|
|
121
|
+
this.checkValueEnd();
|
|
122
|
+
return this.scalar(re);
|
|
123
|
+
}
|
|
124
|
+
if (c === CC_MINUS || c === CC_PLUS) {
|
|
125
|
+
if (c === CC_PLUS)
|
|
126
|
+
this.extension("a leading '+' sign", 'remove the + sign');
|
|
127
|
+
const d = this.pos + 1 < this.text.length ? this.text.charCodeAt(this.pos + 1) : -1;
|
|
128
|
+
if (isAsciiLetterCode(d))
|
|
129
|
+
return this.scalar(this.parseWord());
|
|
130
|
+
return this.scalar(this.parseNumber());
|
|
131
|
+
}
|
|
132
|
+
if (isDigitCode(c)) {
|
|
133
|
+
if (this.mode === 'jsonx') {
|
|
134
|
+
const dt = matchDateTime(this.text, this.pos, this.errCb, this.endCb);
|
|
135
|
+
if (dt !== null)
|
|
136
|
+
return this.scalar(dt[0]);
|
|
137
|
+
}
|
|
138
|
+
return this.scalar(this.parseNumber());
|
|
139
|
+
}
|
|
140
|
+
if (isAsciiLetterCode(c))
|
|
141
|
+
return this.scalar(this.parseWord());
|
|
142
|
+
this.err('invalid value');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
scalar(value) {
|
|
146
|
+
this.emit('value', value);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
checkValueEnd() {
|
|
151
|
+
if (this.pos >= this.text.length)
|
|
152
|
+
return;
|
|
153
|
+
if (!isValueEndCode(this.text.charCodeAt(this.pos)))
|
|
154
|
+
this.err('unexpected character after value');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
parseObject() {
|
|
158
|
+
this.pos++; // consume '{'
|
|
159
|
+
this.emit('open', 'object');
|
|
160
|
+
const obj = {};
|
|
161
|
+
this.skipWs();
|
|
162
|
+
if (this.pos < this.text.length && this.text.charCodeAt(this.pos) === CC_RBRACE) {
|
|
163
|
+
this.pos++;
|
|
164
|
+
this.emit('close', obj);
|
|
165
|
+
return obj;
|
|
166
|
+
}
|
|
167
|
+
for (;;) {
|
|
168
|
+
this.skipWs();
|
|
169
|
+
if (this.pos >= this.text.length || this.text.charCodeAt(this.pos) !== CC_DQUOTE)
|
|
170
|
+
this.err('expected a string key');
|
|
171
|
+
const key = this.parseString();
|
|
172
|
+
this.skipWs();
|
|
173
|
+
if (this.pos >= this.text.length || this.text.charCodeAt(this.pos) !== CC_COLON)
|
|
174
|
+
this.err("expected ':' after key");
|
|
175
|
+
this.pos++;
|
|
176
|
+
this.skipWs();
|
|
177
|
+
this.path.push(key);
|
|
178
|
+
const value = this.parseValue();
|
|
179
|
+
this.path.pop();
|
|
180
|
+
setObjectMember(obj, key, value);
|
|
181
|
+
this.skipWs();
|
|
182
|
+
if (this.pos >= this.text.length)
|
|
183
|
+
this.err('unterminated object', "close the object with '}'");
|
|
184
|
+
const c = this.text.charCodeAt(this.pos);
|
|
185
|
+
if (c === CC_RBRACE) {
|
|
186
|
+
this.pos++;
|
|
187
|
+
this.emit('close', obj);
|
|
188
|
+
return obj;
|
|
189
|
+
}
|
|
190
|
+
if (c === CC_COMMA) {
|
|
191
|
+
this.pos++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
this.err("expected ',' or '}' in object");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
parseArray() {
|
|
199
|
+
this.pos++; // consume '['
|
|
200
|
+
this.emit('open', 'array');
|
|
201
|
+
const arr = [];
|
|
202
|
+
this.skipWs();
|
|
203
|
+
if (this.pos < this.text.length && this.text.charCodeAt(this.pos) === CC_RBRACKET) {
|
|
204
|
+
this.pos++;
|
|
205
|
+
this.emit('close', arr);
|
|
206
|
+
return arr;
|
|
207
|
+
}
|
|
208
|
+
for (;;) {
|
|
209
|
+
this.skipWs();
|
|
210
|
+
this.path.push(arr.length);
|
|
211
|
+
const value = this.parseValue();
|
|
212
|
+
this.path.pop();
|
|
213
|
+
arr.push(value);
|
|
214
|
+
this.skipWs();
|
|
215
|
+
if (this.pos >= this.text.length)
|
|
216
|
+
this.err('unterminated array', "close the array with ']'");
|
|
217
|
+
const c = this.text.charCodeAt(this.pos);
|
|
218
|
+
if (c === CC_RBRACKET) {
|
|
219
|
+
this.pos++;
|
|
220
|
+
this.emit('close', arr);
|
|
221
|
+
return arr;
|
|
222
|
+
}
|
|
223
|
+
if (c === CC_COMMA) {
|
|
224
|
+
this.pos++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
this.err("expected ',' or ']' in array");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
parseString() {
|
|
232
|
+
const [value, end] = decodeString(this.text, this.pos, this.errCb);
|
|
233
|
+
this.pos = end;
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parseWord() {
|
|
238
|
+
const [value, end] = matchWord(this.text, this.pos, this.mode, this.errCb);
|
|
239
|
+
this.pos = end;
|
|
240
|
+
this.checkValueEnd();
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
parseNumber() {
|
|
245
|
+
return matchNumber(this.text, this.pos, this.mode, this.errCb, this.endCb)[0];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Parse JSONX (or, with `mode: 'json'`, strict JSON) text.
|
|
251
|
+
* @param {string} text - Source text
|
|
252
|
+
* @param {object} [options] - Parser options
|
|
253
|
+
* @param {'jsonx'|'json'} [options.mode] - 'json' matches JSON.parse
|
|
254
|
+
* @param {(event: object) => void} [options.onEvent] - Document-order
|
|
255
|
+
* event sink: {type:'open', path, kind}, {type:'value', path, value},
|
|
256
|
+
* {type:'close', path, value} — paths are absolute (JSON-Pointer-able)
|
|
257
|
+
* @returns {*} The parsed value
|
|
258
|
+
* @throws {JsonxSyntaxError} On invalid input
|
|
259
|
+
*/
|
|
260
|
+
export function parseJsonx(text, options = undefined) {
|
|
261
|
+
return new JsonxParser(String(text), options ?? {}).parse();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function stringifyValue(value, seen, gap, depth) {
|
|
265
|
+
switch (typeof value) {
|
|
266
|
+
case 'string':
|
|
267
|
+
return JSON.stringify(value);
|
|
268
|
+
case 'number':
|
|
269
|
+
if (Number.isNaN(value))
|
|
270
|
+
return 'nan';
|
|
271
|
+
if (value === Infinity)
|
|
272
|
+
return 'inf';
|
|
273
|
+
if (value === -Infinity)
|
|
274
|
+
return '-inf';
|
|
275
|
+
return String(value);
|
|
276
|
+
case 'boolean':
|
|
277
|
+
return value ? 'true' : 'false';
|
|
278
|
+
case 'bigint':
|
|
279
|
+
return `${value}n`;
|
|
280
|
+
case 'object':
|
|
281
|
+
break;
|
|
282
|
+
default:
|
|
283
|
+
return undefined; // functions, symbols, undefined
|
|
284
|
+
}
|
|
285
|
+
if (value === null)
|
|
286
|
+
return 'null';
|
|
287
|
+
if (value instanceof Date)
|
|
288
|
+
return value.toISOString();
|
|
289
|
+
if (value instanceof LocalDate || value instanceof LocalTime || value instanceof LocalDateTime)
|
|
290
|
+
return value.toString();
|
|
291
|
+
if (value instanceof RegExp)
|
|
292
|
+
return `/${value.source}/${value.flags}`;
|
|
293
|
+
if (seen.has(value))
|
|
294
|
+
throw new TypeError('Converting circular structure to JSONX');
|
|
295
|
+
seen.add(value);
|
|
296
|
+
const inner = gap === '' ? '' : '\n' + gap.repeat(depth + 1);
|
|
297
|
+
const outer = gap === '' ? '' : '\n' + gap.repeat(depth);
|
|
298
|
+
const sep = gap === '' ? ',' : ',' + inner;
|
|
299
|
+
let out;
|
|
300
|
+
if (Array.isArray(value)) {
|
|
301
|
+
const parts = [];
|
|
302
|
+
for (let i = 0; i < value.length; ++i)
|
|
303
|
+
parts.push(stringifyValue(value[i], seen, gap, depth + 1) ?? 'null');
|
|
304
|
+
out = parts.length === 0 ? '[]' : `[${inner}${parts.join(sep)}${outer}]`;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const parts = [];
|
|
308
|
+
for (const [k, v] of Object.entries(value)) {
|
|
309
|
+
const sv = stringifyValue(v, seen, gap, depth + 1);
|
|
310
|
+
if (sv !== undefined)
|
|
311
|
+
parts.push(`${JSON.stringify(k)}:${gap === '' ? '' : ' '}${sv}`);
|
|
312
|
+
}
|
|
313
|
+
out = parts.length === 0 ? '{}' : `{${inner}${parts.join(sep)}${outer}}`;
|
|
314
|
+
}
|
|
315
|
+
seen.delete(value);
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Serialize a value to JSONX text. With `mode: 'json'` this delegates to
|
|
321
|
+
* `JSON.stringify` for exact backward compatibility (bigints throw, dates
|
|
322
|
+
* become quoted strings, non-finite numbers become null, ...).
|
|
323
|
+
* @param {*} value - The value to serialize
|
|
324
|
+
* @param {object} [options] - Writer options
|
|
325
|
+
* @param {'jsonx'|'json'} [options.mode] - Output dialect
|
|
326
|
+
* @param {number|string} [options.indent] - Pretty-print indentation
|
|
327
|
+
* @returns {string|undefined} The text, or undefined for undefined input
|
|
328
|
+
*/
|
|
329
|
+
export function stringifyJsonx(value, options = {}) {
|
|
330
|
+
if (options.mode === 'json')
|
|
331
|
+
return JSON.stringify(value, null, options.indent);
|
|
332
|
+
const indent = options.indent ?? 0;
|
|
333
|
+
const gap = typeof indent === 'string'
|
|
334
|
+
? indent.slice(0, 10)
|
|
335
|
+
: ' '.repeat(Math.min(10, Math.max(0, Math.trunc(indent))));
|
|
336
|
+
return stringifyValue(value, new Set(), gap, 0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export { JsonxSyntaxError } from './errors.js';
|
|
340
|
+
export { LocalDate, LocalTime, LocalDateTime } from './values.js';
|
|
341
|
+
|
|
342
|
+
//#endregion
|