@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/README.md +24 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +91 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +72 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +1166 -0
- package/dist/lexer.js.map +1 -0
- package/dist/tokens.d.ts +100 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +77 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +26 -0
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
|