@nette/neon 0.0.0-stage → 1.0.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/bin/neon-lint.js +4 -0
- package/dist/builder.d.ts +28 -0
- package/dist/builder.js +76 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +172 -0
- package/dist/dumper.d.ts +5 -0
- package/dist/dumper.js +75 -0
- package/dist/editing.d.ts +41 -0
- package/dist/editing.js +1188 -0
- package/dist/encoder.d.ts +16 -0
- package/dist/encoder.js +292 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.js +32 -0
- package/dist/layout.d.ts +14 -0
- package/dist/layout.js +105 -0
- package/dist/lexer.d.ts +8 -0
- package/dist/lexer.js +59 -0
- package/dist/neon.d.ts +19 -0
- package/dist/neon.js +22 -0
- package/dist/nodes.d.ts +227 -0
- package/dist/nodes.js +635 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +404 -0
- package/dist/position.d.ts +8 -0
- package/dist/position.js +40 -0
- package/dist/printer.d.ts +8 -0
- package/dist/printer.js +40 -0
- package/dist/scanner.d.ts +30 -0
- package/dist/scanner.js +193 -0
- package/dist/token.d.ts +67 -0
- package/dist/token.js +309 -0
- package/dist/traverser.d.ts +31 -0
- package/dist/traverser.js +96 -0
- package/dist/utf8.d.ts +2 -0
- package/dist/utf8.js +44 -0
- package/dist/values.d.ts +75 -0
- package/dist/values.js +316 -0
- package/license.md +21 -0
- package/package.json +60 -6
- package/readme.md +242 -0
- package/README.md +0 -3
package/dist/parser.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { NeonError } from "./errors.js";
|
|
2
|
+
import { setSlots } from "./internal.js";
|
|
3
|
+
import { quote, tokenize } from "./lexer.js";
|
|
4
|
+
import { ArrayItemNode, BlockArrayNode, DocumentNode, EntityChainNode, EntityNode, InlineArrayNode, LiteralNode, StringNode, } from "./nodes.js";
|
|
5
|
+
import { Cursor } from "./position.js";
|
|
6
|
+
import { Chain, EscapeError, evaluateString, Int64Max, isIntegerKey, literalKey, MaxDepth } from "./values.js";
|
|
7
|
+
/** Parses NEON into the lossless syntax tree. */
|
|
8
|
+
export function parse(input) {
|
|
9
|
+
return new Parser(tokenize(input), input).parse();
|
|
10
|
+
}
|
|
11
|
+
const TooDeep = 'Too deep nesting';
|
|
12
|
+
const Pairs = {
|
|
13
|
+
OpenBracket: 'CloseBracket',
|
|
14
|
+
OpenBrace: 'CloseBrace',
|
|
15
|
+
OpenParen: 'CloseParen',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The parser of nette/neon 3.x ported onto tokens with trivia. Line endings are trivia, but the
|
|
19
|
+
* parser sees them as it saw the Newline tokens of 3.x: before token i stands a line break if the
|
|
20
|
+
* trivia between token i-1 and token i contain a line ending.
|
|
21
|
+
*/
|
|
22
|
+
class Parser {
|
|
23
|
+
tokens;
|
|
24
|
+
#i = 0;
|
|
25
|
+
#newlineTaken = false;
|
|
26
|
+
#depth = 0;
|
|
27
|
+
#cursor;
|
|
28
|
+
constructor(tokens, input) {
|
|
29
|
+
this.tokens = tokens;
|
|
30
|
+
this.#cursor = new Cursor(input);
|
|
31
|
+
}
|
|
32
|
+
parse() {
|
|
33
|
+
while (this.consume('Newline')) {
|
|
34
|
+
;
|
|
35
|
+
}
|
|
36
|
+
const value = this.parseBlock(this.getIndentation());
|
|
37
|
+
while (this.consume('Newline')) {
|
|
38
|
+
;
|
|
39
|
+
}
|
|
40
|
+
if (this.isNext()) {
|
|
41
|
+
this.error();
|
|
42
|
+
}
|
|
43
|
+
const document = new DocumentNode(value, this.tokens.at(-1));
|
|
44
|
+
this.verify(document);
|
|
45
|
+
return document;
|
|
46
|
+
}
|
|
47
|
+
parseBlock(indent, onlyBullets = false) {
|
|
48
|
+
const depth = this.#depth;
|
|
49
|
+
try {
|
|
50
|
+
return this.parseBlockItems(indent, onlyBullets);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.#depth = depth;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A block opened on the line of a dash is indented by a tab if the line where its indentation is first
|
|
58
|
+
* compared starts with one, by two spaces otherwise. 3.x tries the tab and then the spaces, which costs time
|
|
59
|
+
* exponential in nested dashes; the spaces fail on that line whenever the tab was chosen, so a block
|
|
60
|
+
* that fails with the tab fails as the spaces would.
|
|
61
|
+
*/
|
|
62
|
+
parseDashBlock(base) {
|
|
63
|
+
const indent = { base, resolved: null, line: null };
|
|
64
|
+
try {
|
|
65
|
+
return this.parseBlock(indent);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e instanceof NeonError && e.message !== TooDeep && indent.resolved?.endsWith('\t') && indent.line) {
|
|
69
|
+
this.#i = indent.line.i;
|
|
70
|
+
this.#newlineTaken = indent.line.newlineTaken;
|
|
71
|
+
this.error('Invalid combination of tabs and spaces');
|
|
72
|
+
}
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** The indentation of a block, decided for a block opened on the line of a dash by the indentation of the next line. */
|
|
77
|
+
resolve(indent, next) {
|
|
78
|
+
if (typeof indent === 'string') {
|
|
79
|
+
return indent;
|
|
80
|
+
}
|
|
81
|
+
else if (indent.resolved === null) {
|
|
82
|
+
const base = this.resolve(indent.base, next);
|
|
83
|
+
indent.resolved = base + (next.startsWith(base + '\t') ? '\t' : ' ');
|
|
84
|
+
indent.line = this.mark();
|
|
85
|
+
}
|
|
86
|
+
return indent.resolved;
|
|
87
|
+
}
|
|
88
|
+
parseBlockItems(pending, onlyBullets) {
|
|
89
|
+
const items = [];
|
|
90
|
+
const keys = new KeyCheck(this);
|
|
91
|
+
while (true) {
|
|
92
|
+
const bullet = this.consumeToken('Dash');
|
|
93
|
+
let key = null;
|
|
94
|
+
let colon = null;
|
|
95
|
+
let value = null;
|
|
96
|
+
if (bullet) {
|
|
97
|
+
if (!items.length) {
|
|
98
|
+
this.enter(bullet);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (!this.isNext() || onlyBullets) {
|
|
102
|
+
return items.length ? new BlockArrayNode(items) : null;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const head = this.parseValue();
|
|
106
|
+
if ((colon = this.consumeToken('Colon', 'Equals'))) {
|
|
107
|
+
key = keys.add(head);
|
|
108
|
+
if (!items.length) {
|
|
109
|
+
this.enter(head.getFirstToken());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (items.length) {
|
|
114
|
+
this.error();
|
|
115
|
+
}
|
|
116
|
+
return head;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (this.consume('Newline')) {
|
|
120
|
+
while (this.consume('Newline')) {
|
|
121
|
+
;
|
|
122
|
+
}
|
|
123
|
+
const nextIndent = this.getIndentation();
|
|
124
|
+
const indent = this.resolve(pending, nextIndent);
|
|
125
|
+
if (!comparable(nextIndent, indent)) {
|
|
126
|
+
this.error('Invalid combination of tabs and spaces');
|
|
127
|
+
}
|
|
128
|
+
else if (nextIndent.length > indent.length) { // open new block
|
|
129
|
+
value = this.parseBlock(nextIndent);
|
|
130
|
+
}
|
|
131
|
+
else if (nextIndent.length < indent.length) { // close block
|
|
132
|
+
items.push(keys.item(bullet, key, colon, null));
|
|
133
|
+
return new BlockArrayNode(items);
|
|
134
|
+
}
|
|
135
|
+
else if (key !== null && this.isNext('Dash')) { // special dash subblock
|
|
136
|
+
value = this.parseBlock(indent, true);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (key === null) { // open new block after dash
|
|
140
|
+
value = this.parseDashBlock(pending);
|
|
141
|
+
}
|
|
142
|
+
else if (this.isNext()) {
|
|
143
|
+
value = this.parseValue();
|
|
144
|
+
if (this.isNext() && !this.isNext('Newline')) {
|
|
145
|
+
this.error();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
items.push(keys.item(bullet, key, colon, value));
|
|
149
|
+
while (this.consume('Newline')) {
|
|
150
|
+
;
|
|
151
|
+
}
|
|
152
|
+
if (!this.isNext()) {
|
|
153
|
+
return new BlockArrayNode(items);
|
|
154
|
+
}
|
|
155
|
+
const nextIndent = this.getIndentation();
|
|
156
|
+
const indent = this.resolve(pending, nextIndent);
|
|
157
|
+
if (!comparable(nextIndent, indent)) {
|
|
158
|
+
this.error('Invalid combination of tabs and spaces');
|
|
159
|
+
}
|
|
160
|
+
else if (nextIndent.length > indent.length) {
|
|
161
|
+
this.error('Bad indentation');
|
|
162
|
+
}
|
|
163
|
+
else if (nextIndent.length < indent.length) { // close block
|
|
164
|
+
return new BlockArrayNode(items);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
parseValue() {
|
|
169
|
+
let node;
|
|
170
|
+
let token;
|
|
171
|
+
if ((token = this.consumeToken('String'))) {
|
|
172
|
+
try {
|
|
173
|
+
evaluateString(token.text);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (e instanceof EscapeError) {
|
|
177
|
+
this.error(e.message, token);
|
|
178
|
+
}
|
|
179
|
+
throw e;
|
|
180
|
+
}
|
|
181
|
+
node = new StringNode(token);
|
|
182
|
+
}
|
|
183
|
+
else if ((token = this.consumeToken('Literal'))) {
|
|
184
|
+
node = new LiteralNode(token);
|
|
185
|
+
}
|
|
186
|
+
else if (this.isNext('OpenBracket', 'OpenParen', 'OpenBrace')) {
|
|
187
|
+
node = this.parseBraces();
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.error();
|
|
191
|
+
}
|
|
192
|
+
return this.parseEntity(node);
|
|
193
|
+
}
|
|
194
|
+
parseEntity(node) {
|
|
195
|
+
if (!this.isNext('OpenParen')) {
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
this.checkEntityHead(node);
|
|
199
|
+
const entities = [new EntityNode(node, this.parseBraces())];
|
|
200
|
+
let token;
|
|
201
|
+
while ((token = this.consumeToken('Literal'))) {
|
|
202
|
+
const head = new LiteralNode(token);
|
|
203
|
+
this.checkEntityHead(head);
|
|
204
|
+
if (this.isNext('OpenParen')) {
|
|
205
|
+
entities.push(new EntityNode(head, this.parseBraces()));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
entities.push(new EntityNode(head, null));
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return entities.length === 1
|
|
213
|
+
? entities[0]
|
|
214
|
+
: new EntityChainNode(entities);
|
|
215
|
+
}
|
|
216
|
+
parseBraces() {
|
|
217
|
+
const depth = this.#depth;
|
|
218
|
+
try {
|
|
219
|
+
return this.parseBracesItems();
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
this.#depth = depth;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
parseBracesItems() {
|
|
226
|
+
const open = this.consumeToken();
|
|
227
|
+
this.enter(open);
|
|
228
|
+
const end = Pairs[open.kind];
|
|
229
|
+
const items = [];
|
|
230
|
+
const keys = new KeyCheck(this);
|
|
231
|
+
while (true) {
|
|
232
|
+
while (this.consume('Newline')) {
|
|
233
|
+
;
|
|
234
|
+
}
|
|
235
|
+
const close = this.consumeToken(end);
|
|
236
|
+
if (close) {
|
|
237
|
+
return new InlineArrayNode(open, items, close);
|
|
238
|
+
}
|
|
239
|
+
const head = this.parseValue();
|
|
240
|
+
let item;
|
|
241
|
+
const colon = this.consumeToken('Colon', 'Equals');
|
|
242
|
+
if (colon) {
|
|
243
|
+
const key = keys.add(head);
|
|
244
|
+
const value = this.isNext('Newline', 'Comma', end) ? null : this.parseValue();
|
|
245
|
+
item = keys.item(null, key, colon, value);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
item = keys.item(null, null, null, head);
|
|
249
|
+
}
|
|
250
|
+
items.push(item);
|
|
251
|
+
const before = this.mark();
|
|
252
|
+
while (this.consume('Newline')) {
|
|
253
|
+
;
|
|
254
|
+
}
|
|
255
|
+
const comma = this.consumeToken('Comma');
|
|
256
|
+
if (comma) {
|
|
257
|
+
item[setSlots]({ separator: comma });
|
|
258
|
+
}
|
|
259
|
+
if (before.i !== this.#i || before.newlineTaken !== this.#newlineTaken) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
else if (!this.isNext(end)) {
|
|
263
|
+
this.error();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** One level deeper into arrays. */
|
|
268
|
+
enter(token) {
|
|
269
|
+
if (++this.#depth > MaxDepth) {
|
|
270
|
+
this.error(TooDeep, token);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
checkEntityHead(node) {
|
|
274
|
+
const value = node instanceof StringNode
|
|
275
|
+
? node.toValue()
|
|
276
|
+
: node instanceof LiteralNode
|
|
277
|
+
? node.token.text
|
|
278
|
+
: null;
|
|
279
|
+
if (value === Chain) {
|
|
280
|
+
this.error(`Entity name '${Chain}' is reserved`, node.getFirstToken());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Every token of the stream is in the tree exactly once, in the order of the stream. */
|
|
284
|
+
verify(document) {
|
|
285
|
+
const tokens = document.getTokens();
|
|
286
|
+
if (tokens.length !== this.tokens.length || tokens.some((token, i) => token !== this.tokens[i])) {
|
|
287
|
+
throw new Error('Internal error: the tree does not hold the tokens of the input in order.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// stream of tokens with line breaks
|
|
291
|
+
newlineBefore(i) {
|
|
292
|
+
const trivia = i === 0 ? this.tokens[0].leadingTrivia : this.tokens[i - 1].trailingTrivia;
|
|
293
|
+
return trivia.some((t) => t.kind === 'LineEnding');
|
|
294
|
+
}
|
|
295
|
+
peek() {
|
|
296
|
+
return !this.#newlineTaken && this.newlineBefore(this.#i)
|
|
297
|
+
? 'Newline'
|
|
298
|
+
: this.tokens[this.#i].kind;
|
|
299
|
+
}
|
|
300
|
+
/** Whether the next item is of one of the kinds; without kinds, whether anything but the end follows. */
|
|
301
|
+
isNext(...kinds) {
|
|
302
|
+
const next = this.peek();
|
|
303
|
+
return kinds.length ? kinds.includes(next) : next !== 'EndOfFile';
|
|
304
|
+
}
|
|
305
|
+
consume(kind) {
|
|
306
|
+
if (this.peek() === kind) {
|
|
307
|
+
this.#newlineTaken = true;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
consumeToken(...kinds) {
|
|
313
|
+
const next = this.peek();
|
|
314
|
+
if (next === 'Newline' || next === 'EndOfFile' || (kinds.length && !kinds.includes(next))) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
this.#newlineTaken = false;
|
|
318
|
+
return this.tokens[this.#i++];
|
|
319
|
+
}
|
|
320
|
+
mark() {
|
|
321
|
+
return { i: this.#i, newlineTaken: this.#newlineTaken };
|
|
322
|
+
}
|
|
323
|
+
/** Indentation of the next token if it starts a line: the whitespace right after a line ending or the start. */
|
|
324
|
+
getIndentation() {
|
|
325
|
+
const i = this.#i;
|
|
326
|
+
const trivia = i === 0
|
|
327
|
+
? this.tokens[0].leadingTrivia.filter((t) => t.kind !== 'ByteOrderMark')
|
|
328
|
+
: [...this.tokens[i - 1].trailingTrivia, ...this.tokens[i].leadingTrivia];
|
|
329
|
+
const last = trivia.at(-1);
|
|
330
|
+
const before = trivia.at(-2);
|
|
331
|
+
return last?.kind === 'Whitespace' && (trivia.length === 1 ? i === 0 : before?.kind === 'LineEnding')
|
|
332
|
+
? last.text
|
|
333
|
+
: '';
|
|
334
|
+
}
|
|
335
|
+
error(message, token) {
|
|
336
|
+
if (token) {
|
|
337
|
+
throw new NeonError(message, token.origin);
|
|
338
|
+
}
|
|
339
|
+
const next = this.peek();
|
|
340
|
+
if (next === 'Newline') {
|
|
341
|
+
const [offset, count] = this.locateNewline();
|
|
342
|
+
throw new NeonError(message ?? `Unexpected '${'<new line>'.repeat(Math.min(count, 40))}'`, this.#cursor.at(offset));
|
|
343
|
+
}
|
|
344
|
+
const current = this.tokens[this.#i];
|
|
345
|
+
throw new NeonError(message ?? (next === 'EndOfFile' ? 'Unexpected end' : `Unexpected '${quote(current.text)}'`), current.origin);
|
|
346
|
+
}
|
|
347
|
+
/** Offset of the line break before the current token, and how many line endings follow one another there. */
|
|
348
|
+
locateNewline() {
|
|
349
|
+
const i = this.#i;
|
|
350
|
+
const prev = this.tokens[i - 1];
|
|
351
|
+
let offset = prev ? prev.origin.utf16Offset + prev.text.length : 0;
|
|
352
|
+
const trivia = [...prev?.trailingTrivia ?? [], ...this.tokens[i].leadingTrivia];
|
|
353
|
+
let k = 0;
|
|
354
|
+
while (trivia[k].kind !== 'LineEnding') {
|
|
355
|
+
offset += trivia[k++].text.length;
|
|
356
|
+
}
|
|
357
|
+
let count = 0;
|
|
358
|
+
while (trivia[k + count]?.kind === 'LineEnding') {
|
|
359
|
+
count++;
|
|
360
|
+
}
|
|
361
|
+
return [offset, count];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Checks explicit keys of one array for duplicates and implicit keys for overflow. */
|
|
365
|
+
class KeyCheck {
|
|
366
|
+
parser;
|
|
367
|
+
#seen = (new Set);
|
|
368
|
+
#max = null;
|
|
369
|
+
constructor(parser) {
|
|
370
|
+
this.parser = parser;
|
|
371
|
+
}
|
|
372
|
+
add(node) {
|
|
373
|
+
if (!(node instanceof StringNode || node instanceof LiteralNode)) {
|
|
374
|
+
this.parser.error('Unacceptable key', node.getFirstToken());
|
|
375
|
+
}
|
|
376
|
+
const key = node instanceof StringNode ? node.toValue() : literalKey(node.token.text);
|
|
377
|
+
if (this.#seen.has(key)) {
|
|
378
|
+
this.parser.error(`Duplicated key '${key}'`, node.token);
|
|
379
|
+
}
|
|
380
|
+
this.#seen.add(key);
|
|
381
|
+
return node;
|
|
382
|
+
}
|
|
383
|
+
/** Makes the item and accounts its key, explicit or implicit. */
|
|
384
|
+
item(bullet, key, colon, value) {
|
|
385
|
+
const item = new ArrayItemNode(bullet, key, colon, value);
|
|
386
|
+
let canonical = item.getKeyValue();
|
|
387
|
+
if (canonical === null) {
|
|
388
|
+
const next = this.#max === null ? 0n : this.#max + 1n;
|
|
389
|
+
if (next > Int64Max) {
|
|
390
|
+
this.parser.error('Implicit key overflow', (value ?? item).getFirstToken());
|
|
391
|
+
}
|
|
392
|
+
canonical = String(next);
|
|
393
|
+
}
|
|
394
|
+
if (isIntegerKey(canonical)) {
|
|
395
|
+
const n = BigInt(canonical);
|
|
396
|
+
this.#max = this.#max === null || n > this.#max ? n : this.#max;
|
|
397
|
+
}
|
|
398
|
+
return item;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function comparable(a, b) {
|
|
402
|
+
const n = Math.min(a.length, b.length);
|
|
403
|
+
return a.slice(0, n) === b.slice(0, n);
|
|
404
|
+
}
|
package/dist/position.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Computes positions of increasing UTF-16 offsets in one pass. */
|
|
2
|
+
export class Cursor {
|
|
3
|
+
s;
|
|
4
|
+
#index = 0;
|
|
5
|
+
#line = 1;
|
|
6
|
+
#column = 1;
|
|
7
|
+
#offset = 0;
|
|
8
|
+
constructor(s) {
|
|
9
|
+
this.s = s;
|
|
10
|
+
}
|
|
11
|
+
at(index) {
|
|
12
|
+
if (index < this.#index) {
|
|
13
|
+
this.#index = 0;
|
|
14
|
+
this.#line = 1;
|
|
15
|
+
this.#column = 1;
|
|
16
|
+
this.#offset = 0;
|
|
17
|
+
}
|
|
18
|
+
const s = this.s;
|
|
19
|
+
for (let i = this.#index; i < index; i++) {
|
|
20
|
+
const code = s.charCodeAt(i);
|
|
21
|
+
if (code === 10) {
|
|
22
|
+
this.#line++;
|
|
23
|
+
this.#column = 1;
|
|
24
|
+
this.#offset++;
|
|
25
|
+
}
|
|
26
|
+
else if (code >= 0xDC00 && code <= 0xDFFF && i > 0 && isHighSurrogate(s.charCodeAt(i - 1))) {
|
|
27
|
+
// second half of a code point
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.#column++;
|
|
31
|
+
this.#offset++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this.#index = index;
|
|
35
|
+
return Object.freeze({ line: this.#line, column: this.#column, offset: this.#offset, utf16Offset: index });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isHighSurrogate(code) {
|
|
39
|
+
return code >= 0xD800 && code <= 0xDBFF;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Node } from './nodes.ts';
|
|
2
|
+
import type { Position, Token } from './token.ts';
|
|
3
|
+
/** The source text of the node, with the trivia of its tokens. */
|
|
4
|
+
export declare function print(node: Node): string;
|
|
5
|
+
/** The text of the node without the leading trivia of its first token and the trailing trivia of its last. */
|
|
6
|
+
export declare function printText(node: Node): string;
|
|
7
|
+
/** Current positions of all tokens of the tree in one walk, for an editor asking about many of them. */
|
|
8
|
+
export declare function locateAll(root: Node): Map<Token, Position>;
|
package/dist/printer.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cursor } from "./position.js";
|
|
2
|
+
/** The source text of the node, with the trivia of its tokens. */
|
|
3
|
+
export function print(node) {
|
|
4
|
+
return node.print();
|
|
5
|
+
}
|
|
6
|
+
/** The text of the node without the leading trivia of its first token and the trailing trivia of its last. */
|
|
7
|
+
export function printText(node) {
|
|
8
|
+
const tokens = node.getTokens();
|
|
9
|
+
let res = '';
|
|
10
|
+
tokens.forEach((token, i) => {
|
|
11
|
+
if (i > 0) {
|
|
12
|
+
res += triviaText(token.leadingTrivia);
|
|
13
|
+
}
|
|
14
|
+
res += token.text;
|
|
15
|
+
if (i < tokens.length - 1) {
|
|
16
|
+
res += triviaText(token.trailingTrivia);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return res;
|
|
20
|
+
}
|
|
21
|
+
/** Current positions of all tokens of the tree in one walk, for an editor asking about many of them. */
|
|
22
|
+
export function locateAll(root) {
|
|
23
|
+
const tokens = root.getTokens();
|
|
24
|
+
const offsets = [];
|
|
25
|
+
let text = '';
|
|
26
|
+
for (const token of tokens) {
|
|
27
|
+
text += triviaText(token.leadingTrivia);
|
|
28
|
+
offsets.push(text.length);
|
|
29
|
+
text += token.text + triviaText(token.trailingTrivia);
|
|
30
|
+
}
|
|
31
|
+
const cursor = new Cursor(text);
|
|
32
|
+
return new Map(tokens.map((token, i) => [token, cursor.at(offsets[i])]));
|
|
33
|
+
}
|
|
34
|
+
function triviaText(trivia) {
|
|
35
|
+
let res = '';
|
|
36
|
+
for (const t of trivia) {
|
|
37
|
+
res += t.text;
|
|
38
|
+
}
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TokenKind, TriviaKind } from './token.ts';
|
|
2
|
+
export type ScannedKind = TokenKind | 'Trivia';
|
|
3
|
+
export interface Scanned {
|
|
4
|
+
kind: ScannedKind;
|
|
5
|
+
triviaKind?: TriviaKind;
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
}
|
|
9
|
+
/** Reads tokens and trivia one by one. Returns null on an error, with errorAt set. */
|
|
10
|
+
export declare class Scanner {
|
|
11
|
+
#private;
|
|
12
|
+
private readonly s;
|
|
13
|
+
errorAt: number;
|
|
14
|
+
constructor(s: string);
|
|
15
|
+
get position(): number;
|
|
16
|
+
next(): Scanned | null;
|
|
17
|
+
/** Is the character one of the given ones, a space, a tab, or a line ending? */
|
|
18
|
+
private isAny;
|
|
19
|
+
private isSpace;
|
|
20
|
+
private isLineEnding;
|
|
21
|
+
private scanLiteral;
|
|
22
|
+
private scanSingleQuoted;
|
|
23
|
+
private scanDoubleQuoted;
|
|
24
|
+
/**
|
|
25
|
+
* Content runs to the first line after the first one that starts with optional spaces and the
|
|
26
|
+
* closing triple; only without such a line may the first line itself close an empty string.
|
|
27
|
+
*/
|
|
28
|
+
private scanMultiline;
|
|
29
|
+
private closingAt;
|
|
30
|
+
}
|