links-notation 0.21.1 → 0.21.3
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 +439 -225
- package/package.json +1 -1
- package/src/StreamParser.js +3 -2
- package/src/comments.js +6 -70
- package/src/grammar.pegjs +54 -76
- package/src/parser-generated.js +304 -186
- package/src/quotes.js +251 -0
- package/tests/Issue316QuoteRuns.test.js +163 -0
- package/tests/NestingDepth.test.js +124 -0
- package/tests/support/nestingDepthWorker.js +15 -0
package/package.json
CHANGED
package/src/StreamParser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { Parser } from './Parser.js';
|
|
3
|
-
import {
|
|
3
|
+
import { DelimitedReferences } from './quotes.js';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
6
6
|
|
|
@@ -249,6 +249,7 @@ function structurallyComplete(document, comments) {
|
|
|
249
249
|
const quotes = ['"', "'", '`'];
|
|
250
250
|
const beforeReference = [' ', '\t', '\n', '\r', '(', ':'];
|
|
251
251
|
const beforeComment = [' ', '\t', '\n', '\r'];
|
|
252
|
+
const references = new DelimitedReferences(document);
|
|
252
253
|
let depth = 0;
|
|
253
254
|
|
|
254
255
|
for (let position = 0; position < document.length; position++) {
|
|
@@ -259,7 +260,7 @@ function structurallyComplete(document, comments) {
|
|
|
259
260
|
quotes.includes(character) &&
|
|
260
261
|
(previous === null || beforeReference.includes(previous))
|
|
261
262
|
) {
|
|
262
|
-
const end =
|
|
263
|
+
const end = references.endAt(position);
|
|
263
264
|
if (end === null) return false;
|
|
264
265
|
position = end - 1;
|
|
265
266
|
continue;
|
package/src/comments.js
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
* the reader can see.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { DelimitedReferences, QUOTES, readReference } from './quotes.js';
|
|
16
|
+
|
|
15
17
|
/** The character that starts a comment. */
|
|
16
18
|
export const COMMENT = '#';
|
|
17
19
|
|
|
18
|
-
/** The delimiters a reference can be written between. */
|
|
19
|
-
const QUOTES = ['"', "'", '`'];
|
|
20
|
-
|
|
21
20
|
/** What a delimited reference may start after. */
|
|
22
21
|
const BEFORE_REFERENCE = [' ', '\t', '\n', '\r', '(', ':'];
|
|
23
22
|
|
|
@@ -38,6 +37,7 @@ export function stripComments(document) {
|
|
|
38
37
|
// parse error reports are both counted in.
|
|
39
38
|
let blanked = null;
|
|
40
39
|
let position = 0;
|
|
40
|
+
const references = new DelimitedReferences(document);
|
|
41
41
|
|
|
42
42
|
while (position < document.length) {
|
|
43
43
|
const character = document[position];
|
|
@@ -46,7 +46,7 @@ export function stripComments(document) {
|
|
|
46
46
|
QUOTES.includes(character) &&
|
|
47
47
|
follows(document, position, BEFORE_REFERENCE)
|
|
48
48
|
) {
|
|
49
|
-
const end =
|
|
49
|
+
const end = references.endAt(position);
|
|
50
50
|
position = end === null ? position + 1 : end;
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
@@ -95,70 +95,6 @@ function follows(document, position, allowed) {
|
|
|
95
95
|
* @returns {number|null} The position just past the reference
|
|
96
96
|
*/
|
|
97
97
|
export function quotedReferenceEnd(document, start) {
|
|
98
|
-
const
|
|
99
|
-
|
|
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 (!/[ \t\n\r]/.test(character)) {
|
|
159
|
-
hasVisible = true;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return hasVisible && depth === 0;
|
|
98
|
+
const reading = readReference(document, start);
|
|
99
|
+
return reading === null ? null : start + reading.length;
|
|
164
100
|
}
|
package/src/grammar.pegjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
{{
|
|
2
|
+
import { DelimitedReferences } from './quotes.js';
|
|
3
|
+
}}
|
|
4
|
+
|
|
1
5
|
{
|
|
2
6
|
let indentationStack = [0];
|
|
3
7
|
let baseIndentation = null;
|
|
@@ -5,14 +9,27 @@
|
|
|
5
9
|
// group opens a nested context that starts fresh at indentation level zero,
|
|
6
10
|
// so line breaks and indentation mean the same thing at every depth.
|
|
7
11
|
let contextStack = [];
|
|
12
|
+
// Lines already found unreadable, keyed by where they start and whether they
|
|
13
|
+
// are inside a parenthesised group. Reading a line depends on nothing else: a
|
|
14
|
+
// group starts a fresh indentation context, and indentation only decides
|
|
15
|
+
// which lines become children, so a line that could not be read once never
|
|
16
|
+
// can be. A line that does not parse as the first child of the line above it
|
|
17
|
+
// is tried again as a sibling at every enclosing indentation level, and
|
|
18
|
+
// without this each of those attempts would read the whole line again.
|
|
19
|
+
let unreadableLines = new Set();
|
|
8
20
|
|
|
9
21
|
function resetState() {
|
|
10
22
|
indentationStack = [0];
|
|
11
23
|
baseIndentation = null;
|
|
12
24
|
contextStack = [];
|
|
25
|
+
unreadableLines = new Set();
|
|
13
26
|
return true;
|
|
14
27
|
}
|
|
15
28
|
|
|
29
|
+
function lineKey(position) {
|
|
30
|
+
return contextStack.length > 0 ? -1 - position : position;
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
function enterNestedContext() {
|
|
17
34
|
contextStack.push({ indentationStack, baseIndentation });
|
|
18
35
|
indentationStack = [0];
|
|
@@ -66,87 +83,18 @@
|
|
|
66
83
|
return indentationStack[indentationStack.length - 1];
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
// n-quote reading is only taken when it carries something the pairs cannot.
|
|
73
|
-
function isSubstantiveBody(content) {
|
|
74
|
-
let depth = 0;
|
|
75
|
-
let hasVisible = false;
|
|
76
|
-
|
|
77
|
-
for (const c of content) {
|
|
78
|
-
if (c === '(') {
|
|
79
|
-
depth++;
|
|
80
|
-
} else if (c === ')') {
|
|
81
|
-
depth--;
|
|
82
|
-
if (depth < 0) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (!/[ \t\n\r]/.test(c)) {
|
|
87
|
-
hasVisible = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return hasVisible && depth === 0;
|
|
92
|
-
}
|
|
86
|
+
// Delimited references of this document, each read once however many
|
|
87
|
+
// alternatives ask for it (see quotes.js for how a reference is read)
|
|
88
|
+
const delimitedReferences = new DelimitedReferences(input);
|
|
93
89
|
|
|
94
90
|
// Universal procedural parser for N-quote strings (any N >= 1)
|
|
95
91
|
// Parses from the given position in the input string
|
|
96
|
-
// A run of an even number of delimiters that does not open a reference with a
|
|
97
|
-
// substantive body is the empty reference: the shortest reading, a bare
|
|
98
|
-
// delimiter pair enclosing nothing, wins over a longer n-quote delimiter.
|
|
99
92
|
// Returns { value, length } or null
|
|
100
93
|
function parseQuotedStringAt(inputStr, startPos, quoteChar) {
|
|
101
94
|
if (startPos >= inputStr.length || inputStr[startPos] !== quoteChar) {
|
|
102
95
|
return null;
|
|
103
96
|
}
|
|
104
|
-
|
|
105
|
-
// Count opening quotes
|
|
106
|
-
let quoteCount = 0;
|
|
107
|
-
let pos = startPos;
|
|
108
|
-
while (pos < inputStr.length && inputStr[pos] === quoteChar) {
|
|
109
|
-
quoteCount++;
|
|
110
|
-
pos++;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const isEvenRun = quoteCount % 2 === 0;
|
|
114
|
-
const emptyReference = isEvenRun ? { value: '', length: quoteCount } : null;
|
|
115
|
-
|
|
116
|
-
const closeSeq = quoteChar.repeat(quoteCount);
|
|
117
|
-
const escapeSeq = quoteChar.repeat(quoteCount * 2);
|
|
118
|
-
|
|
119
|
-
let content = '';
|
|
120
|
-
while (pos < inputStr.length) {
|
|
121
|
-
// Check for escape sequence (2*N quotes)
|
|
122
|
-
if (inputStr.substr(pos, escapeSeq.length) === escapeSeq) {
|
|
123
|
-
content += closeSeq; // 2*N quotes become N quotes
|
|
124
|
-
pos += escapeSeq.length;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Check for closing sequence (exactly N quotes)
|
|
129
|
-
if (inputStr.substr(pos, quoteCount) === closeSeq) {
|
|
130
|
-
// Verify it's exactly N quotes (not followed by more of same char)
|
|
131
|
-
const afterClose = pos + quoteCount;
|
|
132
|
-
if (afterClose >= inputStr.length || inputStr[afterClose] !== quoteChar) {
|
|
133
|
-
// Found valid closing
|
|
134
|
-
if (isEvenRun && !isSubstantiveBody(content)) {
|
|
135
|
-
return emptyReference;
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
value: content,
|
|
139
|
-
length: afterClose - startPos
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Add character to content
|
|
145
|
-
content += inputStr[pos];
|
|
146
|
-
pos++;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return emptyReference; // No valid closing found
|
|
97
|
+
return delimitedReferences.readAt(startPos);
|
|
150
98
|
}
|
|
151
99
|
|
|
152
100
|
// Global state for passing parsed values between predicate and action
|
|
@@ -165,14 +113,39 @@ firstLine = SET_BASE_INDENTATION l:element { return l; }
|
|
|
165
113
|
|
|
166
114
|
line = CHECK_INDENTATION l:element { return l; }
|
|
167
115
|
|
|
168
|
-
|
|
116
|
+
// A line is read once, whether or not indented children follow it: reading it
|
|
117
|
+
// again after looking for children doubled the work at every level of nesting.
|
|
118
|
+
element = &{ return !unreadableLines.has(lineKey(offset())); }
|
|
119
|
+
e:anyLink saved:SAVE_INDENTATION l:(PUSH_INDENTATION @links)? {
|
|
120
|
+
if (l === null) {
|
|
121
|
+
// No child line followed the indentation. Forget what looking for one
|
|
122
|
+
// pushed, so the next line is compared with this line's indentation.
|
|
123
|
+
indentationStack = saved;
|
|
124
|
+
return e;
|
|
125
|
+
}
|
|
169
126
|
return Object.assign({}, e, { children: l });
|
|
170
127
|
}
|
|
171
|
-
|
|
128
|
+
// Remember that this line could not be read. The predicate always fails, so
|
|
129
|
+
// the "." is never reached; it only tells Peggy that this alternative cannot
|
|
130
|
+
// succeed without consuming input.
|
|
131
|
+
/ &{ unreadableLines.add(lineKey(offset())); return false; } .
|
|
172
132
|
|
|
173
133
|
referenceOrLink = l:multiLineAnyLink { return l; } / i:reference { return { id: i }; }
|
|
174
134
|
|
|
175
|
-
|
|
135
|
+
// A line that starts with a parenthesised group reads that group once and then
|
|
136
|
+
// branches on what follows it: the end of the line makes the group the whole
|
|
137
|
+
// link, and more values make it the first value of a value link. Neither an
|
|
138
|
+
// indented ID nor a single-line link can start with a parenthesis, so nothing
|
|
139
|
+
// else is tried for such a line; trying them read the group again, doubling
|
|
140
|
+
// the work at every level of nesting.
|
|
141
|
+
anyLink = &"(" @groupLink / !"(" @(indentedIdLink / singleLineAnyLink)
|
|
142
|
+
|
|
143
|
+
groupLink = g:multiLineAnyLink rest:groupLinkRest {
|
|
144
|
+
return rest === null ? g : { values: [g].concat(rest) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
groupLinkRest = eol { return null; }
|
|
148
|
+
/ @singleLineValueAndWhitespace* eol
|
|
176
149
|
|
|
177
150
|
multiLineAnyLink = nestedGroup
|
|
178
151
|
|
|
@@ -186,6 +159,9 @@ nestedGroup = "(" ENTER_NESTED_CONTEXT body:nestedGroupBody {
|
|
|
186
159
|
exitNestedContext();
|
|
187
160
|
return body;
|
|
188
161
|
}
|
|
162
|
+
// The group was opened but its body did not parse: restore the context it
|
|
163
|
+
// was opened in before failing, just as a parsed group does.
|
|
164
|
+
/ "(" &{ exitNestedContext(); return false; }
|
|
189
165
|
|
|
190
166
|
nestedGroupBody = skipEmptyLines l:links _ ")" { return { nested: l }; }
|
|
191
167
|
/ _ ")" { return { nested: [] }; }
|
|
@@ -258,6 +234,8 @@ backtickQuotedUniversal = &'`' &{
|
|
|
258
234
|
consumeBacktick = c:. cs:consumeBacktickMore* { return [c].concat(cs).join(''); }
|
|
259
235
|
consumeBacktickMore = &{ return parsedLength > 1 && (parsedLength--, true); } c:. { return c; }
|
|
260
236
|
|
|
237
|
+
SAVE_INDENTATION = "" { return indentationStack.slice(); }
|
|
238
|
+
|
|
261
239
|
SET_BASE_INDENTATION = spaces:" "* { setBaseIndentation(spaces); }
|
|
262
240
|
|
|
263
241
|
PUSH_INDENTATION = spaces:" "* &{ return normalizeIndentation(spaces) > getCurrentIndentation(); } { pushIndentation(spaces); }
|