lightview 2.4.4 → 2.5.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/AI-GUIDANCE.md +28 -11
- package/README.md +4 -4
- package/docs/assets/styles/site.css +16 -7
- package/docs/benchmarks/bau-tagged-fragment.js +41 -0
- package/docs/cdom-xpath.html +27 -9
- package/docs/cdom.html +186 -125
- package/docs/dom-benchmark.html +103 -4
- package/docs/getting-started/index.html +40 -0
- package/docs/index.html +40 -31
- package/jprx/README.md +15 -13
- package/jprx/package.json +1 -1
- package/jprx/parser.js +85 -1
- package/lightview-all.js +131 -34
- package/lightview-cdom.js +131 -34
- package/package.json +3 -3
- package/scratch.html +83 -0
- package/src/lightview-cdom.js +86 -44
package/docs/dom-benchmark.html
CHANGED
|
@@ -161,9 +161,14 @@
|
|
|
161
161
|
class="btn btn-outline btn-benchmark border-slate-300">Run Raw oDOM</button>
|
|
162
162
|
<button onclick="runBenchmark('vDOM')"
|
|
163
163
|
class="btn btn-outline btn-benchmark border-slate-300">Run Lightview vDOM</button>
|
|
164
|
+
<button onclick="runBenchmark('JurisODOM')"
|
|
165
|
+
class="btn btn-outline btn-benchmark border-slate-300">Run JurisJS oDOM</button>
|
|
164
166
|
<button onclick="runBenchmark('TaggedScript')"
|
|
165
167
|
class="btn btn-outline btn-benchmark border-slate-300">Run Lightview Tagged
|
|
166
168
|
Script</button>
|
|
169
|
+
<button onclick="runBenchmark('BauTaggedScript')"
|
|
170
|
+
class="btn btn-outline btn-benchmark border-slate-300">Run Bau Tagged
|
|
171
|
+
Script</button>
|
|
167
172
|
<div class="divider"></div>
|
|
168
173
|
<button onclick="runAll()" class="btn btn-primary text-white">Run All Benchmarks</button>
|
|
169
174
|
<button onclick="clearTarget()" class="btn btn-ghost">Clear Target</button>
|
|
@@ -303,6 +308,31 @@
|
|
|
303
308
|
</div>
|
|
304
309
|
</div>
|
|
305
310
|
|
|
311
|
+
<div class="result-card" id="res-JurisODOM">
|
|
312
|
+
<div class="flex justify-between items-start mb-2">
|
|
313
|
+
<h3 class="font-bold text-slate-500 uppercase text-xs tracking-widest">JurisJS oDOM</h3>
|
|
314
|
+
<div class="flex flex-col items-end gap-1">
|
|
315
|
+
<div class="badge badge-outline badge-xs opacity-50"><span class="size">--</span> KB
|
|
316
|
+
</div>
|
|
317
|
+
<div class="badge badge-accent badge-xs text-white opacity-70"><span
|
|
318
|
+
class="gzip-size">--</span> KB gzip</div>
|
|
319
|
+
<div class="badge badge-ghost badge-sm opacity-50">ms</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="text-3xl font-black text-slate-800 mb-4"><span class="time">--</span></div>
|
|
323
|
+
|
|
324
|
+
<div class="grid grid-cols-4 gap-2 text-[10px] uppercase font-bold text-slate-400 mb-3">
|
|
325
|
+
<div>Min <span class="min block text-slate-600">--</span></div>
|
|
326
|
+
<div>Avg <span class="avg block text-slate-600">--</span></div>
|
|
327
|
+
<div>Max <span class="max block text-slate-600">--</span></div>
|
|
328
|
+
<div class="text-right">±SD <span class="std block text-slate-600">--</span></div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="progress-bar-custom">
|
|
332
|
+
<div class="progress-fill" id="fill-JurisODOM"></div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
306
336
|
<div class="result-card" id="res-TaggedScript">
|
|
307
337
|
<div class="flex justify-between items-start mb-2">
|
|
308
338
|
<h3 class="font-bold text-slate-500 uppercase text-xs tracking-widest">Lightview Tagged
|
|
@@ -328,6 +358,32 @@
|
|
|
328
358
|
<div class="progress-fill" id="fill-TaggedScript"></div>
|
|
329
359
|
</div>
|
|
330
360
|
</div>
|
|
361
|
+
|
|
362
|
+
<div class="result-card" id="res-BauTaggedScript">
|
|
363
|
+
<div class="flex justify-between items-start mb-2">
|
|
364
|
+
<h3 class="font-bold text-slate-500 uppercase text-xs tracking-widest">Bau Tagged
|
|
365
|
+
Script</h3>
|
|
366
|
+
<div class="flex flex-col items-end gap-1">
|
|
367
|
+
<div class="badge badge-outline badge-xs opacity-50"><span class="size">--</span> KB
|
|
368
|
+
</div>
|
|
369
|
+
<div class="badge badge-accent badge-xs text-white opacity-70"><span
|
|
370
|
+
class="gzip-size">--</span> KB gzip</div>
|
|
371
|
+
<div class="badge badge-ghost badge-sm opacity-50">ms</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="text-3xl font-black text-slate-800 mb-4"><span class="time">--</span></div>
|
|
375
|
+
|
|
376
|
+
<div class="grid grid-cols-4 gap-2 text-[10px] uppercase font-bold text-slate-400 mb-3">
|
|
377
|
+
<div>Min <span class="min block text-slate-600">--</span></div>
|
|
378
|
+
<div>Avg <span class="avg block text-slate-600">--</span></div>
|
|
379
|
+
<div>Max <span class="max block text-slate-600">--</span></div>
|
|
380
|
+
<div class="text-right">±SD <span class="std block text-slate-600">--</span></div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div class="progress-bar-custom">
|
|
384
|
+
<div class="progress-fill" id="fill-BauTaggedScript"></div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
331
387
|
</div>
|
|
332
388
|
|
|
333
389
|
<div class="card bg-white shadow-sm border border-slate-200">
|
|
@@ -347,6 +403,7 @@
|
|
|
347
403
|
<!-- Core Libraries -->
|
|
348
404
|
<script src="/lightview.js"></script>
|
|
349
405
|
<script src="/lightview-x.js"></script>
|
|
406
|
+
<script src="https://cdn.jsdelivr.net/npm/juris@0.9.0/juris.min.js"></script>
|
|
350
407
|
|
|
351
408
|
<script>
|
|
352
409
|
function generateHTML(count) {
|
|
@@ -468,6 +525,27 @@
|
|
|
468
525
|
return el;
|
|
469
526
|
}
|
|
470
527
|
|
|
528
|
+
function odomToDOM_Juris(onode) {
|
|
529
|
+
if (typeof onode !== 'object' || onode === null) {
|
|
530
|
+
return document.createTextNode(onode);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Create a temporary container for Juris to render into
|
|
534
|
+
const tempContainer = document.createElement('div');
|
|
535
|
+
|
|
536
|
+
// Create a Juris instance with the oDOM structure as the layout
|
|
537
|
+
const jurisInstance = new Juris({
|
|
538
|
+
debug: false, // Disable logging for performance
|
|
539
|
+
layout: onode
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Render to the temporary container
|
|
543
|
+
jurisInstance.render(tempContainer);
|
|
544
|
+
// Return the first child (the actual rendered content)
|
|
545
|
+
return tempContainer.firstChild || tempContainer;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
471
549
|
async function getGzipSize(str) {
|
|
472
550
|
try {
|
|
473
551
|
const stream = new Blob([str]).stream();
|
|
@@ -518,7 +596,7 @@
|
|
|
518
596
|
function clearTarget() {
|
|
519
597
|
target.replaceChildren();
|
|
520
598
|
target.innerHTML = '<div class="p-8 text-center text-slate-400 italic">Target is empty.</div>';
|
|
521
|
-
['innerHTML', 'DOMParser', 'vDOM', 'RawVDOM', 'RawoDOM', 'TaggedScript'].forEach(id => {
|
|
599
|
+
['innerHTML', 'DOMParser', 'vDOM', 'RawVDOM', 'RawoDOM', 'JurisODOM', 'TaggedScript', 'BauTaggedScript'].forEach(id => {
|
|
522
600
|
const card = document.getElementById(`res-${id}`);
|
|
523
601
|
card.classList.remove('active');
|
|
524
602
|
card.querySelector('.time').innerText = '--';
|
|
@@ -568,6 +646,12 @@
|
|
|
568
646
|
const domEl = odomToDOM(json);
|
|
569
647
|
target.replaceChildren(domEl);
|
|
570
648
|
time = performance.now() - start;
|
|
649
|
+
} else if (type === 'JurisODOM') {
|
|
650
|
+
const start = performance.now();
|
|
651
|
+
const json = JSON.parse(data);
|
|
652
|
+
const domEl = odomToDOM_Juris(json);
|
|
653
|
+
target.replaceChildren(domEl);
|
|
654
|
+
time = performance.now() - start;
|
|
571
655
|
} else if (type === 'TaggedScript') {
|
|
572
656
|
const existing = document.getElementById('tagged-script-tag');
|
|
573
657
|
if (existing) existing.remove();
|
|
@@ -580,6 +664,19 @@
|
|
|
580
664
|
document.body.appendChild(script);
|
|
581
665
|
});
|
|
582
666
|
time = window.__taggedBenchmarkTime;
|
|
667
|
+
} else if (type === 'BauTaggedScript') {
|
|
668
|
+
const existing = document.getElementById('bau-tagged-script-tag');
|
|
669
|
+
if (existing) existing.remove();
|
|
670
|
+
|
|
671
|
+
await new Promise(resolve => {
|
|
672
|
+
window.__resolveBauTaggedBenchmark = resolve;
|
|
673
|
+
const script = document.createElement('script');
|
|
674
|
+
script.type = 'module';
|
|
675
|
+
script.id = 'bau-tagged-script-tag';
|
|
676
|
+
script.src = `./benchmarks/bau-tagged-fragment.js?count=${count}&t=${Date.now()}`;
|
|
677
|
+
document.body.appendChild(script);
|
|
678
|
+
});
|
|
679
|
+
time = window.__bauTaggedBenchmarkTime;
|
|
583
680
|
}
|
|
584
681
|
return time;
|
|
585
682
|
}
|
|
@@ -592,10 +689,10 @@
|
|
|
592
689
|
let data = preGeneratedData;
|
|
593
690
|
|
|
594
691
|
// 1. Preparation phase (outside of timing)
|
|
595
|
-
if (!data && type !== 'TaggedScript') {
|
|
692
|
+
if (!data && type !== 'TaggedScript' && type !== 'BauTaggedScript') {
|
|
596
693
|
if (type === 'innerHTML' || type === 'DOMParser') data = generateHTML(count);
|
|
597
694
|
if (type === 'vDOM' || type === 'RawVDOM') data = generateVDOM(count);
|
|
598
|
-
if (type === 'RawoDOM') data = generateODOM(count);
|
|
695
|
+
if (type === 'RawoDOM' || type === 'JurisODOM') data = generateODOM(count);
|
|
599
696
|
}
|
|
600
697
|
|
|
601
698
|
console.log(`Starting ${cycles} cycles for ${type}...`);
|
|
@@ -628,7 +725,9 @@
|
|
|
628
725
|
{ id: 'RawVDOM', data: vdomData },
|
|
629
726
|
{ id: 'RawoDOM', data: odomData },
|
|
630
727
|
{ id: 'vDOM', data: vdomData },
|
|
631
|
-
{ id: '
|
|
728
|
+
{ id: 'JurisODOM', data: odomData },
|
|
729
|
+
{ id: 'TaggedScript', data: null },
|
|
730
|
+
{ id: 'BauTaggedScript', data: null }
|
|
632
731
|
];
|
|
633
732
|
|
|
634
733
|
for (const config of configs) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<button class="btn btn-secondary step-btn" onclick="switchStep(4)">4. Hypermedia</button>
|
|
16
16
|
<button class="btn btn-secondary step-btn" onclick="switchStep(5)">5. Components</button>
|
|
17
17
|
<button class="btn btn-secondary step-btn" onclick="switchStep(6)">6. Gating</button>
|
|
18
|
+
<button class="btn btn-secondary step-btn" onclick="switchStep(7)">7. cDOM</button>
|
|
18
19
|
</div>
|
|
19
20
|
|
|
20
21
|
<!-- Tutorial Layout: Preview on top, Code & Concepts below -->
|
|
@@ -679,6 +680,45 @@ $('#app').content(App);`,
|
|
|
679
680
|
<li style="margin-bottom: 0.75rem;"><strong>Write your own</strong> — Write your own functions and add the to <code>globalThis</code> to make them available to all templates. They have access to <code>this</code> (the element) and can accept arguments like <code>event</code>.</li>
|
|
680
681
|
</ul>
|
|
681
682
|
<p><strong>Try it:</strong> Click the Spam Me button rapidly, or type quickly into the search box to see the limiters in action!</p>`
|
|
683
|
+
},
|
|
684
|
+
7: {
|
|
685
|
+
options: {
|
|
686
|
+
autoRun: true
|
|
687
|
+
},
|
|
688
|
+
code: `// STEP 7: cDOM - Compressed DOM & JPRX
|
|
689
|
+
await import('/lightview-cdom.js');
|
|
690
|
+
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
691
|
+
const { $ } = Lightview;
|
|
692
|
+
|
|
693
|
+
const cdomString = \`{
|
|
694
|
+
div: {
|
|
695
|
+
onmount: =signal(0,"count"),
|
|
696
|
+
children: [
|
|
697
|
+
{ h3: ["Standard JPRX Counter"] },
|
|
698
|
+
{ p: { children: ["Count: ", =/count] }},
|
|
699
|
+
{ div: { children: [
|
|
700
|
+
{ button: { onclick: =decrement(/count), children: ["-"] } },
|
|
701
|
+
{ button: { onclick: =/count++, children: ["+"] } }
|
|
702
|
+
]}}
|
|
703
|
+
]
|
|
704
|
+
}
|
|
705
|
+
}\`;
|
|
706
|
+
|
|
707
|
+
const hydrated = hydrate(parseJPRX(cdomString));
|
|
708
|
+
$('#app').content(hydrated);`,
|
|
709
|
+
concepts: `
|
|
710
|
+
<h3 style="margin-top: 0; color: var(--site-primary);">Step 7: cDOM & JPRX</h3>
|
|
711
|
+
<p><strong><a href="/docs/cdom.html">cDOM</a></strong> (compressed DOM) combined with <strong><a href="/docs/cdom.html#JPRX">JPRX</a></strong> (JSON Path Reactive eXpressions) allows you to define entire reactive UIs as pure JSON-compatible strings.</p>
|
|
712
|
+
|
|
713
|
+
<h4>Key Concepts:</h4>
|
|
714
|
+
<ul style="padding-left: 1.25rem; color: var(--site-text-secondary);">
|
|
715
|
+
<li style="margin-bottom: 0.75rem;"><strong>Portability</strong> — Since the UI is a string, it can be easily stored in databases, sent over the wire, or generated by an LLM without security risks of <code>eval()</code>.</li>
|
|
716
|
+
<li style="margin-bottom: 0.75rem;"><strong>JPRX Expressions</strong> — Use <code>=</code> to denote reactive expressions within the string. For example, <code>=/count</code> binds to a signal.</li>
|
|
717
|
+
<li style="margin-bottom: 0.75rem;"><strong>Flexible Syntax</strong> — Operators and helpers can be called as functions (<code>=decrement(/count)</code>) or using prefix (<code>=--/count</code>), infix (<code>=/count - 1</code>), or postfix (<code>=/count--</code>) notation.</li>
|
|
718
|
+
<li style="margin-bottom: 0.75rem;"><strong>Hydration</strong> — <code>hydrate()</code> turns the parsed JPRX into a live, reactive DOM tree.</li>
|
|
719
|
+
</ul>
|
|
720
|
+
<p><strong>Try it:</strong> Edit the <code>cdomString</code> to change the labels or add new elements!</p>
|
|
721
|
+
`
|
|
682
722
|
}
|
|
683
723
|
};
|
|
684
724
|
|
package/docs/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
More power. Less energy.
|
|
12
12
|
<br>
|
|
13
13
|
<a id="link-hero-ai-safe" href="#ai-safe"
|
|
14
|
-
style="color: var(--site-primary); font-size: 1.25rem; font-weight: 700; text-decoration: none; display: inline-block; margin-top: 0.
|
|
14
|
+
style="color: var(--site-primary); font-size: 1.25rem; font-weight: 700; text-decoration: none; display: inline-block; margin-top: 0.25rem; border-bottom: 2px solid var(--site-primary); padding-bottom: 2px;">AI
|
|
15
15
|
Safe</a>
|
|
16
16
|
</p>
|
|
17
17
|
<div class="hero-actions">
|
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
<div class="hero-stat-label">Build Steps</div>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="hero-stat">
|
|
26
|
-
<div class="hero-stat-value"
|
|
26
|
+
<div class="hero-stat-value"><a id="link-hero-ways-to-build"
|
|
27
|
+
href="http://localhost:3000/docs/api/elements"
|
|
28
|
+
style="color: inherit; text-decoration: none;">5</a></div>
|
|
27
29
|
<div class="hero-stat-label">Ways to Build</div>
|
|
28
30
|
</div>
|
|
29
31
|
<div class="hero-stat">
|
|
30
32
|
<div class="hero-stat-value">40+</div>
|
|
31
|
-
<div class="hero-stat-label"
|
|
33
|
+
<div class="hero-stat-label"><a id="link-hero-components" href="http://localhost:3000/docs/components/"
|
|
34
|
+
style="color: inherit; text-decoration: none;">Components</a></div>
|
|
32
35
|
</div>
|
|
33
36
|
</div>
|
|
34
37
|
</div>
|
|
@@ -38,79 +41,84 @@
|
|
|
38
41
|
<section class="section">
|
|
39
42
|
<div class="section-content">
|
|
40
43
|
<div class="section-header">
|
|
41
|
-
<h2 class="section-title">
|
|
42
|
-
<p class="section-subtitle">
|
|
43
|
-
Have fun, stay light.
|
|
44
|
-
</p>
|
|
44
|
+
<h2 class="section-title">Why Lightview</h2>
|
|
45
45
|
</div>
|
|
46
46
|
<div class="feature-grid">
|
|
47
|
-
<
|
|
47
|
+
<a id="link-feature-signals" href="./api/signals.html" class="feature-card">
|
|
48
48
|
<div class="feature-icon">
|
|
49
49
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
50
50
|
<circle cx="12" cy="12" r="10" />
|
|
51
51
|
<path d="M12 6v6l4 2" />
|
|
52
52
|
</svg>
|
|
53
53
|
</div>
|
|
54
|
-
<h3
|
|
55
|
-
State Reactivity</a></h3>
|
|
54
|
+
<h3 class="feature-title">Signals and Object State Reactivity</h3>
|
|
56
55
|
<p class="feature-description">
|
|
57
|
-
Fine-grained reactivity with signals and
|
|
58
|
-
DOM diffing needed.
|
|
56
|
+
Fine-grained reactivity with signals and object state. Updates propagate automatically—no virtual
|
|
57
|
+
DOM diffing needed. Arrays and Dates are directly reactive without destructing or copying. As a
|
|
58
|
+
bonus, session and local storage are also supported.
|
|
59
59
|
</p>
|
|
60
|
-
</
|
|
61
|
-
<
|
|
60
|
+
</a>
|
|
61
|
+
<a id="link-feature-elements" href="./api/elements.html" class="feature-card">
|
|
62
62
|
<div class="feature-icon">
|
|
63
63
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
64
64
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
65
65
|
<path d="M3 9h18M9 21V9" />
|
|
66
66
|
</svg>
|
|
67
67
|
</div>
|
|
68
|
-
<h3
|
|
69
|
-
</h3>
|
|
68
|
+
<h3 class="feature-title">Multiple Syntaxes</h3>
|
|
70
69
|
<p class="feature-description">
|
|
71
70
|
Tagged API, vDOM, Object DOM, HTML. Pick your style—they all work together.
|
|
72
71
|
</p>
|
|
73
|
-
</
|
|
74
|
-
<
|
|
72
|
+
</a>
|
|
73
|
+
<a id="link-feature-hypermedia" href="/docs/hypermedia/" class="feature-card">
|
|
75
74
|
<div class="feature-icon">
|
|
76
75
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
77
76
|
<path
|
|
78
77
|
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.66 0 3-4 3-9s-1.34-9-3-9m0 18c-1.66 0-3-4-3-9s1.34-9 3-9" />
|
|
79
78
|
</svg>
|
|
80
79
|
</div>
|
|
81
|
-
<h3
|
|
82
|
-
Ready</a></h3>
|
|
80
|
+
<h3 class="feature-title">Hypermedia Ready</h3>
|
|
83
81
|
<p class="feature-description">
|
|
84
82
|
Fetch HTML, vDOM, or Object DOM with the <code>src</code> attribute. HTMX-like patterns for
|
|
85
83
|
<code>src</code> and <code>href</code> built right
|
|
86
84
|
in.
|
|
87
85
|
</p>
|
|
88
|
-
</
|
|
89
|
-
<
|
|
86
|
+
</a>
|
|
87
|
+
<a id="link-feature-components" href="./components/index.html" class="feature-card">
|
|
90
88
|
<div class="feature-icon">
|
|
91
89
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
92
90
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
93
91
|
</svg>
|
|
94
92
|
</div>
|
|
95
|
-
<h3
|
|
96
|
-
Library</a></h3>
|
|
93
|
+
<h3 class="feature-title">Component Library</h3>
|
|
97
94
|
<p class="feature-description">
|
|
98
95
|
Beautiful, accessible components ready to use. Buttons, modals, forms, charts, and more.
|
|
99
96
|
</p>
|
|
100
|
-
</
|
|
101
|
-
<
|
|
97
|
+
</a>
|
|
98
|
+
<a id="link-feature-zerobuild" href="./getting-started/index.html" class="feature-card">
|
|
102
99
|
<div class="feature-icon">
|
|
103
100
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
104
101
|
<path
|
|
105
102
|
d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z" />
|
|
106
103
|
</svg>
|
|
107
104
|
</div>
|
|
108
|
-
<h3
|
|
109
|
-
Step</a></h3>
|
|
105
|
+
<h3 class="feature-title">Zero Build Step</h3>
|
|
110
106
|
<p class="feature-description">
|
|
111
107
|
No bundler required. Drop in a script tag and start building. Works everywhere.
|
|
112
108
|
</p>
|
|
113
|
-
</
|
|
109
|
+
</a>
|
|
110
|
+
<a id="link-feature-aisafe" href="#ai-safe" class="feature-card">
|
|
111
|
+
<div class="feature-icon">
|
|
112
|
+
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
113
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
114
|
+
</svg>
|
|
115
|
+
</div>
|
|
116
|
+
<h3 class="feature-title">AI Safe</h3>
|
|
117
|
+
<p class="feature-description">
|
|
118
|
+
Build reactive UIs with pure JSON or JSON like data. No eval(), no risky code generation. Safe for
|
|
119
|
+
AI agents.
|
|
120
|
+
</p>
|
|
121
|
+
</a>
|
|
114
122
|
|
|
115
123
|
</div>
|
|
116
124
|
</div>
|
|
@@ -150,7 +158,8 @@
|
|
|
150
158
|
<h3 style="color: white; font-size: 1.5rem; margin-bottom: 1rem;">Execution Safety</h3>
|
|
151
159
|
<p style="color: rgba(255, 255, 255, 0.75); line-height: 1.6;">AI-generated UIs usually require
|
|
152
160
|
risky JavaScript strings. cDOM creates reactive interfaces using <strong>pure JSON
|
|
153
|
-
data</strong> and <a id="link-ai-safe-cdom-helpers"
|
|
161
|
+
or JSON like data</strong> and <a id="link-ai-safe-cdom-helpers"
|
|
162
|
+
href="/docs/cdom.html#helpers"
|
|
154
163
|
style="color: #a78bfa; text-decoration: underline; font-weight: 600;">over 50 built-in
|
|
155
164
|
functions</a> that bring spreadsheet-like
|
|
156
165
|
functionality to the DOM. No <code>eval()</code>, no <code>new Function()</code>, and <strong>no
|
|
@@ -223,7 +232,7 @@
|
|
|
223
232
|
<section class="section">
|
|
224
233
|
<div class="section-content">
|
|
225
234
|
<div class="section-header">
|
|
226
|
-
<h2 class="section-title">
|
|
235
|
+
<h2 class="section-title">Lighten Up.</h2>
|
|
227
236
|
<p class="section-subtitle">
|
|
228
237
|
Get started in minutes. No npm install. No webpack or vite build process. Just code.
|
|
229
238
|
</p>
|
package/jprx/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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
|
|
|
5
|
+
> **v1.4.0 Syntax Update**: JPRX now uses a wrapper syntax `=(expr)` for unambiguous expression parsing. Legacy prefix-only syntax (e.g., `=/path`) is still supported but will be deprecated after March 31, 2026.
|
|
6
|
+
|
|
5
7
|
## Overview
|
|
6
8
|
|
|
7
9
|
JPRX is a **syntax** and an **expression engine**. While this repository provides the parser and core helper functions, JPRX is intended to be integrated into UI libraries or state management systems that can "hydrate" these expressions into active reactive bindings.
|
|
@@ -26,17 +28,17 @@ JPRX extends the base JSON Pointer syntax with:
|
|
|
26
28
|
|
|
27
29
|
| Feature | Syntax | Description |
|
|
28
30
|
|---------|--------|-------------|
|
|
29
|
-
| **Global Path** |
|
|
30
|
-
| **Relative Path** |
|
|
31
|
-
| **Parent Path** |
|
|
32
|
-
| **Functions** | `=sum(/items...price)` | Call registered core helpers. |
|
|
33
|
-
| **Explosion** |
|
|
34
|
-
| **Operators** |
|
|
31
|
+
| **Global Path** | `=(/user/name)` | Access global state via an absolute path. |
|
|
32
|
+
| **Relative Path** | `=(./count)` | Access properties relative to the current context. |
|
|
33
|
+
| **Parent Path** | `=(../id)` | Traverse up the state hierarchy (UP-tree search). |
|
|
34
|
+
| **Functions** | `=(sum(/items...price))` | Call registered core helpers. |
|
|
35
|
+
| **Explosion** | `=(/items...name)` | Extract a property from every object in an array (spread). |
|
|
36
|
+
| **Operators** | `=(++/count)`, `=(/a + /b)` | Familiar JS-style prefix, postfix, and infix operators. |
|
|
35
37
|
| **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. |
|
|
37
|
-
| **DOM Patches** | `=move(target, loc)`| Decentralized layout: Move/replace host element into a target. |
|
|
38
|
+
| **Two-Way Binding**| `=(bind(/user/name))`| Create a managed, two-way reactive link for inputs. |
|
|
39
|
+
| **DOM Patches** | `=(move(target, loc))`| Decentralized layout: Move/replace host element into a target. |
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
**Note**: For initialization functions like `state()` and `signal()`, use `=function(...)` without the outer wrapper, as they execute once on mount rather than being reactive expressions.
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
## State Management
|
|
@@ -104,7 +106,7 @@ To ensure unambiguous data flow, `=bind` only accepts direct paths. It cannot be
|
|
|
104
106
|
|
|
105
107
|
### Handling Transformations
|
|
106
108
|
If you need to transform data during a two-way binding, there are two primary approaches:
|
|
107
|
-
1. **Event-Based**: Use a manual `oninput` handler to apply the transformation, e.g., `=set(/name, upper($event/target/value))`.
|
|
109
|
+
1. **Event-Based**: Use a manual `oninput` handler to apply the transformation, e.g., `=(set(/name, upper($event/target/value)))`.
|
|
108
110
|
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
111
|
|
|
110
112
|
---
|
|
@@ -159,9 +161,9 @@ A modern, lifecycle-based reactive counter:
|
|
|
159
161
|
"onmount": "=state({ count: 0 }, { name: 'counter', schema: 'auto', scope: $this })",
|
|
160
162
|
"children": [
|
|
161
163
|
{ "h2": "Modern JPRX Counter" },
|
|
162
|
-
{ "p": ["Current Count: ", "
|
|
163
|
-
{ "button": { "onclick": "
|
|
164
|
-
{ "button": { "onclick": "
|
|
164
|
+
{ "p": ["Current Count: ", "=(/counter/count)"] },
|
|
165
|
+
{ "button": { "onclick": "=(++/counter/count)", "children": ["+"] } },
|
|
166
|
+
{ "button": { "onclick": "=(--/counter/count)", "children": ["-"] } }
|
|
165
167
|
]
|
|
166
168
|
}
|
|
167
169
|
}
|
package/jprx/package.json
CHANGED
package/jprx/parser.js
CHANGED
|
@@ -1730,7 +1730,44 @@ export const parseJPRX = (input) => {
|
|
|
1730
1730
|
continue;
|
|
1731
1731
|
}
|
|
1732
1732
|
|
|
1733
|
-
// Handle JPRX expressions starting with =
|
|
1733
|
+
// Handle JPRX expressions starting with = or #
|
|
1734
|
+
// New wrapper syntax: =(expr) or #(xpath)
|
|
1735
|
+
if ((char === '=' || char === '#') && input[i + 1] === '(') {
|
|
1736
|
+
const prefix = char;
|
|
1737
|
+
let expr = prefix;
|
|
1738
|
+
i++; // skip = or #
|
|
1739
|
+
let parenDepth = 0;
|
|
1740
|
+
let inExprQuote = null;
|
|
1741
|
+
|
|
1742
|
+
while (i < len) {
|
|
1743
|
+
const c = input[i];
|
|
1744
|
+
|
|
1745
|
+
if (inExprQuote) {
|
|
1746
|
+
if (c === inExprQuote && input[i - 1] !== '\\') inExprQuote = null;
|
|
1747
|
+
} else if (c === '"' || c === "'") {
|
|
1748
|
+
inExprQuote = c;
|
|
1749
|
+
} else {
|
|
1750
|
+
if (c === '(') parenDepth++;
|
|
1751
|
+
else if (c === ')') {
|
|
1752
|
+
parenDepth--;
|
|
1753
|
+
if (parenDepth === 0) {
|
|
1754
|
+
expr += c;
|
|
1755
|
+
i++;
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
expr += c;
|
|
1762
|
+
i++;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Use JSON.stringify to safely quote and escape the expression
|
|
1766
|
+
result += JSON.stringify(expr);
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Handle legacy JPRX expressions starting with = (without parentheses)
|
|
1734
1771
|
if (char === '=') {
|
|
1735
1772
|
let expr = '';
|
|
1736
1773
|
let parenDepth = 0;
|
|
@@ -1792,6 +1829,53 @@ export const parseJPRX = (input) => {
|
|
|
1792
1829
|
continue;
|
|
1793
1830
|
}
|
|
1794
1831
|
|
|
1832
|
+
// Handle XPath expressions starting with # (legacy syntax)
|
|
1833
|
+
if (char === '#') {
|
|
1834
|
+
let expr = '';
|
|
1835
|
+
let parenDepth = 0;
|
|
1836
|
+
let bracketDepth = 0;
|
|
1837
|
+
let inExprQuote = null;
|
|
1838
|
+
|
|
1839
|
+
while (i < len) {
|
|
1840
|
+
const c = input[i];
|
|
1841
|
+
|
|
1842
|
+
if (inExprQuote) {
|
|
1843
|
+
if (c === inExprQuote && input[i - 1] !== '\\') inExprQuote = null;
|
|
1844
|
+
} else if (c === '"' || c === "'") {
|
|
1845
|
+
inExprQuote = c;
|
|
1846
|
+
} else {
|
|
1847
|
+
// Check for break BEFORE updating depth
|
|
1848
|
+
if (parenDepth === 0 && bracketDepth === 0) {
|
|
1849
|
+
// Break on structural characters at depth 0
|
|
1850
|
+
if (/[}[\],:]/.test(c) && expr.length > 1) break;
|
|
1851
|
+
// For whitespace, peek ahead
|
|
1852
|
+
if (/\s/.test(c)) {
|
|
1853
|
+
let j = i + 1;
|
|
1854
|
+
while (j < len && /\s/.test(input[j])) j++;
|
|
1855
|
+
if (j < len) {
|
|
1856
|
+
const nextChar = input[j];
|
|
1857
|
+
if (nextChar === '}' || nextChar === ',' || nextChar === ']') {
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (c === '(') parenDepth++;
|
|
1865
|
+
else if (c === ')') parenDepth--;
|
|
1866
|
+
else if (c === '[') bracketDepth++;
|
|
1867
|
+
else if (c === ']') bracketDepth--;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
expr += c;
|
|
1871
|
+
i++;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// Use JSON.stringify to safely quote and escape the expression
|
|
1875
|
+
result += JSON.stringify(expr);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1795
1879
|
// Handle unquoted property names, identifiers, paths, and FUNCTION CALLS
|
|
1796
1880
|
if (/[a-zA-Z_$\/.\/]/.test(char)) {
|
|
1797
1881
|
let word = '';
|