links-notation 0.11.2 → 0.12.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/bun.lock +1 -0
- package/dist/index.js +191 -21
- package/package.json +1 -1
- package/src/FormatConfig.js +19 -0
- package/src/FormatOptions.js +52 -0
- package/src/Link.js +287 -25
- package/src/Parser.js +43 -8
- package/src/index.js +3 -1
- package/tests/ApiTests.test.js +104 -7
- package/tests/EdgeCaseParser.test.js +9 -9
- package/tests/FormatConfig.test.js +126 -0
- package/tests/IndentationConsistency.test.js +1 -1
- package/tests/IndentedIdSyntax.test.js +2 -2
- package/tests/Link.test.js +1 -1
- package/tests/LinksGroup.test.js +28 -1
- package/tests/MixedIndentationModes.test.js +5 -5
- package/tests/MultilineParser.test.js +5 -5
- package/tests/NestedParser.test.js +31 -4
- package/tests/NestedSelfReference.test.js +102 -0
- package/tests/SingleLineParser.test.js +86 -6
package/src/Link.js
CHANGED
|
@@ -1,43 +1,92 @@
|
|
|
1
1
|
export class Link {
|
|
2
|
+
/**
|
|
3
|
+
* Create a new Link
|
|
4
|
+
* @param {string|null} id - Optional identifier for the link
|
|
5
|
+
* @param {Link[]|null} values - Optional array of nested links
|
|
6
|
+
* @throws {TypeError} If values is not an array or null
|
|
7
|
+
*/
|
|
2
8
|
constructor(id = null, values = null) {
|
|
3
9
|
this.id = id;
|
|
4
|
-
|
|
10
|
+
|
|
11
|
+
// Validate that values is an array if provided
|
|
12
|
+
if (values !== null && values !== undefined) {
|
|
13
|
+
if (!Array.isArray(values)) {
|
|
14
|
+
throw new TypeError('values must be an array or null');
|
|
15
|
+
}
|
|
16
|
+
this.values = values;
|
|
17
|
+
} else {
|
|
18
|
+
this.values = [];
|
|
19
|
+
}
|
|
5
20
|
}
|
|
6
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Convert link to string representation
|
|
24
|
+
* @returns {string} String representation of the link
|
|
25
|
+
*/
|
|
7
26
|
toString() {
|
|
8
27
|
return this.format(false);
|
|
9
28
|
}
|
|
10
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Get formatted string of all values
|
|
32
|
+
* @returns {string} Space-separated string of values
|
|
33
|
+
*/
|
|
11
34
|
getValuesString() {
|
|
12
|
-
return (!this.values || this.values.length === 0) ?
|
|
35
|
+
return (!this.values || this.values.length === 0) ?
|
|
13
36
|
'' : this.values.map(v => Link.getValueString(v)).join(' ');
|
|
14
37
|
}
|
|
15
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Simplify the link structure by unwrapping single-value containers
|
|
41
|
+
* @returns {Link} Simplified link
|
|
42
|
+
*/
|
|
16
43
|
simplify() {
|
|
17
44
|
if (!this.values || this.values.length === 0) {
|
|
18
45
|
return this;
|
|
19
46
|
} else if (this.values.length === 1) {
|
|
20
47
|
return this.values[0];
|
|
21
48
|
} else {
|
|
22
|
-
const newValues = this.values.map(v =>
|
|
49
|
+
const newValues = this.values.map(v => {
|
|
50
|
+
// Check if value has simplify method (defensive programming)
|
|
51
|
+
return v && typeof v.simplify === 'function' ? v.simplify() : v;
|
|
52
|
+
});
|
|
23
53
|
return new Link(this.id, newValues);
|
|
24
54
|
}
|
|
25
55
|
}
|
|
26
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Combine this link with another link
|
|
59
|
+
* @param {Link} other - The link to combine with
|
|
60
|
+
* @returns {Link} Combined link
|
|
61
|
+
*/
|
|
27
62
|
combine(other) {
|
|
28
63
|
return new Link(null, [this, other]);
|
|
29
64
|
}
|
|
30
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Get string representation of a value
|
|
68
|
+
* @param {Link} value - The value to stringify
|
|
69
|
+
* @returns {string} String representation
|
|
70
|
+
*/
|
|
31
71
|
static getValueString(value) {
|
|
32
|
-
|
|
72
|
+
// Defensive check for method existence
|
|
73
|
+
return value && typeof value.toLinkOrIdString === 'function' ? value.toLinkOrIdString() : String(value);
|
|
33
74
|
}
|
|
34
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Escape a reference string by adding quotes if necessary
|
|
78
|
+
* @param {string} reference - The reference to escape
|
|
79
|
+
* @returns {string} Escaped reference
|
|
80
|
+
*/
|
|
35
81
|
static escapeReference(reference) {
|
|
36
82
|
if (!reference || reference.trim() === '') {
|
|
37
83
|
return '';
|
|
38
84
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
85
|
+
|
|
86
|
+
const hasSingleQuote = reference.includes("'");
|
|
87
|
+
const hasDoubleQuote = reference.includes('"');
|
|
88
|
+
|
|
89
|
+
const needsQuoting =
|
|
41
90
|
reference.includes(':') ||
|
|
42
91
|
reference.includes('(') ||
|
|
43
92
|
reference.includes(')') ||
|
|
@@ -45,17 +94,38 @@ export class Link {
|
|
|
45
94
|
reference.includes('\t') ||
|
|
46
95
|
reference.includes('\n') ||
|
|
47
96
|
reference.includes('\r') ||
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
hasDoubleQuote ||
|
|
98
|
+
hasSingleQuote;
|
|
99
|
+
|
|
100
|
+
// Handle edge case: reference contains both single and double quotes
|
|
101
|
+
if (hasSingleQuote && hasDoubleQuote) {
|
|
102
|
+
// Escape single quotes and wrap in single quotes
|
|
103
|
+
return `'${reference.replace(/'/g, "\\'")}'`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Prefer single quotes if double quotes are present
|
|
107
|
+
if (hasDoubleQuote) {
|
|
51
108
|
return `'${reference}'`;
|
|
52
|
-
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Use double quotes if single quotes are present
|
|
112
|
+
if (hasSingleQuote) {
|
|
53
113
|
return `"${reference}"`;
|
|
54
|
-
} else {
|
|
55
|
-
return reference;
|
|
56
114
|
}
|
|
115
|
+
|
|
116
|
+
// Use single quotes for special characters
|
|
117
|
+
if (needsQuoting) {
|
|
118
|
+
return `'${reference}'`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No quoting needed
|
|
122
|
+
return reference;
|
|
57
123
|
}
|
|
58
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Convert to string using either just ID or full format
|
|
127
|
+
* @returns {string} String representation
|
|
128
|
+
*/
|
|
59
129
|
toLinkOrIdString() {
|
|
60
130
|
if (!this.values || this.values.length === 0) {
|
|
61
131
|
return this.id === null ? '' : Link.escapeReference(this.id);
|
|
@@ -63,21 +133,53 @@ export class Link {
|
|
|
63
133
|
return this.toString();
|
|
64
134
|
}
|
|
65
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Check equality with another Link
|
|
138
|
+
* @param {*} other - Object to compare with
|
|
139
|
+
* @returns {boolean} True if links are equal
|
|
140
|
+
*/
|
|
66
141
|
equals(other) {
|
|
67
142
|
if (!(other instanceof Link)) return false;
|
|
68
143
|
if (this.id !== other.id) return false;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
144
|
+
|
|
145
|
+
// Handle null/undefined values arrays
|
|
146
|
+
const thisValues = this.values || [];
|
|
147
|
+
const otherValues = other.values || [];
|
|
148
|
+
|
|
149
|
+
if (thisValues.length !== otherValues.length) return false;
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < thisValues.length; i++) {
|
|
152
|
+
// Defensive check for equals method
|
|
153
|
+
if (thisValues[i] && typeof thisValues[i].equals === 'function') {
|
|
154
|
+
if (!thisValues[i].equals(otherValues[i])) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// Fallback to reference equality
|
|
159
|
+
if (thisValues[i] !== otherValues[i]) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
74
162
|
}
|
|
75
163
|
}
|
|
76
164
|
return true;
|
|
77
165
|
}
|
|
78
166
|
|
|
79
|
-
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format the link as a string
|
|
170
|
+
* @param {boolean|FormatOptions} lessParentheses - If true, omit parentheses where safe; or a FormatOptions object
|
|
171
|
+
* @param {boolean} isCompoundValue - If true, this is a value in a compound link
|
|
172
|
+
* @returns {string} Formatted string
|
|
173
|
+
*/
|
|
80
174
|
format(lessParentheses = false, isCompoundValue = false) {
|
|
175
|
+
// Support FormatOptions/FormatConfig as first parameter
|
|
176
|
+
// Check if it's an object with formatting properties (FormatOptions or FormatConfig)
|
|
177
|
+
if (lessParentheses && typeof lessParentheses === 'object' &&
|
|
178
|
+
(lessParentheses.constructor.name === 'FormatOptions' || lessParentheses.constructor.name === 'FormatConfig')) {
|
|
179
|
+
return this._formatWithOptions(lessParentheses, isCompoundValue);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Original implementation for backward compatibility
|
|
81
183
|
// Empty link
|
|
82
184
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
83
185
|
return lessParentheses ? '' : '()';
|
|
@@ -121,35 +223,195 @@ export class Link {
|
|
|
121
223
|
return lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
|
|
122
224
|
}
|
|
123
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Format a value within this link
|
|
228
|
+
* @param {Link} value - The value to format
|
|
229
|
+
* @returns {string} Formatted value string
|
|
230
|
+
*/
|
|
124
231
|
formatValue(value) {
|
|
125
|
-
if (!value.format) {
|
|
126
|
-
return Link.escapeReference(value.id || '');
|
|
232
|
+
if (!value || !value.format) {
|
|
233
|
+
return Link.escapeReference((value && value.id) || '');
|
|
127
234
|
}
|
|
128
|
-
|
|
235
|
+
|
|
129
236
|
// Check if we're in a compound link that was created from path combinations
|
|
130
237
|
// This is indicated by having a parent context passed through
|
|
131
238
|
const isCompoundFromPaths = this._isFromPathCombination === true;
|
|
132
|
-
|
|
239
|
+
|
|
133
240
|
// For compound links from paths, format values with parentheses
|
|
134
241
|
if (isCompoundFromPaths) {
|
|
135
242
|
return value.format(false, true);
|
|
136
243
|
}
|
|
137
|
-
|
|
244
|
+
|
|
138
245
|
// Simple link with just an ID - don't wrap in parentheses when used as a value
|
|
139
246
|
if (!value.values || value.values.length === 0) {
|
|
140
247
|
return Link.escapeReference(value.id);
|
|
141
248
|
}
|
|
142
|
-
|
|
249
|
+
|
|
143
250
|
// Complex value with its own structure - format it normally with parentheses
|
|
144
251
|
return value.format(false, false);
|
|
145
252
|
}
|
|
146
|
-
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if a string needs to be wrapped in parentheses
|
|
256
|
+
* @param {string} str - The string to check
|
|
257
|
+
* @returns {boolean} True if parentheses are needed
|
|
258
|
+
*/
|
|
147
259
|
needsParentheses(str) {
|
|
148
260
|
return str && (str.includes(' ') || str.includes(':') || str.includes('(') || str.includes(')'));
|
|
149
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Format the link using a FormatOptions object
|
|
265
|
+
* @param {FormatOptions} options - FormatOptions object with formatting options
|
|
266
|
+
* @param {boolean} isCompoundValue - If true, this is a value in a compound link
|
|
267
|
+
* @returns {string} Formatted string
|
|
268
|
+
*/
|
|
269
|
+
_formatWithOptions(options, isCompoundValue = false) {
|
|
270
|
+
// Empty link
|
|
271
|
+
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
272
|
+
return options.lessParentheses ? '' : '()';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Link with only ID, no values
|
|
276
|
+
if (!this.values || this.values.length === 0) {
|
|
277
|
+
const escapedId = Link.escapeReference(this.id);
|
|
278
|
+
if (isCompoundValue) {
|
|
279
|
+
return `(${escapedId})`;
|
|
280
|
+
}
|
|
281
|
+
return options.lessParentheses && !this.needsParentheses(this.id) ? escapedId : `(${escapedId})`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check if we should use indented format
|
|
285
|
+
let shouldIndent = false;
|
|
286
|
+
if (options.shouldIndentByRefCount(this.values.length)) {
|
|
287
|
+
shouldIndent = true;
|
|
288
|
+
} else {
|
|
289
|
+
// Try inline format first
|
|
290
|
+
const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
|
|
291
|
+
let testLine;
|
|
292
|
+
if (this.id !== null) {
|
|
293
|
+
const idStr = Link.escapeReference(this.id);
|
|
294
|
+
testLine = options.lessParentheses ? `${idStr}: ${valuesStr}` : `(${idStr}: ${valuesStr})`;
|
|
295
|
+
} else {
|
|
296
|
+
testLine = options.lessParentheses ? valuesStr : `(${valuesStr})`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (options.shouldIndentByLength(testLine)) {
|
|
300
|
+
shouldIndent = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Format with indentation if needed
|
|
305
|
+
if (shouldIndent && options.preferInline === false) {
|
|
306
|
+
return this._formatIndented(options);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Standard inline formatting
|
|
310
|
+
const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
|
|
311
|
+
|
|
312
|
+
// Link with values only (null id)
|
|
313
|
+
if (this.id === null) {
|
|
314
|
+
if (options.lessParentheses) {
|
|
315
|
+
const allSimple = this.values.every(v => !v.values || v.values.length === 0);
|
|
316
|
+
if (allSimple) {
|
|
317
|
+
return this.values.map(v => Link.escapeReference(v.id)).join(' ');
|
|
318
|
+
}
|
|
319
|
+
return valuesStr;
|
|
320
|
+
}
|
|
321
|
+
return `(${valuesStr})`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Link with ID and values
|
|
325
|
+
const idStr = Link.escapeReference(this.id);
|
|
326
|
+
const withColon = `${idStr}: ${valuesStr}`;
|
|
327
|
+
return options.lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Format the link with indentation
|
|
332
|
+
* @param {FormatOptions} options - FormatOptions object with formatting options
|
|
333
|
+
* @returns {string} Indented formatted string
|
|
334
|
+
*/
|
|
335
|
+
_formatIndented(options) {
|
|
336
|
+
if (this.id === null) {
|
|
337
|
+
// Values only - format each on separate line
|
|
338
|
+
const lines = this.values.map(v => options.indentString + this.formatValue(v));
|
|
339
|
+
return lines.join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Link with ID - format as id:\n value1\n value2
|
|
343
|
+
const idStr = Link.escapeReference(this.id);
|
|
344
|
+
const lines = [`${idStr}:`];
|
|
345
|
+
for (const v of this.values) {
|
|
346
|
+
lines.push(options.indentString + this.formatValue(v));
|
|
347
|
+
}
|
|
348
|
+
return lines.join('\n');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Group consecutive links with the same ID
|
|
354
|
+
* @param {Link[]} links - List of links to group
|
|
355
|
+
* @returns {Link[]} New list with consecutive links grouped
|
|
356
|
+
*/
|
|
357
|
+
function _groupConsecutiveLinks(links) {
|
|
358
|
+
if (!links || links.length === 0) {
|
|
359
|
+
return links;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const grouped = [];
|
|
363
|
+
let i = 0;
|
|
364
|
+
|
|
365
|
+
while (i < links.length) {
|
|
366
|
+
const current = links[i];
|
|
367
|
+
|
|
368
|
+
// Look ahead for consecutive links with same ID
|
|
369
|
+
if (current.id !== null && current.values && current.values.length > 0) {
|
|
370
|
+
// Collect all values with same ID
|
|
371
|
+
const sameIdValues = [...current.values];
|
|
372
|
+
let j = i + 1;
|
|
373
|
+
|
|
374
|
+
while (j < links.length) {
|
|
375
|
+
const nextLink = links[j];
|
|
376
|
+
if (nextLink.id === current.id && nextLink.values && nextLink.values.length > 0) {
|
|
377
|
+
sameIdValues.push(...nextLink.values);
|
|
378
|
+
j++;
|
|
379
|
+
} else {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// If we found consecutive links, create grouped link
|
|
385
|
+
if (j > i + 1) {
|
|
386
|
+
const groupedLink = new Link(current.id, sameIdValues);
|
|
387
|
+
grouped.push(groupedLink);
|
|
388
|
+
i = j;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
grouped.push(current);
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return grouped;
|
|
150
398
|
}
|
|
151
399
|
|
|
152
400
|
export function formatLinks(links, lessParentheses = false) {
|
|
153
401
|
if (!links || links.length === 0) return '';
|
|
402
|
+
|
|
403
|
+
// Support FormatOptions/FormatConfig as parameter
|
|
404
|
+
if (lessParentheses && typeof lessParentheses === 'object' &&
|
|
405
|
+
(lessParentheses.constructor.name === 'FormatOptions' || lessParentheses.constructor.name === 'FormatConfig')) {
|
|
406
|
+
const options = lessParentheses;
|
|
407
|
+
// Apply consecutive link grouping if enabled
|
|
408
|
+
let linksToFormat = links;
|
|
409
|
+
if (options.groupConsecutive) {
|
|
410
|
+
linksToFormat = _groupConsecutiveLinks(links);
|
|
411
|
+
}
|
|
412
|
+
return linksToFormat.map(link => link.format(options)).join('\n');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Backward compatibility with boolean parameter
|
|
154
416
|
return links.map(link => link.format(lessParentheses)).join('\n');
|
|
155
417
|
}
|
package/src/Parser.js
CHANGED
|
@@ -2,24 +2,52 @@ 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(`Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`);
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
try {
|
|
10
33
|
const rawResult = parserModule.parse(input);
|
|
11
34
|
return this.transformResult(rawResult);
|
|
12
35
|
} catch (error) {
|
|
13
|
-
|
|
36
|
+
// Preserve original error information
|
|
37
|
+
const parseError = new Error(`Parse error: ${error.message}`);
|
|
38
|
+
parseError.cause = error;
|
|
39
|
+
parseError.location = error.location;
|
|
40
|
+
throw parseError;
|
|
14
41
|
}
|
|
15
42
|
}
|
|
16
43
|
|
|
17
44
|
transformResult(rawResult) {
|
|
18
45
|
const links = [];
|
|
19
46
|
const items = Array.isArray(rawResult) ? rawResult : [rawResult];
|
|
20
|
-
|
|
47
|
+
|
|
21
48
|
for (const item of items) {
|
|
22
|
-
|
|
49
|
+
// Use explicit null/undefined check
|
|
50
|
+
if (item !== null && item !== undefined) {
|
|
23
51
|
this.collectLinks(item, [], links);
|
|
24
52
|
}
|
|
25
53
|
}
|
|
@@ -27,7 +55,8 @@ export class Parser {
|
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
collectLinks(item, parentPath, result) {
|
|
30
|
-
|
|
58
|
+
// Use explicit null/undefined check
|
|
59
|
+
if (item === null || item === undefined) return;
|
|
31
60
|
|
|
32
61
|
// For items with children (indented structure)
|
|
33
62
|
if (item.children && item.children.length > 0) {
|
|
@@ -105,9 +134,15 @@ export class Parser {
|
|
|
105
134
|
}
|
|
106
135
|
|
|
107
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Transform a parsed item into a Link object
|
|
139
|
+
* @param {*} item - The item to transform
|
|
140
|
+
* @returns {Link|null} The transformed Link or null
|
|
141
|
+
*/
|
|
108
142
|
transformLink(item) {
|
|
109
|
-
|
|
110
|
-
|
|
143
|
+
// Use explicit null/undefined check
|
|
144
|
+
if (item === null || item === undefined) return null;
|
|
145
|
+
|
|
111
146
|
if (item instanceof Link) {
|
|
112
147
|
return item;
|
|
113
148
|
}
|
|
@@ -124,7 +159,7 @@ export class Parser {
|
|
|
124
159
|
link.values = item.values.map(v => this.transformLink(v));
|
|
125
160
|
return link;
|
|
126
161
|
}
|
|
127
|
-
|
|
162
|
+
|
|
128
163
|
// Default case
|
|
129
164
|
return new Link(item.id || null, []);
|
|
130
165
|
}
|
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';
|
package/tests/ApiTests.test.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { test, expect } from 'bun:test';
|
|
2
2
|
import { Parser } from '../src/Parser.js';
|
|
3
|
-
import { Link } from '../src/Link.js';
|
|
3
|
+
import { Link, formatLinks } from '../src/Link.js';
|
|
4
|
+
import { FormatOptions } from '../src/FormatOptions.js';
|
|
4
5
|
|
|
5
6
|
const parser = new Parser();
|
|
6
7
|
|
|
8
|
+
test('test_is_ref', () => {
|
|
9
|
+
// JS doesn't have separate Ref/Link types, but we can test simple link behavior
|
|
10
|
+
const simpleLink = new Link('some_value');
|
|
11
|
+
expect(simpleLink.id).toBe('some_value');
|
|
12
|
+
expect(simpleLink.values).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('test_is_link', () => {
|
|
16
|
+
// Test link with values
|
|
17
|
+
const link = new Link('id', [new Link('child')]);
|
|
18
|
+
expect(link.id).toBe('id');
|
|
19
|
+
expect(link.values.length).toBe(1);
|
|
20
|
+
expect(link.values[0].id).toBe('child');
|
|
21
|
+
});
|
|
22
|
+
|
|
7
23
|
test('test_is_ref equivalent', () => {
|
|
8
24
|
// JS doesn't have separate Ref/Link types, but we can test simple link behavior
|
|
9
25
|
const simpleLink = new Link('some_value');
|
|
@@ -28,7 +44,7 @@ test('test_empty_link', () => {
|
|
|
28
44
|
test('test_simple_link', () => {
|
|
29
45
|
const input = '(1: 1 1)';
|
|
30
46
|
const parsed = parser.parse(input);
|
|
31
|
-
|
|
47
|
+
|
|
32
48
|
// Validate regular formatting
|
|
33
49
|
const output = parsed[0].format();
|
|
34
50
|
const expected = '(1: 1 1)'; // JS format matches input
|
|
@@ -38,7 +54,7 @@ test('test_simple_link', () => {
|
|
|
38
54
|
test('test_link_with_source_target', () => {
|
|
39
55
|
const input = '(index: source target)';
|
|
40
56
|
const parsed = parser.parse(input);
|
|
41
|
-
|
|
57
|
+
|
|
42
58
|
// Validate regular formatting
|
|
43
59
|
const output = parsed[0].format();
|
|
44
60
|
expect(output).toBe(input);
|
|
@@ -47,7 +63,7 @@ test('test_link_with_source_target', () => {
|
|
|
47
63
|
test('test_link_with_source_type_target', () => {
|
|
48
64
|
const input = '(index: source type target)';
|
|
49
65
|
const parsed = parser.parse(input);
|
|
50
|
-
|
|
66
|
+
|
|
51
67
|
// Validate regular formatting
|
|
52
68
|
const output = parsed[0].format();
|
|
53
69
|
expect(output).toBe(input);
|
|
@@ -56,7 +72,7 @@ test('test_link_with_source_type_target', () => {
|
|
|
56
72
|
test('test_single_line_format', () => {
|
|
57
73
|
const input = 'id: value1 value2';
|
|
58
74
|
const parsed = parser.parse(input);
|
|
59
|
-
|
|
75
|
+
|
|
60
76
|
// The parser should handle single-line format
|
|
61
77
|
const output = parsed[0].format(true); // lessParentheses mode
|
|
62
78
|
expect(output).toContain('id');
|
|
@@ -67,8 +83,89 @@ test('test_single_line_format', () => {
|
|
|
67
83
|
test('test_quoted_references', () => {
|
|
68
84
|
const input = '("quoted id": "value with spaces")';
|
|
69
85
|
const parsed = parser.parse(input);
|
|
70
|
-
|
|
86
|
+
|
|
71
87
|
const output = parsed[0].format();
|
|
72
88
|
expect(output).toContain('quoted id');
|
|
73
89
|
expect(output).toContain('value with spaces');
|
|
74
|
-
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('test_quoted_references_parsing', () => {
|
|
93
|
+
// Test that quoted references are parsed correctly
|
|
94
|
+
const input = '("quoted id": "value with spaces")';
|
|
95
|
+
const parsed = parser.parse(input);
|
|
96
|
+
|
|
97
|
+
// Verify parsing worked correctly
|
|
98
|
+
const output = formatLinks(parsed);
|
|
99
|
+
expect(output).toContain('quoted id');
|
|
100
|
+
expect(output).toContain('value with spaces');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('test_indented_id_syntax_parsing', () => {
|
|
104
|
+
// Test that indented ID syntax is parsed correctly
|
|
105
|
+
const indented = `id:
|
|
106
|
+
value1
|
|
107
|
+
value2`;
|
|
108
|
+
const inline = '(id: value1 value2)';
|
|
109
|
+
|
|
110
|
+
const indentedParsed = parser.parse(indented);
|
|
111
|
+
const inlineParsed = parser.parse(inline);
|
|
112
|
+
|
|
113
|
+
// Both should produce equivalent structures
|
|
114
|
+
const indentedOutput = formatLinks(indentedParsed);
|
|
115
|
+
const inlineOutput = formatLinks(inlineParsed);
|
|
116
|
+
expect(indentedOutput).toBe(inlineOutput);
|
|
117
|
+
expect(indentedOutput).toBe('(id: value1 value2)');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('test_indented_id_syntax_roundtrip', () => {
|
|
121
|
+
const input = `id:
|
|
122
|
+
value1
|
|
123
|
+
value2`;
|
|
124
|
+
const parsed = parser.parse(input);
|
|
125
|
+
|
|
126
|
+
// Validate that we can format with indented syntax using FormatOptions
|
|
127
|
+
const options = new FormatOptions();
|
|
128
|
+
options.maxInlineRefs = 1; // Force indentation with more than 1 ref
|
|
129
|
+
options.preferInline = false;
|
|
130
|
+
const output = formatLinks(parsed, options);
|
|
131
|
+
expect(output).toBe(input);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('test_multiple_indented_id_syntax_parsing', () => {
|
|
135
|
+
// Test that multiple indented ID links are parsed correctly
|
|
136
|
+
const indented = `id1:
|
|
137
|
+
a
|
|
138
|
+
b
|
|
139
|
+
id2:
|
|
140
|
+
c
|
|
141
|
+
d`;
|
|
142
|
+
const inline = `(id1: a b)
|
|
143
|
+
(id2: c d)`;
|
|
144
|
+
|
|
145
|
+
const indentedParsed = parser.parse(indented);
|
|
146
|
+
const inlineParsed = parser.parse(inline);
|
|
147
|
+
|
|
148
|
+
// Both should produce equivalent structures
|
|
149
|
+
const indentedOutput = formatLinks(indentedParsed);
|
|
150
|
+
const inlineOutput = formatLinks(inlineParsed);
|
|
151
|
+
expect(indentedOutput).toBe(inlineOutput);
|
|
152
|
+
expect(indentedOutput).toBe(`(id1: a b)
|
|
153
|
+
(id2: c d)`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('test_multiple_indented_id_syntax_roundtrip', () => {
|
|
157
|
+
const input = `id1:
|
|
158
|
+
a
|
|
159
|
+
b
|
|
160
|
+
id2:
|
|
161
|
+
c
|
|
162
|
+
d`;
|
|
163
|
+
const parsed = parser.parse(input);
|
|
164
|
+
|
|
165
|
+
// Validate that we can format with indented syntax using FormatOptions
|
|
166
|
+
const options = new FormatOptions();
|
|
167
|
+
options.maxInlineRefs = 1; // Force indentation with more than 1 ref
|
|
168
|
+
options.preferInline = false;
|
|
169
|
+
const output = formatLinks(parsed, options);
|
|
170
|
+
expect(output).toBe(input);
|
|
171
|
+
});
|