jprx 1.1.1 → 1.2.1

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
@@ -1,4 +1,4 @@
1
- # JPRX (JSON Reactive Path eXpressions)
1
+ # JPRX (JSON Path Reactive eXpressions)
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
 
@@ -26,16 +26,18 @@ JPRX extends the base JSON Pointer syntax with:
26
26
 
27
27
  | Feature | Syntax | Description |
28
28
  |---------|--------|-------------|
29
- | **Global Path** | `$/user/name` | Access global state via an absolute path. |
29
+ | **Global Path** | `=/user/name` | Access global state via an absolute path. |
30
30
  | **Relative Path** | `./count` | Access properties relative to the current context. |
31
31
  | **Parent Path** | `../id` | Traverse up the state hierarchy (UP-tree search). |
32
- | **Functions** | `$sum(/items...price)` | Call registered core helpers. |
32
+ | **Functions** | `=sum(/items...price)` | Call registered core helpers. |
33
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. |
34
+ | **Operators** | `=++/count`, `=/a + =/b` | Familiar JS-style prefix, postfix, and infix operators. |
35
35
  | **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. |
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
+
39
+ Once inside a JPRX expression, the `=` prefix is only needed at the start of the expression for paths or function names.
37
40
 
38
- Once inside a JPRX expression, the `$` prefix is only needed at the start of the expression for paths or function names.
39
41
 
40
42
  ## State Management
41
43
 
