cdom 0.0.13 → 0.0.14
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/profile.cdom +64 -0
- package/examples/query-test.html +29 -0
- package/examples/scratch.html +37 -29
- package/index.js +74 -14
- 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>
|
|
@@ -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|=\$|\$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,11 @@
|
|
|
272
272
|
return createPathFunction(path, 'state');
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
// Handle $this and $event
|
|
276
|
-
if (t === '$this' || t === '$event') {
|
|
275
|
+
// Handle $this and $event (and deprecated $this/$event)
|
|
276
|
+
if (t === '=$this' || t === '=$event' || t === '$this' || t === '$event') {
|
|
277
|
+
const isDeprecated = !t.startsWith('=');
|
|
277
278
|
let path = t;
|
|
279
|
+
if (!isDeprecated) path = t.slice(1);
|
|
278
280
|
// Check for property access
|
|
279
281
|
while (true) {
|
|
280
282
|
const p = peek();
|
|
@@ -292,14 +294,15 @@
|
|
|
292
294
|
break;
|
|
293
295
|
}
|
|
294
296
|
}
|
|
295
|
-
|
|
296
|
-
if (path === '$
|
|
297
|
+
const root = path === '$this' ? (ctx) => ctx : (ctx, ev) => ev;
|
|
298
|
+
if (path === '$this') return root;
|
|
299
|
+
if (path === '$event') return root;
|
|
297
300
|
if (path.startsWith('$this.') || path.startsWith('$this/')) return createPathFunction(path.slice(6), '$this');
|
|
298
301
|
if (path.startsWith('$event.') || path.startsWith('$event/')) return createPathFunction(path.slice(7), '$event');
|
|
299
302
|
}
|
|
300
303
|
|
|
301
|
-
// Handle $.propertyName
|
|
302
|
-
if (t === '$') {
|
|
304
|
+
// Handle $.propertyName and =$$.propertyName
|
|
305
|
+
if (t === '$' || t === '=$') {
|
|
303
306
|
const p = peek();
|
|
304
307
|
if (p === '.') {
|
|
305
308
|
next(); // consume '.'
|
|
@@ -324,6 +327,33 @@
|
|
|
324
327
|
}
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
// Handle $query.propertyName and =$query.propertyName
|
|
331
|
+
if (t === '$query' || t === '=$query') {
|
|
332
|
+
const p = peek();
|
|
333
|
+
if (p === '.') {
|
|
334
|
+
next(); // consume '.'
|
|
335
|
+
let path = '';
|
|
336
|
+
while (true) {
|
|
337
|
+
const tok = peek();
|
|
338
|
+
if (tok && /^[\w$]/.test(tok)) {
|
|
339
|
+
path += next();
|
|
340
|
+
} else if (tok === '.' || tok === '/') {
|
|
341
|
+
const nextTok = peek(1);
|
|
342
|
+
if (nextTok && /^[\w$]/.test(nextTok)) {
|
|
343
|
+
path += next(); // eat . or /
|
|
344
|
+
path += next(); // eat identifier
|
|
345
|
+
} else {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return createPathFunction(path, '$query');
|
|
353
|
+
}
|
|
354
|
+
return (ctx) => findContext(ctx, '_queryContext');
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
// Bareword: helper or error
|
|
328
358
|
return (ctx, ev) => {
|
|
329
359
|
const val = findInScope(ctx, t) || getHelper(t);
|
|
@@ -881,7 +911,11 @@
|
|
|
881
911
|
|
|
882
912
|
function evaluateStructural(obj, context, event, unsafe) {
|
|
883
913
|
if (typeof obj !== 'object' || obj === null || obj.nodeType) {
|
|
884
|
-
// Check if it's a string starting with '
|
|
914
|
+
// Check if it's a string starting with '=' (dynamic reference)
|
|
915
|
+
if (typeof obj === 'string' && obj.startsWith('=')) {
|
|
916
|
+
return evaluateStateExpression(obj, context, event);
|
|
917
|
+
}
|
|
918
|
+
// Temporarily support deprecated bare $. for backward compatibility
|
|
885
919
|
if (typeof obj === 'string' && obj.startsWith('$.')) {
|
|
886
920
|
return evaluateStateExpression(obj, context, event);
|
|
887
921
|
}
|
|
@@ -1018,6 +1052,15 @@
|
|
|
1018
1052
|
return registry.get(name);
|
|
1019
1053
|
}
|
|
1020
1054
|
|
|
1055
|
+
function findContext(node, key) {
|
|
1056
|
+
let curr = node;
|
|
1057
|
+
while (curr) {
|
|
1058
|
+
if (curr[key]) return curr[key];
|
|
1059
|
+
curr = curr.parentNode || curr.host || curr._lv_parent;
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1021
1064
|
function evaluateStateExpression(expression, contextNode, event) {
|
|
1022
1065
|
let compiled = stateExpressionCache.get(expression);
|
|
1023
1066
|
if (!compiled) {
|
|
@@ -1052,7 +1095,8 @@
|
|
|
1052
1095
|
let root;
|
|
1053
1096
|
if (type === '$this') root = ctx;
|
|
1054
1097
|
else if (type === '$event') root = ev;
|
|
1055
|
-
else if (type === '$macro') root = ctx
|
|
1098
|
+
else if (type === '$macro') root = findContext(ctx, '_macroContext');
|
|
1099
|
+
else if (type === '$query') root = findContext(ctx, '_queryContext');
|
|
1056
1100
|
else root = findInScope(ctx, name);
|
|
1057
1101
|
|
|
1058
1102
|
if (!root) return (type === 'state') ? `[Unknown: ${name}]` : undefined;
|
|
@@ -1255,6 +1299,9 @@
|
|
|
1255
1299
|
}
|
|
1256
1300
|
|
|
1257
1301
|
if (typeof onode !== 'object') {
|
|
1302
|
+
if (typeof onode === 'string' && onode.startsWith('=') && onode.length > 1) {
|
|
1303
|
+
return cdomToDOM({ "=": onode }, wasString, unsafe, context);
|
|
1304
|
+
}
|
|
1258
1305
|
return document.createTextNode(onode);
|
|
1259
1306
|
}
|
|
1260
1307
|
|
|
@@ -1305,7 +1352,14 @@
|
|
|
1305
1352
|
const el = document.createElement(tag);
|
|
1306
1353
|
if (context) Object.defineProperty(el, '_lv_parent', { value: context, enumerable: false, configurable: true });
|
|
1307
1354
|
|
|
1308
|
-
|
|
1355
|
+
// Check if content is a structural reference object itself
|
|
1356
|
+
const isStructural = (obj) => {
|
|
1357
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj) || obj.nodeType) return false;
|
|
1358
|
+
const keys = Object.keys(obj);
|
|
1359
|
+
return keys.length === 1 && (keys[0] === '=' || keys[0] === '$' || keys[0].startsWith('=') || symbolicOperators.has(keys[0]));
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
if (typeof content === 'object' && content !== null && !Array.isArray(content) && !content.nodeType && !isStructural(content)) {
|
|
1309
1363
|
for (const key in content) {
|
|
1310
1364
|
const val = content[key];
|
|
1311
1365
|
if (key === 'children') {
|
|
@@ -1404,8 +1458,10 @@
|
|
|
1404
1458
|
return;
|
|
1405
1459
|
}
|
|
1406
1460
|
|
|
1407
|
-
// Check if it's a CSS selector
|
|
1408
|
-
if
|
|
1461
|
+
// Check if it's a CSS selector
|
|
1462
|
+
// We avoid querySelector if it looks like a file path or URL to prevent DOMException
|
|
1463
|
+
const isUrlLike = srcValue.includes('/') || srcValue.includes('?');
|
|
1464
|
+
if (!isUrlLike && !srcValue.startsWith('http')) {
|
|
1409
1465
|
try {
|
|
1410
1466
|
const source = document.querySelector(srcValue);
|
|
1411
1467
|
if (source) {
|
|
@@ -1434,13 +1490,17 @@
|
|
|
1434
1490
|
|
|
1435
1491
|
const contentType = response.headers.get('content-type') || '';
|
|
1436
1492
|
const text = await response.text();
|
|
1493
|
+
const pathname = url.pathname.toLowerCase();
|
|
1437
1494
|
|
|
1438
1495
|
// Determine how to handle content
|
|
1439
|
-
const isCDOM =
|
|
1440
|
-
const isHTML =
|
|
1496
|
+
const isCDOM = pathname.endsWith('.cdom') || contentType.includes('application/cdom') || contentType.includes('application/json');
|
|
1497
|
+
const isHTML = pathname.endsWith('.html') || pathname.endsWith('.htm') || contentType.includes('text/html');
|
|
1441
1498
|
|
|
1442
1499
|
if (isCDOM) {
|
|
1443
1500
|
const cdomData = JSON.parse(text);
|
|
1501
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
1502
|
+
element._queryContext = query;
|
|
1503
|
+
if (!element._macroContext) element._macroContext = query;
|
|
1444
1504
|
const dom = cdomToDOM(cdomData, true, false, element);
|
|
1445
1505
|
element.replaceChildren(dom);
|
|
1446
1506
|
} else if (isHTML) {
|