links-notation 0.20.0 → 0.21.1
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 +49 -16
- package/package.json +1 -1
- package/src/ParseError.js +34 -6
- package/src/Parser.js +31 -7
- package/src/comments.js +1 -1
- package/src/grammar.pegjs +1 -1
- package/src/parser-generated.js +1 -1
- package/tests/IndentedIdNestedValues.test.js +38 -0
- package/tests/IndentedIdSyntax.test.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
|
}
|
|
@@ -2334,12 +2355,7 @@ class Parser {
|
|
|
2334
2355
|
return;
|
|
2335
2356
|
if (item.children && item.children.length > 0) {
|
|
2336
2357
|
if (item.id !== undefined && item.id !== null && (!item.values || item.values.length === 0)) {
|
|
2337
|
-
const childValues = item.children.map((child) =>
|
|
2338
|
-
if (child.values && child.values.length === 1) {
|
|
2339
|
-
return this.transformLink(child.values[0]);
|
|
2340
|
-
}
|
|
2341
|
-
return this.transformLink(child);
|
|
2342
|
-
});
|
|
2358
|
+
const childValues = item.children.map((child) => this.transformIndentedValue(child));
|
|
2343
2359
|
const linkWithChildren = {
|
|
2344
2360
|
id: item.id,
|
|
2345
2361
|
values: childValues
|
|
@@ -2371,6 +2387,23 @@ class Parser {
|
|
|
2371
2387
|
}
|
|
2372
2388
|
}
|
|
2373
2389
|
}
|
|
2390
|
+
transformIndentedValue(item) {
|
|
2391
|
+
const children = item.children || [];
|
|
2392
|
+
if (children.length && item.id != null && !item.values?.length) {
|
|
2393
|
+
return new Link(item.id, children.map((child) => this.transformIndentedValue(child)));
|
|
2394
|
+
}
|
|
2395
|
+
const current = this.transformLink(item);
|
|
2396
|
+
if (children.length) {
|
|
2397
|
+
return new Link(current.id, [
|
|
2398
|
+
...current.values,
|
|
2399
|
+
...children.map((child) => this.transformIndentedValue(child))
|
|
2400
|
+
]);
|
|
2401
|
+
}
|
|
2402
|
+
if (item.id == null && item.nested === undefined && current.values.length === 1) {
|
|
2403
|
+
return current.values[0];
|
|
2404
|
+
}
|
|
2405
|
+
return current;
|
|
2406
|
+
}
|
|
2374
2407
|
combinePathElements(pathElements, current) {
|
|
2375
2408
|
if (pathElements.length === 0)
|
|
2376
2409
|
return current;
|
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/Parser.js
CHANGED
|
@@ -80,13 +80,9 @@ export class Parser {
|
|
|
80
80
|
item.id !== null &&
|
|
81
81
|
(!item.values || item.values.length === 0)
|
|
82
82
|
) {
|
|
83
|
-
const childValues = item.children.map((child) =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return this.transformLink(child.values[0]);
|
|
87
|
-
}
|
|
88
|
-
return this.transformLink(child);
|
|
89
|
-
});
|
|
83
|
+
const childValues = item.children.map((child) =>
|
|
84
|
+
this.transformIndentedValue(child)
|
|
85
|
+
);
|
|
90
86
|
const linkWithChildren = {
|
|
91
87
|
id: item.id,
|
|
92
88
|
values: childValues,
|
|
@@ -128,6 +124,34 @@ export class Parser {
|
|
|
128
124
|
}
|
|
129
125
|
}
|
|
130
126
|
|
|
127
|
+
// A line below an indented ID is a value in its own right. Bare single
|
|
128
|
+
// references unwrap, but named links and descendants keep their structure.
|
|
129
|
+
transformIndentedValue(item) {
|
|
130
|
+
const children = item.children || [];
|
|
131
|
+
if (children.length && item.id != null && !item.values?.length) {
|
|
132
|
+
return new Link(
|
|
133
|
+
item.id,
|
|
134
|
+
children.map((child) => this.transformIndentedValue(child))
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const current = this.transformLink(item);
|
|
139
|
+
if (children.length) {
|
|
140
|
+
return new Link(current.id, [
|
|
141
|
+
...current.values,
|
|
142
|
+
...children.map((child) => this.transformIndentedValue(child)),
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
item.id == null &&
|
|
147
|
+
item.nested === undefined &&
|
|
148
|
+
current.values.length === 1
|
|
149
|
+
) {
|
|
150
|
+
return current.values[0];
|
|
151
|
+
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
combinePathElements(pathElements, current) {
|
|
132
156
|
if (pathElements.length === 0) return current;
|
|
133
157
|
if (pathElements.length === 1) {
|
package/src/comments.js
CHANGED
package/src/grammar.pegjs
CHANGED
package/src/parser-generated.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { Parser } from '../src/Parser.js';
|
|
3
|
+
import { formatLinks } from '../src/Link.js';
|
|
4
|
+
|
|
5
|
+
const cases = [
|
|
6
|
+
[
|
|
7
|
+
'outer:\n inner:\n value1\n value2\n value3',
|
|
8
|
+
'(outer: (inner: value1 value2) value3)',
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
'statement:\n subject:\n I\n verb:\n love\n object:\n you:\n very\n much',
|
|
12
|
+
'(statement: (subject: I) (verb: love) (object: (you: very much)))',
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
'document:\n (metadata: title author date)\n content:\n paragraph1\n (paragraph2: text (with: nested structure))\n paragraph3',
|
|
16
|
+
'(document: (metadata: title author date) (content: paragraph1 (paragraph2: text (with: nested structure)) paragraph3))',
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
'image:\n file: .gitpod.Dockerfile\n context: ./docker-content',
|
|
20
|
+
'(image: (file: .gitpod.Dockerfile) (context: ./docker-content))',
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
'level1:\n level2:\n level3a\n level3b\n level2b',
|
|
24
|
+
'(level1: (level2: level3a level3b) level2b)',
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
'root:\n child1\n child2\n grandchild',
|
|
28
|
+
'(root: child1 (child2 grandchild))',
|
|
29
|
+
],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
test('indented ID retains nested values', () => {
|
|
33
|
+
for (const [source, expected] of cases) {
|
|
34
|
+
const parser = new Parser();
|
|
35
|
+
expect(formatLinks(parser.parse(source))).toBe(expected);
|
|
36
|
+
expect(parser.parse(source)).toEqual(parser.parse(expected));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -123,7 +123,6 @@ test('Indented ID with deeper nesting', () => {
|
|
|
123
123
|
child2
|
|
124
124
|
grandchild`;
|
|
125
125
|
|
|
126
|
-
// This should work but the grandchild will be processed as a separate nested structure
|
|
127
126
|
const result = parser.parse(input);
|
|
128
127
|
expect(result.length).toBeGreaterThan(0);
|
|
129
128
|
|
|
@@ -131,6 +130,7 @@ test('Indented ID with deeper nesting', () => {
|
|
|
131
130
|
const rootLink = result[0];
|
|
132
131
|
expect(rootLink.id).toBe('root');
|
|
133
132
|
expect(rootLink.values.length).toBe(2);
|
|
133
|
+
expect(formatLinks(result)).toBe('(root: child1 (child2 grandchild))');
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
test('Empty indented ID should work', () => {
|
|
@@ -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
|
|