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 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>
@@ -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|=\$|\$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
- if (path === '$this') return (ctx) => ctx;
296
- if (path === '$event') return (ctx, ev) => ev;
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 (macro argument reference)
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 '$.' (macro argument reference)
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?._macroContext;
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
- if (typeof content === 'object' && content !== null && !Array.isArray(content) && !content.nodeType) {
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 (doesn't start with http/https or /)
1408
- if (!srcValue.startsWith('http') && !srcValue.startsWith('/') && !srcValue.startsWith('./') && !srcValue.startsWith('../')) {
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 = srcValue.endsWith('.cdom') || contentType.includes('application/cdom');
1440
- const isHTML = contentType.includes('text/html') || srcValue.endsWith('.html') || srcValue.endsWith('.htm');
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdom",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Safe, reactive UIs based on JSON Pointer, XPath, JSON Schema",
5
5
  "keywords": [
6
6
  "JPRX",