naidejs 1.1.0 → 1.3.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/parser.js CHANGED
@@ -14,14 +14,11 @@ export class Parser {
14
14
  }
15
15
 
16
16
  preprocessTokens(tokens) {
17
- // Remove INDENT/DEDENT pairs around pipe continuation lines
18
- // Pattern: NEWLINE INDENT PIPE -> NEWLINE PIPE (and remove matching DEDENT)
19
17
  const result = [...tokens];
20
18
  const indentsToRemove = new Set();
21
19
 
22
20
  for (let i = 0; i < result.length; i++) {
23
21
  if (result[i].type !== T.INDENT) continue;
24
- // Look forward past any newlines for PIPE
25
22
  let j = i + 1;
26
23
  while (j < result.length && result[j].type === T.NEWLINE) j++;
27
24
  if (j < result.length && result[j].type === T.PIPE) {
@@ -31,7 +28,6 @@ export class Parser {
31
28
 
32
29
  if (indentsToRemove.size === 0) return result;
33
30
 
34
- // For each INDENT to remove, find matching DEDENT
35
31
  const dedentsToRemove = new Set();
36
32
  for (const idx of indentsToRemove) {
37
33
  let depth = 0;
@@ -65,7 +61,16 @@ export class Parser {
65
61
  expect(type) {
66
62
  const tok = this.advance();
67
63
  if (tok.type !== type) {
68
- throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')`, tok);
64
+ const hints = {
65
+ COLON: "Missing ':' — blocks (if, fn, server, etc.) need a colon at the end",
66
+ INDENT: "Expected an indented block — check your indentation (use 2 spaces)",
67
+ RPAREN: "Missing closing ')'",
68
+ RBRACKET: "Missing closing ']'",
69
+ RBRACE: "Missing closing '}'",
70
+ IDENT: "Expected an identifier (variable/function name)",
71
+ };
72
+ const hint = hints[type] ? `\n Hint: ${hints[type]}` : '';
73
+ throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')${hint}`, tok);
69
74
  }
70
75
  return tok;
71
76
  }
@@ -85,16 +90,24 @@ export class Parser {
85
90
  return types.includes(this.peek().type);
86
91
  }
87
92
 
93
+ isIdentLike(type) {
94
+ return type === T.IDENT || TYPE_TOKENS.has(type) ||
95
+ type === T.LOG || type === T.DB || type === T.GET ||
96
+ type === T.POST || type === T.PUT || type === T.DEL ||
97
+ type === T.MATCH || type === T.ON || type === T.NEW ||
98
+ type === T.FROM || type === T.AS || type === T.SELF ||
99
+ type === T.SCHEMA || type === T.CRUD || type === T.AUTH ||
100
+ type === T.CORS || type === T.LIMIT || type === T.ENV ||
101
+ type === T.EVERY || type === T.WATCH || type === T.STATIC ||
102
+ type === T.WS || type === T.GROUP || type === T.COOKIE ||
103
+ type === T.NOT || type === T.AND || type === T.OR ||
104
+ type === T.IN || type === T.BREAK || type === T.CONTINUE ||
105
+ type === T.THROW || type === T.MUT || type === T.PUB;
106
+ }
107
+
88
108
  expectPropertyName() {
89
109
  const tok = this.advance();
90
- if (tok.type === T.IDENT || TYPE_TOKENS.has(tok.type) ||
91
- tok.type === T.LOG || tok.type === T.DB || tok.type === T.GET ||
92
- tok.type === T.POST || tok.type === T.PUT || tok.type === T.DEL ||
93
- tok.type === T.MATCH || tok.type === T.ON || tok.type === T.NEW ||
94
- tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF ||
95
- tok.type === T.SCHEMA || tok.type === T.CRUD || tok.type === T.AUTH ||
96
- tok.type === T.CORS || tok.type === T.LIMIT || tok.type === T.ENV ||
97
- tok.type === T.EVERY || tok.type === T.WATCH) {
110
+ if (this.isIdentLike(tok.type)) {
98
111
  return tok.value;
99
112
  }
100
113
  throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
@@ -148,7 +161,9 @@ export class Parser {
148
161
  case T.BREAK: this.advance(); return new ASTNode('Break');
149
162
  case T.CONTINUE: this.advance(); return new ASTNode('Continue');
150
163
  case T.MUT: return this.parseMutVariable();
151
- case T.DB: return this.parseDbStatement();
164
+ case T.DB:
165
+ if (this.peek(1).type === T.DOT) return this.parseDbStatement();
166
+ return this.parseDbDir();
152
167
  case T.AWAIT: return this.parseAwaitStatement();
153
168
  case T.AWAIT_ALL: return this.parseAwaitAll();
154
169
  case T.SCHEMA: return this.parseSchema();
@@ -173,6 +188,19 @@ export class Parser {
173
188
  case T.WATCH:
174
189
  if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
175
190
  return this.parseWatch();
191
+ case T.STATIC:
192
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
193
+ return this.parseStatic();
194
+ case T.WS:
195
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
196
+ return this.parseWs();
197
+ case T.GROUP:
198
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
199
+ return this.parseGroup();
200
+ case T.COOKIE:
201
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
202
+ this.advance();
203
+ return new ASTNode('CookieDecl');
176
204
  default:
177
205
  if (TYPE_TOKENS.has(tok.type)) {
178
206
  return this.parseTypedVariable();
@@ -181,14 +209,10 @@ export class Parser {
181
209
  }
182
210
  }
183
211
 
184
- // use express
185
- // use express from "express"
186
- // use {readFile, writeFile} from "fs/promises"
187
212
  parseUse() {
188
213
  this.advance(); // use
189
214
 
190
215
  if (this.at(T.LBRACE)) {
191
- // use {a, b} from "module"
192
216
  this.advance();
193
217
  const names = [];
194
218
  while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
@@ -226,8 +250,6 @@ export class Parser {
226
250
  return tok.value;
227
251
  }
228
252
 
229
- // fn name(params) -> returnType:
230
- // body
231
253
  parseFunction(isAsync, isPublic) {
232
254
  this.advance(); // fn or fn.async
233
255
  const name = this.expect(T.IDENT).value;
@@ -300,7 +322,6 @@ export class Parser {
300
322
  if (this.at(T.FN_ASYNC)) return this.parseFunction(true, true);
301
323
  if (this.at(T.MODEL)) return this.parseModel(true);
302
324
 
303
- // pub variable
304
325
  if (TYPE_TOKENS.has(this.peek().type)) {
305
326
  const node = this.parseTypedVariable();
306
327
  node.isPublic = true;
@@ -310,24 +331,25 @@ export class Parser {
310
331
  throw this.error('Expected fn, model, or type after pub');
311
332
  }
312
333
 
313
- // ret expression
314
334
  parseReturn() {
315
335
  this.advance(); // ret
316
336
  if (this.at(T.NEWLINE) || this.at(T.EOF) || this.at(T.DEDENT)) {
317
337
  return new ASTNode('Return', { value: null });
318
338
  }
319
- // ret.status 200 {...}
320
339
  if (this.match(T.DOT)) {
321
340
  const method = this.expect(T.IDENT).value;
322
- const statusCode = this.parseExpression();
323
- const body = this.parseExpression();
324
- return new ASTNode('ReturnStatus', { method, statusCode, body });
341
+ if (method === 'status') {
342
+ const statusCode = this.parseExpression();
343
+ const body = this.parseExpression();
344
+ return new ASTNode('ReturnStatus', { method, statusCode, body });
345
+ }
346
+ const value = this.parseExpression();
347
+ return new ASTNode('ReturnMethod', { method, value });
325
348
  }
326
349
  const value = this.parseExpression();
327
350
  return new ASTNode('Return', { value });
328
351
  }
329
352
 
330
- // str name = "hello"
331
353
  parseTypedVariable() {
332
354
  const varType = this.advance().value;
333
355
  const name = this.expect(T.IDENT).value;
@@ -336,7 +358,6 @@ export class Parser {
336
358
  return new ASTNode('TypedVar', { varType, name, value, isMut: false, isPublic: false });
337
359
  }
338
360
 
339
- // mut int counter = 0
340
361
  parseMutVariable() {
341
362
  this.advance(); // mut
342
363
  if (TYPE_TOKENS.has(this.peek().type)) {
@@ -344,19 +365,12 @@ export class Parser {
344
365
  node.isMut = true;
345
366
  return node;
346
367
  }
347
- // mut name = value (no type)
348
368
  const name = this.expect(T.IDENT).value;
349
369
  this.expect(T.ASSIGN);
350
370
  const value = this.parseExpression();
351
371
  return new ASTNode('TypedVar', { varType: null, name, value, isMut: true });
352
372
  }
353
373
 
354
- // if condition:
355
- // body
356
- // elif condition:
357
- // body
358
- // else:
359
- // body
360
374
  parseIf() {
361
375
  this.advance(); // if
362
376
  const condition = this.parseExpression();
@@ -384,15 +398,12 @@ export class Parser {
384
398
  return new ASTNode('If', { condition, body, elifs, elseBody });
385
399
  }
386
400
 
387
- // each item in collection:
388
- // body
389
401
  parseEach() {
390
402
  this.advance(); // each
391
403
  let key = null;
392
404
  const valueName = this.expect(T.IDENT).value;
393
405
  if (this.match(T.COMMA)) {
394
406
  key = valueName;
395
- // The next ident is actually the value
396
407
  const val = this.expect(T.IDENT).value;
397
408
  this.expect(T.IN);
398
409
  const collection = this.parseExpression();
@@ -407,8 +418,6 @@ export class Parser {
407
418
  return new ASTNode('Each', { key: null, value: valueName, collection, body });
408
419
  }
409
420
 
410
- // for i in 0..10:
411
- // body
412
421
  parseFor() {
413
422
  this.advance(); // for
414
423
  const varName = this.expect(T.IDENT).value;
@@ -421,8 +430,6 @@ export class Parser {
421
430
  return new ASTNode('For', { varName, start, end, body });
422
431
  }
423
432
 
424
- // while condition:
425
- // body
426
433
  parseWhile() {
427
434
  this.advance(); // while
428
435
  const condition = this.parseExpression();
@@ -431,9 +438,6 @@ export class Parser {
431
438
  return new ASTNode('While', { condition, body });
432
439
  }
433
440
 
434
- // match value:
435
- // pattern: body
436
- // _: default
437
441
  parseMatch() {
438
442
  this.advance(); // match
439
443
  const value = this.parseExpression();
@@ -471,10 +475,6 @@ export class Parser {
471
475
  return new ASTNode('Match', { value, cases });
472
476
  }
473
477
 
474
- // try:
475
- // body
476
- // fail e:
477
- // handler
478
478
  parseTry() {
479
479
  this.advance(); // try
480
480
  this.expect(T.COLON);
@@ -495,14 +495,10 @@ export class Parser {
495
495
  return new ASTNode('Try', { body, catchVar, catchBody });
496
496
  }
497
497
 
498
- // server app port 3000:
499
- // get "/path" -> type:
500
- // body
501
498
  parseServer() {
502
499
  this.advance(); // server
503
500
  const name = this.expect(T.IDENT).value;
504
501
 
505
- let portKw = null;
506
502
  let port = null;
507
503
  if (this.at(T.IDENT) && this.peek().value === 'port') {
508
504
  this.advance();
@@ -530,6 +526,17 @@ export class Parser {
530
526
  routes.push(this.parseCors());
531
527
  } else if (this.at(T.LIMIT)) {
532
528
  routes.push(this.parseLimit());
529
+ } else if (this.at(T.STATIC)) {
530
+ routes.push(this.parseStatic());
531
+ } else if (this.at(T.WS)) {
532
+ routes.push(this.parseWs());
533
+ } else if (this.at(T.GROUP)) {
534
+ routes.push(this.parseGroup());
535
+ } else if (this.at(T.COOKIE)) {
536
+ this.advance();
537
+ routes.push(new ASTNode('CookieDecl'));
538
+ } else if (this.at(T.IDENT) && this.peek().value === 'error') {
539
+ routes.push(this.parseErrorHandler());
533
540
  } else {
534
541
  routes.push(this.parseStatement());
535
542
  }
@@ -579,11 +586,6 @@ export class Parser {
579
586
  return new ASTNode('Middleware', { name, params, body });
580
587
  }
581
588
 
582
- // model User:
583
- // str name
584
- // int age
585
- // fn greet() -> str:
586
- // ret "hi"
587
589
  parseModel(isPublic = false) {
588
590
  this.advance(); // model
589
591
  const name = this.expect(T.IDENT).value;
@@ -615,7 +617,6 @@ export class Parser {
615
617
  }
616
618
  fields.push({ name: fieldName, type: fieldType, defaultValue });
617
619
  } else {
618
- // skip unknown
619
620
  this.advance();
620
621
  }
621
622
  this.skipNewlines();
@@ -625,8 +626,6 @@ export class Parser {
625
626
  return new ASTNode('Model', { name, parent, fields, methods, isPublic });
626
627
  }
627
628
 
628
- // on event:
629
- // body
630
629
  parseOn() {
631
630
  this.advance(); // on
632
631
  const event = this.parseExpression();
@@ -635,8 +634,6 @@ export class Parser {
635
634
  return new ASTNode('On', { event, body });
636
635
  }
637
636
 
638
- // log "message"
639
- // log.error "message"
640
637
  parseLog() {
641
638
  this.advance(); // log
642
639
  let level = 'log';
@@ -664,17 +661,20 @@ export class Parser {
664
661
  const connectionString = this.parseExpression();
665
662
  return new ASTNode('DbConnect', { connectionString });
666
663
  }
667
- // db.query, db.find, etc -> treat as expression
668
- this.pos -= 3; // rewind to parse as expression
664
+ this.pos -= 3;
669
665
  return this.parseExpressionStatement();
670
666
  }
671
667
 
668
+ parseDbDir() {
669
+ this.advance(); // db
670
+ const path = this.parseString();
671
+ return new ASTNode('DbDir', { path });
672
+ }
673
+
672
674
  parseAwaitStatement() {
673
- // could be await expression or standalone await call
674
675
  return this.parseExpressionStatement();
675
676
  }
676
677
 
677
- // [a, b] = await.all [expr1, expr2]
678
678
  parseAwaitAll() {
679
679
  this.advance(); // await.all
680
680
  const exprs = [];
@@ -696,7 +696,6 @@ export class Parser {
696
696
  parseExpressionStatement() {
697
697
  const expr = this.parseExpression();
698
698
 
699
- // Check for assignment: expr = value
700
699
  if (this.match(T.ASSIGN)) {
701
700
  const value = this.parseExpression();
702
701
  return new ASTNode('Assignment', { target: expr, value });
@@ -713,7 +712,6 @@ export class Parser {
713
712
  return new ASTNode('ExprStatement', { expression: expr });
714
713
  }
715
714
 
716
- // Expression parsing with precedence climbing
717
715
  parseExpression() {
718
716
  return this.parsePipe();
719
717
  }
@@ -730,27 +728,35 @@ export class Parser {
730
728
 
731
729
  matchPipeAcrossLines() {
732
730
  if (this.match(T.PIPE)) return true;
733
- // Lookahead across newlines for pipe (INDENT/DEDENT already preprocessed)
734
731
  let scanPos = this.pos;
735
732
  while (scanPos < this.tokens.length && this.tokens[scanPos].type === T.NEWLINE) {
736
733
  scanPos++;
737
734
  }
738
735
  if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.PIPE) {
739
736
  while (this.at(T.NEWLINE)) this.advance();
740
- this.advance(); // consume PIPE
737
+ this.advance();
741
738
  return true;
742
739
  }
743
740
  return false;
744
741
  }
745
742
 
746
743
  parseTernary() {
744
+ if (this.at(T.IF)) {
745
+ const savedPos = this.pos;
746
+ this.advance();
747
+ const condition = this.parseNullish();
748
+ if (this.match(T.THEN)) {
749
+ const consequent = this.parseNullish();
750
+ this.expect(T.ELSE);
751
+ const alternate = this.parseNullish();
752
+ return new ASTNode('Ternary', { condition, consequent, alternate });
753
+ }
754
+ this.pos = savedPos;
755
+ }
747
756
  let expr = this.parseNullish();
748
- // inline if: value = if cond then a else b
749
757
  if (this.at(T.IF)) {
750
- // Only treat as ternary if we're in an expression context
751
- // Lookahead: if ... then ... else
752
758
  const savedPos = this.pos;
753
- this.advance(); // if
759
+ this.advance();
754
760
  const condition = this.parseNullish();
755
761
  if (this.match(T.THEN)) {
756
762
  const consequent = this.parseNullish();
@@ -758,7 +764,6 @@ export class Parser {
758
764
  const alternate = this.parseNullish();
759
765
  return new ASTNode('Ternary', { condition, consequent, alternate });
760
766
  }
761
- // Not a ternary, restore
762
767
  this.pos = savedPos;
763
768
  }
764
769
  return expr;
@@ -922,12 +927,13 @@ export class Parser {
922
927
  case T.TYPE_STR: case T.TYPE_INT: case T.TYPE_NUM:
923
928
  case T.TYPE_BOOL: case T.TYPE_LIST: case T.TYPE_MAP:
924
929
  case T.TYPE_ANY: case T.TYPE_JSON: case T.TYPE_VOID:
925
- // In expression context, type keywords act as identifiers (e.g., arr.map(), JSON.parse())
926
930
  this.advance();
927
931
  return new ASTNode('Identifier', { name: tok.value });
928
932
 
929
933
  case T.SCHEMA: case T.CRUD: case T.AUTH: case T.CORS:
930
934
  case T.LIMIT: case T.ENV: case T.EVERY: case T.WATCH:
935
+ case T.STATIC: case T.WS: case T.GROUP: case T.COOKIE:
936
+ case T.FROM: case T.AS: case T.IN:
931
937
  this.advance();
932
938
  return new ASTNode('Identifier', { name: tok.value });
933
939
 
@@ -1002,11 +1008,9 @@ export class Parser {
1002
1008
  }
1003
1009
 
1004
1010
  parseGroupOrArrow() {
1005
- // Check if this is an arrow function: (params) => body
1006
1011
  const savedPos = this.pos;
1007
1012
  this.advance(); // (
1008
1013
 
1009
- // Try to parse as arrow function params
1010
1014
  let isArrow = false;
1011
1015
  let depth = 1;
1012
1016
  let scanPos = this.pos;
@@ -1036,7 +1040,6 @@ export class Parser {
1036
1040
  return new ASTNode('ArrowFn', { params, body });
1037
1041
  }
1038
1042
 
1039
- // Regular grouping
1040
1043
  this.pos = savedPos;
1041
1044
  this.advance(); // (
1042
1045
  const expr = this.parseExpression();
@@ -1114,10 +1117,8 @@ export class Parser {
1114
1117
  return new ASTNode('Lambda', { params, returnType, body, isAsync });
1115
1118
  }
1116
1119
 
1117
- // schema User:
1118
- // id auto
1119
- // name str required min(2) max(50)
1120
- // email str required email unique
1120
+ // ===== High-level feature parsers =====
1121
+
1121
1122
  parseSchema() {
1122
1123
  this.advance(); // schema
1123
1124
  const name = this.expect(T.IDENT).value;
@@ -1184,7 +1185,6 @@ export class Parser {
1184
1185
  return { name, type: fieldType, enumValues, modifiers };
1185
1186
  }
1186
1187
 
1187
- // crud "/api/users" User
1188
1188
  parseCrud() {
1189
1189
  this.advance(); // crud
1190
1190
  const path = this.parseString();
@@ -1192,9 +1192,6 @@ export class Parser {
1192
1192
  return new ASTNode('CrudDecl', { path, schemaName });
1193
1193
  }
1194
1194
 
1195
- // auth SECRET:
1196
- // protect "/api/*"
1197
- // public "/api/auth/*"
1198
1195
  parseAuth() {
1199
1196
  this.advance(); // auth
1200
1197
  const secret = this.parseExpression();
@@ -1224,15 +1221,12 @@ export class Parser {
1224
1221
  return new ASTNode('AuthDecl', { secret, protectedPaths, publicPaths });
1225
1222
  }
1226
1223
 
1227
- // cors "*"
1228
- // cors ["origin1", "origin2"]
1229
1224
  parseCors() {
1230
1225
  this.advance(); // cors
1231
1226
  const origins = this.parseExpression();
1232
1227
  return new ASTNode('CorsDecl', { origins });
1233
1228
  }
1234
1229
 
1235
- // limit "/api/*" 100 "1m"
1236
1230
  parseLimit() {
1237
1231
  this.advance(); // limit
1238
1232
  const path = this.parseString();
@@ -1241,9 +1235,6 @@ export class Parser {
1241
1235
  return new ASTNode('LimitDecl', { path, max, window });
1242
1236
  }
1243
1237
 
1244
- // env:
1245
- // PORT int default(3000)
1246
- // JWT_SECRET str required
1247
1238
  parseEnv() {
1248
1239
  this.advance(); // env
1249
1240
  this.expect(T.COLON);
@@ -1296,8 +1287,6 @@ export class Parser {
1296
1287
  return { name, type: fieldType, modifiers };
1297
1288
  }
1298
1289
 
1299
- // every "5m":
1300
- // log "tick"
1301
1290
  parseEvery() {
1302
1291
  this.advance(); // every
1303
1292
  const interval = this.parseExpression();
@@ -1306,8 +1295,6 @@ export class Parser {
1306
1295
  return new ASTNode('EveryDecl', { interval, body });
1307
1296
  }
1308
1297
 
1309
- // watch User.create (event):
1310
- // log event
1311
1298
  parseWatch() {
1312
1299
  this.advance(); // watch
1313
1300
  let eventName = this.expect(T.IDENT).value;
@@ -1326,4 +1313,98 @@ export class Parser {
1326
1313
  const body = this.parseBlock();
1327
1314
  return new ASTNode('WatchDecl', { eventName, params, body });
1328
1315
  }
1316
+
1317
+ // static "/public"
1318
+ parseStatic() {
1319
+ this.advance(); // static
1320
+ const path = this.parseString();
1321
+ return new ASTNode('StaticDecl', { path });
1322
+ }
1323
+
1324
+ // ws "/chat":
1325
+ // on "message" (data):
1326
+ // broadcast(data)
1327
+ // on "connect":
1328
+ // send({type: "welcome"})
1329
+ parseWs() {
1330
+ this.advance(); // ws
1331
+ const path = this.parseString();
1332
+ this.expect(T.COLON);
1333
+ this.skipNewlines();
1334
+ this.expect(T.INDENT);
1335
+
1336
+ const events = [];
1337
+ this.skipNewlines();
1338
+
1339
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1340
+ if (this.at(T.ON)) {
1341
+ this.advance(); // on
1342
+ const eventName = this.parseString();
1343
+ let params = [];
1344
+ if (this.match(T.LPAREN)) {
1345
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1346
+ params.push(this.expect(T.IDENT).value);
1347
+ this.match(T.COMMA);
1348
+ }
1349
+ this.expect(T.RPAREN);
1350
+ }
1351
+ this.expect(T.COLON);
1352
+ const body = this.parseBlock();
1353
+ events.push({ name: eventName, params, body });
1354
+ } else {
1355
+ this.advance();
1356
+ }
1357
+ this.skipNewlines();
1358
+ }
1359
+ this.match(T.DEDENT);
1360
+
1361
+ return new ASTNode('WsDecl', { path, events });
1362
+ }
1363
+
1364
+ parseGroup() {
1365
+ this.advance(); // group
1366
+ const prefix = this.parseString();
1367
+ this.expect(T.COLON);
1368
+ this.skipNewlines();
1369
+ this.expect(T.INDENT);
1370
+
1371
+ const routes = [];
1372
+ this.skipNewlines();
1373
+
1374
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1375
+ if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
1376
+ routes.push(this.parseRoute());
1377
+ } else if (this.at(T.MID)) {
1378
+ routes.push(this.parseMiddleware());
1379
+ } else if (this.at(T.CRUD)) {
1380
+ routes.push(this.parseCrud());
1381
+ } else if (this.at(T.AUTH)) {
1382
+ routes.push(this.parseAuth());
1383
+ } else if (this.at(T.GROUP)) {
1384
+ routes.push(this.parseGroup());
1385
+ } else {
1386
+ routes.push(this.parseStatement());
1387
+ }
1388
+ this.skipNewlines();
1389
+ }
1390
+ this.match(T.DEDENT);
1391
+
1392
+ return new ASTNode('GroupDecl', { prefix, routes });
1393
+ }
1394
+
1395
+ parseErrorHandler() {
1396
+ this.advance(); // error (ident)
1397
+ let params = ['err', 'req', 'res'];
1398
+ if (this.match(T.LPAREN)) {
1399
+ params = [];
1400
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1401
+ params.push(this.expect(T.IDENT).value);
1402
+ this.match(T.COMMA);
1403
+ }
1404
+ this.expect(T.RPAREN);
1405
+ }
1406
+ this.expect(T.COLON);
1407
+ const body = this.parseBlock();
1408
+ return new ASTNode('ErrorHandler', { params, body });
1409
+ }
1329
1410
  }
package/src/preprocess.js CHANGED
@@ -107,6 +107,14 @@ export function preprocess(source) {
107
107
  const rest = line.slice(1).trim();
108
108
  if (rest.startsWith('.s ') || rest.startsWith('.s(')) {
109
109
  out = 'ret.status ' + transformContent(rest.slice(3));
110
+ } else if (rest.startsWith('.r ')) {
111
+ out = 'ret.redirect ' + transformContent(rest.slice(3));
112
+ } else if (rest.startsWith('.h ')) {
113
+ out = 'ret.html ' + transformContent(rest.slice(3));
114
+ } else if (rest.startsWith('.t ')) {
115
+ out = 'ret.text ' + transformContent(rest.slice(3));
116
+ } else if (rest.startsWith('.f ')) {
117
+ out = 'ret.file ' + transformContent(rest.slice(3));
110
118
  } else {
111
119
  out = rest ? 'ret ' + transformContent(rest) : 'ret';
112
120
  }