@oscarpalmer/abydon 0.4.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/bun.lockb CHANGED
Binary file
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,39 +53,447 @@ 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
+ }
67
+
68
+ // node_modules/@oscarpalmer/sentinel/node_modules/@oscarpalmer/atoms/dist/js/queue.mjs
69
+ if (globalThis._atomic_queued == null) {
70
+ const queued = new Set;
71
+ Object.defineProperty(globalThis, "_atomic_queued", {
72
+ get() {
73
+ return queued;
74
+ }
75
+ });
76
+ }
77
+
78
+ // node_modules/@oscarpalmer/sentinel/dist/global.mjs
79
+ if (globalThis._sentinels == null) {
80
+ const effects = [];
81
+ Object.defineProperty(globalThis, "_sentinels", {
82
+ get() {
83
+ return effects;
84
+ }
85
+ });
86
+ }
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
+
127
+ // node_modules/@oscarpalmer/sentinel/dist/helpers/is.mjs
128
+ var isReactive = function(value) {
129
+ return isSentinel(value, /^array|computed|signal|store$/i);
130
+ };
131
+ var isSentinel = function(value, expression) {
132
+ return expression.test(value?.$sentinel ?? "");
133
+ };
134
+
135
+ // node_modules/@oscarpalmer/sentinel/dist/helpers/value.mjs
136
+ var arrayOperations = new Set([
137
+ "copyWithin",
138
+ "fill",
139
+ "pop",
140
+ "push",
141
+ "reverse",
142
+ "shift",
143
+ "sort",
144
+ "splice",
145
+ "unshift"
146
+ ]);
147
+
148
+ // node_modules/@oscarpalmer/sentinel/dist/reactive/index.mjs
149
+ var primitives = new Set(["boolean", "number", "string"]);
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
+
395
+ // src/node/index.ts
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);
401
+ }
402
+ };
403
+ function mapNodes(data2, nodes) {
404
+ const { length } = nodes;
405
+ for (let index = 0;index < length; index += 1) {
406
+ const node = nodes[index];
407
+ if (node instanceof Comment) {
408
+ mapNode(data2, node);
409
+ continue;
410
+ }
411
+ if (isFragmentElement(node)) {
412
+ mapAttributes(data2, node);
413
+ }
414
+ if (node.hasChildNodes()) {
415
+ mapNodes(data2, [...node.childNodes]);
416
+ }
417
+ }
418
+ }
419
+ var mapValue2 = function(comment, value12) {
420
+ switch (true) {
421
+ case typeof value12 === "function":
422
+ mapValue2(comment, value12());
423
+ return;
424
+ case isReactive(value12):
425
+ setReactiveNode(comment, value12);
426
+ break;
427
+ default:
428
+ comment.replaceWith(...createNodes(value12));
429
+ break;
430
+ }
431
+ };
432
+ var commentExpression2 = /^abydon\.(\d+)$/;
36
433
 
37
434
  // src/fragment.ts
