@oscarpalmer/abydon 0.21.0 → 0.23.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.
Files changed (42) hide show
  1. package/README.md +2 -0
  2. package/dist/abydon.full.mjs +422 -359
  3. package/dist/attribute/index.d.mts +7 -0
  4. package/dist/attribute/index.mjs +47 -0
  5. package/dist/{node/attribute → attribute}/value.d.mts +2 -2
  6. package/dist/attribute/value.mjs +53 -0
  7. package/dist/constants.d.mts +8 -8
  8. package/dist/constants.mjs +15 -15
  9. package/dist/fragment.d.mts +30 -13
  10. package/dist/fragment.mjs +40 -18
  11. package/dist/fragments.d.mts +3 -7
  12. package/dist/fragments.mjs +6 -12
  13. package/dist/helpers/dom.d.mts +2 -3
  14. package/dist/helpers/dom.mjs +2 -4
  15. package/dist/helpers/index.d.mts +16 -2
  16. package/dist/helpers/index.mjs +17 -1
  17. package/dist/index.d.mts +35 -7
  18. package/dist/index.mjs +39 -8
  19. package/dist/models.d.mts +6 -4
  20. package/dist/node/event.mjs +9 -4
  21. package/dist/node/index.mjs +26 -32
  22. package/dist/node/value.mjs +70 -56
  23. package/dist/parse.mjs +6 -4
  24. package/package.json +6 -5
  25. package/src/attribute/index.ts +89 -0
  26. package/src/attribute/value.ts +107 -0
  27. package/src/constants.ts +19 -19
  28. package/src/fragment.ts +43 -21
  29. package/src/fragments.ts +8 -15
  30. package/src/helpers/dom.ts +3 -5
  31. package/src/helpers/index.ts +25 -1
  32. package/src/index.ts +47 -7
  33. package/src/models.ts +6 -4
  34. package/src/node/event.ts +12 -5
  35. package/src/node/index.ts +36 -56
  36. package/src/node/value.ts +121 -104
  37. package/src/parse.ts +15 -5
  38. package/dist/node/attribute/index.d.mts +0 -6
  39. package/dist/node/attribute/index.mjs +0 -51
  40. package/dist/node/attribute/value.mjs +0 -65
  41. package/src/node/attribute/index.ts +0 -100
  42. package/src/node/attribute/value.ts +0 -158
package/dist/index.mjs CHANGED
@@ -1,24 +1,55 @@
1
+ import { isFragment, isFragments } from "./helpers/index.mjs";
1
2
  import { Fragments } from "./fragments.mjs";
2
3
  import { Fragment } from "./fragment.mjs";
3
- import "@oscarpalmer/mora";
4
+ import { isArray } from "@oscarpalmer/mora";
4
5
  export * from "@oscarpalmer/mora";
5
6
  //#region src/index.ts
6
7
  /**
7
- * Create Fragments from a reactive array
8
+ * Create a _Fragments_ instance from a reactive array
9
+ *
10
+ * _A Fragments instance can be used to efficiently render a list of items that may change over time, using unique identifiers to track each item, only adding, removing, or updating each related Fragment._
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const fruits = array(['Apple', 'Banana', 'Cherry']);
15
+ * const items = fragments(
16
+ * fruits,
17
+ * fruit => fruit, // Identifies a unique item
18
+ * fruit => html`<p>${fruit}</p>`, // Creates a Fragment from an item
19
+ * );
20
+ * html`${items}`.appendTo(document.body) // Renders '<p>Apple</p><p>Banana</p><p>Cherry</p>'
21
+ * fruits.push(['Date', 'Elderberry', 'Fig']); // Appends '<p>Date</p><p>Elderberry</p><p>Fig</p>'
22
+ * // without re-rendering the existing Fragments
23
+ * ```
24
+ *
8
25
  * @param array Reactive array
9
- * @param identify Function to identify item
10
- * @param fragment Function to create fragment from item
11
- * @returns Fragments
26
+ * @param identify Function to identify item uniquely _(non-nullable)_
27
+ * @param fragment Function to create _Fragment_ from item
28
+ * @returns _Fragments_
12
29
  */
