kopscript 0.16.0 → 0.18.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/LLM.md +20 -9
- package/README.md +34 -19
- package/dist/template_compiler.js +69 -9
- package/dist/template_parser.js +11 -2
- package/package.json +2 -2
package/LLM.md
CHANGED
|
@@ -540,11 +540,22 @@ class Counter : Component {
|
|
|
540
540
|
- One `template` per class; a class with both `template` and a hand-written `Render()` is a
|
|
541
541
|
compile error.
|
|
542
542
|
- Desugars, between parsing and checking, into the exact same `MethodDecl` AST a
|
|
543
|
-
hand-written `Render()` would produce —
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
543
|
+
hand-written `Render()` would produce — building a `VElement` tree as data, no runtime
|
|
544
|
+
template engine, no diffing of its own (Kopular's own `Component` base class does that
|
|
545
|
+
against the tree this produces). `{{ }}`/binding contents are real KopScript, parsed and
|
|
546
|
+
type-checked normally.
|
|
547
|
+
- Bindings: `{{ expr }}` (text interpolation, → `VElement.TextContent = InterpolatedStringLiteral`),
|
|
548
|
+
`(event)="stmt"` (→ assignment to one of `VElement`'s four named event fields —
|
|
549
|
+
`OnClick`/`OnInput`/`OnBlur`/`OnChange`; any other event name is a compile error),
|
|
550
|
+
`[prop]="expr"` (→ a plain field assignment for `id`/`className`/`value`, or
|
|
551
|
+
`VElement.SetAttr("prop", expr)` for anything else), static `attr="..."` (`class` aliases
|
|
552
|
+
to `className`).
|
|
553
|
+
- **Two-way binding**: `[(value)]="Field"` → `[value]="Field"` + an auto-generated
|
|
554
|
+
`(input)="Field = e.target.value"`. `value` only (`KS5017` otherwise — no other
|
|
555
|
+
`VElement` field has an equivalent "user just changed this" event); `Field` must resolve
|
|
556
|
+
to a bare name or `this.Field` (`KS5018` otherwise — anything else has nothing sensible
|
|
557
|
+
to assign back into). A hand-written `Render()` has no equivalent shorthand — it already
|
|
558
|
+
has direct field/handler access, so there's nothing to desugar.
|
|
548
559
|
- Structural directives: `*if="expr"` (→ real `if`), `*for="Type varName of expr"` (→ real
|
|
549
560
|
`for..in`; the element type is explicit — no inference, same stance as Generics).
|
|
550
561
|
At most one structural directive per element.
|
|
@@ -553,12 +564,12 @@ class Counter : Component {
|
|
|
553
564
|
manual `Subscribe` needed for that field. State reached indirectly (through a method call,
|
|
554
565
|
or `this.SomeService.Count`) still needs a manual `Subscribe`, unchanged from before.
|
|
555
566
|
- Exactly one top-level element per template (no auto-wrap — hard error). No mixing text and
|
|
556
|
-
element children under one element. No
|
|
567
|
+
element children under one element. No pipes, no stacked directives.
|
|
557
568
|
- `ks watch` tracks the referenced `.html` file as well as `.ks` dependencies.
|
|
558
569
|
- Layering note: `template from` is kopscript grammar, but what it desugars *to*
|
|
559
|
-
(`
|
|
560
|
-
Kopular's `
|
|
561
|
-
generic pluggable target.
|
|
570
|
+
(`VElement.Create`/`.AppendChild`/`.TextContent`/`.SetAttr`/the named `On*` event fields)
|
|
571
|
+
assumes Kopular's `velement.ks` surface specifically — a deliberate, documented coupling,
|
|
572
|
+
not a generic pluggable target.
|
|
562
573
|
|
|
563
574
|
## Keywords (reserved, lowercase, exact match)
|
|
564
575
|
|
package/README.md
CHANGED
|
@@ -538,12 +538,12 @@ class Counter : Component {
|
|
|
538
538
|
this.Count.Subscribe((number v) => this.Update());
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
public override
|
|
542
|
-
|
|
543
|
-
button.
|
|
544
|
-
button.
|
|
541
|
+
public override VElement Render() {
|
|
542
|
+
VElement button = VElement.Create("button");
|
|
543
|
+
button.TextContent = "Count: " + this.Count.Value;
|
|
544
|
+
button.OnClick = (Event e) => {
|
|
545
545
|
this.Count.Value = this.Count.Value + 1; // Update() fires automatically
|
|
546
|
-
}
|
|
546
|
+
};
|
|
547
547
|
return button;
|
|
548
548
|
}
|
|
549
549
|
}
|
|
@@ -587,11 +587,13 @@ class Counter : Component {
|
|
|
587
587
|
This compiles to exactly the `Render()` method you'd otherwise write by hand — the
|
|
588
588
|
template compiler is a pass that runs between parsing and type-checking, turning the
|
|
589
589
|
markup into ordinary `MethodDecl`/statement/expression AST nodes and splicing the result
|
|
590
|
-
into the class before checking ever runs. There's no separate runtime template engine
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
590
|
+
into the class before checking ever runs. There's no separate runtime template engine and
|
|
591
|
+
no interpreted expression language: `{{ Count.Value }}` and `(click)="Increment()"`
|
|
592
|
+
contain real KopScript, parsed and type-checked exactly like anything else in the file,
|
|
593
|
+
with errors reported at their real position in the `.html` file, not the `.ks` file. The
|
|
594
|
+
compiler itself does no diffing — it just builds a `VElement` tree as data; Kopular's own
|
|
595
|
+
`Component` base class is what diffs that tree against the previous render and patches
|
|
596
|
+
real DOM (see Kopular's own README/CHANGELOG for that engine).
|
|
595
597
|
|
|
596
598
|
A class may have a `template` or a hand-written `Render()`, never both — that's a compile
|
|
597
599
|
error. `ks watch` also tracks the referenced `.html` file, so editing markup alone
|
|
@@ -601,13 +603,25 @@ Supported bindings and directives:
|
|
|
601
603
|
|
|
602
604
|
| Syntax | Desugars to |
|
|
603
605
|
| --------------------------- | --------------------------------------------------------- |
|
|
604
|
-
| `{{ expr }}` (in text) | `el.
|
|
605
|
-
| `(event)="stmt"` | `el.
|
|
606
|
-
| `[prop]="expr"` | `el.
|
|
607
|
-
| `
|
|
606
|
+
| `{{ expr }}` (in text) | `el.TextContent = $"...{expr}...";` (an `InterpolatedStringLiteral`, same as `$"..."`) |
|
|
607
|
+
| `(event)="stmt"` | `el.OnEvent = (Event e) => { stmt };` — `event` must be one of `click`/`input`/`blur`/`change`, `VElement`'s own fixed set of named event fields |
|
|
608
|
+
| `[prop]="expr"` | `el.Prop = expr;` for `id`/`className`/`value` (`VElement`'s own named fields); `el.SetAttr("prop", expr);` for anything else |
|
|
609
|
+
| `[(value)]="Field"` | sugar for `[value]="Field"` + `(input)="Field = e.target.value"` — see below |
|
|
610
|
+
| `class="..."` (static) | `el.ClassName = "...";` (aliased, since `class` is a KopScript keyword) |
|
|
608
611
|
| `*if="expr"` | a real `if (expr) { ... }` around the element's creation |
|
|
609
612
|
| `*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) |
|
|
610
613
|
|
|
614
|
+
**Two-way binding**: `[(value)]="Field"` (a template-only sugar — there's no equivalent
|
|
615
|
+
shorthand for a hand-written `Render()`, which already has direct field/handler access)
|
|
616
|
+
desugars to exactly the pair you'd otherwise write by hand: a `[value]` property binding
|
|
617
|
+
plus an auto-generated `(input)` handler assigning back. Restricted to `value`
|
|
618
|
+
specifically — the one `VElement` field a user can change through direct interaction
|
|
619
|
+
(typing, picking an option); `[(id)]`/`[(className)]` are compile errors (`KS5017`), since
|
|
620
|
+
neither has an equivalent "user just changed this" event. The bound expression must be a
|
|
621
|
+
simple field reference (a bare name or `this.Field`, resolving the same way any other
|
|
622
|
+
binding does) — `[(value)]="Field.Trim()"` is a compile error (`KS5018`), since there's
|
|
623
|
+
nothing sensible to assign back into.
|
|
624
|
+
|
|
611
625
|
**Auto-subscribe**: a `state<T>` field declared directly on the component and referenced
|
|
612
626
|
directly in its template (like `Count` above) gets its `Subscribe((v) => this.Update())`
|
|
613
627
|
wired up automatically — no manual `Subscribe` call needed in the constructor. This is a
|
|
@@ -618,14 +632,15 @@ templates existed.
|
|
|
618
632
|
|
|
619
633
|
v1 cuts, same discipline as generics and nullable types: exactly one top-level element per
|
|
620
634
|
template (no auto-wrapping — a clear error instead); no mixing text and element children
|
|
621
|
-
under one element (
|
|
622
|
-
|
|
635
|
+
under one element (`VElement` has no text-node concept, only `.TextContent`); no pipes, no
|
|
636
|
+
stacking two structural directives on one element.
|
|
623
637
|
|
|
624
638
|
**A deliberate layering note**: the `template from` syntax lives in kopscript's own
|
|
625
639
|
grammar (Kopular can't extend a language it doesn't own), but what it desugars *to* —
|
|
626
|
-
`
|
|
627
|
-
exactly the
|
|
628
|
-
|
|
640
|
+
`VElement.Create`, `.AppendChild`, `.TextContent`, `.SetAttr`, the named `On*` event
|
|
641
|
+
fields — assumes exactly the surface
|
|
642
|
+
[Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s
|
|
643
|
+
`velement.ks` declares. That's a real coupling from the compiler to one specific consumer,
|
|
629
644
|
accepted deliberately rather than building a generic pluggable desugaring-target system
|
|
630
645
|
for a hypothetical second framework that doesn't exist today. If one ever does, that's the
|
|
631
646
|
point to generalize this.
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
const
|
|
1
|
+
const VELEMENT_TYPE = { kind: "NamedType", name: "VElement", typeArgs: null, line: 0, col: 0 };
|
|
2
2
|
const VOID_TYPE = { kind: "NamedType", name: "void", typeArgs: null, line: 0, col: 0 };
|
|
3
3
|
const EVENT_TYPE = { kind: "NamedType", name: "Event", typeArgs: null, line: 0, col: 0 };
|
|
4
|
+
// The only attrs/prop bindings with a real named VElement field — anything
|
|
5
|
+
// else goes through SetAttr instead (see buildAttrAssignment). Keys are
|
|
6
|
+
// post-alias names (see template_parser.ts's PROPERTY_ALIASES — "class" is
|
|
7
|
+
// already "className" by the time it reaches here).
|
|
8
|
+
const KNOWN_FIELD_NAMES = { id: "Id", className: "ClassName", value: "Value" };
|
|
9
|
+
// VElement's own fixed, named event slots (see velement.ks) — there's no
|
|
10
|
+
// generic addEventListener on VElement (event handlers are part of the
|
|
11
|
+
// VNode's own data, not wired against a live DOM node), so a template can
|
|
12
|
+
// only bind one of these four.
|
|
13
|
+
const KNOWN_EVENT_NAMES = { click: "OnClick", input: "OnInput", blur: "OnBlur", change: "OnChange" };
|
|
4
14
|
function ident(name, at) {
|
|
5
15
|
return { kind: "Identifier", name, line: at.line, col: at.col };
|
|
6
16
|
}
|
|
@@ -20,7 +30,7 @@ function assign(target, value, at) {
|
|
|
20
30
|
return exprStatement({ kind: "AssignExpr", target, value, line: at.line, col: at.col }, at);
|
|
21
31
|
}
|
|
22
32
|
function varDecl(name, init, at) {
|
|
23
|
-
return { kind: "VarDecl", isConst: false, name, nameLine: at.line, nameCol: at.col, type:
|
|
33
|
+
return { kind: "VarDecl", isConst: false, name, nameLine: at.line, nameCol: at.col, type: VELEMENT_TYPE, init, line: at.line, col: at.col };
|
|
24
34
|
}
|
|
25
35
|
function block(statements, at) {
|
|
26
36
|
return { kind: "Block", statements, line: at.line, col: at.col };
|
|
@@ -181,7 +191,7 @@ export class TemplateCompiler {
|
|
|
181
191
|
nameLine: templateDeclAt.line,
|
|
182
192
|
nameCol: templateDeclAt.col,
|
|
183
193
|
params: [],
|
|
184
|
-
returnType:
|
|
194
|
+
returnType: VELEMENT_TYPE,
|
|
185
195
|
body: block(statements, templateDeclAt),
|
|
186
196
|
line: templateDeclAt.line,
|
|
187
197
|
col: templateDeclAt.col,
|
|
@@ -200,14 +210,19 @@ export class TemplateCompiler {
|
|
|
200
210
|
const varName = this.freshVar();
|
|
201
211
|
const statements = [];
|
|
202
212
|
const self = ident(varName, at);
|
|
203
|
-
statements.push(varDecl(varName, call(member(ident("
|
|
213
|
+
statements.push(varDecl(varName, call(member(ident("VElement", at), "Create", at), [stringLiteral(node.tag, at)], at), at));
|
|
204
214
|
for (const attr of node.staticAttrs)
|
|
205
|
-
statements.push(
|
|
215
|
+
statements.push(this.buildAttrAssignment(self, attr.name, stringLiteral(attr.value, attr), attr));
|
|
206
216
|
for (const bind of node.propBindings) {
|
|
207
217
|
const value = this.resolve(bind.value, localScope);
|
|
208
|
-
statements.push(
|
|
218
|
+
statements.push(this.buildAttrAssignment(self, bind.name, value, bind));
|
|
209
219
|
}
|
|
210
220
|
for (const bind of node.eventBindings) {
|
|
221
|
+
const fieldName = KNOWN_EVENT_NAMES[bind.name];
|
|
222
|
+
if (!fieldName) {
|
|
223
|
+
this.diagnostics.error("KS5016", `Unsupported event binding '(${bind.name})' — a template can only bind (click), (input), (blur), or (change), the same fixed set of named event fields VElement itself has`, bind.line, bind.col);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
211
226
|
const handler = this.resolve(bind.handler, localScope);
|
|
212
227
|
const handlerParam = { name: "e", type: EVENT_TYPE };
|
|
213
228
|
const lambda = {
|
|
@@ -217,8 +232,10 @@ export class TemplateCompiler {
|
|
|
217
232
|
line: bind.line,
|
|
218
233
|
col: bind.col,
|
|
219
234
|
};
|
|
220
|
-
statements.push(
|
|
235
|
+
statements.push(assign(member(self, fieldName, bind), lambda, bind));
|
|
221
236
|
}
|
|
237
|
+
for (const bind of node.twoWayBindings)
|
|
238
|
+
statements.push(...this.buildTwoWayBinding(self, bind, localScope));
|
|
222
239
|
const elementChildren = node.children.filter((c) => c.kind === "element");
|
|
223
240
|
const textChildren = node.children.filter((c) => c.kind === "text");
|
|
224
241
|
if (textChildren.length > 0 && elementChildren.length > 0) {
|
|
@@ -227,7 +244,7 @@ export class TemplateCompiler {
|
|
|
227
244
|
else if (textChildren.length > 0) {
|
|
228
245
|
const parts = textChildren.flatMap((t) => t.parts).map((p) => (p.kind === "Expr" ? { kind: "Expr", expression: this.resolve(p.expression, localScope) } : p));
|
|
229
246
|
const interpolated = { kind: "InterpolatedStringLiteral", parts, line: at.line, col: at.col };
|
|
230
|
-
statements.push(assign(member(self, "
|
|
247
|
+
statements.push(assign(member(self, "TextContent", at), interpolated, at));
|
|
231
248
|
}
|
|
232
249
|
else {
|
|
233
250
|
for (const child of elementChildren)
|
|
@@ -271,6 +288,49 @@ export class TemplateCompiler {
|
|
|
271
288
|
return [...statements, this.appendChild(parentVar, elVar, node)];
|
|
272
289
|
}
|
|
273
290
|
appendChild(parentVar, childVar, at) {
|
|
274
|
-
return exprStatement(call(member(ident(parentVar, at), "
|
|
291
|
+
return exprStatement(call(member(ident(parentVar, at), "AppendChild", at), [ident(childVar, at)], at), at);
|
|
292
|
+
}
|
|
293
|
+
// A known name (post-alias — "class" already reads "className" by here)
|
|
294
|
+
// becomes a direct assignment to VElement's own named field; anything
|
|
295
|
+
// else — href, src, alt, placeholder, ... — goes through VElement.SetAttr
|
|
296
|
+
// instead, under its original, real HTML attribute name (SetAttr's own
|
|
297
|
+
// name argument is exactly what gets passed to a real setAttribute call
|
|
298
|
+
// at patch time — see vdom.ks).
|
|
299
|
+
buildAttrAssignment(self, name, value, at) {
|
|
300
|
+
const fieldName = KNOWN_FIELD_NAMES[name];
|
|
301
|
+
if (fieldName) {
|
|
302
|
+
return assign(member(self, fieldName, at), value, at);
|
|
303
|
+
}
|
|
304
|
+
return exprStatement(call(member(self, "SetAttr", at), [stringLiteral(name, at), value], at), at);
|
|
305
|
+
}
|
|
306
|
+
// `[(value)]="Field"` desugars to exactly the two-piece pattern you'd
|
|
307
|
+
// otherwise write by hand — `[value]="Field"` (a plain property binding)
|
|
308
|
+
// plus `(input)="Field = e.target.value"` (an auto-generated handler
|
|
309
|
+
// assigning back). Restricted to `value` specifically: it's the one
|
|
310
|
+
// VElement field a user can change through direct interaction (typing,
|
|
311
|
+
// picking an option) with no event of Kopular's own in between — `id`/
|
|
312
|
+
// `className` have no equivalent "user just changed this" event, so
|
|
313
|
+
// there's nothing for a two-way binding to mean for them.
|
|
314
|
+
buildTwoWayBinding(self, bind, localScope) {
|
|
315
|
+
if (bind.name !== "value") {
|
|
316
|
+
this.diagnostics.error("KS5017", `Unsupported two-way binding '[(${bind.name})]' — only [(value)] is supported, the one VElement field a user can change through direct interaction`, bind.line, bind.col);
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const target = this.resolve(bind.target, localScope);
|
|
320
|
+
if (target.kind !== "Identifier" && target.kind !== "MemberExpr") {
|
|
321
|
+
this.diagnostics.error("KS5018", `A two-way binding target must be a simple field reference (a bare name or 'this.Field'), not a complex expression`, bind.line, bind.col);
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
const propAssign = this.buildAttrAssignment(self, bind.name, target, bind);
|
|
325
|
+
const assignBack = assign(target, member(member(ident("e", bind), "target", bind), "value", bind), bind);
|
|
326
|
+
const lambda = {
|
|
327
|
+
kind: "LambdaExpr",
|
|
328
|
+
params: [{ name: "e", type: EVENT_TYPE }],
|
|
329
|
+
body: block([assignBack], bind),
|
|
330
|
+
line: bind.line,
|
|
331
|
+
col: bind.col,
|
|
332
|
+
};
|
|
333
|
+
const eventAssign = assign(member(self, "OnInput", bind), lambda, bind);
|
|
334
|
+
return [propAssign, eventAssign];
|
|
275
335
|
}
|
|
276
336
|
}
|
package/dist/template_parser.js
CHANGED
|
@@ -162,6 +162,7 @@ export class TemplateParser {
|
|
|
162
162
|
const staticAttrs = [];
|
|
163
163
|
const propBindings = [];
|
|
164
164
|
const eventBindings = [];
|
|
165
|
+
const twoWayBindings = [];
|
|
165
166
|
let ifCondition = null;
|
|
166
167
|
let forBinding = null;
|
|
167
168
|
while (this.check(TemplateTokenKind.AttrName)) {
|
|
@@ -171,7 +172,15 @@ export class TemplateParser {
|
|
|
171
172
|
if (!valueTok)
|
|
172
173
|
continue;
|
|
173
174
|
const name = nameTok.lexeme;
|
|
174
|
-
|
|
175
|
+
// Checked before the plain `[prop]` case below — `[(value)]` also
|
|
176
|
+
// starts with `[` and ends with `]`, so it would otherwise be
|
|
177
|
+
// mistaken for a property binding named literally "(value)".
|
|
178
|
+
if (name.startsWith("[(") && name.endsWith(")]")) {
|
|
179
|
+
const propName = name.slice(2, -2);
|
|
180
|
+
const target = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
181
|
+
twoWayBindings.push({ name: propName, target, line: nameTok.line, col: nameTok.col });
|
|
182
|
+
}
|
|
183
|
+
else if (name.startsWith("(") && name.endsWith(")")) {
|
|
175
184
|
const eventName = name.slice(1, -1);
|
|
176
185
|
const handler = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
177
186
|
eventBindings.push({ name: eventName, handler, line: nameTok.line, col: nameTok.col });
|
|
@@ -226,7 +235,7 @@ export class TemplateParser {
|
|
|
226
235
|
this.diagnostics.error("KS5012", `Expected closing tag '</${tag}>'`, tagTok.line, tagTok.col);
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
|
-
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
238
|
+
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, twoWayBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
230
239
|
}
|
|
231
240
|
consumeEquals(attrNameTok) {
|
|
232
241
|
this.consume(TemplateTokenKind.Equals, attrNameTok, `Expected '=' after attribute '${attrNameTok.lexeme}'`);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopscript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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",
|
|
7
7
|
"author": "Joe Koppin <koppinjo@gmail.com>",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://dev.azure.com/koppinator/Koppindependence/_git/
|
|
10
|
+
"url": "https://dev.azure.com/koppinator/Koppindependence/_git/KopScript"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://kopular.dev",
|
|
13
13
|
"keywords": [
|