38
435
  function createFragment(strings, expressions) {
39
- const data = {
436
+ const data2 = {
40
437
  expressions,
41
438
  strings,
42
- nodes: []
439
+ nodes: [],
440
+ values: []
43
441
  };
44
442
  const instance = Object.create({
45
- append(parent) {
46
- if (data.nodes.length > 0) {
47
- return parent.append(...data.nodes);
443
+ appendTo(element2) {
444
+ element2.append(...this.get());
445
+ },
446
+ get() {
447
+ if (data2.nodes.length === 0) {
448
+ const parsed = parse2(data2);
449
+ const templated = createTemplate(parsed);
450
+ data2.nodes.splice(0, data2.nodes.length, ...getNodes(templated));
451
+ mapNodes(data2, data2.nodes);
452
+ }
453
+ return [...data2.nodes];
454
+ },
455
+ remove() {
456
+ const { length } = data2.nodes;
457
+ for (let index = 0;index < length; index += 1) {
458
+ const node2 = data2.nodes[index];
459
+ node2.parentNode?.removeChild(node2);
48
460
  }
49
- const parsed = parse(data);
50
- const templated = createTemplate(parsed);
51
- data.nodes.splice(0, data.nodes.length, ...getNodes(templated));
52
- parent.append(...data.nodes);
461
+ data2.nodes.splice(0, data2.nodes.length);
53
462
  }
54
463
  });
464
+ Object.defineProperty(instance, "$fragment", {
465
+ value: true
466
+ });
55
467
  return instance;
56
468
  }
57
469
 
58
470
  // src/html.ts
471
+ var handleExpression = function(data2, prefix, expression) {
472
+ if (isNullableOrWhitespace(expression)) {
473
+ return prefix;
474
+ }
475
+ if (Array.isArray(expression)) {
476
+ const { length } = expression;
477
+ let expressions = "";
478
+ for (let index = 0;index < length; index += 1) {
479
+ expressions += handleExpression(data2, "", expression[index]);
480
+ }
481
+ return `${prefix}${expressions}`;
482
+ }
483
+ if (typeof expression === "object") {
484
+ const index = data2.values.push(expression) - 1;
485
+ return `${prefix}<!--abydon.${index}-->`;
486
+ }
487
+ return `${prefix}${expression}`;
488
+ };
59
489
  function html2(strings, ...values) {
60
490
  return createFragment(strings, values);
61
491
  }
62
- function parse(data) {
63
- const { length } = data.strings;
492
+ function parse2(data2) {
493
+ const { length } = data2.strings;
64
494
  let template = "";
65
495
  for (let index = 0;index < length; index += 1) {
66
- const part = data.strings[index];
67
- const expression = data.expressions[index];
68
- template += isNullableOrWhitespace(expression) ? part : `${part}${expression}`;
496
+ template += handleExpression(data2, data2.strings[index], data2.expressions[index]);
69
497
  }
70
498
  return template;
71
499
  }
package/package.json CHANGED
@@ -4,7 +4,8 @@
4
4
  "url": "https://oscarpalmer.se"
5
5
  },
6
6
  "dependencies": {
7
- "@oscarpalmer/atoms": "^0.61.0"
7
+ "@oscarpalmer/atoms": "^0.61.0",
8
+ "@oscarpalmer/sentinel": "^0.22.0"
8
9
  },
9
10
  "devDependencies": {
10
11
  "@biomejs/biome": "^1.8.3",
@@ -35,5 +36,5 @@
35
36
  "watch": "bun build ./src/index.ts --outfile ./dist/index.js --watch"
36
37
  },
37
38
  "types": "src/types.d.ts",
38
- "version": "0.4.0"
39
+ "version": "0.6.0"
39
40
  }
package/src/fragment.ts CHANGED
@@ -1,15 +1,7 @@
1
1
  import {createTemplate, getNodes} from './helpers/dom';
2
2
  import {parse} from './html';
3
-
4
- export type Fragment = {
5
- append(parent: Node): void;
6
- };
7
-
8
- export type FragmentData = {
9
- expressions: unknown[];
10
- nodes: Node[];
11
- strings: TemplateStringsArray;
12
- };
3
+ import type {Fragment, FragmentData} from './models';
4
+ import {mapNodes} from './node';
13
5
 
14
6
  export function createFragment(
15
7
  strings: TemplateStringsArray,
@@ -19,21 +11,40 @@ export function createFragment(
19
11
  expressions,
20
12
  strings,
21
13
  nodes: [],
14
+ values: [],
22
15
  };
23
16
 
24
17
  const instance = Object.create({
25
- append(parent: ParentNode) {
26
- if (data.nodes.length > 0) {
27
- return parent.append(...data.nodes);
18
+ appendTo(element: Element) {
19
+ element.append(...this.get());
20
+ },
21
+ get() {
22
+ if (data.nodes.length === 0) {
23
+ const parsed = parse(data);
24
+ const templated = createTemplate(parsed);
25
+
26
+ data.nodes.splice(0, data.nodes.length, ...getNodes(templated));
27
+
28
+ mapNodes(data, data.nodes);
28
29
  }
29
30
 
30
- const parsed = parse(data);
31
- const templated = createTemplate(parsed);
31
+ return [...data.nodes];
32
+ },
33
+ remove() {
34
+ const {length} = data.nodes;
35
+
36
+ for (let index = 0; index < length; index += 1) {
37
+ const node = data.nodes[index];
32
38
 
33
- data.nodes.splice(0, data.nodes.length, ...getNodes(templated));
39
+ node.parentNode?.removeChild(node);
40
+ }
34
41
 
35
- parent.append(...data.nodes);
42
+ data.nodes.splice(0, data.nodes.length);
36
43
  },
44
+ } as Fragment);
45
+
46
+ Object.defineProperty(instance, '$fragment', {
47
+ value: true,
37
48
  });
38
49
 
39
50
  return instance;
@@ -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,5 +1,36 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
- import {createFragment, type FragmentData, type Fragment} from './fragment';
2
+ import {createFragment} from './fragment';
3
+ import type {Fragment, FragmentData} from './models';
4
+
5
+ function handleExpression(
6
+ data: FragmentData,
7
+ prefix: string,
8
+ expression: unknown,
9
+ ): string {
10
+ if (isNullableOrWhitespace(expression)) {
11
+ return prefix;
12
+ }
13
+
14
+ if (Array.isArray(expression)) {
15
+ const {length} = expression;
16
+
17
+ let expressions = '';
18
+
19
+ for (let index = 0; index < length; index += 1) {
20
+ expressions += handleExpression(data, '', expression[index]);
21
+ }
22
+
23
+ return `${prefix}${expressions}`;
24
+ }
25
+
26
+ if (typeof expression === 'object') {
27
+ const index = data.values.push(expression) - 1;
28
+
29
+ return `${prefix}<!--abydon.${index}-->`;
30
+ }
31
+
32
+ return `${prefix}${expression}`;
33
+ }
3
34
 
4
35
  export function html(
5
36
  strings: TemplateStringsArray,
@@ -14,12 +45,11 @@ export function parse(data: FragmentData): string {
14
45
  let template = '';
15
46
 
16
47
  for (let index = 0; index < length; index += 1) {
17
- const part = data.strings[index];
18
- const expression = data.expressions[index];
19
-
20
- template += isNullableOrWhitespace(expression)
21
- ? part
22
- : `${part}${expression}`;
48
+ template += handleExpression(
49
+ data,
50
+ data.strings[index],
51
+ data.expressions[index],
52
+ );
23
53
  }
24
54
 
25
55
  return template;
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
+ }
@@ -0,0 +1,55 @@
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';
7
+
8
+ const commentExpression = /^abydon\.(\d+)$/;
9
+
10
+ function mapNode(data: FragmentData, comment: Comment): void {
11
+ const matches = commentExpression.exec(comment.textContent ?? '');
12
+ const value = matches == null ? null : data.values[+matches[1]];
13
+
14
+ if (value != null) {
15
+ mapValue(comment, value);
16
+ }
17
+ }
18
+
19
+ export function mapNodes(data: FragmentData, nodes: Node[]): void {
20
+ const {length} = nodes;
21
+
22
+ for (let index = 0; index < length; index += 1) {
23
+ const node = nodes[index];
24
+
25
+ if (node instanceof Comment) {
26
+ mapNode(data, node);
27
+
28
+ continue;
29
+ }
30
+
31
+ if (isFragmentElement(node)) {
32
+ mapAttributes(data, node);
33
+ }
34
+
35
+ if (node.hasChildNodes()) {
36
+ mapNodes(data, [...node.childNodes]);
37
+ }
38
+ }
39
+ }
40
+
41
+ function mapValue(comment: Comment, value: unknown): void {
42
+ switch (true) {
43
+ case typeof value === 'function':
44
+ mapValue(comment, value());
45
+ return;
46
+
47
+ case isReactive(value):
48
+ setReactiveNode(comment, value);
49
+ break;
50
+
51
+ default:
52
+ comment.replaceWith(...createNodes(value));
53
+ break;
54
+ }
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,9 +1,2 @@
1
- export type Fragment = {
2
- append(parent: Node): void;
3
- };
4
- export type FragmentData = {
5
- expressions: unknown[];
6
- nodes: Node[];
7
- strings: TemplateStringsArray;
8
- };
1
+ import type { Fragment } from './models';
9
2
  export declare function createFragment(strings: TemplateStringsArray, expressions: unknown[]): 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,3 +1,3 @@
1
- import { type FragmentData, type Fragment } from './fragment';
1
+ import type { Fragment, FragmentData } from './models';
2
2
  export declare function html(strings: TemplateStringsArray, ...values: unknown[]): Fragment;
3
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;
@@ -0,0 +1,2 @@
1
+ import type { FragmentData } from '../models';
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;