lightview 2.3.4 → 2.3.5
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/docs/calculator.html +427 -0
- package/docs/cdom-nav.html +1 -1
- package/docs/cdom.html +2 -2
- package/jprx/README.md +1 -1
- package/jprx/helpers/calc.js +82 -0
- package/jprx/helpers/lookup.js +39 -0
- package/jprx/helpers/math.js +4 -0
- package/jprx/index.js +3 -0
- package/jprx/package.json +2 -2
- package/jprx/parser.js +35 -3
- package/lightview-all.js +1724 -23
- package/lightview-cdom.js +1724 -23
- package/package.json +3 -2
- package/src/lightview-cdom.js +3 -1
- package/start-dev.js +1 -1
- package/cDOMIntro.md +0 -279
|
@@ -0,0 +1,427 @@
|
|
|
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
|
+
<meta name="description"
|
|
8
|
+
content="A beautiful calculator built with Lightview cDOM and JPRX reactive expressions - no custom JavaScript!">
|
|
9
|
+
<title>Calculator | Lightview cDOM</title>
|
|
10
|
+
<style>
|
|
11
|
+
/* === DESIGN TOKENS === */
|
|
12
|
+
:root {
|
|
13
|
+
--calc-bg: linear-gradient(145deg, #1a1b2e 0%, #16172b 100%);
|
|
14
|
+
--calc-surface: rgba(255, 255, 255, 0.03);
|
|
15
|
+
--calc-surface-hover: rgba(255, 255, 255, 0.08);
|
|
16
|
+
--calc-border: rgba(255, 255, 255, 0.08);
|
|
17
|
+
--calc-shadow: 0 25px 60px rgba(0, 0, 0, 0.5);
|
|
18
|
+
--calc-glow: 0 0 80px rgba(99, 102, 241, 0.15);
|
|
19
|
+
|
|
20
|
+
--text-primary: #f8fafc;
|
|
21
|
+
--text-secondary: #94a3b8;
|
|
22
|
+
--text-dimmed: #64748b;
|
|
23
|
+
|
|
24
|
+
--btn-number: linear-gradient(145deg, #2a2d4a, #1e2039);
|
|
25
|
+
--btn-operator: linear-gradient(145deg, #4f46e5, #4338ca);
|
|
26
|
+
--btn-equals: linear-gradient(145deg, #10b981, #059669);
|
|
27
|
+
--btn-clear: linear-gradient(145deg, #ef4444, #dc2626);
|
|
28
|
+
--btn-function: linear-gradient(145deg, #3b3f5c, #2d3047);
|
|
29
|
+
|
|
30
|
+
--btn-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
|
31
|
+
--btn-shadow-pressed: inset 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
32
|
+
|
|
33
|
+
--radius-sm: 12px;
|
|
34
|
+
--radius-md: 16px;
|
|
35
|
+
--radius-lg: 24px;
|
|
36
|
+
--radius-xl: 32px;
|
|
37
|
+
|
|
38
|
+
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* === GLOBAL RESET & BASE === */
|
|
42
|
+
*,
|
|
43
|
+
*::before,
|
|
44
|
+
*::after {
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
margin: 0;
|
|
47
|
+
padding: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
html,
|
|
51
|
+
body {
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
body {
|
|
56
|
+
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
57
|
+
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #0f0f1a 100%);
|
|
58
|
+
min-height: 100vh;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
padding: 20px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Animated background orbs */
|
|
67
|
+
body::before,
|
|
68
|
+
body::after {
|
|
69
|
+
content: '';
|
|
70
|
+
position: fixed;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
filter: blur(80px);
|
|
73
|
+
opacity: 0.4;
|
|
74
|
+
z-index: 0;
|
|
75
|
+
animation: float 8s ease-in-out infinite;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
body::before {
|
|
79
|
+
width: 400px;
|
|
80
|
+
height: 400px;
|
|
81
|
+
background: radial-gradient(circle, #6366f1 0%, transparent 70%);
|
|
82
|
+
top: -100px;
|
|
83
|
+
left: -100px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
body::after {
|
|
87
|
+
width: 350px;
|
|
88
|
+
height: 350px;
|
|
89
|
+
background: radial-gradient(circle, #10b981 0%, transparent 70%);
|
|
90
|
+
bottom: -100px;
|
|
91
|
+
right: -100px;
|
|
92
|
+
animation-delay: -4s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes float {
|
|
96
|
+
|
|
97
|
+
0%,
|
|
98
|
+
100% {
|
|
99
|
+
transform: translate(0, 0) scale(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
50% {
|
|
103
|
+
transform: translate(30px, 30px) scale(1.1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* === CALCULATOR CONTAINER === */
|
|
108
|
+
.calculator {
|
|
109
|
+
position: relative;
|
|
110
|
+
z-index: 1;
|
|
111
|
+
width: 100%;
|
|
112
|
+
max-width: 380px;
|
|
113
|
+
background: var(--calc-bg);
|
|
114
|
+
border-radius: var(--radius-xl);
|
|
115
|
+
padding: 28px;
|
|
116
|
+
box-shadow: var(--calc-shadow), var(--calc-glow);
|
|
117
|
+
border: 1px solid var(--calc-border);
|
|
118
|
+
backdrop-filter: blur(10px);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.calculator::before {
|
|
122
|
+
content: '';
|
|
123
|
+
position: absolute;
|
|
124
|
+
top: 0;
|
|
125
|
+
left: 0;
|
|
126
|
+
right: 0;
|
|
127
|
+
height: 50%;
|
|
128
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, transparent 100%);
|
|
129
|
+
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
|
130
|
+
pointer-events: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* === DISPLAY AREA === */
|
|
134
|
+
.display {
|
|
135
|
+
background: var(--calc-surface);
|
|
136
|
+
border-radius: var(--radius-lg);
|
|
137
|
+
padding: 24px 20px;
|
|
138
|
+
margin-bottom: 24px;
|
|
139
|
+
border: 1px solid var(--calc-border);
|
|
140
|
+
position: relative;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.display::before {
|
|
145
|
+
content: '';
|
|
146
|
+
position: absolute;
|
|
147
|
+
top: 0;
|
|
148
|
+
left: 0;
|
|
149
|
+
right: 0;
|
|
150
|
+
height: 1px;
|
|
151
|
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.expression {
|
|
155
|
+
font-size: 1rem;
|
|
156
|
+
color: var(--text-dimmed);
|
|
157
|
+
min-height: 1.5rem;
|
|
158
|
+
text-align: right;
|
|
159
|
+
font-weight: 400;
|
|
160
|
+
letter-spacing: 0.5px;
|
|
161
|
+
margin-bottom: 8px;
|
|
162
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
163
|
+
word-break: break-all;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.result {
|
|
167
|
+
font-size: 3rem;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
color: var(--text-primary);
|
|
170
|
+
text-align: right;
|
|
171
|
+
line-height: 1.1;
|
|
172
|
+
letter-spacing: -1px;
|
|
173
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
174
|
+
text-shadow: 0 0 30px rgba(99, 102, 241, 0.3);
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* === BUTTON GRID === */
|
|
180
|
+
.buttons {
|
|
181
|
+
display: grid;
|
|
182
|
+
grid-template-columns: repeat(4, 1fr);
|
|
183
|
+
gap: 14px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.btn {
|
|
187
|
+
aspect-ratio: 1;
|
|
188
|
+
border: none;
|
|
189
|
+
border-radius: var(--radius-md);
|
|
190
|
+
font-size: 1.5rem;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
transition: var(--transition);
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
position: relative;
|
|
198
|
+
overflow: hidden;
|
|
199
|
+
font-family: inherit;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.btn::before {
|
|
203
|
+
content: '';
|
|
204
|
+
position: absolute;
|
|
205
|
+
top: 0;
|
|
206
|
+
left: 0;
|
|
207
|
+
right: 0;
|
|
208
|
+
bottom: 0;
|
|
209
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
|
210
|
+
opacity: 0;
|
|
211
|
+
transition: var(--transition);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.btn:hover::before {
|
|
215
|
+
opacity: 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.btn:active {
|
|
219
|
+
transform: scale(0.94);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.btn-number {
|
|
223
|
+
background: var(--btn-number);
|
|
224
|
+
color: var(--text-primary);
|
|
225
|
+
box-shadow: var(--btn-shadow);
|
|
226
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.btn-number:hover {
|
|
230
|
+
background: linear-gradient(145deg, #363a5c, #252846);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.btn-operator {
|
|
234
|
+
background: var(--btn-operator);
|
|
235
|
+
color: white;
|
|
236
|
+
box-shadow: var(--btn-shadow), 0 0 20px rgba(99, 102, 241, 0.3);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.btn-operator:hover {
|
|
240
|
+
background: linear-gradient(145deg, #5d54f5, #4f46e5);
|
|
241
|
+
box-shadow: var(--btn-shadow), 0 0 30px rgba(99, 102, 241, 0.5);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.btn-equals {
|
|
245
|
+
background: var(--btn-equals);
|
|
246
|
+
color: white;
|
|
247
|
+
box-shadow: var(--btn-shadow), 0 0 20px rgba(16, 185, 129, 0.3);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.btn-equals:hover {
|
|
251
|
+
background: linear-gradient(145deg, #22c994, #10b981);
|
|
252
|
+
box-shadow: var(--btn-shadow), 0 0 30px rgba(16, 185, 129, 0.5);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.btn-clear {
|
|
256
|
+
background: var(--btn-clear);
|
|
257
|
+
color: white;
|
|
258
|
+
box-shadow: var(--btn-shadow), 0 0 20px rgba(239, 68, 68, 0.2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.btn-clear:hover {
|
|
262
|
+
background: linear-gradient(145deg, #f87171, #ef4444);
|
|
263
|
+
box-shadow: var(--btn-shadow), 0 0 30px rgba(239, 68, 68, 0.4);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.btn-function {
|
|
267
|
+
background: var(--btn-function);
|
|
268
|
+
color: var(--text-secondary);
|
|
269
|
+
box-shadow: var(--btn-shadow);
|
|
270
|
+
font-size: 1.25rem;
|
|
271
|
+
border: 1px solid rgba(255, 255, 255, 0.03);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.btn-function:hover {
|
|
275
|
+
background: linear-gradient(145deg, #474b6e, #3b3f5c);
|
|
276
|
+
color: var(--text-primary);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.btn-wide {
|
|
280
|
+
grid-column: span 2;
|
|
281
|
+
aspect-ratio: auto;
|
|
282
|
+
height: 70px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* === BRANDING === */
|
|
286
|
+
.branding {
|
|
287
|
+
text-align: center;
|
|
288
|
+
margin-top: 20px;
|
|
289
|
+
padding-top: 16px;
|
|
290
|
+
border-top: 1px solid var(--calc-border);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.branding span {
|
|
294
|
+
font-size: 0.75rem;
|
|
295
|
+
color: var(--text-dimmed);
|
|
296
|
+
letter-spacing: 1px;
|
|
297
|
+
text-transform: uppercase;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.branding a {
|
|
301
|
+
color: #6366f1;
|
|
302
|
+
text-decoration: none;
|
|
303
|
+
font-weight: 600;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.branding a:hover {
|
|
307
|
+
text-decoration: underline;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@media (max-width: 420px) {
|
|
311
|
+
.calculator {
|
|
312
|
+
padding: 20px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.result {
|
|
316
|
+
font-size: 2.5rem;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.btn {
|
|
320
|
+
font-size: 1.25rem;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.buttons {
|
|
324
|
+
gap: 10px;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
</style>
|
|
328
|
+
</head>
|
|
329
|
+
|
|
330
|
+
<body>
|
|
331
|
+
<div id="app"></div>
|
|
332
|
+
|
|
333
|
+
<!-- Load Lightview scripts -->
|
|
334
|
+
<script src="/lightview.js"></script>
|
|
335
|
+
<script src="/lightview-x.js"></script>
|
|
336
|
+
<script src="/lightview-cdom.js"></script>
|
|
337
|
+
|
|
338
|
+
<script>
|
|
339
|
+
// Pure cDOM/JPRX Calculator - No custom JavaScript helpers needed!
|
|
340
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
341
|
+
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
342
|
+
const { $ } = Lightview;
|
|
343
|
+
|
|
344
|
+
// Calculator cDOM definition using only core JPRX helpers:
|
|
345
|
+
// - state, set: state management
|
|
346
|
+
// - if, eq: conditionals
|
|
347
|
+
// - concat, contains: string operations
|
|
348
|
+
// - negate, toPercent: math operations
|
|
349
|
+
// - calc with $(): expression evaluation
|
|
350
|
+
const calculatorCDOM = `{
|
|
351
|
+
div: {
|
|
352
|
+
class: "calculator",
|
|
353
|
+
onmount: =state({
|
|
354
|
+
display: "0",
|
|
355
|
+
expr: "",
|
|
356
|
+
prev: "",
|
|
357
|
+
op: "",
|
|
358
|
+
waiting: false
|
|
359
|
+
}, { name: "c", schema: "polymorphic", scope: $this }),
|
|
360
|
+
children: [
|
|
361
|
+
// Display area
|
|
362
|
+
{
|
|
363
|
+
div: {
|
|
364
|
+
class: "display",
|
|
365
|
+
children: [
|
|
366
|
+
{ div: { class: "expression", children: [=/c/expr] } },
|
|
367
|
+
{ div: { class: "result", children: [=/c/display] } }
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
// Button grid
|
|
372
|
+
{
|
|
373
|
+
div: {
|
|
374
|
+
class: "buttons",
|
|
375
|
+
children: [
|
|
376
|
+
// Row 1: AC, ±, %, ÷
|
|
377
|
+
{ button: { class: "btn btn-clear", onclick: =set(/c, { display: "0", expr: "", prev: "", op: "", waiting: false }), children: ["AC"] } },
|
|
378
|
+
{ button: { class: "btn btn-function", onclick: =set(/c, { display: negate(/c/display), waiting: true, expr: "" }), children: ["±"] } },
|
|
379
|
+
{ button: { class: "btn btn-function", onclick: =set(/c, { display: toPercent(/c/display), waiting: true, expr: "" }), children: ["%"] } },
|
|
380
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " ÷"), op: "/", waiting: true }), children: ["÷"] } },
|
|
381
|
+
|
|
382
|
+
// Row 2: 7, 8, 9, ×
|
|
383
|
+
{ button: { id: "7", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["7"] } },
|
|
384
|
+
{ button: { id: "8", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["8"] } },
|
|
385
|
+
{ button: { id: "9", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["9"] } },
|
|
386
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " ×"), op: "*", waiting: true }), children: ["×"] } },
|
|
387
|
+
|
|
388
|
+
// Row 3: 4, 5, 6, −
|
|
389
|
+
{ button: { id: "4", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["4"] } },
|
|
390
|
+
{ button: { id: "5", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["5"] } },
|
|
391
|
+
{ button: { id: "6", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["6"] } },
|
|
392
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " −"), op: "-", waiting: true }), children: ["−"] } },
|
|
393
|
+
|
|
394
|
+
// Row 4: 1, 2, 3, +
|
|
395
|
+
{ button: { id: "1", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["1"] } },
|
|
396
|
+
{ button: { id: "2", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["2"] } },
|
|
397
|
+
{ button: { id: "3", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["3"] } },
|
|
398
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " +"), op: "+", waiting: true }), children: ["+"] } },
|
|
399
|
+
|
|
400
|
+
// Row 5: 0, ., =
|
|
401
|
+
{ button: { class: "btn btn-number btn-wide", onclick: =set(/c, { display: if(/c/waiting, "0", if(eq(/c/display, "0"), "0", concat(/c/display, "0"))), waiting: false }), children: ["0"] } },
|
|
402
|
+
{ button: { class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, "0.", if(contains(/c/display, "."), /c/display, concat(/c/display, "."))), waiting: false }), children: ["."] } },
|
|
403
|
+
{ button: { class: "btn btn-equals", onclick: =set(/c, { display: if(eq(/c/op, ""), /c/display, calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))), expr: concat(/c/expr, " ", /c/display, " ="), prev: "", op: "", waiting: true }), children: ["="] } }
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
// Branding
|
|
408
|
+
{
|
|
409
|
+
div: {
|
|
410
|
+
class: "branding",
|
|
411
|
+
children: [
|
|
412
|
+
{ span: { children: ["Built with ", { a: { href: "https://github.com/anywhichway/lightview", target: "_blank", children: ["Lightview"] } }, " cDOM • No custom JS!"] } }
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
}`;
|
|
419
|
+
|
|
420
|
+
// Render the calculator
|
|
421
|
+
const hydrated = hydrate(parseJPRX(calculatorCDOM));
|
|
422
|
+
$('#app').content(hydrated);
|
|
423
|
+
});
|
|
424
|
+
</script>
|
|
425
|
+
</body>
|
|
426
|
+
|
|
427
|
+
</html>
|
package/docs/cdom-nav.html
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<a href="#helpers-datetime" class="docs-nav-link" style="font-size: 0.85em;">DateTime</a>
|
|
50
50
|
<a href="#helpers-lookup" class="docs-nav-link" style="font-size: 0.85em;">Lookup</a>
|
|
51
51
|
<a href="#helpers-mutation" class="docs-nav-link" style="font-size: 0.85em;">State Mutation</a>
|
|
52
|
-
<a href="#helpers-network" class="docs-nav-link" style="font-size: 0.85em;">Network
|
|
52
|
+
<a href="#helpers-network" class="docs-nav-link" style="font-size: 0.85em;">Network</a>
|
|
53
53
|
</div>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
package/docs/cdom.html
CHANGED
|
@@ -390,7 +390,7 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
390
390
|
class: "counter",
|
|
391
391
|
children: [
|
|
392
392
|
{ p: { children: ["Count: ", "=/local/count] } },
|
|
393
|
-
{ button: { onclick: =++/local/count, children: ["+"] } }
|
|
393
|
+
{ button: { onclick: "=++/local/count", children: ["+"] } }
|
|
394
394
|
]
|
|
395
395
|
}
|
|
396
396
|
}</code></pre>
|
|
@@ -1058,4 +1058,4 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
1058
1058
|
|
|
1059
1059
|
|
|
1060
1060
|
</main>
|
|
1061
|
-
</div>
|
|
1061
|
+
</div>
|
package/jprx/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
|
|
|
@@ -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/jprx/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/jprx/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/jprx/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/jprx/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": [
|