lightview 2.3.5 → 2.3.6
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/.gemini/XPATH_IMPLEMENTATION.md +190 -0
- package/_headers +5 -0
- package/_routes.json +7 -0
- package/build-bundles.mjs +10 -0
- package/docs/articles/calculator-no-javascript-hackernoon.md +283 -0
- package/docs/articles/calculator-no-javascript.md +290 -0
- package/docs/articles/part1-reference.md +236 -0
- package/docs/calculator.cdomc +77 -0
- package/docs/calculator.css +316 -0
- package/docs/calculator.html +5 -410
- package/functions/_middleware.js +20 -3
- package/jprx/helpers/dom.js +69 -0
- package/jprx/helpers/logic.js +9 -3
- package/jprx/index.js +1 -0
- package/jprx/package.json +1 -1
- package/jprx/parser.js +359 -80
- package/lightview-all.js +459 -72
- package/lightview-cdom.js +452 -69
- package/lightview-x.js +4 -3
- package/lightview.js +14 -0
- package/package.json +2 -2
- package/src/lightview-cdom.js +150 -7
- package/src/lightview-x.js +5 -2
- package/src/lightview.js +19 -0
- package/test-xpath-preprocess.js +18 -0
- package/test-xpath.html +63 -0
- package/test_error.txt +0 -0
- package/test_output.txt +0 -0
- package/test_output_full.txt +0 -0
- package/tests/cdom/operators.test.js +141 -0
- package/wrangler.toml +1 -0
- package/start-dev.js +0 -93
package/lightview-x.js
CHANGED
|
@@ -734,14 +734,15 @@
|
|
|
734
734
|
}
|
|
735
735
|
};
|
|
736
736
|
const parseElements = (content, isJson, isHtml, el, element, isCdom = false, ext = "") => {
|
|
737
|
-
var _a2;
|
|
738
737
|
if (isJson) return Array.isArray(content) ? content : [content];
|
|
739
738
|
if (isCdom && ext === "cdomc") {
|
|
740
|
-
const
|
|
739
|
+
const CDOM = globalThis.LightviewCDOM;
|
|
740
|
+
const parser = CDOM == null ? void 0 : CDOM.parseCDOMC;
|
|
741
741
|
if (parser) {
|
|
742
742
|
try {
|
|
743
743
|
const obj = parser(content);
|
|
744
|
-
|
|
744
|
+
const hydrated = CDOM.hydrate ? CDOM.hydrate(obj) : obj;
|
|
745
|
+
return Array.isArray(hydrated) ? hydrated : [hydrated];
|
|
745
746
|
} catch (e) {
|
|
746
747
|
console.warn("LightviewX: Failed to parse .cdomc:", e);
|
|
747
748
|
return [];
|
package/lightview.js
CHANGED
|
@@ -586,6 +586,11 @@
|
|
|
586
586
|
const makeReactiveAttributes = (attributes, domNode) => {
|
|
587
587
|
const reactiveAttrs = {};
|
|
588
588
|
for (let [key, value] of Object.entries(attributes)) {
|
|
589
|
+
if (value && typeof value === "object" && value.__xpath__ && value.__static__) {
|
|
590
|
+
domNode.setAttribute(`data-xpath-${key}`, value.__xpath__);
|
|
591
|
+
reactiveAttrs[key] = value;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
589
594
|
if (key === "onmount" || key === "onunmount") {
|
|
590
595
|
const state2 = getOrSet(nodeState, domNode, nodeStateFactory);
|
|
591
596
|
state2[key] = value;
|
|
@@ -639,6 +644,7 @@
|
|
|
639
644
|
return reactiveAttrs;
|
|
640
645
|
};
|
|
641
646
|
const processChildren = (children, targetNode, clearExisting = true) => {
|
|
647
|
+
var _a;
|
|
642
648
|
if (clearExisting && targetNode.innerHTML !== void 0) {
|
|
643
649
|
targetNode.innerHTML = "";
|
|
644
650
|
}
|
|
@@ -687,6 +693,11 @@
|
|
|
687
693
|
runner = effect(update);
|
|
688
694
|
trackEffect(startMarker, runner);
|
|
689
695
|
childElements.push(child);
|
|
696
|
+
} else if (child && typeof child === "object" && child.__xpath__ && child.__static__) {
|
|
697
|
+
const textNode = document.createTextNode("");
|
|
698
|
+
textNode.__xpathExpr = child.__xpath__;
|
|
699
|
+
targetNode.appendChild(textNode);
|
|
700
|
+
childElements.push(child);
|
|
690
701
|
} else if (["string", "number", "boolean", "symbol"].includes(type) || child && type === "object" && child instanceof String) {
|
|
691
702
|
targetNode.appendChild(document.createTextNode(child));
|
|
692
703
|
childElements.push(child);
|
|
@@ -706,6 +717,9 @@
|
|
|
706
717
|
childElements.push(childEl);
|
|
707
718
|
}
|
|
708
719
|
}
|
|
720
|
+
if (typeof ((_a = globalThis.LightviewCDOM) == null ? void 0 : _a.resolveStaticXPath) === "function") {
|
|
721
|
+
globalThis.LightviewCDOM.resolveStaticXPath(targetNode);
|
|
722
|
+
}
|
|
709
723
|
return childElements;
|
|
710
724
|
};
|
|
711
725
|
const setupChildrenInTarget = (children, targetNode) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightview",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.6",
|
|
4
4
|
"description": "A lightweight reactive UI library with features of Bau, Juris, and HTMX",
|
|
5
5
|
"main": "lightview.js",
|
|
6
6
|
"workspaces": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"expr-eval": "^2.0.2",
|
|
39
|
-
"jprx": "^1.
|
|
39
|
+
"jprx": "^1.3.0",
|
|
40
40
|
"linkedom": "^0.18.12",
|
|
41
41
|
"marked": "^17.0.1"
|
|
42
42
|
}
|
package/src/lightview-cdom.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerStatsHelpers } from '../jprx/helpers/stats.js';
|
|
|
17
17
|
import { registerStateHelpers, set } from '../jprx/helpers/state.js';
|
|
18
18
|
import { registerNetworkHelpers } from '../jprx/helpers/network.js';
|
|
19
19
|
import { registerCalcHelpers } from '../jprx/helpers/calc.js';
|
|
20
|
+
import { registerDOMHelpers } from '../jprx/helpers/dom.js';
|
|
20
21
|
|
|
21
22
|
import { signal, effect, getRegistry } from './reactivity/signal.js';
|
|
22
23
|
import { state } from './reactivity/state.js';
|
|
@@ -35,6 +36,7 @@ registerStatsHelpers(registerHelper);
|
|
|
35
36
|
registerStateHelpers((name, fn) => registerHelper(name, fn, { pathAware: true }));
|
|
36
37
|
registerNetworkHelpers(registerHelper);
|
|
37
38
|
registerCalcHelpers(registerHelper);
|
|
39
|
+
registerDOMHelpers(registerHelper);
|
|
38
40
|
registerHelper('move', (selector, location = 'beforeend') => {
|
|
39
41
|
return {
|
|
40
42
|
isLazy: true,
|
|
@@ -133,13 +135,14 @@ registerOperator('increment', '++', 'postfix', 80);
|
|
|
133
135
|
registerOperator('decrement', '--', 'prefix', 80);
|
|
134
136
|
registerOperator('decrement', '--', 'postfix', 80);
|
|
135
137
|
registerOperator('toggle', '!!', 'prefix', 80);
|
|
138
|
+
registerOperator('set', '=', 'infix', 20);
|
|
136
139
|
|
|
137
140
|
// Math infix operators (for expression syntax like $/a + $/b)
|
|
138
141
|
// These REQUIRE surrounding whitespace to avoid ambiguity with path separators (especially for /)
|
|
139
142
|
registerOperator('+', '+', 'infix', 50);
|
|
140
|
-
registerOperator('-', '-', 'infix', 50);
|
|
141
|
-
registerOperator('*', '*', 'infix', 60);
|
|
142
|
-
registerOperator('/', '/', 'infix', 60);
|
|
143
|
+
registerOperator('-', '-', 'infix', 50, { requiresWhitespace: true });
|
|
144
|
+
registerOperator('*', '*', 'infix', 60, { requiresWhitespace: true });
|
|
145
|
+
registerOperator('/', '/', 'infix', 60, { requiresWhitespace: true });
|
|
143
146
|
|
|
144
147
|
// Comparison infix operators
|
|
145
148
|
registerOperator('gt', '>', 'infix', 40);
|
|
@@ -147,13 +150,16 @@ registerOperator('lt', '<', 'infix', 40);
|
|
|
147
150
|
registerOperator('gte', '>=', 'infix', 40);
|
|
148
151
|
registerOperator('lte', '<=', 'infix', 40);
|
|
149
152
|
registerOperator('neq', '!=', 'infix', 40);
|
|
153
|
+
registerOperator('strictNeq', '!==', 'infix', 40);
|
|
154
|
+
registerOperator('eq', '==', 'infix', 40);
|
|
155
|
+
registerOperator('strictEq', '===', 'infix', 40);
|
|
150
156
|
|
|
151
157
|
const localStates = new WeakMap();
|
|
152
158
|
|
|
153
159
|
/**
|
|
154
160
|
* Builds a reactive context object for a node by chaining all ancestor states.
|
|
155
161
|
*/
|
|
156
|
-
|
|
162
|
+
const getContext = (node, event = null) => {
|
|
157
163
|
return new Proxy({}, {
|
|
158
164
|
get(_, prop) {
|
|
159
165
|
if (prop === '$event' || prop === 'event') return event;
|
|
@@ -214,7 +220,7 @@ globalThis.Lightview.hooks.processAttribute = (domNode, key, value) => {
|
|
|
214
220
|
/**
|
|
215
221
|
* Legacy activation no longer needed.
|
|
216
222
|
*/
|
|
217
|
-
|
|
223
|
+
const activate = (root = document.body) => { };
|
|
218
224
|
|
|
219
225
|
const makeEventHandler = (expr) => (eventOrNode) => {
|
|
220
226
|
const isEvent = eventOrNode && typeof eventOrNode === 'object' && 'target' in eventOrNode;
|
|
@@ -230,7 +236,7 @@ const makeEventHandler = (expr) => (eventOrNode) => {
|
|
|
230
236
|
* Traverses the object, converting expression strings (=...) into Signals/Computeds.
|
|
231
237
|
* Establishes a __parent__ link for relative path resolution.
|
|
232
238
|
*/
|
|
233
|
-
|
|
239
|
+
const hydrate = (node, parent = null) => {
|
|
234
240
|
if (!node) return node;
|
|
235
241
|
|
|
236
242
|
// 1. Handle Escape and Expressions
|
|
@@ -238,6 +244,14 @@ export const hydrate = (node, parent = null) => {
|
|
|
238
244
|
if (typeof node === 'string' && node.startsWith("'=")) {
|
|
239
245
|
return node.slice(1); // Strip the ' and return as literal
|
|
240
246
|
}
|
|
247
|
+
// Escape sequence: '# at start produces a literal string starting with #
|
|
248
|
+
if (typeof node === 'string' && node.startsWith("'#")) {
|
|
249
|
+
return node.slice(1); // Strip the ' and return as literal
|
|
250
|
+
}
|
|
251
|
+
// XPath expression: # at start marks for static resolution
|
|
252
|
+
if (typeof node === 'string' && node.startsWith('#')) {
|
|
253
|
+
return { __xpath__: node.slice(1), __static__: true };
|
|
254
|
+
}
|
|
241
255
|
if (typeof node === 'string' && node.startsWith('=')) {
|
|
242
256
|
return parseExpression(node, parent);
|
|
243
257
|
}
|
|
@@ -297,6 +311,12 @@ export const hydrate = (node, parent = null) => {
|
|
|
297
311
|
// Escape sequence: '= at start produces a literal string starting with =
|
|
298
312
|
if (typeof attrVal === 'string' && attrVal.startsWith("'=")) {
|
|
299
313
|
value[attrKey] = attrVal.slice(1);
|
|
314
|
+
// Escape sequence: '# at start produces a literal string starting with #
|
|
315
|
+
} else if (typeof attrVal === 'string' && attrVal.startsWith("'#")) {
|
|
316
|
+
value[attrKey] = attrVal.slice(1);
|
|
317
|
+
// XPath expression: # at start marks for static resolution
|
|
318
|
+
} else if (typeof attrVal === 'string' && attrVal.startsWith('#')) {
|
|
319
|
+
value[attrKey] = { __xpath__: attrVal.slice(1), __static__: true };
|
|
300
320
|
} else if (typeof attrVal === 'string' && attrVal.startsWith('=')) {
|
|
301
321
|
if (attrKey.startsWith('on')) {
|
|
302
322
|
value[attrKey] = makeEventHandler(attrVal);
|
|
@@ -313,6 +333,12 @@ export const hydrate = (node, parent = null) => {
|
|
|
313
333
|
// Escape sequence: '= at start produces a literal string starting with =
|
|
314
334
|
if (typeof value === 'string' && value.startsWith("'=")) {
|
|
315
335
|
node[key] = value.slice(1);
|
|
336
|
+
// Escape sequence: '# at start produces a literal string starting with #
|
|
337
|
+
} else if (typeof value === 'string' && value.startsWith("'#")) {
|
|
338
|
+
node[key] = value.slice(1);
|
|
339
|
+
// XPath expression: # at start marks for static resolution
|
|
340
|
+
} else if (typeof value === 'string' && value.startsWith('#')) {
|
|
341
|
+
node[key] = { __xpath__: value.slice(1), __static__: true };
|
|
316
342
|
} else if (typeof value === 'string' && value.startsWith('=')) {
|
|
317
343
|
if (key === 'onmount' || key === 'onunmount' || key.startsWith('on')) {
|
|
318
344
|
node[key] = makeEventHandler(value);
|
|
@@ -329,6 +355,103 @@ export const hydrate = (node, parent = null) => {
|
|
|
329
355
|
return node;
|
|
330
356
|
};
|
|
331
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Validates that an XPath expression only uses backward-looking axes.
|
|
360
|
+
* Throws an error if forward-looking axes are detected.
|
|
361
|
+
* @param {string} xpath - The XPath expression to validate
|
|
362
|
+
*/
|
|
363
|
+
const validateXPath = (xpath) => {
|
|
364
|
+
// Check for forbidden forward-looking axes
|
|
365
|
+
const forbiddenAxes = /\b(child|descendant|following|following-sibling)::/;
|
|
366
|
+
if (forbiddenAxes.test(xpath)) {
|
|
367
|
+
throw new Error(`XPath: Forward-looking axes not allowed during DOM construction: ${xpath}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Also check for shorthand forward references like /div (implies child axis)
|
|
371
|
+
// But allow / for document root like /html
|
|
372
|
+
const hasShorthandChild = /\/[a-zA-Z]/.test(xpath) && !xpath.startsWith('/html');
|
|
373
|
+
if (hasShorthandChild) {
|
|
374
|
+
throw new Error(`XPath: Shorthand child axis (/) not allowed during DOM construction: ${xpath}`);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Resolves static XPath expressions marked during hydration.
|
|
380
|
+
* This is called after the DOM tree is fully constructed.
|
|
381
|
+
* Walks the tree and resolves all __xpath__ markers.
|
|
382
|
+
* @param {Node} rootNode - The root DOM node to start walking from
|
|
383
|
+
*/
|
|
384
|
+
const resolveStaticXPath = (rootNode) => {
|
|
385
|
+
if (!rootNode || !rootNode.nodeType) return;
|
|
386
|
+
|
|
387
|
+
const walker = document.createTreeWalker(
|
|
388
|
+
rootNode,
|
|
389
|
+
NodeFilter.SHOW_ALL
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const nodesToProcess = [];
|
|
393
|
+
let node = walker.nextNode();
|
|
394
|
+
while (node) {
|
|
395
|
+
nodesToProcess.push(node);
|
|
396
|
+
node = walker.nextNode();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Process all nodes
|
|
400
|
+
for (const node of nodesToProcess) {
|
|
401
|
+
// Check for XPath markers in attributes
|
|
402
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
403
|
+
const attributes = [...node.attributes];
|
|
404
|
+
for (const attr of attributes) {
|
|
405
|
+
if (attr.name.startsWith('data-xpath-')) {
|
|
406
|
+
const realAttr = attr.name.replace('data-xpath-', '');
|
|
407
|
+
const xpath = attr.value;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
validateXPath(xpath);
|
|
411
|
+
const result = document.evaluate(
|
|
412
|
+
xpath,
|
|
413
|
+
node,
|
|
414
|
+
null,
|
|
415
|
+
XPathResult.STRING_TYPE,
|
|
416
|
+
null
|
|
417
|
+
);
|
|
418
|
+
node.setAttribute(realAttr, result.stringValue);
|
|
419
|
+
node.removeAttribute(attr.name);
|
|
420
|
+
} catch (e) {
|
|
421
|
+
globalThis.console?.error(`[Lightview-CDOM] XPath resolution failed for attribute "${realAttr}":`, e.message);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Check for XPath markers in text nodes
|
|
428
|
+
if (node.__xpathExpr) {
|
|
429
|
+
const xpath = node.__xpathExpr;
|
|
430
|
+
try {
|
|
431
|
+
validateXPath(xpath);
|
|
432
|
+
const result = document.evaluate(
|
|
433
|
+
xpath,
|
|
434
|
+
node, // Use text node as context, not its parent!
|
|
435
|
+
null,
|
|
436
|
+
XPathResult.STRING_TYPE,
|
|
437
|
+
null
|
|
438
|
+
);
|
|
439
|
+
node.textContent = result.stringValue;
|
|
440
|
+
delete node.__xpathExpr;
|
|
441
|
+
} catch (e) {
|
|
442
|
+
globalThis.console?.error(`[Lightview-CDOM] XPath resolution failed for text node:`, e.message);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
// Prevent tree-shaking of parser functions by creating a side-effect
|
|
450
|
+
// These are used externally by lightview-x.js for .cdomc file loading
|
|
451
|
+
// The typeof check creates a runtime branch the bundler can't eliminate
|
|
452
|
+
if (typeof parseCDOMC !== 'function') throw new Error('parseCDOMC not found');
|
|
453
|
+
if (typeof parseJPRX !== 'function') throw new Error('parseJPRX not found');
|
|
454
|
+
|
|
332
455
|
const LightviewCDOM = {
|
|
333
456
|
registerHelper,
|
|
334
457
|
registerOperator,
|
|
@@ -344,12 +467,32 @@ const LightviewCDOM = {
|
|
|
344
467
|
handleCDOMBind: () => { },
|
|
345
468
|
activate,
|
|
346
469
|
hydrate,
|
|
470
|
+
resolveStaticXPath,
|
|
347
471
|
version: '1.0.0'
|
|
348
472
|
};
|
|
349
473
|
|
|
350
474
|
// Global export for non-module usage
|
|
351
475
|
if (typeof window !== 'undefined') {
|
|
352
|
-
globalThis.LightviewCDOM =
|
|
476
|
+
globalThis.LightviewCDOM = {};
|
|
477
|
+
Object.assign(globalThis.LightviewCDOM, LightviewCDOM);
|
|
353
478
|
}
|
|
354
479
|
|
|
480
|
+
|
|
481
|
+
export {
|
|
482
|
+
registerHelper,
|
|
483
|
+
registerOperator,
|
|
484
|
+
parseExpression,
|
|
485
|
+
resolvePath,
|
|
486
|
+
resolvePathAsContext,
|
|
487
|
+
resolveExpression,
|
|
488
|
+
parseCDOMC,
|
|
489
|
+
parseJPRX,
|
|
490
|
+
unwrapSignal,
|
|
491
|
+
BindingTarget,
|
|
492
|
+
getContext,
|
|
493
|
+
activate,
|
|
494
|
+
hydrate,
|
|
495
|
+
resolveStaticXPath
|
|
496
|
+
};
|
|
497
|
+
|
|
355
498
|
export default LightviewCDOM;
|
package/src/lightview-x.js
CHANGED
|
@@ -587,11 +587,14 @@ const fetchContent = async (src) => {
|
|
|
587
587
|
const parseElements = (content, isJson, isHtml, el, element, isCdom = false, ext = '') => {
|
|
588
588
|
if (isJson) return Array.isArray(content) ? content : [content];
|
|
589
589
|
if (isCdom && ext === 'cdomc') {
|
|
590
|
-
const
|
|
590
|
+
const CDOM = globalThis.LightviewCDOM;
|
|
591
|
+
const parser = CDOM?.parseCDOMC;
|
|
591
592
|
if (parser) {
|
|
592
593
|
try {
|
|
593
594
|
const obj = parser(content);
|
|
594
|
-
|
|
595
|
+
// Hydrate the parsed object to convert expression strings to reactive signals
|
|
596
|
+
const hydrated = CDOM.hydrate ? CDOM.hydrate(obj) : obj;
|
|
597
|
+
return Array.isArray(hydrated) ? hydrated : [hydrated];
|
|
595
598
|
} catch (e) {
|
|
596
599
|
console.warn('LightviewX: Failed to parse .cdomc:', e);
|
|
597
600
|
return [];
|
package/src/lightview.js
CHANGED
|
@@ -339,6 +339,14 @@ const makeReactiveAttributes = (attributes, domNode) => {
|
|
|
339
339
|
const reactiveAttrs = {};
|
|
340
340
|
|
|
341
341
|
for (let [key, value] of Object.entries(attributes)) {
|
|
342
|
+
// Handle XPath markers from hydration
|
|
343
|
+
if (value && typeof value === 'object' && value.__xpath__ && value.__static__) {
|
|
344
|
+
// Mark attribute for later XPath resolution
|
|
345
|
+
domNode.setAttribute(`data-xpath-${key}`, value.__xpath__);
|
|
346
|
+
reactiveAttrs[key] = value;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
342
350
|
if (key === 'onmount' || key === 'onunmount') {
|
|
343
351
|
const state = getOrSet(nodeState, domNode, nodeStateFactory);
|
|
344
352
|
state[key] = value;
|
|
@@ -483,6 +491,12 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
483
491
|
runner = effect(update);
|
|
484
492
|
trackEffect(startMarker, runner);
|
|
485
493
|
childElements.push(child);
|
|
494
|
+
} else if (child && typeof child === 'object' && child.__xpath__ && child.__static__) {
|
|
495
|
+
// XPath marker - create text node with marker for later resolution
|
|
496
|
+
const textNode = document.createTextNode('');
|
|
497
|
+
textNode.__xpathExpr = child.__xpath__;
|
|
498
|
+
targetNode.appendChild(textNode);
|
|
499
|
+
childElements.push(child);
|
|
486
500
|
} else if (['string', 'number', 'boolean', 'symbol'].includes(type) || (child && type === 'object' && child instanceof String)) {
|
|
487
501
|
// Static text
|
|
488
502
|
targetNode.appendChild(document.createTextNode(child));
|
|
@@ -506,6 +520,11 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
506
520
|
}
|
|
507
521
|
}
|
|
508
522
|
|
|
523
|
+
// Resolve static XPath expressions after DOM tree is constructed
|
|
524
|
+
if (typeof globalThis.LightviewCDOM?.resolveStaticXPath === 'function') {
|
|
525
|
+
globalThis.LightviewCDOM.resolveStaticXPath(targetNode);
|
|
526
|
+
}
|
|
527
|
+
|
|
509
528
|
return childElements;
|
|
510
529
|
};
|
|
511
530
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Test preprocessXPath function
|
|
2
|
+
import { parseCDOMC } from './jprx/parser.js';
|
|
3
|
+
|
|
4
|
+
const testInput = `{
|
|
5
|
+
button: {
|
|
6
|
+
id: "7",
|
|
7
|
+
children: [#../@id]
|
|
8
|
+
}
|
|
9
|
+
}`;
|
|
10
|
+
|
|
11
|
+
console.log('Test input:', testInput);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = parseCDOMC(testInput);
|
|
15
|
+
console.log('Parsed successfully:', result);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error('Parse failed:', e.message);
|
|
18
|
+
}
|
package/test-xpath.html
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>XPath Test</title>
|
|
8
|
+
<script type="module">
|
|
9
|
+
import { parseCDOMC, hydrate } from './src/lightview-cdom.js';
|
|
10
|
+
|
|
11
|
+
// Test 1: Static XPath in attributes
|
|
12
|
+
const cdomcInput = `{
|
|
13
|
+
tag: div,
|
|
14
|
+
attributes: {
|
|
15
|
+
id: parent-div,
|
|
16
|
+
data-theme: dark
|
|
17
|
+
},
|
|
18
|
+
children: [{
|
|
19
|
+
tag: button,
|
|
20
|
+
attributes: {
|
|
21
|
+
data-parent-id: #../@id,
|
|
22
|
+
data-parent-theme: #../@data-theme
|
|
23
|
+
},
|
|
24
|
+
children: ["Button with parent data"]
|
|
25
|
+
}]
|
|
26
|
+
}`;
|
|
27
|
+
|
|
28
|
+
console.log('Parsing cDOMC input...');
|
|
29
|
+
const parsed = parseCDOMC(cdomcInput);
|
|
30
|
+
console.log('Parsed:', parsed);
|
|
31
|
+
|
|
32
|
+
console.log('Hydrating...');
|
|
33
|
+
const hydrated = hydrate(parsed);
|
|
34
|
+
console.log('Hydrated:', hydrated);
|
|
35
|
+
|
|
36
|
+
// Mount to DOM
|
|
37
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
38
|
+
const container = document.getElementById('app');
|
|
39
|
+
if (hydrated && hydrated.tag) {
|
|
40
|
+
const el = globalThis.Lightview.tags[hydrated.tag](
|
|
41
|
+
hydrated.attributes || {},
|
|
42
|
+
hydrated.children || []
|
|
43
|
+
);
|
|
44
|
+
container.appendChild(el.domEl);
|
|
45
|
+
console.log('Mounted to DOM');
|
|
46
|
+
console.log('Button element:', el.domEl.querySelector('button'));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
</head>
|
|
51
|
+
|
|
52
|
+
<body>
|
|
53
|
+
<h1>XPath Test</h1>
|
|
54
|
+
<div id="app"></div>
|
|
55
|
+
|
|
56
|
+
<h2>Test Cases:</h2>
|
|
57
|
+
<ul>
|
|
58
|
+
<li>Button should have <code>data-parent-id="parent-div"</code></li>
|
|
59
|
+
<li>Button should have <code>data-parent-theme="dark"</code></li>
|
|
60
|
+
</ul>
|
|
61
|
+
</body>
|
|
62
|
+
|
|
63
|
+
</html>
|
package/test_error.txt
ADDED
|
Binary file
|
package/test_output.txt
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import Lightview from '../../src/lightview.js';
|
|
3
|
+
import LightviewX from '../../src/lightview-x.js';
|
|
4
|
+
import LightviewCDOM from '../../src/lightview-cdom.js';
|
|
5
|
+
import { resolveExpression, unwrapSignal } from '../../jprx/parser.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Operator Tests
|
|
9
|
+
*
|
|
10
|
+
* Tests all registered operators using property-based context.
|
|
11
|
+
*/
|
|
12
|
+
describe('JPRX Operators', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
globalThis.window = globalThis;
|
|
15
|
+
globalThis.Lightview = Lightview;
|
|
16
|
+
globalThis.LightviewX = LightviewX;
|
|
17
|
+
globalThis.LightviewCDOM = LightviewCDOM;
|
|
18
|
+
Lightview.registry.clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('Context-based Evaluation', () => {
|
|
22
|
+
it('resolves simple properties from context', () => {
|
|
23
|
+
const context = { a: 10, b: 20 };
|
|
24
|
+
expect(resolveExpression('a', context)).toBe(10);
|
|
25
|
+
expect(resolveExpression('b', context)).toBe(20);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('performs math with context variables', () => {
|
|
29
|
+
const context = { a: 10, b: 5 };
|
|
30
|
+
expect(resolveExpression('a + b', context)).toBe(15);
|
|
31
|
+
expect(resolveExpression('a - b', context)).toBe(5);
|
|
32
|
+
expect(resolveExpression('a * b', context)).toBe(50);
|
|
33
|
+
expect(resolveExpression('a / b', context)).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles comparison operators', () => {
|
|
37
|
+
const context = { a: 10, b: 5, c: 10 };
|
|
38
|
+
expect(resolveExpression('a > b', context)).toBe(true);
|
|
39
|
+
expect(resolveExpression('b > a', context)).toBe(false);
|
|
40
|
+
expect(resolveExpression('a < b', context)).toBe(false);
|
|
41
|
+
expect(resolveExpression('b < a', context)).toBe(true);
|
|
42
|
+
expect(resolveExpression('a >= c', context)).toBe(true);
|
|
43
|
+
expect(resolveExpression('a <= c', context)).toBe(true);
|
|
44
|
+
expect(resolveExpression('a == c', context)).toBe(true);
|
|
45
|
+
expect(resolveExpression('a === c', context)).toBe(true);
|
|
46
|
+
expect(resolveExpression('a != b', context)).toBe(true);
|
|
47
|
+
expect(resolveExpression('a == b', context)).toBe(false);
|
|
48
|
+
expect(resolveExpression('a === b', context)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('handles equality with different types', () => {
|
|
52
|
+
const context = { a: 5, b: '5' };
|
|
53
|
+
expect(resolveExpression('a == b', context)).toBe(true);
|
|
54
|
+
expect(resolveExpression('a === b', context)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Mutation Operators (Context-based)', () => {
|
|
59
|
+
it('increments a property: ++count', () => {
|
|
60
|
+
const context = { count: 5 };
|
|
61
|
+
const result = resolveExpression('++count', context);
|
|
62
|
+
expect(result).toBe(6);
|
|
63
|
+
expect(context.count).toBe(6);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('decrements a property: count--', () => {
|
|
67
|
+
const context = { count: 10 };
|
|
68
|
+
const result = resolveExpression('count--', context);
|
|
69
|
+
expect(result).toBe(9);
|
|
70
|
+
expect(context.count).toBe(9);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('toggles a property: !!flag', () => {
|
|
74
|
+
const context = { flag: false };
|
|
75
|
+
const result = resolveExpression('!!flag', context);
|
|
76
|
+
expect(result).toBe(true);
|
|
77
|
+
expect(context.flag).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('Assignment Operator', () => {
|
|
82
|
+
it('assigns value to context property: x = 42', () => {
|
|
83
|
+
const context = { x: 0 };
|
|
84
|
+
const result = resolveExpression('x = 42', context);
|
|
85
|
+
expect(result).toBe(42);
|
|
86
|
+
expect(context.x).toBe(42);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('assigns complex values: x = a + b', () => {
|
|
90
|
+
const context = { x: 0, a: 10, b: 20 };
|
|
91
|
+
// Note: complex right-hand side currently requires helper syntax or better Pratt integration
|
|
92
|
+
// But with current parser, it should work if we use precedence correctly
|
|
93
|
+
const result = resolveExpression('x = a + b', context);
|
|
94
|
+
expect(result).toBe(30);
|
|
95
|
+
expect(context.x).toBe(30);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('assigns object literals: user = { name: "Alice" }', () => {
|
|
99
|
+
const context = { user: null };
|
|
100
|
+
resolveExpression('user = { name: "Alice" }', context);
|
|
101
|
+
expect(context.user).toEqual({ name: 'Alice' });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Whitespace and Ambiguity', () => {
|
|
106
|
+
it('handles packed assignment: count=0', () => {
|
|
107
|
+
const context = { count: 5 };
|
|
108
|
+
resolveExpression('count=0', context);
|
|
109
|
+
expect(context.count).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles packed addition: a+b', () => {
|
|
113
|
+
const context = { a: 1, b: 2 };
|
|
114
|
+
expect(resolveExpression('a+b', context)).toBe(3);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('requires whitespace for division: a / b', () => {
|
|
118
|
+
const context = { a: 10, b: 2, 'a/b': 99 };
|
|
119
|
+
// Correct division
|
|
120
|
+
expect(resolveExpression('a / b', context)).toBe(5);
|
|
121
|
+
// Path resolution (no spaces)
|
|
122
|
+
expect(resolveExpression('a/b', context)).toBe(99);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('requires whitespace for subtraction: a - b', () => {
|
|
126
|
+
const context = { a: 10, b: 2, 'a-b': 42 };
|
|
127
|
+
// Subtraction
|
|
128
|
+
expect(resolveExpression('a - b', context)).toBe(8);
|
|
129
|
+
// Kebab-case path
|
|
130
|
+
expect(resolveExpression('a-b', context)).toBe(42);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('Registry Integration (Global Paths)', () => {
|
|
135
|
+
it('works with global paths: =/global/x + y', () => {
|
|
136
|
+
LightviewX.state({ x: 100 }, 'global');
|
|
137
|
+
const context = { y: 50 };
|
|
138
|
+
expect(resolveExpression('=/global/x + y', context)).toBe(150);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
package/wrangler.toml
CHANGED