links-notation 0.18.0 → 0.20.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 +91 -4
- package/README.ru.md +60 -4
- package/dist/index.js +476 -21
- package/index.d.ts +95 -0
- package/package.json +1 -1
- package/src/Link.js +4 -0
- package/src/Parser.js +8 -1
- package/src/StreamParser.js +284 -0
- 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 +15 -13
- package/tests/StreamParser.test.js +165 -0
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) {
|
|
@@ -2183,6 +2419,222 @@ class Parser {
|
|
|
2183
2419
|
return new Link(item.id ?? null, []);
|
|
2184
2420
|
}
|
|
2185
2421
|
}
|
|
2422
|
+
// src/StreamParser.js
|
|
2423
|
+
import { EventEmitter } from "events";
|
|
2424
|
+
var DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
2425
|
+
|
|
2426
|
+
class StreamParseError extends Error {
|
|
2427
|
+
constructor(error, startOffset, startLine) {
|
|
2428
|
+
const localOffset = error?.offset ?? 0;
|
|
2429
|
+
const localLine = error?.line ?? 1;
|
|
2430
|
+
const column = error?.column ?? 1;
|
|
2431
|
+
const line = startLine + localLine - 1;
|
|
2432
|
+
super(`Stream parse error at line ${line}, column ${column}: ${error.message}`);
|
|
2433
|
+
this.name = "StreamParseError";
|
|
2434
|
+
this.cause = error;
|
|
2435
|
+
this.offset = startOffset + localOffset;
|
|
2436
|
+
this.line = line;
|
|
2437
|
+
this.column = column;
|
|
2438
|
+
this.found = error?.found ?? null;
|
|
2439
|
+
this.lineText = error?.lineText ?? "";
|
|
2440
|
+
this.snippet = error?.snippet ?? "";
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
class StreamParser extends EventEmitter {
|
|
2445
|
+
constructor(options = {}) {
|
|
2446
|
+
super();
|
|
2447
|
+
this.parser = options.parser ?? new Parser(options);
|
|
2448
|
+
this.comments = options.comments ?? this.parser.comments ?? true;
|
|
2449
|
+
this.maxBufferSize = options.maxBufferSize ?? options.maxInputSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
2450
|
+
this.collect = options.collect ?? true;
|
|
2451
|
+
this.reset();
|
|
2452
|
+
}
|
|
2453
|
+
write(chunk) {
|
|
2454
|
+
if (typeof chunk !== "string") {
|
|
2455
|
+
return this._fail(new TypeError("Input must be a string"));
|
|
2456
|
+
}
|
|
2457
|
+
if (this.ended) {
|
|
2458
|
+
return this._fail(new Error("Cannot write after end()"));
|
|
2459
|
+
}
|
|
2460
|
+
const emitted = [];
|
|
2461
|
+
for (let index = 0;index < chunk.length; index++) {
|
|
2462
|
+
const character = chunk[index];
|
|
2463
|
+
this.currentLine += character;
|
|
2464
|
+
this.offset += 1;
|
|
2465
|
+
if (character === `
|
|
2466
|
+
`) {
|
|
2467
|
+
this.buffer += this.currentLine;
|
|
2468
|
+
this.currentLine = "";
|
|
2469
|
+
this.lineClassified = false;
|
|
2470
|
+
this.line += 1;
|
|
2471
|
+
this.column = 1;
|
|
2472
|
+
} else {
|
|
2473
|
+
if (!this.lineClassified && character !== " " && character !== "\t" && character !== "\r") {
|
|
2474
|
+
this.lineClassified = true;
|
|
2475
|
+
const isComment = this.comments && character === "#";
|
|
2476
|
+
if (!isComment) {
|
|
2477
|
+
const indentation = leadingSpaces(this.currentLine);
|
|
2478
|
+
this._startContentLine(indentation, emitted);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
this.column += 1;
|
|
2482
|
+
}
|
|
2483
|
+
if (this.buffer.length + this.currentLine.length > this.maxBufferSize) {
|
|
2484
|
+
return this._fail(new RangeError(`Buffered record exceeds maximum size of ${this.maxBufferSize} characters`));
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return emitted;
|
|
2488
|
+
}
|
|
2489
|
+
end(chunk = "") {
|
|
2490
|
+
const emitted = chunk === "" ? [] : this.write(chunk);
|
|
2491
|
+
if (this.ended) {
|
|
2492
|
+
return this.collect ? [...this.links] : emitted;
|
|
2493
|
+
}
|
|
2494
|
+
const document = this.buffer + this.currentLine;
|
|
2495
|
+
if (document.length > 0) {
|
|
2496
|
+
let links;
|
|
2497
|
+
try {
|
|
2498
|
+
links = this.parser.parse(document);
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
const streamError = new StreamParseError(error, this.segmentOffset, this.segmentLine);
|
|
2501
|
+
return this._fail(streamError);
|
|
2502
|
+
}
|
|
2503
|
+
this._publish(links, emitted);
|
|
2504
|
+
this._advanceSegment(document);
|
|
2505
|
+
}
|
|
2506
|
+
this.buffer = "";
|
|
2507
|
+
this.currentLine = "";
|
|
2508
|
+
this.baseIndentation = null;
|
|
2509
|
+
this.ended = true;
|
|
2510
|
+
const result = this.collect ? [...this.links] : emitted;
|
|
2511
|
+
this.emit("end", result);
|
|
2512
|
+
return result;
|
|
2513
|
+
}
|
|
2514
|
+
drain() {
|
|
2515
|
+
const links = this.links;
|
|
2516
|
+
this.links = [];
|
|
2517
|
+
return links;
|
|
2518
|
+
}
|
|
2519
|
+
reset() {
|
|
2520
|
+
this.buffer = "";
|
|
2521
|
+
this.currentLine = "";
|
|
2522
|
+
this.baseIndentation = null;
|
|
2523
|
+
this.lineClassified = false;
|
|
2524
|
+
this.links = [];
|
|
2525
|
+
this.offset = 0;
|
|
2526
|
+
this.line = 1;
|
|
2527
|
+
this.column = 1;
|
|
2528
|
+
this.segmentOffset = 0;
|
|
2529
|
+
this.segmentLine = 1;
|
|
2530
|
+
this.ended = false;
|
|
2531
|
+
return this;
|
|
2532
|
+
}
|
|
2533
|
+
position() {
|
|
2534
|
+
return {
|
|
2535
|
+
offset: this.offset,
|
|
2536
|
+
line: this.line,
|
|
2537
|
+
column: this.column,
|
|
2538
|
+
buffered: this.buffer.length + this.currentLine.length
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
static *parse(chunks, options = {}) {
|
|
2542
|
+
const parser = new StreamParser({ ...options, collect: false });
|
|
2543
|
+
for (const chunk of chunks) {
|
|
2544
|
+
yield* parser.write(chunk);
|
|
2545
|
+
}
|
|
2546
|
+
yield* parser.end();
|
|
2547
|
+
}
|
|
2548
|
+
static async* parseAsync(chunks, options = {}) {
|
|
2549
|
+
const parser = new StreamParser({ ...options, collect: false });
|
|
2550
|
+
for await (const chunk of chunks) {
|
|
2551
|
+
yield* parser.write(chunk);
|
|
2552
|
+
}
|
|
2553
|
+
yield* parser.end();
|
|
2554
|
+
}
|
|
2555
|
+
_startContentLine(indentation, emitted) {
|
|
2556
|
+
if (this.buffer.length > 0 && this.baseIndentation !== null && indentation <= this.baseIndentation && structurallyComplete(this.buffer, this.comments)) {
|
|
2557
|
+
let links;
|
|
2558
|
+
try {
|
|
2559
|
+
links = this.parser.parse(this.buffer);
|
|
2560
|
+
} catch {
|
|
2561
|
+
links = null;
|
|
2562
|
+
}
|
|
2563
|
+
if (links !== null) {
|
|
2564
|
+
this._publish(links, emitted);
|
|
2565
|
+
this._advanceSegment(this.buffer);
|
|
2566
|
+
this.buffer = "";
|
|
2567
|
+
this.baseIndentation = null;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
if (this.baseIndentation === null) {
|
|
2571
|
+
this.baseIndentation = indentation;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
_publish(links, emitted) {
|
|
2575
|
+
for (const link of links) {
|
|
2576
|
+
emitted.push(link);
|
|
2577
|
+
if (this.collect)
|
|
2578
|
+
this.links.push(link);
|
|
2579
|
+
this.emit("link", link);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
_advanceSegment(document) {
|
|
2583
|
+
this.segmentOffset += document.length;
|
|
2584
|
+
this.segmentLine += countNewlines(document);
|
|
2585
|
+
}
|
|
2586
|
+
_fail(error) {
|
|
2587
|
+
this.emit("error", error);
|
|
2588
|
+
throw error;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
function leadingSpaces(line) {
|
|
2592
|
+
let indentation = 0;
|
|
2593
|
+
while (line[indentation] === " ")
|
|
2594
|
+
indentation += 1;
|
|
2595
|
+
return indentation;
|
|
2596
|
+
}
|
|
2597
|
+
function countNewlines(value) {
|
|
2598
|
+
let count = 0;
|
|
2599
|
+
for (const character of value) {
|
|
2600
|
+
if (character === `
|
|
2601
|
+
`)
|
|
2602
|
+
count += 1;
|
|
2603
|
+
}
|
|
2604
|
+
return count;
|
|
2605
|
+
}
|
|
2606
|
+
function structurallyComplete(document, comments) {
|
|
2607
|
+
const quotes = ['"', "'", "`"];
|
|
2608
|
+
const beforeReference = [" ", "\t", `
|
|
2609
|
+
`, "\r", "(", ":"];
|
|
2610
|
+
const beforeComment = [" ", "\t", `
|
|
2611
|
+
`, "\r"];
|
|
2612
|
+
let depth = 0;
|
|
2613
|
+
for (let position = 0;position < document.length; position++) {
|
|
2614
|
+
const character = document[position];
|
|
2615
|
+
const previous = position === 0 ? null : document[position - 1];
|
|
2616
|
+
if (quotes.includes(character) && (previous === null || beforeReference.includes(previous))) {
|
|
2617
|
+
const end = quotedReferenceEnd(document, position);
|
|
2618
|
+
if (end === null)
|
|
2619
|
+
return false;
|
|
2620
|
+
position = end - 1;
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
if (comments && character === "#" && (previous === null || beforeComment.includes(previous))) {
|
|
2624
|
+
const newline = document.indexOf(`
|
|
2625
|
+
`, position);
|
|
2626
|
+
if (newline === -1)
|
|
2627
|
+
return true;
|
|
2628
|
+
position = newline;
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
if (character === "(")
|
|
2632
|
+
depth += 1;
|
|
2633
|
+
if (character === ")")
|
|
2634
|
+
depth -= 1;
|
|
2635
|
+
}
|
|
2636
|
+
return depth === 0;
|
|
2637
|
+
}
|
|
2186
2638
|
// src/FormatOptions.js
|
|
2187
2639
|
class FormatOptions {
|
|
2188
2640
|
constructor(options = {}) {
|
|
@@ -2218,5 +2670,8 @@ export {
|
|
|
2218
2670
|
LinksGroup,
|
|
2219
2671
|
ParseError,
|
|
2220
2672
|
Parser,
|
|
2221
|
-
|
|
2673
|
+
StreamParseError,
|
|
2674
|
+
StreamParser,
|
|
2675
|
+
formatLinks,
|
|
2676
|
+
stripComments
|
|
2222
2677
|
};
|
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,90 @@ export class Parser {
|
|
|
149
160
|
parse(input: string): Link[];
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
/** Options for incremental parsing. */
|
|
164
|
+
export interface StreamParserOptions extends ParserOptions {
|
|
165
|
+
/** Canonical parser instance to use for completed records. */
|
|
166
|
+
parser?: Parser;
|
|
167
|
+
|
|
168
|
+
/** Largest unresolved record retained in memory (default: 10MB). */
|
|
169
|
+
maxBufferSize?: number;
|
|
170
|
+
|
|
171
|
+
/** Retain emitted links for end() and drain() (default: true). */
|
|
172
|
+
collect?: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Position immediately after the last character written. */
|
|
176
|
+
export interface StreamPosition {
|
|
177
|
+
offset: number;
|
|
178
|
+
line: number;
|
|
179
|
+
column: number;
|
|
180
|
+
buffered: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Canonical parse error translated to coordinates in the complete stream. */
|
|
184
|
+
export class StreamParseError extends Error {
|
|
185
|
+
cause: Error;
|
|
186
|
+
offset: number;
|
|
187
|
+
line: number;
|
|
188
|
+
column: number;
|
|
189
|
+
found: string | null;
|
|
190
|
+
lineText: string;
|
|
191
|
+
snippet: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Incrementally emits complete top-level Links Notation records. */
|
|
195
|
+
export class StreamParser {
|
|
196
|
+
parser: Parser;
|
|
197
|
+
comments: boolean;
|
|
198
|
+
maxBufferSize: number;
|
|
199
|
+
collect: boolean;
|
|
200
|
+
|
|
201
|
+
constructor(options?: StreamParserOptions);
|
|
202
|
+
|
|
203
|
+
/** Feed a chunk and return links made complete by it. */
|
|
204
|
+
write(chunk: string): Link[];
|
|
205
|
+
|
|
206
|
+
/** Finish the stream, optionally after consuming one final chunk. */
|
|
207
|
+
end(chunk?: string): Link[];
|
|
208
|
+
|
|
209
|
+
/** Return and forget retained links. */
|
|
210
|
+
drain(): Link[];
|
|
211
|
+
|
|
212
|
+
/** Reuse this parser while preserving its options and listeners. */
|
|
213
|
+
reset(): this;
|
|
214
|
+
|
|
215
|
+
/** Return the absolute stream position and unresolved buffer size. */
|
|
216
|
+
position(): StreamPosition;
|
|
217
|
+
|
|
218
|
+
on(event: 'link', listener: (link: Link) => void): this;
|
|
219
|
+
on(event: 'end', listener: (links: Link[]) => void): this;
|
|
220
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
221
|
+
once(event: 'link', listener: (link: Link) => void): this;
|
|
222
|
+
once(event: 'end', listener: (links: Link[]) => void): this;
|
|
223
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
224
|
+
off(event: string, listener: (...arguments_: any[]) => void): this;
|
|
225
|
+
|
|
226
|
+
/** Lazily parse a synchronous iterable of chunks. */
|
|
227
|
+
static parse(
|
|
228
|
+
chunks: Iterable<string>,
|
|
229
|
+
options?: StreamParserOptions
|
|
230
|
+
): IterableIterator<Link>;
|
|
231
|
+
|
|
232
|
+
/** Lazily parse an asynchronous iterable of chunks. */
|
|
233
|
+
static parseAsync(
|
|
234
|
+
chunks: AsyncIterable<string> | Iterable<string>,
|
|
235
|
+
options?: StreamParserOptions
|
|
236
|
+
): AsyncIterableIterator<Link>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Blank out every comment in a document, keeping its length so that every
|
|
241
|
+
* character stays at the offset it was written at.
|
|
242
|
+
* @param document - The document to read
|
|
243
|
+
* @returns The document, of the same length, without its comments
|
|
244
|
+
*/
|
|
245
|
+
export function stripComments(document: string): string;
|
|
246
|
+
|
|
152
247
|
/**
|
|
153
248
|
* The position a parse stopped at, as the generated parser reports it
|
|
154
249
|
*/
|
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(')') ||
|