@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.
- package/.editorconfig +9 -0
- package/.gitattributes +2 -0
- package/.github/dependabot.yml +25 -0
- package/.github/workflows/bump.yml +9 -0
- package/.github/workflows/dependabot.yml +12 -0
- package/.github/workflows/publish.yml +16 -0
- package/README.md +385 -0
- package/build/attributes.d.ts +5 -0
- package/build/attributes.js +212 -0
- package/build/compiler/codegen.d.ts +21 -0
- package/build/compiler/codegen.js +303 -0
- package/build/compiler/constants.d.ts +16 -0
- package/build/compiler/constants.js +19 -0
- package/build/compiler/index.d.ts +14 -0
- package/build/compiler/index.js +61 -0
- package/build/compiler/parser.d.ts +19 -0
- package/build/compiler/parser.js +164 -0
- package/build/compiler/plugins/tsc.d.ts +3 -0
- package/build/compiler/plugins/tsc.js +4 -0
- package/build/compiler/plugins/vite.d.ts +13 -0
- package/build/compiler/plugins/vite.js +8 -0
- package/build/compiler/ts-analyzer.d.ts +4 -0
- package/build/compiler/ts-analyzer.js +63 -0
- package/build/compiler/ts-parser.d.ts +24 -0
- package/build/compiler/ts-parser.js +67 -0
- package/build/constants.d.ts +12 -0
- package/build/constants.js +25 -0
- package/build/event/index.d.ts +10 -0
- package/build/event/index.js +90 -0
- package/build/event/onconnect.d.ts +3 -0
- package/build/event/onconnect.js +15 -0
- package/build/event/onresize.d.ts +3 -0
- package/build/event/onresize.js +26 -0
- package/build/event/ontick.d.ts +6 -0
- package/build/event/ontick.js +41 -0
- package/build/html.d.ts +9 -0
- package/build/html.js +7 -0
- package/build/index.d.ts +8 -0
- package/build/index.js +12 -0
- package/build/render.d.ts +3 -0
- package/build/render.js +8 -0
- package/build/slot/array.d.ts +25 -0
- package/build/slot/array.js +189 -0
- package/build/slot/cleanup.d.ts +4 -0
- package/build/slot/cleanup.js +23 -0
- package/build/slot/effect.d.ts +12 -0
- package/build/slot/effect.js +85 -0
- package/build/slot/index.d.ts +7 -0
- package/build/slot/index.js +14 -0
- package/build/slot/render.d.ts +2 -0
- package/build/slot/render.js +44 -0
- package/build/svg.d.ts +5 -0
- package/build/svg.js +14 -0
- package/build/types.d.ts +23 -0
- package/build/types.js +1 -0
- package/build/utilities.d.ts +7 -0
- package/build/utilities.js +31 -0
- package/package.json +43 -0
- package/src/attributes.ts +313 -0
- package/src/compiler/codegen.ts +492 -0
- package/src/compiler/constants.ts +25 -0
- package/src/compiler/index.ts +87 -0
- package/src/compiler/parser.ts +242 -0
- package/src/compiler/plugins/tsc.ts +6 -0
- package/src/compiler/plugins/vite.ts +10 -0
- package/src/compiler/ts-analyzer.ts +89 -0
- package/src/compiler/ts-parser.ts +112 -0
- package/src/constants.ts +44 -0
- package/src/event/index.ts +130 -0
- package/src/event/onconnect.ts +22 -0
- package/src/event/onresize.ts +37 -0
- package/src/event/ontick.ts +59 -0
- package/src/html.ts +18 -0
- package/src/index.ts +19 -0
- package/src/llm.txt +403 -0
- package/src/render.ts +13 -0
- package/src/slot/array.ts +257 -0
- package/src/slot/cleanup.ts +37 -0
- package/src/slot/effect.ts +114 -0
- package/src/slot/index.ts +17 -0
- package/src/slot/render.ts +61 -0
- package/src/svg.ts +27 -0
- package/src/types.ts +40 -0
- package/src/utilities.ts +53 -0
- package/storage/compiler-architecture-2026-01-13.md +420 -0
- package/test/dist/test.js +1912 -0
- package/test/dist/test.js.map +1 -0
- package/test/index.ts +648 -0
- package/test/vite.config.ts +23 -0
- package/tsconfig.json +8 -0
package/.editorconfig
ADDED
package/.gitattributes
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
|
|
8
|
+
registries:
|
|
9
|
+
npm-npmjs:
|
|
10
|
+
token: ${{secrets.NPM_TOKEN}}
|
|
11
|
+
type: npm-registry
|
|
12
|
+
url: https://registry.npmjs.org
|
|
13
|
+
|
|
14
|
+
updates:
|
|
15
|
+
- package-ecosystem: "npm"
|
|
16
|
+
directory: "/"
|
|
17
|
+
groups:
|
|
18
|
+
production-dependencies:
|
|
19
|
+
dependency-type: "production"
|
|
20
|
+
development-dependencies:
|
|
21
|
+
dependency-type: "development"
|
|
22
|
+
registries:
|
|
23
|
+
- npm-npmjs
|
|
24
|
+
schedule:
|
|
25
|
+
interval: "daily"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
workflow_run:
|
|
8
|
+
workflows: [bump version]
|
|
9
|
+
types:
|
|
10
|
+
- completed
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
secrets:
|
|
15
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
16
|
+
uses: esportsplus/typescript/.github/workflows/publish.yml@main
|
package/README.md
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# @esportsplus/template
|
|
2
|
+
|
|
3
|
+
High-performance, compiler-optimized HTML templating library for JavaScript/TypeScript. Templates are transformed at build time into optimized DOM manipulation code with zero runtime parsing overhead.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Compile-time transformation** - Templates converted to optimized code during build
|
|
8
|
+
- **Zero runtime parsing** - No template parsing at runtime
|
|
9
|
+
- **Reactive integration** - Works with `@esportsplus/reactivity` for dynamic updates
|
|
10
|
+
- **Event delegation** - Efficient event handling with automatic delegation
|
|
11
|
+
- **Lifecycle events** - `onconnect`, `ondisconnect`, `onrender`, `onresize`, `ontick`
|
|
12
|
+
- **Type-safe** - Full TypeScript support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @esportsplus/template
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Transformer Plugins
|
|
21
|
+
|
|
22
|
+
The library requires a build-time transformer to convert template literals into optimized code. Choose the plugin for your build tool:
|
|
23
|
+
|
|
24
|
+
### Vite
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// vite.config.ts
|
|
28
|
+
import { defineConfig } from 'vite';
|
|
29
|
+
import template from '@esportsplus/template/compiler/vite';
|
|
30
|
+
|
|
31
|
+
export default defineConfig({
|
|
32
|
+
plugins: [template]
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### TypeScript Compiler (tsc)
|
|
37
|
+
|
|
38
|
+
For direct `tsc` compilation, use the transformer:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// tsconfig.json (with ts-patch or ttypescript)
|
|
42
|
+
{
|
|
43
|
+
"compilerOptions": {
|
|
44
|
+
"plugins": [
|
|
45
|
+
{ "transform": "@esportsplus/template/compiler/tsc" }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Basic Usage
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { html, render } from '@esportsplus/template';
|
|
55
|
+
|
|
56
|
+
// Static template
|
|
57
|
+
const greeting = html`<div class="greeting">Hello World</div>`;
|
|
58
|
+
|
|
59
|
+
// Dynamic text
|
|
60
|
+
const message = (text: string) => html`<div>${text}</div>`;
|
|
61
|
+
|
|
62
|
+
// Dynamic attributes
|
|
63
|
+
const button = (cls: string) => html`<button class="${cls}">Click</button>`;
|
|
64
|
+
|
|
65
|
+
// Render to DOM
|
|
66
|
+
render(document.body, greeting);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Template Syntax
|
|
70
|
+
|
|
71
|
+
### Text Slots
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// Basic text interpolation
|
|
75
|
+
const text = (value: string) => html`<span>${value}</span>`;
|
|
76
|
+
|
|
77
|
+
// Multiple slots
|
|
78
|
+
const multi = (a: string, b: string) => html`<p>${a} and ${b}</p>`;
|
|
79
|
+
|
|
80
|
+
// Adjacent slots
|
|
81
|
+
const adjacent = (a: string, b: string, c: string) => html`<div>${a}${b}${c}</div>`;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Attribute Slots
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Class
|
|
88
|
+
const dynamic = (cls: string) => html`<div class="${cls}"></div>`;
|
|
89
|
+
|
|
90
|
+
// Mixed static and dynamic
|
|
91
|
+
const mixed = (dynamic: string) => html`<div class="base ${dynamic}"></div>`;
|
|
92
|
+
|
|
93
|
+
// Multiple attributes
|
|
94
|
+
const attrs = (id: string, cls: string, style: string) =>
|
|
95
|
+
html`<div id="${id}" class="${cls}" style="${style}"></div>`;
|
|
96
|
+
|
|
97
|
+
// Data attributes
|
|
98
|
+
const data = (value: string) => html`<div data-value="${value}"></div>`;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Spread Attributes
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Spread object as attributes
|
|
105
|
+
const spread = (attrs: Record<string, unknown>) => html`<div ${attrs}></div>`;
|
|
106
|
+
|
|
107
|
+
// With static attributes
|
|
108
|
+
const spreadMixed = (attrs: Record<string, unknown>) =>
|
|
109
|
+
html`<div class="base" ${attrs}></div>`;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Nested Templates
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Map to nested templates
|
|
116
|
+
const list = (items: string[]) =>
|
|
117
|
+
html`<ul>${items.map(item => html`<li>${item}</li>`)}</ul>`;
|
|
118
|
+
|
|
119
|
+
// Deeply nested
|
|
120
|
+
const nested = (sections: { title: string; items: string[] }[]) =>
|
|
121
|
+
html`<div>
|
|
122
|
+
${sections.map(section => html`
|
|
123
|
+
<section>
|
|
124
|
+
<h2>${section.title}</h2>
|
|
125
|
+
<ul>${section.items.map(item => html`<li>${item}</li>`)}</ul>
|
|
126
|
+
</section>
|
|
127
|
+
`)}
|
|
128
|
+
</div>`;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Conditional Rendering
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Conditional class
|
|
135
|
+
const toggle = (active: boolean) =>
|
|
136
|
+
html`<div class="${active ? 'active' : 'inactive'}"></div>`;
|
|
137
|
+
|
|
138
|
+
// Conditional content
|
|
139
|
+
const conditional = (show: boolean, content: string) =>
|
|
140
|
+
html`<div>${show ? content : ''}</div>`;
|
|
141
|
+
|
|
142
|
+
// Conditional template
|
|
143
|
+
const conditionalTemplate = (items: string[] | null) =>
|
|
144
|
+
html`<div>
|
|
145
|
+
${items ? html`<ul>${items.map(i => html`<li>${i}</li>`)}</ul>` : html`<p>No items</p>`}
|
|
146
|
+
</div>`;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Reactive Bindings
|
|
150
|
+
|
|
151
|
+
### Effect Slots
|
|
152
|
+
|
|
153
|
+
Function expressions automatically become reactive effect slots:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { html } from '@esportsplus/template';
|
|
157
|
+
|
|
158
|
+
// Arrow function = reactive
|
|
159
|
+
const counter = (state: { count: number }) =>
|
|
160
|
+
html`<div>${() => state.count}</div>`;
|
|
161
|
+
|
|
162
|
+
// Reactive class
|
|
163
|
+
const toggle = (state: { active: boolean }) =>
|
|
164
|
+
html`<div class="${() => state.active ? 'on' : 'off'}"></div>`;
|
|
165
|
+
|
|
166
|
+
// Reactive style
|
|
167
|
+
const progress = (state: { width: number }) =>
|
|
168
|
+
html`<div style="${() => `width: ${state.width}px`}"></div>`;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Array Slots
|
|
172
|
+
|
|
173
|
+
For reactive arrays, use `html.reactive`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { html } from '@esportsplus/template';
|
|
177
|
+
|
|
178
|
+
const todoList = (todos: string[]) =>
|
|
179
|
+
html`<ul>${html.reactive(todos, todo => html`<li>${todo}</li>`)}</ul>`;
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Events
|
|
183
|
+
|
|
184
|
+
### Standard DOM Events
|
|
185
|
+
|
|
186
|
+
Events use delegation by default for efficiency:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Click
|
|
190
|
+
const button = (handler: () => void) =>
|
|
191
|
+
html`<button onclick="${handler}">Click</button>`;
|
|
192
|
+
|
|
193
|
+
// Keyboard
|
|
194
|
+
const input = (handler: (e: KeyboardEvent) => void) =>
|
|
195
|
+
html`<input onkeydown="${handler}">`;
|
|
196
|
+
|
|
197
|
+
// Input
|
|
198
|
+
const textbox = (handler: (e: Event) => void) =>
|
|
199
|
+
html`<input oninput="${handler}">`;
|
|
200
|
+
|
|
201
|
+
// Multiple events
|
|
202
|
+
const interactive = (click: () => void, hover: () => void) =>
|
|
203
|
+
html`<div onclick="${click}" onmouseenter="${hover}">Interact</div>`;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Lifecycle Events
|
|
207
|
+
|
|
208
|
+
Custom lifecycle events for DOM attachment:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Called when element is added to DOM
|
|
212
|
+
const connect = (handler: (el: HTMLElement) => void) =>
|
|
213
|
+
html`<div onconnect="${handler}">Connected</div>`;
|
|
214
|
+
|
|
215
|
+
// Called when element is removed from DOM
|
|
216
|
+
const disconnect = (handler: (el: HTMLElement) => void) =>
|
|
217
|
+
html`<div ondisconnect="${handler}">Will disconnect</div>`;
|
|
218
|
+
|
|
219
|
+
// Called after template renders
|
|
220
|
+
const render = (handler: (el: HTMLElement) => void) =>
|
|
221
|
+
html`<div onrender="${handler}">Rendered</div>`;
|
|
222
|
+
|
|
223
|
+
// Called on element resize
|
|
224
|
+
const resize = (handler: (el: HTMLElement) => void) =>
|
|
225
|
+
html`<div onresize="${handler}">Resizable</div>`;
|
|
226
|
+
|
|
227
|
+
// Called on animation frame (with dispose function)
|
|
228
|
+
const tick = (handler: (dispose: () => void, el: HTMLElement) => void) =>
|
|
229
|
+
html`<div ontick="${handler}">Animating</div>`;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Direct Attach Events
|
|
233
|
+
|
|
234
|
+
Some events don't bubble and are attached directly:
|
|
235
|
+
|
|
236
|
+
- `onfocus`, `onblur`, `onfocusin`, `onfocusout`
|
|
237
|
+
- `onload`, `onerror`
|
|
238
|
+
- `onplay`, `onpause`, `onended`, `ontimeupdate`
|
|
239
|
+
- `onscroll`
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const input = (focus: () => void, blur: () => void) =>
|
|
243
|
+
html`<input onfocus="${focus}" onblur="${blur}">`;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## SVG Support
|
|
247
|
+
|
|
248
|
+
Use `svg` for SVG templates:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { svg } from '@esportsplus/template';
|
|
252
|
+
|
|
253
|
+
const circle = (fill: string) =>
|
|
254
|
+
svg`<svg width="100" height="100">
|
|
255
|
+
<circle cx="50" cy="50" r="40" fill="${fill}"/>
|
|
256
|
+
</svg>`;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## API Reference
|
|
260
|
+
|
|
261
|
+
### Exports
|
|
262
|
+
|
|
263
|
+
| Export | Description |
|
|
264
|
+
|--------|-------------|
|
|
265
|
+
| `html` | Template literal tag for HTML |
|
|
266
|
+
| `svg` | Template literal tag for SVG |
|
|
267
|
+
| `render` | Mount renderable to DOM element |
|
|
268
|
+
| `attributes` | Attribute manipulation utilities |
|
|
269
|
+
| `event` | Event system |
|
|
270
|
+
| `slot` | Slot rendering |
|
|
271
|
+
| `ArraySlot` | Reactive array rendering |
|
|
272
|
+
| `EffectSlot` | Reactive effect rendering |
|
|
273
|
+
|
|
274
|
+
### Types
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
type Renderable = DocumentFragment | Node | string | number | null | undefined;
|
|
278
|
+
type Element = HTMLElement & { [STORE]?: Record<string, unknown> };
|
|
279
|
+
type Attributes = Record<string, unknown>;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### render(parent, renderable)
|
|
283
|
+
|
|
284
|
+
Mounts a renderable to a parent element.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { html, render } from '@esportsplus/template';
|
|
288
|
+
|
|
289
|
+
const app = html`<div>App</div>`;
|
|
290
|
+
render(document.getElementById('root'), app);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Complete Example
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { html, render } from '@esportsplus/template';
|
|
297
|
+
|
|
298
|
+
type Todo = { id: number; text: string; done: boolean };
|
|
299
|
+
|
|
300
|
+
const TodoItem = (todo: Todo, onToggle: () => void, onDelete: () => void) => html`
|
|
301
|
+
<li class="${() => todo.done ? 'completed' : ''}">
|
|
302
|
+
<input type="checkbox"
|
|
303
|
+
checked="${() => todo.done}"
|
|
304
|
+
onchange="${onToggle}">
|
|
305
|
+
<span>${todo.text}</span>
|
|
306
|
+
<button onclick="${onDelete}">Delete</button>
|
|
307
|
+
</li>
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
const TodoApp = (state: {
|
|
311
|
+
todos: Todo[];
|
|
312
|
+
input: string;
|
|
313
|
+
addTodo: () => void;
|
|
314
|
+
toggleTodo: (id: number) => void;
|
|
315
|
+
deleteTodo: (id: number) => void;
|
|
316
|
+
setInput: (value: string) => void;
|
|
317
|
+
}) => html`
|
|
318
|
+
<div class="todo-app">
|
|
319
|
+
<h1>Todos</h1>
|
|
320
|
+
<form onsubmit="${(e: Event) => { e.preventDefault(); state.addTodo(); }}">
|
|
321
|
+
<input type="text"
|
|
322
|
+
value="${() => state.input}"
|
|
323
|
+
oninput="${(e: Event) => state.setInput((e.target as HTMLInputElement).value)}"
|
|
324
|
+
placeholder="Add todo...">
|
|
325
|
+
<button type="submit">Add</button>
|
|
326
|
+
</form>
|
|
327
|
+
<ul>
|
|
328
|
+
${html.reactive(state.todos, todo =>
|
|
329
|
+
TodoItem(
|
|
330
|
+
todo,
|
|
331
|
+
() => state.toggleTodo(todo.id),
|
|
332
|
+
() => state.deleteTodo(todo.id)
|
|
333
|
+
)
|
|
334
|
+
)}
|
|
335
|
+
</ul>
|
|
336
|
+
</div>
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
// Mount
|
|
340
|
+
render(document.body, TodoApp(/* state */));
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## How Transformation Works
|
|
344
|
+
|
|
345
|
+
The transformer converts template literals into optimized code at build time:
|
|
346
|
+
|
|
347
|
+
**Input:**
|
|
348
|
+
```typescript
|
|
349
|
+
const greeting = (name: string) => html`<div class="hello">${name}</div>`;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Output (simplified):**
|
|
353
|
+
```typescript
|
|
354
|
+
const $template = (() => {
|
|
355
|
+
let cached;
|
|
356
|
+
return () => {
|
|
357
|
+
if (!cached) cached = template('<div class="hello"><!--$--></div>');
|
|
358
|
+
return clone(cached);
|
|
359
|
+
};
|
|
360
|
+
})();
|
|
361
|
+
|
|
362
|
+
const greeting = (name: string) => {
|
|
363
|
+
const fragment = $template();
|
|
364
|
+
const $0 = firstChild(fragment);
|
|
365
|
+
slot($0.firstChild, name);
|
|
366
|
+
return fragment;
|
|
367
|
+
};
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Key optimizations:
|
|
371
|
+
- Template HTML is parsed once and cached
|
|
372
|
+
- Slot positions are computed at compile time
|
|
373
|
+
- Type analysis determines optimal slot binding
|
|
374
|
+
- No runtime template parsing
|
|
375
|
+
|
|
376
|
+
## Dependencies
|
|
377
|
+
|
|
378
|
+
- `@esportsplus/reactivity` - Reactive primitives
|
|
379
|
+
- `@esportsplus/queue` - Object pooling
|
|
380
|
+
- `@esportsplus/typescript` - TypeScript compiler utilities
|
|
381
|
+
- `@esportsplus/utilities` - Utility functions
|
|
382
|
+
|
|
383
|
+
## License
|
|
384
|
+
|
|
385
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Attributes, Element } from './types.js';
|
|
2
|
+
declare const setList: (element: Element, name: "class" | "style", value: unknown, attributes?: Record<string, string>) => void;
|
|
3
|
+
declare const setProperty: (element: Element, name: string, value: unknown) => void;
|
|
4
|
+
declare const setProperties: (element: Element, properties: Attributes | Attributes[] | false | null | undefined, attributes?: Record<string, string>) => void;
|
|
5
|
+
export { setList, setProperty, setProperties };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { effect } from '@esportsplus/reactivity';
|
|
2
|
+
import { isArray, isObject } from '@esportsplus/utilities';
|
|
3
|
+
import { ATTRIBUTE_DELIMITERS, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE } from './constants.js';
|
|
4
|
+
import { raf } from './utilities.js';
|
|
5
|
+
import { runtime } from './event/index.js';
|
|
6
|
+
import q from '@esportsplus/queue';
|
|
7
|
+
let queue = q(64), scheduled = false;
|
|
8
|
+
function apply(element, name, value) {
|
|
9
|
+
if (value == null || value === false || value === '') {
|
|
10
|
+
element.removeAttribute(name);
|
|
11
|
+
}
|
|
12
|
+
else if (name === 'class') {
|
|
13
|
+
element.className = value;
|
|
14
|
+
}
|
|
15
|
+
else if (name === 'style' || (name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
|
|
16
|
+
element.setAttribute(name, value);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
element[name] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function context(element) {
|
|
23
|
+
return (element[STORE] ??= { element });
|
|
24
|
+
}
|
|
25
|
+
function list(ctx, element, id, name, state, value) {
|
|
26
|
+
if (value == null || value === false || value === '') {
|
|
27
|
+
value = '';
|
|
28
|
+
}
|
|
29
|
+
let changed = false, delimiter = ATTRIBUTE_DELIMITERS[name], store = (ctx ??= context(element)).store ??= {}, dynamic = store[name];
|
|
30
|
+
if (!dynamic) {
|
|
31
|
+
store[name + '.static'] = (element.getAttribute(name) || '').trim();
|
|
32
|
+
store[name] = dynamic = new Set();
|
|
33
|
+
}
|
|
34
|
+
if (id === null) {
|
|
35
|
+
if (value && typeof value === 'string') {
|
|
36
|
+
changed = true;
|
|
37
|
+
store[name + '.static'] += (store[name + '.static'] ? delimiter : '') + value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (store[id + '.raw'] !== value) {
|
|
41
|
+
let hot = {};
|
|
42
|
+
if (value && typeof value === 'string') {
|
|
43
|
+
let part, parts = value.split(delimiter);
|
|
44
|
+
while (part = parts.pop()) {
|
|
45
|
+
part = part.trim();
|
|
46
|
+
if (part === '') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!dynamic.has(part)) {
|
|
50
|
+
changed = true;
|
|
51
|
+
dynamic.add(part);
|
|
52
|
+
}
|
|
53
|
+
hot[part] = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let cold = store[id];
|
|
57
|
+
if (cold !== undefined) {
|
|
58
|
+
for (let part in cold) {
|
|
59
|
+
if (hot[part] === true) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
changed = true;
|
|
63
|
+
dynamic.delete(part);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
store[id + '.raw'] = value;
|
|
67
|
+
store[id] = hot;
|
|
68
|
+
}
|
|
69
|
+
if (!changed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
value = store[name + '.static'];
|
|
73
|
+
for (let key of dynamic) {
|
|
74
|
+
value += (value ? delimiter : '') + key;
|
|
75
|
+
}
|
|
76
|
+
if (state === STATE_HYDRATING) {
|
|
77
|
+
apply(element, name, value);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
schedule(ctx, element, name, state, value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function property(ctx, element, id, name, state, value) {
|
|
84
|
+
if (value == null || value === false || value === '') {
|
|
85
|
+
value = '';
|
|
86
|
+
}
|
|
87
|
+
if (id !== null) {
|
|
88
|
+
ctx ??= context(element);
|
|
89
|
+
if (ctx[name] === value) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
ctx[name] = value;
|
|
93
|
+
}
|
|
94
|
+
if (state === STATE_HYDRATING) {
|
|
95
|
+
apply(element, name, value);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
schedule(ctx, element, name, state, value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function reactive(element, name, state, value) {
|
|
102
|
+
let ctx = context(element), fn = (name === 'class' || name === 'style') ? list : property;
|
|
103
|
+
ctx.effect ??= 0;
|
|
104
|
+
let id = ctx.effect++;
|
|
105
|
+
effect(() => {
|
|
106
|
+
let v = value(element);
|
|
107
|
+
if (v == null || typeof v !== 'object') {
|
|
108
|
+
fn(ctx, element, id, name, state, v);
|
|
109
|
+
}
|
|
110
|
+
else if (isArray(v)) {
|
|
111
|
+
let last = v.length - 1;
|
|
112
|
+
for (let i = 0, n = v.length; i < n; i++) {
|
|
113
|
+
fn(ctx, element, id, name, state === STATE_HYDRATING
|
|
114
|
+
? state
|
|
115
|
+
: i !== last ? STATE_WAITING : state, v[i]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
state = STATE_NONE;
|
|
120
|
+
}
|
|
121
|
+
function schedule(ctx, element, name, state, value) {
|
|
122
|
+
ctx ??= context(element);
|
|
123
|
+
(ctx.updates ??= {})[name] = value;
|
|
124
|
+
if (state === STATE_NONE && !ctx.updating) {
|
|
125
|
+
ctx.updating = true;
|
|
126
|
+
queue.add(ctx);
|
|
127
|
+
}
|
|
128
|
+
if (scheduled) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
scheduled = true;
|
|
132
|
+
raf(task);
|
|
133
|
+
}
|
|
134
|
+
function task() {
|
|
135
|
+
let context, n = queue.length;
|
|
136
|
+
while ((context = queue.next()) && n--) {
|
|
137
|
+
let { element, updates } = context;
|
|
138
|
+
for (let name in updates) {
|
|
139
|
+
apply(element, name, updates[name]);
|
|
140
|
+
}
|
|
141
|
+
context.updates = {};
|
|
142
|
+
context.updating = false;
|
|
143
|
+
}
|
|
144
|
+
if (queue.length) {
|
|
145
|
+
raf(task);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
scheduled = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const setList = (element, name, value, attributes = {}) => {
|
|
152
|
+
let ctx = context(element), store = ctx.store ??= {};
|
|
153
|
+
store[name] ??= new Set();
|
|
154
|
+
store[name + '.static'] ??= '';
|
|
155
|
+
store[name + '.static'] += `${attributes[name] && store[name + '.static'] ? ATTRIBUTE_DELIMITERS[name] : ''}${attributes[name]}`;
|
|
156
|
+
if (typeof value === 'function') {
|
|
157
|
+
reactive(element, name, STATE_HYDRATING, value);
|
|
158
|
+
}
|
|
159
|
+
else if (typeof value !== 'object') {
|
|
160
|
+
list(ctx, element, null, name, STATE_HYDRATING, value);
|
|
161
|
+
}
|
|
162
|
+
else if (isArray(value)) {
|
|
163
|
+
for (let i = 0, n = value.length; i < n; i++) {
|
|
164
|
+
let v = value[i];
|
|
165
|
+
if (v == null || v === false || v === '') {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
list(ctx, element, null, name, STATE_HYDRATING, v);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const setProperty = (element, name, value) => {
|
|
173
|
+
if (typeof value === 'function') {
|
|
174
|
+
reactive(element, name, STATE_HYDRATING, value);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
property(null, element, null, name, STATE_HYDRATING, value);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const setProperties = function (element, properties, attributes = {}) {
|
|
181
|
+
if (!properties) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
else if (isObject(properties)) {
|
|
185
|
+
for (let name in properties) {
|
|
186
|
+
let value = properties[name];
|
|
187
|
+
if (value == null || value === false || value === '') {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (name === 'class' || name === 'style') {
|
|
191
|
+
setList(element, name, value, attributes);
|
|
192
|
+
}
|
|
193
|
+
else if (typeof value === 'function') {
|
|
194
|
+
if (name[0] === 'o' && name[1] === 'n') {
|
|
195
|
+
runtime(element, name, value);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
reactive(element, name, STATE_HYDRATING, value);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
property(null, element, null, name, STATE_HYDRATING, value);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (isArray(properties)) {
|
|
207
|
+
for (let i = 0, n = properties.length; i < n; i++) {
|
|
208
|
+
setProperties(element, properties[i], attributes);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
export { setList, setProperty, setProperties };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import type { TemplateInfo } from './ts-parser.js';
|
|
3
|
+
import { ts } from '@esportsplus/typescript';
|
|
4
|
+
type CodegenContext = {
|
|
5
|
+
checker?: ts.TypeChecker;
|
|
6
|
+
sourceFile: ts.SourceFile;
|
|
7
|
+
templates: Map<string, string>;
|
|
8
|
+
};
|
|
9
|
+
type CodegenResult = {
|
|
10
|
+
prepend: string[];
|
|
11
|
+
replacements: ReplacementIntent[];
|
|
12
|
+
templates: Map<string, string>;
|
|
13
|
+
};
|
|
14
|
+
declare let printer: ts.Printer;
|
|
15
|
+
declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, callRanges?: {
|
|
16
|
+
end: number;
|
|
17
|
+
start: number;
|
|
18
|
+
}[]) => CodegenResult;
|
|
19
|
+
declare const rewriteExpression: (ctx: CodegenContext, expr: ts.Expression) => string;
|
|
20
|
+
export { generateCode, printer, rewriteExpression };
|
|
21
|
+
export type { CodegenResult };
|