links-notation 0.17.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 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
 
@@ -279,11 +314,44 @@ Container for grouping related links.
279
314
  - `constructor(links)` - Create a new group
280
315
  - `format()` - Format the group as a string
281
316
 
317
+ #### `ParseError`
318
+
319
+ Thrown by `parse(input)` when the document does not parse. The message says
320
+ where the document stopped making sense and quotes the offending line:
321
+
322
+ ```js
323
+ import { Parser, ParseError } from 'links-notation';
324
+
325
+ try {
326
+ new Parser().parse('ci_gate x\nstage: rust: nextest\n');
327
+ } catch (error) {
328
+ console.error(error.message);
329
+ if (error instanceof ParseError) {
330
+ console.error(`${error.line}:${error.column} (offset ${error.offset})`);
331
+ }
332
+ }
333
+ ```
334
+
335
+ ```text
336
+ Syntax error at line 2, column 12: Expected "(", [ \t], [\r\n], or [^ \t\n\r(:)] but ":" found.
337
+ 2 | stage: rust: nextest
338
+ | ^
339
+ ```
340
+
341
+ - `offset` - Offset of the offending position from the start of the document
342
+ - `line`, `column` - Where the document stopped parsing, counted from 1
343
+ - `found` - The character found instead, or `null` at the end of the document
344
+ - `lineText` - The offending line, as written
345
+ - `snippet` - The offending line with a caret under the offending column
346
+ - `location` - The position as the generated parser reports it
347
+ - `cause` - The error the generated parser threw
348
+
282
349
  ## Project Structure
283
350
 
284
351
  - `src/grammar.pegjs` - Peggy.js grammar definition
285
352
  - `src/Link.js` - Link data structure
286
353
  - `src/LinksGroup.js` - Links group container
354
+ - `src/ParseError.js` - Parse error with the position of the defect
287
355
  - `src/Parser.js` - Parser wrapper
288
356
  - `src/index.js` - Main entry point
289
357
  - `tests/` - Test files
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
 
@@ -198,11 +233,44 @@ console.log(formatLinks(links)); // (value ((id 1) (label one)))
198
233
  - `constructor(links)` - Создание новой группы
199
234
  - `format()` - Форматирование группы в строку
200
235
 
236
+ #### `ParseError`
237
+
238
+ Выбрасывается методом `parse(input)`, когда документ не разбирается. Сообщение
239
+ говорит, где документ перестал быть понятным, и цитирует сломанную строку:
240
+
241
+ ```js
242
+ import { Parser, ParseError } from 'links-notation';
243
+
244
+ try {
245
+ new Parser().parse('ci_gate x\nstage: rust: nextest\n');
246
+ } catch (error) {
247
+ console.error(error.message);
248
+ if (error instanceof ParseError) {
249
+ console.error(`${error.line}:${error.column} (смещение ${error.offset})`);
250
+ }
251
+ }
252
+ ```
253
+
254
+ ```text
255
+ Syntax error at line 2, column 12: Expected "(", [ \t], [\r\n], or [^ \t\n\r(:)] but ":" found.
256
+ 2 | stage: rust: nextest
257
+ | ^
258
+ ```
259
+
260
+ - `offset` - Смещение сломанной позиции от начала документа
261
+ - `line`, `column` - Где документ перестал разбираться, счет с 1
262
+ - `found` - Найденный символ или `null` в конце документа
263
+ - `lineText` - Сломанная строка, как она написана
264
+ - `snippet` - Сломанная строка с указателем под нужным столбцом
265
+ - `location` - Позиция в том виде, в каком ее сообщает сгенерированный парсер
266
+ - `cause` - Ошибка, которую выбросил сгенерированный парсер
267
+
201
268
  ## Структура проекта
202
269
 
203
270
  - `src/grammar.pegjs` - Определение грамматики Peggy.js
204
271
  - `src/Link.js` - Структура данных связи
205
272
  - `src/LinksGroup.js` - Контейнер групп связей
273
+ - `src/ParseError.js` - Ошибка разбора с позицией дефекта
206
274
  - `src/Parser.js` - Обертка парсера
