kopular 1.4.0 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/GUIDE.md +42 -131
  2. package/LLM.md +6 -2
  3. package/package.json +2 -2
package/GUIDE.md CHANGED
@@ -49,65 +49,46 @@ building, for when you only want the errors.
49
49
  ## The language you need
50
50
 
51
51
  - `Type name = value;` for every local — no `let`/`var`, no inference on declarations.
52
- - No implicit `this` — every member reference is `this.Field`/`this.Method()`, always,
53
- including inside a lambda.
54
- - `if`/`else`/`while`/`for`/`foreach (Type x in xs)` — all statements, no ternary. Use
55
- `match` for a conditional value: `match x { "a" => "one", _ => "other" }`. **The subject
56
- must be `string` or an `enum` — never `bool`/`number`/anything else, and every pattern
57
- must be a string literal (or, for an enum subject, an `Enum.Member` name) — not a number,
58
- not `true`/`false`.** The `_` arm is required unless every case is covered (only possible
59
- for an enum subject, by naming every member). For a `bool`, use `if`/`else` instead —
60
- there's no equivalent shorthand.
52
+ - No implicit `this`: every member is `this.Field`/`this.Method()`, inside lambdas too.
53
+ - `if`/`else`/`while`/`for`/`foreach (Type x in xs)`; no ternary. For a conditional value use
54
+ `match x { "a" => "one", _ => "other" }` — **the subject must be a `string` or an `enum`,
55
+ every pattern a string literal (or `Enum.Member`), never a number or `true`/`false`.** `_`
56
+ is required unless an enum's members are all named. For a `bool`, use `if`/`else`.
61
57
  - Lambdas need explicit parameter types: `(number x) => x * 2`.
62
- - Operators: `+ - * / %`, `== != < > <= >=`, `&& || !` — `!` is a real unary operator on a
63
- `bool` (`if (!done)`, `bool flipped = !done;`). Precedence, low → high: `=` → `||` →
64
- `&&` → `==` `!=` → `<` `>` `<=` `>=` → `+` `-` → `*` `/` `%` → unary `-` `!` →
65
- `.member`/`(call)`/`[index]`. **No `++`/`--`/`+=`** — write `i = i + 1;`.
66
- - **number → string**: `+` with a string on either side (`"count: " + n`), or interpolation
67
- (`$"{n} items left"`). There is no `.ToString()` — calling it is `KS4085`.
68
- - `try { } catch (string e) { } finally { }` and `throw "message";` all exist — one `catch`
69
- per `try`, and its parameter type is your choice (it is *not* checked against what was
70
- actually thrown).
71
- - **Comments are `//` only** — there is no `/* ... */` block comment, and a `/*` is a parse
72
- error (`KS2020`), not an ignored region.
73
- - **Reserved words**: naming a local or parameter one of these is a parse error, not
74
- shadowing — `raw`, `state`, `task`, `from`, `as`, `get`, `set`, `in`, `base` and `match`
75
- are the ones easy to pick by accident. Full list: `using extern raw template styles from
76
- as const class interface enum constructor public private protected static virtual override
77
- get set return if else while for foreach in break continue match this base new void true
78
- false null task state async await try catch finally throw`.
79
- - Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
80
- narrowing is scoped to that `if` block, not reachability-based (an early
81
- `if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
82
- `if (x != null) { ... }` instead).
58
+ - Operators: `+ - * / %`, `== != < > <= >=`, `&& || !` (`!` works on a `bool`: `if (!done)`).
59
+ Precedence low → high: `=`, `||`, `&&`, `== !=`, `< > <= >=`, `+ -`, `* / %`, unary `- !`,
60
+ then `.member`/`(call)`/`[index]`. **No `++`/`--`/`+=`** — write `i = i + 1;`.
61
+ - **number → string**: `"count: " + n` or `$"{n} items left"`. There is no `.ToString()`
62
+ (`KS4085`).
63
+ - `try { } catch (string e) { } finally { }` and `throw "message";` — one `catch` per `try`;
64
+ its parameter type isn't checked against what was thrown.
65
+ - **Comments are `//` only** — `/*` is a parse error (`KS2020`).
66
+ - **Reserved words** — easy to pick by accident as a local or parameter name, and a parse
67
+ error if you do: `raw`, `state`, `task`, `from`, `as`, `get`, `set`, `in`, `base`, `match`,
68
+ `template`, `styles`, `const`, `async`, `await`, plus the usual keywords.
69
+ - Nullable: `string? name` must be checked with `if (x != null) { ... }` before use, and
70
+ narrowing only lasts inside that block — an early `if (x == null) { return; }` does NOT
71
+ narrow `x` afterward.
83
72
  - Strings: `s.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
84
73
  `.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
85
- Both `"double"` and `'single'` quotes work — use whichever the other isn't, e.g. inside
86
- a template attribute (`*if="Name != ''"`).
87
- - Arrays are created with a literal and nothing else: `number[] xs = [];`, `[1, 2, 3]`,
88
- `[new Todo("a")]`. **There is no `new number[3]` form** — that's a parse error, not an
89
- empty array.
90
- - Arrays: `xs.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
91
- `.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
74
+ `"double"` and `'single'` quotes both work — handy inside a template attribute
75
+ (`*if="Name != ''"`).
76
+ - Arrays come only from literals: `number[] xs = [];`, `[1, 2, 3]`, `[new Todo("a")]` —
77
+ **`new number[3]` is a parse error.** Methods: `xs.Length`,
78
+ `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
79
+ `.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating** — they return a new array),
92
80
  `.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
93
- - `x == null` / `x != null` also match `undefined` — a missed `Array.Find`, an absent
94
- optional field, etc. all read as `null`.
95
- - **No `.Match()`/`.Test()`/`.Exec()` on `string` — there is no direct regex-execute method
96
- at all.** A regex literal (`r"^[a-z]+$"`) only means something as a `match` *pattern*.
97
- The idiomatic way to classify or extract by character class is `.Split("")` (splits into
98
- a `string[]` of single characters) plus `match` per character:
81
+ - `x == null` also matches `undefined` (a missed `Find`, an absent field).
82
+ - **No regex methods on `string`** (`.Match()`/`.Test()`/`.Exec()` don't exist). A regex
83
+ literal works only as a `match` pattern, so to classify or tokenize by character class,
84
+ `.Split("")` into characters and `match` each one, appending to an accumulator and flushing
85
+ it at each non-matching character and at the end:
99
86
  ```ks
