gofront 1.3.12 → 1.4.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 +18 -11
- package/package.json +1 -1
- package/src/codegen/index.js +26 -1
- package/src/codegen/runtime.js +11 -0
- package/src/codegen/templ.js +29 -0
- package/src/compiler.js +7 -1
- package/src/resolver.js +3 -1
- package/src/templ-lexer.js +107 -3
- package/src/templ-parser.js +29 -0
- package/src/typechecker/index.js +67 -2
package/README.md
CHANGED
|
@@ -283,6 +283,12 @@ example/templ/
|
|
|
283
283
|
`.templ` syntax at a glance:
|
|
284
284
|
|
|
285
285
|
```go
|
|
286
|
+
css cardStyle() {
|
|
287
|
+
background: #ffffff;
|
|
288
|
+
border-radius: 8px;
|
|
289
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
290
|
+
}
|
|
291
|
+
|
|
286
292
|
templ TodoItem(t Todo) {
|
|
287
293
|
<li class={ todoItemClass(t) } draggable="true" data-id={ t.id }>
|
|
288
294
|
<input type="checkbox" checked?={ t.done }/>
|
|
@@ -294,7 +300,7 @@ templ TodoItem(t Todo) {
|
|
|
294
300
|
}
|
|
295
301
|
|
|
296
302
|
templ AppView() {
|
|
297
|
-
<div class=
|
|
303
|
+
<div class={ cardStyle() }>
|
|
298
304
|
@Header(highCount(), syncMsg, syncCls)
|
|
299
305
|
@TodoList(visibleTodos())
|
|
300
306
|
</div>
|
|
@@ -302,6 +308,7 @@ templ AppView() {
|
|
|
302
308
|
```
|
|
303
309
|
|
|
304
310
|
Key syntax features:
|
|
311
|
+
- `css Name() { ... }` — scoped CSS declarations (templ.guide spec); compiles to a deterministic scoped class name (`gfc_<name>_<hash>`) and injects styles automatically into `<head>`
|
|
305
312
|
- `{ expr }` — interpolate any Go expression (strings auto-cast, others use `String()`)
|
|
306
313
|
- `attr={ expr }` — dynamic attribute value (expression)
|
|
307
314
|
- `attr?={ expr }` — boolean attribute (present/absent based on truthiness)
|
|
@@ -369,8 +376,8 @@ parity with the other examples (priority mode, validation, localStorage persiste
|
|
|
369
376
|
sync status, drag-and-drop reordering).
|
|
370
377
|
|
|
371
378
|
The templ example additionally demonstrates: **`.templ` file compilation**, template
|
|
372
|
-
components with parameters, `{
|
|
373
|
-
attributes, `@Component()` calls inside templates, `if / else if / else` chains,
|
|
379
|
+
components with parameters, scoped `css` declarations (`css Name() { ... }`), `{ expr }` interpolation,
|
|
380
|
+
`attr?={}` conditional boolean attributes, `@Component()` calls inside templates, `if / else if / else` chains,
|
|
374
381
|
`switch` blocks, `for range` loops, and `@templ.Raw()` for trusted HTML injection —
|
|
375
382
|
all inside template bodies.
|
|
376
383
|
|
|
@@ -620,7 +627,7 @@ signatures into GoFront's internal type representation.
|
|
|
620
627
|
| Unused import detection | ✓ |
|
|
621
628
|
| External `.d.ts` types | ✓ |
|
|
622
629
|
| npm package type resolution | ✓ |
|
|
623
|
-
| `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components visible across the whole package |
|
|
630
|
+
| `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components and `css` declarations visible across the whole package |
|
|
624
631
|
|
|
625
632
|
---
|
|
626
633
|
|
|
@@ -694,18 +701,18 @@ npm run test:e2e # E2E tests (Playwright, headless Chromium)
|
|
|
694
701
|
npm run test:all # all of the above
|
|
695
702
|
```
|
|
696
703
|
|
|
697
|
-
**Unit tests** (~1,
|
|
704
|
+
**Unit tests** (~1,400+) cover language features, type errors, edge cases, DOM (jsdom),
|
|
698
705
|
external `.d.ts`, npm resolver, multi-file compilation, embedded structs, string
|
|
699
706
|
formatting, map iteration order, integer overflow semantics, unused variable detection,
|
|
700
707
|
unused import detection, semantic difference verification, stdlib shim packages, generics,
|
|
701
708
|
the `testing` framework itself, and `.templ` file compilation (element rendering,
|
|
702
|
-
interpolation, boolean attrs, component calls, `
|
|
703
|
-
`switch/case/default`, `@templ.Raw()` raw HTML injection,
|
|
704
|
-
`.go`+`.templ` packages).
|
|
709
|
+
interpolation, boolean attrs, component calls, scoped `css` declarations with class hashing,
|
|
710
|
+
`if/else/else-if` chains, `for range`, `switch/case/default`, `@templ.Raw()` raw HTML injection,
|
|
711
|
+
SVG namespace handling, mixed `.go`+`.templ` packages).
|
|
705
712
|
|
|
706
|
-
**E2E tests** (~
|
|
713
|
+
**E2E tests** (~105, Playwright) run all five example apps in a real browser and verify
|
|
707
714
|
CRUD, filtering, priority mode, persistence (reload), drag-and-drop reordering, and sync
|
|
708
715
|
status. Per-app suites check app-specific behaviour: scoped styles, stats bar, loading
|
|
709
|
-
placeholder, `gom.If` conditional rendering, templ-specific features (`
|
|
710
|
-
priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
|
|
716
|
+
placeholder, `gom.If` conditional rendering, templ-specific features (scoped `css` injection,
|
|
717
|
+
`if/else` priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
|
|
711
718
|
rendering.
|
package/package.json
CHANGED
package/src/codegen/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
HELPER_ERROR,
|
|
28
28
|
HELPER_ERROR_IS,
|
|
29
29
|
HELPER_IFACE_BOX,
|
|
30
|
+
HELPER_INJECT_STYLES,
|
|
30
31
|
HELPER_LEN,
|
|
31
32
|
HELPER_PATH_CLEAN,
|
|
32
33
|
HELPER_S,
|
|
@@ -97,6 +98,7 @@ export class CodeGen {
|
|
|
97
98
|
this.structNames = new Set();
|
|
98
99
|
this.structFields = new Map();
|
|
99
100
|
this.namedWrapperNames = new Set();
|
|
101
|
+
this.collectedCss = [];
|
|
100
102
|
this.jsImports = jsImports;
|
|
101
103
|
this.bundledPackages = bundledPackages;
|
|
102
104
|
this.namedReturnVars = null; // names of current function's named return vars
|
|
@@ -191,6 +193,10 @@ export class CodeGen {
|
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
_callInitAndMain(initNames, program, isTest = false) {
|
|
196
|
+
if (this.collectedCss.length > 0) {
|
|
197
|
+
const uniqueCss = [...new Set(this.collectedCss)];
|
|
198
|
+
this.line(`__injectStyles(${JSON.stringify(uniqueCss.join("\n\n"))});`);
|
|
199
|
+
}
|
|
194
200
|
for (const name of initNames) this.line(`${name}();`);
|
|
195
201
|
if (
|
|
196
202
|
!isTest &&
|
|
@@ -201,6 +207,14 @@ export class CodeGen {
|
|
|
201
207
|
|
|
202
208
|
generate(program, options = {}) {
|
|
203
209
|
const isTest = options.isTest ?? false;
|
|
210
|
+
// Ensure _pkgName is set on all decls (generateAll sets it for
|
|
211
|
+
// multi-file; for single-file compiles we derive it from the AST).
|
|
212
|
+
const pkgName = program.pkg?.name;
|
|
213
|
+
if (pkgName) {
|
|
214
|
+
for (const d of program.decls) {
|
|
215
|
+
if (!d._pkgName) d._pkgName = pkgName;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
204
218
|
this._renameReservedIdents(program.decls);
|
|
205
219
|
const methods = this._collectDecls(program);
|
|
206
220
|
this._emitJsImports();
|
|
@@ -227,7 +241,7 @@ export class CodeGen {
|
|
|
227
241
|
return this.out.join("\n");
|
|
228
242
|
}
|
|
229
243
|
|
|
230
|
-
// Emits FuncDecl and
|
|
244
|
+
// Emits FuncDecl, TemplDecl, and CssDecl nodes; returns renamed init function names.
|
|
231
245
|
_emitFuncDecls(program) {
|
|
232
246
|
let initCount = 0;
|
|
233
247
|
const initNames = [];
|
|
@@ -247,6 +261,10 @@ export class CodeGen {
|
|
|
247
261
|
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
248
262
|
this.genTemplDecl(d);
|
|
249
263
|
this.blank();
|
|
264
|
+
} else if (d.kind === "CssDecl") {
|
|
265
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
266
|
+
this.genCssDecl(d);
|
|
267
|
+
this.blank();
|
|
250
268
|
}
|
|
251
269
|
}
|
|
252
270
|
return initNames;
|
|
@@ -306,6 +324,7 @@ export class CodeGen {
|
|
|
306
324
|
MethodDecl: "_renameMethodDeclNode",
|
|
307
325
|
FuncLit: "_renameSignature",
|
|
308
326
|
TemplDecl: "_renameSignature",
|
|
327
|
+
CssDecl: "_renameSignature",
|
|
309
328
|
VarDecl: "_renameDeclSpecs",
|
|
310
329
|
ConstDecl: "_renameDeclSpecs",
|
|
311
330
|
TypeSwitchStmt: "_renameTypeSwitchNode",
|
|
@@ -381,6 +400,7 @@ export class CodeGen {
|
|
|
381
400
|
_prependHelpers(isTest = false) {
|
|
382
401
|
const needsTesting = isTest || this._usesTesting;
|
|
383
402
|
const HELPER_MAP = [
|
|
403
|
+
[this.collectedCss.length > 0, HELPER_INJECT_STYLES],
|
|
384
404
|
[this._usesLen, HELPER_LEN],
|
|
385
405
|
[this._usesAppend, HELPER_APPEND],
|
|
386
406
|
[this._usesSliceGuard, HELPER_S],
|
|
@@ -414,12 +434,17 @@ export class CodeGen {
|
|
|
414
434
|
for (let i = 0; i < programs.length; i++) {
|
|
415
435
|
for (const decl of programs[i].decls) {
|
|
416
436
|
decl._srcFileIdx = i;
|
|
437
|
+
decl._pkgName = programs[i].pkg?.name;
|
|
417
438
|
}
|
|
418
439
|
}
|
|
419
440
|
const merged = { decls: programs.flatMap((p) => p.decls) };
|
|
420
441
|
return this.generate(merged, options);
|
|
421
442
|
}
|
|
422
443
|
|
|
444
|
+
getCss() {
|
|
445
|
+
return [...new Set(this.collectedCss)].join("\n\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
423
448
|
getMappings() {
|
|
424
449
|
return this._srcMappings;
|
|
425
450
|
}
|
package/src/codegen/runtime.js
CHANGED
|
@@ -312,3 +312,14 @@ var __GoFront_T = __GoFront_T || class {
|
|
|
312
312
|
}
|
|
313
313
|
};
|
|
314
314
|
var __testing_T = __GoFront_T;`;
|
|
315
|
+
|
|
316
|
+
export const HELPER_INJECT_STYLES = `var __injectStyles = __injectStyles || function(css) {
|
|
317
|
+
if (typeof document === "undefined" || !document || !document.head || !css) return;
|
|
318
|
+
let s = document.getElementById("gofront-styles");
|
|
319
|
+
if (!s) {
|
|
320
|
+
s = document.createElement("style");
|
|
321
|
+
s.id = "gofront-styles";
|
|
322
|
+
document.head.appendChild(s);
|
|
323
|
+
}
|
|
324
|
+
s.textContent += "\\n" + css;
|
|
325
|
+
};`;
|
package/src/codegen/templ.js
CHANGED
|
@@ -26,6 +26,35 @@ export const templGenMethods = {
|
|
|
26
26
|
this.line("}");
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
+
genCssDecl(decl) {
|
|
30
|
+
if (decl.params && decl.params.length > 0) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`css ${decl.name}: parameterized css declarations are not yet supported`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const className = this.getCssClassName(decl);
|
|
36
|
+
const scopedCss = `.${className} {\n${decl.cssText.trim()}\n}`;
|
|
37
|
+
this.collectedCss.push(scopedCss);
|
|
38
|
+
|
|
39
|
+
this.line(`function ${decl.name}() {`);
|
|
40
|
+
this.indented(() => {
|
|
41
|
+
this.line(`return ${JSON.stringify(className)};`);
|
|
42
|
+
});
|
|
43
|
+
this.line("}");
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getCssClassName(decl) {
|
|
47
|
+
if (decl._className) return decl._className;
|
|
48
|
+
let h = 2166136261 >>> 0;
|
|
49
|
+
const str = `${decl._pkgName || ""}_${decl.name}_${decl.cssText}`;
|
|
50
|
+
for (let i = 0; i < str.length; i++) {
|
|
51
|
+
h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
const hash = (h >>> 0).toString(36);
|
|
54
|
+
decl._className = `gfc_${decl.name}_${hash}`;
|
|
55
|
+
return decl._className;
|
|
56
|
+
},
|
|
57
|
+
|
|
29
58
|
// Emit mount statements for an array of TemplNodes into parent variable `p`.
|
|
30
59
|
_genTemplNodes(nodes, p) {
|
|
31
60
|
if (!nodes || nodes.length === 0) return;
|
package/src/compiler.js
CHANGED
|
@@ -310,7 +310,7 @@ export function compileSingleFile(inputPath, options = {}) {
|
|
|
310
310
|
attachSourceMap(merged, outputDir);
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
return { js: merged.js };
|
|
313
|
+
return { js: merged.js, css: cg.getCss() };
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
export function compileDir(dir, options = {}) {
|
|
@@ -398,9 +398,15 @@ export function compileFiles(files, options = {}) {
|
|
|
398
398
|
attachSourceMap(merged, outputDir);
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
const mainCss = codegen.getCss();
|
|
402
|
+
const allCss = [...preambles.map((p) => p.css).filter(Boolean), mainCss]
|
|
403
|
+
.filter(Boolean)
|
|
404
|
+
.join("\n\n");
|
|
405
|
+
|
|
401
406
|
return {
|
|
402
407
|
pkgName,
|
|
403
408
|
js: merged.js,
|
|
409
|
+
css: allCss,
|
|
404
410
|
programs,
|
|
405
411
|
exportedSymbols: checker.getExportedSymbols(),
|
|
406
412
|
exportedTypes: checker.getExportedTypes(),
|
package/src/resolver.js
CHANGED
|
@@ -65,7 +65,9 @@ export function resolveGwDir(importPath, fromFile) {
|
|
|
65
65
|
const fromDir = dirname(resolve(fromFile));
|
|
66
66
|
const dir = resolve(fromDir, importPath);
|
|
67
67
|
if (!existsSync(dir)) return null;
|
|
68
|
-
const gwFiles = readdirSync(dir).filter(
|
|
68
|
+
const gwFiles = readdirSync(dir).filter(
|
|
69
|
+
(f) => f.endsWith(".go") || f.endsWith(".templ"),
|
|
70
|
+
);
|
|
69
71
|
return gwFiles.length > 0 ? dir : null;
|
|
70
72
|
}
|
|
71
73
|
|
package/src/templ-lexer.js
CHANGED
|
@@ -35,6 +35,8 @@ export const TT = {
|
|
|
35
35
|
TEMPL_CASE: "TEMPL_CASE", // case expr:
|
|
36
36
|
TEMPL_DEFAULT: "TEMPL_DEFAULT", // default:
|
|
37
37
|
TEMPL_END: "TEMPL_END", // closing } of if/for/switch/templ body
|
|
38
|
+
CSS_KW: "css",
|
|
39
|
+
CSS_BODY: "CSS_BODY",
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
// Describes a parsed HTML attribute
|
|
@@ -123,6 +125,13 @@ export class TemplLexer {
|
|
|
123
125
|
continue;
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
// Detect `css` keyword at identifier boundaries
|
|
129
|
+
if (this._matchKeyword("css")) {
|
|
130
|
+
this.tokens.push(new Token(TT.CSS_KW, "css", this.line, this.col));
|
|
131
|
+
this._lexCssDecl();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
126
135
|
// Everything else: scan to end of top-level declaration using the
|
|
127
136
|
// raw Go lexer, accumulating tokens until the next `templ` keyword
|
|
128
137
|
// or EOF.
|
|
@@ -135,7 +144,7 @@ export class TemplLexer {
|
|
|
135
144
|
|
|
136
145
|
// ── Go chunk lexing ──────────────────────────────────────────
|
|
137
146
|
|
|
138
|
-
// Scan ahead to find the next `templ ` keyword at top level (not inside
|
|
147
|
+
// Scan ahead to find the next `templ ` or `css ` keyword at top level (not inside
|
|
139
148
|
// braces/parens/strings/comments). Collect all source text up to that
|
|
140
149
|
// point, lex it as normal Go, and append the tokens.
|
|
141
150
|
_lexGoChunk() {
|
|
@@ -143,8 +152,12 @@ export class TemplLexer {
|
|
|
143
152
|
let depth = 0; // brace depth
|
|
144
153
|
|
|
145
154
|
while (!this.eof()) {
|
|
146
|
-
// Check for `templ` keyword at top level (depth === 0)
|
|
147
|
-
if (
|
|
155
|
+
// Check for `templ` or `css` keyword at top level (depth === 0)
|
|
156
|
+
if (
|
|
157
|
+
depth === 0 &&
|
|
158
|
+
(this._peekKeyword("templ") || this._peekKeyword("css"))
|
|
159
|
+
)
|
|
160
|
+
break;
|
|
148
161
|
|
|
149
162
|
const ch = this.peek();
|
|
150
163
|
|
|
@@ -222,6 +235,97 @@ export class TemplLexer {
|
|
|
222
235
|
this._lexHtmlBody(1);
|
|
223
236
|
}
|
|
224
237
|
|
|
238
|
+
// ── CSS declaration lexing ───────────────────────────────────
|
|
239
|
+
|
|
240
|
+
_lexCssDecl() {
|
|
241
|
+
this.skipWhitespace();
|
|
242
|
+
|
|
243
|
+
// Name (identifier)
|
|
244
|
+
const nameStart = this.pos;
|
|
245
|
+
while (!this.eof() && /\w/.test(this.peek())) this.advance();
|
|
246
|
+
const name = this.src.slice(nameStart, this.pos);
|
|
247
|
+
if (!name) this.err("expected name after 'css'");
|
|
248
|
+
this.tokens.push(new Token(T.IDENT, name, this.line, this.col));
|
|
249
|
+
|
|
250
|
+
this.skipWhitespace();
|
|
251
|
+
|
|
252
|
+
// Parameter list (balanced parens)
|
|
253
|
+
if (this.peek() !== "(") this.err("expected '(' after css name");
|
|
254
|
+
const paramStart = this.pos;
|
|
255
|
+
this._skipBalanced("(", ")");
|
|
256
|
+
const paramSrc = this.src.slice(paramStart, this.pos);
|
|
257
|
+
for (const t of this._goTokens(paramSrc)) this.tokens.push(t);
|
|
258
|
+
|
|
259
|
+
this.skipWhitespace();
|
|
260
|
+
|
|
261
|
+
// Opening { of body
|
|
262
|
+
if (this.peek() !== "{") this.err("expected '{' after css params");
|
|
263
|
+
this.advance(); // consume {
|
|
264
|
+
this.tokens.push(new Token(T.LBRACE, "{", this.line, this.col));
|
|
265
|
+
|
|
266
|
+
this._lexCssBody();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_skipCssString(quote) {
|
|
270
|
+
this.advance(); // consume opening quote
|
|
271
|
+
while (!this.eof()) {
|
|
272
|
+
const ch = this.peek();
|
|
273
|
+
if (ch === "\\") {
|
|
274
|
+
this.advance();
|
|
275
|
+
if (!this.eof()) this.advance();
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (ch === quote) {
|
|
279
|
+
this.advance();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (ch === "\n") {
|
|
283
|
+
this.err("unterminated string in css block");
|
|
284
|
+
}
|
|
285
|
+
this.advance();
|
|
286
|
+
}
|
|
287
|
+
this.err("unterminated string in css block");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_lexCssBody() {
|
|
291
|
+
const start = this.pos;
|
|
292
|
+
let depth = 1;
|
|
293
|
+
while (!this.eof()) {
|
|
294
|
+
const ch = this.peek();
|
|
295
|
+
// Skip CSS comments /* ... */
|
|
296
|
+
if (ch === "/" && this.peek(1) === "*") {
|
|
297
|
+
this.advance();
|
|
298
|
+
this.advance();
|
|
299
|
+
this.skipBlockComment();
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// Skip strings
|
|
303
|
+
if (ch === '"' || ch === "'") {
|
|
304
|
+
this._skipCssString(ch);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (ch === "{") {
|
|
308
|
+
depth++;
|
|
309
|
+
this.advance();
|
|
310
|
+
} else if (ch === "}") {
|
|
311
|
+
depth--;
|
|
312
|
+
if (depth === 0) {
|
|
313
|
+
const cssText = this.src.slice(start, this.pos);
|
|
314
|
+
this.tokens.push(
|
|
315
|
+
new Token(TT.CSS_BODY, cssText, this.line, this.col),
|
|
316
|
+
);
|
|
317
|
+
this.advance(); // consume closing }
|
|
318
|
+
this.tokens.push(new Token(T.RBRACE, "}", this.line, this.col));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.advance();
|
|
322
|
+
} else {
|
|
323
|
+
this.advance();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.err("unclosed css block");
|
|
327
|
+
}
|
|
328
|
+
|
|
225
329
|
// Lex the HTML body of a templ declaration.
|
|
226
330
|
// depth: current brace depth (starts at 1 for the opening { of templ)
|
|
227
331
|
_lexHtmlBody(depth) {
|
package/src/templ-parser.js
CHANGED
|
@@ -42,9 +42,38 @@ export class TemplParser extends Parser {
|
|
|
42
42
|
|
|
43
43
|
parseTopDecl() {
|
|
44
44
|
if (this.check(TT.TEMPL_KW)) return this.parseTemplDecl();
|
|
45
|
+
if (this.check(TT.CSS_KW)) return this.parseCssDecl();
|
|
45
46
|
return super.parseTopDecl();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// ── CssDecl ───────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
parseCssDecl() {
|
|
52
|
+
const _line = this.peek().line;
|
|
53
|
+
this.advance(); // consume CSS_KW
|
|
54
|
+
|
|
55
|
+
const name = this.expect(T.IDENT).value;
|
|
56
|
+
|
|
57
|
+
// Parse parameter list (standard Go params: LPAREN...RPAREN)
|
|
58
|
+
const params = this.parseParamList();
|
|
59
|
+
|
|
60
|
+
// Optional semicolon between ) and {
|
|
61
|
+
this.semi();
|
|
62
|
+
|
|
63
|
+
// Consume opening {
|
|
64
|
+
this.expect(T.LBRACE);
|
|
65
|
+
|
|
66
|
+
// Consume CSS_BODY
|
|
67
|
+
const bodyToken = this.expect(TT.CSS_BODY);
|
|
68
|
+
const cssText = bodyToken.value;
|
|
69
|
+
|
|
70
|
+
// Consume closing }
|
|
71
|
+
this.expect(T.RBRACE);
|
|
72
|
+
this.semi();
|
|
73
|
+
|
|
74
|
+
return { kind: "CssDecl", name, params, cssText, _line };
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
// ── TemplDecl ─────────────────────────────────────────────────
|
|
49
78
|
|
|
50
79
|
parseTemplDecl() {
|
package/src/typechecker/index.js
CHANGED
|
@@ -241,6 +241,7 @@ export class TypeChecker {
|
|
|
241
241
|
if (d.kind === "FuncDecl" || d.kind === "MethodDecl")
|
|
242
242
|
this.collectFunc(d);
|
|
243
243
|
else if (d.kind === "TemplDecl") this._collectTemplDecl(d);
|
|
244
|
+
else if (d.kind === "CssDecl") this._collectCssDecl(d);
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
}
|
|
@@ -267,7 +268,11 @@ export class TypeChecker {
|
|
|
267
268
|
for (const p of programs) {
|
|
268
269
|
this._setCurrentFile(p);
|
|
269
270
|
for (const d of p.decls) {
|
|
270
|
-
if (
|
|
271
|
+
if (
|
|
272
|
+
d.kind === "FuncDecl" ||
|
|
273
|
+
d.kind === "MethodDecl" ||
|
|
274
|
+
d.kind === "TemplDecl"
|
|
275
|
+
) {
|
|
271
276
|
this.checkTopDecl(d, this.globals);
|
|
272
277
|
}
|
|
273
278
|
}
|
|
@@ -457,11 +462,71 @@ export class TypeChecker {
|
|
|
457
462
|
this.checkConstDecl(decl, scope);
|
|
458
463
|
break;
|
|
459
464
|
case "TypeDecl":
|
|
465
|
+
case "CssDecl":
|
|
466
|
+
break;
|
|
460
467
|
case "TemplDecl":
|
|
461
|
-
|
|
468
|
+
this._markTemplNodesUsed(decl.body);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_markTokensUsed(tokens) {
|
|
474
|
+
if (!tokens) return;
|
|
475
|
+
for (const t of tokens) {
|
|
476
|
+
if (t.type === "IDENT" || t.type?.name === "IDENT") {
|
|
477
|
+
this.globals.lookup(t.value);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_markTemplNodesUsed(nodes) {
|
|
483
|
+
if (!nodes) return;
|
|
484
|
+
for (const n of nodes) {
|
|
485
|
+
if (n.kind === "TemplElement") {
|
|
486
|
+
for (const a of n.attrs || []) {
|
|
487
|
+
if (a.tokens) this._markTokensUsed(a.tokens);
|
|
488
|
+
}
|
|
489
|
+
this._markTemplNodesUsed(n.children);
|
|
490
|
+
} else if (n.kind === "TemplExpr" || n.kind === "TemplComponent") {
|
|
491
|
+
this._markTokensUsed(n.tokens);
|
|
492
|
+
} else if (n.kind === "TemplIf") {
|
|
493
|
+
this._markTokensUsed(n.condTokens);
|
|
494
|
+
this._markTemplNodesUsed(n.then);
|
|
495
|
+
this._markTemplNodesUsed(n.else_);
|
|
496
|
+
} else if (n.kind === "TemplFor") {
|
|
497
|
+
this._markTokensUsed(n.stmtTokens);
|
|
498
|
+
this._markTemplNodesUsed(n.body);
|
|
499
|
+
} else if (n.kind === "TemplSwitch") {
|
|
500
|
+
this._markTokensUsed(n.exprTokens);
|
|
501
|
+
for (const c of n.cases || []) {
|
|
502
|
+
this._markTokensUsed(c.caseTokens);
|
|
503
|
+
this._markTemplNodesUsed(c.body);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
462
506
|
}
|
|
463
507
|
}
|
|
464
508
|
|
|
509
|
+
_collectCssDecl(decl) {
|
|
510
|
+
const paramTypes = (decl.params || []).map((p) =>
|
|
511
|
+
this.resolveTypeNode(p.type, this.globals),
|
|
512
|
+
);
|
|
513
|
+
const stringType = this.types.get("string") ?? {
|
|
514
|
+
kind: "basic",
|
|
515
|
+
name: "string",
|
|
516
|
+
};
|
|
517
|
+
const funcType = {
|
|
518
|
+
kind: "func",
|
|
519
|
+
params: paramTypes,
|
|
520
|
+
returns: [stringType],
|
|
521
|
+
variadic: false,
|
|
522
|
+
async: false,
|
|
523
|
+
};
|
|
524
|
+
if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
|
|
525
|
+
this.err(`${decl.name} redeclared in this block`, decl);
|
|
526
|
+
}
|
|
527
|
+
this.globals.define(decl.name, funcType);
|
|
528
|
+
}
|
|
529
|
+
|
|
465
530
|
_collectTemplDecl(decl) {
|
|
466
531
|
const paramTypes = decl.params.map((p) =>
|
|
467
532
|
this.resolveTypeNode(p.type, this.globals),
|