links-notation 0.21.3 → 0.22.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/README.md +19 -3
- package/README.ru.md +17 -0
- package/dist/index.js +182 -111
- package/index.d.ts +15 -1
- package/package.json +1 -1
- package/src/ParseError.js +8 -1
- package/src/Parser.js +12 -3
- package/src/grammar.pegjs +40 -3
- package/src/index.js +1 -1
- package/src/parser-generated.js +179 -108
- package/tests/NestingLimit.test.js +132 -0
- package/tests/support/nestingDepthWorker.js +3 -1
package/index.d.ts
CHANGED
|
@@ -106,6 +106,12 @@ export class Link {
|
|
|
106
106
|
/**
|
|
107
107
|
* Parser options for configuring the parser behavior
|
|
108
108
|
*/
|
|
109
|
+
/**
|
|
110
|
+
* How deep links may nest when nothing says otherwise (64). The same limit
|
|
111
|
+
* applies in every Links Notation implementation.
|
|
112
|
+
*/
|
|
113
|
+
export const DEFAULT_MAX_DEPTH: number;
|
|
114
|
+
|
|
109
115
|
export interface ParserOptions {
|
|
110
116
|
/**
|
|
111
117
|
* Maximum input size in bytes (default: 10MB)
|
|
@@ -113,7 +119,9 @@ export interface ParserOptions {
|
|
|
113
119
|
maxInputSize?: number;
|
|
114
120
|
|
|
115
121
|
/**
|
|
116
|
-
*
|
|
122
|
+
* How deep links may nest: every parenthesized group and every indentation
|
|
123
|
+
* level is one level. A document nested deeper is refused with a ParseError
|
|
124
|
+
* whose maxDepth is set (default: 64)
|
|
117
125
|
*/
|
|
118
126
|
maxDepth?: number;
|
|
119
127
|
|
|
@@ -293,6 +301,12 @@ export class ParseError extends Error {
|
|
|
293
301
|
* The position the generated parser reported
|
|
294
302
|
*/
|
|
295
303
|
location: ParseErrorLocation;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The deepest nesting allowed, when the document is nested deeper than
|
|
307
|
+
* that; null for any other error
|
|
308
|
+
*/
|
|
309
|
+
maxDepth: number | null;
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
/**
|
package/package.json
CHANGED
package/src/ParseError.js
CHANGED
|
@@ -12,6 +12,9 @@ const ELLIPSIS = '...';
|
|
|
12
12
|
* message it writes says what it expected without saying where. This error puts
|
|
13
13
|
* the line and the column in the message as well, and quotes the offending line
|
|
14
14
|
* with a caret under it, the way the Rust and C# ports do.
|
|
15
|
+
*
|
|
16
|
+
* A document nested deeper than the parser's maxDepth is refused with this
|
|
17
|
+
* error too; then maxDepth says how deep the nesting may go.
|
|
15
18
|
*/
|
|
16
19
|
export class ParseError extends Error {
|
|
17
20
|
/**
|
|
@@ -24,7 +27,9 @@ export class ParseError extends Error {
|
|
|
24
27
|
const lineText = lineAt(input, start.offset);
|
|
25
28
|
const summary = `line ${start.line}, column ${start.column}: ${error.message}`;
|
|
26
29
|
const snippet = quote(start.line, lineText, start.column);
|
|
27
|
-
|
|
30
|
+
const maxDepth = error?.maxDepth ?? null;
|
|
31
|
+
const kind = maxDepth === null ? 'Syntax error' : 'Nesting too deep';
|
|
32
|
+
super(`${kind} at ${summary}\n${snippet}`);
|
|
28
33
|
|
|
29
34
|
this.name = 'ParseError';
|
|
30
35
|
/** @type {Error} The error the generated parser threw */
|
|
@@ -43,6 +48,8 @@ export class ParseError extends Error {
|
|
|
43
48
|
this.lineText = lineText;
|
|
44
49
|
/** @type {string} The offending line with a caret under the offending column */
|
|
45
50
|
this.snippet = snippet;
|
|
51
|
+
/** @type {number|null} The deepest nesting allowed, when the document is nested deeper; null for any other error */
|
|
52
|
+
this.maxDepth = maxDepth;
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
|
package/src/Parser.js
CHANGED
|
@@ -3,17 +3,24 @@ import { ParseError } from './ParseError.js';
|
|
|
3
3
|
import { stripComments } from './comments.js';
|
|
4
4
|
import * as parserModule from './parser-generated.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* How deep links may nest when nothing says otherwise. The parser recurses once
|
|
8
|
+
* per level, and the same limit applies in every Links Notation implementation,
|
|
9
|
+
* so a document one of them reads is not too deep for another.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MAX_DEPTH = 64;
|
|
12
|
+
|
|
6
13
|
export class Parser {
|
|
7
14
|
/**
|
|
8
15
|
* Create a new Parser instance
|
|
9
16
|
* @param {Object} options - Parser options
|
|
10
17
|
* @param {number} options.maxInputSize - Maximum input size in bytes (default: 10MB)
|
|
11
|
-
* @param {number} options.maxDepth -
|
|
18
|
+
* @param {number} options.maxDepth - How deep links may nest: every parenthesized group and every indentation level is one level (default: 64)
|
|
12
19
|
* @param {boolean} options.comments - If false, read `#` as an ordinary character instead of the start of a comment (default: true)
|
|
13
20
|
*/
|
|
14
21
|
constructor(options = {}) {
|
|
15
22
|
this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024; // 10MB default
|
|
16
|
-
this.maxDepth = options.maxDepth
|
|
23
|
+
this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
17
24
|
this.comments = options.comments ?? true;
|
|
18
25
|
}
|
|
19
26
|
|
|
@@ -40,7 +47,9 @@ export class Parser {
|
|
|
40
47
|
const prepared = this.comments ? stripComments(input) : input;
|
|
41
48
|
|
|
42
49
|
try {
|
|
43
|
-
const rawResult = parserModule.parse(prepared
|
|
50
|
+
const rawResult = parserModule.parse(prepared, {
|
|
51
|
+
maxDepth: this.maxDepth,
|
|
52
|
+
});
|
|
44
53
|
return this.transformResult(rawResult);
|
|
45
54
|
} catch (error) {
|
|
46
55
|
// A syntax error knows where it stopped; anything else is passed on with
|
package/src/grammar.pegjs
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
// group opens a nested context that starts fresh at indentation level zero,
|
|
10
10
|
// so line breaks and indentation mean the same thing at every depth.
|
|
11
11
|
let contextStack = [];
|
|
12
|
+
// How deeply the current context is nested: the enclosing groups and the
|
|
13
|
+
// indentation levels around them. Every level is a level of recursion, so
|
|
14
|
+
// links nested deeper than maxDepth are refused rather than recursed into
|
|
15
|
+
// until the stack overflows.
|
|
16
|
+
let contextDepth = 0;
|
|
17
|
+
const maxDepth = options.maxDepth ?? Infinity;
|
|
12
18
|
// Lines already found unreadable, keyed by where they start and whether they
|
|
13
19
|
// are inside a parenthesised group. Reading a line depends on nothing else: a
|
|
14
20
|
// group starts a fresh indentation context, and indentation only decides
|
|
@@ -22,6 +28,7 @@
|
|
|
22
28
|
indentationStack = [0];
|
|
23
29
|
baseIndentation = null;
|
|
24
30
|
contextStack = [];
|
|
31
|
+
contextDepth = 0;
|
|
25
32
|
unreadableLines = new Set();
|
|
26
33
|
return true;
|
|
27
34
|
}
|
|
@@ -31,7 +38,8 @@
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
function enterNestedContext() {
|
|
34
|
-
contextStack.push({ indentationStack, baseIndentation });
|
|
41
|
+
contextStack.push({ indentationStack, baseIndentation, contextDepth });
|
|
42
|
+
contextDepth = depth() + 1;
|
|
35
43
|
indentationStack = [0];
|
|
36
44
|
baseIndentation = null;
|
|
37
45
|
return true;
|
|
@@ -42,10 +50,32 @@
|
|
|
42
50
|
if (saved) {
|
|
43
51
|
indentationStack = saved.indentationStack;
|
|
44
52
|
baseIndentation = saved.baseIndentation;
|
|
53
|
+
contextDepth = saved.contextDepth;
|
|
45
54
|
}
|
|
46
55
|
return true;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
// The nesting depth of the line being read: every enclosing group and every
|
|
59
|
+
// indentation level is one level, and the lines of a document are at level 0.
|
|
60
|
+
function depth() {
|
|
61
|
+
return contextDepth + indentationStack.length - 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Refuses links at the given depth when it is deeper than maxDepth. The error
|
|
65
|
+
// is thrown rather than returned as a failed match, so no alternative is
|
|
66
|
+
// tried in its place, and says where the nesting got too deep.
|
|
67
|
+
function checkDepth(levels, where) {
|
|
68
|
+
if (levels <= maxDepth) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
error(`nesting depth exceeds the maximum of ${maxDepth}`, where);
|
|
73
|
+
} catch (tooDeep) {
|
|
74
|
+
tooDeep.maxDepth = maxDepth;
|
|
75
|
+
throw tooDeep;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
function isInsideNestedContext() {
|
|
50
80
|
return contextStack.length > 0;
|
|
51
81
|
}
|
|
@@ -115,8 +145,11 @@ line = CHECK_INDENTATION l:element { return l; }
|
|
|
115
145
|
|
|
116
146
|
// A line is read once, whether or not indented children follow it: reading it
|
|
117
147
|
// again after looking for children doubled the work at every level of nesting.
|
|
148
|
+
// Only a line that parsed counts towards the depth, so trailing spaces indented
|
|
149
|
+
// past the limit are still read as the whitespace they are.
|
|
118
150
|
element = &{ return !unreadableLines.has(lineKey(offset())); }
|
|
119
|
-
e:anyLink
|
|
151
|
+
start:HERE e:anyLink &{ return checkDepth(depth(), start); }
|
|
152
|
+
saved:SAVE_INDENTATION l:(PUSH_INDENTATION @links)? {
|
|
120
153
|
if (l === null) {
|
|
121
154
|
// No child line followed the indentation. Forget what looking for one
|
|
122
155
|
// pushed, so the next line is compared with this line's indentation.
|
|
@@ -155,7 +188,8 @@ singleLineAnyLink = fl:singleLineLink eol { return fl; }
|
|
|
155
188
|
// A parenthesised group opens a nested context that follows exactly the same
|
|
156
189
|
// rules as the root of the document: line breaks separate links and
|
|
157
190
|
// indentation nests them, starting fresh at indentation level zero.
|
|
158
|
-
nestedGroup = "("
|
|
191
|
+
nestedGroup = start:HERE "(" &{ return checkDepth(depth() + 1, start); }
|
|
192
|
+
ENTER_NESTED_CONTEXT body:nestedGroupBody {
|
|
159
193
|
exitNestedContext();
|
|
160
194
|
return body;
|
|
161
195
|
}
|
|
@@ -168,6 +202,9 @@ nestedGroupBody = skipEmptyLines l:links _ ")" { return { nested: l }; }
|
|
|
168
202
|
|
|
169
203
|
ENTER_NESTED_CONTEXT = &{ return enterNestedContext(); }
|
|
170
204
|
|
|
205
|
+
// Where the parser is, to say where nesting got too deep.
|
|
206
|
+
HERE = "" { return location(); }
|
|
207
|
+
|
|
171
208
|
singleLineValueAndWhitespace = __ value:referenceOrLink { return value; }
|
|
172
209
|
|
|
173
210
|
singleLineValues = list:singleLineValueAndWhitespace+ { return list; }
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Link, formatLinks } from './Link.js';
|
|
2
2
|
export { LinksGroup } from './LinksGroup.js';
|
|
3
|
-
export { Parser } from './Parser.js';
|
|
3
|
+
export { Parser, DEFAULT_MAX_DEPTH } from './Parser.js';
|
|
4
4
|
export { StreamParseError, StreamParser } from './StreamParser.js';
|
|
5
5
|
export { ParseError } from './ParseError.js';
|
|
6
6
|
export { FormatConfig } from './FormatConfig.js';
|