json-6 1.1.3 → 1.1.5

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/lib/json6.js CHANGED
@@ -1,10 +1,12 @@
1
+ // GENERATED FILE -- do not edit.
2
+ // Built from src/json6.js by build/src-to-lib.js (npm run build); edits here are overwritten.
1
3
  // json6.js
2
4
  // JSON for Humans. See README.md for details.
3
5
  //
4
6
  // This file is based off of https://github.com/d3x0r/sack ./src/netlib/html5.websocket/json6_parser.c
5
7
  //
6
8
  "use strict";
7
- const version = "1.1.1";
9
+ const version = "1.1.5";
8
10
  const VALUE_UNDEFINED = -1;
9
11
  const VALUE_UNSET = 0;
10
12
  const VALUE_NULL = 1;
@@ -60,33 +62,121 @@ const CONTEXT_IN_ARRAY = 1;
60
62
  const CONTEXT_OBJECT_FIELD = 3;
61
63
  const CONTEXT_OBJECT_FIELD_VALUE = 4;
62
64
 
65
+ // Matches an ECMAScript `IdentifierName` (the production that may appear
66
+ // unquoted as an object-literal property key in JavaScript). Used only by the
67
+ // opt-in `esStrictCompatible` mode to reject unquoted keys JavaScript would
68
+ // not accept.
69
+ const ES_IDENTIFIER_NAME = /^[$_\p{ID_Start}][$_\p{ID_Continue}]*$/u;
70
+
71
+ // Matches a NumericLiteralSeparator ('_') only where it is legal: between two
72
+ // digits. Used only by `esStrictCompatible` to recognize unquoted numeric keys
73
+ // like `1_000` as valid JS -- `Number()` itself rejects the underscore.
74
+ const NUMERIC_SEPARATOR = /(\d)_(?=\d)/g;
75
+
76
+ /**
77
+ * @typedef {{
78
+ * n: number,
79
+ * buf: null|string
80
+ * }} Json6Node
81
+ */
82
+
83
+ /**
84
+ * Options accepted by {@link JSON6.parse} and {@link JSON6.begin}. All are
85
+ * off by default: with no options JSON6 accepts its full historical grammar.
86
+ *
87
+ * @typedef {object} Json6ParseOptions
88
+ * @property {boolean} [esStrictCompatible] When `true`, reject the JSON6
89
+ * extensions that are not valid in ECMAScript strict mode, so accepted
90
+ * input can be pasted into a module or `eval`'d unchanged: legacy-octal /
91
+ * leading-zero numbers (`0123`); legacy octal string escapes (`"\012"`,
92
+ * `"\1"`..`"\9"`); literal line terminators inside `'`/`"` strings
93
+ * (backtick strings still allow them); backtick-quoted object keys;
94
+ * unquoted object keys that are neither identifiers nor numbers; and two or
95
+ * more consecutive unary `+`/`-` signs. Defaults to `false`.
96
+ * @property {boolean} [forbidTemplateSubstitution] When `true`, reject an
97
+ * unescaped `${` inside a backtick-quoted string, so a document cannot
98
+ * silently turn into a template-literal substitution if later evaluated as
99
+ * JavaScript. Independent of `esStrictCompatible`. Defaults to `false`.
100
+ * @property {boolean} [warnWithCommentWithoutEOL] When `true`, a `//` comment
101
+ * that reaches the end of the document with no terminating line break logs
102
+ * a console warning. The input is accepted either way. Defaults to `false`.
103
+ * @property {boolean} [normalizeNumericKeys] When `true`, an unquoted object
104
+ * key that parses as a number is replaced with `String(Number(key))`, so
105
+ * `{1e3: true}` gets the key `"1000"` instead of the source text `"1e3"` --
106
+ * matching what a real JS object literal would produce. Independent of
107
+ * `esStrictCompatible` (does not require it). Note: `Number()` parses a
108
+ * leading-zero digit string like `"0123"` as decimal (123), while a real JS
109
+ * numeric literal `0123` is legacy octal (83) in sloppy mode; combine with
110
+ * `esStrictCompatible` (which forbids leading-zero numbers outright) for a
111
+ * guaranteed match. Defaults to `false`.
112
+ */
113
+
114
+ /**
115
+ * @template T
116
+ * @typedef {object} NodeInfo
117
+ * @property {T} node
118
+ * @property {NodeInfo<T>|null|undefined} next
119
+ * @property {NodeInfo<T>|null} prior
120
+ */
121
+
122
+ /**
123
+ * @typedef {object} Context
124
+ * @property {0|1|3|4} context
125
+ * @property {Record<string, unknown>|null|undefined} elements
126
+ * @property {unknown[]|null} element_array
127
+ * @property {string|null} name
128
+ */
129
+
130
+ /**
131
+ * @type {Context[]}
132
+ */
63
133
  const contexts = [];
64
134
  function getContext() {
65
- return contexts.pop() || {
135
+ return /** @type {Context} */ (contexts.pop()) || /** @type {Context} */ ({
66
136
  context : CONTEXT_UNKNOWN,
67
137
  elements : null,
68
- element_array : null
69
- };
138
+ element_array : null,
139
+ name : null
140
+ });
70
141
  }
142
+
143
+ /**
144
+ * @param {Context} ctx
145
+ */
71
146
  function dropContext(ctx) {
72
147
  contexts.push( ctx );
73
148
  }
74
149
 
150
+ /** @type {Json6Node[]} */
75
151
  const buffers = [];
76
152
  function getBuffer() {
153
+ /** @type {Json6Node|undefined} */
77
154
  let buf = buffers.pop();
78
155
  if( !buf ) buf = { buf:null, n:0 };
79
156
  else buf.n = 0;
80
- return buf;
157
+ return /** @type {Json6Node} */ (buf);
81
158
  }
159
+
160
+ /**
161
+ * @param {Json6Node} buf
162
+ */
82
163
  function dropBuffer(buf) {
83
164
  buffers.push( buf );
84
165
  }
85
166
 
86
- const JSON6 = typeof exports === 'object'
87
- ? exports
88
- // istanbul ignore next
89
- : {};
167
+ /**
168
+ * @template T
169
+ * @param {T|undefined} value
170
+ * @param {string} [msg]
171
+ * @returns {T}
172
+ */
173
+ function ensureDefined(value, msg) {
174
+ /* istanbul ignore next -- every call site checks buf.length before consuming a code point, so codePointAt never actually returns undefined here */
175
+ if( value === undefined ) throw new Error( msg || "Unexpected undefined value" );
176
+ return value;
177
+ }
178
+
179
+ const JSON6 = {};
90
180
 
91
181
  /*
92
182
  let _DEBUG_LL = true;
@@ -107,6 +197,10 @@ const log = function(type) {
107
197
  };
108
198
  */
109
199
 