13
30
  function fragments(array, identify, fragment) {
31
+ if (!isArray(array)) throw new TypeError("Fragments array must be a reactive array");
32
+ if (typeof identify !== "function") throw new TypeError("Fragments identify must be a function");
33
+ if (typeof fragment !== "function") throw new TypeError("Fragments fragment must be a function");
14
34
  return new Fragments(array, identify, fragment);
15
35
  }
16
36
  /**
17
- * Create Fragment from a template
18
- * @returns Fragment
37
+ * Create a _Fragment_ from a template
38
+ *
39
+ * _A Fragment can be used to efficiently render a template that may change over time, only updating the necessary parts of the DOM._
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const name = signal('World');
44
+ * const fragment = html`<p>Hello, ${name}!</p>`;
45
+ * fragment.appendTo(document.body); // Renders '<p>Hello, World!</p>'
46
+ * name.set('Alice'); // Replaces 'World' with 'Alice'
47
+ * ```
48
+ *
49
+ * @returns _Fragment_
19
50
  */
20
51
  function html(template, ...values) {
21
52
  return new Fragment(template, values);
22
53
  }
23
54
  //#endregion
24
- export { fragments, html };
55
+ export { fragments, html, isFragment, isFragments };
package/dist/models.d.mts CHANGED
@@ -2,16 +2,18 @@ import { Fragment } from "./fragment.mjs";
2
2
  import { Reactive, ReactiveArray, Unsubscribe } from "@oscarpalmer/mora";
3
3
 
4
4
  //#region src/models.d.ts
5
+ /**
6
+ * Configuration for a _Fragment_
7
+ */
5
8
  type FragmentConfiguration = {
6
9
  /**
7
10
  * Should the template be cached? _(defaults to `true`)_
8
11
  */
9
12
  cache?: boolean;
10
13
  /**
11
- * Identifier for the fragment
14
+ * Identifier for the _Fragment_
12
15
  *
13
- * _(An identifier can be used to uniquely identify a fragment,
14
- * which helps prevent re-rendering in certain scenarios)_
16
+ * _An identifier can be used to uniquely identify a Fragment, which helps prevent re-rendering in reactive arrays and Fragments_
15
17
  */
16
18
  identifier?: unknown;
17
19
  };
@@ -19,7 +21,7 @@ type FragmentData = {
19
21
  expressions: unknown[];
20
22
  items: FragmentItem[];
21
23
  mora: MoraData;
22
- strings: TemplateStringsArray;
24
+ strings: TemplateStringsArray | string[];
23
25
  template?: string;
24
26
  values: unknown[];
25
27
  };
@@ -1,4 +1,4 @@
1
- import { EVENT_CHANGE, EVENT_DEFAULTS, EVENT_INPUT, EVENT_SUBMIT, EXPRESSION_EVENT_CHANGE_TYPES, EXPRESSION_EVENT_NAME, EXPRESSION_EVENT_OPTIONS_ACTIVE, EXPRESSION_EVENT_OPTIONS_CAPTURE, EXPRESSION_EVENT_OPTIONS_ONCE } from "../constants.mjs";
1
+ import { CHANGE_INPUTS, EVENT_CHANGE, EVENT_DEFAULTS, EVENT_INPUT, EVENT_SUBMIT, EXPRESSION_EVENT_NAME, EXPRESSION_EVENT_OPTIONS_ACTIVE, EXPRESSION_EVENT_OPTIONS_CAPTURE, EXPRESSION_EVENT_OPTIONS_ONCE } from "../constants.mjs";
2
2
  import { on } from "@oscarpalmer/toretto/event";
3
3
  //#region src/node/event.ts