207
275
  - `src/index.js` - Главная точка входа
208
276
  - `tests/` - Файлы тестов
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, "\\'")}'`;
@@ -269,6 +269,144 @@ class LinksGroup {
269
269
  return list.map((item) => `(${item.id || item})`).join(" ");
270
270
  }
271
271
  }
272
+ // src/ParseError.js
273
+ var QUOTED_LINE_WIDTH = 80;
274
+ var ELLIPSIS = "...";
275
+
276
+ class ParseError extends Error {
277
+ constructor(input, error) {
278
+ const start = error?.location?.start ?? { offset: 0, line: 1, column: 1 };
279
+ const lineText = lineAt(input, start.offset);
280
+ const summary = `line ${start.line}, column ${start.column}: ${error.message}`;
281
+ const snippet = quote(start.line, lineText, start.column);
282
+ super(`Syntax error at ${summary}
283
+ ${snippet}`);
284
+ this.name = "ParseError";
285
+ this.cause = error;
286
+ this.location = error.location;
287
+ this.offset = start.offset;
288
+ this.line = start.line;
289
+ this.column = start.column;
290
+ this.found = error.found ?? null;
291
+ this.lineText = lineText;
292
+ this.snippet = snippet;
293
+ }
294
+ }
295
+ function lineAt(input, offset) {
296
+ const at = Math.max(0, Math.min(offset, input.length));
297
+ const start = input.lastIndexOf(`
298
+ `, at - 1) + 1;
299
+ const end = input.indexOf(`
300
+ `, start);
301
+ const line = input.slice(start, end === -1 ? input.length : end);
302
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
303
+ }
304
+ function quote(number, lineText, column) {
305
+ const [quoted, at] = windowAround(lineText, column);
306
+ const gutter = " ".repeat(String(number).length);
307
+ return `${number} | ${quoted}
308
+ ${gutter} | ${" ".repeat(at - 1)}^`;
309
+ }
310
+ function windowAround(lineText, column) {
311
+ if (lineText.length <= QUOTED_LINE_WIDTH) {
312
+ return [lineText, column];
313
+ }
314
+ const target = column - 1;
315
+ const lastStart = lineText.length - QUOTED_LINE_WIDTH;
316
+ const start = Math.min(Math.max(target - Math.floor(QUOTED_LINE_WIDTH / 2), 0), lastStart);
317
+ const end = start + QUOTED_LINE_WIDTH;
318
+ const quoted = (start > 0 ? ELLIPSIS : "") + lineText.slice(start, end) + (end < lineText.length ? ELLIPSIS : "");
319
+ const shift = start > 0 ? ELLIPSIS.length : 0;
320
+ return [quoted, target - start + shift + 1];
321
+ }
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
+
272
410
  // src/parser-generated.js
