links-notation 0.18.0 → 0.19.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/README.md +39 -4
- package/README.ru.md +39 -4
- package/dist/index.js +258 -21
- package/index.d.ts +19 -0
- package/package.json +1 -1
- package/src/Link.js +4 -0
- package/src/Parser.js +8 -1
- package/src/comments.js +164 -0
- package/src/grammar.pegjs +7 -1
- package/src/index.js +1 -0
- package/src/parser-generated.js +152 -18
- package/tests/Comments.test.js +160 -0
- package/tests/ParseErrorPosition.test.js +15 -13
package/README.md
CHANGED
|
@@ -252,6 +252,39 @@ const links = new Parser().parse(`value (
|
|
|
252
252
|
console.log(formatLinks(links)); // (value ((id 1) (label one)))
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
+
### Comments
|
|
256
|
+
|
|
257
|
+
A `#` hides the rest of the line it stands on, so a document can carry prose
|
|
258
|
+
about itself:
|
|
259
|
+
|
|
260
|
+
```lino
|
|
261
|
+
# the machines this deploys to
|
|
262
|
+
deploy: staging # only staging, for now
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Both comments are gone by the time the document is read, leaving the single
|
|
266
|
+
link `(deploy: staging)`. A `#` only opens a comment where a reference could
|
|
267
|
+
begin, so a `#` inside a token (`issue#1047`) and a `#` inside a delimited
|
|
268
|
+
reference (`"#"`) stay ordinary characters.
|
|
269
|
+
|
|
270
|
+
A formatter keeps the same rule from the other side: a reference that begins
|
|
271
|
+
with a `#` is written quoted (`'#tag'`), so a document it writes reads back as
|
|
272
|
+
itself.
|
|
273
|
+
|
|
274
|
+
Comments are on by default, and a parser can be told to read `#` as an ordinary
|
|
275
|
+
character again, for documents written before comments existed:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
import { Parser, formatLinks } from 'links-notation';
|
|
279
|
+
|
|
280
|
+
const document =
|
|
281
|
+
'# the machines this deploys to\ndeploy: staging # only staging, for now\n';
|
|
282
|
+
console.log(formatLinks(new Parser().parse(document))); // (deploy: staging)
|
|
283
|
+
|
|
284
|
+
const plain = new Parser({ comments: false });
|
|
285
|
+
console.log(formatLinks(plain.parse('# a b\n'))); // (# a b)
|
|
286
|
+
```
|
|
287
|
+
|
|
255
288
|
## API Reference
|
|
256
289
|
|
|
257
290
|
### Classes
|
|
@@ -260,6 +293,8 @@ console.log(formatLinks(links)); // (value ((id 1) (label one)))
|
|
|
260
293
|
|
|
261
294
|
Main parser class for converting strings to links.
|
|
262
295
|
|
|
296
|
+
- `constructor(options)` - Create a parser; `options.comments` set to `false`
|
|
297
|
+
reads `#` as an ordinary character instead of the start of a comment
|
|
263
298
|
- `initialize()` - Initialize the parser (async)
|
|
264
299
|
- `parse(input)` - Parse a Lino string and return links
|
|
265
300
|
|
|
@@ -288,7 +323,7 @@ where the document stopped making sense and quotes the offending line:
|
|
|
288
323
|
import { Parser, ParseError } from 'links-notation';
|
|
289
324
|
|
|
290
325
|
try {
|
|
291
|
-
new Parser().parse('
|
|
326
|
+
new Parser().parse('ci_gate x\nstage: rust: nextest\n');
|
|
292
327
|
} catch (error) {
|
|
293
328
|
console.error(error.message);
|
|
294
329
|
if (error instanceof ParseError) {
|
|
@@ -298,9 +333,9 @@ try {
|
|
|
298
333
|
```
|
|
299
334
|
|
|
300
335
|
```text
|
|
301
|
-
Syntax error at line 2, column
|
|
302
|
-
2 |
|
|
303
|
-
|
|
|
336
|
+
Syntax error at line 2, column 12: Expected "(", [ \t], [\r\n], or [^ \t\n\r(:)] but ":" found.
|
|
337
|
+
2 | stage: rust: nextest
|
|
338
|
+
| ^
|
|
304
339
|
```
|
|
305
340
|
|
|
306
341
|
- `offset` - Offset of the offending position from the start of the document
|
package/README.ru.md
CHANGED
|
@@ -171,6 +171,39 @@ const links = new Parser().parse(`value (
|
|
|
171
171
|
console.log(formatLinks(links)); // (value ((id 1) (label one)))
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
### Комментарии
|
|
175
|
+
|
|
176
|
+
`#` скрывает остаток строки, на которой стоит, поэтому документ может нести
|
|
177
|
+
пояснения о самом себе:
|
|
178
|
+
|
|
179
|
+
```lino
|
|
180
|
+
# машины, на которые идёт выкладка
|
|
181
|
+
deploy: staging # пока только staging
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
К моменту чтения документа обоих комментариев уже нет, остаётся одна связь
|
|
185
|
+
`(deploy: staging)`. `#` открывает комментарий только там, где могла бы
|
|
186
|
+
начаться ссылка, поэтому `#` внутри токена (`issue#1047`) и `#` внутри ссылки
|
|
187
|
+
в кавычках (`"#"`) остаются обычными символами.
|
|
188
|
+
|
|
189
|
+
Форматтер соблюдает то же правило с другой стороны: ссылка, начинающаяся с `#`,
|
|
190
|
+
записывается в кавычках (`'#tag'`), поэтому написанный им документ читается
|
|
191
|
+
обратно как он сам.
|
|
192
|
+
|
|
193
|
+
Комментарии включены по умолчанию, а парсеру можно велеть снова читать `#` как
|
|
194
|
+
обычный символ - для документов, написанных до появления комментариев:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
import { Parser, formatLinks } from 'links-notation';
|
|
198
|
+
|
|
199
|
+
const document =
|
|
200
|
+
'# машины, на которые идёт выкладка\ndeploy: staging # пока только staging\n';
|
|
201
|
+
console.log(formatLinks(new Parser().parse(document))); // (deploy: staging)
|
|
202
|
+
|
|
203
|
+
const plain = new Parser({ comments: false });
|
|
204
|
+
console.log(formatLinks(plain.parse('# a b\n'))); // (# a b)
|
|
205
|
+
```
|
|
206
|
+
|
|
174
207
|
## Справочник API
|
|
175
208
|
|
|
176
209
|
### Классы
|
|
@@ -179,6 +212,8 @@ console.log(formatLinks(links)); // (value ((id 1) (label one)))
|
|
|
179
212
|
|
|
180
213
|
Основной класс парсера для преобразования строк в связи.
|
|
181
214
|
|
|
215
|
+
- `constructor(options)` - Создать парсер; `options.comments` со значением
|
|
216
|
+
`false` читает `#` как обычный символ, а не как начало комментария
|
|
182
217
|
- `initialize()` - Инициализация парсера (асинхронно)
|
|
183
218
|
- `parse(input)` - Парсинг строки Lino и возвращение связей
|
|
184
219
|
|
|
@@ -207,7 +242,7 @@ console.log(formatLinks(links)); // (value ((id 1) (label one)))
|
|
|
207
242
|
import { Parser, ParseError } from 'links-notation';
|
|
208
243
|
|
|
209
244
|
try {
|
|
210
|
-
new Parser().parse('
|
|
245
|
+
new Parser().parse('ci_gate x\nstage: rust: nextest\n');
|
|
211
246
|
} catch (error) {
|
|
212
247
|
console.error(error.message);
|
|
213
248
|
if (error instanceof ParseError) {
|
|
@@ -217,9 +252,9 @@ try {
|
|
|
217
252
|
```
|
|
218
253
|
|
|
219
254
|
```text
|
|
220
|
-
Syntax error at line 2, column
|
|
221
|
-
2 |
|
|
222
|
-
|
|
|
255
|
+
Syntax error at line 2, column 12: Expected "(", [ \t], [\r\n], or [^ \t\n\r(:)] but ":" found.
|
|
256
|
+
2 | stage: rust: nextest
|
|
257
|
+
| ^
|
|
223
258
|
```
|
|
224
259
|
|
|
225
260
|
- `offset` - Смещение сломанной позиции от начала документа
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ class Link {
|
|
|
44
44
|
}
|
|
45
45
|
const hasSingleQuote = reference.includes("'");
|
|
46
46
|
const hasDoubleQuote = reference.includes('"');
|
|
47
|
-
const needsQuoting = reference.includes(":") || reference.includes("(") || reference.includes(")") || reference.includes(" ") || reference.includes("\t") || reference.includes(`
|
|
47
|
+
const needsQuoting = reference.startsWith("#") || reference.includes(":") || reference.includes("(") || reference.includes(")") || reference.includes(" ") || reference.includes("\t") || reference.includes(`
|
|
48
48
|
`) || reference.includes("\r") || hasDoubleQuote || hasSingleQuote;
|
|
49
49
|
if (hasSingleQuote && hasDoubleQuote) {
|
|
50
50
|
return `'${reference.replace(/'/g, "\\'")}'`;
|
|
@@ -320,6 +320,93 @@ function windowAround(lineText, column) {
|
|
|
320
320
|
return [quoted, target - start + shift + 1];
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
// src/comments.js
|
|
324
|
+
var COMMENT = "#";
|
|
325
|
+
var QUOTES = ['"', "'", "`"];
|
|
326
|
+
var BEFORE_REFERENCE = [" ", "\t", `
|
|
327
|
+
`, "\r", "(", ":"];
|
|
328
|
+
var BEFORE_COMMENT = [" ", "\t", `
|
|
329
|
+
`, "\r"];
|
|
330
|
+
function stripComments(document) {
|
|
331
|
+
let blanked = null;
|
|
332
|
+
let position = 0;
|
|
333
|
+
while (position < document.length) {
|
|
334
|
+
const character = document[position];
|
|
335
|
+
if (QUOTES.includes(character) && follows(document, position, BEFORE_REFERENCE)) {
|
|
336
|
+
const end = quotedReferenceEnd(document, position);
|
|
337
|
+
position = end === null ? position + 1 : end;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (character === COMMENT && follows(document, position, BEFORE_COMMENT)) {
|
|
341
|
+
blanked = blanked ?? document.split("");
|
|
342
|
+
while (position < document.length && document[position] !== `
|
|
343
|
+
` && document[position] !== "\r") {
|
|
344
|
+
blanked[position] = " ";
|
|
345
|
+
position++;
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
position++;
|
|
350
|
+
}
|
|
351
|
+
return blanked === null ? document : blanked.join("");
|
|
352
|
+
}
|
|
353
|
+
function follows(document, position, allowed) {
|
|
354
|
+
return position === 0 || allowed.includes(document[position - 1]);
|
|
355
|
+
}
|
|
356
|
+
function quotedReferenceEnd(document, start) {
|
|
357
|
+
const quote = document[start];
|
|
358
|
+
if (!QUOTES.includes(quote)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
let position = start;
|
|
362
|
+
while (position < document.length && document[position] === quote) {
|
|
363
|
+
position++;
|
|
364
|
+
}
|
|
365
|
+
const count = position - start;
|
|
366
|
+
const isEvenRun = count % 2 === 0;
|
|
367
|
+
const emptyReference = isEvenRun ? start + count : null;
|
|
368
|
+
const closing = quote.repeat(count);
|
|
369
|
+
const escape = quote.repeat(count * 2);
|
|
370
|
+
let content = "";
|
|
371
|
+
while (position < document.length) {
|
|
372
|
+
if (document.startsWith(escape, position)) {
|
|
373
|
+
content += closing;
|
|
374
|
+
position += escape.length;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (document.startsWith(closing, position)) {
|
|
378
|
+
const afterClosing = position + count;
|
|
379
|
+
if (afterClosing >= document.length || document[afterClosing] !== quote) {
|
|
380
|
+
if (isEvenRun && !isSubstantiveBody(content)) {
|
|
381
|
+
return emptyReference;
|
|
382
|
+
}
|
|
383
|
+
return afterClosing;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
content += document[position];
|
|
387
|
+
position++;
|
|
388
|
+
}
|
|
389
|
+
return emptyReference;
|
|
390
|
+
}
|
|
391
|
+
function isSubstantiveBody(content) {
|
|
392
|
+
let depth = 0;
|
|
393
|
+
let hasVisible = false;
|
|
394
|
+
for (const character of content) {
|
|
395
|
+
if (character === "(") {
|
|
396
|
+
depth++;
|
|
397
|
+
} else if (character === ")") {
|
|
398
|
+
depth--;
|
|
399
|
+
if (depth < 0) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!/\s/.test(character)) {
|
|
404
|
+
hasVisible = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return hasVisible && depth === 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
323
410
|
// src/parser-generated.js
|
|
324
411
|
class peg$SyntaxError extends SyntaxError {
|
|
325
412
|
constructor(message, expected, found, location) {
|
|
@@ -1751,42 +1838,189 @@ function peg$parse(input, options) {
|
|
|
1751
1838
|
return s0;
|
|
1752
1839
|
}
|
|
1753
1840
|
function peg$parseeol() {
|
|
1754
|
-
let s0, s1, s2
|
|
1841
|
+
let s0, s1, s2;
|
|
1755
1842
|
s0 = peg$currPos;
|
|
1756
1843
|
s1 = peg$parse__();
|
|
1757
|
-
s2 =
|
|
1758
|
-
|
|
1759
|
-
|
|
1844
|
+
s2 = peg$parselineBreaks();
|
|
1845
|
+
if (s2 === peg$FAILED) {
|
|
1846
|
+
s2 = peg$parseeof();
|
|
1847
|
+
if (s2 === peg$FAILED) {
|
|
1848
|
+
s2 = peg$parsenestedGroupEnd();
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (s2 !== peg$FAILED) {
|
|
1852
|
+
s1 = [s1, s2];
|
|
1853
|
+
s0 = s1;
|
|
1854
|
+
} else {
|
|
1855
|
+
peg$currPos = s0;
|
|
1856
|
+
s0 = peg$FAILED;
|
|
1857
|
+
}
|
|
1858
|
+
return s0;
|
|
1859
|
+
}
|
|
1860
|
+
function peg$parselineBreaks() {
|
|
1861
|
+
let s0, s1, s2, s3, s4, s5, s6;
|
|
1862
|
+
s0 = peg$currPos;
|
|
1863
|
+
s1 = [];
|
|
1864
|
+
s2 = input.charAt(peg$currPos);
|
|
1865
|
+
if (peg$r1.test(s2)) {
|
|
1760
1866
|
peg$currPos++;
|
|
1761
1867
|
} else {
|
|
1762
|
-
|
|
1868
|
+
s2 = peg$FAILED;
|
|
1763
1869
|
if (peg$silentFails === 0) {
|
|
1764
1870
|
peg$fail(peg$e1);
|
|
1765
1871
|
}
|
|
1766
1872
|
}
|
|
1767
|
-
if (
|
|
1768
|
-
while (
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
if (peg$r1.test(
|
|
1873
|
+
if (s2 !== peg$FAILED) {
|
|
1874
|
+
while (s2 !== peg$FAILED) {
|
|
1875
|
+
s1.push(s2);
|
|
1876
|
+
s2 = input.charAt(peg$currPos);
|
|
1877
|
+
if (peg$r1.test(s2)) {
|
|
1772
1878
|
peg$currPos++;
|
|
1773
1879
|
} else {
|
|
1774
|
-
|
|
1880
|
+
s2 = peg$FAILED;
|
|
1775
1881
|
if (peg$silentFails === 0) {
|
|
1776
1882
|
peg$fail(peg$e1);
|
|
1777
1883
|
}
|
|
1778
1884
|
}
|
|
1779
1885
|
}
|
|
1780
1886
|
} else {
|
|
1781
|
-
|
|
1887
|
+
s1 = peg$FAILED;
|
|
1782
1888
|
}
|
|
1783
|
-
if (
|
|
1784
|
-
s2 =
|
|
1785
|
-
|
|
1786
|
-
|
|
1889
|
+
if (s1 !== peg$FAILED) {
|
|
1890
|
+
s2 = [];
|
|
1891
|
+
s3 = peg$currPos;
|
|
1892
|
+
s4 = [];
|
|
1893
|
+
s5 = input.charAt(peg$currPos);
|
|
1894
|
+
if (peg$r0.test(s5)) {
|
|
1895
|
+
peg$currPos++;
|
|
1896
|
+
} else {
|
|
1897
|
+
s5 = peg$FAILED;
|
|
1898
|
+
if (peg$silentFails === 0) {
|
|
1899
|
+
peg$fail(peg$e0);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (s5 !== peg$FAILED) {
|
|
1903
|
+
while (s5 !== peg$FAILED) {
|
|
1904
|
+
s4.push(s5);
|
|
1905
|
+
s5 = input.charAt(peg$currPos);
|
|
1906
|
+
if (peg$r0.test(s5)) {
|
|
1907
|
+
peg$currPos++;
|
|
1908
|
+
} else {
|
|
1909
|
+
s5 = peg$FAILED;
|
|
1910
|
+
if (peg$silentFails === 0) {
|
|
1911
|
+
peg$fail(peg$e0);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
} else {
|
|
1916
|
+
s4 = peg$FAILED;
|
|
1917
|
+
}
|
|
1918
|
+
if (s4 !== peg$FAILED) {
|
|
1919
|
+
s5 = [];
|
|
1920
|
+
s6 = input.charAt(peg$currPos);
|
|
1921
|
+
if (peg$r1.test(s6)) {
|
|
1922
|
+
peg$currPos++;
|
|
1923
|
+
} else {
|
|
1924
|
+
s6 = peg$FAILED;
|
|
1925
|
+
if (peg$silentFails === 0) {
|
|
1926
|
+
peg$fail(peg$e1);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (s6 !== peg$FAILED) {
|
|
1930
|
+
while (s6 !== peg$FAILED) {
|
|
1931
|
+
s5.push(s6);
|
|
1932
|
+
s6 = input.charAt(peg$currPos);
|
|
1933
|
+
if (peg$r1.test(s6)) {
|
|
1934
|
+
peg$currPos++;
|
|
1935
|
+
} else {
|
|
1936
|
+
s6 = peg$FAILED;
|
|
1937
|
+
if (peg$silentFails === 0) {
|
|
1938
|
+
peg$fail(peg$e1);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
} else {
|
|
1943
|
+
s5 = peg$FAILED;
|
|
1944
|
+
}
|
|
1945
|
+
if (s5 !== peg$FAILED) {
|
|
1946
|
+
s4 = [s4, s5];
|
|
1947
|
+
s3 = s4;
|
|
1948
|
+
} else {
|
|
1949
|
+
peg$currPos = s3;
|
|
1950
|
+
s3 = peg$FAILED;
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
peg$currPos = s3;
|
|
1954
|
+
s3 = peg$FAILED;
|
|
1955
|
+
}
|
|
1956
|
+
while (s3 !== peg$FAILED) {
|
|
1957
|
+
s2.push(s3);
|
|
1958
|
+
s3 = peg$currPos;
|
|
1959
|
+
s4 = [];
|
|
1960
|
+
s5 = input.charAt(peg$currPos);
|
|
1961
|
+
if (peg$r0.test(s5)) {
|
|
1962
|
+
peg$currPos++;
|
|
1963
|
+
} else {
|
|
1964
|
+
s5 = peg$FAILED;
|
|
1965
|
+
if (peg$silentFails === 0) {
|
|
1966
|
+
peg$fail(peg$e0);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (s5 !== peg$FAILED) {
|
|
1970
|
+
while (s5 !== peg$FAILED) {
|
|
1971
|
+
s4.push(s5);
|
|
1972
|
+
s5 = input.charAt(peg$currPos);
|
|
1973
|
+
if (peg$r0.test(s5)) {
|
|
1974
|
+
peg$currPos++;
|
|
1975
|
+
} else {
|
|
1976
|
+
s5 = peg$FAILED;
|
|
1977
|
+
if (peg$silentFails === 0) {
|
|
1978
|
+
peg$fail(peg$e0);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
} else {
|
|
1983
|
+
s4 = peg$FAILED;
|
|
1984
|
+
}
|
|
1985
|
+
if (s4 !== peg$FAILED) {
|
|
1986
|
+
s5 = [];
|
|
1987
|
+
s6 = input.charAt(peg$currPos);
|
|
1988
|
+
if (peg$r1.test(s6)) {
|
|
1989
|
+
peg$currPos++;
|
|
1990
|
+
} else {
|
|
1991
|
+
s6 = peg$FAILED;
|
|
1992
|
+
if (peg$silentFails === 0) {
|
|
1993
|
+
peg$fail(peg$e1);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (s6 !== peg$FAILED) {
|
|
1997
|
+
while (s6 !== peg$FAILED) {
|
|
1998
|
+
s5.push(s6);
|
|
1999
|
+
s6 = input.charAt(peg$currPos);
|
|
2000
|
+
if (peg$r1.test(s6)) {
|
|
2001
|
+
peg$currPos++;
|
|
2002
|
+
} else {
|
|
2003
|
+
s6 = peg$FAILED;
|
|
2004
|
+
if (peg$silentFails === 0) {
|
|
2005
|
+
peg$fail(peg$e1);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
} else {
|
|
2010
|
+
s5 = peg$FAILED;
|
|
2011
|
+
}
|
|
2012
|
+
if (s5 !== peg$FAILED) {
|
|
2013
|
+
s4 = [s4, s5];
|
|
2014
|
+
s3 = s4;
|
|
2015
|
+
} else {
|
|
2016
|
+
peg$currPos = s3;
|
|
2017
|
+
s3 = peg$FAILED;
|
|
2018
|
+
}
|
|
2019
|
+
} else {
|
|
2020
|
+
peg$currPos = s3;
|
|
2021
|
+
s3 = peg$FAILED;
|
|
2022
|
+
}
|
|
1787
2023
|
}
|
|
1788
|
-
}
|
|
1789
|
-
if (s2 !== peg$FAILED) {
|
|
1790
2024
|
s1 = [s1, s2];
|
|
1791
2025
|
s0 = s1;
|
|
1792
2026
|
} else {
|
|
@@ -2063,6 +2297,7 @@ class Parser {
|
|
|
2063
2297
|
constructor(options = {}) {
|
|
2064
2298
|
this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024;
|
|
2065
2299
|
this.maxDepth = options.maxDepth || 1000;
|
|
2300
|
+
this.comments = options.comments ?? true;
|
|
2066
2301
|
}
|
|
2067
2302
|
parse(input) {
|
|
2068
2303
|
if (typeof input !== "string") {
|
|
@@ -2071,8 +2306,9 @@ class Parser {
|
|
|
2071
2306
|
if (input.length > this.maxInputSize) {
|
|
2072
2307
|
throw new Error(`Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`);
|
|
2073
2308
|
}
|
|
2309
|
+
const prepared = this.comments ? stripComments(input) : input;
|
|
2074
2310
|
try {
|
|
2075
|
-
const rawResult = peg$parse(
|
|
2311
|
+
const rawResult = peg$parse(prepared);
|
|
2076
2312
|
return this.transformResult(rawResult);
|
|
2077
2313
|
} catch (error) {
|
|
2078
2314
|
if (error && error.location) {
|
|
@@ -2218,5 +2454,6 @@ export {
|
|
|
2218
2454
|
LinksGroup,
|
|
2219
2455
|
ParseError,
|
|
2220
2456
|
Parser,
|
|
2221
|
-
formatLinks
|
|
2457
|
+
formatLinks,
|
|
2458
|
+
stripComments
|
|
2222
2459
|
};
|
package/index.d.ts
CHANGED
|
@@ -116,6 +116,12 @@ export interface ParserOptions {
|
|
|
116
116
|
* Maximum nesting depth (default: 1000)
|
|
117
117
|
*/
|
|
118
118
|
maxDepth?: number;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* If false, read `#` as an ordinary character instead of the start of a
|
|
122
|
+
* comment (default: true)
|
|
123
|
+
*/
|
|
124
|
+
comments?: boolean;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
/**
|
|
@@ -134,6 +140,11 @@ export class Parser {
|
|
|
134
140
|
*/
|
|
135
141
|
maxDepth: number;
|
|
136
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether `#` starts a comment that runs to the end of its line
|
|
145
|
+
*/
|
|
146
|
+
comments: boolean;
|
|
147
|
+
|
|
137
148
|
/**
|
|
138
149
|
* Create a new Parser instance
|
|
139
150
|
* @param options - Parser options
|
|
@@ -149,6 +160,14 @@ export class Parser {
|
|
|
149
160
|
parse(input: string): Link[];
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Blank out every comment in a document, keeping its length so that every
|
|
165
|
+
* character stays at the offset it was written at.
|
|
166
|
+
* @param document - The document to read
|
|
167
|
+
* @returns The document, of the same length, without its comments
|
|
168
|
+
*/
|
|
169
|
+
export function stripComments(document: string): string;
|
|
170
|
+
|
|
152
171
|
/**
|
|
153
172
|
* The position a parse stopped at, as the generated parser reports it
|
|
154
173
|
*/
|
package/package.json
CHANGED
package/src/Link.js
CHANGED
|
@@ -95,7 +95,11 @@ export class Link {
|
|
|
95
95
|
const hasSingleQuote = reference.includes("'");
|
|
96
96
|
const hasDoubleQuote = reference.includes('"');
|
|
97
97
|
|
|
98
|
+
// A reference that begins with a `#` has to be quoted, or it would read
|
|
99
|
+
// back as a comment. A `#` anywhere else in a reference is content
|
|
100
|
+
// (`issue#1047`), so only the first character matters.
|
|
98
101
|
const needsQuoting =
|
|
102
|
+
reference.startsWith('#') ||
|
|
99
103
|
reference.includes(':') ||
|
|
100
104
|
reference.includes('(') ||
|
|
101
105
|
reference.includes(')') ||
|
package/src/Parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Link } from './Link.js';
|
|
2
2
|
import { ParseError } from './ParseError.js';
|
|
3
|
+
import { stripComments } from './comments.js';
|
|
3
4
|
import * as parserModule from './parser-generated.js';
|
|
4
5
|
|
|
5
6
|
export class Parser {
|
|
@@ -8,10 +9,12 @@ export class Parser {
|
|
|
8
9
|
* @param {Object} options - Parser options
|
|
9
10
|
* @param {number} options.maxInputSize - Maximum input size in bytes (default: 10MB)
|
|
10
11
|
* @param {number} options.maxDepth - Maximum nesting depth (default: 1000)
|
|
12
|
+
* @param {boolean} options.comments - If false, read `#` as an ordinary character instead of the start of a comment (default: true)
|
|
11
13
|
*/
|
|
12
14
|
constructor(options = {}) {
|
|
13
15
|
this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024; // 10MB default
|
|
14
16
|
this.maxDepth = options.maxDepth || 1000;
|
|
17
|
+
this.comments = options.comments ?? true;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -32,8 +35,12 @@ export class Parser {
|
|
|
32
35
|
);
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
// Comments are blanked rather than removed, so a position reported for the
|
|
39
|
+
// prepared document is the same position in the document the caller wrote.
|
|
40
|
+
const prepared = this.comments ? stripComments(input) : input;
|
|
41
|
+
|
|
35
42
|
try {
|
|
36
|
-
const rawResult = parserModule.parse(
|
|
43
|
+
const rawResult = parserModule.parse(prepared);
|
|
37
44
|
return this.transformResult(rawResult);
|
|
38
45
|
} catch (error) {
|
|
39
46
|
// A syntax error knows where it stopped; anything else is passed on with
|
package/src/comments.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comments, and the rule that decides where one starts.
|
|
3
|
+
*
|
|
4
|
+
* `#` starts a comment when it opens a token: at the start of the document or
|
|
5
|
+
* after a space, a tab or a line break. A `#` written inside a token
|
|
6
|
+
* (`issue#1047`) or inside a delimited reference (`"# not a comment"`) is an
|
|
7
|
+
* ordinary character.
|
|
8
|
+
*
|
|
9
|
+
* {@link stripComments} replaces every character of every comment with a space
|
|
10
|
+
* rather than removing it, so every later character keeps the offset it had in
|
|
11
|
+
* the document the caller wrote, and a parse error still points at the position
|
|
12
|
+
* the reader can see.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The character that starts a comment. */
|
|
16
|
+
export const COMMENT = '#';
|
|
17
|
+
|
|
18
|
+
/** The delimiters a reference can be written between. */
|
|
19
|
+
const QUOTES = ['"', "'", '`'];
|
|
20
|
+
|
|
21
|
+
/** What a delimited reference may start after. */
|
|
22
|
+
const BEFORE_REFERENCE = [' ', '\t', '\n', '\r', '(', ':'];
|
|
23
|
+
|
|
24
|
+
/** What a comment may start after. */
|
|
25
|
+
const BEFORE_COMMENT = [' ', '\t', '\n', '\r'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The document with every comment blanked out.
|
|
29
|
+
* @param {string} document - The document to read
|
|
30
|
+
* @returns {string} The document, of the same length, without its comments
|
|
31
|
+
* @example
|
|
32
|
+
* stripComments('a: b # why\n'); // 'a: b \n'
|
|
33
|
+
* stripComments('"# kept"\n'); // '"# kept"\n'
|
|
34
|
+
* stripComments('issue#1047\n'); // 'issue#1047\n'
|
|
35
|
+
*/
|
|
36
|
+
export function stripComments(document) {
|
|
37
|
+
// Split into UTF-16 units, the units the positions here and the positions a
|
|
38
|
+
// parse error reports are both counted in.
|
|
39
|
+
let blanked = null;
|
|
40
|
+
let position = 0;
|
|
41
|
+
|
|
42
|
+
while (position < document.length) {
|
|
43
|
+
const character = document[position];
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
QUOTES.includes(character) &&
|
|
47
|
+
follows(document, position, BEFORE_REFERENCE)
|
|
48
|
+
) {
|
|
49
|
+
const end = quotedReferenceEnd(document, position);
|
|
50
|
+
position = end === null ? position + 1 : end;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (character === COMMENT && follows(document, position, BEFORE_COMMENT)) {
|
|
55
|
+
blanked = blanked ?? document.split('');
|
|
56
|
+
while (
|
|
57
|
+
position < document.length &&
|
|
58
|
+
document[position] !== '\n' &&
|
|
59
|
+
document[position] !== '\r'
|
|
60
|
+
) {
|
|
61
|
+
blanked[position] = ' ';
|
|
62
|
+
position++;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
position++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return blanked === null ? document : blanked.join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the character before the given position is one the caller allows,
|
|
75
|
+
* the start of the document counting as allowed.
|
|
76
|
+
* @param {string} document - The document being read
|
|
77
|
+
* @param {number} position - Position of the character being judged
|
|
78
|
+
* @param {string[]} allowed - The characters that may stand before it
|
|
79
|
+
* @returns {boolean} True when the position opens a token
|
|
80
|
+
*/
|
|
81
|
+
function follows(document, position, allowed) {
|
|
82
|
+
return position === 0 || allowed.includes(document[position - 1]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The position just past the delimited reference that starts at `start`, or
|
|
87
|
+
* `null` when nothing there opens one.
|
|
88
|
+
*
|
|
89
|
+
* This follows the same rules as the `parseQuotedStringAt` of the grammar: a
|
|
90
|
+
* run of N delimiters closes at the next run of exactly N, a run of 2N is that
|
|
91
|
+
* delimiter escaped, and an even run that encloses nothing substantive is the
|
|
92
|
+
* empty reference.
|
|
93
|
+
* @param {string} document - The document being read
|
|
94
|
+
* @param {number} start - Position of the opening delimiter
|
|
95
|
+
* @returns {number|null} The position just past the reference
|
|
96
|
+
*/
|
|
97
|
+
export function quotedReferenceEnd(document, start) {
|
|
98
|
+
const quote = document[start];
|
|
99
|
+
if (!QUOTES.includes(quote)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let position = start;
|
|
104
|
+
while (position < document.length && document[position] === quote) {
|
|
105
|
+
position++;
|
|
106
|
+
}
|
|
107
|
+
const count = position - start;
|
|
108
|
+
const isEvenRun = count % 2 === 0;
|
|
109
|
+
const emptyReference = isEvenRun ? start + count : null;
|
|
110
|
+
|
|
111
|
+
const closing = quote.repeat(count);
|
|
112
|
+
const escape = quote.repeat(count * 2);
|
|
113
|
+
let content = '';
|
|
114
|
+
|
|
115
|
+
while (position < document.length) {
|
|
116
|
+
if (document.startsWith(escape, position)) {
|
|
117
|
+
content += closing;
|
|
118
|
+
position += escape.length;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (document.startsWith(closing, position)) {
|
|
123
|
+
const afterClosing = position + count;
|
|
124
|
+
if (afterClosing >= document.length || document[afterClosing] !== quote) {
|
|
125
|
+
if (isEvenRun && !isSubstantiveBody(content)) {
|
|
126
|
+
return emptyReference;
|
|
127
|
+
}
|
|
128
|
+
return afterClosing;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
content += document[position];
|
|
133
|
+
position++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return emptyReference;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether a body written between an even run of delimiters carries something a
|
|
141
|
+
* pair of delimiters enclosing nothing cannot.
|
|
142
|
+
* @param {string} content - The body between the delimiters
|
|
143
|
+
* @returns {boolean} True when the n-quote reading is the one to take
|
|
144
|
+
*/
|
|
145
|
+
function isSubstantiveBody(content) {
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let hasVisible = false;
|
|
148
|
+
|
|
149
|
+
for (const character of content) {
|
|
150
|
+
if (character === '(') {
|
|
151
|
+
depth++;
|
|
152
|
+
} else if (character === ')') {
|
|
153
|
+
depth--;
|
|
154
|
+
if (depth < 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!/\s/.test(character)) {
|
|
159
|
+
hasVisible = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return hasVisible && depth === 0;
|
|
164
|
+
}
|
package/src/grammar.pegjs
CHANGED
|
@@ -264,7 +264,13 @@ PUSH_INDENTATION = spaces:" "* &{ return normalizeIndentation(spaces) > getCurre
|
|
|
264
264
|
|
|
265
265
|
CHECK_INDENTATION = spaces:" "* &{ return checkIndentation(spaces); }
|
|
266
266
|
|
|
267
|
-
eol = __ (
|
|
267
|
+
eol = __ (lineBreaks / eof / nestedGroupEnd)
|
|
268
|
+
|
|
269
|
+
// A line ends at the first line break, and the blank lines that follow it
|
|
270
|
+
// belong to that ending: a line holding nothing but spaces or tabs separates
|
|
271
|
+
// links exactly the way an empty line does. Blanking a comment leaves such a
|
|
272
|
+
// line behind, so this is also what lets a comment stand on a line of its own.
|
|
273
|
+
lineBreaks = [\r\n]+ ([ \t]+ [\r\n]+)*
|
|
268
274
|
|
|
269
275
|
// Inside a parenthesised group the closing parenthesis terminates a line the
|
|
270
276
|
// same way the end of the input does at the root.
|
package/src/index.js
CHANGED
package/src/parser-generated.js
CHANGED
|
@@ -1487,39 +1487,173 @@ function peg$parse(input, options) {
|
|
|
1487
1487
|
}
|
|
1488
1488
|
|
|
1489
1489
|
function peg$parseeol() {
|
|
1490
|
-
let s0, s1, s2
|
|
1490
|
+
let s0, s1, s2;
|
|
1491
1491
|
|
|
1492
1492
|
s0 = peg$currPos;
|
|
1493
1493
|
s1 = peg$parse__();
|
|
1494
|
-
s2 =
|
|
1495
|
-
|
|
1496
|
-
|
|
1494
|
+
s2 = peg$parselineBreaks();
|
|
1495
|
+
if (s2 === peg$FAILED) {
|
|
1496
|
+
s2 = peg$parseeof();
|
|
1497
|
+
if (s2 === peg$FAILED) {
|
|
1498
|
+
s2 = peg$parsenestedGroupEnd();
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (s2 !== peg$FAILED) {
|
|
1502
|
+
s1 = [s1, s2];
|
|
1503
|
+
s0 = s1;
|
|
1504
|
+
} else {
|
|
1505
|
+
peg$currPos = s0;
|
|
1506
|
+
s0 = peg$FAILED;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return s0;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function peg$parselineBreaks() {
|
|
1513
|
+
let s0, s1, s2, s3, s4, s5, s6;
|
|
1514
|
+
|
|
1515
|
+
s0 = peg$currPos;
|
|
1516
|
+
s1 = [];
|
|
1517
|
+
s2 = input.charAt(peg$currPos);
|
|
1518
|
+
if (peg$r1.test(s2)) {
|
|
1497
1519
|
peg$currPos++;
|
|
1498
1520
|
} else {
|
|
1499
|
-
|
|
1521
|
+
s2 = peg$FAILED;
|
|
1500
1522
|
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1501
1523
|
}
|
|
1502
|
-
if (
|
|
1503
|
-
while (
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (peg$r1.test(
|
|
1524
|
+
if (s2 !== peg$FAILED) {
|
|
1525
|
+
while (s2 !== peg$FAILED) {
|
|
1526
|
+
s1.push(s2);
|
|
1527
|
+
s2 = input.charAt(peg$currPos);
|
|
1528
|
+
if (peg$r1.test(s2)) {
|
|
1507
1529
|
peg$currPos++;
|
|
1508
1530
|
} else {
|
|
1509
|
-
|
|
1531
|
+
s2 = peg$FAILED;
|
|
1510
1532
|
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1511
1533
|
}
|
|
1512
1534
|
}
|
|
1513
1535
|
} else {
|
|
1514
|
-
|
|
1536
|
+
s1 = peg$FAILED;
|
|
1515
1537
|
}
|
|
1516
|
-
if (
|
|
1517
|
-
s2 =
|
|
1518
|
-
|
|
1519
|
-
|
|
1538
|
+
if (s1 !== peg$FAILED) {
|
|
1539
|
+
s2 = [];
|
|
1540
|
+
s3 = peg$currPos;
|
|
1541
|
+
s4 = [];
|
|
1542
|
+
s5 = input.charAt(peg$currPos);
|
|
1543
|
+
if (peg$r0.test(s5)) {
|
|
1544
|
+
peg$currPos++;
|
|
1545
|
+
} else {
|
|
1546
|
+
s5 = peg$FAILED;
|
|
1547
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1548
|
+
}
|
|
1549
|
+
if (s5 !== peg$FAILED) {
|
|
1550
|
+
while (s5 !== peg$FAILED) {
|
|
1551
|
+
s4.push(s5);
|
|
1552
|
+
s5 = input.charAt(peg$currPos);
|
|
1553
|
+
if (peg$r0.test(s5)) {
|
|
1554
|
+
peg$currPos++;
|
|
1555
|
+
} else {
|
|
1556
|
+
s5 = peg$FAILED;
|
|
1557
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
s4 = peg$FAILED;
|
|
1562
|
+
}
|
|
1563
|
+
if (s4 !== peg$FAILED) {
|
|
1564
|
+
s5 = [];
|
|
1565
|
+
s6 = input.charAt(peg$currPos);
|
|
1566
|
+
if (peg$r1.test(s6)) {
|
|
1567
|
+
peg$currPos++;
|
|
1568
|
+
} else {
|
|
1569
|
+
s6 = peg$FAILED;
|
|
1570
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1571
|
+
}
|
|
1572
|
+
if (s6 !== peg$FAILED) {
|
|
1573
|
+
while (s6 !== peg$FAILED) {
|
|
1574
|
+
s5.push(s6);
|
|
1575
|
+
s6 = input.charAt(peg$currPos);
|
|
1576
|
+
if (peg$r1.test(s6)) {
|
|
1577
|
+
peg$currPos++;
|
|
1578
|
+
} else {
|
|
1579
|
+
s6 = peg$FAILED;
|
|
1580
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
} else {
|
|
1584
|
+
s5 = peg$FAILED;
|
|
1585
|
+
}
|
|
1586
|
+
if (s5 !== peg$FAILED) {
|
|
1587
|
+
s4 = [s4, s5];
|
|
1588
|
+
s3 = s4;
|
|
1589
|
+
} else {
|
|
1590
|
+
peg$currPos = s3;
|
|
1591
|
+
s3 = peg$FAILED;
|
|
1592
|
+
}
|
|
1593
|
+
} else {
|
|
1594
|
+
peg$currPos = s3;
|
|
1595
|
+
s3 = peg$FAILED;
|
|
1596
|
+
}
|
|
1597
|
+
while (s3 !== peg$FAILED) {
|
|
1598
|
+
s2.push(s3);
|
|
1599
|
+
s3 = peg$currPos;
|
|
1600
|
+
s4 = [];
|
|
1601
|
+
s5 = input.charAt(peg$currPos);
|
|
1602
|
+
if (peg$r0.test(s5)) {
|
|
1603
|
+
peg$currPos++;
|
|
1604
|
+
} else {
|
|
1605
|
+
s5 = peg$FAILED;
|
|
1606
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1607
|
+
}
|
|
1608
|
+
if (s5 !== peg$FAILED) {
|
|
1609
|
+
while (s5 !== peg$FAILED) {
|
|
1610
|
+
s4.push(s5);
|
|
1611
|
+
s5 = input.charAt(peg$currPos);
|
|
1612
|
+
if (peg$r0.test(s5)) {
|
|
1613
|
+
peg$currPos++;
|
|
1614
|
+
} else {
|
|
1615
|
+
s5 = peg$FAILED;
|
|
1616
|
+
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
} else {
|
|
1620
|
+
s4 = peg$FAILED;
|
|
1621
|
+
}
|
|
1622
|
+
if (s4 !== peg$FAILED) {
|
|
1623
|
+
s5 = [];
|
|
1624
|
+
s6 = input.charAt(peg$currPos);
|
|
1625
|
+
if (peg$r1.test(s6)) {
|
|
1626
|
+
peg$currPos++;
|
|
1627
|
+
} else {
|
|
1628
|
+
s6 = peg$FAILED;
|
|
1629
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1630
|
+
}
|
|
1631
|
+
if (s6 !== peg$FAILED) {
|
|
1632
|
+
while (s6 !== peg$FAILED) {
|
|
1633
|
+
s5.push(s6);
|
|
1634
|
+
s6 = input.charAt(peg$currPos);
|
|
1635
|
+
if (peg$r1.test(s6)) {
|
|
1636
|
+
peg$currPos++;
|
|
1637
|
+
} else {
|
|
1638
|
+
s6 = peg$FAILED;
|
|
1639
|
+
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
} else {
|
|
1643
|
+
s5 = peg$FAILED;
|
|
1644
|
+
}
|
|
1645
|
+
if (s5 !== peg$FAILED) {
|
|
1646
|
+
s4 = [s4, s5];
|
|
1647
|
+
s3 = s4;
|
|
1648
|
+
} else {
|
|
1649
|
+
peg$currPos = s3;
|
|
1650
|
+
s3 = peg$FAILED;
|
|
1651
|
+
}
|
|
1652
|
+
} else {
|
|
1653
|
+
peg$currPos = s3;
|
|
1654
|
+
s3 = peg$FAILED;
|
|
1655
|
+
}
|
|
1520
1656
|
}
|
|
1521
|
-
}
|
|
1522
|
-
if (s2 !== peg$FAILED) {
|
|
1523
1657
|
s1 = [s1, s2];
|
|
1524
1658
|
s0 = s1;
|
|
1525
1659
|
} else {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Conformance tests for line comments (issue #301).
|
|
2
|
+
//
|
|
3
|
+
// `#` starts a comment that runs to the end of the line, unless it sits inside
|
|
4
|
+
// a token or inside a delimited reference. Parsers accept comments by default
|
|
5
|
+
// and can be told to treat `#` as an ordinary character again. The table below
|
|
6
|
+
// is shared with the Rust, Python, C#, Go, Java and PHP suites, so a document
|
|
7
|
+
// written by one implementation reads the same in all of them.
|
|
8
|
+
|
|
9
|
+
import { test, expect } from 'bun:test';
|
|
10
|
+
import {
|
|
11
|
+
Link,
|
|
12
|
+
Parser,
|
|
13
|
+
ParseError,
|
|
14
|
+
formatLinks,
|
|
15
|
+
stripComments,
|
|
16
|
+
} from '../src/index.js';
|
|
17
|
+
|
|
18
|
+
const parser = new Parser();
|
|
19
|
+
|
|
20
|
+
// Render a parsed node unambiguously: every reference is wrapped in angle
|
|
21
|
+
// brackets, so a `#` that survived as content is visible.
|
|
22
|
+
function render(node) {
|
|
23
|
+
if (!node.values || node.values.length === 0) {
|
|
24
|
+
return `<${node.id ?? ''}>`;
|
|
25
|
+
}
|
|
26
|
+
const head =
|
|
27
|
+
node.id === null || node.id === undefined ? '' : `<${node.id}>: `;
|
|
28
|
+
return `(${head}${node.values.map(render).join(' ')})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parsesAs(input) {
|
|
32
|
+
return parser.parse(input).map(render).join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('a line that starts with a hash is a comment', () => {
|
|
36
|
+
expect(parsesAs('# a b\n')).toBe('');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('a comment may hold a colon', () => {
|
|
40
|
+
// The document from #301: prose with a colon used to be a parse error.
|
|
41
|
+
expect(parsesAs('# a: b\n')).toBe('');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('a comment may hold anything at all', () => {
|
|
45
|
+
expect(parsesAs('# ) : ( " \' ` #\n')).toBe('');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a comment ends at the end of its line', () => {
|
|
49
|
+
expect(parsesAs('# note\na: b\n')).toBe('(<a>: <b>)');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a comment may follow a link', () => {
|
|
53
|
+
expect(parsesAs('a: b # why\n')).toBe('(<a>: <b>)');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a comment may follow a group', () => {
|
|
57
|
+
expect(parsesAs('(a b) # why\n')).toBe('(<a> <b>)');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a comment needs no closing newline', () => {
|
|
61
|
+
expect(parsesAs('a: b # why')).toBe('(<a>: <b>)');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a comment line inside an indented block is skipped', () => {
|
|
65
|
+
expect(parsesAs('parent\n # what the child is for\n child\n')).toBe(
|
|
66
|
+
parsesAs('parent\n child\n')
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a comment line inside a group is skipped', () => {
|
|
71
|
+
expect(parsesAs('(\n a\n # why\n b\n)\n')).toBe(
|
|
72
|
+
parsesAs('(\n a\n b\n)\n')
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a line of spaces separates links the way an empty line does', () => {
|
|
77
|
+
// Blanking a comment leaves a line of spaces behind, so such a line has to
|
|
78
|
+
// read as a blank line.
|
|
79
|
+
expect(parsesAs('a\n \nb\n')).toBe(parsesAs('a\n\nb\n'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('a document of comments alone holds no links', () => {
|
|
83
|
+
expect(parsesAs('# one\n# two\n')).toBe('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a hash inside a token is an ordinary character', () => {
|
|
87
|
+
expect(parsesAs('issue#1047\n')).toBe('(<issue#1047>)');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a hash that opens a token is an ordinary character', () => {
|
|
91
|
+
expect(parsesAs('a: b#c\n')).toBe('(<a>: <b#c>)');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('a hash inside a delimited reference is content', () => {
|
|
95
|
+
expect(parsesAs('"# not a comment" a\n')).toBe('(<# not a comment> <a>)');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a comment may follow a delimited reference', () => {
|
|
99
|
+
expect(parsesAs('"a" # why\n')).toBe('(<a>)');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a hash inside a multiline delimited reference is content', () => {
|
|
103
|
+
expect(parsesAs('"a # b\nc" d\n')).toBe('(<a # b\nc> <d>)');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('a parser without comments keeps the hash', () => {
|
|
107
|
+
const plain = new Parser({ comments: false });
|
|
108
|
+
|
|
109
|
+
expect(plain.parse('# a b\n').map(render).join('\n')).toBe('(<#> <a> <b>)');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('a parser without comments still rejects the document from the issue', () => {
|
|
113
|
+
const plain = new Parser({ comments: false });
|
|
114
|
+
|
|
115
|
+
expect(() => plain.parse('# a: b\n')).toThrow(ParseError);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('comments are on by default', () => {
|
|
119
|
+
expect(new Parser().comments).toBe(true);
|
|
120
|
+
expect(new Parser({}).comments).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a comment does not move the position a later error is reported at', () => {
|
|
124
|
+
// Blanking a comment keeps every later character where it was, so the
|
|
125
|
+
// position reported for a defect is the position in the original document.
|
|
126
|
+
let error = null;
|
|
127
|
+
try {
|
|
128
|
+
parser.parse('# a comment\nstage: rust: nextest\n');
|
|
129
|
+
} catch (thrown) {
|
|
130
|
+
error = thrown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
expect(error).toBeInstanceOf(ParseError);
|
|
134
|
+
expect(error.line).toBe(2);
|
|
135
|
+
expect(error.column).toBe(12);
|
|
136
|
+
expect(error.lineText).toBe('stage: rust: nextest');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('blanking a comment keeps the length of the document', () => {
|
|
140
|
+
expect(stripComments('a: b # why\n')).toBe('a: b \n');
|
|
141
|
+
expect(stripComments('"# kept"\n')).toBe('"# kept"\n');
|
|
142
|
+
expect(stripComments('issue#1047\n')).toBe('issue#1047\n');
|
|
143
|
+
expect(stripComments('a: b\n').length).toBe(5);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('a reference that begins with a hash is written quoted', () => {
|
|
147
|
+
// Without the quotes the document would read as `a` followed by a comment.
|
|
148
|
+
const document = formatLinks([
|
|
149
|
+
new Link(null, [new Link('a'), new Link('#tag')]),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
expect(document).toBe("(a '#tag')");
|
|
153
|
+
expect(parser.parse(document).map(render).join('\n')).toBe('(<a> <#tag>)');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a hash that cannot open a comment is left unquoted', () => {
|
|
157
|
+
expect(Link.escapeReference('issue#1047')).toBe('issue#1047');
|
|
158
|
+
expect(Link.escapeReference('#')).toBe("'#'");
|
|
159
|
+
expect(Link.escapeReference('#ff0000')).toBe("'#ff0000'");
|
|
160
|
+
});
|
|
@@ -22,22 +22,24 @@ function syntaxError(document) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
test('reports the line and column of the defect', () => {
|
|
25
|
-
// The
|
|
26
|
-
//
|
|
27
|
-
const error = syntaxError(
|
|
25
|
+
// The defect is the second colon on line 2, and the two lines after it are
|
|
26
|
+
// fine.
|
|
27
|
+
const error = syntaxError(
|
|
28
|
+
'ci_gate x\nstage: rust: nextest\nnext stage\n clippy'
|
|
29
|
+
);
|
|
28
30
|
|
|
29
31
|
expect(error.line).toBe(2);
|
|
30
|
-
expect(error.column).toBe(
|
|
32
|
+
expect(error.column).toBe(12);
|
|
31
33
|
expect(error.found).toBe(':');
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
test('offset agrees with the other implementations', () => {
|
|
35
|
-
// Rust and C# report offset
|
|
36
|
-
const error = syntaxError('
|
|
37
|
+
// Rust and C# report offset 21, line 2, column 12 for this document.
|
|
38
|
+
const error = syntaxError('ci_gate x\nstage: rust: nextest\n');
|
|
37
39
|
|
|
38
|
-
expect(error.offset).toBe(
|
|
40
|
+
expect(error.offset).toBe(21);
|
|
39
41
|
expect(error.line).toBe(2);
|
|
40
|
-
expect(error.column).toBe(
|
|
42
|
+
expect(error.column).toBe(12);
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
test('reports the line a late defect is on', () => {
|
|
@@ -68,20 +70,20 @@ test('reports an unmatched closing parenthesis', () => {
|
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
test('message says where the document broke', () => {
|
|
71
|
-
const error = syntaxError('
|
|
73
|
+
const error = syntaxError('ci_gate x\nstage: rust: nextest\n');
|
|
72
74
|
|
|
73
|
-
expect(error.message.startsWith('Syntax error at line 2, column
|
|
75
|
+
expect(error.message.startsWith('Syntax error at line 2, column 12:')).toBe(
|
|
74
76
|
true
|
|
75
77
|
);
|
|
76
|
-
expect(error.snippet).toBe('2 |
|
|
78
|
+
expect(error.snippet).toBe('2 | stage: rust: nextest\n | ^');
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
test('message quotes one line rather than the rest of the document', () => {
|
|
80
|
-
const document =
|
|
82
|
+
const document = `ci_gate x\nstage: rust: nextest\n${'trailing line\n'.repeat(500)}`;
|
|
81
83
|
|
|
82
84
|
const error = syntaxError(document);
|
|
83
85
|
|
|
84
|
-
expect(error.message).toContain('line 2, column
|
|
86
|
+
expect(error.message).toContain('line 2, column 12');
|
|
85
87
|
expect(error.message).not.toContain('trailing line');
|
|
86
88
|
expect(error.message.length).toBeLessThan(200);
|
|
87
89
|
});
|