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 +68 -0
- package/README.ru.md +68 -0
- package/dist/index.js +313 -22
- package/index.d.ts +71 -1
- package/package.json +1 -1
- package/src/Link.js +4 -0
- package/src/ParseError.js +104 -0
- package/src/Parser.js +14 -3
- package/src/comments.js +164 -0
- package/src/grammar.pegjs +7 -1
- package/src/index.js +2 -0
- package/src/parser-generated.js +152 -18
- package/tests/Comments.test.js +160 -0
- package/tests/ParseErrorPosition.test.js +111 -0
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
|
|
1841
|
+
let s0, s1, s2;
|
|
1704
1842
|
s0 = peg$currPos;
|
|
1705
1843
|
s1 = peg$parse__();
|
|
1706
|
-
s2 =
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1868
|
+
s2 = peg$FAILED;
|
|
1712
1869
|
if (peg$silentFails === 0) {
|
|
1713
1870
|
peg$fail(peg$e1);
|
|
1714
1871
|
}
|
|
1715
1872
|
}
|
|
1716
|
-
if (
|
|
1717
|
-
while (
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
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)) {
|
|
1721
1878
|
peg$currPos++;
|
|
1722
1879
|
} else {
|
|
1723
|
-
|
|
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
|
-
|
|
1887
|
+
s1 = peg$FAILED;
|
|
1731
1888
|
}
|
|
1732
|
-
if (
|
|
1733
|
-
s2 =
|
|
1734
|
-
|
|
1735
|
-
|
|
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(
|
|
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 {
|
|
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
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(')') ||
|