kopular 0.13.0 → 0.14.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 +134 -72
- package/README.md +95 -68
- package/package.json +2 -1
- package/src/component.js +14 -6
- package/src/component.js.map +1 -1
- package/src/component.ks +37 -15
- package/src/directives.js +1 -0
- package/src/directives.js.map +1 -1
- package/src/directives.ks +5 -4
- package/src/dom.js.map +1 -1
- package/src/dom.ks +18 -1
- package/src/forms.js.map +1 -1
- package/src/forms.ks +1 -1
- package/src/router.js +9 -4
- package/src/router.js.map +1 -1
- package/src/router.ks +23 -10
- package/src/vdom.js +136 -0
- package/src/vdom.js.map +1 -0
- package/src/vdom.ks +221 -0
- package/src/velement.js +39 -0
- package/src/velement.js.map +1 -0
- package/src/velement.ks +95 -0
package/src/forms.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forms.js","sources":["forms.ks"],"sourcesContent":["// FormField<T>: a single form input's value, validation state, and touched\n// flag, built on the same state<T> reactivity Component already uses — no\n// new reactive primitive, and deliberately no DOM binding of its own\n// (wiring Value to a real <input> is one addEventListener call in your own\n// Render(), the same as Counter's own click handler — see Kopular's README).\n//\n// A validator is a plain (T) => string? function: null means valid, the\n// same \"null means nothing to report\" convention KopScript's nullable\n// types already use elsewhere. There's no array of validators — KopScript\n// has no syntax for an array of function values — so combining more than\n// one check is either an if-chain in one lambda, hand-written:\n//\n// FormField<string> name = new FormField<string>(\"\", (string v) => {\n// string? required = Validators.Required(v);\n// if (required != null) { return required; }\n// return Validators.MaxLength(v, 40);\n// });\n//\n// or the CombineValidators2/CombineValidators3 free functions below, for\n// the common case of just chaining a couple of already-built validators\n// with no custom logic of their own:\n//\n// FormField<string> name = new FormField<string>(\"\",\n// CombineValidators2((string v) => Validators.Required(v), (string v) => Validators.MaxLength(v, 40)));\n//\n// Free functions, not static Validators methods, and fixed-arity (2 and 3),\n// not a general array-taking Validators.All(...): KopScript has no\n// array-of-function-values type to accept a variable-length list with, and\n// a generic function can only be a *free* function in v1 — a class's own\n// static method can't introduce a new type parameter of its own beyond the\n// class's (Validators itself isn't generic) — see
|
|
1
|
+
{"version":3,"file":"forms.js","sources":["forms.ks"],"sourcesContent":["// FormField<T>: a single form input's value, validation state, and touched\n// flag, built on the same state<T> reactivity Component already uses — no\n// new reactive primitive, and deliberately no DOM binding of its own\n// (wiring Value to a real <input> is one addEventListener call in your own\n// Render(), the same as Counter's own click handler — see Kopular's README).\n//\n// A validator is a plain (T) => string? function: null means valid, the\n// same \"null means nothing to report\" convention KopScript's nullable\n// types already use elsewhere. There's no array of validators — KopScript\n// has no syntax for an array of function values — so combining more than\n// one check is either an if-chain in one lambda, hand-written:\n//\n// FormField<string> name = new FormField<string>(\"\", (string v) => {\n// string? required = Validators.Required(v);\n// if (required != null) { return required; }\n// return Validators.MaxLength(v, 40);\n// });\n//\n// or the CombineValidators2/CombineValidators3 free functions below, for\n// the common case of just chaining a couple of already-built validators\n// with no custom logic of their own:\n//\n// FormField<string> name = new FormField<string>(\"\",\n// CombineValidators2((string v) => Validators.Required(v), (string v) => Validators.MaxLength(v, 40)));\n//\n// Free functions, not static Validators methods, and fixed-arity (2 and 3),\n// not a general array-taking Validators.All(...): KopScript has no\n// array-of-function-values type to accept a variable-length list with, and\n// a generic function can only be a *free* function in v1 — a class's own\n// static method can't introduce a new type parameter of its own beyond the\n// class's (Validators itself isn't generic) — see KopScript's README/LLM.md\n// \"Generics\" section for exactly that cut. Add CombineValidators4 etc. the\n// same way if a real form ever needs to chain more than three.\n(T) => string? CombineValidators2<T>((T) => string? v1, (T) => string? v2) {\n return (T value) => {\n string? r1 = v1(value);\n if (r1 != null) { return r1; }\n return v2(value);\n };\n}\n\n(T) => string? CombineValidators3<T>((T) => string? v1, (T) => string? v2, (T) => string? v3) {\n return (T value) => {\n string? r1 = v1(value);\n if (r1 != null) { return r1; }\n string? r2 = v2(value);\n if (r2 != null) { return r2; }\n return v3(value);\n };\n}\nclass FormField<T> {\n public state<T> Value;\n public state<string?> Error;\n public state<bool> Touched;\n\n constructor(T initial, (T) => string? validate) {\n this.Value = state(initial);\n this.Error = state(validate(initial));\n this.Touched = state(false);\n this.Value.Subscribe((T v) => { this.Error.Value = validate(v); });\n }\n\n // Call on blur — separate from Error so a fresh, untouched field with an\n // invalid initial value (e.g. Required on an empty string) doesn't show\n // an error message before the user has had a chance to type anything.\n public void Touch() {\n this.Touched.Value = true;\n }\n\n public bool Valid() {\n return this.Error.Value == null;\n }\n}\n\n// A small set of common checks, each returning an error message or null —\n// not a validation framework, just the handful of checks almost every form\n// needs, so most fields don't have to hand-write string-length arithmetic.\nclass Validators {\n public static string? Required(string value) {\n if (value.Trim().Length == 0) { return \"Required\"; }\n return null;\n }\n\n public static string? MinLength(string value, number min) {\n if (value.Length < min) { return $\"Must be at least {min} characters\"; }\n return null;\n }\n\n public static string? MaxLength(string value, number max) {\n if (value.Length > max) { return $\"Must be at most {max} characters\"; }\n return null;\n }\n\n public static string? Email(string value) {\n return match value {\n r\"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\" => null,\n _ => \"Must be a valid email\"\n };\n }\n\n public static string? Min(number value, number min) {\n if (value < min) { return $\"Must be at least {min}\"; }\n return null;\n }\n\n public static string? Max(number value, number max) {\n if (value > max) { return $\"Must be at most {max}\"; }\n return null;\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;AAiCA;EACE;EACE;EACA;IAAkB;;EAClB;;;AAIJ;EACE;EACE;EACA;IAAkB;;EAClB;EACA;IAAkB;;EAClB;;;AAGJ;EAKE;IACa;IACA;IACE;IACO;EAA6B;;;;EAM5C;IACc;;;EAGd;IACL;;;AAOJ;EACgB;IACZ;MAAgC;;IAChC;;;EAGY;IACZ;MAA0B;;IAC1B;;;EAGY;IACZ;MAA0B;;IAC1B;;;EAGY;IACZ;;EACE;;;EACA;;;;;;EAIU;IACZ;MAAmB;;IACnB;;;EAGY;IACZ;MAAmB;;IACnB"}
|
package/src/forms.ks
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// array-of-function-values type to accept a variable-length list with, and
|
|
29
29
|
// a generic function can only be a *free* function in v1 — a class's own
|
|
30
30
|
// static method can't introduce a new type parameter of its own beyond the
|
|
31
|
-
// class's (Validators itself isn't generic) — see
|
|
31
|
+
// class's (Validators itself isn't generic) — see KopScript's README/LLM.md
|
|
32
32
|
// "Generics" section for exactly that cut. Add CombineValidators4 etc. the
|
|
33
33
|
// same way if a real form ever needs to chain more than three.
|
|
34
34
|
(T) => string? CombineValidators2<T>((T) => string? v1, (T) => string? v2) {
|
package/src/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
|
|
2
2
|
import { Component } from "./component.js";
|
|
3
|
+
import { VElement } from "./velement.js";
|
|
3
4
|
|
|
4
5
|
export class Router extends Component {
|
|
5
6
|
constructor(notFoundPage) {
|
|
@@ -132,12 +133,16 @@ export class Router extends Component {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
Render() {
|
|
135
|
-
let outlet =
|
|
136
|
-
outlet.
|
|
136
|
+
let outlet = VElement.Create("div");
|
|
137
|
+
outlet.ClassName = "router-outlet";
|
|
138
|
+
return outlet;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
AfterRender(root) {
|
|
137
142
|
let path = (location.pathname + location.search);
|
|
138
143
|
let page = this.Match(path);
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
root.textContent = "";
|
|
145
|
+
page.Mount(root);
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
|
package/src/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sources":["router.ks"],"sourcesContent":["using \"./dom\";\nusing \"./component\";\n\n// History-API routing (real paths — `/about`, not `#/about`) via\n// `history.pushState`/the browser's native `popstate` event. This needs a\n// server that falls back to the app shell for any path it doesn't\n// recognize as a real file (see KopularDemo's scripts/serve.mjs) — a plain\n// static file server has nothing to serve at `/about` on a direct\n// visit/refresh otherwise, unlike hash-based routing, which never leaves\n// the one HTML file the server actually has. Route registration is\n// imperative (`AddRoute` calls), not a config object — one thing to learn,\n// not a routing DSL.\n//\n// Routes are registered as already-constructed Component instances, not\n// factories — `Component[]`, not `(() => Component)[]` (KopScript's type\n// grammar has no way to write \"array of function type\" in v1, since a\n// parenthesized function type isn't a general grouping construct). This\n// turns out to be a genuine feature, not just a workaround: each page\n// Component is built once and kept alive for the Router's own lifetime, so\n// a page's own state<T> fields survive navigating away and back — no state\n// gets reset just because a route wasn't showing for a while.\n//\n// No nullable types in v1 means \"no route matched\" can't be represented as\n// null — a `NotFoundPage` is required up front instead, the same way a\n// `match` expression requires its own `_` wildcard arm.\nclass Router : Component {\n private string[] Paths;\n private Component[] Pages;\n private Component NotFoundPage;\n\n // The FIRST captured dynamic segment's value (e.g. \"42\" for a\n // \"/dogs/:id\" route matching \"/dogs/42\"), or \"\" if the matched route has\n // none — kept exactly as-is (a plain string, not the general Params()\n // lookup below) so every existing single-dynamic-segment consumer keeps\n // working unchanged. For a route with more than one captured segment,\n // use Params(name) instead; this still holds the first one either way.\n //\n // A plain field, deliberately **not** state<T> — Render() below always\n // rebuilds a fresh outlet and re-Mount()s the matched page into it on\n // every Navigate()/popstate, which already re-runs that page's own\n // Render() (reading the fresh Param) with no extra step. Subscribing to\n // it, the way a page reacts to its *own* state<T>, would fire during\n // Match() itself — before Render() has finished swapping the matched page\n // in — which crashes the very first time a route no page has been\n // Mount()ed into yet matches, since a not-yet-mounted Component's\n // inherited Update() has no ParentElement to replaceChild into.\n public string Param;\n\n // Every named dynamic segment captured by whatever route just matched\n // (\":id\"/\":toyId\" for \"/dogs/:id/toys/:toyId\"), plus the pseudo-name \"*\"\n // for a trailing wildcard segment's captured remainder — see Params()\n // below. Parallel arrays (ParamNames[i] <-> ParamValues[i]), the same\n // convention Paths/Pages above already use — not a Dictionary type,\n // since KopScript has none.\n private string[] ParamNames;\n private string[] ParamValues;\n\n // The current URL's query string, parsed once per navigation/popstate —\n // see Query() below. Never part of route *matching* itself in v1: every\n // route sees whatever query string is actually in the URL, matched or\n // not. Values are NOT percent-decoded (a deliberate v1 cut — no\n // decodeURIComponent binding exists yet); a value containing `%20` or\n // `+` for a space arrives exactly as written in the URL.\n private string[] QueryKeys;\n private string[] QueryValues;\n\n // Checked before every navigation, including a direct load/refresh — not\n // just Navigate()/popstate. Deliberately one guard for the whole Router,\n // not per-route: the guard function itself decides which paths it cares\n // about (typically `path.StartsWith(\"/admin\")`-style checks), the same\n // \"no config object, just a function\" style Http/DI already use, rather\n // than a parallel array of per-route guards (which also can't be written\n // as a type anyway — see AddRoute's own comment on array-of-function\n // types). Defaults to always-allow, not null — a nullable function TYPE\n // has the same \"can't parenthesize for a postfix `?`\" problem as an\n // array of one, so \"no guard configured\" is a real function that always\n // returns true, not a null check.\n private (string) => bool Guard;\n private string RedirectPath;\n\n constructor(Component notFoundPage) : base() {\n this.Paths = [];\n this.Pages = [];\n this.NotFoundPage = notFoundPage;\n this.Param = \"\";\n this.ParamNames = [];\n this.ParamValues = [];\n this.QueryKeys = [];\n this.QueryValues = [];\n this.Guard = (string path) => true;\n this.RedirectPath = \"\";\n // 'popstate' only fires on browser back/forward (or history.go/back/\n // forward) — never on pushState itself, unlike hashchange firing\n // whenever location.hash is set. Navigate() below calls Update()\n // directly for that reason; this listener only covers the back/forward\n // case, where the URL changes without any of our own code running.\n window.addEventListener(\"popstate\", (Event e) => this.Update());\n }\n\n // A path segment written \":name\" (e.g. \"dogs/:id\") matches any single\n // non-empty segment; every other segment must match literally, except a\n // trailing \"*\" segment (e.g. \"files/*\"), which matches one or more\n // remaining segments as a single captured group (see Params(\"*\")).\n // Comparing segment-by-segment (rather than the whole string at once) is\n // what makes a static route's own matching still exactly as strict as a\n // plain `==` was before — same segment count, same literal text\n // throughout (a trailing \"*\" route aside, which only ever needs *at\n // least* as many path segments as its static prefix).\n public void AddRoute(string path, Component page) {\n this.Paths = this.Paths.Push(path);\n this.Pages = this.Pages.Push(page);\n }\n\n // `guard` is called with the path being navigated to; returning false\n // redirects to `redirectPath` instead (updating the URL via pushState, so\n // a refresh on the blocked path lands on the redirect too, not back on\n // the page the guard just rejected). `redirectPath` itself is never\n // guard-checked — pick one the guard always allows, or it'll loop.\n public void SetGuard(string redirectPath, (string) => bool guard) {\n this.RedirectPath = redirectPath;\n this.Guard = guard;\n }\n\n public void Navigate(string path) {\n history.pushState(\"\", \"\", path);\n this.Update();\n }\n\n // The captured value for a named dynamic segment (e.g. \"toyId\" for\n // \":toyId\" in \"/dogs/:id/toys/:toyId\") or \"*\" for a trailing wildcard\n // segment's matched remainder — \"\" if `name` wasn't captured by whatever\n // route just matched, the same \"empty string, not null\" convention\n // `Param` itself already uses.\n public string Params(string name) {\n for (number i = 0; i < this.ParamNames.Length; i = i + 1) {\n if (this.ParamNames[i] == name) {\n return this.ParamValues[i];\n }\n }\n return \"\";\n }\n\n // The current URL's query-string value for `key` (e.g. \"name\" for\n // \"?sort=name\") — \"\" if `key` isn't present. See QueryKeys/QueryValues'\n // own comment for the percent-decoding cut.\n public string Query(string key) {\n for (number i = 0; i < this.QueryKeys.Length; i = i + 1) {\n if (this.QueryKeys[i] == key) {\n return this.QueryValues[i];\n }\n }\n return \"\";\n }\n\n // KopScript strings have no index-based substring/slice method (only\n // arrays do — see Array.Slice), so splitting on the literal separator and\n // taking the piece after it is the idiomatic way to strip a known\n // single-character prefix here, both for \"?\" (query string) and \":\"\n // (a pattern segment's own leading marker, used below in Match).\n private void ParseQuery(string pathWithQuery) {\n this.QueryKeys = [];\n this.QueryValues = [];\n string[] afterMark = pathWithQuery.Split(\"?\");\n if (afterMark.Length < 2) { return; }\n string rawQuery = afterMark[1];\n if (rawQuery.Length == 0) { return; }\n string[] pairs = rawQuery.Split(\"&\");\n foreach (string pair in pairs) {\n string[] kv = pair.Split(\"=\");\n if (kv.Length == 2) {\n this.QueryKeys = this.QueryKeys.Push(kv[0]);\n this.QueryValues = this.QueryValues.Push(kv[1]);\n }\n }\n }\n\n private Component Match(string fullPath) {\n string effectivePath = fullPath;\n if (!this.Guard(fullPath)) {\n history.pushState(\"\", \"\", this.RedirectPath);\n effectivePath = this.RedirectPath;\n }\n\n this.ParseQuery(effectivePath);\n string[] pathAndQuery = effectivePath.Split(\"?\");\n string[] pathSegments = pathAndQuery[0].Split(\"/\");\n\n Component found = this.NotFoundPage;\n string[] foundNames = [];\n string[] foundValues = [];\n\n for (number i = 0; i < this.Paths.Length; i = i + 1) {\n string[] patternSegments = this.Paths[i].Split(\"/\");\n bool hasWildcard = patternSegments.Length > 0 && patternSegments[patternSegments.Length - 1] == \"*\";\n\n number staticCount = patternSegments.Length;\n if (hasWildcard) {\n staticCount = patternSegments.Length - 1;\n }\n\n if (hasWildcard) {\n if (pathSegments.Length < staticCount) { continue; }\n } else {\n if (patternSegments.Length != pathSegments.Length) { continue; }\n }\n\n bool matched = true;\n string[] names = [];\n string[] values = [];\n for (number j = 0; j < staticCount; j = j + 1) {\n if (patternSegments[j].StartsWith(\":\")) {\n names = names.Push(patternSegments[j].Split(\":\")[1]);\n values = values.Push(pathSegments[j]);\n } else if (patternSegments[j] != pathSegments[j]) {\n matched = false;\n break;\n }\n }\n\n if (matched && hasWildcard) {\n names = names.Push(\"*\");\n values = values.Push(pathSegments.Slice(staticCount, pathSegments.Length).Join(\"/\"));\n }\n\n if (matched) {\n found = this.Pages[i];\n foundNames = names;\n foundValues = values;\n break;\n }\n }\n\n this.ParamNames = foundNames;\n this.ParamValues = foundValues;\n this.Param = \"\";\n if (foundValues.Length > 0) {\n this.Param = foundValues[0];\n }\n return found;\n }\n\n // Rebuilding a fresh outlet every render and re-Mount()ing the matched\n // page into it means there's nothing to explicitly unmount: Update()\n // (inherited from Component) discards the whole outlet in one\n // replaceChild when it swaps in the new one. The page instance itself\n // isn't rebuilt, just re-attached — see the class comment above.\n public override Element Render() {\n Element outlet = document.createElement(\"div\");\n outlet.className = \"router-outlet\";\n string path = location.pathname + location.search;\n Component page = this.Match(path);\n page.Mount(outlet);\n return outlet;\n }\n}\n"],"names":[],"mappings":";;;AAyBA;EAuDE;;IACa;IACA;IACO;IACP;IACK;IACC;IACF;IACE;IACN;IACO;IAMK;;;EAYlB;IACM;IACA;;;EAQN;IACa;IACP;;;EAGN;IACY;IACN;;;EAQN;IACL;MACE;QACE;;;IAGJ;;;EAMK;IACL;MACE;QACE;;;IAGJ;;;EAQM;IACS;IACE;IACjB;IACA;MAA4B;;IAC5B;IACA;MAA4B;;IAC5B;IACA;MACE;MACA;QACiB;QACE;;;;;EAKf;IACN;IACA;MACmB;MACH;;IAGD;IACf;IACA;IAEA;IACA;IACA;IAEA;MACE;MACA;MAEA;MACA;QACc;;MAGd;QACE;UAAyC;;;QAEzC;UAAqD;;;MAGvD;MACA;MACA;MACA;QACE;UACQ;UACC;;UAEC;UACR;;;MAIJ;QACQ;QACC;;MAGT;QACQ;QACK;QACC;QACZ;;;IAIY;IACC;IACN;IACX;MACa;;IAEb;;;EAQc;IACd;IACiB;IACjB;IACA;IACU;IACV"}
|
|
1
|
+
{"version":3,"file":"router.js","sources":["router.ks"],"sourcesContent":["using \"./dom\";\nusing \"./component\";\nusing \"./velement\";\n\n// History-API routing (real paths — `/about`, not `#/about`) via\n// `history.pushState`/the browser's native `popstate` event. This needs a\n// server that falls back to the app shell for any path it doesn't\n// recognize as a real file (see KopularDemo's scripts/serve.mjs) — a plain\n// static file server has nothing to serve at `/about` on a direct\n// visit/refresh otherwise, unlike hash-based routing, which never leaves\n// the one HTML file the server actually has. Route registration is\n// imperative (`AddRoute` calls), not a config object — one thing to learn,\n// not a routing DSL.\n//\n// Routes are registered as already-constructed Component instances, not\n// factories — `Component[]`, not `(() => Component)[]` (KopScript's type\n// grammar has no way to write \"array of function type\" in v1, since a\n// parenthesized function type isn't a general grouping construct). This\n// turns out to be a genuine feature, not just a workaround: each page\n// Component is built once and kept alive for the Router's own lifetime, so\n// a page's own state<T> fields survive navigating away and back — no state\n// gets reset just because a route wasn't showing for a while.\n//\n// No nullable types in v1 means \"no route matched\" can't be represented as\n// null — a `NotFoundPage` is required up front instead, the same way a\n// `match` expression requires its own `_` wildcard arm.\nclass Router : Component {\n private string[] Paths;\n private Component[] Pages;\n private Component NotFoundPage;\n\n // The FIRST captured dynamic segment's value (e.g. \"42\" for a\n // \"/dogs/:id\" route matching \"/dogs/42\"), or \"\" if the matched route has\n // none — kept exactly as-is (a plain string, not the general Params()\n // lookup below) so every existing single-dynamic-segment consumer keeps\n // working unchanged. For a route with more than one captured segment,\n // use Params(name) instead; this still holds the first one either way.\n //\n // A plain field, deliberately **not** state<T> — Render() below always\n // rebuilds a fresh outlet and re-Mount()s the matched page into it on\n // every Navigate()/popstate, which already re-runs that page's own\n // Render() (reading the fresh Param) with no extra step. Subscribing to\n // it, the way a page reacts to its *own* state<T>, would fire during\n // Match() itself — before Render() has finished swapping the matched page\n // in — which crashes the very first time a route no page has been\n // Mount()ed into yet matches, since a not-yet-mounted Component's\n // inherited Update() has no ParentElement to replaceChild into.\n public string Param;\n\n // Every named dynamic segment captured by whatever route just matched\n // (\":id\"/\":toyId\" for \"/dogs/:id/toys/:toyId\"), plus the pseudo-name \"*\"\n // for a trailing wildcard segment's captured remainder — see Params()\n // below. Parallel arrays (ParamNames[i] <-> ParamValues[i]), the same\n // convention Paths/Pages above already use — not a Dictionary type,\n // since KopScript has none.\n private string[] ParamNames;\n private string[] ParamValues;\n\n // The current URL's query string, parsed once per navigation/popstate —\n // see Query() below. Never part of route *matching* itself in v1: every\n // route sees whatever query string is actually in the URL, matched or\n // not. Values are NOT percent-decoded (a deliberate v1 cut — no\n // decodeURIComponent binding exists yet); a value containing `%20` or\n // `+` for a space arrives exactly as written in the URL.\n private string[] QueryKeys;\n private string[] QueryValues;\n\n // Checked before every navigation, including a direct load/refresh — not\n // just Navigate()/popstate. Deliberately one guard for the whole Router,\n // not per-route: the guard function itself decides which paths it cares\n // about (typically `path.StartsWith(\"/admin\")`-style checks), the same\n // \"no config object, just a function\" style Http/DI already use, rather\n // than a parallel array of per-route guards (which also can't be written\n // as a type anyway — see AddRoute's own comment on array-of-function\n // types). Defaults to always-allow, not null — a nullable function TYPE\n // has the same \"can't parenthesize for a postfix `?`\" problem as an\n // array of one, so \"no guard configured\" is a real function that always\n // returns true, not a null check.\n private (string) => bool Guard;\n private string RedirectPath;\n\n constructor(Component notFoundPage) : base() {\n this.Paths = [];\n this.Pages = [];\n this.NotFoundPage = notFoundPage;\n this.Param = \"\";\n this.ParamNames = [];\n this.ParamValues = [];\n this.QueryKeys = [];\n this.QueryValues = [];\n this.Guard = (string path) => true;\n this.RedirectPath = \"\";\n // 'popstate' only fires on browser back/forward (or history.go/back/\n // forward) — never on pushState itself, unlike hashchange firing\n // whenever location.hash is set. Navigate() below calls Update()\n // directly for that reason; this listener only covers the back/forward\n // case, where the URL changes without any of our own code running.\n window.addEventListener(\"popstate\", (Event e) => this.Update());\n }\n\n // A path segment written \":name\" (e.g. \"dogs/:id\") matches any single\n // non-empty segment; every other segment must match literally, except a\n // trailing \"*\" segment (e.g. \"files/*\"), which matches one or more\n // remaining segments as a single captured group (see Params(\"*\")).\n // Comparing segment-by-segment (rather than the whole string at once) is\n // what makes a static route's own matching still exactly as strict as a\n // plain `==` was before — same segment count, same literal text\n // throughout (a trailing \"*\" route aside, which only ever needs *at\n // least* as many path segments as its static prefix).\n public void AddRoute(string path, Component page) {\n this.Paths = this.Paths.Push(path);\n this.Pages = this.Pages.Push(page);\n }\n\n // `guard` is called with the path being navigated to; returning false\n // redirects to `redirectPath` instead (updating the URL via pushState, so\n // a refresh on the blocked path lands on the redirect too, not back on\n // the page the guard just rejected). `redirectPath` itself is never\n // guard-checked — pick one the guard always allows, or it'll loop.\n public void SetGuard(string redirectPath, (string) => bool guard) {\n this.RedirectPath = redirectPath;\n this.Guard = guard;\n }\n\n public void Navigate(string path) {\n history.pushState(\"\", \"\", path);\n this.Update();\n }\n\n // The captured value for a named dynamic segment (e.g. \"toyId\" for\n // \":toyId\" in \"/dogs/:id/toys/:toyId\") or \"*\" for a trailing wildcard\n // segment's matched remainder — \"\" if `name` wasn't captured by whatever\n // route just matched, the same \"empty string, not null\" convention\n // `Param` itself already uses.\n public string Params(string name) {\n for (number i = 0; i < this.ParamNames.Length; i = i + 1) {\n if (this.ParamNames[i] == name) {\n return this.ParamValues[i];\n }\n }\n return \"\";\n }\n\n // The current URL's query-string value for `key` (e.g. \"name\" for\n // \"?sort=name\") — \"\" if `key` isn't present. See QueryKeys/QueryValues'\n // own comment for the percent-decoding cut.\n public string Query(string key) {\n for (number i = 0; i < this.QueryKeys.Length; i = i + 1) {\n if (this.QueryKeys[i] == key) {\n return this.QueryValues[i];\n }\n }\n return \"\";\n }\n\n // KopScript strings have no index-based substring/slice method (only\n // arrays do — see Array.Slice), so splitting on the literal separator and\n // taking the piece after it is the idiomatic way to strip a known\n // single-character prefix here, both for \"?\" (query string) and \":\"\n // (a pattern segment's own leading marker, used below in Match).\n private void ParseQuery(string pathWithQuery) {\n this.QueryKeys = [];\n this.QueryValues = [];\n string[] afterMark = pathWithQuery.Split(\"?\");\n if (afterMark.Length < 2) { return; }\n string rawQuery = afterMark[1];\n if (rawQuery.Length == 0) { return; }\n string[] pairs = rawQuery.Split(\"&\");\n foreach (string pair in pairs) {\n string[] kv = pair.Split(\"=\");\n if (kv.Length == 2) {\n this.QueryKeys = this.QueryKeys.Push(kv[0]);\n this.QueryValues = this.QueryValues.Push(kv[1]);\n }\n }\n }\n\n private Component Match(string fullPath) {\n string effectivePath = fullPath;\n if (!this.Guard(fullPath)) {\n history.pushState(\"\", \"\", this.RedirectPath);\n effectivePath = this.RedirectPath;\n }\n\n this.ParseQuery(effectivePath);\n string[] pathAndQuery = effectivePath.Split(\"?\");\n string[] pathSegments = pathAndQuery[0].Split(\"/\");\n\n Component found = this.NotFoundPage;\n string[] foundNames = [];\n string[] foundValues = [];\n\n for (number i = 0; i < this.Paths.Length; i = i + 1) {\n string[] patternSegments = this.Paths[i].Split(\"/\");\n bool hasWildcard = patternSegments.Length > 0 && patternSegments[patternSegments.Length - 1] == \"*\";\n\n number staticCount = patternSegments.Length;\n if (hasWildcard) {\n staticCount = patternSegments.Length - 1;\n }\n\n if (hasWildcard) {\n if (pathSegments.Length < staticCount) { continue; }\n } else {\n if (patternSegments.Length != pathSegments.Length) { continue; }\n }\n\n bool matched = true;\n string[] names = [];\n string[] values = [];\n for (number j = 0; j < staticCount; j = j + 1) {\n if (patternSegments[j].StartsWith(\":\")) {\n names = names.Push(patternSegments[j].Split(\":\")[1]);\n values = values.Push(pathSegments[j]);\n } else if (patternSegments[j] != pathSegments[j]) {\n matched = false;\n break;\n }\n }\n\n if (matched && hasWildcard) {\n names = names.Push(\"*\");\n values = values.Push(pathSegments.Slice(staticCount, pathSegments.Length).Join(\"/\"));\n }\n\n if (matched) {\n found = this.Pages[i];\n foundNames = names;\n foundValues = values;\n break;\n }\n }\n\n this.ParamNames = foundNames;\n this.ParamValues = foundValues;\n this.Param = \"\";\n if (foundValues.Length > 0) {\n this.Param = foundValues[0];\n }\n return found;\n }\n\n // The outlet itself is a plain, unchanging `<div>` — its tag/className\n // never change between navigations, so Component's own diffing (see\n // vdom.ks) reuses the exact same real outlet element across every\n // navigation now, rather than rebuilding it. What goes *inside* it is a\n // nested Component (the matched page), which isn't something a VElement\n // tree can describe as data — handled imperatively in AfterRender below\n // instead, once the outlet is real. The page instance itself is always\n // freshly Mount()ed into it on every navigation (matching this class's\n // pre-diffing behavior exactly) — a page's own re-renders, once mounted,\n // still go through the normal diffed Update() path when its own state\n // changes; only the *outer* page-switch itself stays a full remount, the\n // same documented \"parent/child reconciliation is out of scope\" trade-off\n // Component's own class comment already makes.\n public override VElement Render() {\n VElement outlet = VElement.Create(\"div\");\n outlet.ClassName = \"router-outlet\";\n return outlet;\n }\n\n protected override void AfterRender(Element root) {\n string path = location.pathname + location.search;\n Component page = this.Match(path);\n root.textContent = \"\";\n page.Mount(root);\n }\n}\n"],"names":[],"mappings":";;;;AA0BA;EAuDE;;IACa;IACA;IACO;IACP;IACK;IACC;IACF;IACE;IACN;IACO;IAMK;;;EAYlB;IACM;IACA;;;EAQN;IACa;IACP;;;EAGN;IACY;IACN;;;EAQN;IACL;MACE;QACE;;;IAGJ;;;EAMK;IACL;MACE;QACE;;;IAGJ;;;EAQM;IACS;IACE;IACjB;IACA;MAA4B;;IAC5B;IACA;MAA4B;;IAC5B;IACA;MACE;MACA;QACiB;QACE;;;;;EAKf;IACN;IACA;MACmB;MACH;;IAGD;IACf;IACA;IAEA;IACA;IACA;IAEA;MACE;MACA;MAEA;MACA;QACc;;MAGd;QACE;UAAyC;;;QAEzC;UAAqD;;;MAGvD;MACA;MACA;MACA;QACE;UACQ;UACC;;UAEC;UACR;;;MAIJ;QACQ;QACC;;MAGT;QACQ;QACK;QACC;QACZ;;;IAIY;IACC;IACN;IACX;MACa;;IAEb;;;EAgBc;IACd;IACiB;IACjB;;;EAGiB;IACjB;IACA;IACiB;IACP"}
|
package/src/router.ks
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
using "./dom";
|
|
2
2
|
using "./component";
|
|
3
|
+
using "./velement";
|
|
3
4
|
|
|
4
5
|
// History-API routing (real paths — `/about`, not `#/about`) via
|
|
5
6
|
// `history.pushState`/the browser's native `popstate` event. This needs a
|
|
@@ -239,17 +240,29 @@ class Router : Component {
|
|
|
239
240
|
return found;
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
// The outlet itself is a plain, unchanging `<div>` — its tag/className
|
|
244
|
+
// never change between navigations, so Component's own diffing (see
|
|
245
|
+
// vdom.ks) reuses the exact same real outlet element across every
|
|
246
|
+
// navigation now, rather than rebuilding it. What goes *inside* it is a
|
|
247
|
+
// nested Component (the matched page), which isn't something a VElement
|
|
248
|
+
// tree can describe as data — handled imperatively in AfterRender below
|
|
249
|
+
// instead, once the outlet is real. The page instance itself is always
|
|
250
|
+
// freshly Mount()ed into it on every navigation (matching this class's
|
|
251
|
+
// pre-diffing behavior exactly) — a page's own re-renders, once mounted,
|
|
252
|
+
// still go through the normal diffed Update() path when its own state
|
|
253
|
+
// changes; only the *outer* page-switch itself stays a full remount, the
|
|
254
|
+
// same documented "parent/child reconciliation is out of scope" trade-off
|
|
255
|
+
// Component's own class comment already makes.
|
|
256
|
+
public override VElement Render() {
|
|
257
|
+
VElement outlet = VElement.Create("div");
|
|
258
|
+
outlet.ClassName = "router-outlet";
|
|
259
|
+
return outlet;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
protected override void AfterRender(Element root) {
|
|
250
263
|
string path = location.pathname + location.search;
|
|
251
264
|
Component page = this.Match(path);
|
|
252
|
-
|
|
253
|
-
|
|
265
|
+
root.textContent = "";
|
|
266
|
+
page.Mount(root);
|
|
254
267
|
}
|
|
255
268
|
}
|
package/src/vdom.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
|
|
2
|
+
import { VElement } from "./velement.js";
|
|
3
|
+
|
|
4
|
+
export function Materialize(tree) {
|
|
5
|
+
let el = document.createElement(tree.Tag);
|
|
6
|
+
if ((tree.RawHtml.length > 0)) {
|
|
7
|
+
el.innerHTML = tree.RawHtml;
|
|
8
|
+
} else if ((tree.Children.length > 0)) {
|
|
9
|
+
for (const child of tree.Children) {
|
|
10
|
+
el.appendChild(Materialize(child));
|
|
11
|
+
}
|
|
12
|
+
} else {
|
|
13
|
+
el.textContent = tree.TextContent;
|
|
14
|
+
}
|
|
15
|
+
el.className = tree.ClassName;
|
|
16
|
+
el.id = tree.Id;
|
|
17
|
+
el.value = tree.Value;
|
|
18
|
+
for (let i = 0; (i < tree.ExtraNames.length); i = (i + 1)) {
|
|
19
|
+
el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);
|
|
20
|
+
}
|
|
21
|
+
el.addEventListener("click", tree.OnClick);
|
|
22
|
+
el.addEventListener("input", tree.OnInput);
|
|
23
|
+
el.addEventListener("blur", tree.OnBlur);
|
|
24
|
+
el.addEventListener("change", tree.OnChange);
|
|
25
|
+
tree.RealNode = el;
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
export function Patch(parent, old, updated) {
|
|
29
|
+
if ((old !== null)) {
|
|
30
|
+
let oldTree = old;
|
|
31
|
+
let maybeOldNode = oldTree.RealNode;
|
|
32
|
+
if ((maybeOldNode !== null)) {
|
|
33
|
+
let realNode = maybeOldNode;
|
|
34
|
+
if ((oldTree.Tag !== updated.Tag)) {
|
|
35
|
+
let created = Materialize(updated);
|
|
36
|
+
parent.replaceChild(created, realNode);
|
|
37
|
+
return created;
|
|
38
|
+
} else {
|
|
39
|
+
updated.RealNode = realNode;
|
|
40
|
+
if (((updated.RawHtml.length > 0) || (oldTree.RawHtml.length > 0))) {
|
|
41
|
+
if ((updated.RawHtml !== oldTree.RawHtml)) {
|
|
42
|
+
realNode.innerHTML = updated.RawHtml;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
if ((updated.TextContent !== oldTree.TextContent)) {
|
|
46
|
+
realNode.textContent = updated.TextContent;
|
|
47
|
+
}
|
|
48
|
+
PatchChildren(realNode, oldTree.Children, updated.Children);
|
|
49
|
+
}
|
|
50
|
+
if ((updated.ClassName !== oldTree.ClassName)) {
|
|
51
|
+
realNode.className = updated.ClassName;
|
|
52
|
+
}
|
|
53
|
+
if ((updated.Id !== oldTree.Id)) {
|
|
54
|
+
realNode.id = updated.Id;
|
|
55
|
+
}
|
|
56
|
+
realNode.value = updated.Value;
|
|
57
|
+
for (let i = 0; (i < updated.ExtraNames.length); i = (i + 1)) {
|
|
58
|
+
realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);
|
|
59
|
+
}
|
|
60
|
+
realNode.removeEventListener("click", oldTree.OnClick);
|
|
61
|
+
realNode.addEventListener("click", updated.OnClick);
|
|
62
|
+
realNode.removeEventListener("input", oldTree.OnInput);
|
|
63
|
+
realNode.addEventListener("input", updated.OnInput);
|
|
64
|
+
realNode.removeEventListener("blur", oldTree.OnBlur);
|
|
65
|
+
realNode.addEventListener("blur", updated.OnBlur);
|
|
66
|
+
realNode.removeEventListener("change", oldTree.OnChange);
|
|
67
|
+
realNode.addEventListener("change", updated.OnChange);
|
|
68
|
+
return realNode;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
let created = Materialize(updated);
|
|
72
|
+
parent.appendChild(created);
|
|
73
|
+
return created;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
let created = Materialize(updated);
|
|
77
|
+
parent.appendChild(created);
|
|
78
|
+
return created;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function PatchChildren(parent, oldChildren, newChildren) {
|
|
82
|
+
let oldConsumed = [];
|
|
83
|
+
for (let i = 0; (i < oldChildren.length); i = (i + 1)) {
|
|
84
|
+
oldConsumed = [...oldConsumed, false];
|
|
85
|
+
}
|
|
86
|
+
let matchedOldIndex = [];
|
|
87
|
+
for (let i = 0; (i < newChildren.length); i = (i + 1)) {
|
|
88
|
+
matchedOldIndex = [...matchedOldIndex, -1];
|
|
89
|
+
}
|
|
90
|
+
for (let i = 0; (i < newChildren.length); i = (i + 1)) {
|
|
91
|
+
if ((newChildren[i].Id.length === 0)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (let j = 0; (j < oldChildren.length); j = (j + 1)) {
|
|
95
|
+
if ((!oldConsumed[j] && (oldChildren[j].Id === newChildren[i].Id))) {
|
|
96
|
+
matchedOldIndex[i] = j;
|
|
97
|
+
oldConsumed[j] = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let nextOld = 0;
|
|
103
|
+
for (let i = 0; (i < newChildren.length); i = (i + 1)) {
|
|
104
|
+
if ((matchedOldIndex[i] >= 0)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
while (((nextOld < oldChildren.length) && oldConsumed[nextOld])) {
|
|
108
|
+
nextOld = (nextOld + 1);
|
|
109
|
+
}
|
|
110
|
+
if ((nextOld < oldChildren.length)) {
|
|
111
|
+
matchedOldIndex[i] = nextOld;
|
|
112
|
+
oldConsumed[nextOld] = true;
|
|
113
|
+
nextOld = (nextOld + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (let i = 0; (i < newChildren.length); i = (i + 1)) {
|
|
117
|
+
let matchedOld = null;
|
|
118
|
+
if ((matchedOldIndex[i] >= 0)) {
|
|
119
|
+
matchedOld = oldChildren[matchedOldIndex[i]];
|
|
120
|
+
}
|
|
121
|
+
let childNode = Patch(parent, matchedOld, newChildren[i]);
|
|
122
|
+
parent.appendChild(childNode);
|
|
123
|
+
}
|
|
124
|
+
for (let j = 0; (j < oldChildren.length); j = (j + 1)) {
|
|
125
|
+
if (oldConsumed[j]) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let maybeOldNode = oldChildren[j].RealNode;
|
|
129
|
+
if ((maybeOldNode !== null)) {
|
|
130
|
+
let oldNode = maybeOldNode;
|
|
131
|
+
parent.removeChild(oldNode);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//# sourceMappingURL=vdom.js.map
|
package/src/vdom.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vdom.js","sources":["vdom.ks"],"sourcesContent":["using \"./dom\";\nusing \"./velement\";\n\n// The diff/patch engine behind real vdom diffing: Component.Update() (see\n// component.ks) calls Patch() with the PREVIOUS render's VElement tree\n// (which carries each node's real, live DOM counterpart via its own\n// RealNode field) and the NEW tree Render() just produced, and gets back\n// real DOM mutated/reused in place wherever possible instead of a full\n// subtree rebuild.\n//\n// Deliberately never reads the live DOM back to rediscover structure (no\n// \"get current children\"/\"get current tag\" API exists, or is needed) —\n// the retained *previous* VElement tree already records everything Patch()\n// needs to know about what's currently there. This is what makes the whole\n// approach work without a generic children/attributes read-back API that\n// KopScript's narrow, curated DOM binding doesn't have.\n\n// Builds a brand-new, fully real DOM subtree from a VElement tree with no\n// diffing at all — first mount, or whenever Patch() decides a subtree must\n// be replaced outright (no previous node to reuse, or the tag changed).\n// Mutates `tree.RealNode` (and recursively every descendant's) as a side\n// effect, so the tree this was called on becomes the new \"previous tree\"\n// the next Patch() call diffs against.\nElement Materialize(VElement tree) {\n Element el = document.createElement(tree.Tag);\n\n if (tree.RawHtml.Length > 0) {\n el.innerHTML = tree.RawHtml;\n } else if (tree.Children.Length > 0) {\n foreach (VElement child in tree.Children) {\n el.appendChild(Materialize(child));\n }\n } else {\n el.textContent = tree.TextContent;\n }\n\n el.className = tree.ClassName;\n el.id = tree.Id;\n el.value = tree.Value;\n\n for (number i = 0; i < tree.ExtraNames.Length; i = i + 1) {\n el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);\n }\n\n el.addEventListener(\"click\", tree.OnClick);\n el.addEventListener(\"input\", tree.OnInput);\n el.addEventListener(\"blur\", tree.OnBlur);\n el.addEventListener(\"change\", tree.OnChange);\n\n tree.RealNode = el;\n return el;\n}\n\n// Diffs `updated` against `old` (the previous render's tree for this exact\n// position, or null if there is none — first mount) and returns the real\n// DOM node now representing `updated`, reusing `old`'s real node in place\n// whenever the tag matches. `parent` is only used to attach/replace at the\n// top of whatever subtree Patch() is called on — child-level attach/replace\n// happens inside PatchChildren.\n//\n// Deliberately all positive-branch `if (x != null) { ... } else { ... }`,\n// never an early-return guard clause — KopScript's nullable narrowing is\n// scope-based, not reachability-based, so `if (x == null) { return; }\n// use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md \"Common\n// mistakes\"). Every nullable member-access path (`oldTree.RealNode`,\n// never narrows directly either) is read into a local first for the same\n// reason.\nElement Patch(Element parent, VElement? old, VElement updated) {\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element realNode = maybeOldNode;\n if (oldTree.Tag != updated.Tag) {\n Element created = Materialize(updated);\n parent.replaceChild(created, realNode);\n return created;\n } else {\n updated.RealNode = realNode;\n\n if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {\n if (updated.RawHtml != oldTree.RawHtml) {\n realNode.innerHTML = updated.RawHtml;\n }\n } else {\n if (updated.TextContent != oldTree.TextContent) {\n realNode.textContent = updated.TextContent;\n }\n PatchChildren(realNode, oldTree.Children, updated.Children);\n }\n\n if (updated.ClassName != oldTree.ClassName) {\n realNode.className = updated.ClassName;\n }\n if (updated.Id != oldTree.Id) {\n realNode.id = updated.Id;\n }\n // Always assigned, never conditionally on updated.Value != oldTree.Value\n // — unlike TextContent/ClassName/Id, an <input>/<select>'s live value\n // can diverge from the last-recorded VElement.Value purely through\n // user interaction (typing, picking an option) with no Update() ever\n // running in between (a real, deliberate pattern — see KopularDemo's\n // dogs_page.ks, which never Update()s on input/change). The recorded\n // oldTree.Value only reflects the tree as of the last actual render,\n // so comparing against it can't tell \"genuinely unchanged\" apart from\n // \"changed live in the DOM since then, framework never told\" — the\n // same reason a real \"controlled input\" (React's own term for this)\n // always writes value on every render rather than diffing it.\n realNode.value = updated.Value;\n\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n\n // Always swap all four listeners — cheap, and sidesteps needing to\n // compare function identity (every Render() call creates fresh\n // closures, so \"did the handler actually change\" isn't answerable\n // any other way).\n realNode.removeEventListener(\"click\", oldTree.OnClick);\n realNode.addEventListener(\"click\", updated.OnClick);\n realNode.removeEventListener(\"input\", oldTree.OnInput);\n realNode.addEventListener(\"input\", updated.OnInput);\n realNode.removeEventListener(\"blur\", oldTree.OnBlur);\n realNode.addEventListener(\"blur\", updated.OnBlur);\n realNode.removeEventListener(\"change\", oldTree.OnChange);\n realNode.addEventListener(\"change\", updated.OnChange);\n\n return realNode;\n }\n } else {\n // Shouldn't happen in practice (every previously-rendered tree has a\n // real node by the time a second render diffs against it) — treated\n // as \"nothing to reuse\" rather than a crash, same defensive spirit\n // as Component's own IsMounted guard.\n Element created = Materialize(updated);\n parent.appendChild(created);\n return created;\n }\n } else {\n Element created = Materialize(updated);\n parent.appendChild(created);\n return created;\n }\n}\n\n// Keyed reconciliation: each VElement's own Id is its key when non-empty —\n// a real, existing DOM convention, needing no new API or syntax. A new\n// child whose Id matches an old child's Id is patched against that old\n// child (reusing its real node) regardless of position; a new child with\n// no Id, or an Id not present among the old children, falls back to\n// pairing positionally against whatever old children are still unconsumed,\n// in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered\n// list still produces the correct final output, but a given item's real\n// DOM node (and anything stateful attached to it, like focus) isn't\n// guaranteed to follow its data across the reorder — give list items a\n// stable Id for that guarantee.\nvoid PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {\n bool[] oldConsumed = [];\n for (number i = 0; i < oldChildren.Length; i = i + 1) {\n oldConsumed = oldConsumed.Push(false);\n }\n\n number[] matchedOldIndex = [];\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n matchedOldIndex = matchedOldIndex.Push(-1);\n }\n\n // Pass 1: keyed matches, by Id.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (newChildren[i].Id.Length == 0) { continue; }\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {\n matchedOldIndex[i] = j;\n oldConsumed[j] = true;\n break;\n }\n }\n }\n\n // Pass 2: positional fallback for everything Pass 1 didn't match —\n // pair each remaining new child against the next still-unconsumed old\n // child, in order.\n number nextOld = 0;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] >= 0) { continue; }\n while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {\n nextOld = nextOld + 1;\n }\n if (nextOld < oldChildren.Length) {\n matchedOldIndex[i] = nextOld;\n oldConsumed[nextOld] = true;\n nextOld = nextOld + 1;\n }\n }\n\n // Pass 3: patch/create each new child in order, then move it into its\n // correct final position — appendChild on a node already attached\n // elsewhere in the DOM MOVES it (real DOM semantics), so processing new\n // children in their final desired order and always appending naturally\n // builds up the correct sequence, no separate insertBefore/reference-\n // node bookkeeping needed. Safe because VElement.Children is always the\n // COMPLETE list of a node's children — nothing else ever shares `parent`.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n VElement? matchedOld = null;\n if (matchedOldIndex[i] >= 0) {\n matchedOld = oldChildren[matchedOldIndex[i]];\n }\n Element childNode = Patch(parent, matchedOld, newChildren[i]);\n parent.appendChild(childNode);\n }\n\n // Pass 4: remove whatever old children never got reused.\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (oldConsumed[j]) { continue; }\n Element? maybeOldNode = oldChildren[j].RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n"],"names":[],"mappings":";;;AAuBA;EACE;EAEA;IACe;;IAEb;MACgB;;;IAGD;;EAGJ;EACP;EACG;EAET;IACiB;;EAGE;EACA;EACA;EACA;EAEL;EACd;;AAiBF;EACE;IACE;IACA;IACA;MACE;MACA;QACE;QACmB;QACnB;;QAEiB;QAEjB;UACE;YACqB;;;UAGrB;YACuB;;UAEV;;QAGf;UACqB;;QAErB;UACc;;QAaC;QAEf;UACuB;;QAOK;QACH;QACG;QACH;QACG;QACH;QACG;QACH;QAEzB;;;MAOF;MACkB;MAClB;;;IAGF;IACkB;IAClB;;;AAeJ;EACE;EACA;IACc;;EAGd;EACA;IACkB;;EAIlB;IACE;MAAqC;;IACrC;MACE;QACqB;QACJ;QACf;;;;EAQN;EACA;IACE;MAA+B;;IAC/B;MACU;;IAEV;MACqB;MACE;MACb;;;EAWZ;IACE;IACA;MACa;;IAEb;IACkB;;EAIpB;IACE;MAAsB;;IACtB;IACA;MACE;MACkB"}
|
package/src/vdom.ks
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
using "./dom";
|
|
2
|
+
using "./velement";
|
|
3
|
+
|
|
4
|
+
// The diff/patch engine behind real vdom diffing: Component.Update() (see
|
|
5
|
+
// component.ks) calls Patch() with the PREVIOUS render's VElement tree
|
|
6
|
+
// (which carries each node's real, live DOM counterpart via its own
|
|
7
|
+
// RealNode field) and the NEW tree Render() just produced, and gets back
|
|
8
|
+
// real DOM mutated/reused in place wherever possible instead of a full
|
|
9
|
+
// subtree rebuild.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately never reads the live DOM back to rediscover structure (no
|
|
12
|
+
// "get current children"/"get current tag" API exists, or is needed) —
|
|
13
|
+
// the retained *previous* VElement tree already records everything Patch()
|
|
14
|
+
// needs to know about what's currently there. This is what makes the whole
|
|
15
|
+
// approach work without a generic children/attributes read-back API that
|
|
16
|
+
// KopScript's narrow, curated DOM binding doesn't have.
|
|
17
|
+
|
|
18
|
+
// Builds a brand-new, fully real DOM subtree from a VElement tree with no
|
|
19
|
+
// diffing at all — first mount, or whenever Patch() decides a subtree must
|
|
20
|
+
// be replaced outright (no previous node to reuse, or the tag changed).
|
|
21
|
+
// Mutates `tree.RealNode` (and recursively every descendant's) as a side
|
|
22
|
+
// effect, so the tree this was called on becomes the new "previous tree"
|
|
23
|
+
// the next Patch() call diffs against.
|
|
24
|
+
Element Materialize(VElement tree) {
|
|
25
|
+
Element el = document.createElement(tree.Tag);
|
|
26
|
+
|
|
27
|
+
if (tree.RawHtml.Length > 0) {
|
|
28
|
+
el.innerHTML = tree.RawHtml;
|
|
29
|
+
} else if (tree.Children.Length > 0) {
|
|
30
|
+
foreach (VElement child in tree.Children) {
|
|
31
|
+
el.appendChild(Materialize(child));
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
el.textContent = tree.TextContent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
el.className = tree.ClassName;
|
|
38
|
+
el.id = tree.Id;
|
|
39
|
+
el.value = tree.Value;
|
|
40
|
+
|
|
41
|
+
for (number i = 0; i < tree.ExtraNames.Length; i = i + 1) {
|
|
42
|
+
el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
el.addEventListener("click", tree.OnClick);
|
|
46
|
+
el.addEventListener("input", tree.OnInput);
|
|
47
|
+
el.addEventListener("blur", tree.OnBlur);
|
|
48
|
+
el.addEventListener("change", tree.OnChange);
|
|
49
|
+
|
|
50
|
+
tree.RealNode = el;
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Diffs `updated` against `old` (the previous render's tree for this exact
|
|
55
|
+
// position, or null if there is none — first mount) and returns the real
|
|
56
|
+
// DOM node now representing `updated`, reusing `old`'s real node in place
|
|
57
|
+
// whenever the tag matches. `parent` is only used to attach/replace at the
|
|
58
|
+
// top of whatever subtree Patch() is called on — child-level attach/replace
|
|
59
|
+
// happens inside PatchChildren.
|
|
60
|
+
//
|
|
61
|
+
// Deliberately all positive-branch `if (x != null) { ... } else { ... }`,
|
|
62
|
+
// never an early-return guard clause — KopScript's nullable narrowing is
|
|
63
|
+
// scope-based, not reachability-based, so `if (x == null) { return; }
|
|
64
|
+
// use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md "Common
|
|
65
|
+
// mistakes"). Every nullable member-access path (`oldTree.RealNode`,
|
|
66
|
+
// never narrows directly either) is read into a local first for the same
|
|
67
|
+
// reason.
|
|
68
|
+
Element Patch(Element parent, VElement? old, VElement updated) {
|
|
69
|
+
if (old != null) {
|
|
70
|
+
VElement oldTree = old;
|
|
71
|
+
Element? maybeOldNode = oldTree.RealNode;
|
|
72
|
+
if (maybeOldNode != null) {
|
|
73
|
+
Element realNode = maybeOldNode;
|
|
74
|
+
if (oldTree.Tag != updated.Tag) {
|
|
75
|
+
Element created = Materialize(updated);
|
|
76
|
+
parent.replaceChild(created, realNode);
|
|
77
|
+
return created;
|
|
78
|
+
} else {
|
|
79
|
+
updated.RealNode = realNode;
|
|
80
|
+
|
|
81
|
+
if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {
|
|
82
|
+
if (updated.RawHtml != oldTree.RawHtml) {
|
|
83
|
+
realNode.innerHTML = updated.RawHtml;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (updated.TextContent != oldTree.TextContent) {
|
|
87
|
+
realNode.textContent = updated.TextContent;
|
|
88
|
+
}
|
|
89
|
+
PatchChildren(realNode, oldTree.Children, updated.Children);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (updated.ClassName != oldTree.ClassName) {
|
|
93
|
+
realNode.className = updated.ClassName;
|
|
94
|
+
}
|
|
95
|
+
if (updated.Id != oldTree.Id) {
|
|
96
|
+
realNode.id = updated.Id;
|
|
97
|
+
}
|
|
98
|
+
// Always assigned, never conditionally on updated.Value != oldTree.Value
|
|
99
|
+
// — unlike TextContent/ClassName/Id, an <input>/<select>'s live value
|
|
100
|
+
// can diverge from the last-recorded VElement.Value purely through
|
|
101
|
+
// user interaction (typing, picking an option) with no Update() ever
|
|
102
|
+
// running in between (a real, deliberate pattern — see KopularDemo's
|
|
103
|
+
// dogs_page.ks, which never Update()s on input/change). The recorded
|
|
104
|
+
// oldTree.Value only reflects the tree as of the last actual render,
|
|
105
|
+
// so comparing against it can't tell "genuinely unchanged" apart from
|
|
106
|
+
// "changed live in the DOM since then, framework never told" — the
|
|
107
|
+
// same reason a real "controlled input" (React's own term for this)
|
|
108
|
+
// always writes value on every render rather than diffing it.
|
|
109
|
+
realNode.value = updated.Value;
|
|
110
|
+
|
|
111
|
+
for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {
|
|
112
|
+
realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Always swap all four listeners — cheap, and sidesteps needing to
|
|
116
|
+
// compare function identity (every Render() call creates fresh
|
|
117
|
+
// closures, so "did the handler actually change" isn't answerable
|
|
118
|
+
// any other way).
|
|
119
|
+
realNode.removeEventListener("click", oldTree.OnClick);
|
|
120
|
+
realNode.addEventListener("click", updated.OnClick);
|
|
121
|
+
realNode.removeEventListener("input", oldTree.OnInput);
|
|
122
|
+
realNode.addEventListener("input", updated.OnInput);
|
|
123
|
+
realNode.removeEventListener("blur", oldTree.OnBlur);
|
|
124
|
+
realNode.addEventListener("blur", updated.OnBlur);
|
|
125
|
+
realNode.removeEventListener("change", oldTree.OnChange);
|
|
126
|
+
realNode.addEventListener("change", updated.OnChange);
|
|
127
|
+
|
|
128
|
+
return realNode;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Shouldn't happen in practice (every previously-rendered tree has a
|
|
132
|
+
// real node by the time a second render diffs against it) — treated
|
|
133
|
+
// as "nothing to reuse" rather than a crash, same defensive spirit
|
|
134
|
+
// as Component's own IsMounted guard.
|
|
135
|
+
Element created = Materialize(updated);
|
|
136
|
+
parent.appendChild(created);
|
|
137
|
+
return created;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
Element created = Materialize(updated);
|
|
141
|
+
parent.appendChild(created);
|
|
142
|
+
return created;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Keyed reconciliation: each VElement's own Id is its key when non-empty —
|
|
147
|
+
// a real, existing DOM convention, needing no new API or syntax. A new
|
|
148
|
+
// child whose Id matches an old child's Id is patched against that old
|
|
149
|
+
// child (reusing its real node) regardless of position; a new child with
|
|
150
|
+
// no Id, or an Id not present among the old children, falls back to
|
|
151
|
+
// pairing positionally against whatever old children are still unconsumed,
|
|
152
|
+
// in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered
|
|
153
|
+
// list still produces the correct final output, but a given item's real
|
|
154
|
+
// DOM node (and anything stateful attached to it, like focus) isn't
|
|
155
|
+
// guaranteed to follow its data across the reorder — give list items a
|
|
156
|
+
// stable Id for that guarantee.
|
|
157
|
+
void PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {
|
|
158
|
+
bool[] oldConsumed = [];
|
|
159
|
+
for (number i = 0; i < oldChildren.Length; i = i + 1) {
|
|
160
|
+
oldConsumed = oldConsumed.Push(false);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
number[] matchedOldIndex = [];
|
|
164
|
+
for (number i = 0; i < newChildren.Length; i = i + 1) {
|
|
165
|
+
matchedOldIndex = matchedOldIndex.Push(-1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Pass 1: keyed matches, by Id.
|
|
169
|
+
for (number i = 0; i < newChildren.Length; i = i + 1) {
|
|
170
|
+
if (newChildren[i].Id.Length == 0) { continue; }
|
|
171
|
+
for (number j = 0; j < oldChildren.Length; j = j + 1) {
|
|
172
|
+
if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {
|
|
173
|
+
matchedOldIndex[i] = j;
|
|
174
|
+
oldConsumed[j] = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Pass 2: positional fallback for everything Pass 1 didn't match —
|
|
181
|
+
// pair each remaining new child against the next still-unconsumed old
|
|
182
|
+
// child, in order.
|
|
183
|
+
number nextOld = 0;
|
|
184
|
+
for (number i = 0; i < newChildren.Length; i = i + 1) {
|
|
185
|
+
if (matchedOldIndex[i] >= 0) { continue; }
|
|
186
|
+
while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {
|
|
187
|
+
nextOld = nextOld + 1;
|
|
188
|
+
}
|
|
189
|
+
if (nextOld < oldChildren.Length) {
|
|
190
|
+
matchedOldIndex[i] = nextOld;
|
|
191
|
+
oldConsumed[nextOld] = true;
|
|
192
|
+
nextOld = nextOld + 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Pass 3: patch/create each new child in order, then move it into its
|
|
197
|
+
// correct final position — appendChild on a node already attached
|
|
198
|
+
// elsewhere in the DOM MOVES it (real DOM semantics), so processing new
|
|
199
|
+
// children in their final desired order and always appending naturally
|
|
200
|
+
// builds up the correct sequence, no separate insertBefore/reference-
|
|
201
|
+
// node bookkeeping needed. Safe because VElement.Children is always the
|
|
202
|
+
// COMPLETE list of a node's children — nothing else ever shares `parent`.
|
|
203
|
+
for (number i = 0; i < newChildren.Length; i = i + 1) {
|
|
204
|
+
VElement? matchedOld = null;
|
|
205
|
+
if (matchedOldIndex[i] >= 0) {
|
|
206
|
+
matchedOld = oldChildren[matchedOldIndex[i]];
|
|
207
|
+
}
|
|
208
|
+
Element childNode = Patch(parent, matchedOld, newChildren[i]);
|
|
209
|
+
parent.appendChild(childNode);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pass 4: remove whatever old children never got reused.
|
|
213
|
+
for (number j = 0; j < oldChildren.Length; j = j + 1) {
|
|
214
|
+
if (oldConsumed[j]) { continue; }
|
|
215
|
+
Element? maybeOldNode = oldChildren[j].RealNode;
|
|
216
|
+
if (maybeOldNode != null) {
|
|
217
|
+
Element oldNode = maybeOldNode;
|
|
218
|
+
parent.removeChild(oldNode);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/velement.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
|
|
2
|
+
|
|
3
|
+
export class VElement {
|
|
4
|
+
constructor(tag) {
|
|
5
|
+
this.Tag = tag;
|
|
6
|
+
this.TextContent = "";
|
|
7
|
+
this.ClassName = "";
|
|
8
|
+
this.Id = "";
|
|
9
|
+
this.Value = "";
|
|
10
|
+
this.Children = [];
|
|
11
|
+
this.RawHtml = "";
|
|
12
|
+
this.OnClick = (e) => {
|
|
13
|
+
};
|
|
14
|
+
this.OnInput = (e) => {
|
|
15
|
+
};
|
|
16
|
+
this.OnBlur = (e) => {
|
|
17
|
+
};
|
|
18
|
+
this.OnChange = (e) => {
|
|
19
|
+
};
|
|
20
|
+
this.ExtraNames = [];
|
|
21
|
+
this.ExtraValues = [];
|
|
22
|
+
this.RealNode = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static Create(tag) {
|
|
26
|
+
return new VElement(tag);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
AppendChild(child) {
|
|
30
|
+
this.Children = [...this.Children, child];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
SetAttr(name, value) {
|
|
34
|
+
this.ExtraNames = [...this.ExtraNames, name];
|
|
35
|
+
this.ExtraValues = [...this.ExtraValues, value];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=velement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"velement.js","sources":["velement.ks"],"sourcesContent":["using \"./dom\";\n\n// A lightweight, framework-owned description of one DOM element — Render()\n// builds a tree of these instead of real DOM nodes, so Update() can DIFF\n// the new tree against the previous one and patch only what changed,\n// instead of discarding and rebuilding the whole real DOM subtree every\n// time (see src/vdom.ks for the diff/patch engine, src/component.ks for\n// where Render()'s return type changed from Element to VElement).\n//\n// Fixed, named fields — not a generic prop bag — because KopScript has no\n// object-literal syntax to build one with. Fixed, named event slots — not\n// an array of handlers — because KopScript has no array-of-function-values\n// type either. Both are real language constraints, not an oversight; see\n// SetAttr below for the escape hatch covering everything not common enough\n// to deserve its own named field.\nclass VElement {\n public string Tag;\n // Mutually exclusive with Children and RawHtml — set at most one of the\n // three. TextContent/Children mirrors the same \"no mixed text/element\n // content\" rule KopScript's own templates already enforce.\n public string TextContent;\n public string ClassName;\n public string Id;\n // The one property patched via direct assignment, never setAttribute —\n // see Element.value's own comment in dom.ks for why (the \"default value\n // attribute\" vs \"current live value property\" DOM footgun — the exact\n // property behind the original typing bug this whole effort traces to).\n public string Value;\n public VElement[] Children;\n // An opaque, undiffed leaf — set instead of TextContent/Children for the\n // existing raw-HTML-then-wire-handlers pattern (header.ks/nav.ks). The\n // patch engine treats two VElements with different RawHtml as a single\n // innerHTML assignment, never recursing inside it — the same \"opaque\n // blob\" treatment `raw string` already gets everywhere else.\n public string RawHtml;\n\n // Each defaults to a real no-op, never null — KopScript has no nullable\n // *function* type to fall back on for \"no handler set\" (see Router.Guard\n // for the same default-real-function pattern already established).\n public (Event) => void OnClick;\n public (Event) => void OnInput;\n public (Event) => void OnBlur;\n public (Event) => void OnChange;\n\n // A real HTML attribute not common enough for its own named field (href,\n // src, alt, placeholder, ...) — parallel arrays, since KopScript has no\n // Dictionary type. Never Value (see its own field comment above). Public,\n // read directly by the patch engine (src/vdom.ks) rather than through\n // accessor methods — plain data, same style as this codebase's other\n // plain classes (Note, Dog, ...).\n public string[] ExtraNames;\n public string[] ExtraValues;\n\n // Set only once this VElement has been materialized into (or reused as)\n // a real DOM node — null on a freshly-built tree from a not-yet-patched\n // Render() call. The patch engine reads the *previous* render's tree's\n // RealNode to know what to reuse/patch; it never reads the live DOM back\n // to rediscover this (see vdom.ks's own header comment for why).\n public Element? RealNode;\n\n constructor(string tag) {\n this.Tag = tag;\n this.TextContent = \"\";\n this.ClassName = \"\";\n this.Id = \"\";\n this.Value = \"\";\n this.Children = [];\n this.RawHtml = \"\";\n this.OnClick = (Event e) => {};\n this.OnInput = (Event e) => {};\n this.OnBlur = (Event e) => {};\n this.OnChange = (Event e) => {};\n this.ExtraNames = [];\n this.ExtraValues = [];\n this.RealNode = null;\n }\n\n public static VElement Create(string tag) {\n return new VElement(tag);\n }\n\n public void AppendChild(VElement child) {\n this.Children = this.Children.Push(child);\n }\n\n // Last call for a given name wins if SetAttr is called more than once\n // with the same name on one VElement — the patch engine applies\n // ExtraNames/ExtraValues in order, so a later entry's setAttribute call\n // simply overwrites an earlier one for the same name, no special\n // dedup/replace logic needed here.\n public void SetAttr(string name, string value) {\n this.ExtraNames = this.ExtraNames.Push(name);\n this.ExtraValues = this.ExtraValues.Push(value);\n }\n}\n"],"names":[],"mappings":";;AAeA;EA6CE;IACW;IACQ;IACF;IACP;IACG;IACG;IACD;IACA;;IACA;;IACD;;IACE;;IACE;IACC;IACH;;;EAGF;IACZ;;;EAGK;IACS;;;EAQT;IACW;IACC"}
|