links-notation 0.11.2 → 0.13.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/.eslintrc.json +28 -0
- package/.prettierignore +4 -0
- package/.prettierrc +4 -0
- package/README.md +52 -7
- package/README.ru.md +3 -6
- package/bun.lock +200 -0
- package/dist/index.js +582 -119
- package/package.json +6 -3
- package/src/FormatConfig.js +19 -0
- package/src/FormatOptions.js +52 -0
- package/src/Link.js +340 -40
- package/src/LinksGroup.js +2 -2
- package/src/Parser.js +57 -22
- package/src/grammar.pegjs +105 -4
- package/src/index.js +3 -1
- package/src/parser-generated.js +401 -88
- package/tests/ApiTests.test.js +104 -7
- package/tests/EdgeCaseParser.test.js +12 -12
- package/tests/FormatConfig.test.js +148 -0
- package/tests/IndentationConsistency.test.js +1 -1
- package/tests/IndentedIdSyntax.test.js +39 -39
- package/tests/Link.test.js +3 -3
- package/tests/LinksGroup.test.js +35 -5
- package/tests/MixedIndentationModes.test.js +35 -35
- package/tests/MultiQuoteParser.test.js +463 -0
- package/tests/MultilineParser.test.js +17 -17
- package/tests/MultilineQuotedString.test.js +9 -9
- package/tests/NestedParser.test.js +36 -7
- package/tests/NestedSelfReference.test.js +104 -0
- package/tests/SingleLineParser.test.js +88 -8
package/src/Parser.js
CHANGED
|
@@ -2,24 +2,54 @@ import { Link } from './Link.js';
|
|
|
2
2
|
import * as parserModule from './parser-generated.js';
|
|
3
3
|
|
|
4
4
|
export class Parser {
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Create a new Parser instance
|
|
7
|
+
* @param {Object} options - Parser options
|
|
8
|
+
* @param {number} options.maxInputSize - Maximum input size in bytes (default: 10MB)
|
|
9
|
+
* @param {number} options.maxDepth - Maximum nesting depth (default: 1000)
|
|
10
|
+
*/
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024; // 10MB default
|
|
13
|
+
this.maxDepth = options.maxDepth || 1000;
|
|
6
14
|
}
|
|
7
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Parse Lino notation text into Link objects
|
|
18
|
+
* @param {string} input - The Lino notation text to parse
|
|
19
|
+
* @returns {Link[]} Array of parsed Link objects
|
|
20
|
+
* @throws {Error} If parsing fails
|
|
21
|
+
*/
|
|
8
22
|
parse(input) {
|
|
23
|
+
// Validate input
|
|
24
|
+
if (typeof input !== 'string') {
|
|
25
|
+
throw new TypeError('Input must be a string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (input.length > this.maxInputSize) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
try {
|
|
10
35
|
const rawResult = parserModule.parse(input);
|
|
11
36
|
return this.transformResult(rawResult);
|
|
12
37
|
} catch (error) {
|
|
13
|
-
|
|
38
|
+
// Preserve original error information
|
|
39
|
+
const parseError = new Error(`Parse error: ${error.message}`);
|
|
40
|
+
parseError.cause = error;
|
|
41
|
+
parseError.location = error.location;
|
|
42
|
+
throw parseError;
|
|
14
43
|
}
|
|
15
44
|
}
|
|
16
45
|
|
|
17
46
|
transformResult(rawResult) {
|
|
18
47
|
const links = [];
|
|
19
48
|
const items = Array.isArray(rawResult) ? rawResult : [rawResult];
|
|
20
|
-
|
|
49
|
+
|
|
21
50
|
for (const item of items) {
|
|
22
|
-
|
|
51
|
+
// Use explicit null/undefined check
|
|
52
|
+
if (item !== null && item !== undefined) {
|
|
23
53
|
this.collectLinks(item, [], links);
|
|
24
54
|
}
|
|
25
55
|
}
|
|
@@ -27,7 +57,8 @@ export class Parser {
|
|
|
27
57
|
}
|
|
28
58
|
|
|
29
59
|
collectLinks(item, parentPath, result) {
|
|
30
|
-
|
|
60
|
+
// Use explicit null/undefined check
|
|
61
|
+
if (item === null || item === undefined) return;
|
|
31
62
|
|
|
32
63
|
// For items with children (indented structure)
|
|
33
64
|
if (item.children && item.children.length > 0) {
|
|
@@ -43,10 +74,10 @@ export class Parser {
|
|
|
43
74
|
});
|
|
44
75
|
const linkWithChildren = {
|
|
45
76
|
id: item.id,
|
|
46
|
-
values: childValues
|
|
77
|
+
values: childValues,
|
|
47
78
|
};
|
|
48
79
|
const currentLink = this.transformLink(linkWithChildren);
|
|
49
|
-
|
|
80
|
+
|
|
50
81
|
if (parentPath.length === 0) {
|
|
51
82
|
result.push(currentLink);
|
|
52
83
|
} else {
|
|
@@ -55,17 +86,17 @@ export class Parser {
|
|
|
55
86
|
} else {
|
|
56
87
|
// Regular indented structure - process as before
|
|
57
88
|
const currentLink = this.transformLink(item);
|
|
58
|
-
|
|
89
|
+
|
|
59
90
|
// Add the link combined with parent path
|
|
60
91
|
if (parentPath.length === 0) {
|
|
61
92
|
result.push(currentLink);
|
|
62
93
|
} else {
|
|
63
94
|
result.push(this.combinePathElements(parentPath, currentLink));
|
|
64
95
|
}
|
|
65
|
-
|
|
96
|
+
|
|
66
97
|
// Process each child with this item in the path
|
|
67
98
|
const newPath = [...parentPath, currentLink];
|
|
68
|
-
|
|
99
|
+
|
|
69
100
|
for (const child of item.children) {
|
|
70
101
|
this.collectLinks(child, newPath, result);
|
|
71
102
|
}
|
|
@@ -73,7 +104,7 @@ export class Parser {
|
|
|
73
104
|
} else {
|
|
74
105
|
// Leaf item or item with inline values
|
|
75
106
|
const currentLink = this.transformLink(item);
|
|
76
|
-
|
|
107
|
+
|
|
77
108
|
if (parentPath.length === 0) {
|
|
78
109
|
result.push(currentLink);
|
|
79
110
|
} else {
|
|
@@ -81,7 +112,7 @@ export class Parser {
|
|
|
81
112
|
}
|
|
82
113
|
}
|
|
83
114
|
}
|
|
84
|
-
|
|
115
|
+
|
|
85
116
|
combinePathElements(pathElements, current) {
|
|
86
117
|
if (pathElements.length === 0) return current;
|
|
87
118
|
if (pathElements.length === 1) {
|
|
@@ -89,25 +120,30 @@ export class Parser {
|
|
|
89
120
|
combined._isFromPathCombination = true;
|
|
90
121
|
return combined;
|
|
91
122
|
}
|
|
92
|
-
|
|
123
|
+
|
|
93
124
|
// For multiple path elements, we need to build proper nesting
|
|
94
125
|
// The last element in the path should be combined with its parent
|
|
95
126
|
const parentPath = pathElements.slice(0, -1);
|
|
96
127
|
const lastElement = pathElements[pathElements.length - 1];
|
|
97
|
-
|
|
128
|
+
|
|
98
129
|
// Build the parent structure
|
|
99
130
|
let parent = this.combinePathElements(parentPath, lastElement);
|
|
100
|
-
|
|
131
|
+
|
|
101
132
|
// Add current element to the built structure
|
|
102
133
|
const combined = new Link(null, [parent, current]);
|
|
103
134
|
combined._isFromPathCombination = true;
|
|
104
135
|
return combined;
|
|
105
136
|
}
|
|
106
|
-
|
|
107
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Transform a parsed item into a Link object
|
|
140
|
+
* @param {*} item - The item to transform
|
|
141
|
+
* @returns {Link|null} The transformed Link or null
|
|
142
|
+
*/
|
|
108
143
|
transformLink(item) {
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
// Use explicit null/undefined check
|
|
145
|
+
if (item === null || item === undefined) return null;
|
|
146
|
+
|
|
111
147
|
if (item instanceof Link) {
|
|
112
148
|
return item;
|
|
113
149
|
}
|
|
@@ -121,12 +157,11 @@ export class Parser {
|
|
|
121
157
|
if (item.values && Array.isArray(item.values)) {
|
|
122
158
|
// Create a link with id (if present) and transformed values
|
|
123
159
|
const link = new Link(item.id || null, []);
|
|
124
|
-
link.values = item.values.map(v => this.transformLink(v));
|
|
160
|
+
link.values = item.values.map((v) => this.transformLink(v));
|
|
125
161
|
return link;
|
|
126
162
|
}
|
|
127
|
-
|
|
163
|
+
|
|
128
164
|
// Default case
|
|
129
165
|
return new Link(item.id || null, []);
|
|
130
166
|
}
|
|
131
|
-
|
|
132
|
-
}
|
|
167
|
+
}
|
package/src/grammar.pegjs
CHANGED
|
@@ -34,6 +34,59 @@
|
|
|
34
34
|
function getCurrentIndentation() {
|
|
35
35
|
return indentationStack[indentationStack.length - 1];
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
// Universal procedural parser for N-quote strings (any N >= 1)
|
|
39
|
+
// Parses from the given position in the input string
|
|
40
|
+
// Returns { value, length } or null
|
|
41
|
+
function parseQuotedStringAt(inputStr, startPos, quoteChar) {
|
|
42
|
+
if (startPos >= inputStr.length || inputStr[startPos] !== quoteChar) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Count opening quotes
|
|
47
|
+
let quoteCount = 0;
|
|
48
|
+
let pos = startPos;
|
|
49
|
+
while (pos < inputStr.length && inputStr[pos] === quoteChar) {
|
|
50
|
+
quoteCount++;
|
|
51
|
+
pos++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const closeSeq = quoteChar.repeat(quoteCount);
|
|
55
|
+
const escapeSeq = quoteChar.repeat(quoteCount * 2);
|
|
56
|
+
|
|
57
|
+
let content = '';
|
|
58
|
+
while (pos < inputStr.length) {
|
|
59
|
+
// Check for escape sequence (2*N quotes)
|
|
60
|
+
if (inputStr.substr(pos, escapeSeq.length) === escapeSeq) {
|
|
61
|
+
content += closeSeq; // 2*N quotes become N quotes
|
|
62
|
+
pos += escapeSeq.length;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for closing sequence (exactly N quotes)
|
|
67
|
+
if (inputStr.substr(pos, quoteCount) === closeSeq) {
|
|
68
|
+
// Verify it's exactly N quotes (not followed by more of same char)
|
|
69
|
+
const afterClose = pos + quoteCount;
|
|
70
|
+
if (afterClose >= inputStr.length || inputStr[afterClose] !== quoteChar) {
|
|
71
|
+
// Found valid closing
|
|
72
|
+
return {
|
|
73
|
+
value: content,
|
|
74
|
+
length: afterClose - startPos
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add character to content
|
|
80
|
+
content += inputStr[pos];
|
|
81
|
+
pos++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null; // No valid closing found
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Global state for passing parsed values between predicate and action
|
|
88
|
+
let parsedValue = null;
|
|
89
|
+
let parsedLength = 0;
|
|
37
90
|
}
|
|
38
91
|
|
|
39
92
|
document = &{ indentationStack = [0]; baseIndentation = null; return true; } skipEmptyLines links:links _ eof { return links; }
|
|
@@ -79,13 +132,61 @@ multiLineValueLink = "(" v:multiLineValues _ ")" { return { values: v }; }
|
|
|
79
132
|
|
|
80
133
|
indentedIdLink = id:reference __ ":" eol { return { id: id, values: [] }; }
|
|
81
134
|
|
|
82
|
-
|
|
135
|
+
// Reference can be quoted (with any number of quotes N >= 1) or simple unquoted
|
|
136
|
+
// Universal approach: use procedural parsing for all quote types and counts
|
|
137
|
+
reference = quotedReference / simpleReference
|
|
83
138
|
|
|
84
139
|
simpleReference = chars:referenceSymbol+ { return chars.join(''); }
|
|
85
140
|
|
|
86
|
-
|
|
141
|
+
// Universal quoted reference - handles any N quotes for all quote types
|
|
142
|
+
// Uses procedural parsing with input/offset() for clean, simple logic
|
|
143
|
+
quotedReference = doubleQuotedUniversal / singleQuotedUniversal / backtickQuotedUniversal
|
|
144
|
+
|
|
145
|
+
// Double quotes: peek at input, parse procedurally, consume exact chars
|
|
146
|
+
doubleQuotedUniversal = &'"' &{
|
|
147
|
+
const pos = offset();
|
|
148
|
+
const result = parseQuotedStringAt(input, pos, '"');
|
|
149
|
+
if (result) {
|
|
150
|
+
parsedValue = result.value;
|
|
151
|
+
parsedLength = result.length;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
} chars:consumeDouble { return parsedValue; }
|
|
156
|
+
|
|
157
|
+
// Consume exactly parsedLength characters for double quotes
|
|
158
|
+
consumeDouble = c:. cs:consumeDoubleMore* { return [c].concat(cs).join(''); }
|
|
159
|
+
consumeDoubleMore = &{ return parsedLength > 1 && (parsedLength--, true); } c:. { return c; }
|
|
160
|
+
|
|
161
|
+
// Single quotes
|
|
162
|
+
singleQuotedUniversal = &"'" &{
|
|
163
|
+
const pos = offset();
|
|
164
|
+
const result = parseQuotedStringAt(input, pos, "'");
|
|
165
|
+
if (result) {
|
|
166
|
+
parsedValue = result.value;
|
|
167
|
+
parsedLength = result.length;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
} chars:consumeSingle { return parsedValue; }
|
|
172
|
+
|
|
173
|
+
consumeSingle = c:. cs:consumeSingleMore* { return [c].concat(cs).join(''); }
|
|
174
|
+
consumeSingleMore = &{ return parsedLength > 1 && (parsedLength--, true); } c:. { return c; }
|
|
175
|
+
|
|
176
|
+
// Backticks
|
|
177
|
+
backtickQuotedUniversal = &'`' &{
|
|
178
|
+
const pos = offset();
|
|
179
|
+
const result = parseQuotedStringAt(input, pos, '`');
|
|
180
|
+
if (result) {
|
|
181
|
+
parsedValue = result.value;
|
|
182
|
+
parsedLength = result.length;
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
} chars:consumeBacktick { return parsedValue; }
|
|
87
187
|
|
|
88
|
-
|
|
188
|
+
consumeBacktick = c:. cs:consumeBacktickMore* { return [c].concat(cs).join(''); }
|
|
189
|
+
consumeBacktickMore = &{ return parsedLength > 1 && (parsedLength--, true); } c:. { return c; }
|
|
89
190
|
|
|
90
191
|
SET_BASE_INDENTATION = spaces:" "* { setBaseIndentation(spaces); }
|
|
91
192
|
|
|
@@ -103,4 +204,4 @@ _ = whiteSpaceSymbol*
|
|
|
103
204
|
|
|
104
205
|
whiteSpaceSymbol = [ \t\n\r]
|
|
105
206
|
|
|
106
|
-
referenceSymbol = [^ \t\n\r(:)]
|
|
207
|
+
referenceSymbol = [^ \t\n\r(:)]
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { Link, formatLinks } from './Link.js';
|
|
2
2
|
export { LinksGroup } from './LinksGroup.js';
|
|
3
|
-
export { Parser } from './Parser.js';
|
|
3
|
+
export { Parser } from './Parser.js';
|
|
4
|
+
export { FormatConfig } from './FormatConfig.js';
|
|
5
|
+
export { FormatOptions } from './FormatOptions.js';
|