xcdn 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/src/index.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * xCDN - JavaScript Implementation
3
+ *
4
+ * A parser and serializer for the xCDN data format.
5
+ */
6
+
7
+ // Re-export AST types
8
+ export * from './ast.js';
9
+
10
+ // Re-export error types
11
+ export * from './error.js';
12
+
13
+ // Re-export lexer
14
+ export * from './lexer.js';
15
+
16
+ // Re-export parser
17
+ export { Parser, parseStr, parseReader } from './parser.js';
18
+
19
+ // Re-export serializer
20
+ export {
21
+ Format,
22
+ Serializer,
23
+ toStringPretty,
24
+ toStringCompact,
25
+ toStringWithFormat
26
+ } from './serializer.js';
27
+
28
+ // Default export for convenience
29
+ import { parseStr } from './parser.js';
30
+ import { toStringPretty, toStringCompact } from './serializer.js';
31
+
32
+ export default {
33
+ parse: parseStr,
34
+ stringify: toStringPretty,
35
+ stringifyCompact: toStringCompact
36
+ };
package/src/lexer.js ADDED
@@ -0,0 +1,475 @@
1
+ /**
2
+ * xCDN Lexer Module
3
+ * Tokenization for the xCDN format
4
+ */
5
+
6
+ import { Span, XCDNError, ErrorKind } from './error.js';
7
+
8
+ /**
9
+ * Token types
10
+ */
11
+ export const TokenType = {
12
+ // Structure
13
+ LBRACE: 'LBRACE', // {
14
+ RBRACE: 'RBRACE', // }
15
+ LBRACKET: 'LBRACKET', // [
16
+ RBRACKET: 'RBRACKET', // ]
17
+ LPAREN: 'LPAREN', // (
18
+ RPAREN: 'RPAREN', // )
19
+ COLON: 'COLON', // :
20
+ COMMA: 'COMMA', // ,
21
+ DOLLAR: 'DOLLAR', // $
22
+ HASH: 'HASH', // #
23
+ AT: 'AT', // @
24
+
25
+ // Keywords
26
+ TRUE: 'TRUE',
27
+ FALSE: 'FALSE',
28
+ NULL: 'NULL',
29
+
30
+ // Values
31
+ IDENT: 'IDENT', // Unquoted identifiers
32
+ INT: 'INT', // Integer numbers
33
+ FLOAT: 'FLOAT', // Floating point numbers
34
+ STRING: 'STRING', // Strings "..."
35
+ TRIPLE_STRING: 'TRIPLE_STRING', // Strings """..."""
36
+
37
+ // Typed strings
38
+ D_QUOTED: 'D_QUOTED', // d"decimal"
39
+ B_QUOTED: 'B_QUOTED', // b"base64"
40
+ U_QUOTED: 'U_QUOTED', // u"uuid"
41
+ T_QUOTED: 'T_QUOTED', // t"datetime"
42
+ R_QUOTED: 'R_QUOTED', // r"duration"
43
+
44
+ // End of input
45
+ EOF: 'EOF',
46
+ };
47
+
48
+ /**
49
+ * Single token
50
+ */
51
+ export class Token {
52
+ /**
53
+ * @param {string} kind - Token type (from TokenType)
54
+ * @param {Span} span - Position in source
55
+ * @param {*} value - Extracted value (optional)
56
+ */
57
+ constructor(kind, span, value = null) {
58
+ this.kind = kind;
59
+ this.span = span;
60
+ this.value = value;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Lexer for xCDN
66
+ */
67
+ export class Lexer {
68
+ /**
69
+ * @param {string} source - Source code
70
+ */
71
+ constructor(source) {
72
+ this.source = source;
73
+ this.pos = 0;
74
+ this.line = 1;
75
+ this.column = 1;
76
+ }
77
+
78
+ /**
79
+ * Creates a Span for the current position
80
+ * @returns {Span}
81
+ */
82
+ span() {
83
+ return new Span(this.pos, this.line, this.column);
84
+ }
85
+
86
+ /**
87
+ * Reads the next character without consuming it
88
+ * @returns {string|null}
89
+ */
90
+ peek() {
91
+ if (this.pos >= this.source.length) {
92
+ return null;
93
+ }
94
+ return this.source[this.pos];
95
+ }
96
+
97
+ /**
98
+ * Looks at the second character ahead without consuming
99
+ * @returns {string|null}
100
+ */
101
+ peekNext() {
102
+ if (this.pos + 1 >= this.source.length) {
103
+ return null;
104
+ }
105
+ return this.source[this.pos + 1];
106
+ }
107
+
108
+ /**
109
+ * Consumes and returns the next character
110
+ * @returns {string|null}
111
+ */
112
+ bump() {
113
+ if (this.pos >= this.source.length) {
114
+ return null;
115
+ }
116
+ const ch = this.source[this.pos];
117
+ this.pos++;
118
+ if (ch === '\n') {
119
+ this.line++;
120
+ this.column = 1;
121
+ } else {
122
+ this.column++;
123
+ }
124
+ return ch;
125
+ }
126
+
127
+ /**
128
+ * Skips whitespace and comments
129
+ */
130
+ skipWsAndComments() {
131
+ while (true) {
132
+ const ch = this.peek();
133
+ if (ch === null) break;
134
+
135
+ // Whitespace
136
+ if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') {
137
+ this.bump();
138
+ continue;
139
+ }
140
+
141
+ // Comments
142
+ if (ch === '/') {
143
+ const next = this.peekNext();
144
+ if (next === '/') {
145
+ // Line comment
146
+ this.bump(); // /
147
+ this.bump(); // /
148
+ while (this.peek() !== null && this.peek() !== '\n') {
149
+ this.bump();
150
+ }
151
+ continue;
152
+ } else if (next === '*') {
153
+ // Block comment
154
+ this.bump(); // /
155
+ this.bump(); // *
156
+ while (true) {
157
+ const c = this.bump();
158
+ if (c === null) {
159
+ throw new XCDNError(ErrorKind.Eof, this.span(), 'Unterminated block comment');
160
+ }
161
+ if (c === '*' && this.peek() === '/') {
162
+ this.bump(); // /
163
+ break;
164
+ }
165
+ }
166
+ continue;
167
+ }
168
+ }
169
+
170
+ break;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Reads an identifier or keyword
176
+ * @returns {Token}
177
+ */
178
+ readIdent() {
179
+ const startSpan = this.span();
180
+ let value = '';
181
+
182
+ while (true) {
183
+ const ch = this.peek();
184
+ if (ch === null) break;
185
+ if (this.isIdentContinue(ch)) {
186
+ value += this.bump();
187
+ } else {
188
+ break;
189
+ }
190
+ }
191
+
192
+ // Check keywords
193
+ if (value === 'true') {
194
+ return new Token(TokenType.TRUE, startSpan, true);
195
+ } else if (value === 'false') {
196
+ return new Token(TokenType.FALSE, startSpan, false);
197
+ } else if (value === 'null') {
198
+ return new Token(TokenType.NULL, startSpan, null);
199
+ }
200
+
201
+ return new Token(TokenType.IDENT, startSpan, value);
202
+ }
203
+
204
+ /**
205
+ * Checks if the character can start an identifier
206
+ * @param {string} ch
207
+ * @returns {boolean}
208
+ */
209
+ isIdentStart(ch) {
210
+ return (ch >= 'a' && ch <= 'z') ||
211
+ (ch >= 'A' && ch <= 'Z') ||
212
+ ch === '_';
213
+ }
214
+
215
+ /**
216
+ * Checks if the character can continue an identifier
217
+ * @param {string} ch
218
+ * @returns {boolean}
219
+ */
220
+ isIdentContinue(ch) {
221
+ return this.isIdentStart(ch) ||
222
+ (ch >= '0' && ch <= '9') ||
223
+ ch === '-';
224
+ }
225
+
226
+ /**
227
+ * Reads a number (integer or float)
228
+ * @returns {Token}
229
+ */
230
+ readNumber() {
231
+ const startSpan = this.span();
232
+ let value = '';
233
+ let isFloat = false;
234
+
235
+ // Optional sign
236
+ if (this.peek() === '+' || this.peek() === '-') {
237
+ value += this.bump();
238
+ }
239
+
240
+ // Integer part
241
+ while (this.peek() !== null && this.peek() >= '0' && this.peek() <= '9') {
242
+ value += this.bump();
243
+ }
244
+
245
+ // Decimal part
246
+ if (this.peek() === '.' && this.peekNext() >= '0' && this.peekNext() <= '9') {
247
+ isFloat = true;
248
+ value += this.bump(); // .
249
+ while (this.peek() !== null && this.peek() >= '0' && this.peek() <= '9') {
250
+ value += this.bump();
251
+ }
252
+ }
253
+
254
+ // Exponent
255
+ if (this.peek() === 'e' || this.peek() === 'E') {
256
+ isFloat = true;
257
+ value += this.bump(); // e/E
258
+ if (this.peek() === '+' || this.peek() === '-') {
259
+ value += this.bump();
260
+ }
261
+ while (this.peek() !== null && this.peek() >= '0' && this.peek() <= '9') {
262
+ value += this.bump();
263
+ }
264
+ }
265
+
266
+ if (isFloat) {
267
+ const floatVal = parseFloat(value);
268
+ if (isNaN(floatVal)) {
269
+ throw new XCDNError(ErrorKind.InvalidNumber, startSpan, value);
270
+ }
271
+ return new Token(TokenType.FLOAT, startSpan, floatVal);
272
+ } else {
273
+ // Try BigInt for large numbers
274
+ try {
275
+ const intVal = BigInt(value);
276
+ return new Token(TokenType.INT, startSpan, intVal);
277
+ } catch {
278
+ throw new XCDNError(ErrorKind.InvalidNumber, startSpan, value);
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Reads a normal string "..."
285
+ * @returns {string}
286
+ */
287
+ readStringContent() {
288
+ let value = '';
289
+
290
+ while (true) {
291
+ const ch = this.bump();
292
+ if (ch === null) {
293
+ throw new XCDNError(ErrorKind.Eof, this.span(), 'Unterminated string');
294
+ }
295
+ if (ch === '"') {
296
+ break;
297
+ }
298
+ if (ch === '\\') {
299
+ const escaped = this.bump();
300
+ if (escaped === null) {
301
+ throw new XCDNError(ErrorKind.Eof, this.span(), 'Unterminated escape sequence');
302
+ }
303
+ switch (escaped) {
304
+ case '"': value += '"'; break;
305
+ case '\\': value += '\\'; break;
306
+ case '/': value += '/'; break;
307
+ case 'b': value += '\b'; break;
308
+ case 'f': value += '\f'; break;
309
+ case 'n': value += '\n'; break;
310
+ case 'r': value += '\r'; break;
311
+ case 't': value += '\t'; break;
312
+ case 'u': {
313
+ // Unicode escape \uXXXX
314
+ let hex = '';
315
+ for (let i = 0; i < 4; i++) {
316
+ const h = this.bump();
317
+ if (h === null || !this.isHexDigit(h)) {
318
+ throw new XCDNError(ErrorKind.InvalidEscape, this.span(), '\\u requires 4 hex digits');
319
+ }
320
+ hex += h;
321
+ }
322
+ value += String.fromCharCode(parseInt(hex, 16));
323
+ break;
324
+ }
325
+ default:
326
+ throw new XCDNError(ErrorKind.InvalidEscape, this.span(), `Unknown escape: \\${escaped}`);
327
+ }
328
+ } else {
329
+ value += ch;
330
+ }
331
+ }
332
+
333
+ return value;
334
+ }
335
+
336
+ /**
337
+ * Checks if the character is a hexadecimal digit
338
+ * @param {string} ch
339
+ * @returns {boolean}
340
+ */
341
+ isHexDigit(ch) {
342
+ return (ch >= '0' && ch <= '9') ||
343
+ (ch >= 'a' && ch <= 'f') ||
344
+ (ch >= 'A' && ch <= 'F');
345
+ }
346
+
347
+ /**
348
+ * Reads a triple string """..."""
349
+ * @returns {string}
350
+ */
351
+ readTripleString() {
352
+ let value = '';
353
+
354
+ while (true) {
355
+ const ch = this.bump();
356
+ if (ch === null) {
357
+ throw new XCDNError(ErrorKind.Eof, this.span(), 'Unterminated triple-quoted string');
358
+ }
359
+ if (ch === '"' && this.peek() === '"' && this.peekNext() === '"') {
360
+ this.bump(); // "
361
+ this.bump(); // "
362
+ break;
363
+ }
364
+ value += ch;
365
+ }
366
+
367
+ return value;
368
+ }
369
+
370
+ /**
371
+ * Reads the next token
372
+ * @returns {Token}
373
+ */
374
+ nextToken() {
375
+ this.skipWsAndComments();
376
+
377
+ const startSpan = this.span();
378
+ const ch = this.peek();
379
+
380
+ if (ch === null) {
381
+ return new Token(TokenType.EOF, startSpan);
382
+ }
383
+
384
+ // Single structural characters
385
+ switch (ch) {
386
+ case '{':
387
+ this.bump();
388
+ return new Token(TokenType.LBRACE, startSpan);
389
+ case '}':
390
+ this.bump();
391
+ return new Token(TokenType.RBRACE, startSpan);
392
+ case '[':
393
+ this.bump();
394
+ return new Token(TokenType.LBRACKET, startSpan);
395
+ case ']':
396
+ this.bump();
397
+ return new Token(TokenType.RBRACKET, startSpan);
398
+ case '(':
399
+ this.bump();
400
+ return new Token(TokenType.LPAREN, startSpan);
401
+ case ')':
402
+ this.bump();
403
+ return new Token(TokenType.RPAREN, startSpan);
404
+ case ':':
405
+ this.bump();
406
+ return new Token(TokenType.COLON, startSpan);
407
+ case ',':
408
+ this.bump();
409
+ return new Token(TokenType.COMMA, startSpan);
410
+ case '$':
411
+ this.bump();
412
+ return new Token(TokenType.DOLLAR, startSpan);
413
+ case '#':
414
+ this.bump();
415
+ return new Token(TokenType.HASH, startSpan);
416
+ case '@':
417
+ this.bump();
418
+ return new Token(TokenType.AT, startSpan);
419
+ }
420
+
421
+ // Strings
422
+ if (ch === '"') {
423
+ this.bump(); // opening "
424
+
425
+ // Check for triple-quoted string
426
+ if (this.peek() === '"' && this.peekNext() === '"') {
427
+ this.bump(); // "
428
+ this.bump(); // "
429
+ const value = this.readTripleString();
430
+ return new Token(TokenType.TRIPLE_STRING, startSpan, value);
431
+ }
432
+
433
+ const value = this.readStringContent();
434
+ return new Token(TokenType.STRING, startSpan, value);
435
+ }
436
+
437
+ // Typed strings (d", b", u", t", r")
438
+ if ((ch === 'd' || ch === 'b' || ch === 'u' || ch === 't' || ch === 'r') &&
439
+ this.peekNext() === '"') {
440
+ const prefix = this.bump();
441
+ this.bump(); // "
442
+ const value = this.readStringContent();
443
+
444
+ switch (prefix) {
445
+ case 'd': return new Token(TokenType.D_QUOTED, startSpan, value);
446
+ case 'b': return new Token(TokenType.B_QUOTED, startSpan, value);
447
+ case 'u': return new Token(TokenType.U_QUOTED, startSpan, value);
448
+ case 't': return new Token(TokenType.T_QUOTED, startSpan, value);
449
+ case 'r': return new Token(TokenType.R_QUOTED, startSpan, value);
450
+ }
451
+ }
452
+
453
+ // Identifiers
454
+ if (this.isIdentStart(ch)) {
455
+ return this.readIdent();
456
+ }
457
+
458
+ // Numbers
459
+ if ((ch >= '0' && ch <= '9') ||
460
+ ch === '.' ||
461
+ ch === '+' ||
462
+ ch === '-') {
463
+ // Verify it's a valid number
464
+ if (ch === '.' && !(this.peekNext() >= '0' && this.peekNext() <= '9')) {
465
+ throw new XCDNError(ErrorKind.InvalidToken, startSpan, ch);
466
+ }
467
+ if ((ch === '+' || ch === '-') && !(this.peekNext() >= '0' && this.peekNext() <= '9') && this.peekNext() !== '.') {
468
+ throw new XCDNError(ErrorKind.InvalidToken, startSpan, ch);
469
+ }
470
+ return this.readNumber();
471
+ }
472
+
473
+ throw new XCDNError(ErrorKind.InvalidToken, startSpan, ch);
474
+ }
475
+ }