lightview 2.4.7 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-GUIDANCE.md +7 -5
- package/README.md +4 -4
- package/docs/cdom-xpath.html +27 -9
- package/docs/cdom.html +67 -45
- package/docs/dom-benchmark.html +7 -35
- package/jprx/README.md +15 -13
- package/jprx/package.json +1 -1
- package/jprx/parser.js +85 -1
- package/lightview-all.js +131 -34
- package/lightview-cdom.js +131 -34
- package/package.json +2 -2
- package/scratch.html +83 -0
- package/src/lightview-cdom.js +86 -44
- package/build_tmp/lightview-cdom.js +0 -3934
- package/build_tmp/lightview-router.js +0 -185
- package/build_tmp/lightview-x.js +0 -1739
- package/build_tmp/lightview.js +0 -740
package/AI-GUIDANCE.md
CHANGED
|
@@ -151,9 +151,11 @@ Lightview X allows embedded `${}` expressions in attributes for simple reactivit
|
|
|
151
151
|
|
|
152
152
|
**cDOM (Computed DOM)** and **JPRX (JSON Reactive Expressions)** allow you to build fully reactive UIs using **only JSON**.
|
|
153
153
|
|
|
154
|
-
### Syntax
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
### Expression Syntax
|
|
155
|
+
* **`=(expr)` (JPRX)**: Wraps reactive expressions that navigate **reactive state** (Signals/States). Use `=(/path)` for paths, `=(function(...))` for helpers.
|
|
156
|
+
* **`#(xpath)` (cDOM XPath)**: Wraps XPath expressions that navigate the **DOM tree** during construction. Use `#(../../@id)` to navigate ancestors and access attributes.
|
|
157
|
+
|
|
158
|
+
**Note**: For initialization functions like `state()` and `signal()`, use `=function(...)` without the outer wrapper, as they execute once on mount rather than being reactive expressions.
|
|
157
159
|
|
|
158
160
|
### Name Resolution & Scoping
|
|
159
161
|
JPRX uses an **up-tree search** to find signals or states:
|
|
@@ -178,7 +180,7 @@ Use `{ scope: $this }` in `=state` or `=signal` to register data specifically at
|
|
|
178
180
|
| **Stats** | `sum`, `avg`, `min`, `max`, `median`, `stdev`, `variance` |
|
|
179
181
|
| **Data** | `lookup(val, searchArr, resultArr)` (VLOOKUP-style) |
|
|
180
182
|
| **State** | `state(val, opts)`, `set(path, val)`, `bind(path)`, `increment`, `decrement`, `toggle` |
|
|
181
|
-
| **DOM** | `#path` (XPath Navigation), `move(target, loc)` |
|
|
183
|
+
| **DOM** | `#(path)` (XPath Navigation), `move(target, loc)` |
|
|
182
184
|
| **Net** | `fetchHelper(url, opts)`, `mount(url, opts)` |
|
|
183
185
|
|
|
184
186
|
### The "Decentralized Layout" Pattern (AI Strategy)
|
|
@@ -192,7 +194,7 @@ Use `{ scope: $this }` in `=state` or `=signal` to register data specifically at
|
|
|
192
194
|
div: {
|
|
193
195
|
id: "widget-1",
|
|
194
196
|
onmount: ["=state({val:0}, {name:'w1', scope:$this})", "=move('#sidebar')"],
|
|
195
|
-
children: [ { p: "Val:
|
|
197
|
+
children: [ { p: ["Val: ", =(/w1/val)] } ]
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
```
|
package/README.md
CHANGED
|
@@ -43,17 +43,17 @@ Traditional UI development requires AI agents to generate imperative JavaScript
|
|
|
43
43
|
|
|
44
44
|
```json
|
|
45
45
|
{
|
|
46
|
-
"state": { "count": 0 },
|
|
47
46
|
"div": {
|
|
47
|
+
"onmount": "=state({ count: 0 }, 'counter')",
|
|
48
48
|
"children": [
|
|
49
|
-
{ "p": "
|
|
50
|
-
{ "button": { "onclick": "
|
|
49
|
+
{ "p": ["Count: ", "=(/counter/count)"] },
|
|
50
|
+
{ "button": { "onclick": "=(++/counter/count)", "children": ["Increment"] } }
|
|
51
51
|
]
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
This JSON is **the entire application**. The `
|
|
56
|
+
This JSON is **the entire application**. The `=()` expressions are JPRX—a sandboxed, spreadsheet-like formula language (inspired by **XPath** and **JSON Pointers**) that resolves paths, calls registered helpers, and triggers reactivity automatically.
|
|
57
57
|
|
|
58
58
|
### Learn More
|
|
59
59
|
|
package/docs/cdom-xpath.html
CHANGED
|
@@ -69,9 +69,9 @@ const cdom = `{
|
|
|
69
69
|
{ h3: "User Profile" },
|
|
70
70
|
{ button: {
|
|
71
71
|
id: "7",
|
|
72
|
-
// XPath
|
|
73
|
-
// XPath
|
|
74
|
-
children: ["Button ",
|
|
72
|
+
// XPath #(@id) gets "7" from this button's id
|
|
73
|
+
// XPath #(../@id) gets "profile-container" from the parent div
|
|
74
|
+
children: ["Button ", #(@id), " in section ", #(../@id)]
|
|
75
75
|
}}
|
|
76
76
|
]
|
|
77
77
|
}
|
|
@@ -116,17 +116,35 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
116
116
|
<strong>Forbidden:</strong> <code>child::</code>, <code>descendant::</code>, and <code>following::</code>
|
|
117
117
|
axes are disabled as they could create infinite loops or reference nodes that do not exist yet.
|
|
118
118
|
</p>
|
|
119
|
+
<div
|
|
120
|
+
style="margin-top: 1rem; padding: 1rem; background: var(--site-bg-secondary); border-left: 4px solid var(--site-primary); border-radius: var(--site-radius);">
|
|
121
|
+
<p style="margin: 0; font-size: 0.95rem;">
|
|
122
|
+
<strong>💡 Need Forward-Looking XPath?</strong> For advanced use cases requiring <code>child::</code>,
|
|
123
|
+
<code>descendant::</code>, or <code>following::</code> axes, use the reactive <code>=xpath()</code>
|
|
124
|
+
helper
|
|
125
|
+
in JPRX instead of static <code>#()</code> in cDOM. The reactive helper evaluates XPath expressions
|
|
126
|
+
after the DOM is fully constructed and can access any part of the tree. See the
|
|
127
|
+
<a href="#reactive-xpath">Reactive XPath section</a> below for details.
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
119
130
|
|
|
120
131
|
<h2 id="reactive-xpath">Reactive XPath (<code>=xpath()</code>)</h2>
|
|
121
132
|
<p>
|
|
122
|
-
If you need an XPath expression to update reactively
|
|
123
|
-
use
|
|
133
|
+
If you need an XPath expression to update reactively when attributes elsewhere in the DOM change,
|
|
134
|
+
or if you need to use <strong>forward-looking axes</strong> (like <code>child::</code>,
|
|
135
|
+
<code>descendant::</code>,
|
|
136
|
+
or <code>following::</code>), use the <code>=xpath()</code> helper within a JPRX expression.
|
|
137
|
+
</p>
|
|
138
|
+
<p>
|
|
139
|
+
Unlike static <code>#()</code> expressions which are evaluated once during construction,
|
|
140
|
+
<code>=xpath()</code> creates a reactive computed signal that re-evaluates whenever its dependencies change,
|
|
141
|
+
and it has <strong>no axis restrictions</strong> since the DOM is already fully constructed.
|
|
124
142
|
</p>
|
|
125
143
|
<div class="code-block">
|
|
126
144
|
<pre><code>{
|
|
127
145
|
div: {
|
|
128
|
-
title: "=xpath('../@data-section')",
|
|
129
|
-
class: "=concat('item ', xpath('../@theme'))"
|
|
146
|
+
title: "=(xpath('../@data-section'))",
|
|
147
|
+
class: "=(concat('item ', xpath('../@theme')))"
|
|
130
148
|
}
|
|
131
149
|
}</code></pre>
|
|
132
150
|
</div>
|
|
@@ -142,12 +160,12 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
142
160
|
{
|
|
143
161
|
button: {
|
|
144
162
|
id: "7",
|
|
145
|
-
children: [
|
|
163
|
+
children: [#(../@id)]
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
// Support for complex paths with predicates
|
|
150
|
-
{ span: [#ancestor::div[@data-role='container']/@title] }</code></pre>
|
|
168
|
+
{ span: [#(ancestor::div[@data-role='container']/@title)] }</code></pre>
|
|
151
169
|
</div>
|
|
152
170
|
|
|
153
171
|
<h2 id="safety">Security & Safety</h2>
|
package/docs/cdom.html
CHANGED
|
@@ -89,6 +89,20 @@
|
|
|
89
89
|
helper functions, and integration patterns are subject to change as we continue to evolve the
|
|
90
90
|
library. Use with caution in production environments.
|
|
91
91
|
</p>
|
|
92
|
+
<p
|
|
93
|
+
style="margin: 0.75rem 0 0 0; padding-top: 0.75rem; border-top: 1px solid var(--site-border); font-size: 0.95rem; opacity: 0.9;">
|
|
94
|
+
<strong>v2.5.0 Syntax Update:</strong> Starting with Lightview v2.5.0 and JPRX v1.4.0,
|
|
95
|
+
expressions use a new wrapper syntax:
|
|
96
|
+
<code
|
|
97
|
+
style="background: var(--site-bg-secondary); padding: 0.125rem 0.375rem; border-radius: 3px;">=(expr)</code>
|
|
98
|
+
for JPRX and
|
|
99
|
+
<code
|
|
100
|
+
style="background: var(--site-bg-secondary); padding: 0.125rem 0.375rem; border-radius: 3px;">#(xpath)</code>
|
|
101
|
+
for XPath.
|
|
102
|
+
The legacy prefix-only syntax (e.g., <code
|
|
103
|
+
style="background: var(--site-bg-secondary); padding: 0.125rem 0.375rem; border-radius: 3px;">=path</code>)
|
|
104
|
+
will be <strong>fully deprecated after March 31, 2026</strong>.
|
|
105
|
+
</p>
|
|
92
106
|
</div>
|
|
93
107
|
</div>
|
|
94
108
|
</div>
|
|
@@ -136,10 +150,10 @@ const cdom = `{
|
|
|
136
150
|
"id": "Counter",
|
|
137
151
|
"onmount": "=state({ count: 0 }, { name: 'local', schema: 'auto', scope: $this })",
|
|
138
152
|
"children": [
|
|
139
|
-
{ "h2": "
|
|
140
|
-
{ "p": ["Count: ", "
|
|
141
|
-
{ "button": { "onclick": "
|
|
142
|
-
{ "button": { "onclick": "
|
|
153
|
+
{ "h2": "#(../../@id)" }, // XPath: text node -> h2 -> div
|
|
154
|
+
{ "p": ["Count: ", "=(/local/count)"] },
|
|
155
|
+
{ "button": { "onclick": "=(++/local/count)", "children": ["+"] } },
|
|
156
|
+
{ "button": { "onclick": "=(--/local/count)", "children": ["-"] } }
|
|
143
157
|
]
|
|
144
158
|
}
|
|
145
159
|
}`;
|
|
@@ -158,10 +172,11 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
158
172
|
<li><code>=/local/count</code> — A path that reactively displays the count value.</li>
|
|
159
173
|
</ul>
|
|
160
174
|
<p style="font-size: 0.95rem; font-style: italic; color: var(--site-text-secondary);">
|
|
161
|
-
<strong>Note:</strong> In cDOM,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
reactive state.
|
|
175
|
+
<strong>Note:</strong> In cDOM, expressions use a wrapper syntax for clarity: <code>#(xpath)</code> for
|
|
176
|
+
<strong>XPath</strong> expressions that
|
|
177
|
+
navigate and extract data from the DOM, and <code>=(expr)</code> for <strong>JPRX</strong>
|
|
178
|
+
expressions that navigate or apply functions to reactive state. Legacy syntax without parentheses is still
|
|
179
|
+
supported.
|
|
165
180
|
</p>
|
|
166
181
|
<p>
|
|
167
182
|
The UI automatically updates whenever <code>count</code> changes — no manual DOM manipulation required. It
|
|
@@ -226,9 +241,9 @@ const cdom = `{
|
|
|
226
241
|
{ h3: "User Profile" },
|
|
227
242
|
{ button: {
|
|
228
243
|
id: "7",
|
|
229
|
-
// XPath
|
|
230
|
-
// XPath
|
|
231
|
-
children: ["Button ",
|
|
244
|
+
// XPath #(../@id) gets the "7" from this button's id
|
|
245
|
+
// XPath #(../../@id) gets "profile-container" from the g-parent div
|
|
246
|
+
children: ["Button ", #(../@id), " in section ", #(../../@id)]
|
|
232
247
|
}}
|
|
233
248
|
]
|
|
234
249
|
}
|
|
@@ -238,7 +253,7 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
238
253
|
</div>
|
|
239
254
|
<p>
|
|
240
255
|
In the example above, the button's text is derived entirely from its own <code>id</code> and its
|
|
241
|
-
parent's <code>id</code> using <code
|
|
256
|
+
parent's <code>id</code> using <code>#(../@id)</code> and <code>#(../../@id)</code>.
|
|
242
257
|
</p>
|
|
243
258
|
<p>
|
|
244
259
|
For more details, see the <a href="/docs/cdom-xpath.html">Full XPath Documentation</a>.
|
|
@@ -252,54 +267,61 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
252
267
|
with reactivity, relative paths, and helper functions.
|
|
253
268
|
</p>
|
|
254
269
|
|
|
255
|
-
<h3 id="JPRX-delimiters">
|
|
270
|
+
<h3 id="JPRX-delimiters">Expression Syntax</h3>
|
|
256
271
|
<p>
|
|
257
|
-
JPRX expressions
|
|
272
|
+
JPRX expressions use a <strong>wrapper syntax</strong> for unambiguous parsing. The recommended syntax is
|
|
273
|
+
<code>=(expression)</code>:
|
|
258
274
|
</p>
|
|
259
275
|
<table class="api-table">
|
|
260
276
|
<thead>
|
|
261
277
|
<tr>
|
|
262
|
-
<th>
|
|
278
|
+
<th>Syntax</th>
|
|
263
279
|
<th>Purpose</th>
|
|
264
280
|
<th>Example</th>
|
|
265
281
|
</tr>
|
|
266
282
|
</thead>
|
|
267
283
|
<tbody>
|
|
268
284
|
<tr>
|
|
269
|
-
<td><code
|
|
285
|
+
<td><code>=(/path)</code></td>
|
|
270
286
|
<td>Access a path in the global registry</td>
|
|
271
|
-
<td><code
|
|
287
|
+
<td><code>=(/users/0/name)</code></td>
|
|
272
288
|
</tr>
|
|
273
289
|
<tr>
|
|
274
|
-
<td><code>=function(</code></td>
|
|
290
|
+
<td><code>=(function(...))</code></td>
|
|
275
291
|
<td>Call a helper function</td>
|
|
276
|
-
<td><code>=sum(/items...price)</code></td>
|
|
292
|
+
<td><code>=(sum(/items...price))</code></td>
|
|
293
|
+
</tr>
|
|
294
|
+
<tr>
|
|
295
|
+
<td><code>#(xpath)</code></td>
|
|
296
|
+
<td>XPath expression for DOM navigation</td>
|
|
297
|
+
<td><code>#(../../@id)</code></td>
|
|
277
298
|
</tr>
|
|
278
299
|
</tbody>
|
|
279
300
|
</table>
|
|
280
301
|
<p>
|
|
281
|
-
|
|
282
|
-
|
|
302
|
+
The wrapper syntax <code>=()</code> and <code>#()</code> makes expressions unambiguous and eliminates
|
|
303
|
+
conflicts with literal strings.
|
|
304
|
+
Legacy syntax without parentheses (e.g., <code>=/path</code>) is still supported for backward compatibility.
|
|
283
305
|
</p>
|
|
284
306
|
|
|
285
|
-
<h3 id="JPRX-
|
|
307
|
+
<h3 id="JPRX-literal-strings">Literal Strings</h3>
|
|
286
308
|
<p>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
with a single quote:
|
|
309
|
+
With the new wrapper syntax, you no longer need escape sequences. Any string that doesn't match the wrapper
|
|
310
|
+
pattern
|
|
311
|
+
is treated as a literal:
|
|
291
312
|
</p>
|
|
292
313
|
<div class="code-block" style="margin-bottom: 1rem;">
|
|
293
|
-
<pre><code>//
|
|
294
|
-
{ "p": "
|
|
314
|
+
<pre><code>// JPRX expression (wrapped):
|
|
315
|
+
{ "p": "=(/user/name)" } // Resolves the path /user/name
|
|
295
316
|
|
|
296
|
-
//
|
|
297
|
-
{ "p": "
|
|
298
|
-
{ "p": "
|
|
317
|
+
// Literal strings (no wrapper):
|
|
318
|
+
{ "p": "=E=mc²" } // Renders as "=E=mc²"
|
|
319
|
+
{ "p": "=42" } // Renders as "=42"
|
|
320
|
+
{ "p": "#ff0000" } // Renders as "#ff0000" (hex color)</code></pre>
|
|
299
321
|
</div>
|
|
300
322
|
<p>
|
|
301
|
-
The
|
|
302
|
-
the
|
|
323
|
+
The wrapper syntax eliminates ambiguity: <code>=(expr)</code> is always an expression, while strings without
|
|
324
|
+
the wrapper pattern are always literals.
|
|
303
325
|
</p>
|
|
304
326
|
|
|
305
327
|
<h3 id="JPRX-anatomy">Anatomy of a Path</h3>
|
|
@@ -723,11 +745,11 @@ const cdomString = `{
|
|
|
723
745
|
children: [
|
|
724
746
|
{ h3: "Shopping Cart" },
|
|
725
747
|
{ ul: {
|
|
726
|
-
children: =map(/store/cart/items, { li: { children: [_/name, " - ", currency(_/price)] } })
|
|
748
|
+
children: =(map(/store/cart/items, { li: { children: [_/name, " - ", currency(_/price)] } }))
|
|
727
749
|
}},
|
|
728
750
|
{ p: {
|
|
729
751
|
style: "font-weight: bold; margin-top: 1rem;",
|
|
730
|
-
children: ["Total: ", =currency(sum(/store/cart/items...price))]
|
|
752
|
+
children: ["Total: ", =(currency(sum(/store/cart/items...price)))]
|
|
731
753
|
}}
|
|
732
754
|
]
|
|
733
755
|
}
|
|
@@ -772,10 +794,10 @@ const cdomString = `{
|
|
|
772
794
|
onmount: "=signal(0, 'count')",
|
|
773
795
|
"children": [
|
|
774
796
|
{ "h3": ["Standard JPRX Counter"] },
|
|
775
|
-
{ "p": { "children": ["Count: ", "
|
|
797
|
+
{ "p": { "children": ["Count: ", "=(/count)"] }},
|
|
776
798
|
{ "div": { "children": [
|
|
777
|
-
{ "button": { "onclick": "=decrement(/count)", "children": ["-"] } },
|
|
778
|
-
{ "button": { "onclick": "=increment(/count)", "children": ["+"] } }
|
|
799
|
+
{ "button": { "onclick": "=(decrement(/count))", "children": ["-"] } },
|
|
800
|
+
{ "button": { "onclick": "=(increment(/count))", "children": ["+"] } }
|
|
779
801
|
]}}
|
|
780
802
|
]
|
|
781
803
|
}
|
|
@@ -806,10 +828,10 @@ const cdomString = `{
|
|
|
806
828
|
onmount: =signal(0, 'count'),
|
|
807
829
|
children: [
|
|
808
830
|
{ h3: ["Compressed Counter"] },
|
|
809
|
-
{ p: { children: ["Count: ",
|
|
831
|
+
{ p: { children: ["Count: ", =(/count)] }},
|
|
810
832
|
{ div: { children: [
|
|
811
|
-
{ button: { onclick: =decrement(/count), children: ["-"] } },
|
|
812
|
-
{ button: { onclick: =increment(/count), children: ["+"] } }
|
|
833
|
+
{ button: { onclick: =(decrement(/count)), children: ["-"] } },
|
|
834
|
+
{ button: { onclick: =(increment(/count)), children: ["+"] } }
|
|
813
835
|
]}}
|
|
814
836
|
]
|
|
815
837
|
}
|
|
@@ -840,11 +862,11 @@ const cdomString = `{
|
|
|
840
862
|
onmount: =signal(0, 'count'),
|
|
841
863
|
children: [
|
|
842
864
|
{ h3: ["Operator Counter"] },
|
|
843
|
-
{ p: { children: ["Count: ",
|
|
865
|
+
{ p: { children: ["Count: ", =(/count)] }},
|
|
844
866
|
{ div: { children: [
|
|
845
|
-
// Prefix operators:
|
|
846
|
-
{ button: { onclick:
|
|
847
|
-
{ button: { onclick:
|
|
867
|
+
// Prefix operators: =(--/count) and =(++/count)
|
|
868
|
+
{ button: { onclick: =(--/count), children: ["-"] } },
|
|
869
|
+
{ button: { onclick: =(++/count), children: ["+"] } }
|
|
848
870
|
]}}
|
|
849
871
|
]
|
|
850
872
|
}
|
package/docs/dom-benchmark.html
CHANGED
|
@@ -533,42 +533,14 @@
|
|
|
533
533
|
// Create a temporary container for Juris to render into
|
|
534
534
|
const tempContainer = document.createElement('div');
|
|
535
535
|
|
|
536
|
-
//
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
error: console.error,
|
|
542
|
-
info: console.info,
|
|
543
|
-
debug: console.debug
|
|
544
|
-
};
|
|
545
|
-
const noop = () => { };
|
|
546
|
-
console.log = noop;
|
|
547
|
-
console.dir = noop;
|
|
548
|
-
console.warn = noop;
|
|
549
|
-
console.error = noop;
|
|
550
|
-
console.info = noop;
|
|
551
|
-
console.debug = noop;
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
// Create a Juris instance with the oDOM structure as the layout
|
|
555
|
-
const jurisInstance = new Juris({
|
|
556
|
-
debug: false, // Disable logging for performance
|
|
557
|
-
layout: onode
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
// Render to the temporary container
|
|
561
|
-
jurisInstance.render(tempContainer);
|
|
562
|
-
} finally {
|
|
563
|
-
// Restore original console methods
|
|
564
|
-
console.log = originalConsole.log;
|
|
565
|
-
console.dir = originalConsole.dir;
|
|
566
|
-
console.warn = originalConsole.warn;
|
|
567
|
-
console.error = originalConsole.error;
|
|
568
|
-
console.info = originalConsole.info;
|
|
569
|
-
console.debug = originalConsole.debug;
|
|
570
|
-
}
|
|
536
|
+
// Create a Juris instance with the oDOM structure as the layout
|
|
537
|
+
const jurisInstance = new Juris({
|
|
538
|
+
debug: false, // Disable logging for performance
|
|
539
|
+
layout: onode
|
|
540
|
+
});
|
|
571
541
|
|
|
542
|
+
// Render to the temporary container
|
|
543
|
+
jurisInstance.render(tempContainer);
|
|
572
544
|
// Return the first child (the actual rendered content)
|
|
573
545
|
return tempContainer.firstChild || tempContainer;
|
|
574
546
|
}
|
package/jprx/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**JPRX** is a declarative, reactive expression syntax designed for JSON-based data structures. It extends [JSON Pointer (RFC 6901)](https://www.rfc-editor.org/rfc/rfc6901) with reactivity, relative paths, operator syntax, and a rich library of helper functions.
|
|
4
4
|
|
|
5
|
+
> **v1.4.0 Syntax Update**: JPRX now uses a wrapper syntax `=(expr)` for unambiguous expression parsing. Legacy prefix-only syntax (e.g., `=/path`) is still supported but will be deprecated after March 31, 2026.
|
|
6
|
+
|
|
5
7
|
## Overview
|
|
6
8
|
|
|
7
9
|
JPRX is a **syntax** and an **expression engine**. While this repository provides the parser and core helper functions, JPRX is intended to be integrated into UI libraries or state management systems that can "hydrate" these expressions into active reactive bindings.
|
|
@@ -26,17 +28,17 @@ JPRX extends the base JSON Pointer syntax with:
|
|
|
26
28
|
|
|
27
29
|
| Feature | Syntax | Description |
|
|
28
30
|
|---------|--------|-------------|
|
|
29
|
-
| **Global Path** |
|
|
30
|
-
| **Relative Path** |
|
|
31
|
-
| **Parent Path** |
|
|
32
|
-
| **Functions** | `=sum(/items...price)` | Call registered core helpers. |
|
|
33
|
-
| **Explosion** |
|
|
34
|
-
| **Operators** |
|
|
31
|
+
| **Global Path** | `=(/user/name)` | Access global state via an absolute path. |
|
|
32
|
+
| **Relative Path** | `=(./count)` | Access properties relative to the current context. |
|
|
33
|
+
| **Parent Path** | `=(../id)` | Traverse up the state hierarchy (UP-tree search). |
|
|
34
|
+
| **Functions** | `=(sum(/items...price))` | Call registered core helpers. |
|
|
35
|
+
| **Explosion** | `=(/items...name)` | Extract a property from every object in an array (spread). |
|
|
36
|
+
| **Operators** | `=(++/count)`, `=(/a + /b)` | Familiar JS-style prefix, postfix, and infix operators. |
|
|
35
37
|
| **Placeholders** | `_` (item), `$this`, `$event` | Context-aware placeholders for iteration and interaction. |
|
|
36
|
-
| **Two-Way Binding**| `=bind(/user/name)`| Create a managed, two-way reactive link for inputs. |
|
|
37
|
-
| **DOM Patches** | `=move(target, loc)`| Decentralized layout: Move/replace host element into a target. |
|
|
38
|
+
| **Two-Way Binding**| `=(bind(/user/name))`| Create a managed, two-way reactive link for inputs. |
|
|
39
|
+
| **DOM Patches** | `=(move(target, loc))`| Decentralized layout: Move/replace host element into a target. |
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
**Note**: For initialization functions like `state()` and `signal()`, use `=function(...)` without the outer wrapper, as they execute once on mount rather than being reactive expressions.
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
## State Management
|
|
@@ -104,7 +106,7 @@ To ensure unambiguous data flow, `=bind` only accepts direct paths. It cannot be
|
|
|
104
106
|
|
|
105
107
|
### Handling Transformations
|
|
106
108
|
If you need to transform data during a two-way binding, there are two primary approaches:
|
|
107
|
-
1. **Event-Based**: Use a manual `oninput` handler to apply the transformation, e.g., `=set(/name, upper($event/target/value))`.
|
|
109
|
+
1. **Event-Based**: Use a manual `oninput` handler to apply the transformation, e.g., `=(set(/name, upper($event/target/value)))`.
|
|
108
110
|
2. **Schema-Based**: Define a `transform` or `pattern` in the schema for the path. The `=bind` helper will respect the schema rules during the write-back phase.
|
|
109
111
|
|
|
110
112
|
---
|
|
@@ -159,9 +161,9 @@ A modern, lifecycle-based reactive counter:
|
|
|
159
161
|
"onmount": "=state({ count: 0 }, { name: 'counter', schema: 'auto', scope: $this })",
|
|
160
162
|
"children": [
|
|
161
163
|
{ "h2": "Modern JPRX Counter" },
|
|
162
|
-
{ "p": ["Current Count: ", "
|
|
163
|
-
{ "button": { "onclick": "
|
|
164
|
-
{ "button": { "onclick": "
|
|
164
|
+
{ "p": ["Current Count: ", "=(/counter/count)"] },
|
|
165
|
+
{ "button": { "onclick": "=(++/counter/count)", "children": ["+"] } },
|
|
166
|
+
{ "button": { "onclick": "=(--/counter/count)", "children": ["-"] } }
|
|
165
167
|
]
|
|
166
168
|
}
|
|
167
169
|
}
|
package/jprx/package.json
CHANGED
package/jprx/parser.js
CHANGED
|
@@ -1730,7 +1730,44 @@ export const parseJPRX = (input) => {
|
|
|
1730
1730
|
continue;
|
|
1731
1731
|
}
|
|
1732
1732
|
|
|
1733
|
-
// Handle JPRX expressions starting with =
|
|
1733
|
+
// Handle JPRX expressions starting with = or #
|
|
1734
|
+
// New wrapper syntax: =(expr) or #(xpath)
|
|
1735
|
+
if ((char === '=' || char === '#') && input[i + 1] === '(') {
|
|
1736
|
+
const prefix = char;
|
|
1737
|
+
let expr = prefix;
|
|
1738
|
+
i++; // skip = or #
|
|
1739
|
+
let parenDepth = 0;
|
|
1740
|
+
let inExprQuote = null;
|
|
1741
|
+
|
|
1742
|
+
while (i < len) {
|
|
1743
|
+
const c = input[i];
|
|
1744
|
+
|
|
1745
|
+
if (inExprQuote) {
|
|
1746
|
+
if (c === inExprQuote && input[i - 1] !== '\\') inExprQuote = null;
|
|
1747
|
+
} else if (c === '"' || c === "'") {
|
|
1748
|
+
inExprQuote = c;
|
|
1749
|
+
} else {
|
|
1750
|
+
if (c === '(') parenDepth++;
|
|
1751
|
+
else if (c === ')') {
|
|
1752
|
+
parenDepth--;
|
|
1753
|
+
if (parenDepth === 0) {
|
|
1754
|
+
expr += c;
|
|
1755
|
+
i++;
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
expr += c;
|
|
1762
|
+
i++;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Use JSON.stringify to safely quote and escape the expression
|
|
1766
|
+
result += JSON.stringify(expr);
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Handle legacy JPRX expressions starting with = (without parentheses)
|
|
1734
1771
|
if (char === '=') {
|
|
1735
1772
|
let expr = '';
|
|
1736
1773
|
let parenDepth = 0;
|
|
@@ -1792,6 +1829,53 @@ export const parseJPRX = (input) => {
|
|
|
1792
1829
|
continue;
|
|
1793
1830
|
}
|
|
1794
1831
|
|
|
1832
|
+
// Handle XPath expressions starting with # (legacy syntax)
|
|
1833
|
+
if (char === '#') {
|
|
1834
|
+
let expr = '';
|
|
1835
|
+
let parenDepth = 0;
|
|
1836
|
+
let bracketDepth = 0;
|
|
1837
|
+
let inExprQuote = null;
|
|
1838
|
+
|
|
1839
|
+
while (i < len) {
|
|
1840
|
+
const c = input[i];
|
|
1841
|
+
|
|
1842
|
+
if (inExprQuote) {
|
|
1843
|
+
if (c === inExprQuote && input[i - 1] !== '\\') inExprQuote = null;
|
|
1844
|
+
} else if (c === '"' || c === "'") {
|
|
1845
|
+
inExprQuote = c;
|
|
1846
|
+
} else {
|
|
1847
|
+
// Check for break BEFORE updating depth
|
|
1848
|
+
if (parenDepth === 0 && bracketDepth === 0) {
|
|
1849
|
+
// Break on structural characters at depth 0
|
|
1850
|
+
if (/[}[\],:]/.test(c) && expr.length > 1) break;
|
|
1851
|
+
// For whitespace, peek ahead
|
|
1852
|
+
if (/\s/.test(c)) {
|
|
1853
|
+
let j = i + 1;
|
|
1854
|
+
while (j < len && /\s/.test(input[j])) j++;
|
|
1855
|
+
if (j < len) {
|
|
1856
|
+
const nextChar = input[j];
|
|
1857
|
+
if (nextChar === '}' || nextChar === ',' || nextChar === ']') {
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (c === '(') parenDepth++;
|
|
1865
|
+
else if (c === ')') parenDepth--;
|
|
1866
|
+
else if (c === '[') bracketDepth++;
|
|
1867
|
+
else if (c === ']') bracketDepth--;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
expr += c;
|
|
1871
|
+
i++;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// Use JSON.stringify to safely quote and escape the expression
|
|
1875
|
+
result += JSON.stringify(expr);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1795
1879
|
// Handle unquoted property names, identifiers, paths, and FUNCTION CALLS
|
|
1796
1880
|
if (/[a-zA-Z_$\/.\/]/.test(char)) {
|
|
1797
1881
|
let word = '';
|