@ryupold/vode 0.9.6 → 0.11.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.
@@ -6,17 +6,13 @@ on:
6
6
  - 'main'
7
7
 
8
8
  jobs:
9
- build:
9
+ test-build-and-publish-to-npm:
10
10
  runs-on: ubuntu-latest
11
11
  permissions:
12
12
  contents: read
13
13
  id-token: write
14
14
  steps:
15
15
  - uses: actions/checkout@v4
16
- - uses: actions/setup-node@v4
17
- with:
18
- node-version: '22.x'
19
- registry-url: 'https://registry.npmjs.org'
20
16
  - uses: oven-sh/setup-bun@v2
21
17
  - run: bun run build
22
18
  - run: |
package/README.md CHANGED
@@ -7,7 +7,14 @@ The state is a singleton object that can be updated, and the UI will re-render w
7
7
  ## Install
8
8
 
9
9
  ```bash
10
+ # npm
10
11
  npm install @ryupold/vode --save
12
+
13
+ # yarn
14
+ yarn add @ryupold/vode
15
+
16
+ # bun
17
+ bun add @ryupold/vode
11
18
  ```
12
19
 
13
20
  ## Patch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "0.9.6",
3
+ "version": "0.11.0",
4
4
  "description": "Small web framework for minimal websites",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
@@ -22,7 +22,8 @@
22
22
  "homepage": "https://github.com/ryupold/vode#readme",
23
23
  "module": "index.ts",
