links-notation 0.13.0 → 0.16.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 +55 -0
- package/bun.lock +74 -94
- package/dist/index.js +238 -166
- package/eslint.config.js +34 -0
- package/examples/empty_reference.js +43 -0
- package/examples/typescript-example.ts +95 -0
- package/index.d.ts +312 -0
- package/package.json +19 -7
- package/src/Link.js +7 -1
- package/src/Parser.js +36 -3
- package/src/grammar.pegjs +86 -12
- package/src/parser-generated.js +221 -147
- package/tests/EmptyReference.test.js +91 -0
- package/tests/Link.test.js +2 -2
- package/tests/MultilineQuotedString.test.js +4 -2
- package/tests/NestedIndentation.test.js +114 -0
- package/tests/SingleLineParser.test.js +7 -7
- package/tsconfig.json +15 -0
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
|
|
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 = &{
|
|
93
|
-
/ &{
|
|
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 {
|
|
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 =
|
|
177
|
+
multiLineAnyLink = nestedGroup
|
|
113
178
|
|
|
114
179
|
singleLineAnyLink = fl:singleLineLink eol { return fl; }
|
|
115
180
|
/ vl:singleLineValueLink eol { return vl; }
|
|
116
181
|
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|