@esportsplus/template 0.16.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 (90) hide show
  1. package/.editorconfig +9 -0
  2. package/.gitattributes +2 -0
  3. package/.github/dependabot.yml +25 -0
  4. package/.github/workflows/bump.yml +9 -0
  5. package/.github/workflows/dependabot.yml +12 -0
  6. package/.github/workflows/publish.yml +16 -0
  7. package/README.md +385 -0
  8. package/build/attributes.d.ts +5 -0
  9. package/build/attributes.js +212 -0
  10. package/build/compiler/codegen.d.ts +21 -0
  11. package/build/compiler/codegen.js +303 -0
  12. package/build/compiler/constants.d.ts +16 -0
  13. package/build/compiler/constants.js +19 -0
  14. package/build/compiler/index.d.ts +14 -0
  15. package/build/compiler/index.js +61 -0
  16. package/build/compiler/parser.d.ts +19 -0
  17. package/build/compiler/parser.js +164 -0
  18. package/build/compiler/plugins/tsc.d.ts +3 -0
  19. package/build/compiler/plugins/tsc.js +4 -0
  20. package/build/compiler/plugins/vite.d.ts +13 -0
  21. package/build/compiler/plugins/vite.js +8 -0
  22. package/build/compiler/ts-analyzer.d.ts +4 -0
  23. package/build/compiler/ts-analyzer.js +63 -0
  24. package/build/compiler/ts-parser.d.ts +24 -0
  25. package/build/compiler/ts-parser.js +67 -0
  26. package/build/constants.d.ts +12 -0
  27. package/build/constants.js +25 -0
  28. package/build/event/index.d.ts +10 -0
  29. package/build/event/index.js +90 -0
  30. package/build/event/onconnect.d.ts +3 -0
  31. package/build/event/onconnect.js +15 -0
  32. package/build/event/onresize.d.ts +3 -0
  33. package/build/event/onresize.js +26 -0
  34. package/build/event/ontick.d.ts +6 -0
  35. package/build/event/ontick.js +41 -0
  36. package/build/html.d.ts +9 -0
  37. package/build/html.js +7 -0
  38. package/build/index.d.ts +8 -0
  39. package/build/index.js +12 -0
  40. package/build/render.d.ts +3 -0
  41. package/build/render.js +8 -0
  42. package/build/slot/array.d.ts +25 -0
  43. package/build/slot/array.js +189 -0
  44. package/build/slot/cleanup.d.ts +4 -0
  45. package/build/slot/cleanup.js +23 -0
  46. package/build/slot/effect.d.ts +12 -0
  47. package/build/slot/effect.js +85 -0
  48. package/build/slot/index.d.ts +7 -0
  49. package/build/slot/index.js +14 -0
  50. package/build/slot/render.d.ts +2 -0
  51. package/build/slot/render.js +44 -0
  52. package/build/svg.d.ts +5 -0
  53. package/build/svg.js +14 -0
  54. package/build/types.d.ts +23 -0
  55. package/build/types.js +1 -0
  56. package/build/utilities.d.ts +7 -0
  57. package/build/utilities.js +31 -0
  58. package/package.json +43 -0
  59. package/src/attributes.ts +313 -0
  60. package/src/compiler/codegen.ts +492 -0
  61. package/src/compiler/constants.ts +25 -0
  62. package/src/compiler/index.ts +87 -0
  63. package/src/compiler/parser.ts +242 -0
  64. package/src/compiler/plugins/tsc.ts +6 -0
  65. package/src/compiler/plugins/vite.ts +10 -0
  66. package/src/compiler/ts-analyzer.ts +89 -0
  67. package/src/compiler/ts-parser.ts +112 -0
  68. package/src/constants.ts +44 -0
  69. package/src/event/index.ts +130 -0
  70. package/src/event/onconnect.ts +22 -0
  71. package/src/event/onresize.ts +37 -0
  72. package/src/event/ontick.ts +59 -0
  73. package/src/html.ts +18 -0
  74. package/src/index.ts +19 -0
  75. package/src/llm.txt +403 -0
  76. package/src/render.ts +13 -0
  77. package/src/slot/array.ts +257 -0
  78. package/src/slot/cleanup.ts +37 -0
  79. package/src/slot/effect.ts +114 -0
  80. package/src/slot/index.ts +17 -0
  81. package/src/slot/render.ts +61 -0
  82. package/src/svg.ts +27 -0
  83. package/src/types.ts +40 -0
  84. package/src/utilities.ts +53 -0
  85. package/storage/compiler-architecture-2026-01-13.md +420 -0
  86. package/test/dist/test.js +1912 -0
  87. package/test/dist/test.js.map +1 -0
  88. package/test/index.ts +648 -0
  89. package/test/vite.config.ts +23 -0
  90. package/tsconfig.json +8 -0
