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 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
- "$.basePrice",
320
- { "+": [1, "$.taxRate"] },
321
- { "-": [1, "$.discount"] }
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 `$.propertyName` to reference inputs
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. Object-Based Helper Arguments
361
+ ### 10. Hypermedia Query Parameters (`$query`)
362
362
 
363
- Helpers can now accept either **positional arguments** (array) or **named arguments** (object):
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
@@ -143,12 +143,12 @@
143
143
  {
144
144
  "div": {
145
145
  "class": "grid", "children": [
146
- Metric('Process Load', { "=": "=/cpuUsage" }, '%', '⚡'),
146
+ Metric('Process Load', "=/cpuUsage", '%', '⚡'),
147
147
  // Using standard helpers 'round'
148
- Metric('Memory Heap', { "=round": [{ "=": "=/memUsage" }, 1] }, '%', '💾'),
148
+ Metric('Memory Heap', { "=round": ["=/memUsage", 1] }, '%', '💾'),
149
149
  // Using standard helpers 'fixed' and 'divide'
150
- Metric('Net Throughput', { "=fixed": [{ "=divide": [{ "=": "=/reqCount" }, 10] }, 1] }, 'k/s', '🚀'),
151
- Metric('Network Jitter', { "=": "=/latency" }, 'ms', '📡')
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&amp;role=Lead+Designer&amp;region=North+America&amp;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>
@@ -88,8 +88,8 @@
88
88
  "div": {
89
89
  "class": "test-section",
90
90
  "children": [
91
- { "h2": { "children": ["Test 1: Define Macro"] } },
92
- { "p": { "children": ["Defining a macro called 'adjusted_price' with schema validation..."] } },
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
- "$.basePrice",
112
- { "+": [1, "$.taxRate"] },
113
- { "-": [1, "$.discount"] }
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
- "$.basePrice",
136
- { "+": [1, "$.taxRate"] },
137
- { "-": [1, "$.discount"] }
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": { "children": ["Test 2: Call Macro with Literal Values"] } },
156
- { "p": { "children": ["Calling macro with basePrice=100, taxRate=0.08, discount=0.10"] } },
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": { "children": ["Expected: $97.20 (100 × 1.08 × 0.90)"] } }
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": { "children": ["Test 3: Call Macro with State References"] } },
195
- { "p": { "children": ["Calling macro with values from state..."] } },
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": { "children": ["Test 4: Macro Without Schema"] } },
233
- { "p": { "children": ["Defining a simple macro without schema validation..."] } },
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": { "+": ["$.a", "$.b"] }
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": { "+": ["$.a", "$.b"] }
250
+ "body": { "+": ["=$.a", "=$.b"] }
251
251
  }
252
252
  },
253
253
  { "p": { "class": "success", "children": ["✓ simple_sum macro registered (no schema)"] } },
254
- { "p": { "children": ["Calling: simple_sum(a=10, b=25)"] } },
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": { "children": ["Test 5: Macro With String Operations"] } },
291
- { "p": { "children": ["A macro that formats a greeting..."] } },
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, ", "$.name", "!"] }
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, ", "$.name", "!"] }
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>
@@ -1,9 +1,15 @@
1
+ <!DOCTYPE html>
1
2
  <html>
2
- <script src="../index.js"></script>
3
3
 
4
- <body>
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
- "App Name: ", { "=": "=/appName" }, { br: {} },
31
- "Local Counter (Scoped): ", { "=": "=/localCounter/count" },
36
+ { h1: "cDOM Scratchpad" },
37
+ "App Name: ", "=/appName", { br: {} },
38
+ "Local Counter (Scoped): ", "=/localCounter/count",
32
39
  {
33
- button: {
34
- onclick: [
35
- { "=log": ["Starting array execution..."] },
36
- { "++": ["=/localCounter/count"] },
37
- { "=log": ["Counter is now:", "=/localCounter/count"] },
38
- { "set": ["=/appName", "cDOM Rocked!"] }
39
- ],
40
- children: ["Array Handler Demo"]
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
- "This div can also see the local counter: ", { "=": "=/localCounter/count" },
48
- { br: {} },
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)" }, { br: {} },
62
- "XPath (Button Label): ", { "$": "//button/text()" }, { br: {} },
63
- "Dynamic Helper { =sum: [1, 2, 3] }: ", { "=sum": [1, 2, 3] }, { br: {} },
64
- "Dynamic Helper{ =currency: [=/localCounter/count] }: ", { "=currency": ["=/localCounter/count"] }, { br: {} },
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+|"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|=\/|\.{1,2}\/|\/|\$this|\$event|[a-zA-Z_$][\w$]*|==|!=|<=|>=|&&|\|\||[-+*/%^<>!?:.,(){}[\]])\s*/g;
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 $this and $event
276
- if (t === '$this' || t === '$event') {
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
- if (path === '$this') return (ctx) => ctx;
296
- if (path === '$event') return (ctx, ev) => ev;
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 $.propertyName (macro argument reference)
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 '$.' (macro argument reference)
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 === '$this' || v.startsWith('$this/') || v.startsWith('$this.') ||
937
- v === '$event' || v.startsWith('$event/') || v.startsWith('$event.')
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 === '$this' || arg.startsWith('$this/') || arg.startsWith('$this.') ||
958
- arg === '$event' || arg.startsWith('$event/') || arg.startsWith('$event.')
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?._macroContext;
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
- if (typeof content === 'object' && content !== null && !Array.isArray(content) && !content.nodeType) {
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 (doesn't start with http/https or /)
1408
- if (!srcValue.startsWith('http') && !srcValue.startsWith('/') && !srcValue.startsWith('./') && !srcValue.startsWith('../')) {
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 = srcValue.endsWith('.cdom') || contentType.includes('application/cdom');
1440
- const isHTML = contentType.includes('text/html') || srcValue.endsWith('.html') || srcValue.endsWith('.htm');
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdom",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Safe, reactive UIs based on JSON Pointer, XPath, JSON Schema",
5
5
  "keywords": [
6
6
  "JPRX",