4
4
  function getOptions(options) {
@@ -12,14 +12,19 @@ function getOptions(options) {
12
12
  function getType(element, type) {
13
13
  if (type !== "on") return type;
14
14
  if (element instanceof HTMLInputElement) {
15
- if (EXPRESSION_EVENT_CHANGE_TYPES.test(element.type)) return EVENT_CHANGE;
15
+ if (CHANGE_INPUTS.has(element.type)) return EVENT_CHANGE;
16
16
  return element.type === "submit" ? EVENT_SUBMIT : EVENT_INPUT;
17
17
  }
18
18
  return EVENT_DEFAULTS[element.tagName] ?? type;
19
19
  }
20
20
  function mapEvent(element, name, value) {
21
- const [, type, options] = EXPRESSION_EVENT_NAME.exec(name) ?? [];
22
- if (type != null && typeof value === "function") on(element, getType(element, type), value, getOptions(options ?? ""));
21
+ const [, type, options] = EXPRESSION_EVENT_NAME.exec(name);
22
+ const values = Array.isArray(value) ? value : [value];
23
+ const { length } = values;
24
+ for (let index = 0; index < length; index += 1) {
25
+ const item = values[index];
26
+ if (typeof item === "function") on(element, getType(element, type), item, getOptions(options ?? ""));
27
+ }
23
28
  }
24
29
  //#endregion
25
30
  export { mapEvent };
@@ -1,15 +1,14 @@
1
1
  import { EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE } from "../constants.mjs";
2
- import { isFragment, isFragments } from "../helpers/index.mjs";
3
- import { handleFragments } from "../fragments.mjs";
2
+ import { isFragment, isFragments, setComputedValue } from "../helpers/index.mjs";
3
+ import { fragmentsStates, handleFragments } from "../fragments.mjs";
4
4
  import { createNodes } from "../helpers/dom.mjs";
5
- import { mapEvent } from "./event.mjs";
6
- import { mapAttributes } from "./attribute/index.mjs";
5
+ import { mapAttributeValue, mapAttributes } from "../attribute/index.mjs";
7
6
  import { setReactiveValue } from "./value.mjs";
8
- import { computed, isComputed, isReactive, isSignal } from "@oscarpalmer/mora";
7
+ import { isReactive } from "@oscarpalmer/mora";
9
8
  import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
10
9
  //#region src/node/index.ts
11
10
  function mapNode(data, comment) {
12
- const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent ?? "");
11
+ const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent);
13
12
  const value = matches == null ? null : data.values[+matches[1]];
14
13
  if (value != null) mapValue(data, comment, value);
15
14
  }
@@ -21,39 +20,29 @@ function mapNodes(data, nodes) {
21
20
  mapNode(data, node);
22
21
  continue;
23
22
  }
24
- if (node instanceof HTMLTextAreaElement) mapTextarea(data, node);
25
- if (isHTMLOrSVGElement(node)) mapAttributes(data, node);
23
+ if (isHTMLOrSVGElement(node)) {
24
+ let ignoreValueAttribute = false;
25
+ if (node instanceof HTMLTextAreaElement) ignoreValueAttribute = mapTextarea(data, node);
26
+ mapAttributes(data, node, ignoreValueAttribute);
27
+ }
26
28
  if (node.hasChildNodes()) mapNodes(data, [...node.childNodes]);
27
29
  }
28
30
  }
29
31
  function mapTextarea(data, element) {
30
- const [, index] = EXPRESSION_TEXTAREA_VALUE.exec(element.value) ?? [];
31
- if (index == null) return;
32
+ const [, index] = EXPRESSION_TEXTAREA_VALUE.exec(element.textContent) ?? EXPRESSION_TEXTAREA_VALUE.exec(element.value) ?? [];
33
+ if (index == null) return false;
34
+ element.textContent = "";
32
35
  element.value = "";
33
- const value = data.values[Number.parseInt(index, 10)];
34
- if (isSignal(value)) {
35
- element.value = String(value.peek());
36
- mapEvent(element, "@on", () => {
37
- value.set(element.value);
38
- });
39
- return;
40
- }
41
- let reactive;
42
- if (typeof value === "function") reactive = computed(value);
43
- else if (isComputed(value)) reactive = value;
44
- if (reactive == null) element.value = String(value);
45
- else data.mora.subscribers.add(reactive.subscribe((value) => {
46
- element.value = String(value);
47
- }));
36
+ mapAttributeValue(data, element, "value", data.values[Number.parseInt(index, 10)]);
37
+ return true;
48
38
  }
