@nette/neon 0.0.0-stage → 1.0.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/nodes.js ADDED
@@ -0,0 +1,635 @@
1
+ var _a;
2
+ import { NeonError } from "./errors.js";
3
+ import { arrayOf, addItem, CommentPolicy, detachNode, insertItem, removeByPath, removeItem, replaceNode, resolvePath, setByPath, setDocumentNode, setItemKey, setItemNode, setItemValue, } from "./editing.js";
4
+ import { atomic, record, setItems, setParent, setSlots } from "./internal.js";
5
+ import { Token } from "./token.js";
6
+ import { Chain, Entity, EscapeError, evaluateString as evaluateStringText, evaluateLiteral, evaluateNumber, Int64Max, isIntegerKey, literalKey, } from "./values.js";
7
+ /** Base of all nodes of the syntax tree. */
8
+ export class Node {
9
+ #parent = null;
10
+ get parent() {
11
+ return this.#parent;
12
+ }
13
+ getTokens() {
14
+ const res = [];
15
+ const collect = (node) => {
16
+ for (const child of node.getChildren()) {
17
+ if (child instanceof _a) {
18
+ collect(child);
19
+ }
20
+ else {
21
+ res.push(child);
22
+ }
23
+ }
24
+ };
25
+ collect(this);
26
+ return res;
27
+ }
28
+ /** Current position of the first token, computed by a walk from the root. */
29
+ getPosition() {
30
+ return this.getFirstToken().getPosition();
31
+ }
32
+ /** The source text of the node, with the trivia of its tokens. */
33
+ print() {
34
+ return this.getTokens().map(String).join('');
35
+ }
36
+ toString() {
37
+ return this.print();
38
+ }
39
+ /** Whether the node spans more than one line, its outer trivia not counted. */
40
+ isMultiLine() {
41
+ const tokens = this.getTokens();
42
+ return tokens.some((token, i) => (i > 0 && token.leadingTrivia.some((t) => t.kind === 'LineEnding'))
43
+ || /\n/.test(token.text)
44
+ || (i < tokens.length - 1 && token.trailingTrivia.some((t) => t.kind === 'LineEnding')));
45
+ }
46
+ hasLeadingComment() {
47
+ return this.getLeadingComments().length > 0;
48
+ }
49
+ /** Comments in the leading trivia of the first token. */
50
+ getLeadingComments() {
51
+ return this.getFirstToken().leadingTrivia.filter((t) => t.kind === 'Comment');
52
+ }
53
+ /** Comments in the trailing trivia of the last token. */
54
+ getTrailingComments() {
55
+ return this.getLastToken().trailingTrivia.filter((t) => t.kind === 'Comment');
56
+ }
57
+ findAncestor(type) {
58
+ for (let node = this.#parent; node; node = node.parent) {
59
+ if (node instanceof type) {
60
+ return node;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ getNextSibling() {
66
+ const siblings = this.#siblings();
67
+ return siblings[siblings.indexOf(this) + 1] ?? null;
68
+ }
69
+ getPreviousSibling() {
70
+ const siblings = this.#siblings();
71
+ return siblings[siblings.indexOf(this) - 1] ?? null;
72
+ }
73
+ #siblings() {
74
+ return this.#parent?.getChildren().filter((child) => child instanceof _a) ?? [];
75
+ }
76
+ /** Descendants of the class, in document order. */
77
+ find(type, predicate) {
78
+ const res = [];
79
+ this.#walk((node) => {
80
+ if (node instanceof type && (!predicate || predicate(node))) {
81
+ res.push(node);
82
+ }
83
+ return false;
84
+ });
85
+ return res;
86
+ }
87
+ findFirst(type, predicate) {
88
+ let res = null;
89
+ this.#walk((node) => {
90
+ if (node instanceof type && (!predicate || predicate(node))) {
91
+ res = node;
92
+ return true;
93
+ }
94
+ return false;
95
+ });
96
+ return res;
97
+ }
98
+ /** Visits the descendants in document order until the visitor returns true. */
99
+ #walk(visitor) {
100
+ for (const child of this.getChildren()) {
101
+ if (child instanceof _a && (visitor(child) || child.#walk(visitor))) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
107
+ /** @internal */
108
+ [setParent](parent) {
109
+ const old = this.#parent;
110
+ record(() => this.#parent = old);
111
+ this.#parent = parent;
112
+ }
113
+ }
114
+ _a = Node;
115
+ /** A node that has a value. */
116
+ export class ValueNode extends Node {
117
+ /** Puts the node into the slot of this one, which decides what that means; the node gets the trivia at the edges of this one in place of its own. */
118
+ replaceWith(node) {
119
+ atomic(() => replaceNode(this, node));
120
+ }
121
+ /** Takes the node out of the tree, with the trivia at its edges; the emptied slot gets null. */
122
+ detach() {
123
+ atomic(() => detachNode(this));
124
+ return this;
125
+ }
126
+ }
127
+ /** Puts a child into a slot of the parent and releases the previous one. */
128
+ function adopt(parent, child, previous = null) {
129
+ if (child === previous) {
130
+ return child;
131
+ }
132
+ else if (child) {
133
+ if (child.parent) {
134
+ throw new Error('The node already has a parent, detach() or clone() it first.');
135
+ }
136
+ for (let node = parent; node; node = node.parent) {
137
+ if (node === child) {
138
+ throw new Error('A node cannot be put into its own subtree.');
139
+ }
140
+ }
141
+ child[setParent](parent);
142
+ }
143
+ previous?.[setParent](null);
144
+ return child;
145
+ }
146
+ export class DocumentNode extends Node {
147
+ #value = null;
148
+ #endOfFile;
149
+ /** Whether removing the last item leaves null instead of an empty collection. */
150
+ nullable = false;
151
+ /** @internal */
152
+ constructor(value, endOfFile) {
153
+ super();
154
+ this.#value = adopt(this, value);
155
+ this.#endOfFile = adopt(this, endOfFile);
156
+ }
157
+ get value() {
158
+ return this.#value;
159
+ }
160
+ /** The same as setValue([], node): the node takes the place of the root, null empties the document. */
161
+ set value(node) {
162
+ atomic(() => setDocumentNode(this, node));
163
+ }
164
+ get endOfFile() {
165
+ return this.#endOfFile;
166
+ }
167
+ getChildren() {
168
+ return this.#value ? [this.#value, this.#endOfFile] : [this.#endOfFile];
169
+ }
170
+ getFirstToken() {
171
+ return this.#value?.getFirstToken() ?? this.#endOfFile;
172
+ }
173
+ getLastToken() {
174
+ return this.#endOfFile;
175
+ }
176
+ clone() {
177
+ const res = new DocumentNode(this.#value?.clone() ?? null, this.#endOfFile.clone());
178
+ res.nullable = this.nullable;
179
+ return res;
180
+ }
181
+ toValue() {
182
+ return this.#value?.toValue() ?? null;
183
+ }
184
+ /** The item at the path of keys, the same keys decode() gives; a path continues into the attributes of an entity. */
185
+ findItem(path) {
186
+ try {
187
+ return path.length ? resolvePath(this, path) : null;
188
+ }
189
+ catch (e) {
190
+ if (e instanceof NeonError) {
191
+ return null;
192
+ }
193
+ throw e;
194
+ }
195
+ }
196
+ /** Writes the value at the path; a missing last key is added, unless create is false. */
197
+ setValue(path, value, options = {}) {
198
+ atomic(() => setByPath(this, path, value, options.create ?? true));
199
+ }
200
+ /** Adds an item to the array at the path, without a key unless given one. */
201
+ addItem(path, value, options = {}) {
202
+ return atomic(() => {
203
+ const owner = path.length ? resolvePath(this, path) : null;
204
+ const container = owner ? owner.value : this.#value;
205
+ const array = arrayOf(container);
206
+ if (array) {
207
+ return addItem(array, options.key ?? null, value, options.position);
208
+ }
209
+ else if (container === null) {
210
+ if (options.position !== undefined && options.position !== 0) {
211
+ throw new RangeError(`Position ${options.position} is out of the array.`);
212
+ }
213
+ const key = options.key ?? null;
214
+ const placeholder = value instanceof ValueNode ? null : value; // a node comes into the item as into any other
215
+ setByPath(this, path, key === null ? [placeholder] : new Map([[key, placeholder]]), true);
216
+ const item = arrayOf(owner ? owner.value : this.#value).items[0];
217
+ if (value instanceof ValueNode) {
218
+ item.setValue(value);
219
+ }
220
+ return item;
221
+ }
222
+ throw new NeonError(`The value at ${path.join('.')} is not an array`);
223
+ });
224
+ }
225
+ /** Removes the item at the path with the comments it owns. */
226
+ removeItem(path, options = {}) {
227
+ atomic(() => removeByPath(this, path, options.comments ?? CommentPolicy.Drop, options.nullable, options.mergeBlankLines));
228
+ }
229
+ /** Renames the key of the item at the path. */
230
+ setKey(path, key) {
231
+ atomic(() => setItemKey(resolvePath(this, path), key));
232
+ }
233
+ /** @internal Writes the slot as it is, for an edit that moves the trivia itself. */
234
+ [setSlots](slots) {
235
+ const old = this.#value;
236
+ this.#value = adopt(this, slots.value, old);
237
+ record(() => this.#value = old);
238
+ }
239
+ }
240
+ /** Base of the two kinds of arrays. */
241
+ export class ArrayNode extends ValueNode {
242
+ #items = [];
243
+ get items() {
244
+ return this.#items;
245
+ }
246
+ /** @internal Replaces the list; items that leave it lose their parent, new ones must have none. */
247
+ [setItems](items) {
248
+ const old = this.#items;
249
+ const kept = new Set(items);
250
+ const present = new Set(old);
251
+ for (const item of old) {
252
+ if (!kept.has(item)) {
253
+ item[setParent](null);
254
+ }
255
+ }
256
+ for (const item of items) {
257
+ if (!present.has(item)) {
258
+ adopt(this, item);
259
+ }
260
+ }
261
+ this.#items = Object.freeze([...items]);
262
+ record(() => this.#items = old);
263
+ }
264
+ /** The item with the canonical key, as decode() would find it. */
265
+ findItem(key) {
266
+ const map = resolveKeys(this.items);
267
+ return map.get(String(key)) ?? null;
268
+ }
269
+ /** Adds an item built from the value, at the end or before the item at the position. */
270
+ addItem(value, options = {}) {
271
+ return atomic(() => addItem(this, options.key ?? null, value, options.position));
272
+ }
273
+ /** Removes the item with the comments it owns. */
274
+ removeItem(item, comments = CommentPolicy.Drop) {
275
+ if (item.parent !== this) {
276
+ throw new Error('The item is not in this array.');
277
+ }
278
+ atomic(() => removeItem(item, comments));
279
+ }
280
+ /** Inserts an item taken out of a tree by remove(), reindented to its new place, with its comments. */
281
+ insert(position, item) {
282
+ atomic(() => insertItem(this, item, position));
283
+ }
284
+ toValue() {
285
+ const entries = (new Map);
286
+ for (const [key, item] of resolveKeys(this.items)) {
287
+ entries.set(key, item.getValue());
288
+ }
289
+ const keys = [...entries.keys()];
290
+ if (!keys.length) {
291
+ return this.isEmptyMap() ? {} : [];
292
+ }
293
+ else if (keys.every((key, i) => key === String(i))) {
294
+ return [...entries.values()];
295
+ }
296
+ const res = {};
297
+ for (const [key, value] of entries) {
298
+ Object.defineProperty(res, key, { value, writable: true, enumerable: true, configurable: true });
299
+ }
300
+ return res;
301
+ }
302
+ }
303
+ /** Keys of the items with implicit keys assigned; for overwritten keys the winning item, at the first position. */
304
+ function resolveKeys(items) {
305
+ const keys = itemKeys(items);
306
+ return new Map(items.map((item, i) => [keys[i], item]));
307
+ }
308
+ /** The key of each item, explicit or implicit. */
309
+ export function itemKeys(items) {
310
+ let max = null;
311
+ return items.map((item) => {
312
+ let key = item.getKeyValue();
313
+ if (key === null) {
314
+ const next = max === null ? 0n : max + 1n;
315
+ if (next > Int64Max) {
316
+ throw new NeonError('Implicit key overflow', item.getPosition());
317
+ }
318
+ key = String(next);
319
+ }
320
+ if (isIntegerKey(key)) {
321
+ const n = BigInt(key);
322
+ max = max === null || n > max ? n : max;
323
+ }
324
+ return key;
325
+ });
326
+ }
327
+ export class BlockArrayNode extends ArrayNode {
328
+ /** @internal */
329
+ constructor(items) {
330
+ super();
331
+ this[setItems](items);
332
+ }
333
+ isEmptyMap() {
334
+ return false;
335
+ }
336
+ getChildren() {
337
+ return [...this.items];
338
+ }
339
+ getFirstToken() {
340
+ return this.items[0].getFirstToken();
341
+ }
342
+ getLastToken() {
343
+ return this.items.at(-1).getLastToken();
344
+ }
345
+ clone() {
346
+ return new BlockArrayNode(this.items.map((item) => item.clone()));
347
+ }
348
+ }
349
+ export class InlineArrayNode extends ArrayNode {
350
+ #openBracket;
351
+ #closeBracket;
352
+ /** @internal */
353
+ constructor(openBracket, items, closeBracket) {
354
+ super();
355
+ this.#openBracket = adopt(this, openBracket);
356
+ this[setItems](items);
357
+ this.#closeBracket = adopt(this, closeBracket);
358
+ }
359
+ isEmptyMap() {
360
+ return this.#openBracket.kind === 'OpenBrace';
361
+ }
362
+ get openBracket() {
363
+ return this.#openBracket;
364
+ }
365
+ get closeBracket() {
366
+ return this.#closeBracket;
367
+ }
368
+ getChildren() {
369
+ return [this.#openBracket, ...this.items, this.#closeBracket];
370
+ }
371
+ getFirstToken() {
372
+ return this.#openBracket;
373
+ }
374
+ getLastToken() {
375
+ return this.#closeBracket;
376
+ }
377
+ clone() {
378
+ return new InlineArrayNode(this.#openBracket.clone(), this.items.map((item) => item.clone()), this.#closeBracket.clone());
379
+ }
380
+ }
381
+ export class ArrayItemNode extends Node {
382
+ #bullet;
383
+ #key;
384
+ #colon;
385
+ #value;
386
+ #separator;
387
+ /** Whether removing the last item of the value leaves null instead of an empty collection. */
388
+ nullable = false;
389
+ /** @internal */
390
+ constructor(bullet, key, colon, value, separator = null) {
391
+ super();
392
+ this.#bullet = adopt(this, bullet);
393
+ this.#key = adopt(this, key);
394
+ this.#colon = adopt(this, colon);
395
+ this.#value = adopt(this, value);
396
+ this.#separator = adopt(this, separator);
397
+ }
398
+ get bullet() {
399
+ return this.#bullet;
400
+ }
401
+ get key() {
402
+ return this.#key;
403
+ }
404
+ get colon() {
405
+ return this.#colon;
406
+ }
407
+ get value() {
408
+ return this.#value;
409
+ }
410
+ /** The same as setValue(node) with a node: the node takes the place of the value, null empties it. */
411
+ set value(node) {
412
+ atomic(() => setItemNode(this, node));
413
+ }
414
+ get separator() {
415
+ return this.#separator;
416
+ }
417
+ /** @internal Writes the slots as they are, for an edit that moves the trivia itself. */
418
+ [setSlots](slots) {
419
+ const old = [this.#bullet, this.#key, this.#colon, this.#value, this.#separator];
420
+ record(() => [this.#bullet, this.#key, this.#colon, this.#value, this.#separator] = old);
421
+ if (slots.bullet !== undefined) {
422
+ this.#bullet = adopt(this, slots.bullet, this.#bullet);
423
+ }
424
+ if (slots.key !== undefined) {
425
+ this.#key = adopt(this, slots.key, this.#key);
426
+ }
427
+ if (slots.colon !== undefined) {
428
+ this.#colon = adopt(this, slots.colon, this.#colon);
429
+ }
430
+ if (slots.value !== undefined) {
431
+ this.#value = adopt(this, slots.value, this.#value);
432
+ }
433
+ if (slots.separator !== undefined) {
434
+ this.#separator = adopt(this, slots.separator, this.#separator);
435
+ }
436
+ }
437
+ getChildren() {
438
+ return [this.#bullet, this.#key, this.#colon, this.#value, this.#separator].filter((child) => child !== null);
439
+ }
440
+ getFirstToken() {
441
+ return this.#bullet ?? this.#key?.getFirstToken() ?? this.#value.getFirstToken();
442
+ }
443
+ getLastToken() {
444
+ return this.#separator ?? this.#value?.getLastToken() ?? this.#colon ?? this.#bullet;
445
+ }
446
+ clone() {
447
+ const res = new ArrayItemNode(this.#bullet?.clone() ?? null, this.#key?.clone() ?? null, this.#colon?.clone() ?? null, this.#value?.clone() ?? null, this.#separator?.clone() ?? null);
448
+ res.nullable = this.nullable;
449
+ return res;
450
+ }
451
+ /** The canonical key, or null for an item without a key. */
452
+ getKeyValue() {
453
+ const key = this.#key;
454
+ return key instanceof StringNode
455
+ ? key.toValue()
456
+ : key ? literalKey(key.token.text) : null;
457
+ }
458
+ /** The value of the item, null without one. */
459
+ getValue() {
460
+ return this.#value?.toValue() ?? null;
461
+ }
462
+ /** The indentation of the line the item starts on. */
463
+ getIndentation() {
464
+ const trivia = this.getFirstToken().leadingTrivia.at(-1);
465
+ return trivia?.kind === 'Whitespace' ? trivia.text : '';
466
+ }
467
+ /**
468
+ * Writes the value: a value of the host or a node. Writing an equal value changes nothing; a new
469
+ * scalar keeps the style of the old one where it can.
470
+ */
471
+ setValue(value) {
472
+ atomic(() => setItemValue(this, value));
473
+ }
474
+ /** Renames the key. */
475
+ setKey(key) {
476
+ atomic(() => setItemKey(this, key));
477
+ }
478
+ /** Removes the item from its array, with the comments it owns. */
479
+ remove(comments = CommentPolicy.Drop, mergeBlankLines = false) {
480
+ atomic(() => removeItem(this, comments, undefined, mergeBlankLines));
481
+ }
482
+ }
483
+ export class EntityNode extends ValueNode {
484
+ #value;
485
+ #attributes;
486
+ /** @internal */
487
+ constructor(value, attributes) {
488
+ super();
489
+ this.#value = adopt(this, value);
490
+ this.#attributes = adopt(this, attributes);
491
+ }
492
+ get value() {
493
+ return this.#value;
494
+ }
495
+ get attributes() {
496
+ return this.#attributes;
497
+ }
498
+ /** @internal */
499
+ [setSlots](slots) {
500
+ const old = [this.#value, this.#attributes];
501
+ record(() => [this.#value, this.#attributes] = old);
502
+ if (slots.value) {
503
+ this.#value = adopt(this, slots.value, this.#value);
504
+ }
505
+ if (slots.attributes) {
506
+ this.#attributes = adopt(this, slots.attributes, this.#attributes);
507
+ }
508
+ }
509
+ getChildren() {
510
+ return this.#attributes ? [this.#value, this.#attributes] : [this.#value];
511
+ }
512
+ getFirstToken() {
513
+ return this.#value.getFirstToken();
514
+ }
515
+ getLastToken() {
516
+ return this.#attributes?.closeBracket ?? this.#value.getLastToken();
517
+ }
518
+ clone() {
519
+ return new EntityNode(this.#value.clone(), this.#attributes?.clone() ?? null);
520
+ }
521
+ toValue() {
522
+ const attributes = this.#attributes?.toValue() ?? [];
523
+ return new Entity(this.#value.toValue(), attributes);
524
+ }
525
+ }
526
+ export class EntityChainNode extends ValueNode {
527
+ #entities;
528
+ /** @internal */
529
+ constructor(entities) {
530
+ super();
531
+ this.#entities = Object.freeze([...entities]);
532
+ for (const entity of entities) {
533
+ adopt(this, entity);
534
+ }
535
+ }
536
+ get entities() {
537
+ return this.#entities;
538
+ }
539
+ /** @internal */
540
+ [setSlots](slots) {
541
+ const old = this.#entities;
542
+ for (const entity of old) {
543
+ if (!slots.entities.includes(entity)) {
544
+ entity[setParent](null);
545
+ }
546
+ }
547
+ for (const entity of slots.entities) {
548
+ if (!old.includes(entity)) {
549
+ adopt(this, entity);
550
+ }
551
+ }
552
+ this.#entities = Object.freeze([...slots.entities]);
553
+ record(() => this.#entities = old);
554
+ }
555
+ getChildren() {
556
+ return [...this.#entities];
557
+ }
558
+ getFirstToken() {
559
+ return this.#entities[0].getFirstToken();
560
+ }
561
+ getLastToken() {
562
+ return this.#entities.at(-1).getLastToken();
563
+ }
564
+ clone() {
565
+ return new EntityChainNode(this.#entities.map((entity) => entity.clone()));
566
+ }
567
+ toValue() {
568
+ return new Entity(Chain, this.#entities.map((entity) => entity.toValue()));
569
+ }
570
+ }
571
+ export class StringNode extends ValueNode {
572
+ #token;
573
+ /** @internal */
574
+ constructor(token) {
575
+ super();
576
+ this.#token = adopt(this, token);
577
+ }
578
+ get token() {
579
+ return this.#token;
580
+ }
581
+ getChildren() {
582
+ return [this.#token];
583
+ }
584
+ getFirstToken() {
585
+ return this.#token;
586
+ }
587
+ getLastToken() {
588
+ return this.#token;
589
+ }
590
+ clone() {
591
+ return new StringNode(this.#token.clone());
592
+ }
593
+ toValue() {
594
+ return evaluateStringToken(this.#token);
595
+ }
596
+ }
597
+ export class LiteralNode extends ValueNode {
598
+ #token;
599
+ /** @internal */
600
+ constructor(token) {
601
+ super();
602
+ this.#token = adopt(this, token);
603
+ }
604
+ get token() {
605
+ return this.#token;
606
+ }
607
+ getChildren() {
608
+ return [this.#token];
609
+ }
610
+ getFirstToken() {
611
+ return this.#token;
612
+ }
613
+ getLastToken() {
614
+ return this.#token;
615
+ }
616
+ clone() {
617
+ return new LiteralNode(this.#token.clone());
618
+ }
619
+ /** The value of the literal; a key keeps words and dates as strings. */
620
+ toValue(isKey = false) {
621
+ const text = this.#token.text;
622
+ return isKey ? evaluateNumber(text) ?? text : evaluateLiteral(text);
623
+ }
624
+ }
625
+ function evaluateStringToken(token) {
626
+ try {
627
+ return evaluateStringText(token.text);
628
+ }
629
+ catch (e) {
630
+ if (e instanceof EscapeError) {
631
+ throw new NeonError(e.message, token.getPosition() ?? token.origin);
632
+ }
633
+ throw e;
634
+ }
635
+ }
@@ -0,0 +1,3 @@
1
+ import { DocumentNode } from './nodes.ts';
2
+ /** Parses NEON into the lossless syntax tree. */
3
+ export declare function parse(input: string): DocumentNode;