jprx 1.2.0 → 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 +1 -1
- package/helpers/calc.js +82 -0
- package/helpers/lookup.js +39 -0
- package/helpers/math.js +4 -0
- package/index.js +3 -0
- package/package.json +2 -2
- package/parser.js +35 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# JPRX (JSON Reactive
|
|
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
|
|
package/helpers/calc.js
ADDED
|
@@ -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/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.2.
|
|
4
|
-
"description": "JSON Reactive
|
|
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
|
@@ -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
|
}
|
|
@@ -1512,7 +1511,7 @@ export const parseJPRX = (input) => {
|
|
|
1512
1511
|
continue;
|
|
1513
1512
|
}
|
|
1514
1513
|
|
|
1515
|
-
// Handle unquoted property names, identifiers, and
|
|
1514
|
+
// Handle unquoted property names, identifiers, paths, and FUNCTION CALLS
|
|
1516
1515
|
if (/[a-zA-Z_$/./]/.test(char)) {
|
|
1517
1516
|
let word = '';
|
|
1518
1517
|
while (i < len && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
|
|
@@ -1520,13 +1519,46 @@ export const parseJPRX = (input) => {
|
|
|
1520
1519
|
i++;
|
|
1521
1520
|
}
|
|
1522
1521
|
|
|
1523
|
-
// Skip whitespace to check
|
|
1522
|
+
// Skip whitespace to check what follows
|
|
1524
1523
|
let j = i;
|
|
1525
1524
|
while (j < len && /\s/.test(input[j])) j++;
|
|
1526
1525
|
|
|
1527
1526
|
if (input[j] === ':') {
|
|
1528
1527
|
// It's a property name - quote it
|
|
1529
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);
|
|
1530
1562
|
} else {
|
|
1531
1563
|
// It's a value - check if it's a keyword
|
|
1532
1564
|
if (word === 'true' || word === 'false' || word === 'null') {
|