@oscarpalmer/abydon 0.7.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ // src/index.ts
2
+ import {html} from "./html";
3
+ export {
4
+ html
5
+ };
File without changes
@@ -0,0 +1,35 @@
1
+ // src/node/attribute/index.ts
2
+ import {isReactive} from "@oscarpalmer/sentinel";
3
+ import {mapEvent} from "../event";
4
+ import {setAttribute} from "./value";
5
+ function getValue(data, original) {
6
+ const matches = commentExpression.exec(original ?? "");
7
+ return matches == null ? original : data.values[+matches[1]];
8
+ }
9
+ function mapAttributes(data, element) {
10
+ const attributes = [...element.attributes];
11
+ const { length } = attributes;
12
+ for (let index = 0;index < length; index += 1) {
13
+ const { name, value: value2 } = attributes[index];
14
+ const actual = getValue(data, value2);
15
+ if (name.startsWith("@")) {
16
+ mapEvent(element, name, actual);
17
+ } else if (name.includes(".") || isReactive(actual)) {
18
+ mapValue(element, name, actual);
19
+ }
20
+ }
21
+ }
22
+ function mapValue(element, name, value2) {
23
+ switch (true) {
24
+ case typeof value2 === "function":
25
+ mapValue(element, name, value2());
26
+ return;
27
+ default:
28
+ setAttribute(element, name, value2);
29
+ break;
30
+ }
31
+ }
32
+ var commentExpression = /^<!--abydon\.(\d+)-->$/;
33
+ export {
34
+ mapAttributes
35
+ };
@@ -0,0 +1,89 @@
1
+ // src/node/attribute/value.ts
2
+ import {getString} from "@oscarpalmer/atoms/string";
3
+ import {effect, isReactive} from "@oscarpalmer/sentinel";
4
+ import {
5
+ isBooleanAttribute,
6
+ setAttribute as setAttr
7
+ } from "@oscarpalmer/toretto/attribute";
8
+ function setAttribute(element, name, value) {
9
+ element.removeAttribute(name);
10
+ switch (true) {
11
+ case classPattern.test(name):
12
+ setClasses(element, name, value);
13
+ return;
14
+ case stylePattern.test(name):
15
+ setStyle(element, name, value);
16
+ return;
17
+ default:
18
+ setValue(element, name, value);
19
+ break;
20
+ }
21
+ }
22
+ function setClasses(element, name, value) {
23
+ function update(value2) {
24
+ if (/^(|true)$/.test(getString(value2))) {
25
+ element.classList.add(...classes);
26
+ } else {
27
+ element.classList.remove(...classes);
28
+ }
29
+ }
30
+ const classes = name.slice(6).split(".");
31
+ if (isReactive(value)) {
32
+ effect(() => {
33
+ update(value.get());
34
+ });
35
+ } else {
36
+ update(value);
37
+ }
38
+ }
39
+ function setStyle(element, name, value) {
40
+ function update(value2) {
41
+ if (value2 == null || value2 === false || value2 === true && unit == null) {
42
+ element.style.removeProperty(property);
43
+ } else {
44
+ element.style.setProperty(property, value2 === true ? unit : getString(value2));
45
+ }
46
+ }
47
+ const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
48
+ if (property == null) {
49
+ return;
50
+ }
51
+ if (isReactive(value)) {
52
+ effect(() => {
53
+ update(value.get());
54
+ });
55
+ } else {
56
+ update(value);
57
+ }
58
+ }
59
+ function setValue(element, name, value) {
60
+ const callback = isBooleanAttribute(name) && name in element ? name === "selected" ? updateSelected : updateProperty : setAttr;
61
+ if (isReactive(value)) {
62
+ effect(() => {
63
+ callback(element, name, value.get());
64
+ });
65
+ } else {
66
+ callback(element, name, value);
67
+ }
68
+ }
69
+ function triggerSelectChange(select) {
70
+ if (select != null) {
71
+ select.dispatchEvent(new Event("change", { bubbles: true }));
72
+ }
73
+ }
74
+ function updateProperty(element, name, value) {
75
+ element[name] = /^(|true)$/.test(getString(value));
76
+ }
77
+ function updateSelected(element, name, value) {
78
+ const select = element.closest("select");
79
+ const options = [...select?.options ?? []];
80
+ if (select != null && options.includes(element)) {
81
+ triggerSelectChange(select);
82
+ }
83
+ updateProperty(element, name, value);
84
+ }
85
+ var classPattern = /^class\./;
86
+ var stylePattern = /^style\./;
87
+ export {
88
+ setAttribute
89
+ };
@@ -0,0 +1,19 @@
1
+ // src/node/event.ts
2
+ function getOptions(options) {
3
+ const parts = options.split(":");
4
+ return {
5
+ capture: parts.includes("c") || parts.includes("capture"),
6
+ once: parts.includes("o") || parts.includes("once"),
7
+ passive: !parts.includes("a") && !parts.includes("active")
8
+ };
9
+ }
10
+ function mapEvent(element, name, value) {
11
+ element.removeAttribute(name);
12
+ const [, type, options] = /^@(\w+)(?::([a-z:]+))?$/.exec(name) ?? [];
13
+ if (typeof value === "function" && type != null) {
14
+ element.addEventListener(type, value, getOptions(options ?? ""));
15
+ }
16
+ }
17
+ export {
18
+ mapEvent
19
+ };
@@ -0,0 +1,55 @@
1
+ // src/node/index.ts
2
+ import {isReactive} from "@oscarpalmer/sentinel";
3
+ import {isFragment, isProperElement} from "../helpers";
4
+ import {createNodes} from "../helpers/dom";
5
+ import {mapAttributes} from "./attribute";
6
+ import {setReactiveNode} from "./value";
7
+ function mapNode(data, comment) {
8
+ const matches = commentExpression.exec(comment.textContent ?? "");
9
+ const value2 = matches == null ? null : data.values[+matches[1]];
10
+ if (value2 != null) {
11
+ mapValue(data, comment, value2);
12
+ }
13
+ }
14
+ function mapNodes(data, nodes) {
15
+ const { length } = nodes;
16
+ for (let index = 0;index < length; index += 1) {
17
+ const node = nodes[index];
18
+ if (node instanceof Comment) {
19
+ mapNode(data, node);
20
+ continue;
21
+ }
22
+ if (isProperElement(node)) {
23
+ mapAttributes(data, node);
24
+ }
25
+ if (node.hasChildNodes()) {
26
+ mapNodes(data, [...node.childNodes]);
27
+ }
28
+ }
29
+ }
30
+ function mapValue(data, comment, value2) {
31
+ switch (true) {
32
+ case typeof value2 === "function":
33
+ mapValue(data, comment, value2());
34
+ return;
35
+ case isReactive(value2):
36
+ setReactiveNode(data, comment, value2);
37
+ break;
38
+ default:
39
+ replaceComment(data, comment, value2);
40
+ break;
41
+ }
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
+ }
52
+ var commentExpression = /^abydon\.(\d+)$/;
53
+ export {
54
+ mapNodes
55
+ };
@@ -0,0 +1,57 @@
1
+ // src/node/value.ts
2
+ import {isNullableOrWhitespace} from "@oscarpalmer/atoms/is";
3
+ import {getString} from "@oscarpalmer/atoms/string";
4
+ import {effect} from "@oscarpalmer/sentinel";
5
+ import {isFragment, isProperNode} from "../helpers";
6
+ import {createNodes, replaceNodes} from "../helpers/dom";
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));
22
+ const text = new Text;
23
+ let nodes;
24
+ effect(() => {
25
+ const value = reactive.get();
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;
31
+ }
32
+ if (item != null) {
33
+ item.fragment = valueIsFragment ? value : undefined;
34
+ item.nodes = [...nodes ?? [comment]];
35
+ }
36
+ });
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
+ }
55
+ export {
56
+ setReactiveNode
57
+ };
package/package.json CHANGED
@@ -4,23 +4,31 @@
4
4
  "url": "https://oscarpalmer.se"
