@altopelago/aeon-lexer 0.9.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/lexer.js ADDED
@@ -0,0 +1,1166 @@
1
+ import { TokenType, createPosition, createSpan, createToken, } from './tokens.js';
2
+ import { UnexpectedCharacterError, UnterminatedStringError, InvalidEscapeSequenceError, InvalidNumberError, InvalidTimeError, InvalidDateError, InvalidDateTimeError, UnterminatedBlockCommentError, } from './errors.js';
3
+ /**
4
+ * Keywords mapping
5
+ */
6
+ const KEYWORDS = new Map([
7
+ ['true', TokenType.True],
8
+ ['false', TokenType.False],
9
+ ['yes', TokenType.Yes],
10
+ ['no', TokenType.No],
11
+ ['on', TokenType.On],
12
+ ['off', TokenType.Off],
13
+ ]);
14
+ /**
15
+ * Check if character is a letter
16
+ */
17
+ function isLetter(c) {
18
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
19
+ }
20
+ /**
21
+ * Check if character is a digit
22
+ */
23
+ function isDigit(c) {
24
+ return c >= '0' && c <= '9';
25
+ }
26
+ /**
27
+ * Check if character is a hex digit
28
+ */
29
+ function isHexDigit(c) {
30
+ return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
31
+ }
32
+ /**
33
+ * Check if character is alphanumeric or underscore
34
+ */
35
+ function isAlphanumeric(c) {
36
+ return isLetter(c) || isDigit(c) || c === '_';
37
+ }
38
+ function isEncodingChar(c) {
39
+ return isLetter(c)
40
+ || isDigit(c)
41
+ || c === '+'
42
+ || c === '/'
43
+ || c === '='
44
+ || c === '-'
45
+ || c === '_'
46
+ || c === '.';
47
+ }
48
+ function isEncodingStartChar(c) {
49
+ return c !== '=' && isEncodingChar(c);
50
+ }
51
+ function isRadixChar(c) {
52
+ return isLetter(c)
53
+ || isDigit(c)
54
+ || c === '+'
55
+ || c === '-'
56
+ || c === '.'
57
+ || c === '_'
58
+ || c === '&'
59
+ || c === '!';
60
+ }
61
+ function isRadixStartChar(c) {
62
+ return c === '+' || c === '-' || c === '.' || isLetter(c) || isDigit(c) || c === '&' || c === '!';
63
+ }
64
+ function isPrintableAscii(c) {
65
+ const code = c.charCodeAt(0);
66
+ return code >= 0x21 && code <= 0x7e;
67
+ }
68
+ function isSlashChannelMarker(c) {
69
+ return c === '#' || c === '@' || c === '?' || c === '{' || c === '[' || c === '(';
70
+ }
71
+ function slashChannelClosingMarker(openMarker) {
72
+ if (openMarker === '{')
73
+ return '}';
74
+ if (openMarker === '[')
75
+ return ']';
76
+ if (openMarker === '(')
77
+ return ')';
78
+ return openMarker;
79
+ }
80
+ function isSeparatorRawChar(c) {
81
+ return /[A-Za-z0-9!#$%&*+\-.:;=?@^_|~<>]/.test(c);
82
+ }
83
+ /**
84
+ * AEON Lexer
85
+ *
86
+ * Hand-written lexer for AEON documents. Produces a stream of tokens
87
+ * with accurate span information for error reporting.
88
+ */
89
+ export class Lexer {
90
+ input;
91
+ options;
92
+ offset = 0;
93
+ line = 1;
94
+ column = 1;
95
+ sawLeadingShebang = false;
96
+ tokens = [];
97
+ errors = [];
98
+ constructor(input, options = {}) {
99
+ this.input = input;
100
+ this.options = options;
101
+ }
102
+ /**
103
+ * Tokenize the input
104
+ */
105
+ tokenize() {
106
+ while (!this.isAtEnd()) {
107
+ this.scanToken();
108
+ }
109
+ // Add EOF token
110
+ const pos = this.currentPosition();
111
+ this.tokens.push(createToken(TokenType.EOF, '', createSpan(pos, pos)));
112
+ return {
113
+ tokens: this.tokens,
114
+ errors: this.errors,
115
+ };
116
+ }
117
+ isAtEnd() {
118
+ return this.offset >= this.input.length;
119
+ }
120
+ currentPosition() {
121
+ return createPosition(this.line, this.column, this.offset);
122
+ }
123
+ peek() {
124
+ if (this.isAtEnd())
125
+ return '\0';
126
+ return this.input[this.offset];
127
+ }
128
+ peekNext() {
129
+ if (this.offset + 1 >= this.input.length)
130
+ return '\0';
131
+ return this.input[this.offset + 1];
132
+ }
133
+ peekN(distance) {
134
+ const index = this.offset + distance - 1;
135
+ if (index >= this.input.length)
136
+ return '\0';
137
+ return this.input[index];
138
+ }
139
+ advance() {
140
+ const c = this.input[this.offset];
141
+ this.offset++;
142
+ if (c === '\n') {
143
+ this.line++;
144
+ this.column = 1;
145
+ }
146
+ else {
147
+ this.column++;
148
+ }
149
+ return c;
150
+ }
151
+ match(expected) {
152
+ if (this.isAtEnd())
153
+ return false;
154
+ if (this.input[this.offset] !== expected)
155
+ return false;
156
+ this.advance();
157
+ return true;
158
+ }
159
+ addToken(type, value, start) {
160
+ const end = this.currentPosition();
161
+ this.tokens.push(createToken(type, value, createSpan(start, end)));
162
+ }
163
+ scanToken() {
164
+ const start = this.currentPosition();
165
+ const c = this.advance();
166
+ switch (c) {
167
+ // Single character tokens
168
+ case '{':
169
+ this.addToken(TokenType.LeftBrace, c, start);
170
+ break;
171
+ case '}':
172
+ this.addToken(TokenType.RightBrace, c, start);
173
+ break;
174
+ case '[':
175
+ this.addToken(TokenType.LeftBracket, c, start);
176
+ break;
177
+ case ']':
178
+ this.addToken(TokenType.RightBracket, c, start);
179
+ break;
180
+ case '(':
181
+ this.addToken(TokenType.LeftParen, c, start);
182
+ break;
183
+ case ')':
184
+ this.addToken(TokenType.RightParen, c, start);
185
+ break;
186
+ case '<':
187
+ this.addToken(TokenType.LeftAngle, c, start);
188
+ break;
189
+ case '>':
190
+ this.addToken(TokenType.RightAngle, c, start);
191
+ break;
192
+ case '=':
193
+ this.addToken(TokenType.Equals, c, start);
194
+ break;
195
+ case ':':
196
+ this.addToken(TokenType.Colon, c, start);
197
+ break;
198
+ case ',':
199
+ this.addToken(TokenType.Comma, c, start);
200
+ break;
201
+ case '.':
202
+ if (isDigit(this.peek())) {
203
+ this.scanNumber(c, start);
204
+ }
205
+ else {
206
+ this.addToken(TokenType.Dot, c, start);
207
+ }
208
+ break;
209
+ case '@':
210
+ this.addToken(TokenType.At, c, start);
211
+ break;
212
+ case '&':
213
+ this.addToken(TokenType.Ampersand, c, start);
214
+ break;
215
+ case ';':
216
+ this.addToken(TokenType.Semicolon, c, start);
217
+ break;
218
+ // Tilde (may be ~ or ~>)
219
+ case '~':
220
+ if (this.match('>')) {
221
+ this.addToken(TokenType.TildeArrow, '~>', start);
222
+ }
223
+ else {
224
+ this.addToken(TokenType.Tilde, '~', start);
225
+ }
226
+ break;
227
+ // Separator literal (^content)
228
+ case '^':
229
+ this.scanSeparatorLiteral(start);
230
+ break;
231
+ // Hex literal (#FF00AA)
232
+ case '#':
233
+ if (this.isLeadingShebangStart(start) && this.peek() === '!') {
234
+ this.scanShebangComment(start);
235
+ break;
236
+ }
237
+ if (isHexDigit(this.peek())) {
238
+ this.scanHexLiteral(start);
239
+ }
240
+ else {
241
+ this.addToken(TokenType.Hash, c, start);
242
+ }
243
+ break;
244
+ // Encoding literal ($Base64...)
245
+ case '$':
246
+ if (this.peek() === '.') {
247
+ this.addToken(TokenType.Dollar, c, start);
248
+ }
249
+ else if (isEncodingStartChar(this.peek())) {
250
+ this.scanEncodingLiteral(start);
251
+ }
252
+ else {
253
+ this.addToken(TokenType.Dollar, c, start);
254
+ }
255
+ break;
256
+ // Radix literal (%1011)
257
+ case '%':
258
+ if (isRadixStartChar(this.peek())) {
259
+ this.scanRadixLiteral(start);
260
+ }
261
+ else {
262
+ this.addToken(TokenType.Percent, c, start);
263
+ }
264
+ break;
265
+ // Comment or division
266
+ case '/':
267
+ if (this.match('/')) {
268
+ this.scanLineComment(start);
269
+ }
270
+ else if (this.match('*')) {
271
+ this.scanBlockComment(start);
272
+ }
273
+ else if (isSlashChannelMarker(this.peek())) {
274
+ this.scanSlashChannelBlockComment(start);
275
+ }
276
+ else {
277
+ this.addToken(TokenType.Symbol, c, start);
278
+ }
279
+ break;
280
+ // String literals
281
+ case '"':
282
+ case "'":
283
+ case '`':
284
+ this.scanString(c, start);
285
+ break;
286
+ // Newline
287
+ case '\n':
288
+ if (this.options.includeNewlines) {
289
+ this.addToken(TokenType.Newline, '\n', start);
290
+ }
291
+ break;
292
+ // Whitespace
293
+ case ' ':
294
+ case '\t':
295
+ case '\r':
296
+ // Skip whitespace
297
+ break;
298
+ // Numbers (including negative)
299
+ case '-':
300
+ case '+':
301
+ if (isDigit(this.peek())) {
302
+ this.scanNumber(c, start);
303
+ }
304
+ else if (this.peek() === '.' && isDigit(this.peekN(2))) {
305
+ this.advance(); // consume .
306
+ this.scanNumber(`${c}.`, start);
307
+ }
308
+ else {
309
+ this.addToken(TokenType.Symbol, c, start);
310
+ }
311
+ break;
312
+ default:
313
+ if (isDigit(c)) {
314
+ this.scanNumber(c, start);
315
+ }
316
+ else if (isLetter(c) || c === '_') {
317
+ this.scanIdentifierOrKeyword(c, start);
318
+ }
319
+ else if (isPrintableAscii(c)) {
320
+ this.addToken(TokenType.Symbol, c, start);
321
+ }
322
+ else {
323
+ this.errors.push(new UnexpectedCharacterError(c, createSpan(start, this.currentPosition())));
324
+ }
325
+ break;
326
+ }
327
+ }
328
+ scanString(delimiter, start) {
329
+ const isMultiline = delimiter === '`';
330
+ let value = '';
331
+ const initialErrorCount = this.errors.length;
332
+ while (!this.isAtEnd()) {
333
+ const c = this.peek();
334
+ if (c === delimiter) {
335
+ this.advance();
336
+ if (this.errors.length > initialErrorCount) {
337
+ return;
338
+ }
339
+ const end = this.currentPosition();
340
+ this.tokens.push({
341
+ type: TokenType.String,
342
+ value,
343
+ span: createSpan(start, end),
344
+ quote: delimiter,
345
+ });
346
+ return;
347
+ }
348
+ if (c === '\n' && !isMultiline) {
349
+ this.errors.push(new UnterminatedStringError(delimiter, createSpan(start, this.currentPosition())));
350
+ return;
351
+ }
352
+ if (c === '\\') {
353
+ this.advance();
354
+ const escaped = this.scanEscapeSequence(start);
355
+ if (escaped !== null) {
356
+ value += escaped;
357
+ }
358
+ }
359
+ else {
360
+ value += this.advance();
361
+ }
362
+ }
363
+ this.errors.push(new UnterminatedStringError(delimiter, createSpan(start, this.currentPosition())));
364
+ }
365
+ scanEscapeSequence(_stringStart) {
366
+ if (this.isAtEnd()) {
367
+ return null;
368
+ }
369
+ const escapeStart = this.currentPosition();
370
+ const c = this.advance();
371
+ switch (c) {
372
+ case '"': return '"';
373
+ case "'": return "'";
374
+ case '`': return '`';
375
+ case '\\': return '\\';
376
+ case 'n': return '\n';
377
+ case 'r': return '\r';
378
+ case 't': return '\t';
379
+ case 'b': return '\b';
380
+ case 'f': return '\f';
381
+ case 'u':
382
+ return this.scanUnicodeEscape(escapeStart);
383
+ default:
384
+ this.errors.push(new InvalidEscapeSequenceError(`\\${c}`, createSpan(escapeStart, this.currentPosition())));
385
+ return c;
386
+ }
387
+ }
388
+ scanUnicodeEscape(start) {
389
+ // Check for \u{XXXXX} (1-6 hex digits)
390
+ if (this.peek() === '{') {
391
+ this.advance();
392
+ let hex = '';
393
+ while (!this.isAtEnd() && this.peek() !== '}') {
394
+ if (isHexDigit(this.peek())) {
395
+ hex += this.advance();
396
+ }
397
+ else {
398
+ this.errors.push(new InvalidEscapeSequenceError(`\\u{${hex}${this.peek()}`, createSpan(start, this.currentPosition())));
399
+ return null;
400
+ }
401
+ }
402
+ if (this.isAtEnd() || this.peek() !== '}') {
403
+ this.errors.push(new InvalidEscapeSequenceError(`\\u{${hex}`, createSpan(start, this.currentPosition())));
404
+ return null;
405
+ }
406
+ this.advance(); // consume }
407
+ if (hex.length < 1 || hex.length > 6) {
408
+ this.errors.push(new InvalidEscapeSequenceError(`\\u{${hex}}`, createSpan(start, this.currentPosition())));
409
+ return null;
410
+ }
411
+ const codePoint = parseInt(hex, 16);
412
+ if (codePoint > 0x10FFFF) {
413
+ this.errors.push(new InvalidEscapeSequenceError(`\\u{${hex}}`, createSpan(start, this.currentPosition())));
414
+ return null;
415
+ }
416
+ return String.fromCodePoint(codePoint);
417
+ }
418
+ // Standard \uXXXX (4 hex digits)
419
+ let hex = '';
420
+ for (let i = 0; i < 4; i++) {
421
+ if (this.isAtEnd() || !isHexDigit(this.peek())) {
422
+ this.errors.push(new InvalidEscapeSequenceError(`\\u${hex}`, createSpan(start, this.currentPosition())));
423
+ return null;
424
+ }
425
+ hex += this.advance();
426
+ }
427
+ const codeUnit = parseInt(hex, 16);
428
+ if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF) {
429
+ if (this.peek() !== '\\') {
430
+ this.errors.push(new InvalidEscapeSequenceError(`\\u${hex}`, createSpan(start, this.currentPosition())));
431
+ return null;
432
+ }
433
+ const slashPos = this.currentPosition();
434
+ this.advance();
435
+ if (this.isAtEnd() || this.peek() !== 'u') {
436
+ this.errors.push(new InvalidEscapeSequenceError(`\\u${hex}`, createSpan(start, slashPos)));
437
+ return null;
438
+ }
439
+ this.advance();
440
+ let lowHex = '';
441
+ for (let i = 0; i < 4; i++) {
442
+ if (this.isAtEnd() || !isHexDigit(this.peek())) {
443
+ this.errors.push(new InvalidEscapeSequenceError(`\\u${hex}`, createSpan(start, this.currentPosition())));
444
+ return null;
445
+ }
446
+ lowHex += this.advance();
447
+ }
448
+ const lowCodeUnit = parseInt(lowHex, 16);
449
+ if (lowCodeUnit < 0xDC00 || lowCodeUnit > 0xDFFF) {
450
+ this.errors.push(new InvalidEscapeSequenceError(`\\u${hex}\\u${lowHex}`, createSpan(start, this.currentPosition())));
451
+ return null;
452
+ }
453
+ const codePoint = 0x10000 + ((codeUnit - 0xD800) << 10) + (lowCodeUnit - 0xDC00);
454
+ return String.fromCodePoint(codePoint);
455
+ }
456
+ if (codeUnit >= 0xDC00 && codeUnit <= 0xDFFF) {
457
+ this.errors.push(new InvalidEscapeSequenceError(`\\u${hex}`, createSpan(start, this.currentPosition())));
458
+ return null;
459
+ }
460
+ return String.fromCharCode(codeUnit);
461
+ }
462
+ scanNumber(first, start) {
463
+ let value = first;
464
+ let hasError = false;
465
+ const startsWithLeadingDot = value === '.' || value === '-.' || value === '+.';
466
+ // Helper to scan digits with underscores (only between digits)
467
+ // Returns false if underscore rules are violated
468
+ const scanDigitsWithUnderscores = (allowUnderscores) => {
469
+ let lastWasUnderscore = false;
470
+ let scannedAny = false;
471
+ while (isDigit(this.peek()) || this.peek() === '_') {
472
+ if (this.peek() === '_') {
473
+ if (!allowUnderscores) {
474
+ value += this.advance();
475
+ hasError = true;
476
+ continue;
477
+ }
478
+ // Check: must have a digit before (either in value or just scanned)
479
+ const lastChar = value[value.length - 1];
480
+ if (lastWasUnderscore || (lastChar !== undefined && !isDigit(lastChar))) {
481
+ // Consecutive underscores or underscore not after digit
482
+ value += this.advance(); // consume the bad underscore
483
+ hasError = true;
484
+ continue;
485
+ }
486
+ lastWasUnderscore = true;
487
+ }
488
+ else {
489
+ lastWasUnderscore = false;
490
+ }
491
+ value += this.advance();
492
+ scannedAny = true;
493
+ }
494
+ // Cannot end with underscore
495
+ if (lastWasUnderscore) {
496
+ hasError = true;
497
+ }
498
+ return scannedAny || !lastWasUnderscore;
499
+ };
500
+ // Integer part
501
+ scanDigitsWithUnderscores(true);
502
+ // Check for time literal (HH:MM:SS with optional fractional seconds / zone)
503
+ if (!hasError && !startsWithLeadingDot && first !== '+' && first !== '-' && this.peek() === ':' && !value.includes('_')) {
504
+ this.scanTime(value, start);
505
+ return;
506
+ }
507
+ // Check for date literal (YYYY-MM-DD)
508
+ if (!hasError && !startsWithLeadingDot && this.peek() === '-' && value.length === 4 && !value.includes('_')) {
509
+ this.scanDateOrDateTime(value, start);
510
+ return;
511
+ }
512
+ if (!hasError && !startsWithLeadingDot && this.peek() === '-' && isDigit(this.peekNext()) && !value.includes('_')) {
513
+ while (!this.isAtEnd()) {
514
+ const ch = this.peek();
515
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === ',' || ch === ']' || ch === ')' || ch === '}') {
516
+ break;
517
+ }
518
+ value += this.advance();
519
+ }
520
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
521
+ return;
522
+ }
523
+ // Fractional part - check for . followed by _ (invalid: 1._2)
524
+ // or . followed by digit (valid: 1.2)
525
+ if (!startsWithLeadingDot && this.peek() === '.') {
526
+ const nextChar = this.peekNext();
527
+ if (nextChar === '_') {
528
+ // Invalid: 1._2
529
+ value += this.advance(); // consume .
530
+ value += this.advance(); // consume _
531
+ hasError = true;
532
+ // Continue scanning to consume the rest
533
+ scanDigitsWithUnderscores(true);
534
+ }
535
+ else if (isDigit(nextChar)) {
536
+ value += this.advance(); // consume .
537
+ scanDigitsWithUnderscores(true);
538
+ }
539
+ // else: standalone . is not part of the number (e.g., 1.foo)
540
+ }
541
+ // Exponent
542
+ if (this.peek() === 'e' || this.peek() === 'E') {
543
+ value += this.advance();
544
+ if (this.peek() === '+' || this.peek() === '-') {
545
+ value += this.advance();
546
+ }
547
+ // After 'e', 'e+', or 'e-', next char must be digit, not underscore.
548
+ // Once the exponent starts, underscores are allowed between digits.
549
+ if (this.peek() === '_') {
550
+ value += this.advance(); // consume the bad underscore
551
+ hasError = true;
552
+ // Continue scanning to consume the rest
553
+ scanDigitsWithUnderscores(true);
554
+ }
555
+ else if (isDigit(this.peek())) {
556
+ scanDigitsWithUnderscores(true);
557
+ }
558
+ else {
559
+ // No digits after exponent - invalid
560
+ hasError = true;
561
+ }
562
+ }
563
+ if (hasError) {
564
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
565
+ return;
566
+ }
567
+ if (startsWithLeadingDot && !/\.\d/.test(value)) {
568
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
569
+ return;
570
+ }
571
+ // Validate: no leading zeros (except 0 itself, 0.xxx, or 0e...)
572
+ const normalized = value.replace(/_/g, '');
573
+ const normalizedBody = normalized[0] === '+' || normalized[0] === '-'
574
+ ? normalized.slice(1)
575
+ : normalized;
576
+ if (normalizedBody.length > 1
577
+ && normalizedBody[0] === '0'
578
+ && normalizedBody[1] !== '.'
579
+ && normalizedBody[1] !== 'e'
580
+ && normalizedBody[1] !== 'E') {
581
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
582
+ return;
583
+ }
584
+ this.addToken(TokenType.Number, value, start);
585
+ }
586
+ scanTime(hours, start) {
587
+ let value = hours;
588
+ while (isDigit(this.peek()) || this.peek() === ':' || this.peek() === '.') {
589
+ value += this.advance();
590
+ }
591
+ if (this.peek() === 'Z') {
592
+ value += this.advance();
593
+ }
594
+ else if (this.peek() === '+' || this.peek() === '-') {
595
+ value += this.advance();
596
+ while (isDigit(this.peek()) || this.peek() === ':') {
597
+ value += this.advance();
598
+ }
599
+ }
600
+ if (isValidTimeLiteral(value)) {
601
+ this.addToken(TokenType.Time, value, start);
602
+ return;
603
+ }
604
+ this.errors.push(new InvalidTimeError(value, createSpan(start, this.currentPosition())));
605
+ }
606
+ scanDateOrDateTime(year, start) {
607
+ let value = year;
608
+ // Consume -MM-DD
609
+ value += this.advance(); // -
610
+ for (let i = 0; i < 2 && isDigit(this.peek()); i++) {
611
+ value += this.advance();
612
+ }
613
+ if (this.peek() === '-') {
614
+ value += this.advance();
615
+ for (let i = 0; i < 2 && isDigit(this.peek()); i++) {
616
+ value += this.advance();
617
+ }
618
+ }
619
+ // Check for T (datetime)
620
+ if (this.peek() === 'T') {
621
+ value += this.advance();
622
+ // Time part
623
+ while (isDigit(this.peek()) || this.peek() === ':' || this.peek() === '.') {
624
+ value += this.advance();
625
+ }
626
+ // Timezone
627
+ if (this.peek() === 'Z') {
628
+ value += this.advance();
629
+ }
630
+ else if (this.peek() === '+' || this.peek() === '-') {
631
+ value += this.advance();
632
+ while (isDigit(this.peek()) || this.peek() === ':') {
633
+ value += this.advance();
634
+ }
635
+ }
636
+ // ZRUT zone (& followed by zone id)
637
+ if (this.peek() === '&') {
638
+ value += this.advance();
639
+ let zone = '';
640
+ while (isAlphanumeric(this.peek())
641
+ || this.peek() === '/'
642
+ || this.peek() === '_'
643
+ || this.peek() === '-'
644
+ || this.peek() === '+') {
645
+ const ch = this.advance();
646
+ value += ch;
647
+ zone += ch;
648
+ }
649
+ if (!isValidZrutZone(zone)) {
650
+ this.errors.push(new InvalidDateTimeError(value, createSpan(start, this.currentPosition())));
651
+ return;
652
+ }
653
+ }
654
+ if (isValidDateTimeLiteral(value)) {
655
+ this.addToken(TokenType.DateTime, value, start);
656
+ }
657
+ else {
658
+ this.errors.push(new InvalidDateTimeError(value, createSpan(start, this.currentPosition())));
659
+ }
660
+ }
661
+ else {
662
+ if (isValidDateLiteral(value)) {
663
+ this.addToken(TokenType.Date, value, start);
664
+ }
665
+ else {
666
+ this.errors.push(new InvalidDateError(value, createSpan(start, this.currentPosition())));
667
+ }
668
+ }
669
+ }
670
+ scanHexLiteral(start) {
671
+ let value = '#';
672
+ while (isHexDigit(this.peek()) || this.peek() === '_') {
673
+ value += this.advance();
674
+ }
675
+ if (value.length === 1) {
676
+ this.errors.push(new UnexpectedCharacterError('#', createSpan(start, this.currentPosition())));
677
+ return;
678
+ }
679
+ if (value.endsWith('_')) {
680
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
681
+ return;
682
+ }
683
+ if (!hasValidLiteralUnderscores(value)) {
684
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
685
+ return;
686
+ }
687
+ this.addToken(TokenType.HexLiteral, value, start);
688
+ }
689
+ scanRadixLiteral(start) {
690
+ let value = '%';
691
+ while (isRadixChar(this.peek())) {
692
+ value += this.advance();
693
+ }
694
+ if (value.length === 1) {
695
+ this.errors.push(new UnexpectedCharacterError('%', createSpan(start, this.currentPosition())));
696
+ return;
697
+ }
698
+ if (!isValidRadixPayload(value.slice(1))) {
699
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
700
+ return;
701
+ }
702
+ this.addToken(TokenType.RadixLiteral, value, start);
703
+ }
704
+ scanEncodingLiteral(start) {
705
+ let value = '$';
706
+ // Keep root-qualified paths (`$.a`) lexically distinct from encoding literals.
707
+ if (!isEncodingStartChar(this.peek())) {
708
+ this.addToken(TokenType.Dollar, value, start);
709
+ return;
710
+ }
711
+ while (!this.isAtEnd()) {
712
+ const c = this.peek();
713
+ if (isEncodingChar(c)) {
714
+ value += this.advance();
715
+ }
716
+ else {
717
+ break;
718
+ }
719
+ }
720
+ if (value.length === 1) {
721
+ this.errors.push(new UnexpectedCharacterError('$', createSpan(start, this.currentPosition())));
722
+ return;
723
+ }
724
+ if (!isValidEncodingPayload(value.slice(1))) {
725
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
726
+ return;
727
+ }
728
+ this.addToken(TokenType.EncodingLiteral, value, start);
729
+ }
730
+ scanSeparatorLiteral(start) {
731
+ let value = '^';
732
+ let sawPayload = false;
733
+ while (!this.isAtEnd()) {
734
+ const c = this.peek();
735
+ if (c === '"' || c === "'") {
736
+ const quote = this.advance();
737
+ value += quote;
738
+ sawPayload = true;
739
+ while (!this.isAtEnd()) {
740
+ const inner = this.peek();
741
+ if (inner === '\n' || inner === '\r') {
742
+ this.errors.push(new UnterminatedStringError(quote, createSpan(start, this.currentPosition())));
743
+ return;
744
+ }
745
+ if (inner === '\\') {
746
+ value += this.advance();
747
+ if (!this.isAtEnd()) {
748
+ value += this.advance();
749
+ }
750
+ continue;
751
+ }
752
+ value += this.advance();
753
+ if (inner === quote) {
754
+ break;
755
+ }
756
+ }
757
+ if (!value.endsWith(quote)) {
758
+ this.errors.push(new UnterminatedStringError(quote, createSpan(start, this.currentPosition())));
759
+ return;
760
+ }
761
+ continue;
762
+ }
763
+ if (!isSeparatorRawChar(c)) {
764
+ break;
765
+ }
766
+ value += this.advance();
767
+ sawPayload = true;
768
+ }
769
+ if (!sawPayload) {
770
+ this.addToken(TokenType.Caret, value, start);
771
+ return;
772
+ }
773
+ if (!isValidSeparatorPayload(value.slice(1))) {
774
+ this.errors.push(new InvalidNumberError(value, createSpan(start, this.currentPosition())));
775
+ return;
776
+ }
777
+ this.addToken(TokenType.SeparatorLiteral, value, start);
778
+ }
779
+ scanIdentifierOrKeyword(first, start) {
780
+ let value = first;
781
+ while (isAlphanumeric(this.peek())) {
782
+ value += this.advance();
783
+ }
784
+ const keywordType = KEYWORDS.get(value);
785
+ if (keywordType !== undefined) {
786
+ this.addToken(keywordType, value, start);
787
+ }
788
+ else {
789
+ this.addToken(TokenType.Identifier, value, start);
790
+ }
791
+ }
792
+ scanLineComment(start) {
793
+ let value = '//';
794
+ while (!this.isAtEnd() && this.peek() !== '\n') {
795
+ value += this.advance();
796
+ }
797
+ if (this.options.includeComments) {
798
+ this.addCommentToken(TokenType.LineComment, value, start, this.classifyLineComment(value, start));
799
+ }
800
+ }
801
+ scanShebangComment(start) {
802
+ let value = '#';
803
+ value += this.advance(); // !
804
+ while (!this.isAtEnd() && this.peek() !== '\n') {
805
+ value += this.advance();
806
+ }
807
+ this.sawLeadingShebang = true;
808
+ if (this.options.includeComments) {
809
+ this.addCommentToken(TokenType.LineComment, value, start, { channel: 'plain', form: 'line' });
810
+ }
811
+ }
812
+ scanBlockComment(start) {
813
+ let value = '/*';
814
+ while (!this.isAtEnd()) {
815
+ if (this.peek() === '*' && this.peekNext() === '/') {
816
+ value += this.advance(); // *
817
+ value += this.advance(); // /
818
+ if (this.options.includeComments) {
819
+ this.addCommentToken(TokenType.BlockComment, value, start, classifyComment(value, 'block'));
820
+ }
821
+ return;
822
+ }
823
+ value += this.advance();
824
+ }
825
+ // Unterminated block comment - emit error with span from start to EOF
826
+ this.errors.push(new UnterminatedBlockCommentError(createSpan(start, this.currentPosition())));
827
+ if (this.options.includeComments) {
828
+ this.addCommentToken(TokenType.BlockComment, value, start, classifyComment(value, 'block'));
829
+ }
830
+ }
831
+ scanSlashChannelBlockComment(start) {
832
+ const marker = this.advance();
833
+ const closingMarker = slashChannelClosingMarker(marker);
834
+ let value = `/${marker}`;
835
+ while (!this.isAtEnd()) {
836
+ if (this.peek() === closingMarker && this.peekNext() === '/') {
837
+ value += this.advance(); // closing marker
838
+ value += this.advance(); // /
839
+ if (this.options.includeComments) {
840
+ this.addCommentToken(TokenType.BlockComment, value, start, classifyComment(value, 'block'));
841
+ }
842
+ return;
843
+ }
844
+ value += this.advance();
845
+ }
846
+ this.errors.push(new UnterminatedBlockCommentError(createSpan(start, this.currentPosition())));
847
+ if (this.options.includeComments) {
848
+ this.addCommentToken(TokenType.BlockComment, value, start, classifyComment(value, 'block'));
849
+ }
850
+ }
851
+ addCommentToken(type, value, start, comment) {
852
+ const end = this.currentPosition();
853
+ this.tokens.push(createToken(type, value, createSpan(start, end), comment));
854
+ }
855
+ isLeadingShebangStart(start) {
856
+ return start.offset === 0 && start.line === 1 && start.column === 1;
857
+ }
858
+ isHostDirectiveSlot(start) {
859
+ if (start.column !== 1) {
860
+ return false;
861
+ }
862
+ if (start.line === 1) {
863
+ return true;
864
+ }
865
+ return start.line === 2 && this.sawLeadingShebang;
866
+ }
867
+ classifyLineComment(value, start) {
868
+ if (value.startsWith('//!') && !this.isHostDirectiveSlot(start)) {
869
+ return { channel: 'plain', form: 'line' };
870
+ }
871
+ return classifyComment(value, 'line');
872
+ }
873
+ }
874
+ function classifyComment(value, form) {
875
+ const marker = getStructuredMarker(value, form);
876
+ if (marker === null) {
877
+ return { channel: 'plain', form };
878
+ }
879
+ if (marker === '#') {
880
+ return { channel: 'doc', form };
881
+ }
882
+ if (marker === '@') {
883
+ return { channel: 'annotation', form };
884
+ }
885
+ if (marker === '?') {
886
+ return { channel: 'hint', form };
887
+ }
888
+ if (marker === '!') {
889
+ return { channel: 'host', form };
890
+ }
891
+ const subtype = reservedSubtypeFromMarker(marker);
892
+ if (subtype) {
893
+ return { channel: 'reserved', form, subtype };
894
+ }
895
+ return { channel: 'plain', form };
896
+ }
897
+ function getStructuredMarker(value, form) {
898
+ if (form === 'line') {
899
+ if (!value.startsWith('//') || value.length < 3) {
900
+ return null;
901
+ }
902
+ return value[2] ?? null;
903
+ }
904
+ if (value.length < 3 || value[0] !== '/') {
905
+ return null;
906
+ }
907
+ if (value[1] === '*') {
908
+ // All C-style block comments are plain in r6.
909
+ return null;
910
+ }
911
+ if (!isSlashChannelMarker(value[1] ?? '')) {
912
+ return null;
913
+ }
914
+ return value[1] ?? null;
915
+ }
916
+ function reservedSubtypeFromMarker(marker) {
917
+ if (marker === '{') {
918
+ return 'structure';
919
+ }
920
+ if (marker === '[') {
921
+ return 'profile';
922
+ }
923
+ if (marker === '(') {
924
+ return 'instructions';
925
+ }
926
+ return null;
927
+ }
928
+ function isValidDateLiteral(value) {
929
+ if (value.length !== 10
930
+ || value[4] !== '-'
931
+ || value[7] !== '-'
932
+ || !/^\d{4}$/.test(value.slice(0, 4))
933
+ || !/^\d{2}$/.test(value.slice(5, 7))
934
+ || !/^\d{2}$/.test(value.slice(8, 10))) {
935
+ return false;
936
+ }
937
+ const year = Number.parseInt(value.slice(0, 4), 10);
938
+ const month = Number.parseInt(value.slice(5, 7), 10);
939
+ const day = Number.parseInt(value.slice(8, 10), 10);
940
+ return isValidDateParts(year, month, day);
941
+ }
942
+ function isValidTimeLiteral(value) {
943
+ return matchesTimeCore(value, true) || matchesZonedTime(value);
944
+ }
945
+ function isValidDateTimeLiteral(value) {
946
+ const tIndex = value.indexOf('T');
947
+ if (tIndex === -1)
948
+ return false;
949
+ const date = value.slice(0, tIndex);
950
+ const rest = value.slice(tIndex + 1);
951
+ if (!isValidDateLiteral(date))
952
+ return false;
953
+ if (matchesDateTimeTime(rest) || matchesDateTimeZonedTime(rest))
954
+ return true;
955
+ const ampIndex = rest.indexOf('&');
956
+ if (ampIndex === -1)
957
+ return false;
958
+ const base = rest.slice(0, ampIndex);
959
+ const zone = rest.slice(ampIndex + 1);
960
+ return zone.length > 0 && isValidZrutZone(zone) && (matchesDateTimeTime(base) || matchesDateTimeZonedTime(base));
961
+ }
962
+ function matchesTimeCore(value, allowHourPrecisionMarker) {
963
+ if (value.length === 3) {
964
+ return allowHourPrecisionMarker
965
+ && value[2] === ':'
966
+ && /^\d{2}$/.test(value.slice(0, 2))
967
+ && isValidHour(Number.parseInt(value.slice(0, 2), 10));
968
+ }
969
+ if (value.length === 5) {
970
+ return value[2] === ':'
971
+ && /^\d{2}$/.test(value.slice(0, 2))
972
+ && /^\d{2}$/.test(value.slice(3, 5))
973
+ && isValidHour(Number.parseInt(value.slice(0, 2), 10))
974
+ && isValidMinuteOrSecond(Number.parseInt(value.slice(3, 5), 10));
975
+ }
976
+ return matchesHms(value);
977
+ }
978
+ function matchesDateTimeCore(value) {
979
+ if (value.length === 2) {
980
+ return /^\d{2}$/.test(value);
981
+ }
982
+ return matchesTimeCore(value, false);
983
+ }
984
+ function matchesDateTimeTime(value) {
985
+ return matchesDateTimeCore(value) || matchesTimeCore(value, true);
986
+ }
987
+ function matchesHms(value) {
988
+ return value.length === 8
989
+ && value[2] === ':'
990
+ && value[5] === ':'
991
+ && /^\d{2}$/.test(value.slice(0, 2))
992
+ && /^\d{2}$/.test(value.slice(3, 5))
993
+ && /^\d{2}$/.test(value.slice(6, 8))
994
+ && isValidHour(Number.parseInt(value.slice(0, 2), 10))
995
+ && isValidMinuteOrSecond(Number.parseInt(value.slice(3, 5), 10))
996
+ && isValidMinuteOrSecond(Number.parseInt(value.slice(6, 8), 10));
997
+ }
998
+ function matchesZonedTime(value) {
999
+ if (matchesTimeCore(value, true))
1000
+ return true;
1001
+ if (value.endsWith('Z')) {
1002
+ return matchesTimeCore(value.slice(0, -1), true);
1003
+ }
1004
+ const plusIndex = value.lastIndexOf('+');
1005
+ const minusIndex = value.lastIndexOf('-');
1006
+ const splitIndex = Math.max(plusIndex, minusIndex);
1007
+ if (splitIndex === -1)
1008
+ return false;
1009
+ const base = value.slice(0, splitIndex);
1010
+ const offset = value.slice(splitIndex + 1);
1011
+ return matchesTimeCore(base, true) && matchesOffset(offset);
1012
+ }
1013
+ function matchesDateTimeZonedTime(value) {
1014
+ if (value.endsWith('Z')) {
1015
+ return matchesDateTimeTime(value.slice(0, -1));
1016
+ }
1017
+ const plusIndex = value.lastIndexOf('+');
1018
+ const minusIndex = value.lastIndexOf('-');
1019
+ const splitIndex = Math.max(plusIndex, minusIndex);
1020
+ if (splitIndex === -1)
1021
+ return false;
1022
+ const base = value.slice(0, splitIndex);
1023
+ const offset = value.slice(splitIndex + 1);
1024
+ return matchesDateTimeTime(base) && matchesOffset(offset);
1025
+ }
1026
+ function matchesOffset(value) {
1027
+ return value.length === 5
1028
+ && value[2] === ':'
1029
+ && /^\d{2}$/.test(value.slice(0, 2))
1030
+ && /^\d{2}$/.test(value.slice(3, 5))
1031
+ && isValidHour(Number.parseInt(value.slice(0, 2), 10))
1032
+ && isValidMinuteOrSecond(Number.parseInt(value.slice(3, 5), 10));
1033
+ }
1034
+ function isValidDateParts(year, month, day) {
1035
+ if (month < 1 || month > 12 || day < 1) {
1036
+ return false;
1037
+ }
1038
+ const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1039
+ return day <= daysInMonth[month - 1];
1040
+ }
1041
+ function isLeapYear(year) {
1042
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
1043
+ }
1044
+ function isValidHour(value) {
1045
+ return value >= 0 && value <= 23;
1046
+ }
1047
+ function isValidMinuteOrSecond(value) {
1048
+ return value >= 0 && value <= 59;
1049
+ }
1050
+ function isValidZrutZone(zone) {
1051
+ if (zone.length === 0)
1052
+ return false;
1053
+ if (zone.startsWith('/'))
1054
+ return false;
1055
+ if (zone.endsWith('/'))
1056
+ return false;
1057
+ if (zone.includes('//'))
1058
+ return false;
1059
+ if (zone.includes('/*'))
1060
+ return false;
1061
+ if (zone.includes('/['))
1062
+ return false;
1063
+ return true;
1064
+ }
1065
+ function isValidSeparatorPayload(payload) {
1066
+ if (payload.length === 0)
1067
+ return false;
1068
+ let index = 0;
1069
+ while (index < payload.length) {
1070
+ const c = payload[index];
1071
+ if (c === '"' || c === "'") {
1072
+ const quote = c;
1073
+ index += 1;
1074
+ while (index < payload.length) {
1075
+ const inner = payload[index];
1076
+ if (inner === '\n') {
1077
+ return false;
1078
+ }
1079
+ if (inner === '\\') {
1080
+ index += 2;
1081
+ continue;
1082
+ }
1083
+ index += 1;
1084
+ if (inner === quote) {
1085
+ break;
1086
+ }
1087
+ }
1088
+ if (index > payload.length || payload[index - 1] !== quote) {
1089
+ return false;
1090
+ }
1091
+ continue;
1092
+ }
1093
+ if (!isSeparatorRawChar(c)) {
1094
+ return false;
1095
+ }
1096
+ index += 1;
1097
+ }
1098
+ return true;
1099
+ }
1100
+ function isValidRadixDigit(c) {
1101
+ return isLetter(c) || isDigit(c) || c === '&' || c === '!';
1102
+ }
1103
+ function isValidRadixPayload(payload) {
1104
+ if (payload.length === 0)
1105
+ return false;
1106
+ let index = 0;
1107
+ if (payload[index] === '+' || payload[index] === '-') {
1108
+ index += 1;
1109
+ }
1110
+ if (index >= payload.length)
1111
+ return false;
1112
+ let sawDigit = false;
1113
+ let sawDecimal = false;
1114
+ let prevWasDigit = false;
1115
+ let prevWasUnderscore = false;
1116
+ for (; index < payload.length; index += 1) {
1117
+ const c = payload[index];
1118
+ if (isValidRadixDigit(c)) {
1119
+ sawDigit = true;
1120
+ prevWasDigit = true;
1121
+ prevWasUnderscore = false;
1122
+ continue;
1123
+ }
1124
+ if (c === '_') {
1125
+ if (!prevWasDigit || index + 1 >= payload.length || !isValidRadixDigit(payload[index + 1])) {
1126
+ return false;
1127
+ }
1128
+ prevWasDigit = false;
1129
+ prevWasUnderscore = true;
1130
+ continue;
1131
+ }
1132
+ if (c === '.') {
1133
+ if (sawDecimal || index + 1 >= payload.length || !isValidRadixDigit(payload[index + 1])) {
1134
+ return false;
1135
+ }
1136
+ sawDecimal = true;
1137
+ prevWasDigit = false;
1138
+ prevWasUnderscore = false;
1139
+ continue;
1140
+ }
1141
+ return false;
1142
+ }
1143
+ return sawDigit && !prevWasUnderscore && prevWasDigit;
1144
+ }
1145
+ function isValidEncodingPayload(payload) {
1146
+ if (payload.length === 0)
1147
+ return false;
1148
+ if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(payload))
1149
+ return false;
1150
+ const firstPadding = payload.indexOf('=');
1151
+ if (firstPadding === -1)
1152
+ return true;
1153
+ return payload.slice(firstPadding).split('').every((c) => c === '=');
1154
+ }
1155
+ function hasValidLiteralUnderscores(raw) {
1156
+ const body = raw.slice(1);
1157
+ return body.length > 0 && !body.startsWith('_') && !body.endsWith('_') && !body.includes('__');
1158
+ }
1159
+ /**
1160
+ * Tokenize an AEON document
1161
+ */
1162
+ export function tokenize(input, options) {
1163
+ const lexer = new Lexer(input, options);
1164
+ return lexer.tokenize();
1165
+ }
1166
+ //# sourceMappingURL=lexer.js.map