kopular 0.13.0 → 0.15.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/src/vdom.ks ADDED
@@ -0,0 +1,277 @@
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
+ // Skip attaching VElement's own shared no-op (see velement.ks) — it does
46
+ // nothing when invoked, so registering it costs real work (a listener
47
+ // list entry, held onto for nothing) for zero benefit. A real handler
48
+ // (never equal to the shared no-op) always gets attached as before.
49
+ if (tree.OnClick != NoOpEventHandler) { el.addEventListener("click", tree.OnClick); }
50
+ if (tree.OnInput != NoOpEventHandler) { el.addEventListener("input", tree.OnInput); }
51
+ if (tree.OnBlur != NoOpEventHandler) { el.addEventListener("blur", tree.OnBlur); }
52
+ if (tree.OnChange != NoOpEventHandler) { el.addEventListener("change", tree.OnChange); }
53
+
54
+ tree.RealNode = el;
55
+ return el;
56
+ }
57
+
58
+ // Diffs `updated` against `old` (the previous render's tree for this exact
59
+ // position, or null if there is none — first mount) and returns the real
60
+ // DOM node now representing `updated`, reusing `old`'s real node in place
61
+ // whenever the tag matches. `parent` is only used to attach/replace at the
62
+ // top of whatever subtree Patch() is called on — child-level attach/replace
63
+ // happens inside PatchChildren.
64
+ //
65
+ // Deliberately all positive-branch `if (x != null) { ... } else { ... }`,
66
+ // never an early-return guard clause — KopScript's nullable narrowing is
67
+ // scope-based, not reachability-based, so `if (x == null) { return; }
68
+ // use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md "Common
69
+ // mistakes"). Every nullable member-access path (`oldTree.RealNode`,
70
+ // never narrows directly either) is read into a local first for the same
71
+ // reason.
72
+ Element Patch(Element parent, VElement? old, VElement updated) {
73
+ if (old != null) {
74
+ VElement oldTree = old;
75
+ Element? maybeOldNode = oldTree.RealNode;
76
+ if (maybeOldNode != null) {
77
+ Element realNode = maybeOldNode;
78
+ if (oldTree.Tag != updated.Tag) {
79
+ Element created = Materialize(updated);
80
+ parent.replaceChild(created, realNode);
81
+ return created;
82
+ } else {
83
+ updated.RealNode = realNode;
84
+
85
+ if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {
86
+ if (updated.RawHtml != oldTree.RawHtml) {
87
+ realNode.innerHTML = updated.RawHtml;
88
+ }
89
+ } else {
90
+ if (updated.TextContent != oldTree.TextContent) {
91
+ realNode.textContent = updated.TextContent;
92
+ }
93
+ PatchChildren(realNode, oldTree.Children, updated.Children);
94
+ }
95
+
96
+ if (updated.ClassName != oldTree.ClassName) {
97
+ realNode.className = updated.ClassName;
98
+ }
99
+ if (updated.Id != oldTree.Id) {
100
+ realNode.id = updated.Id;
101
+ }
102
+ // Always assigned, never conditionally on updated.Value != oldTree.Value
103
+ // — unlike TextContent/ClassName/Id, an <input>/<select>'s live value
104
+ // can diverge from the last-recorded VElement.Value purely through
105
+ // user interaction (typing, picking an option) with no Update() ever
106
+ // running in between (a real, deliberate pattern — see KopularDemo's
107
+ // dogs_page.ks, which never Update()s on input/change). The recorded
108
+ // oldTree.Value only reflects the tree as of the last actual render,
109
+ // so comparing against it can't tell "genuinely unchanged" apart from
110
+ // "changed live in the DOM since then, framework never told" — the
111
+ // same reason a real "controlled input" (React's own term for this)
112
+ // always writes value on every render rather than diffing it.
113
+ realNode.value = updated.Value;
114
+
115
+ // Same-length is the overwhelmingly common case (the same Render()
116
+ // code path calls SetAttr the same number of times, in the same
117
+ // order, on every call) — compare aligned by index and only touch
118
+ // the real DOM for an entry that actually changed, rather than
119
+ // reapplying every extra attribute on every patch regardless. A
120
+ // length mismatch (the rarer case: a SetAttr call was added,
121
+ // removed, or made conditional between renders) falls back to
122
+ // reapplying everything, since index-aligned comparison isn't
123
+ // meaningful once the two arrays don't correspond entry-for-entry.
124
+ if (updated.ExtraNames.Length == oldTree.ExtraNames.Length) {
125
+ for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {
126
+ if (updated.ExtraNames[i] != oldTree.ExtraNames[i] || updated.ExtraValues[i] != oldTree.ExtraValues[i]) {
127
+ realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);
128
+ }
129
+ }
130
+ } else {
131
+ for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {
132
+ realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);
133
+ }
134
+ }
135
+
136
+ // Swap a listener only when the handler reference actually changed.
137
+ // A node that never sets a real OnClick/OnInput/OnBlur/OnChange
138
+ // keeps VElement's own shared NoOpEventHandler reference on both
139
+ // sides (see velement.ks) — comparing by `!=` costs nothing and
140
+ // skips two real DOM API calls per event per node for the (common)
141
+ // case of "this node has no handler of this kind either time,"
142
+ // which matters a lot on a list with hundreds/thousands of rows
143
+ // most of which set at most one or two of the four. A node WITH a
144
+ // real handler still gets a fresh closure every render (it
145
+ // captures per-render values, like a loop's own item), so it still
146
+ // swaps every time — correctly, since the old closure really is
147
+ // stale.
148
+ if (updated.OnClick != oldTree.OnClick) {
149
+ realNode.removeEventListener("click", oldTree.OnClick);
150
+ realNode.addEventListener("click", updated.OnClick);
151
+ }
152
+ if (updated.OnInput != oldTree.OnInput) {
153
+ realNode.removeEventListener("input", oldTree.OnInput);
154
+ realNode.addEventListener("input", updated.OnInput);
155
+ }
156
+ if (updated.OnBlur != oldTree.OnBlur) {
157
+ realNode.removeEventListener("blur", oldTree.OnBlur);
158
+ realNode.addEventListener("blur", updated.OnBlur);
159
+ }
160
+ if (updated.OnChange != oldTree.OnChange) {
161
+ realNode.removeEventListener("change", oldTree.OnChange);
162
+ realNode.addEventListener("change", updated.OnChange);
163
+ }
164
+
165
+ return realNode;
166
+ }
167
+ } else {
168
+ // Shouldn't happen in practice (every previously-rendered tree has a
169
+ // real node by the time a second render diffs against it) — treated
170
+ // as "nothing to reuse" rather than a crash, same defensive spirit
171
+ // as Component's own IsMounted guard.
172
+ Element created = Materialize(updated);
173
+ parent.appendChild(created);
174
+ return created;
175
+ }
176
+ } else {
177
+ Element created = Materialize(updated);
178
+ parent.appendChild(created);
179
+ return created;
180
+ }
181
+ }
182
+
183
+ // Keyed reconciliation: each VElement's own Id is its key when non-empty —
184
+ // a real, existing DOM convention, needing no new API or syntax. A new
185
+ // child whose Id matches an old child's Id is patched against that old
186
+ // child (reusing its real node) regardless of position; a new child with
187
+ // no Id, or an Id not present among the old children, falls back to
188
+ // pairing positionally against whatever old children are still unconsumed,
189
+ // in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered
190
+ // list still produces the correct final output, but a given item's real
191
+ // DOM node (and anything stateful attached to it, like focus) isn't
192
+ // guaranteed to follow its data across the reorder — give list items a
193
+ // stable Id for that guarantee.
194
+ void PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {
195
+ // Map, not a Push loop — Push is deliberately non-mutating (a real
196
+ // spread-copy every call, see KopScript's own README), so building an
197
+ // n-length array by Push-ing once per element in a loop is an
198
+ // accidental O(n^2) on every single PatchChildren call, however small
199
+ // the actual diff. Map is a real, single O(n) pass straight to
200
+ // Array.prototype.map.
201
+ bool[] oldConsumed = oldChildren.Map((VElement c) => false);
202
+ number[] matchedOldIndex = newChildren.Map((VElement c) => -1);
203
+
204
+ // Pass 1: keyed matches, by Id.
205
+ for (number i = 0; i < newChildren.Length; i = i + 1) {
206
+ if (newChildren[i].Id.Length == 0) { continue; }
207
+ for (number j = 0; j < oldChildren.Length; j = j + 1) {
208
+ if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {
209
+ matchedOldIndex[i] = j;
210
+ oldConsumed[j] = true;
211
+ break;
212
+ }
213
+ }
214
+ }
215
+
216
+ // Pass 2: positional fallback for everything Pass 1 didn't match —
217
+ // pair each remaining new child against the next still-unconsumed old
218
+ // child, in order.
219
+ number nextOld = 0;
220
+ for (number i = 0; i < newChildren.Length; i = i + 1) {
221
+ if (matchedOldIndex[i] >= 0) { continue; }
222
+ while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {
223
+ nextOld = nextOld + 1;
224
+ }
225
+ if (nextOld < oldChildren.Length) {
226
+ matchedOldIndex[i] = nextOld;
227
+ oldConsumed[nextOld] = true;
228
+ nextOld = nextOld + 1;
229
+ }
230
+ }
231
+
232
+ // Whether anything is actually moving at all — same length, and every
233
+ // new position matched the *same* old position. The extremely common
234
+ // case for a list that's only had some of its rows' own content change
235
+ // (e.g. "update every 10th row"), where reconciliation still has real
236
+ // work to do (see Pass 1/2 above and Patch() itself) but nothing needs
237
+ // to physically move in the DOM at all.
238
+ bool needsReorder = oldChildren.Length != newChildren.Length;
239
+ for (number i = 0; i < newChildren.Length; i = i + 1) {
240
+ if (matchedOldIndex[i] != i) {
241
+ needsReorder = true;
242
+ break;
243
+ }
244
+ }
245
+
246
+ // Pass 3: patch/create each new child in order, then — only if the list
247
+ // actually needs reordering — move it into its correct final position.
248
+ // appendChild on a node already attached elsewhere in the DOM MOVES it
249
+ // (real DOM semantics), so processing new children in their final
250
+ // desired order and always appending naturally builds up the correct
251
+ // sequence, no separate insertBefore/reference-node bookkeeping needed.
252
+ // Safe because VElement.Children is always the COMPLETE list of a
253
+ // node's children — nothing else ever shares `parent`. Skipping the
254
+ // move entirely when `needsReorder` is false avoids a real DOM API call
255
+ // per child for the common no-reorder case — Patch() itself already
256
+ // updates or replaces a reused/changed node exactly in place either way.
257
+ for (number i = 0; i < newChildren.Length; i = i + 1) {
258
+ VElement? matchedOld = null;
259
+ if (matchedOldIndex[i] >= 0) {
260
+ matchedOld = oldChildren[matchedOldIndex[i]];
261
+ }
262
+ Element childNode = Patch(parent, matchedOld, newChildren[i]);
263
+ if (needsReorder) {
264
+ parent.appendChild(childNode);
265
+ }
266
+ }
267
+
268
+ // Pass 4: remove whatever old children never got reused.
269
+ for (number j = 0; j < oldChildren.Length; j = j + 1) {
270
+ if (oldConsumed[j]) { continue; }
271
+ Element? maybeOldNode = oldChildren[j].RealNode;
272
+ if (maybeOldNode != null) {
273
+ Element oldNode = maybeOldNode;
274
+ parent.removeChild(oldNode);
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,37 @@
1
+ import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
2
+
3
+ export function NoOpEventHandler(e) {
4
+ }
5
+ export class VElement {
6
+ constructor(tag) {
7
+ this.Tag = tag;
8
+ this.TextContent = "";
9
+ this.ClassName = "";
10
+ this.Id = "";
11
+ this.Value = "";
12
+ this.Children = [];
13
+ this.RawHtml = "";
14
+ this.OnClick = NoOpEventHandler;
15
+ this.OnInput = NoOpEventHandler;
16
+ this.OnBlur = NoOpEventHandler;
17
+ this.OnChange = NoOpEventHandler;
18
+ this.ExtraNames = [];
19
+ this.ExtraValues = [];
20
+ this.RealNode = null;
21
+ }
22
+
23
+ static Create(tag) {
24
+ return new VElement(tag);
25
+ }
26
+
27
+ AppendChild(child) {
28
+ this.Children = [...this.Children, child];
29
+ }
30
+
31
+ SetAttr(name, value) {
32
+ this.ExtraNames = [...this.ExtraNames, name];
33
+ this.ExtraValues = [...this.ExtraValues, value];
34
+ }
35
+ }
36
+
37
+ //# 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// A single shared no-op, not a fresh closure per VElement — every instance\n// that never sets a real handler gets this exact same function reference,\n// so Patch() (vdom.ks) can tell \"no handler either time\" apart from \"a\n// handler changed\" with a plain `!=` reference check instead of always\n// removing/re-adding all four DOM listeners on every patch regardless of\n// whether anything about them actually changed. A fresh `(Event e) => {}`\n// closure per instance would defeat this — two \"empty\" handlers would\n// never compare equal, even when nothing meaningful differs.\nvoid NoOpEventHandler(Event e) {\n}\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 = NoOpEventHandler;\n this.OnInput = NoOpEventHandler;\n this.OnBlur = NoOpEventHandler;\n this.OnChange = NoOpEventHandler;\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":";;AAiBA;;AASA;EA6CE;IACW;IACQ;IACF;IACP;IACG;IACG;IACD;IACA;IACA;IACD;IACE;IACE;IACC;IACH;;;EAGF;IACZ;;;EAGK;IACS;;;EAQT;IACW;IACC"}
@@ -0,0 +1,106 @@
1
+ using "./dom";
2
+
3
+ // A lightweight, framework-owned description of one DOM element — Render()
4
+ // builds a tree of these instead of real DOM nodes, so Update() can DIFF
5
+ // the new tree against the previous one and patch only what changed,
6
+ // instead of discarding and rebuilding the whole real DOM subtree every
7
+ // time (see src/vdom.ks for the diff/patch engine, src/component.ks for
8
+ // where Render()'s return type changed from Element to VElement).
9
+ //
10
+ // A single shared no-op, not a fresh closure per VElement — every instance
11
+ // that never sets a real handler gets this exact same function reference,
12
+ // so Patch() (vdom.ks) can tell "no handler either time" apart from "a
13
+ // handler changed" with a plain `!=` reference check instead of always
14
+ // removing/re-adding all four DOM listeners on every patch regardless of
15
+ // whether anything about them actually changed. A fresh `(Event e) => {}`
16
+ // closure per instance would defeat this — two "empty" handlers would
17
+ // never compare equal, even when nothing meaningful differs.
18
+ void NoOpEventHandler(Event e) {
19
+ }
20
+
21
+ // Fixed, named fields — not a generic prop bag — because KopScript has no
22
+ // object-literal syntax to build one with. Fixed, named event slots — not
23
+ // an array of handlers — because KopScript has no array-of-function-values
24
+ // type either. Both are real language constraints, not an oversight; see
25
+ // SetAttr below for the escape hatch covering everything not common enough
26
+ // to deserve its own named field.
27
+ class VElement {
28
+ public string Tag;
29
+ // Mutually exclusive with Children and RawHtml — set at most one of the
30
+ // three. TextContent/Children mirrors the same "no mixed text/element
31
+ // content" rule KopScript's own templates already enforce.
32
+ public string TextContent;
33
+ public string ClassName;
34
+ public string Id;
35
+ // The one property patched via direct assignment, never setAttribute —
36
+ // see Element.value's own comment in dom.ks for why (the "default value
37
+ // attribute" vs "current live value property" DOM footgun — the exact
38
+ // property behind the original typing bug this whole effort traces to).
39
+ public string Value;
40
+ public VElement[] Children;
41
+ // An opaque, undiffed leaf — set instead of TextContent/Children for the
42
+ // existing raw-HTML-then-wire-handlers pattern (header.ks/nav.ks). The
43
+ // patch engine treats two VElements with different RawHtml as a single
44
+ // innerHTML assignment, never recursing inside it — the same "opaque
45
+ // blob" treatment `raw string` already gets everywhere else.
46
+ public string RawHtml;
47
+
48
+ // Each defaults to a real no-op, never null — KopScript has no nullable
49
+ // *function* type to fall back on for "no handler set" (see Router.Guard
50
+ // for the same default-real-function pattern already established).
51
+ public (Event) => void OnClick;
52
+ public (Event) => void OnInput;
53
+ public (Event) => void OnBlur;
54
+ public (Event) => void OnChange;
55
+
56
+ // A real HTML attribute not common enough for its own named field (href,
57
+ // src, alt, placeholder, ...) — parallel arrays, since KopScript has no
58
+ // Dictionary type. Never Value (see its own field comment above). Public,
59
+ // read directly by the patch engine (src/vdom.ks) rather than through
60
+ // accessor methods — plain data, same style as this codebase's other
61
+ // plain classes (Note, Dog, ...).
62
+ public string[] ExtraNames;
63
+ public string[] ExtraValues;
64
+
65
+ // Set only once this VElement has been materialized into (or reused as)
66
+ // a real DOM node — null on a freshly-built tree from a not-yet-patched
67
+ // Render() call. The patch engine reads the *previous* render's tree's
68
+ // RealNode to know what to reuse/patch; it never reads the live DOM back
69
+ // to rediscover this (see vdom.ks's own header comment for why).
70
+ public Element? RealNode;
71
+
72
+ constructor(string tag) {
73
+ this.Tag = tag;
74
+ this.TextContent = "";
75
+ this.ClassName = "";
76
+ this.Id = "";
77
+ this.Value = "";
78
+ this.Children = [];
79
+ this.RawHtml = "";
80
+ this.OnClick = NoOpEventHandler;
81
+ this.OnInput = NoOpEventHandler;
82
+ this.OnBlur = NoOpEventHandler;
83
+ this.OnChange = NoOpEventHandler;
84
+ this.ExtraNames = [];
85
+ this.ExtraValues = [];
86
+ this.RealNode = null;
87
+ }
88
+
89
+ public static VElement Create(string tag) {
90
+ return new VElement(tag);
91
+ }
92
+
93
+ public void AppendChild(VElement child) {
94
+ this.Children = this.Children.Push(child);
95
+ }
96
+
97
+ // Last call for a given name wins if SetAttr is called more than once
98
+ // with the same name on one VElement — the patch engine applies
99
+ // ExtraNames/ExtraValues in order, so a later entry's setAttribute call
100
+ // simply overwrites an earlier one for the same name, no special
101
+ // dedup/replace logic needed here.
102
+ public void SetAttr(string name, string value) {
103
+ this.ExtraNames = this.ExtraNames.Push(name);
104
+ this.ExtraValues = this.ExtraValues.Push(value);
105
+ }
106
+ }