@@ -46,9 +48,9 @@ States can be attached to specific scopes (such as a DOM node or Component insta
46
48
  - **Up-tree Resolution**: When resolving a path, JPRX walks up the provided scope chain looking for the nearest owner of that name.
47
49
  - **Future Signals**: JPRX allows subscription to a named signal *before* it is initialized. The system will automatically "hook up" once the state is created via `$state` or `$signal`.
48
50
 
49
- ### The `$state` and `$signal` Helpers
50
- - `$state(value, { name: 'user', schema: 'UserProfile', scope: event.target })`
51
- - `$signal(0, { name: 'count', schema: 'auto' })`
51
+ ### The `=state` and `=signal` Helpers
52
+ - `=state(value, { name: 'user', schema: 'UserProfile', scope: event.target })`
53
+ - `=signal(0, { name: 'count', schema: 'auto' })`
52
54
 
53
55
  ## Schema Registry & Validation
54
56
 
@@ -64,10 +66,10 @@ jprx.registerSchema('UserProfile', {
64
66
  });
65
67
 
66
68
  // 2. Reference the registered schema by name (Scoped)
67
- const user = $state({}, { name: 'user', schema: 'UserProfile', scope: $this });
69
+ const user = =state({}, { name: 'user', schema: 'UserProfile', scope: $this });
68
70
 
69
71
  // 3. Use the 'polymorphic' shorthand for auto-coercion
70
- const settings = $state({ volume: 50 }, { name: 'settings', schema: 'polymorphic' });
72
+ const settings = =state({ volume: 50 }, { name: 'settings', schema: 'polymorphic' });
71
73
  // Result: settings.volume = "60" will automatically coerce to the number 60.
72
74
  ```
73
75
 
@@ -91,19 +93,59 @@ Schemas can define transformations that occur during state updates, ensuring dat
91
93
  }
92
94
  }
93
95
  ```
94
- *Note: The `$bind` helper uses these transformations to automatically clean data as the user types.*
96
+ *Note: The `=bind` helper uses these transformations to automatically clean data as the user types.*
95
97
 
96
- ## Two-Way Binding with `$bind`
98
+ ## Two-Way Binding with `=bind`
97
99
 
98
- The `$bind(path)` helper creates a managed, two-way link between the UI and a state path.
100
+ The `=bind(path)` helper creates a managed, two-way link between the UI and a state path.
99
101
 
100
102
  ### Strictness
101
- To ensure unambiguous data flow, `$bind` only accepts direct paths. It cannot be used directly with computed expressions like `$bind(upper(/name))`.
103
+ To ensure unambiguous data flow, `=bind` only accepts direct paths. It cannot be used directly with computed expressions like `=bind(upper(/name))`.
102
104
 
103
105
  ### Handling Transformations
104
106
  If you need to transform data during a two-way binding, there are two primary approaches:
105
- 1. **Event-Based**: Use a manual `oninput` handler to apply the transformation, e.g., `$set(/name, upper($event/target/value))`.
106
- 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.
107
+ 1. **Event-Based**: Use a manual `oninput` handler to apply the transformation, e.g., `=set(/name, upper($event/target/value))`.
108
+ 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
+
110
+ ---
111
+
112
+ ## DOM Patches & Decentralized Layouts
113
+
114
+ One of the most powerful features of JPRX in UI environments (like Lightview) is the ability to perform **Decentralized DOM Patches** via the `=move(target, location)` helper.
115
+
116
+ ### The Problem: LLM Streaming & Layouts
117
+ When an LLM streams UI components, it often knows *what* it is creating before it knows exactly *where* that item belongs in a complex dashboard, or it may need to update an existing element that was created minutes ago.
118
+
119
+ ### The Solution: `=move`
120
+ The `=move` helper allows a component to "place itself" into the document upon mounting.
121
+
122
+ ```json
123
+ {
124
+ "tag": "div",
125
+ "id": "weather-widget",
126
+ "onmount": "=move('#dashboard-sidebar', 'afterbegin')",
127
+ "content": "Sunny, 75°F"
128
+ }
129
+ ```
130
+
131
+ **Key Behaviors:**
132
+ 1. **Teleportation**: The host element is physically moved from the "Stream Container" (where it was born) to the specified `target` (e.g., `#dashboard-sidebar`).
133
+ 2. **Idempotent Updates**: If the moving element has an `id` (e.g., `weather-widget`) and an element with that same ID already exists at the destination, the existing element is **replaced**. This allows the LLM to "patch" the UI simply by streaming the updated version of the component.
134
+ 3. **Flicker-Free**: By rendering the stream in a hidden container, the element is moved and appears in its final destination instantly.
135
+
136
+ ### The Delivery Vehicle: `=mount`
137
+ The `=mount(url, options?)` helper is the primary mechanism for fetching these decentralized updates.
138
+
139
+ 1. **Arrival**: `=mount` fetches the content and "lands" it at the end of the `document.body` (by default).
140
+ 2. **Mounting**: The content is hydrated and added to the DOM, triggering its `onmount` hook.
141
+ 3. **Teleportation**: If the content contains `=move`, it immediately relocates itself to its destination.
142
+
143
+ This separation of concerns makes the system incredibly robust: `=mount` handles the **delivery**, while `=move` handles the **logic** of where the content belongs.
144
+
145
+ ### Why Decentralized?
146
+ - **Low Overhead**: The LLM doesn't need to maintain a map of the entire DOM; it just needs to know the ID or Selector of where it wants to "push" its content.
147
+ - **Independence**: Components are self-contained. A "Notification" component knows how to display itself AND where notification stacks live.
148
+ - **Context Preservation**: In Lightview, moved elements retain their original reactive context ("backpack"), allowing them to stay linked to the stream that created them.
107
149
 
108
150
  ---
109
151
 
@@ -114,12 +156,12 @@ A modern, lifecycle-based reactive counter:
114
156
  ```json
115
157
  {
116
158
  "div": {
117
- "onmount": $state({ count: 0 }, { name: 'counter', schema: 'auto', scope: $this }),
159
+ "onmount": "=state({ count: 0 }, { name: 'counter', schema: 'auto', scope: $this })",
118
160
  "children": [
119
161
  { "h2": "Modern JPRX Counter" },
120
- { "p": ["Current Count: ", $/counter/count] },
121
- { "button": { "onclick": $++/counter/count, "children": ["+"] } },
122
- { "button": { "onclick": $--/counter/count, "children": ["-"] } }
162
+ { "p": ["Current Count: ", "=/counter/count"] },
163
+ { "button": { "onclick": "=++/counter/count", "children": ["+"] } },
164
+ { "button": { "onclick": "=--/counter/count", "children": ["-"] } }
123
165
  ]
124
166
  }
125
167
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * JPRX CALC HELPER
3
+ * Safe expression evaluation using expr-eval.
4
+ * Uses the global $ helper for reactive path lookups.
5
+ */
6
+
7
+ import { Parser } from 'expr-eval';
8
+ import { resolvePath, unwrapSignal } from '../parser.js';
9
+
10
+ /**
11
+ * Evaluates a mathematical expression string.
12
+ * Supports $() for reactive path lookups within the expression.
13
+ *
14
+ * @param {string} expression - The expression to evaluate (e.g., "$('/price') * 1.08")
15
+ * @param {object} context - The JPRX context for path resolution
16
+ * @returns {number|string} - The result of the evaluation
17
+ *
18
+ * @example
19
+ * =calc("$('/width') * $('/height')")
20
+ * =calc("5 + 3 * 2")
21
+ * =calc("($('/subtotal') + $('/tax')) * 0.9")
22
+ */
23
+ export const calc = (expression, context) => {
24
+ if (typeof expression !== 'string') {
25
+ return expression;
26
+ }
27
+
28
+ let processedExpression = expression;
29
+ try {
30
+ const pathResolver = (path) => {
31
+ let currentPath = path;
32
+ let value;
33
+ let depth = 0;
34
+
35
+ // Recursively resolve if the value is another path string (e.g., "/c/display")
36
+ while (typeof currentPath === 'string' && (currentPath.startsWith('/') || currentPath.startsWith('=/')) && depth < 5) {
37
+ const normalizedPath = currentPath.startsWith('/') ? '=' + currentPath : currentPath;
38
+ const resolved = resolvePath(normalizedPath, context);
39
+ value = unwrapSignal(resolved);
40
+
41
+ // If the new value is a different path string, keep going
42
+ if (typeof value === 'string' && (value.startsWith('/') || value.startsWith('=/')) && value !== currentPath) {
43
+ currentPath = value;
44
+ depth++;
45
+ } else {
46
+ break;
47
+ }
48
+ }
49
+
50
+ if (typeof value === 'number') return value;
51
+ if (typeof value === 'string') {
52
+ const num = parseFloat(value);
53
+ if (!isNaN(num) && isFinite(Number(value))) return num;
54
+ return value === '' ? 0 : `"${value.replace(/"/g, '\\"')}"`;
55
+ }
56
+ return value === undefined || value === null ? 0 : value;
57
+ };
58
+
59
+ const pathRegex = /\$\(\s*['"](.*?)['"]\s*\)/g;
60
+ processedExpression = expression.replace(pathRegex, (match, path) => {
61
+ const val = pathResolver(path);
62
+ return val;
63
+ });
64
+
65
+ const parser = new Parser();
66
+ const parsed = parser.parse(processedExpression);
67
+ return parsed.evaluate();
68
+
69
+ } catch (error) {
70
+ console.error('JPRX calc error:', error.message);
71
+ console.error('Original expression:', expression);
72
+ console.error('Processed expression:', processedExpression);
73
+ return NaN;
74
+ }
75
+ };
76
+
77
+ /**
78
+ * Register the calc helper.
79
+ */
80
+ export const registerCalcHelpers = (register) => {
81
+ register('calc', calc, { pathAware: true });
82
+ };
package/helpers/lookup.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * cdom LOOKUP HELPERS
3
3
  */
