Skip to content

Commit b9ecc3b

Browse files
authored
Merge pull request #17 from stoplightio/master
add onLineBreak visit function
2 parents 77ae9d4 + 85b5249 commit b9ecc3b

File tree

5 files changed

+156
-46
lines changed

5 files changed

+156
-46
lines changed

README.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export interface JSONScanner {
6868
* The length of the last read token.
6969
*/
7070
getTokenLength(): number;
71+
/**
72+
* The zero-based start line number of the last read token.
73+
*/
74+
getTokenStartLine(): number;
75+
/**
76+
* The zero-based start character (column) of the last read token.
77+
*/
78+
getTokenStartCharacter(): number;
7179
/**
7280
* An error code of the last scan.
7381
*/
@@ -96,39 +104,39 @@ export interface JSONVisitor {
96104
/**
97105
* Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
98106
*/
99-
onObjectBegin?: (offset: number, length: number) => void;
107+
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
100108
/**
101109
* Invoked when a property is encountered. The offset and length represent the location of the property name.
102110
*/
103-
onObjectProperty?: (property: string, offset: number, length: number) => void;
111+
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
104112
/**
105113
* Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
106114
*/
107-
onObjectEnd?: (offset: number, length: number) => void;
115+
onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
108116
/**
109117
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
110118
*/
111-
onArrayBegin?: (offset: number, length: number) => void;
119+
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
112120
/**
113121
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
114122
*/
115-
onArrayEnd?: (offset: number, length: number) => void;
123+
onArrayEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
116124
/**
117125
* Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
118126
*/
119-
onLiteralValue?: (value: any, offset: number, length: number) => void;
127+
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number) => void;
120128
/**
121129
* Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
122130
*/
123-
onSeparator?: (charcter: string, offset: number, length: number) => void;
124-
/**
125-
* When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment.
126-
*/
127-
onComment?: (offset: number, length: number) => void;
131+
onSeparator?: (character: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
132+
/**
133+
* When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment.
134+
*/
135+
onComment?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
128136
/**
129137
* Invoked on an error.
130138
*/
131-
onError?: (error: ParseErrorCode, offset: number, length: number) => void;
139+
onError?: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => void;
132140
}
133141

134142
/**
@@ -298,4 +306,4 @@ License
298306

299307
(MIT License)
300308

301-
Copyright 2018, Microsoft
309+
Copyright 2018, Microsoft

src/impl/parser.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@
66

77
import { createScanner } from './scanner';
88
import {
9-
ScanError, SyntaxKind, Node, NodeType, Edit, JSONPath, FormattingOptions,
10-
ModificationOptions, ParseError, ParseErrorCode, Location, Segment, ParseOptions, JSONVisitor
9+
JSONPath,
10+
JSONVisitor,
11+
Location,
12+
Node,
13+
NodeType,
14+
ParseError,
15+
ParseErrorCode,
16+
ParseOptions,
17+
ScanError,
18+
Segment,
19+
SyntaxKind
1120
} from '../main';
1221

1322
namespace ParseOptions {
@@ -376,11 +385,11 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions
376385

377386
let _scanner = createScanner(text, false);
378387

379-
function toNoArgVisit(visitFunction?: (offset: number, length: number) => void): () => void {
380-
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength()) : () => true;
388+
function toNoArgVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void {
389+
return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
381390
}
382-
function toOneArgVisit<T>(visitFunction?: (arg: T, offset: number, length: number) => void): (arg: T) => void {
383-
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength()) : () => true;
391+
function toOneArgVisit<T>(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number) => void): (arg: T) => void {
392+
return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
384393
}
385394

386395
let onObjectBegin = toNoArgVisit(visitor.onObjectBegin),
@@ -650,4 +659,4 @@ function getLiteralNodeType(value: any): NodeType {
650659
case 'string': return 'string';
651660
default: return 'null';
652661
}
653-
}
662+
}

src/impl/scanner.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export function createScanner(text: string, ignoreTrivia: boolean = false): JSON
1717
value: string = '',
1818
tokenOffset = 0,
1919
token: SyntaxKind = SyntaxKind.Unknown,
20+
lineNumber = 0,
21+
lineStartOffset = 0,
22+
tokenLineStartOffset = 0,
23+
prevTokenLineStartOffset = 0,
2024
scanError: ScanError = ScanError.None;
2125

2226
function scanHexDigits(count: number, exact?: boolean): number {
@@ -179,6 +183,8 @@ export function createScanner(text: string, ignoreTrivia: boolean = false): JSON
179183
scanError = ScanError.None;
180184

181185
tokenOffset = pos;
186+
lineStartOffset = lineNumber;
187+
prevTokenLineStartOffset = tokenLineStartOffset;
182188

183189
if (pos >= len) {
184190
// at the end
@@ -206,6 +212,8 @@ export function createScanner(text: string, ignoreTrivia: boolean = false): JSON
206212
pos++;
207213
value += '\n';
208214
}
215+
lineNumber++;
216+
tokenLineStartOffset = pos;
209217
return token = SyntaxKind.LineBreakTrivia;
210218
}
211219

@@ -268,7 +276,17 @@ export function createScanner(text: string, ignoreTrivia: boolean = false): JSON
268276
commentClosed = true;
269277
break;
270278
}
279+
271280
pos++;
281+
282+
if (isLineBreak(ch)) {
283+
if (ch === CharacterCodes.carriageReturn && text.charCodeAt(pos) === CharacterCodes.lineFeed) {
284+
pos++;
285+
}
286+
287+
lineNumber++;
288+
tokenLineStartOffset = pos;
289+
}
272290
}
273291

274292
if (!commentClosed) {
@@ -365,7 +383,9 @@ export function createScanner(text: string, ignoreTrivia: boolean = false): JSON
365383
getTokenValue: () => value,
366384
getTokenOffset: () => tokenOffset,
367385
getTokenLength: () => pos - tokenOffset,
368-
getTokenError: () => scanError
386+
getTokenStartLine: () => lineStartOffset,
387+
getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
388+
getTokenError: () => scanError,
369389
};
370390
}
371391

src/main.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export interface JSONScanner {
7777
* The length of the last read token.
7878
*/
7979
getTokenLength(): number;
80+
/**
81+
* The zero-based start line number of the last read token.
82+
*/
83+
getTokenStartLine(): number;
84+
/**
85+
* The zero-based start character (column) of the last read token.
86+
*/
87+
getTokenStartCharacter(): number;
8088
/**
8189
* An error code of the last scan.
8290
*/
@@ -225,47 +233,47 @@ export interface JSONVisitor {
225233
/**
226234
* Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
227235
*/
228-
onObjectBegin?: (offset: number, length: number) => void;
236+
onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
229237

230238
/**
231239
* Invoked when a property is encountered. The offset and length represent the location of the property name.
232240
*/
233-
onObjectProperty?: (property: string, offset: number, length: number) => void;
241+
onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
234242

235243
/**
236244
* Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
237245
*/
238-
onObjectEnd?: (offset: number, length: number) => void;
246+
onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
239247

240248
/**
241249
* Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
242250
*/
243-
onArrayBegin?: (offset: number, length: number) => void;
251+
onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
244252

245253
/**
246254
* Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
247255
*/
248-
onArrayEnd?: (offset: number, length: number) => void;
256+
onArrayEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
249257

250258
/**
251259
* Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
252260
*/
253-
onLiteralValue?: (value: any, offset: number, length: number) => void;
261+
onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number) => void;
254262

255263
/**
256264
* Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
257265
*/
258-
onSeparator?: (character: string, offset: number, length: number) => void;
266+
onSeparator?: (character: string, offset: number, length: number, startLine: number, startCharacter: number) => void;
259267

260268
/**
261269
* When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment.
262270
*/
263-
onComment?: (offset: number, length: number) => void;
271+
onComment?: (offset: number, length: number, startLine: number, startCharacter: number) => void;
264272

265273
/**
266274
* Invoked on an error.
267275
*/
268-
onError?: (error: ParseErrorCode, offset: number, length: number) => void;
276+
onError?: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => void;
269277
}
270278