200
+ /**
201
+ * @param {string} string
202
+ * @returns {string}
203
+ */
110
204
  JSON6.escape = function(string) {
111
205
  let output = '';
112
206
  if( !string ) return string;
@@ -120,33 +214,56 @@ JSON6.escape = function(string) {
120
214
  return output;
121
215
  };
122
216
 
123
-
124
- JSON6.begin = function( cb, reviver ) {
125
-
126
- const val = { name : null, // name of this value (if it's contained in an object)
217
+ /**
218
+ * @callback Json6Reviver
219
+ * @param {string} key
220
+ * @param {unknown} value
221
+ * @returns {unknown}
222
+ */
223
+
224
+ /**
225
+ * @param {(value: unknown) => void} [cb]
226
+ * @param {Json6Reviver|undefined} [reviver]
227
+ * @param {Json6ParseOptions} [options]
228
+ */
229
+ JSON6.begin = function( cb, reviver, options ) {
230
+
231
+ const beginOptions = options;
232
+ /** @type {boolean} */
233
+ let esStrictCompatible = !!( beginOptions && beginOptions.esStrictCompatible );
234
+ /** @type {boolean} */
235
+ let forbidTemplateSubstitution = !!( beginOptions && beginOptions.forbidTemplateSubstitution );
236
+ /** @type {boolean} */
237
+ let warnWithCommentWithoutEOL = !!( beginOptions && beginOptions.warnWithCommentWithoutEOL );
238
+ /** @type {boolean} */
239
+ let normalizeNumericKeys = !!( beginOptions && beginOptions.normalizeNumericKeys );
240
+
241
+ const val = /** @type {{ name: string|null, value_type: number, string: string, contains: unknown }} */ ({ name : null, // name of this value (if it's contained in an object)
127
242
  value_type: VALUE_UNSET, // value from above indiciating the type of this value
128
243
  string : '', // the string value of this value (strings and number types only)
129
244
  contains : null,
130
- };
131
-
132
- const pos = { line:1, col:1 };
133
- let n = 0;
134
- let word = WORD_POS_RESET,
245
+ });
246
+ // col is incremented as each character is taken, so it starts at 0 and the first
247
+ // character of a line reports as column 1.
248
+ const pos = /** @type {{ line: number, col: number }} */ ({ line:1, col:0 });
249
+ /** @type {number} */ let n = 0;
250
+ /** @type {number} */ let word = WORD_POS_RESET,
135
251
  status = true,
136
252
  negative = false,
137
- result = null,
138
- elements = undefined,
139
- element_array = [],
140
- parse_context = CONTEXT_UNKNOWN,
253
+ /** @type {unknown} */ result = null,
254
+ /** @type {Record<string, unknown>|null|undefined} */ elements = undefined,
255
+ /** @type {unknown[]|null} */ element_array = [],
256
+ /** @type {0|1|3|4} */ parse_context = CONTEXT_UNKNOWN,
141
257
  comment = 0,
142
258
  fromHex = false,
143
259
  decimal = false,
144
260
  exponent = false,
145
261
  exponent_sign = false,
146
262
  exponent_digit = false,
147
- gatheringStringFirstChar = null,
263
+ /** @type {number|null} */ gatheringStringFirstChar = null,
148
264
  gatheringString = false,
149
265
  gatheringNumber = false,
266
+ signSeen = false, // '+' or '-' seen; no number or keyword has followed it yet
150
267
  stringEscape = false,
151
268
  cr_escaped = false,
152
269
  unicodeWide = false,
@@ -154,61 +271,94 @@ JSON6.begin = function( cb, reviver ) {
154
271
  stringHex = false,
155
272
  hex_char = 0,
156
273
  hex_char_len = 0,
157
- completed = false
158
- ;
159
-
160
- const context_stack = {
161
- first : null,
162
- last : null,
163
- saved : null,
164
- push(node) {
165
- let recover = this.saved;
166
- if( recover ) { this.saved = recover.next; recover.node = node; recover.next = null; recover.prior = this.last; }
167
- else { recover = { node : node, next : null, prior : this.last }; }
168
- if( !this.last ) this.first = recover;
169
- this.last = recover;
170
- },
171
- pop() {
172
- const result = this.last;
173
- if( !(this.last = result.prior ) ) this.first = null;
174
- result.next = this.saved; this.saved = result;
175
- return result.node;
176
- }
177
- };
178
-
179
- const inQueue = {
180
- first : null,
181
- last : null,
182
- saved : null,
183
- push(node) {
184
- let recover = this.saved;
185
- if( recover ) {this.saved = recover.next; recover.node = node; recover.next = null; recover.prior = this.last; }
186
- else { recover = { node : node, next : null, prior : this.last }; }
187
- if( !this.last ) this.first = recover;
188
- else this.last.next = recover;
189
- this.last = recover;
190
- },
191
- shift() {
192
- const result = this.first;
193
- if( !result ) return null;
194
- this.first = result.next;
195
- if( !this.first ) this.last = null;
196
- result.next = this.saved;
197
- this.saved = result;
198
- // node is in saved...
199
- return result.node;
200
- },
201
- unshift(node) {
202
- // usage in this module, recover will ALWAYS have a saved to use.
203
- const recover = this.saved;
204
- //if( recover ) {
205
- this.saved = recover.next; recover.node = node; recover.next = this.first; recover.prior = null;
206
- //} else { recover = { node : node, next : this.first, prior : null }; }
207
- if( !this.first ) this.last = recover;
208
- this.first = recover;
209
- }
210
- };
274
+ completed = false,
275
+ dollarSeen = false;
276
+
277
+ const context_stack = /** @type {{
278
+ first: NodeInfo<Context>|null,
279
+ last: NodeInfo<Context>|null,
280
+ saved: NodeInfo<Context>|null|undefined,
281
+ push: (node: Context) => void,
282
+ pop: () => Context
283
+ }} */ ({
284
+ first : null,
285
+ last : null,
286
+ saved : null,
287
+ /**
288
+ * @param {Context} node
289
+ */
290
+ push(node) {
291
+ /** @type {NodeInfo<Context>|null|undefined} */
292
+ let recover = this.saved;
293
+ if( recover ) { this.saved = recover.next; recover.node = node; recover.next = null; recover.prior = this.last; }
294
+ else { recover = /** @type {NodeInfo<Context>} */ ({ node : node, next : null, prior : this.last }); }
295
+ if( !this.last ) this.first = recover;
296
+ this.last = recover;
297
+ },
298
+ pop() {
299
+ const resultNode = this.last;
300
+ /* istanbul ignore next -- pop() is only ever called from parse_context states that were entered via a matching push(), so the stack can never be empty here */
301
+ if( !resultNode ) throw new Error( "context stack underflow" );
302
+ if( !(this.last = resultNode.prior ) ) this.first = null;
303
+ resultNode.next = this.saved; this.saved = resultNode;
304
+ return resultNode.node;
305
+ }
306
+ });
307
+
308
+ const inQueue = /** @type {{
309
+ save: NodeInfo<Json6Node>|null|undefined,
310
+ first: NodeInfo<Json6Node>|null|undefined,
311
+ last: NodeInfo<Json6Node>|null,
312
+ saved: NodeInfo<Json6Node>|null|undefined,
313
+ push: (node: Json6Node) => void,
314
+ shift: () => Json6Node|null,
315
+ unshift: (node: Json6Node) => void
316
+ }} */ ({
317
+ /** @type {NodeInfo<Json6Node>|null|undefined} */
318
+ save: undefined,
319
+ /** @type {NodeInfo<Json6Node>|null|undefined} */
320
+ first : null,
321
+ /** @type {NodeInfo<Json6Node>|null} */
322
+ last : null,
323
+ /** @type {NodeInfo<Json6Node>|null|undefined} */
324
+ saved : null,
325
+ /**
326
+ * @param {Json6Node} node
327
+ */
328
+ push(node) {
329
+ /** @type {NodeInfo<Json6Node>|null|undefined} */
330
+ let recover = this.saved;
331
+ if( recover ) {this.saved = recover.next; recover.node = node; recover.next = null; recover.prior = this.last; }
332
+ else { recover = /** @type {NodeInfo<Json6Node>} */ ({ node : node, next : null, prior : this.last }); }
333
+ if( !this.last ) this.first = recover;
334
+ else this.last.next = recover;
335
+ this.last = recover;
336
+ },
337
+ shift() {
338
+ const resultNode = this.first;
339
+ if( !resultNode ) return null;
340
+ this.first = resultNode.next;
341
+ if( !this.first ) this.last = null;
342
+ resultNode.next = this.saved;
343
+ this.saved = resultNode;
344
+ // node is in saved...
345
+ return resultNode.node;
346
+ },
347
+ /**
348
+ * @param {Json6Node} node
349
+ */
350
+ unshift(node) {
351
+ // usage in this module, recover will ALWAYS have a saved to use.
352
+ const recover = /** @type {NodeInfo<Json6Node>} */ (this.saved);
353
+ this.saved = recover.next; recover.node = node; recover.next = this.first; recover.prior = null;
354
+ if( !this.first ) this.last = recover;
355
+ this.first = recover;
356
+ }
357
+ });
211
358
 
359
+ /**
360
+ * @param {string} leader
361
+ */
212
362
  function throwEndError( leader /* , c */ ) {
213
363
  throw new Error( `${leader} at ${n} [${pos.line}:${pos.col}]`);
214
364
  }
@@ -221,7 +371,8 @@ JSON6.begin = function( cb, reviver ) {
221
371
  case 1:
222
372
  return throwEndError( "Comment began at end of document" );
223
373
  case 2:
224
- console.log( "Warning: '//' comment without end of line ended document" );
374
+ if( warnWithCommentWithoutEOL )
375
+ console.log( "Warning: '//' comment without end of line ended document" );
225
376
  break;
226
377
  case 3:
227
378
  return throwEndError( "Open comment '/*' is missing close at end of document" );
@@ -237,14 +388,24 @@ JSON6.begin = function( cb, reviver ) {
237
388
  result = undefined;
238
389
  return r;
239
390
  },
240
- reset() {
391
+ /**
392
+ * @param {Json6ParseOptions} [resetOptions] Options for the next parse.
393
+ * When omitted, the options passed to `begin()` (if any) stay in
394
+ * effect.
395
+ */
396
+ reset( resetOptions ) {
397
+ const activeOptions = resetOptions === undefined ? beginOptions : resetOptions;
398
+ esStrictCompatible = !!( activeOptions && activeOptions.esStrictCompatible );
399
+ forbidTemplateSubstitution = !!( activeOptions && activeOptions.forbidTemplateSubstitution );
400
+ warnWithCommentWithoutEOL = !!( activeOptions && activeOptions.warnWithCommentWithoutEOL );
401
+ normalizeNumericKeys = !!( activeOptions && activeOptions.normalizeNumericKeys );
241
402
  word = WORD_POS_RESET;
242
403
  status = true;
243
404
  if( inQueue.last ) inQueue.last.next = inQueue.save;
244
405
  inQueue.save = inQueue.first;
245
406
  inQueue.first = inQueue.last = null;
246
- if( context_stack.last ) context_stack.last.next = context_stack.save;
247
- context_stack.save = inQueue.first;
407
+ if( context_stack.last ) context_stack.last.next = context_stack.saved;
408
+ context_stack.saved = inQueue.first;
248
409
  context_stack.first = context_stack.last = null;//= [];
249
410
  element_array = null;
250
411
  elements = undefined;
@@ -253,39 +414,61 @@ JSON6.begin = function( cb, reviver ) {
253
414
  val.name = null;
254
415
  val.string = '';
255
416
  pos.line = 1;
256
- pos.col = 1;
417
+ pos.col = 0;
257
418
  negative = false;
419
+ signSeen = false;
258
420
  comment = 0;
259
421
  completed = false;
422
+ signSeen = false;
423
+ dollarSeen = false;
424
+ gatheringStringFirstChar = null;
260
425
  gatheringString = false;
261
426
  stringEscape = false; // string stringEscape intro
262
427
  cr_escaped = false; // carraige return escaped
428
+ unicodeWide = false;
429
+ stringUnicode = false;
430
+ stringHex = false;
431
+ hex_char = 0;
432
+ hex_char_len = 0;
263
433
  //stringUnicode = false; // reading \u
264
434
  //unicodeWide = false; // reading \u{} in string
265
435
  //stringHex = false; // reading \x in string
266
436
  },
437
+ /**
438
+ * @param {unknown} [msg]
439
+ */
267
440
  write(msg) {
441
+ /** @type {number} */
268
442
  let retcode;
443
+ // write() is the push interface; values only leave it through the callback.
444
+ // parse() drives the same parser through _write()/value() and needs no callback.
445
+ if( !cb ) throw new Error( "Callback function must be passed to begin, or you can't get values from the stream." );
269
446
  if( msg !== undefined && typeof msg !== "string") msg = String(msg);
270
447
  if( !status ) throw new Error( "Parser is in an error state, please reset." );
271
- for( retcode = this._write(msg,false); retcode > 0; retcode = this._write() ) {
448
+ for( retcode = this._write(/** @type {string|undefined|null} */ (msg),false); retcode > 0; retcode = this._write() ) {
272
449
  this.finalError();
273
- if( typeof reviver === 'function' ) (function walk(holder, key) {
274
- const value = holder[key];
275
- if (value && typeof value === 'object') {
276
- for (const k in value) {
277
- if (Object.prototype.hasOwnProperty.call(value, k)) {
278
- const v = walk(value, k);
279
- if (v !== undefined) {
280
- value[k] = v;
281
- } else {
282
- delete value[k];
450
+ if( typeof reviver === 'function' ) (/**
451
+ * @param {Record<string, unknown>} holder
452
+ * @param {string} key
453
+ * @returns {unknown}
454
+ */
455
+ function walk(holder, key) {
456
+ const value = holder[key];
457
+ if (value && typeof value === 'object') {
458
+ const record = /** @type {Record<string, unknown>} */ (value);
459
+ for (const k in record) {
460
+ if (Object.prototype.hasOwnProperty.call(record, k)) {
461
+ const v = walk(record, k);
462
+ if (v !== undefined) {
463
+ record[k] = v;
464
+ } else {
465
+ delete record[k];
466
+ }
283
467
  }
284
468
  }
285
469
  }
286
- }
287
- return reviver.call(holder, key, value);
288
- }({'': result}, ''));
470
+ return reviver.call(holder, key, value);
471
+ }({'': result}, ''));
289
472
  cb( result );
290
473
  result = undefined;
291
474
 
@@ -295,110 +478,191 @@ JSON6.begin = function( cb, reviver ) {
295
478
  if( retcode )
296
479
  this.finalError();
297
480
  },
481
+ /**
482
+ * @param {string|undefined|null} [msg]
483
+ * @param {boolean} [complete_at_end]
484
+ */
298
485
  _write(msg,complete_at_end) {
486
+ /** @type {Json6Node|null} */
299
487
  let input;
300
- let buf;
488
+ /** @type {string} */
489
+ let buf = "";
490
+ /** @type {number} */
301
491
  let retval = 0;
302
492
 
493
+ /**
494
+ * @param {string} leader
495
+ * @param {number} c
496
+ */
303
497
  function throwError( leader, c ) {
304
498
  throw new Error( `${leader} '${String.fromCodePoint( c )}' unexpected at ${n} (near '${buf.substr(n>4?(n-4):0,n>4?3:(n-1))}[${String.fromCodePoint( c )}]${buf.substr(n, 10)}') [${pos.line}:${pos.col}]`);
305
499
  }
306
500
 
501
+ // Line bookkeeping for a character that has just been taken (n is past it).
502
+ // CR, LF, LS and PS each end a line; a CR directly followed by LF counts once,
503
+ // at the LF. (A CR that ends one write() chunk with the LF in the next counts
504
+ // twice; that is the one case this does not see.) Column resets to 0 so the
505
+ // next character lands on column 1.
506
+ /**
507
+ * @param {number} cInt
508
+ */
509
+ function countLine( cInt ) {
510
+ if( cInt === 13/*'\r'*/ ) {
511
+ if( n < buf.length && buf.charCodeAt( n ) === 10 ) return;
512
+ }
513
+ else if( cInt !== 10/*'\n'*/ && cInt !== 0x2028 && cInt !== 0x2029 ) return;
514
+ pos.line++;
515
+ pos.col = 0;
516
+ }
517
+
307
518
  function RESET_VAL() {
308
519
  val.value_type = VALUE_UNSET;
309
520
  val.string = '';
521
+ signSeen = false;
522
+ }
523
+
524
+ // A delimiter, or the end of the document, arrived while a value was still
525
+ // being spelled out: 'tru', 'nul', or a sign with nothing after it. Without
526
+ // this, '[tru]' was [] and 'tru' alone was undefined.
527
+ /**
528
+ * @param {number} [cInt] the delimiter; omitted at end of document
529
+ */
530
+ function checkPartialValue( cInt ) {
531
+ if( word > WORD_POS_RESET && word < WORD_POS_FIELD ) {
532
+ if( cInt === undefined ) throwEndError( "Incomplete keyword" );
533
+ else throwError( "Incomplete keyword", cInt );
534
+ }
535
+ if( signSeen && val.value_type == VALUE_UNSET ) {
536
+ if( cInt === undefined ) throwEndError( "Sign with no number following" );
537
+ else throwError( "Sign with no number following", cInt );
538
+ }
539
+ }
540
+
541
+ // A new value is starting; there must not already be a finished one waiting
542
+ // for a separator. Without this, '[1 2]' kept only the 2.
543
+ /**
544
+ * @param {number} cInt
545
+ */
546
+ function checkValueSeparator( cInt ) {
547
+ if( val.value_type != VALUE_UNSET )
548
+ throwError( "Two values with no separator between them", cInt );
549
+ }
550
+
551
+ // collectNumber only admits digits, a sign, one '.', one 'e' and a radix
552
+ // prefix, so a token Number() cannot read is malformed: '1e', '.5.', '.e3', '0x'.
553
+ // These used to come back as NaN.
554
+ function checkNumber() {
555
+ if( Number.isNaN( Number( val.string ) ) ) {
556
+ status = false;
557
+ throw new Error( `Invalid number '${val.string}' at ${n} [${pos.line}:${pos.col}]` );
558
+ }
310
559
  }
311
560
 
312
561
  function arrayPush() {
562
+ // invariant: only called while parse_context is CONTEXT_IN_ARRAY, which is only
563
+ // entered after '[' sets element_array to a real array.
564
+ const arr = /** @type {unknown[]} */ (element_array);
313
565
  switch( val.value_type ){
314
566
  case VALUE_NUMBER:
315
- element_array.push( (negative?-1:1) * Number( val.string ) );
567
+ arr.push( (negative?-1:1) * Number( val.string ) );
316
568
  break;
317
569
  case VALUE_STRING:
318
- element_array.push( val.string );
570
+ arr.push( val.string );
319
571
  break;
320
572
  case VALUE_TRUE:
321
- element_array.push( true );
573
+ arr.push( true );
322
574
  break;
323
575
  case VALUE_FALSE:
324
- element_array.push( false );
576
+ arr.push( false );
325
577
  break;
326
578
  case VALUE_NEG_NAN:
327
- element_array.push( -NaN );
579
+ arr.push( -NaN );
328
580
  break;
329
581
  case VALUE_NAN:
330
- element_array.push( NaN );
582
+ arr.push( NaN );
331
583
  break;
332
584
  case VALUE_NEG_INFINITY:
333
- element_array.push( -Infinity );
585
+ arr.push( -Infinity );
334
586
  break;
335
587
  case VALUE_INFINITY:
336
- element_array.push( Infinity );
588
+ arr.push( Infinity );
337
589
  break;
338
590
  case VALUE_NULL:
339
- element_array.push( null );
591
+ arr.push( null );
340
592
  break;
341
593
  case VALUE_UNDEFINED:
342
- element_array.push( undefined );
594
+ arr.push( undefined );
343
595
  break;
344
596
  case VALUE_EMPTY:
345
- element_array.push( undefined );
346
- delete element_array[element_array.length-1];
597
+ arr.push( undefined );
598
+ delete arr[arr.length-1];
347
599
  break;
348
600
  case VALUE_OBJECT:
349
- element_array.push( val.contains );
601
+ arr.push( val.contains );
350
602
  break;
351
603
  case VALUE_ARRAY:
352
- element_array.push( val.contains );
604
+ arr.push( val.contains );
353
605
  break;
354
606
  }
355
607
  }
356
608
  function objectPush() {
609
+ // invariant: only called while parse_context is CONTEXT_OBJECT_FIELD_VALUE, which is
610
+ // only entered after '{' sets elements to a real object.
611
+ const obj = /** @type {Record<string, unknown>} */ (elements);
612
+ const name = /** @type {string} */ (val.name);
357
613
  switch( val.value_type ){
358
614
  case VALUE_NUMBER:
359
- elements[val.name] = ((negative?-1:1) * Number( val.string ));
615
+ obj[name] = ((negative?-1:1) * Number( val.string ));
360
616
  break;
361
617
  case VALUE_STRING:
362
- elements[val.name] = ( val.string );
618
+ obj[name] = ( val.string );
363
619
  break;
364
620
  case VALUE_TRUE:
365
- elements[val.name] = ( true );
621
+ obj[name] = ( true );
366
622
  break;
367
623
  case VALUE_FALSE:
368
- elements[val.name] = ( false );
624
+ obj[name] = ( false );
369
625
  break;
370
626
  case VALUE_NEG_NAN:
371
- elements[val.name] = ( -NaN );
627
+ obj[name] = ( -NaN );
372
628
  break;
373
629
  case VALUE_NAN:
374
- elements[val.name] = ( NaN );
630
+ obj[name] = ( NaN );
375
631
  break;
376
632
  case VALUE_NEG_INFINITY:
377
- elements[val.name] = ( -Infinity );
633
+ obj[name] = ( -Infinity );
378
634
  break;
379
635
  case VALUE_INFINITY:
380
- elements[val.name] = ( Infinity );
636
+ obj[name] = ( Infinity );
381
637
  break;
382
638
  case VALUE_NULL:
383
- elements[val.name] = ( null );
639
+ obj[name] = ( null );
384
640
  break;
385
641
  case VALUE_UNDEFINED:
386
- elements[val.name] = ( undefined );
642
+ obj[name] = ( undefined );
387
643
  break;
388
644
  case VALUE_OBJECT:
389
- elements[val.name] = val.contains;
645
+ obj[name] = val.contains;
390
646
  break;
391
647
  case VALUE_ARRAY:
392
- elements[val.name] = val.contains;
648
+ obj[name] = val.contains;
393
649
  break;
394
650
  }
395
651
  }
396
652
 
653
+ /**
654
+ * @param {number} start_c
655
+ */
397
656
  function gatherString( start_c ) {
398
657
  let retval = 0;
658
+ // Hoisted out of the per-character loop: these are constant for
659
+ // the whole string, so the default (non-strict) path adds no
660
+ // work beyond one predictable branch.
661
+ const strictQuote = esStrictCompatible && start_c !== 96/*'`'*/;
662
+ const trackTemplate = start_c === 96/*'`'*/ && forbidTemplateSubstitution;
399
663
  while( retval == 0 && ( n < buf.length ) ) {
400
664
  let str = buf.charAt(n);
401
- const cInt = buf.codePointAt(n++);
665
+ const cInt = ensureDefined( buf.codePointAt(n++), "Unexpected end of string" );
402
666
  if( cInt >= 0x10000 ) { str += buf.charAt(n); n++; }
403
667
  //console.log( "gathering....", stringEscape, str, cInt, unicodeWide, stringHex, stringUnicode, hex_char_len );
404
668
  pos.col++;
@@ -470,22 +734,15 @@ JSON6.begin = function( cb, reviver ) {
470
734
  }
471
735
  switch( cInt ) {
472
736
  case 13/*'\r'*/:
737
+ // \\ \r : a continuation; if \n follows it is counted there
473
738
  cr_escaped = true;
474
- pos.col = 1;
739
+ countLine( cInt );
475
740
  continue;
476
741
  case 0x2028: // LS (Line separator)
477
742
  case 0x2029: // PS (paragraph separator)
478
- pos.col = 1; // no return to get newline reset, so reset line pos.
479
- // Fallthrough
480
743
  case 10/*'\n'*/:
481
- if( cr_escaped ) {
482
- // \\ \r \n
483
- cr_escaped = false;
484
- } else {
485
- // \\ \n
486
- pos.col = 1;
487
- }
488
- pos.line++;
744
+ cr_escaped = false;
745
+ countLine( cInt );
489
746
  break;
490
747
  case 116/*'t'*/:
491
748
  val.string += '\t';
@@ -494,6 +751,8 @@ JSON6.begin = function( cb, reviver ) {
494
751
  val.string += '\b';
495
752
  break;
496
753
  case 48/*'0'*/:
754
+ if( esStrictCompatible && buf.charCodeAt( n ) >= 48/*'0'*/ && buf.charCodeAt( n ) <= 57/*'9'*/ )
755
+ throwError( "Legacy octal string escapes are not allowed when the 'esStrictCompatible' option is set", buf.charCodeAt( n ) );
497
756
  val.string += '\0';
498
757
  break;
499
758
  case 110/*'n'*/:
@@ -519,6 +778,8 @@ JSON6.begin = function( cb, reviver ) {
519
778
  hex_char = 0;
520
779
  continue;
521
780
  default:
781
+ if( esStrictCompatible && cInt >= 49/*'1'*/ && cInt <= 57/*'9'*/ )
782
+ throwError( "Legacy octal string escapes are not allowed when the 'esStrictCompatible' option is set", cInt );
522
783
  val.string += str;
523
784
  break;
524
785
  }
@@ -527,14 +788,21 @@ JSON6.begin = function( cb, reviver ) {
527
788
  }
528
789
  else if( cInt === 92/*'\\'*/ ) {
529
790
  stringEscape = true;
791
+ dollarSeen = false;
530
792
  }
531
793
  else {
532
- if( cr_escaped ) {
533
- cr_escaped = false;
534
- // \\ \r <any other character>
535
- pos.line++;
536
- pos.col = 2; // newline, plus one character.
794
+ // U+2028/U+2029 are legal, unescaped, inside string literals since
795
+ // ES2019 (the JSON-superset proposal); only \n and \r are illegal.
796
+ if( strictQuote && ( cInt === 10/*'\n'*/ || cInt === 13/*'\r'*/ ) )
797
+ throwError( "Literal line terminators inside single- or double-quoted strings require the 'esStrictCompatible' option to be off", cInt );
798
+ if( trackTemplate ) {
799
+ if( dollarSeen && cInt === 123/*'{'*/ )
800
+ throwError( "Template substitution ${ } is not allowed when the 'forbidTemplateSubstitution' option is set", cInt );
801
+ dollarSeen = cInt === 36/*'$'*/;
537
802
  }
803
+ // \\ \r <any other character> : the CR was counted when seen
804
+ cr_escaped = false;
805
+ countLine( cInt );
538
806
  val.string += str;
539
807
  }
540
808
  }
@@ -546,16 +814,18 @@ JSON6.begin = function( cb, reviver ) {
546
814
  let _n;
547
815
  while( (_n = n) < buf.length ) {
548
816
  const str = buf.charAt(_n);
549
- const cInt = buf.codePointAt(n++);
817
+ const cInt = ensureDefined( buf.codePointAt(n++), "Unexpected end of number" );
550
818
  if( cInt >= 0x10000 ) {
551
819
  throwError( "fault while parsing number;", cInt );
552
820
  }
553
821
  //log('_DEBUG_PARSING', "in getting number:", n, cInt, String.fromCodePoint(cInt) );
822
+ pos.col++;
554
823
  if( cInt == 95 /*_*/ )
555
824
  continue;
556
- pos.col++;
557
825
  // leading zeros should be forbidden.
558
826
  if( cInt >= 48/*'0'*/ && cInt <= 57/*'9'*/ ) {
827
+ if( esStrictCompatible && val.string === '0' )
828
+ throwError( "Legacy octal / leading-zero numbers are not allowed when the 'esStrictCompatible' option is set", cInt );
559
829
  if( exponent ) {
560
830
  exponent_digit = true;
561
831
  }
@@ -604,6 +874,7 @@ JSON6.begin = function( cb, reviver ) {
604
874
  }
605
875
  } else {
606
876
  if( cInt == 32/*' '*/ || cInt == 160/* &nbsp */ || cInt == 13 || cInt == 10 || cInt == 9
877
+ || cInt == 11/*VT*/ || cInt == 12/*FF*/ || cInt == 0x2028/*LS*/ || cInt == 0x2029/*PS*/
607
878
  || cInt == 0xFEFF || cInt == 44/*','*/ || cInt == 125/*'}'*/ || cInt == 93/*']'*/
608
879
  || cInt == 58/*':'*/ ) {
609
880
  break;
@@ -617,6 +888,9 @@ JSON6.begin = function( cb, reviver ) {
617
888
  }
618
889
  }
619
890
  }
891
+ // the character that ended the number was counted here and is handed back to
892
+ // the main loop, which counts it again.
893
+ if( n > _n ) pos.col--;
620
894
  n = _n;
621
895
 
622
896
  if( (!complete_at_end) && n == buf.length ) {
@@ -624,6 +898,7 @@ JSON6.begin = function( cb, reviver ) {
624
898
  }
625
899
  else {
626
900
  gatheringNumber = false;
901
+ checkNumber();
627
902
  val.value_type = VALUE_NUMBER;
628
903
  if( parse_context == CONTEXT_UNKNOWN ) {
629
904
  completed = true;
@@ -641,6 +916,7 @@ JSON6.begin = function( cb, reviver ) {
641
916
  if( gatheringNumber ) {
642
917
  //console.log( "Force completed.")
643
918
  gatheringNumber = false;
919
+ checkNumber();
644
920
  val.value_type = VALUE_NUMBER;
645
921
  if( parse_context == CONTEXT_UNKNOWN ) {
646
922
  completed = true;
@@ -653,8 +929,13 @@ JSON6.begin = function( cb, reviver ) {
653
929
 
654
930
  while( status && ( input = inQueue.shift() ) ) {
655
931
  n = input.n;
656
- buf = input.buf;
932
+ // invariant: queued buffers are always given a string via `input.buf = msg` before being pushed.
933
+ buf = /** @type {string} */ (input.buf);
657
934
  if( gatheringString ) {
935
+ /* istanbul ignore next -- gatheringString and gatheringStringFirstChar are always set/cleared together; this guards state that the public API can never actually produce */
936
+ if( gatheringStringFirstChar === null ) {
937
+ throw new Error( "Parser string start was unset" );
938
+ }
658
939
  const string_status = gatherString( gatheringStringFirstChar );
659
940
  if( string_status > 0 ) {
660
941
  gatheringString = false;
@@ -667,12 +948,13 @@ JSON6.begin = function( cb, reviver ) {
667
948
 
668
949
  while( !completed && status && ( n < buf.length ) ) {
669
950
  let str = buf.charAt(n);
670
- const cInt = buf.codePointAt(n++);
951
+ const cInt = ensureDefined( buf.codePointAt(n++), "Unexpected end of input while parsing" );
671
952
  if( cInt >= 0x10000 ) { str += buf.charAt(n); n++; }
672
953
  //// log('_DEBUG_PARSING', "parsing at ", cInt, str );
673
954
  //log('_DEBUG_LL', "processing: ", cInt, str, pos, comment, parse_context, word, val );
674
955
  pos.col++;
675
956
  if( comment ) { // '/'
957
+ countLine( cInt );
676
958
  if( comment == 1 ) { // '/'
677
959
  if( cInt == 42/*'*'*/ ) { comment = 3; } // '/*'
678
960
  else if( cInt != 47/*'/'*/ ) { // '//'(NOT)
@@ -682,7 +964,8 @@ JSON6.begin = function( cb, reviver ) {
682
964
  }
683
965
  else if( comment == 2 ) { // '// ...'
684
966
  if( cInt == 10/*'\n'*/
685
- || cInt == 13/*'\r'*/ ) comment = 0;
967
+ || cInt == 13/*'\r'*/
968
+ || cInt == 0x2028/*LS*/ || cInt == 0x2029/*PS*/ ) comment = 0;
686
969
  }
687
970
  else if( comment == 3 ){ // '/*... '
688
971
  if( cInt == 42/*'*'*/ ) comment = 4;
@@ -702,6 +985,7 @@ JSON6.begin = function( cb, reviver ) {
702
985
  throwError( "fault while parsing; getting field name unexpected ", cInt );
703
986
  // break;
704
987
  }
988
+ checkValueSeparator( cInt );
705
989
  {
706
990
  const old_context = getContext();
707
991
  //log('_DEBUG_PARSING', "Begin a new object; previously pushed into elements; but wait until trailing comma or close previously:%d", val.value_type );
@@ -734,13 +1018,15 @@ JSON6.begin = function( cb, reviver ) {
734
1018
  //log('_DEBUG_PARSING', "Begin a new array; previously pushed into elements; but wait until trailing comma or close previously:%d", val.value_type );
735
1019
 
736
1020
  val.value_type = VALUE_ARRAY;
1021
+ /** @type {unknown[]} */
737
1022
  const tmparr = [];
738
1023
  if( parse_context == CONTEXT_UNKNOWN )
739
1024
  result = element_array = tmparr;
740
1025
  //else if( parse_context == CONTEXT_IN_ARRAY )
741
1026
  // element_array.push( tmparr );
742
1027
  else if( parse_context == CONTEXT_OBJECT_FIELD_VALUE )
743
- elements[val.name] = tmparr;
1028
+ // invariant: CONTEXT_OBJECT_FIELD_VALUE means elements/val.name were set by ':'.
1029
+ /** @type {Record<string, unknown>} */ (elements)[/** @type {string} */ (val.name)] = tmparr;
744
1030
 
745
1031
  old_context.context = parse_context;
746
1032
  old_context.elements = elements;
@@ -761,6 +1047,15 @@ JSON6.begin = function( cb, reviver ) {
761
1047
  ////log('_DEBUG_PARSING', "colon context:", parse_context );
762
1048
  if( parse_context == CONTEXT_OBJECT_FIELD ) {
763
1049
  word = WORD_POS_RESET;
1050
+ if( esStrictCompatible && val.value_type !== VALUE_STRING
1051
+ && !ES_IDENTIFIER_NAME.test( val.string )
1052
+ && Number.isNaN( Number( val.string.replace( NUMERIC_SEPARATOR, '$1' ) ) ) )
1053
+ throwError( "Unquoted keys must be valid identifiers when the 'esStrictCompatible' option is set", cInt );
1054
+ if( normalizeNumericKeys && val.value_type !== VALUE_STRING && val.string !== '' ) {
1055
+ const numericKey = Number( val.string.replace( NUMERIC_SEPARATOR, '$1' ) );
1056
+ if( !Number.isNaN( numericKey ) )
1057
+ val.string = String( numericKey );
1058
+ }
764
1059
  val.name = val.string;
765
1060
  val.string = '';
766
1061
  parse_context = CONTEXT_OBJECT_FIELD_VALUE;
@@ -779,8 +1074,13 @@ JSON6.begin = function( cb, reviver ) {
779
1074
  // allow starting a new word
780
1075
  word = WORD_POS_RESET;
781
1076
  }
1077
+ checkPartialValue( cInt );
782
1078
  // coming back after pushing an array or sub-object will reset the context to FIELD, so an end with a field should still push value.
783
1079
  if( ( parse_context == CONTEXT_OBJECT_FIELD ) ) {
1080
+ // a field name was collected but no ':' followed it: '{a}', '{a }', '{"a"}'.
1081
+ // This used to silently produce {}.
1082
+ if( word == WORD_POS_FIELD || word == WORD_POS_AFTER_FIELD || val.value_type == VALUE_STRING )
1083
+ throwError( "Object field name with no value", cInt );
784
1084
  //log('_DEBUG_PARSING', "close object; empty object %d", val.value_type );
785
1085
  //RESET_VAL();
786
1086
  val.value_type = VALUE_OBJECT;
@@ -823,9 +1123,11 @@ JSON6.begin = function( cb, reviver ) {
823
1123
  throwError( "Fault while parsing; unexpected", cInt );
824
1124
  }
825
1125
  negative = false;
1126
+ signSeen = false;
826
1127
  break;
827
1128
  case 93/*']'*/:
828
1129
  if( word == WORD_POS_END ) word = WORD_POS_RESET;
1130
+ checkPartialValue( cInt );
829
1131
  if( parse_context == CONTEXT_IN_ARRAY ) {
830
1132
  //log('_DEBUG_PARSING', "close array, push last element: %d", val.value_type );
831
1133
  if( val.value_type != VALUE_UNSET ) {
@@ -850,9 +1152,11 @@ JSON6.begin = function( cb, reviver ) {
850
1152
  throwError( `bad context ${parse_context}; fault while parsing`, cInt );// fault
851
1153
  }
852
1154
  negative = false;
1155
+ signSeen = false;
853
1156
  break;
854
1157
  case 44/*','*/:
855
1158
  if( word == WORD_POS_END ) word = WORD_POS_RESET; // allow collect new keyword
1159
+ checkPartialValue( cInt );
856
1160
  //log('_DEBUG_PARSING', "comma context:", parse_context, val );
857
1161
  if( parse_context == CONTEXT_IN_ARRAY ) {
858
1162
  if( val.value_type == VALUE_UNSET )
@@ -878,12 +1182,16 @@ JSON6.begin = function( cb, reviver ) {
878
1182
  throwError( "bad context; excessive commas while parsing;", cInt );// fault
879
1183
  }
880
1184
  negative = false;
1185
+ signSeen = false;
881
1186
  break;
882
1187
 
883
1188
  default:
884
1189
  if( parse_context == CONTEXT_OBJECT_FIELD ) {
885
1190
  switch( cInt ) {
886
1191
  case 96://'`':
1192
+ if( esStrictCompatible )
1193
+ throwError( "Backtick-quoted keys are not allowed when the 'esStrictCompatible' option is set", cInt );
1194
+ // falls through
887
1195
  case 34://'"':
888
1196
  case 39://'\'':
889
1197
  if( word == WORD_POS_RESET ) {
@@ -903,14 +1211,16 @@ JSON6.begin = function( cb, reviver ) {
903
1211
 
904
1212
  break;
905
1213
  case 10://'\n':
906
- pos.line++;
907
- pos.col = 1;
908
- // fall through to normal space handling - just updated line/col position
909
1214
  case 13://'\r':
910
1215
  case 32://' ':
911
1216
  case 160://&nbsp:
912
1217
  case 9://'\t':
1218
+ case 11://VT
1219
+ case 12://FF
1220
+ case 0x2028://LS
1221
+ case 0x2029://PS
913
1222
  case 0xFEFF: // ZWNBS is WS though
1223
+ countLine( cInt );
914
1224
  if( word == WORD_POS_END ) { // allow collect new keyword
915
1225
  word = WORD_POS_RESET;
916
1226
  }
@@ -947,16 +1257,19 @@ JSON6.begin = function( cb, reviver ) {
947
1257
  break;
948
1258
  }
949
1259
  case 10://'\n':
950
- pos.line++;
951
- pos.col = 1;
952
- // Fallthrough
953
1260
  case 32://' ':
954
1261
  case 160:// &nbsp
955
1262
  case 9://'\t':
956
1263
  case 13://'\r':
1264
+ case 11://VT
1265
+ case 12://FF
1266
+ case 0x2028://LS
1267
+ case 0x2029://PS
957
1268
  case 0xFEFF://'\uFEFF':
1269
+ countLine( cInt );
958
1270
  if( word == WORD_POS_END ) {
959
1271
  word = WORD_POS_RESET;
1272
+ signSeen = false;
960
1273
  if( parse_context == CONTEXT_UNKNOWN ) {
961
1274
  completed = true;
962
1275
  }
@@ -970,7 +1283,7 @@ JSON6.begin = function( cb, reviver ) {
970
1283
  //----------------------------------------------------------
971
1284
  // catch characters for true/false/null/undefined which are values outside of quotes
972
1285
  case 116://'t':
973
- if( word == WORD_POS_RESET ) word = WORD_POS_TRUE_1;
1286
+ if( word == WORD_POS_RESET ) { checkValueSeparator( cInt ); word = WORD_POS_TRUE_1; }
974
1287
  else if( word == WORD_POS_INFINITY_6 ) word = WORD_POS_INFINITY_7;
975
1288
  else { status = false; throwError( "fault parsing", cInt ); }// fault
976
1289
  break;
@@ -981,7 +1294,7 @@ JSON6.begin = function( cb, reviver ) {
981
1294
  case 117://'u':
982
1295
  if( word == WORD_POS_TRUE_2 ) word = WORD_POS_TRUE_3;
983
1296
  else if( word == WORD_POS_NULL_1 ) word = WORD_POS_NULL_2;
984
- else if( word == WORD_POS_RESET ) word = WORD_POS_UNDEFINED_1;
1297
+ else if( word == WORD_POS_RESET ) { checkValueSeparator( cInt ); word = WORD_POS_UNDEFINED_1; }
985
1298
  else { status = false; throwError( "fault parsing", cInt ); }// fault
986
1299
  break;
987
1300
  case 101://'e':
@@ -996,7 +1309,7 @@ JSON6.begin = function( cb, reviver ) {
996
1309
  else { status = false; throwError( "fault parsing", cInt ); }// fault
997
1310
  break;
998
1311
  case 110://'n':
999
- if( word == WORD_POS_RESET ) word = WORD_POS_NULL_1;
1312
+ if( word == WORD_POS_RESET ) { checkValueSeparator( cInt ); word = WORD_POS_NULL_1; }
1000
1313
  else if( word == WORD_POS_UNDEFINED_1 ) word = WORD_POS_UNDEFINED_2;
1001
1314
  else if( word == WORD_POS_UNDEFINED_6 ) word = WORD_POS_UNDEFINED_7;
1002
1315
  else if( word == WORD_POS_INFINITY_1 ) word = WORD_POS_INFINITY_2;
@@ -1023,7 +1336,7 @@ JSON6.begin = function( cb, reviver ) {
1023
1336
  else { status = false; throwError( "fault parsing", cInt ); }// fault
1024
1337
  break;
1025
1338
  case 102://'f':
1026
- if( word == WORD_POS_RESET ) word = WORD_POS_FALSE_1;
1339
+ if( word == WORD_POS_RESET ) { checkValueSeparator( cInt ); word = WORD_POS_FALSE_1; }
1027
1340
  else if( word == WORD_POS_UNDEFINED_4 ) word = WORD_POS_UNDEFINED_5;
1028
1341
  else if( word == WORD_POS_INFINITY_2 ) word = WORD_POS_INFINITY_3;
1029
1342
  else { status = false; throwError( "fault parsing", cInt ); }// fault
@@ -1038,11 +1351,11 @@ JSON6.begin = function( cb, reviver ) {
1038
1351
  else { status = false; throwError( "fault parsing", cInt ); }// fault
1039
1352
  break;
1040
1353
  case 73://'I':
1041
- if( word == WORD_POS_RESET ) word = WORD_POS_INFINITY_1;
1354
+ if( word == WORD_POS_RESET ) { checkValueSeparator( cInt ); word = WORD_POS_INFINITY_1; }
1042
1355
  else { status = false; throwError( "fault parsing", cInt ); }// fault
1043
1356
  break;
1044
1357
  case 78://'N':
1045
- if( word == WORD_POS_RESET ) word = WORD_POS_NAN_1;
1358
+ if( word == WORD_POS_RESET ) { checkValueSeparator( cInt ); word = WORD_POS_NAN_1; }
1046
1359
  else if( word == WORD_POS_NAN_2 ) { val.value_type = negative ? VALUE_NEG_NAN : VALUE_NAN; negative = false; word = WORD_POS_END; }
1047
1360
  else { status = false; throwError( "fault parsing", cInt ); }// fault
1048
1361
  break;
@@ -1051,21 +1364,35 @@ JSON6.begin = function( cb, reviver ) {
1051
1364
  else { status = false; throwError( "fault parsing", cInt ); }// fault
1052
1365
  break;
1053
1366
  case 45://'-':
1054
- if( word == WORD_POS_RESET ) negative = !negative;
1367
+ if( word == WORD_POS_RESET ) {
1368
+ checkValueSeparator( cInt );
1369
+ if( esStrictCompatible && signSeen )
1370
+ throwError( "Multiple consecutive signs are not allowed when the 'esStrictCompatible' option is set", cInt );
1371
+ signSeen = true;
1372
+ negative = !negative;
1373
+ }
1055
1374
  else { status = false; throwError( "fault parsing", cInt ); }// fault
1056
1375
  break;
1057
1376
  case 43://'+':
1058
1377
  if( word !== WORD_POS_RESET ) { status = false; throwError( "fault parsing", cInt ); }// fault
1378
+ else {
1379
+ checkValueSeparator( cInt );
1380
+ if( esStrictCompatible && signSeen )
1381
+ throwError( "Multiple consecutive signs are not allowed when the 'esStrictCompatible' option is set", cInt );
1382
+ signSeen = true;
1383
+ }
1059
1384
  break;
1060
1385
  //
1061
1386
  //----------------------------------------------------------
1062
1387
  default:
1063
1388
  if( ( cInt >= 48/*'0'*/ && cInt <= 57/*'9'*/ ) || ( cInt == 43/*'+'*/ ) || ( cInt == 46/*'.'*/ ) || ( cInt == 45/*'-'*/ ) ) {
1389
+ checkValueSeparator( cInt );
1064
1390
  fromHex = false;
1065
1391
  exponent = false;
1066
1392
  exponent_sign = false;
1067
1393
  exponent_digit = false;
1068
1394
  decimal = false;
1395
+ signSeen = false;
1069
1396
  val.string = str;
1070
1397
  input.n = n;
1071
1398
  collectNumber();
@@ -1088,6 +1415,7 @@ JSON6.begin = function( cb, reviver ) {
1088
1415
 
1089
1416
  if( n == buf.length ) {
1090
1417
  dropBuffer( input );
1418
+ if( complete_at_end ) checkPartialValue();
1091
1419
  if( gatheringString || gatheringNumber || parse_context == CONTEXT_OBJECT_FIELD ) {
1092
1420
  retval = 0;
1093
1421
  }
@@ -1161,61 +1489,105 @@ JSON6.begin = function( cb, reviver ) {
1161
1489
 
1162
1490
  const _parser = [Object.freeze( JSON6.begin() )];
1163
1491
  let _parse_level = 0;
1164
- JSON6.parse = function( msg, reviver ) {
1492
+
1493
+ /**
1494
+ * @param {unknown} msg
1495
+ * @param {Json6Reviver} [reviver]
1496
+ * @param {Json6ParseOptions} [options]
1497
+ */
1498
+ JSON6.parse = function( msg, reviver, options ) {
1165
1499
  //var parser = JSON6.begin();
1166
1500
  const parse_level = _parse_level++;
1167
1501
  if( _parser.length <= parse_level )
1168
1502
  _parser.push( Object.freeze( JSON6.begin() ) );
1169
1503
  const parser = _parser[parse_level];
1170
1504
  if (typeof msg !== "string") msg = String(msg);
1171
- parser.reset();
1172
- if( parser._write( msg, true ) > 0 ) {
1505
+ parser.reset( options );
1506
+ try {
1507
+ if( parser._write( /** @type {string} */ (msg), true ) <= 0 ) {
1508
+ // nothing completed: empty document, whitespace, or only comments.
1509
+ parser.finalError();
1510
+ throw new Error( "No value found in document" );
1511
+ }
1173
1512
  const result = parser.value();
1174
- if (typeof reviver === 'function') (function walk(holder, key) {
1175
- const value = holder[key];
1176
- if (value && typeof value === 'object') {
1177
- for (const k in value) {
1178
- if (Object.prototype.hasOwnProperty.call(value, k)) {
1179
- const v = walk(value, k);
1180
- if (v !== undefined) {
1181
- value[k] = v;
1182
- } else {
1183
- delete value[k];
1513
+ if (typeof reviver === 'function') (/**
1514
+ * @param {Record<string, unknown>} holder
1515
+ * @param {string} key
1516
+ * @returns {unknown}
1517
+ */
1518
+ function walk(holder, key) {
1519
+ const value = holder[key];
1520
+ if (value && typeof value === 'object') {
1521
+ const record = /** @type {Record<string, unknown>} */ (value);
1522
+ for (const k in record) {
1523
+ if (Object.prototype.hasOwnProperty.call(record, k)) {
1524
+ const v = walk(record, k);
1525
+ if (v !== undefined) {
1526
+ record[k] = v;
1527
+ } else {
1528
+ delete record[k];
1529
+ }
1184
1530
  }
1185
1531
  }
1186
1532
  }
1187
- }
1188
- return reviver.call(holder, key, value);
1189
- }({'': result}, ''));
1190
- _parse_level--;
1533
+ return reviver.call(holder, key, value);
1534
+ }({'': result}, ''));
1191
1535
  return result;
1192
- } else parser.finalError();
1193
- return undefined;
1536
+ } finally {
1537
+ _parse_level--;
1538
+ }
1194
1539
  };
1195
- JSON6.stringify = JSON.stringify;
1196
1540
 
1197
1541
  //---------------------------------------------------------------------------
1198
1542
  // Stringify
1199
1543
  //---------------------------------------------------------------------------
1200
1544
 
1545
+ /**
1546
+ * @typedef {((key: string, value: unknown) => unknown) | Array<string|number> | null} Json6Replacer
1547
+ */
1548
+
1549
+ /**
1550
+ * @typedef {object} Json6Stringifier
1551
+ * @property {(o?: unknown, r?: Json6Replacer, s?: string|number|null, as?: string) => string|undefined} stringify
1552
+ * @property {(q: string) => void} setQuote
1553
+ * @property {boolean} ignoreNonEnumerable
1554
+ * @property {boolean} sortKeys
1555
+ */
1556
+
1557
+ /** @type {Json6Stringifier|null} */
1201
1558
  JSON6.stringifierActive = null;
1202
1559
  JSON6.stringifier = function() {
1203
1560
  const keywords = { ["true"]:true,["false"]:false,["null"]:null,["NaN"]:NaN,["Infinity"]:Infinity,["undefined"]:undefined };
1204
1561
  let useQuote = '"';
1205
1562
 
1206
1563
  let ignoreNonEnumerable = false;
1564
+ // preserves the historical (sorted) output by default; set to `false` to
1565
+ // emit keys in their own enumeration order instead.
1566
+ let sortKeys = true;
1207
1567
 
1208
1568
  return {
1569
+ /**
1570
+ * @param {unknown} [o]
1571
+ * @param {Json6Replacer} [r]
1572
+ * @param {string|number|null} [s]
1573
+ * @param {string} [as]
1574
+ * @returns {string|undefined}
1575
+ */
1209
1576
  stringify(o,r,s,as) { return stringify(this, o,r,s,as); },
1577
+ /**
1578
+ * @param {string} q
1579
+ */
1210
1580
  setQuote(q) { useQuote = q; },
1211
1581
  get ignoreNonEnumerable() { return ignoreNonEnumerable; },
1212
1582
  set ignoreNonEnumerable(val) { ignoreNonEnumerable = val; },
1583
+ get sortKeys() { return sortKeys; },
1584
+ set sortKeys(val) { sortKeys = val; },
1213
1585
  };
1214
1586
 
1587
+ /**
1588
+ * @param {string} s
1589
+ */
1215
1590
  function getIdentifier(s) {
1216
- if( "number" === typeof s && !isNaN( s ) ) {
1217
- return ["'",s.toString(),"'"].join();
1218
- }
1219
1591
  if( !s.length ) return useQuote+useQuote;
1220
1592
  // should check also for if any non ident in string...
1221
1593
  return ( ( s in keywords /* [ "true","false","null","NaN","Infinity","undefined"].find( keyword=>keyword===s )*/
@@ -1223,16 +1595,21 @@ JSON6.stringifier = function() {
1223
1595
  || /((\n|\r|\t)|[ #{}()<>!+\-*/.:,])/.test( s ) )?(useQuote + JSON6.escape(s) +useQuote):s );
1224
1596
  }
1225
1597
 
1598
+ /**
1599
+ * @param {Json6Stringifier} stringifier
1600
+ * @param {unknown} object
1601
+ * @param {Json6Replacer} [replacer]
1602
+ * @param {string|number|null} [space]
1603
+ * @param {string} [asField]
1604
+ * @returns {string|undefined}
1605
+ */
1226
1606
  function stringify( stringifier, object, replacer, space, asField ) {
1227
1607
  if( object === undefined ) return "undefined";
1228
1608
  if( object === null ) return "null";
1229
- let gap;
1230
- let indent;
1231
- let i;
1232
- const spaceType = typeof space;
1233
- const repType = typeof replacer;
1234
- gap = "";
1235
- indent = "";
1609
+ /** @type {string} */
1610
+ let gap = "";
1611
+ /** @type {string} */
1612
+ let indent = "";
1236
1613
  const stringifier_ = JSON6.stringifierActive;
1237
1614
  JSON6.stringifierActive = stringifier;
1238
1615
 
@@ -1243,21 +1620,21 @@ JSON6.stringifier = function() {
1243
1620
 
1244
1621
  // If the space parameter is a number, make an indent string containing that
1245
1622
  // many spaces.
1246
- if (spaceType === "number") {
1247
- for (i = 0; i < space; i += 1) {
1623
+ if (typeof space === "number") {
1624
+ for (let i = 0; i < space; i += 1) {
1248
1625
  indent += " ";
1249
1626
  }
1250
1627
 
1251
1628
  // If the space parameter is a string, it will be used as the indent string.
1252
- } else if (spaceType === "string") {
1629
+ } else if (typeof space === "string") {
1253
1630
  indent = space;
1254
1631
  }
1255
1632
 
1256
1633
  // If there is a replacer, it must be a function or an array.
1257
1634
  // Otherwise, throw an error.
1258
1635
  const rep = replacer;
1259
- if( replacer && repType !== "function"
1260
- && ( repType !== "object"
1636
+ if( replacer && typeof replacer !== "function"
1637
+ && ( typeof replacer !== "object"
1261
1638
  || typeof replacer.length !== "number"
1262
1639
  )) {
1263
1640
  throw new Error("JSON6.stringify unknown replacer type.");
@@ -1270,16 +1647,26 @@ JSON6.stringifier = function() {
1270
1647
 
1271
1648
 
1272
1649
  // from https://github.com/douglascrockford/JSON-js/blob/master/json2.js#L181
1650
+ /**
1651
+ * @param {string} key
1652
+ * @param {Record<string, unknown>} holder
1653
+ * @returns {string|undefined}
1654
+ */
1273
1655
  function str(key, holder) {
1274
1656
  // Produce a string from holder[key].
1275
- let i; // The loop counter.
1276
- let k; // The member key.
1277
- let v; // The member value.
1278
- let length;
1279
1657
  const mind = gap;
1280
- let partial;
1658
+ /** @type {unknown} */
1281
1659
  let value = holder[key];
1282
- if( "string" === typeof value ) value = getIdentifier( value );
1660
+ // A string *value* must always be quoted: unlike an object key,
1661
+ // a bareword here would parse back as an identifier/variable
1662
+ // reference rather than as the string itself, so `getIdentifier`
1663
+ // (which intentionally leaves identifier-shaped keys unquoted)
1664
+ // is not applicable to values. Using it here previously emitted
1665
+ // invalid, non-reparseable output for any identifier-shaped
1666
+ // string value (e.g. `"hello"` became the bare word `hello`).
1667
+ if( "string" === typeof value ) {
1668
+ value = useQuote + JSON6.escape( value ) + useQuote;
1669
+ }
1283
1670
 
1284
1671
  if( value !== undefined
1285
1672
  && value !== null
@@ -1305,7 +1692,7 @@ JSON6.stringifier = function() {
1305
1692
  return '' + value;//useQuote+JSOX.escape( value )+useQuote;
1306
1693
  case "boolean":
1307
1694
  return String(value);
1308
- case "object":
1695
+ case "object": {
1309
1696
 
1310
1697
  //_DEBUG_STRINGIFY && console.log( "ENTERING OBJECT EMISSION WITH:", v );
1311
1698
  //if( v ) return v;
@@ -1316,19 +1703,50 @@ JSON6.stringifier = function() {
1316
1703
  return "null";
1317
1704
  }
1318
1705
 
1706
+ if( Array.isArray( value ) ) {
1707
+ // Arrays used to fall through to the object path below and come out
1708
+ // as {"0":..,"1":..}. Holes stay holes ( [1,,3] ) and undefined stays
1709
+ // undefined; both are JSON6 syntax and both parse back to what they were.
1710
+ const arr = /** @type {unknown[]} */ (value);
1711
+ const holder_ = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (arr));
1712
+ gap += indent;
1713
+ /** @type {string[]} */
1714
+ const items = [];
1715
+ for( let i = 0; i < arr.length; i++ ) {
1716
+ if( !( i in arr ) ) {
1717
+ items.push( "" );
1718
+ continue;
1719
+ }
1720
+ const v = str( String( i ), holder_ );
1721
+ items.push( v === undefined ? "undefined" : v );
1722
+ }
1723
+ // a trailing hole needs its own comma, since "[1,]" parses as [1].
1724
+ const trailingHole = arr.length > 0 && !( ( arr.length - 1 ) in arr );
1725
+ const v = items.length === 0
1726
+ ? "[]"
1727
+ : gap
1728
+ ? "[\n" + gap + items.join( ",\n" + gap ) + ( trailingHole ? "," : "" ) + "\n" + mind + "]"
1729
+ : "[" + items.join( "," ) + ( trailingHole ? "," : "" ) + "]";
1730
+ gap = mind;
1731
+ return v;
1732
+ }
1733
+
1734
+ const record = /** @type {Record<string, unknown>} */ (value);
1735
+
1319
1736
  // Make an array to hold the partial results of stringifying this object value.
1320
1737
 
1321
1738
  gap += indent;
1322
- partial = [];
1739
+ /** @type {string[]} */
1740
+ const partial = [];
1323
1741
 
1324
1742
  // If the replacer is an array, use it to select the members to be stringified.
1325
1743
  if (rep && typeof rep === "object") {
1326
- length = rep.length;
1744
+ const length = rep.length;
1327
1745
  //_DEBUG_STRINGIFY && console.log( "Working through replacer" );
1328
- for (i = 0; i < length; i += 1) {
1746
+ for (let i = 0; i < length; i += 1) {
1329
1747
  if (typeof rep[i] === "string") {
1330
- k = rep[i];
1331
- v = str(k, value);
1748
+ const k = /** @type {string} */ (rep[i]);
1749
+ const v = str(k, record);
1332
1750
  if (v) {
1333
1751
  partial.push(getIdentifier(k) + (
1334
1752
  (gap)
@@ -1341,32 +1759,37 @@ JSON6.stringifier = function() {
1341
1759
  } else {
1342
1760
 
1343
1761
  // Otherwise, iterate through all of the keys in the object.
1762
+ /** @type {string[]} */
1344
1763
  const keys = [];
1345
1764
  //_DEBUG_STRINGIFY && console.log( "is something in something?", k, value );
1346
- for (k in value) {
1765
+ for (const k in record) {
1347
1766
  if( ignoreNonEnumerable )
1348
- if( !Object.prototype.propertyIsEnumerable.call( value, k ) ){
1767
+ if( !Object.prototype.propertyIsEnumerable.call( record, k ) ){
1349
1768
  //_DEBUG_STRINGIFY && console.log( "skipping non-enuerable?", k );
1350
1769
  continue;
1351
1770
  }
1352
1771
 
1353
- // sort properties into keys.
1354
- if (Object.prototype.hasOwnProperty.call(value, k)) {
1355
- let n;
1356
- for( n = 0; n < keys.length; n++ )
1357
- if( keys[n] > k ) {
1358
- keys.splice(n,0,k );
1359
- break;
1360
- }
1361
- if( n === keys.length )
1772
+ if (Object.prototype.hasOwnProperty.call(record, k)) {
1773
+ if( sortKeys ) {
1774
+ // sort properties into keys.
1775
+ let n;
1776
+ for( n = 0; n < keys.length; n++ )
1777
+ if( keys[n] > k ) {
1778
+ keys.splice(n,0,k );
1779
+ break;
1780
+ }
1781
+ if( n === keys.length )
1782
+ keys.push(k);
1783
+ } else {
1362
1784
  keys.push(k);
1785
+ }
1363
1786
  }
1364
1787
  }
1365
1788
  //_DEBUG_STRINGIFY && console.log( "Expanding object keys:", v, keys );
1366
1789
  for(let n = 0; n < keys.length; n++) {
1367
- k = keys[n];
1368
- if (Object.prototype.hasOwnProperty.call(value, k)) {
1369
- v = str(k, value);
1790
+ const k = keys[n];
1791
+ if (Object.prototype.hasOwnProperty.call(record, k)) {
1792
+ const v = str(k, record);
1370
1793
  if (v) {
1371
1794
  partial.push(getIdentifier(k)+ (
1372
1795
  (gap)
@@ -1381,7 +1804,7 @@ JSON6.stringifier = function() {
1381
1804
  // Join all of the member texts together, separated with commas,
1382
1805
  // and wrap them in braces.
1383
1806
  //_DEBUG_STRINGIFY && console.log( "partial:", partial, protoConverter )
1384
- v = '' +
1807
+ const v = '' +
1385
1808
  ( partial.length === 0
1386
1809
  ? "{}"
1387
1810
  : gap
@@ -1392,13 +1815,33 @@ JSON6.stringifier = function() {
1392
1815
  //_DEBUG_STRINGIFY && console.log(" Resulting phrase from this part is:", v );
1393
1816
  return v;
1394
1817
  }
1818
+ }
1395
1819
  }
1396
1820
  }
1397
1821
  };
1398
1822
 
1399
- JSON6.stringify = function( object, replacer, space ) {
1823
+ /**
1824
+ * @typedef {object} Json6StringifyOptions
1825
+ * @property {boolean} [sortKeys] When `false`, object keys are emitted in their
1826
+ * own enumeration order instead of sorted. Defaults to `true`.
1827
+ */
1828
+
1829
+ /**
1830
+ * @param {unknown} object
1831
+ * @param {Json6Replacer} [replacer]
1832
+ * @param {string|number} [space]
1833
+ * @param {Json6StringifyOptions} [options]
1834
+ * @returns {string|undefined}
1835
+ */
1836
+ JSON6.stringify = function( object, replacer, space, options ) {
1400
1837
  const stringifier = JSON6.stringifier();
1838
+ if( options && options.sortKeys === false ) stringifier.sortKeys = false;
1401
1839
  return stringifier.stringify( object, replacer, space );
1402
1840
  };
1403
1841
 
1404
1842
  JSON6.version = version;
1843
+
1844
+ // CommonJS export; guarded so lib/json6.js can also be loaded directly with a
1845
+ // <script> tag, where JSON6 is simply left as a global.
1846
+ /* istanbul ignore next -- the non-CommonJS branch is only reachable from a browser <script> tag */
1847
+ if( typeof module === "object" && module && module.exports ) module.exports = JSON6;