gofront 0.0.8 → 0.0.9

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.
@@ -0,0 +1,235 @@
1
+ // Templ parser: extends the Go Parser to handle TemplDecl nodes.
2
+ //
3
+ // A .templ file is tokenised by TemplLexer into a mixed stream of standard Go
4
+ // tokens (T.*) and HTML/templ tokens (TT.*). This parser handles TT.TEMPL_KW
5
+ // in parseTopDecl; everything else is delegated to the base Parser.
6
+ //
7
+ // Produced AST additions:
8
+ // TemplDecl { kind, name, params, body: TemplNode[] }
9
+ // TemplElement { kind, tag, attrs, children }
10
+ // TemplText { kind, value }
11
+ // TemplExpr { kind, tokens } ← raw Go token array
12
+ // TemplComponent { kind, tokens } ← raw Go token array for call
13
+ // TemplChildren { kind }
14
+ // TemplIf { kind, condTokens, then: TemplNode[], else_: TemplNode[] | null }
15
+ // TemplFor { kind, stmtTokens, body: TemplNode[] }
16
+ // TemplSwitch { kind, exprTokens, cases: TemplCase[] }
17
+ // TemplCase { kind, caseTokens: tokens | null (null = default), body: TemplNode[] }
18
+
19
+ import { T } from "./lexer.js";
20
+ import { Parser } from "./parser.js";
21
+ import { TT } from "./templ-lexer.js";
22
+
23
+ const VOID_ELEMENTS = new Set([
24
+ "area",
25
+ "base",
26
+ "br",
27
+ "col",
28
+ "embed",
29
+ "hr",
30
+ "img",
31
+ "input",
32
+ "link",
33
+ "meta",
34
+ "param",
35
+ "source",
36
+ "track",
37
+ "wbr",
38
+ ]);
39
+
40
+ export class TemplParser extends Parser {
41
+ // ── Top-level declaration override ───────────────────────────
42
+
43
+ parseTopDecl() {
44
+ if (this.check(TT.TEMPL_KW)) return this.parseTemplDecl();
45
+ return super.parseTopDecl();
46
+ }
47
+
48
+ // ── TemplDecl ─────────────────────────────────────────────────
49
+
50
+ parseTemplDecl() {
51
+ const _line = this.peek().line;
52
+ this.advance(); // consume TEMPL_KW
53
+
54
+ const name = this.expect(T.IDENT).value;
55
+
56
+ // Parse parameter list (standard Go params: LPAREN...RPAREN)
57
+ const params = this.parseParamList();
58
+
59
+ // Optional semicolon between ) and {
60
+ this.semi();
61
+
62
+ // Consume the opening { of the HTML body
63
+ this.expect(T.LBRACE);
64
+
65
+ // Parse the HTML body; parseHtmlChildren consumes the closing RBRACE
66
+ const body = this.parseHtmlChildren();
67
+
68
+ this.semi();
69
+
70
+ return { kind: "TemplDecl", name, params, body, _line };
71
+ }
72
+
73
+ // ── HTML node parsing ─────────────────────────────────────────
74
+
75
+ // Parse zero or more HTML nodes until RBRACE (end of body) or EOF.
76
+ // Consumes the RBRACE.
77
+ parseHtmlChildren() {
78
+ const nodes = [];
79
+ while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
80
+ if (this.check(TT.TEMPL_ELSE)) break; // handled by caller (parseTemplIf)
81
+ const node = this.parseHtmlNode();
82
+ if (node) nodes.push(node);
83
+ }
84
+ if (this.check(T.RBRACE)) this.advance(); // consume }
85
+ return nodes;
86
+ }
87
+
88
+ // Parse a single HTML node from the current token.
89
+ parseHtmlNode() {
90
+ const t = this.peek();
91
+ switch (t.type) {
92
+ case TT.HTML_OPEN:
93
+ return this.parseHtmlElement();
94
+ case TT.HTML_SELF: {
95
+ this.advance();
96
+ return {
97
+ kind: "TemplElement",
98
+ tag: t.value.tag,
99
+ attrs: t.value.attrs,
100
+ children: [],
101
+ };
102
+ }
103
+ case TT.HTML_TEXT:
104
+ this.advance();
105
+ return { kind: "TemplText", value: t.value };
106
+ case TT.TEMPL_EXPR:
107
+ this.advance();
108
+ return { kind: "TemplExpr", tokens: t.value };
109
+ case TT.TEMPL_COMP:
110
+ this.advance();
111
+ return { kind: "TemplComponent", tokens: t.value };
112
+ case TT.TEMPL_CHILDREN:
113
+ this.advance();
114
+ return { kind: "TemplChildren" };
115
+ case TT.TEMPL_IF: {
116
+ this.advance();
117
+ return this.parseTemplIf(t);
118
+ }
119
+ case TT.TEMPL_FOR: {
120
+ this.advance();
121
+ return this.parseTemplFor(t);
122
+ }
123
+ case TT.TEMPL_SWITCH: {
124
+ this.advance();
125
+ return this.parseTemplSwitch(t);
126
+ }
127
+ default:
128
+ // Unknown token inside HTML body — skip it to avoid infinite loops
129
+ this.advance();
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // Parse children of an element until the matching close tag.
135
+ parseHtmlElement() {
136
+ const openTok = this.advance(); // HTML_OPEN
137
+ const { tag, attrs } = openTok.value;
138
+
139
+ // Void elements have no children and no close tag even without />
140
+ if (VOID_ELEMENTS.has(tag)) {
141
+ return { kind: "TemplElement", tag, attrs, children: [] };
142
+ }
143
+
144
+ const children = [];
145
+ while (!this.check(T.EOF) && !this.check(T.RBRACE)) {
146
+ if (this.check(TT.HTML_CLOSE)) break;
147
+ // Also break on a close tag for a different ancestor (shouldn't happen in
148
+ // well-formed HTML, but prevents infinite loops on malformed input)
149
+ if (this.check(TT.TEMPL_ELSE)) break;
150
+ const node = this.parseHtmlNode();
151
+ if (node) children.push(node);
152
+ }
153
+ if (this.check(TT.HTML_CLOSE)) {
154
+ const closeTag = this.peek().value;
155
+ if (closeTag !== tag) {
156
+ throw new Error(
157
+ `${this.filename ?? "<templ>"}:${this.peek().line}: unclosed tag <${tag}>, got </${closeTag}>`,
158
+ );
159
+ }
160
+ this.advance(); // consume HTML_CLOSE
161
+ } else if (!VOID_ELEMENTS.has(tag)) {
162
+ // Non-void element with no close tag — parse error
163
+ throw new Error(
164
+ `${this.filename ?? "<templ>"}:${openTok.line}: unclosed tag <${tag}>`,
165
+ );
166
+ }
167
+ return { kind: "TemplElement", tag, attrs, children };
168
+ }
169
+
170
+ // Parse a TemplIf node. `tok` is the TEMPL_IF token (already advanced).
171
+ parseTemplIf(tok) {
172
+ const condTokens = tok.value; // array of Go tokens
173
+ const then = this.parseHtmlChildren(); // reads body until RBRACE
174
+ let else_ = null;
175
+ if (this.check(TT.TEMPL_ELSE)) {
176
+ this.advance(); // consume TEMPL_ELSE
177
+ if (this.check(TT.TEMPL_IF)) {
178
+ const t2 = this.advance();
179
+ else_ = [this.parseTemplIf(t2)]; // else if → wrap in array
180
+ } else {
181
+ else_ = this.parseHtmlChildren(); // else body until RBRACE
182
+ }
183
+ }
184
+ return { kind: "TemplIf", condTokens, then, else_ };
185
+ }
186
+
187
+ // Parse a TemplFor node. `tok` is the TEMPL_FOR token (already advanced).
188
+ parseTemplFor(tok) {
189
+ const stmtTokens = tok.value;
190
+ const body = this.parseHtmlChildren();
191
+ return { kind: "TemplFor", stmtTokens, body };
192
+ }
193
+
194
+ // Parse a TemplSwitch node. `tok` is the TEMPL_SWITCH token (already advanced).
195
+ parseTemplSwitch(tok) {
196
+ const exprTokens = tok.value;
197
+ const cases = [];
198
+ while (
199
+ !this.check(TT.TEMPL_END) &&
200
+ !this.check(T.RBRACE) &&
201
+ !this.check(T.EOF)
202
+ ) {
203
+ if (this.check(TT.TEMPL_CASE)) {
204
+ const caseTok = this.advance();
205
+ const body = this.parseTemplCaseBody();
206
+ cases.push({ kind: "TemplCase", caseTokens: caseTok.value, body });
207
+ } else if (this.check(TT.TEMPL_DEFAULT)) {
208
+ this.advance();
209
+ const body = this.parseTemplCaseBody();
210
+ cases.push({ kind: "TemplCase", caseTokens: null, body });
211
+ } else {
212
+ this.advance(); // skip unexpected
213
+ }
214
+ }
215
+ if (this.check(TT.TEMPL_END)) this.advance(); // consume closing }
216
+ return { kind: "TemplSwitch", exprTokens, cases };
217
+ }
218
+
219
+ // Parse HTML nodes for a switch case body — stops at CASE/DEFAULT/END/RBRACE.
220
+ parseTemplCaseBody() {
221
+ const nodes = [];
222
+ while (
223
+ !this.check(TT.TEMPL_CASE) &&
224
+ !this.check(TT.TEMPL_DEFAULT) &&
225
+ !this.check(TT.TEMPL_END) &&
226
+ !this.check(T.RBRACE) &&
227
+ !this.check(T.EOF)
228
+ ) {
229
+ if (this.check(TT.TEMPL_ELSE)) break;
230
+ const node = this.parseHtmlNode();
231
+ if (node) nodes.push(node);
232
+ }
233
+ return nodes;
234
+ }
235
+ }
@@ -15,19 +15,22 @@
15
15
  export class TypeCheckError extends Error {
16
16
  constructor(msg, node, filename, sourceCode) {
17
17
  const lineNum = node?.line || node?._line;
18
+ const colNum = node?.col || node?._col;
18
19
  const loc = filename
19
20
  ? lineNum
20
- ? ` in ${filename} at line ${lineNum}`
21
+ ? ` in ${filename} at line ${lineNum}:${colNum ?? 1}`
21
22
  : ` in ${filename}`
22
23
  : lineNum
23
- ? ` at line ${lineNum}`
24
+ ? ` at line ${lineNum}:${colNum ?? 1}`
24
25
  : "";
25
26
  let lineContext = "";
26
27
  if (lineNum && sourceCode) {
27
28
  const lines = sourceCode.split("\n");
28
29
  const lineStr = lines[lineNum - 1];
29
30
  if (lineStr !== undefined) {
30
- lineContext = `\n ${lineNum} | ${lineStr}`;
31
+ const prefix = ` ${lineNum} | `;
32
+ const caretPad = " ".repeat((colNum ?? 1) - 1);
33
+ lineContext = `\n${prefix}${lineStr}\n${" ".repeat(prefix.length)}${caretPad}^`;
31
34
  }
32
35
  }
33
36
  super(`Type error${loc}: ${msg}${lineContext}`);
@@ -157,6 +157,8 @@ export class TypeChecker {
157
157
  for (const decl of program.decls) {
158
158
  if (decl.kind === "FuncDecl" || decl.kind === "MethodDecl") {
159
159
  this.collectFunc(decl);
160
+ } else if (decl.kind === "TemplDecl") {
161
+ this._collectTemplDecl(decl);
160
162
  }
161
163
  }
162
164
  // Pass 2.1: Promote embedded methods
@@ -203,9 +205,11 @@ export class TypeChecker {
203
205
  for (const p of programs) {
204
206
  this._currentFile = p._filename ?? null;
205
207
  this._currentSource = p._source ?? null;
206
- for (const d of p.decls)
208
+ for (const d of p.decls) {
207
209
  if (d.kind === "FuncDecl" || d.kind === "MethodDecl")
208
210
  this.collectFunc(d);
211
+ else if (d.kind === "TemplDecl") this._collectTemplDecl(d);
212
+ }
209
213
  }
210
214
 
211
215
  // Pass 2.1: Promote embedded methods
@@ -407,8 +411,32 @@ export class TypeChecker {
407
411
  this.checkConstDecl(decl, scope);
408
412
  break;
409
413
  case "TypeDecl":
410
- break; // already collected
414
+ case "TemplDecl":
415
+ break; // already collected / no body to check
416
+ }
417
+ }
418
+
419
+ _collectTemplDecl(decl) {
420
+ const paramTypes = decl.params.map((p) =>
421
+ this.resolveTypeNode(p.type, this.globals),
422
+ );
423
+ const gomNodeType = this.types.get("gom.Node") ?? {
424
+ kind: "basic",
425
+ name: "any",
426
+ };
427
+ const isVariadic =
428
+ decl.params.length > 0 && decl.params[decl.params.length - 1].variadic;
429
+ const funcType = {
430
+ kind: "func",
431
+ params: paramTypes,
432
+ returns: [gomNodeType],
433
+ variadic: isVariadic,
434
+ async: false,
435
+ };
436
+ if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
437
+ this.err(`${decl.name} redeclared in this block`, decl);
411
438
  }
439
+ this.globals.define(decl.name, funcType);
412
440
  }
413
441
 
414
442
  checkFuncDecl(decl, outer) {