100
87
  bool isLetter = match c { r"^[a-z]$" => true, _ => false };
101
88
  ```
102
- Build up runs (words, tokens, ...) by iterating those characters and appending to an
103
- accumulator string, flushing it whenever a non-matching character (or the end) is
104
- reached — this covers tokenizing/extracting by character class without ever needing a
105
- hand-written JS shim. Reach for a real shim (below) only for something a `match` pattern
106
- genuinely can't express, like capturing a submatch.
107
- - No object-literal syntax anywhere (`{ key: value }` doesn't exist as a value). A JS API
108
- that needs one (rare — `fetch`'s options, `addEventListener`'s options object) needs a
109
- small hand-written `.js` shim; Kopular's own `Http`/`FormField` cover the common cases so
110
- you'll rarely hit this.
89
+ Only something a pattern can't express (capturing a submatch) needs a `.js` shim.
90
+ - No object literals (`{ key: value }` isn't a value). The rare JS API that needs one
91
+ needs a small `.js` shim; `Http`/`FormField` cover the common cases.
111
92
 
112
93
  ## Components
113
94
 
@@ -123,7 +104,8 @@ handlers don't count. State on *another* object does still need one:
123
104
  **Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
124
105
  `id`/`className`/`value`/`disabled`/`checked`; anything else is a plain attribute),
125
106
  `(click)`/`(input)`/`(blur)`/`(change)`/`(submit)` events (`(submit)` calls `preventDefault()`
126
- for you, and Enter in a form's input fires it), text mixed with elements
107
+ for you, and Enter in a form's input fires it; validating in code? put `novalidate` on the
108
+ `<form>`, or the browser's own `type="email"`/`required` checks block the submit first), text mixed with elements
127
109
  (`<label>Email <input></label>`), `*if="expr"`, `*for="Type v of expr"`
128
110
  (element type required, no inference), `*mount="expr"` (embeds a live child component,
129
111
  composes with `*for`).
@@ -150,44 +132,8 @@ otherwise.** Wrap multiple top-level pieces in one real container element:
150
132
  </div>
151
133
  ```
152
134
 
