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 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 ?? { offset: 0, line: 1, column: 1 };
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 start = input.lastIndexOf(`
298
- `, at - 1) + 1;
299
- const end = input.indexOf(`
300
- `, start);
301
- const line = input.slice(start, end === -1 ? input.length : end);
302
- return line.endsWith("\r") ? line.slice(0, -1) : line;
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 (!/\s/.test(character)) {
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 (!/\s/.test(c)) {
2244
+ if (!/[ \t\n\r]/.test(c)) {
2224
2245
  hasVisible = true;
2225
2246
  }
2226
2247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "links-notation",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Links Notation parser for JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "index.d.ts",
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 ?? { offset: 0, line: 1, column: 1 };
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 start = input.lastIndexOf('\n', at - 1) + 1;
57
- const end = input.indexOf('\n', start);
58
- const line = input.slice(start, end === -1 ? input.length : end);
59
- return line.endsWith('\r') ? line.slice(0, -1) : line;
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
@@ -155,7 +155,7 @@ function isSubstantiveBody(content) {
155
155
  return false;
156
156
  }
157
157
  }
158
- if (!/\s/.test(character)) {
158
+ if (!/[ \t\n\r]/.test(character)) {
159
159
  hasVisible = true;
160
160
  }
161
161
  }
package/src/grammar.pegjs CHANGED
@@ -83,7 +83,7 @@
83
83
  return false;
84
84
  }
85
85
  }
86
- if (!/\s/.test(c)) {
86
+ if (!/[ \t\n\r]/.test(c)) {
87
87
  hasVisible = true;
88
88
  }
89
89
  }
@@ -1881,7 +1881,7 @@ function peg$parse(input, options) {
1881
1881
  return false;
1882
1882
  }
1883
1883
  }
1884
- if (!/\s/.test(c)) {
1884
+ if (!/[ \t\n\r]/.test(c)) {
1885
1885
  hasVisible = true;
1886
1886
  }
1887
1887
  }
@@ -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