@constela/runtime 0.10.2 → 0.11.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/README.md +103 -245
- package/dist/index.d.ts +30 -1
- package/dist/index.js +89 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @constela/runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Executes Constela JSON programs in the browser with fine-grained reactivity.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,299 +8,157 @@ Runtime DOM renderer for the Constela UI framework with fine-grained reactivity.
|
|
|
8
8
|
npm install @constela/runtime
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## How It Works
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Your JSON program:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
### createApp
|
|
22
|
-
|
|
23
|
-
Creates and mounts a Constela application.
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { createApp } from '@constela/runtime';
|
|
27
|
-
|
|
28
|
-
const app = createApp(compiledProgram, document.getElementById('app'));
|
|
29
|
-
|
|
30
|
-
// Later: cleanup
|
|
31
|
-
app.destroy();
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Parameters:**
|
|
35
|
-
- `program: CompiledProgram` - Compiled program from `@constela/compiler`
|
|
36
|
-
- `mount: HTMLElement` - DOM element to mount to
|
|
37
|
-
|
|
38
|
-
**Returns:** `AppInstance`
|
|
39
|
-
|
|
40
|
-
### hydrateApp
|
|
41
|
-
|
|
42
|
-
Hydrates server-rendered HTML without DOM reconstruction.
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { hydrateApp } from '@constela/runtime';
|
|
46
|
-
|
|
47
|
-
const app = hydrateApp({
|
|
48
|
-
program: compiledProgram,
|
|
49
|
-
mount: document.getElementById('app'),
|
|
50
|
-
route: {
|
|
51
|
-
params: { id: '123' },
|
|
52
|
-
query: { tab: 'details' },
|
|
53
|
-
path: '/users/123'
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"version": "1.0",
|
|
18
|
+
"state": {
|
|
19
|
+
"count": { "type": "number", "initial": 0 }
|
|
54
20
|
},
|
|
55
|
-
|
|
56
|
-
|
|
21
|
+
"actions": [
|
|
22
|
+
{
|
|
23
|
+
"name": "increment",
|
|
24
|
+
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"view": {
|
|
28
|
+
"kind": "element",
|
|
29
|
+
"tag": "button",
|
|
30
|
+
"props": { "onClick": { "event": "click", "action": "increment" } },
|
|
31
|
+
"children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
|
|
57
32
|
}
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
**HydrateOptions:**
|
|
62
|
-
- `program: CompiledProgram` - Compiled program
|
|
63
|
-
- `mount: HTMLElement` - Container element
|
|
64
|
-
- `route?: RouteContext` - Route parameters
|
|
65
|
-
- `imports?: Record<string, unknown>` - Import data
|
|
66
|
-
|
|
67
|
-
### AppInstance
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
interface AppInstance {
|
|
71
|
-
destroy(): void;
|
|
72
|
-
setState(name: string, value: unknown): void;
|
|
73
|
-
getState(name: string): unknown;
|
|
74
|
-
subscribe(name: string, fn: (value: unknown) => void): () => void;
|
|
75
33
|
}
|
|
76
34
|
```
|
|
77
35
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
36
|
+
Becomes an interactive app with:
|
|
37
|
+
- **Reactive state management** - Signal-based updates without virtual DOM
|
|
38
|
+
- **Efficient DOM updates** - Fine-grained reactivity
|
|
39
|
+
- **Event handling** - Declarative action binding
|
|
81
40
|
|
|
82
|
-
|
|
41
|
+
## Features
|
|
83
42
|
|
|
84
|
-
|
|
43
|
+
### Markdown Rendering
|
|
85
44
|
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"kind": "markdown",
|
|
48
|
+
"content": { "expr": "state", "name": "markdownContent" }
|
|
49
|
+
}
|
|
89
50
|
```
|
|
90
51
|
|
|
91
|
-
|
|
52
|
+
Rendered with [marked](https://marked.js.org/) and sanitized with [DOMPurify](https://github.com/cure53/DOMPurify).
|
|
92
53
|
|
|
93
|
-
|
|
54
|
+
### Code Blocks
|
|
94
55
|
|
|
95
|
-
```
|
|
96
|
-
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"kind": "code",
|
|
59
|
+
"code": { "expr": "lit", "value": "const x: number = 42;" },
|
|
60
|
+
"language": { "expr": "lit", "value": "typescript" }
|
|
61
|
+
}
|
|
97
62
|
```
|
|
98
63
|
|
|
99
|
-
|
|
64
|
+
Features:
|
|
65
|
+
- Syntax highlighting with [Shiki](https://shiki.style/)
|
|
66
|
+
- Dual theme support (light/dark)
|
|
67
|
+
- Built-in copy button
|
|
100
68
|
|
|
101
|
-
|
|
69
|
+
### Hydration
|
|
102
70
|
|
|
103
|
-
|
|
104
|
-
const unsubscribe = app.subscribe('count', (value) => {
|
|
105
|
-
console.log('count changed:', value);
|
|
106
|
-
});
|
|
71
|
+
Server-rendered HTML is hydrated on the client without DOM reconstruction:
|
|
107
72
|
|
|
108
|
-
|
|
109
|
-
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"version": "1.0",
|
|
76
|
+
"state": { "theme": { "type": "string", "initial": "light" } },
|
|
77
|
+
"lifecycle": {
|
|
78
|
+
"onMount": "loadTheme"
|
|
79
|
+
},
|
|
80
|
+
"actions": [
|
|
81
|
+
{
|
|
82
|
+
"name": "loadTheme",
|
|
83
|
+
"steps": [
|
|
84
|
+
{
|
|
85
|
+
"do": "storage",
|
|
86
|
+
"operation": "get",
|
|
87
|
+
"key": { "expr": "lit", "value": "theme" },
|
|
88
|
+
"storage": "local",
|
|
89
|
+
"result": "savedTheme",
|
|
90
|
+
"onSuccess": [
|
|
91
|
+
{ "do": "set", "target": "theme", "value": { "expr": "var", "name": "savedTheme" } }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"view": { ... }
|
|
98
|
+
}
|
|
110
99
|
```
|
|
111
100
|
|
|
112
|
-
##
|
|
113
|
-
|
|
114
|
-
Low-level reactive APIs for advanced usage.
|
|
115
|
-
|
|
116
|
-
### createSignal
|
|
117
|
-
|
|
118
|
-
Creates a reactive signal with fine-grained dependency tracking.
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import { createSignal } from '@constela/runtime';
|
|
122
|
-
|
|
123
|
-
const count = createSignal(0);
|
|
101
|
+
## Security
|
|
124
102
|
|
|
125
|
-
|
|
126
|
-
console.log(count.get()); // 0
|
|
103
|
+
The runtime includes security measures:
|
|
127
104
|
|
|
128
|
-
|
|
129
|
-
|
|
105
|
+
- **Prototype Pollution Prevention** - Blocks `__proto__`, `constructor`, `prototype`
|
|
106
|
+
- **Safe Globals** - Only exposes `JSON`, `Math`, `Date`, `Object`, `Array`, `String`, `Number`, `Boolean`, `console`
|
|
107
|
+
- **HTML Sanitization** - DOMPurify for Markdown content
|
|
130
108
|
|
|
131
|
-
|
|
132
|
-
const unsubscribe = count.subscribe((value) => {
|
|
133
|
-
console.log('Value:', value);
|
|
134
|
-
});
|
|
135
|
-
```
|
|
109
|
+
## Internal API
|
|
136
110
|
|
|
137
|
-
|
|
111
|
+
> For framework developers only. End users should use the CLI.
|
|
138
112
|
|
|
139
|
-
|
|
113
|
+
### createApp
|
|
140
114
|
|
|
141
115
|
```typescript
|
|
142
|
-
import {
|
|
143
|
-
|
|
144
|
-
const name = createSignal('World');
|
|
145
|
-
|
|
146
|
-
const cleanup = createEffect(() => {
|
|
147
|
-
console.log(`Hello, ${name.get()}!`);
|
|
148
|
-
|
|
149
|
-
// Optional cleanup function
|
|
150
|
-
return () => {
|
|
151
|
-
console.log('Effect cleaned up');
|
|
152
|
-
};
|
|
153
|
-
});
|
|
116
|
+
import { createApp } from '@constela/runtime';
|
|
154
117
|
|
|
155
|
-
|
|
118
|
+
const app = createApp(compiledProgram, document.getElementById('app'));
|
|
156
119
|
|
|
157
|
-
|
|
120
|
+
// Cleanup
|
|
121
|
+
app.destroy();
|
|
158
122
|
```
|
|
159
123
|
|
|
160
|
-
###
|
|
161
|
-
|
|
162
|
-
Centralized state management with signal-based reactivity.
|
|
124
|
+
### hydrateApp
|
|
163
125
|
|
|
164
126
|
```typescript
|
|
165
|
-
import {
|
|
127
|
+
import { hydrateApp } from '@constela/runtime';
|
|
166
128
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
129
|
+
const app = hydrateApp({
|
|
130
|
+
program: compiledProgram,
|
|
131
|
+
mount: document.getElementById('app'),
|
|
132
|
+
route: { params: { id: '123' }, query: new URLSearchParams(), path: '/users/123' },
|
|
133
|
+
imports: { config: { apiUrl: 'https://api.example.com' } }
|
|
170
134
|
});
|
|
171
|
-
|
|
172
|
-
store.get('count'); // 0
|
|
173
|
-
store.set('count', 5);
|
|
174
|
-
store.subscribe('count', (value) => console.log(value));
|
|
175
135
|
```
|
|
176
136
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
### evaluate
|
|
180
|
-
|
|
181
|
-
Evaluates compiled expressions.
|
|
137
|
+
### AppInstance
|
|
182
138
|
|
|
183
139
|
```typescript
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
imports: { config: { apiUrl: '...' } },
|
|
191
|
-
data: { posts: [...] },
|
|
192
|
-
refs: { inputEl: document.querySelector('#input') }
|
|
193
|
-
});
|
|
140
|
+
interface AppInstance {
|
|
141
|
+
destroy(): void;
|
|
142
|
+
setState(name: string, value: unknown): void;
|
|
143
|
+
getState(name: string): unknown;
|
|
144
|
+
subscribe(name: string, fn: (value: unknown) => void): () => void;
|
|
145
|
+
}
|
|
194
146
|
```
|
|
195
147
|
|
|
196
|
-
|
|
197
|
-
- Literals, state reads, variables
|
|
198
|
-
- Binary operations, logical not, conditionals
|
|
199
|
-
- Property access, array indexing
|
|
200
|
-
- Route parameters/query/path
|
|
201
|
-
- Imports and loaded data
|
|
202
|
-
- DOM refs
|
|
203
|
-
|
|
204
|
-
## Action Execution
|
|
205
|
-
|
|
206
|
-
### executeAction
|
|
207
|
-
|
|
208
|
-
Executes compiled actions.
|
|
148
|
+
### Reactive Primitives
|
|
209
149
|
|
|
210
150
|
```typescript
|
|
211
|
-
import {
|
|
212
|
-
|
|
213
|
-
await executeAction(action, {
|
|
214
|
-
state: stateStore,
|
|
215
|
-
locals: {},
|
|
216
|
-
route: { ... },
|
|
217
|
-
imports: { ... },
|
|
218
|
-
refs: { ... },
|
|
219
|
-
subscriptions: []
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Supported Steps:**
|
|
224
|
-
- `set`, `update` - State mutations
|
|
225
|
-
- `fetch` - HTTP requests
|
|
226
|
-
- `storage` - localStorage/sessionStorage
|
|
227
|
-
- `clipboard` - Clipboard operations
|
|
228
|
-
- `navigate` - Page navigation
|
|
229
|
-
- `import`, `call` - Dynamic imports and function calls
|
|
230
|
-
- `subscribe`, `dispose` - Event subscriptions
|
|
231
|
-
- `dom` - DOM manipulation
|
|
232
|
-
- `if` - Conditional execution
|
|
233
|
-
|
|
234
|
-
## Rendering
|
|
235
|
-
|
|
236
|
-
### render
|
|
151
|
+
import { createSignal, createEffect } from '@constela/runtime';
|
|
237
152
|
|
|
238
|
-
|
|
153
|
+
const count = createSignal(0);
|
|
154
|
+
count.get(); // Read
|
|
155
|
+
count.set(1); // Write
|
|
239
156
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const domNode = render(compiledNode, {
|
|
244
|
-
state: stateStore,
|
|
245
|
-
actions: compiledProgram.actions,
|
|
246
|
-
components: {},
|
|
247
|
-
locals: {},
|
|
248
|
-
route: { ... },
|
|
249
|
-
imports: { ... },
|
|
250
|
-
refs: {},
|
|
251
|
-
subscriptions: [],
|
|
252
|
-
cleanups: []
|
|
157
|
+
const cleanup = createEffect(() => {
|
|
158
|
+
console.log(`Count: ${count.get()}`);
|
|
253
159
|
});
|
|
254
160
|
```
|
|
255
161
|
|
|
256
|
-
**Supported Nodes:**
|
|
257
|
-
- Element nodes with props and event handlers
|
|
258
|
-
- Text nodes with reactive updates
|
|
259
|
-
- Conditional rendering (`if/else`)
|
|
260
|
-
- List rendering (`each`)
|
|
261
|
-
- Markdown with sanitization
|
|
262
|
-
- Code blocks with Shiki highlighting
|
|
263
|
-
|
|
264
|
-
## Markdown & Code Blocks
|
|
265
|
-
|
|
266
|
-
The runtime includes built-in support for rendering Markdown and syntax-highlighted code.
|
|
267
|
-
|
|
268
|
-
### Markdown
|
|
269
|
-
|
|
270
|
-
Rendered using [marked](https://marked.js.org/) with [DOMPurify](https://github.com/cure53/DOMPurify) sanitization.
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
{
|
|
274
|
-
"kind": "markdown",
|
|
275
|
-
"content": { "expr": "state", "name": "markdownContent" }
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Code Blocks
|
|
280
|
-
|
|
281
|
-
Rendered with [Shiki](https://shiki.style/) syntax highlighting.
|
|
282
|
-
|
|
283
|
-
```json
|
|
284
|
-
{
|
|
285
|
-
"kind": "code",
|
|
286
|
-
"code": { "expr": "lit", "value": "const x = 1;" },
|
|
287
|
-
"language": { "expr": "lit", "value": "typescript" }
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
**Features:**
|
|
292
|
-
- Dual theme support (light/dark)
|
|
293
|
-
- Copy button with feedback
|
|
294
|
-
- Dynamic language loading
|
|
295
|
-
|
|
296
|
-
## Security
|
|
297
|
-
|
|
298
|
-
The runtime includes security measures:
|
|
299
|
-
|
|
300
|
-
- **Prototype Pollution Prevention** - Blocks `__proto__`, `constructor`, `prototype`
|
|
301
|
-
- **Safe Globals** - Only exposes `JSON`, `Math`, `Date`, `Object`, `Array`, `String`, `Number`, `Boolean`, `console`
|
|
302
|
-
- **HTML Sanitization** - DOMPurify for Markdown content
|
|
303
|
-
|
|
304
162
|
## License
|
|
305
163
|
|
|
306
164
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,17 @@ declare function createStateStore(definitions: Record<string, StateDefinition>):
|
|
|
47
47
|
* - Property access (get)
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Style preset definition - matches @constela/core StylePreset
|
|
52
|
+
*/
|
|
53
|
+
interface StylePreset {
|
|
54
|
+
base: string;
|
|
55
|
+
variants?: Record<string, Record<string, string>>;
|
|
56
|
+
defaultVariants?: Record<string, string>;
|
|
57
|
+
compoundVariants?: Array<Record<string, string> & {
|
|
58
|
+
class: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
50
61
|
interface EvaluationContext {
|
|
51
62
|
state: StateStore;
|
|
52
63
|
locals: Record<string, unknown>;
|
|
@@ -57,8 +68,25 @@ interface EvaluationContext {
|
|
|
57
68
|
};
|
|
58
69
|
imports?: Record<string, unknown>;
|
|
59
70
|
refs?: Record<string, Element>;
|
|
71
|
+
styles?: Record<string, StylePreset>;
|
|
60
72
|
}
|
|
61
73
|
declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unknown;
|
|
74
|
+
/**
|
|
75
|
+
* Style expression type for evaluateStyle
|
|
76
|
+
*/
|
|
77
|
+
interface StyleExprInput {
|
|
78
|
+
expr: 'style';
|
|
79
|
+
name: string;
|
|
80
|
+
variants?: Record<string, CompiledExpression>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Evaluates a style expression to produce CSS class names
|
|
84
|
+
*
|
|
85
|
+
* @param expr - The style expression to evaluate
|
|
86
|
+
* @param ctx - The evaluation context containing styles presets
|
|
87
|
+
* @returns The computed CSS class string, or undefined if preset not found
|
|
88
|
+
*/
|
|
89
|
+
declare function evaluateStyle(expr: StyleExprInput, ctx: EvaluationContext): string | undefined;
|
|
62
90
|
|
|
63
91
|
/**
|
|
64
92
|
* Action Executor - Executes compiled action steps
|
|
@@ -105,6 +133,7 @@ interface RenderContext {
|
|
|
105
133
|
imports?: Record<string, unknown>;
|
|
106
134
|
cleanups?: (() => void)[];
|
|
107
135
|
refs?: Record<string, Element>;
|
|
136
|
+
inSvg?: boolean;
|
|
108
137
|
}
|
|
109
138
|
declare function render(node: CompiledNode, ctx: RenderContext): Node;
|
|
110
139
|
|
|
@@ -174,4 +203,4 @@ interface HydrateOptions {
|
|
|
174
203
|
*/
|
|
175
204
|
declare function hydrateApp(options: HydrateOptions): AppInstance;
|
|
176
205
|
|
|
177
|
-
export { type ActionContext, type AppInstance, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, createApp, createEffect, createSignal, createStateStore, evaluate, executeAction, hydrateApp, render };
|
|
206
|
+
export { type ActionContext, type AppInstance, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, type StylePreset, createApp, createEffect, createSignal, createStateStore, evaluate, evaluateStyle, executeAction, hydrateApp, render };
|
package/dist/index.js
CHANGED
|
@@ -251,6 +251,8 @@ function evaluate(expr, ctx) {
|
|
|
251
251
|
case "param": {
|
|
252
252
|
return void 0;
|
|
253
253
|
}
|
|
254
|
+
case "style":
|
|
255
|
+
return evaluateStyle(expr, ctx);
|
|
254
256
|
default: {
|
|
255
257
|
const _exhaustiveCheck = expr;
|
|
256
258
|
throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
|
|
@@ -349,6 +351,36 @@ function evaluateBinary(op, left, right, ctx) {
|
|
|
349
351
|
throw new Error("Unknown binary operator: " + op);
|
|
350
352
|
}
|
|
351
353
|
}
|
|
354
|
+
function evaluateStyle(expr, ctx) {
|
|
355
|
+
const preset = ctx.styles?.[expr.name];
|
|
356
|
+
if (!preset) return void 0;
|
|
357
|
+
let classes = preset.base;
|
|
358
|
+
if (preset.variants) {
|
|
359
|
+
for (const variantKey of Object.keys(preset.variants)) {
|
|
360
|
+
let variantValueStr = null;
|
|
361
|
+
if (expr.variants?.[variantKey]) {
|
|
362
|
+
let variantValue;
|
|
363
|
+
try {
|
|
364
|
+
variantValue = evaluate(expr.variants[variantKey], ctx);
|
|
365
|
+
} catch {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (variantValue != null) {
|
|
369
|
+
variantValueStr = String(variantValue);
|
|
370
|
+
}
|
|
371
|
+
} else if (preset.defaultVariants?.[variantKey] !== void 0) {
|
|
372
|
+
variantValueStr = preset.defaultVariants[variantKey];
|
|
373
|
+
}
|
|
374
|
+
if (variantValueStr !== null) {
|
|
375
|
+
const variantClasses = preset.variants[variantKey]?.[variantValueStr];
|
|
376
|
+
if (variantClasses) {
|
|
377
|
+
classes += " " + variantClasses;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return classes.trim();
|
|
383
|
+
}
|
|
352
384
|
|
|
353
385
|
// src/action/executor.ts
|
|
354
386
|
function createEvalContext(ctx) {
|
|
@@ -2421,12 +2453,12 @@ function createDOMPurify() {
|
|
|
2421
2453
|
let URI_SAFE_ATTRIBUTES = null;
|
|
2422
2454
|
const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary", "title", "value", "style", "xmlns"]);
|
|
2423
2455
|
const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
|
|
2424
|
-
const
|
|
2456
|
+
const SVG_NAMESPACE2 = "http://www.w3.org/2000/svg";
|
|
2425
2457
|
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
2426
2458
|
let NAMESPACE = HTML_NAMESPACE;
|
|
2427
2459
|
let IS_EMPTY_INPUT = false;
|
|
2428
2460
|
let ALLOWED_NAMESPACES = null;
|
|
2429
|
-
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE,
|
|
2461
|
+
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE2, HTML_NAMESPACE], stringToString);
|
|
2430
2462
|
let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]);
|
|
2431
2463
|
let HTML_INTEGRATION_POINTS = addToSet({}, ["annotation-xml"]);
|
|
2432
2464
|
const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ["title", "style", "font", "a", "script"]);
|
|
@@ -2600,7 +2632,7 @@ function createDOMPurify() {
|
|
|
2600
2632
|
if (!ALLOWED_NAMESPACES[element2.namespaceURI]) {
|
|
2601
2633
|
return false;
|
|
2602
2634
|
}
|
|
2603
|
-
if (element2.namespaceURI ===
|
|
2635
|
+
if (element2.namespaceURI === SVG_NAMESPACE2) {
|
|
2604
2636
|
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
2605
2637
|
return tagName === "svg";
|
|
2606
2638
|
}
|
|
@@ -2613,13 +2645,13 @@ function createDOMPurify() {
|
|
|
2613
2645
|
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
2614
2646
|
return tagName === "math";
|
|
2615
2647
|
}
|
|
2616
|
-
if (parent.namespaceURI ===
|
|
2648
|
+
if (parent.namespaceURI === SVG_NAMESPACE2) {
|
|
2617
2649
|
return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName];
|
|
2618
2650
|
}
|
|
2619
2651
|
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
2620
2652
|
}
|
|
2621
2653
|
if (element2.namespaceURI === HTML_NAMESPACE) {
|
|
2622
|
-
if (parent.namespaceURI ===
|
|
2654
|
+
if (parent.namespaceURI === SVG_NAMESPACE2 && !HTML_INTEGRATION_POINTS[parentTagName]) {
|
|
2623
2655
|
return false;
|
|
2624
2656
|
}
|
|
2625
2657
|
if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
|
|
@@ -12831,6 +12863,40 @@ async function highlightCode(code, language) {
|
|
|
12831
12863
|
}
|
|
12832
12864
|
|
|
12833
12865
|
// src/renderer/index.ts
|
|
12866
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
12867
|
+
var SVG_TAGS = /* @__PURE__ */ new Set([
|
|
12868
|
+
"svg",
|
|
12869
|
+
"path",
|
|
12870
|
+
"line",
|
|
12871
|
+
"circle",
|
|
12872
|
+
"rect",
|
|
12873
|
+
"ellipse",
|
|
12874
|
+
"polyline",
|
|
12875
|
+
"polygon",
|
|
12876
|
+
"g",
|
|
12877
|
+
"defs",
|
|
12878
|
+
"use",
|
|
12879
|
+
"text",
|
|
12880
|
+
"tspan",
|
|
12881
|
+
"clipPath",
|
|
12882
|
+
"mask",
|
|
12883
|
+
"linearGradient",
|
|
12884
|
+
"radialGradient",
|
|
12885
|
+
"stop",
|
|
12886
|
+
"pattern",
|
|
12887
|
+
"symbol",
|
|
12888
|
+
"marker",
|
|
12889
|
+
"image",
|
|
12890
|
+
"filter",
|
|
12891
|
+
"foreignObject",
|
|
12892
|
+
"animate",
|
|
12893
|
+
"animateTransform",
|
|
12894
|
+
"desc",
|
|
12895
|
+
"title"
|
|
12896
|
+
]);
|
|
12897
|
+
function isSvgTag(tag) {
|
|
12898
|
+
return SVG_TAGS.has(tag);
|
|
12899
|
+
}
|
|
12834
12900
|
function isEventHandler(value) {
|
|
12835
12901
|
return typeof value === "object" && value !== null && "event" in value && "action" in value;
|
|
12836
12902
|
}
|
|
@@ -12853,7 +12919,10 @@ function render(node, ctx) {
|
|
|
12853
12919
|
}
|
|
12854
12920
|
}
|
|
12855
12921
|
function renderElement(node, ctx) {
|
|
12856
|
-
const
|
|
12922
|
+
const tag = node.tag;
|
|
12923
|
+
const inSvgContext = ctx.inSvg || tag === "svg";
|
|
12924
|
+
const useSvgNamespace = inSvgContext && isSvgTag(tag);
|
|
12925
|
+
const el = useSvgNamespace ? document.createElementNS(SVG_NAMESPACE, tag) : document.createElement(tag);
|
|
12857
12926
|
if (node.ref && ctx.refs) {
|
|
12858
12927
|
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production" && ctx.refs[node.ref]) {
|
|
12859
12928
|
console.warn(`Duplicate ref name "${node.ref}" detected. The later element will overwrite the earlier one.`);
|
|
@@ -12896,21 +12965,31 @@ function renderElement(node, ctx) {
|
|
|
12896
12965
|
} else {
|
|
12897
12966
|
const cleanup = createEffect(() => {
|
|
12898
12967
|
const value = evaluate(propValue, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
|
|
12899
|
-
applyProp(el, propName, value);
|
|
12968
|
+
applyProp(el, propName, value, useSvgNamespace);
|
|
12900
12969
|
});
|
|
12901
12970
|
ctx.cleanups?.push(cleanup);
|
|
12902
12971
|
}
|
|
12903
12972
|
}
|
|
12904
12973
|
}
|
|
12905
12974
|
if (node.children) {
|
|
12975
|
+
const childInSvg = tag === "foreignObject" ? false : inSvgContext;
|
|
12976
|
+
const childCtx = childInSvg !== ctx.inSvg ? { ...ctx, inSvg: childInSvg } : ctx;
|
|
12906
12977
|
for (const child of node.children) {
|
|
12907
|
-
const childNode = render(child,
|
|
12978
|
+
const childNode = render(child, childCtx);
|
|
12908
12979
|
el.appendChild(childNode);
|
|
12909
12980
|
}
|
|
12910
12981
|
}
|
|
12911
12982
|
return el;
|
|
12912
12983
|
}
|
|
12913
|
-
function applyProp(el, propName, value) {
|
|
12984
|
+
function applyProp(el, propName, value, isSvg = false) {
|
|
12985
|
+
if (isSvg && propName === "className") {
|
|
12986
|
+
if (value) {
|
|
12987
|
+
el.setAttribute("class", String(value));
|
|
12988
|
+
} else {
|
|
12989
|
+
el.removeAttribute("class");
|
|
12990
|
+
}
|
|
12991
|
+
return;
|
|
12992
|
+
}
|
|
12914
12993
|
if (propName === "className") {
|
|
12915
12994
|
el.className = String(value ?? "");
|
|
12916
12995
|
} else if (propName === "style" && typeof value === "string") {
|
|
@@ -13722,6 +13801,7 @@ export {
|
|
|
13722
13801
|
createSignal,
|
|
13723
13802
|
createStateStore,
|
|
13724
13803
|
evaluate,
|
|
13804
|
+
evaluateStyle,
|
|
13725
13805
|
executeAction,
|
|
13726
13806
|
hydrateApp,
|
|
13727
13807
|
render
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Runtime DOM renderer for Constela UI framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"dompurify": "^3.3.1",
|
|
19
19
|
"marked": "^17.0.1",
|
|
20
20
|
"shiki": "^3.20.0",
|
|
21
|
-
"@constela/
|
|
22
|
-
"@constela/
|
|
21
|
+
"@constela/compiler": "0.8.0",
|
|
22
|
+
"@constela/core": "0.8.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/dompurify": "^3.2.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"tsup": "^8.0.0",
|
|
30
30
|
"typescript": "^5.3.0",
|
|
31
31
|
"vitest": "^2.0.0",
|
|
32
|
-
"@constela/server": "
|
|
32
|
+
"@constela/server": "4.0.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=20.0.0"
|