links-notation 0.21.2 → 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 +383 -135
- 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 +85 -5
- package/src/index.js +1 -1
- package/src/parser-generated.js +382 -127
- package/tests/NestingDepth.test.js +124 -0
- package/tests/NestingLimit.test.js +132 -0
- package/tests/support/nestingDepthWorker.js +17 -0
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,16 +9,37 @@
|
|
|
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;
|
|
18
|
+
// Lines already found unreadable, keyed by where they start and whether they
|
|
19
|
+
// are inside a parenthesised group. Reading a line depends on nothing else: a
|
|
20
|
+
// group starts a fresh indentation context, and indentation only decides
|
|
21
|
+
// which lines become children, so a line that could not be read once never
|
|
22
|
+
// can be. A line that does not parse as the first child of the line above it
|
|
23
|
+
// is tried again as a sibling at every enclosing indentation level, and
|
|
24
|
+
// without this each of those attempts would read the whole line again.
|
|
25
|
+
let unreadableLines = new Set();
|
|
12
26
|
|
|
13
27
|
function resetState() {
|
|
14
28
|
indentationStack = [0];
|
|
15
29
|
baseIndentation = null;
|
|
16
30
|
contextStack = [];
|
|
31
|
+
contextDepth = 0;
|
|
32
|
+
unreadableLines = new Set();
|
|
17
33
|
return true;
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
function lineKey(position) {
|
|
37
|
+
return contextStack.length > 0 ? -1 - position : position;
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
function enterNestedContext() {
|
|
21
|
-
contextStack.push({ indentationStack, baseIndentation });
|
|
41
|
+
contextStack.push({ indentationStack, baseIndentation, contextDepth });
|
|
42
|
+
contextDepth = depth() + 1;
|
|
22
43
|
indentationStack = [0];
|
|
23
44
|
baseIndentation = null;
|
|
24
45
|
return true;
|
|
@@ -29,10 +50,32 @@
|
|
|
29
50
|
if (saved) {
|
|
30
51
|
indentationStack = saved.indentationStack;
|
|
31
52
|
baseIndentation = saved.baseIndentation;
|
|
53
|
+
contextDepth = saved.contextDepth;
|
|
32
54
|
}
|
|
33
55
|
return true;
|
|
34
56
|
}
|
|
35
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
|
+
|
|
36
79
|
function isInsideNestedContext() {
|
|
37
80
|
return contextStack.length > 0;
|
|
38
81
|
}
|
|
@@ -100,14 +143,42 @@ firstLine = SET_BASE_INDENTATION l:element { return l; }
|
|
|
100
143
|
|
|
101
144
|
line = CHECK_INDENTATION l:element { return l; }
|
|
102
145
|
|
|
103
|
-
|
|
146
|
+
// A line is read once, whether or not indented children follow it: reading it
|
|
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.
|
|
150
|
+
element = &{ return !unreadableLines.has(lineKey(offset())); }
|
|
151
|
+
start:HERE e:anyLink &{ return checkDepth(depth(), start); }
|
|
152
|
+
saved:SAVE_INDENTATION l:(PUSH_INDENTATION @links)? {
|
|
153
|
+
if (l === null) {
|
|
154
|
+
// No child line followed the indentation. Forget what looking for one
|
|
155
|
+
// pushed, so the next line is compared with this line's indentation.
|
|
156
|
+
indentationStack = saved;
|
|
157
|
+
return e;
|
|
158
|
+
}
|
|
104
159
|
return Object.assign({}, e, { children: l });
|
|
105
160
|
}
|
|
106
|
-
|
|
161
|
+
// Remember that this line could not be read. The predicate always fails, so
|
|
162
|
+
// the "." is never reached; it only tells Peggy that this alternative cannot
|
|
163
|
+
// succeed without consuming input.
|
|
164
|
+
/ &{ unreadableLines.add(lineKey(offset())); return false; } .
|
|
107
165
|
|
|
108
166
|
referenceOrLink = l:multiLineAnyLink { return l; } / i:reference { return { id: i }; }
|
|
109
167
|
|
|
110
|
-
|
|
168
|
+
// A line that starts with a parenthesised group reads that group once and then
|
|
169
|
+
// branches on what follows it: the end of the line makes the group the whole
|
|
170
|
+
// link, and more values make it the first value of a value link. Neither an
|
|
171
|
+
// indented ID nor a single-line link can start with a parenthesis, so nothing
|
|
172
|
+
// else is tried for such a line; trying them read the group again, doubling
|
|
173
|
+
// the work at every level of nesting.
|
|
174
|
+
anyLink = &"(" @groupLink / !"(" @(indentedIdLink / singleLineAnyLink)
|
|
175
|
+
|
|
176
|
+
groupLink = g:multiLineAnyLink rest:groupLinkRest {
|
|
177
|
+
return rest === null ? g : { values: [g].concat(rest) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
groupLinkRest = eol { return null; }
|
|
181
|
+
/ @singleLineValueAndWhitespace* eol
|
|
111
182
|
|
|
112
183
|
multiLineAnyLink = nestedGroup
|
|
113
184
|
|
|
@@ -117,16 +188,23 @@ singleLineAnyLink = fl:singleLineLink eol { return fl; }
|
|
|
117
188
|
// A parenthesised group opens a nested context that follows exactly the same
|
|
118
189
|
// rules as the root of the document: line breaks separate links and
|
|
119
190
|
// indentation nests them, starting fresh at indentation level zero.
|
|
120
|
-
nestedGroup = "("
|
|
191
|
+
nestedGroup = start:HERE "(" &{ return checkDepth(depth() + 1, start); }
|
|
192
|
+
ENTER_NESTED_CONTEXT body:nestedGroupBody {
|
|
121
193
|
exitNestedContext();
|
|
122
194
|
return body;
|
|
123
195
|
}
|
|
196
|
+
// The group was opened but its body did not parse: restore the context it
|
|
197
|
+
// was opened in before failing, just as a parsed group does.
|
|
198
|
+
/ "(" &{ exitNestedContext(); return false; }
|
|
124
199
|
|
|
125
200
|
nestedGroupBody = skipEmptyLines l:links _ ")" { return { nested: l }; }
|
|
126
201
|
/ _ ")" { return { nested: [] }; }
|
|
127
202
|
|
|
128
203
|
ENTER_NESTED_CONTEXT = &{ return enterNestedContext(); }
|
|
129
204
|
|
|
205
|
+
// Where the parser is, to say where nesting got too deep.
|
|
206
|
+
HERE = "" { return location(); }
|
|
207
|
+
|
|
130
208
|
singleLineValueAndWhitespace = __ value:referenceOrLink { return value; }
|
|
131
209
|
|
|
132
210
|
singleLineValues = list:singleLineValueAndWhitespace+ { return list; }
|
|
@@ -193,6 +271,8 @@ backtickQuotedUniversal = &'`' &{
|
|
|
193
271
|
consumeBacktick = c:. cs:consumeBacktickMore* { return [c].concat(cs).join(''); }
|
|
194
272
|
consumeBacktickMore = &{ return parsedLength > 1 && (parsedLength--, true); } c:. { return c; }
|
|
195
273
|
|
|
274
|
+
SAVE_INDENTATION = "" { return indentationStack.slice(); }
|
|
275
|
+
|
|
196
276
|
SET_BASE_INDENTATION = spaces:" "* { setBaseIndentation(spaces); }
|
|
197
277
|
|
|
198
278
|
PUSH_INDENTATION = spaces:" "* &{ return normalizeIndentation(spaces) > getCurrentIndentation(); } { pushIndentation(spaces); }
|
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';
|