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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "links-notation",
3
- "version": "0.21.1",
3
+ "version": "0.21.3",
4
4
  "description": "Links Notation parser for JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { Parser } from './Parser.js';
3
- import { quotedReferenceEnd } from './comments.js';
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 = quotedReferenceEnd(document, position);
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 = quotedReferenceEnd(document, position);
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 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 (!/[ \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
- // A body written between an even run of delimiters is substantive when it
70
- // holds at least one visible character and does not straddle a parenthesis.
71
- // An even run can always be read as delimiter pairs enclosing nothing, so the
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
- element = e:anyLink PUSH_INDENTATION l:links {
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
- / e:anyLink { return e; }
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
- anyLink = ml:multiLineAnyLink eol { return ml; } / il:indentedIdLink { return il; } / sl:singleLineAnyLink { return sl; }
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); }