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 CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "@linksplatform/protocols-lino",
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
- this.values = values || [];
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) => v.simplify());
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 needsSingleQuotes = reference.includes(":") || reference.includes("(") || reference.includes(")") || reference.includes(" ") || reference.includes("\t") || reference.includes(`
34
- `) || reference.includes("\r") || reference.includes('"');
35
- if (needsSingleQuotes) {
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
- } else if (reference.includes("'")) {
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
- if (this.values.length !== other.values.length)
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 < this.values.length; i++) {
57
- if (!this.values[i].equals(other.values[i])) {
58
- return false;
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
- throw new Error(`Parse error: ${error.message}`);
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 (!item)
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 (!item)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "links-notation",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Links Notation parser for JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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
+ }