Skip to content

Commit 22daec1

Browse files
authored
Added maxIndent, noIndent, and propertyFilter stringification options. (#15)
* Added `maxIndent`, `noIndent`, and `propertyFilter` stringification options. * Added `context` callback parameter for both replacers and revivers with `context.holder` and `context.stack` values. * The above results in a breaking change from 5.x versions of JSON-Z, where the third parameter of a replacer/reviver callback _might_ be expected, _depending on context_, to be a holder object. This change results in a more consistent callback paradigm.
1 parent cb18f6f commit 22daec1

14 files changed

Lines changed: 912 additions & 608 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### 6.0.0
2+
3+
* Added `maxIndent`, `noIndent`, and `propertyFilter` stringification options.
4+
* Added `context` callback parameter for both replacers and revivers with `context.holder` and `context.stack` values.
5+
* The above results in a breaking change from 5.x versions of JSON-Z, where the third parameter of a replacer/reviver callback _might_ be expected, _depending on context_, to be a holder object. This change results in a more consistent callback paradigm.
6+
17
### 5.2.0
28

39
* Added `JSON.EXCISE` return value for replacers and revivers, allowing array items to be conveniently deleted along with their slot in an array.

GRAMMAR.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ explicit-octal = "0o", octal-digit, { [ "_" ], octal-digit } ;
107107
108108
implied-octal = "0", octal-digit, { [ "_" ], octal-digit } ;
109109
110-
decimal = "0", [ decimal-sequence, [ "_" ] ], { non-octal-sequence }-, [ "_" ], [ decimal-sequence ] | non-zero-digit, [ [ "_" ], decimal-sequence ] ;
110+
decimal = "0", [ "_" ], [ decimal-sequence, [ "_" ] ], { non-octal-sequence }-, [ "_" ], [ decimal-sequence ] | non-zero-digit, [ [ "_" ], decimal-sequence ] ;
111111
112112
decimal-sequence = decimal-digit, { [ "_" ], decimal-digit } ;
113113
@@ -139,7 +139,7 @@ binary (* JSON-Z *) = "0b", { binary-digit}- ;
139139
140140
octal (* JSON-Z *) = "0o", { octal-digit }- ;
141141
142-
decimal = { decimal-digit }- ;
142+
decimal = 0 | non-zero-digit, { decimal-digit } ;
143143
144144
hex (* JSON5 *) = "0x", { hex-digit }- ;
145145
@@ -187,7 +187,7 @@ comment (* JSONC *) = block-comment | line-comment ;
187187
188188
block-comment = "/*", ? any characters not containing the sequence "*/" ?, "*/";
189189
190-
line-comment = "//", ? any characters other than \n, \r, \u2028, or \u2029 ?, ( "\n" | "\r\n" | "\r" | ? LINE SEPARATOR ? (* JSON5 *) | ? PARAGRAPH SEPARATOR ? (* JSON5 *) ) ;
190+
line-comment = "//", ? any characters other than \n, \r, \u2028, or \u2029 ?, ( "\n" | "\r\n" | "\r" |? end of input ? | ? LINE SEPARATOR ? (* JSON5 *) | ? PARAGRAPH SEPARATOR ? (* JSON5 *) ) ;
191191
192192
@endebnf
193193
```

README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ JSON-Z is a superset of [JSON] (and of [JSONC] and [JSON5] as well) which aims t
1212

1313
JSON-Z is designed to increase flexibility when parsing while, by default, maintaining maximum compatibility with standard JSON when stringifying data (unless the user, through optional settings, eschews this compatibility).
1414

15-
JSON-Z output, like JSON and JSON5, is also valid JavaScript (with two *optional* exceptions).
15+
JSON-Z output, like JSON and JSON5, is also valid JavaScript (with two *optional* exceptions). JSON-Z output can be valid JSON with the right optional setttings.
1616

1717
Even when the additional grammar features of JSON-Z are not needed, this library's replacer functions, reviver functions, and formatting capabilities provide additional capabilities which can be useful when dealing JSON and JSON5.
1818

@@ -210,14 +210,16 @@ This works very much like [`JSON.parse`](https://developer.mozilla.org/en-US/doc
210210

211211
A JSON-Z reviver function is a callback that works much like the not-quite-yet-standard `JSON.parse` reviver function from this proposal: https://github.com/tc39/proposal-json-parse-with-source, a proposal which has already been widely implemented.
212212

213-
The third argument passed to a JSON-Z reviver *might* be different from what a `JSON.parse` reviver receives, according to the above proposal. There is a forth argument that clarifies the difference.
213+
The `context` argument passed to a JSON-Z reviver differs in that in provides two extra values described below.
214214

215-
> `reviver(key, value[, extra[, noContext]])`
215+
> `reviver(key, value[, context])`
216216
>
217217
> - `key`: The object key (or array index) of the value being parsed. The `key` is an empty string if the `value` is the root value.
218218
> - `value`: A value as originally parsed, which should be returned by the reviver as-is if the reviver is not modifying the original value.
219-
> - `extra`: This is either a `context` object containing a `source` string, as described at the link above, or the holder of the value, i.e., the object or array, if any, which contains the given key/value pair.
220-
> - `noContext`: if `true`, `extra` is the `holder` object or array which contains the current key/value pair. Otherwise, `value` is a primitive value, and `extra` functions like the (nearly) standard `context` object, containing a `source` string, but also containing a `holder` value as well.
219+
> - `context`:
220+
> - `context.source`: A `source` string, as described at the link above, providing the original text from which a primitive value has been parsed. No `source` is provided for object or array values.
221+
> - `context.holder`: The object or array, if any, which contains a given key/value pair.
222+
> - `context.stack`: An array containing the heirarchy of keys/array indices leading up to the given value. For example, when parsing `"[0, 1, {foo: 77}]"`, and a reviver receives the value `77`, `context.stack` will be `['2', 'foo']`.
221223
>
222224
> Returns: Either the original `value`, `JSONZ.DELETE`, `JSONZ.EXCISE`, or a modified value (using `JSONZ.UNDEFINED` to change a value to `undefined`).
223225
@@ -245,18 +247,21 @@ This works very much like [`JSON.stringify`](https://developer.mozilla.org/en-US
245247
- `value`: The value to convert to a JSON-Z string.
246248
- `replacer`: A function which alters the behavior of the stringification process, or an array of String and Number objects that serve as an allowlist for selecting/filtering the properties of the value object to be included in the JSON-Z string. If this value is null or not provided, all properties of the object are included in the resulting JSON-Z string.
247249

248-
When using the standard `JSON.stringify()`, a replacer function is called with two arguments: `key` and `value`. JSON-Z adds a third argument, `holder`. This value is already available to standard replacer `function`s as `this`, but `this` won't be bound the holder when using an anonymous (arrow) function as a replacer. The JSON-Z third argument (which can be ignored if not needed) provides alternative access to the holder value.<br><br>
249-
> `replacer(key, value[, holder])`
250+
When using the standard `JSON.stringify()`, a replacer function is called with two arguments: `key` and `value`. JSON-Z adds a third argument, `context`. The `context` argument here (which can be dropped if not needed) is similar to the JSON-Z reviver `context` argument, albeit with no `context.source` value.<br><br>
251+
> `replacer(key, value[, context])`
250252
251253
<br>Please note that if you want to shrink the size of a parent array when using a replacer to delete an array element, return `JSONZ.EXCISE`. If you return `JSONZ.DELETE` from the replacer function, the result will be a sparse parent array, retaining its original length and containing an empty slot.
252254

253255
- `space`: A string or number used to insert whitespace into the output JSON-Z string for readability purposes. If this is a number, it indicates the number of space characters to use as whitespace; this number is capped at 10. Values less than 1 indicate that no space should be used. If `space` is a string, that string (or the first 10 characters of the string if it's longer) is used as white space. A single space adds white space without adding indentation. If this parameter is not provided (or is null), no whitespace is added. If indenting white space is used, trailing commas can optionally appear in objects and arrays.
254256
- `options`: This can either be an `OptionSet` value (see [below](#jsonzsetoptionsoptions-additionaloptions)), or an object with the following properties:
255257
- `extendedPrimitives`: If `true` (the default is `false`) this enables direct stringification of `Infinity`, `-Infinity`, `NaN`, and `undefined`. Otherwise, these values become `null`.
256258
- `extendedTypes`: If `JSONZ.ExtendedTypeMode.AS_FUNCTIONS` or `JSONZ.ExtendedTypeMode.AS_OBJECTS` (the default is `JSONZ.ExtendedTypeMode.OFF`), this enables special representation of additional data types, such as `_Date("2019-07-28T08:49:58.202Z")`, which can be parsed directly as a JavaScript `Date` object, or `{"_$_": "Date", "_$_value": "2019-07-28T08:49:58.202Z"}`, which can be automatically rendered as a `Date` object by a built-in replacer.
257-
- `primitiveBigDecimal`: 🧪 If `true` (the default is `false`) this enables direct stringification of arbitrary-precision big decimals using the '`m`' suffix. Otherwise, big decimals must be provided as quoted strings or extended types. _(Note: The '`m`' suffix can't be parsed as current valid JavaScript, but it is potentially a future valid standard notation.)_
259+
- `maxIndent`: If a non-zero integer, this option limits levels of indentation, deeper than which object content will be rendered on a single line.
260+
- `oneLiners`: An list of property names, the values for which should be rendered on a single line. This argument can be provided as an array, a `Set`, or a comma-delimited string of property names.
261+
- `primitiveBigDecimal`: 🧪 If `true` (the default is `false`) this enables direct stringification of arbitrary-precision big decimals using the '`m`' suffix. Otherwise, big decimals must be provided as quoted strings or extended types. _(Note: The '`m`' suffix cannot be parsed as valid JavaScript, making this notation a deviation from JavaScript parsing compatibility.)_
258262
- `primitiveBigInt`: If `true` (the default is `false`) this enables direct stringification of big integers using the '`n`' suffix. Otherwise, big integers are provided as quoted strings or extended types.
259-
- `primitiveDecimal`: 🧪 If `true` (the default is `false`) this enables direct stringification of fixed-precision big decimals using the '`d`' suffix. Otherwise, big decimals must be provided as quoted strings or extended types. _(Note: The '`d`' suffix can't be parsed as current valid JavaScript, but it is potentially a future valid standard notation.)_
263+
- `primitiveDecimal`: 🧪 If `true` (the default is `false`) this enables direct stringification of fixed-precision big decimals using the '`d`' suffix. Otherwise, big decimals must be provided as quoted strings or extended types. _(Note: The '`d`' suffix cannot be parsed as valid JavaScript, making this notation a deviation from JavaScript parsing compatibility.)_
264+
- `propertyFilter`: This option provides functionality identical to providing an array of property names for the `replacer`/`options` argument &mdash; only the object properties listed here will be rendered. Using this option allows additional options to be used simultaneously.
260265
- `quote`: A string representing the quote character to use when serializing strings (single quote `'` or double quote `"`), or one of the following values:
261266
- `JSONZ.Quote.DOUBLE`: Always quote with double quotes (this is the default).
262267
- `JSONZ.Quote.SINGLE`: Always quote with single quotes.

docs/numbers.png

1.68 KB
Loading

docs/simplified-numbers.png

3.27 KB
Loading

lib/options-manager.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export const enum ExtendedTypeMode {
1414
}
1515

1616
export type JsonZAllowedKeys = (string | number)[];
17-
18-
export type JsonZReplacer = (holder: any, key: string, value: any) => any;
17+
export type ReplacerContext = { holder?: any, stack?: string[] };
18+
export type JsonZReplacer = (key: string, value: any, context?: ReplacerContext) => any;
1919

2020
export const enum OptionSet {
2121
MAX_COMPATIBILITY,
@@ -26,6 +26,9 @@ export const enum OptionSet {
2626
export interface JsonZOptions {
2727
extendedPrimitives?: boolean,
2828
extendedTypes?: ExtendedTypeMode,
29+
maxIndent?: number | '';
30+
oneLiners?: string | string[] | Set<string>;
31+
propertyFilter?: (string | number | String | Number)[];
2932
primitiveBigDecimal?: boolean;
3033
primitiveBigInt?: boolean;
3134
primitiveDecimal?: boolean;

lib/parse.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { JsonZParseOptions } from './options-manager';
22

3-
export type JsonZReviver = (key: string, value: any, extra?: (any | { source: string }),
4-
noContext?: boolean) => any;
5-
3+
export type ReviverContext = { source?: string, holder?: any, stack?: string[] };
4+
export type JsonZReviver = (key: string, value: any, context?: ReviverContext) => any;
65
export function parse<T = any>(text: string, options?: JsonZParseOptions): T;
76
export function parse<T = any>(text: string, reviver?: JsonZReviver, options?: JsonZParseOptions): T;

lib/parse.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function parse(text, reviver, newOptions) {
209209
} while (token.type);
210210

211211
if (reviver) {
212-
const result = internalize({ '': root }, '', reviver);
212+
const result = internalize({ '': root }, '');
213213

214214
root = (result === util.DELETE || result === util.EXCISE ? undefined : result);
215215
}
@@ -1334,8 +1334,10 @@ function parse(text, reviver, newOptions) {
13341334
throw invalidChar(read());
13351335
}
13361336

1337-
function internalize(holder, name, reviver) {
1338-
let value = holder[name];
1337+
function internalize(holder, key, keyStack) {
1338+
let value = holder[key];
1339+
1340+
keyStack = keyStack ? (keyStack.push(key), keyStack) : [];
13391341

13401342
if (value && typeof value === 'object' && !isBigNumber(value) && !(value instanceof ValueSourceWrapper)) {
13411343
// noinspection JSUnresolvedReference
@@ -1344,40 +1346,48 @@ function parse(text, reviver, newOptions) {
13441346
const isArray = Array.isArray(value);
13451347
let lastLength = value.length;
13461348

1347-
for (const key of keys) {
1348-
const original = unwrap(value[key]);
1349-
const replacement = internalize(value, key, reviver);
1349+
for (const subkey of keys) {
1350+
const original = unwrap(value[subkey]);
1351+
const replacement = internalize(value, subkey, keyStack);
13501352

13511353
if (replacement === util.EXCISE && isArray) {
1352-
value.splice(parseInt(key), 1);
1354+
value.splice(parseInt(subkey), 1);
13531355
--lastLength;
13541356
}
13551357
else if ((replacement === undefined && original !== undefined) ||
13561358
replacement === util.DELETE || replacement === util.EXCISE) {
13571359
if (!isArray || lastLength === value.length) {
1358-
delete value[key];
1360+
delete value[subkey];
13591361
}
13601362
else {
13611363
lastLength = value.length;
13621364
}
13631365
}
13641366
else {
1365-
setObjectProperty(value, key, replacement === util.UNDEFINED ? undefined : replacement);
1367+
setObjectProperty(value, subkey, replacement === util.UNDEFINED ? undefined : replacement);
13661368
}
13671369
}
13681370
}
13691371

1370-
let extra = holder;
1372+
let result;
13711373

1372-
if (value instanceof ValueSourceWrapper) {
1373-
extra = {
1374-
holder,
1375-
source: value.source
1376-
};
1377-
value = value.value;
1374+
if (reviver.length < 3) {
1375+
result = reviver.call(holder, key, value);
13781376
}
1377+
else {
1378+
const context = { holder, stack: keyStack };
1379+
1380+
if (value instanceof ValueSourceWrapper) {
1381+
context.source = value.source;
1382+
value = value.value;
1383+
}
1384+
1385+
result = reviver.call(holder, key, value, context);
1386+
}
1387+
1388+
keyStack.pop();
13791389

1380-
return reviver.call(holder, name, value, extra, extra === holder);
1390+
return result;
13811391
}
13821392

13831393
function peek() {
@@ -1686,7 +1696,7 @@ function parse(text, reviver, newOptions) {
16861696
let revived;
16871697

16881698
try {
1689-
const arg = reviver ? internalize({ '': current.arg }, '', reviver) : current.arg;
1699+
const arg = reviver ? internalize({ '': current.arg }, '') : current.arg;
16901700

16911701
revived = optionsMgr.reviveTypeValue(current.name, arg);
16921702
}

lib/platform-specifics.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ try {
1616
}
1717
catch (err) {}
1818

19+
const isArrayLike = obj => (obj instanceof Uint8Array) || (obj instanceof Uint8ClampedArray);
20+
1921
const uint8ArrayHandler = {
2022
name: 'Uint8Array',
21-
test: obj => (obj instanceof Uint8Array) || (obj instanceof Uint8ClampedArray),
23+
test: isArrayLike,
2224
creator: fromBase64,
2325
serializer: toBase64
2426
};
@@ -76,5 +78,6 @@ module.exports = {
7678
}
7779
},
7880

79-
uint8ArrayHandler
81+
uint8ArrayHandler,
82+
isArrayLike
8083
};

0 commit comments

Comments
 (0)