@@ -0,0 +1,257 @@
1
+ import { read, root, signal, write, Reactive } from '@esportsplus/reactivity';
2
+ import { ARRAY_SLOT } from '../constants';
3
+ import { Element, SlotGroup } from '../types';
4
+ import { clone, fragment, marker, raf } from '../utilities';
5
+ import { ondisconnect, remove } from './cleanup';
6
+ import html from '../html';
7
+
8
+
9
+ type ArraySlotOp<T> =
10
+ | { items: T[]; op: 'concat' }
11
+ | { deleteCount: number; items: T[]; op: 'splice'; start: number }
12
+ | { items: T[]; op: 'push' }
13
+ | { items: T[]; op: 'unshift' }
14
+ | { op: 'clear' }
15
+ | { op: 'pop' }
16
+ | { op: 'reverse' }
17
+ | { op: 'shift' }
18
+ | { op: 'sort'; order: number[] };
19
+
20
+
21
+ const EMPTY_FRAGMENT = fragment('');
22
+
23
+
24
+ class ArraySlot<T> {
25
+ private marker: Element;
26
+ private nodes: SlotGroup[] = [];
27
+ private queue: ArraySlotOp<T>[] = [];
28
+ private scheduled = false;
29
+ private signal;
30
+ private template: (...args: Parameters<(value: Reactive<T[]>[number]) => ReturnType<typeof html>>) => SlotGroup;
31
+
32
+ readonly fragment: DocumentFragment;
33
+
34
+
35
+ constructor(private array: Reactive<T[]>, template: ((value: Reactive<T[]>[number]) => ReturnType<typeof html>)) {
36
+ let fragment = this.fragment = clone(EMPTY_FRAGMENT);
37
+
38
+ this.marker = marker.cloneNode() as unknown as Element;
39
+ this.signal = signal(array.length);
40
+ this.template = function (data) {
41
+ let dispose: VoidFunction,
42
+ frag = root((d) => {
43
+ dispose = d;
44
+ return template(data);
45
+ }),
46
+ group = {
47
+ head: frag.firstChild as unknown as Element,
48
+ tail: frag.lastChild as unknown as Element
49
+ };
50
+
51
+ fragment.append(frag);
52
+ ondisconnect(group.head, dispose!);
53
+
54
+ return group;
55
+ };
56
+
57
+ fragment.append(this.marker);
58
+
59
+ if (array.length) {
60
+ root(() => {
61
+ this.nodes = array.map(this.template);
62
+ });
63
+ }
64
+
65
+ array.on('clear', () => {
66
+ this.queue.length = 0;
67
+ this.schedule({ op: 'clear' });
68
+ });
69
+ array.on('concat', ({ items }) => {
70
+ this.schedule({ items, op: 'concat' });
71
+ });
72
+ array.on('pop', () => {
73
+ this.schedule({ op: 'pop' });
74
+ });
75
+ array.on('push', ({ items }) => {
76
+ this.schedule({ items, op: 'push' });
77
+ });
78
+ array.on('reverse', () => {
79
+ this.schedule({ op: 'reverse' });
80
+ });
81
+ array.on('shift', () => {
82
+ this.schedule({ op: 'shift' });
83
+ });
84
+ array.on('sort', ({ order }) => {
85
+ this.schedule({ op: 'sort', order });
86
+ });
87
+ array.on('splice', ({ deleteCount, items, start }) => {
88
+ this.schedule({ deleteCount, items, op: 'splice', start });
89
+ });
90
+ array.on('unshift', ({ items }) => {
91
+ this.schedule({ items, op: 'unshift' });
92
+ });
93
+ }
94
+
95
+
96
+ private anchor(index: number = this.nodes.length - 1) {
97
+ let node = this.nodes[index];
98
+
99
+ if (node) {
100
+ return node.tail || node.head;
101
+ }
102
+
103
+ return this.marker;
104
+ }
105
+
106
+ private clear() {
107
+ remove(...this.nodes.splice(0));
108
+ }
109
+
110
+ private pop() {
111
+ let group = this.nodes.pop();
112
+
113
+ if (group) {
114
+ remove(group);
115
+ }
116
+ }
117
+
118
+ private push(items: T[]) {
119
+ let anchor = this.anchor();
120
+
121
+ this.nodes.push(...items.map(this.template));
122
+ anchor.after(this.fragment);
123
+ }
124
+
125
+ private schedule(op: ArraySlotOp<T>) {
126
+ this.queue.push(op);
127
+
128
+ if (this.scheduled) {
129
+ return;
130
+ }
131
+
132
+ this.scheduled = true;
133
+
134
+ raf(() => {
135
+ let queue = this.queue;
136
+
137
+ this.queue = [];
138
+ this.scheduled = false;
139
+
140
+ root(() => {
141
+ for (let i = 0, n = queue.length; i < n; i++) {
142
+ let op = queue[i];
143
+
144
+ switch (op.op) {
145
+ case 'clear':
146
+ this.clear();
147
+ break;
148
+ case 'concat':
149
+ this.push(op.items);
150
+ break;
151
+ case 'pop':
152
+ this.pop();
153
+ break;
154
+ case 'push':
155
+ this.push(op.items);
156
+ break;
157
+ case 'reverse':
158
+ this.nodes.reverse();
159
+ this.sync();
160
+ break;
161
+ case 'shift':
162
+ this.shift();
163
+ break;
164
+ case 'sort':
165
+ this.sort(op.order);
166
+ break;
167
+ case 'splice':
168
+ this.splice(op.start, op.deleteCount, op.items);
169
+ break;
170
+ case 'unshift':
171
+ this.unshift(op.items);
172
+ break;
173
+ }
174
+ }
175
+ });
176
+
177
+ write(this.signal, this.nodes.length);
178
+ });
179
+ }
180
+
181
+ private shift() {
182
+ let group = this.nodes.shift();
183
+
184
+ if (group) {
185
+ remove(group);
186
+ }
187
+ }
188
+
189
+ private sort(order: number[]) {
190
+ let nodes = this.nodes,
191
+ n = nodes.length;
192
+
193
+ if (n !== order.length) {
194
+ remove(...nodes.splice(0));
195
+ this.nodes = this.array.map(this.template);
196
+ this.marker.after(this.fragment);
197
+ return;
198
+ }
199
+
200
+ let sorted = new Array(n) as SlotGroup[];
201
+
202
+ for (let i = 0; i < n; i++) {
203
+ sorted[i] = nodes[order[i]];
204
+ }
205
+
206
+ this.nodes = sorted;
207
+ this.sync();
208
+ }
209
+
210
+ private splice(start: number, stop: number = this.nodes.length, items: T[]) {
211
+ if (!items.length) {
212
+ remove(...this.nodes.splice(start, stop));
213
+ return;
214
+ }
215
+
216
+ remove(...this.nodes.splice(start, stop, ...items.map(this.template)));
217
+ this.anchor(start - 1).after(this.fragment);
218
+ }
219
+
220
+ private sync() {
221
+ let nodes = this.nodes,
222
+ n = nodes.length;
223
+
224
+ if (!n) {
225
+ return;
226
+ }
227
+
228
+ for (let i = 0; i < n; i++) {
229
+ let group = nodes[i],
230
+ next: Node | null,
231
+ node: Node | null = group.head;
232
+
233
+ while (node) {
234
+ next = node === group.tail ? null : node.nextSibling;
235
+ this.fragment.append(node);
236
+ node = next;
237
+ }
238
+ }
239
+
240
+ this.marker.after(this.fragment);
241
+ }
242
+
243
+ private unshift(items: T[]) {
244
+ this.nodes.unshift(...items.map(this.template));
245
+ this.marker.after(this.fragment);
246
+ }
247
+
248
+
249
+ get length() {
250
+ return read(this.signal);
251
+ }
252
+ }
253
+
254
+ Object.defineProperty(ArraySlot.prototype, ARRAY_SLOT, { value: true });
255
+
256
+
257
+ export { ArraySlot };
@@ -0,0 +1,37 @@
1
+ import { CLEANUP } from '../constants';
2
+ import { Element, SlotGroup } from '../types';
3
+
4
+
5
+ const ondisconnect = (element: Element, fn: VoidFunction) => {
6
+ ((element as any)[CLEANUP] ??= []).push(fn);
7
+ };
8
+
9
+ const remove = (...groups: SlotGroup[]) => {
10
+ for (let i = 0, n = groups.length; i < n; i++) {
11
+ let fns, fn,
12
+ group = groups[i],
13
+ head = group.head,
14
+ next,
15
+ tail = group.tail || head;
16
+
17
+ while (tail) {
18
+ if (fns = tail[CLEANUP] as VoidFunction[] | undefined) {
19
+ while (fn = fns.pop()) {
20
+ fn();
21
+ }
22
+ }
23
+
24
+ next = tail.previousSibling as unknown as Element;
25
+ tail.remove();
26
+
27
+ if (head === tail) {
28
+ break;
29
+ }
30
+
31
+ tail = next;
32
+ }
33
+ }
34
+ };
35
+
36
+
37
+ export { ondisconnect, remove };
@@ -0,0 +1,114 @@
1
+ import { effect } from '@esportsplus/reactivity';
2
+ import { Element, Renderable, SlotGroup } from '../types';
3
+ import { raf, text } from '../utilities'
4
+ import { remove } from './cleanup';
5
+ import render from './render';
6
+
7
+
8
+ function read(value: unknown): unknown {
9
+ if (typeof value === 'function') {
10
+ return read( value() );
11
+ }
12
+
13
+ if (value == null || value === false) {
14
+ return '';
15
+ }
16
+
17
+ return value;
18
+ }
19
+
20
+
21
+ class EffectSlot {
22
+ anchor: Element;
23
+ disposer: VoidFunction;
24
+ group: SlotGroup | null = null;
25
+ scheduled = false;
26
+ textnode: Node | null = null;
27
+
28
+
29
+ constructor(anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) {
30
+ let dispose = fn.length ? () => this.dispose() : undefined,
31
+ value: unknown;
32
+
33
+ this.anchor = anchor;
34
+ this.disposer = effect(() => {
35
+ value = read( fn(dispose) );
36
+
37
+ if (!this.disposer) {
38
+ this.update(value);
39
+ }
40
+ else if (!this.scheduled) {
41
+ this.scheduled = true;
42
+
43
+ raf(() => {
44
+ this.scheduled = false;
45
+ this.update(value);
46
+ });
47
+ }
48
+ });
49
+ }
50
+
51
+
52
+ dispose() {
53
+ let { anchor, group, textnode } = this;
54
+
55
+ if (textnode) {
56
+ group = { head: anchor, tail: textnode as Element };
57
+ }
58
+ else if (group) {
59
+ group.head = anchor;
60
+ }
61
+
62
+ this.disposer();
63
+
64
+ if (group) {
65
+ remove(group);
66
+ }
67
+ }
68
+
69
+ update(value: unknown): void {
70
+ let { anchor, group, textnode } = this;
71
+
72
+ if (group) {
73
+ remove(group);
74
+ this.group = null;
75
+ }
76
+
77
+ if (typeof value !== 'object') {
78
+ if (typeof value !== 'string') {
79
+ value = String(value);
80
+ }
81
+
82
+ if (textnode) {
83
+ textnode.nodeValue = value as string;
84
+
85
+ if (!textnode.isConnected) {
86
+ anchor.after(textnode);
87
+ }
88
+ }
89
+ else {
90
+ anchor.after( this.textnode = text(value as string) );
91
+ }
92
+ }
93
+ else {
94
+ let fragment = render(anchor, value),
95
+ head = fragment.firstChild;
96
+
97
+ if (textnode?.isConnected) {
98
+ remove({ head: textnode as Element, tail: textnode as Element });
99
+ }
100
+
101
+ if (head) {
102
+ this.group = {
103
+ head: head as Element,
104
+ tail: fragment.lastChild as Element
105
+ };
106
+
107
+ anchor.after(fragment);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+
114
+ export { EffectSlot };
@@ -0,0 +1,17 @@
1
+ import { Element, Renderable } from '../types';
2
+ import { EffectSlot } from './effect';
3
+ import render from './render';
4
+
5
+
6
+ export default <T>(anchor: Element, renderable: Renderable<T>) => {
7
+ if (typeof renderable === 'function') {
8
+ new EffectSlot(anchor, renderable);
9
+ }
10
+ else {
11
+ anchor.after( render(anchor, renderable) );
12
+ }
13
+ };
14
+ export * from './array';
15
+ export * from './cleanup';
16
+ export * from './effect';
17
+ export { default as render } from './render';
@@ -0,0 +1,61 @@
1
+ import { isArray } from '@esportsplus/utilities';
2
+ import { ARRAY_SLOT } from '../constants';
3
+ import { Element } from '../types';
4
+ import { clone, fragment, text } from '../utilities';
5
+ import { ArraySlot } from './array';
6
+
7
+
8
+ const EMPTY_FRAGMENT = fragment('');
9
+
10
+
11
+ export default function render(anchor: Element, value: unknown): Node {
12
+ if (value == null || value === false || value === '') {
13
+ return EMPTY_FRAGMENT;
14
+ }
15
+
16
+ if (typeof value !== 'object') {
17
+ return text(value as any);
18
+ }
19
+
20
+ if ((value as any)[ARRAY_SLOT] === true) {
21
+ return (value as ArraySlot<unknown>).fragment;
22
+ }
23
+
24
+ if ((value as any).nodeType !== undefined) {
25
+ return value as Node;
26
+ }
27
+
28
+ let n = (value as any).length;
29
+
30
+ if (typeof n === 'number') {
31
+ if (n === 0) {
32
+ return EMPTY_FRAGMENT;
33
+ }
34
+ else if (n === 1) {
35
+ return render(anchor, (value as any)[0]);
36
+ }
37
+ }
38
+
39
+ if (isArray(value)) {
40
+ let fragment = clone(EMPTY_FRAGMENT) as DocumentFragment;
41
+
42
+ for (let i = 0; i < n; i++) {
43
+ fragment.append(render(anchor, value[i]));
44
+ anchor = fragment.lastChild as Element;
45
+ }
46
+
47
+ return fragment;
48
+ }
49
+
50
+ if (value instanceof NodeList) {
51
+ let fragment = clone(EMPTY_FRAGMENT) as DocumentFragment;
52
+
53
+ for (let i = 0; i < n; i++) {
54
+ fragment.append(value[i]);
55
+ }
56
+
57
+ return fragment;
58
+ }
59
+
60
+ return text(value as any);
61
+ };
package/src/svg.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { setProperty } from './attributes';
2
+ import { template } from './utilities';
3
+ import { Element } from './types';
4
+ import html from './html';
5
+
6
+
7
+ let factory = template('<svg><use /></svg>');
8
+
9
+
10
+ const svg = html.bind(null) as typeof html & {
11
+ sprite: (href: string) => DocumentFragment
12
+ };
13
+
14
+ svg.sprite = (href: string) => {
15
+ if (href[0] !== '#') {
16
+ href = '#' + href;
17
+ }
18
+
19
+ let root = factory();
20
+
21
+ setProperty(root.firstChild!.firstChild as Element, 'href', href);
22
+
23
+ return root;
24
+ };
25
+
26
+
27
+ export default svg;
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { ArraySlot } from './slot';
2
+
3
+
4
+ type Attribute = Effect<Primitive | Primitive[]> | ((...args: any[]) => void) | Primitive;
5
+
6
+ type Attributes<T extends HTMLElement = Element> = {
7
+ class?: Attribute | Attribute[];
8
+ onconnect?: (element: T) => void;
9
+ ondisconnect?: (element: T) => void;
10
+ onrender?: (element: T) => void;
11
+ ontick?: (dispose: VoidFunction, element: T) => void;
12
+ style?: Attribute | Attribute[];
13
+ [key: `aria-${string}`]: string | number | boolean | undefined;
14
+ [key: `data-${string}`]: string | undefined;
15
+ } & {
16
+ [K in keyof GlobalEventHandlersEventMap as `on${string & K}`]?: (this: T, event: GlobalEventHandlersEventMap[K]) => void;
17
+ } & Record<PropertyKey, unknown>;
18
+
19
+ type Effect<T> = () => T extends [] ? Renderable<T>[] : Renderable<T>;
20
+
21
+ type Element = HTMLElement & Attributes<any>;
22
+
23
+ // Copied from '@esportsplus/utilities'
24
+ // - Importing from ^ causes 'cannot be named without a reference to...' error
25
+ type Primitive = bigint | boolean | null | number | string | undefined;
26
+
27
+ type Renderable<T> = ArraySlot<T> | DocumentFragment | Effect<T> | Node | NodeList | Primitive | Renderable<T>[];
28
+
29
+ type SlotGroup = {
30
+ head: Element;
31
+ tail: Element;
32
+ };
33
+
34
+
35
+ export type {
36
+ Attribute, Attributes,
37
+ Effect, Element,
38
+ Renderable,
39
+ SlotGroup
40
+ };
@@ -0,0 +1,53 @@
1
+ import { SLOT_HTML } from './constants';
2
+
3
+
4
+ let tmpl = document.createElement('template'),
5
+ txt = document.createTextNode('');
6
+
7
+
8
+ // Firefox's importNode outperforms cloneNode in certain scenarios
9
+ const clone = typeof navigator !== 'undefined' && navigator.userAgent.includes('Firefox')
10
+ ? document.importNode.bind(document)
11
+ : <T extends DocumentFragment | Node>(node: T, deep: boolean = true) => node.cloneNode(deep) as T;
12
+
13
+ // Create a fragment from HTML string
14
+ const fragment = (html: string): DocumentFragment => {
15
+ let element = tmpl.cloneNode() as HTMLTemplateElement;
16
+
17
+ element.innerHTML = html;
18
+
19
+ return element.content;
20
+ };
21
+
22
+ const marker = fragment(SLOT_HTML).firstChild!;
23
+
24
+ const raf = globalThis?.requestAnimationFrame;
25
+
26
+ // Factory that caches the fragment for repeated cloning
27
+ const template = (html: string) => {
28
+ let cached: DocumentFragment | undefined;
29
+
30
+ return () => {
31
+ if (!cached) {
32
+ let element = tmpl.cloneNode() as HTMLTemplateElement;
33
+
34
+ element.innerHTML = html;
35
+ cached = element.content;
36
+ }
37
+
38
+ return clone(cached, true) as DocumentFragment;
39
+ };
40
+ };
41
+
42
+ const text = (value: string) => {
43
+ let element = txt.cloneNode();
44
+
45
+ if (value !== '') {
46
+ element.nodeValue = value;
47
+ }
48
+
49
+ return element;
50
+ };
51
+
52
+
53
+ export { clone, fragment, template, marker, raf, text };