Skip to content

Commit 35d94cd

Browse files
authored
Merge pull request #62 from Marcono1234/marcono1234/visitor-json-path
Add JSON path supplier parameter to visitor functions
2 parents fee184d + c1fcd82 commit 35d94cd

File tree

6 files changed

+238
-95
lines changed

6 files changed

+238
-95
lines changed

README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export interface JSONScanner {
5050
*/
5151
scan(): SyntaxKind;
5252
/**
53-
* Returns the current scan position, which is after the last read token.
53+
* Returns the zero-based current scan position, which is after the last read token.
5454
*/
5555
getPosition(): number;
5656
/**
@@ -62,7 +62,7 @@ export interface JSONScanner {
6262
*/
6363
getTokenValue(): string;
6464
/**
65-
* The start offset of the last read token.
65+
* The zero-based start offset of the last read token.
6666
*/
6767
getTokenOffset(): number;
6868
/**
@@ -103,31 +103,45 @@ export declare function parse(text: string, errors?: {error: ParseErrorCode;}[],
103103
*/
104104
export declare function visit(text: string, visitor: JSONVisitor, options?: ParseOptions): any;
105105

106+
/**
107+
* Visitor called by {@linkcode visit} when parsing JSON.
108+
*
109+
* The visitor functions have the following common parameters:
110+
* - `offset`: Global offset within the JSON document, starting at 0
111+
* - `startLine`: Line number, starting at 0
112+
* - `startCharacter`: Start character (column) within the current line, starting at 0
113+
*
114+
* Additionally some functions have a `pathSupplier` parameter which can be used to obtain the
115+
* current `JSONPath` within the document.
116+
*/
106117
export interface JSONVisitor {
107118
/**
108119
* Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
109120
*/
110-
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
121+
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
122+
111123
/**
112124
* Invoked when a property is encountered. The offset and length represent the location of the property name.
125+
* The `JSONPath` created by the `pathSupplier` refers to the enclosing JSON object, it does not include the
126+
* property name yet.
113127
*/
114-
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
128+
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
115129
/**
116130
* Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
117131
*/
118132
onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
119133
/**
120134
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
121135
*/
122-
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
136+
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
123137
/**
124138
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
125139
*/
126140
onArrayEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
127141
/**
128142
* Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
129143
*/
130-
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number) => void;
144+
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
131145
/**
132146
* Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
133147
*/
@@ -174,6 +188,10 @@ export declare function stripComments(text: string, replaceCh?: string): string;
174188
*/
175189
export declare function getLocation(text: string, position: number): Location;
176190

191+
/**
192+
* A {@linkcode JSONPath} segment. Either a string representing an object property name
193+
* or a number (starting at 0) for array indices.
194+
*/
177195
export declare type Segment = string | number;
178196
export declare type JSONPath = Segment[];
179197
export interface Location {

src/impl/parser.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,20 +386,29 @@ export function findNodeAtOffset(node: Node, offset: number, includeRightBound =
386386
export function visit(text: string, visitor: JSONVisitor, options: ParseOptions = ParseOptions.DEFAULT): any {
387387

388388
const _scanner = createScanner(text, false);
389+
// Important: Only pass copies of this to visitor functions to prevent accidental modification, and
390+
// to not affect visitor functions which stored a reference to a previous JSONPath
391+
const _jsonPath: JSONPath = [];
389392

390393
function toNoArgVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void {
391394
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
392395
}
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+
}
393399
function toOneArgVisit<T>(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number) => void): (arg: T) => void {
394400
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
395401
}
402+
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+
}
396405

397-
const onObjectBegin = toNoArgVisit(visitor.onObjectBegin),
398-
onObjectProperty = toOneArgVisit(visitor.onObjectProperty),
406+
const onObjectBegin = toNoArgVisitWithPath(visitor.onObjectBegin),
407+
onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty),
399408
onObjectEnd = toNoArgVisit(visitor.onObjectEnd),
400-
onArrayBegin = toNoArgVisit(visitor.onArrayBegin),
409+
onArrayBegin = toNoArgVisitWithPath(visitor.onArrayBegin),
401410
onArrayEnd = toNoArgVisit(visitor.onArrayEnd),
402-
onLiteralValue = toOneArgVisit(visitor.onLiteralValue),
411+
onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue),
403412
onSeparator = toOneArgVisit(visitor.onSeparator),
404413
onComment = toNoArgVisit(visitor.onComment),
405414
onError = toOneArgVisit(visitor.onError);
@@ -474,6 +483,8 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
474483
onLiteralValue(value);
475484
} else {
476485
onObjectProperty(value);
486+
// add property name afterwards
487+
_jsonPath.push(value);
477488
}
478489
scanNext();
479490
return true;
@@ -524,6 +535,7 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
524535
} else {
525536
handleError(ParseErrorCode.ColonExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]);
526537
}
538+
_jsonPath.pop(); // remove processed property name
527539
return true;
528540
}
529541