273
411
  class peg$SyntaxError extends SyntaxError {
274
412
  constructor(message, expected, found, location) {
@@ -1700,42 +1838,189 @@ function peg$parse(input, options) {
1700
1838
  return s0;
1701
1839
  }
1702
1840
  function peg$parseeol() {
1703
- let s0, s1, s2, s3;
1841
+ let s0, s1, s2;
1704
1842
  s0 = peg$currPos;
1705
1843
  s1 = peg$parse__();
1706
- s2 = [];
1707
- s3 = input.charAt(peg$currPos);
1708
- if (peg$r1.test(s3)) {
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)) {
1709
1866
  peg$currPos++;
1710
1867
  } else {
1711
- s3 = peg$FAILED;
1868
+ s2 = peg$FAILED;
1712
1869
  if (peg$silentFails === 0) {
1713
1870
  peg$fail(peg$e1);
1714
1871
  }
1715
1872
  }
1716
- if (s3 !== peg$FAILED) {
1717
- while (s3 !== peg$FAILED) {
1718
- s2.push(s3);
1719
- s3 = input.charAt(peg$currPos);
1720
- if (peg$r1.test(s3)) {
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)) {
1721
1878
  peg$currPos++;
1722
1879
  } else {
1723
- s3 = peg$FAILED;
1880
+ s2 = peg$FAILED;
1724
1881
  if (peg$silentFails === 0) {
1725
1882
  peg$fail(peg$e1);
1726
1883
  }
1727
1884
  }
1728
1885
  }
1729
1886
  } else {
1730
- s2 = peg$FAILED;
1887
+ s1 = peg$FAILED;
1731
1888
  }
1732
- if (s2 === peg$FAILED) {
1733
- s2 = peg$parseeof();
1734
- if (s2 === peg$FAILED) {
1735
- s2 = peg$parsenestedGroupEnd();
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
+ }
1736
2023
  }
1737
- }
1738
- if (s2 !== peg$FAILED) {
1739
2024
  s1 = [s1, s2];
1740
2025
  s0 = s1;
1741
2026
  } else {
@@ -2012,6 +2297,7 @@ class Parser {
2012
2297
  constructor(options = {}) {
2013
2298
  this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024;
2014
2299
  this.maxDepth = options.maxDepth || 1000;
2300
+ this.comments = options.comments ?? true;
2015
2301
  }
2016
2302
  parse(input) {
2017
2303
  if (typeof input !== "string") {
@@ -2020,13 +2306,16 @@ class Parser {
2020
2306
  if (input.length > this.maxInputSize) {
2021
2307
  throw new Error(`Input size exceeds maximum allowed size of ${this.maxInputSize} bytes`);
2022
2308
  }
2309
+ const prepared = this.comments ? stripComments(input) : input;
2023
2310
  try {
2024
- const rawResult = peg$parse(input);
2311
+ const rawResult = peg$parse(prepared);
2025
2312
  return this.transformResult(rawResult);
2026
2313
  } catch (error) {
2314
+ if (error && error.location) {
2315
+ throw new ParseError(input, error);
2316
+ }
2027
2317
  const parseError = new Error(`Parse error: ${error.message}`);
2028
2318
  parseError.cause = error;
2029
- parseError.location = error.location;
2030
2319
  throw parseError;
2031
2320
  }
2032
2321
  }
@@ -2163,6 +2452,8 @@ export {
2163
2452
  FormatOptions,
2164
2453
  Link,
2165
2454
  LinksGroup,
2455
+ ParseError,
2166
2456
  Parser,
2167
- formatLinks
2457
+ formatLinks,
2458
+ stripComments
2168
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
@@ -144,11 +155,70 @@ export class Parser {
144
155
  * Parse Lino notation text into Link objects
145
156
  * @param input - The Lino notation text to parse
146
157
  * @returns Array of parsed Link objects
147
- * @throws {Error} If parsing fails
158
+ * @throws {ParseError} If the text does not parse
148
159
  */
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
+
171
+ /**
172
+ * The position a parse stopped at, as the generated parser reports it
173
+ */
174
+ export interface ParseErrorLocation {
175
+ start: { offset: number; line: number; column: number };
176
+ end: { offset: number; line: number; column: number };
177
+ }
178
+
179
+ /**
180
+ * The error thrown when a document does not parse
181
+ *
182
+ * The message says where the document stopped making sense and quotes the
183
+ * offending line with a caret under it.
184
+ */
185
+ export class ParseError extends Error {
186
+ /**
187
+ * Offset of the offending position from the start of the document
188
+ */
189
+ offset: number;
190
+
191
+ /**
192
+ * Line the offending position is on, counted from 1
193
+ */
194
+ line: number;
195
+
196
+ /**
197
+ * Column the offending position is at, counted from 1
198
+ */
199
+ column: number;
200
+
201
+ /**
202
+ * The character found instead, or null at the end of the document
203
+ */
204
+ found: string | null;
205
+
206
+ /**
207
+ * The offending line, as written, without its line ending
208
+ */
209
+ lineText: string;
210
+
211
+ /**
212
+ * The offending line with a caret under the offending column
213
+ */
214
+ snippet: string;
215
+
216
+ /**
217
+ * The position the generated parser reported
218
+ */
219
+ location: ParseErrorLocation;
220
+ }
221
+
152
222
  /**
153
223
  * Options for formatting links
154
224
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "links-notation",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Links Notation parser for JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "index.d.ts",
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(')') ||