24
24
  "scripts": {
25
- "build": "bun build index.ts --outfile index.js",
25
+ "build": "bun build index.ts --outfile vode.mjs",
26
+ "build-min": "bun build index.ts --outfile vode.min.mjs --minify",
26
27
  "pack": "rm *.tgz && bun run build && bun pm pack",
27
28
  "publish": "bun publish --provenance --access public",
28
29
  "clean": "tsc -b --clean",
package/src/vode.ts CHANGED
@@ -12,7 +12,7 @@ export type Component<S> = (s: S) => ChildVode<S>;
12
12
  export type Patch<S> =
13
13
  | NoRenderPatch // ignored
14
14
  | typeof EmptyPatch | DeepPartial<S> // render patches
15
- | Promise<Patch<S>> | Effect<S> // effects resulting in patches
15
+ | Promise<Patch<S>> | Effect<S>; // effects resulting in patches
16
16
 
17
17
  export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
18
18
  export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
@@ -28,9 +28,6 @@ export type Effect<S> =
28
28
 
29
29
  export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
30
30
 
31
- export type Dispatch<S> = (action: Patch<S>) => void;
32
- export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
33
-
34
31
  export type Props<S> = Partial<
35
32
  Omit<HTMLElement,
36
33
  keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
@@ -48,51 +45,66 @@ export type Props<S> = Partial<
48
45
  export type MountFunction<S> =
49
46
  | ((s: S, node: HTMLElement) => Patch<S>)
50
47
  | ((s: S, node: SVGSVGElement) => Patch<S>)
51
- | ((s: S, node: MathMLElement) => Patch<S>)
48
+ | ((s: S, node: MathMLElement) => Patch<S>);
52
49
 
53
50
  export type ClassProp =
54
51
  | "" | false | null | undefined // no class
55
52
  | string // "class1 class2"
56
53
  | string[] // ["class1", "class2"]
57
- | Record<string, boolean | undefined | null> // { class1: true, class2: false }
54
+ | Record<string, boolean | undefined | null>; // { class1: true, class2: false }
58
55
 
59
56
  export type StyleProp = Record<number, never> & {
60
57
  [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
61
- }
58
+ };
62
59
 
63
60
  export type EventsMap =
64
61
  & { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
65
62
  & { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
66
63
  & { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
67
- & { onsearch: Event }
64
+ & { onsearch: Event };
68
65
 
69
66
  export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
70
67
 
68
+
69
+ export type Dispatch<S> = (action: Patch<S>) => void;
70
+ export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
71
+
71
72
  export type ContainerNode<S> = HTMLElement & {
72
- state: PatchableState<S>,
73
- vode: AttachedVode<S>,
74
- patch: Dispatch<S>,
75
- render: () => void,
76
- q: Patch<S>[]
77
- isRendering: boolean,
78
- stats: {
79
- patchCount: number,
80
- liveEffectCount: number,
81
- renderPatchCount: number,
82
- renderCount: number,
83
- renderTime: number,
84
- queueLengthBeforeRender: number,
85
- queueLengthAfterRender: number,
86
- },
73
+ /** the `_vode` property is added to the container in `app()`.
74
+ * it contains all necessary stuff for the vode app to function.
75
+ * delete it to clear all resources of the vode app, or remove the container itself */
76
+ _vode: {
77
+ state: PatchableState<S>, // can touch this, but let it be an object
78
+ vode: AttachedVode<S>, //don't touch this
79
+ patch: Dispatch<S>, // can't touch this
80
+ render: () => void, // can't touch this
81
+ q: Patch<S>[], // this will change in the future, so don't touch it
82
+ isRendering: boolean, // under no circumstances touch this
83
+ /** stats about the overall patches & last render time */
84
+ stats: {
85
+ patchCount: number,
86
+ liveEffectCount: number,
87
+ renderPatchCount: number,
88
+ renderCount: number,
89
+ renderTime: number,
90
+ queueLengthBeforeRender: number,
91
+ queueLengthAfterRender: number,
92
+ },
93
+ }
87
94
  };
88
95
 
96
+ /** create a state object used as initial state for `app()`. it is updated with `PatchableState.patch()` using `merge()` */
97
+ export function createState<S extends object | unknown>(state: S): PatchableState<S> { return state as PatchableState<S>; }
98
+
99
+ /** type safe way to create a patch. useful for type inference and autocompletion. */
100
+ export function createPatch<S extends object | unknown>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): Patch<S> { return p; }
101
+
89
102
  /** type-safe way to create a vode. useful for type inference and autocompletion.
90
103
  *
91
- * overloads:
92
- * - just a tag: `vode("div") // => ["div"]`
93
- * - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
94
- * - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
95
- * - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
104
+ * - just a tag: `vode("div")` => `["div"]` --*rendered*-> `<div></div>`
105
+ * - tag and props: `vode("div", { class: "foo" })` => `["div", { class: "foo" }]` --*rendered*-> `<div class="foo"></div>`
106
+ * - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"])` => `["div", { class: "foo" }, ["span", "bar"]]` --*rendered*-> `<div class="foo"><span>bar</span></div>`
107
+ * - identity: `vode(["div", ["span", "bar"]])` => `["div", ["span", "bar"]]` --*rendered*-> `<div><span>bar</span></div>`
96
108
  */
97
109
  export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
98
110
  if (Array.isArray(tag)) {
@@ -104,118 +116,129 @@ export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Pro
104
116
  return [tag, ...children];
105
117
  }
106
118
 
107
- /** pass an object whose type determines the initial state */
108
- export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
109
-
110
-
111
- /** for a type safe way to create a deeply partial patch object or effect */
112
- export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
113
-
114
- /**
115
- * create a vode app inside a container element
119
+ /** create a vode app inside a container element
116
120
  * @param container will use this container as root and places the result of the dom function and further renderings in it
117
- * @param initialState @see createState
121
+ * @param initialState
118
122
  * @param dom creates the initial dom from the state and is called on every render
119
123
  * @param initialPatches variadic list of patches that are applied after the first render
120
124
  * @returns a patch function that can be used to update the state
121
125
  */
122
- export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
123
- const root = container as ContainerNode<S>;
124
- root.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
126
+ export function app<S extends object | unknown>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
127
+ const _vode = {} as ContainerNode<S>["_vode"];
128
+ _vode.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
125
129
 
126
130
  Object.defineProperty(initialState, "patch", {
127
131
  enumerable: false, configurable: true,
128
132
  writable: false, value: async (action: Patch<S>) => {
129
133
  if (!action || (typeof action !== "function" && typeof action !== "object")) return;
130
- root.stats.patchCount++;
134
+ _vode.stats.patchCount++;
131
135
 
132
136
  if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
133
137
  const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
134
- root.stats.liveEffectCount++;
138
+ _vode.stats.liveEffectCount++;
135
139
  try {
136
140
  let v = await generator.next();
137
141
  while (v.done === false) {
138
- root.stats.liveEffectCount++;
142
+ _vode.stats.liveEffectCount++;
139
143
  try {
140
- root.patch!(v.value);
144
+ _vode.patch!(v.value);
141
145
  v = await generator.next();
142
146
  } finally {
143
- root.stats.liveEffectCount--;
147
+ _vode.stats.liveEffectCount--;
144
148
  }
145
149
  }
146
- root.patch!(v.value as Patch<S>);
150
+ _vode.patch!(v.value as Patch<S>);
147
151
  } finally {
148
- root.stats.liveEffectCount--;
152
+ _vode.stats.liveEffectCount--;
149
153
  }
150
154
  } else if ((action as Promise<S>).then) {
151
- root.stats.liveEffectCount++;
155
+ _vode.stats.liveEffectCount++;
152
156
  try {
153
157
  const nextState = await (action as Promise<S>);
154
- root.patch!(<Patch<S>>nextState);
158
+ _vode.patch!(<Patch<S>>nextState);
155
159
  } finally {
156
- root.stats.liveEffectCount--;
160
+ _vode.stats.liveEffectCount--;
157
161
  }
158
162
  } else if (Array.isArray(action)) {
159
163
  if (typeof action[0] === "function") {
160
164
  if (action.length > 1)
161
- root.patch!(action[0](root.state!, ...(action as any[]).slice(1)));
162
- else root.patch!(action[0](root.state!));
165
+ _vode.patch!(action[0](_vode.state!, ...(action as any[]).slice(1)));
166
+ else _vode.patch!(action[0](_vode.state!));
163
167
  } else {
164
- root.stats.patchCount--;
168
+ _vode.stats.patchCount--;
165
169
  }
166
170
  } else if (typeof action === "function") {
167
- root.patch!((<EffectFunction<S>>action)(root.state));
171
+ _vode.patch!((<EffectFunction<S>>action)(_vode.state));
168
172
  } else {
169
- root.stats.renderPatchCount++;
170
- root.q!.push(<Patch<S>>action);
171
- if (!root.isRendering) root.render!();
173
+ _vode.stats.renderPatchCount++;
174
+ _vode.q!.push(<Patch<S>>action);
175
+ if (!_vode.isRendering) _vode.render!();
172
176
  }
173
177
  }
174
178
  });
175
179
 
176
- Object.defineProperty(root, "render", {
180
+ Object.defineProperty(_vode, "render", {
177
181
  enumerable: false, configurable: true,
178
182
  writable: false, value: () => requestAnimationFrame(() => {
179
- if (root.isRendering || root.q!.length === 0) return;
180
- root.isRendering = true;
183
+ if (_vode.isRendering || _vode.q!.length === 0) return;
184
+ _vode.isRendering = true;
181
185
  const sw = Date.now();
182
186
  try {
183
- root.stats.queueLengthBeforeRender = root.q!.length;
187
+ _vode.stats.queueLengthBeforeRender = _vode.q!.length;
184
188
 
185
- while (root.q!.length > 0) {
186
- const patch = root.q!.shift();
187
- if(patch === EmptyPatch) continue;
188
- mergeState(root.state, patch);
189
+ while (_vode.q!.length > 0) {
190
+ const patch = _vode.q!.shift();
191
+ if (patch === EmptyPatch) continue;
192
+ mergeState(_vode.state, patch);
189
193
  }
190
- root.vode = render(root.state, root.patch, container, 0, root.vode, dom(root.state))!;
194
+ _vode.vode = render(_vode.state, _vode.patch, container, 0, _vode.vode, dom(_vode.state))!;
191
195
  } finally {
192
- root.isRendering = false;
193
- root.stats.renderCount++;
194
- root.stats.renderTime = Date.now() - sw;
195
- root.stats.queueLengthAfterRender = root.q!.length;
196
- if (root.q!.length > 0) {
197
- root.render!();
196
+ _vode.isRendering = false;
197
+ _vode.stats.renderCount++;
198
+ _vode.stats.renderTime = Date.now() - sw;
199
+ _vode.stats.queueLengthAfterRender = _vode.q!.length;
200
+ if (_vode.q!.length > 0) {
201
+ _vode.render!();
198
202
  }
199
203
  }
200
204
  })
201
205
  });
202
206
 
203
- root.patch = (<PatchableState<S>>initialState).patch;
204
- root.state = <PatchableState<S>>initialState;
205
- root.q = [];
207
+ _vode.patch = (<PatchableState<S>>initialState).patch;
208
+ _vode.state = <PatchableState<S>>initialState;
209
+ _vode.q = [];
210
+
211
+ const root = container as ContainerNode<S>;
212
+ root._vode = _vode;
213
+
206
214
  const initialVode = dom(<S>initialState);
207
- root.vode = <AttachedVode<S>>initialVode;
208
- root.vode = render(<S>initialState, root.patch!, container, 0, undefined, initialVode)!;
215
+ _vode.vode = <AttachedVode<S>>initialVode;
216
+ _vode.vode = render(<S>initialState, _vode.patch!, container, 0, undefined, initialVode)!;
209
217
 
210
218
  for (const effect of initialPatches) {
211
- root.patch!(effect);
219
+ _vode.patch!(effect);
212
220
  }
213
221
 
214
- return root.patch;
222
+ return _vode.patch;
223
+ }
224
+
225
+ /** memoizes the resulting component or props by comparing element by element (===) with the
226
+ * `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)*/
227
+ export function memo<S>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
228
+ (<any>componentOrProps).__memo = compare;
229
+ return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
230
+ }
231
+
232
+ /** html tag of the vode or `#text` if it is a text node */
233
+ export function tag<S>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
234
+ return !!v ? (Array.isArray(v)
235
+ ? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
236
+ ? "#text" : undefined) as Tag
237
+ : undefined;
215
238
  }
216
239
 
217
- /** get properties of a vode, if there are any */
218
- export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
240
+ /** get properties object of a vode, if there is any */
241
+ export function props<S>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
219
242
  if (Array.isArray(vode)
220
243
  && vode.length > 1
221
244
  && vode[1]
@@ -232,6 +255,7 @@ export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedV
232
255
  return undefined;
233
256
  }
234
257
 
258
+ /** merge `ClassProp`s regardless of structure */
235
259
  export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
236
260
  if (!a) return b;
237
261
  if (!b) return a;
@@ -282,48 +306,14 @@ export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
282
306
  throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
283
307
  }
284
308
 
285
- export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
286
- if (!Array.isArray(vode)) return;
287
-
288
- if (vode.length > 1) {
289
- if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
290
- vode[1] = merge(vode[1], props);
291
- return;
292
- }
293
-
294
- if (childCount(vode) > 0) {
295
- (<FullVode<S>>vode).push(null);
296
- }
297
- for (let i = vode.length - 1; i > 0; i--) {
298
- if (i > 1) vode[i] = vode[i - 1];
299
- }
300
- vode[1] = props;
301
- } else {
302
- (<FullVode<S>>vode).push(props);
303
- }
304
- }
305
-
306
309
  /** get a slice of all children of a vode, if there are any */