@@ -562,6 +574,7 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
562574
function parseArray(): boolean {
563575
onArrayBegin();
564576
scanNext(); // consume open bracket
577+
let isFirstElement = true;
565578

566579
let needsComma = false;
567580
while (_scanner.getToken() !== SyntaxKind.CloseBracketToken && _scanner.getToken() !== SyntaxKind.EOF) {
@@ -577,12 +590,21 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
577590
} else if (needsComma) {
578591
handleError(ParseErrorCode.CommaExpected, [], []);
579592
}
593+
if (isFirstElement) {
594+
_jsonPath.push(0);
595+
isFirstElement = false;
596+
} else {
597+
(_jsonPath[_jsonPath.length - 1] as number)++;
598+
}
580599
if (!parseValue()) {
581600
handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBracketToken, SyntaxKind.CommaToken]);
582601
}
583602
needsComma = true;
584603
}
585604
onArrayEnd();
605+
if (!isFirstElement) {
606+
_jsonPath.pop(); // remove array index
607+
}
586608
if (_scanner.getToken() !== SyntaxKind.CloseBracketToken) {
587609
handleError(ParseErrorCode.CloseBracketExpected, [SyntaxKind.CloseBracketToken], []);
588610
} else {

src/main.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export interface JSONScanner {
5858
*/
5959
scan(): SyntaxKind;
6060
/**
61-
* Returns the current scan position, which is after the last read token.
61+
* Returns the zero-based current scan position, which is after the last read token.
6262
*/
6363
getPosition(): number;
6464
/**
@@ -70,7 +70,7 @@ export interface JSONScanner {
7070
*/
7171
getTokenValue(): string;
7272
/**
73-
* The start offset of the last read token.
73+
* The zero-based start offset of the last read token.
7474
*/
7575
getTokenOffset(): number;
7676
/**
@@ -198,6 +198,10 @@ export interface Node {
198198
readonly children?: Node[];
199199
}
200200

201+
/**
202+
* A {@linkcode JSONPath} segment. Either a string representing an object property name
203+
* or a number (starting at 0) for array indices.
204+
*/
201205
export type Segment = string | number;
202206
export type JSONPath = Segment[];
203207

@@ -229,16 +233,29 @@ export interface ParseOptions {
229233
allowEmptyContent?: boolean;
230234
}
231235

236+
/**
237+
* Visitor called by {@linkcode visit} when parsing JSON.
238+
*
239+
* The visitor functions have the following common parameters:
240+
* - `offset`: Global offset within the JSON document, starting at 0
241+
* - `startLine`: Line number, starting at 0
242+
* - `startCharacter`: Start character (column) within the current line, starting at 0
243+
*
244+
* Additionally some functions have a `pathSupplier` parameter which can be used to obtain the
245+
* current `JSONPath` within the document.
246+
*/
232247
export interface JSONVisitor {
233248
/**
234249
* Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
235250
*/
236-
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
251+
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
237252

238253
/**
239254
* Invoked when a property is encountered. The offset and length represent the location of the property name.
255+
* The `JSONPath` created by the `pathSupplier` refers to the enclosing JSON object, it does not include the
256+
* property name yet.
240257
*/
241-
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
258+
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
242259

243260
/**
244261
* Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
@@ -248,7 +265,7 @@ export interface JSONVisitor {
248265
/**
249266
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
250267
*/
251-
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
268+
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
252269

253270
/**
254271
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
@@ -258,7 +275,7 @@ export interface JSONVisitor {
258275
/**
259276
* Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
260277
*/
261-
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number) => void;
278+
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
262279

263280
/**
264281
* Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.

src/test/edit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ suite('JSON - edits', () => {
2020
lastEditOffset = edit.offset;
2121
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
2222
}
23-
assert.equal(content, expected);
23+
assert.strictEqual(content, expected);
2424
}
2525

2626
let formattingOptions: FormattingOptions = {

src/test/format.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ suite('JSON - formatter', () => {
3131
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
3232
}
3333

34-
assert.equal(content, expected);
34+
assert.strictEqual(content, expected);
3535
}
3636

3737
test('object - single property', () => {

0 commit comments

Comments
 (0)