@oscarpalmer/abydon 0.6.0 → 0.8.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.
@@ -0,0 +1,43 @@
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 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
+ function mapAttributes(data, element) {
13
+ const attributes = [...element.attributes];
14
+ const { length } = attributes;
15
+ for (let index = 0;index < length; index += 1) {
16
+ const { name, value: value2 } = attributes[index];
17
+ if (isBadAttribute(name, value2)) {
18
+ element.removeAttribute(name);
19
+ continue;
20
+ }
21
+ const actual = getValue(data, value2);
22
+ if (name.startsWith("@")) {
23
+ mapEvent(element, name, actual);
24
+ } else if (name.includes(".") || isReactive(actual)) {
25
+ mapValue(element, name, actual);
26
+ }
27
+ }
28
+ }
29
+ function mapValue(element, name, value2) {
30
+ switch (true) {
31
+ case typeof value2 === "function":
32
+ mapValue(element, name, value2());
33
+ return;
34
+ default:
35
+ setAttribute(element, name, value2);
36
+ break;
37
+ }
38
+ }
39
+ var commentExpression = /^<!--abydon\.(\d+)-->$/;
40
+ export {
41
+ mapAttributes,
42
+ isBadAttribute
43
+ };
@@ -0,0 +1,106 @@
1
+ // src/node/attribute/value.ts
2
+ import {closest} from "@oscarpalmer/atoms/element";
3
+ import {getString} from "@oscarpalmer/atoms/string";
4
+ import {effect, isReactive} from "@oscarpalmer/sentinel";
5
+ function setAttribute(element2, name, value) {
6
+ element2.removeAttribute(name);
7
+ switch (true) {
8
+ case classPattern.test(name):
9
+ setClasses(element2, name, value);
10
+ return;
11
+ case stylePattern.test(name):
12
+ setStyle(element2, name, value);
13
+ return;
14
+ default:
15
+ setValue(element2, name, value);
16
+ break;
17
+ }
18
+ }
19
+ function setClasses(element2, name, value) {
20
+ function update(value2) {
21
+ if (/^(|true)$/.test(getString(value2))) {
22
+ element2.classList.add(...classes);
23
+ } else {
24
+ element2.classList.remove(...classes);
25
+ }
26
+ }
27
+ const classes = name.slice(6).split(".");
28
+ if (isReactive(value)) {
29
+ effect(() => {
30
+ update(value.get());
31
+ });
32
+ } else {
33
+ update(value);
34
+ }
35
+ }
36
+ function setStyle(element2, name, value) {
37
+ function update(value2) {
38
+ if (value2 == null || value2 === false || value2 === true && unit == null) {
39
+ element2.style.removeProperty(property);
40
+ } else {
41
+ element2.style.setProperty(property, value2 === true ? unit : getString(value2));
42
+ }
43
+ }
44
+ const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
45
+ if (property == null) {
46
+ return;
47
+ }
48
+ if (isReactive(value)) {
49
+ effect(() => {
50
+ update(value.get());
51
+ });
52
+ } else {
53
+ update(value);
54
+ }
55
+ }
56
+ function setValue(element2, name, value) {
57
+ const callback = booleanAttributes.has(name) && name in element2 ? name === "selected" ? updateSelected(element2) : updateProperty : updateAttribute;
58
+ if (isReactive(value)) {
59
+ effect(() => {
60
+ callback(element2, name, value.get());
61
+ });
62
+ } else {
63
+ callback(element2, name, value);
64
+ }
65
+ }
66
+ function triggerSelectChange(select) {
67
+ if (select != null) {
68
+ select.dispatchEvent(new Event("change", { bubbles: true }));
69
+ }
70
+ }
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));
80
+ }
81
+ function updateSelected(element2) {
82
+ const select = closest(element2, "select")[0];
83
+ 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
+ };
90
+ }
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
+ var classPattern = /^class\./;
103
+ var stylePattern = /^style\./;
104
+ export {
105
+ setAttribute
106
+ };
@@ -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,46 @@
1
+ // src/node/index.ts
2
+ import {isReactive} from "@oscarpalmer/sentinel";
3
+ import {isFragmentElement} 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(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 (isFragmentElement(node)) {
23
+ mapAttributes(data, node);
24
+ }
25
+ if (node.hasChildNodes()) {
26
+ mapNodes(data, [...node.childNodes]);
27
+ }
28
+ }
29
+ }
30
+ function mapValue(comment, value2) {
31
+ switch (true) {
32
+ case typeof value2 === "function":
33
+ mapValue(comment, value2());
34
+ return;
35
+ case isReactive(value2):
36
+ setReactiveNode(comment, value2);
37
+ break;
38
+ default:
39
+ comment.replaceWith(...createNodes(value2));
40
+ break;
41
+ }
42
+ }
43
+ var commentExpression = /^abydon\.(\d+)$/;
44
+ export {
45
+ mapNodes
46
+ };
@@ -0,0 +1,40 @@
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, isFragmentNode} from "../helpers";
6
+ import {createNodes, replaceNodes} from "../helpers/dom";
7
+ function setReactiveNode(comment, reactive) {
8
+ const text = new Text;
9
+ let nodes;
10
+ effect(() => {
11
+ 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;
25
+ }
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);
34
+ }
35
+ nodes = undefined;
36
+ });
37
+ }
38
+ export {
39
+ setReactiveNode
40
+ };
package/package.json CHANGED
@@ -4,23 +4,30 @@
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.67.0",
8
+ "@oscarpalmer/sentinel": "^0.23.0"
9
9
  },
