links-notation 0.28.0 → 0.30.1
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 +224 -341
- package/package.json +7 -4
- package/src/Cst.js +9 -26
- package/src/Link.js +18 -5
- 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/SharedTextParity.test.js +53 -1
- 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.1",
|
|
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/Link.js
CHANGED
|
@@ -196,7 +196,7 @@ export class Link {
|
|
|
196
196
|
// Original implementation for backward compatibility
|
|
197
197
|
// Empty link
|
|
198
198
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
199
|
-
return
|
|
199
|
+
return '()';
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// Link with only ID, no values
|
|
@@ -217,7 +217,7 @@ export class Link {
|
|
|
217
217
|
// Link with values only (null id)
|
|
218
218
|
if (this.id === null) {
|
|
219
219
|
// For lessParentheses mode with simple values, don't wrap the whole thing
|
|
220
|
-
if (lessParentheses) {
|
|
220
|
+
if (lessParentheses && !this._hasLoneNestedValue()) {
|
|
221
221
|
// Check if all values are simple (no nested values)
|
|
222
222
|
const allSimple = this.values.every(
|
|
223
223
|
(v) => !v.values || v.values.length === 0
|
|
@@ -288,6 +288,16 @@ export class Link {
|
|
|
288
288
|
);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Whether this link holds exactly one link that has values. The parentheses
|
|
293
|
+
* of an anonymous link are what nest such a value: `((b c))` without them is
|
|
294
|
+
* `(b c)`, a different link, so lessParentheses has to keep them.
|
|
295
|
+
* @returns {boolean} True if the only value has values of its own
|
|
296
|
+
*/
|
|
297
|
+
_hasLoneNestedValue() {
|
|
298
|
+
return this.values.length === 1 && this.values[0].values?.length > 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
291
301
|
/**
|
|
292
302
|
* Format the link using a FormatOptions object
|
|
293
303
|
* @param {FormatOptions} options - FormatOptions object with formatting options
|
|
@@ -297,7 +307,7 @@ export class Link {
|
|
|
297
307
|
_formatWithOptions(options, isCompoundValue = false) {
|
|
298
308
|
// Empty link
|
|
299
309
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
300
|
-
return
|
|
310
|
+
return '()';
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
// Link with only ID, no values
|
|
@@ -325,7 +335,10 @@ export class Link {
|
|
|
325
335
|
? `${idStr}: ${valuesStr}`
|
|
326
336
|
: `(${idStr}: ${valuesStr})`;
|
|
327
337
|
} else {
|
|
328
|
-
testLine =
|
|
338
|
+
testLine =
|
|
339
|
+
options.lessParentheses && !this._hasLoneNestedValue()
|
|
340
|
+
? valuesStr
|
|
341
|
+
: `(${valuesStr})`;
|
|
329
342
|
}
|
|
330
343
|
|
|
331
344
|
if (options.shouldIndentByLength(testLine)) {
|
|
@@ -343,7 +356,7 @@ export class Link {
|
|
|
343
356
|
|
|
344
357
|
// Link with values only (null id)
|
|
345
358
|
if (this.id === null) {
|
|
346
|
-
if (options.lessParentheses) {
|
|
359
|
+
if (options.lessParentheses && !this._hasLoneNestedValue()) {
|
|
347
360
|
const allSimple = this.values.every(
|
|
348
361
|
(v) => !v.values || v.values.length === 0
|
|
349
362
|
);
|
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
|