kopscript 0.5.1 → 0.7.1
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/LLM.md +45 -2
- package/README.md +91 -9
- package/dist/cli.js +10 -2
- package/dist/lexer.js +1 -0
- package/dist/modules.js +113 -8
- package/dist/parser.js +56 -1
- package/dist/template_compiler.js +276 -0
- package/dist/template_lexer.js +162 -0
- package/dist/template_parser.js +249 -0
- package/dist/tokens.js +1 -0
- package/package.json +8 -3
package/LLM.md
CHANGED
|
@@ -421,9 +421,50 @@ exactly the members used, from that package's real module specifier. See Kopular
|
|
|
421
421
|
raw string HeroHtml from "./hero.html"; // reads the file at compile time, type is always string
|
|
422
422
|
```
|
|
423
423
|
|
|
424
|
+
General-purpose only — for a component's actual markup, use `template from` below instead.
|
|
425
|
+
|
|
426
|
+
## Templates — `template from` (markup files)
|
|
427
|
+
|
|
428
|
+
```ks
|
|
429
|
+
class Counter : Component {
|
|
430
|
+
public state<number> Count;
|
|
431
|
+
constructor() : base() { this.Count = state(0); }
|
|
432
|
+
public void Increment() { this.Count.Value = this.Count.Value + 1; }
|
|
433
|
+
template from "./counter.html"; // replaces a hand-written Render() entirely
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```html
|
|
438
|
+
<!-- counter.html -->
|
|
439
|
+
<button (click)="Increment()">Count: {{ Count.Value }}</button>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- One `template` per class; a class with both `template` and a hand-written `Render()` is a
|
|
443
|
+
compile error.
|
|
444
|
+
- Desugars, between parsing and checking, into the exact same `MethodDecl` AST a
|
|
445
|
+
hand-written `Render()` would produce — no runtime template engine, no virtual DOM.
|
|
446
|
+
`{{ }}`/binding contents are real KopScript, parsed and type-checked normally.
|
|
447
|
+
- Bindings: `{{ expr }}` (text interpolation, → `InterpolatedStringLiteral`), `(event)="stmt"`
|
|
448
|
+
(→ `addEventListener`), `[prop]="expr"` (→ plain assignment, checked like any member
|
|
449
|
+
assignment), static `attr="..."` (`class` aliases to `className`).
|
|
450
|
+
- Structural directives: `*if="expr"` (→ real `if`), `*for="Type varName of expr"` (→ real
|
|
451
|
+
`for..in`; the element type is explicit — no inference, same stance as Generics).
|
|
452
|
+
At most one structural directive per element.
|
|
453
|
+
- **Auto-subscribe**: a `state<T>` field declared directly on the class and referenced
|
|
454
|
+
directly in the template gets `Subscribe((v) => this.Update())` wired automatically — no
|
|
455
|
+
manual `Subscribe` needed for that field. State reached indirectly (through a method call,
|
|
456
|
+
or `this.SomeService.Count`) still needs a manual `Subscribe`, unchanged from before.
|
|
457
|
+
- Exactly one top-level element per template (no auto-wrap — hard error). No mixing text and
|
|
458
|
+
element children under one element. No two-way binding, no pipes, no stacked directives.
|
|
459
|
+
- `ks watch` tracks the referenced `.html` file as well as `.ks` dependencies.
|
|
460
|
+
- Layering note: `template from` is kopscript grammar, but what it desugars *to*
|
|
461
|
+
(`document.createElement`/`.appendChild`/`.textContent`/`.addEventListener`) assumes
|
|
462
|
+
Kopular's `dom.ks` DOM surface specifically — a deliberate, documented coupling, not a
|
|
463
|
+
generic pluggable target.
|
|
464
|
+
|
|
424
465
|
## Keywords (reserved, lowercase, exact match)
|
|
425
466
|
|
|
426
|
-
`using extern raw from as const class interface enum constructor public private
|
|
467
|
+
`using extern raw template from as const class interface enum constructor public private
|
|
427
468
|
protected static virtual override get set return if else while for foreach in break
|
|
428
469
|
continue match this base new void true false null task state async await try catch
|
|
429
470
|
finally throw`
|
|
@@ -438,7 +479,9 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
|
|
|
438
479
|
|
|
439
480
|
## Does not exist (don't reach for these)
|
|
440
481
|
|
|
441
|
-
Generics
|
|
482
|
+
Generics beyond a single unconstrained type parameter (no `Map<K, V>`, no `T : IFoo`
|
|
483
|
+
constraints, no generic functions, no generic inheritance, no variance — see Generics
|
|
484
|
+
above for what *is* supported) · `any`/`unknown` annotations ·
|
|
442
485
|
a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
|
|
443
486
|
reflection · type inference on declarations · ternary expression · union/tuple types ·
|
|
444
487
|
**object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
|
package/README.md
CHANGED
|
@@ -50,6 +50,10 @@ LLM's context, as opposed to this README's narrative explanation.
|
|
|
50
50
|
- **Reactive state without RxJS**: `state<T>` is a small reactive box (`.Value` get/set,
|
|
51
51
|
`.Subscribe((T) => void)`) for holding state and reacting to it changing — no
|
|
52
52
|
Observables, no operators, no manual unsubscribe bookkeeping.
|
|
53
|
+
- **Real markup templates**: `template from "./x.html";` compiles a separate, almost-pure-HTML
|
|
54
|
+
file — interpolation, event/property bindings, `*if`/`*for` — down to the exact same
|
|
55
|
+
AST a hand-written `Render()` would produce, with auto-`Subscribe` wiring for state
|
|
56
|
+
referenced directly in the markup. See [Templates](#templates).
|
|
53
57
|
- **A companion framework, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)**:
|
|
54
58
|
components, constructor-injected services via a composition root (no DI container), and
|
|
55
59
|
real-URL routing (no config DSL) — built entirely on the features above, in a separate
|
|
@@ -387,10 +391,11 @@ void Main() {
|
|
|
387
391
|
```
|
|
388
392
|
|
|
389
393
|
Unlike `extern`, there's no real JS export being described here — the compiler fabricates
|
|
390
|
-
the value itself — so there's no `as "jsName"` clause. This
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
the value itself — so there's no `as "jsName"` clause. This is a general-purpose,
|
|
395
|
+
compile-time-only file embedding primitive — useful for anything that wants a file's
|
|
396
|
+
exact contents as a string constant with no runtime file access. (If what you actually
|
|
397
|
+
want is markup that becomes real DOM construction code, not just a string — see
|
|
398
|
+
[Templates](#templates) below, which is the more specific answer for that case.)
|
|
394
399
|
|
|
395
400
|
### async/await, task<T>, and try/catch
|
|
396
401
|
|
|
@@ -488,6 +493,82 @@ of a DI container, and hash-based routing instead of a config DSL — both built
|
|
|
488
493
|
on ordinary KopScript, no further compiler features required beyond `state<T>` and the
|
|
489
494
|
`extern`/`virtual` support above.
|
|
490
495
|
|
|
496
|
+
Hand-writing `Render()` like this, statement by statement, is still fully supported — but
|
|
497
|
+
a Kopular component today more commonly expresses it as a **template** instead. See
|
|
498
|
+
[Templates](#templates) below for the markup-based alternative to this same method, and
|
|
499
|
+
for how `Count.Subscribe(...)` above can often be skipped entirely.
|
|
500
|
+
|
|
501
|
+
### Templates
|
|
502
|
+
|
|
503
|
+
`template from "<path>";` inside a class body replaces a hand-written `Render()` with a
|
|
504
|
+
real, separate markup file — almost pure HTML, with data bindings and a small set of
|
|
505
|
+
structural directives standing in for the imperative DOM code above:
|
|
506
|
+
|
|
507
|
+
```ks
|
|
508
|
+
class Counter : Component {
|
|
509
|
+
public state<number> Count;
|
|
510
|
+
constructor() : base() { this.Count = state(0); }
|
|
511
|
+
public void Increment() { this.Count.Value = this.Count.Value + 1; }
|
|
512
|
+
template from "./counter.html";
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
```html
|
|
517
|
+
<!-- counter.html -->
|
|
518
|
+
<button (click)="Increment()">Count: {{ Count.Value }}</button>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
This compiles to exactly the `Render()` method you'd otherwise write by hand — the
|
|
522
|
+
template compiler is a pass that runs between parsing and type-checking, turning the
|
|
523
|
+
markup into ordinary `MethodDecl`/statement/expression AST nodes and splicing the result
|
|
524
|
+
into the class before checking ever runs. There's no separate runtime template engine, no
|
|
525
|
+
virtual DOM diffing, and no interpreted expression language: `{{ Count.Value }}` and
|
|
526
|
+
`(click)="Increment()"` contain real KopScript, parsed and type-checked exactly like
|
|
527
|
+
anything else in the file, with errors reported at their real position in the `.html`
|
|
528
|
+
file, not the `.ks` file.
|
|
529
|
+
|
|
530
|
+
A class may have a `template` or a hand-written `Render()`, never both — that's a compile
|
|
531
|
+
error. `ks watch` also tracks the referenced `.html` file, so editing markup alone
|
|
532
|
+
triggers a rebuild.
|
|
533
|
+
|
|
534
|
+
Supported bindings and directives:
|
|
535
|
+
|
|
536
|
+
| Syntax | Desugars to |
|
|
537
|
+
| --------------------------- | --------------------------------------------------------- |
|
|
538
|
+
| `{{ expr }}` (in text) | `el.textContent = $"...{expr}...";` (an `InterpolatedStringLiteral`, same as `$"..."`) |
|
|
539
|
+
| `(event)="stmt"` | `el.addEventListener("event", (Event e) => { stmt });` |
|
|
540
|
+
| `[prop]="expr"` | `el.prop = expr;` — a plain assignment, checked like any other |
|
|
541
|
+
| `class="..."` (static) | `el.className = "...";` (aliased, since `class` is a KopScript keyword) |
|
|
542
|
+
| `*if="expr"` | a real `if (expr) { ... }` around the element's creation |
|
|
543
|
+
| `*for="Type var of expr"` | a real `for (Type var in expr) { ... }` — the element type is explicit, matching KopScript's no-inference stance elsewhere (Generics, Nullable types) |
|
|
544
|
+
|
|
545
|
+
**Auto-subscribe**: a `state<T>` field declared directly on the component and referenced
|
|
546
|
+
directly in its template (like `Count` above) gets its `Subscribe((v) => this.Update())`
|
|
547
|
+
wired up automatically — no manual `Subscribe` call needed in the constructor. This is a
|
|
548
|
+
syntactic check against the class's own declared fields, not a type-checker query, so it
|
|
549
|
+
only covers *direct* field access; state reached indirectly — through a method call, or
|
|
550
|
+
through `this.SomeService.Count` — still needs a manual `Subscribe`, same as before
|
|
551
|
+
templates existed.
|
|
552
|
+
|
|
553
|
+
v1 cuts, same discipline as generics and nullable types: exactly one top-level element per
|
|
554
|
+
template (no auto-wrapping — a clear error instead); no mixing text and element children
|
|
555
|
+
under one element (Kopular's DOM surface has no text-node type, only `.textContent`); no
|
|
556
|
+
two-way binding, no pipes, no stacking two structural directives on one element.
|
|
557
|
+
|
|
558
|
+
**A deliberate layering note**: the `template from` syntax lives in kopscript's own
|
|
559
|
+
grammar (Kopular can't extend a language it doesn't own), but what it desugars *to* —
|
|
560
|
+
`document.createElement`, `.appendChild`, `.textContent`, `.addEventListener` — assumes
|
|
561
|
+
exactly the DOM surface [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s
|
|
562
|
+
`dom.ks` declares. That's a real coupling from the compiler to one specific consumer,
|
|
563
|
+
accepted deliberately rather than building a generic pluggable desugaring-target system
|
|
564
|
+
for a hypothetical second framework that doesn't exist today. If one ever does, that's the
|
|
565
|
+
point to generalize this.
|
|
566
|
+
|
|
567
|
+
`raw string <Name> from "./x.html";` (above) still exists as a general-purpose
|
|
568
|
+
compile-time file embedding feature — but for the specific "component markup in its own
|
|
569
|
+
file" use case, `template from` is the real answer; `raw` no longer needs to stand in for
|
|
570
|
+
it.
|
|
571
|
+
|
|
491
572
|
### String interpolation
|
|
492
573
|
|
|
493
574
|
```ks
|
|
@@ -692,8 +773,9 @@ use `node dist/cli.js` directly.
|
|
|
692
773
|
|
|
693
774
|
`watch` rebuilds on a save to *any* `.ks` file it reached while compiling — the entry and
|
|
694
775
|
everything it (transitively, non-transitively per-file) `using`s — not just the entry
|
|
695
|
-
file,
|
|
696
|
-
|
|
776
|
+
file, plus any `.html` file referenced via `template from "...";` in one of those classes,
|
|
777
|
+
and re-establishes its watch list after every rebuild since the dependency set
|
|
778
|
+
itself can change (a `using` added or removed, or a `template` declaration's path). This is the piece that makes a
|
|
697
779
|
browser-facing dev loop bearable: run `ks watch` in one terminal, a static file server in
|
|
698
780
|
another, and refreshing the browser after a save is the only manual step left — there's
|
|
699
781
|
no watch-triggered auto-refresh, since KopScript has no dev-server integration to push
|
|
@@ -732,6 +814,6 @@ lives in its own repo, published to npm as `kopular` and consumed like any other
|
|
|
732
814
|
(no local checkout or `file:` dependency needed). A real app built on both lives in
|
|
733
815
|
[KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) — several
|
|
734
816
|
routed pages, `state<T>`-driven reactivity, constructor-injected services via a
|
|
735
|
-
composition root (no DI container),
|
|
736
|
-
|
|
737
|
-
and in an actual browser.
|
|
817
|
+
composition root (no DI container), real-URL (History API) routing, and Angular-style
|
|
818
|
+
markup templates (see [Templates](#templates) above) with `*if`/`*for` structural
|
|
819
|
+
directives — verified against a real DOM via jsdom and in an actual browser.
|
package/dist/cli.js
CHANGED
|
@@ -39,6 +39,14 @@ function printHumanDiagnostics(result) {
|
|
|
39
39
|
// .js file next to each .ks source. With `jsonMode`, nothing but a single
|
|
40
40
|
// JSON object goes to stdout (no "Wrote ..." line, no human diagnostics) —
|
|
41
41
|
// see `printJson` for the shape.
|
|
42
|
+
//
|
|
43
|
+
// A module's watch list is its own path plus any `template from "...";`
|
|
44
|
+
// file(s) it references — those aren't part of `result.order` (they export
|
|
45
|
+
// nothing a `using` could pull in), but editing one needs to trigger a
|
|
46
|
+
// rebuild exactly the way editing a `using`'d file already does.
|
|
47
|
+
function allWatchFiles(result) {
|
|
48
|
+
return result.order.flatMap((absPath) => [absPath, ...(result.modules.get(absPath)?.extraWatchFiles ?? [])]);
|
|
49
|
+
}
|
|
42
50
|
function build(filePath, jsonMode = false) {
|
|
43
51
|
const result = compileGraph(filePath);
|
|
44
52
|
if (result.entryMissing) {
|
|
@@ -55,14 +63,14 @@ function build(filePath, jsonMode = false) {
|
|
|
55
63
|
else
|
|
56
64
|
printHumanDiagnostics(result);
|
|
57
65
|
process.exitCode = 1;
|
|
58
|
-
return { outPath: null, watchFiles: result
|
|
66
|
+
return { outPath: null, watchFiles: allWatchFiles(result) };
|
|
59
67
|
}
|
|
60
68
|
for (const absPath of result.order) {
|
|
61
69
|
writeFileSync(outputPathFor(absPath), result.outputs.get(absPath), "utf-8");
|
|
62
70
|
}
|
|
63
71
|
if (jsonMode)
|
|
64
72
|
printJson(result, result.order.map(outputPathFor));
|
|
65
|
-
return { outPath: outputPathFor(filePath), watchFiles: result
|
|
73
|
+
return { outPath: outputPathFor(filePath), watchFiles: allWatchFiles(result) };
|
|
66
74
|
}
|
|
67
75
|
// Type-checks the graph without writing any output — for CI or an editor/
|
|
68
76
|
// agent that wants pass/fail plus diagnostics without touching the
|
package/dist/lexer.js
CHANGED
package/dist/modules.js
CHANGED
|
@@ -5,6 +5,93 @@ import { Parser } from "./parser.js";
|
|
|
5
5
|
import { Checker, emptyModuleExports } from "./checker.js";
|
|
6
6
|
import { CodeGenerator } from "./codegen.js";
|
|
7
7
|
import { DiagnosticBag } from "./diagnostics.js";
|
|
8
|
+
import { TemplateParser } from "./template_parser.js";
|
|
9
|
+
import { TemplateCompiler } from "./template_compiler.js";
|
|
10
|
+
// Expands every `template from "./x.html";` declaration in `program` into a
|
|
11
|
+
// real, synthetic `Render()` MethodDecl, spliced into that class's own
|
|
12
|
+
// `methods` — before any type-checking happens, so checker.ts/codegen.ts
|
|
13
|
+
// need no knowledge that a template was ever involved (see
|
|
14
|
+
// template_compiler.ts's own header for the full design rationale). Returns
|
|
15
|
+
// the resolved template paths found, for `extraWatchFiles`.
|
|
16
|
+
//
|
|
17
|
+
// Runs entirely on parse-time information: the enclosing ClassDecl's own
|
|
18
|
+
// already-parsed fields (for auto-subscribe field detection) and
|
|
19
|
+
// kopscript's own expression/type parser (for binding expressions) — no
|
|
20
|
+
// dependency on type-checking having run first.
|
|
21
|
+
function expandTemplates(program, absPath, diagnostics, fileOverrides) {
|
|
22
|
+
const watchFiles = [];
|
|
23
|
+
for (const stmt of program.statements) {
|
|
24
|
+
if (stmt.kind !== "ClassDecl" || !stmt.template)
|
|
25
|
+
continue;
|
|
26
|
+
const templateRef = stmt.template;
|
|
27
|
+
if (stmt.methods.some((m) => m.name === "Render" && !m.isStatic)) {
|
|
28
|
+
diagnostics.error(`Class '${stmt.name}' has both a 'template' declaration and a hand-written 'Render()' method — remove one`, templateRef.line, templateRef.col);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const templatePath = resolve(dirname(absPath), templateRef.path);
|
|
32
|
+
watchFiles.push(templatePath);
|
|
33
|
+
const exists = fileOverrides?.has(templatePath) || existsSync(templatePath);
|
|
34
|
+
if (!exists) {
|
|
35
|
+
diagnostics.error(`Cannot find template file '${templateRef.path}' (looked for '${displayPath(templatePath)}')`, templateRef.line, templateRef.col);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const templateSource = fileOverrides?.get(templatePath) ?? readFileSync(templatePath, "utf-8");
|
|
39
|
+
const templateDiagnostics = new DiagnosticBag();
|
|
40
|
+
const root = new TemplateParser(templateSource, templateDiagnostics).parseDocument();
|
|
41
|
+
for (const d of templateDiagnostics.diagnostics) {
|
|
42
|
+
diagnostics.error(`[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
|
|
43
|
+
}
|
|
44
|
+
if (!root)
|
|
45
|
+
continue;
|
|
46
|
+
const stateFieldNames = new Set(stmt.fields.filter((f) => f.type.kind === "StateType").map((f) => f.name));
|
|
47
|
+
const memberNames = new Set([...stmt.fields.map((f) => f.name), ...stmt.properties.map((p) => p.name), ...stmt.methods.map((m) => m.name)]);
|
|
48
|
+
const compileDiagnostics = new DiagnosticBag();
|
|
49
|
+
const { renderMethod, autoSubscribeFields } = new TemplateCompiler(memberNames, stateFieldNames, compileDiagnostics).compile(root, templateRef);
|
|
50
|
+
for (const d of compileDiagnostics.diagnostics) {
|
|
51
|
+
diagnostics.error(`[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
|
|
52
|
+
}
|
|
53
|
+
stmt.methods.push(renderMethod);
|
|
54
|
+
if (autoSubscribeFields.length > 0) {
|
|
55
|
+
if (!stmt.constructor) {
|
|
56
|
+
diagnostics.error(`Class '${stmt.name}' has a template referencing state<T> field(s) (${autoSubscribeFields.join(", ")}) but no constructor to subscribe from`, templateRef.line, templateRef.col);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const fieldName of autoSubscribeFields) {
|
|
60
|
+
const field = stmt.fields.find((f) => f.name === fieldName);
|
|
61
|
+
const valueType = field.type.valueType;
|
|
62
|
+
const subscribeCall = {
|
|
63
|
+
kind: "ExpressionStatement",
|
|
64
|
+
expression: {
|
|
65
|
+
kind: "CallExpr",
|
|
66
|
+
callee: { kind: "MemberExpr", object: { kind: "MemberExpr", object: { kind: "ThisExpr", line: 0, col: 0 }, property: fieldName, line: 0, col: 0 }, property: "Subscribe", line: 0, col: 0 },
|
|
67
|
+
args: [
|
|
68
|
+
{
|
|
69
|
+
kind: "LambdaExpr",
|
|
70
|
+
params: [{ name: "__v", type: valueType }],
|
|
71
|
+
body: {
|
|
72
|
+
kind: "Block",
|
|
73
|
+
statements: [
|
|
74
|
+
{ kind: "ExpressionStatement", expression: { kind: "CallExpr", callee: { kind: "MemberExpr", object: { kind: "ThisExpr", line: 0, col: 0 }, property: "Update", line: 0, col: 0 }, args: [], line: 0, col: 0 }, line: 0, col: 0 },
|
|
75
|
+
],
|
|
76
|
+
line: 0,
|
|
77
|
+
col: 0,
|
|
78
|
+
},
|
|
79
|
+
line: 0,
|
|
80
|
+
col: 0,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
line: 0,
|
|
84
|
+
col: 0,
|
|
85
|
+
},
|
|
86
|
+
line: 0,
|
|
87
|
+
col: 0,
|
|
88
|
+
};
|
|
89
|
+
stmt.constructor.body.statements.push(subscribeCall);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return watchFiles;
|
|
94
|
+
}
|
|
8
95
|
function displayPath(absPath) {
|
|
9
96
|
return relative(process.cwd(), absPath);
|
|
10
97
|
}
|
|
@@ -15,21 +102,28 @@ function displayPath(absPath) {
|
|
|
15
102
|
// cyclic dependency is reported on the referencing `using` statement and
|
|
16
103
|
// simply isn't added to that file's dependency list, so the rest of the
|
|
17
104
|
// graph can still be explored and reported on in one pass.
|
|
18
|
-
|
|
105
|
+
// `fileOverrides` (absPath -> source) lets a caller substitute content that
|
|
106
|
+
// hasn't been saved to disk yet — an editor's live buffer, most likely —
|
|
107
|
+
// for any file in the graph, falling back to the real file for everything
|
|
108
|
+
// else. Without this, editor tooling can only ever see what's on disk,
|
|
109
|
+
// which is stale the moment a file has unsaved edits.
|
|
110
|
+
export function loadModuleGraph(entryAbsPath, fileOverrides) {
|
|
19
111
|
const modules = new Map();
|
|
20
112
|
const order = [];
|
|
21
113
|
const stack = [];
|
|
22
|
-
|
|
114
|
+
const exists = (absPath) => fileOverrides?.has(absPath) || existsSync(absPath);
|
|
115
|
+
if (!exists(entryAbsPath)) {
|
|
23
116
|
return { modules, order, entryMissing: true };
|
|
24
117
|
}
|
|
25
118
|
function visit(absPath) {
|
|
26
119
|
if (modules.has(absPath))
|
|
27
120
|
return;
|
|
28
|
-
const source = readFileSync(absPath, "utf-8");
|
|
121
|
+
const source = fileOverrides?.get(absPath) ?? readFileSync(absPath, "utf-8");
|
|
29
122
|
const diagnostics = new DiagnosticBag();
|
|
30
123
|
const tokens = new Lexer(source, diagnostics).tokenize();
|
|
31
124
|
const program = new Parser(tokens, diagnostics).parseProgram();
|
|
32
|
-
const
|
|
125
|
+
const extraWatchFiles = expandTemplates(program, absPath, diagnostics, fileOverrides);
|
|
126
|
+
const record = { absPath, source, tokens, program, diagnostics, dependencies: [], hoverEntries: [], extraWatchFiles };
|
|
33
127
|
modules.set(absPath, record);
|
|
34
128
|
stack.push(absPath);
|
|
35
129
|
for (const u of program.usings) {
|
|
@@ -38,7 +132,7 @@ export function loadModuleGraph(entryAbsPath) {
|
|
|
38
132
|
continue;
|
|
39
133
|
}
|
|
40
134
|
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
41
|
-
if (!
|
|
135
|
+
if (!exists(depPath)) {
|
|
42
136
|
diagnostics.error(`Cannot find module '${u.path}' (looked for '${displayPath(depPath)}')`, u.line, u.col);
|
|
43
137
|
continue;
|
|
44
138
|
}
|
|
@@ -60,8 +154,8 @@ export function loadModuleGraph(entryAbsPath) {
|
|
|
60
154
|
// with its direct dependencies' merged exports), and — only if the entire
|
|
61
155
|
// graph is error-free — generates one JS file's contents per module, with
|
|
62
156
|
// real ES `import`/`export` statements wiring them together.
|
|
63
|
-
export function compileGraph(entryAbsPath) {
|
|
64
|
-
const { modules, order, entryMissing } = loadModuleGraph(entryAbsPath);
|
|
157
|
+
export function compileGraph(entryAbsPath, fileOverrides) {
|
|
158
|
+
const { modules, order, entryMissing } = loadModuleGraph(entryAbsPath, fileOverrides);
|
|
65
159
|
if (entryMissing) {
|
|
66
160
|
return { success: false, entryMissing: true, modules, order, outputs: new Map() };
|
|
67
161
|
}
|
|
@@ -106,7 +200,18 @@ export function compileGraph(entryAbsPath) {
|
|
|
106
200
|
merged.enums.set(name, info);
|
|
107
201
|
}
|
|
108
202
|
const checker = new Checker(mod.program, mod.diagnostics, merged, absPath);
|
|
109
|
-
|
|
203
|
+
try {
|
|
204
|
+
checker.check();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// A malformed-enough program (most likely: mid-edit in an editor,
|
|
208
|
+
// where the rest of the graph is still worth reporting on) can trip
|
|
209
|
+
// an assertion deep in the checker — whatever hover entries/exports
|
|
210
|
+
// it recorded before that are still worth keeping, and this module is
|
|
211
|
+
// conservatively treated as erroring so codegen never runs on a
|
|
212
|
+
// half-checked result.
|
|
213
|
+
hasErrors = true;
|
|
214
|
+
}
|
|
110
215
|
mod.hoverEntries = checker.hoverEntries;
|
|
111
216
|
if (mod.diagnostics.hasErrors)
|
|
112
217
|
hasErrors = true;
|
package/dist/parser.js
CHANGED
|
@@ -26,6 +26,49 @@ export class Parser {
|
|
|
26
26
|
}
|
|
27
27
|
return { kind: "Program", usings, statements };
|
|
28
28
|
}
|
|
29
|
+
// Parses a single, complete expression from `tokens` and nothing else —
|
|
30
|
+
// for embedding a real KopScript expression inside something that isn't
|
|
31
|
+
// itself a .ks file (a template binding's attribute value, `(click)=
|
|
32
|
+
// "Increment()"` or `{{ Count.Value }}` — see template_compiler.ts).
|
|
33
|
+
// Errors (including "didn't consume everything") report through the same
|
|
34
|
+
// `diagnostics` bag this Parser was constructed with, so a caller that
|
|
35
|
+
// re-maps positions back into the original file gets ordinary Diagnostics
|
|
36
|
+
// to work with, not a separate error channel. A genuinely malformed
|
|
37
|
+
// fragment (e.g. an unclosed paren) throws internally the same way any
|
|
38
|
+
// other parse failure does (see `parseStatement`'s own try/catch) — this
|
|
39
|
+
// catches that the same way, returning an inert placeholder expression
|
|
40
|
+
// (a `null` literal) rather than propagating the exception; the real
|
|
41
|
+
// diagnostic was already recorded before the throw either way.
|
|
42
|
+
parseStandaloneExpression() {
|
|
43
|
+
try {
|
|
44
|
+
const expr = this.parseExpression();
|
|
45
|
+
if (!this.check(TokenKind.EOF)) {
|
|
46
|
+
const t = this.peek();
|
|
47
|
+
this.diagnostics.error(`Unexpected token '${t.lexeme}' after expression`, t.line, t.col);
|
|
48
|
+
}
|
|
49
|
+
return expr;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
const t = this.peek();
|
|
53
|
+
return { kind: "NullLiteral", line: t.line, col: t.col };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Same idea as `parseStandaloneExpression`, for a type reference — used
|
|
57
|
+
// for `*for="Item item of Items"`'s explicit element type.
|
|
58
|
+
parseStandaloneType() {
|
|
59
|
+
try {
|
|
60
|
+
const type = this.parseType();
|
|
61
|
+
if (!this.check(TokenKind.EOF)) {
|
|
62
|
+
const t = this.peek();
|
|
63
|
+
this.diagnostics.error(`Unexpected token '${t.lexeme}' after type`, t.line, t.col);
|
|
64
|
+
}
|
|
65
|
+
return type;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
const t = this.peek();
|
|
69
|
+
return { kind: "NamedType", name: "void", typeArgs: null, line: t.line, col: t.col };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
29
72
|
parseUsing() {
|
|
30
73
|
const start = this.advance(); // 'using'
|
|
31
74
|
const pathTok = this.consume(TokenKind.String, "Expected a module path string after 'using'");
|
|
@@ -225,7 +268,19 @@ export class Parser {
|
|
|
225
268
|
const properties = [];
|
|
226
269
|
const methods = [];
|
|
227
270
|
let ctor = null;
|
|
271
|
+
let template = null;
|
|
228
272
|
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
273
|
+
if (this.check(TokenKind.Template)) {
|
|
274
|
+
const templateStart = this.advance();
|
|
275
|
+
if (template) {
|
|
276
|
+
this.diagnostics.error(`Class '${name}' already has a 'template' declaration`, templateStart.line, templateStart.col);
|
|
277
|
+
}
|
|
278
|
+
this.consume(TokenKind.From, "Expected 'from \"<path>\"' after 'template'");
|
|
279
|
+
const pathTok = this.consume(TokenKind.String, "Expected a file path string after 'from'");
|
|
280
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'template' declaration");
|
|
281
|
+
template = { path: pathTok.lexeme, line: templateStart.line, col: templateStart.col };
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
229
284
|
let visibility = "public";
|
|
230
285
|
if (this.check(TokenKind.Public) || this.check(TokenKind.Private) || this.check(TokenKind.Protected)) {
|
|
231
286
|
const v = this.advance().kind;
|
|
@@ -357,7 +412,7 @@ export class Parser {
|
|
|
357
412
|
}
|
|
358
413
|
}
|
|
359
414
|
this.consume(TokenKind.RBrace, "Expected '}' after class body");
|
|
360
|
-
return { kind: "ClassDecl", isExported, name, typeParam, baseList, fields, properties, constructor: ctor, methods, line: start.line, col: start.col };
|
|
415
|
+
return { kind: "ClassDecl", isExported, name, typeParam, baseList, fields, properties, constructor: ctor, methods, template, line: start.line, col: start.col };
|
|
361
416
|
}
|
|
362
417
|
parseInterfaceDecl(isExported) {
|
|
363
418
|
const start = this.advance(); // 'interface'
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
const ELEMENT_TYPE = { kind: "NamedType", name: "Element", typeArgs: null, line: 0, col: 0 };
|
|
2
|
+
const VOID_TYPE = { kind: "NamedType", name: "void", typeArgs: null, line: 0, col: 0 };
|
|
3
|
+
const EVENT_TYPE = { kind: "NamedType", name: "Event", typeArgs: null, line: 0, col: 0 };
|
|
4
|
+
function ident(name, at) {
|
|
5
|
+
return { kind: "Identifier", name, line: at.line, col: at.col };
|
|
6
|
+
}
|
|
7
|
+
function stringLiteral(value, at) {
|
|
8
|
+
return { kind: "StringLiteral", value, line: at.line, col: at.col };
|
|
9
|
+
}
|
|
10
|
+
function member(object, property, at) {
|
|
11
|
+
return { kind: "MemberExpr", object, property, line: at.line, col: at.col };
|
|
12
|
+
}
|
|
13
|
+
function call(callee, args, at) {
|
|
14
|
+
return { kind: "CallExpr", callee, args, line: at.line, col: at.col };
|
|
15
|
+
}
|
|
16
|
+
function exprStatement(expression, at) {
|
|
17
|
+
return { kind: "ExpressionStatement", expression, line: at.line, col: at.col };
|
|
18
|
+
}
|
|
19
|
+
function assign(target, value, at) {
|
|
20
|
+
return exprStatement({ kind: "AssignExpr", target, value, line: at.line, col: at.col }, at);
|
|
21
|
+
}
|
|
22
|
+
function varDecl(name, init, at) {
|
|
23
|
+
return { kind: "VarDecl", isConst: false, name, nameLine: at.line, nameCol: at.col, type: ELEMENT_TYPE, init, line: at.line, col: at.col };
|
|
24
|
+
}
|
|
25
|
+
function block(statements, at) {
|
|
26
|
+
return { kind: "Block", statements, line: at.line, col: at.col };
|
|
27
|
+
}
|
|
28
|
+
// Visits every sub-expression reachable from `expr` — used only to find
|
|
29
|
+
// direct references to a state<T> field (see `TemplateCompiler.trackState`)
|
|
30
|
+
// while walking a binding's already-parsed expression; not a general
|
|
31
|
+
// checker replacement.
|
|
32
|
+
function walkExpr(expr, visit) {
|
|
33
|
+
visit(expr);
|
|
34
|
+
switch (expr.kind) {
|
|
35
|
+
case "UnaryExpr":
|
|
36
|
+
walkExpr(expr.operand, visit);
|
|
37
|
+
return;
|
|
38
|
+
case "BinaryExpr":
|
|
39
|
+
case "LogicalExpr":
|
|
40
|
+
walkExpr(expr.left, visit);
|
|
41
|
+
walkExpr(expr.right, visit);
|
|
42
|
+
return;
|
|
43
|
+
case "AssignExpr":
|
|
44
|
+
walkExpr(expr.target, visit);
|
|
45
|
+
walkExpr(expr.value, visit);
|
|
46
|
+
return;
|
|
47
|
+
case "CallExpr":
|
|
48
|
+
walkExpr(expr.callee, visit);
|
|
49
|
+
expr.args.forEach((a) => walkExpr(a, visit));
|
|
50
|
+
return;
|
|
51
|
+
case "NewExpr":
|
|
52
|
+
expr.args.forEach((a) => walkExpr(a, visit));
|
|
53
|
+
return;
|
|
54
|
+
case "MemberExpr":
|
|
55
|
+
walkExpr(expr.object, visit);
|
|
56
|
+
return;
|
|
57
|
+
case "IndexExpr":
|
|
58
|
+
walkExpr(expr.object, visit);
|
|
59
|
+
walkExpr(expr.index, visit);
|
|
60
|
+
return;
|
|
61
|
+
case "InterpolatedStringLiteral":
|
|
62
|
+
for (const part of expr.parts)
|
|
63
|
+
if (part.kind === "Expr")
|
|
64
|
+
walkExpr(part.expression, visit);
|
|
65
|
+
return;
|
|
66
|
+
case "ArrayLiteral":
|
|
67
|
+
expr.elements.forEach((e) => walkExpr(e, visit));
|
|
68
|
+
return;
|
|
69
|
+
case "AwaitExpr":
|
|
70
|
+
walkExpr(expr.operand, visit);
|
|
71
|
+
return;
|
|
72
|
+
case "StateExpr":
|
|
73
|
+
walkExpr(expr.initializer, visit);
|
|
74
|
+
return;
|
|
75
|
+
case "MatchExpr":
|
|
76
|
+
walkExpr(expr.subject, visit);
|
|
77
|
+
for (const arm of expr.arms)
|
|
78
|
+
walkExpr(arm.result, visit);
|
|
79
|
+
return;
|
|
80
|
+
default:
|
|
81
|
+
return; // literals, Identifier, ThisExpr, LambdaExpr (deliberately not descending into a lambda body — see module header)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// KopScript has no implicit `this` anywhere else in the language — every
|
|
85
|
+
// real example in this codebase writes `this.Foo()`, never a bare `Foo()`
|
|
86
|
+
// meaning the same thing. Template bindings are a deliberate, documented
|
|
87
|
+
// exception: `{{ Count.Value }}`/`(click)="Increment()"` read a bare
|
|
88
|
+
// `Count`/`Increment` the way Angular's own templates do, with `this.`
|
|
89
|
+
// implied. This rebuilds `expr`, rewriting a bare `Identifier` into
|
|
90
|
+
// `this.<name>` exactly when `name` is one of the enclosing class's own
|
|
91
|
+
// directly-declared members (`memberNames`) and isn't shadowed by a
|
|
92
|
+
// closer-in-scope local (`localScope` — a `*for` loop variable, most
|
|
93
|
+
// likely) — anything else (a real global like `print`, a loop variable)
|
|
94
|
+
// resolves exactly as bare KopScript already would.
|
|
95
|
+
//
|
|
96
|
+
// Known limitation: `memberNames` only ever contains the class's *own*
|
|
97
|
+
// declared members, not anything inherited from a base class — this runs
|
|
98
|
+
// at parse time, before the checker has resolved cross-file inheritance,
|
|
99
|
+
// so an inherited member needs an explicit `this.` in a template binding
|
|
100
|
+
// to be found. Most template-relevant state (a component's own counters,
|
|
101
|
+
// handler methods) is declared directly on the component itself in
|
|
102
|
+
// practice; this is a real, accepted v1 gap, not an oversight.
|
|
103
|
+
function qualifyThis(expr, memberNames, localScope) {
|
|
104
|
+
const q = (e) => qualifyThis(e, memberNames, localScope);
|
|
105
|
+
switch (expr.kind) {
|
|
106
|
+
case "Identifier":
|
|
107
|
+
if (localScope.has(expr.name))
|
|
108
|
+
return expr;
|
|
109
|
+
if (memberNames.has(expr.name))
|
|
110
|
+
return member({ kind: "ThisExpr", line: expr.line, col: expr.col }, expr.name, expr);
|
|
111
|
+
return expr;
|
|
112
|
+
case "UnaryExpr":
|
|
113
|
+
return { ...expr, operand: q(expr.operand) };
|
|
114
|
+
case "BinaryExpr":
|
|
115
|
+
case "LogicalExpr":
|
|
116
|
+
return { ...expr, left: q(expr.left), right: q(expr.right) };
|
|
117
|
+
case "AssignExpr":
|
|
118
|
+
return { ...expr, target: q(expr.target), value: q(expr.value) };
|
|
119
|
+
case "CallExpr":
|
|
120
|
+
return { ...expr, callee: q(expr.callee), args: expr.args.map(q) };
|
|
121
|
+
case "NewExpr":
|
|
122
|
+
return { ...expr, args: expr.args.map(q) };
|
|
123
|
+
case "MemberExpr":
|
|
124
|
+
return { ...expr, object: q(expr.object) };
|
|
125
|
+
case "IndexExpr":
|
|
126
|
+
return { ...expr, object: q(expr.object), index: q(expr.index) };
|
|
127
|
+
case "InterpolatedStringLiteral":
|
|
128
|
+
return { ...expr, parts: expr.parts.map((p) => (p.kind === "Expr" ? { kind: "Expr", expression: q(p.expression) } : p)) };
|
|
129
|
+
case "ArrayLiteral":
|
|
130
|
+
return { ...expr, elements: expr.elements.map(q) };
|
|
131
|
+
case "AwaitExpr":
|
|
132
|
+
return { ...expr, operand: q(expr.operand) };
|
|
133
|
+
case "StateExpr":
|
|
134
|
+
return { ...expr, initializer: q(expr.initializer) };
|
|
135
|
+
case "MatchExpr":
|
|
136
|
+
return { ...expr, subject: q(expr.subject), arms: expr.arms.map((arm) => ({ ...arm, result: q(arm.result) })) };
|
|
137
|
+
default:
|
|
138
|
+
return expr; // literals, ThisExpr, LambdaExpr — see walkExpr's own comment
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const NO_LOCALS = new Set();
|
|
142
|
+
export class TemplateCompiler {
|
|
143
|
+
// `memberNames`: every field/property/method declared directly on the
|
|
144
|
+
// enclosing class — what a bare identifier in a binding is allowed to
|
|
145
|
+
// implicitly mean `this.<name>` for (see `qualifyThis`'s own header).
|
|
146
|
+
// `stateFieldNames`: the subset of those that are `state<T>` — tracked
|
|
147
|
+
// separately for auto-subscribe, since not every member qualifies.
|
|
148
|
+
constructor(memberNames, stateFieldNames, diagnostics) {
|
|
149
|
+
this.memberNames = memberNames;
|
|
150
|
+
this.stateFieldNames = stateFieldNames;
|
|
151
|
+
this.diagnostics = diagnostics;
|
|
152
|
+
this.varCounter = 0;
|
|
153
|
+
this.autoSubscribeFields = new Set();
|
|
154
|
+
}
|
|
155
|
+
freshVar() {
|
|
156
|
+
return `__el${this.varCounter++}`;
|
|
157
|
+
}
|
|
158
|
+
// Qualifies bare member references with `this.`, then records any
|
|
159
|
+
// directly-referenced `state<T>` field for auto-subscribe — run once per
|
|
160
|
+
// binding expression, at the point each is pulled out of the template.
|
|
161
|
+
resolve(expr, localScope) {
|
|
162
|
+
const qualified = qualifyThis(expr, this.memberNames, localScope);
|
|
163
|
+
walkExpr(qualified, (e) => {
|
|
164
|
+
if (e.kind === "MemberExpr" && e.object.kind === "ThisExpr" && this.stateFieldNames.has(e.property)) {
|
|
165
|
+
this.autoSubscribeFields.add(e.property);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return qualified;
|
|
169
|
+
}
|
|
170
|
+
compile(root, templateDeclAt) {
|
|
171
|
+
const { varName, statements } = this.buildElement(root, NO_LOCALS);
|
|
172
|
+
statements.push({ kind: "ReturnStatement", value: ident(varName, templateDeclAt), line: templateDeclAt.line, col: templateDeclAt.col });
|
|
173
|
+
const renderMethod = {
|
|
174
|
+
kind: "MethodDecl",
|
|
175
|
+
visibility: "public",
|
|
176
|
+
isStatic: false,
|
|
177
|
+
isAsync: false,
|
|
178
|
+
isVirtual: false,
|
|
179
|
+
isOverride: true,
|
|
180
|
+
name: "Render",
|
|
181
|
+
nameLine: templateDeclAt.line,
|
|
182
|
+
nameCol: templateDeclAt.col,
|
|
183
|
+
params: [],
|
|
184
|
+
returnType: ELEMENT_TYPE,
|
|
185
|
+
body: block(statements, templateDeclAt),
|
|
186
|
+
line: templateDeclAt.line,
|
|
187
|
+
col: templateDeclAt.col,
|
|
188
|
+
};
|
|
189
|
+
return { renderMethod, autoSubscribeFields: [...this.autoSubscribeFields] };
|
|
190
|
+
}
|
|
191
|
+
// Builds `node` (ignoring any *if/*for on it — see buildAppendStatements,
|
|
192
|
+
// which strips those before delegating here) and returns the local
|
|
193
|
+
// variable name holding the finished Element, plus the statements that
|
|
194
|
+
// build it. Doesn't append it anywhere; the caller decides that (the
|
|
195
|
+
// root just returns it, a non-root child appends it to its parent).
|
|
196
|
+
// `localScope`: names that resolve as plain locals rather than `this.`
|
|
197
|
+
// members within this subtree — a `*for` loop variable, most likely.
|
|
198
|
+
buildElement(node, localScope) {
|
|
199
|
+
const at = node;
|
|
200
|
+
const varName = this.freshVar();
|
|
201
|
+
const statements = [];
|
|
202
|
+
const self = ident(varName, at);
|
|
203
|
+
statements.push(varDecl(varName, call(member(ident("document", at), "createElement", at), [stringLiteral(node.tag, at)], at), at));
|
|
204
|
+
for (const attr of node.staticAttrs)
|
|
205
|
+
statements.push(assign(member(self, attr.name, attr), stringLiteral(attr.value, attr), attr));
|
|
206
|
+
for (const bind of node.propBindings) {
|
|
207
|
+
const value = this.resolve(bind.value, localScope);
|
|
208
|
+
statements.push(assign(member(self, bind.name, bind), value, bind));
|
|
209
|
+
}
|
|
210
|
+
for (const bind of node.eventBindings) {
|
|
211
|
+
const handler = this.resolve(bind.handler, localScope);
|
|
212
|
+
const handlerParam = { name: "e", type: EVENT_TYPE };
|
|
213
|
+
const lambda = {
|
|
214
|
+
kind: "LambdaExpr",
|
|
215
|
+
params: [{ name: handlerParam.name, type: handlerParam.type }],
|
|
216
|
+
body: block([exprStatement(handler, bind)], bind),
|
|
217
|
+
line: bind.line,
|
|
218
|
+
col: bind.col,
|
|
219
|
+
};
|
|
220
|
+
statements.push(exprStatement(call(member(self, "addEventListener", bind), [stringLiteral(bind.name, bind), lambda], bind), bind));
|
|
221
|
+
}
|
|
222
|
+
const elementChildren = node.children.filter((c) => c.kind === "element");
|
|
223
|
+
const textChildren = node.children.filter((c) => c.kind === "text");
|
|
224
|
+
if (textChildren.length > 0 && elementChildren.length > 0) {
|
|
225
|
+
this.diagnostics.error(`<${node.tag}> cannot mix text and element children — Kopular's DOM bindings have no text-node API, so text can only be a whole element's content`, node.line, node.col);
|
|
226
|
+
}
|
|
227
|
+
else if (textChildren.length > 0) {
|
|
228
|
+
const parts = textChildren.flatMap((t) => t.parts).map((p) => (p.kind === "Expr" ? { kind: "Expr", expression: this.resolve(p.expression, localScope) } : p));
|
|
229
|
+
const interpolated = { kind: "InterpolatedStringLiteral", parts, line: at.line, col: at.col };
|
|
230
|
+
statements.push(assign(member(self, "textContent", at), interpolated, at));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
for (const child of elementChildren)
|
|
234
|
+
statements.push(...this.buildAppendStatements(child, varName, localScope));
|
|
235
|
+
}
|
|
236
|
+
return { varName, statements };
|
|
237
|
+
}
|
|
238
|
+
buildAppendStatements(node, parentVar, localScope) {
|
|
239
|
+
if (node.forBinding && node.ifCondition) {
|
|
240
|
+
// Already reported by the parser (only one structural directive per
|
|
241
|
+
// element) — avoid also emitting a confusing double-wrapped loop/if.
|
|
242
|
+
node = { ...node, ifCondition: null };
|
|
243
|
+
}
|
|
244
|
+
if (node.forBinding) {
|
|
245
|
+
const forBind = node.forBinding;
|
|
246
|
+
// The iterable is evaluated in the *enclosing* scope — the loop
|
|
247
|
+
// variable it's about to introduce isn't in scope for it yet.
|
|
248
|
+
const iterable = this.resolve(forBind.iterable, localScope);
|
|
249
|
+
const innerScope = new Set([...localScope, forBind.varName]);
|
|
250
|
+
const { varName: elVar, statements: buildStatements } = this.buildElement({ ...node, forBinding: null }, innerScope);
|
|
251
|
+
const body = block([...buildStatements, this.appendChild(parentVar, elVar, node)], node);
|
|
252
|
+
const forIn = {
|
|
253
|
+
kind: "ForInStatement",
|
|
254
|
+
varType: forBind.varType,
|
|
255
|
+
varName: forBind.varName,
|
|
256
|
+
iterable,
|
|
257
|
+
body,
|
|
258
|
+
line: forBind.line,
|
|
259
|
+
col: forBind.col,
|
|
260
|
+
};
|
|
261
|
+
return [forIn];
|
|
262
|
+
}
|
|
263
|
+
if (node.ifCondition) {
|
|
264
|
+
const condition = this.resolve(node.ifCondition, localScope);
|
|
265
|
+
const { varName: elVar, statements: buildStatements } = this.buildElement({ ...node, ifCondition: null }, localScope);
|
|
266
|
+
const thenBranch = block([...buildStatements, this.appendChild(parentVar, elVar, node)], node);
|
|
267
|
+
const ifStmt = { kind: "IfStatement", condition, thenBranch, elseBranch: null, line: node.line, col: node.col };
|
|
268
|
+
return [ifStmt];
|
|
269
|
+
}
|
|
270
|
+
const { varName: elVar, statements } = this.buildElement(node, localScope);
|
|
271
|
+
return [...statements, this.appendChild(parentVar, elVar, node)];
|
|
272
|
+
}
|
|
273
|
+
appendChild(parentVar, childVar, at) {
|
|
274
|
+
return exprStatement(call(member(ident(parentVar, at), "appendChild", at), [ident(childVar, at)], at), at);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export var TemplateTokenKind;
|
|
2
|
+
(function (TemplateTokenKind) {
|
|
3
|
+
TemplateTokenKind["TagOpen"] = "TagOpen";
|
|
4
|
+
TemplateTokenKind["TagSelfClose"] = "TagSelfClose";
|
|
5
|
+
TemplateTokenKind["TagOpenEnd"] = "TagOpenEnd";
|
|
6
|
+
TemplateTokenKind["TagClose"] = "TagClose";
|
|
7
|
+
TemplateTokenKind["AttrName"] = "AttrName";
|
|
8
|
+
TemplateTokenKind["Equals"] = "Equals";
|
|
9
|
+
TemplateTokenKind["AttrValue"] = "AttrValue";
|
|
10
|
+
TemplateTokenKind["Text"] = "Text";
|
|
11
|
+
TemplateTokenKind["EOF"] = "EOF";
|
|
12
|
+
})(TemplateTokenKind || (TemplateTokenKind = {}));
|
|
13
|
+
export class TemplateLexer {
|
|
14
|
+
constructor(source, diagnostics) {
|
|
15
|
+
this.source = source;
|
|
16
|
+
this.diagnostics = diagnostics;
|
|
17
|
+
this.pos = 0;
|
|
18
|
+
this.line = 1;
|
|
19
|
+
this.col = 1;
|
|
20
|
+
// Set while scanning inside a tag (between '<name' and the closing '>'
|
|
21
|
+
// or '/>') — text-vs-markup scanning rules differ there (attribute
|
|
22
|
+
// syntax vs. raw text content).
|
|
23
|
+
this.inTag = false;
|
|
24
|
+
}
|
|
25
|
+
tokenize() {
|
|
26
|
+
const tokens = [];
|
|
27
|
+
while (true) {
|
|
28
|
+
const token = this.inTag ? this.nextTagToken() : this.nextContentToken();
|
|
29
|
+
tokens.push(token);
|
|
30
|
+
if (token.kind === TemplateTokenKind.EOF)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return tokens;
|
|
34
|
+
}
|
|
35
|
+
isAtEnd() {
|
|
36
|
+
return this.pos >= this.source.length;
|
|
37
|
+
}
|
|
38
|
+
peek(offset = 0) {
|
|
39
|
+
return this.source[this.pos + offset] ?? "";
|
|
40
|
+
}
|
|
41
|
+
advance() {
|
|
42
|
+
const c = this.source[this.pos++];
|
|
43
|
+
if (c === "\n") {
|
|
44
|
+
this.line++;
|
|
45
|
+
this.col = 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.col++;
|
|
49
|
+
}
|
|
50
|
+
return c;
|
|
51
|
+
}
|
|
52
|
+
make(kind, lexeme, line, col) {
|
|
53
|
+
return { kind, lexeme, line, col };
|
|
54
|
+
}
|
|
55
|
+
skipComment() {
|
|
56
|
+
// Already past '<!--' when called.
|
|
57
|
+
while (!this.isAtEnd() && !(this.peek() === "-" && this.peek(1) === "-" && this.peek(2) === ">"))
|
|
58
|
+
this.advance();
|
|
59
|
+
if (!this.isAtEnd()) {
|
|
60
|
+
this.advance();
|
|
61
|
+
this.advance();
|
|
62
|
+
this.advance(); // '-->'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Outside a tag: either a '<...' (comment, closing tag, or opening tag)
|
|
66
|
+
// or a run of plain text up to the next '<'.
|
|
67
|
+
nextContentToken() {
|
|
68
|
+
if (this.isAtEnd())
|
|
69
|
+
return this.make(TemplateTokenKind.EOF, "", this.line, this.col);
|
|
70
|
+
if (this.peek() === "<") {
|
|
71
|
+
if (this.peek(1) === "!" && this.peek(2) === "-" && this.peek(3) === "-") {
|
|
72
|
+
this.advance();
|
|
73
|
+
this.advance();
|
|
74
|
+
this.advance();
|
|
75
|
+
this.advance(); // '<!--'
|
|
76
|
+
this.skipComment();
|
|
77
|
+
return this.nextContentToken(); // comments produce no token at all
|
|
78
|
+
}
|
|
79
|
+
if (this.peek(1) === "/") {
|
|
80
|
+
const line = this.line;
|
|
81
|
+
const col = this.col;
|
|
82
|
+
this.advance();
|
|
83
|
+
this.advance(); // '</'
|
|
84
|
+
const name = this.readName();
|
|
85
|
+
this.skipInlineWhitespace();
|
|
86
|
+
if (this.peek() === ">")
|
|
87
|
+
this.advance();
|
|
88
|
+
return this.make(TemplateTokenKind.TagClose, name, line, col);
|
|
89
|
+
}
|
|
90
|
+
const line = this.line;
|
|
91
|
+
const col = this.col;
|
|
92
|
+
this.advance(); // '<'
|
|
93
|
+
const name = this.readName();
|
|
94
|
+
this.inTag = true;
|
|
95
|
+
return this.make(TemplateTokenKind.TagOpen, name, line, col);
|
|
96
|
+
}
|
|
97
|
+
const line = this.line;
|
|
98
|
+
const col = this.col;
|
|
99
|
+
let text = "";
|
|
100
|
+
while (!this.isAtEnd() && this.peek() !== "<")
|
|
101
|
+
text += this.advance();
|
|
102
|
+
return this.make(TemplateTokenKind.Text, text, line, col);
|
|
103
|
+
}
|
|
104
|
+
// Inside a tag, after the tag name: attribute names/values, '=', and the
|
|
105
|
+
// '>' or '/>' that ends it.
|
|
106
|
+
nextTagToken() {
|
|
107
|
+
this.skipInlineWhitespace();
|
|
108
|
+
if (this.isAtEnd())
|
|
109
|
+
return this.make(TemplateTokenKind.EOF, "", this.line, this.col);
|
|
110
|
+
const line = this.line;
|
|
111
|
+
const col = this.col;
|
|
112
|
+
if (this.peek() === "/" && this.peek(1) === ">") {
|
|
113
|
+
this.advance();
|
|
114
|
+
this.advance();
|
|
115
|
+
this.inTag = false;
|
|
116
|
+
return this.make(TemplateTokenKind.TagSelfClose, "/>", line, col);
|
|
117
|
+
}
|
|
118
|
+
if (this.peek() === ">") {
|
|
119
|
+
this.advance();
|
|
120
|
+
this.inTag = false;
|
|
121
|
+
return this.make(TemplateTokenKind.TagOpenEnd, ">", line, col);
|
|
122
|
+
}
|
|
123
|
+
if (this.peek() === "=") {
|
|
124
|
+
this.advance();
|
|
125
|
+
return this.make(TemplateTokenKind.Equals, "=", line, col);
|
|
126
|
+
}
|
|
127
|
+
if (this.peek() === '"') {
|
|
128
|
+
this.advance();
|
|
129
|
+
let value = "";
|
|
130
|
+
while (!this.isAtEnd() && this.peek() !== '"')
|
|
131
|
+
value += this.advance();
|
|
132
|
+
if (this.isAtEnd()) {
|
|
133
|
+
this.diagnostics.error("Unterminated attribute value", line, col);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.advance(); // closing '"'
|
|
137
|
+
}
|
|
138
|
+
return this.make(TemplateTokenKind.AttrValue, value, line, col);
|
|
139
|
+
}
|
|
140
|
+
// An attribute name: `(click)`, `[className]`, `*if`, `*for`, or a
|
|
141
|
+
// plain `id` — every character up to whitespace/'='/'>'/'/'.
|
|
142
|
+
let name = "";
|
|
143
|
+
while (!this.isAtEnd() && !/[\s=>/]/.test(this.peek()))
|
|
144
|
+
name += this.advance();
|
|
145
|
+
if (name.length === 0) {
|
|
146
|
+
this.diagnostics.error(`Unexpected character '${this.peek()}' in tag`, line, col);
|
|
147
|
+
this.advance(); // avoid an infinite loop on a genuinely unexpected character
|
|
148
|
+
return this.nextTagToken();
|
|
149
|
+
}
|
|
150
|
+
return this.make(TemplateTokenKind.AttrName, name, line, col);
|
|
151
|
+
}
|
|
152
|
+
skipInlineWhitespace() {
|
|
153
|
+
while (!this.isAtEnd() && /\s/.test(this.peek()))
|
|
154
|
+
this.advance();
|
|
155
|
+
}
|
|
156
|
+
readName() {
|
|
157
|
+
let name = "";
|
|
158
|
+
while (!this.isAtEnd() && /[A-Za-z0-9-]/.test(this.peek()))
|
|
159
|
+
name += this.advance();
|
|
160
|
+
return name;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Builds a template's element/text tree from TemplateLexer's token stream,
|
|
2
|
+
// parsing every binding's value (event/property bindings, *if/*for,
|
|
3
|
+
// {{ }} interpolation) as a real KopScript expression via kopscript's own
|
|
4
|
+
// Lexer/Parser — reusing parseStandaloneExpression/parseStandaloneType
|
|
5
|
+
// (see parser.ts) rather than inventing a second expression language.
|
|
6
|
+
//
|
|
7
|
+
// A fragment parsed out of an attribute value or a {{ }} span starts its
|
|
8
|
+
// own line/col numbering at (1, 1); `remapPosition` translates a position
|
|
9
|
+
// found while parsing one back into real coordinates in the .html file, so
|
|
10
|
+
// a mistake inside a binding ("Cuont.Value") reports at the right place in
|
|
11
|
+
// the template, not "line 1, column 6" every time.
|
|
12
|
+
import { TokenKind } from "./tokens.js";
|
|
13
|
+
import { Lexer } from "./lexer.js";
|
|
14
|
+
import { Parser } from "./parser.js";
|
|
15
|
+
import { DiagnosticBag } from "./diagnostics.js";
|
|
16
|
+
import { TemplateLexer, TemplateTokenKind } from "./template_lexer.js";
|
|
17
|
+
const PROPERTY_ALIASES = { class: "className" };
|
|
18
|
+
function remapPosition(fragmentLine, fragmentCol, localLine, localCol) {
|
|
19
|
+
if (localLine === 1)
|
|
20
|
+
return { line: fragmentLine, col: fragmentCol + localCol - 1 };
|
|
21
|
+
return { line: fragmentLine + localLine - 1, col: localCol };
|
|
22
|
+
}
|
|
23
|
+
export class TemplateParser {
|
|
24
|
+
constructor(source, diagnostics) {
|
|
25
|
+
this.diagnostics = diagnostics;
|
|
26
|
+
this.pos = 0;
|
|
27
|
+
this.tokens = new TemplateLexer(source, diagnostics).tokenize();
|
|
28
|
+
}
|
|
29
|
+
peek() {
|
|
30
|
+
return this.tokens[this.pos];
|
|
31
|
+
}
|
|
32
|
+
advance() {
|
|
33
|
+
return this.tokens[this.pos++];
|
|
34
|
+
}
|
|
35
|
+
check(kind) {
|
|
36
|
+
return this.peek().kind === kind;
|
|
37
|
+
}
|
|
38
|
+
// Parses a fragment of real KopScript source (an attribute value, or the
|
|
39
|
+
// text inside {{ }}) as a standalone expression, remapping any
|
|
40
|
+
// diagnostics it produces back into the template file's own coordinates.
|
|
41
|
+
parseEmbeddedExpression(source, fragmentLine, fragmentCol) {
|
|
42
|
+
const localDiagnostics = new DiagnosticBag();
|
|
43
|
+
const tokens = new Lexer(source, localDiagnostics).tokenize();
|
|
44
|
+
const expr = new Parser(tokens, localDiagnostics).parseStandaloneExpression();
|
|
45
|
+
for (const d of localDiagnostics.diagnostics) {
|
|
46
|
+
const { line, col } = remapPosition(fragmentLine, fragmentCol, d.line, d.col);
|
|
47
|
+
this.diagnostics.error(d.message, line, col);
|
|
48
|
+
}
|
|
49
|
+
return expr;
|
|
50
|
+
}
|
|
51
|
+
// "Item item of Items" — an explicit element type (matching KopScript's
|
|
52
|
+
// own no-inference philosophy), a variable name, the contextual "of"
|
|
53
|
+
// separator (a plain identifier, not a reserved keyword), then the
|
|
54
|
+
// iterable expression.
|
|
55
|
+
parseForBinding(source, fragmentLine, fragmentCol) {
|
|
56
|
+
const localDiagnostics = new DiagnosticBag();
|
|
57
|
+
const tokens = new Lexer(source, localDiagnostics).tokenize();
|
|
58
|
+
const ofIndex = tokens.findIndex((t) => t.kind === TokenKind.Identifier && t.lexeme === "of");
|
|
59
|
+
if (ofIndex < 1 || tokens[ofIndex - 1].kind !== TokenKind.Identifier) {
|
|
60
|
+
this.diagnostics.error(`Expected '*for="Type varName of iterable"', got '${source}'`, fragmentLine, fragmentCol);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const varNameToken = tokens[ofIndex - 1];
|
|
64
|
+
const typeTokens = [...tokens.slice(0, ofIndex - 1), tokens[tokens.length - 1]]; // real tokens + the shared EOF
|
|
65
|
+
const iterableTokens = tokens.slice(ofIndex + 1);
|
|
66
|
+
const varType = new Parser(typeTokens, localDiagnostics).parseStandaloneType();
|
|
67
|
+
const iterable = new Parser(iterableTokens, localDiagnostics).parseStandaloneExpression();
|
|
68
|
+
for (const d of localDiagnostics.diagnostics) {
|
|
69
|
+
const { line, col } = remapPosition(fragmentLine, fragmentCol, d.line, d.col);
|
|
70
|
+
this.diagnostics.error(d.message, line, col);
|
|
71
|
+
}
|
|
72
|
+
return { varType, varName: varNameToken.lexeme, iterable, line: fragmentLine, col: fragmentCol };
|
|
73
|
+
}
|
|
74
|
+
// Splits raw text into alternating literal/`{{ expr }}` parts, matching
|
|
75
|
+
// AST.InterpolatedPart — the exact shape `$"...{expr}..."` already
|
|
76
|
+
// produces, so a text node desugars straight into an InterpolatedStringLiteral
|
|
77
|
+
// with zero new codegen (see template_compiler.ts).
|
|
78
|
+
parseTextParts(text, startLine, startCol) {
|
|
79
|
+
const parts = [];
|
|
80
|
+
let i = 0;
|
|
81
|
+
let line = startLine;
|
|
82
|
+
let col = startCol;
|
|
83
|
+
let literal = "";
|
|
84
|
+
const advancePos = (chunk) => {
|
|
85
|
+
for (const ch of chunk) {
|
|
86
|
+
if (ch === "\n") {
|
|
87
|
+
line++;
|
|
88
|
+
col = 1;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
col++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
while (i < text.length) {
|
|
96
|
+
if (text[i] === "{" && text[i + 1] === "{") {
|
|
97
|
+
if (literal) {
|
|
98
|
+
parts.push({ kind: "Text", text: literal });
|
|
99
|
+
literal = "";
|
|
100
|
+
}
|
|
101
|
+
const exprStartOffset = i + 2;
|
|
102
|
+
const end = text.indexOf("}}", exprStartOffset);
|
|
103
|
+
const exprLine = line;
|
|
104
|
+
const exprCol = col;
|
|
105
|
+
advancePos(text.slice(i, exprStartOffset));
|
|
106
|
+
if (end === -1) {
|
|
107
|
+
this.diagnostics.error("Unterminated '{{' interpolation", exprLine, exprCol);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const exprSource = text.slice(exprStartOffset, end);
|
|
111
|
+
const expr = this.parseEmbeddedExpression(exprSource, exprLine, exprCol);
|
|
112
|
+
parts.push({ kind: "Expr", expression: expr });
|
|
113
|
+
advancePos(text.slice(exprStartOffset, end + 2));
|
|
114
|
+
i = end + 2;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
literal += text[i];
|
|
118
|
+
advancePos(text[i]);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
if (literal)
|
|
122
|
+
parts.push({ kind: "Text", text: literal });
|
|
123
|
+
return parts;
|
|
124
|
+
}
|
|
125
|
+
// Parses the whole template, enforcing the single-root-element rule
|
|
126
|
+
// (Component.Render() returns exactly one Element — no silent
|
|
127
|
+
// auto-wrapping if the author writes multiple top-level tags).
|
|
128
|
+
parseDocument() {
|
|
129
|
+
const roots = [];
|
|
130
|
+
while (!this.check(TemplateTokenKind.EOF)) {
|
|
131
|
+
if (this.check(TemplateTokenKind.Text) && this.peek().lexeme.trim() === "") {
|
|
132
|
+
this.advance(); // insignificant top-level whitespace
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const node = this.parseNode();
|
|
136
|
+
if (node)
|
|
137
|
+
roots.push(node);
|
|
138
|
+
}
|
|
139
|
+
const elementRoots = roots.filter((r) => r.kind === "element");
|
|
140
|
+
if (elementRoots.length !== 1) {
|
|
141
|
+
const at = roots[0] ?? { line: 1, col: 1 };
|
|
142
|
+
this.diagnostics.error(`A template must have exactly one top-level element, found ${elementRoots.length}`, at.line, at.col);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return elementRoots[0];
|
|
146
|
+
}
|
|
147
|
+
parseNode() {
|
|
148
|
+
if (this.check(TemplateTokenKind.Text)) {
|
|
149
|
+
const t = this.advance();
|
|
150
|
+
return { kind: "text", parts: this.parseTextParts(t.lexeme, t.line, t.col), line: t.line, col: t.col };
|
|
151
|
+
}
|
|
152
|
+
if (this.check(TemplateTokenKind.TagOpen)) {
|
|
153
|
+
return this.parseElement();
|
|
154
|
+
}
|
|
155
|
+
const t = this.advance();
|
|
156
|
+
this.diagnostics.error(`Unexpected token in template ('${t.lexeme}')`, t.line, t.col);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
parseElement() {
|
|
160
|
+
const tagTok = this.advance(); // TagOpen
|
|
161
|
+
const tag = tagTok.lexeme;
|
|
162
|
+
const staticAttrs = [];
|
|
163
|
+
const propBindings = [];
|
|
164
|
+
const eventBindings = [];
|
|
165
|
+
let ifCondition = null;
|
|
166
|
+
let forBinding = null;
|
|
167
|
+
while (this.check(TemplateTokenKind.AttrName)) {
|
|
168
|
+
const nameTok = this.advance();
|
|
169
|
+
this.consumeEquals(nameTok);
|
|
170
|
+
const valueTok = this.consumeAttrValue(nameTok);
|
|
171
|
+
if (!valueTok)
|
|
172
|
+
continue;
|
|
173
|
+
const name = nameTok.lexeme;
|
|
174
|
+
if (name.startsWith("(") && name.endsWith(")")) {
|
|
175
|
+
const eventName = name.slice(1, -1);
|
|
176
|
+
const handler = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
177
|
+
eventBindings.push({ name: eventName, handler, line: nameTok.line, col: nameTok.col });
|
|
178
|
+
}
|
|
179
|
+
else if (name.startsWith("[") && name.endsWith("]")) {
|
|
180
|
+
const propName = name.slice(1, -1);
|
|
181
|
+
const value = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
182
|
+
propBindings.push({ name: PROPERTY_ALIASES[propName] ?? propName, value, line: nameTok.line, col: nameTok.col });
|
|
183
|
+
}
|
|
184
|
+
else if (name === "*if") {
|
|
185
|
+
if (ifCondition || forBinding) {
|
|
186
|
+
this.diagnostics.error(`Only one structural directive ('*if'/'*for') is allowed per element`, nameTok.line, nameTok.col);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
ifCondition = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (name === "*for") {
|
|
193
|
+
if (ifCondition || forBinding) {
|
|
194
|
+
this.diagnostics.error(`Only one structural directive ('*if'/'*for') is allowed per element`, nameTok.line, nameTok.col);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
forBinding = this.parseForBinding(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
staticAttrs.push({ name: PROPERTY_ALIASES[name] ?? name, value: valueTok.lexeme, line: nameTok.line, col: nameTok.col });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const children = [];
|
|
205
|
+
if (this.check(TemplateTokenKind.TagSelfClose)) {
|
|
206
|
+
this.advance();
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.consume(TemplateTokenKind.TagOpenEnd, tagTok, `Expected '>' or '/>' to close '<${tag}'`);
|
|
210
|
+
while (!this.check(TemplateTokenKind.TagClose) && !this.check(TemplateTokenKind.EOF)) {
|
|
211
|
+
if (this.check(TemplateTokenKind.Text) && this.peek().lexeme.trim() === "") {
|
|
212
|
+
this.advance(); // insignificant whitespace between child elements
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const child = this.parseNode();
|
|
216
|
+
if (child)
|
|
217
|
+
children.push(child);
|
|
218
|
+
}
|
|
219
|
+
if (this.check(TemplateTokenKind.TagClose)) {
|
|
220
|
+
const closeTok = this.advance();
|
|
221
|
+
if (closeTok.lexeme !== tag) {
|
|
222
|
+
this.diagnostics.error(`Mismatched closing tag: expected '</${tag}>', got '</${closeTok.lexeme}>'`, closeTok.line, closeTok.col);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
this.diagnostics.error(`Expected closing tag '</${tag}>'`, tagTok.line, tagTok.col);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
230
|
+
}
|
|
231
|
+
consumeEquals(attrNameTok) {
|
|
232
|
+
this.consume(TemplateTokenKind.Equals, attrNameTok, `Expected '=' after attribute '${attrNameTok.lexeme}'`);
|
|
233
|
+
}
|
|
234
|
+
consumeAttrValue(attrNameTok) {
|
|
235
|
+
if (!this.check(TemplateTokenKind.AttrValue)) {
|
|
236
|
+
this.diagnostics.error(`Expected a quoted value after '${attrNameTok.lexeme}='`, attrNameTok.line, attrNameTok.col);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return this.advance();
|
|
240
|
+
}
|
|
241
|
+
consume(kind, at, message) {
|
|
242
|
+
if (this.check(kind)) {
|
|
243
|
+
this.advance();
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
this.diagnostics.error(message, at.line, at.col);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
package/dist/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopscript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript, with generics and nullable types",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,12 +9,17 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://dev.azure.com/koppinator/Koppindependence/_git/Kop"
|
|
11
11
|
},
|
|
12
|
+
"homepage": "https://kopular.dev",
|
|
12
13
|
"keywords": [
|
|
13
14
|
"language",
|
|
14
15
|
"compiler",
|
|
15
16
|
"transpiler",
|
|
16
17
|
"typescript",
|
|
17
|
-
"javascript"
|
|
18
|
+
"javascript",
|
|
19
|
+
"ai",
|
|
20
|
+
"llm",
|
|
21
|
+
"codegen",
|
|
22
|
+
"agent"
|
|
18
23
|
],
|
|
19
24
|
"main": "./dist/modules.js",
|
|
20
25
|
"files": [
|
|
@@ -37,7 +42,7 @@
|
|
|
37
42
|
"@types/node": "^20.14.0",
|
|
38
43
|
"tsx": "^4.16.0",
|
|
39
44
|
"typescript": "^5.5.0",
|
|
40
|
-
"vitest": "^
|
|
45
|
+
"vitest": "^4.1.11"
|
|
41
46
|
},
|
|
42
47
|
"engines": {
|
|
43
48
|
"node": ">=18"
|