jprx 1.0.0 → 1.1.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 CHANGED
@@ -10,8 +10,15 @@ JPRX is a **syntax** and an **expression engine**. While this repository provide
10
10
 
11
11
  - **Declarative Power**: Define relationships between data points as easily as writing an Excel formula.
12
12
  - **Security**: JPRX strictly avoids `eval()`. Expressions are handled by a custom high-performance Pratt parser and a registry of pre-defined helpers, making it safe for dynamic content.
13
- - **Portability**: Because JPRX expressions are strings within JSON structures, they are easily serialized, stored, and sent over the wire.
14
- - **Platform Agnostic**: While Lightview is the first implementation, JPRX can be used in any environment that manages reactive state.
13
+ - **Portability**: JPRX expressions are strings within JSON, making them easily serialized and platform-agnostic.
14
+ - **Schema-First**: Integrated support for JSON Schema and shorthand descriptors provides industrial-strength validation and "future-proof" reactivity.
15
+
16
+ ## UI Library Requirements
17
+
18
+ To fully support JPRX, an underlying UI library **MUST** provide:
19
+ - **Mount Lifecycle**: A hook (e.g., `onmount`) where state initialization can occur. JPRX relies on the library to trigger these initializers.
20
+ - **Event Handling**: Support for standard event handlers (like `oninput`, `onclick`) **SHOULD** be provided, though exact implementations may vary by platform.
21
+ - **Reactivity**: A way to resolve paths to reactive primitives (e.g., Signals or Proxies).
15
22
 
16
23
  ## Syntax & Features
17
24
 
@@ -21,110 +28,102 @@ JPRX extends the base JSON Pointer syntax with:
21
28
  |---------|--------|-------------|
22
29
  | **Global Path** | `$/user/name` | Access global state via an absolute path. |
23
30
  | **Relative Path** | `./count` | Access properties relative to the current context. |
24
- | **Parent Path** | `../id` | Traverse up the state hierarchy. |
31
+ | **Parent Path** | `../id` | Traverse up the state hierarchy (UP-tree search). |
25
32
  | **Functions** | `$sum(/items...price)` | Call registered core helpers. |
26
33
  | **Explosion** | `/items...name` | Extract a property from every object in an array (spread). |
27
34
  | **Operators** | `$++/count`, `$/a + $/b` | Familiar JS-style prefix, postfix, and infix operators. |
28
- | **Placeholders** | `_` (item), `$event` | Context-aware placeholders for iteration and interaction. |
29
-
30
- ## Human & AI Utility
31
-
32
- JPRX is uniquely positioned to bridge the gap between human developers and AI coding assistants:
33
-
34
- ### For Humans: "The Excel Paradigm"
35
- Humans are often familiar with the "recalculation" model of spreadsheets. JPRX brings this to UI development. Instead of writing complex "glue code" (event listeners, state updates, DOM manipulation), developers specify the *formula* for a UI element once, and it stays updated forever.
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
37
 
37
- ### For AI: Structured & Concise
38
- Large Language Models (LLMs) are exceptionally good at generating structured data (JSON) and formulaic expressions. They are often prone to errors when generating large blocks of imperative JavaScript logic. JPRX provides a high-level, declarative "target" for AI to aim at, resulting in:
39
- - **Higher Accuracy**: Less boilerplate means fewer places for the AI to hallucinate.
40
- - **Safety**: AI can generate UI logic that remains sandboxed and secure.
41
- - **Compactness**: Entire interactive components can be described in a few lines of JSON.
38
+ Once inside a JPRX expression, the `$` prefix is only needed at the start of the expression for paths or function names.
42
39
 
43
- ## Operators
40
+ ## State Management
44
41
 
45
- JPRX supports a wide range of operators that provide a more concise and familiar syntax than function calls.
42
+ JPRX utilizes explicit state initializers within lifecycle hooks:
46
43
 
