@oscarpalmer/abydon 0.5.0 → 0.6.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/dist/index.js CHANGED
@@ -15,7 +15,27 @@ var isNullableOrWhitespace = function(value) {
15
15
  return value == null || /^\s*$/.test(getString(value));
16
16
  };
17
17
 
18
+ // src/helpers/index.ts
19
+ function isFragment(value) {
20
+ return typeof value === "object" && value != null && "$fragment" in value && value.$fragment === true;
21
+ }
22
+ function isFragmentElement(value) {
23
+ return value instanceof HTMLElement || value instanceof SVGElement;
24
+ }
25
+ function isFragmentNode(value) {
26
+ return value instanceof Comment || value instanceof Element || value instanceof Text;
27
+ }
28
+
18
29
  // src/helpers/dom.ts
30
+ function createNodes(value) {
31
+ if (isFragmentNode(value)) {
32
+ return [value];
33
+ }
34
+ if (isFragment(value)) {
35
+ return value.get();
36
+ }
37
+ return [new Text(getString(value))];
38
+ }
19
39
  function createTemplate(html) {
20
40
  const template = document.createElement("template");
21
41
  template.innerHTML = html;
@@ -33,6 +53,17 @@ function createTemplate(html) {
33
53
  function getNodes(node) {
34
54
  return /^documentfragment$/i.test(node.constructor.name) ? [...node.childNodes] : [node];
35
55
  }
56
+ function replaceNodes(from, to) {
57
+ const { length } = from;
58
+ for (let index = 0;index < length; index += 1) {
59
+ const node = from[index];
60
+ if (index === 0) {
61
+ node.replaceWith(...to);
62
+ } else {
63
+ node.remove();
64
+ }
65
+ }
66
+ }
36
67
 
37
68
  // node_modules/@oscarpalmer/sentinel/node_modules/@oscarpalmer/atoms/dist/js/queue.mjs
38
69
  if (globalThis._atomic_queued == null) {
@@ -54,6 +85,45 @@ if (globalThis._sentinels == null) {
54
85
  });
55
86
  }
56
87
 
88
+ // node_modules/@oscarpalmer/sentinel/dist/effect.mjs
89
+ var effect = function(callback) {
90
+ const state = {
91
+ callback,
92
+ active: false,
93
+ reactives: new Set
94
+ };
95
+ const instance = Object.create({
96
+ start() {
97
+ if (!state.active) {
98
+ state.active = true;
99
+ const index = globalThis._sentinels.push(state) - 1;
100
+ state.callback();
101
+ globalThis._sentinels.splice(index, 1);
102
+ }
103
+ },
104
+ stop() {
105
+ if (state.active) {
106
+ state.active = false;
107
+ for (const reactive of state.reactives) {
108
+ reactive.callbacks.any.delete(state);
109
+ for (const [key, keyed] of reactive.callbacks.values) {
110
+ keyed.delete(state);
111
+ if (keyed.size === 0) {
112
+ reactive.callbacks.keys.delete(key);
113
+ }
114
+ }
115
+ }
116
+ state.reactives.clear();
117
+ }
118
+ }
119
+ });
120
+ Object.defineProperty(instance, "$sentinel", {
121
+ value: "effect"
122
+ });
123
+ instance.start();
124
+ return instance;
125
+ };
126
+
57
127
  // node_modules/@oscarpalmer/sentinel/dist/helpers/is.mjs
58
128
  var isReactive = function(value) {
59
129
  return isSentinel(value, /^array|computed|signal|store$/i);
@@ -78,77 +148,317 @@ var arrayOperations = new Set([
78
148
  // node_modules/@oscarpalmer/sentinel/dist/reactive/index.mjs
79
149
  var primitives = new Set(["boolean", "number", "string"]);
80
150
 
151
+ // src/node/value.ts
152
+ function setReactiveNode(comment, reactive3) {
153
+ const text = new Text;
154
+ let nodes;
155
+ effect(() => {
156
+ const value10 = reactive3.get();
157
+ if (isFragment(value10) || isFragmentNode(value10)) {
158
+ const next = createNodes(value10);
159
+ if (nodes == null) {
160
+ if (comment.parentNode != null) {
161
+ replaceNodes([comment], next);
162
+ } else if (text.parentNode != null) {
163
+ replaceNodes([text], next);
164
+ }
165
+ } else {
166
+ replaceNodes(nodes, next);
167
+ }
168
+ nodes = next;
169
+ return;
170
+ }
171
+ const isNullable = isNullableOrWhitespace(value10);
172
+ text.textContent = getString(value10);
173
+ if (nodes != null) {
174
+ replaceNodes(nodes, [isNullable ? comment : text]);
175
+ } else if (isNullable && comment.parentNode == null) {
176
+ text.replaceWith(comment);
177
+ } else if (!isNullable && text.parentNode == null) {
178
+ comment.replaceWith(text);
179
+ }
180
+ nodes = undefined;
181
+ });
182
+ }
183
+
184
+ // src/node/event.ts
185
+ function mapEvent(element, name, value10) {
186
+ element.removeAttribute(name);
187
+ if (typeof value10 !== "function") {
188
+ return;
189
+ }
190
+ }
191
+
192
+ // node_modules/@oscarpalmer/atoms/dist/js/element/closest.mjs
193
+ var closest = function(origin, selector, context) {
194
+ const elements = [...(context ?? document).querySelectorAll(selector)];
195
+ const { length } = elements;
196
+ if (length === 0) {
197
+ return [];
198
+ }
199
+ const distances = [];
200
+ let minimum = null;
201
+ for (let index = 0;index < length; index += 1) {
202
+ const element = elements[index];
203
+ const distance = calculateDistance(origin, element);
204
+ if (distance < 0) {
205
+ continue;
206
+ }
207
+ if (minimum == null || distance < minimum) {
208
+ minimum = distance;
209
+ }
210
+ distances.push({
211
+ distance,
212
+ element
213
+ });
214
+ }
215
+ return minimum == null ? [] : distances.filter((found) => found.distance === minimum).map((found) => found.element);
216
+ };
217
+ var calculateDistance = function(origin, target) {
218
+ const comparison = origin.compareDocumentPosition(target);
219
+ const children = [...origin.parentElement?.children ?? []];
220
+ switch (true) {
221
+ case children.includes(target):
222
+ return Math.abs(children.indexOf(origin) - children.indexOf(target));
223
+ case !!(comparison & 2 || comparison & 8):
224
+ return traverse(origin, target);
225
+ case !!(comparison & 4 || comparison & 16):
226
+ return traverse(target, origin);
227
+ default:
228
+ return -1;
229
+ }
230
+ };
231
+ var traverse = function(from, to) {
232
+ const children = [...to.children];
233
+ if (children.includes(from)) {
234
+ return children.indexOf(from) + 1;
235
+ }
236
+ let current = from;
237
+ let distance = 0;
238
+ let parent = from.parentElement;
239
+ while (parent != null) {
240
+ if (parent === to) {
241
+ return distance + 1;
242
+ }
243
+ const children2 = [...parent.children ?? []];
244
+ if (children2.includes(to)) {
245
+ return distance + Math.abs(children2.indexOf(current) - children2.indexOf(to));
246
+ }
247
+ const index = children2.findIndex((child) => child.contains(to));
248
+ if (index > -1) {
249
+ return distance + Math.abs(index - children2.indexOf(current)) + traverse(to, children2[index]);
250
+ }
251
+ current = parent;
252
+ distance += 1;
253
+ parent = parent.parentElement;
254
+ }
255
+ return -1e6;
256
+ };
257
+ // src/node/attribute/value.ts
258
+ function setAttribute(element2, name, value10) {
259
+ element2.removeAttribute(name);
260
+ switch (true) {
261
+ case classPattern.test(name):
262
+ setClasses(element2, name, value10);
263
+ return;
264
+ case stylePattern.test(name):
265
+ setStyle(element2, name, value10);
266
+ return;
267
+ default:
268
+ setValue2(element2, name, value10);
269
+ break;
270
+ }
271
+ }
272
+ var setClasses = function(element2, name, value10) {
273
+ function update(value11) {
274
+ if (/^(|true)$/.test(getString(value11))) {
275
+ element2.classList.add(...classes);
276
+ } else {
277
+ element2.classList.remove(...classes);
278
+ }
279
+ }
280
+ const classes = name.slice(6).split(".");
281
+ if (isReactive(value10)) {
282
+ effect(() => {
283
+ update(value10.get());
284
+ });
285
+ } else {
286
+ update(value10);
287
+ }
288
+ };
289
+ var setStyle = function(element2, name, value10) {
290
+ function update(value11) {
291
+ if (value11 == null || value11 === false || value11 === true && unit == null) {
292
+ element2.style.removeProperty(property);
293
+ } else {
294
+ element2.style.setProperty(property, value11 === true ? unit : getString(value11));
295
+ }
296
+ }
297
+ const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
298
+ if (property == null) {
299
+ return;
300
+ }
301
+ if (isReactive(value10)) {
302
+ effect(() => {
303
+ update(value10.get());
304
+ });
305
+ } else {
306
+ update(value10);
307
+ }
308
+ };
309
+ var setValue2 = function(element2, name, value10) {
310
+ const callback = booleanAttributes.has(name) && name in element2 ? name === "selected" ? updateSelected(element2) : updateProperty : updateAttribute;
311
+ if (isReactive(value10)) {
312
+ effect(() => {
313
+ callback(element2, name, value10.get());
314
+ });
315
+ } else {
316
+ callback(element2, name, value10);
317
+ }
318
+ };
319
+ var triggerSelectChange = function(select) {
320
+ if (select != null) {
321
+ select.dispatchEvent(new Event("change", { bubbles: true }));
322
+ }
323
+ };
324
+ var updateAttribute = function(element2, name, value10) {
325
+ if (value10 == null) {
326
+ element2.removeAttribute(name);
327
+ } else {
328
+ element2.setAttribute(name, getString(value10));
329
+ }
330
+ };
331
+ var updateProperty = function(element2, name, value10) {
332
+ element2[name] = /^(|true)$/.test(getString(value10));
333
+ };
334
+ var updateSelected = function(element2) {
335
+ const select = closest(element2, "select")[0];
336
+ const options = [...select?.options ?? []];
337
+ return (element3, name, value10) => {
338
+ if (select != null && options.includes(element3)) {
339
+ triggerSelectChange(select);
340
+ }
341
+ updateProperty(element3, name, value10);
342
+ };
343
+ };
344
+ var booleanAttributes = new Set([
345
+ "checked",
346
+ "disabled",
347
+ "hidden",
348
+ "inert",
349
+ "multiple",
350
+ "open",
351
+ "readOnly",
352
+ "required",
353
+ "selected"
354
+ ]);
355
+ var classPattern = /^class\./;
356
+ var stylePattern = /^style\./;
357
+
358
+ // src/node/attribute/index.ts
359
+ var getValue2 = function(data2, original) {
360
+ const matches = commentExpression.exec(original ?? "");
361
+ return matches == null ? original : data2.values[+matches[1]];
362
+ };
363
+ function isBadAttribute(name, value11) {
364
+ return /^on/i.test(name) || /^(href|src|xlink:href)$/i.test(name) && /(data:text\/html|javascript:)/i.test(value11);
365
+ }
366
+ function mapAttributes(data2, element2) {
367
+ const attributes = [...element2.attributes];
368
+ const { length } = attributes;
369
+ for (let index = 0;index < length; index += 1) {
370
+ const { name, value: value11 } = attributes[index];
371
+ if (isBadAttribute(name, value11)) {
372
+ element2.removeAttribute(name);
373
+ continue;
374
+ }
375
+ const actual = getValue2(data2, value11);
376
+ if (name.startsWith("@")) {
377
+ mapEvent(element2, name, actual);
378
+ } else if (name.includes(".") || isReactive(actual)) {
379
+ mapValue(element2, name, actual);
380
+ }
381
+ }
382
+ }
383
+ var mapValue = function(element2, name, value11) {
384
+ switch (true) {
385
+ case typeof value11 === "function":
386
+ mapValue(element2, name, value11());
387
+ return;
388
+ default:
389
+ setAttribute(element2, name, value11);
390
+ break;
391
+ }
392
+ };
393
+ var commentExpression = /^<!--abydon\.(\d+)-->$/;
394
+
81
395
  // src/node/index.ts
82
- var mapNode = function(data, comment) {
83
- const matches = commentExpression.exec(comment.textContent ?? "");
84
- const value10 = matches == null ? null : data.values[+matches[1]];
85
- if (value10 != null) {
86
- mapValue(comment, value10);
396
+ var mapNode = function(data2, comment) {
397
+ const matches = commentExpression2.exec(comment.textContent ?? "");
398
+ const value12 = matches == null ? null : data2.values[+matches[1]];
399
+ if (value12 != null) {
400
+ mapValue2(comment, value12);
87
401
  }
88
402
  };
89
- function mapNodes(data, nodes) {
403
+ function mapNodes(data2, nodes) {
90
404
  const { length } = nodes;
91
405
  for (let index = 0;index < length; index += 1) {
92
406
  const node = nodes[index];
93
407
  if (node instanceof Comment) {
94
- mapNode(data, node);
408
+ mapNode(data2, node);
95
409
  continue;
96
410
  }
97
- if (node instanceof Element) {
411
+ if (isFragmentElement(node)) {
412
+ mapAttributes(data2, node);
98
413
  }
99
414
  if (node.hasChildNodes()) {
100
- mapNodes(data, [...node.childNodes]);
415
+ mapNodes(data2, [...node.childNodes]);
101
416
  }
102
417
  }
103
418
  }
104
- var mapReactive = function(comment, value10) {
105
- };
106
- var mapValue = function(comment, value10) {
419
+ var mapValue2 = function(comment, value12) {
107
420
  switch (true) {
108
- case value10 instanceof Node:
109
- comment.replaceWith(value10);
421
+ case typeof value12 === "function":
422
+ mapValue2(comment, value12());
110
423
  return;
111
- case isFragment(value10):
112
- comment.replaceWith(value10.get());
424
+ case isReactive(value12):
425
+ setReactiveNode(comment, value12);
113
426
  break;
114
- case isReactive(value10):
115
- mapReactive(comment, value10);
116
- break;
117
- case typeof value10 === "function":
118
- mapValue(comment, value10());
119
- return;
120
- case typeof value10 === "object":
121
- comment.replaceWith(new Text(getString(value10)));
427
+ default:
428
+ comment.replaceWith(...createNodes(value12));
122
429
  break;
123
430
  }
124
431
  };
125
- var commentExpression = /^abydon\.(\d+)$/;
432
+ var commentExpression2 = /^abydon\.(\d+)$/;
126
433
 
127
434
  // src/fragment.ts
128
435
  function createFragment(strings, expressions) {
129
- const data = {
436
+ const data2 = {
130
437
  expressions,
131
438
  strings,
132
439
  nodes: [],
133
440
  values: []
134
441
  };
135
442
  const instance = Object.create({
443
+ appendTo(element2) {
444
+ element2.append(...this.get());
445
+ },
136
446
  get() {
137
- if (data.nodes.length === 0) {
138
- const parsed = parse(data);
447
+ if (data2.nodes.length === 0) {
448
+ const parsed = parse2(data2);
139
449
  const templated = createTemplate(parsed);
140
- data.nodes.splice(0, data.nodes.length, ...getNodes(templated));
141
- mapNodes(data, data.nodes);
450
+ data2.nodes.splice(0, data2.nodes.length, ...getNodes(templated));
451
+ mapNodes(data2, data2.nodes);
142
452
  }
143
- return getFragment(data.nodes);
453
+ return [...data2.nodes];
144
454
  },
145
455
  remove() {
146
- const { length } = data.nodes;
456
+ const { length } = data2.nodes;
147
457
  for (let index = 0;index < length; index += 1) {
148
- const node2 = data.nodes[index];
458
+ const node2 = data2.nodes[index];
149
459
  node2.parentNode?.removeChild(node2);
150
460
  }
151
- data.nodes.splice(0, data.nodes.length);
461
+ data2.nodes.splice(0, data2.nodes.length);
152
462
  }
153
463
  });
154
464
  Object.defineProperty(instance, "$fragment", {
@@ -156,17 +466,9 @@ function createFragment(strings, expressions) {
156
466
  });
157
467
  return instance;
158
468
  }
159
- function isFragment(value10) {
160
- return typeof value10 === "object" && value10 != null && "$fragment" in value10;
161
- }
162
469
 
163
470
  // src/html.ts
164
- function getFragment(nodes) {
165
- const fragment3 = document.createDocumentFragment();
166
- fragment3.append(...nodes);
167
- return fragment3;
168
- }
169
- var handleExpression = function(data, prefix, expression) {
471
+ var handleExpression = function(data2, prefix, expression) {
170
472
  if (isNullableOrWhitespace(expression)) {
171
473
  return prefix;
172
474
  }
@@ -174,12 +476,12 @@ var handleExpression = function(data, prefix, expression) {
174
476
  const { length } = expression;
175
477
  let expressions = "";
176
478
  for (let index = 0;index < length; index += 1) {
177
- expressions += handleExpression(data, "", expression[index]);
479
+ expressions += handleExpression(data2, "", expression[index]);
178
480
  }
179
481
  return `${prefix}${expressions}`;
180
482
  }
181
483
  if (typeof expression === "object") {
182
- const index = data.values.push(expression) - 1;
484
+ const index = data2.values.push(expression) - 1;
183
485
  return `${prefix}<!--abydon.${index}-->`;
184
486
  }
185
487
  return `${prefix}${expression}`;
@@ -187,11 +489,11 @@ var handleExpression = function(data, prefix, expression) {
187
489
  function html2(strings, ...values) {
188
490
  return createFragment(strings, values);
189
491
  }
190
- function parse(data) {
191
- const { length } = data.strings;
492
+ function parse2(data2) {
493
+ const { length } = data2.strings;
192
494
  let template = "";
193
495
  for (let index = 0;index < length; index += 1) {
194
- template += handleExpression(data, data.strings[index], data.expressions[index]);
496
+ template += handleExpression(data2, data2.strings[index], data2.expressions[index]);
195
497
  }
196
498
  return template;
197
499
  }
package/package.json CHANGED
@@ -36,5 +36,5 @@
36
36
  "watch": "bun build ./src/index.ts --outfile ./dist/index.js --watch"
37
37
  },
38
38
  "types": "src/types.d.ts",
39
- "version": "0.5.0"
39
+ "version": "0.6.0"
40
40
  }
package/src/fragment.ts CHANGED
@@ -1,19 +1,8 @@
1
1
  import {createTemplate, getNodes} from './helpers/dom';
2
- import {getFragment, parse} from './html';
2
+ import {parse} from './html';
3
+ import type {Fragment, FragmentData} from './models';
3
4
  import {mapNodes} from './node';
4
5
 
5
- export type Fragment = {
6
- get(): DocumentFragment;
7
- remove(): void;
8
- };
9
-
10
- export type FragmentData = {
11
- expressions: unknown[];
12
- nodes: Node[];
13
- strings: TemplateStringsArray;
14
- values: unknown[];
15
- };
16
-
17
6
  export function createFragment(
18
7
  strings: TemplateStringsArray,
19
8
  expressions: unknown[],
@@ -26,7 +15,10 @@ export function createFragment(
26
15
  };
27
16
 
28
17
  const instance = Object.create({
29
- get(): Node {
18
+ appendTo(element: Element) {
19
+ element.append(...this.get());
20
+ },
21
+ get() {
30
22
  if (data.nodes.length === 0) {
31
23
  const parsed = parse(data);
32
24
  const templated = createTemplate(parsed);
@@ -36,9 +28,9 @@ export function createFragment(
36
28
  mapNodes(data, data.nodes);
37
29
  }
38
30
 
39
- return getFragment(data.nodes);
31
+ return [...data.nodes];
40
32
  },
41
- remove(): void {
33
+ remove() {
42
34
  const {length} = data.nodes;
43
35
 
44
36
  for (let index = 0; index < length; index += 1) {
@@ -49,7 +41,7 @@ export function createFragment(
49
41
 
50
42
  data.nodes.splice(0, data.nodes.length);
51
43
  },
52
- });
44
+ } as Fragment);
53
45
 
54
46
  Object.defineProperty(instance, '$fragment', {
55
47
  value: true,
@@ -57,7 +49,3 @@ export function createFragment(
57
49
 
58
50
  return instance;
59
51
  }
60
-
61
- export function isFragment(value: unknown): value is Fragment {
62
- return typeof value === 'object' && value != null && '$fragment' in value;
63
- }
@@ -1,3 +1,19 @@
1
+ import {getString} from '@oscarpalmer/atoms/string';
2
+ import type {FragmentNode} from '../models';
3
+ import {isFragment, isFragmentNode} from './index';
4
+
5
+ export function createNodes(value: unknown): FragmentNode[] {
6
+ if (isFragmentNode(value)) {
7
+ return [value];
8
+ }
9
+
10
+ if (isFragment(value)) {
11
+ return value.get();
12
+ }
13
+
14
+ return [new Text(getString(value))];
15
+ }
16
+
1
17
  export function createTemplate(html: string): Node {
2
18
  const template = document.createElement('template');
3
19
 
@@ -25,3 +41,17 @@ export function getNodes(node: Node): Node[] {
25
41
  ? [...node.childNodes]
26
42
  : [node];
27
43
  }
44
+
45
+ export function replaceNodes(from: FragmentNode[], to: FragmentNode[]): void {
46
+ const {length} = from;
47
+
48
+ for (let index = 0; index < length; index += 1) {
49
+ const node = from[index];
50
+
51
+ if (index === 0) {
52
+ node.replaceWith(...to);
53
+ } else {
54
+ node.remove();
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,22 @@
1
+ import type {Fragment, FragmentElement, FragmentNode} from '../models';
2
+
3
+ export function isFragment(value: unknown): value is Fragment {
4
+ return (
5
+ typeof value === 'object' &&
6
+ value != null &&
7
+ '$fragment' in value &&
8
+ value.$fragment === true
9
+ );
10
+ }
11
+
12
+ export function isFragmentElement(value: unknown): value is FragmentElement {
13
+ return value instanceof HTMLElement || value instanceof SVGElement;
14
+ }
15
+
16
+ export function isFragmentNode(value: unknown): value is FragmentNode {
17
+ return (
18
+ value instanceof Comment ||
19
+ value instanceof Element ||
20
+ value instanceof Text
21
+ );
22
+ }
package/src/html.ts CHANGED
@@ -1,13 +1,6 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
- import {type Fragment, type FragmentData, createFragment} from './fragment';
3
-
4
- export function getFragment(nodes: Node[]): DocumentFragment {
5
- const fragment = document.createDocumentFragment();
6
-
7
- fragment.append(...nodes);
8
-
9
- return fragment;
10
- }
2
+ import {createFragment} from './fragment';
3
+ import type {Fragment, FragmentData} from './models';
11
4
 
12
5
  function handleExpression(
13
6
  data: FragmentData,
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type {Fragment} from './fragment';
1
+ export type {Fragment} from './models';
2
2
  export {html} from './html';
package/src/models.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type Fragment = {
2
+ appendTo(element: Element): void;
3
+ get(): FragmentNode[];
4
+ remove(): void;
5
+ };
6
+
7
+ export type FragmentData = {
8
+ expressions: unknown[];
9
+ nodes: Node[];
10
+ strings: TemplateStringsArray;
11
+ values: unknown[];
12
+ };
13
+
14
+ export type FragmentElement = HTMLElement | SVGElement;
15
+
16
+ export type FragmentNode = Comment | Element | Text;
@@ -0,0 +1,62 @@
1
+ import {isReactive} from '@oscarpalmer/sentinel';
2
+ import type {FragmentData, FragmentElement} from '../../models';
3
+ import {mapEvent} from '../event';
4
+ import {setAttribute} from './value';
5
+
6
+ const commentExpression = /^<!--abydon\.(\d+)-->$/;
7
+
8
+ function getValue(data: FragmentData, original: string): unknown {
9
+ const matches = commentExpression.exec(original ?? '');
10
+
11
+ return matches == null ? original : data.values[+matches[1]];
12
+ }
13
+
14
+ export function isBadAttribute(name: string, value: string): boolean {
15
+ return (
16
+ /^on/i.test(name) ||
17
+ (/^(href|src|xlink:href)$/i.test(name) &&
18
+ /(data:text\/html|javascript:)/i.test(value))
19
+ );
20
+ }
21
+
22
+ export function mapAttributes(
23
+ data: FragmentData,
24
+ element: FragmentElement,
25
+ ): void {
26
+ const attributes = [...element.attributes];
27
+ const {length} = attributes;
28
+
29
+ for (let index = 0; index < length; index += 1) {
30
+ const {name, value} = attributes[index];
31
+
32
+ if (isBadAttribute(name, value)) {
33
+ element.removeAttribute(name);
34
+
35
+ continue;
36
+ }
37
+
38
+ const actual = getValue(data, value);
39
+
40
+ if (name.startsWith('@')) {
41
+ mapEvent(element, name, actual);
42
+ } else if (name.includes('.') || isReactive(actual)) {
43
+ mapValue(element, name, actual);
44
+ }
45
+ }
46
+ }
47
+
48
+ function mapValue(
49
+ element: FragmentElement,
50
+ name: string,
51
+ value: unknown,
52
+ ): void {
53
+ switch (true) {
54
+ case typeof value === 'function':
55
+ mapValue(element, name, value());
56
+ return;
57
+
58
+ default:
59
+ setAttribute(element, name, value);
60
+ break;
61
+ }
62
+ }
@@ -0,0 +1,159 @@
1
+ import type {PlainObject} from '@oscarpalmer/atoms/models';
2
+ import {getString} from '@oscarpalmer/atoms/string';
3
+ import {effect, isReactive} from '@oscarpalmer/sentinel';
4
+ import type {FragmentElement} from '../../models';
5
+ import {closest} from '@oscarpalmer/atoms/element';
6
+
7
+ const booleanAttributes = new Set([
8
+ 'checked',
9
+ 'disabled',
10
+ 'hidden',
11
+ 'inert',
12
+ 'multiple',
13
+ 'open',
14
+ 'readOnly',
15
+ 'required',
16
+ 'selected',
17
+ ]);
18
+
19
+ const classPattern = /^class\./;
20
+ const stylePattern = /^style\./;
21
+
22
+ export function setAttribute(
23
+ element: FragmentElement,
24
+ name: string,
25
+ value: unknown,
26
+ ): void {
27
+ element.removeAttribute(name);
28
+
29
+ switch (true) {
30
+ case classPattern.test(name):
31
+ setClasses(element, name, value);
32
+ return;
33
+
34
+ case stylePattern.test(name):
35
+ setStyle(element, name, value);
36
+ return;
37
+
38
+ default:
39
+ setValue(element, name, value);
40
+ break;
41
+ }
42
+ }
43
+
44
+ function setClasses(
45
+ element: FragmentElement,
46
+ name: string,
47
+ value: unknown,
48
+ ): void {
49
+ function update(value: unknown): void {
50
+ if (/^(|true)$/.test(getString(value))) {
51
+ element.classList.add(...classes);
52
+ } else {
53
+ element.classList.remove(...classes);
54
+ }
55
+ }
56
+
57
+ const classes = name.slice(6).split('.');
58
+
59
+ if (isReactive(value)) {
60
+ effect(() => {
61
+ update(value.get());
62
+ });
63
+ } else {
64
+ update(value);
65
+ }
66
+ }
67
+
68
+ function setStyle(
69
+ element: FragmentElement,
70
+ name: string,
71
+ value: unknown,
72
+ ): void {
73
+ function update(value: unknown): void {
74
+ if (value == null || value === false || (value === true && unit == null)) {
75
+ element.style.removeProperty(property);
76
+ } else {
77
+ element.style.setProperty(
78
+ property,
79
+ value === true ? unit : getString(value),
80
+ );
81
+ }
82
+ }
83
+
84
+ const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
85
+
86
+ if (property == null) {
87
+ return;
88
+ }
89
+
90
+ if (isReactive(value)) {
91
+ effect(() => {
92
+ update(value.get());
93
+ });
94
+ } else {
95
+ update(value);
96
+ }
97
+ }
98
+
99
+ function setValue(
100
+ element: FragmentElement,
101
+ name: string,
102
+ value: unknown,
103
+ ): void {
104
+ const callback =
105
+ booleanAttributes.has(name) && name in element
106
+ ? name === 'selected'
107
+ ? updateSelected(element)
108
+ : updateProperty
109
+ : updateAttribute;
110
+
111
+ if (isReactive(value)) {
112
+ effect(() => {
113
+ callback(element, name, value.get());
114
+ });
115
+ } else {
116
+ callback(element, name, value);
117
+ }
118
+ }
119
+
120
+ function triggerSelectChange(select: HTMLSelectElement): void {
121
+ if (select != null) {
122
+ select.dispatchEvent(new Event('change', {bubbles: true}));
123
+ }
124
+ }
125
+
126
+ function updateAttribute(
127
+ element: FragmentElement,
128
+ name: string,
129
+ value: unknown,
130
+ ): void {
131
+ if (value == null) {
132
+ element.removeAttribute(name);
133
+ } else {
134
+ element.setAttribute(name, getString(value));
135
+ }
136
+ }
137
+
138
+ function updateProperty(
139
+ element: FragmentElement,
140
+ name: string,
141
+ value: unknown,
142
+ ): void {
143
+ (element as unknown as PlainObject)[name] = /^(|true)$/.test(
144
+ getString(value),
145
+ );
146
+ }
147
+
148
+ function updateSelected(element: FragmentElement) {
149
+ const select = closest(element, 'select')[0] as HTMLSelectElement;
150
+ const options = [...(select?.options ?? [])];
151
+
152
+ return (element: FragmentElement, name: string, value: unknown): void => {
153
+ if (select != null && options.includes(element as HTMLOptionElement)) {
154
+ triggerSelectChange(select);
155
+ }
156
+
157
+ updateProperty(element, name, value);
158
+ };
159
+ }
@@ -0,0 +1,15 @@
1
+ import type {FragmentElement} from '../models';
2
+
3
+ export function mapEvent(
4
+ element: FragmentElement,
5
+ name: string,
6
+ value: unknown,
7
+ ): void {
8
+ element.removeAttribute(name);
9
+
10
+ if (typeof value !== 'function') {
11
+ return;
12
+ }
13
+
14
+ // TODO
15
+ }
package/src/node/index.ts CHANGED
@@ -1,6 +1,9 @@
1
- import {getString} from '@oscarpalmer/atoms/string';
2
- import {isReactive, type Reactive} from '@oscarpalmer/sentinel';
3
- import {type FragmentData, isFragment} from '../fragment';
1
+ import {isReactive} from '@oscarpalmer/sentinel';
2
+ import {isFragmentElement} from '../helpers';
3
+ import {createNodes} from '../helpers/dom';
4
+ import type {FragmentData} from '../models';
5
+ import {setReactiveNode} from './value';
6
+ import {mapAttributes} from './attribute';
4
7
 
5
8
  const commentExpression = /^abydon\.(\d+)$/;
6
9
 
@@ -25,8 +28,8 @@ export function mapNodes(data: FragmentData, nodes: Node[]): void {
25
28
  continue;
26
29
  }
27
30
 
28
- if (node instanceof Element) {
29
- // Attributes
31
+ if (isFragmentElement(node)) {
32
+ mapAttributes(data, node);
30
33
  }
31
34
 
32
35
  if (node.hasChildNodes()) {
@@ -35,30 +38,18 @@ export function mapNodes(data: FragmentData, nodes: Node[]): void {
35
38
  }
36
39
  }
37
40
 
38
- function mapReactive(comment: Comment, value: Reactive): void {
39
- // ?
40
- }
41
-
42
41
  function mapValue(comment: Comment, value: unknown): void {
43
42
  switch (true) {
44
- case value instanceof Node:
45
- comment.replaceWith(value);
43
+ case typeof value === 'function':
44
+ mapValue(comment, value());
46
45
  return;
47
46
 
48
- case isFragment(value):
49
- comment.replaceWith(value.get());
50
- break;
51
-
52
47
  case isReactive(value):
53
- mapReactive(comment, value);
48
+ setReactiveNode(comment, value);
54
49
  break;
55
50
 
56
- case typeof value === 'function':
57
- mapValue(comment, value());
58
- return;
59
-
60
- case typeof value === 'object':
61
- comment.replaceWith(new Text(getString(value)));
51
+ default:
52
+ comment.replaceWith(...createNodes(value));
62
53
  break;
63
54
  }
64
55
  }
@@ -0,0 +1,48 @@
1
+ import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
+ import {getString} from '@oscarpalmer/atoms/string';
3
+ import {type Reactive, effect} from '@oscarpalmer/sentinel';
4
+ import {isFragment, isFragmentNode} from '../helpers';
5
+ import {createNodes, replaceNodes} from '../helpers/dom';
6
+ import type {FragmentNode} from '../models';
7
+
8
+ export function setReactiveNode(comment: Comment, reactive: Reactive): void {
9
+ const text = new Text();
10
+
11
+ let nodes: FragmentNode[] | undefined;
12
+
13
+ effect(() => {
14
+ const value = reactive.get();
15
+
16
+ if (isFragment(value) || isFragmentNode(value)) {
17
+ const next = createNodes(value);
18
+
19
+ if (nodes == null) {
20
+ if (comment.parentNode != null) {
21
+ replaceNodes([comment], next);
22
+ } else if (text.parentNode != null) {
23
+ replaceNodes([text], next);
24
+ }
25
+ } else {
26
+ replaceNodes(nodes, next);
27
+ }
28
+
29
+ nodes = next;
30
+
31
+ return;
32
+ }
33
+
34
+ const isNullable = isNullableOrWhitespace(value);
35
+
36
+ text.textContent = getString(value);
37
+
38
+ if (nodes != null) {
39
+ replaceNodes(nodes, [isNullable ? comment : text]);
40
+ } else if (isNullable && comment.parentNode == null) {
41
+ text.replaceWith(comment);
42
+ } else if (!isNullable && text.parentNode == null) {
43
+ comment.replaceWith(text);
44
+ }
45
+
46
+ nodes = undefined;
47
+ });
48
+ }
@@ -1,12 +1,2 @@
1
- export type Fragment = {
2
- get(): DocumentFragment;
3
- remove(): void;
4
- };
5
- export type FragmentData = {
6
- expressions: unknown[];
7
- nodes: Node[];
8
- strings: TemplateStringsArray;
9
- values: unknown[];
10
- };
1
+ import type { Fragment } from './models';
11
2
  export declare function createFragment(strings: TemplateStringsArray, expressions: unknown[]): Fragment;
12
- export declare function isFragment(value: unknown): value is Fragment;
@@ -1,2 +1,5 @@
1
+ import type { FragmentNode } from '../models';
2
+ export declare function createNodes(value: unknown): FragmentNode[];
1
3
  export declare function createTemplate(html: string): Node;
2
4
  export declare function getNodes(node: Node): Node[];
5
+ export declare function replaceNodes(from: FragmentNode[], to: FragmentNode[]): void;
@@ -0,0 +1,4 @@
1
+ import type { Fragment, FragmentElement, FragmentNode } from '../models';
2
+ export declare function isFragment(value: unknown): value is Fragment;
3
+ export declare function isFragmentElement(value: unknown): value is FragmentElement;
4
+ export declare function isFragmentNode(value: unknown): value is FragmentNode;
package/types/html.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { type Fragment, type FragmentData } from './fragment';
2
- export declare function getFragment(nodes: Node[]): DocumentFragment;
1
+ import type { Fragment, FragmentData } from './models';
3
2
  export declare function html(strings: TemplateStringsArray, ...values: unknown[]): Fragment;
4
3
  export declare function parse(data: FragmentData): string;
package/types/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { Fragment } from './fragment';
1
+ export type { Fragment } from './models';
2
2
  export { html } from './html';
@@ -0,0 +1,13 @@
1
+ export type Fragment = {
2
+ appendTo(element: Element): void;
3
+ get(): FragmentNode[];
4
+ remove(): void;
5
+ };
6
+ export type FragmentData = {
7
+ expressions: unknown[];
8
+ nodes: Node[];
9
+ strings: TemplateStringsArray;
10
+ values: unknown[];
11
+ };
12
+ export type FragmentElement = HTMLElement | SVGElement;
13
+ export type FragmentNode = Comment | Element | Text;
@@ -0,0 +1,3 @@
1
+ import type { FragmentData, FragmentElement } from '../../models';
2
+ export declare function isBadAttribute(name: string, value: string): boolean;
3
+ export declare function mapAttributes(data: FragmentData, element: FragmentElement): void;
@@ -0,0 +1,2 @@
1
+ import type { FragmentElement } from '../../models';
2
+ export declare function setAttribute(element: FragmentElement, name: string, value: unknown): void;
@@ -0,0 +1,2 @@
1
+ import type { FragmentElement } from '../models';
2
+ export declare function mapEvent(element: FragmentElement, name: string, value: unknown): void;
@@ -1,2 +1,2 @@
1
- import { type FragmentData } from '../fragment';
1
+ import type { FragmentData } from '../models';
2
2
  export declare function mapNodes(data: FragmentData, nodes: Node[]): void;
@@ -0,0 +1,2 @@
1
+ import { type Reactive } from '@oscarpalmer/sentinel';
2
+ export declare function setReactiveNode(comment: Comment, reactive: Reactive): void;