links-notation 0.12.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "links-notation",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Links Notation parser for JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -8,7 +8,9 @@
8
8
  "build": "bun run build:grammar && bun build src/index.js --outdir=dist --target=node",
9
9
  "build:grammar": "peggy --format es -o src/parser-generated.js src/grammar.pegjs",
10
10
  "test": "bun test",
11
- "test:watch": "bun test --watch"
11
+ "test:watch": "bun test --watch",
12
+ "lint": "eslint src/ tests/",
13
+ "lint:fix": "eslint src/ tests/ --fix"
12
14
  },
13
15
  "keywords": [
14
16
  "lino",
@@ -20,7 +22,8 @@
20
22
  "license": "Unlicense",
21
23
  "devDependencies": {
22
24
  "bun-types": "^1.2.19",
25
+ "eslint": "^8.56.0",
23
26
  "peggy": "^5.0.6"
24
27
  },
25
28
  "dependencies": {}
26
- }
29
+ }
@@ -21,7 +21,7 @@ export class FormatOptions {
21
21
  this.indentLongLines = options.indentLongLines ?? false;
22
22
  this.maxInlineRefs = options.maxInlineRefs ?? null;
23
23
  this.groupConsecutive = options.groupConsecutive ?? false;
24
- this.indentString = options.indentString ?? " ";
24
+ this.indentString = options.indentString ?? ' ';
25
25
  this.preferInline = options.preferInline ?? true;
26
26
  }
27
27
 