5
5
  },
6
6
  "dependencies": {
7
- "@oscarpalmer/atoms": "^0.61.0",
8
- "@oscarpalmer/sentinel": "^0.22.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.20",
14
+ "bun": "^1.1.24",
15
+ "dts-bundle-generator": "^9.5.1",
14
16
  "typescript": "^5.5.4"
15
17
  },
16
18
  "exports": {
17
19
  ".": {
18
- "types": "./types/index.d.ts",
19
20
  "bun": "./src/index.ts",
20
- "import": "./dist/index.mjs",
21
- "require": "./dist/index.js"
21
+ "import": {
22
+ "types": "./types/index.d.ts",
23
+ "default": "./dist/index.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./types/index.d.cts",
27
+ "default": "./dist/index.js"
28
+ }
22
29
  }
23
30
  },
31
+ "files": ["dist/index.js", "dist/**/*.mjs", "src", "types"],
24
32
  "license": "MIT",
25
33
  "main": "dist/index.js",
26
34
  "module": "dist/index.mjs",
@@ -32,9 +40,12 @@
32
40
  "scripts": {
33
41
  "build": "bun run clean && bunx bun ./.bun.ts && bunx bun ./.bun.ts --mjs && bun run types",
34
42
  "clean": "rm -rf ./dist && rm -rf ./types && rm -rf ./tsconfig.tsbuildinfo",
35
- "types": "bunx tsc -p ./tsconfig.json",
43
+ "types": "bun run types:cjs && bun run types:esm",
44
+ "types:cjs": "bunx dts-bundle-generator --out-file ./types/index.d.cts --no-check --silent ./src/index.ts",
45
+ "types:esm": "bunx tsc -p ./tsconfig.json",
36
46
  "watch": "bun build ./src/index.ts --outfile ./dist/index.js --watch"
37
47
  },
