jprx 1.3.0 → 1.4.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/README.md +15 -13
- package/index.js +2 -1
- package/package.json +1 -1
- package/parser.js +89 -3
package/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/index.js
CHANGED
package/package.json
CHANGED
package/parser.js
CHANGED
|
@@ -1409,6 +1409,7 @@ export const parseExpression = (expr, context) => {
|
|
|
1409
1409
|
* Supports unquoted keys/values and strictly avoids 'eval'.
|
|
1410
1410
|
*/
|
|
1411
1411
|
export const parseCDOMC = (input) => {
|
|
1412
|
+
if (typeof input !== 'string') return input;
|
|
1412
1413
|
let i = 0;
|
|
1413
1414
|
const len = input.length;
|
|
1414
1415
|
|
|
@@ -1666,6 +1667,7 @@ export const parseCDOMC = (input) => {
|
|
|
1666
1667
|
* @returns {object} - Parsed JSON object
|
|
1667
1668
|
*/
|
|
1668
1669
|
export const parseJPRX = (input) => {
|
|
1670
|
+
if (typeof input !== 'string') return input;
|
|
1669
1671
|
let result = '';
|
|
1670
1672
|
let i = 0;
|
|
1671
1673
|
const len = input.length;
|
|
@@ -1728,7 +1730,44 @@ export const parseJPRX = (input) => {
|
|
|
1728
1730
|
continue;
|
|
1729
1731
|
}
|
|
1730
1732
|
|
|
1731
|
-
// 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)
|
|
1732
1771
|
if (char === '=') {
|
|
1733
1772
|
let expr = '';
|
|
1734
1773
|
let parenDepth = 0;
|
|
@@ -1790,10 +1829,57 @@ export const parseJPRX = (input) => {
|
|
|
1790
1829
|
continue;
|
|
1791
1830
|
}
|
|
1792
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
|
+
|
|
1793
1879
|
// Handle unquoted property names, identifiers, paths, and FUNCTION CALLS
|
|
1794
|
-
if (/[a-zA-Z_
|
|
1880
|
+
if (/[a-zA-Z_$\/.\/]/.test(char)) {
|
|
1795
1881
|
let word = '';
|
|
1796
|
-
while (i < len && /[a-zA-Z0-9_
|
|
1882
|
+
while (i < len && /[a-zA-Z0-9_$\/.-]/.test(input[i])) {
|
|
1797
1883
|
word += input[i];
|
|
1798
1884
|
i++;
|
|
1799
1885
|
}
|