links-notation 0.13.0 → 0.15.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/src/grammar.pegjs CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  let indentationStack = [0];
3
3
  let baseIndentation = null;
4
+ // Saved indentation contexts of the enclosing scopes. Every parenthesised
5
+ // group opens a nested context that starts fresh at indentation level zero,
6
+ // so line breaks and indentation mean the same thing at every depth.
7
+ let contextStack = [];
8
+
9
+ function resetState() {
10
+ indentationStack = [0];
11
+ baseIndentation = null;
12
+ contextStack = [];
13
+ return true;
14
+ }
15
+
16
+ function enterNestedContext() {
17
+ contextStack.push({ indentationStack, baseIndentation });
18
+ indentationStack = [0];
19
+ baseIndentation = null;
20
+ return true;
21
+ }
22
+
23
+ function exitNestedContext() {
24
+ const saved = contextStack.pop();
25
+ if (saved) {
26
+ indentationStack = saved.indentationStack;
27
+ baseIndentation = saved.baseIndentation;
28
+ }
29
+ return true;
30
+ }
31
+
32
+ function isInsideNestedContext() {
33
+ return contextStack.length > 0;
34
+ }
4
35
 
5
36
  function setBaseIndentation(spaces) {
6
37
  if (baseIndentation === null) {
@@ -35,8 +66,36 @@
35
66
  return indentationStack[indentationStack.length - 1];
36
67
  }
37
68
 
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 (!/\s/.test(c)) {
87
+ hasVisible = true;
88
+ }
89
+ }
90
+
91
+ return hasVisible && depth === 0;
92
+ }
93
+
38
94
  // Universal procedural parser for N-quote strings (any N >= 1)
39
95
  // 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.
40
99
  // Returns { value, length } or null
41
100
  function parseQuotedStringAt(inputStr, startPos, quoteChar) {
42
101
  if (startPos >= inputStr.length || inputStr[startPos] !== quoteChar) {
@@ -51,6 +110,9 @@
51
110
  pos++;
52
111
  }
53
112
 
113
+ const isEvenRun = quoteCount % 2 === 0;
114
+ const emptyReference = isEvenRun ? { value: '', length: quoteCount } : null;
115
+
54
116
  const closeSeq = quoteChar.repeat(quoteCount);
55
117
  const escapeSeq = quoteChar.repeat(quoteCount * 2);
56
118
 
@@ -69,6 +131,9 @@
69
131
  const afterClose = pos + quoteCount;
70
132
  if (afterClose >= inputStr.length || inputStr[afterClose] !== quoteChar) {
71
133
  // Found valid closing
134
+ if (isEvenRun && !isSubstantiveBody(content)) {
135
+ return emptyReference;
136
+ }
72
137
  return {
73
138
  value: content,
74
139
  length: afterClose - startPos
@@ -81,7 +146,7 @@
81
146
  pos++;
82
147
  }
83
148
 
84
- return null; // No valid closing found
149
+ return emptyReference; // No valid closing found
85
150
  }
86
151
 
87
152
  // Global state for passing parsed values between predicate and action
@@ -89,8 +154,8 @@
89
154
  let parsedLength = 0;
90
155
  }
91
156
 
92
- document = &{ indentationStack = [0]; baseIndentation = null; return true; } skipEmptyLines links:links _ eof { return links; }
93
- / &{ indentationStack = [0]; baseIndentation = null; return true; } _ eof { return []; }
157
+ document = &{ return resetState(); } skipEmptyLines links:links _ eof { return links; }
158
+ / &{ return resetState(); } _ eof { return []; }
94
159
 
95
160
  skipEmptyLines = ([ \t]* [\r\n])*
96
161
 
@@ -101,7 +166,7 @@ firstLine = SET_BASE_INDENTATION l:element { return l; }
101
166
  line = CHECK_INDENTATION l:element { return l; }
102
167
 
103
168
  element = e:anyLink PUSH_INDENTATION l:links {
104
- return { id: e.id, values: e.values, children: l };
169
+ return Object.assign({}, e, { children: l });
105
170
  }
106
171
  / e:anyLink { return e; }
107
172
 
@@ -109,14 +174,23 @@ referenceOrLink = l:multiLineAnyLink { return l; } / i:reference { return { id:
109
174
 
110
175
  anyLink = ml:multiLineAnyLink eol { return ml; } / il:indentedIdLink { return il; } / sl:singleLineAnyLink { return sl; }
111
176
 
112
- multiLineAnyLink = multiLineValueLink / multiLineLink
177
+ multiLineAnyLink = nestedGroup
113
178
 
114
179
  singleLineAnyLink = fl:singleLineLink eol { return fl; }
115
180
  / vl:singleLineValueLink eol { return vl; }
116
181
 
117
- multiLineValueAndWhitespace = value:referenceOrLink _ { return value; }
182
+ // A parenthesised group opens a nested context that follows exactly the same
183
+ // rules as the root of the document: line breaks separate links and
184
+ // indentation nests them, starting fresh at indentation level zero.
185
+ nestedGroup = "(" ENTER_NESTED_CONTEXT body:nestedGroupBody {
186
+ exitNestedContext();
187
+ return body;
188
+ }
189
+
190
+ nestedGroupBody = skipEmptyLines l:links _ ")" { return { nested: l }; }
191
+ / _ ")" { return { nested: [] }; }
118
192
 
119
- multiLineValues = _ list:multiLineValueAndWhitespace* { return list; }
193
+ ENTER_NESTED_CONTEXT = &{ return enterNestedContext(); }
120
194
 
121
195
  singleLineValueAndWhitespace = __ value:referenceOrLink { return value; }
122
196
 
@@ -124,12 +198,8 @@ singleLineValues = list:singleLineValueAndWhitespace+ { return list; }
124
198
 
125
199
  singleLineLink = __ id:reference __ ":" v:singleLineValues { return { id: id, values: v }; }
126
200
 
127
- multiLineLink = "(" _ id:reference _ ":" v:multiLineValues _ ")" { return { id: id, values: v }; }
128
-
129
201
  singleLineValueLink = v:singleLineValues { return { values: v }; }
130
202
 
131
- multiLineValueLink = "(" v:multiLineValues _ ")" { return { values: v }; }
132
-
133
203
  indentedIdLink = id:reference __ ":" eol { return { id: id, values: [] }; }
134
204
 
135
205
  // Reference can be quoted (with any number of quotes N >= 1) or simple unquoted
@@ -194,7 +264,11 @@ PUSH_INDENTATION = spaces:" "* &{ return normalizeIndentation(spaces) > getCurre
194
264
 
195
265
  CHECK_INDENTATION = spaces:" "* &{ return checkIndentation(spaces); }
196
266
 
197
- eol = __ ([\r\n]+ / eof)
267
+ eol = __ ([\r\n]+ / eof / nestedGroupEnd)
268
+
269
+ // Inside a parenthesised group the closing parenthesis terminates a line the
270
+ // same way the end of the input does at the root.
271
+ nestedGroupEnd = &{ return isInsideNestedContext(); } &")"
198
272
 
199
273
  eof = !.
200
274