Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON path supplier parameter to visitor functions #62

Merged
merged 1 commit into from
Dec 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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