Skip to content

Commit 9e24580

Browse files
Vbbabaeschli
andauthored
Allow the visitor to cease callbacks (#88)
* Allow visitor to cease callbacks * polish * update docs --------- Co-authored-by: Martin Aeschlimann <[email protected]>
1 parent cfe32ac commit 9e24580

File tree

5 files changed

+101
-53
lines changed

5 files changed

+101
-53
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
3.3.0 2022-06-24
2+
=================
3+
- `JSONVisitor.onObjectBegin` and `JSONVisitor.onArrayBegin` can now return `false` to instruct the visitor that no children should be visited.
4+
15

26
3.2.0 2022-08-30
37
=================

Diff for: README.md

+18-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ JSONC is JSON with JavaScript style comments. This node module provides a scanne
1212
- the *scanner* tokenizes the input string into tokens and token offsets
1313
- the *visit* function implements a 'SAX' style parser with callbacks for the encountered properties and values.
1414
- the *parseTree* function computes a hierarchical DOM with offsets representing the encountered properties and values.
15-
- the *parse* function evaluates the JavaScript object represented by JSON string in a fault tolerant fashion.
15+
- the *parse* function evaluates the JavaScript object represented by JSON string in a fault tolerant fashion.
1616
- the *getLocation* API returns a location object that describes the property or value located at a given offset in a JSON document.
1717
- the *findNodeAtLocation* API finds the node at a given location path in a JSON DOM.
1818
- the *format* API computes edits to format a JSON document.
@@ -37,7 +37,7 @@ API
3737
* If ignoreTrivia is set, whitespaces or comments are ignored.
3838
*/
3939
export function createScanner(text: string, ignoreTrivia: boolean = false): JSONScanner;
40-
40+
4141
/**
4242
* The scanner object, representing a JSON scanner at a position in the input string.
4343
*/
@@ -106,20 +106,21 @@ export declare function visit(text: string, visitor: JSONVisitor, options?: Pars
106106

107107
/**
108108
* Visitor called by {@linkcode visit} when parsing JSON.
109-
*
109+
*
110110
* The visitor functions have the following common parameters:
111111
* - `offset`: Global offset within the JSON document, starting at 0
112112
* - `startLine`: Line number, starting at 0
113113
* - `startCharacter`: Start character (column) within the current line, starting at 0
114-
*
114+
*
115115
* Additionally some functions have a `pathSupplier` parameter which can be used to obtain the
116116
* current `JSONPath` within the document.
117117
*/
118118
export interface JSONVisitor {
119119
/**
120120
* Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
121+
* When `false` is returned, the array items will not be visited.
121122
*/
122-
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
123+
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void | boolean;
123124

124125
/**
125126
* Invoked when a property is encountered. The offset and length represent the location of the property name.
@@ -133,8 +134,9 @@ export interface JSONVisitor {
133134
onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
134135
/**
135136
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
137+
* When `false` is returned, the array items will not be visited.*
136138
*/
137-
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
139+
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void | boolean;
138140
/**
139141
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
140142
*/
@@ -233,14 +235,14 @@ export function findNodeAtOffset(root: Node, offset: number, includeRightBound?:
233235
export function getNodePath(node: Node): JSONPath;
234236

235237
/**
236-
* Evaluates the JavaScript object of the given JSON DOM node
238+
* Evaluates the JavaScript object of the given JSON DOM node
237239
*/
238240
export function getNodeValue(node: Node): any;
239241

240242
/**
241243
* Computes the edit operations needed to format a JSON document.
242-
*
243-
* @param documentText The input text
244+
*
245+
* @param documentText The input text
244246
* @param range The range to format or `undefined` to format the full content
245247
* @param options The formatting options
246248
* @returns The edit operations describing the formatting changes to the original document following the format described in {@linkcode EditResult}.
@@ -250,10 +252,10 @@ export function format(documentText: string, range: Range, options: FormattingOp
250252

251253
/**
252254
* Computes the edit operations needed to modify a value in the JSON document.
253-
*
254-
* @param documentText The input text
255+
*
256+
* @param documentText The input text
255257
* @param path The path of the value to change. The path represents either to the document root, a property or an array item.
256-
* If the path points to an non-existing property or item, it will be created.
258+
* If the path points to an non-existing property or item, it will be created.
257259
* @param value The new value for the specified property or item. If the value is undefined,
258260
* the property or item will be removed.
259261
* @param options Options
@@ -264,7 +266,7 @@ export function modify(text: string, path: JSONPath, value: any, options: Modifi
264266

265267
/**
266268
* Applies edits to an input string.
267-
* @param text The input text
269+
* @param text The input text
268270
* @param edits Edit operations following the format described in {@linkcode EditResult}.
269271
* @returns The text with the applied edits.
270272
* @throws An error if the edit operations are not well-formed as described in {@linkcode EditResult}.
@@ -306,7 +308,7 @@ export interface Edit {
306308
*/
307309
export interface Range {
308310
/**
309-
* The start offset of the range.
311+
* The start offset of the range.
310312
*/
311313
offset: number;
312314
/**
@@ -315,7 +317,7 @@ export interface Range {
315317
length: number;
316318
}
317319

318-
/**
320+
/**
319321
* Options used by {@linkcode format} when computing the formatting edit operations
320322
*/
321323
export interface FormattingOptions {
@@ -333,7 +335,7 @@ export interface FormattingOptions {
333335
eol: string;
334336
}
335337

336-
/**
338+
/**
337339
* Options used by {@linkcode modify} when computing the modification edit operations
338340
*/
339341
export interface ModificationOptions {

Diff for: src/impl/parser.ts

+30-10
Original file line numberDiff line numberDiff line change
@@ -390,24 +390,44 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
390390
// to not affect visitor functions which stored a reference to a previous JSONPath
391391
const _jsonPath: JSONPath = [];
392392

393+
// Depth of onXXXBegin() callbacks suppressed. onXXXEnd() decrements this if it isn't 0 already.
394+
// Callbacks are only called when this value is 0.
395+
let suppressedCallbacks = 0;
396+
393397
function toNoArgVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void {
394-
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
395-
}
396-
function toNoArgVisitWithPath(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void): () => void {
397-
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
398+
return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
398399
}
399400
function toOneArgVisit<T>(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number) => void): (arg: T) => void {
400-
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
401+
return visitFunction ? (arg: T) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
401402
}
402403
function toOneArgVisitWithPath<T>(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void): (arg: T) => void {
403-
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
404+
return visitFunction ? (arg: T) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
405+
}
406+
function toBeginVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => boolean | void): () => void {
407+
return visitFunction ?
408+
() => {
409+
if (suppressedCallbacks > 0) { suppressedCallbacks++; }
410+
else {
411+
let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
412+
if (cbReturn === false) { suppressedCallbacks = 1; }
413+
}
414+
}
415+
: () => true;
416+
}
417+
function toEndVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void {
418+
return visitFunction ?
419+
() => {
420+
if (suppressedCallbacks > 0) { suppressedCallbacks--; }
421+
if (suppressedCallbacks === 0) { visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()); }
422+
}
423+
: () => true;
404424
}
405425

406-
const onObjectBegin = toNoArgVisitWithPath(visitor.onObjectBegin),
426+
const onObjectBegin = toBeginVisit(visitor.onObjectBegin),
407427
onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty),
408-
onObjectEnd = toNoArgVisit(visitor.onObjectEnd),
409-
onArrayBegin = toNoArgVisitWithPath(visitor.onArrayBegin),
410-
onArrayEnd = toNoArgVisit(visitor.onArrayEnd),
428+
onObjectEnd = toEndVisit(visitor.onObjectEnd),
429+
onArrayBegin = toBeginVisit(visitor.onArrayBegin),
430+
onArrayEnd = toEndVisit(visitor.onArrayEnd),
411431
onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue),
412432
onSeparator = toOneArgVisit(visitor.onSeparator),
413433
onComment = toNoArgVisit(visitor.onComment),

Diff for: src/main.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const findNodeAtOffset: (root: Node, offset: number, includeRightBound?:
124124
export const getNodePath: (node: Node) => JSONPath = parser.getNodePath;
125125

126126
/**
127-
* Evaluates the JavaScript object of the given JSON DOM node
127+
* Evaluates the JavaScript object of the given JSON DOM node
128128
*/
129129
export const getNodeValue: (node: Node) => any = parser.getNodeValue;
130130

@@ -235,20 +235,21 @@ export interface ParseOptions {
235235

236236
/**
237237
* Visitor called by {@linkcode visit} when parsing JSON.
238-
*
238+
*
239239
* The visitor functions have the following common parameters:
240240
* - `offset`: Global offset within the JSON document, starting at 0
241241
* - `startLine`: Line number, starting at 0
242242
* - `startCharacter`: Start character (column) within the current line, starting at 0
243-
*
243+
*
244244
* Additionally some functions have a `pathSupplier` parameter which can be used to obtain the
245245
* current `JSONPath` within the document.
246246
*/
247247
export interface JSONVisitor {
248248
/**
249249
* Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
250+
* When `false` is returned, the object properties will not be visited.
250251
*/
251-
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
252+
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => boolean | void;
252253

253254
/**
254255
* Invoked when a property is encountered. The offset and length represent the location of the property name.
@@ -264,8 +265,9 @@ export interface JSONVisitor {
264265

265266
/**
266267
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
268+
* When `false` is returned, the array items will not be visited.
267269
*/
268-
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
270+
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => boolean | void;
269271

270272
/**
271273
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
@@ -328,7 +330,7 @@ export interface Edit {
328330
*/
329331
export interface Range {
330332
/**
331-
* The start offset of the range.
333+
* The start offset of the range.
332334
*/
333335
offset: number;
334336
/**
@@ -337,7 +339,7 @@ export interface Range {
337339
length: number;
338340
}
339341

340-
/**
342+
/**
341343
* Options used by {@linkcode format} when computing the formatting edit operations
342344
*/
343345
export interface FormattingOptions {
@@ -365,8 +367,8 @@ export interface FormattingOptions {
365367

366368
/**
367369
* Computes the edit operations needed to format a JSON document.
368-
*
369-
* @param documentText The input text
370+
*
371+
* @param documentText The input text
370372
* @param range The range to format or `undefined` to format the full content
371373
* @param options The formatting options
372374
* @returns The edit operations describing the formatting changes to the original document following the format described in {@linkcode EditResult}.
@@ -376,7 +378,7 @@ export function format(documentText: string, range: Range | undefined, options:
376378
return formatter.format(documentText, range, options);
377379
}
378380

379-
/**
381+
/**
380382
* Options used by {@linkcode modify} when computing the modification edit operations
381383
*/
382384
export interface ModificationOptions {
@@ -397,10 +399,10 @@ export interface ModificationOptions {
397399

398400
/**
399401
* Computes the edit operations needed to modify a value in the JSON document.
400-
*
401-
* @param documentText The input text
402+
*
403+
* @param documentText The input text
402404
* @param path The path of the value to change. The path represents either to the document root, a property or an array item.
403-
* If the path points to an non-existing property or item, it will be created.
405+
* If the path points to an non-existing property or item, it will be created.
404406
* @param value The new value for the specified property or item. If the value is undefined,
405407
* the property or item will be removed.
406408
* @param options Options
@@ -413,7 +415,7 @@ export function modify(text: string, path: JSONPath, value: any, options: Modifi
413415

414416
/**
415417
* Applies edits to an input string.
416-
* @param text The input text
418+
* @param text The input text
417419
* @param edits Edit operations following the format described in {@linkcode EditResult}.
418420
* @returns The text with the applied edits.
419421
* @throws An error if the edit operations are not well-formed as described in {@linkcode EditResult}.

Diff for: src/test/json.test.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,22 @@ interface VisitorError extends ParseError {
7979
startCharacter: number;
8080
}
8181

82-
function assertVisit(input: string, expected: VisitorCallback[], expectedErrors: VisitorError[] = [], disallowComments = false): void {
82+
function assertVisit(input: string, expected: VisitorCallback[], expectedErrors: VisitorError[] = [], disallowComments = false, stopOffsets?: number[]): void {
8383
let errors: VisitorError[] = [];
8484
let actuals: VisitorCallback[] = [];
85-
let noArgHalder = (id: keyof JSONVisitor) => (offset: number, length: number, startLine: number, startCharacter: number) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter });
86-
let noArgHalderWithPath = (id: keyof JSONVisitor) => (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, path: pathSupplier() });
87-
let oneArgHalder = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number, startLine: number, startCharacter: number) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, arg });
88-
let oneArgHalderWithPath = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, arg, path: pathSupplier() });
85+
let noArgHandler = (id: keyof JSONVisitor) => (offset: number, length: number, startLine: number, startCharacter: number) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter });
86+
let oneArgHandler = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number, startLine: number, startCharacter: number) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, arg });
87+
let oneArgHandlerWithPath = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, arg, path: pathSupplier() });
88+
let beginHandler = (id: keyof JSONVisitor) => (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => { actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, path: pathSupplier() }); return !stopOffsets || (stopOffsets.indexOf(offset) === -1); };
8989
visit(input, {
90-
onObjectBegin: noArgHalderWithPath('onObjectBegin'),
91-
onObjectProperty: oneArgHalderWithPath('onObjectProperty'),
92-
onObjectEnd: noArgHalder('onObjectEnd'),
93-
onArrayBegin: noArgHalderWithPath('onArrayBegin'),
94-
onArrayEnd: noArgHalder('onArrayEnd'),
95-
onLiteralValue: oneArgHalderWithPath('onLiteralValue'),
96-
onSeparator: oneArgHalder('onSeparator'),
97-
onComment: noArgHalder('onComment'),
90+
onObjectBegin: beginHandler('onObjectBegin'),
91+
onObjectProperty: oneArgHandlerWithPath('onObjectProperty'),
92+
onObjectEnd: noArgHandler('onObjectEnd'),
93+
onArrayBegin: beginHandler('onArrayBegin'),
94+
onArrayEnd: noArgHandler('onArrayEnd'),
95+
onLiteralValue: oneArgHandlerWithPath('onLiteralValue'),
96+
onSeparator: oneArgHandler('onSeparator'),
97+
onComment: noArgHandler('onComment'),
9898
onError: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => {
9999
errors.push({ error, offset, length, startLine, startCharacter });
100100
}
@@ -458,6 +458,18 @@ suite('JSON', () => {
458458
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 20 },
459459
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 22 },
460460
]);
461+
assertVisit('{ "foo": "bar", "a": {"b": "c"} }', [
462+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] },
463+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 32 },
464+
], [], false, [0]);
465+
assertVisit('{ "a": { "b": "c", "d": { "e": "f" } } }', [
466+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] },
467+
{ id: 'onObjectProperty', text: '"a"', startLine: 0, startCharacter: 2, arg: 'a', path: [] },
468+
{ id: 'onSeparator', text: ':', startLine: 0, startCharacter: 5, arg: ':' },
469+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 7, path: ['a'] },
470+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 37 },
471+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 39 }
472+
], [], true, [7]);
461473
});
462474

463475
test('visit: array', () => {
@@ -514,6 +526,14 @@ suite('JSON', () => {
514526
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 58 },
515527
{ id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 60 },
516528
]);
529+
assertVisit('{ "foo": [ { "a": "b", "c:": "d", "d": { "e": "f" } } ] }', [
530+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] },
531+
{ id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 2, arg: 'foo', path: [] },
532+
{ id: 'onSeparator', text: ':', startLine: 0, startCharacter: 7, arg: ':' },
533+
{ id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 9, path: ['foo'] },
534+
{ id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 54 },
535+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 56 }
536+
], [], true, [9]);
517537
});
518538

519539
test('visit: comment', () => {

0 commit comments

Comments
 (0)