gofront 0.0.8 → 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.
Files changed (51) hide show
  1. package/README.md +88 -12
  2. package/package.json +8 -4
  3. package/src/codegen/expressions.js +520 -1717
  4. package/src/{codegen.js → codegen/index.js} +155 -184
  5. package/src/codegen/runtime.js +151 -0
  6. package/src/codegen/statements.js +305 -296
  7. package/src/codegen/stdlib/builder.js +36 -0
  8. package/src/codegen/stdlib/bytes.js +62 -0
  9. package/src/codegen/stdlib/errors.js +17 -0
  10. package/src/codegen/stdlib/fmt.js +101 -0
  11. package/src/codegen/stdlib/gom.js +152 -0
  12. package/src/codegen/stdlib/html.js +17 -0
  13. package/src/codegen/stdlib/index.js +95 -0
  14. package/src/codegen/stdlib/io.js +33 -0
  15. package/src/codegen/stdlib/maps.js +42 -0
  16. package/src/codegen/stdlib/math.js +53 -0
  17. package/src/codegen/stdlib/os.js +10 -0
  18. package/src/codegen/stdlib/path.js +31 -0
  19. package/src/codegen/stdlib/rand.js +24 -0
  20. package/src/codegen/stdlib/regexp.js +51 -0
  21. package/src/codegen/stdlib/slices.js +48 -0
  22. package/src/codegen/stdlib/sort.js +28 -0
  23. package/src/codegen/stdlib/strconv.js +30 -0
  24. package/src/codegen/stdlib/strings.js +83 -0
  25. package/src/codegen/stdlib/time.js +54 -0
  26. package/src/codegen/stdlib/unicode.js +22 -0
  27. package/src/codegen/stdlib/utf8.js +23 -0
  28. package/src/codegen/templ.js +281 -0
  29. package/src/compiler.js +102 -62
  30. package/src/dts-parser.js +355 -371
  31. package/src/index.js +22 -54
  32. package/src/lexer.js +200 -286
  33. package/src/minifier.js +305 -318
  34. package/src/parser/expressions.js +54 -36
  35. package/src/{parser.js → parser/index.js} +12 -5
  36. package/src/parser/statements.js +29 -35
  37. package/src/parser/types.js +31 -29
  38. package/src/templ-lexer.js +737 -0
  39. package/src/templ-parser.js +235 -0
  40. package/src/tokens.js +109 -0
  41. package/src/typechecker/assignability.js +258 -0
  42. package/src/typechecker/expressions.js +559 -549
  43. package/src/typechecker/index.js +555 -0
  44. package/src/typechecker/resolve.js +421 -0
  45. package/src/typechecker/statements.js +267 -280
  46. package/src/typechecker/stdlib/core.js +592 -0
  47. package/src/typechecker/stdlib/extended.js +332 -0
  48. package/src/typechecker/stdlib.js +9 -1060
  49. package/src/typechecker/termination.js +83 -0
  50. package/src/typechecker/types.js +6 -3
  51. package/src/typechecker.js +0 -1271
