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/bun.lock
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
class Link {
|
|
3
3
|
constructor(id = null, values = null) {
|
|
4
4
|
this.id = id;
|
|
5
|
-
|
|
5
|
+
if (values !== null && values !== undefined) {
|
|
6
|
+
if (!Array.isArray(values)) {
|
|
7
|
+
throw new TypeError("values must be an array or null");
|
|
8
|
+
}
|
|
9
|
+
this.values = values;
|
|
10
|
+
} else {
|
|
11
|
+
this.values = [];
|
|
12
|
+
}
|
|
6
13
|
}
|
|
7
14
|
toString() {
|
|
8
15
|
return this.format(false);
|
|
@@ -16,7 +23,9 @@ class Link {
|
|
|
16
23
|
} else if (this.values.length === 1) {
|
|
17
24
|
return this.values[0];
|
|
18
25
|
} else {
|
|
19
|
-
const newValues = this.values.map((v) =>
|
|
26
|
+
const newValues = this.values.map((v) => {
|
|
27
|
+
return v && typeof v.simplify === "function" ? v.simplify() : v;
|
|
28
|
+
});
|
|
20
29
|
return new Link(this.id, newValues);
|
|
21
30
|
}
|
|
22
31
|
}
|
|
@@ -24,21 +33,29 @@ class Link {
|
|
|
24
33
|
return new Link(null, [this, other]);
|
|
25
34
|
}
|
|
26
35
|
static getValueString(value) {
|
|
27
|
-
return value.toLinkOrIdString();
|
|
36
|
+
return value && typeof value.toLinkOrIdString === "function" ? value.toLinkOrIdString() : String(value);
|
|
28
37
|
}
|
|
29
38
|
static escapeReference(reference) {
|
|
30
39
|
if (!reference || reference.trim() === "") {
|
|
31
40
|
return "";
|
|
32
41
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
const hasSingleQuote = reference.includes("'");
|
|
43
|
+
const hasDoubleQuote = reference.includes('"');
|
|
44
|
+
const needsQuoting = reference.includes(":") || reference.includes("(") || reference.includes(")") || reference.includes(" ") || reference.includes("\t") || reference.includes(`
|
|
45
|
+
`) || reference.includes("\r") || hasDoubleQuote || hasSingleQuote;
|
|
46
|
+
if (hasSingleQuote && hasDoubleQuote) {
|
|
47
|
+
return `'${reference.replace(/'/g, "\\'")}'`;
|
|
48
|
+
}
|
|
49
|
+
if (hasDoubleQuote) {
|
|
36
50
|
return `'${reference}'`;
|
|
37
|
-
}
|
|
51
|
+
}
|
|
52
|
+
if (hasSingleQuote) {
|
|
38
53
|
return `"${reference}"`;
|
|
39
|
-
} else {
|
|
40
|
-
return reference;
|
|
41
54
|
}
|
|
55
|
+
if (needsQuoting) {
|
|
56
|
+
return `'${reference}'`;
|
|
57
|
+
}
|
|
58
|
+
return reference;
|
|
42
59
|
}
|
|
43
60
|
toLinkOrIdString() {
|
|
44
61
|
if (!this.values || this.values.length === 0) {
|
|
@@ -51,16 +68,27 @@ class Link {
|
|
|
51
68
|
return false;
|
|
52
69
|
if (this.id !== other.id)
|
|
53
70
|
return false;
|
|
54
|
-
|
|
71
|
+
const thisValues = this.values || [];
|
|
72
|
+
const otherValues = other.values || [];
|
|
73
|
+
if (thisValues.length !== otherValues.length)
|
|
55
74
|
return false;
|
|
56
|
-
for (let i = 0;i <
|
|
57
|
-
if (
|
|
58
|
-
|
|
75
|
+
for (let i = 0;i < thisValues.length; i++) {
|
|
76
|
+
if (thisValues[i] && typeof thisValues[i].equals === "function") {
|
|
77
|
+
if (!thisValues[i].equals(otherValues[i])) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (thisValues[i] !== otherValues[i]) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
return true;
|
|
62
87
|
}
|
|
63
88
|
format(lessParentheses = false, isCompoundValue = false) {
|
|
89
|
+
if (lessParentheses && typeof lessParentheses === "object" && (lessParentheses.constructor.name === "FormatOptions" || lessParentheses.constructor.name === "FormatConfig")) {
|
|
90
|
+
return this._formatWithOptions(lessParentheses, isCompoundValue);
|
|
91
|
+
}
|
|
64
92
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
65
93
|
return lessParentheses ? "" : "()";
|
|
66
94
|
}
|
|
@@ -88,8 +116,8 @@ class Link {
|
|
|
88
116
|
return lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
|
|
89
117
|
}
|
|
90
118
|
formatValue(value) {
|
|
91
|
-
if (!value.format) {
|
|
92
|
-
return Link.escapeReference(value.id || "");
|
|
119
|
+
if (!value || !value.format) {
|
|
120
|
+
return Link.escapeReference(value && value.id || "");
|
|
93
121
|
}
|
|
94
122
|
const isCompoundFromPaths = this._isFromPathCombination === true;
|
|
95
123
|
if (isCompoundFromPaths) {
|
|
@@ -103,10 +131,110 @@ class Link {
|
|
|
103
131
|
needsParentheses(str) {
|
|
104
132
|
return str && (str.includes(" ") || str.includes(":") || str.includes("(") || str.includes(")"));
|
|
105
133
|
}
|
|
134
|
+
_formatWithOptions(options, isCompoundValue = false) {
|
|
135
|
+
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
136
|
+
return options.lessParentheses ? "" : "()";
|
|
137
|
+
}
|
|
138
|
+
if (!this.values || this.values.length === 0) {
|
|
139
|
+
const escapedId = Link.escapeReference(this.id);
|
|
140
|
+
if (isCompoundValue) {
|
|
141
|
+
return `(${escapedId})`;
|
|
142
|
+
}
|
|
143
|
+
return options.lessParentheses && !this.needsParentheses(this.id) ? escapedId : `(${escapedId})`;
|
|
144
|
+
}
|
|
145
|
+
let shouldIndent = false;
|
|
146
|
+
if (options.shouldIndentByRefCount(this.values.length)) {
|
|
147
|
+
shouldIndent = true;
|
|
148
|
+
} else {
|
|
149
|
+
const valuesStr2 = this.values.map((v) => this.formatValue(v)).join(" ");
|
|
150
|
+
let testLine;
|
|
151
|
+
if (this.id !== null) {
|
|
152
|
+
const idStr2 = Link.escapeReference(this.id);
|
|
153
|
+
testLine = options.lessParentheses ? `${idStr2}: ${valuesStr2}` : `(${idStr2}: ${valuesStr2})`;
|
|
154
|
+
} else {
|
|
155
|
+
testLine = options.lessParentheses ? valuesStr2 : `(${valuesStr2})`;
|
|
156
|
+
}
|
|
157
|
+
if (options.shouldIndentByLength(testLine)) {
|
|
158
|
+
shouldIndent = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (shouldIndent && options.preferInline === false) {
|
|
162
|
+
return this._formatIndented(options);
|
|
163
|
+
}
|
|
164
|
+
const valuesStr = this.values.map((v) => this.formatValue(v)).join(" ");
|
|
165
|
+
if (this.id === null) {
|
|
166
|
+
if (options.lessParentheses) {
|
|
167
|
+
const allSimple = this.values.every((v) => !v.values || v.values.length === 0);
|
|
168
|
+
if (allSimple) {
|
|
169
|
+
return this.values.map((v) => Link.escapeReference(v.id)).join(" ");
|
|
170
|
+
}
|
|
171
|
+
return valuesStr;
|
|
172
|
+
}
|
|
173
|
+
return `(${valuesStr})`;
|
|
174
|
+
}
|
|
175
|
+
const idStr = Link.escapeReference(this.id);
|
|
176
|
+
const withColon = `${idStr}: ${valuesStr}`;
|
|
177
|
+
return options.lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
|
|
178
|
+
}
|
|
179
|
+
_formatIndented(options) {
|
|
180
|
+
if (this.id === null) {
|
|
181
|
+
const lines2 = this.values.map((v) => options.indentString + this.formatValue(v));
|
|
182
|
+
return lines2.join(`
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
const idStr = Link.escapeReference(this.id);
|
|
186
|
+
const lines = [`${idStr}:`];
|
|
187
|
+
for (const v of this.values) {
|
|
188
|
+
lines.push(options.indentString + this.formatValue(v));
|
|
189
|
+
}
|
|
190
|
+
return lines.join(`
|
|
191
|
+
`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function _groupConsecutiveLinks(links) {
|
|
195
|
+
if (!links || links.length === 0) {
|
|
196
|
+
return links;
|
|
197
|
+
}
|
|
198
|
+
const grouped = [];
|
|
199
|
+
let i = 0;
|
|
200
|
+
while (i < links.length) {
|
|
201
|
+
const current = links[i];
|
|
202
|
+
if (current.id !== null && current.values && current.values.length > 0) {
|
|
203
|
+
const sameIdValues = [...current.values];
|
|
204
|
+
let j = i + 1;
|
|
205
|
+
while (j < links.length) {
|
|
206
|
+
const nextLink = links[j];
|
|
207
|
+
if (nextLink.id === current.id && nextLink.values && nextLink.values.length > 0) {
|
|
208
|
+
sameIdValues.push(...nextLink.values);
|
|
209
|
+
j++;
|
|
210
|
+
} else {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (j > i + 1) {
|
|
215
|
+
const groupedLink = new Link(current.id, sameIdValues);
|
|
216
|
+
grouped.push(groupedLink);
|
|
217
|
+
i = j;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
grouped.push(current);
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
return grouped;
|
|
106
225
|
}
|
|
107
226
|
function formatLinks(links, lessParentheses = false) {
|
|
108
227
|
if (!links || links.length === 0)
|
|
109
228
|
return "";
|
|
229
|
+
if (lessParentheses && typeof lessParentheses === "object" && (lessParentheses.constructor.name === "FormatOptions" || lessParentheses.constructor.name === "FormatConfig")) {
|
|
230
|
+
const options = lessParentheses;
|
|
231
|
+
let linksToFormat = links;
|
|
232
|
+
if (options.groupConsecutive) {
|
|
233
|
+
linksToFormat = _groupConsecutiveLinks(links);
|
|
234
|
+
}
|
|
235
|
+
return linksToFormat.map((link) => link.format(options)).join(`
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
110
238
|
return links.map((link) => link.format(lessParentheses)).join(`
|
|
111
239
|
`);
|
|
112
240
|
}
|
|
@@ -1532,27 +1660,39 @@ function peg$parse(input, options) {
|
|
|
1532
1660
|
|
|
1533
1661
|
// src/Parser.js
|
|
1534
1662
|
class Parser {
|
|
1535
|
-
constructor() {
|
|
1663
|
+
constructor(options = {}) {
|
|
1664
|
+
this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024;
|
|
1665
|
+
this.maxDepth = options.maxDepth || 1000;
|
|
1666
|
+
}
|
|
1536
1667
|
parse(input) {
|
|
1668
|
+
if (typeof input !== "string") {
|
|
1669
|
+
throw new TypeError("Input must be a string");
|
|
1670
|
+
}
|
|
1671
|
+
if (input.length > this.maxInputSize) {
|
|
1672
|
+
throw new Error(`Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`);
|
|
1673
|
+
}
|
|
1537
1674
|
try {
|
|
1538
1675
|
const rawResult = peg$parse(input);
|
|
1539
1676
|
return this.transformResult(rawResult);
|
|
1540
1677
|
} catch (error) {
|
|
1541
|
-
|
|
1678
|
+
const parseError = new Error(`Parse error: ${error.message}`);
|
|
1679
|
+
parseError.cause = error;
|
|
1680
|
+
parseError.location = error.location;
|
|
1681
|
+
throw parseError;
|
|
1542
1682
|
}
|
|
1543
1683
|
}
|
|
1544
1684
|
transformResult(rawResult) {
|
|
1545
1685
|
const links = [];
|
|
1546
1686
|
const items = Array.isArray(rawResult) ? rawResult : [rawResult];
|
|
1547
1687
|
for (const item of items) {
|
|
1548
|
-
if (item) {
|
|
1688
|
+
if (item !== null && item !== undefined) {
|
|
1549
1689
|
this.collectLinks(item, [], links);
|
|
1550
1690
|
}
|
|
1551
1691
|
}
|
|
1552
1692
|
return links;
|
|
1553
1693
|
}
|
|
1554
1694
|
collectLinks(item, parentPath, result) {
|
|
1555
|
-
if (
|
|
1695
|
+
if (item === null || item === undefined)
|
|
1556
1696
|
return;
|
|
1557
1697
|
if (item.children && item.children.length > 0) {
|
|
1558
1698
|
if (item.id && (!item.values || item.values.length === 0)) {
|
|
@@ -1609,7 +1749,7 @@ class Parser {
|
|
|
1609
1749
|
return combined;
|
|
1610
1750
|
}
|
|
1611
1751
|
transformLink(item) {
|
|
1612
|
-
if (
|
|
1752
|
+
if (item === null || item === undefined)
|
|
1613
1753
|
return null;
|
|
1614
1754
|
if (item instanceof Link) {
|
|
1615
1755
|
return item;
|
|
@@ -1625,9 +1765,39 @@ class Parser {
|
|
|
1625
1765
|
return new Link(item.id || null, []);
|
|
1626
1766
|
}
|
|
1627
1767
|
}
|
|
1768
|
+
// src/FormatOptions.js
|
|
1769
|
+
class FormatOptions {
|
|
1770
|
+
constructor(options = {}) {
|
|
1771
|
+
this.lessParentheses = options.lessParentheses ?? false;
|
|
1772
|
+
this.maxLineLength = options.maxLineLength ?? 80;
|
|
1773
|
+
this.indentLongLines = options.indentLongLines ?? false;
|
|
1774
|
+
this.maxInlineRefs = options.maxInlineRefs ?? null;
|
|
1775
|
+
this.groupConsecutive = options.groupConsecutive ?? false;
|
|
1776
|
+
this.indentString = options.indentString ?? " ";
|
|
1777
|
+
this.preferInline = options.preferInline ?? true;
|
|
1778
|
+
}
|
|
1779
|
+
shouldIndentByLength(line) {
|
|
1780
|
+
if (!this.indentLongLines) {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
return line.length > this.maxLineLength;
|
|
1784
|
+
}
|
|
1785
|
+
shouldIndentByRefCount(refCount) {
|
|
1786
|
+
if (this.maxInlineRefs === null) {
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
return refCount > this.maxInlineRefs;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/FormatConfig.js
|
|
1794
|
+
class FormatConfig extends FormatOptions {
|
|
1795
|
+
}
|
|
1628
1796
|
export {
|
|
1629
1797
|
formatLinks,
|
|
1630
1798
|
Parser,
|
|
1631
1799
|
LinksGroup,
|
|
1632
|
-
Link
|
|
1800
|
+
Link,
|
|
1801
|
+
FormatOptions,
|
|
1802
|
+
FormatConfig
|
|
1633
1803
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormatConfig is an alias for FormatOptions to maintain API consistency
|
|
3
|
+
* with the Python implementation.
|
|
4
|
+
*
|
|
5
|
+
* JavaScript uses 'FormatOptions' internally, but exports 'FormatConfig'
|
|
6
|
+
* as the primary name to match Python's naming convention.
|
|
7
|
+
*/
|
|
8
|
+
import { FormatOptions } from './FormatOptions.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* FormatConfig for Lino notation formatting.
|
|
12
|
+
*
|
|
13
|
+
* This is an alias for FormatOptions to maintain API consistency across languages.
|
|
14
|
+
* Python uses 'FormatConfig', so this export provides the same name for JavaScript.
|
|
15
|
+
*/
|
|
16
|
+
export class FormatConfig extends FormatOptions {
|
|
17
|
+
// FormatConfig inherits all functionality from FormatOptions
|
|
18
|
+
// No additional implementation needed - this is purely for naming consistency
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormatOptions for Lino notation formatting.
|
|
3
|
+
*
|
|
4
|
+
* Provides configuration options for controlling how Link objects are formatted.
|
|
5
|
+
*/
|
|
6
|
+
export class FormatOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Create a new FormatOptions instance
|
|
9
|
+
* @param {Object} options - Configuration options
|
|
10
|
+
* @param {boolean} [options.lessParentheses=false] - If true, omit parentheses where safe
|
|
11
|
+
* @param {number} [options.maxLineLength=80] - Maximum line length before auto-indenting
|
|
12
|
+
* @param {boolean} [options.indentLongLines=false] - If true, indent lines exceeding maxLineLength
|
|
13
|
+
* @param {number|null} [options.maxInlineRefs=null] - Maximum number of references before auto-indenting
|
|
14
|
+
* @param {boolean} [options.groupConsecutive=false] - If true, group consecutive links with same ID
|
|
15
|
+
* @param {string} [options.indentString=" "] - String to use for indentation
|
|
16
|
+
* @param {boolean} [options.preferInline=true] - If true, prefer inline format when under thresholds
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.lessParentheses = options.lessParentheses ?? false;
|
|
20
|
+
this.maxLineLength = options.maxLineLength ?? 80;
|
|
21
|
+
this.indentLongLines = options.indentLongLines ?? false;
|
|
22
|
+
this.maxInlineRefs = options.maxInlineRefs ?? null;
|
|
23
|
+
this.groupConsecutive = options.groupConsecutive ?? false;
|
|
24
|
+
this.indentString = options.indentString ?? " ";
|
|
25
|
+
this.preferInline = options.preferInline ?? true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if line should be indented based on length
|
|
30
|
+
* @param {string} line - The line to check
|
|
31
|
+
* @returns {boolean} True if line should be indented based on length threshold
|
|
32
|
+
*/
|
|
33
|
+
shouldIndentByLength(line) {
|
|
34
|
+
if (!this.indentLongLines) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Count printable unicode characters
|
|
38
|
+
return line.length > this.maxLineLength;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if link should be indented based on reference count
|
|
43
|
+
* @param {number} refCount - Number of references in the link
|
|
44
|
+
* @returns {boolean} True if link should be indented based on reference count threshold
|
|
45
|
+
*/
|
|
46
|
+
shouldIndentByRefCount(refCount) {
|
|
47
|
+
if (this.maxInlineRefs === null) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return refCount > this.maxInlineRefs;
|
|
51
|
+
}
|
|
52
|
+
}
|