Skip to content

Commit

Permalink
Merge pull request #62 from Marcono1234/marcono1234/visitor-json-path
Browse files Browse the repository at this point in the history
Add JSON path supplier parameter to visitor functions
  • Loading branch information
aeschli authored Dec 30, 2021
2 parents fee184d + c1fcd82 commit 35d94cd
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 95 deletions.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface JSONScanner {
*/
scan(): SyntaxKind;
/**
* Returns the current scan position, which is after the last read token.
* Returns the zero-based current scan position, which is after the last read token.
*/
getPosition(): number;
/**
Expand All @@ -62,7 +62,7 @@ export interface JSONScanner {
*/
getTokenValue(): string;
/**
* The start offset of the last read token.
* The zero-based start offset of the last read token.
*/
getTokenOffset(): number;
/**
Expand Down Expand Up @@ -103,31 +103,45 @@ export declare function parse(text: string, errors?: {error: ParseErrorCode;}[],
*/
export declare function visit(text: string, visitor: JSONVisitor, options?: ParseOptions): any;

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

/**
* Invoked when a property is encountered. The offset and length represent the location of the property name.
* The `JSONPath` created by the `pathSupplier` refers to the enclosing JSON object, it does not include the
* property name yet.
*/
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
/**
* Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
*/
onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
/**
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
*/
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
/**
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
*/
onArrayEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
/**
* Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
*/
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number) => void;
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void;
/**
* Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
*/
Expand Down Expand Up @@ -174,6 +188,10 @@ export declare function stripComments(text: string, replaceCh?: string): string;
*/
export declare function getLocation(text: string, position: number): Location;

/**
* A {@linkcode JSONPath} segment. Either a string representing an object property name
* or a number (starting at 0) for array indices.
*/
export declare type Segment = string | number;
export declare type JSONPath = Segment[];
export interface Location {
Expand Down
30 changes: 26 additions & 4 deletions src/impl/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,20 +386,29 @@ export function findNodeAtOffset(node: Node, offset: number, includeRightBound =
export function visit(text: string, visitor: JSONVisitor, options: ParseOptions = ParseOptions.DEFAULT): any {

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

function toNoArgVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void {
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
}
function toNoArgVisitWithPath(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void): () => void {
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
}
function toOneArgVisit<T>(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number) => void): (arg: T) => void {
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
}
function toOneArgVisitWithPath<T>(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void): (arg: T) => void {
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
}

const onObjectBegin = toNoArgVisit(visitor.onObjectBegin),
onObjectProperty = toOneArgVisit(visitor.onObjectProperty),
const onObjectBegin = toNoArgVisitWithPath(visitor.onObjectBegin),
onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty),
onObjectEnd = toNoArgVisit(visitor.onObjectEnd),
onArrayBegin = toNoArgVisit(visitor.onArrayBegin),
onArrayBegin = toNoArgVisitWithPath(visitor.onArrayBegin),
onArrayEnd = toNoArgVisit(visitor.onArrayEnd),
onLiteralValue = toOneArgVisit(visitor.onLiteralValue),
onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue),
onSeparator = toOneArgVisit(visitor.onSeparator),
onComment = toNoArgVisit(visitor.onComment),
onError = toOneArgVisit(visitor.onError);
Expand Down Expand Up @@ -474,6 +483,8 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
onLiteralValue(value);
} else {
onObjectProperty(value);
// add property name afterwards
_jsonPath.push(value);
}
scanNext();
return true;
Expand Down Expand Up @@ -524,6 +535,7 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
} else {
handleError(ParseErrorCode.ColonExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]);
}
_jsonPath.pop(); // remove processed property name
return true;
}

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

let needsComma = false;
while (_scanner.getToken() !== SyntaxKind.CloseBracketToken && _scanner.getToken() !== SyntaxKind.EOF) {
Expand All @@ -577,12 +590,21 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
} else if (needsComma) {
handleError(ParseErrorCode.CommaExpected, [], []);
}
if (isFirstElement) {
_jsonPath.push(0);
isFirstElement = false;
} else {
(_jsonPath[_jsonPath.length - 1] as number)++;
}
if (!parseValue()) {
handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBracketToken, SyntaxKind.CommaToken]);
}
needsComma = true;
}
onArrayEnd();
if (!isFirstElement) {
_jsonPath.pop(); // remove array index
}
if (_scanner.getToken() !== SyntaxKind.CloseBracketToken) {
handleError(ParseErrorCode.CloseBracketExpected, [SyntaxKind.CloseBracketToken], []);
} else {
Expand Down
29 changes: 23 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface JSONScanner {
*/
scan(): SyntaxKind;
/**
* Returns the current scan position, which is after the last read token.
* Returns the zero-based current scan position, which is after the last read token.
*/
getPosition(): number;
/**
Expand All @@ -70,7 +70,7 @@ export interface JSONScanner {
*/
getTokenValue(): string;
/**
* The start offset of the last read token.
* The zero-based start offset of the last read token.
*/
getTokenOffset(): number;
/**
Expand Down Expand Up @@ -198,6 +198,10 @@ export interface Node {
readonly children?: Node[];
}

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

Expand Down Expand Up @@ -229,16 +233,29 @@ export interface ParseOptions {
allowEmptyContent?: boolean;
}

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

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

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

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

/**
* Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
Expand Down
2 changes: 1 addition & 1 deletion src/test/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ suite('JSON - edits', () => {
lastEditOffset = edit.offset;
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
}
assert.equal(content, expected);
assert.strictEqual(content, expected);
}

let formattingOptions: FormattingOptions = {
Expand Down
2 changes: 1 addition & 1 deletion src/test/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ suite('JSON - formatter', () => {
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
}

assert.equal(content, expected);
assert.strictEqual(content, expected);
}

test('object - single property', () => {
Expand Down
Loading

0 comments on commit 35d94cd

Please sign in to comment.