links-notation 0.27.1 → 0.28.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.
@@ -224,7 +224,7 @@ function peg$parse(input, options) {
224
224
  }
225
225
  function peg$f10() { unreadableLines.add(lineKey(offset())); return false; }
226
226
  function peg$f11(l) { return l; }
227
- function peg$f12(i) { return { id: i }; }
227
+ function peg$f12(r) { return { id: r.value, rawId: r.raw }; }
228
228
  function peg$f13(g, rest) {
229
229
  return rest === null ? g : { values: [g].concat(rest) };
230
230
  }
@@ -243,18 +243,22 @@ function peg$parse(input, options) {
243
243
  function peg$f23() { return location(); }
244
244
  function peg$f24(value) { return value; }
245
245
  function peg$f25(list) { return list; }
246
- function peg$f26(id, v) { return { id: id, values: v }; }
246
+ function peg$f26(id, v) { return { id: id.value, rawId: id.raw, values: v }; }
247
247
  function peg$f27(v) { return { values: v }; }
248
- function peg$f28(id) { return { id: id, values: [] }; }
249
- function peg$f29() {
248
+ function peg$f28(id) { return { id: id.value, rawId: id.raw, values: [] }; }
249
+ function peg$f29(value) {
250
+ const { start, end } = range();
251
+ return { value: value, raw: source.slice(start, end) };
252
+ }
253
+ function peg$f30() {
250
254
  return hasReferenceLiteralPrefix(input, offset());
251
255
  }
252
- function peg$f30(literal) {
256
+ function peg$f31(literal) {
253
257
  return decodeReferenceLiteral(literal);
254
258
  }
255
- function peg$f31() { return !hasReferenceLiteralPrefix(input, offset()); }
256
- function peg$f32(chars) { return chars.join(''); }
257
- function peg$f33() {
259
+ function peg$f32() { return !hasReferenceLiteralPrefix(input, offset()); }
260
+ function peg$f33(chars) { return chars.join(''); }
261
+ function peg$f34() {
258
262
  const pos = offset();
259
263
  const result = parseQuotedStringAt(input, pos, '"');
260
264
  if (result) {
@@ -264,11 +268,11 @@ function peg$parse(input, options) {
264
268
  }
265
269
  return false;
266
270
  }
267
- function peg$f34(chars) { return parsedValue; }
268
- function peg$f35(c, cs) { return [c].concat(cs).join(''); }
269
- function peg$f36() { return parsedLength > 1 && (parsedLength--, true); }
270
- function peg$f37(c) { return c; }
271
- function peg$f38() {
271
+ function peg$f35(chars) { return parsedValue; }
272
+ function peg$f36(c, cs) { return [c].concat(cs).join(''); }
273
+ function peg$f37() { return parsedLength > 1 && (parsedLength--, true); }
274
+ function peg$f38(c) { return c; }
275
+ function peg$f39() {
272
276
  const pos = offset();
273
277
  const result = parseQuotedStringAt(input, pos, "'");
274
278
  if (result) {
@@ -278,11 +282,11 @@ function peg$parse(input, options) {
278
282
  }
279
283
  return false;
280
284
  }
281
- function peg$f39(chars) { return parsedValue; }
282
- function peg$f40(c, cs) { return [c].concat(cs).join(''); }
283
- function peg$f41() { return parsedLength > 1 && (parsedLength--, true); }
284
- function peg$f42(c) { return c; }
285
- function peg$f43() {
285
+ function peg$f40(chars) { return parsedValue; }
286
+ function peg$f41(c, cs) { return [c].concat(cs).join(''); }
287
+ function peg$f42() { return parsedLength > 1 && (parsedLength--, true); }
288
+ function peg$f43(c) { return c; }
289
+ function peg$f44() {
286
290
  const pos = offset();
287
291
  const result = parseQuotedStringAt(input, pos, '`');
288
292
  if (result) {
@@ -292,16 +296,16 @@ function peg$parse(input, options) {
292
296
  }
293
297
  return false;
294
298
  }
295
- function peg$f44(chars) { return parsedValue; }
296
- function peg$f45(c, cs) { return [c].concat(cs).join(''); }
297
- function peg$f46() { return parsedLength > 1 && (parsedLength--, true); }
298
- function peg$f47(c) { return c; }
299
- function peg$f48() { return indentationStack.slice(); }
300
- function peg$f49(spaces) { setBaseIndentation(spaces); }
301
- function peg$f50(spaces) { return normalizeIndentation(spaces) > getCurrentIndentation(); }
302
- function peg$f51(spaces) { pushIndentation(spaces); }
303
- function peg$f52(spaces) { return checkIndentation(spaces); }
304
- function peg$f53() { return isInsideNestedContext(); }
299
+ function peg$f45(chars) { return parsedValue; }
300
+ function peg$f46(c, cs) { return [c].concat(cs).join(''); }
301
+ function peg$f47() { return parsedLength > 1 && (parsedLength--, true); }
302
+ function peg$f48(c) { return c; }
303
+ function peg$f49() { return indentationStack.slice(); }
304
+ function peg$f50(spaces) { setBaseIndentation(spaces); }
305
+ function peg$f51(spaces) { return normalizeIndentation(spaces) > getCurrentIndentation(); }
306
+ function peg$f52(spaces) { pushIndentation(spaces); }
307
+ function peg$f53(spaces) { return checkIndentation(spaces); }
308
+ function peg$f54() { return isInsideNestedContext(); }
305
309
  let peg$currPos = options.peg$currPos | 0;
306
310
  let peg$savedPos = peg$currPos;
307
311
  const peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -1218,6 +1222,20 @@ function peg$parse(input, options) {
1218
1222
  }
1219
1223
 
1220
1224
  function peg$parsereference() {
1225
+ let s0, s1;
1226
+
1227
+ s0 = peg$currPos;
1228
+ s1 = peg$parsereferenceValue();
1229
+ if (s1 !== peg$FAILED) {
1230
+ peg$savedPos = s0;
1231
+ s1 = peg$f29(s1);
1232
+ }
1233
+ s0 = s1;
1234
+
1235
+ return s0;
1236
+ }
1237
+
1238
+ function peg$parsereferenceValue() {
1221
1239
  let s0;
1222
1240
 
1223
1241
  s0 = peg$parsereferenceLiteral();
@@ -1253,7 +1271,7 @@ function peg$parse(input, options) {
1253
1271
  }
1254
1272
  if (s1 !== peg$FAILED) {
1255
1273
  peg$savedPos = peg$currPos;
1256
- s2 = peg$f29();
1274
+ s2 = peg$f30();
1257
1275
  if (s2) {
1258
1276
  s2 = undefined;
1259
1277
  } else {
@@ -1352,7 +1370,7 @@ function peg$parse(input, options) {
1352
1370
  }
1353
1371
  if (s3 !== peg$FAILED) {
1354
1372
  peg$savedPos = s0;
1355
- s0 = peg$f30(s3);
1373
+ s0 = peg$f31(s3);
1356
1374
  } else {
1357
1375
  peg$currPos = s0;
1358
1376
  s0 = peg$FAILED;
@@ -1374,7 +1392,7 @@ function peg$parse(input, options) {
1374
1392
 
1375
1393
  s0 = peg$currPos;
1376
1394
  peg$savedPos = peg$currPos;
1377
- s1 = peg$f31();
1395
+ s1 = peg$f32();
1378
1396
  if (s1) {
1379
1397
  s1 = undefined;
1380
1398
  } else {
@@ -1393,7 +1411,7 @@ function peg$parse(input, options) {
1393
1411
  }
1394
1412
  if (s2 !== peg$FAILED) {
1395
1413
  peg$savedPos = s0;
1396
- s0 = peg$f32(s2);
1414
+ s0 = peg$f33(s2);
1397
1415
  } else {
1398
1416
  peg$currPos = s0;
1399
1417
  s0 = peg$FAILED;
@@ -1442,7 +1460,7 @@ function peg$parse(input, options) {
1442
1460
  }
1443
1461
  if (s1 !== peg$FAILED) {
1444
1462
  peg$savedPos = peg$currPos;
1445
- s2 = peg$f33();
1463
+ s2 = peg$f34();
1446
1464
  if (s2) {
1447
1465
  s2 = undefined;
1448
1466
  } else {
@@ -1452,7 +1470,7 @@ function peg$parse(input, options) {
1452
1470
  s3 = peg$parseconsumeDouble();
1453
1471
  if (s3 !== peg$FAILED) {
1454
1472
  peg$savedPos = s0;
1455
- s0 = peg$f34(s3);
1473
+ s0 = peg$f35(s3);
1456
1474
  } else {
1457
1475
  peg$currPos = s0;
1458
1476
  s0 = peg$FAILED;
@@ -1488,7 +1506,7 @@ function peg$parse(input, options) {
1488
1506
  s3 = peg$parseconsumeDoubleMore();
1489
1507
  }
1490
1508
  peg$savedPos = s0;
1491
- s0 = peg$f35(s1, s2);
1509
+ s0 = peg$f36(s1, s2);
1492
1510
  } else {
1493
1511
  peg$currPos = s0;
1494
1512
  s0 = peg$FAILED;
@@ -1502,7 +1520,7 @@ function peg$parse(input, options) {
1502
1520
 
1503
1521
  s0 = peg$currPos;
1504
1522
  peg$savedPos = peg$currPos;
1505
- s1 = peg$f36();
1523
+ s1 = peg$f37();
1506
1524
  if (s1) {
1507
1525
  s1 = undefined;
1508
1526
  } else {
@@ -1518,7 +1536,7 @@ function peg$parse(input, options) {
1518
1536
  }
1519
1537
  if (s2 !== peg$FAILED) {
1520
1538
  peg$savedPos = s0;
1521
- s0 = peg$f37(s2);
1539
+ s0 = peg$f38(s2);
1522
1540
  } else {
1523
1541
  peg$currPos = s0;
1524
1542
  s0 = peg$FAILED;
@@ -1553,7 +1571,7 @@ function peg$parse(input, options) {
1553
1571
  }
1554
1572
  if (s1 !== peg$FAILED) {
1555
1573
  peg$savedPos = peg$currPos;
1556
- s2 = peg$f38();
1574
+ s2 = peg$f39();
1557
1575
  if (s2) {
1558
1576
  s2 = undefined;
1559
1577
  } else {
@@ -1563,7 +1581,7 @@ function peg$parse(input, options) {
1563
1581
  s3 = peg$parseconsumeSingle();
1564
1582
  if (s3 !== peg$FAILED) {
1565
1583
  peg$savedPos = s0;
1566
- s0 = peg$f39(s3);
1584
+ s0 = peg$f40(s3);
1567
1585
  } else {
1568
1586
  peg$currPos = s0;
1569
1587
  s0 = peg$FAILED;
@@ -1599,7 +1617,7 @@ function peg$parse(input, options) {
1599
1617
  s3 = peg$parseconsumeSingleMore();
1600
1618
  }
1601
1619
  peg$savedPos = s0;
1602
- s0 = peg$f40(s1, s2);
1620
+ s0 = peg$f41(s1, s2);
1603
1621
  } else {
1604
1622
  peg$currPos = s0;
1605
1623
  s0 = peg$FAILED;
@@ -1613,7 +1631,7 @@ function peg$parse(input, options) {
1613
1631
 
1614
1632
  s0 = peg$currPos;
1615
1633
  peg$savedPos = peg$currPos;
1616
- s1 = peg$f41();
1634
+ s1 = peg$f42();
1617
1635
  if (s1) {
1618
1636
  s1 = undefined;
1619
1637
  } else {
@@ -1629,7 +1647,7 @@ function peg$parse(input, options) {
1629
1647
  }
1630
1648
  if (s2 !== peg$FAILED) {
1631
1649
  peg$savedPos = s0;
1632
- s0 = peg$f42(s2);
1650
+ s0 = peg$f43(s2);
1633
1651
  } else {
1634
1652
  peg$currPos = s0;
1635
1653
  s0 = peg$FAILED;
@@ -1664,7 +1682,7 @@ function peg$parse(input, options) {
1664
1682
  }
1665
1683
  if (s1 !== peg$FAILED) {
1666
1684
  peg$savedPos = peg$currPos;
1667
- s2 = peg$f43();
1685
+ s2 = peg$f44();
1668
1686
  if (s2) {
1669
1687
  s2 = undefined;
1670
1688
  } else {
@@ -1674,7 +1692,7 @@ function peg$parse(input, options) {
1674
1692
  s3 = peg$parseconsumeBacktick();
1675
1693
  if (s3 !== peg$FAILED) {
1676
1694
  peg$savedPos = s0;
1677
- s0 = peg$f44(s3);
1695
+ s0 = peg$f45(s3);
1678
1696
  } else {
1679
1697
  peg$currPos = s0;
1680
1698
  s0 = peg$FAILED;
@@ -1710,7 +1728,7 @@ function peg$parse(input, options) {
1710
1728
  s3 = peg$parseconsumeBacktickMore();
1711
1729
  }
1712
1730
  peg$savedPos = s0;
1713
- s0 = peg$f45(s1, s2);
1731
+ s0 = peg$f46(s1, s2);
1714
1732
  } else {
1715
1733
  peg$currPos = s0;
1716
1734
  s0 = peg$FAILED;
@@ -1724,7 +1742,7 @@ function peg$parse(input, options) {
1724
1742
 
1725
1743
  s0 = peg$currPos;
1726
1744
  peg$savedPos = peg$currPos;
1727
- s1 = peg$f46();
1745
+ s1 = peg$f47();
1728
1746
  if (s1) {
1729
1747
  s1 = undefined;
1730
1748
  } else {
@@ -1740,7 +1758,7 @@ function peg$parse(input, options) {
1740
1758
  }
1741
1759
  if (s2 !== peg$FAILED) {
1742
1760
  peg$savedPos = s0;
1743
- s0 = peg$f47(s2);
1761
+ s0 = peg$f48(s2);
1744
1762
  } else {
1745
1763
  peg$currPos = s0;
1746
1764
  s0 = peg$FAILED;
@@ -1759,7 +1777,7 @@ function peg$parse(input, options) {
1759
1777
  s0 = peg$currPos;
1760
1778
  s1 = '';
1761
1779
  peg$savedPos = s0;
1762
- s1 = peg$f48();
1780
+ s1 = peg$f49();
1763
1781
  s0 = s1;
1764
1782
 
1765
1783
  return s0;
@@ -1788,7 +1806,7 @@ function peg$parse(input, options) {
1788
1806
  }
1789
1807
  }
1790
1808
  peg$savedPos = s0;
1791
- s1 = peg$f49(s1);
1809
+ s1 = peg$f50(s1);
1792
1810
  s0 = s1;
1793
1811
 
1794
1812
  return s0;
@@ -1817,7 +1835,7 @@ function peg$parse(input, options) {
1817
1835
  }
1818
1836
  }
1819
1837
  peg$savedPos = peg$currPos;
1820
- s2 = peg$f50(s1);
1838
+ s2 = peg$f51(s1);
1821
1839
  if (s2) {
1822
1840
  s2 = undefined;
1823
1841
  } else {
@@ -1825,7 +1843,7 @@ function peg$parse(input, options) {
1825
1843
  }
1826
1844
  if (s2 !== peg$FAILED) {
1827
1845
  peg$savedPos = s0;
1828
- s0 = peg$f51(s1);
1846
+ s0 = peg$f52(s1);
1829
1847
  } else {
1830
1848
  peg$currPos = s0;
1831
1849
  s0 = peg$FAILED;
@@ -1857,7 +1875,7 @@ function peg$parse(input, options) {
1857
1875
  }
1858
1876
  }
1859
1877
  peg$savedPos = peg$currPos;
1860
- s2 = peg$f52(s1);
1878
+ s2 = peg$f53(s1);
1861
1879
  if (s2) {
1862
1880
  s2 = undefined;
1863
1881
  } else {
@@ -2057,7 +2075,7 @@ function peg$parse(input, options) {
2057
2075
 
2058
2076
  s0 = peg$currPos;
2059
2077
  peg$savedPos = peg$currPos;
2060
- s1 = peg$f53();
2078
+ s1 = peg$f54();
2061
2079
  if (s1) {
2062
2080
  s1 = undefined;
2063
2081
  } else {
@@ -2197,6 +2215,8 @@ function peg$parse(input, options) {
2197
2215
  // until the stack overflows.
2198
2216
  let contextDepth = 0;
2199
2217
  const maxDepth = options.maxDepth ?? Infinity;
2218
+ // The document as the caller wrote it, comments included.
2219
+ const source = options.source ?? input;
2200
2220
  // Lines already found unreadable, keyed by where they start and whether they
2201
2221
  // are inside a parenthesised group. Reading a line depends on nothing else: a
2202
2222
  // group starts a fresh indentation context, and indentation only decides
package/src/references.js CHANGED
@@ -61,3 +61,21 @@ export function unescapeReference(reference) {
61
61
  }
62
62
  return reading.value;
63
63
  }
64
+
65
+ /**
66
+ * The delimiter a reference was written between, read from the reference as it
67
+ * was written: `'`, `"` or `` ` `` for a quoted reference (however many of them
68
+ * open it), and null for a bare reference, a versioned literal, or no text.
69
+ * @param {string|null|undefined} raw - One reference exactly as written
70
+ * @returns {string|null} The delimiter character, or null
71
+ * @example
72
+ * referenceQuote("'1'"); // "'"
73
+ * referenceQuote('""a b""'); // '"'
74
+ * referenceQuote('1'); // null
75
+ * referenceQuote("'unclosed"); // null: a bare reference that starts with '
76
+ */
77
+ export function referenceQuote(raw) {
78
+ if (typeof raw !== 'string' || !QUOTES.includes(raw[0])) return null;
79
+ const reading = readReference(raw, 0);
80
+ return reading !== null && reading.length === raw.length ? raw[0] : null;
81
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { CstKind, CstNode, Parser } from '../src/index.js';
3
+
4
+ const roundTrip = (input, options) =>
5
+ new Parser(options).parseCst(input).toString();
6
+
7
+ describe('concrete syntax tree (issue #364)', () => {
8
+ test('is the default lossless view of a document', () => {
9
+ const input = "(t 1 '1')";
10
+ const cst = new Parser().parseCst(input);
11
+ expect(cst).toBeInstanceOf(CstNode);
12
+ expect(cst.kind).toBe(CstKind.DOCUMENT);
13
+ expect(cst.toString()).toBe(input);
14
+ expect(cst.toSExpression()).toBe(
15
+ '(document (line (group open_paren:"(" (line reference:"t" whitespace:" " reference:"1" whitespace:" " reference:"\'1\'") close_paren:")")))'
16
+ );
17
+ });
18
+
19
+ test('reference tokens carry their value and delimiter', () => {
20
+ const references = new Parser()
21
+ .parseCst("(a \"b c\" ~1{64} ''e''''f'')")
22
+ .tokens()
23
+ .filter((token) => token.kind === CstKind.REFERENCE);
24
+ expect(
25
+ references.map((token) => [token.text, token.value, token.quote])
26
+ ).toEqual([
27
+ ['a', 'a', null],
28
+ ['"b c"', 'b c', '"'],
29
+ ['~1{64}', 'd', null],
30
+ ["''e''''f''", "e''f", "'"],
31
+ ]);
32
+ });
33
+
34
+ test.each([
35
+ ['empty', ''],
36
+ ['only whitespace', ' \t '],
37
+ ['blank lines', '\n\n'],
38
+ ['CRLF and CR', 'a\r\nb\rc\r\n'],
39
+ ['tabs and trailing spaces', 'a\t b \n'],
40
+ ['comments everywhere', '# head\na: b # tail\n # inner\n c\n# end'],
41
+ ['multiline quotes', '(note: "one\n two\r\nthree")'],
42
+ ['nested groups over lines', '(a (b\n (c d)\n) e)\n'],
43
+ ['indentation', 'root:\n child:\n leaf\n tail'],
44
+ ['literals', '(~1{} ~1{e6a0b9})'],
45
+ ['comment marks inside quotes', '(\'#a\' "b # c" `#`)'],
46
+ ['unicode', '(根: café 🙂)\n'],
47
+ ])('recovers %s exactly', (_, input) => {
48
+ expect(roundTrip(input)).toBe(input);
49
+ });
50
+
51
+ test('without comment support, # is part of references', () => {
52
+ const input = 'a #b';
53
+ const cst = new Parser({ comments: false }).parseCst(input);
54
+ expect(cst.toString()).toBe(input);
55
+ expect(cst.tokens().map((token) => token.kind)).toEqual([
56
+ 'reference',
57
+ 'whitespace',
58
+ 'reference',
59
+ ]);
60
+ });
61
+
62
+ test('a group belongs to the line it opens on', () => {
63
+ const cst = new Parser().parseCst('a (b\nc)\nd');
64
+ expect(cst.children.map((line) => line.toString())).toEqual([
65
+ 'a (b\nc)\n',
66
+ 'd',
67
+ ]);
68
+ const group = cst.children[0].children[2];
69
+ expect(group.kind).toBe(CstKind.GROUP);
70
+ expect(group.children.map((child) => child.kind)).toEqual([
71
+ 'open_paren',
72
+ 'line',
73
+ 'line',
74
+ 'close_paren',
75
+ ]);
76
+ });
77
+
78
+ test('reads versioned literals in linear time', () => {
79
+ // CodeQL js/polynomial-redos flagged the regular expression that used to
80
+ // find where a literal ends; the scanner that replaced it is one pass.
81
+ const input = [
82
+ `~1{${'61'.repeat(50000)}}`,
83
+ `~${'1'.repeat(50000)}x`,
84
+ '~1{61} '.repeat(20000),
85
+ ].join('\n');
86
+ const started = Date.now();
87
+ const cst = new Parser().parseCst(input);
88
+ expect(Date.now() - started).toBeLessThan(5000);
89
+ expect(cst.toString()).toBe(input);
90
+ expect(
91
+ new Parser()
92
+ .parseCst('(~1{64} ~{} ~a{b} ~1x)')
93
+ .tokens()
94
+ .filter((token) => token.kind === CstKind.REFERENCE)
95
+ .map((token) => [token.text, token.value])
96
+ ).toEqual([
97
+ ['~1{64}', 'd'],
98
+ ['~{}', '~{}'],
99
+ ['~a{b}', '~a{b}'],
100
+ ['~1x', '~1x'],
101
+ ]);
102
+ });
103
+
104
+ test('rejects what parse() rejects', () => {
105
+ expect(() => new Parser().parseCst('(a')).toThrow();
106
+ expect(() => new Parser().parseCst('a)')).toThrow();
107
+ expect(() => new Parser({ maxDepth: 1 }).parseCst('((a))')).toThrow();
108
+ });
109
+ });
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { URL } from 'node:url';
3
+ import { expect, test } from 'bun:test';
4
+ import { Parser, StreamParser } from '../src/index.js';
5
+
6
+ const read = (name) =>
7
+ JSON.parse(
8
+ readFileSync(new URL(`../../parity/${name}`, import.meta.url), 'utf8')
9
+ );
10
+ const syntaxFixtures = read('syntax-fixtures.json');
11
+ const textFixtures = read('text-fixtures.json');
12
+ const tree = (links) =>
13
+ links.map((link) => ({
14
+ id: link.id,
15
+ raw: link.rawId,
16
+ quote: link.quote,
17
+ values: tree(link.values),
18
+ }));
19
+
20
+ test('shared syntax parity: raw references and concrete syntax trees', () => {
21
+ for (const fixture of syntaxFixtures) {
22
+ const parser = new Parser();
23
+ expect(tree(parser.parse(fixture.input)), fixture.id).toEqual(
24
+ fixture.links
25
+ );
26
+ const cst = parser.parseCst(fixture.input);
27
+ expect(cst.toSExpression(), fixture.id).toBe(fixture.cst);
28
+ expect(cst.toString(), fixture.id).toBe(fixture.input);
29
+ const stream = new StreamParser();
30
+ for (const character of fixture.input) stream.write(character);
31
+ expect(tree(stream.end()), fixture.id).toEqual(fixture.links);
32
+ }
33
+ });
34
+
35
+ test('every shared input is recovered exactly from its concrete syntax tree', () => {
36
+ for (const { id, input } of [...textFixtures, ...syntaxFixtures]) {
37
+ const cst = new Parser().parseCst(input);
38
+ expect(cst.toString(), id).toBe(input);
39
+ expect(
40
+ cst
41
+ .tokens()
42
+ .map((token) => token.text)
43
+ .join(''),
44
+ id
45
+ ).toBe(input);
46
+ }
47
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ Link,
4
+ Parser,
5
+ StreamParser,
6
+ formatLinks,
7
+ referenceQuote,
8
+ } from '../src/index.js';
9
+
10
+ describe('syntax metadata on parsed links (issue #364)', () => {
11
+ test("1 and '1' are the same reference but keep how they were written", () => {
12
+ const [link] = new Parser().parse("(t 1 '1')");
13
+ const [, bare, quoted] = link.values;
14
+ expect(bare.id).toBe('1');
15
+ expect(quoted.id).toBe('1');
16
+ expect(bare.rawId).toBe('1');
17
+ expect(quoted.rawId).toBe("'1'");
18
+ expect(bare.quote).toBeNull();
19
+ expect(quoted.quote).toBe("'");
20
+ // Equality and formatting ignore the metadata.
21
+ expect(bare.equals(quoted)).toBe(true);
22
+ expect(link.format()).toBe('(t 1 1)');
23
+ });
24
+
25
+ test('records every quote kind and run length', () => {
26
+ const [link] = new Parser().parse('("a": \'b\' `c` ""d"" ~1{65} f)');
27
+ expect(link.rawId).toBe('"a"');
28
+ expect(link.quote).toBe('"');
29
+ expect(link.values.map((value) => [value.rawId, value.quote])).toEqual([
30
+ ["'b'", "'"],
31
+ ['`c`', '`'],
32
+ ['""d""', '"'],
33
+ ['~1{65}', null],
34
+ ['f', null],
35
+ ]);
36
+ });
37
+
38
+ test('keeps raw text from the source, not from comment stripping', () => {
39
+ const [link] = new Parser().parse("(a 'b # c') # d");
40
+ expect(link.values[1].rawId).toBe("'b # c'");
41
+ });
42
+
43
+ test('leaves links without written IDs without raw text', () => {
44
+ const [link] = new Parser().parse('(a b)');
45
+ expect(link.rawId).toBeNull();
46
+ expect(link.quote).toBeNull();
47
+ expect(new Link('x').rawId).toBeNull();
48
+ expect(new Link('x').quote).toBeNull();
49
+ });
50
+
51
+ test('does not change equality, formatting or serialization', () => {
52
+ const quoted = new Parser().parse("('a': 'b')");
53
+ const bare = new Parser().parse('(a: b)');
54
+ expect(quoted[0].equals(bare[0])).toBe(true);
55
+ expect(formatLinks(quoted)).toBe(formatLinks(bare));
56
+ expect(quoted[0].toString()).toBe(bare[0].toString());
57
+ expect(JSON.stringify(quoted)).toBe(JSON.stringify(bare));
58
+ expect(quoted).toEqual(bare);
59
+ });
60
+
61
+ test('AST mode (syntax: false) drops the metadata', () => {
62
+ const [link] = new Parser({ syntax: false }).parse("('a': 'b')");
63
+ expect(link.rawId).toBeNull();
64
+ expect(link.values[0].rawId).toBeNull();
65
+ expect(link.values[0].quote).toBeNull();
66
+ expect(link.equals(new Parser().parse("('a': 'b')")[0])).toBe(true);
67
+ });
68
+
69
+ test('parseGroups and StreamParser keep the metadata', () => {
70
+ const groups = new Parser().parseGroups('\'root\':\n "child"');
71
+ expect(groups[0].element.rawId).toBe("'root'");
72
+ expect(groups[0].children[0].element.values[0].rawId).toBe('"child"');
73
+ const stream = new StreamParser();
74
+ stream.write("(a '");
75
+ stream.write("b')");
76
+ const [link] = stream.end();
77
+ expect(link.values[1].rawId).toBe("'b'");
78
+ expect(link.values[1].quote).toBe("'");
79
+ });
80
+
81
+ test('rawId can be set on links built in code and must be text or null', () => {
82
+ const link = new Link('a');
83
+ link.rawId = '"a"';
84
+ expect(link.quote).toBe('"');
85
+ link.rawId = null;
86
+ expect(link.quote).toBeNull();
87
+ expect(() => {
88
+ link.rawId = 1;
89
+ }).toThrow(TypeError);
90
+ });
91
+
92
+ test('referenceQuote reads the delimiter of one complete reference', () => {
93
+ expect(referenceQuote("'1'")).toBe("'");
94
+ expect(referenceQuote('""a b""')).toBe('"');
95
+ expect(referenceQuote('`x`')).toBe('`');
96
+ expect(referenceQuote('1')).toBeNull();
97
+ expect(referenceQuote('~1{31}')).toBeNull();
98
+ expect(referenceQuote("'unclosed")).toBeNull();
99
+ expect(referenceQuote("'a' b")).toBeNull();
100
+ expect(referenceQuote(null)).toBeNull();
101
+ });
102
+ });