4
4
 
5
+ import { resolvePath, unwrapSignal } from '../parser.js';
6
+
5
7
  export const lookup = (val, searchArr, resultArr) => {
6
8
  if (!Array.isArray(searchArr)) return undefined;
7
9
  const idx = searchArr.indexOf(val);
@@ -17,9 +19,46 @@ export const vlookup = (val, table, colIdx) => {
17
19
  export const index = (arr, idx) => Array.isArray(arr) ? arr[idx] : undefined;
18
20
  export const match = (val, arr) => Array.isArray(arr) ? arr.indexOf(val) : -1;
19
21
 
22
+ /**
23
+ * $ - Reactive path lookup helper.
24
+ * Resolves a JPRX path string and returns the unwrapped value.
25
+ *
26
+ * @param {string} path - The path to resolve (e.g., '/state/count')
27
+ * @param {object} context - The JPRX context (automatically provided by pathAware)
28
+ * @returns {any} - The resolved and unwrapped value
29
+ *
30
+ * @example
31
+ * =calc("$('/price') * 1.08")
32
+ * =$('/user/name')
33
+ */
34
+ export const pathRef = (path, context) => {
35
+ // If path is already a BindingTarget or has a .value property, use it directly
36
+ if (path && typeof path === 'object' && 'value' in path) {
37
+ return unwrapSignal(path.value);
38
+ }
39
+
40
+ // Fallback for string paths
41
+ if (typeof path === 'string') {
42
+ const normalized = path.startsWith('=') ? path : '=' + path;
43
+ const resolved = resolvePath(normalized, context);
44
+ const value = unwrapSignal(resolved);
45
+ // Convert to number if it looks like a number for calc compatibility
46
+ if (typeof value === 'number') return value;
47
+ if (typeof value === 'string' && value !== '' && !isNaN(parseFloat(value)) && isFinite(Number(value))) {
48
+ return parseFloat(value);
49
+ }
50
+ return value;
51
+ }
52
+
53
+ return unwrapSignal(path);
54
+ };
55
+
20
56
  export const registerLookupHelpers = (register) => {
21
57
  register('lookup', lookup);
22
58
  register('vlookup', vlookup);
23
59
  register('index', index);
24
60
  register('match', match);
61
+ register('$', pathRef, { pathAware: true });
62
+ register('val', pathRef, { pathAware: true });
63
+ register('indirect', pathRef, { pathAware: true });
25
64
  };
package/helpers/math.js CHANGED
@@ -14,6 +14,8 @@ export const abs = (val) => Math.abs(val);
14
14
  export const mod = (a, b) => a % b;
15
15
  export const pow = (a, b) => Math.pow(a, b);
16
16
  export const sqrt = (val) => Math.sqrt(val);
17
+ export const negate = (val) => -Number(val);
18
+ export const toPercent = (val) => Number(val) / 100;
17
19
 
18
20
  export const registerMathHelpers = (register) => {
19
21
  register('+', add);
@@ -31,4 +33,6 @@ export const registerMathHelpers = (register) => {
31
33
  register('mod', mod);
32
34
  register('pow', pow);
33
35
  register('sqrt', sqrt);
36
+ register('negate', negate);
37
+ register('toPercent', toPercent);
34
38
  };
package/helpers/state.js CHANGED
@@ -64,7 +64,7 @@ export const clear = (target) => {
64
64
  return set(target, null);
65
65
  };
66
66
 
67
- export function $state(val, options) {
67
+ export function state(val, options) {
68
68
  if (globalThis.Lightview) {
69
69
  const finalOptions = typeof options === 'string' ? { name: options } : options;
70
70
  return globalThis.Lightview.state(val, finalOptions);
@@ -72,7 +72,7 @@ export function $state(val, options) {
72
72
  throw new Error('JPRX: $state requires a UI library implementation.');
73
73
  }
74
74
 
75
- export function $signal(val, options) {
75
+ export function signal(val, options) {
76
76
  if (globalThis.Lightview) {
77
77
  const finalOptions = typeof options === 'string' ? { name: options } : options;
78
78
  return globalThis.Lightview.signal(val, finalOptions);
@@ -80,7 +80,7 @@ export function $signal(val, options) {
80
80
  throw new Error('JPRX: $signal requires a UI library implementation.');
81
81
  }
82
82
 
83
- export const $bind = (path, options) => ({ __JPRX_BIND__: true, path, options });
83
+ export const bind = (path, options) => ({ __JPRX_BIND__: true, path, options });
84
84
 
85
85
  export const registerStateHelpers = (register) => {
86
86
  const opts = { pathAware: true };
@@ -95,7 +95,7 @@ export const registerStateHelpers = (register) => {
95
95
  register('pop', pop, opts);
96
96
  register('assign', assign, opts);
97
97
  register('clear', clear, opts);
98
- register('state', $state);
99
- register('signal', $signal);
100
- register('bind', $bind);
98
+ register('state', state);
99
+ register('signal', signal);
100
+ register('bind', bind);
101
101
  };
package/index.js CHANGED
@@ -38,6 +38,7 @@ export { registerLookupHelpers } from './helpers/lookup.js';
38
38
  export { registerStatsHelpers } from './helpers/stats.js';
39
39
  export { registerStateHelpers, set } from './helpers/state.js';
40
40
  export { registerNetworkHelpers } from './helpers/network.js';
41
+ export { registerCalcHelpers, calc } from './helpers/calc.js';
41
42
 
42
43
  // Convenience function to register all standard helpers
43
44
  export const registerAllHelpers = (registerFn) => {
@@ -53,6 +54,7 @@ export const registerAllHelpers = (registerFn) => {
53
54
  const { registerStatsHelpers } = require('./helpers/stats.js');
54
55
  const { registerStateHelpers } = require('./helpers/state.js');
55
56
  const { registerNetworkHelpers } = require('./helpers/network.js');
57
+ const { registerCalcHelpers } = require('./helpers/calc.js');
56
58
 
57
59
  registerMathHelpers(registerFn);
58
60
  registerLogicHelpers(registerFn);
@@ -66,4 +68,5 @@ export const registerAllHelpers = (registerFn) => {
66
68
  registerStatsHelpers(registerFn);
67
69
  registerStateHelpers((name, fn) => registerFn(name, fn, { pathAware: true }));
68
70
  registerNetworkHelpers(registerFn);
71
+ registerCalcHelpers(registerFn);
69
72
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jprx",
3
- "version": "1.1.1",
4
- "description": "JSON Reactive Path eXpressions - A reactive expression language for JSON data",
3
+ "version": "1.2.1",
4
+ "description": "JSON Path Reactive eXpressions - A reactive expression language for JSON data",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "keywords": [
package/parser.js CHANGED
@@ -136,8 +136,8 @@ export const resolvePath = (path, context) => {
136
136
  // Current context: .
137
137
  if (path === '.') return unwrapSignal(context);
138
138
 
139
- // Global absolute path: $/something
140
- if (path.startsWith('$/')) {
139
+ // Global absolute path: =/something
140
+ if (path.startsWith('=/')) {
141
141
  const [rootName, ...rest] = path.slice(2).split('/');
142
142
  const LV = getLV();
143
143
  const root = LV ? LV.get(rootName, { scope: context?.__node__ || context }) : registry?.get(rootName);
@@ -164,7 +164,6 @@ export const resolvePath = (path, context) => {
164
164
  const unwrappedContext = unwrapSignal(context);
165
165
  if (unwrappedContext && typeof unwrappedContext === 'object') {
166
166
  if (path in unwrappedContext || unwrappedContext[path] !== undefined) {
167
- // Use traverse with one segment to ensure signal unwrapping if context[path] is a signal
168
167
  return traverse(unwrappedContext, [path]);
169
168
  }
170
169
  }
@@ -184,8 +183,8 @@ export const resolvePathAsContext = (path, context) => {
184
183
  // Current context: .
185
184
  if (path === '.') return context;
186
185
 
187
- // Global absolute path: $/something
188
- if (path.startsWith('$/')) {
186
+ // Global absolute path: =/something
187
+ if (path.startsWith('=/')) {
189
188
  const segments = path.slice(2).split(/[/.]/);
190
189
  const rootName = segments.shift();
191
190
  const LV = getLV();
@@ -304,10 +303,9 @@ const resolveArgument = (arg, context, globalMode = false) => {
304
303
  try {
305
304
  const data = parseJPRX(arg);
306
305
 
307
- // Define a recursive resolver for template objects
308
306
  const resolveTemplate = (node, context) => {
309
307
  if (typeof node === 'string') {
310
- if (node.startsWith('$')) {
308
+ if (node.startsWith('=')) {
311
309
  const res = resolveExpression(node, context);
312
310
  const final = (res instanceof LazyValue) ? res.resolve(context) : res;
313
311
  return unwrapSignal(final);
@@ -340,10 +338,9 @@ const resolveArgument = (arg, context, globalMode = false) => {
340
338
  return node;
341
339
  };
342
340
 
343
- // Check if it contains any reactive parts
344
341
  const hasReactive = (obj) => {
345
342
  if (typeof obj === 'string') {
346
- return obj.startsWith('$') || obj.startsWith('_') || obj.startsWith('../');
343
+ return obj.startsWith('=') || obj.startsWith('_') || obj.startsWith('../');
347
344
  }
348
345
  if (Array.isArray(obj)) return obj.some(hasReactive);
349
346
  if (obj && typeof obj === 'object') return Object.values(obj).some(hasReactive);
@@ -366,9 +363,9 @@ const resolveArgument = (arg, context, globalMode = false) => {
366
363
  if (arg.includes('(')) {
367
364
  let nestedExpr = arg;
368
365
  if (arg.startsWith('/')) {
369
- nestedExpr = '$' + arg;
370
- } else if (globalMode && !arg.startsWith('$') && !arg.startsWith('./')) {
371
- nestedExpr = `$/${arg}`;
366
+ nestedExpr = '=' + arg;
367
+ } else if (globalMode && !arg.startsWith('=') && !arg.startsWith('./')) {
368
+ nestedExpr = `=/${arg}`;
372
369
  }
373
370
 
374
371
  const val = resolveExpression(nestedExpr, context);
@@ -381,11 +378,11 @@ const resolveArgument = (arg, context, globalMode = false) => {
381
378
  // 8. Path normalization
382
379
  let normalizedPath;
383
380
  if (arg.startsWith('/')) {
384
- normalizedPath = '$' + arg;
385
- } else if (arg.startsWith('$') || arg.startsWith('./') || arg.startsWith('../')) {
381
+ normalizedPath = '=' + arg;
382
+ } else if (arg.startsWith('=') || arg.startsWith('./') || arg.startsWith('../')) {
386
383
  normalizedPath = arg;
387
384
  } else if (globalMode) {
388
- normalizedPath = `$/${arg}`;
385
+ normalizedPath = `=/${arg}`;
389
386
  } else {
390
387
  normalizedPath = `./${arg}`;
391
388
  }
@@ -483,10 +480,10 @@ const tokenize = (expr) => {
483
480
  continue;
484
481
  }
485
482
 
486
- // Special: $ followed immediately by an operator symbol
487
- // In expressions like "$++/count", the $ is just the JPRX delimiter
483
+ // Special: = followed immediately by an operator symbol
484
+ // In expressions like "=++/count", the = is just the JPRX delimiter
488
485
  // and ++ is a prefix operator applied to /count
489
- if (expr[i] === '$' && i + 1 < len) {
486
+ if (expr[i] === '=' && i + 1 < len) {
490
487
  // Check if next chars are a PREFIX operator (sort by length to match longest first)
491
488
  const prefixOps = [...operators.prefix.keys()].sort((a, b) => b.length - a.length);
492
489
  let isPrefixOp = false;
@@ -497,7 +494,7 @@ const tokenize = (expr) => {
497
494
  }
498
495
  }
499
496
  if (isPrefixOp) {
500
- // Skip the $, it's just a delimiter for a prefix operator (e.g., $++/count)
497
+ // Skip the =, it's just a delimiter for a prefix operator (e.g., =++/count)
501
498
  i++;
502
499
  continue;
503
500
  }
@@ -554,7 +551,7 @@ const tokenize = (expr) => {
554
551
  tokens[tokens.length - 1].type === TokenType.LPAREN ||
555
552
  tokens[tokens.length - 1].type === TokenType.COMMA ||
556
553
  tokens[tokens.length - 1].type === TokenType.OPERATOR;
557
- const validAfter = /[\s($./'"0-9_]/.test(after) ||
554
+ const validAfter = /[\s(=./'"0-9_]/.test(after) ||
558
555
  i + op.length >= len ||
559
556
  opSymbols.some(o => expr.slice(i + op.length).startsWith(o));
560
557
 
@@ -651,8 +648,8 @@ const tokenize = (expr) => {
651
648
  continue;
652
649
  }
653
650
 
654
- // Paths: start with $, ., or /
655
- if (expr[i] === '$' || expr[i] === '.' || expr[i] === '/') {
651
+ // Paths: start with =, ., or /
652
+ if (expr[i] === '=' || expr[i] === '.' || expr[i] === '/') {
656
653
  let path = '';
657
654
  // Consume the path, but stop at operators
658
655
  while (i < len) {
@@ -734,9 +731,9 @@ const tokenize = (expr) => {
734
731
  * Used to determine whether to use Pratt parser or legacy parser.
735
732
  *
736
733
  * CONSERVATIVE: Only detect explicit patterns to avoid false positives.
737
- * - Prefix: $++/path, $--/path, $!!/path (operator immediately after $ before path)
738
- * - Postfix: $/path++ or $/path-- (operator at end of expression, not followed by ()
739
- * - Infix with spaces: $/path + $/other (spaces around operator)
734
+ * - Prefix: =++/path, =--/path, =!!/path (operator immediately after = before path)
735
+ * - Postfix: =/path++ or =/path-- (operator at end of expression, not followed by ()
736
+ * - Infix with spaces: =/path + =/other (spaces around operator)
740
737
  */
741
738
  const hasOperatorSyntax = (expr) => {
742
739
  if (!expr || typeof expr !== 'string') return false;
@@ -744,19 +741,19 @@ const hasOperatorSyntax = (expr) => {
744
741
  // Skip function calls - they use legacy parser
745
742
  if (expr.includes('(')) return false;
746
743
 
747
- // Check for prefix operator pattern: $++ or $-- followed by /
748
- // This catches: $++/counter, $--/value
749
- if (/^\$(\+\+|--|!!)\/?/.test(expr)) {
744
+ // Check for prefix operator pattern: =++ or =-- followed by /
745
+ // This catches: =++/counter, =--/value
746
+ if (/^=(\+\+|--|!!)\/?/.test(expr)) {
750
747
  return true;
751
748
  }
752
749
 
753
750
  // Check for postfix operator pattern: path ending with ++ or --
754
- // This catches: $/counter++, $/value--
751
+ // This catches: =/counter++, =/value--
755
752
  if (/(\+\+|--)$/.test(expr)) {
756
753
  return true;
757
754
  }
758
755
 
759
- // Check for infix with explicit whitespace: $/a + $/b
756
+ // Check for infix with explicit whitespace: =/a + =/b
760
757
  // The spaces make it unambiguous that the symbol is an operator, not part of a path
761
758
  if (/\s+([+\-*/]|>|<|>=|<=|!=)\s+/.test(expr)) {
762
759
  return true;
@@ -1084,19 +1081,19 @@ export const resolveExpression = (expr, context) => {
1084
1081
  const argsStr = expr.slice(funcStart + 1, -1);
1085
1082
 
1086
1083
  const segments = fullPath.split('/');
1087
- let funcName = segments.pop().replace(/^\$/, '');
1084
+ let funcName = segments.pop().replace(/^=/, '');
1088
1085
 
1089
- // Handle case where path ends in / (like $/ for division helper)
1086
+ // Handle case where path ends in / (like =/ for division helper)
1090
1087
  if (funcName === '' && (segments.length > 0 || fullPath === '/')) {
1091
1088
  funcName = '/';
1092
1089
  }
1093
1090
 
1094
1091
  const navPath = segments.join('/');
1095
1092
 
1096
- const isGlobalExpr = expr.startsWith('$/') || expr.startsWith('$');
1093
+ const isGlobalExpr = expr.startsWith('=/') || expr.startsWith('=');
1097
1094
 
1098
1095
  let baseContext = context;
1099
- if (navPath && navPath !== '$') {
1096
+ if (navPath && navPath !== '=') {
1100
1097
  baseContext = resolvePathAsContext(navPath, context);
1101
1098
  }
1102
1099
 
@@ -1134,7 +1131,7 @@ export const resolveExpression = (expr, context) => {
1134
1131
  let hasLazy = false;
1135
1132
  for (let i = 0; i < argsList.length; i++) {
1136
1133
  const arg = argsList[i];
1137
- const useGlobalMode = isGlobalExpr && (navPath === '$' || !navPath);
1134
+ const useGlobalMode = isGlobalExpr && (navPath === '=' || !navPath);
1138
1135
  const res = resolveArgument(arg, baseContext, useGlobalMode);
1139
1136
 
1140
1137
  if (res.isLazy) hasLazy = true;
@@ -1299,8 +1296,8 @@ export const parseCDOMC = (input) => {
1299
1296
 
1300
1297
  const word = input.slice(start, i);
1301
1298
 
1302
- // If word starts with $, preserve it as a string for cDOM expression parsing
1303
- if (word.startsWith('$')) {
1299
+ // If word starts with =, preserve it as a string for cDOM expression parsing
1300
+ if (word.startsWith('=')) {
1304
1301
  return word;
1305
1302
  }
1306
1303
 
@@ -1476,8 +1473,8 @@ export const parseJPRX = (input) => {
1476
1473
  continue;
1477
1474
  }
1478
1475
 
1479
- // Handle JPRX expressions starting with $ (MUST come before word handler!)
1480
- if (char === '$') {
1476
+ // Handle JPRX expressions starting with = (MUST come before word handler!)
1477
+ if (char === '=') {
1481
1478
  let expr = '';
1482
1479
  let parenDepth = 0;
1483
1480
  let braceDepth = 0;
@@ -1514,21 +1511,54 @@ export const parseJPRX = (input) => {
1514
1511
  continue;
1515
1512
  }
1516
1513
 
1517
- // Handle unquoted property names, identifiers, and paths
1518
- if (/[a-zA-Z_./]/.test(char)) {
1514
+ // Handle unquoted property names, identifiers, paths, and FUNCTION CALLS
1515
+ if (/[a-zA-Z_$/./]/.test(char)) {
1519
1516
  let word = '';
1520
1517
  while (i < len && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
1521
1518
  word += input[i];
1522
1519
  i++;
1523
1520
  }
1524
1521
 
1525
- // Skip whitespace to check for :
1522
+ // Skip whitespace to check what follows
1526
1523
  let j = i;
1527
1524
  while (j < len && /\s/.test(input[j])) j++;
1528
1525
 
1529
1526
  if (input[j] === ':') {
1530
1527
  // It's a property name - quote it
1531
1528
  result += `"${word}"`;
1529
+ } else if (input[j] === '(') {
1530
+ // It's a FUNCTION CALL - capture the entire expression with balanced parens
1531
+ let expr = word;
1532
+ i = j; // move to the opening paren
1533
+ let parenDepth = 0;
1534
+ let inQuote = null;
1535
+
1536
+ while (i < len) {
1537
+ const c = input[i];
1538
+
1539
+ // Track quotes to avoid false paren matches inside strings
1540
+ if (inQuote) {
1541
+ if (c === inQuote && input[i - 1] !== '\\') inQuote = null;
1542
+ } else if (c === '"' || c === "'") {
1543
+ inQuote = c;
1544
+ } else {
1545
+ if (c === '(') parenDepth++;
1546
+ else if (c === ')') {
1547
+ parenDepth--;
1548
+ if (parenDepth === 0) {
1549
+ expr += c;
1550
+ i++;
1551
+ break;
1552
+ }
1553
+ }
1554
+ }
1555
+
1556
+ expr += c;
1557
+ i++;
1558
+ }
1559
+
1560
+ // Treat the function call as a JPRX expression by prefixing with =
1561
+ result += JSON.stringify('=' + expr);
1532
1562
  } else {
1533
1563
  // It's a value - check if it's a keyword
1534
1564
  if (word === 'true' || word === 'false' || word === 'null') {
@@ -4,7 +4,7 @@
4
4
  "state": {
5
5
  "userName": "Alice"
6
6
  },
7
- "expression": "$/userName",
7
+ "expression": "=/userName",
8
8
  "expected": "Alice"
9
9
  },
10
10
  {
@@ -16,7 +16,7 @@
16
16
  }
17
17
  }
18
18
  },
19
- "expression": "$/user/profile/name",
19
+ "expression": "=/user/profile/name",
20
20
  "expected": "Bob"
21
21
  },
22
22
  {
@@ -33,7 +33,7 @@
33
33
  "a": 5,
34
34
  "b": 10
35
35
  },
36
- "expression": "$+(/a, /b)",
36
+ "expression": "=+(/a, /b)",
37
37
  "expected": 15
38
38
  },
39
39
  {
@@ -42,7 +42,7 @@
42
42
  "a": 5,
43
43
  "b": 10
44
44
  },
45
- "expression": "$/a + $/b",
45
+ "expression": "=/a + =/b",
46
46
  "expected": 15
47
47
  },
48
48
  {
@@ -50,7 +50,7 @@
50
50
  "state": {
51
51
  "isVip": true
52
52
  },
53
- "expression": "$if(/isVip, \"Gold\", \"Silver\")",
53
+ "expression": "=if(/isVip, \"Gold\", \"Silver\")",
54
54
  "expected": "Gold"
55
55
  },
56
56
  {
@@ -65,7 +65,7 @@
65
65
  }
66
66
  ]
67
67
  },
68
- "expression": "$sum(/items...p)",
68
+ "expression": "=sum(/items...p)",
69
69
  "expected": 30
70
70
  }
71
71
  ]
@@ -1,102 +1,102 @@
1
1
  [
2
2
  {
3
3
  "name": "Math: Subtraction",
4
- "expression": "$-(10, 3)",
4
+ "expression": "=-(10, 3)",
5
5
  "expected": 7
6
6
  },
7
7
  {
8
8
  "name": "Math: Multiplication",
9
- "expression": "$*(4, 5)",
9
+ "expression": "=*(4, 5)",
10
10
  "expected": 20
11
11
  },
12
12
  {
13
13
  "name": "Math: Division",
14
- "expression": "$/(20, 4)",
14
+ "expression": "=/(20, 4)",
15
15
  "expected": 5
16
16
  },
17
17
  {
18
18
  "name": "Math: Round",
19
- "expression": "$round(3.7)",
19
+ "expression": "=round(3.7)",
20
20
  "expected": 4
21
21
  },
22
22
  {
23
23
  "name": "String: Upper",
24
- "expression": "$upper('hello')",
24
+ "expression": "=upper('hello')",
25
25
  "expected": "HELLO"
26
26
  },
27
27
  {
28
28
  "name": "String: Lower",
29
- "expression": "$lower('WORLD')",
29
+ "expression": "=lower('WORLD')",
30
30
  "expected": "world"
31
31
  },
32
32
  {
33
33
  "name": "String: Concat",
34
- "expression": "$concat('Hello', ' ', 'World')",
34
+ "expression": "=concat('Hello', ' ', 'World')",
35
35
  "expected": "Hello World"
36
36
  },
37
37
  {
38
38
  "name": "String: Trim",
39
- "expression": "$trim(' spaced ')",
39
+ "expression": "=trim(' spaced ')",
40
40
  "expected": "spaced"
41
41
  },
42
42
  {
43
43
  "name": "Logic: And (true)",
44
- "expression": "$and(true, true)",
44
+ "expression": "=and(true, true)",
45
45
  "expected": true
46
46
  },
47
47
  {
48
48
  "name": "Logic: And (false)",
49
- "expression": "$and(true, false)",
49
+ "expression": "=and(true, false)",
50
50
  "expected": false
51
51
  },
52
52
  {
53
53
  "name": "Logic: Or",
54
- "expression": "$or(false, true)",
54
+ "expression": "=or(false, true)",
55
55
  "expected": true
56
56
  },
57
57
  {
58
58
  "name": "Logic: Not",
59
- "expression": "$not(false)",
59
+ "expression": "=not(false)",
60
60
  "expected": true
61
61
  },
62
62
  {
63
63
  "name": "Compare: Greater Than",
64
- "expression": "$gt(10, 5)",
64
+ "expression": "=gt(10, 5)",
65
65
  "expected": true
66
66
  },
67
67
  {
68
68
  "name": "Compare: Less Than",
69
- "expression": "$lt(3, 7)",
69
+ "expression": "=lt(3, 7)",
70
70
  "expected": true
71
71
  },
72
72
  {
73
73
  "name": "Compare: Equal",
74
- "expression": "$eq(5, 5)",
74
+ "expression": "=eq(5, 5)",
75
75
  "expected": true
76
76
  },
77
77
  {
78
78
  "name": "Conditional: If (true branch)",
79
- "expression": "$if(true, 'Yes', 'No')",
79
+ "expression": "=if(true, 'Yes', 'No')",
80
80
  "expected": "Yes"
81
81
  },
82
82
  {
83
83
  "name": "Conditional: If (false branch)",
84
- "expression": "$if(false, 'Yes', 'No')",
84
+ "expression": "=if(false, 'Yes', 'No')",
85
85
  "expected": "No"
86
86
  },
87
87
  {
88
88
  "name": "Stats: Average",
89
- "expression": "$avg(10, 20, 30)",
89
+ "expression": "=avg(10, 20, 30)",
90
90
  "expected": 20
91
91
  },
92
92
  {
93
93
  "name": "Stats: Min",
94
- "expression": "$min(5, 2, 8)",
94
+ "expression": "=min(5, 2, 8)",
95
95
  "expected": 2
96
96
  },
97
97
  {
98
98
  "name": "Stats: Max",
99
- "expression": "$max(5, 2, 8)",
99
+ "expression": "=max(5, 2, 8)",
100
100
  "expected": 8
101
101
  },
102
102
  {
@@ -108,7 +108,7 @@
108
108
  "c"
109
109
  ]
110
110
  },
111
- "expression": "$len(/items)",
111
+ "expression": "=len(/items)",
112
112
  "expected": 3
113
113
  },
114
114
  {
@@ -120,7 +120,7 @@
120
120
  "c"
121
121
  ]
122
122
  },
123
- "expression": "$join(/items, '-')",
123
+ "expression": "=join(/items, '-')",
124
124
  "expected": "a-b-c"
125
125
  },
126
126
  {
@@ -132,7 +132,7 @@
132
132
  "c"
133
133
  ]
134
134
  },
135
- "expression": "$first(/items)",
135
+ "expression": "=first(/items)",
136
136
  "expected": "a"
137
137
  },
138
138
  {
@@ -144,7 +144,7 @@
144
144
  "c"
145
145
  ]
146
146
  },
147
- "expression": "$last(/items)",
147
+ "expression": "=last(/items)",
148
148
  "expected": "c"
149
149
  }
150
150
  ]