links-notation 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +31 -10
- package/package.json +1 -1
- package/src/ParseError.js +34 -6
- package/src/comments.js +1 -1
- package/src/grammar.pegjs +1 -1
- package/src/parser-generated.js +1 -1
- package/tests/Issue312Conformance.test.js +46 -0
- package/tests/ParseErrorPosition.test.js +8 -0
package/dist/index.js
CHANGED
|
@@ -275,7 +275,8 @@ var ELLIPSIS = "...";
|
|
|
275
275
|
|
|
276
276
|
class ParseError extends Error {
|
|
277
277
|
constructor(input, error) {
|
|
278
|
-
const start = error?.location?.start ??
|
|
278
|
+
const start = positionAt(input, error?.location?.start?.offset ?? 0);
|
|
279
|
+
const end = positionAt(input, error?.location?.end?.offset ?? start.offset);
|
|
279
280
|
const lineText = lineAt(input, start.offset);
|
|
280
281
|
const summary = `line ${start.line}, column ${start.column}: ${error.message}`;
|
|
281
282
|
const snippet = quote(start.line, lineText, start.column);
|
|
@@ -283,7 +284,7 @@ class ParseError extends Error {
|
|
|
283
284
|
${snippet}`);
|
|
284
285
|
this.name = "ParseError";
|
|
285
286
|
this.cause = error;
|
|
286
|
-
this.location = error.location;
|
|
287
|
+
this.location = { ...error.location, start, end };
|
|
287
288
|
this.offset = start.offset;
|
|
288
289
|
this.line = start.line;
|
|
289
290
|
this.column = start.column;
|
|
@@ -294,12 +295,32 @@ ${snippet}`);
|
|
|
294
295
|
}
|
|
295
296
|
function lineAt(input, offset) {
|
|
296
297
|
const at = Math.max(0, Math.min(offset, input.length));
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
const before = input.slice(0, at);
|
|
299
|
+
const start = Math.max(before.lastIndexOf(`
|
|
300
|
+
`), before.lastIndexOf("\r")) + 1;
|
|
301
|
+
const end = input.slice(start).search(/[\r\n]/);
|
|
302
|
+
return input.slice(start, end === -1 ? input.length : start + end);
|
|
303
|
+
}
|
|
304
|
+
function positionAt(input, offset) {
|
|
305
|
+
const at = Math.max(0, Math.min(offset, input.length));
|
|
306
|
+
let line = 1;
|
|
307
|
+
let column = 1;
|
|
308
|
+
for (let index = 0;index < at; index++) {
|
|
309
|
+
if (input[index] === "\r") {
|
|
310
|
+
line++;
|
|
311
|
+
column = 1;
|
|
312
|
+
if (input[index + 1] === `
|
|
313
|
+
` && index + 1 < at)
|
|
314
|
+
index++;
|
|
315
|
+
} else if (input[index] === `
|
|
316
|
+
`) {
|
|
317
|
+
line++;
|
|
318
|
+
column = 1;
|
|
319
|
+
} else {
|
|
320
|
+
column++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { offset: at, line, column };
|
|
303
324
|
}
|
|
304
325
|
function quote(number, lineText, column) {
|
|
305
326
|
const [quoted, at] = windowAround(lineText, column);
|
|
@@ -400,7 +421,7 @@ function isSubstantiveBody(content) {
|
|
|
400
421
|
return false;
|
|
401
422
|
}
|
|
402
423
|
}
|
|
403
|
-
if (
|
|
424
|
+
if (!/[ \t\n\r]/.test(character)) {
|
|
404
425
|
hasVisible = true;
|
|
405
426
|
}
|
|
406
427
|
}
|
|
@@ -2220,7 +2241,7 @@ function peg$parse(input, options) {
|
|
|
2220
2241
|
return false;
|
|
2221
2242
|
}
|
|
2222
2243
|
}
|
|
2223
|
-
if (
|
|
2244
|
+
if (!/[ \t\n\r]/.test(c)) {
|
|
2224
2245
|
hasVisible = true;
|
|
2225
2246
|
}
|
|
2226
2247
|
}
|
package/package.json
CHANGED
package/src/ParseError.js
CHANGED
|
@@ -19,7 +19,8 @@ export class ParseError extends Error {
|
|
|
19
19
|
* @param {Error} error - The error the generated parser threw
|
|
20
20
|
*/
|
|
21
21
|
constructor(input, error) {
|
|
22
|
-
const start = error?.location?.start ??
|
|
22
|
+
const start = positionAt(input, error?.location?.start?.offset ?? 0);
|
|
23
|
+
const end = positionAt(input, error?.location?.end?.offset ?? start.offset);
|
|
23
24
|
const lineText = lineAt(input, start.offset);
|
|
24
25
|
const summary = `line ${start.line}, column ${start.column}: ${error.message}`;
|
|
25
26
|
const snippet = quote(start.line, lineText, start.column);
|
|
@@ -29,7 +30,7 @@ export class ParseError extends Error {
|
|
|
29
30
|
/** @type {Error} The error the generated parser threw */
|
|
30
31
|
this.cause = error;
|
|
31
32
|
/** @type {{start: {offset: number, line: number, column: number}}} */
|
|
32
|
-
this.location = error.location;
|
|
33
|
+
this.location = { ...error.location, start, end };
|
|
33
34
|
/** @type {number} Offset of the offending position from the start of the document */
|
|
34
35
|
this.offset = start.offset;
|
|
35
36
|
/** @type {number} Line the offending position is on, counted from 1 */
|
|
@@ -53,10 +54,37 @@ export class ParseError extends Error {
|
|
|
53
54
|
*/
|
|
54
55
|
function lineAt(input, offset) {
|
|
55
56
|
const at = Math.max(0, Math.min(offset, input.length));
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const before = input.slice(0, at);
|
|
58
|
+
const start =
|
|
59
|
+
Math.max(before.lastIndexOf('\n'), before.lastIndexOf('\r')) + 1;
|
|
60
|
+
const end = input.slice(start).search(/[\r\n]/);
|
|
61
|
+
return input.slice(start, end === -1 ? input.length : start + end);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Count CR, LF and CRLF as line breaks, as the grammar does. Peggy's error
|
|
66
|
+
* location counts a lone CR as an ordinary character.
|
|
67
|
+
* @param {string} input - The document being parsed
|
|
68
|
+
* @param {number} offset - Offset of the position
|
|
69
|
+
* @returns {{offset: number, line: number, column: number}} The position
|
|
70
|
+
*/
|
|
71
|
+
function positionAt(input, offset) {
|
|
72
|
+
const at = Math.max(0, Math.min(offset, input.length));
|
|
73
|
+
let line = 1;
|
|
74
|
+
let column = 1;
|
|
75
|
+
for (let index = 0; index < at; index++) {
|
|
76
|
+
if (input[index] === '\r') {
|
|
77
|
+
line++;
|
|
78
|
+
column = 1;
|
|
79
|
+
if (input[index + 1] === '\n' && index + 1 < at) index++;
|
|
80
|
+
} else if (input[index] === '\n') {
|
|
81
|
+
line++;
|
|
82
|
+
column = 1;
|
|
83
|
+
} else {
|
|
84
|
+
column++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { offset: at, line, column };
|
|
60
88
|
}
|
|
61
89
|
|
|
62
90
|
/**
|
package/src/comments.js
CHANGED
package/src/grammar.pegjs
CHANGED
package/src/parser-generated.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { Parser } from '../src/Parser.js';
|
|
3
|
+
|
|
4
|
+
const parser = new Parser();
|
|
5
|
+
|
|
6
|
+
function render(node) {
|
|
7
|
+
if (!node.values || node.values.length === 0) {
|
|
8
|
+
return `<${node.id ?? ''}>`;
|
|
9
|
+
}
|
|
10
|
+
const head =
|
|
11
|
+
node.id === null || node.id === undefined ? '' : `<${node.id}>: `;
|
|
12
|
+
return `(${head}${node.values.map(render).join(' ')})`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parsesAs(source) {
|
|
16
|
+
return parser.parse(source).map(render).join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('CR is a line break', () => {
|
|
20
|
+
expect(parsesAs('a\rb')).toBe('(<a>)\n(<b>)');
|
|
21
|
+
expect(parsesAs('a\r\nb')).toBe('(<a>)\n(<b>)');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('only grammar whitespace makes an empty document', () => {
|
|
25
|
+
expect(parsesAs(' \t\n\r')).toBe('');
|
|
26
|
+
for (const character of ['\u0085', '\u00a0', '\u2028', '\u3000', '\ufeff']) {
|
|
27
|
+
expect(parsesAs(character)).toBe(`(<${character}>)`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('Unicode spaces are substantive even quote bodies', () => {
|
|
32
|
+
for (const character of ['\u0085', '\u00a0', '\u2028', '\u3000', '\ufeff']) {
|
|
33
|
+
expect(parsesAs(`''${character}''`)).toBe(`(<${character}>)`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('trailing indentation is document whitespace', () => {
|
|
38
|
+
expect(parsesAs('a\n ')).toBe('(<a>)');
|
|
39
|
+
expect(parsesAs('a\n \t')).toBe('(<a>)');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a single reference is a link value at every depth', () => {
|
|
43
|
+
expect(parsesAs('1')).toBe('(<1>)');
|
|
44
|
+
expect(parsesAs('(1)')).toBe('(<1>)');
|
|
45
|
+
expect(parsesAs('((1))')).toBe('((<1>))');
|
|
46
|
+
});
|
|
@@ -50,6 +50,14 @@ test('reports the line a late defect is on', () => {
|
|
|
50
50
|
expect(error.lineText).toBe('e: f: g');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
test('lone CR counts as one line break in diagnostics', () => {
|
|
54
|
+
const error = syntaxError('a\rb: c: d');
|
|
55
|
+
|
|
56
|
+
expect(error.line).toBe(2);
|
|
57
|
+
expect(error.column).toBe(5);
|
|
58
|
+
expect(error.lineText).toBe('b: c: d');
|
|
59
|
+
});
|
|
60
|
+
|
|
53
61
|
test('reports the end of the document when a group is never closed', () => {
|
|
54
62
|
const error = syntaxError('a (b\n');
|
|
55
63
|
|