lightview 2.3.4 → 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 +22 -0
- package/docs/cdom-nav.html +1 -1
- package/docs/cdom.html +2 -2
- package/functions/_middleware.js +20 -3
- package/jprx/README.md +1 -1
- package/jprx/helpers/calc.js +82 -0
- package/jprx/helpers/dom.js +69 -0
- package/jprx/helpers/logic.js +9 -3
- package/jprx/helpers/lookup.js +39 -0
- package/jprx/helpers/math.js +4 -0
- package/jprx/index.js +4 -0
- package/jprx/package.json +2 -2
- package/jprx/parser.js +394 -83
- package/lightview-all.js +2256 -168
- package/lightview-cdom.js +2176 -92
- package/lightview-x.js +4 -3
- package/lightview.js +14 -0
- package/package.json +3 -2
- package/src/lightview-cdom.js +153 -8
- 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/cDOMIntro.md +0 -279
- package/start-dev.js +0 -93
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# XPath Integration for cDOM - Implementation Summary
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
XPath support has been added to cDOM, allowing developers to navigate the DOM structure during element definition. This enables referencing parent/ancestor attributes without duplication.
|
|
5
|
+
|
|
6
|
+
## Features Implemented
|
|
7
|
+
|
|
8
|
+
### 1. Static XPath (`#` prefix)
|
|
9
|
+
- **Syntax:** `#xpath-expression`
|
|
10
|
+
- **Evaluation:** Once during DOM construction (two-pass approach)
|
|
11
|
+
- **Reactivity:** None - becomes a static string value
|
|
12
|
+
- **Escape Sequence:** `'#` produces literal `#` string
|
|
13
|
+
|
|
14
|
+
**Example:**
|
|
15
|
+
```javascript
|
|
16
|
+
{
|
|
17
|
+
tag: "button",
|
|
18
|
+
attributes: {
|
|
19
|
+
"data-parent-id": "#../@id",
|
|
20
|
+
"aria-labelledby": "#../../@label-id"
|
|
21
|
+
},
|
|
22
|
+
children: ["#../@aria-label"]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Reactive XPath (`=xpath()` helper)
|
|
27
|
+
- **Syntax:** `=xpath('expression')`
|
|
28
|
+
- **Evaluation:** As JPRX helper, returns computed signal
|
|
29
|
+
- **Reactivity:** Re-evaluates when observed DOM changes (TODO: full MutationObserver)
|
|
30
|
+
- **Composable:** Can mix with JPRX expressions
|
|
31
|
+
|
|
32
|
+
**Example:**
|
|
33
|
+
```javascript
|
|
34
|
+
{
|
|
35
|
+
attributes: {
|
|
36
|
+
title: "=xpath('../@id')",
|
|
37
|
+
class: "=concat(xpath('../@disabled'), ' ', $/theme)"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Allowed XPath Axes (Backward-Only)
|
|
43
|
+
✅ `self::` or `.` - current node
|
|
44
|
+
✅ `parent::` or `..` - parent node
|
|
45
|
+
✅ `ancestor::` - all ancestors
|
|
46
|
+
✅ `ancestor-or-self::`
|
|
47
|
+
✅ `preceding-sibling::` - earlier siblings
|
|
48
|
+
✅ `preceding::` - all preceding nodes
|
|
49
|
+
|
|
50
|
+
❌ `child::` - forward reference (not allowed)
|
|
51
|
+
❌ `descendant::` - forward reference (not allowed)
|
|
52
|
+
❌ `following::` - forward reference (not allowed)
|
|
53
|
+
❌ `following-sibling::` - forward reference (not allowed)
|
|
54
|
+
|
|
55
|
+
## Implementation Details
|
|
56
|
+
|
|
57
|
+
### Files Modified
|
|
58
|
+
|
|
59
|
+
1. **`jprx/parser.js`**
|
|
60
|
+
- Added `preprocessXPath()` function to quote unquoted `#xpath` expressions in cDOMC
|
|
61
|
+
- Handles escape sequences (`'#`)
|
|
62
|
+
- Skips quoted strings and comments
|
|
63
|
+
|
|
64
|
+
2. **`src/lightview-cdom.js`**
|
|
65
|
+
- Updated `hydrate()` to detect and mark `#xpath` expressions
|
|
66
|
+
- Added `validateXPath()` to ensure no forward-looking axes
|
|
67
|
+
- Added `resolveStaticXPath()` for two-pass XPath resolution
|
|
68
|
+
- Registered `registerDOMHelpers` for reactive xpath() helper
|
|
69
|
+
- Exported `resolveStaticXPath` for global access
|
|
70
|
+
|
|
71
|
+
3. **`src/lightview.js`**
|
|
72
|
+
- Updated `makeReactiveAttributes()` to handle `__xpath__` markers from hydration
|
|
73
|
+
- Updated `processChildren()` to handle XPath markers in children arrays
|
|
74
|
+
- Added call to `resolveStaticXPath()` after DOM tree construction
|
|
75
|
+
|
|
76
|
+
4. **`jprx/helpers/dom.js`** (NEW)
|
|
77
|
+
- Created reactive `xpath()` helper function
|
|
78
|
+
- Returns computed signal for XPath evaluation
|
|
79
|
+
- Validates against forward-looking axes
|
|
80
|
+
- TODO: Add Mutation Observer for full reactivity
|
|
81
|
+
|
|
82
|
+
5. **`jprx/index.js`**
|
|
83
|
+
- Exported `registerDOMHelpers`
|
|
84
|
+
|
|
85
|
+
### Two-Pass Resolution Flow
|
|
86
|
+
|
|
87
|
+
**Pass 1: Build DOM Tree**
|
|
88
|
+
1. `parseCDOMC()` preprocesses input, quoting unquoted `#xpath` expressions
|
|
89
|
+
2. `hydrate()` detects `#xpath` strings, marks with `{ __xpath__: 'expr', __static__: true }`
|
|
90
|
+
3. `element()` creates DOM nodes
|
|
91
|
+
4. `makeReactiveAttributes()` sets `data-xpath-{attrname}` markers on nodes
|
|
92
|
+
5. `processChildren()` creates text nodes with `__xpathExpr` property
|
|
93
|
+
6. Tree is fully constructed via `appendChild()`
|
|
94
|
+
|
|
95
|
+
**Pass 2: Resolve XPath**
|
|
96
|
+
7. `resolveStaticXPath()` called after tree construction
|
|
97
|
+
8. Walks tree with `TreeWalker`
|
|
98
|
+
9. Finds nodes with `data-xpath-*` attributes or `__xpathExpr` properties
|
|
99
|
+
10. Validates XPath expressions (no forward-looking)
|
|
100
|
+
11. Calls browser's `document.evaluate()` with node as context
|
|
101
|
+
12. Sets resolved values and cleans up markers
|
|
102
|
+
|
|
103
|
+
## Usage Examples
|
|
104
|
+
|
|
105
|
+
### cDOMC (Unquoted)
|
|
106
|
+
```javascript
|
|
107
|
+
{
|
|
108
|
+
tag: div,
|
|
109
|
+
attributes: {
|
|
110
|
+
id: parent-div,
|
|
111
|
+
data-theme: dark
|
|
112
|
+
},
|
|
113
|
+
children: [{
|
|
114
|
+
tag: button,
|
|
115
|
+
attributes: {
|
|
116
|
+
data-parent-id: #../@id,
|
|
117
|
+
class: "=concat(xpath('../@data-theme'), '-button')"
|
|
118
|
+
},
|
|
119
|
+
children: [#../@aria-label]
|
|
120
|
+
}]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### JSON
|
|
125
|
+
```javascript
|
|
126
|
+
{
|
|
127
|
+
"tag": "div",
|
|
128
|
+
"attributes": {
|
|
129
|
+
"id": "parent-div",
|
|
130
|
+
"data-theme": "dark"
|
|
131
|
+
},
|
|
132
|
+
"children": [{
|
|
133
|
+
"tag": "button",
|
|
134
|
+
"attributes": {
|
|
135
|
+
"data-parent-id": "#../@id",
|
|
136
|
+
"class": "=concat(xpath('../@data-theme'), '-button')"
|
|
137
|
+
},
|
|
138
|
+
"children": ["#../@aria-label"]
|
|
139
|
+
}]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
Test file created: `test-xpath.html`
|
|
146
|
+
|
|
147
|
+
Open in browser to verify:
|
|
148
|
+
- Static XPath in attributes
|
|
149
|
+
- Parent attribute access
|
|
150
|
+
- Escape sequences
|
|
151
|
+
|
|
152
|
+
## Key Features
|
|
153
|
+
|
|
154
|
+
1. **Unquoted XPath in cDOMC**: Fully supported through structural integration with the parser character loop.
|
|
155
|
+
- ✅ Working: `children: [#../@id]`
|
|
156
|
+
- ✅ Working: `children: [#div[@id='1']]` (Complex paths with brackets now work!)
|
|
157
|
+
|
|
158
|
+
2. **Reactive xpath() helper**: Currently evaluates once via computed signal. Full reactivity with MutationObserver is TODO.
|
|
159
|
+
|
|
160
|
+
3. **XPath complexity**: Works with paths like `../@id`, `../../@attr`, and predicate-based paths.
|
|
161
|
+
|
|
162
|
+
## Implementation: Structural Parser Integration
|
|
163
|
+
|
|
164
|
+
Instead of a string preprocessor hack, XPath support is now integrated directly into the `parseCDOMC` word parser.
|
|
165
|
+
|
|
166
|
+
- When `parseWord` encounters `#`, it enters a permissive "expression mode" (similar to `=` JPRX).
|
|
167
|
+
- In this mode, it tracks nesting depth for `[]`, `{}`, and `()`.
|
|
168
|
+
- It handles internal quotes within the XPath safely.
|
|
169
|
+
- It only terminates the unquoted word when nesting depth is `0` and it encounters a structural cDOMC delimiter (e.g., `]`, `}`, or `,`).
|
|
170
|
+
|
|
171
|
+
## Bug Fixes Applied
|
|
172
|
+
|
|
173
|
+
1. **Structural Delimiters**: Fixed the issue where unquoted XPath would consume the closing bracket of its container.
|
|
174
|
+
2. **Complex Paths**: Enabled support for square brackets and other structural characters within the XPath itself.
|
|
175
|
+
3. **Text node context**: Fixed XPath evaluation to use the text node itself as context, not its parent element.
|
|
176
|
+
|
|
177
|
+
## Next Steps
|
|
178
|
+
|
|
179
|
+
1. Add MutationObserver to reactive xpath() helper for full reactivity
|
|
180
|
+
2. Add comprehensive unit tests for both static and reactive XPath
|
|
181
|
+
3. Document XPath support in cDOM documentation
|
|
182
|
+
4. Add more examples showing different XPath axes
|
|
183
|
+
5. Performance optimization for large DOMs
|
|
184
|
+
|
|
185
|
+
## Lint Issues
|
|
186
|
+
|
|
187
|
+
Note: The following lint warnings exist but don't affect functionality:
|
|
188
|
+
- **File length**: `/mnt/c/Users/Owner/AntigravityProjects/lightview/jprx/parser.js` has 1552 lines (limit 500)
|
|
189
|
+
- This is a large parser file; could be split/refactored in the future
|
|
190
|
+
- **Escape characters**: Some unnecessary escapes in regex (cosmetic issue)
|
package/_headers
CHANGED
package/_routes.json
ADDED
package/build-bundles.mjs
CHANGED
|
@@ -102,4 +102,14 @@ if (isWatch) {
|
|
|
102
102
|
}, 300); // 300ms debounce
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
// Also watch jprx directory for parser changes
|
|
107
|
+
watch(resolve(__dirname, 'jprx'), { recursive: true }, (event, filename) => {
|
|
108
|
+
if (filename && !filename.includes('~')) {
|
|
109
|
+
clearTimeout(debounceTimer);
|
|
110
|
+
debounceTimer = setTimeout(() => {
|
|
111
|
+
runBuilds();
|
|
112
|
+
}, 300);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
105
115
|
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Building a Fully Functional Calculator with Zero Custom JavaScript
|
|
2
|
+
|
|
3
|
+
*How a JSON or JSON-like language enables the next generation of safe human and AI-generated UIs*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
In [The Future of AI-Generated UI](https://hackernoon.com/the-future-of-ai-generated-ui-why-i-built-cdom-and-jprx), I made the case that raw JavaScript is a security nightmare for AI-generated code. I argued that we need declarative, sandboxed formats like **cDOM** (structure) and **JPRX** (behavior) if we ever want to trust an LLM to build our interfaces.
|
|
8
|
+
|
|
9
|
+
But let's be real: reactive counters and to-do lists are easy. Any framework looks elegant when the logic fits on a napkin. To prove a JSON-based approach actually holds up in production, you need a problem with messy state, edge cases, and distinct modes of operation.
|
|
10
|
+
|
|
11
|
+
You need a calculator.
|
|
12
|
+
|
|
13
|
+
Calculators are inherently tricky:
|
|
14
|
+
- **Input Modes**: Are we typing a fresh number or appending to an existing one?
|
|
15
|
+
- **Chaining**: What happens when you hit `+` then `-` then `*` without hitting equals?
|
|
16
|
+
- **DRY Logic**: How do we avoid writing 10 separate handlers for buttons 0-9?
|
|
17
|
+
|
|
18
|
+
So I built (well actually Claude Opus built) a fully functional, iOS-style calculator using **zero custom JavaScript functions** - just declarative cDOM and JPRX expressions.
|
|
19
|
+
|
|
20
|
+
## The Result
|
|
21
|
+
|
|
22
|
+
A fully functional, iOS-style interface is handled purely by the expression language. No hidden `script` tags, no helper functions.
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## The State Machine
|
|
27
|
+
|
|
28
|
+
A calculator feels stateless, but it's actually a strict state machine. You're never just "typing a number"; you're either entering the first operand, waiting for an operator, or typing the second operand.
|
|
29
|
+
|
|
30
|
+
We model this brain directly in the component's mount event:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
onmount: =state({
|
|
34
|
+
display: "0", // What you see on screen
|
|
35
|
+
expr: "", // History string (e.g., "8 + 5 =")
|
|
36
|
+
prev: "", // The value stored before an operation
|
|
37
|
+
op: "", // The active operator (+, -, *, /)
|
|
38
|
+
waiting: false // True when expecting new number input vs an operator
|
|
39
|
+
}, { name: "c", schema: "polymorphic", scope: $this })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This creates a reactive state object named `c`. We can read it anywhere with paths like `/c/display`.
|
|
43
|
+
|
|
44
|
+
## Solving the "10 Buttons" Problem
|
|
45
|
+
|
|
46
|
+
The classic DRY (Don't Repeat Yourself) challenge: I have 10 number buttons. Do I write 10 handlers? Do I write a loop? In React or Vue, you'd probably map over an array.
|
|
47
|
+
|
|
48
|
+
With JPRX, the DOM *is* the data key.
|
|
49
|
+
|
|
50
|
+
By giving each button an `id` (e.g., `id: "7"`), we write a **single logic expression** that adapts to whichever element triggered it. We just reference `$this.id`:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
{ button: {
|
|
54
|
+
id: "7",
|
|
55
|
+
class: "btn btn-number",
|
|
56
|
+
onclick: =set(/c, {
|
|
57
|
+
display: if(/c/waiting,
|
|
58
|
+
$this.id,
|
|
59
|
+
if(eq(/c/display, "0"),
|
|
60
|
+
$this.id,
|
|
61
|
+
concat(/c/display, $this.id)
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
waiting: false
|
|
65
|
+
}),
|
|
66
|
+
children: ["7"]
|
|
67
|
+
} }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Here's what is happening:
|
|
71
|
+
1. **Waiting for input?** (e.g., just hit `+`) → Replace the display with the button's ID.
|
|
72
|
+
2. **Displaying "0"?** → Replace it (avoids "07").
|
|
73
|
+
3. **Otherwise:** → Append the button's ID.
|
|
74
|
+
|
|
75
|
+
This works identically for every number button. No loops, no external helper functions.
|
|
76
|
+
|
|
77
|
+
## Operators and "Indirect" Values
|
|
78
|
+
|
|
79
|
+
This is where it gets tricky. When you click `+`, you can't just link `prev` to `display`. If you did, `prev` would update every time you selected a new digit for the *second* number, breaking the math.
|
|
80
|
+
|
|
81
|
+
We need a snapshot of the value *at that exact moment*.
|
|
82
|
+
|
|
83
|
+
Excel solves this with `INDIRECT`, effectively dereferencing a cell. JPRX borrows the same concept:
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
{ button: {
|
|
87
|
+
class: "btn btn-operator",
|
|
88
|
+
onclick: =set(/c, {
|
|
89
|
+
prev: indirect(/c/display), // Capture the value right NOW
|
|
90
|
+
expr: concat(/c/display, " +"),
|
|
91
|
+
op: "+",
|
|
92
|
+
waiting: true
|
|
93
|
+
}),
|
|
94
|
+
children: ["+"]
|
|
95
|
+
} }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`indirect(/c/display)` reads the path and returns its **value** at that exact moment. It essentially dereferences the pointer.
|
|
99
|
+
|
|
100
|
+
## The `calc()` Helper
|
|
101
|
+
|
|
102
|
+
Finally, the math. We need to say: *"Take the snapshot we stored, apply the current operator, and combine it with what's on screen now."*
|
|
103
|
+
|
|
104
|
+
This is the job of `calc()`. To keep the syntax clean, we use `$('path')` as a shorthand for capturing values right before evaluation.
|
|
105
|
+
|
|
106
|
+
> **Note:** `$('/path')` is just syntactic sugar for `indirect('/path')`.
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
{ button: {
|
|
110
|
+
class: "btn btn-equals",
|
|
111
|
+
onclick: =set(/c, {
|
|
112
|
+
display: if(eq(/c/op, ""),
|
|
113
|
+
/c/display, // No op? Do nothing.
|
|
114
|
+
calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))
|
|
115
|
+
),
|
|
116
|
+
expr: concat(/c/expr, " ", /c/display, " ="),
|
|
117
|
+
prev: "",
|
|
118
|
+
op: "",
|
|
119
|
+
waiting: true
|
|
120
|
+
}),
|
|
121
|
+
children: ["="]
|
|
122
|
+
} }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If `prev` is 8, `op` is `*`, and `display` is 5, `calc` receives the string `"8 * 5"` and evaluates it safely.
|
|
126
|
+
|
|
127
|
+
## The Complete cDOMC Source
|
|
128
|
+
|
|
129
|
+
Here is the full source code for the calculator in `.cdomc` format:
|
|
130
|
+
|
|
131
|
+
> **Note:** cDOMC (compressed cDOM) supports comments and does not require quoting property names, making it more readable and maintainable than strict JSON. Lightview also supports parsing cDOM from regular JSON with quotes, so you can use it with any JSON parser.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// Pure cDOM/JPRX Calculator - No custom JavaScript helpers needed!
|
|
135
|
+
// Uses only core JPRX helpers:
|
|
136
|
+
// - state, set: state management
|
|
137
|
+
// - if, eq: conditionals
|
|
138
|
+
// - concat, contains: string operations
|
|
139
|
+
// - negate, toPercent: math operations
|
|
140
|
+
// - calc with $(): expression evaluation
|
|
141
|
+
{
|
|
142
|
+
div: {
|
|
143
|
+
class: "calculator",
|
|
144
|
+
onmount: =state({
|
|
145
|
+
display: "0",
|
|
146
|
+
expr: "",
|
|
147
|
+
prev: "",
|
|
148
|
+
op: "",
|
|
149
|
+
waiting: false
|
|
150
|
+
}, { name: "c", schema: "polymorphic", scope: $this }),
|
|
151
|
+
children: [
|
|
152
|
+
// Display area
|
|
153
|
+
{
|
|
154
|
+
div: {
|
|
155
|
+
class: "display",
|
|
156
|
+
children: [
|
|
157
|
+
{ div: { class: "expression", children: [=/c/expr] } },
|
|
158
|
+
{ div: { class: "result", children: [=/c/display] } }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
// Button grid
|
|
163
|
+
{
|
|
164
|
+
div: {
|
|
165
|
+
class: "buttons",
|
|
166
|
+
children: [
|
|
167
|
+
// Row 1: AC, ±, %, ÷
|
|
168
|
+
{ button: { class: "btn btn-clear", onclick: =set(/c, { display: "0", expr: "", prev: "", op: "", waiting: false }), children: ["AC"] } },
|
|
169
|
+
{ button: { class: "btn btn-function", onclick: =set(/c, { display: negate(/c/display), waiting: true, expr: "" }), children: ["±"] } },
|
|
170
|
+
{ button: { class: "btn btn-function", onclick: =set(/c, { display: toPercent(/c/display), waiting: true, expr: "" }), children: ["%"] } },
|
|
171
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " ÷"), op: "/", waiting: true }), children: ["÷"] } },
|
|
172
|
+
|
|
173
|
+
// Row 2: 7, 8, 9, ×
|
|
174
|
+
{ 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"] } },
|
|
175
|
+
{ 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"] } },
|
|
176
|
+
{ 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"] } },
|
|
177
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " ×"), op: "*", waiting: true }), children: ["×"] } },
|
|
178
|
+
|
|
179
|
+
// Row 3: 4, 5, 6, −
|
|
180
|
+
{ 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"] } },
|
|
181
|
+
{ 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"] } },
|
|
182
|
+
{ 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"] } },
|
|
183
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " −"), op: "-", waiting: true }), children: ["−"] } },
|
|
184
|
+
|
|
185
|
+
// Row 4: 1, 2, 3, +
|
|
186
|
+
{ 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"] } },
|
|
187
|
+
{ 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"] } },
|
|
188
|
+
{ 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"] } },
|
|
189
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " +"), op: "+", waiting: true }), children: ["+"] } },
|
|
190
|
+
|
|
191
|
+
// Row 5: 0, ., =
|
|
192
|
+
{ 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"] } },
|
|
193
|
+
{ 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: ["."] } },
|
|
194
|
+
{ 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: ["="] } }
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
// Branding
|
|
199
|
+
{
|
|
200
|
+
div: {
|
|
201
|
+
class: "branding",
|
|
202
|
+
children: [
|
|
203
|
+
{ span: { children: ["Built with ", { a: { href: "https://github.com/anywhichway/lightview", target: "_blank", children: ["Lightview"] } }, " cDOM • No custom JS!"] } }
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Loading cDOM via Hypermedia
|
|
213
|
+
|
|
214
|
+
Lightview supports loading cDOM content from external files using its hypermedia capability. Simply add a `src` attribute to any element, and Lightview will fetch and hydrate the cDOM content:
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<!DOCTYPE html>
|
|
218
|
+
<html lang="en">
|
|
219
|
+
|
|
220
|
+
<head>
|
|
221
|
+
<meta charset="UTF-8">
|
|
222
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
223
|
+
<meta name="description"
|
|
224
|
+
content="A beautiful calculator built with Lightview cDOM and JPRX reactive expressions - no custom JavaScript!">
|
|
225
|
+
<title>Calculator | Lightview cDOM</title>
|
|
226
|
+
<link rel="stylesheet" href="calculator.css">
|
|
227
|
+
<!-- Load Lightview scripts -->
|
|
228
|
+
<script src="/lightview.js"></script> <!-- DOM as JSON and reactivity support -->
|
|
229
|
+
<script src="/lightview-x.js"></script> <!-- hypermedia support -->
|
|
230
|
+
<script src="/lightview-cdom.js"></script> <-- cDOM/JPRX support -->
|
|
231
|
+
</head>
|
|
232
|
+
|
|
233
|
+
<body>
|
|
234
|
+
<!-- The calculator cDOM is loaded via Lightview's hypermedia src attribute -->
|
|
235
|
+
<div id="app" src="./calculator.cdomc"></div>
|
|
236
|
+
</body>
|
|
237
|
+
|
|
238
|
+
</html>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The `src` attribute works like an HTML `<img>` or `<script>` tag—Lightview automatically fetches the `.cdomc` file, parses it, and renders the reactive content into the target element. This approach:
|
|
242
|
+
|
|
243
|
+
1. **Separates concerns:** Your HTML remains minimal while cDOM handles the component logic.
|
|
244
|
+
2. **Enables caching:** The browser can cache `.cdomc` files like any other static asset.
|
|
245
|
+
3. **Supports composition:** You can load different cDOM components into different parts of your page.
|
|
246
|
+
|
|
247
|
+
## Why Build This Way?
|
|
248
|
+
|
|
249
|
+
You might look at `concat("$('/c/prev') ...")` and ask: *Why in the world wouldn't you just write `parseFloat(prev) + parseFloat(curr)`?*
|
|
250
|
+
|
|
251
|
+
If you are a human writing code for yourself? You probably should. Lightview supports standard JS handlers for exactly that reason.
|
|
252
|
+
|
|
253
|
+
But if you are building infrastructure for **AI Agents**, the calculus changes. Sticking to a declarative, JSON-based path offers things raw code can't:
|
|
254
|
+
|
|
255
|
+
1. **Sandboxing:** It executes in a controlled environment. The logic can't access `window`, make global fetch requests, or execute arbitrary secondary code. This makes it safe to "hot swap" UI logic generated by an LLM in real-time.
|
|
256
|
+
2. **Portability:** This entire UI—logic and all—is just data. It can be sent from a server, stored in a database, or streamed from an AI model.
|
|
257
|
+
3. **Mental Model:** It forces a clear separation between state transformations and view structure, which is exactly how LLMs reason best.
|
|
258
|
+
|
|
259
|
+
This calculator proves that "declarative" doesn't have to mean "dumb." With the right primitives—state, conditionals, and path-based referencing—you can build rich, complex interactions without ever leaving the data structure.
|
|
260
|
+
|
|
261
|
+
> **A Note on Authorship:** This calculator wasn't hand-crafted. It was generated by **Claude Opus** after reading the [cDOM documentation](https://lightview.dev/docs/cdom). The fact that an AI could produce a robust, edge-case-handling calculator purely from documentation proves the point: cDOM and JPRX aren't just new syntax. They are a protocol for human-machine collaboration.
|
|
262
|
+
|
|
263
|
+
## The Bigger Picture
|
|
264
|
+
|
|
265
|
+
This series isn't just about a new library. It's about finding the right abstraction layer for the AI age.
|
|
266
|
+
|
|
267
|
+
**In [Part 1](https://hackernoon.com/the-future-of-ai-generated-ui-why-i-built-cdom-and-jprx)**, we looked at the security risks of letting LLMs write raw scripts and introduced the "Data as UI" philosophy.
|
|
268
|
+
|
|
269
|
+
**In this article**, we proved that "Data as UI" doesn't mean "dumb UI." We handled state, context, data snapshots, and math without executing a single line of imperative code.
|
|
270
|
+
|
|
271
|
+
cDOM defines structure. JPRX defines behavior. It’s reactivity without the compilation and UI without the security risks.
|
|
272
|
+
|
|
273
|
+
### Try It Yourself
|
|
274
|
+
|
|
275
|
+
The complete calculator is available at:
|
|
276
|
+
|
|
277
|
+
**[Live Demo](https://lightview.dev/docs/calculator.html)**
|
|
278
|
+
|
|
279
|
+
**[Source Code](https://github.com/anywhichway/lightview/blob/main/docs/calculator.html)**
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
*Lightview is an open-source reactive UI library. Learn more at [github.com/anywhichway/lightview](https://github.com/anywhichway/lightview).*
|