package/src/Link.js CHANGED
@@ -32,8 +32,9 @@ export class Link {
32
32
  * @returns {string} Space-separated string of values
33
33
  */
34
34
  getValuesString() {
35
- return (!this.values || this.values.length === 0) ?
36
- '' : this.values.map(v => Link.getValueString(v)).join(' ');
35
+ return !this.values || this.values.length === 0
36
+ ? ''
37
+ : this.values.map((v) => Link.getValueString(v)).join(' ');
37
38
  }
38
39
 
39
40
  /**
@@ -46,7 +47,7 @@ export class Link {
46
47
  } else if (this.values.length === 1) {
47
48
  return this.values[0];
48
49
  } else {
49
- const newValues = this.values.map(v => {
50
+ const newValues = this.values.map((v) => {
50
51
  // Check if value has simplify method (defensive programming)
51
52
  return v && typeof v.simplify === 'function' ? v.simplify() : v;
52
53
  });
@@ -70,7 +71,9 @@ export class Link {
70
71
  */
71
72
  static getValueString(value) {
72
73
  // Defensive check for method existence
73
- return value && typeof value.toLinkOrIdString === 'function' ? value.toLinkOrIdString() : String(value);
74
+ return value && typeof value.toLinkOrIdString === 'function'
75
+ ? value.toLinkOrIdString()
76
+ : String(value);
74
77
  }
75
78
 
76
79
  /**
@@ -164,7 +167,6 @@ export class Link {
164
167
  return true;
165
168
  }
166
169
 
167
-
168
170
  /**
169
171
  * Format the link as a string
170
172
  * @param {boolean|FormatOptions} lessParentheses - If true, omit parentheses where safe; or a FormatOptions object
@@ -174,8 +176,12 @@ export class Link {
174
176
  format(lessParentheses = false, isCompoundValue = false) {
175
177
  // Support FormatOptions/FormatConfig as first parameter
176
178
  // 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
+ if (
180
+ lessParentheses &&
181
+ typeof lessParentheses === 'object' &&
182
+ (lessParentheses.constructor.name === 'FormatOptions' ||
183
+ lessParentheses.constructor.name === 'FormatConfig')
184
+ ) {
179
185
  return this._formatWithOptions(lessParentheses, isCompoundValue);
180
186
  }
181
187
 
@@ -184,7 +190,7 @@ export class Link {
184
190
  if (this.id === null && (!this.values || this.values.length === 0)) {
185
191
  return lessParentheses ? '' : '()';
186
192
  }
187
-
193
+
188
194
  // Link with only ID, no values
189
195
  if (!this.values || this.values.length === 0) {
190
196
  const escapedId = Link.escapeReference(this.id);
@@ -192,21 +198,27 @@ export class Link {
192
198
  if (isCompoundValue) {
193
199
  return `(${escapedId})`;
194
200
  }
195
- return lessParentheses && !this.needsParentheses(this.id) ? escapedId : `(${escapedId})`;
201
+ return lessParentheses && !this.needsParentheses(this.id)
202
+ ? escapedId
203
+ : `(${escapedId})`;
196
204
  }
197
-
198
- // Format values recursively
199
- const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
200
-
205
+
206
+ // Format values recursively
207
+ const valuesStr = this.values.map((v) => this.formatValue(v)).join(' ');
208
+
201
209
  // Link with values only (null id)
202
210
  if (this.id === null) {
203
211
  // For lessParentheses mode with simple values, don't wrap the whole thing
204
212
  if (lessParentheses) {
205
213
  // Check if all values are simple (no nested values)
206
- const allSimple = this.values.every(v => !v.values || v.values.length === 0);
214
+ const allSimple = this.values.every(
215
+ (v) => !v.values || v.values.length === 0
216
+ );
207
217
  if (allSimple) {
208
218
  // Format each value without extra wrapping
209
- const simpleValuesStr = this.values.map(v => Link.escapeReference(v.id)).join(' ');
219
+ const simpleValuesStr = this.values
220
+ .map((v) => Link.escapeReference(v.id))
221
+ .join(' ');
210
222
  return simpleValuesStr;
211
223
  }
212
224
  // For mixed or complex values in lessParentheses mode, still avoid outer wrapper
@@ -216,13 +228,15 @@ export class Link {
216
228
  // For normal mode, wrap in parentheses
217
229
  return `(${valuesStr})`;
218
230
  }
219
-
231
+
220
232
  // Link with ID and values
221
233
  const idStr = Link.escapeReference(this.id);
222
234
  const withColon = `${idStr}: ${valuesStr}`;
223
- return lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
235
+ return lessParentheses && !this.needsParentheses(this.id)
236
+ ? withColon
237
+ : `(${withColon})`;
224
238
  }
225
-
239
+
226
240
  /**
227
241
  * Format a value within this link
228
242
  * @param {Link} value - The value to format
@@ -257,7 +271,13 @@ export class Link {
257
271
  * @returns {boolean} True if parentheses are needed
258
272
  */
259
273
  needsParentheses(str) {
260
- return str && (str.includes(' ') || str.includes(':') || str.includes('(') || str.includes(')'));
274
+ return (
275
+ str &&
276
+ (str.includes(' ') ||
277
+ str.includes(':') ||
278
+ str.includes('(') ||
279
+ str.includes(')'))
280
+ );
261
281
  }
262
282
 
263
283
  /**
@@ -278,7 +298,9 @@ export class Link {
278
298
  if (isCompoundValue) {
279
299
  return `(${escapedId})`;
280
300
  }
281
- return options.lessParentheses && !this.needsParentheses(this.id) ? escapedId : `(${escapedId})`;
301
+ return options.lessParentheses && !this.needsParentheses(this.id)
302
+ ? escapedId
303
+ : `(${escapedId})`;
282
304
  }
283
305
 
284
306
  // Check if we should use indented format
@@ -287,11 +309,13 @@ export class Link {
287
309
  shouldIndent = true;
288
310
  } else {
289
311
  // Try inline format first
290
- const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
312
+ const valuesStr = this.values.map((v) => this.formatValue(v)).join(' ');
291
313
  let testLine;
292
314
  if (this.id !== null) {
293
315
  const idStr = Link.escapeReference(this.id);
294
- testLine = options.lessParentheses ? `${idStr}: ${valuesStr}` : `(${idStr}: ${valuesStr})`;
316
+ testLine = options.lessParentheses
317
+ ? `${idStr}: ${valuesStr}`
318
+ : `(${idStr}: ${valuesStr})`;
295
319
  } else {
296
320
  testLine = options.lessParentheses ? valuesStr : `(${valuesStr})`;
297
321
  }
@@ -307,14 +331,16 @@ export class Link {
307
331
  }
308
332
 
309
333
  // Standard inline formatting
310
- const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
334
+ const valuesStr = this.values.map((v) => this.formatValue(v)).join(' ');
311
335
 
312
336
  // Link with values only (null id)
313
337
  if (this.id === null) {
314
338
  if (options.lessParentheses) {
315
- const allSimple = this.values.every(v => !v.values || v.values.length === 0);
339
+ const allSimple = this.values.every(
340
+ (v) => !v.values || v.values.length === 0
341
+ );
316
342
  if (allSimple) {
317
- return this.values.map(v => Link.escapeReference(v.id)).join(' ');
343
+ return this.values.map((v) => Link.escapeReference(v.id)).join(' ');
318
344
  }
319
345
  return valuesStr;
320
346
  }
@@ -324,7 +350,9 @@ export class Link {
324
350
  // Link with ID and values
325
351
  const idStr = Link.escapeReference(this.id);
326
352
  const withColon = `${idStr}: ${valuesStr}`;
327
- return options.lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
353
+ return options.lessParentheses && !this.needsParentheses(this.id)
354
+ ? withColon
355
+ : `(${withColon})`;
328
356
  }
329
357
 
330
358
  /**
@@ -335,7 +363,9 @@ export class Link {
335
363
  _formatIndented(options) {
336
364
  if (this.id === null) {
337
365
  // Values only - format each on separate line
338
- const lines = this.values.map(v => options.indentString + this.formatValue(v));
366
+ const lines = this.values.map(
367
+ (v) => options.indentString + this.formatValue(v)
368
+ );
339
369
  return lines.join('\n');
340
370
  }
341
371
 
@@ -373,7 +403,11 @@ function _groupConsecutiveLinks(links) {
373
403
 
374
404
  while (j < links.length) {
375
405
  const nextLink = links[j];
376
- if (nextLink.id === current.id && nextLink.values && nextLink.values.length > 0) {
406
+ if (
407
+ nextLink.id === current.id &&
408
+ nextLink.values &&
409
+ nextLink.values.length > 0
410
+ ) {
377
411
  sameIdValues.push(...nextLink.values);
378
412
  j++;
379
413
  } else {
@@ -401,17 +435,21 @@ export function formatLinks(links, lessParentheses = false) {
401
435
  if (!links || links.length === 0) return '';
402
436
 
403
437
  // Support FormatOptions/FormatConfig as parameter
404
- if (lessParentheses && typeof lessParentheses === 'object' &&
405
- (lessParentheses.constructor.name === 'FormatOptions' || lessParentheses.constructor.name === 'FormatConfig')) {
438
+ if (
439
+ lessParentheses &&
440
+ typeof lessParentheses === 'object' &&
441
+ (lessParentheses.constructor.name === 'FormatOptions' ||
442
+ lessParentheses.constructor.name === 'FormatConfig')
443
+ ) {
406
444
  const options = lessParentheses;
407
445
  // Apply consecutive link grouping if enabled
408
446
  let linksToFormat = links;
409
447
  if (options.groupConsecutive) {
410
448
  linksToFormat = _groupConsecutiveLinks(links);
411
449
  }
412
- return linksToFormat.map(link => link.format(options)).join('\n');
450
+ return linksToFormat.map((link) => link.format(options)).join('\n');
413
451
  }
414
452
 
415
453
  // Backward compatibility with boolean parameter
416
- return links.map(link => link.format(lessParentheses)).join('\n');
417
- }
454
+ return links.map((link) => link.format(lessParentheses)).join('\n');
455
+ }
package/src/LinksGroup.js CHANGED
@@ -25,6 +25,6 @@ export class LinksGroup {
25
25
 
26
26
  toString() {
27
27
  const list = this.toList();
28
- return list.map(item => `(${item.id || item})`).join(' ');
28
+ return list.map((item) => `(${item.id || item})`).join(' ');
29
29
  }
30
- }
30
+ }
package/src/Parser.js CHANGED
@@ -26,7 +26,9 @@ export class Parser {
26
26
  }
27
27
 
28
28
  if (input.length > this.maxInputSize) {
29
- throw new Error(`Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`);
29
+ throw new Error(
30
+ `Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`
31
+ );
30
32
  }
31
33
 
32
34
  try {
@@ -72,10 +74,10 @@ export class Parser {
72
74
  });
73
75
  const linkWithChildren = {
74
76
  id: item.id,
75
- values: childValues
77
+ values: childValues,
76
78
  };
77
79
  const currentLink = this.transformLink(linkWithChildren);
78
-
80
+
79
81
  if (parentPath.length === 0) {
80
82
  result.push(currentLink);
81
83
  } else {
@@ -84,17 +86,17 @@ export class Parser {
84
86
  } else {
85
87
  // Regular indented structure - process as before
86
88
  const currentLink = this.transformLink(item);
87
-
89
+
88
90
  // Add the link combined with parent path
89
91
  if (parentPath.length === 0) {
90
92
  result.push(currentLink);
91
93
  } else {
92
94
  result.push(this.combinePathElements(parentPath, currentLink));
93
95
  }
94
-
96
+
95
97
  // Process each child with this item in the path
96
98
  const newPath = [...parentPath, currentLink];
97
-
99
+
98
100
  for (const child of item.children) {
99
101
  this.collectLinks(child, newPath, result);
100
102
  }
@@ -102,7 +104,7 @@ export class Parser {
102
104
  } else {
103
105
  // Leaf item or item with inline values
104
106
  const currentLink = this.transformLink(item);
105
-
107
+
106
108
  if (parentPath.length === 0) {
107
109
  result.push(currentLink);
108
110
  } else {
@@ -110,7 +112,7 @@ export class Parser {
110
112
  }
111
113
  }
112
114
  }
113
-
115
+
114
116
  combinePathElements(pathElements, current) {
115
117
  if (pathElements.length === 0) return current;
116
118
  if (pathElements.length === 1) {
@@ -118,21 +120,20 @@ export class Parser {
118
120
  combined._isFromPathCombination = true;
119
121
  return combined;
120
122
  }
121
-
123
+
122
124
  // For multiple path elements, we need to build proper nesting
123
125
  // The last element in the path should be combined with its parent
124
126
  const parentPath = pathElements.slice(0, -1);
125
127
  const lastElement = pathElements[pathElements.length - 1];
126
-
128
+
127
129
  // Build the parent structure
128
130
  let parent = this.combinePathElements(parentPath, lastElement);
129
-
131
+
130
132
  // Add current element to the built structure
131
133
  const combined = new Link(null, [parent, current]);
132
134
  combined._isFromPathCombination = true;
133
135
  return combined;
134
136
  }
135
-
136
137
 
137
138
  /**
138
139
  * Transform a parsed item into a Link object
@@ -156,12 +157,11 @@ export class Parser {
156
157
  if (item.values && Array.isArray(item.values)) {
157
158
  // Create a link with id (if present) and transformed values
158
159
  const link = new Link(item.id || null, []);
159
- link.values = item.values.map(v => this.transformLink(v));
160
+ link.values = item.values.map((v) => this.transformLink(v));
160
161
  return link;
161
162
  }
162
163
 
163
164
  // Default case
164
165
  return new Link(item.id || null, []);
165
166
  }
166
-
167
- }
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
- reference = doubleQuotedReference / singleQuotedReference / simpleReference
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
- doubleQuotedReference = '"' r:[^"]+ '"' { return r.join(''); }
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
- singleQuotedReference = "'" r:[^']+ "'" { return r.join(''); }
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
@@ -2,4 +2,4 @@ export { Link, formatLinks } from './Link.js';
2
2
  export { LinksGroup } from './LinksGroup.js';
3
3
  export { Parser } from './Parser.js';
4
4
  export { FormatConfig } from './FormatConfig.js';
5
- export { FormatOptions } from './FormatOptions.js';
5
+ export { FormatOptions } from './FormatOptions.js';