links-notation 0.18.0 → 0.20.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 +91 -4
- package/README.ru.md +60 -4
- package/dist/index.js +476 -21
- package/index.d.ts +95 -0
- package/package.json +1 -1
- package/src/Link.js +4 -0
- package/src/Parser.js +8 -1
- package/src/StreamParser.js +284 -0
- package/src/comments.js +164 -0
- package/src/grammar.pegjs +7 -1
- package/src/index.js +2 -0
- package/src/parser-generated.js +152 -18
- package/tests/Comments.test.js +160 -0
- package/tests/ParseErrorPosition.test.js +15 -13
- package/tests/StreamParser.test.js +165 -0
package/src/Parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Link } from './Link.js';
|
|
2
2
|
import { ParseError } from './ParseError.js';
|
|
3
|
+
import { stripComments } from './comments.js';
|
|
3
4
|
import * as parserModule from './parser-generated.js';
|
|
4
5
|
|
|
5
6
|
export class Parser {
|
|
@@ -8,10 +9,12 @@ export class Parser {
|
|
|
8
9
|
* @param {Object} options - Parser options
|
|
9
10
|
* @param {number} options.maxInputSize - Maximum input size in bytes (default: 10MB)
|
|
10
11
|
* @param {number} options.maxDepth - Maximum nesting depth (default: 1000)
|
|
12
|
+
* @param {boolean} options.comments - If false, read `#` as an ordinary character instead of the start of a comment (default: true)
|
|
11
13
|
*/
|
|
12
14
|
constructor(options = {}) {
|
|
13
15
|
this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024; // 10MB default
|
|
14
16
|
this.maxDepth = options.maxDepth || 1000;
|
|
17
|
+
this.comments = options.comments ?? true;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -32,8 +35,12 @@ export class Parser {
|
|
|
32
35
|
);
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
// Comments are blanked rather than removed, so a position reported for the
|
|
39
|
+
// prepared document is the same position in the document the caller wrote.
|
|
40
|
+
const prepared = this.comments ? stripComments(input) : input;
|
|
41
|
+
|
|
35
42
|
try {
|
|
36
|
-
const rawResult = parserModule.parse(
|
|
43
|
+
const rawResult = parserModule.parse(prepared);
|
|
37
44
|
return this.transformResult(rawResult);
|
|
38
45
|
} catch (error) {
|
|
39
46
|
// A syntax error knows where it stopped; anything else is passed on with
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Parser } from './Parser.js';
|
|
3
|
+
import { quotedReferenceEnd } from './comments.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An error raised while finishing a streamed document.
|
|
9
|
+
*
|
|
10
|
+
* The canonical parser reports positions relative to the currently buffered
|
|
11
|
+
* record. This wrapper translates them back to the complete stream.
|
|
12
|
+
*/
|
|
13
|
+
export class StreamParseError extends Error {
|
|
14
|
+
constructor(error, startOffset, startLine) {
|
|
15
|
+
const localOffset = error?.offset ?? 0;
|
|
16
|
+
const localLine = error?.line ?? 1;
|
|
17
|
+
const column = error?.column ?? 1;
|
|
18
|
+
const line = startLine + localLine - 1;
|
|
19
|
+
super(
|
|
20
|
+
`Stream parse error at line ${line}, column ${column}: ${error.message}`
|
|
21
|
+
);
|
|
22
|
+
this.name = 'StreamParseError';
|
|
23
|
+
this.cause = error;
|
|
24
|
+
this.offset = startOffset + localOffset;
|
|
25
|
+
this.line = line;
|
|
26
|
+
this.column = column;
|
|
27
|
+
this.found = error?.found ?? null;
|
|
28
|
+
this.lineText = error?.lineText ?? '';
|
|
29
|
+
this.snippet = error?.snippet ?? '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Incrementally parses complete top-level Links Notation records.
|
|
35
|
+
*
|
|
36
|
+
* A line can acquire indented children, so its terminating newline alone does
|
|
37
|
+
* not prove that the record is complete. The parser commits a buffered record
|
|
38
|
+
* as soon as the first non-indented character of the next record arrives. It
|
|
39
|
+
* uses the regular Parser as the source of truth, which keeps quoted strings,
|
|
40
|
+
* nested contexts, comments, and future grammar changes in one place.
|
|
41
|
+
*/
|
|
42
|
+
export class StreamParser extends EventEmitter {
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
super();
|
|
45
|
+
this.parser = options.parser ?? new Parser(options);
|
|
46
|
+
this.comments = options.comments ?? this.parser.comments ?? true;
|
|
47
|
+
this.maxBufferSize =
|
|
48
|
+
options.maxBufferSize ?? options.maxInputSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
49
|
+
this.collect = options.collect ?? true;
|
|
50
|
+
this.reset();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Feed a string chunk and return the links made complete by that chunk. */
|
|
54
|
+
write(chunk) {
|
|
55
|
+
if (typeof chunk !== 'string') {
|
|
56
|
+
return this._fail(new TypeError('Input must be a string'));
|
|
57
|
+
}
|
|
58
|
+
if (this.ended) {
|
|
59
|
+
return this._fail(new Error('Cannot write after end()'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const emitted = [];
|
|
63
|
+
for (let index = 0; index < chunk.length; index++) {
|
|
64
|
+
const character = chunk[index];
|
|
65
|
+
this.currentLine += character;
|
|
66
|
+
this.offset += 1;
|
|
67
|
+
|
|
68
|
+
if (character === '\n') {
|
|
69
|
+
this.buffer += this.currentLine;
|
|
70
|
+
this.currentLine = '';
|
|
71
|
+
this.lineClassified = false;
|
|
72
|
+
this.line += 1;
|
|
73
|
+
this.column = 1;
|
|
74
|
+
} else {
|
|
75
|
+
if (
|
|
76
|
+
!this.lineClassified &&
|
|
77
|
+
character !== ' ' &&
|
|
78
|
+
character !== '\t' &&
|
|
79
|
+
character !== '\r'
|
|
80
|
+
) {
|
|
81
|
+
this.lineClassified = true;
|
|
82
|
+
const isComment = this.comments && character === '#';
|
|
83
|
+
if (!isComment) {
|
|
84
|
+
const indentation = leadingSpaces(this.currentLine);
|
|
85
|
+
this._startContentLine(indentation, emitted);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.column += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.buffer.length + this.currentLine.length > this.maxBufferSize) {
|
|
92
|
+
return this._fail(
|
|
93
|
+
new RangeError(
|
|
94
|
+
`Buffered record exceeds maximum size of ${this.maxBufferSize} characters`
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return emitted;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Finish the stream. With collection enabled (the default), returns every
|
|
104
|
+
* undrained link; otherwise returns only links completed by this call.
|
|
105
|
+
*/
|
|
106
|
+
end(chunk = '') {
|
|
107
|
+
const emitted = chunk === '' ? [] : this.write(chunk);
|
|
108
|
+
if (this.ended) {
|
|
109
|
+
return this.collect ? [...this.links] : emitted;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const document = this.buffer + this.currentLine;
|
|
113
|
+
if (document.length > 0) {
|
|
114
|
+
let links;
|
|
115
|
+
try {
|
|
116
|
+
links = this.parser.parse(document);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const streamError = new StreamParseError(
|
|
119
|
+
error,
|
|
120
|
+
this.segmentOffset,
|
|
121
|
+
this.segmentLine
|
|
122
|
+
);
|
|
123
|
+
return this._fail(streamError);
|
|
124
|
+
}
|
|
125
|
+
this._publish(links, emitted);
|
|
126
|
+
this._advanceSegment(document);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.buffer = '';
|
|
130
|
+
this.currentLine = '';
|
|
131
|
+
this.baseIndentation = null;
|
|
132
|
+
this.ended = true;
|
|
133
|
+
const result = this.collect ? [...this.links] : emitted;
|
|
134
|
+
this.emit('end', result);
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Return and forget links retained since the previous drain. */
|
|
139
|
+
drain() {
|
|
140
|
+
const links = this.links;
|
|
141
|
+
this.links = [];
|
|
142
|
+
return links;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Reuse this parser for a new stream while keeping listeners and options. */
|
|
146
|
+
reset() {
|
|
147
|
+
this.buffer = '';
|
|
148
|
+
this.currentLine = '';
|
|
149
|
+
this.baseIndentation = null;
|
|
150
|
+
this.lineClassified = false;
|
|
151
|
+
this.links = [];
|
|
152
|
+
this.offset = 0;
|
|
153
|
+
this.line = 1;
|
|
154
|
+
this.column = 1;
|
|
155
|
+
this.segmentOffset = 0;
|
|
156
|
+
this.segmentLine = 1;
|
|
157
|
+
this.ended = false;
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Position immediately after the last character written. */
|
|
162
|
+
position() {
|
|
163
|
+
return {
|
|
164
|
+
offset: this.offset,
|
|
165
|
+
line: this.line,
|
|
166
|
+
column: this.column,
|
|
167
|
+
buffered: this.buffer.length + this.currentLine.length,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Lazily parse a synchronous iterable of chunks. */
|
|
172
|
+
static *parse(chunks, options = {}) {
|
|
173
|
+
const parser = new StreamParser({ ...options, collect: false });
|
|
174
|
+
for (const chunk of chunks) {
|
|
175
|
+
yield* parser.write(chunk);
|
|
176
|
+
}
|
|
177
|
+
yield* parser.end();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Lazily parse an async iterable of chunks. */
|
|
181
|
+
static async *parseAsync(chunks, options = {}) {
|
|
182
|
+
const parser = new StreamParser({ ...options, collect: false });
|
|
183
|
+
for await (const chunk of chunks) {
|
|
184
|
+
yield* parser.write(chunk);
|
|
185
|
+
}
|
|
186
|
+
yield* parser.end();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_startContentLine(indentation, emitted) {
|
|
190
|
+
if (
|
|
191
|
+
this.buffer.length > 0 &&
|
|
192
|
+
this.baseIndentation !== null &&
|
|
193
|
+
indentation <= this.baseIndentation &&
|
|
194
|
+
structurallyComplete(this.buffer, this.comments)
|
|
195
|
+
) {
|
|
196
|
+
let links;
|
|
197
|
+
try {
|
|
198
|
+
links = this.parser.parse(this.buffer);
|
|
199
|
+
} catch {
|
|
200
|
+
links = null;
|
|
201
|
+
}
|
|
202
|
+
if (links !== null) {
|
|
203
|
+
this._publish(links, emitted);
|
|
204
|
+
this._advanceSegment(this.buffer);
|
|
205
|
+
this.buffer = '';
|
|
206
|
+
this.baseIndentation = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.baseIndentation === null) {
|
|
211
|
+
this.baseIndentation = indentation;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_publish(links, emitted) {
|
|
216
|
+
for (const link of links) {
|
|
217
|
+
emitted.push(link);
|
|
218
|
+
if (this.collect) this.links.push(link);
|
|
219
|
+
this.emit('link', link);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_advanceSegment(document) {
|
|
224
|
+
this.segmentOffset += document.length;
|
|
225
|
+
this.segmentLine += countNewlines(document);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_fail(error) {
|
|
229
|
+
this.emit('error', error);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function leadingSpaces(line) {
|
|
235
|
+
let indentation = 0;
|
|
236
|
+
while (line[indentation] === ' ') indentation += 1;
|
|
237
|
+
return indentation;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function countNewlines(value) {
|
|
241
|
+
let count = 0;
|
|
242
|
+
for (const character of value) {
|
|
243
|
+
if (character === '\n') count += 1;
|
|
244
|
+
}
|
|
245
|
+
return count;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function structurallyComplete(document, comments) {
|
|
249
|
+
const quotes = ['"', "'", '`'];
|
|
250
|
+
const beforeReference = [' ', '\t', '\n', '\r', '(', ':'];
|
|
251
|
+
const beforeComment = [' ', '\t', '\n', '\r'];
|
|
252
|
+
let depth = 0;
|
|
253
|
+
|
|
254
|
+
for (let position = 0; position < document.length; position++) {
|
|
255
|
+
const character = document[position];
|
|
256
|
+
const previous = position === 0 ? null : document[position - 1];
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
quotes.includes(character) &&
|
|
260
|
+
(previous === null || beforeReference.includes(previous))
|
|
261
|
+
) {
|
|
262
|
+
const end = quotedReferenceEnd(document, position);
|
|
263
|
+
if (end === null) return false;
|
|
264
|
+
position = end - 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
comments &&
|
|
270
|
+
character === '#' &&
|
|
271
|
+
(previous === null || beforeComment.includes(previous))
|
|
272
|
+
) {
|
|
273
|
+
const newline = document.indexOf('\n', position);
|
|
274
|
+
if (newline === -1) return true;
|
|
275
|
+
position = newline;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (character === '(') depth += 1;
|
|
280
|
+
if (character === ')') depth -= 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return depth === 0;
|
|
284
|
+
}
|
package/src/comments.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comments, and the rule that decides where one starts.
|
|
3
|
+
*
|
|
4
|
+
* `#` starts a comment when it opens a token: at the start of the document or
|
|
5
|
+
* after a space, a tab or a line break. A `#` written inside a token
|
|
6
|
+
* (`issue#1047`) or inside a delimited reference (`"# not a comment"`) is an
|
|
7
|
+
* ordinary character.
|
|
8
|
+
*
|
|
9
|
+
* {@link stripComments} replaces every character of every comment with a space
|
|
10
|
+
* rather than removing it, so every later character keeps the offset it had in
|
|
11
|
+
* the document the caller wrote, and a parse error still points at the position
|
|
12
|
+
* the reader can see.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The character that starts a comment. */
|
|
16
|
+
export const COMMENT = '#';
|
|
17
|
+
|
|
18
|
+
/** The delimiters a reference can be written between. */
|
|
19
|
+
const QUOTES = ['"', "'", '`'];
|
|
20
|
+
|
|
21
|
+
/** What a delimited reference may start after. */
|
|
22
|
+
const BEFORE_REFERENCE = [' ', '\t', '\n', '\r', '(', ':'];
|
|
23
|
+
|
|
24
|
+
/** What a comment may start after. */
|
|
25
|
+
const BEFORE_COMMENT = [' ', '\t', '\n', '\r'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The document with every comment blanked out.
|
|
29
|
+
* @param {string} document - The document to read
|
|
30
|
+
* @returns {string} The document, of the same length, without its comments
|
|
31
|
+
* @example
|
|
32
|
+
* stripComments('a: b # why\n'); // 'a: b \n'
|
|
33
|
+
* stripComments('"# kept"\n'); // '"# kept"\n'
|
|
34
|
+
* stripComments('issue#1047\n'); // 'issue#1047\n'
|
|
35
|
+
*/
|
|
36
|
+
export function stripComments(document) {
|
|
37
|
+
// Split into UTF-16 units, the units the positions here and the positions a
|
|
38
|
+
// parse error reports are both counted in.
|
|
39
|
+
let blanked = null;
|
|
40
|
+
let position = 0;
|
|
41
|
+
|
|
42
|
+
while (position < document.length) {
|
|
43
|
+
const character = document[position];
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
QUOTES.includes(character) &&
|
|
47
|
+
follows(document, position, BEFORE_REFERENCE)
|
|
48
|
+
) {
|
|
49
|
+
const end = quotedReferenceEnd(document, position);
|
|
50
|
+
position = end === null ? position + 1 : end;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (character === COMMENT && follows(document, position, BEFORE_COMMENT)) {
|
|
55
|
+
blanked = blanked ?? document.split('');
|
|
56
|
+
while (
|
|
57
|
+
position < document.length &&
|
|
58
|
+
document[position] !== '\n' &&
|
|
59
|
+
document[position] !== '\r'
|
|
60
|
+
) {
|
|
61
|
+
blanked[position] = ' ';
|
|
62
|
+
position++;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
position++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return blanked === null ? document : blanked.join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the character before the given position is one the caller allows,
|
|
75
|
+
* the start of the document counting as allowed.
|
|
76
|
+
* @param {string} document - The document being read
|
|
77
|
+
* @param {number} position - Position of the character being judged
|
|
78
|
+
* @param {string[]} allowed - The characters that may stand before it
|
|
79
|
+
* @returns {boolean} True when the position opens a token
|
|
80
|
+
*/
|
|
81
|
+
function follows(document, position, allowed) {
|
|
82
|
+
return position === 0 || allowed.includes(document[position - 1]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The position just past the delimited reference that starts at `start`, or
|
|
87
|
+
* `null` when nothing there opens one.
|
|
88
|
+
*
|
|
89
|
+
* This follows the same rules as the `parseQuotedStringAt` of the grammar: a
|
|
90
|
+
* run of N delimiters closes at the next run of exactly N, a run of 2N is that
|
|
91
|
+
* delimiter escaped, and an even run that encloses nothing substantive is the
|
|
92
|
+
* empty reference.
|
|
93
|
+
* @param {string} document - The document being read
|
|
94
|
+
* @param {number} start - Position of the opening delimiter
|
|
95
|
+
* @returns {number|null} The position just past the reference
|
|
96
|
+
*/
|
|
97
|
+
export function quotedReferenceEnd(document, start) {
|
|
98
|
+
const quote = document[start];
|
|
99
|
+
if (!QUOTES.includes(quote)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let position = start;
|
|
104
|
+
while (position < document.length && document[position] === quote) {
|
|
105
|
+
position++;
|
|
106
|
+
}
|
|
107
|
+
const count = position - start;
|
|
108
|
+
const isEvenRun = count % 2 === 0;
|
|
109
|
+
const emptyReference = isEvenRun ? start + count : null;
|
|
110
|
+
|
|
111
|
+
const closing = quote.repeat(count);
|
|
112
|
+
const escape = quote.repeat(count * 2);
|
|
113
|
+
let content = '';
|
|
114
|
+
|
|
115
|
+
while (position < document.length) {
|
|
116
|
+
if (document.startsWith(escape, position)) {
|
|
117
|
+
content += closing;
|
|
118
|
+
position += escape.length;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (document.startsWith(closing, position)) {
|
|
123
|
+
const afterClosing = position + count;
|
|
124
|
+
if (afterClosing >= document.length || document[afterClosing] !== quote) {
|
|
125
|
+
if (isEvenRun && !isSubstantiveBody(content)) {
|
|
126
|
+
return emptyReference;
|
|
127
|
+
}
|
|
128
|
+
return afterClosing;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
content += document[position];
|
|
133
|
+
position++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return emptyReference;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether a body written between an even run of delimiters carries something a
|
|
141
|
+
* pair of delimiters enclosing nothing cannot.
|
|
142
|
+
* @param {string} content - The body between the delimiters
|
|
143
|
+
* @returns {boolean} True when the n-quote reading is the one to take
|
|
144
|
+
*/
|
|
145
|
+
function isSubstantiveBody(content) {
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let hasVisible = false;
|
|
148
|
+
|
|
149
|
+
for (const character of content) {
|
|
150
|
+
if (character === '(') {
|
|
151
|
+
depth++;
|
|
152
|
+
} else if (character === ')') {
|
|
153
|
+
depth--;
|
|
154
|
+
if (depth < 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!/\s/.test(character)) {
|
|
159
|
+
hasVisible = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return hasVisible && depth === 0;
|
|
164
|
+
}
|
package/src/grammar.pegjs
CHANGED
|
@@ -264,7 +264,13 @@ PUSH_INDENTATION = spaces:" "* &{ return normalizeIndentation(spaces) > getCurre
|
|
|
264
264
|
|
|
265
265
|
CHECK_INDENTATION = spaces:" "* &{ return checkIndentation(spaces); }
|
|
266
266
|
|
|
267
|
-
eol = __ (
|
|
267
|
+
eol = __ (lineBreaks / eof / nestedGroupEnd)
|
|
268
|
+
|
|
269
|
+
// A line ends at the first line break, and the blank lines that follow it
|
|
270
|
+
// belong to that ending: a line holding nothing but spaces or tabs separates
|
|
271
|
+
// links exactly the way an empty line does. Blanking a comment leaves such a
|
|
272
|
+
// line behind, so this is also what lets a comment stand on a line of its own.
|
|
273
|
+
lineBreaks = [\r\n]+ ([ \t]+ [\r\n]+)*
|
|
268
274
|
|
|
269
275
|
// Inside a parenthesised group the closing parenthesis terminates a line the
|
|
270
276
|
// same way the end of the input does at the root.
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Link, formatLinks } from './Link.js';
|
|
2
2
|
export { LinksGroup } from './LinksGroup.js';
|
|
3
3
|
export { Parser } from './Parser.js';
|
|
4
|
+
export { StreamParseError, StreamParser } from './StreamParser.js';
|
|
4
5
|
export { ParseError } from './ParseError.js';
|
|
5
6
|
export { FormatConfig } from './FormatConfig.js';
|
|
6
7
|
export { FormatOptions } from './FormatOptions.js';
|
|
8
|
+
export { stripComments } from './comments.js';
|
package/src/parser-generated.js
CHANGED
|
@@ -1487,39 +1487,173 @@ function peg$parse(input, options) {
|
|
|
1487
1487
|
}
|
|
1488
1488
|
|
|
1489
1489
|
function peg$parseeol() {
|
|
1490
|
-
let s0, s1, s2
|
|
1490
|
+
let s0, s1, s2;
|
|
1491
1491
|
|
|
1492
1492
|
s0 = peg$currPos;
|
|
1493
1493
|
s1 = peg$parse__();
|
|
1494
|
-
s2 =
|
|
1495
|
-
|
|
1496
|
-
|
|
1494
|
+
s2 = peg$parselineBreaks();
|
|
1495
|
+
if (s2 === peg$FAILED) {
|
|
1496
|
+
s2 = peg$parseeof();
|
|
1497
|
+
if (s2 === peg$FAILED) {
|
|
1498
|
+
s2 = peg$parsenestedGroupEnd();
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (s2 !== peg$FAILED) {
|
|
1502
|
+
s1 = [s1, s2];
|
|
1503
|
+
s0 = s1;
|
|
1504
|
+
} else {
|
|
1505
|
+
peg$currPos = s0;
|
|
1506
|
+
s0 = peg$FAILED;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return s0;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function peg$parselineBreaks() {
|
|
1513
|
+
let s0, s1, s2, s3, s4, s5, s6;
|
|
1514
|
+
|
|
1515
|
+
s0 = peg$currPos;
|
|
1516
|
+
s1 = [];
|
|
1517
|
+
s2 = input.charAt(peg$currPos);
|
|
1518
|
+
if (peg$r1.test(s2)) {
|
|
1497
1519
|
peg$currPos++;
|
|
1498
1520
|
} else {
|
|
1499
|
-
|
|
1521
|
+
s2 = peg$FAILED;
|
|
1500
1522
|
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1501
1523
|
}
|
|
1502
|
-
if (
|
|
1503
|
-
while (
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (peg$r1.test(
|
|
1524
|
+
if (s2 !== peg$FAILED) {
|
|
1525
|
+
while (s2 !== peg$FAILED) {
|
|
1526
|
+
s1.push(s2);
|
|
1527
|
+
s2 = input.charAt(peg$currPos);
|
|
1528
|
+
if (peg$r1.test(s2)) {
|
|
1507
1529
|
peg$currPos++;
|
|
1508
1530
|
} else {
|
|
1509
|
-
|
|
1531
|
+
s2 = peg$FAILED;
|
|
1510
1532
|
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1511
1533
|
}
|
|
1512
1534
|
}
|
|
1513
1535
|
} else {
|
|
1514
|
-
|
|
1536
|
+
s1 = peg$FAILED;
|
|
1515
1537
|
}
|
|
1516
|
-
if (
|
|
1517
|
-
s2 =
|
|
1518
|
-
|
|
1519
|
-
|
|
1538
|
+
if (s1 !== peg$FAILED) {
|
|
1539
|
+
s2 = [];
|
|
1540
|
+
s3 = peg$currPos;
|
|
1541
|
+
s4 = [];
|
|
1542
|
+
s5 = input.charAt(peg$currPos);
|
|
1543
|
+
if (peg$r0.test(s5)) {
|
|
1544
|
+
peg$currPos++;
|
|
1545
|
+
} else {
|
|
1546
|
+
s5 = peg$FAILED;
|
|
1547
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1548
|
+
}
|
|
1549
|
+
if (s5 !== peg$FAILED) {
|
|
1550
|
+
while (s5 !== peg$FAILED) {
|
|
1551
|
+
s4.push(s5);
|
|
1552
|
+
s5 = input.charAt(peg$currPos);
|
|
1553
|
+
if (peg$r0.test(s5)) {
|
|
1554
|
+
peg$currPos++;
|
|
1555
|
+
} else {
|
|
1556
|
+
s5 = peg$FAILED;
|
|
1557
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
s4 = peg$FAILED;
|
|
1562
|
+
}
|
|
1563
|
+
if (s4 !== peg$FAILED) {
|
|
1564
|
+
s5 = [];
|
|
1565
|
+
s6 = input.charAt(peg$currPos);
|
|
1566
|
+
if (peg$r1.test(s6)) {
|
|
1567
|
+
peg$currPos++;
|
|
1568
|
+
} else {
|
|
1569
|
+
s6 = peg$FAILED;
|
|
1570
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1571
|
+
}
|
|
1572
|
+
if (s6 !== peg$FAILED) {
|
|
1573
|
+
while (s6 !== peg$FAILED) {
|
|
1574
|
+
s5.push(s6);
|
|
1575
|
+
s6 = input.charAt(peg$currPos);
|
|
1576
|
+
if (peg$r1.test(s6)) {
|
|
1577
|
+
peg$currPos++;
|
|
1578
|
+
} else {
|
|
1579
|
+
s6 = peg$FAILED;
|
|
1580
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
} else {
|
|
1584
|
+
s5 = peg$FAILED;
|
|
1585
|
+
}
|
|
1586
|
+
if (s5 !== peg$FAILED) {
|
|
1587
|
+
s4 = [s4, s5];
|
|
1588
|
+
s3 = s4;
|
|
1589
|
+
} else {
|
|
1590
|
+
peg$currPos = s3;
|
|
1591
|
+
s3 = peg$FAILED;
|
|
1592
|
+
}
|
|
1593
|
+
} else {
|
|
1594
|
+
peg$currPos = s3;
|
|
1595
|
+
s3 = peg$FAILED;
|
|
1596
|
+
}
|
|
1597
|
+
while (s3 !== peg$FAILED) {
|
|
1598
|
+
s2.push(s3);
|
|
1599
|
+
s3 = peg$currPos;
|
|
1600
|
+
s4 = [];
|
|
1601
|
+
s5 = input.charAt(peg$currPos);
|
|
1602
|
+
if (peg$r0.test(s5)) {
|
|
1603
|
+
peg$currPos++;
|
|
1604
|
+
} else {
|
|
1605
|
+
s5 = peg$FAILED;
|
|
1606
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1607
|
+
}
|
|
1608
|
+
if (s5 !== peg$FAILED) {
|
|
1609
|
+
while (s5 !== peg$FAILED) {
|
|
1610
|
+
s4.push(s5);
|
|
1611
|
+
s5 = input.charAt(peg$currPos);
|
|
1612
|
+
if (peg$r0.test(s5)) {
|
|
1613
|
+
peg$currPos++;
|
|
1614
|
+
} else {
|
|
1615
|
+
s5 = peg$FAILED;
|
|
1616
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
} else {
|
|
1620
|
+
s4 = peg$FAILED;
|
|
1621
|
+
}
|
|
1622
|
+
if (s4 !== peg$FAILED) {
|
|
1623
|
+
s5 = [];
|
|
1624
|
+
s6 = input.charAt(peg$currPos);
|
|
1625
|
+
if (peg$r1.test(s6)) {
|
|
1626
|
+
peg$currPos++;
|
|
1627
|
+
} else {
|
|
1628
|
+
s6 = peg$FAILED;
|
|
1629
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1630
|
+
}
|
|
1631
|
+
if (s6 !== peg$FAILED) {
|
|
1632
|
+
while (s6 !== peg$FAILED) {
|
|
1633
|
+
s5.push(s6);
|
|
1634
|
+
s6 = input.charAt(peg$currPos);
|
|
1635
|
+
if (peg$r1.test(s6)) {
|
|
1636
|
+
peg$currPos++;
|
|
1637
|
+
} else {
|
|
1638
|
+
s6 = peg$FAILED;
|
|
1639
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
} else {
|
|
1643
|
+
s5 = peg$FAILED;
|
|
1644
|
+
}
|
|
1645
|
+
if (s5 !== peg$FAILED) {
|
|
1646
|
+
s4 = [s4, s5];
|
|
1647
|
+
s3 = s4;
|
|
1648
|
+
} else {
|
|
1649
|
+
peg$currPos = s3;
|
|
1650
|
+
s3 = peg$FAILED;
|
|
1651
|
+
}
|
|
1652
|
+
} else {
|
|
1653
|
+
peg$currPos = s3;
|
|
1654
|
+
s3 = peg$FAILED;
|
|
1655
|
+
}
|
|
1520
1656
|
}
|
|
1521
|
-
}
|
|
1522
|
-
if (s2 !== peg$FAILED) {
|
|
1523
1657
|
s1 = [s1, s2];
|
|
1524
1658
|
s0 = s1;
|
|
1525
1659
|
} else {
|