@@ -0,0 +1,737 @@
1
+ // Templ lexer: tokenizes .templ files into a stream of tokens.
2
+ //
3
+ // A .templ file is a Go-like file where `templ Name(params) { ... }` declarations
4
+ // contain HTML bodies instead of Go statements. All other constructs (package,
5
+ // import, regular func, var, const, type) are lexed as normal Go.
6
+ //
7
+ // Token types produced (in addition to the standard Go token types from lexer.js):
8
+ // TEMPL_KW — the `templ` keyword
9
+ // HTML_OPEN — <tag attr...> (includes the `>`)
10
+ // HTML_CLOSE — </tag>
11
+ // HTML_SELF — <tag .../>
12
+ // HTML_TEXT — literal text between tags
13
+ // TEMPL_EXPR — { goExpr } inside HTML (the goExpr tokens are inlined)
14
+ // TEMPL_COMP — @Call(...) inside HTML
15
+ // TEMPL_CHILDREN — { children... }
16
+ //
17
+ // The approach: scan the file character-by-character. Outside templ bodies, use
18
+ // the existing Lexer class to tokenise Go. Inside templ bodies, scan HTML.
19
+
20
+ import { Lexer, T, Token } from "./lexer.js";
21
+
22
+ export const TT = {
23
+ TEMPL_KW: "templ",
24
+ HTML_OPEN: "HTML_OPEN",
25
+ HTML_CLOSE: "HTML_CLOSE",
26
+ HTML_SELF: "HTML_SELF",
27
+ HTML_TEXT: "HTML_TEXT",
28
+ TEMPL_EXPR: "TEMPL_EXPR", // inline Go expression token stream
29
+ TEMPL_COMP: "TEMPL_COMP", // @Component(args) call
30
+ TEMPL_CHILDREN: "TEMPL_CHILDREN",
31
+ TEMPL_IF: "TEMPL_IF",
32
+ TEMPL_ELSE: "TEMPL_ELSE",
33
+ TEMPL_FOR: "TEMPL_FOR",
34
+ TEMPL_SWITCH: "TEMPL_SWITCH", // switch expr {
35
+ TEMPL_CASE: "TEMPL_CASE", // case expr:
36
+ TEMPL_DEFAULT: "TEMPL_DEFAULT", // default:
37
+ TEMPL_END: "TEMPL_END", // closing } of if/for/switch/templ body
38
+ };
39
+
40
+ // Describes a parsed HTML attribute
41
+ // { kind: "static", name, value } | { kind: "expr", name, tokens } | { kind: "bool", name }
42
+
43
+ export class TemplLexer {
44
+ constructor(source, filename = "<templ>") {
45
+ this.src = source;
46
+ this.pos = 0;
47
+ this.line = 1;
48
+ this.col = 1;
49
+ this.filename = filename;
50
+ this.tokens = [];
51
+ }
52
+
53
+ // ── Character primitives ────────────────────────────────────
54
+
55
+ peek(n = 0) {
56
+ return this.src[this.pos + n];
57
+ }
58
+
59
+ advance() {
60
+ const ch = this.src[this.pos++];
61
+ if (ch === "\n") {
62
+ this.line++;
63
+ this.col = 1;
64
+ } else {
65
+ this.col++;
66
+ }
67
+ return ch;
68
+ }
69
+
70
+ eof() {
71
+ return this.pos >= this.src.length;
72
+ }
73
+
74
+ skipWhitespace() {
75
+ while (!this.eof() && /[ \t\r\n]/.test(this.peek())) this.advance();
76
+ }
77
+
78
+ skipLineComment() {
79
+ while (!this.eof() && this.peek() !== "\n") this.advance();
80
+ }
81
+
82
+ skipBlockComment() {
83
+ while (!this.eof()) {
84
+ if (this.peek() === "*" && this.peek(1) === "/") {
85
+ this.advance();
86
+ this.advance();
87
+ return;
88
+ }
89
+ this.advance();
90
+ }
91
+ this.err("unterminated block comment");
92
+ }
93
+
94
+ err(msg) {
95
+ throw new Error(`${this.filename}:${this.line}:${this.col}: ${msg}`);
96
+ }
97
+
98
+ // ── Main tokenise entry point ────────────────────────────────
99
+
100
+ tokenize() {
101
+ // Scan the entire file. We identify `templ Name(params) {` blocks
102
+ // and handle their bodies in HTML mode; everything else is Go.
103
+ while (!this.eof()) {
104
+ this.skipWhitespace();
105
+ if (this.eof()) break;
106
+
107
+ // Skip Go-style comments
108
+ if (this.peek() === "/" && this.peek(1) === "/") {
109
+ this.skipLineComment();
110
+ continue;
111
+ }
112
+ if (this.peek() === "/" && this.peek(1) === "*") {
113
+ this.advance();
114
+ this.advance();
115
+ this.skipBlockComment();
116
+ continue;
117
+ }
118
+
119
+ // Detect `templ` keyword at identifier boundaries
120
+ if (this._matchKeyword("templ")) {
121
+ this.tokens.push(new Token(TT.TEMPL_KW, "templ", this.line, this.col));
122
+ this._lexTemplDecl();
123
+ continue;
124
+ }
125
+
126
+ // Everything else: scan to end of top-level declaration using the
127
+ // raw Go lexer, accumulating tokens until the next `templ` keyword
128
+ // or EOF.
129
+ this._lexGoChunk();
130
+ }
131
+
132
+ this.tokens.push(new Token(T.EOF, "", this.line, this.col));
133
+ return this.tokens;
134
+ }
135
+
136
+ // ── Go chunk lexing ──────────────────────────────────────────
137
+
138
+ // Scan ahead to find the next `templ ` keyword at top level (not inside
139
+ // braces/parens/strings/comments). Collect all source text up to that
140
+ // point, lex it as normal Go, and append the tokens.
141
+ _lexGoChunk() {
142
+ const start = this.pos;
143
+ let depth = 0; // brace depth
144
+
145
+ while (!this.eof()) {
146
+ // Check for `templ` keyword at top level (depth === 0)
147
+ if (depth === 0 && this._peekKeyword("templ")) break;
148
+
149
+ const ch = this.peek();
150
+
151
+ // Strings (raw or interpreted)
152
+ if (ch === '"' || ch === "'") {
153
+ this._skipGoString(ch);
154
+ continue;
155
+ }
156
+ if (ch === "`") {
157
+ this._skipRawString();
158
+ continue;
159
+ }
160
+
161
+ // Comments
162
+ if (ch === "/" && this.peek(1) === "/") {
163
+ this.skipLineComment();
164
+ continue;
165
+ }
166
+ if (ch === "/" && this.peek(1) === "*") {
167
+ this.advance();
168
+ this.advance();
169
+ this.skipBlockComment();
170
+ continue;
171
+ }
172
+
173
+ if (ch === "{") depth++;
174
+ else if (ch === "}") {
175
+ if (depth === 0) break; // should not happen at top level, stop
176
+ depth--;
177
+ }
178
+ this.advance();
179
+ }
180
+
181
+ if (this.pos === start) {
182
+ // Nothing to lex; skip one char to avoid infinite loop
183
+ this.advance();
184
+ return;
185
+ }
186
+
187
+ const chunk = this.src.slice(start, this.pos);
188
+ for (const t of this._goTokens(chunk)) this.tokens.push(t);
189
+ }
190
+
191
+ // ── Templ declaration lexing ─────────────────────────────────
192
+
193
+ // Called right after consuming `templ` keyword.
194
+ // Lexes: Name(params) { <HTML body> }
195
+ _lexTemplDecl() {
196
+ this.skipWhitespace();
197
+
198
+ // Name
199
+ const nameStart = this.pos;
200
+ while (!this.eof() && /\w/.test(this.peek())) this.advance();
201
+ const name = this.src.slice(nameStart, this.pos);
202
+ if (!name) this.err("expected component name after 'templ'");
203
+ this.tokens.push(new Token(T.IDENT, name, this.line, this.col));
204
+
205
+ this.skipWhitespace();
206
+
207
+ // Parameter list ( ... ) — lex as Go
208
+ if (this.peek() !== "(") this.err("expected '(' after templ name");
209
+ const paramStart = this.pos;
210
+ this._skipBalanced("(", ")");
211
+ const paramSrc = this.src.slice(paramStart, this.pos);
212
+ for (const t of this._goTokens(paramSrc)) this.tokens.push(t);
213
+
214
+ this.skipWhitespace();
215
+
216
+ // Opening { of body
217
+ if (this.peek() !== "{") this.err("expected '{' after templ params");
218
+ this.advance(); // consume {
219
+ this.tokens.push(new Token(T.LBRACE, "{", this.line, this.col));
220
+
221
+ // HTML body
222
+ this._lexHtmlBody(1);
223
+ }
224
+
225
+ // Lex the HTML body of a templ declaration.
226
+ // depth: current brace depth (starts at 1 for the opening { of templ)
227
+ _lexHtmlBody(depth) {
228
+ while (!this.eof()) {
229
+ this.skipWhitespace();
230
+ if (this.eof()) break;
231
+
232
+ const ch = this.peek();
233
+
234
+ // Closing brace: if depth becomes 0, we're done
235
+ if (ch === "}") {
236
+ if (depth === 1) {
237
+ this.advance(); // consume }
238
+ this.tokens.push(new Token(T.RBRACE, "}", this.line, this.col));
239
+ return;
240
+ }
241
+ // depth > 1 should not happen at the top of a templ body; treat as end
242
+ this.advance();
243
+ this.tokens.push(new Token(TT.TEMPL_END, "}", this.line, this.col));
244
+ return;
245
+ }
246
+
247
+ this._lexHtmlNode();
248
+ }
249
+ }
250
+
251
+ // Lex HTML nodes for a switch-case body until case/default/} without consuming
252
+ // the terminator (the caller handles it).
253
+ _lexCaseBody() {
254
+ while (!this.eof()) {
255
+ this.skipWhitespace();
256
+ if (this.eof()) break;
257
+ const ch = this.peek();
258
+ if (ch === "}") break;
259
+ if (this._peekKeyword("case") || this._peekKeyword("default")) break;
260
+ this._lexHtmlNode();
261
+ }
262
+ }
263
+
264
+ // Lex a single HTML node at the current position.
265
+ _lexHtmlNode() {
266
+ const ch = this.peek();
267
+
268
+ // HTML comment <!-- ... -->
269
+ if (ch === "<" && this.src.startsWith("<!--", this.pos)) {
270
+ this._skipHtmlComment();
271
+ return;
272
+ }
273
+ // Closing tag </tag>
274
+ if (ch === "<" && this.peek(1) === "/") {
275
+ this._lexHtmlCloseTag();
276
+ return;
277
+ }
278
+ // Opening tag <tag ...> or <tag .../>
279
+ if (ch === "<") {
280
+ this._lexHtmlOpenTag();
281
+ return;
282
+ }
283
+ // @Component(args) call
284
+ if (ch === "@") {
285
+ this._lexTemplComponent();
286
+ return;
287
+ }
288
+ // { children... } or { goExpr }
289
+ if (ch === "{") {
290
+ this._lexBraceExprOrControl();
291
+ return;
292
+ }
293
+ // Control flow keywords
294
+ if (this._peekKeyword("if")) {
295
+ this._lexTemplIf();
296
+ return;
297
+ }
298
+ if (this._peekKeyword("for")) {
299
+ this._lexTemplFor();
300
+ return;
301
+ }
302
+ if (this._peekKeyword("switch")) {
303
+ this._lexTemplSwitch();
304
+ return;
305
+ }
306
+ // Plain text content
307
+ this._lexHtmlText();
308
+ }
309
+
310
+ // ── HTML element lexing ───────────────────────────────────────
311
+
312
+ _lexHtmlOpenTag() {
313
+ const line = this.line;
314
+ const col = this.col;
315
+ this.advance(); // consume <
316
+ this.skipWhitespace();
317
+
318
+ // Tag name
319
+ const tagName = this._readHtmlName();
320
+ if (!tagName) this.err("expected tag name");
321
+
322
+ const attrs = [];
323
+ let selfClose = false;
324
+
325
+ // Attributes
326
+ while (!this.eof()) {
327
+ this.skipWhitespace();
328
+ const c = this.peek();
329
+
330
+ if (c === "/" && this.peek(1) === ">") {
331
+ this.advance();
332
+ this.advance(); // />
333
+ selfClose = true;
334
+ break;
335
+ }
336
+ if (c === ">") {
337
+ this.advance(); // >
338
+ break;
339
+ }
340
+
341
+ // Attribute name
342
+ const attrName = this._readHtmlAttrName();
343
+ if (!attrName) break;
344
+
345
+ this.skipWhitespace();
346
+
347
+ // Conditional boolean attribute: name?={expr}
348
+ if (this.peek() === "?" && this.peek(1) === "=") {
349
+ this.advance();
350
+ this.advance(); // ?=
351
+ this.skipWhitespace();
352
+ if (this.peek() !== "{") this.err("expected '{' after ?=");
353
+ const exprTokens = this._readGoExprInBraces();
354
+ attrs.push({ kind: "cond-bool", name: attrName, tokens: exprTokens });
355
+ continue;
356
+ }
357
+
358
+ if (this.peek() !== "=") {
359
+ // Boolean attribute: no value
360
+ attrs.push({ kind: "bool", name: attrName });
361
+ continue;
362
+ }
363
+ this.advance(); // =
364
+ this.skipWhitespace();
365
+
366
+ if (this.peek() === "{") {
367
+ // Expression attribute: name={ goExpr }
368
+ const exprTokens = this._readGoExprInBraces();
369
+ attrs.push({ kind: "expr", name: attrName, tokens: exprTokens });
370
+ } else if (this.peek() === '"') {
371
+ // Static string attribute
372
+ const value = this._readQuotedString();
373
+ attrs.push({ kind: "static", name: attrName, value });
374
+ } else {
375
+ // Unquoted attribute value
376
+ let val = "";
377
+ while (!this.eof() && !/[\s>]/.test(this.peek())) val += this.advance();
378
+ attrs.push({ kind: "static", name: attrName, value: val });
379
+ }
380
+ }
381
+
382
+ const kind = selfClose ? TT.HTML_SELF : TT.HTML_OPEN;
383
+ this.tokens.push(new Token(kind, { tag: tagName, attrs }, line, col));
384
+ }
385
+
386
+ _lexHtmlCloseTag() {
387
+ const line = this.line;
388
+ this.advance(); // <
389
+ this.advance(); // /
390
+ this.skipWhitespace();
391
+ const tagName = this._readHtmlName();
392
+ this.skipWhitespace();
393
+ if (this.peek() === ">") this.advance();
394
+ this.tokens.push(new Token(TT.HTML_CLOSE, tagName, line, this.col));
395
+ }
396
+
397
+ _skipHtmlComment() {
398
+ // <!-- ... -->
399
+ this.pos += 4; // <!--
400
+ while (!this.eof()) {
401
+ if (this.src.startsWith("-->", this.pos)) {
402
+ this.pos += 3;
403
+ return;
404
+ }
405
+ if (this.peek() === "\n") {
406
+ this.line++;
407
+ this.col = 1;
408
+ } else {
409
+ this.col++;
410
+ }
411
+ this.pos++;
412
+ }
413
+ }
414
+
415
+ // ── Expression / control flow inside HTML ─────────────────────
416
+
417
+ _lexBraceExprOrControl() {
418
+ // { children... } → TEMPL_CHILDREN
419
+ // { goExpr } → TEMPL_EXPR
420
+ const saveLine = this.line;
421
+ const saveCol = this.col;
422
+
423
+ this.advance(); // {
424
+ this.skipWhitespace();
425
+
426
+ // Check for children...
427
+ if (this.src.startsWith("children...", this.pos)) {
428
+ this.pos += "children...".length;
429
+ this.skipWhitespace();
430
+ if (this.peek() === "}") this.advance();
431
+ this.tokens.push(new Token(TT.TEMPL_CHILDREN, null, saveLine, saveCol));
432
+ return;
433
+ }
434
+
435
+ // Otherwise lex the Go expression inside the braces
436
+ const exprTokens = this._readGoExprTokensUntilClose();
437
+ this.tokens.push(new Token(TT.TEMPL_EXPR, exprTokens, saveLine, saveCol));
438
+ }
439
+
440
+ _lexTemplComponent() {
441
+ const line = this.line;
442
+ const col = this.col;
443
+ this.advance(); // @
444
+
445
+ // Read the call expression (Go tokens until end of balanced parens)
446
+ const nameStart = this.pos;
447
+ while (!this.eof() && /[\w.]/.test(this.peek())) this.advance();
448
+ const name = this.src.slice(nameStart, this.pos);
449
+ if (!name) this.err("expected component name after @");
450
+
451
+ this.skipWhitespace();
452
+ if (this.peek() !== "(") this.err("expected '(' after @component name");
453
+
454
+ const argsStart = this.pos;
455
+ this._skipBalanced("(", ")");
456
+ const argsSrc = name + this.src.slice(argsStart, this.pos);
457
+
458
+ const callTokens = this._goTokens(argsSrc);
459
+ this.tokens.push(new Token(TT.TEMPL_COMP, callTokens, line, col));
460
+ }
461
+
462
+ _lexTemplIf() {
463
+ const line = this.line;
464
+ const condTokens = this._lexGoExprBeforeBrace();
465
+ this.tokens.push(new Token(TT.TEMPL_IF, condTokens, line, this.col));
466
+ this._lexHtmlBody(1);
467
+
468
+ // Check for else
469
+ this.skipWhitespace();
470
+ if (this._peekKeyword("else")) {
471
+ this._readWord(); // consume `else`
472
+ this.skipWhitespace();
473
+ this.tokens.push(new Token(TT.TEMPL_ELSE, null, this.line, this.col));
474
+ if (this._peekKeyword("if")) {
475
+ // else if
476
+ this._lexTemplIf();
477
+ } else {
478
+ // else block
479
+ if (this.peek() !== "{") this.err("expected '{' after else");
480
+ this.advance(); // {
481
+ this._lexHtmlBody(1);
482
+ }
483
+ }
484
+ }
485
+
486
+ _lexTemplFor() {
487
+ const line = this.line;
488
+ const stmtTokens = this._lexGoExprBeforeBrace();
489
+ this.tokens.push(new Token(TT.TEMPL_FOR, stmtTokens, line, this.col));
490
+ this._lexHtmlBody(1);
491
+ }
492
+
493
+ _lexTemplSwitch() {
494
+ const line = this.line;
495
+ this._readWord(); // consume "switch"
496
+ this.skipWhitespace();
497
+ const exprTokens = this._lexGoExprBeforeBrace();
498
+ this.tokens.push(new Token(TT.TEMPL_SWITCH, exprTokens, line, this.col));
499
+
500
+ // Lex case/default entries until the closing }
501
+ while (!this.eof()) {
502
+ this.skipWhitespace();
503
+ if (this.eof()) break;
504
+
505
+ if (this.peek() === "}") {
506
+ this.advance(); // consume }
507
+ this.tokens.push(new Token(TT.TEMPL_END, "}", this.line, this.col));
508
+ return;
509
+ }
510
+
511
+ if (this._peekKeyword("case")) {
512
+ const caseLine = this.line;
513
+ this._readWord(); // consume "case"
514
+ this.skipWhitespace();
515
+ // Read case expression up to ":"
516
+ const caseExprStart = this.pos;
517
+ // Skip to the colon, handling string literals
518
+ while (!this.eof() && this.peek() !== ":") {
519
+ const c = this.peek();
520
+ if (c === '"' || c === "'") {
521
+ this._skipGoString(c);
522
+ } else if (c === "`") {
523
+ this._skipRawString();
524
+ } else {
525
+ this.advance();
526
+ }
527
+ }
528
+ const caseExprSrc = this.src.slice(caseExprStart, this.pos).trimEnd();
529
+ if (this.peek() === ":") this.advance(); // :
530
+ const caseTokens = this._goTokens(caseExprSrc);
531
+ this.tokens.push(
532
+ new Token(TT.TEMPL_CASE, caseTokens, caseLine, this.col),
533
+ );
534
+ this._lexCaseBody();
535
+ } else if (this._peekKeyword("default")) {
536
+ const defaultLine = this.line;
537
+ this._readWord(); // consume "default"
538
+ this.skipWhitespace();
539
+ if (this.peek() === ":") this.advance(); // :
540
+ this.tokens.push(
541
+ new Token(TT.TEMPL_DEFAULT, null, defaultLine, this.col),
542
+ );
543
+ this._lexCaseBody();
544
+ } else {
545
+ // Unexpected token inside switch body — skip to avoid infinite loop
546
+ this.advance();
547
+ }
548
+ }
549
+ }
550
+
551
+ // ── HTML text content ─────────────────────────────────────────
552
+
553
+ _lexHtmlText() {
554
+ let text = "";
555
+ const line = this.line;
556
+ while (!this.eof()) {
557
+ const c = this.peek();
558
+ if (c === "<" || c === "{" || c === "@" || c === "}") break;
559
+ // Detect control-flow keywords at line start
560
+ if (
561
+ (c === "i" && this._peekKeyword("if")) ||
562
+ (c === "f" && this._peekKeyword("for")) ||
563
+ (c === "s" && this._peekKeyword("switch")) ||
564
+ (c === "c" && this._peekKeyword("case")) ||
565
+ (c === "d" && this._peekKeyword("default"))
566
+ )
567
+ break;
568
+ text += this.advance();
569
+ }
570
+ const trimmed = text.trim();
571
+ if (trimmed)
572
+ this.tokens.push(new Token(TT.HTML_TEXT, trimmed, line, this.col));
573
+ }
574
+
575
+ // ── Helpers ───────────────────────────────────────────────────
576
+
577
+ _goTokens(src) {
578
+ return new Lexer(src, this.filename)
579
+ .tokenize()
580
+ .filter((t) => t.type !== T.EOF);
581
+ }
582
+
583
+ // Consume text up to the opening `{`, advance past it, and return Go tokens.
584
+ _lexGoExprBeforeBrace() {
585
+ const start = this.pos;
586
+ this._skipToOpenBrace();
587
+ const src = this.src.slice(start, this.pos).trimEnd();
588
+ this.advance(); // consume {
589
+ return src ? this._goTokens(src) : [];
590
+ }
591
+
592
+ // Read Go expression tokens until the closing }
593
+ _readGoExprInBraces() {
594
+ this.advance(); // {
595
+ return this._readGoExprTokensUntilClose();
596
+ }
597
+
598
+ _readGoExprTokensUntilClose() {
599
+ const start = this.pos;
600
+ let depth = 1;
601
+ while (!this.eof() && depth > 0) {
602
+ const c = this.peek();
603
+ if (c === "{") depth++;
604
+ else if (c === "}") {
605
+ depth--;
606
+ if (depth === 0) break;
607
+ } else if (c === '"' || c === "'") {
608
+ this._skipGoString(c);
609
+ continue;
610
+ } else if (c === "`") {
611
+ this._skipRawString();
612
+ continue;
613
+ }
614
+ this.advance();
615
+ }
616
+ const exprSrc = this.src.slice(start, this.pos).trim();
617
+ if (this.peek() === "}") this.advance(); // consume }
618
+ if (!exprSrc) return [];
619
+ return this._goTokens(exprSrc);
620
+ }
621
+
622
+ _readHtmlName() {
623
+ let name = "";
624
+ while (!this.eof() && /[a-zA-Z0-9_-]/.test(this.peek()))
625
+ name += this.advance();
626
+ return name;
627
+ }
628
+
629
+ _readHtmlAttrName() {
630
+ let name = "";
631
+ while (!this.eof() && /[a-zA-Z0-9_:.-]/.test(this.peek()))
632
+ name += this.advance();
633
+ return name;
634
+ }
635
+
636
+ _readQuotedString() {
637
+ this.advance(); // opening "
638
+ let s = "";
639
+ while (!this.eof() && this.peek() !== '"') {
640
+ if (this.peek() === "\\") {
641
+ this.advance();
642
+ s += this.advance();
643
+ } else s += this.advance();
644
+ }
645
+ if (this.peek() === '"') this.advance();
646
+ return s;
647
+ }
648
+
649
+ _readWord() {
650
+ let w = "";
651
+ while (!this.eof() && /\w/.test(this.peek())) w += this.advance();
652
+ return w;
653
+ }
654
+
655
+ _skipToOpenBrace() {
656
+ // Skip text until we hit an unbalanced {, tracking parens/brackets
657
+ let paren = 0,
658
+ bracket = 0;
659
+ while (!this.eof()) {
660
+ const c = this.peek();
661
+ if (c === "(") paren++;
662
+ else if (c === ")") paren--;
663
+ else if (c === "[") bracket++;
664
+ else if (c === "]") bracket--;
665
+ else if (c === "{" && paren === 0 && bracket === 0) return;
666
+ else if (c === '"' || c === "'") {
667
+ this._skipGoString(c);
668
+ continue;
669
+ } else if (c === "`") {
670
+ this._skipRawString();
671
+ continue;
672
+ }
673
+ this.advance();
674
+ }
675
+ }
676
+
677
+ _skipBalanced(open, close) {
678
+ let depth = 0;
679
+ while (!this.eof()) {
680
+ const c = this.peek();
681
+ if (c === '"' || c === "'") {
682
+ this._skipGoString(c);
683
+ continue;
684
+ }
685
+ if (c === "`") {
686
+ this._skipRawString();
687
+ continue;
688
+ }
689
+ if (c === open) depth++;
690
+ else if (c === close) {
691
+ depth--;
692
+ if (depth === 0) {
693
+ this.advance();
694
+ return;
695
+ }
696
+ }
697
+ this.advance();
698
+ }
699
+ }
700
+
701
+ _skipGoString(quote) {
702
+ this.advance(); // opening quote
703
+ while (!this.eof()) {
704
+ const c = this.peek();
705
+ if (c === "\\") {
706
+ this.advance();
707
+ this.advance();
708
+ continue;
709
+ }
710
+ if (c === quote) {
711
+ this.advance();
712
+ return;
713
+ }
714
+ this.advance();
715
+ }
716
+ }
717
+
718
+ _skipRawString() {
719
+ this.advance(); // `
720
+ while (!this.eof() && this.peek() !== "`") this.advance();
721
+ if (!this.eof()) this.advance();
722
+ }
723
+
724
+ // Returns true if src[pos..] starts with `word` followed by a non-word char
725
+ _peekKeyword(word) {
726
+ if (!this.src.startsWith(word, this.pos)) return false;
727
+ const after = this.src[this.pos + word.length];
728
+ return !after || /\W/.test(after);
729
+ }
730
+
731
+ // Consume a keyword word
732
+ _matchKeyword(word) {
733
+ if (!this._peekKeyword(word)) return false;
734
+ for (let i = 0; i < word.length; i++) this.advance();
735
+ return true;
736
+ }
737
+ }