153
- **Hand-written `Render()`** (needed when logic is too dynamic for a template, or a template
154
- would obscure more than it clarifies):
155
-
156
- ```ks
157
- public override VElement Render() {
158
- VElement button = VElement.Create("button");
159
- button.TextContent = "Count: " + this.Count.Value;
160
- button.OnClick = (Event e) => { this.Increment(); };
161
- return button;
162
- }
163
- ```
164
- `VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
165
- `.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the
166
- escape hatch for anything without a named field). Both styles compile to the same thing and
167
- mix freely across a project.
168
-
169
- **Conditional value**: there's no ternary, so to pick between two elements use `If()` (from
170
- `kopular/directives`, already covered by `using "kopular"`):
171
- ```ks
172
- root.AppendChild(If(this.On.Value, () => this.Yes(), () => this.No()));
173
- ```
174
-
175
- **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
176
- uncaught crash if `Render()` throws.
177
-
178
- ## Nested components & content projection
179
-
180
- ```ks
181
- this.Items.ForEach((Item item) => {
182
- VElement slot = VElement.Mount(item); // item : Component, e.g. its own `public string Id;`
183
- slot.Id = item.Id; // stable key for reordering
184
- list.AppendChild(slot);
185
- });
186
- ```
187
- A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
188
-
189
- Content projection (React's `children`): pass a `() => VElement` into a constructor, call
190
- it from `Render()` — no separate mechanism needed.
135
+ **Beyond templates** — a hand-written `Render()` (the `VElement` API), `If()`, and the
136
+ `RenderError` error boundary are in `LLM.md`; templates cover typical app work.
191
137
 
192
138
  ## Services — no DI container
193
139
 
@@ -232,7 +178,7 @@ nav.Mount(document.body);
232
178
  `redirectPath` (via `pushState`, so the URL changes too). `guard` itself decides which
233
179
  paths it cares about, and `redirectPath` is never guard-checked, so pick one the guard
234
180
  always allows. Defaults to always-allow.
235
- - `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
181
+ - `AddLazyRoute(path, loader)` for real code-splitting — see `LLM.md`.
236
182
  - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
237
183
  matching file) — a plain HTTP limitation, not a Kopular one.
238
184
 
@@ -311,46 +257,11 @@ match r.Status.Value {
311
257
  };
312
258
  ```
313
259
 
314
- ## Scoped styles
315
-
316
- ```ks
317
- class Widget : Component {
318
- constructor() : base() { } // required: 'styles from' has to have one to initialize from
319
- template from "./widget.html";
320
- styles from "./widget.css";
321
- }
322
- ```
323
- Every selector in `widget.css` is rewritten to only match this class's own elements —
324
- never a sibling's or child's. Needs a constructor to exist on the class.
325
-
326
- ## Lazy routes (real code-splitting)
260
+ ## More in `node_modules/kopular/LLM.md`
327
261
 
328
- ```js
329
- // admin_page_loader.js — hand-written, not compiled from .ks
330
- export async function LoadAdminPage() {
331
- const { AdminPage } = await import("./admin_page.js");
332
- return new AdminPage();
333
- }
334
- ```
335
- ```ks
336
- extern task<Component> LoadAdminPage() from "./admin_page_loader";
337
- nav.AddLazyRoute("/admin", LoadAdminPage);
338
- ```
339
- The lazy page (`admin_page.ks`) must be built as its own entry too — it's deliberately not
340
- `using`'d from your app's entry (that's what keeps it out of the eager bundle), so add a
341
- second build line: `ks build src/app.ks && ks build src/admin_page.ks`. Testing it via
342
- `kopular/testing`'s `runKopularApp` needs no extra setup — it compiles any file in your
343
- `srcDir` the entry doesn't reach.
344
-
345
- ## Testing
346
-
347
- ```js
348
- import { runKopularApp } from "kopular/testing";
349
- const { window, cleanup } = await runKopularApp(srcDir, "app.ks", { includeKopularPackage: true });
350
- // assert against window.document, then:
351
- cleanup();
352
- ```
353
- Real jsdom, real compile, real DOM assertions — not a mock.
262
+ Nested components (`*mount`, `VElement.Mount`) and content projection, scoped styles
263
+ (`styles from`), lazy routes (`AddLazyRoute`), and testing (`kopular/testing`'s
264
+ `runKopularApp`).
354
265
 
355
266
  ## Common mistakes
356
267
 
package/LLM.md CHANGED
@@ -125,8 +125,12 @@ extern class VElement {
125
125
  (Event) => void OnInput { get; set; }
126
126
  (Event) => void OnBlur { get; set; }
127
127
  (Event) => void OnChange { get; set; }
128
+ // Calls preventDefault() before the handler — a form submit never reloads the page.
129
+ (Event) => void OnSubmit { get; set; }
128
130
  void AppendChild(VElement child);
129
131
  void SetAttr(string name, string value);
132
+ // A bare text node, for text next to elements in one parent.
133
+ static VElement Text(string content);
130
134
  // Embeds a live child Component as this slot's content — see "Nested
131
135
  // component composition" below. Declared taking a concrete Component
132
136
  // rather than Kopular's own Mountable interface: extern declarations
@@ -299,8 +303,8 @@ w.Bump(); // re-render + diff + patch
299
303
  `innerHTML`, unescaped: only ever assign static/developer-authored content — a `raw
300
304
  string ... from "<path>.html";` compile-time constant, in every real use in this
301
305
  ecosystem today — never anything reachable from user input, a fetched `Http` response
302
- body, or a `FormField<T>`'s `.Value`, or it's a real XSS hole**), the four fixed named
303
- event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange` (each a real no-op by default, never
306
+ body, or a `FormField<T>`'s `.Value`, or it's a real XSS hole**), the five fixed named
307
+ event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange`/`OnSubmit` (each a real no-op by default, never
304
308
  null — no nullable function type to fall back on), `AppendChild(child)`, and
305
309
  `SetAttr(name, value)` (the escape hatch for any other real HTML attribute — `href`,
306
310
  `src`, `alt`, `placeholder`, ...; never `Value`, see above). `Id` doubles as a stable key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "1.4.0",
3
+ "version": "1.4.3",
4
4
  "description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -66,7 +66,7 @@
66
66
  "@types/jsdom": "^30.0.0",
67
67
  "@types/node": "^20.14.0",
68
68
  "jsdom": "^25.0.1",
69
- "kopscript": "^1.3.0",
69
+ "kopscript": "^1.4.0",
70
70
  "typescript": "^5.5.0",
71
71
  "vitest": "^4.1.11"
72
72
  },