271279
/**

src/test/json.test.ts

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,20 @@ function assertTree(input: string, expected: any, expectedErrors: ParseError[] =
7070
interface VisitorCallback {
7171
id: keyof JSONVisitor,
7272
text: string;
73+
startLine: number;
74+
startCharacter: number;
7375
arg?: any;
7476
};
77+
interface VisitorError extends ParseError {
78+
startLine: number;
79+
startCharacter: number;
80+
}
7581

76-
function assertVisit(input: string, expected: VisitorCallback[], expectedErrors: ParseError[] = [], disallowComments = false): void {
77-
let errors: ParseError[] = [];
82+
function assertVisit(input: string, expected: VisitorCallback[], expectedErrors: VisitorError[] = [], disallowComments = false): void {
83+
let errors: VisitorError[] = [];
7884
let actuals: VisitorCallback[] = [];
79-
let noArgHalder = (id: keyof JSONVisitor) => (offset: number, length: number) => actuals.push({ id, text: input.substr(offset, length) });
80-
let oneArgHalder = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number) => actuals.push({ id, text: input.substr(offset, length), arg });
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 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 });
8187
visit(input, {
8288
onObjectBegin: noArgHalder('onObjectBegin'),
8389
onObjectProperty: oneArgHalder('onObjectProperty'),
@@ -87,8 +93,8 @@ function assertVisit(input: string, expected: VisitorCallback[], expectedErrors:
8793
onLiteralValue: oneArgHalder('onLiteralValue'),
8894
onSeparator: oneArgHalder('onSeparator'),
8995
onComment: noArgHalder('onComment'),
90-
onError: (error: ParseErrorCode, offset: number, length: number) => {
91-
errors.push({ error, offset, length })
96+
onError: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => {
97+
errors.push({ error, offset, length, startLine, startCharacter })
9298
}
9399
}, {
94100
disallowComments
@@ -282,7 +288,7 @@ suite('JSON', () => {
282288
assertInvalidParse('[ ,1, 2, 3, ]', [1, 2, 3]);
283289
});
284290

285-
test('parse: disallow commments', () => {
291+
test('parse: disallow comments', () => {
286292
let options = { disallowComments: true };
287293

288294
assertValidParse('[ 1, 2, null, "foo" ]', [1, 2, null, 'foo'], options);
@@ -422,21 +428,80 @@ suite('JSON', () => {
422428
});
423429

424430
test('visit: object', () => {
425-
assertVisit('{ }', [{ id: 'onObjectBegin', text: '{' }, { id: 'onObjectEnd', text: '}' }]);
426-
assertVisit('{ "foo": "bar" }', [{ id: 'onObjectBegin', text: '{' }, { id: 'onObjectProperty', text: '"foo"', arg: 'foo' }, { id: 'onSeparator', text: ':', arg: ':' }, { id: 'onLiteralValue', text: '"bar"', arg: 'bar' }, { id: 'onObjectEnd', text: '}' }]);
427-
assertVisit('{ "foo": { "goo": 3 } }', [{ id: 'onObjectBegin', text: '{' }, { id: 'onObjectProperty', text: '"foo"', arg: 'foo' }, { id: 'onSeparator', text: ':', arg: ':' }, { id: 'onObjectBegin', text: '{' }, { id: 'onObjectProperty', text: '"goo"', arg: 'goo' }, { id: 'onSeparator', text: ':', arg: ':' }, { id: 'onLiteralValue', text: '3', arg: 3 }, { id: 'onObjectEnd', text: '}' }, { id: 'onObjectEnd', text: '}' }]);
431+
assertVisit('{ }', [{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0 }, { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 2 }]);
432+
assertVisit('{ "foo": "bar" }', [
433+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0 },
434+
{ id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 2, arg: 'foo' },
435+
{ id: 'onSeparator', text: ':', startLine: 0, startCharacter: 7, arg: ':' },
436+
{ id: 'onLiteralValue', text: '"bar"', startLine: 0, startCharacter: 9, arg: 'bar' },
437+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 15 },
438+
]);
439+
assertVisit('{ "foo": { "goo": 3 } }', [
440+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0 },
441+
{ id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 2, arg: 'foo' },
442+
{ id: 'onSeparator', text: ':', startLine: 0, startCharacter: 7, arg: ':' },
443+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 9 },
444+
{ id: 'onObjectProperty', text: '"goo"', startLine: 0, startCharacter: 11, arg: 'goo' },
445+
{ id: 'onSeparator', text: ':', startLine: 0, startCharacter: 16, arg: ':' },
446+
{ id: 'onLiteralValue', text: '3', startLine: 0, startCharacter: 18, arg: 3 },
447+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 20 },
448+
{ id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 22 },
449+
]);
428450
});
429451

430452
test('visit: array', () => {
431-
assertVisit('[]', [{ id: 'onArrayBegin', text: '[' }, { id: 'onArrayEnd', text: ']' }]);
432-
assertVisit('[ true, null, [] ]', [{ id: 'onArrayBegin', text: '[' }, { id: 'onLiteralValue', text: 'true', arg: true }, { id: 'onSeparator', text: ',', arg: ',' }, { id: 'onLiteralValue', text: 'null', arg: null }, { id: 'onSeparator', text: ',', arg: ',' }, { id: 'onArrayBegin', text: '[' }, { id: 'onArrayEnd', text: ']' }, { id: 'onArrayEnd', text: ']' }]);
453+
assertVisit('[]', [{ id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0 }, { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 1 }]);
454+
assertVisit('[ true, null, [] ]', [
455+
{ id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0 },
456+
{ id: 'onLiteralValue', text: 'true', startLine: 0, startCharacter: 2, arg: true },
457+
{ id: 'onSeparator', text: ',', startLine: 0, startCharacter: 6, arg: ',' },
458+
{ id: 'onLiteralValue', text: 'null', startLine: 0, startCharacter: 8, arg: null },
459+
{ id: 'onSeparator', text: ',', startLine: 0, startCharacter: 12, arg: ',' },
460+
{ id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 14 },
461+
{ id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 15 },
462+
{ id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 17 },
463+
]);
464+
assertVisit('[\r\n0,\r\n1,\r\n2\r\n]', [
465+
{ id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0 },
466+
{ id: 'onLiteralValue', text: '0', startLine: 1, startCharacter: 0, arg: 0 },
467+
{ id: 'onSeparator', text: ',', startLine: 1, startCharacter: 1, arg: ',' },
468+
{ id: 'onLiteralValue', text: '1', startLine: 2, startCharacter: 0, arg: 1 },
469+
{ id: 'onSeparator', text: ',', startLine: 2, startCharacter: 1, arg: ',' },
470+
{ id: 'onLiteralValue', text: '2', startLine: 3, startCharacter: 0, arg: 2 },
471+
{ id: 'onArrayEnd', text: ']', startLine: 4, startCharacter: 0 }]);
433472
});
434473

435474
test('visit: comment', () => {
436-
assertVisit('/* g */ { "foo": //f\n"bar" }', [{ id: 'onComment', text: '/* g */' }, { id: 'onObjectBegin', text: '{' }, { id: 'onObjectProperty', text: '"foo"', arg: 'foo' }, { id: 'onSeparator', text: ':', arg: ':' }, { id: 'onComment', text: '//f' }, { id: 'onLiteralValue', text: '"bar"', arg: 'bar' }, { id: 'onObjectEnd', text: '}' }]);
437-
assertVisit('/* g */ { "foo": //f\n"bar" }',
438-
[{ id: 'onObjectBegin', text: '{' }, { id: 'onObjectProperty', text: '"foo"', arg: 'foo' }, { id: 'onSeparator', text: ':', arg: ':' }, { id: 'onLiteralValue', text: '"bar"', arg: 'bar' }, { id: 'onObjectEnd', text: '}' }],
439-
[{ error: ParseErrorCode.InvalidCommentToken, offset: 0, length: 7 }, { error: ParseErrorCode.InvalidCommentToken, offset: 17, length: 3 }],
475+
assertVisit('/* g */ { "foo": //f\n"bar" }', [
476+
{ id: 'onComment', text: '/* g */', startLine: 0, startCharacter: 0 },
477+
{ id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 8 },
478+
{ id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 10, arg: 'foo' },
479+
{ id: 'onSeparator', text: ':', startLine: 0, startCharacter: 15, arg: ':' },
480+
{ id: 'onComment', text: '//f', startLine: 0, startCharacter: 17 },
481+
{ id: 'onLiteralValue', text: '"bar"', startLine: 1, startCharacter: 0, arg: 'bar' },
482+
{ id: 'onObjectEnd', text: '}', startLine: 1, startCharacter: 6 },
483+
]);
484+
assertVisit('/* g\r\n */ { "foo": //f\n"bar" }', [
485+
{ id: 'onComment', text: '/* g\r\n */', startLine: 0, startCharacter: 0 },
486+
{ id: 'onObjectBegin', text: '{', startLine: 1, startCharacter: 4 },
487+
{ id: 'onObjectProperty', text: '"foo"', startLine: 1, startCharacter: 6, arg: 'foo' },
488+
{ id: 'onSeparator', text: ':', startLine: 1, startCharacter: 11, arg: ':' },
489+
{ id: 'onComment', text: '//f', startLine: 1, startCharacter: 13 },
490+
{ id: 'onLiteralValue', text: '"bar"', startLine: 2, startCharacter: 0, arg: 'bar' },
491+
{ id: 'onObjectEnd', text: '}', startLine: 2, startCharacter: 6 },
492+
]);
493+
assertVisit('/* g\n */ { "foo": //f\n"bar"\n}',
494+
[
495+
{ id: 'onObjectBegin', text: '{', startLine: 1, startCharacter: 4 },
496+
{ id: 'onObjectProperty', text: '"foo"', startLine: 1, startCharacter: 6, arg: 'foo' },
497+
{ id: 'onSeparator', text: ':', startLine: 1, startCharacter: 11, arg: ':' },
498+
{ id: 'onLiteralValue', text: '"bar"', startLine: 2, startCharacter: 0, arg: 'bar' },
499+
{ id: 'onObjectEnd', text: '}', startLine: 3, startCharacter: 0 },
500+
],
501+
[
502+
{ error: ParseErrorCode.InvalidCommentToken, offset: 0, length: 8, startLine: 0, startCharacter: 0 },
503+
{ error: ParseErrorCode.InvalidCommentToken, offset: 18, length: 3, startLine: 1, startCharacter: 13 },
504+
],
440505
true);
441506
});
442507

0 commit comments

Comments
 (0)