38
- "types": "src/types.d.ts",
39
- "version": "0.7.0"
48
+ "type": "module",
49
+ "types": "types/index.d.ts",
50
+ "version": "0.9.0"
40
51
  }
package/src/fragment.ts CHANGED
@@ -1,51 +1,78 @@
1
- import {createTemplate, getNodes} from './helpers/dom';
1
+ import {html} from '@oscarpalmer/toretto/html';
2
2
  import {parse} from './html';
3
- import type {Fragment, FragmentData} from './models';
3
+ import type {FragmentData, ProperNode} from './models';
4
4
  import {mapNodes} from './node';
5
5
 
6
- export function createFragment(
7
- strings: TemplateStringsArray,
8
- expressions: unknown[],
9
- ): Fragment {
10
- const data: FragmentData = {
11
- expressions,
12
- strings,
13
- nodes: [],
14
- values: [],
15
- };
16
-
17
- const instance = Object.create({
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);
29
- }
6
+ export class Fragment {
7
+ private readonly $fragment = true;
8
+ private readonly data: FragmentData;
30
9
 
31
- return [...data.nodes];
32
- },
33
- remove() {
34
- const {length} = data.nodes;
10
+ constructor(strings: TemplateStringsArray, expressions: unknown[]) {
11
+ this.data = {
12
+ expressions,
13
+ strings,
14
+ items: [],
15
+ values: [],
16
+ };
17
+ }
35
18
 
36
- for (let index = 0; index < length; index += 1) {
37
- const node = data.nodes[index];
19
+ /**
20
+ * Appends the fragment to the given element
21
+ */
22
+ appendTo(element: Element): void {
23
+ element.append(...this.get());
24
+ }
38
25
 
39
- node.parentNode?.removeChild(node);
40
- }
26
+ /**
27
+ * Gets a list of the fragment's nodes
28
+ */
29
+ get(): Node[] {
30
+ if (this.data.items.length === 0) {
31
+ const parsed = parse(this.data);
32
+
33
+ const templated = html(parsed, {
34
+ sanitiseBooleanAttributes: false,
35
+ });
36
+
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
+ );
51
+ }
41
52
 
42
- data.nodes.splice(0, data.nodes.length);
43
- },
44
- } as Fragment);
53
+ return [
54
+ ...(this.data.items.flatMap(
55
+ item => item.fragment?.get() ?? item.nodes,
56
+ ) as ProperNode[]),
57
+ ];
58
+ }
45
59
 