10
10
  "devDependencies": {
11
11
  "@biomejs/biome": "^1.8.3",
12
12
  "@types/bun": "^1.1.6",
13
- "bun": "^1.1.20",
13
+ "bun": "^1.1.21",
14
+ "dts-bundle-generator": "^9.5.1",
14
15
  "typescript": "^5.5.4"
15
16
  },
16
17
  "exports": {
17
18
  ".": {
18
- "types": "./types/index.d.ts",
19
19
  "bun": "./src/index.ts",
20
- "import": "./dist/index.mjs",
21
- "require": "./dist/index.js"
20
+ "import": {
21
+ "types": "./types/index.d.ts",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "require": {
25
+ "types": "./types/index.d.cts",
26
+ "default": "./dist/index.js"
27
+ }
22
28
  }
23
29
  },
30
+ "files": ["dist/index.js", "dist/**/*.mjs", "src", "types"],
24
31
  "license": "MIT",
25
32
  "main": "dist/index.js",
26
33
  "module": "dist/index.mjs",
@@ -32,9 +39,12 @@
32
39
  "scripts": {
33
40
  "build": "bun run clean && bunx bun ./.bun.ts && bunx bun ./.bun.ts --mjs && bun run types",
34
41
  "clean": "rm -rf ./dist && rm -rf ./types && rm -rf ./tsconfig.tsbuildinfo",
35
- "types": "bunx tsc -p ./tsconfig.json",
42
+ "types": "bun run types:cjs && bun run types:esm",
43
+ "types:cjs": "bunx dts-bundle-generator --out-file ./types/index.d.cts --no-check --silent ./src/index.ts",
44
+ "types:esm": "bunx tsc -p ./tsconfig.json",
36
45
  "watch": "bun build ./src/index.ts --outfile ./dist/index.js --watch"
37
46
  },
38
- "types": "src/types.d.ts",
39
- "version": "0.6.0"
47
+ "type": "module",
48
+ "types": "types/index.d.ts",
49
+ "version": "0.8.0"
40
50
  }
package/src/fragment.ts CHANGED
@@ -1,51 +1,56 @@
1
1
  import {createTemplate, getNodes} from './helpers/dom';
2
2
  import {parse} from './html';