49
39
  function mapValue(data, comment, value) {
50
40
  switch (true) {
51
41
  case typeof value === "function":
52
- setComputedValue(data, comment, value);
42
+ setComputedNode(data, comment, value);
53
43
  break;
54
44
  case isFragments(value):
55
- handleFragments(value, false);
56
- setReactiveValue(data, comment, value.items);
45
+ setFragmentsNode(data, comment, value);
57
46
  break;
58
47
  case isReactive(value):
59
48
  setReactiveValue(data, comment, value);
@@ -72,10 +61,15 @@ function replaceComment(data, comment, value) {
72
61
  }
73
62
  comment.replaceWith(...nodes);
74
63
  }
75
- function setComputedValue(data, comment, callback) {
76
- const value = computed(callback);
77
- data.mora.values.add(value);
78
- setReactiveValue(data, comment, value);
64
+ function setComputedNode(data, comment, callback) {
65
+ setComputedValue(data, callback, (computation) => {
66
+ setReactiveValue(data, comment, computation);
67
+ });
68
+ }
69
+ function setFragmentsNode(data, comment, fragments) {
70
+ const state = fragmentsStates.get(fragments);
71
+ handleFragments(state, false);
72
+ setReactiveValue(data, comment, state.mapped);
79
73
  }
80
74
  //#endregion
81
75
  export { mapNodes };
@@ -7,7 +7,7 @@ import { isChildNode } from "@oscarpalmer/toretto/is";
7
7
  //#region src/node/value.ts
8
8
  function addToArray(identifiers, items, nodes, added) {
9
9
  let position = nodes[0];
10
- const before = added && !identifiers.previous.has(items.templates[0].identifier);
10
+ const before = added && !identifiers.previous.set.has(items.templates[0].identifier);
11
11
  const next = items.next.flatMap((fragment) => fragment.get().flatMap((node) => ({
12
12
  identifier: fragment.identifier,
13
13
  value: node
@@ -15,19 +15,48 @@ function addToArray(identifiers, items, nodes, added) {
15
15
  const { length } = next;
16
16
  for (let index = 0; index < length; index += 1) {
17
17
  const node = next[index];
18
- if (!(added && identifiers.previous.has(node.identifier))) if (index === 0 && before) position.before(node.value);
18
+ if (!(added && identifiers.previous.set.has(node.identifier))) if (index === 0 && before) position.before(node.value);
19
19
  else position.after(node.value);
20
20
  position = node.value;
21
21
  }
22
22
  }
23
+ function getArrayData(item, values) {
24
+ const { length } = values;
25
+ const next = [];
26
+ let templates = [];
27
+ for (let index = 0; index < length; index += 1) {
28
+ const value = values[index];
29
+ if (isFragment(value) && value.identifier != null) {
30
+ next.push(value.identifier);
31
+ templates.push(value);
32
+ }
33
+ }
34
+ const previous = item.fragments?.map((fragment) => fragment.identifier) ?? [];
35
+ const nextSet = new Set(next);
36
+ if (nextSet.size !== templates.length) templates = [];
37
+ return {
38
+ next: {
39
+ array: next,
40
+ set: nextSet
41
+ },
42
+ previous: {
43
+ array: previous,
44
+ set: new Set(previous)
45
+ },
46
+ template: {
47
+ empty: templates.length === 0,
48
+ items: templates
49
+ }
50
+ };
51
+ }
23
52
  function handleArray(identifiers, items, nodes) {
24
- const next = items.templates.map((template) => items.fragments?.find((fragment) => fragment.identifier === template.identifier) ?? template);
25
- const comparison = compareArrays(items.fragments ?? [], items.templates);
53
+ const next = items.templates.map((template) => items.fragments.find((fragment) => fragment.identifier === template.identifier) ?? template);
54
+ const comparison = compareArrays(identifiers.previous.array, identifiers.next.array);
26
55
  if (comparison !== "removed") addToArray(identifiers, {
27
56
  ...items,
28
57
  next
29
58
  }, nodes, comparison === ARRAY_COMPARISON_ADDED);
30
- const toRemove = items.fragments?.filter((fragment) => !identifiers.next.has(fragment.identifier)) ?? [];
59
+ const toRemove = items.fragments.filter((fragment) => !identifiers.next.set.has(fragment.identifier));
31
60
  const { length } = toRemove;
32
61
  for (let index = 0; index < length; index += 1) toRemove[index].remove();
33
62
  return {
@@ -35,82 +64,67 @@ function handleArray(identifiers, items, nodes) {
35
64
  nodes: next.flatMap((fragment) => fragment.get())
36
65
  };
37
66
  }
38
- function removeFragments(fragments) {
39
- if (fragments != null) {
40
- const { length } = fragments;
41
- for (let index = 0; index < length; index += 1) fragments[index].remove();
42
- }
67
+ function removeArray(item, comment) {
68
+ const fragments = (item.fragments ?? []).slice();
69
+ const result = { nodes: setText(item, comment) };
70
+ removeFragmentsItems(fragments);
71
+ return result;
43
72
  }
44
- function replaceText(item, comment, isNullable) {
45
- let to;
46
- if (isNullable) to = [comment];
47
- else to = item.text == null ? [] : [item.text];
48
- replaceNodes(item.nodes ?? [], to);
73
+ function removeFragmentsItems(fragments) {
74
+ const { length } = fragments;
75
+ for (let index = 0; index < length; index += 1) fragments[index].remove();
49
76
  }
50
77
  function setArray(item, comment, value) {
51
- if (value.length === 0) return { nodes: setText(item, comment, value) };
52
- let templates = value.filter((item) => isFragment(item) && item.identifier != null);
53
- const next = templates.map((fragment) => fragment.identifier);
54
- const previous = item.fragments?.map((fragment) => fragment.identifier) ?? [];
55
- if (new Set(next).size !== templates.length) templates = [];
56
- const noTemplates = templates.length === 0;
57
- if (noTemplates || item.nodes == null || previous.some((identifier) => identifier == null)) return {
58
- fragments: noTemplates ? void 0 : templates,
59
- nodes: setNodes(item, comment, noTemplates ? value.flatMap((item) => createNodes(item)) : templates.flatMap((template) => template.get()))
60
- };
78
+ if (value.length === 0) return removeArray(item, comment);
79
+ const { next, previous, template } = getArrayData(item, value);
80
+ if (template.empty || item.nodes == null || previous.array.some((identifier) => identifier == null)) {
81
+ const fragments = (item.fragments ?? []).slice();
82
+ const result = {
83
+ fragments: template.empty ? void 0 : template.items,
84
+ nodes: replaceNodes(item.nodes ?? [comment], template.empty ? value.flatMap((item) => createNodes(item)) : template.items.flatMap((template) => template.get()))
85
+ };
86
+ removeFragmentsItems(fragments);
87
+ return result;
88
+ }
61
89
  return handleArray({
62
- next: new Set(next),
63
- previous: new Set(previous)
90
+ next,
91
+ previous
64
92
  }, {
65
- templates,
66
- fragments: item.fragments ?? []
93
+ fragments: item.fragments ?? [],
94
+ templates: template.items
67
95
  }, item.nodes);
68
96
  }
69
97
  function setNodes(item, comment, next) {
70
- if (item.nodes == null) {
71
- if (comment.parentNode != null) replaceNodes([comment], next);
72
- else if (item.text?.parentNode != null) replaceNodes([item.text], next);
73
- } else replaceNodes(item.nodes, next);
74
- removeFragments(item.fragments);
75
- return next;
98
+ return replaceNodes(item.nodes ?? [comment], next);
76
99
  }
77
100
  function setReactiveValue(data, comment, reactive) {
78
101
  let item = data.items.find((item) => item.nodes?.includes(comment));
79
102
  item ??= {};
80
- item.text = new Text();
81
103
  data.mora.subscribers.add(reactive.subscribe((value) => {
82
104
  if (Array.isArray(value)) setReactiveValueForArray(item, comment, value);
83
105
  else setReactiveValueForSingle(item, comment, value);
84
- item.nodes = [...item.nodes ?? [comment]];
85
106
  }));
86
107
  }
87
108
  function setReactiveValueForArray(item, comment, value) {
88
- const result = setArray(item, comment, value);
89
- item.fragments = typeof result === "boolean" ? void 0 : result?.fragments;
90
- if (typeof result === "boolean") if (result) item.nodes = item.text == null ? [] : [item.text];
91
- else item.nodes = void 0;
92
- else item.nodes = result?.nodes;
109
+ const { fragments, nodes } = setArray(item, comment, value);
110
+ item.fragments = fragments;
111
+ item.nodes = nodes;
93
112
  }
94
113
  function setReactiveValueForSingle(item, comment, value) {
114
+ const fragments = (item.fragments ?? []).slice();
95
115
  const valueIsFragment = isFragment(value);
96
- item.fragments = valueIsFragment ? [value] : void 0;
97
116
  if (valueIsFragment || isChildNode(value)) item.nodes = setNodes(item, comment, createNodes(value));
98
117
  else item.nodes = setText(item, comment, value);
118
+ item.fragments = valueIsFragment ? [value] : void 0;
119
+ removeFragmentsItems(fragments);
99
120
  }
100
121
  function setText(item, comment, value) {
101
- const isNullable = isNullableOrWhitespace(value);
102
- if (item.text != null) item.text.textContent = isNullable ? "" : getString(value);
103
- let result = false;
104
- if (item.nodes != null) {
105
- replaceText(item, comment, isNullable);
106
- result = !isNullable;
107
- } else if (isNullable && comment.parentNode == null) item.text?.replaceWith(comment);
108
- else if (!isNullable && item?.text?.parentNode == null) {
109
- if (item.text != null) comment.replaceWith(item.text);
110
- result = true;
111
- }
112
- removeFragments(item.fragments);
113
- if (result) return item.text == null ? [] : [item.text];
122
+ const valueIsNullable = isNullableOrWhitespace(value);
123
+ item.text ??= new Text();
124
+ item.text.textContent = valueIsNullable ? "" : getString(value);
125
+ const result = valueIsNullable ? [comment] : [item.text];
126
+ replaceNodes(item.nodes ?? [comment], result);
127
+ return result;
114
128
  }
115
129
  //#endregion
116
130
  export { setReactiveValue };
package/dist/parse.mjs CHANGED
@@ -1,15 +1,17 @@
1
- import { EXPRESSION_ABYDON_ATTRIBUTE_FULL } from "./constants.mjs";
2
- import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
1
+ import { EXPRESSION_ABYDON_ATTRIBUTE_FULL, EXPRESSION_EVENT_ATTRIBUTE, WHITESPACE } from "./constants.mjs";
2
+ import { getString } from "@oscarpalmer/atoms/string";
3
3
  //#region src/parse.ts
4
4
  function handleExpression(data, prefix, expression) {
5
5
  if (Array.isArray(expression)) {
6
+ if (EXPRESSION_EVENT_ATTRIBUTE.test(prefix.split(WHITESPACE).at(-1))) return transformExpression(prefix, data.values.push(expression) - 1);
6
7
  const { length } = expression;
7
8
  let expressions = "";
8
9
  for (let index = 0; index < length; index += 1) expressions += handleExpression(data, "", expression[index]);
9
10
  return `${prefix}${expressions}`;
10
11
  }
11
12
  if (typeof expression === "function" || typeof expression === "object" && expression != null) return transformExpression(prefix, data.values.push(expression) - 1);
12
- return isNullableOrWhitespace(expression) ? prefix : `${prefix}${expression}`;
13
+ const asString = getString(expression);
14
+ return asString.trim().length === 0 ? prefix : `${prefix}${asString}`;
13
15
  }
14
16
  function parse(data) {
15
17
  if (data.template != null) return data.template;
@@ -22,7 +24,7 @@ function parse(data) {
22
24
  return data.template;
23
25
  }
24
26
  function transformAttribute(_, name, index) {
25
- return `_${name}="@abydon.${index}@"`;
27
+ return `abydon-${name}="abydon.${index}"`;
26
28
  }
27
29
  function transformExpression(prefix, index) {
28
30
  return `${prefix}<!--abydon.${index}-->`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/abydon",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "keywords": [
5
5
  "components",
6
6
  "dom",
@@ -42,15 +42,16 @@
42
42
  "test:leak": "npx vp test run --detect-async-leaks --coverage"
43
43
  },
44
44
  "dependencies": {
45
- "@oscarpalmer/atoms": "^0.183",
46
- "@oscarpalmer/mora": "^0.28",
47
- "@oscarpalmer/toretto": "^0.42"
45
+ "@oscarpalmer/atoms": "^0.185",
46
+ "@oscarpalmer/mora": "^0.29",
47
+ "@oscarpalmer/toretto": "^0.45"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@oscarpalmer/oui": "^0.20",
51
+ "@oxlint/plugins": "^1.62",
51
52
  "@types/node": "^25.6",
52
53
  "@vitest/coverage-istanbul": "^4.1",
53
- "jsdom": "^29",
54
+ "jsdom": "^29.1",
54
55
  "tsdown": "^0.21",
55
56
  "typescript": "^5.9",
56
57
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",
@@ -0,0 +1,89 @@
1
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {isSignal} from '@oscarpalmer/mora';
3
+ import {isInputElement} from '@oscarpalmer/toretto/is';
4
+ import {
5
+ EVENT_ON_VALUE,
6
+ EXPRESSION_ABYDON_ATTRIBUTE_PREFIX,
7
+ EXPRESSION_ABYDON_CONTENT,
8
+ EXPRESSION_EVENT_PREFIX,
9
+ PROPERTY_VALUE,
10
+ } from '../constants';
11
+ import {setComputedValue} from '../helpers';
12
+ import type {FragmentData} from '../models';
13
+ import {mapEvent} from '../node/event';
14
+ import {setAttribute} from './value';
15
+
16
+ function compareAttributes(first: Attr, second: Attr): number {
17
+ return first.name.localeCompare(second.name);
18
+ }
19
+
20
+ function getValue(data: FragmentData, original: string): unknown {
21
+ const matches = EXPRESSION_ABYDON_CONTENT.exec(original);
22
+
23
+ return matches == null ? original : data.values[+matches[1]];
24
+ }
25
+
26
+ export function mapAttributeValue(
27
+ data: FragmentData,
28
+ element: HTMLElement | SVGElement,
29
+ name: string,
30
+ value: unknown,
31
+ ): void {
32
+ if (typeof value === 'function') {
33
+ setComputedAttribute(data, element, name, value as GenericCallback);
34
+ } else {
35
+ setAttribute(data, element, name, value);
36
+ }
37
+
38
+ if (isInputElement(element) && isSignal(value) && name in element) {
39
+ mapEvent(element, EVENT_ON_VALUE, () => {
40
+ value.set(element[name as keyof typeof element]);
41
+ });
42
+ }
43
+ }
44
+
45
+ export function mapAttributes(
46
+ data: FragmentData,
47
+ element: HTMLElement | SVGElement,
48
+ ignoreValue: boolean,
49
+ ): void {
50
+ const attributes = [...element.attributes].sort(compareAttributes);
51
+
52
+ const {length} = attributes;
53
+
54
+ for (let index = 0; index < length; index += 1) {
55
+ const {name, value} = attributes[index];
56
+
57
+ const actualName = name.replace(EXPRESSION_ABYDON_ATTRIBUTE_PREFIX, '');
58
+ const actualValue = getValue(data, value);
59
+
60
+ if (actualName !== name) {
61
+ element.removeAttribute(name);
62
+ }
63
+
64
+ if (actualName === PROPERTY_VALUE && ignoreValue) {
65
+ continue;
66
+ }
67
+
68
+ switch (true) {
69
+ case EXPRESSION_EVENT_PREFIX.test(actualName):
70
+ mapEvent(element, actualName, actualValue);
71
+ break;
72
+
73
+ default:
74
+ mapAttributeValue(data, element, actualName, actualValue);
75
+ break;
76
+ }
77
+ }
78
+ }
79
+
80
+ function setComputedAttribute(
81
+ data: FragmentData,
82
+ element: HTMLElement | SVGElement,
83
+ name: string,
84
+ callback: GenericCallback,
85
+ ): void {
86
+ setComputedValue(data, callback, computation => {
87
+ setAttribute(data, element, name, computation);
88
+ });
89
+ }
@@ -0,0 +1,107 @@
1
+ import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
+ import {getString} from '@oscarpalmer/atoms/string';
3
+ import {isReactive} from '@oscarpalmer/mora';
4
+ import {setAttribute as setTorettoAttribute} from '@oscarpalmer/toretto/attribute';
5
+ import {setStyle} from '@oscarpalmer/toretto/style';
6
+ import {
7
+ ATTRIBUTE_CLASS_PREFIX_LENGTH,
8
+ ATTRIBUTE_NAME_DELIMITER,
9
+ EXPRESSION_ATTRIBUTE_CLASS,
10
+ EXPRESSION_ATTRIBUTE_STYLE_FULL,
11
+ EXPRESSION_ATTRIBUTE_STYLE_PREFIX,
12
+ EXPRESSION_ATTRIBUTE_STYLE_VARIABLE,
13
+ VALUE_TRUE,
14
+ } from '../constants';
15
+ import type {FragmentData} from '../models';
16
+
17
+ function getStyleValue(value: unknown, unit: string, isVariable: boolean): string | undefined {
18
+ if (removeStyleValue(value, unit, isVariable)) {
19
+ return;
20
+ }
21
+
22
+ return value === true || value === VALUE_TRUE ? unit : `${getString(value)}${unit ?? ''}`;
23
+ }
24
+
25
+ function removeStyleValue(value: unknown, unit: unknown, isVariable: boolean): boolean {
26
+ if (value == null || value === false || (isVariable && isNullableOrWhitespace(value))) {
27
+ return true;
28
+ }
29
+
30
+ return unit == null && (value === true || value === VALUE_TRUE);
31
+ }
32
+
33
+ export function setAttribute(
34
+ data: FragmentData,
35
+ element: HTMLElement | SVGElement,
36
+ name: string,
37
+ value: unknown,
38
+ ): void {
39
+ switch (true) {
40
+ case EXPRESSION_ATTRIBUTE_CLASS.test(name):
41
+ setClassValues(data, element, name, value);
42
+ return;
43
+
44
+ case EXPRESSION_ATTRIBUTE_STYLE_PREFIX.test(name):
45
+ setStyleValues(data, element, name, value);
46
+ return;
47
+
48
+ default:
49
+ setValue(data, element, name, value);
50
+ break;
51
+ }
52
+ }
53
+
54
+ function setClassValues(
55
+ data: FragmentData,
56
+ element: HTMLElement | SVGElement,
57
+ name: string,
58
+ value: unknown,
59
+ ): void {
60
+ const classes = name.slice(ATTRIBUTE_CLASS_PREFIX_LENGTH).split(ATTRIBUTE_NAME_DELIMITER);
61
+
62
+ updateValue(data, value, value => {
63
+ if (value === true || value === VALUE_TRUE) {
64
+ element.classList.add(...classes);
65
+ } else {
66
+ element.classList.remove(...classes);
67
+ }
68
+ });
69
+ }
70
+
71
+ function setStyleValues(
72
+ data: FragmentData,
73
+ element: HTMLElement | SVGElement,
74
+ name: string,
75
+ value: unknown,
76
+ ): void {
77
+ let [, property, unit] = EXPRESSION_ATTRIBUTE_STYLE_FULL.exec(name)!;
78
+
79
+ const isVariable = EXPRESSION_ATTRIBUTE_STYLE_VARIABLE.test(name);
80
+
81
+ updateValue(data, value, value => {
82
+ setStyle(
83
+ element,
84
+ property as keyof CSSStyleDeclaration,
85
+ getStyleValue(value, unit, isVariable),
86
+ );
87
+ });
88
+ }
89
+
90
+ function setValue(
91
+ data: FragmentData,
92
+ element: HTMLElement | SVGElement,
93
+ name: string,
94
+ value: unknown,
95
+ ): void {
96
+ updateValue(data, value, value => {
97
+ setTorettoAttribute(element, name, value);
98
+ });
99
+ }
100
+
101
+ function updateValue(data: FragmentData, value: unknown, updater: (value: unknown) => void): void {
102
+ if (isReactive(value)) {
103
+ data.mora.subscribers.add(value.subscribe(updater));
104
+ } else {
105
+ updater(value);
106
+ }
107
+ }