46
- Object.defineProperty(instance, '$fragment', {
47
- value: true,
48
- });
60
+ /**
61
+ * Removes the fragment from the DOM
62
+ */
63
+ remove(): void {
64
+ const {length} = this.data.items;
65
+
66
+ for (let index = 0; index < length; index += 1) {
67
+ const item = this.data.items[index];
68
+
69
+ item.fragment?.remove();
70
+
71
+ for (const node of item.nodes) {
72
+ node.remove();
73
+ }
74
+ }
49
75
 
50
- return instance;
76
+ this.data.items.splice(0, length);
77
+ }
51
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();
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,4 +1,5 @@
1
- import type {Fragment, FragmentElement, FragmentNode} from '../models';
1
+ import type {Fragment} from '../fragment';
2
+ import type {ProperElement, ProperNode} from '../models';
2
3
 
3
4
  export function isFragment(value: unknown): value is Fragment {
4
5
  return (
@@ -9,14 +10,10 @@ export function isFragment(value: unknown): value is Fragment {
9
10
  );
10
11
  }
11
12
 
12
- export function isFragmentElement(value: unknown): value is FragmentElement {
13
+ export function isProperElement(value: unknown): value is ProperElement {
13
14
  return value instanceof HTMLElement || value instanceof SVGElement;
14
15
  }
15
16
 
16
- export function isFragmentNode(value: unknown): value is FragmentNode {
17
- return (
18
- value instanceof Comment ||
19
- value instanceof Element ||
20
- value instanceof Text
21
- );
17
+ export function isProperNode(value: unknown): value is ProperNode {
18
+ return value instanceof CharacterData || value instanceof Element;
22
19
  }
package/src/html.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
- import {createFragment} from './fragment';
3
- import type {Fragment, FragmentData} from './models';
2
+ import {Fragment} from './fragment';
3
+ import type {FragmentData} from './models';
4
4
 
5
5
  function handleExpression(
6
6
  data: FragmentData,
@@ -36,7 +36,7 @@ export function html(
36
36
  strings: TemplateStringsArray,
37
37
  ...values: unknown[]
38
38
  ): Fragment {
39
- return createFragment(strings, values);
39
+ return new Fragment(strings, values);
40
40
  }
41
41
 
42
42
  export function parse(data: FragmentData): string {
@@ -53,4 +53,4 @@ export function parse(data: FragmentData): string {
53
53
  }
54
54
 
55
55
  return template;
56
- }
56
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type {Fragment} from './models';
1
+ export type {Fragment} from './fragment';
2
2
  export {html} from './html';
package/src/models.ts CHANGED
@@ -1,16 +1,17 @@
1
- export type Fragment = {
2
- appendTo(element: Element): void;
3
- get(): FragmentNode[];
4
- remove(): void;
5
- };
1
+ import type {Fragment} from './fragment';
6
2
 
7
3
  export type FragmentData = {
8
4
  expressions: unknown[];
9
- nodes: Node[];
5
+ items: FragmentItem[];
10
6
  strings: TemplateStringsArray;
11
7
  values: unknown[];
12
8
  };
13
9
 
14
- export type FragmentElement = HTMLElement | SVGElement;
10
+ export type FragmentItem = {
11
+ fragment?: Fragment;
12
+ nodes: ProperNode[];
13
+ };
14
+
15
+ export type ProperElement = HTMLElement | SVGElement;
15
16
 
16
- 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());