lightview 2.2.2 → 2.3.4
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/cDOMIntro.md +279 -0
- package/docs/about.html +15 -12
- package/docs/api/computed.html +1 -1
- package/docs/api/effects.html +1 -1
- package/docs/api/elements.html +56 -25
- package/docs/api/enhance.html +1 -1
- package/docs/api/hypermedia.html +1 -1
- package/docs/api/index.html +1 -1
- package/docs/api/nav.html +28 -3
- package/docs/api/signals.html +1 -1
- package/docs/api/state.html +283 -85
- package/docs/assets/js/examplify.js +2 -1
- package/docs/cdom-nav.html +3 -2
- package/docs/cdom.html +383 -114
- package/jprx/README.md +112 -71
- package/jprx/helpers/state.js +21 -0
- package/jprx/package.json +1 -1
- package/jprx/parser.js +136 -86
- package/jprx/specs/expressions.json +71 -0
- package/jprx/specs/helpers.json +150 -0
- package/lightview-all.js +618 -431
- package/lightview-cdom.js +311 -605
- package/lightview-router.js +6 -0
- package/lightview-x.js +226 -54
- package/lightview.js +351 -42
- package/package.json +2 -1
- package/src/lightview-cdom.js +211 -315
- package/src/lightview-router.js +10 -0
- package/src/lightview-x.js +121 -1
- package/src/lightview.js +88 -16
- package/src/reactivity/signal.js +73 -29
- package/src/reactivity/state.js +84 -21
- package/tests/cdom/fixtures/helpers.cdomc +24 -24
- package/tests/cdom/helpers.test.js +28 -28
- package/tests/cdom/parser.test.js +39 -114
- package/tests/cdom/reactivity.test.js +32 -29
- package/tests/jprx/spec.test.js +99 -0
- package/tests/cdom/loader.test.js +0 -125
package/jprx/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
|
|
|
@@ -133,28 +136,13 @@ export const resolvePath = (path, context) => {
|
|
|
133
136
|
// Current context: .
|
|
134
137
|
if (path === '.') return unwrapSignal(context);
|
|
135
138
|
|
|
136
|
-
// Global absolute path:
|
|
137
|
-
|
|
138
|
-
// This allows $/cart to resolve from cdom-state: { cart: {...} }
|
|
139
|
-
if (path.startsWith('$/')) {
|
|
139
|
+
// Global absolute path: =/something
|
|
140
|
+
if (path.startsWith('=/')) {
|
|
140
141
|
const [rootName, ...rest] = path.slice(2).split('/');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
@@ -196,27 +184,14 @@ export const resolvePathAsContext = (path, context) => {
|
|
|
196
184
|
// Current context: .
|
|
197
185
|
if (path === '.') return context;
|
|
198
186
|
|
|
199
|
-
// Global absolute path:
|
|
200
|
-
|
|
201
|
-
if (path.startsWith('$/')) {
|
|
187
|
+
// Global absolute path: =/something
|
|
188
|
+
if (path.startsWith('=/')) {
|
|
202
189
|
const segments = path.slice(2).split(/[/.]/);
|
|
203
190
|
const rootName = segments.shift();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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.
|
|
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((
|
|
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);
|
|
@@ -311,14 +304,25 @@ const resolveArgument = (arg, context, globalMode = false) => {
|
|
|
311
304
|
try {
|
|
312
305
|
const data = parseJPRX(arg);
|
|
313
306
|
|
|
314
|
-
// Define a recursive resolver for template objects
|
|
315
307
|
const resolveTemplate = (node, context) => {
|
|
316
308
|
if (typeof node === 'string') {
|
|
317
|
-
if (node.startsWith('
|
|
309
|
+
if (node.startsWith('=')) {
|
|
318
310
|
const res = resolveExpression(node, context);
|
|
319
311
|
const final = (res instanceof LazyValue) ? res.resolve(context) : res;
|
|
320
312
|
return unwrapSignal(final);
|
|
321
313
|
}
|
|
314
|
+
if (node === '$this' || node.startsWith('$this/') || node.startsWith('$this.')) {
|
|
315
|
+
const path = (node.startsWith('$this.') || node.startsWith('$this/')) ? node.slice(6) : node.slice(6);
|
|
316
|
+
const ctxNode = context?.__node__ || context;
|
|
317
|
+
const res = node === '$this' ? ctxNode : resolvePath(path, ctxNode);
|
|
318
|
+
return unwrapSignal(res);
|
|
319
|
+
}
|
|
320
|
+
if (node === '$event' || node.startsWith('$event/') || node.startsWith('$event.')) {
|
|
321
|
+
const path = (node.startsWith('$event.') || node.startsWith('$event/')) ? node.slice(7) : node.slice(7);
|
|
322
|
+
const event = context?.$event || context?.event || (context && !isNode(context) ? context : null);
|
|
323
|
+
const res = node === '$event' ? event : resolvePath(path, event);
|
|
324
|
+
return unwrapSignal(res);
|
|
325
|
+
}
|
|
322
326
|
if (node === '_' || node.startsWith('_/') || node.startsWith('_.')) {
|
|
323
327
|
const path = (node.startsWith('_.') || node.startsWith('_/')) ? node.slice(2) : node.slice(2);
|
|
324
328
|
const res = node === '_' ? context : resolvePath(path, context);
|
|
@@ -335,10 +339,9 @@ const resolveArgument = (arg, context, globalMode = false) => {
|
|
|
335
339
|
return node;
|
|
336
340
|
};
|
|
337
341
|
|
|
338
|
-
// Check if it contains any reactive parts
|
|
339
342
|
const hasReactive = (obj) => {
|
|
340
343
|
if (typeof obj === 'string') {
|
|
341
|
-
return obj.startsWith('
|
|
344
|
+
return obj.startsWith('=') || obj.startsWith('_') || obj.startsWith('../');
|
|
342
345
|
}
|
|
343
346
|
if (Array.isArray(obj)) return obj.some(hasReactive);
|
|
344
347
|
if (obj && typeof obj === 'object') return Object.values(obj).some(hasReactive);
|
|
@@ -361,9 +364,9 @@ const resolveArgument = (arg, context, globalMode = false) => {
|
|
|
361
364
|
if (arg.includes('(')) {
|
|
362
365
|
let nestedExpr = arg;
|
|
363
366
|
if (arg.startsWith('/')) {
|
|
364
|
-
nestedExpr = '
|
|
365
|
-
} else if (globalMode && !arg.startsWith('
|
|
366
|
-
nestedExpr =
|
|
367
|
+
nestedExpr = '=' + arg;
|
|
368
|
+
} else if (globalMode && !arg.startsWith('=') && !arg.startsWith('./')) {
|
|
369
|
+
nestedExpr = `=/${arg}`;
|
|
367
370
|
}
|
|
368
371
|
|
|
369
372
|
const val = resolveExpression(nestedExpr, context);
|
|
@@ -376,11 +379,11 @@ const resolveArgument = (arg, context, globalMode = false) => {
|
|
|
376
379
|
// 8. Path normalization
|
|
377
380
|
let normalizedPath;
|
|
378
381
|
if (arg.startsWith('/')) {
|
|
379
|
-
normalizedPath = '
|
|
380
|
-
} else if (arg.startsWith('
|
|
382
|
+
normalizedPath = '=' + arg;
|
|
383
|
+
} else if (arg.startsWith('=') || arg.startsWith('./') || arg.startsWith('../')) {
|
|
381
384
|
normalizedPath = arg;
|
|
382
385
|
} else if (globalMode) {
|
|
383
|
-
normalizedPath =
|
|
386
|
+
normalizedPath = `=/${arg}`;
|
|
384
387
|
} else {
|
|
385
388
|
normalizedPath = `./${arg}`;
|
|
386
389
|
}
|
|
@@ -433,6 +436,7 @@ const TokenType = {
|
|
|
433
436
|
COMMA: 'COMMA', // ,
|
|
434
437
|
EXPLOSION: 'EXPLOSION', // ... suffix
|
|
435
438
|
PLACEHOLDER: 'PLACEHOLDER', // _, _/path
|
|
439
|
+
THIS: 'THIS', // $this
|
|
436
440
|
EVENT: 'EVENT', // $event, $event.target
|
|
437
441
|
EOF: 'EOF'
|
|
438
442
|
};
|
|
@@ -477,20 +481,21 @@ const tokenize = (expr) => {
|
|
|
477
481
|
continue;
|
|
478
482
|
}
|
|
479
483
|
|
|
480
|
-
// Special:
|
|
481
|
-
// In expressions like "
|
|
484
|
+
// Special: = followed immediately by an operator symbol
|
|
485
|
+
// In expressions like "=++/count", the = is just the JPRX delimiter
|
|
482
486
|
// and ++ is a prefix operator applied to /count
|
|
483
|
-
if (expr[i] === '
|
|
484
|
-
// Check if next chars are
|
|
485
|
-
|
|
486
|
-
|
|
487
|
+
if (expr[i] === '=' && i + 1 < len) {
|
|
488
|
+
// Check if next chars are a PREFIX operator (sort by length to match longest first)
|
|
489
|
+
const prefixOps = [...operators.prefix.keys()].sort((a, b) => b.length - a.length);
|
|
490
|
+
let isPrefixOp = false;
|
|
491
|
+
for (const op of prefixOps) {
|
|
487
492
|
if (expr.slice(i + 1, i + 1 + op.length) === op) {
|
|
488
|
-
|
|
493
|
+
isPrefixOp = true;
|
|
489
494
|
break;
|
|
490
495
|
}
|
|
491
496
|
}
|
|
492
|
-
if (
|
|
493
|
-
// Skip the
|
|
497
|
+
if (isPrefixOp) {
|
|
498
|
+
// Skip the =, it's just a delimiter for a prefix operator (e.g., =++/count)
|
|
494
499
|
i++;
|
|
495
500
|
continue;
|
|
496
501
|
}
|
|
@@ -547,7 +552,7 @@ const tokenize = (expr) => {
|
|
|
547
552
|
tokens[tokens.length - 1].type === TokenType.LPAREN ||
|
|
548
553
|
tokens[tokens.length - 1].type === TokenType.COMMA ||
|
|
549
554
|
tokens[tokens.length - 1].type === TokenType.OPERATOR;
|
|
550
|
-
const validAfter = /[\s(
|
|
555
|
+
const validAfter = /[\s(=./'"0-9_]/.test(after) ||
|
|
551
556
|
i + op.length >= len ||
|
|
552
557
|
opSymbols.some(o => expr.slice(i + op.length).startsWith(o));
|
|
553
558
|
|
|
@@ -620,6 +625,18 @@ const tokenize = (expr) => {
|
|
|
620
625
|
continue;
|
|
621
626
|
}
|
|
622
627
|
|
|
628
|
+
// $this placeholder
|
|
629
|
+
if (expr.slice(i, i + 5) === '$this') {
|
|
630
|
+
let thisPath = '$this';
|
|
631
|
+
i += 5;
|
|
632
|
+
while (i < len && /[a-zA-Z0-9_./]/.test(expr[i])) {
|
|
633
|
+
thisPath += expr[i];
|
|
634
|
+
i++;
|
|
635
|
+
}
|
|
636
|
+
tokens.push({ type: TokenType.THIS, value: thisPath });
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
623
640
|
// $event placeholder
|
|
624
641
|
if (expr.slice(i, i + 6) === '$event') {
|
|
625
642
|
let eventPath = '$event';
|
|
@@ -632,8 +649,8 @@ const tokenize = (expr) => {
|
|
|
632
649
|
continue;
|
|
633
650
|
}
|
|
634
651
|
|
|
635
|
-
// Paths: start with
|
|
636
|
-
if (expr[i] === '
|
|
652
|
+
// Paths: start with =, ., or /
|
|
653
|
+
if (expr[i] === '=' || expr[i] === '.' || expr[i] === '/') {
|
|
637
654
|
let path = '';
|
|
638
655
|
// Consume the path, but stop at operators
|
|
639
656
|
while (i < len) {
|
|
@@ -715,9 +732,9 @@ const tokenize = (expr) => {
|
|
|
715
732
|
* Used to determine whether to use Pratt parser or legacy parser.
|
|
716
733
|
*
|
|
717
734
|
* CONSERVATIVE: Only detect explicit patterns to avoid false positives.
|
|
718
|
-
* - Prefix:
|
|
719
|
-
* - Postfix:
|
|
720
|
-
* - Infix with spaces:
|
|
735
|
+
* - Prefix: =++/path, =--/path, =!!/path (operator immediately after = before path)
|
|
736
|
+
* - Postfix: =/path++ or =/path-- (operator at end of expression, not followed by ()
|
|
737
|
+
* - Infix with spaces: =/path + =/other (spaces around operator)
|
|
721
738
|
*/
|
|
722
739
|
const hasOperatorSyntax = (expr) => {
|
|
723
740
|
if (!expr || typeof expr !== 'string') return false;
|
|
@@ -725,19 +742,19 @@ const hasOperatorSyntax = (expr) => {
|
|
|
725
742
|
// Skip function calls - they use legacy parser
|
|
726
743
|
if (expr.includes('(')) return false;
|
|
727
744
|
|
|
728
|
-
// Check for prefix operator pattern:
|
|
729
|
-
// This catches:
|
|
730
|
-
if (
|
|
745
|
+
// Check for prefix operator pattern: =++ or =-- followed by /
|
|
746
|
+
// This catches: =++/counter, =--/value
|
|
747
|
+
if (/^=(\+\+|--|!!)\/?/.test(expr)) {
|
|
731
748
|
return true;
|
|
732
749
|
}
|
|
733
750
|
|
|
734
751
|
// Check for postfix operator pattern: path ending with ++ or --
|
|
735
|
-
// This catches:
|
|
752
|
+
// This catches: =/counter++, =/value--
|
|
736
753
|
if (/(\+\+|--)$/.test(expr)) {
|
|
737
754
|
return true;
|
|
738
755
|
}
|
|
739
756
|
|
|
740
|
-
// Check for infix with explicit whitespace:
|
|
757
|
+
// Check for infix with explicit whitespace: =/a + =/b
|
|
741
758
|
// The spaces make it unambiguous that the symbol is an operator, not part of a path
|
|
742
759
|
if (/\s+([+\-*/]|>|<|>=|<=|!=)\s+/.test(expr)) {
|
|
743
760
|
return true;
|
|
@@ -796,7 +813,7 @@ class PrattParser {
|
|
|
796
813
|
const prec = this.getInfixPrecedence(tok.value);
|
|
797
814
|
if (prec < minPrecedence) break;
|
|
798
815
|
|
|
799
|
-
// Check if it's a postfix operator
|
|
816
|
+
// Check if it's a postfix-only operator
|
|
800
817
|
if (operators.postfix.has(tok.value) && !operators.infix.has(tok.value)) {
|
|
801
818
|
this.consume();
|
|
802
819
|
left = { type: 'Postfix', operator: tok.value, operand: left };
|
|
@@ -814,6 +831,12 @@ class PrattParser {
|
|
|
814
831
|
continue;
|
|
815
832
|
}
|
|
816
833
|
|
|
834
|
+
// Operator not registered as postfix or infix - treat as unknown and stop
|
|
835
|
+
// This prevents infinite loops when operators are tokenized but not registered
|
|
836
|
+
if (!operators.postfix.has(tok.value) && !operators.infix.has(tok.value)) {
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
|
|
817
840
|
// Postfix that's also infix - context determines
|
|
818
841
|
// If next token is a value, treat as infix
|
|
819
842
|
this.consume();
|
|
@@ -871,6 +894,12 @@ class PrattParser {
|
|
|
871
894
|
return { type: 'Placeholder', value: tok.value };
|
|
872
895
|
}
|
|
873
896
|
|
|
897
|
+
// This
|
|
898
|
+
if (tok.type === TokenType.THIS) {
|
|
899
|
+
this.consume();
|
|
900
|
+
return { type: 'This', value: tok.value };
|
|
901
|
+
}
|
|
902
|
+
|
|
874
903
|
// Event
|
|
875
904
|
if (tok.type === TokenType.EVENT) {
|
|
876
905
|
this.consume();
|
|
@@ -927,8 +956,18 @@ const evaluateAST = (ast, context, forMutation = false) => {
|
|
|
927
956
|
});
|
|
928
957
|
}
|
|
929
958
|
|
|
959
|
+
case 'This': {
|
|
960
|
+
return new LazyValue((context) => {
|
|
961
|
+
const node = context?.__node__ || context;
|
|
962
|
+
if (ast.value === '$this') return node;
|
|
963
|
+
const path = ast.value.startsWith('$this.') ? ast.value.slice(6) : ast.value.slice(6);
|
|
964
|
+
return resolvePath(path, node);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
930
968
|
case 'Event': {
|
|
931
|
-
return new LazyValue((
|
|
969
|
+
return new LazyValue((context) => {
|
|
970
|
+
const event = context?.$event || context?.event || context;
|
|
932
971
|
if (ast.value === '$event') return event;
|
|
933
972
|
const path = ast.value.startsWith('$event.') ? ast.value.slice(7) : ast.value.slice(7);
|
|
934
973
|
return resolvePath(path, event);
|
|
@@ -985,7 +1024,18 @@ const evaluateAST = (ast, context, forMutation = false) => {
|
|
|
985
1024
|
// For infix, typically first arg might be pathAware
|
|
986
1025
|
const left = evaluateAST(ast.left, context, opts.pathAware);
|
|
987
1026
|
const right = evaluateAST(ast.right, context, false);
|
|
988
|
-
|
|
1027
|
+
|
|
1028
|
+
const finalArgs = [];
|
|
1029
|
+
|
|
1030
|
+
// Handle potentially exploded arguments (like in sum(/items...p))
|
|
1031
|
+
// Although infix operators usually take exactly 2 args, we treat them consistently
|
|
1032
|
+
if (Array.isArray(left) && ast.left.type === 'Explosion') finalArgs.push(...left);
|
|
1033
|
+
else finalArgs.push(unwrapSignal(left));
|
|
1034
|
+
|
|
1035
|
+
if (Array.isArray(right) && ast.right.type === 'Explosion') finalArgs.push(...right);
|
|
1036
|
+
else finalArgs.push(unwrapSignal(right));
|
|
1037
|
+
|
|
1038
|
+
return helper(...finalArgs);
|
|
989
1039
|
}
|
|
990
1040
|
|
|
991
1041
|
default:
|
|
@@ -1032,19 +1082,19 @@ export const resolveExpression = (expr, context) => {
|
|
|
1032
1082
|
const argsStr = expr.slice(funcStart + 1, -1);
|
|
1033
1083
|
|
|
1034
1084
|
const segments = fullPath.split('/');
|
|
1035
|
-
let funcName = segments.pop().replace(
|
|
1085
|
+
let funcName = segments.pop().replace(/^=/, '');
|
|
1036
1086
|
|
|
1037
|
-
// Handle case where path ends in / (like
|
|
1087
|
+
// Handle case where path ends in / (like =/ for division helper)
|
|
1038
1088
|
if (funcName === '' && (segments.length > 0 || fullPath === '/')) {
|
|
1039
1089
|
funcName = '/';
|
|
1040
1090
|
}
|
|
1041
1091
|
|
|
1042
1092
|
const navPath = segments.join('/');
|
|
1043
1093
|
|
|
1044
|
-
const isGlobalExpr = expr.startsWith('
|
|
1094
|
+
const isGlobalExpr = expr.startsWith('=/') || expr.startsWith('=');
|
|
1045
1095
|
|
|
1046
1096
|
let baseContext = context;
|
|
1047
|
-
if (navPath && navPath !== '
|
|
1097
|
+
if (navPath && navPath !== '=') {
|
|
1048
1098
|
baseContext = resolvePathAsContext(navPath, context);
|
|
1049
1099
|
}
|
|
1050
1100
|
|
|
@@ -1082,7 +1132,7 @@ export const resolveExpression = (expr, context) => {
|
|
|
1082
1132
|
let hasLazy = false;
|
|
1083
1133
|
for (let i = 0; i < argsList.length; i++) {
|
|
1084
1134
|
const arg = argsList[i];
|
|
1085
|
-
const useGlobalMode = isGlobalExpr && (navPath === '
|
|
1135
|
+
const useGlobalMode = isGlobalExpr && (navPath === '=' || !navPath);
|
|
1086
1136
|
const res = resolveArgument(arg, baseContext, useGlobalMode);
|
|
1087
1137
|
|
|
1088
1138
|
if (res.isLazy) hasLazy = true;
|
|
@@ -1117,7 +1167,7 @@ export const resolveExpression = (expr, context) => {
|
|
|
1117
1167
|
});
|
|
1118
1168
|
}
|
|
1119
1169
|
|
|
1120
|
-
const result = helper(
|
|
1170
|
+
const result = helper.apply(context?.__node__ || null, resolvedArgs);
|
|
1121
1171
|
return unwrapSignal(result);
|
|
1122
1172
|
}
|
|
1123
1173
|
|
|
@@ -1247,8 +1297,8 @@ export const parseCDOMC = (input) => {
|
|
|
1247
1297
|
|
|
1248
1298
|
const word = input.slice(start, i);
|
|
1249
1299
|
|
|
1250
|
-
// If word starts with
|
|
1251
|
-
if (word.startsWith('
|
|
1300
|
+
// If word starts with =, preserve it as a string for cDOM expression parsing
|
|
1301
|
+
if (word.startsWith('=')) {
|
|
1252
1302
|
return word;
|
|
1253
1303
|
}
|
|
1254
1304
|
|
|
@@ -1424,8 +1474,8 @@ export const parseJPRX = (input) => {
|
|
|
1424
1474
|
continue;
|
|
1425
1475
|
}
|
|
1426
1476
|
|
|
1427
|
-
// Handle JPRX expressions starting with
|
|
1428
|
-
if (char === '
|
|
1477
|
+
// Handle JPRX expressions starting with = (MUST come before word handler!)
|
|
1478
|
+
if (char === '=') {
|
|
1429
1479
|
let expr = '';
|
|
1430
1480
|
let parenDepth = 0;
|
|
1431
1481
|
let braceDepth = 0;
|
|
@@ -1463,7 +1513,7 @@ export const parseJPRX = (input) => {
|
|
|
1463
1513
|
}
|
|
1464
1514
|
|
|
1465
1515
|
// Handle unquoted property names, identifiers, and paths
|
|
1466
|
-
if (/[a-zA-Z_
|
|
1516
|
+
if (/[a-zA-Z_$/./]/.test(char)) {
|
|
1467
1517
|
let word = '';
|
|
1468
1518
|
while (i < len && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
|
|
1469
1519
|
word += input[i];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "Global Path Resolution",
|
|
4
|
+
"state": {
|
|
5
|
+
"userName": "Alice"
|
|
6
|
+
},
|
|
7
|
+
"expression": "=/userName",
|
|
8
|
+
"expected": "Alice"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "Deep Global Path Resolution",
|
|
12
|
+
"state": {
|
|
13
|
+
"user": {
|
|
14
|
+
"profile": {
|
|
15
|
+
"name": "Bob"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"expression": "=/user/profile/name",
|
|
20
|
+
"expected": "Bob"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "Relative Path Resolution",
|
|
24
|
+
"context": {
|
|
25
|
+
"age": 30
|
|
26
|
+
},
|
|
27
|
+
"expression": "./age",
|
|
28
|
+
"expected": 30
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "Math: Addition",
|
|
32
|
+
"state": {
|
|
33
|
+
"a": 5,
|
|
34
|
+
"b": 10
|
|
35
|
+
},
|
|
36
|
+
"expression": "=+(/a, /b)",
|
|
37
|
+
"expected": 15
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "Operator: Addition (Infix)",
|
|
41
|
+
"state": {
|
|
42
|
+
"a": 5,
|
|
43
|
+
"b": 10
|
|
44
|
+
},
|
|
45
|
+
"expression": "=/a + =/b",
|
|
46
|
+
"expected": 15
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "Logic: If (True)",
|
|
50
|
+
"state": {
|
|
51
|
+
"isVip": true
|
|
52
|
+
},
|
|
53
|
+
"expression": "=if(/isVip, \"Gold\", \"Silver\")",
|
|
54
|
+
"expected": "Gold"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "Explosion Operator",
|
|
58
|
+
"state": {
|
|
59
|
+
"items": [
|
|
60
|
+
{
|
|
61
|
+
"p": 10
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"p": 20
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"expression": "=sum(/items...p)",
|
|
69
|
+
"expected": 30
|
|
70
|
+
}
|
|
71
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "Math: Subtraction",
|
|
4
|
+
"expression": "=-(10, 3)",
|
|
5
|
+
"expected": 7
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "Math: Multiplication",
|
|
9
|
+
"expression": "=*(4, 5)",
|
|
10
|
+
"expected": 20
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "Math: Division",
|
|
14
|
+
"expression": "=/(20, 4)",
|
|
15
|
+
"expected": 5
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "Math: Round",
|
|
19
|
+
"expression": "=round(3.7)",
|
|
20
|
+
"expected": 4
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "String: Upper",
|
|
24
|
+
"expression": "=upper('hello')",
|
|
25
|
+
"expected": "HELLO"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "String: Lower",
|
|
29
|
+
"expression": "=lower('WORLD')",
|
|
30
|
+
"expected": "world"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "String: Concat",
|
|
34
|
+
"expression": "=concat('Hello', ' ', 'World')",
|
|
35
|
+
"expected": "Hello World"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "String: Trim",
|
|
39
|
+
"expression": "=trim(' spaced ')",
|
|
40
|
+
"expected": "spaced"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Logic: And (true)",
|
|
44
|
+
"expression": "=and(true, true)",
|
|
45
|
+
"expected": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Logic: And (false)",
|
|
49
|
+
"expression": "=and(true, false)",
|
|
50
|
+
"expected": false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "Logic: Or",
|
|
54
|
+
"expression": "=or(false, true)",
|
|
55
|
+
"expected": true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "Logic: Not",
|
|
59
|
+
"expression": "=not(false)",
|
|
60
|
+
"expected": true
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "Compare: Greater Than",
|
|
64
|
+
"expression": "=gt(10, 5)",
|
|
65
|
+
"expected": true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "Compare: Less Than",
|
|
69
|
+
"expression": "=lt(3, 7)",
|
|
70
|
+
"expected": true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "Compare: Equal",
|
|
74
|
+
"expression": "=eq(5, 5)",
|
|
75
|
+
"expected": true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "Conditional: If (true branch)",
|
|
79
|
+
"expression": "=if(true, 'Yes', 'No')",
|
|
80
|
+
"expected": "Yes"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "Conditional: If (false branch)",
|
|
84
|
+
"expression": "=if(false, 'Yes', 'No')",
|
|
85
|
+
"expected": "No"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "Stats: Average",
|
|
89
|
+
"expression": "=avg(10, 20, 30)",
|
|
90
|
+
"expected": 20
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "Stats: Min",
|
|
94
|
+
"expression": "=min(5, 2, 8)",
|
|
95
|
+
"expected": 2
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "Stats: Max",
|
|
99
|
+
"expression": "=max(5, 2, 8)",
|
|
100
|
+
"expected": 8
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "Array: Length",
|
|
104
|
+
"state": {
|
|
105
|
+
"items": [
|
|
106
|
+
"a",
|
|
107
|
+
"b",
|
|
108
|
+
"c"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"expression": "=len(/items)",
|
|
112
|
+
"expected": 3
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "Array: Join",
|
|
116
|
+
"state": {
|
|
117
|
+
"items": [
|
|
118
|
+
"a",
|
|
119
|
+
"b",
|
|
120
|
+
"c"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"expression": "=join(/items, '-')",
|
|
124
|
+
"expected": "a-b-c"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "Array: First",
|
|
128
|
+
"state": {
|
|
129
|
+
"items": [
|
|
130
|
+
"a",
|
|
131
|
+
"b",
|
|
132
|
+
"c"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"expression": "=first(/items)",
|
|
136
|
+
"expected": "a"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Array: Last",
|
|
140
|
+
"state": {
|
|
141
|
+
"items": [
|
|
142
|
+
"a",
|
|
143
|
+
"b",
|
|
144
|
+
"c"
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
"expression": "=last(/items)",
|
|
148
|
+
"expected": "c"
|
|
149
|
+
}
|
|
150
|
+
]
|