@oscarpalmer/abydon 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,18 +6,11 @@ function getValue(data, original) {
6
6
  const matches = commentExpression.exec(original ?? "");
7
7
  return matches == null ? original : data.values[+matches[1]];
8
8
  }
9
- function isBadAttribute(name, value2) {
10
- return /^on/i.test(name) || /^(href|src|xlink:href)$/i.test(name) && /(data:text\/html|javascript:)/i.test(value2);
11
- }
12
9
  function mapAttributes(data, element) {
13
10
  const attributes = [...element.attributes];
14
11
  const { length } = attributes;
15
12
  for (let index = 0;index < length; index += 1) {
16
13
  const { name, value: value2 } = attributes[index];
17
- if (isBadAttribute(name, value2)) {
18
- element.removeAttribute(name);
19
- continue;
20
- }
21
14
  const actual = getValue(data, value2);
22
15
  if (name.startsWith("@")) {
23
16
  mapEvent(element, name, actual);
@@ -38,6 +31,5 @@ function mapValue(element, name, value2) {
38
31
  }
39
32
  var commentExpression = /^<!--abydon\.(\d+)-->$/;
40
33
  export {
41
- mapAttributes,
42
- isBadAttribute
34
+ mapAttributes
43
35
  };
@@ -1,27 +1,30 @@
1
1
  // src/node/attribute/value.ts
2
- import {closest} from "@oscarpalmer/atoms/element";
3
2
  import {getString} from "@oscarpalmer/atoms/string";
4
3
  import {effect, isReactive} from "@oscarpalmer/sentinel";
5
- function setAttribute(element2, name, value) {
6
- element2.removeAttribute(name);
4
+ import {
5
+ isBooleanAttribute,
6
+ setAttribute as setAttr
7
+ } from "@oscarpalmer/toretto/attribute";
8
+ function setAttribute(element, name, value) {
9
+ element.removeAttribute(name);
7
10
  switch (true) {
8
11
  case classPattern.test(name):
9
- setClasses(element2, name, value);
12
+ setClasses(element, name, value);
10
13
  return;
11
14
  case stylePattern.test(name):
12
- setStyle(element2, name, value);
15
+ setStyle(element, name, value);
13
16
  return;
14
17
  default:
15
- setValue(element2, name, value);
18
+ setValue(element, name, value);
16
19
  break;
17
20
  }
18
21
  }
19
- function setClasses(element2, name, value) {
22
+ function setClasses(element, name, value) {
20
23
  function update(value2) {
21
24
  if (/^(|true)$/.test(getString(value2))) {
22
- element2.classList.add(...classes);
25
+ element.classList.add(...classes);
23
26
  } else {
24
- element2.classList.remove(...classes);
27
+ element.classList.remove(...classes);
25
28
  }
26
29
  }
27
30
  const classes = name.slice(6).split(".");
@@ -33,12 +36,12 @@ function setClasses(element2, name, value) {
33
36
  update(value);
34
37
  }
35
38
  }
36
- function setStyle(element2, name, value) {
39
+ function setStyle(element, name, value) {
37
40
  function update(value2) {
38
41
  if (value2 == null || value2 === false || value2 === true && unit == null) {
39
- element2.style.removeProperty(property);
42
+ element.style.removeProperty(property);
40
43
  } else {
41
- element2.style.setProperty(property, value2 === true ? unit : getString(value2));
44
+ element.style.setProperty(property, value2 === true ? unit : getString(value2));
42
45
  }
43
46
  }
44
47
  const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
@@ -53,14 +56,14 @@ function setStyle(element2, name, value) {
53
56
  update(value);
54
57
  }
55
58
  }
56
- function setValue(element2, name, value) {
57
- const callback = booleanAttributes.has(name) && name in element2 ? name === "selected" ? updateSelected(element2) : updateProperty : updateAttribute;
59
+ function setValue(element, name, value) {
60
+ const callback = isBooleanAttribute(name) && name in element ? name === "selected" ? updateSelected : updateProperty : setAttr;
58
61
  if (isReactive(value)) {
59
62
  effect(() => {
60
- callback(element2, name, value.get());
63
+ callback(element, name, value.get());
61
64
  });
62
65
  } else {
63
- callback(element2, name, value);
66
+ callback(element, name, value);
64
67
  }
65
68
  }
66
69
  function triggerSelectChange(select) {
@@ -68,37 +71,17 @@ function triggerSelectChange(select) {
68
71
  select.dispatchEvent(new Event("change", { bubbles: true }));
69
72
  }
70
73
  }
71
- function updateAttribute(element2, name, value) {
72
- if (value == null) {
73
- element2.removeAttribute(name);
74
- } else {
75
- element2.setAttribute(name, getString(value));
76
- }
77
- }
78
- function updateProperty(element2, name, value) {
79
- element2[name] = /^(|true)$/.test(getString(value));
74
+ function updateProperty(element, name, value) {
75
+ element[name] = /^(|true)$/.test(getString(value));
80
76
  }
81
- function updateSelected(element2) {
82
- const select = closest(element2, "select")[0];
77
+ function updateSelected(element, name, value) {
78
+ const select = element.closest("select");
83
79
  const options = [...select?.options ?? []];
84
- return (element3, name, value) => {
85
- if (select != null && options.includes(element3)) {
86
- triggerSelectChange(select);
87
- }
88
- updateProperty(element3, name, value);
89
- };
80
+ if (select != null && options.includes(element)) {
81
+ triggerSelectChange(select);
82
+ }
83
+ updateProperty(element, name, value);
90
84
  }
91
- var booleanAttributes = new Set([
92
- "checked",
93
- "disabled",
94
- "hidden",
95
- "inert",
96
- "multiple",
97
- "open",
98
- "readOnly",
99
- "required",
100
- "selected"
101
- ]);
102
85
  var classPattern = /^class\./;
103
86
  var stylePattern = /^style\./;
104
87
  export {
@@ -1,6 +1,6 @@
1
1
  // src/node/index.ts
2
2
  import {isReactive} from "@oscarpalmer/sentinel";
3
- import {isFragmentElement} from "../helpers";
3
+ import {isFragment, isProperElement} from "../helpers";
4
4
  import {createNodes} from "../helpers/dom";
5
5
  import {mapAttributes} from "./attribute";
6
6
  import {setReactiveNode} from "./value";
@@ -8,7 +8,7 @@ function mapNode(data, comment) {
8
8
  const matches = commentExpression.exec(comment.textContent ?? "");
9
9
  const value2 = matches == null ? null : data.values[+matches[1]];
10
10
  if (value2 != null) {
11
- mapValue(comment, value2);
11
+ mapValue(data, comment, value2);
12
12
  }
13
13
  }
14
14
  function mapNodes(data, nodes) {
@@ -19,7 +19,7 @@ function mapNodes(data, nodes) {
19
19
  mapNode(data, node);
20
20
  continue;
21
21
  }
22
- if (isFragmentElement(node)) {
22
+ if (isProperElement(node)) {
23
23
  mapAttributes(data, node);
24
24
  }
25
25
  if (node.hasChildNodes()) {
@@ -27,19 +27,28 @@ function mapNodes(data, nodes) {
27
27
  }
28
28
  }
29
29
  }
30
- function mapValue(comment, value2) {
30
+ function mapValue(data, comment, value2) {
31
31
  switch (true) {
32
32
  case typeof value2 === "function":
33
- mapValue(comment, value2());
33
+ mapValue(data, comment, value2());
34
34
  return;
35
35
  case isReactive(value2):
36
- setReactiveNode(comment, value2);
36
+ setReactiveNode(data, comment, value2);
37
37
  break;
38
38
  default:
39
- comment.replaceWith(...createNodes(value2));
39
+ replaceComment(data, comment, value2);
40
40
  break;
41
41
  }
42
42
  }
43
+ function replaceComment(data, comment, value2) {
44
+ const item = data.items.find((item2) => item2.nodes.includes(comment));
45
+ const nodes = createNodes(value2);
46
+ if (item != null) {
47
+ item.fragment = isFragment(value2) ? value2 : undefined;
48
+ item.nodes = nodes;
49
+ }
50
+ comment.replaceWith(...nodes);
51
+ }
43
52
  var commentExpression = /^abydon\.(\d+)$/;
44
53
  export {
45
54
  mapNodes
@@ -2,39 +2,56 @@
2
2
  import {isNullableOrWhitespace} from "@oscarpalmer/atoms/is";
3
3
  import {getString} from "@oscarpalmer/atoms/string";
4
4
  import {effect} from "@oscarpalmer/sentinel";
5
- import {isFragment, isFragmentNode} from "../helpers";
5
+ import {isFragment, isProperNode} from "../helpers";
6
6
  import {createNodes, replaceNodes} from "../helpers/dom";
7
- function setReactiveNode(comment, reactive) {
7
+ function setNodes(nodes, comment, text, value) {
8
+ const next = createNodes(value);
9
+ if (nodes == null) {
10
+ if (comment.parentNode != null) {
11
+ replaceNodes([comment], next);
12
+ } else if (text.parentNode != null) {
13
+ replaceNodes([text], next);
14
+ }
15
+ } else {
16
+ replaceNodes(nodes, next);
17
+ }
18
+ return next;
19
+ }
20
+ function setReactiveNode(data, comment, reactive) {
21
+ const item = data.items.find((item2) => item2.nodes.includes(comment));
8
22
  const text = new Text;
9
23
  let nodes;
10
24
  effect(() => {
11
25
  const value = reactive.get();
12
- if (isFragment(value) || isFragmentNode(value)) {
13
- const next = createNodes(value);
14
- if (nodes == null) {
15
- if (comment.parentNode != null) {
16
- replaceNodes([comment], next);
17
- } else if (text.parentNode != null) {
18
- replaceNodes([text], next);
19
- }
20
- } else {
21
- replaceNodes(nodes, next);
22
- }
23
- nodes = next;
24
- return;
26
+ const valueIsFragment = isFragment(value);
27
+ if (valueIsFragment || isProperNode(value)) {
28
+ nodes = setNodes(nodes, comment, text, value);
29
+ } else {
30
+ nodes = setText(nodes, comment, text, value) ? [text] : undefined;
25
31
  }
26
- const isNullable = isNullableOrWhitespace(value);
27
- text.textContent = getString(value);
28
- if (nodes != null) {
29
- replaceNodes(nodes, [isNullable ? comment : text]);
30
- } else if (isNullable && comment.parentNode == null) {
31
- text.replaceWith(comment);
32
- } else if (!isNullable && text.parentNode == null) {
33
- comment.replaceWith(text);
32
+ if (item != null) {
33
+ item.fragment = valueIsFragment ? value : undefined;
34
+ item.nodes = [...nodes ?? [comment]];
34
35
  }
35
- nodes = undefined;
36
36
  });
37
37
  }
38
+ function setText(nodes, comment, text, value) {
39
+ const isNullable = isNullableOrWhitespace(value);
40
+ text.textContent = isNullable ? "" : getString(value);
41
+ if (nodes != null) {
42
+ replaceNodes(nodes, [isNullable ? comment : text]);
43
+ return !isNullable;
44
+ }
45
+ if (isNullable && comment.parentNode == null) {
46
+ text.replaceWith(comment);
47
+ return false;
48
+ }
49
+ if (!isNullable && text.parentNode == null) {
50
+ comment.replaceWith(text);
51
+ return true;
52
+ }
53
+ return false;
54
+ }
38
55
  export {
39
56
  setReactiveNode
40
57
  };
package/package.json CHANGED
@@ -4,13 +4,14 @@
4
4
  "url": "https://oscarpalmer.se"
5
5
  },
6
6
  "dependencies": {
7
- "@oscarpalmer/atoms": "^0.67.0",
8
- "@oscarpalmer/sentinel": "^0.23.0"
7
+ "@oscarpalmer/atoms": "^0.71.0",
8
+ "@oscarpalmer/sentinel": "^0.23.0",
9
+ "@oscarpalmer/toretto": "0.12.1"
9
10
  },
10
11
  "devDependencies": {
11
12
  "@biomejs/biome": "^1.8.3",
12
13
  "@types/bun": "^1.1.6",
13
- "bun": "^1.1.21",
14
+ "bun": "^1.1.24",
14
15
  "dts-bundle-generator": "^9.5.1",
15
16
  "typescript": "^5.5.4"
16
17
  },
@@ -46,5 +47,5 @@
46
47
  },
47
48
  "type": "module",
48
49
  "types": "types/index.d.ts",
49
- "version": "0.8.0"
50
+ "version": "0.9.0"
50
51
  }
package/src/fragment.ts CHANGED
@@ -1,17 +1,17 @@
1
- import {createTemplate, getNodes} from './helpers/dom';
1
+ import {html} from '@oscarpalmer/toretto/html';
2
2
  import {parse} from './html';
3
- import type {FragmentData} from './models';
3
+ import type {FragmentData, ProperNode} from './models';
4
4
  import {mapNodes} from './node';
5
5
 
6
6
  export class Fragment {
7
- private declare readonly $fragment = true;
7
+ private readonly $fragment = true;
8
8
  private readonly data: FragmentData;
9
9
 
10
10
  constructor(strings: TemplateStringsArray, expressions: unknown[]) {
11
11
  this.data = {
12
12
  expressions,
13
13
  strings,
14
- nodes: [],
14
+ items: [],
15
15
  values: [],
16
16
  };
17
17
  }
@@ -27,30 +27,52 @@ export class Fragment {
27
27
  * Gets a list of the fragment's nodes
28
28
  */
29
29
  get(): Node[] {
30
- if (this.data.nodes.length === 0) {
30
+ if (this.data.items.length === 0) {
31
31
  const parsed = parse(this.data);
32
- const templated = createTemplate(parsed);
33
32
 
34
- this.data.nodes.splice(0, this.data.nodes.length, ...getNodes(templated));
33
+ const templated = html(parsed, {
34
+ sanitiseBooleanAttributes: false,
35
+ });
35
36
 
36
- mapNodes(this.data, this.data.nodes);
37
+ this.data.items.splice(
38
+ 0,
39
+ this.data.items.length,
40
+ ...templated.map(node => ({
41
+ nodes: [node as ProperNode],
42
+ })),
43
+ );
44
+
45
+ mapNodes(
46
+ this.data,
47
+ this.data.items.flatMap(
48
+ item => item.fragment?.get() ?? item.nodes,
49
+ ) as ProperNode[],
50
+ );
37
51
  }
38
52
 
39
- return [...this.data.nodes];
53
+ return [
54
+ ...(this.data.items.flatMap(
55
+ item => item.fragment?.get() ?? item.nodes,
56
+ ) as ProperNode[]),
57
+ ];
40
58
  }
41
59
 
42
60
  /**
43
61
  * Removes the fragment from the DOM
44
62
  */
45
63
  remove(): void {
46
- const {length} = this.data.nodes;
64
+ const {length} = this.data.items;
47
65
 
48
66
  for (let index = 0; index < length; index += 1) {
49
- const node = this.data.nodes[index];
67
+ const item = this.data.items[index];
68
+
69
+ item.fragment?.remove();
50
70
 
51
- node.parentNode?.removeChild(node);
71
+ for (const node of item.nodes) {
72
+ node.remove();
73
+ }
52
74
  }
53
75
 
54
- this.data.nodes.splice(0, length);
76
+ this.data.items.splice(0, length);
55
77
  }
56
78
  }
@@ -1,48 +1,20 @@
1
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
- }
2
+ import type {ProperNode} from '../models';
3
+ import {isFragment, isProperNode} from './index';
9
4
 
5
+ export function createNodes(value: unknown): ProperNode[] {
10
6
  if (isFragment(value)) {
11
- return value.get() as FragmentNode[];
7
+ return value.get() as ProperNode[];
12
8
  }
13
9
 
14
- return [new Text(getString(value))];
15
- }
16
-
17
- export function createTemplate(html: string): Node {
18
- const template = document.createElement('template');
19
-
20
- template.innerHTML = html;
21
-
22
- const cloned = template.content.cloneNode(true);
23
-
24
- const scripts = [
25
- ...(cloned instanceof Element ? cloned.querySelectorAll('script') : []),
26
- ];
27
-
28
- const {length} = scripts;
29
-
30
- for (let index = 0; index < length; index += 1) {
31
- scripts[index].remove();
10
+ if (isProperNode(value)) {
11
+ return [value];
32
12
  }
33
13
 
34
- cloned.normalize();
35
-
36
- return cloned;
37
- }
38
-
39
- export function getNodes(node: Node): Node[] {
40
- return /^documentfragment$/i.test(node.constructor.name)
41
- ? [...node.childNodes]
42
- : [node];
14
+ return [new Text(getString(value))];
43
15
  }
44
16
 
45
- export function replaceNodes(from: FragmentNode[], to: FragmentNode[]): void {
17
+ export function replaceNodes(from: ProperNode[], to: ProperNode[]): void {
46
18
  const {length} = from;
47
19
 
48
20
  for (let index = 0; index < length; index += 1) {
@@ -1,5 +1,5 @@
1
1
  import type {Fragment} from '../fragment';
2
- import type {FragmentElement, FragmentNode} from '../models';
2
+ import type {ProperElement, ProperNode} from '../models';
3
3
 
4
4
  export function isFragment(value: unknown): value is Fragment {
5
5
  return (
@@ -10,14 +10,10 @@ export function isFragment(value: unknown): value is Fragment {
10
10
  );
11
11
  }
12
12
 
13
- export function isFragmentElement(value: unknown): value is FragmentElement {
13
+ export function isProperElement(value: unknown): value is ProperElement {
14
14
  return value instanceof HTMLElement || value instanceof SVGElement;
15
15
  }
16
16
 
17
- export function isFragmentNode(value: unknown): value is FragmentNode {
18
- return (
19
- value instanceof Comment ||
20
- value instanceof Element ||
21
- value instanceof Text
22
- );
17
+ export function isProperNode(value: unknown): value is ProperNode {
18
+ return value instanceof CharacterData || value instanceof Element;
23
19
  }
package/src/models.ts CHANGED
@@ -1,10 +1,17 @@
1
+ import type {Fragment} from './fragment';
2
+
1
3
  export type FragmentData = {
2
4
  expressions: unknown[];
3
- nodes: Node[];
5
+ items: FragmentItem[];
4
6
  strings: TemplateStringsArray;
5
7
  values: unknown[];
6
8
  };
7
9
 
8
- export type FragmentElement = HTMLElement | SVGElement;
10
+ export type FragmentItem = {
11
+ fragment?: Fragment;
12
+ nodes: ProperNode[];
13
+ };
14
+
15
+ export type ProperElement = HTMLElement | SVGElement;
9
16
 
10
- export type FragmentNode = Comment | Element | Text;
17
+ export type ProperNode = CharacterData | Element;
@@ -1,5 +1,5 @@
1
1
  import {isReactive} from '@oscarpalmer/sentinel';
2
- import type {FragmentData, FragmentElement} from '../../models';
2
+ import type {FragmentData, ProperElement} from '../../models';
3
3
  import {mapEvent} from '../event';
4
4
  import {setAttribute} from './value';
5
5
 
@@ -11,45 +11,24 @@ function getValue(data: FragmentData, original: string): unknown {
11
11
  return matches == null ? original : data.values[+matches[1]];
12
12
  }
13
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];
14
+ export function mapAttributes(data: FragmentData, element: ProperElement): void {
15
+ const attributes = [...element.attributes];
16
+ const {length} = attributes;
31
17
 
32
- if (isBadAttribute(name, value)) {
33
- element.removeAttribute(name);
18
+ for (let index = 0; index < length; index += 1) {
19
+ const {name, value} = attributes[index];
34
20
 
35
- continue;
36
- }
37
-
38
- const actual = getValue(data, value);
21
+ const actual = getValue(data, value);
39
22
 
40
- if (name.startsWith('@')) {
41
- mapEvent(element, name, actual);
42
- } else if (name.includes('.') || isReactive(actual)) {
43
- mapValue(element, name, actual);
23
+ if (name.startsWith('@')) {
24
+ mapEvent(element, name, actual);
25
+ } else if (name.includes('.') || isReactive(actual)) {
26
+ mapValue(element, name, actual);
27
+ }
44
28
  }
45
29
  }
46
- }
47
30
 
48
- function mapValue(
49
- element: FragmentElement,
50
- name: string,
51
- value: unknown,
52
- ): void {
31
+ function mapValue(element: ProperElement, name: string, value: unknown): void {
53
32
  switch (true) {
54
33
  case typeof value === 'function':
55
34
  mapValue(element, name, value());