links-notation 0.13.0 → 0.15.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.
@@ -1,4 +1,4 @@
1
- // @generated by Peggy 5.0.6.
1
+ // @generated by Peggy 5.1.0.
2
2
  //
3
3
  // https://peggyjs.org/
4
4
 
@@ -164,9 +164,9 @@ function peg$parse(input, options) {
164
164
  };
165
165
  let peg$startRuleFunction = peg$parsedocument;
166
166
 
167
- const peg$c0 = ":";
168
- const peg$c1 = "(";
169
- const peg$c2 = ")";
167
+ const peg$c0 = "(";
168
+ const peg$c1 = ")";
169
+ const peg$c2 = ":";
170
170
  const peg$c3 = "\"";
171
171
  const peg$c4 = "'";
172
172
  const peg$c5 = "`";
@@ -179,9 +179,9 @@ function peg$parse(input, options) {
179
179
 
180
180
  const peg$e0 = peg$classExpectation([" ", "\t"], false, false, false);
181
181
  const peg$e1 = peg$classExpectation(["\r", "\n"], false, false, false);
182
- const peg$e2 = peg$literalExpectation(":", false);
183
- const peg$e3 = peg$literalExpectation("(", false);
184
- const peg$e4 = peg$literalExpectation(")", false);
182
+ const peg$e2 = peg$literalExpectation("(", false);
183
+ const peg$e3 = peg$literalExpectation(")", false);
184
+ const peg$e4 = peg$literalExpectation(":", false);
185
185
  const peg$e5 = peg$literalExpectation("\"", false);
186
186
  const peg$e6 = peg$anyExpectation();
187
187
  const peg$e7 = peg$literalExpectation("'", false);
@@ -190,15 +190,15 @@ function peg$parse(input, options) {
190
190
  const peg$e10 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
191
191
  const peg$e11 = peg$classExpectation([" ", "\t", "\n", "\r", "(", ":", ")"], true, false, false);
192
192
 
193
- function peg$f0() { indentationStack = [0]; baseIndentation = null; return true; }
193
+ function peg$f0() { return resetState(); }
194
194
  function peg$f1(links) { return links; }
195
- function peg$f2() { indentationStack = [0]; baseIndentation = null; return true; }
195
+ function peg$f2() { return resetState(); }
196
196
  function peg$f3() { return []; }
197
197
  function peg$f4(fl, list) { popIndentation(); return [fl].concat(list || []); }
198
198
  function peg$f5(l) { return l; }
199
199
  function peg$f6(l) { return l; }
200
200
  function peg$f7(e, l) {
201
- return { id: e.id, values: e.values, children: l };
201
+ return Object.assign({}, e, { children: l });
202
202
  }
203
203
  function peg$f8(e) { return e; }
204
204
  function peg$f9(l) { return l; }
@@ -208,13 +208,16 @@ function peg$parse(input, options) {
208
208
  function peg$f13(sl) { return sl; }
209
209
  function peg$f14(fl) { return fl; }
210
210
  function peg$f15(vl) { return vl; }
211
- function peg$f16(value) { return value; }
212
- function peg$f17(list) { return list; }
213
- function peg$f18(value) { return value; }
214
- function peg$f19(list) { return list; }
215
- function peg$f20(id, v) { return { id: id, values: v }; }
216
- function peg$f21(id, v) { return { id: id, values: v }; }
217
- function peg$f22(v) { return { values: v }; }
211
+ function peg$f16(body) {
212
+ exitNestedContext();
213
+ return body;
214
+ }
215
+ function peg$f17(l) { return { nested: l }; }
216
+ function peg$f18() { return { nested: [] }; }
217
+ function peg$f19() { return enterNestedContext(); }
218
+ function peg$f20(value) { return value; }
219
+ function peg$f21(list) { return list; }
220
+ function peg$f22(id, v) { return { id: id, values: v }; }
218
221
  function peg$f23(v) { return { values: v }; }
219
222
  function peg$f24(id) { return { id: id, values: [] }; }
220
223
  function peg$f25(chars) { return chars.join(''); }
@@ -264,6 +267,7 @@ function peg$parse(input, options) {
264
267
  function peg$f42(spaces) { return normalizeIndentation(spaces) > getCurrentIndentation(); }
265
268
  function peg$f43(spaces) { pushIndentation(spaces); }
266
269
  function peg$f44(spaces) { return checkIndentation(spaces); }
270
+ function peg$f45() { return isInsideNestedContext(); }
267
271
  let peg$currPos = options.peg$currPos | 0;
268
272
  let peg$savedPos = peg$currPos;
269
273
  const peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -673,7 +677,7 @@ function peg$parse(input, options) {
673
677
  let s0, s1;
674
678
 
675
679
  s0 = peg$currPos;
676
- s1 = peg$parsemultiLineAnyLink();
680
+ s1 = peg$parsenestedGroup();
677
681
  if (s1 !== peg$FAILED) {
678
682
  peg$savedPos = s0;
679
683
  s1 = peg$f9(s1);
@@ -696,7 +700,7 @@ function peg$parse(input, options) {
696
700
  let s0, s1, s2;
697
701
 
698
702
  s0 = peg$currPos;
699
- s1 = peg$parsemultiLineAnyLink();
703
+ s1 = peg$parsenestedGroup();
700
704
  if (s1 !== peg$FAILED) {
701
705
  s2 = peg$parseeol();
702
706
  if (s2 !== peg$FAILED) {
@@ -732,17 +736,6 @@ function peg$parse(input, options) {
732
736
  return s0;
733
737
  }
734
738
 
735
- function peg$parsemultiLineAnyLink() {
736
- let s0;
737
-
738
- s0 = peg$parsemultiLineValueLink();
739
- if (s0 === peg$FAILED) {
740
- s0 = peg$parsemultiLineLink();
741
- }
742
-
743
- return s0;
744
- }
745
-
746
739
  function peg$parsesingleLineAnyLink() {
747
740
  let s0, s1, s2;
748
741
 
@@ -782,15 +775,32 @@ function peg$parse(input, options) {
782
775
  return s0;
783
776
  }
784
777
 
785
- function peg$parsemultiLineValueAndWhitespace() {
786
- let s0, s1, s2;
778
+ function peg$parsenestedGroup() {
779
+ let s0, s1, s2, s3;
787
780
 
788
781
  s0 = peg$currPos;
789
- s1 = peg$parsereferenceOrLink();
782
+ if (input.charCodeAt(peg$currPos) === 40) {
783
+ s1 = peg$c0;
784
+ peg$currPos++;
785
+ } else {
786
+ s1 = peg$FAILED;
787
+ if (peg$silentFails === 0) { peg$fail(peg$e2); }
788
+ }
790
789
  if (s1 !== peg$FAILED) {
791
- s2 = peg$parse_();
792
- peg$savedPos = s0;
793
- s0 = peg$f16(s1);
790
+ s2 = peg$parseENTER_NESTED_CONTEXT();
791
+ if (s2 !== peg$FAILED) {
792
+ s3 = peg$parsenestedGroupBody();
793
+ if (s3 !== peg$FAILED) {
794
+ peg$savedPos = s0;
795
+ s0 = peg$f16(s3);
796
+ } else {
797
+ peg$currPos = s0;
798
+ s0 = peg$FAILED;
799
+ }
800
+ } else {
801
+ peg$currPos = s0;
802
+ s0 = peg$FAILED;
803
+ }
794
804
  } else {
795
805
  peg$currPos = s0;
796
806
  s0 = peg$FAILED;
@@ -799,19 +809,64 @@ function peg$parse(input, options) {
799
809
  return s0;
800
810
  }
801
811
 
802
- function peg$parsemultiLineValues() {
803
- let s0, s1, s2, s3;
812
+ function peg$parsenestedGroupBody() {
813
+ let s0, s1, s2, s3, s4;
804
814
 
805
815
  s0 = peg$currPos;
806
- s1 = peg$parse_();
807
- s2 = [];
808
- s3 = peg$parsemultiLineValueAndWhitespace();
809
- while (s3 !== peg$FAILED) {
810
- s2.push(s3);
811
- s3 = peg$parsemultiLineValueAndWhitespace();
816
+ s1 = peg$parseskipEmptyLines();
817
+ s2 = peg$parselinks();
818
+ if (s2 !== peg$FAILED) {
819
+ s3 = peg$parse_();
820
+ if (input.charCodeAt(peg$currPos) === 41) {
821
+ s4 = peg$c1;
822
+ peg$currPos++;
823
+ } else {
824
+ s4 = peg$FAILED;
825
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
826
+ }
827
+ if (s4 !== peg$FAILED) {
828
+ peg$savedPos = s0;
829
+ s0 = peg$f17(s2);
830
+ } else {
831
+ peg$currPos = s0;
832
+ s0 = peg$FAILED;
833
+ }
834
+ } else {
835
+ peg$currPos = s0;
836
+ s0 = peg$FAILED;
837
+ }
838
+ if (s0 === peg$FAILED) {
839
+ s0 = peg$currPos;
840
+ s1 = peg$parse_();
841
+ if (input.charCodeAt(peg$currPos) === 41) {
842
+ s2 = peg$c1;
843
+ peg$currPos++;
844
+ } else {
845
+ s2 = peg$FAILED;
846
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
847
+ }
848
+ if (s2 !== peg$FAILED) {
849
+ peg$savedPos = s0;
850
+ s0 = peg$f18();
851
+ } else {
852
+ peg$currPos = s0;
853
+ s0 = peg$FAILED;
854
+ }
855
+ }
856
+
857
+ return s0;
858
+ }
859
+
860
+ function peg$parseENTER_NESTED_CONTEXT() {
861
+ let s0;
862
+
863
+ peg$savedPos = peg$currPos;
864
+ s0 = peg$f19();
865
+ if (s0) {
866
+ s0 = undefined;
867
+ } else {
868
+ s0 = peg$FAILED;
812
869
  }
813
- peg$savedPos = s0;
814
- s0 = peg$f17(s2);
815
870
 
816
871
  return s0;
817
872
  }
@@ -824,7 +879,7 @@ function peg$parse(input, options) {
824
879
  s2 = peg$parsereferenceOrLink();
825
880
  if (s2 !== peg$FAILED) {
826
881
  peg$savedPos = s0;
827
- s0 = peg$f18(s2);
882
+ s0 = peg$f20(s2);
828
883
  } else {
829
884
  peg$currPos = s0;
830
885
  s0 = peg$FAILED;
@@ -849,7 +904,7 @@ function peg$parse(input, options) {
849
904
  }
850
905
  if (s1 !== peg$FAILED) {
851
906
  peg$savedPos = s0;
852
- s1 = peg$f19(s1);
907
+ s1 = peg$f21(s1);
853
908
  }
854
909
  s0 = s1;
855
910
 
@@ -865,73 +920,17 @@ function peg$parse(input, options) {
865
920
  if (s2 !== peg$FAILED) {
866
921
  s3 = peg$parse__();
867
922
  if (input.charCodeAt(peg$currPos) === 58) {
868
- s4 = peg$c0;
923
+ s4 = peg$c2;
869
924
  peg$currPos++;
870
925
  } else {
871
926
  s4 = peg$FAILED;
872
- if (peg$silentFails === 0) { peg$fail(peg$e2); }
927
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
873
928
  }
874
929
  if (s4 !== peg$FAILED) {
875
930
  s5 = peg$parsesingleLineValues();
876
931
  if (s5 !== peg$FAILED) {
877
932
  peg$savedPos = s0;
878
- s0 = peg$f20(s2, s5);
879
- } else {
880
- peg$currPos = s0;
881
- s0 = peg$FAILED;
882
- }
883
- } else {
884
- peg$currPos = s0;
885
- s0 = peg$FAILED;
886
- }
887
- } else {
888
- peg$currPos = s0;
889
- s0 = peg$FAILED;
890
- }
891
-
892
- return s0;
893
- }
894
-
895
- function peg$parsemultiLineLink() {
896
- let s0, s1, s2, s3, s4, s5, s6, s7, s8;
897
-
898
- s0 = peg$currPos;
899
- if (input.charCodeAt(peg$currPos) === 40) {
900
- s1 = peg$c1;
901
- peg$currPos++;
902
- } else {
903
- s1 = peg$FAILED;
904
- if (peg$silentFails === 0) { peg$fail(peg$e3); }
905
- }
906
- if (s1 !== peg$FAILED) {
907
- s2 = peg$parse_();
908
- s3 = peg$parsereference();
909
- if (s3 !== peg$FAILED) {
910
- s4 = peg$parse_();
911
- if (input.charCodeAt(peg$currPos) === 58) {
912
- s5 = peg$c0;
913
- peg$currPos++;
914
- } else {
915
- s5 = peg$FAILED;
916
- if (peg$silentFails === 0) { peg$fail(peg$e2); }
917
- }
918
- if (s5 !== peg$FAILED) {
919
- s6 = peg$parsemultiLineValues();
920
- s7 = peg$parse_();
921
- if (input.charCodeAt(peg$currPos) === 41) {
922
- s8 = peg$c2;
923
- peg$currPos++;
924
- } else {
925
- s8 = peg$FAILED;
926
- if (peg$silentFails === 0) { peg$fail(peg$e4); }
927
- }
928
- if (s8 !== peg$FAILED) {
929
- peg$savedPos = s0;
930
- s0 = peg$f21(s3, s6);
931
- } else {
932
- peg$currPos = s0;
933
- s0 = peg$FAILED;
934
- }
933
+ s0 = peg$f22(s2, s5);
935
934
  } else {
936
935
  peg$currPos = s0;
937
936
  s0 = peg$FAILED;
@@ -955,49 +954,13 @@ function peg$parse(input, options) {
955
954
  s1 = peg$parsesingleLineValues();
956
955
  if (s1 !== peg$FAILED) {
957
956
  peg$savedPos = s0;
958
- s1 = peg$f22(s1);
957
+ s1 = peg$f23(s1);
959
958
  }
960
959
  s0 = s1;
961
960
 
962
961
  return s0;
963
962
  }
964
963
 
965
- function peg$parsemultiLineValueLink() {
966
- let s0, s1, s2, s3, s4;
967
-
968
- s0 = peg$currPos;
969
- if (input.charCodeAt(peg$currPos) === 40) {
970
- s1 = peg$c1;
971
- peg$currPos++;
972
- } else {
973
- s1 = peg$FAILED;
974
- if (peg$silentFails === 0) { peg$fail(peg$e3); }
975
- }
976
- if (s1 !== peg$FAILED) {
977
- s2 = peg$parsemultiLineValues();
978
- s3 = peg$parse_();
979
- if (input.charCodeAt(peg$currPos) === 41) {
980
- s4 = peg$c2;
981
- peg$currPos++;
982
- } else {
983
- s4 = peg$FAILED;
984
- if (peg$silentFails === 0) { peg$fail(peg$e4); }
985
- }
986
- if (s4 !== peg$FAILED) {
987
- peg$savedPos = s0;
988
- s0 = peg$f23(s2);
989
- } else {
990
- peg$currPos = s0;
991
- s0 = peg$FAILED;
992
- }
993
- } else {
994
- peg$currPos = s0;
995
- s0 = peg$FAILED;
996
- }
997
-
998
- return s0;
999
- }
1000
-
1001
964
  function peg$parseindentedIdLink() {
1002
965
  let s0, s1, s2, s3, s4;
1003
966
 
@@ -1006,11 +969,11 @@ function peg$parse(input, options) {
1006
969
  if (s1 !== peg$FAILED) {
1007
970
  s2 = peg$parse__();
1008
971
  if (input.charCodeAt(peg$currPos) === 58) {
1009
- s3 = peg$c0;
972
+ s3 = peg$c2;
1010
973
  peg$currPos++;
1011
974
  } else {
1012
975
  s3 = peg$FAILED;
1013
- if (peg$silentFails === 0) { peg$fail(peg$e2); }
976
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
1014
977
  }
1015
978
  if (s3 !== peg$FAILED) {
1016
979
  s4 = peg$parseeol();
@@ -1552,6 +1515,9 @@ function peg$parse(input, options) {
1552
1515
  }
1553
1516
  if (s2 === peg$FAILED) {
1554
1517
  s2 = peg$parseeof();
1518
+ if (s2 === peg$FAILED) {
1519
+ s2 = peg$parsenestedGroupEnd();
1520
+ }
1555
1521
  }
1556
1522
  if (s2 !== peg$FAILED) {
1557
1523
  s1 = [s1, s2];
@@ -1564,6 +1530,49 @@ function peg$parse(input, options) {
1564
1530
  return s0;
1565
1531
  }
1566
1532
 
1533
+ function peg$parsenestedGroupEnd() {
1534
+ let s0, s1, s2, s3;
1535
+
1536
+ s0 = peg$currPos;
1537
+ peg$savedPos = peg$currPos;
1538
+ s1 = peg$f45();
1539
+ if (s1) {
1540
+ s1 = undefined;
1541
+ } else {
1542
+ s1 = peg$FAILED;
1543
+ }
1544
+ if (s1 !== peg$FAILED) {
1545
+ s2 = peg$currPos;
1546
+ peg$silentFails++;
1547
+ if (input.charCodeAt(peg$currPos) === 41) {
1548
+ s3 = peg$c1;
1549
+ peg$currPos++;
1550
+ } else {
1551
+ s3 = peg$FAILED;
1552
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
1553
+ }
1554
+ peg$silentFails--;
1555
+ if (s3 !== peg$FAILED) {
1556
+ peg$currPos = s2;
1557
+ s2 = undefined;
1558
+ } else {
1559
+ s2 = peg$FAILED;
1560
+ }
1561
+ if (s2 !== peg$FAILED) {
1562
+ s1 = [s1, s2];
1563
+ s0 = s1;
1564
+ } else {
1565
+ peg$currPos = s0;
1566
+ s0 = peg$FAILED;
1567
+ }
1568
+ } else {
1569
+ peg$currPos = s0;
1570
+ s0 = peg$FAILED;
1571
+ }
1572
+
1573
+ return s0;
1574
+ }
1575
+
1567
1576
  function peg$parseeof() {
1568
1577
  let s0, s1;
1569
1578
 
@@ -1656,6 +1665,37 @@ function peg$parse(input, options) {
1656
1665
 
1657
1666
  let indentationStack = [0];
1658
1667
  let baseIndentation = null;
1668
+ // Saved indentation contexts of the enclosing scopes. Every parenthesised
1669
+ // group opens a nested context that starts fresh at indentation level zero,
1670
+ // so line breaks and indentation mean the same thing at every depth.
1671
+ let contextStack = [];
1672
+
1673
+ function resetState() {
1674
+ indentationStack = [0];
1675
+ baseIndentation = null;
1676
+ contextStack = [];
1677
+ return true;
1678
+ }
1679
+
1680
+ function enterNestedContext() {
1681
+ contextStack.push({ indentationStack, baseIndentation });
1682
+ indentationStack = [0];
1683
+ baseIndentation = null;
1684
+ return true;
1685
+ }
1686
+
1687
+ function exitNestedContext() {
1688
+ const saved = contextStack.pop();
1689
+ if (saved) {
1690
+ indentationStack = saved.indentationStack;
1691
+ baseIndentation = saved.baseIndentation;
1692
+ }
1693
+ return true;
1694
+ }
1695
+
1696
+ function isInsideNestedContext() {
1697
+ return contextStack.length > 0;
1698
+ }
1659
1699
 
1660
1700
  function setBaseIndentation(spaces) {
1661
1701
  if (baseIndentation === null) {
@@ -1690,8 +1730,36 @@ function peg$parse(input, options) {
1690
1730
  return indentationStack[indentationStack.length - 1];
1691
1731
  }
1692
1732
 
1733
+ // A body written between an even run of delimiters is substantive when it
1734
+ // holds at least one visible character and does not straddle a parenthesis.
1735
+ // An even run can always be read as delimiter pairs enclosing nothing, so the
1736
+ // n-quote reading is only taken when it carries something the pairs cannot.
1737
+ function isSubstantiveBody(content) {
1738
+ let depth = 0;
1739
+ let hasVisible = false;
1740
+
1741
+ for (const c of content) {
1742
+ if (c === '(') {
1743
+ depth++;
1744
+ } else if (c === ')') {
1745
+ depth--;
1746
+ if (depth < 0) {
1747
+ return false;
1748
+ }
1749
+ }
1750
+ if (!/\s/.test(c)) {
1751
+ hasVisible = true;
1752
+ }
1753
+ }
1754
+
1755
+ return hasVisible && depth === 0;
1756
+ }
1757
+
1693
1758
  // Universal procedural parser for N-quote strings (any N >= 1)
1694
1759
  // Parses from the given position in the input string
1760
+ // A run of an even number of delimiters that does not open a reference with a
1761
+ // substantive body is the empty reference: the shortest reading, a bare
1762
+ // delimiter pair enclosing nothing, wins over a longer n-quote delimiter.
1695
1763
  // Returns { value, length } or null
1696
1764
  function parseQuotedStringAt(inputStr, startPos, quoteChar) {
1697
1765
  if (startPos >= inputStr.length || inputStr[startPos] !== quoteChar) {
@@ -1706,6 +1774,9 @@ function peg$parse(input, options) {
1706
1774
  pos++;
1707
1775
  }
1708
1776
 
1777
+ const isEvenRun = quoteCount % 2 === 0;
1778
+ const emptyReference = isEvenRun ? { value: '', length: quoteCount } : null;
1779
+
1709
1780
  const closeSeq = quoteChar.repeat(quoteCount);
1710
1781
  const escapeSeq = quoteChar.repeat(quoteCount * 2);
1711
1782
 
@@ -1724,6 +1795,9 @@ function peg$parse(input, options) {
1724
1795
  const afterClose = pos + quoteCount;
1725
1796
  if (afterClose >= inputStr.length || inputStr[afterClose] !== quoteChar) {
1726
1797
  // Found valid closing
1798
+ if (isEvenRun && !isSubstantiveBody(content)) {
1799
+ return emptyReference;
1800
+ }
1727
1801
  return {
1728
1802
  value: content,
1729
1803
  length: afterClose - startPos
@@ -1736,7 +1810,7 @@ function peg$parse(input, options) {
1736
1810
  pos++;
1737
1811
  }
1738
1812
 
1739
- return null; // No valid closing found
1813
+ return emptyReference; // No valid closing found
1740
1814
  }
1741
1815
 
1742
1816
  // Global state for passing parsed values between predicate and action
@@ -0,0 +1,91 @@
1
+ // Conformance tests for the empty reference (issue #288).
2
+ //
3
+ // A bare delimiter pair is the empty reference. The three delimiters `"`, `'`
4
+ // and ` (backtick) behave identically, and every longer n-quote run keeps the
5
+ // meaning it already had. The table below is shared with the Rust, Python, C#,
6
+ // Go, Java and PHP suites, so a document written by one implementation reads
7
+ // the same in all of them.
8
+
9
+ import { test, expect } from 'bun:test';
10
+ import { Parser } from '../src/Parser.js';
11
+ import { formatLinks } from '../src/Link.js';
12
+
13
+ const parser = new Parser();
14
+
15
+ // Render a parsed node unambiguously: every reference is wrapped in angle
16
+ // brackets so an empty one is visible as `<>`.
17
+ function render(node) {
18
+ if (!node.values || node.values.length === 0) {
19
+ return `<${node.id ?? ''}>`;
20
+ }
21
+ const head =
22
+ node.id === null || node.id === undefined ? '' : `<${node.id}>: `;
23
+ return `(${head}${node.values.map(render).join(' ')})`;
24
+ }
25
+
26
+ function parsesAs(input) {
27
+ return parser.parse(input).map(render).join('\n');
28
+ }
29
+
30
+ test('TestBareDelimiterPairIsTheEmptyReference', () => {
31
+ expect(parsesAs('(a "" b)')).toBe('(<a> <> <b>)');
32
+ });
33
+
34
+ test('TestEveryDelimiterStyleYieldsTheSameEmptyReference', () => {
35
+ expect(parsesAs('(a "" b)')).toBe('(<a> <> <b>)');
36
+ expect(parsesAs("(a '' b)")).toBe('(<a> <> <b>)');
37
+ expect(parsesAs('(a `` b)')).toBe('(<a> <> <b>)');
38
+ });
39
+
40
+ test('TestAdjacentEmptyReferencesStaySeparate', () => {
41
+ expect(parsesAs('(a "" "" b)')).toBe('(<a> <> <> <b>)');
42
+ expect(parsesAs("(a '' '' b)")).toBe('(<a> <> <> <b>)');
43
+ expect(parsesAs('(a `` `` b)')).toBe('(<a> <> <> <b>)');
44
+ expect(parsesAs('(a "" "" b)')).toBe('(<a> <> <> <b>)');
45
+ });
46
+
47
+ test('TestNestedEmptyReferencesParse', () => {
48
+ expect(parsesAs('("" ("" 1))')).toBe('(<> (<> <1>))');
49
+ expect(parsesAs('("" (\'\' 1))')).toBe('(<> (<> <1>))');
50
+ expect(parsesAs('("x" ("" 1))')).toBe('(<x> (<> <1>))');
51
+ expect(parsesAs('("" ("x" 1))')).toBe('(<> (<x> <1>))');
52
+ expect(parsesAs('("" x ("" 1))')).toBe('(<> <x> (<> <1>))');
53
+ expect(parsesAs('("" 1 ("" 1))')).toBe('(<> <1> (<> <1>))');
54
+ });
55
+
56
+ test('TestEmptyReferenceIsValidAsAnId', () => {
57
+ expect(parsesAs('("": 1)')).toBe('(<>: <1>)');
58
+ expect(parsesAs('(o: ("" (o: ("" 1))))')).toBe('(<o>: (<> (<o>: (<> <1>))))');
59
+ });
60
+
61
+ test('TestNQuoteDelimitedBodiesAreUnchanged', () => {
62
+ // A run that encloses a substantive body keeps its n-quote meaning.
63
+ expect(parsesAs('(a ""x"" b)')).toBe('(<a> <x> <b>)');
64
+ expect(parsesAs('(x "" " "")')).toBe('(<x> < " >)');
65
+ expect(parsesAs("(x ' \" ')")).toBe('(<x> < " >)');
66
+ // An n-quote-delimited empty is still empty.
67
+ expect(parsesAs('(a """" b)')).toBe('(<a> <> <b>)');
68
+ });
69
+
70
+ test('TestSingleSpaceStillReadsAsASpace', () => {
71
+ expect(parsesAs('(a " " b)')).toBe('(<a> < > <b>)');
72
+ });
73
+
74
+ test('TestEmptyReferenceSurvivesARoundTrip', () => {
75
+ const inputs = [
76
+ '(a "" b)',
77
+ '(a "" "" b)',
78
+ '("" ("" 1))',
79
+ '("": 1)',
80
+ '(o: ("" (o: ("" 1))))',
81
+ ];
82
+ for (const input of inputs) {
83
+ const links = parser.parse(input);
84
+ const formatted = formatLinks(links);
85
+ expect(parser.parse(formatted)).toEqual(links);
86
+ }
87
+ });
88
+
89
+ test('TestEmptyReferenceIsWrittenAsADelimiterPair', () => {
90
+ expect(formatLinks(parser.parse('(a "" b)'))).toBe('(a "" b)');
91
+ });
@@ -38,8 +38,8 @@ test('Link escapeReference with special characters', () => {
38
38
  expect(Link.escapeReference('has:colon')).toBe("'has:colon'");
39
39
  expect(Link.escapeReference('has space')).toBe("'has space'");
40
40
  expect(Link.escapeReference('has(paren)')).toBe("'has(paren)'");
41
- expect(Link.escapeReference('has"quote')).toBe(`'has"quote'`);
42
- expect(Link.escapeReference("has'quote")).toBe(`"has'quote"`);
41
+ expect(Link.escapeReference('has"quote')).toBe("'has\"quote'");
42
+ expect(Link.escapeReference("has'quote")).toBe('"has\'quote"');
43
43
  });
44
44
 
45
45
  test('Link simplify', () => {
@@ -18,16 +18,18 @@ as another reference'
18
18
  expect(result.length).toBeGreaterThan(0);
19
19
  expect(result.length).toBe(1);
20
20
 
21
+ // The two references sit on separate lines inside the parentheses, so the
22
+ // nested context turns each of them into its own link.
21
23
  const link = result[0];
22
24
  expect(link.id).toBe(null);
23
25
  expect(link.values).toBeTruthy();
24
26
  expect(link.values.length).toBe(2);
25
27
 
26
- expect(link.values[0].id).toBe(`long
28
+ expect(link.values[0].values[0].id).toBe(`long
27
29
  string literal representing
28
30
  the reference`);
29
31
 
30
- expect(link.values[1].id).toBe(`another
32
+ expect(link.values[1].values[0].id).toBe(`another
31
33
  long string literal
32
34
  as another reference`);
33
35
  });