cdom 0.0.13 → 0.0.15
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 +64 -6
- package/examples/dashboard.html +4 -4
- package/examples/hypermedia-demo.html +206 -0
- package/examples/macros.html +22 -22
- package/examples/profile.cdom +64 -0
- package/examples/query-test.html +29 -0
- package/examples/scratch.html +37 -29
- package/index.js +78 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -316,9 +316,9 @@ Macros allow you to define reusable logic templates entirely in JSON, without wr
|
|
|
316
316
|
},
|
|
317
317
|
"body": {
|
|
318
318
|
"*": [
|
|
319
|
-
"
|
|
320
|
-
{ "+": [1, "
|
|
321
|
-
{ "-": [1, "
|
|
319
|
+
"=$.basePrice",
|
|
320
|
+
{ "+": [1, "=$.taxRate"] },
|
|
321
|
+
{ "-": [1, "=$.discount"] }
|
|
322
322
|
]
|
|
323
323
|
}
|
|
324
324
|
}
|
|
@@ -328,7 +328,7 @@ Macros allow you to define reusable logic templates entirely in JSON, without wr
|
|
|
328
328
|
**Fields:**
|
|
329
329
|
- **`name`**: The macro identifier (becomes a callable helper)
|
|
330
330
|
- **`schema`** (optional): JSON Schema for input validation
|
|
331
|
-
- **`body`**: The template structure using
|
|
331
|
+
- **`body`**: The template structure using `=$.propertyName` to reference inputs
|
|
332
332
|
|
|
333
333
|
#### Calling a Macro
|
|
334
334
|
|
|
@@ -358,9 +358,67 @@ Result: `97.2` (100 × 1.08 × 0.90)
|
|
|
358
358
|
}
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
-
### 10.
|
|
361
|
+
### 10. Hypermedia Query Parameters (`$query`)
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
When loading `.cdom` files via the `src` or `href` attributes, you can pass parameters via the query string. These parameters are automatically available within the loaded component as **implicit macro arguments**.
|
|
364
|
+
|
|
365
|
+
#### Basic Usage
|
|
366
|
+
|
|
367
|
+
URL: `profile.cdom?name=Joe&id=123`
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"div": {
|
|
372
|
+
"children": [
|
|
373
|
+
{ "h2": ["Hello, ", "=$.name"] },
|
|
374
|
+
{ "p": ["User ID: ", "=$.id"] }
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Collision Resolution (`=$query`)
|
|
381
|
+
|
|
382
|
+
If you are inside a macro that uses the same argument name as a query parameter, the macro argument **shadows** the query parameter. To access the original URL parameters explicitly, use the `=$query` sigil.
|
|
383
|
+
|
|
384
|
+
```json
|
|
385
|
+
{
|
|
386
|
+
"=greet": { "name": "MacroUser" }
|
|
387
|
+
}
|
|
388
|
+
// Inside the greet macro body:
|
|
389
|
+
{ "p": ["Hello ", "=$.name"] } // Returns "Hello MacroUser"
|
|
390
|
+
{ "p": ["Original ", "=$query.name"] } // Returns "Original Joe"
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 11. The Dynamic Sigil Standard (`=`)
|
|
394
|
+
|
|
395
|
+
To ensure zero ambiguity between literal strings and dynamic references, cDOM follows a strict rule: **Everything dynamic starts with `=`.**
|
|
396
|
+
|
|
397
|
+
| Sigil | Target | Description |
|
|
398
|
+
| :--- | :--- | :--- |
|
|
399
|
+
| **`=/`** | Global State | Look up value in in-memory state proxy |
|
|
400
|
+
| **`=$.`** | Macro Argument | Reference an input passed to the current macro |
|
|
401
|
+
| **`=$this`** | Context Node | Reference the current DOM element |
|
|
402
|
+
| **`=$event`** | Logic Event | Reference the current DOM event (in handlers) |
|
|
403
|
+
| **`=$query`** | URL Query | Explicitly access Hypermedia URL parameters |
|
|
404
|
+
|
|
405
|
+
#### Shorthand Child Evaluation
|
|
406
|
+
As of v2.6.0, you can place these strings directly as children without an object wrapper:
|
|
407
|
+
|
|
408
|
+
```json
|
|
409
|
+
// ✅ Cleanest (Recommended)
|
|
410
|
+
{ "h1": "=$.title" }
|
|
411
|
+
|
|
412
|
+
// ✅ Also works
|
|
413
|
+
{ "h1": { "=": "=$.title" } }
|
|
414
|
+
|
|
415
|
+
// ⚠️ Deprecated
|
|
416
|
+
{ "h1": { "=": "$.title" } }
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### 11. Object-Based Helper Arguments
|
|
420
|
+
|
|
421
|
+
Helpers can accept either **positional arguments** (array) or **named arguments** (object):
|
|
364
422
|
|
|
365
423
|
**Positional (traditional):**
|
|
366
424
|
```json
|
package/examples/dashboard.html
CHANGED
|
@@ -143,12 +143,12 @@
|
|
|
143
143
|
{
|
|
144
144
|
"div": {
|
|
145
145
|
"class": "grid", "children": [
|
|
146
|
-
Metric('Process Load',
|
|
146
|
+
Metric('Process Load', "=/cpuUsage", '%', '⚡'),
|
|
147
147
|
// Using standard helpers 'round'
|
|
148
|
-
Metric('Memory Heap', { "=round": [
|
|
148
|
+
Metric('Memory Heap', { "=round": ["=/memUsage", 1] }, '%', '💾'),
|
|
149
149
|
// Using standard helpers 'fixed' and 'divide'
|
|
150
|
-
Metric('Net Throughput', { "=fixed": [{ "=divide": [
|
|
151
|
-
Metric('Network Jitter',
|
|
150
|
+
Metric('Net Throughput', { "=fixed": [{ "=divide": ["=/reqCount", 10] }, 1] }, 'k/s', '🚀'),
|
|
151
|
+
Metric('Network Jitter', "=/latency", 'ms', '📡')
|
|
152
152
|
]
|
|
153
153
|
}
|
|
154
154
|
},
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>cDOM | Hypermedia & $query Demo</title>
|
|
8
|
+
<script src="../index.js"></script>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap"
|
|
12
|
+
rel="stylesheet">
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--primary: #6366f1;
|
|
16
|
+
--bg: #f8fafc;
|
|
17
|
+
--card-bg: #ffffff;
|
|
18
|
+
--text: #1e293b;
|
|
19
|
+
--code-bg: #1e1e2e;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: 'Inter', sans-serif;
|
|
24
|
+
background-color: var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 2rem;
|
|
28
|
+
line-height: 1.5;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.container {
|
|
32
|
+
max-width: 1000px;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
header {
|
|
37
|
+
margin-bottom: 3rem;
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
h1 {
|
|
42
|
+
font-weight: 800;
|
|
43
|
+
letter-spacing: -0.025em;
|
|
44
|
+
margin-bottom: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.subtitle {
|
|
48
|
+
color: #64748b;
|
|
49
|
+
font-size: 1.125rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.demo-grid {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: 1fr 1fr;
|
|
55
|
+
gap: 2rem;
|
|
56
|
+
margin-bottom: 4rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 768px) {
|
|
60
|
+
.demo-grid {
|
|
61
|
+
grid-template-columns: 1fr;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.panel {
|
|
66
|
+
background: var(--card-bg);
|
|
67
|
+
border-radius: 16px;
|
|
68
|
+
border: 1px solid #e2e8f0;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.panel-header {
|
|
75
|
+
padding: 0.75rem 1.25rem;
|
|
76
|
+
background: #f1f5f9;
|
|
77
|
+
border-bottom: 1px solid #e2e8f0;
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
align-items: center;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.panel-title {
|
|
84
|
+
font-size: 0.75rem;
|
|
85
|
+
font-weight: 700;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: 0.05em;
|
|
88
|
+
color: #64748b;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.url-badge {
|
|
92
|
+
font-family: 'JetBrains Mono', monospace;
|
|
93
|
+
font-size: 0.75rem;
|
|
94
|
+
background: #e2e8f0;
|
|
95
|
+
padding: 0.2rem 0.6rem;
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
color: #475569;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.preview-area {
|
|
101
|
+
padding: 2rem;
|
|
102
|
+
flex: 1;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
background-image: radial-gradient(#e2e8f0 1px, transparent 1px);
|
|
107
|
+
background-size: 20px 20px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.code-area {
|
|
111
|
+
background: var(--code-bg);
|
|
112
|
+
color: #cdd6f4;
|
|
113
|
+
padding: 1.5rem;
|
|
114
|
+
font-family: 'JetBrains Mono', monospace;
|
|
115
|
+
font-size: 0.875rem;
|
|
116
|
+
margin: 0;
|
|
117
|
+
overflow: auto;
|
|
118
|
+
max-height: 500px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.tag {
|
|
122
|
+
display: inline-block;
|
|
123
|
+
padding: 0.25rem 0.75rem;
|
|
124
|
+
background: var(--primary);
|
|
125
|
+
color: white;
|
|
126
|
+
border-radius: 20px;
|
|
127
|
+
font-size: 0.75rem;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
margin-bottom: 1rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pre {
|
|
133
|
+
margin: 0;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
|
|
138
|
+
<body>
|
|
139
|
+
<div class="container">
|
|
140
|
+
<header>
|
|
141
|
+
<div class="tag">NEW FEATURE: $QUERY</div>
|
|
142
|
+
<h1>Hypermedia & Query Parameters</h1>
|
|
143
|
+
<p class="subtitle">Demonstrating $query resolution and implicit macro arguments in loaded components.</p>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<section class="demo-section">
|
|
147
|
+
<div class="demo-grid">
|
|
148
|
+
<!-- Result Panel -->
|
|
149
|
+
<div class="panel">
|
|
150
|
+
<div class="panel-header">
|
|
151
|
+
<span class="panel-title">Rendered Output</span>
|
|
152
|
+
<span class="url-badge">profile.cdom?name=Alice&id=USR-789</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="preview-area">
|
|
155
|
+
<!-- cDOM will load here -->
|
|
156
|
+
<div src="profile.cdom?name=Alice&role=Lead+Designer&region=North+America&id=USR-789"
|
|
157
|
+
style="width: 100%; max-width: 350px;"></div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Code Panel -->
|
|
162
|
+
<div class="panel">
|
|
163
|
+
<div class="panel-header">
|
|
164
|
+
<span class="panel-title">Source: profile.cdom</span>
|
|
165
|
+
</div>
|
|
166
|
+
<pre class="code-area" id="code-display">Loading source code...</pre>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</section>
|
|
170
|
+
|
|
171
|
+
<section style="background: white; padding: 2.5rem; border-radius: 24px; border: 1px solid #e2e8f0;">
|
|
172
|
+
<h2 style="margin-top: 0;">How it works</h2>
|
|
173
|
+
<p>
|
|
174
|
+
The <code>src</code> attribute on a <code>div</code> (or any non-native element) triggers a hypermedia
|
|
175
|
+
load.
|
|
176
|
+
Any query parameters in the URL are automatically parsed and injected as <b>implicit macro
|
|
177
|
+
arguments</b>.
|
|
178
|
+
</p>
|
|
179
|
+
<ul style="color: #475569; padding-left: 1.5rem;">
|
|
180
|
+
<li style="margin-bottom: 1rem;">
|
|
181
|
+
<strong>$.name</strong>: Accesses the parameter from the URL context. If a local macro defines a
|
|
182
|
+
<code>name</code> argument, it will shadow this value.
|
|
183
|
+
</li>
|
|
184
|
+
<li style="margin-bottom: 1rem;">
|
|
185
|
+
<strong>$query.name</strong>: Explicitly accesses the URL parameter, bypassing any local macro
|
|
186
|
+
shadowing.
|
|
187
|
+
</li>
|
|
188
|
+
<li style="margin-bottom: 1rem;">
|
|
189
|
+
<strong>Scope Bubbling</strong>: The engine searches up the component tree to find the nearest
|
|
190
|
+
match, ensuring that nested components can still reach global query parameters.
|
|
191
|
+
</li>
|
|
192
|
+
</ul>
|
|
193
|
+
</section>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<script>
|
|
197
|
+
// Fetch and display the source code of the .cdom file
|
|
198
|
+
fetch('profile.cdom')
|
|
199
|
+
.then(response => response.text())
|
|
200
|
+
.then(text => {
|
|
201
|
+
document.getElementById('code-display').textContent = text;
|
|
202
|
+
});
|
|
203
|
+
</script>
|
|
204
|
+
</body>
|
|
205
|
+
|
|
206
|
+
</html>
|
package/examples/macros.html
CHANGED
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
"div": {
|
|
89
89
|
"class": "test-section",
|
|
90
90
|
"children": [
|
|
91
|
-
{ "h2":
|
|
92
|
-
{ "p":
|
|
91
|
+
{ "h2": "Test 1: Define Macro" },
|
|
92
|
+
{ "p": "Defining a macro called 'adjusted_price' with schema validation..." },
|
|
93
93
|
{ "div": { "class": "json-label", "children": ["Macro Definition:"] } },
|
|
94
94
|
{
|
|
95
95
|
"pre": {
|
|
@@ -108,9 +108,9 @@
|
|
|
108
108
|
},
|
|
109
109
|
"body": {
|
|
110
110
|
"*": [
|
|
111
|
-
"
|
|
112
|
-
{ "+": [1, "
|
|
113
|
-
{ "-": [1, "
|
|
111
|
+
"=$.basePrice",
|
|
112
|
+
{ "+": [1, "=$.taxRate"] },
|
|
113
|
+
{ "-": [1, "=$.discount"] }
|
|
114
114
|
]
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -132,9 +132,9 @@
|
|
|
132
132
|
},
|
|
133
133
|
"body": {
|
|
134
134
|
"*": [
|
|
135
|
-
"
|
|
136
|
-
{ "+": [1, "
|
|
137
|
-
{ "-": [1, "
|
|
135
|
+
"=$.basePrice",
|
|
136
|
+
{ "+": [1, "=$.taxRate"] },
|
|
137
|
+
{ "-": [1, "=$.discount"] }
|
|
138
138
|
]
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
"div": {
|
|
153
153
|
"class": "test-section",
|
|
154
154
|
"children": [
|
|
155
|
-
{ "h2":
|
|
156
|
-
{ "p":
|
|
155
|
+
{ "h2": "Test 2: Call Macro with Literal Values" },
|
|
156
|
+
{ "p": "Calling macro with basePrice=100, taxRate=0.08, discount=0.10" },
|
|
157
157
|
{ "div": { "class": "json-label", "children": ["Macro Invocation:"] } },
|
|
158
158
|
{
|
|
159
159
|
"pre": {
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
]
|
|
184
184
|
}
|
|
185
185
|
},
|
|
186
|
-
{ "p":
|
|
186
|
+
{ "p": "Expected: $97.20 (100 × 1.08 × 0.90)" }
|
|
187
187
|
]
|
|
188
188
|
}
|
|
189
189
|
},
|
|
@@ -191,8 +191,8 @@
|
|
|
191
191
|
"div": {
|
|
192
192
|
"class": "test-section",
|
|
193
193
|
"children": [
|
|
194
|
-
{ "h2":
|
|
195
|
-
{ "p":
|
|
194
|
+
{ "h2": "Test 3: Call Macro with State References" },
|
|
195
|
+
{ "p": "Calling macro with values from state..." },
|
|
196
196
|
{ "div": { "class": "json-label", "children": ["Macro Invocation (State References):"] } },
|
|
197
197
|
{
|
|
198
198
|
"pre": {
|
|
@@ -229,8 +229,8 @@
|
|
|
229
229
|
"div": {
|
|
230
230
|
"class": "test-section",
|
|
231
231
|
"children": [
|
|
232
|
-
{ "h2":
|
|
233
|
-
{ "p":
|
|
232
|
+
{ "h2": "Test 4: Macro Without Schema" },
|
|
233
|
+
{ "p": "Defining a simple macro without schema validation..." },
|
|
234
234
|
{ "div": { "class": "json-label", "children": ["Macro Definition:"] } },
|
|
235
235
|
{
|
|
236
236
|
"pre": {
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
JSON.stringify({
|
|
239
239
|
"=macro": {
|
|
240
240
|
"name": "simple_sum",
|
|
241
|
-
"body": { "+": ["
|
|
241
|
+
"body": { "+": ["=$.a", "=$.b"] }
|
|
242
242
|
}
|
|
243
243
|
}, null, 4)
|
|
244
244
|
]
|
|
@@ -247,11 +247,11 @@
|
|
|
247
247
|
{
|
|
248
248
|
"=macro": {
|
|
249
249
|
"name": "simple_sum",
|
|
250
|
-
"body": { "+": ["
|
|
250
|
+
"body": { "+": ["=$.a", "=$.b"] }
|
|
251
251
|
}
|
|
252
252
|
},
|
|
253
253
|
{ "p": { "class": "success", "children": ["✓ simple_sum macro registered (no schema)"] } },
|
|
254
|
-
{ "p":
|
|
254
|
+
{ "p": "Calling: simple_sum(a=10, b=25)" },
|
|
255
255
|
{ "div": { "class": "json-label", "children": ["Macro Invocation:"] } },
|
|
256
256
|
{
|
|
257
257
|
"pre": {
|
|
@@ -287,8 +287,8 @@
|
|
|
287
287
|
"div": {
|
|
288
288
|
"class": "test-section",
|
|
289
289
|
"children": [
|
|
290
|
-
{ "h2":
|
|
291
|
-
{ "p":
|
|
290
|
+
{ "h2": "Test 5: Macro With String Operations" },
|
|
291
|
+
{ "p": "A macro that formats a greeting..." },
|
|
292
292
|
{ "div": { "class": "json-label", "children": ["Macro Definition:"] } },
|
|
293
293
|
{
|
|
294
294
|
"pre": {
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
JSON.stringify({
|
|
297
297
|
"=macro": {
|
|
298
298
|
"name": "greeting",
|
|
299
|
-
"body": { "=concat": ["Hello, ", "
|
|
299
|
+
"body": { "=concat": ["Hello, ", "=$.name", "!"] }
|
|
300
300
|
}
|
|
301
301
|
}, null, 4)
|
|
302
302
|
]
|
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
{
|
|
306
306
|
"=macro": {
|
|
307
307
|
"name": "greeting",
|
|
308
|
-
"body": { "=concat": ["Hello, ", "
|
|
308
|
+
"body": { "=concat": ["Hello, ", "=$.name", "!"] }
|
|
309
309
|
}
|
|
310
310
|
},
|
|
311
311
|
{ "p": { "class": "success", "children": ["✓ greeting macro registered"] } },
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"div": {
|
|
3
|
+
"class": "profile-card",
|
|
4
|
+
"style": "padding: 1.5rem; background: white; border-radius: 12px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); border: 1px solid #e5e7eb; font-family: 'Inter', sans-serif;",
|
|
5
|
+
"children": [
|
|
6
|
+
{
|
|
7
|
+
"div": {
|
|
8
|
+
"style": "display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;",
|
|
9
|
+
"children": [
|
|
10
|
+
{
|
|
11
|
+
"div": {
|
|
12
|
+
"style": "width: 48px; height: 48px; background: #6366f1; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 1.25rem;",
|
|
13
|
+
"children": [{ "=left": ["=$.name", 1] }]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"div": {
|
|
18
|
+
"children": [
|
|
19
|
+
{ "h3": { "style": "margin: 0; color: #111827;", "children": ["=$.name"] } },
|
|
20
|
+
{ "p": { "style": "margin: 0; color: #6b7280; font-size: 0.875rem;", "children": ["=$.role"] } }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"div": {
|
|
29
|
+
"style": "font-size: 0.875rem; color: #374151; display: flex; flex-direction: column; gap: 0.5rem;",
|
|
30
|
+
"children": [
|
|
31
|
+
{ "div": ["📍 Region: ", { "b": "=$.region" }] },
|
|
32
|
+
{ "div": ["🔑 ID: ", { "code": "=$query.id" }] }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"div": {
|
|
38
|
+
"style": "margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #f3f4f6;",
|
|
39
|
+
"oncreate": {
|
|
40
|
+
"=macro": {
|
|
41
|
+
"name": "Badge",
|
|
42
|
+
"body": {
|
|
43
|
+
"span": {
|
|
44
|
+
"style": "display: inline-flex; align-items: center; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; background: #e0e7ff; color: #4338ca;",
|
|
45
|
+
"children": ["=$.label"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"children": [
|
|
51
|
+
{ "p": { "style": "font-size: 0.75rem; color: #9ca3af; margin-bottom: 0.5rem;", "children": ["Generated via macro shadowing $.name:"] } },
|
|
52
|
+
{ "=Badge": { "label": { "=concat": ["Active: ", "=$.name"] } } },
|
|
53
|
+
{
|
|
54
|
+
"p": {
|
|
55
|
+
"style": "font-size: 0.75rem; color: #9ca3af; margin-top: 0.75rem;",
|
|
56
|
+
"children": ["Original URL param accessible via $query.name: ", { "b": "=$query.name" }]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>cDOM Query Param Test</title>
|
|
8
|
+
<script src="../index.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body style="padding: 2rem; font-family: sans-serif;">
|
|
12
|
+
<h1>cDOM Query Parameter Testing</h1>
|
|
13
|
+
|
|
14
|
+
<div style="margin-bottom: 2rem;">
|
|
15
|
+
<h3>Dynamic Load (Hypermedia)</h3>
|
|
16
|
+
<p>Loading <code>profile.cdom?name=Joe&location=London&id=123</code></p>
|
|
17
|
+
<div src="profile.cdom?name=Joe&location=London&id=123"></div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<hr>
|
|
21
|
+
|
|
22
|
+
<div style="margin-bottom: 2rem;">
|
|
23
|
+
<h3>Another Load</h3>
|
|
24
|
+
<p>Loading <code>profile.cdom?name=Alice&location=New York&id=456</code></p>
|
|
25
|
+
<div src="profile.cdom?name=Alice&location=New York&id=456"></div>
|
|
26
|
+
</div>
|
|
27
|
+
</body>
|
|
28
|
+
|
|
29
|
+
</html>
|
package/examples/scratch.html
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
1
2
|
<html>
|
|
2
|
-
<script src="../index.js"></script>
|
|
3
3
|
|
|
4
|
-
<
|
|
4
|
+
<head>
|
|
5
|
+
<title>cDOM | Scratchpad</title>
|
|
6
|
+
<script src="../index.js"></script>
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body style="font-family: 'Inter', sans-serif; padding: 2rem; line-height: 1.6;">
|
|
5
10
|
<script>
|
|
6
11
|
const { $, _ } = cDOM;
|
|
12
|
+
|
|
7
13
|
cDOM.helper('increment', (ref) => {
|
|
8
14
|
if (ref && typeof ref === 'object' && 'value' in ref) ref.value++;
|
|
9
15
|
else if (ref && typeof ref === 'object' && ref.count !== undefined) ref.count++;
|
|
@@ -27,42 +33,44 @@
|
|
|
27
33
|
console.log('App mounted to DOM!', this);
|
|
28
34
|
},
|
|
29
35
|
children: [
|
|
30
|
-
|
|
31
|
-
"
|
|
36
|
+
{ h1: "cDOM Scratchpad" },
|
|
37
|
+
"App Name: ", "=/appName", { br: {} },
|
|
38
|
+
"Local Counter (Scoped): ", "=/localCounter/count",
|
|
32
39
|
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
div: {
|
|
41
|
+
style: "margin: 1.5rem 0",
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
button: {
|
|
45
|
+
onclick: [
|
|
46
|
+
{ "=log": ["Starting array execution..."] },
|
|
47
|
+
{ "++": ["=/localCounter/count"] },
|
|
48
|
+
{ "=log": ["Counter is now:", "=/localCounter/count"] },
|
|
49
|
+
{ "set": ["=/appName", "cDOM Rocked!"] }
|
|
50
|
+
],
|
|
51
|
+
children: ["Array Handler Demo"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
41
55
|
}
|
|
42
56
|
},
|
|
43
57
|
{
|
|
44
58
|
div: {
|
|
59
|
+
style: "background: #f8fafc; padding: 1.5rem; border-radius: 12px; border: 1px solid #e2e8f0;",
|
|
45
60
|
children: [
|
|
61
|
+
"This div can also see the local counter: ", "=/localCounter/count",
|
|
46
62
|
{ br: {} },
|
|
47
|
-
"
|
|
48
|
-
{
|
|
49
|
-
{ br: {} },
|
|
50
|
-
"Math expression (count * 2 + 5): ", { "=": "=/localCounter/count * 2 + 5" },
|
|
51
|
-
{ br: {} },
|
|
52
|
-
{ br: {} },
|
|
63
|
+
{ b: ["Math expression (count * 2 + 5): ", "=/localCounter/count * 2 + 5"] },
|
|
64
|
+
{ hr: { style: "margin: 1.5rem 0; border: 0; border-top: 1px solid #e2e8f0;" } },
|
|
53
65
|
{
|
|
54
66
|
div: {
|
|
55
|
-
style:
|
|
56
|
-
"margin-top": "10px",
|
|
57
|
-
"color": "#666",
|
|
58
|
-
"font-size": "0.9em"
|
|
59
|
-
},
|
|
67
|
+
style: "font-size: 0.9em; color: #64748b; font-family: monospace;",
|
|
60
68
|
children: [
|
|
61
|
-
"XPath (Active Button Count): ", { "$": "count(//button)" }
|
|
62
|
-
"XPath (Button Label): ", { "$": "//button/text()" }
|
|
63
|
-
"Dynamic Helper { =sum: [1, 2, 3] }: ", { "=sum": [1, 2, 3] }
|
|
64
|
-
"Dynamic Helper{ =currency: [=/localCounter/count] }: ", { "=currency": ["=/localCounter/count"] }
|
|
65
|
-
"Undefined Helper: ", { "=This.Should.Not.Exist": [] }
|
|
69
|
+
{ div: ["XPath (Active Button Count): ", { "$": "count(//button)" }] },
|
|
70
|
+
{ div: ["XPath (Button Label): ", { "$": "//button/text()" }] },
|
|
71
|
+
{ div: ["Dynamic Helper { =sum: [1, 2, 3] }: ", { "=sum": [1, 2, 3] }] },
|
|
72
|
+
{ div: ["Dynamic Helper { =currency: [=/localCounter/count] }: ", { "=currency": ["=/localCounter/count"] }] },
|
|
73
|
+
{ div: ["Undefined Helper: ", { "=This.Should.Not.Exist": [] }] }
|
|
66
74
|
]
|
|
67
75
|
}
|
|
68
76
|
}
|
|
@@ -71,7 +79,7 @@
|
|
|
71
79
|
}
|
|
72
80
|
]
|
|
73
81
|
}
|
|
74
|
-
}
|
|
82
|
+
});
|
|
75
83
|
</script>
|
|
76
84
|
</body>
|
|
77
85
|
|
package/index.js
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
function tokenize(src) {
|
|
43
43
|
const results = [];
|
|
44
|
-
const tokenRegex = /\s*(\d*\.\d+|\d+|"([^"\\]|\\.)*"|'([^'\\]|\\.)*'
|
|
44
|
+
const tokenRegex = /\s*(\d*\.\d+|\d+|"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|=\/|=\$this|=\$event|=\$query|=\$|[a-zA-Z_$][\w$]*|==|!=|<=|>=|&&|\|\||[-+*/%^<>!?:.,(){}[\]])\s*/g;
|
|
45
45
|
let match;
|
|
46
46
|
while ((match = tokenRegex.exec(src)) !== null) {
|
|
47
47
|
results.push(match[1]);
|
|
@@ -272,9 +272,9 @@
|
|
|
272
272
|
return createPathFunction(path, 'state');
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
// Handle
|
|
276
|
-
if (t === '
|
|
277
|
-
let path = t;
|
|
275
|
+
// Handle =$this and =$event
|
|
276
|
+
if (t === '=$this' || t === '=$event') {
|
|
277
|
+
let path = t.slice(1); // Remove the '=' prefix
|
|
278
278
|
// Check for property access
|
|
279
279
|
while (true) {
|
|
280
280
|
const p = peek();
|
|
@@ -292,14 +292,15 @@
|
|
|
292
292
|
break;
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
|
-
|
|
296
|
-
if (path === '$
|
|
295
|
+
const root = path === '$this' ? (ctx) => ctx : (ctx, ev) => ev;
|
|
296
|
+
if (path === '$this') return root;
|
|
297
|
+
if (path === '$event') return root;
|
|
297
298
|
if (path.startsWith('$this.') || path.startsWith('$this/')) return createPathFunction(path.slice(6), '$this');
|
|
298
299
|
if (path.startsWith('$event.') || path.startsWith('$event/')) return createPathFunction(path.slice(7), '$event');
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
// Handle
|
|
302
|
-
if (t === '
|
|
302
|
+
// Handle =$.propertyName
|
|
303
|
+
if (t === '=$') {
|
|
303
304
|
const p = peek();
|
|
304
305
|
if (p === '.') {
|
|
305
306
|
next(); // consume '.'
|
|
@@ -324,6 +325,33 @@
|
|
|
324
325
|
}
|
|
325
326
|
}
|
|
326
327
|
|
|
328
|
+
// Handle =$query.propertyName
|
|
329
|
+
if (t === '=$query') {
|
|
330
|
+
const p = peek();
|
|
331
|
+
if (p === '.') {
|
|
332
|
+
next(); // consume '.'
|
|
333
|
+
let path = '';
|
|
334
|
+
while (true) {
|
|
335
|
+
const tok = peek();
|
|
336
|
+
if (tok && /^[\w$]/.test(tok)) {
|
|
337
|
+
path += next();
|
|
338
|
+
} else if (tok === '.' || tok === '/') {
|
|
339
|
+
const nextTok = peek(1);
|
|
340
|
+
if (nextTok && /^[\w$]/.test(nextTok)) {
|
|
341
|
+
path += next(); // eat . or /
|
|
342
|
+
path += next(); // eat identifier
|
|
343
|
+
} else {
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return createPathFunction(path, '$query');
|
|
351
|
+
}
|
|
352
|
+
return (ctx) => findContext(ctx, '_queryContext');
|
|
353
|
+
}
|
|
354
|
+
|
|
327
355
|
// Bareword: helper or error
|
|
328
356
|
return (ctx, ev) => {
|
|
329
357
|
const val = findInScope(ctx, t) || getHelper(t);
|
|
@@ -881,8 +909,8 @@
|
|
|
881
909
|
|
|
882
910
|
function evaluateStructural(obj, context, event, unsafe) {
|
|
883
911
|
if (typeof obj !== 'object' || obj === null || obj.nodeType) {
|
|
884
|
-
// Check if it's a string starting with '
|
|
885
|
-
if (typeof obj === 'string' && obj.startsWith('
|
|
912
|
+
// Check if it's a string starting with '=' (dynamic reference)
|
|
913
|
+
if (typeof obj === 'string' && obj.startsWith('=')) {
|
|
886
914
|
return evaluateStateExpression(obj, context, event);
|
|
887
915
|
}
|
|
888
916
|
return obj;
|
|
@@ -932,9 +960,10 @@
|
|
|
932
960
|
const v = val[k];
|
|
933
961
|
const isPath = typeof v === 'string' && (
|
|
934
962
|
v.startsWith('=/') ||
|
|
935
|
-
v.startsWith('
|
|
936
|
-
v
|
|
937
|
-
v
|
|
963
|
+
v.startsWith('=$.') ||
|
|
964
|
+
v.startsWith('=$this') ||
|
|
965
|
+
v.startsWith('=$event') ||
|
|
966
|
+
v.startsWith('=$query')
|
|
938
967
|
);
|
|
939
968
|
|
|
940
969
|
if (isPath) {
|
|
@@ -953,9 +982,10 @@
|
|
|
953
982
|
const resolvedArgs = args.map(arg => {
|
|
954
983
|
const isPath = typeof arg === 'string' && (
|
|
955
984
|
arg.startsWith('=/') ||
|
|
956
|
-
arg.startsWith('
|
|
957
|
-
arg
|
|
958
|
-
arg
|
|
985
|
+
arg.startsWith('=$.') ||
|
|
986
|
+
arg.startsWith('=$this') ||
|
|
987
|
+
arg.startsWith('=$event') ||
|
|
988
|
+
arg.startsWith('=$query')
|
|
959
989
|
);
|
|
960
990
|
|
|
961
991
|
if (isPath) {
|
|
@@ -1018,6 +1048,15 @@
|
|
|
1018
1048
|
return registry.get(name);
|
|
1019
1049
|
}
|
|
1020
1050
|
|
|
1051
|
+
function findContext(node, key) {
|
|
1052
|
+
let curr = node;
|
|
1053
|
+
while (curr) {
|
|
1054
|
+
if (curr[key]) return curr[key];
|
|
1055
|
+
curr = curr.parentNode || curr.host || curr._lv_parent;
|
|
1056
|
+
}
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1021
1060
|
function evaluateStateExpression(expression, contextNode, event) {
|
|
1022
1061
|
let compiled = stateExpressionCache.get(expression);
|
|
1023
1062
|
if (!compiled) {
|
|
@@ -1052,7 +1091,8 @@
|
|
|
1052
1091
|
let root;
|
|
1053
1092
|
if (type === '$this') root = ctx;
|
|
1054
1093
|
else if (type === '$event') root = ev;
|
|
1055
|
-
else if (type === '$macro') root = ctx
|
|
1094
|
+
else if (type === '$macro') root = findContext(ctx, '_macroContext');
|
|
1095
|
+
else if (type === '$query') root = findContext(ctx, '_queryContext');
|
|
1056
1096
|
else root = findInScope(ctx, name);
|
|
1057
1097
|
|
|
1058
1098
|
if (!root) return (type === 'state') ? `[Unknown: ${name}]` : undefined;
|
|
@@ -1255,6 +1295,9 @@
|
|
|
1255
1295
|
}
|
|
1256
1296
|
|
|
1257
1297
|
if (typeof onode !== 'object') {
|
|
1298
|
+
if (typeof onode === 'string' && onode.startsWith('=') && onode.length > 1) {
|
|
1299
|
+
return cdomToDOM({ "=": onode }, wasString, unsafe, context);
|
|
1300
|
+
}
|
|
1258
1301
|
return document.createTextNode(onode);
|
|
1259
1302
|
}
|
|
1260
1303
|
|
|
@@ -1305,7 +1348,14 @@
|
|
|
1305
1348
|
const el = document.createElement(tag);
|
|
1306
1349
|
if (context) Object.defineProperty(el, '_lv_parent', { value: context, enumerable: false, configurable: true });
|
|
1307
1350
|
|
|
1308
|
-
|
|
1351
|
+
// Check if content is a structural reference object itself
|
|
1352
|
+
const isStructural = (obj) => {
|
|
1353
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj) || obj.nodeType) return false;
|
|
1354
|
+
const keys = Object.keys(obj);
|
|
1355
|
+
return keys.length === 1 && (keys[0] === '=' || keys[0] === '$' || keys[0].startsWith('=') || symbolicOperators.has(keys[0]));
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
if (typeof content === 'object' && content !== null && !Array.isArray(content) && !content.nodeType && !isStructural(content)) {
|
|
1309
1359
|
for (const key in content) {
|
|
1310
1360
|
const val = content[key];
|
|
1311
1361
|
if (key === 'children') {
|
|
@@ -1404,8 +1454,10 @@
|
|
|
1404
1454
|
return;
|
|
1405
1455
|
}
|
|
1406
1456
|
|
|
1407
|
-
// Check if it's a CSS selector
|
|
1408
|
-
if
|
|
1457
|
+
// Check if it's a CSS selector
|
|
1458
|
+
// We avoid querySelector if it looks like a file path or URL to prevent DOMException
|
|
1459
|
+
const isUrlLike = srcValue.includes('/') || srcValue.includes('?');
|
|
1460
|
+
if (!isUrlLike && !srcValue.startsWith('http')) {
|
|
1409
1461
|
try {
|
|
1410
1462
|
const source = document.querySelector(srcValue);
|
|
1411
1463
|
if (source) {
|
|
@@ -1434,13 +1486,17 @@
|
|
|
1434
1486
|
|
|
1435
1487
|
const contentType = response.headers.get('content-type') || '';
|
|
1436
1488
|
const text = await response.text();
|
|
1489
|
+
const pathname = url.pathname.toLowerCase();
|
|
1437
1490
|
|
|
1438
1491
|
// Determine how to handle content
|
|
1439
|
-
const isCDOM =
|
|
1440
|
-
const isHTML =
|
|
1492
|
+
const isCDOM = pathname.endsWith('.cdom') || contentType.includes('application/cdom') || contentType.includes('application/json');
|
|
1493
|
+
const isHTML = pathname.endsWith('.html') || pathname.endsWith('.htm') || contentType.includes('text/html');
|
|
1441
1494
|
|
|
1442
1495
|
if (isCDOM) {
|
|
1443
1496
|
const cdomData = JSON.parse(text);
|
|
1497
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
1498
|
+
element._queryContext = query;
|
|
1499
|
+
if (!element._macroContext) element._macroContext = query;
|
|
1444
1500
|
const dom = cdomToDOM(cdomData, true, false, element);
|
|
1445
1501
|
element.replaceChildren(dom);
|
|
1446
1502
|
} else if (isHTML) {
|