links-notation 0.28.0 → 0.30.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/dist/index.js +216 -336
- package/package.json +7 -4
- package/src/Cst.js +9 -26
- package/src/ReferenceLiteral.js +22 -0
- package/src/StreamParser.js +33 -13
- package/src/comments.js +50 -11
- package/src/grammar.pegjs +4 -2
- package/src/parser-generated.js +110 -274
- package/src/quotes.js +13 -7
- package/tests/Comments.test.js +31 -0
- package/tests/StreamParser.test.js +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "links-notation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Links Notation parser for JavaScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -32,10 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/link-foundation/links-notation#readme",
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"bun-types": "^1.4.
|
|
36
|
-
"eslint": "^10.
|
|
37
|
-
"peggy": "^5.1.
|
|
35
|
+
"bun-types": "^1.4.4",
|
|
36
|
+
"eslint": "^10.14.0",
|
|
37
|
+
"peggy": "^5.1.1",
|
|
38
38
|
"prettier": "^3.9.10",
|
|
39
39
|
"typescript": "^7.0.2"
|
|
40
|
+
},
|
|
41
|
+
"overrides": {
|
|
42
|
+
"minimatch": "^11.0.4"
|
|
40
43
|
}
|
|
41
44
|
}
|
package/src/Cst.js
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
import { stripComments } from './comments.js';
|
|
19
19
|
import { DelimitedReferences, QUOTES } from './quotes.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
decodeReferenceLiteral,
|
|
22
|
+
referenceLiteralEnd as literalEnd,
|
|
23
|
+
} from './ReferenceLiteral.js';
|
|
21
24
|
|
|
22
25
|
/** The kinds of nodes and tokens of a concrete syntax tree. */
|
|
23
26
|
export const CstKind = Object.freeze({
|
|
@@ -131,28 +134,6 @@ function quoteText(text) {
|
|
|
131
134
|
return `"${text.replace(/[\\"\n\r\t]/g, (character) => ESCAPES[character])}"`;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
/** What a versioned literal's payload cannot contain. */
|
|
135
|
-
const LITERAL_PAYLOAD_END = new Set(['}', ' ', '\t', '\r', '\n', '(', ')']);
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Where the versioned literal `~<digits>{<payload>}` that starts at `start`
|
|
139
|
-
* ends, or -1 if none starts there. A single forward scan, so it stays linear
|
|
140
|
-
* on any input (a regular expression here was flagged as a ReDoS risk).
|
|
141
|
-
*/
|
|
142
|
-
function literalEnd(text, start) {
|
|
143
|
-
if (text[start] !== '~') return -1;
|
|
144
|
-
let index = start + 1;
|
|
145
|
-
while (index < text.length && text[index] >= '0' && text[index] <= '9') {
|
|
146
|
-
index++;
|
|
147
|
-
}
|
|
148
|
-
if (index === start + 1 || text[index] !== '{') return -1;
|
|
149
|
-
index++;
|
|
150
|
-
while (index < text.length && !LITERAL_PAYLOAD_END.has(text[index])) {
|
|
151
|
-
index++;
|
|
152
|
-
}
|
|
153
|
-
return text[index] === '}' ? index + 1 : -1;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
137
|
/** What ends a bare reference. */
|
|
157
138
|
const BARE_REFERENCE_END = new Set([' ', '\t', '\r', '\n', '(', ':', ')']);
|
|
158
139
|
|
|
@@ -164,10 +145,12 @@ const BARE_REFERENCE_END = new Set([' ', '\t', '\r', '\n', '(', ':', ')']);
|
|
|
164
145
|
* @returns {CstNode} The `document` node
|
|
165
146
|
*/
|
|
166
147
|
export function buildCst(input, { comments = true } = {}) {
|
|
167
|
-
//
|
|
168
|
-
// comments blanked, which keeps every position of the one written.
|
|
148
|
+
// Tokens end where they end in the document the grammar reads: the one with
|
|
149
|
+
// its comments blanked, which keeps every position of the one written.
|
|
150
|
+
// Delimited references are read, as the grammar reads them, in the document
|
|
151
|
+
// as written, so a comment never changes what a reference holds.
|
|
169
152
|
const prepared = comments ? stripComments(input) : input;
|
|
170
|
-
const references = new DelimitedReferences(
|
|
153
|
+
const references = new DelimitedReferences(input);
|
|
171
154
|
const document = new CstNode(CstKind.DOCUMENT);
|
|
172
155
|
// The open containers, innermost last, each with the line being filled.
|
|
173
156
|
const containers = [{ node: document, line: null }];
|
package/src/ReferenceLiteral.js
CHANGED
|
@@ -5,6 +5,28 @@ export function hasReferenceLiteralPrefix(text, start = 0) {
|
|
|
5
5
|
return /^~[0-9]+\{/.test(text.slice(start));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** What a versioned literal's payload cannot contain. */
|
|
9
|
+
const LITERAL_PAYLOAD_END = new Set(['}', ' ', '\t', '\r', '\n', '(', ')']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where the versioned literal `~<digits>{<payload>}` that starts at `start`
|
|
13
|
+
* ends, or -1 if none starts there. A single forward scan, so it stays linear
|
|
14
|
+
* on any input (a regular expression here was flagged as a ReDoS risk).
|
|
15
|
+
*/
|
|
16
|
+
export function referenceLiteralEnd(text, start) {
|
|
17
|
+
if (text[start] !== '~') return -1;
|
|
18
|
+
let index = start + 1;
|
|
19
|
+
while (index < text.length && text[index] >= '0' && text[index] <= '9') {
|
|
20
|
+
index++;
|
|
21
|
+
}
|
|
22
|
+
if (index === start + 1 || text[index] !== '{') return -1;
|
|
23
|
+
index++;
|
|
24
|
+
while (index < text.length && !LITERAL_PAYLOAD_END.has(text[index])) {
|
|
25
|
+
index++;
|
|
26
|
+
}
|
|
27
|
+
return text[index] === '}' ? index + 1 : -1;
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
/** Encode exact Unicode text as a version 1 UTF-8 hexadecimal reference. */
|
|
9
31
|
export function encodeReferenceLiteral(text) {
|
|
10
32
|
if (typeof text !== 'string' || !text.isWellFormed()) {
|
package/src/StreamParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Emitter } from './Emitter.js';
|
|
2
2
|
import { Parser } from './Parser.js';
|
|
3
3
|
import { DelimitedReferences } from './quotes.js';
|
|
4
|
+
import { referenceLiteralEnd } from './ReferenceLiteral.js';
|
|
4
5
|
|
|
5
6
|
const DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
6
7
|
|
|
@@ -246,26 +247,20 @@ function countNewlines(value) {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
function structurallyComplete(document, comments) {
|
|
250
|
+
// Tokens are read as the comment reader and the grammar read them: a
|
|
251
|
+
// reference may start after a separator, and right after a delimited
|
|
252
|
+
// reference or a literal (issue #377).
|
|
249
253
|
const quotes = ['"', "'", '`'];
|
|
250
|
-
const
|
|
254
|
+
const separators = [' ', '\t', '\n', '\r', '(', ':', ')'];
|
|
251
255
|
const beforeComment = [' ', '\t', '\n', '\r'];
|
|
252
256
|
const references = new DelimitedReferences(document);
|
|
253
257
|
let depth = 0;
|
|
258
|
+
let tokenStart = true;
|
|
254
259
|
|
|
255
260
|
for (let position = 0; position < document.length; position++) {
|
|
256
261
|
const character = document[position];
|
|
257
262
|
const previous = position === 0 ? null : document[position - 1];
|
|
258
263
|
|
|
259
|
-
if (
|
|
260
|
-
quotes.includes(character) &&
|
|
261
|
-
(previous === null || beforeReference.includes(previous))
|
|
262
|
-
) {
|
|
263
|
-
const end = references.endAt(position);
|
|
264
|
-
if (end === null) return false;
|
|
265
|
-
position = end - 1;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
264
|
if (
|
|
270
265
|
comments &&
|
|
271
266
|
character === '#' &&
|
|
@@ -274,11 +269,36 @@ function structurallyComplete(document, comments) {
|
|
|
274
269
|
const newline = document.indexOf('\n', position);
|
|
275
270
|
if (newline === -1) return true;
|
|
276
271
|
position = newline;
|
|
272
|
+
tokenStart = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (separators.includes(character)) {
|
|
277
|
+
if (character === '(') depth += 1;
|
|
278
|
+
if (character === ')') depth -= 1;
|
|
279
|
+
tokenStart = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (tokenStart && quotes.includes(character)) {
|
|
284
|
+
// A reference nothing closes yet may be closed by a later line, which
|
|
285
|
+
// changes what it holds: an even run is the empty reference only when
|
|
286
|
+
// no run in the whole document closes it.
|
|
287
|
+
const reading = references.readAt(position);
|
|
288
|
+
if (reading === null || !reading.closed) return false;
|
|
289
|
+
position += reading.length - 1;
|
|
277
290
|
continue;
|
|
278
291
|
}
|
|
279
292
|
|
|
280
|
-
if (
|
|
281
|
-
|
|
293
|
+
if (tokenStart) {
|
|
294
|
+
const end = referenceLiteralEnd(document, position);
|
|
295
|
+
if (end !== -1) {
|
|
296
|
+
position = end - 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
tokenStart = false;
|
|
282
302
|
}
|
|
283
303
|
|
|
284
304
|
return depth === 0;
|
package/src/comments.js
CHANGED
|
@@ -13,23 +13,35 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { DelimitedReferences, QUOTES, readReference } from './quotes.js';
|
|
16
|
+
import { referenceLiteralEnd } from './ReferenceLiteral.js';
|
|
16
17
|
|
|
17
18
|
/** The character that starts a comment. */
|
|
18
19
|
export const COMMENT = '#';
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* What ends a bare reference. A token starts after one of these, at the start
|
|
23
|
+
* of the document, and right after a delimited reference or a versioned
|
|
24
|
+
* literal: the places the grammar reads a reference from.
|
|
25
|
+
*/
|
|
26
|
+
const SEPARATORS = [' ', '\t', '\n', '\r', '(', ':', ')'];
|
|
22
27
|
|
|
23
28
|
/** What a comment may start after. */
|
|
24
29
|
const BEFORE_COMMENT = [' ', '\t', '\n', '\r'];
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* The document with every comment blanked out.
|
|
33
|
+
*
|
|
34
|
+
* The document is read token by token, the way the grammar reads it, so a `#`
|
|
35
|
+
* inside a delimited reference that starts right after a group, another
|
|
36
|
+
* reference or a literal is content, as it is in one that starts after a
|
|
37
|
+
* space. References are read in the document as written, as the grammar
|
|
38
|
+
* reads them.
|
|
28
39
|
* @param {string} document - The document to read
|
|
29
40
|
* @returns {string} The document, of the same length, without its comments
|
|
30
41
|
* @example
|
|
31
42
|
* stripComments('a: b # why\n'); // 'a: b \n'
|
|
32
43
|
* stripComments('"# kept"\n'); // '"# kept"\n'
|
|
44
|
+
* stripComments('(a)" #kept"\n'); // '(a)" #kept"\n'
|
|
33
45
|
* stripComments('issue#1047\n'); // 'issue#1047\n'
|
|
34
46
|
*/
|
|
35
47
|
export function stripComments(document) {
|
|
@@ -37,20 +49,13 @@ export function stripComments(document) {
|
|
|
37
49
|
// parse error reports are both counted in.
|
|
38
50
|
let blanked = null;
|
|
39
51
|
let position = 0;
|
|
52
|
+
// Whether a token may start at `position`.
|
|
53
|
+
let tokenStart = true;
|
|
40
54
|
const references = new DelimitedReferences(document);
|
|
41
55
|
|
|
42
56
|
while (position < document.length) {
|
|
43
57
|
const character = document[position];
|
|
44
58
|
|
|
45
|
-
if (
|
|
46
|
-
QUOTES.includes(character) &&
|
|
47
|
-
follows(document, position, BEFORE_REFERENCE)
|
|
48
|
-
) {
|
|
49
|
-
const end = references.endAt(position);
|
|
50
|
-
position = end === null ? position + 1 : end;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
59
|
if (character === COMMENT && follows(document, position, BEFORE_COMMENT)) {
|
|
55
60
|
blanked = blanked ?? document.split('');
|
|
56
61
|
while (
|
|
@@ -64,12 +69,46 @@ export function stripComments(document) {
|
|
|
64
69
|
continue;
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
if (SEPARATORS.includes(character)) {
|
|
73
|
+
tokenStart = true;
|
|
74
|
+
position++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (tokenStart) {
|
|
79
|
+
const end = delimitedTokenEnd(document, position, references);
|
|
80
|
+
if (end !== null) {
|
|
81
|
+
position = end;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Anything else is part of a bare reference, which runs to a separator.
|
|
87
|
+
tokenStart = false;
|
|
67
88
|
position++;
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
return blanked === null ? document : blanked.join('');
|
|
71
92
|
}
|
|
72
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The position just past the delimited reference or versioned literal that
|
|
96
|
+
* starts the token at `start`, or `null` when the token there is a bare
|
|
97
|
+
* reference: a delimiter run that never closes is read as one, as the grammar
|
|
98
|
+
* reads it.
|
|
99
|
+
* @param {string} document - The document being read
|
|
100
|
+
* @param {number} start - Position where a token starts
|
|
101
|
+
* @param {DelimitedReferences} references - The references of the document
|
|
102
|
+
* @returns {number|null} The position just past the token
|
|
103
|
+
*/
|
|
104
|
+
export function delimitedTokenEnd(document, start, references) {
|
|
105
|
+
const literal = referenceLiteralEnd(document, start);
|
|
106
|
+
if (literal !== -1) {
|
|
107
|
+
return literal;
|
|
108
|
+
}
|
|
109
|
+
return QUOTES.includes(document[start]) ? references.endAt(start) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
73
112
|
/**
|
|
74
113
|
* Whether the character before the given position is one the caller allows,
|
|
75
114
|
* the start of the document counting as allowed.
|
package/src/grammar.pegjs
CHANGED
|
@@ -117,8 +117,10 @@
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// Delimited references of this document, each read once however many
|
|
120
|
-
// alternatives ask for it (see quotes.js for how a reference is read)
|
|
121
|
-
|
|
120
|
+
// alternatives ask for it (see quotes.js for how a reference is read). They
|
|
121
|
+
// are read in the document as written, as the comment reader reads them, so
|
|
122
|
+
// blanking a comment never changes how far a reference reaches (issue #377).
|
|
123
|
+
const delimitedReferences = new DelimitedReferences(source);
|
|
122
124
|
|
|
123
125
|
// Universal procedural parser for N-quote strings (any N >= 1)
|
|
124
126
|
// Parses from the given position in the input string
|