3
- import type {Fragment, FragmentData} from './models';
3
+ import type {FragmentData} 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
- }
30
-
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];
38
-
39
- node.parentNode?.removeChild(node);
40
- }
41
-
42
- data.nodes.splice(0, data.nodes.length);
43
- },
44
- } as Fragment);
45
-
46
- Object.defineProperty(instance, '$fragment', {
47
- value: true,
48
- });
49
-
50
- return instance;
6
+ export class Fragment {
7
+ private declare readonly $fragment = true;
8
+ private readonly data: FragmentData;
9
+
10
+ constructor(strings: TemplateStringsArray, expressions: unknown[]) {
11
+ this.data = {
12
+ expressions,
13
+ strings,
14
+ nodes: [],
15
+ values: [],
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Appends the fragment to the given element
21
+ */
22
+ appendTo(element: Element): void {
23
+ element.append(...this.get());
24
+ }
25
+
26
+ /**
27
+ * Gets a list of the fragment's nodes
28
+ */
29
+ get(): Node[] {
30
+ if (this.data.nodes.length === 0) {
31
+ const parsed = parse(this.data);
32
+ const templated = createTemplate(parsed);
33
+
34
+ this.data.nodes.splice(0, this.data.nodes.length, ...getNodes(templated));
35
+
36
+ mapNodes(this.data, this.data.nodes);
37
+ }
38
+
39
+ return [...this.data.nodes];
40
+ }
41
+
42
+ /**
43
+ * Removes the fragment from the DOM
44
+ */
45
+ remove(): void {
46
+ const {length} = this.data.nodes;
47
+
48
+ for (let index = 0; index < length; index += 1) {
49
+ const node = this.data.nodes[index];
50
+
51
+ node.parentNode?.removeChild(node);
52
+ }
53
+
54
+ this.data.nodes.splice(0, length);
55
+ }
51
56
  }
@@ -8,7 +8,7 @@ export function createNodes(value: unknown): FragmentNode[] {
8
8
  }
9
9
 
10
10
  if (isFragment(value)) {
11
- return value.get();
11
+ return value.get() as FragmentNode[];
12
12
  }
13
13
 
14
14
  return [new Text(getString(value))];
@@ -1,4 +1,5 @@
1
- import type {Fragment, FragmentElement, FragmentNode} from '../models';
1
+ import type {Fragment} from '../fragment';
2
+ import type {FragmentElement, FragmentNode} from '../models';
2
3
 
3
4
  export function isFragment(value: unknown): value is Fragment {
4
5
  return (
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,
@@ -23,7 +23,7 @@ function handleExpression(
23
23
  return `${prefix}${expressions}`;
24
24
  }
25
25
 
26
- if (typeof expression === 'object') {
26
+ if (typeof expression === 'function' || typeof expression === 'object') {
27
27
  const index = data.values.push(expression) - 1;
28
28
 
29
29
  return `${prefix}<!--abydon.${index}-->`;
@@ -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,9 +1,3 @@
1
- export type Fragment = {
2
- appendTo(element: Element): void;
3
- get(): FragmentNode[];
4
- remove(): void;
5
- };
6
-
7
1
  export type FragmentData = {
8
2
  expressions: unknown[];
9
3
  nodes: Node[];
@@ -1,8 +1,8 @@
1
+ import {closest} from '@oscarpalmer/atoms/element';
1
2
  import type {PlainObject} from '@oscarpalmer/atoms/models';
2
3
  import {getString} from '@oscarpalmer/atoms/string';
3
4
  import {effect, isReactive} from '@oscarpalmer/sentinel';
4
5
  import type {FragmentElement} from '../../models';
5
- import {closest} from '@oscarpalmer/atoms/element';
6
6
 
7
7
  const booleanAttributes = new Set([
8
8
  'checked',
package/src/node/event.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import type {FragmentElement} from '../models';
2
2
 
3
+ function getOptions(options: string): AddEventListenerOptions {
4
+ const parts = options.split(':');
5
+
6
+ return {
7
+ capture: parts.includes('c') || parts.includes('capture'),
8
+ once: parts.includes('o') || parts.includes('once'),
9
+ passive: !parts.includes('a') && !parts.includes('active'),
10
+ };
11
+ }
12
+
3
13
  export function mapEvent(
4
14
  element: FragmentElement,
5
15
  name: string,
@@ -7,9 +17,13 @@ export function mapEvent(
7
17
  ): void {
8
18
  element.removeAttribute(name);
9
19
 
10
- if (typeof value !== 'function') {
11
- return;
12
- }
20
+ const [, type, options] = /^@(\w+)(?::([a-z:]+))?$/.exec(name) ?? [];
13
21
 
14
- // TODO
15
- }
22
+ if (typeof value === 'function' && type != null) {
23
+ element.addEventListener(
24
+ type,
25
+ value as EventListener,
26
+ getOptions(options ?? ''),
27
+ );
28
+ }
29
+ }
package/src/node/index.ts CHANGED
@@ -2,8 +2,8 @@ import {isReactive} from '@oscarpalmer/sentinel';
2
2
  import {isFragmentElement} from '../helpers';
3
3
  import {createNodes} from '../helpers/dom';
4
4
  import type {FragmentData} from '../models';
5
- import {setReactiveNode} from './value';
6
5
  import {mapAttributes} from './attribute';
6
+ import {setReactiveNode} from './value';
7
7
 
8
8
  const commentExpression = /^abydon\.(\d+)$/;
9
9
 
package/src/node/value.ts CHANGED
@@ -45,4 +45,4 @@ export function setReactiveNode(comment: Comment, reactive: Reactive): void {
45
45
 
46
46
  nodes = undefined;
47
47
  });
48
- }
48
+ }
@@ -1,2 +1,17 @@
1
- import type { Fragment } from './models';
2
- export declare function createFragment(strings: TemplateStringsArray, expressions: unknown[]): Fragment;
1
+ export declare class Fragment {
2
+ private readonly $fragment;
3
+ private readonly data;
4
+ constructor(strings: TemplateStringsArray, expressions: unknown[]);
5
+ /**
6
+ * Appends the fragment to the given element
7
+ */
8
+ appendTo(element: Element): void;
9
+ /**
10
+ * Gets a list of the fragment's nodes
11
+ */
12
+ get(): Node[];
13
+ /**
14
+ * Removes the fragment from the DOM
15
+ */
16
+ remove(): void;
17
+ }
@@ -1,4 +1,5 @@
1
- import type { Fragment, FragmentElement, FragmentNode } from '../models';
1
+ import type { Fragment } from '../fragment';
2
+ import type { FragmentElement, FragmentNode } from '../models';
2
3
  export declare function isFragment(value: unknown): value is Fragment;
3
4
  export declare function isFragmentElement(value: unknown): value is FragmentElement;
4
5
  export declare function isFragmentNode(value: unknown): value is FragmentNode;
package/types/html.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- import type { Fragment, FragmentData } from './models';
1
+ import { Fragment } from './fragment';
2
+ import type { FragmentData } from './models';
2
3
  export declare function html(strings: TemplateStringsArray, ...values: unknown[]): Fragment;
3
4
  export declare function parse(data: FragmentData): string;
@@ -0,0 +1,22 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export declare class Fragment {
4
+ private readonly $fragment;
5
+ private readonly data;
6
+ constructor(strings: TemplateStringsArray, expressions: unknown[]);
7
+ /**
8
+ * Appends the fragment to the given element
9
+ */
10
+ appendTo(element: Element): void;
11
+ /**
12
+ * Gets a list of the fragment's nodes
13
+ */
14
+ get(): Node[];
15
+ /**
16
+ * Removes the fragment from the DOM
17
+ */
18
+ remove(): void;
19
+ }
20
+ export declare function html(strings: TemplateStringsArray, ...values: unknown[]): Fragment;
21
+
22
+ export {};
package/types/index.d.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/types/models.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- export type Fragment = {
2
- appendTo(element: Element): void;
3
- get(): FragmentNode[];
4
- remove(): void;
5
- };
6
1
  export type FragmentData = {
7
2
  expressions: unknown[];
8
3
  nodes: Node[];
package/.bun.ts DELETED
@@ -1,32 +0,0 @@
1
- import * as fs from 'node:fs/promises';
2
-
3
- const directory = String(__dirname).replace(/\\/g, '/');
4
- const isMjs = process.argv.includes('--mjs');
5
-
6
- async function getFiles(path: string): Promise<string[]> {
7
- const entries = await fs.readdir(path, {withFileTypes: true});
8
-
9
- const files = entries
10
- .filter(entry => entry.isFile() && entry.name.endsWith('.ts'))
11
- .map(file => `${path}/${file.name}`);
12
-
13
- const folders = entries.filter(entry => entry.isDirectory());
14
-
15
- for (const folder of folders) {
16
- files.push(...(await getFiles(`${path}/${folder.name}`)));
17
- }
18
-
19
- return files;
20
- }
21
-
22
- getFiles('./src').then(async files => {
23
- for (const file of files) {
24
- await Bun.build({
25
- entrypoints: [`${directory}/${file}`],
26
- external: isMjs ? ['*'] : [],
27
- naming: isMjs ? '[dir]/[name].mjs' : undefined,
28
- outdir: './dist',
29
- root: './src',
30
- });
31
- }
32
- });
package/.gitattributes DELETED
@@ -1 +0,0 @@
1
- * text=auto eol=lf