307
- export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
310
+ export function children<S>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | null {
308
311
  const start = childrenStart(vode);
309
312
  if (start > 0) {
310
313
  return (<Vode<S>>vode).slice(start) as Vode<S>[];
311
314
  }
312
315
 
313
- return undefined;
314
- }
315
-
316
- /** index in vode at which child-vodes start */
317
- export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
318
- return props(vode) ? 2 : 1;
319
- }
320
-
321
- /** html tag of the vode or #text if it is a text node */
322
- export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
323
- return !!v ? (Array.isArray(v)
324
- ? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
325
- ? "#text" : undefined) as Tag
326
- : undefined;
316
+ return null;
327
317
  }
328
318
 
329
319
  export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
@@ -332,11 +322,13 @@ export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined
332
322
  return vode[index + childrenStart(vode)] as ChildVode<S>;
333
323
  }
334
324
 
335
- /** merge multiple objects into one, applying from left to right
336
- * @param first object to merge
337
- * @returns merged object
338
- */
339
- export function merge(first?: any, ...p: any[]): any {
325
+ /** index in vode at which child-vodes start */
326
+ export function childrenStart<S>(vode: ChildVode<S> | AttachedVode<S>): number {
327
+ return props(vode) ? 2 : 1;
328
+ }
329
+
330
+ /** @returns multiple merged objects as one, applying from left to right ({}, first, ...p) */
331
+ export function merge(first?: object | unknown, ...p: (object | unknown)[]): object {
340
332
  first = mergeState({}, first);
341
333
  for (const pp of p) {
342
334
  if (!pp) continue;
@@ -345,66 +337,40 @@ export function merge(first?: any, ...p: any[]): any {
345
337
  return first!;
346
338
  }
347
339
 
348
- function classString(classProp: ClassProp): string {
349
- if (typeof classProp === "string") {
350
- return classProp;
351
- } else if (Array.isArray(classProp)) {
352
- return classProp.map(classString).join(" ");
353
- } else if (typeof classProp === "object") {
354
- return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
355
- } else {
356
- return "";
357
- }
358
- }
359
-
360
- function isNaturalVode(x: ChildVode<any>) {
361
- return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
362
- }
363
-
364
- function isTextVode(x: ChildVode<any>) {
365
- return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
366
- }
367
-
368
- function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
369
- if (typeof c === "function") {
370
- return unwrap(c(s), s);
371
- } else {
372
- return c;
373
- }
374
- }
375
-
376
- /** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
377
- export function memo<S extends object | unknown>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
378
- (<any>componentOrProps).__memo = compare;
379
- return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
380
- }
381
-
382
- function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
383
- if (typeof present !== "function")
384
- return present;
385
-
386
- const presentMemo = present?.__memo;
387
- const pastMemo = past?.__memo;
340
+ function mergeState(target: any, source: any) {
341
+ if (!source) return target;
388
342
 
389
- if (Array.isArray(presentMemo)
390
- && Array.isArray(pastMemo)
391
- && presentMemo.length === pastMemo.length
392
- ) {
393
- let same = true;
394
- for (let i = 0; i < presentMemo.length; i++) {
395
- if (presentMemo[i] !== pastMemo[i]) {
396
- same = false;
397
- break;
343
+ for (const key in source) {
344
+ const value = source[key];
345
+ if (value && typeof value === "object") {
346
+ const targetValue = target[key];
347
+ if (targetValue) {
348
+ if (Array.isArray(value)) {
349
+ target[key] = [...value];
350
+ } else if (value instanceof Date && targetValue !== value) {
351
+ target[key] = new Date(value);
352
+ } else {
353
+ if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
354
+ else if (typeof targetValue === "object") mergeState(target[key], value);
355
+ else target[key] = mergeState({}, value);
356
+ }
357
+ } else if (Array.isArray(value)) {
358
+ target[key] = [...value];
359
+ } else if (value instanceof Date) {
360
+ target[key] = new Date(value);
361
+ } else {
362
+ target[key] = mergeState({}, value);
398
363
  }
399
364
  }
400
- if (same) return past;
401
- }
402
- const newRender = unwrap(present, state);
403
- if (typeof newRender === "object") {
404
- (<any>newRender).__memo = present?.__memo;
365
+ else if (value === undefined) {
366
+ delete target[key];
367
+ }
368
+ else {
369
+ target[key] = value;
370
+ }
405
371
  }
406
- return newRender;
407
- }
372
+ return target;
373
+ };
408
374
 
409
375
  function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, svg?: boolean): AttachedVode<S> | undefined {
410
376
  // unwrap component if it is memoized
@@ -561,6 +527,49 @@ function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex:
561
527
  return undefined;
562
528
  }
563
529
 
530
+ function isNaturalVode(x: ChildVode<any>) {
531
+ return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
532
+ }
533
+
534
+ function isTextVode(x: ChildVode<any>) {
535
+ return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
536
+ }
537
+
538
+ function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
539
+ if (typeof present !== "function")
540
+ return present;
541
+
542
+ const presentMemo = present?.__memo;
543
+ const pastMemo = past?.__memo;
544
+
545
+ if (Array.isArray(presentMemo)
546
+ && Array.isArray(pastMemo)
547
+ && presentMemo.length === pastMemo.length
548
+ ) {
549
+ let same = true;
550
+ for (let i = 0; i < presentMemo.length; i++) {
551
+ if (presentMemo[i] !== pastMemo[i]) {
552
+ same = false;
553
+ break;
554
+ }
555
+ }
556
+ if (same) return past;
557
+ }
558
+ const newRender = unwrap(present, state);
559
+ if (typeof newRender === "object") {
560
+ (<any>newRender).__memo = present?.__memo;
561
+ }
562
+ return newRender;
563
+ }
564
+
565
+ function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
566
+ if (typeof c === "function") {
567
+ return unwrap(c(s), s);
568
+ } else {
569
+ return c;
570
+ }
571
+ }
572
+
564
573
  function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
565
574
  if (!newProps && !oldProps) return;
566
575
  if (!oldProps) { // set new props
@@ -658,37 +667,14 @@ function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | key
658
667
  return newValue;
659
668
  }
660
669
 
661
- function mergeState(target: any, source: any) {
662
- if (!source) return target;
663
-
664
- for (const key in source) {
665
- const value = source[key];
666
- if (value && typeof value === "object") {
667
- const targetValue = target[key];
668
- if (targetValue) {
669
- if (Array.isArray(value)) {
670
- target[key] = [...value];
671
- } else if (value instanceof Date && targetValue !== value) {
672
- target[key] = new Date(value);
673
- } else {
674
- if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
675
- else if (typeof targetValue === "object") mergeState(target[key], value);
676
- else target[key] = mergeState({}, value);
677
- }
678
- } else if (Array.isArray(value)) {
679
- target[key] = [...value];
680
- } else if (value instanceof Date) {
681
- target[key] = new Date(value);
682
- } else {
683
- target[key] = mergeState({}, value);
684
- }
685
- }
686
- else if (value === undefined) {
687
- delete target[key];
688
- }
689
- else {
690
- target[key] = value;
691
- }
670
+ function classString(classProp: ClassProp): string {
671
+ if (typeof classProp === "string") {
672
+ return classProp;
673
+ } else if (Array.isArray(classProp)) {
674
+ return classProp.map(classString).join(" ");
675
+ } else if (typeof classProp === "object") {
676
+ return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
677
+ } else {
678
+ return "";
692
679
  }
693
- return target;
694
- };
680
+ }