47
- ### Arithmetic & Logic (Infix)
48
- Infix operators require surrounding whitespace in JPRX to avoid ambiguity with path separators.
44
+ ### Scoped State
45
+ States can be attached to specific scopes (such as a DOM node or Component instance) using the `scope` property in the options argument.
46
+ - **Up-tree Resolution**: When resolving a path, JPRX walks up the provided scope chain looking for the nearest owner of that name.
47
+ - **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`.
49
48
 
50
- - **Arithmetic**: `+`, `-`, `*`, `/`, `mod`, `pow`
51
- - **Comparison**: `>`, `<`, `>=`, `<=`, `==`, `!=`
52
- - **Logic**: `&&`, `||`
49
+ ### The `$state` and `$signal` Helpers
50
+ - `$state(value, { name: 'user', schema: 'UserProfile', scope: event.target })`
51
+ - `$signal(0, { name: 'count', schema: 'auto' })`
53
52
 
54
- *Example:* `$/a + $/b * 10 > $/threshold`
53
+ ## Schema Registry & Validation
55
54
 
56
- ### Mutation & Unary (Prefix/Postfix)
57
- These operators are typically used in event handlers or for immediate state transformation.
55
+ JPRX integrates with a global Schema Registry via `jprx.registerSchema(name, definition)`.
58
56
 
59
- - **Increment**: `$++/count` (prefix) or `$/count++` (postfix)
60
- - **Decrement**: `$--/count` (prefix) or `$/count--` (postfix)
61
- - **Toggle**: `$!!/enabled` (logical NOT/toggle)
57
+ ### Registering and Using Schemas
58
+ ```javascript
59
+ // 1. Register a schema centrally
60
+ jprx.registerSchema('UserProfile', {
61
+ name: "string",
62
+ age: "number",
63
+ email: { type: "string", format: "email" }
64
+ });
62
65
 
63
- ## Helper Functions
66
+ // 2. Reference the registered schema by name (Scoped)
67
+ const user = $state({}, { name: 'user', schema: 'UserProfile', scope: $this });
64
68
 
65
- JPRX includes a comprehensive library of built-in helpers. For security, only registered helpers are available—there is no access to the global JavaScript environment.
66
-
67
- ### Math
68
- `add`, `sub`, `mul`, `div`, `mod`, `pow`, `sqrt`, `abs`, `round`, `ceil`, `floor`, `min`, `max`
69
-
70
- ### Stats
71
- `sum`, `avg`, `min`, `max`, `median`, `stdev`, `var`
69
+ // 3. Use the 'polymorphic' shorthand for auto-coercion
70
+ const settings = $state({ volume: 50 }, { name: 'settings', schema: 'polymorphic' });
71
+ // Result: settings.volume = "60" will automatically coerce to the number 60.
72
+ ```
72
73
 
73
- ### String
74
- `upper`, `lower`, `trim`, `capitalize`, `titleCase`, `contains`, `startsWith`, `endsWith`, `replace`, `split`, `join`, `concat`, `len`, `default`
74
+ - **Polymorphic Schemas**:
75
+ - **`"auto"`**: Infers the fixed schema from the initial value. Strict type checking (e.g., setting a number to a string throws). New properties are not allowed.
76
+ - **`"dynamic"`**: Like `auto`, but allows new properties to be added to the state object.
77
+ - **`"polymorphic"`**: Includes **`dynamic`** behavior and automatically coerces values to match the inferred type (e.g., "50" -> 50) rather than throwing.
78
+ - **Shorthand**: A simple object like `{ name: "string" }` is internally normalized to a JSON Schema.
75
79
 
76
- ### Array
77
- `count`, `map`, `filter`, `find`, `unique`, `sort`, `reverse`, `first`, `last`, `slice`, `flatten`, `join`, `len`
80
+ ### Transformation Schemas
81
+ Schemas can define transformations that occur during state updates, ensuring data remains in a consistent format regardless of how it was input.
78
82
 
79
- ### Logic & Comparison
80
- `if`, `and`, `or`, `not`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `between`, `in`
83
+ ```json
84
+ {
85
+ "type": "object",
86
+ "properties": {
87
+ "username": {
88
+ "type": "string",
89
+ "transform": "lower"
90
+ }
91
+ }
92
+ }
93
+ ```
94
+ *Note: The `$bind` helper uses these transformations to automatically clean data as the user types.*
81
95
 
82
- ### Formatting
83
- `number`, `currency`, `percent`, `thousands`
96
+ ## Two-Way Binding with `$bind`
84
97
 
85
- ### DateTime
86
- `now`, `today`, `date`, `formatDate`, `year`, `month`, `day`, `weekday`, `addDays`, `dateDiff`
98
+ The `$bind(path)` helper creates a managed, two-way link between the UI and a state path.
87
99
 
88
- ### Lookup
89
- `lookup`, `vlookup`, `index`, `match`
100
+ ### Strictness
101
+ To ensure unambiguous data flow, `$bind` only accepts direct paths. It cannot be used directly with computed expressions like `$bind(upper(/name))`.
90
102
 
91
- ### State Mutation
92
- `set`, `increment`, `decrement`, `toggle`, `push`, `pop`, `assign`, `clear`
103
+ ### Handling Transformations
104
+ 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.
93
107
 
94
- ### Network
95
- `fetch(url, options?)` - *Auto-serializes JSON bodies and handles content-types.*
108
+ ---
96
109
 
97
110
  ## Example
98
111
 
99
- A simple reactive counter described in JPRX syntax:
112
+ A modern, lifecycle-based reactive counter:
100
113
 
101
114
  ```json
