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.
Files changed (4) hide show
  1. package/README.md +15 -13
  2. package/index.js +2 -1
  3. package/package.json +1 -1
  4. 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** | `=/user/name` | Access global state via an absolute path. |
30
- | **Relative Path** | `./count` | Access properties relative to the current context. |
31
- | **Parent Path** | `../id` | Traverse up the state hierarchy (UP-tree search). |
32
- | **Functions** | `=sum(/items...price)` | Call registered core helpers. |
33
- | **Explosion** | `/items...name` | Extract a property from every object in an array (spread). |
34
- | **Operators** | `=++/count`, `=/a + =/b` | Familiar JS-style prefix, postfix, and infix 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
- Once inside a JPRX expression, the `=` prefix is only needed at the start of the expression for paths or function names.
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: ", "=/counter/count"] },
163
- { "button": { "onclick": "=++/counter/count", "children": ["+"] } },
164
- { "button": { "onclick": "=--/counter/count", "children": ["-"] } }
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
@@ -19,7 +19,8 @@ export {
19
19
  resolvePathAsContext,
20
20
  resolveExpression,
21
21
  parseCDOMC,
22
- parseJPRX,
22
+ parseCDOMC as parseJPRX,
23
+ parseJPRX as oldParseJPRX,
23
24
  unwrapSignal,
24
25
  getRegistry,
25
26
  BindingTarget
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jprx",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "JSON Path Reactive eXpressions - A reactive expression language for JSON data",
5
5
  "main": "index.js",
6
6
  "type": "module",
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 = (MUST come before word handler!)
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_$/./]/.test(char)) {
1880
+ if (/[a-zA-Z_$\/.\/]/.test(char)) {
1795
1881
  let word = '';
1796
- while (i < len && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
1882
+ while (i < len && /[a-zA-Z0-9_$\/.-]/.test(input[i])) {
1797
1883
  word += input[i];
1798
1884
  i++;
1799
1885
  }