@miurajs/miura-render 0.0.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 (2) hide show
  1. package/README.md +329 -0
  2. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,329 @@
1
+ # @miura/miura-render
2
+
3
+ The rendering engine for the miura framework. Provides tagged template literals (`html`/`css`), a state-machine parser, a binding manager, structural directives, and performance utilities including LIS-based keyed diffing, async rendering, and virtual scrolling.
4
+
5
+ ## Features
6
+
7
+ - **Tagged Templates** — `html` and `css` tagged template literals
8
+ - **State-Machine Parser** — Correctly handles text, attribute, and multi-expression contexts
9
+ - **Binding Manager** — 10 binding types: Node, Property, Event, Boolean, Class, Style, Attribute, Reference, Directive, Bind
10
+ - **Structural Directives** — `#if`, `#for`, `#switch` with lazy loading support
11
+ - **Functional Directives** — `when()`, `choose()`, `repeat()`, `resolveAsync()`, `computeVirtualSlice()`
12
+ - **LIS-Based Keyed Diff** — O(n log n) algorithm for minimal DOM moves during list reconciliation
13
+ - **Template Instance Reuse** — Same template structure = update values in place, skip DOM teardown
14
+ - **Directive System** — Extensible with `@directive` / `@lazyDirective` decorators
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @miura/miura-render
20
+ ```
21
+
22
+ ## Template Syntax
23
+
24
+ ### Text Interpolation
25
+
26
+ ```typescript
27
+ html`<h1>Hello ${this.name}</h1>`
28
+ ```
29
+
30
+ ### Binding Prefixes
31
+
32
+ | Prefix | Type | Description |
33
+ |--------|------|-------------|
34
+ | *(none)* | Node | Text content or nested templates |
35
+ | `@` | Event | DOM event listener with modifier support |
36
+ | `.` | Property | Set a DOM property directly |
37
+ | `?` | Boolean | Toggle an HTML attribute on/off |
38
+ | `&` | Bind | Two-way binding (property + event listener) |
39
+ | `#` | Directive / Ref | Structural directives or element references |
40
+ | `class` | Class | Object map to class list |
41
+ | `style` | Style | Object map to inline styles |
42
+
43
+ ### Event Binding
44
+
45
+ ```typescript
46
+ html`<button @click=${this.handleClick}>Click</button>`
47
+
48
+ // With modifiers
49
+ html`<form @submit|prevent=${this.handleSubmit}>...</form>`
50
+ html`<button @click|prevent,stop=${this.handler}>Go</button>`
51
+ ```
52
+
53
+ ### Property Binding
54
+
55
+ ```typescript
56
+ html`<input .value=${this.text}>`
57
+ html`<my-component .data=${this.config}>`
58
+ ```
59
+
60
+ ### Boolean Binding
61
+
62
+ ```typescript
63
+ html`<button ?disabled=${this.loading}>Submit</button>`
64
+ html`<details ?open=${this.expanded}>...</details>`
65
+ ```
66
+
67
+ ### Two-Way Binding (`&`)
68
+
69
+ ```typescript
70
+ // Tuple form: [currentValue, setter]
71
+ html`<input &value=${[this.name, (v) => this.name = v]}>`
72
+
73
+ // Binder object form: { value, set }
74
+ html`<input &value=${{ value: this.name, set: (v) => this.name = v }}>`
75
+ ```
76
+
77
+ Auto-detected events: `value` -> `input`, `checked`/`selected`/`files` -> `change`.
78
+
79
+ ### Class Binding
80
+
81
+ ```typescript
82
+ html`<div class=${{ active: this.isActive, disabled: this.off }}>...</div>`
83
+ ```
84
+
85
+ ### Style Binding
86
+
87
+ ```typescript
88
+ html`<div style=${{ color: 'red', fontSize: '16px' }}>...</div>`
89
+ ```
90
+
91
+ ### Multi-Expression Attributes
92
+
93
+ ```typescript
94
+ html`<div title="Hello ${this.first} ${this.last}">...</div>`
95
+ ```
96
+
97
+ Multiple expressions in the same attribute are automatically grouped and concatenated.
98
+
99
+ ## Functional Directives
100
+
101
+ ### `when(condition, trueCase, falseCase?)`
102
+
103
+ Conditional rendering. Only the active branch is evaluated.
104
+
105
+ ```typescript
106
+ ${when(this.loggedIn,
107
+ () => html`<user-panel></user-panel>`,
108
+ () => html`<login-form></login-form>`
109
+ )}
110
+ ```
111
+
112
+ ### `choose(value, cases, defaultCase?)`
113
+
114
+ Multi-branch conditional (like a switch expression).
115
+
116
+ ```typescript
117
+ ${choose(this.view, [
118
+ ['list', () => html`<list-view></list-view>`],
119
+ ['grid', () => html`<grid-view></grid-view>`],
120
+ ['detail', () => html`<detail-view></detail-view>`],
121
+ ], () => html`<not-found></not-found>`)}
122
+ ```
123
+
124
+ ### `repeat(items, keyFn, templateFn)`
125
+
126
+ Keyed list rendering with **LIS-based diffing**.
127
+
128
+ ```typescript
129
+ ${repeat(this.items,
130
+ (item) => item.id,
131
+ (item, index) => html`<item-row .data=${item}></item-row>`
132
+ )}
133
+ ```
134
+
135
+ The algorithm identifies items already in correct relative order (via Longest Increasing Subsequence), then only moves out-of-order items. Minimizes DOM operations from O(n) to O(n - LIS length).
136
+
137
+ ### `resolveAsync(tracker, resolved, pending?, rejected?)`
138
+
139
+ Declarative promise-based rendering.
140
+
141
+ ```typescript
142
+ import { createAsyncTracker, resolveAsync } from '@miura/miura-render';
143
+
144
+ // Create a tracker
145
+ const tracker = createAsyncTracker(
146
+ fetch('/api/user').then(r => r.json()),
147
+ () => this.requestUpdate()
148
+ );
149
+
150
+ // Render based on state
151
+ ${resolveAsync(tracker,
152
+ (data) => html`<p>${data.name}</p>`,
153
+ () => html`<p>Loading...</p>`,
154
+ (err) => html`<p>Error: ${err.message}</p>`
155
+ )}
156
+ ```
157
+
158
+ ### `#async` Directive
159
+
160
+ Directive that tracks a Promise and renders `<template pending>`, `<template resolved>`, or `<template rejected>` — the same pattern as `#switch` with `<template case>` / `<template default>`:
161
+
162
+ ```html
163
+ <div #async=${this.userPromise}>
164
+ <template pending>
165
+ <p>Loading…</p>
166
+ </template>
167
+ <template resolved>
168
+ <p>Data loaded!</p>
169
+ </template>
170
+ <template rejected>
171
+ <p>Something went wrong.</p>
172
+ </template>
173
+ </div>
174
+ ```
175
+
176
+ The directive:
177
+ - Scans child `<template>` elements for `pending`, `resolved`, `rejected` attributes
178
+ - Shows the `pending` template immediately when a new promise is assigned
179
+ - Swaps to `resolved` or `rejected` when the promise settles
180
+ - Ignores stale promises if a new one is assigned before settlement
181
+
182
+ ### `#virtualScroll` Directive
183
+
184
+ Structural directive that virtualizes a large list. Manages the scroll container, spacer, and visible slice internally — no manual scroll listeners needed:
185
+
186
+ ```typescript
187
+ html`<div #virtualScroll=${{
188
+ items: this.allItems, // full array
189
+ itemHeight: 40, // px per row
190
+ containerHeight: 400, // viewport px
191
+ render: (item, i) => html`<div>${item.name}</div>`,
192
+ overscan: 3, // buffer rows
193
+ }}></div>`
194
+ ```
195
+
196
+ The directive:
197
+ - Creates a scroll container with the specified height
198
+ - Adds a spacer div for the correct total scrollable height
199
+ - Renders only the visible items plus overscan buffer
200
+ - Updates on scroll via `requestAnimationFrame` (no reactive cycle needed)
201
+
202
+ ### `computeVirtualSlice(config, scrollTop)`
203
+
204
+ Lower-level pure function for custom virtual scroll implementations:
205
+
206
+ ```typescript
207
+ import { computeVirtualSlice } from '@miura/miura-render';
208
+
209
+ const vs = computeVirtualSlice({
210
+ items: this.allItems,
211
+ itemHeight: 40,
212
+ containerHeight: 400,
213
+ render: (item, i) => html`<div>${item.name}</div>`,
214
+ overscan: 3,
215
+ }, this.scrollTop);
216
+
217
+ // Use vs.visibleItems, vs.totalHeight, vs.startIndex, etc.
218
+ ```
219
+
220
+ ## Structural Directives
221
+
222
+ Built-in directives that control DOM structure:
223
+
224
+ | Directive | Description |
225
+ |-----------|-------------|
226
+ | `#if` | Conditional rendering |
227
+ | `#for` | List iteration (callback mode or template mode with `{{$item}}`/`{{$index}}`) |
228
+ | `#switch` | Multi-case rendering |
229
+ | `#async` | Promise-driven pending/resolved/rejected rendering |
230
+ | `#virtualScroll` | Virtual scrolling for large lists |
231
+
232
+ Custom directives can be registered via `@directive` or `@lazyDirective` decorators. Lazy directives are only loaded when first used.
233
+
234
+ ## Architecture
235
+
236
+ ### Parser
237
+
238
+ A state-machine (`TemplateParser`) that walks template strings character by character, tracking context (text, tag, attribute name, attribute value, comment) to correctly identify binding positions. Outputs an HTML string with markers and a `TemplateBinding[]` array.
239
+
240
+ ### Binding Manager
241
+
242
+ `BindingManager` creates binding instances from the parser output and initializes them with values. Each binding type implements the `Binding` interface:
243
+
244
+ ```typescript
245
+ interface Binding {
246
+ setValue(value: unknown, context?: unknown): void | Promise<void>;
247
+ clear(): void;
248
+ disconnect?(): void;
249
+ }
250
+ ```
251
+
252
+ ### Template Instance Reuse
253
+
254
+ When a `NodeBinding` receives a new `TemplateResult` with the same `strings` reference as the previous render, it calls `instance.update(newValues)` instead of tearing down and rebuilding the DOM.
255
+
256
+ ### Keyed Diff (LIS Algorithm)
257
+
258
+ `KeyedListState` manages keyed list reconciliation:
259
+
260
+ 1. Compute new keys, remove items with deleted keys
261
+ 2. Reuse existing `TemplateInstance` objects for surviving keys
262
+ 3. Build a position map (old index per key) and compute the **Longest Increasing Subsequence**
263
+ 4. Items in the LIS stay in place; all others are moved via `insertBefore`
264
+
265
+ This is the same algorithm used by Vue and Svelte for list reconciliation.
266
+
267
+ ## AOT Compiler
268
+
269
+ In addition to the default JIT rendering path, `miura-render` ships a `TemplateCompiler` that generates optimised `render()`/`update()` JS functions via `new Function()`. Component classes opt in with `static compiler = 'AOT' as const` on `MiuraElement`.
270
+
271
+ ### How it works
272
+
273
+ ```
274
+ Template string → TemplateParser → ParsedTemplate (HTML + TemplateBinding[])
275
+
276
+ CodeFactory.generateRenderFunction()
277
+ CodeFactory.generateUpdateFunction()
278
+
279
+ CompiledTemplate { render, update, nodeBindingIndices, directiveBindingInfos }
280
+ ```
281
+
282
+ **First render** — `compiled.render(values)` clones the template, walks it once with `TreeWalker` to build a `refs[]` array (element/comment node refs indexed by binding marker), applies initial values, returns `{ fragment, refs }`.
283
+
284
+ **Subsequent updates** — `compiled.update(refs, values)` patches `refs[N].el.value`, `refs[N].el.setAttribute(…)` etc. **directly on cached refs** — zero DOM queries.
285
+
286
+ ### Three-tier binding strategy
287
+
288
+ | Binding kind | Compiled code | External manager |
289
+ |---|---|---|
290
+ | Property / Boolean / Event / Class / Style / ObjectClass / ObjectStyle / Spread / Bind / Async / Reference | ✅ Inlined in generated JS | — |
291
+ | **Node** (text, `TemplateResult`, `repeat()`) | — | `NodeBinding` instance per ref |
292
+ | **Directive** (`#if`, `#for`, `#switch`, custom) | — | `DirectiveBinding` instance per ref |
293
+
294
+ `CompiledTemplate` exposes `nodeBindingIndices` and `directiveBindingInfos` so the caller can wire up the correct instances after the initial DOM render.
295
+
296
+ ### Direct usage
297
+
298
+ ```typescript
299
+ import { TemplateCompiler } from '@miura/miura-render';
300
+
301
+ const compiler = new TemplateCompiler();
302
+
303
+ // First call — parses + compiles (cached by strings reference)
304
+ const compiled = compiler.compile(result);
305
+
306
+ // First render
307
+ const { fragment, refs } = compiled.render(result.values);
308
+ shadowRoot.appendChild(fragment);
309
+
310
+ // Subsequent updates — zero DOM queries
311
+ compiled.update(refs, newResult.values);
312
+ ```
313
+
314
+ ## CSS Tagged Template
315
+
316
+ ```typescript
317
+ import { css } from '@miura/miura-render';
318
+
319
+ const styles = css`
320
+ :host { display: block; }
321
+ .title { font-weight: bold; }
322
+ `;
323
+ ```
324
+
325
+ Returns a `CSSResult` that can be applied to a shadow root via `adoptedStyleSheets` or a `<style>` element.
326
+
327
+ ## License
328
+
329
+ MIT
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@miurajs/miura-render",
3
+ "version": "0.0.0",
4
+ "main": "./index.ts",
5
+ "module": "./index.ts",
6
+ "types": "./index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "import": "./index.ts",
11
+ "require": "./index.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "vitest",
17
+ "test:watch": "vitest watch",
18
+ "test:coverage": "vitest run --coverage"
19
+ },
20
+ "type": "module",
21
+ "files": [
22
+ "index.js",
23
+ "index.d.ts"
24
+ ],
25
+ "dependencies": {
26
+ "@miura/miura-debugger": "workspace:*"
27
+ },
28
+ "devDependencies": {
29
+ "@types/jsdom": "^21.1.6",
30
+ "@vitest/coverage-v8": "^1.6.0",
31
+ "jsdom": "^24.0.0",
32
+ "typescript": "^5.4.5",
33
+ "vitest": "^1.6.0"
34
+ }
35
+ }