102
115
  {
103
116
  "div": {
104
- "cdom-state": { "count": 0 },
117
+ "onmount": $state({ count: 0 }, { name: 'counter', schema: 'auto', scope: $this }),
105
118
  "children": [
106
- { "h2": "Counter" },
107
- { "p": ["Current Count: ", "$/count"] },
108
- { "button": { "onclick": "$increment(/count)", "children": ["+"] } },
109
- { "button": { "onclick": "$decrement(/count)", "children": ["-"] } }
119
+ { "h2": "Modern JPRX Counter" },
120
+ { "p": ["Current Count: ", $/counter/count] },
121
+ { "button": { "onclick": $++/counter/count, "children": ["+"] } },
122
+ { "button": { "onclick": $--/counter/count, "children": ["-"] } }
110
123
  ]
111
124
  }
112
125
  }
113
126
  ```
114
127
 
115
- ## Reference Implementation: Lightview
116
-
117
- JPRX was originally developed for [Lightview](https://github.com/anywhichway/lightview) to power its **Computational DOM (cDOM)**. Lightview serves as the primary example of how a UI library can hydrate JPRX expressions into a live, reactive interface.
118
-
119
- If you are building a UI library and want to support reactive JSON structures, this parser provides the foundation.
120
-
121
- ## Getting Started
122
-
123
- The JPRX package contains:
124
- 1. `parser.js`: The core Pratt parser and path resolution logic.
125
- 2. `helpers/`: A comprehensive library of math, logic, string, array, formatting, and state helpers.
126
-
127
- To use JPRX, you typically register your state-management primitives (like Signals or Proxies) with the parser's registry, and then call `hydrate()` or `resolveExpression()` to activate the logic.
128
-
129
128
  ---
130
129
  © 2026 Simon Y. Blackwell, AnyWhichWay LLC. Licensed under MIT.
package/helpers/state.js CHANGED
@@ -64,6 +64,24 @@ export const clear = (target) => {
64
64
  return set(target, null);
65
65
  };
66
66
 
67
+ export function $state(val, options) {
68
+ if (globalThis.Lightview) {
69
+ const finalOptions = typeof options === 'string' ? { name: options } : options;
70
+ return globalThis.Lightview.state(val, finalOptions);
71
+ }
72
+ throw new Error('JPRX: $state requires a UI library implementation.');
73
+ }
74
+
75
+ export function $signal(val, options) {
76
+ if (globalThis.Lightview) {
77
+ const finalOptions = typeof options === 'string' ? { name: options } : options;
78
+ return globalThis.Lightview.signal(val, finalOptions);
79
+ }
80
+ throw new Error('JPRX: $signal requires a UI library implementation.');
81
+ }
82
+
83
+ export const $bind = (path, options) => ({ __JPRX_BIND__: true, path, options });
84
+
67
85
  export const registerStateHelpers = (register) => {
68
86
  const opts = { pathAware: true };
69
87
  register('set', set, opts);
@@ -77,4 +95,7 @@ export const registerStateHelpers = (register) => {
77
95
  register('pop', pop, opts);
78
96
  register('assign', assign, opts);
79
97
  register('clear', clear, opts);
98
+ register('state', $state);
99
+ register('signal', $signal);
100
+ register('bind', $bind);
80
101
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jprx",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "JSON Reactive Path eXpressions - A reactive expression language for JSON data",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/parser.js CHANGED
@@ -28,6 +28,9 @@ const DEFAULT_PRECEDENCE = {
28
28
  */
29
29
  export const registerHelper = (name, fn, options = {}) => {
30
30
  helpers.set(name, fn);
31
+ if (globalThis.__LIGHTVIEW_INTERNALS__) {
32
+ globalThis.__LIGHTVIEW_INTERNALS__.helpers.set(name, fn);
33
+ }
31
34
  if (options) helperOptions.set(name, options);
32
35
  };
33
36
 
@@ -134,27 +137,12 @@ export const resolvePath = (path, context) => {
134
137
  if (path === '.') return unwrapSignal(context);
135
138
 
136
139
  // Global absolute path: $/something
137
- // First check if the root is in the local context's state (cdom-state)
138
- // This allows $/cart to resolve from cdom-state: { cart: {...} }
139
140
  if (path.startsWith('$/')) {
140
141
  const [rootName, ...rest] = path.slice(2).split('/');
141
-
142
- // Check local state chain first (via __state__ property set by handleCDOMState)
143
- let cur = context;
144
- while (cur) {
145
- const localState = cur.__state__;
146
- if (localState && rootName in localState) {
147
- return traverse(localState[rootName], rest);
148
- }
149
- cur = cur.__parent__;
150
- }
151
-
152
- // Then check global registry
153
- const rootSignal = registry?.get(rootName);
154
- if (!rootSignal) return undefined;
155
-
156
- // Root can be a signal or a state proxy
157
- return traverse(rootSignal, rest);
142
+ const LV = getLV();
143
+ const root = LV ? LV.get(rootName, { scope: context?.__node__ || context }) : registry?.get(rootName);
144
+ if (!root) return undefined;
145
+ return traverse(root, rest);
158
146
  }
159
147
 
160
148
  // Relative path from current context
@@ -197,26 +185,13 @@ export const resolvePathAsContext = (path, context) => {
197
185
  if (path === '.') return context;
198
186
 
199
187
  // Global absolute path: $/something
200
- // First check if the root is in the local context's state (cdom-state)
201
188
  if (path.startsWith('$/')) {
202
189
  const segments = path.slice(2).split(/[/.]/);
203
190
  const rootName = segments.shift();
204
-
205
- // Check local state chain first
206
- let cur = context;
207
- while (cur) {
208
- const localState = cur.__state__;
209
- if (localState && rootName in localState) {
210
- return traverseAsContext(localState[rootName], segments);
211
- }
212
- cur = cur.__parent__;
213
- }
214
-
215
- // Then check global registry
216
- const rootSignal = registry?.get(rootName);
217
- if (!rootSignal) return undefined;
218
-
219
- return traverseAsContext(rootSignal, segments);
191
+ const LV = getLV();
192
+ const root = LV ? LV.get(rootName, { scope: context?.__node__ || context }) : registry?.get(rootName);
193
+ if (!root) return undefined;
194
+ return traverseAsContext(root, segments);
220
195
  }
221
196
 
222
197
  // Relative path from current context
@@ -260,6 +235,11 @@ class LazyValue {
260
235
  }
261
236
  }
262
237
 
238
+ /**
239
+ * Node helper - identifies if a value is a DOM node.
240
+ */
241
+ const isNode = (val) => val && typeof val === 'object' && globalThis.Node && val instanceof globalThis.Node;
242
+
263
243
  /**
264
244
  * Helper to resolve an argument which could be a literal, a path, or an explosion.
265
245
  * @param {string} arg - The argument string
@@ -294,10 +274,23 @@ const resolveArgument = (arg, context, globalMode = false) => {
294
274
  };
295
275
  }
296
276
 
297
- // 5. Event Placeholder ($event)
277
+ // 5. Context Identifiers ($this, $event)
278
+ if (arg === '$this' || arg.startsWith('$this/') || arg.startsWith('$this.')) {
279
+ return {
280
+ value: new LazyValue((context) => {
281
+ const node = context?.__node__ || context;
282
+ if (arg === '$this') return node;
283
+ const path = arg.startsWith('$this.') ? arg.slice(6) : arg.slice(6);
284
+ return resolvePath(path, node);
285
+ }),
286
+ isLazy: true
287
+ };
288
+ }
289
+
298
290
  if (arg === '$event' || arg.startsWith('$event/') || arg.startsWith('$event.')) {
299
291
  return {
300
- value: new LazyValue((event) => {
292
+ value: new LazyValue((context) => {
293
+ const event = context?.$event || context?.event || context;
301
294
  if (arg === '$event') return event;
302
295
  const path = arg.startsWith('$event.') ? arg.slice(7) : arg.slice(7);
303
296
  return resolvePath(path, event);
@@ -319,6 +312,18 @@ const resolveArgument = (arg, context, globalMode = false) => {
319
312
  const final = (res instanceof LazyValue) ? res.resolve(context) : res;
320
313
  return unwrapSignal(final);
321
314
  }
315
+ if (node === '$this' || node.startsWith('$this/') || node.startsWith('$this.')) {
316
+ const path = (node.startsWith('$this.') || node.startsWith('$this/')) ? node.slice(6) : node.slice(6);
317
+ const ctxNode = context?.__node__ || context;
318
+ const res = node === '$this' ? ctxNode : resolvePath(path, ctxNode);
319
+ return unwrapSignal(res);
320
+ }
321
+ if (node === '$event' || node.startsWith('$event/') || node.startsWith('$event.')) {
322
+ const path = (node.startsWith('$event.') || node.startsWith('$event/')) ? node.slice(7) : node.slice(7);
323
+ const event = context?.$event || context?.event || (context && !isNode(context) ? context : null);
324
+ const res = node === '$event' ? event : resolvePath(path, event);
325
+ return unwrapSignal(res);
326
+ }
322
327
  if (node === '_' || node.startsWith('_/') || node.startsWith('_.')) {
323
328
  const path = (node.startsWith('_.') || node.startsWith('_/')) ? node.slice(2) : node.slice(2);
324
329
  const res = node === '_' ? context : resolvePath(path, context);
@@ -433,6 +438,7 @@ const TokenType = {
433
438
  COMMA: 'COMMA', // ,
434
439
  EXPLOSION: 'EXPLOSION', // ... suffix
435
440
  PLACEHOLDER: 'PLACEHOLDER', // _, _/path
441
+ THIS: 'THIS', // $this
436
442
  EVENT: 'EVENT', // $event, $event.target
437
443
  EOF: 'EOF'
438
444
  };
@@ -620,6 +626,18 @@ const tokenize = (expr) => {
620
626
  continue;
621
627
  }
622
628
 
629
+ // $this placeholder
630
+ if (expr.slice(i, i + 5) === '$this') {
631
+ let thisPath = '$this';
632
+ i += 5;
633
+ while (i < len && /[a-zA-Z0-9_./]/.test(expr[i])) {
634
+ thisPath += expr[i];
635
+ i++;
636
+ }
637
+ tokens.push({ type: TokenType.THIS, value: thisPath });
638
+ continue;
639
+ }
640
+
623
641
  // $event placeholder
624
642
  if (expr.slice(i, i + 6) === '$event') {
625
643
  let eventPath = '$event';
@@ -871,6 +889,12 @@ class PrattParser {
871
889
  return { type: 'Placeholder', value: tok.value };
872
890
  }
873
891
 
892
+ // This
893
+ if (tok.type === TokenType.THIS) {
894
+ this.consume();
895
+ return { type: 'This', value: tok.value };
896
+ }
897
+
874
898
  // Event
875
899
  if (tok.type === TokenType.EVENT) {
876
900
  this.consume();
@@ -927,8 +951,18 @@ const evaluateAST = (ast, context, forMutation = false) => {
927
951
  });
928
952
  }
929
953
 
954
+ case 'This': {
955
+ return new LazyValue((context) => {
956
+ const node = context?.__node__ || context;
957
+ if (ast.value === '$this') return node;
958
+ const path = ast.value.startsWith('$this.') ? ast.value.slice(6) : ast.value.slice(6);
959
+ return resolvePath(path, node);
960
+ });
961
+ }
962
+
930
963
  case 'Event': {
931
- return new LazyValue((event) => {
964
+ return new LazyValue((context) => {
965
+ const event = context?.$event || context?.event || context;
932
966
  if (ast.value === '$event') return event;
933
967
  const path = ast.value.startsWith('$event.') ? ast.value.slice(7) : ast.value.slice(7);
934
968
  return resolvePath(path, event);
@@ -1117,7 +1151,7 @@ export const resolveExpression = (expr, context) => {
1117
1151
  });
1118
1152
  }
1119
1153
 
1120
- const result = helper(...resolvedArgs);
1154
+ const result = helper.apply(context?.__node__ || null, resolvedArgs);
1121
1155
  return unwrapSignal(result);
1122
1156
  }
1123
1157