lightview 2.3.6 → 2.3.8
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 +8 -12
- package/docs/cdom-nav.html +37 -31
- package/docs/cdom-xpath.html +160 -0
- package/docs/cdom.html +72 -12
- package/jprx/index.js +2 -1
- package/jprx/package.json +1 -1
- package/jprx/parser.js +4 -2
- package/lightview-all.js +70 -58
- package/lightview-cdom.js +72 -55
- package/lightview.js +0 -4
- package/package.json +2 -2
- package/src/lightview-cdom.js +93 -62
- package/src/lightview.js +1 -4
- package/test-xpath-preprocess.js +0 -18
|
@@ -11,18 +11,12 @@ XPath support has been added to cDOM, allowing developers to navigate the DOM st
|
|
|
11
11
|
- **Reactivity:** None - becomes a static string value
|
|
12
12
|
- **Escape Sequence:** `'#` produces literal `#` string
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
```javascript
|
|
16
|
-
{
|
|
17
|
-
tag: "button",
|
|
18
|
-
attributes: {
|
|
19
|
-
"data-parent-id": "#../@id",
|
|
20
|
-
"aria-labelledby": "#../../@label-id"
|
|
21
|
-
},
|
|
22
|
-
children: ["#../@aria-label"]
|
|
14
|
+
children: ["#@aria-label"]
|
|
23
15
|
}
|
|
24
16
|
```
|
|
25
17
|
|
|
18
|
+
**Note on Text Nodes:** Even though the XPath refers to a child of the element, it is evaluated relative to the element itself (`self::*`). This improves consistency between attributes and children, and avoids "Operation is not supported" errors in browsers when using a Text node as an XPath context.
|
|
19
|
+
|
|
26
20
|
### 2. Reactive XPath (`=xpath()` helper)
|
|
27
21
|
- **Syntax:** `=xpath('expression')`
|
|
28
22
|
- **Evaluation:** As JPRX helper, returns computed signal
|
|
@@ -116,7 +110,7 @@ XPath support has been added to cDOM, allowing developers to navigate the DOM st
|
|
|
116
110
|
data-parent-id: #../@id,
|
|
117
111
|
class: "=concat(xpath('../@data-theme'), '-button')"
|
|
118
112
|
},
|
|
119
|
-
children: [
|
|
113
|
+
children: [#@aria-label]
|
|
120
114
|
}]
|
|
121
115
|
}
|
|
122
116
|
```
|
|
@@ -135,7 +129,7 @@ XPath support has been added to cDOM, allowing developers to navigate the DOM st
|
|
|
135
129
|
"data-parent-id": "#../@id",
|
|
136
130
|
"class": "=concat(xpath('../@data-theme'), '-button')"
|
|
137
131
|
},
|
|
138
|
-
"children": ["
|
|
132
|
+
"children": ["#@aria-label"]
|
|
139
133
|
}]
|
|
140
134
|
}
|
|
141
135
|
```
|
|
@@ -152,13 +146,15 @@ Open in browser to verify:
|
|
|
152
146
|
## Key Features
|
|
153
147
|
|
|
154
148
|
1. **Unquoted XPath in cDOMC**: Fully supported through structural integration with the parser character loop.
|
|
155
|
-
- ✅ Working: `children: [
|
|
149
|
+
- ✅ Working: `children: [#@id]`
|
|
156
150
|
- ✅ Working: `children: [#div[@id='1']]` (Complex paths with brackets now work!)
|
|
157
151
|
|
|
158
152
|
2. **Reactive xpath() helper**: Currently evaluates once via computed signal. Full reactivity with MutationObserver is TODO.
|
|
159
153
|
|
|
160
154
|
3. **XPath complexity**: Works with paths like `../@id`, `../../@attr`, and predicate-based paths.
|
|
161
155
|
|
|
156
|
+
4. **Consistency**: Both attributes and children evaluate XPath relative to the Element node. This avoids browser issues with Text node context and simplifies the developer's mental model.
|
|
157
|
+
|
|
162
158
|
## Implementation: Structural Parser Integration
|
|
163
159
|
|
|
164
160
|
Instead of a string preprocessor hack, XPath support is now integrated directly into the `parseCDOMC` word parser.
|
package/docs/cdom-nav.html
CHANGED
|
@@ -1,55 +1,61 @@
|
|
|
1
1
|
<nav class="docs-nav">
|
|
2
2
|
<div class="docs-nav-section">
|
|
3
3
|
<div class="docs-nav-title">cDOM</div>
|
|
4
|
-
<a href="#overview" class="docs-nav-link">Overview</a>
|
|
5
|
-
<a href="#simple-example" class="docs-nav-link">Simple Example</a>
|
|
6
|
-
<a href="
|
|
4
|
+
<a href="/docs/cdom.html#overview" class="docs-nav-link">Overview</a>
|
|
5
|
+
<a href="/docs/cdom.html#simple-example" class="docs-nav-link">Simple Example</a>
|
|
6
|
+
<a href="/docs/cdom-xpath.html" class="docs-nav-link">Using XPath</a>
|
|
7
|
+
<a href="/docs/cdom.html#advantages" class="docs-nav-link">Advantages</a>
|
|
7
8
|
</div>
|
|
8
9
|
<div class="docs-nav-section">
|
|
9
10
|
<div class="docs-nav-title">JPRX</div>
|
|
10
|
-
<a href="#JPRX" class="docs-nav-link">Introduction</a>
|
|
11
|
-
<a href="#JPRX-delimiters" class="docs-nav-link">Delimiters</a>
|
|
12
|
-
<a href="#JPRX-escaping" class="docs-nav-link">Escaping</a>
|
|
13
|
-
<a href="#JPRX-anatomy" class="docs-nav-link">Anatomy of a Path</a>
|
|
14
|
-
<a href="#JPRX-placeholders" class="docs-nav-link">Placeholders</a>
|
|
11
|
+
<a href="/docs/cdom.html#JPRX" class="docs-nav-link">Introduction</a>
|
|
12
|
+
<a href="/docs/cdom.html#JPRX-delimiters" class="docs-nav-link">Delimiters</a>
|
|
13
|
+
<a href="/docs/cdom.html#JPRX-escaping" class="docs-nav-link">Escaping</a>
|
|
14
|
+
<a href="/docs/cdom.html#JPRX-anatomy" class="docs-nav-link">Anatomy of a Path</a>
|
|
15
|
+
<a href="/docs/cdom.html#JPRX-placeholders" class="docs-nav-link">Placeholders</a>
|
|
15
16
|
</div>
|
|
16
17
|
<div class="docs-nav-section">
|
|
17
18
|
<div class="docs-nav-title">Concepts</div>
|
|
18
|
-
<a href="#comparison" class="docs-nav-link">Comparison to Excel</a>
|
|
19
|
-
<a href="#integration" class="docs-nav-link">Lightview Integration</a>
|
|
20
|
-
<a href="#dom-patches" class="docs-nav-link">Decentralized Layouts (=move)</a>
|
|
19
|
+
<a href="/docs/cdom.html#comparison" class="docs-nav-link">Comparison to Excel</a>
|
|
20
|
+
<a href="/docs/cdom.html#integration" class="docs-nav-link">Lightview Integration</a>
|
|
21
|
+
<a href="/docs/cdom.html#dom-patches" class="docs-nav-link">Decentralized Layouts (=move)</a>
|
|
21
22
|
</div>
|
|
22
23
|
<div class="docs-nav-section">
|
|
23
24
|
<div class="docs-nav-title">Examples</div>
|
|
24
|
-
<a href="#shopping-cart" class="docs-nav-link">Shopping Cart</a>
|
|
25
|
-
<a href="#interactive-example" class="docs-nav-link">Interactive Counter</a>
|
|
26
|
-
<a href="#operator-syntax" class="docs-nav-link">Operator Syntax</a>
|
|
25
|
+
<a href="/docs/cdom.html#shopping-cart" class="docs-nav-link">Shopping Cart</a>
|
|
26
|
+
<a href="/docs/cdom.html#interactive-example" class="docs-nav-link">Interactive Counter</a>
|
|
27
|
+
<a href="/docs/cdom.html#operator-syntax" class="docs-nav-link">Operator Syntax</a>
|
|
27
28
|
</div>
|
|
28
29
|
<div class="docs-nav-section">
|
|
29
30
|
<div class="docs-nav-title">Events</div>
|
|
30
|
-
<a href="#events" class="docs-nav-link">Events & Interaction</a>
|
|
31
|
-
<a href="#events-manual" class="docs-nav-link">Manual Implementation</a>
|
|
32
|
-
<a href="#events-llm" class="docs-nav-link">LLM-Generated</a>
|
|
33
|
-
<a href="#events-lifecycle" class="docs-nav-link">Interaction Lifecycle</a>
|
|
31
|
+
<a href="/docs/cdom.html#events" class="docs-nav-link">Events & Interaction</a>
|
|
32
|
+
<a href="/docs/cdom.html#events-manual" class="docs-nav-link">Manual Implementation</a>
|
|
33
|
+
<a href="/docs/cdom.html#events-llm" class="docs-nav-link">LLM-Generated</a>
|
|
34
|
+
<a href="/docs/cdom.html#events-lifecycle" class="docs-nav-link">Interaction Lifecycle</a>
|
|
34
35
|
</div>
|
|
35
36
|
<div class="docs-nav-section">
|
|
36
37
|
<div class="docs-nav-title">API</div>
|
|
37
|
-
<a href="#js-api" class="docs-nav-link">JavaScript API</a>
|
|
38
|
+
<a href="/docs/cdom.html#js-api" class="docs-nav-link">JavaScript API</a>
|
|
38
39
|
<div class="docs-nav-item">
|
|
39
|
-
<a href="#helpers" class="docs-nav-link">JPRX Helpers</a>
|
|
40
|
+
<a href="/docs/cdom.html#helpers" class="docs-nav-link">JPRX Helpers</a>
|
|
40
41
|
<div class="docs-nav-subsection" style="margin-left: 1rem; border-left: 1px solid var(--site-border);">
|
|
41
|
-
<a href="#helpers-math" class="docs-nav-link" style="font-size: 0.85em;">Math</a>
|
|
42
|
-
<a href="#helpers-stats" class="docs-nav-link" style="font-size: 0.85em;">Stats</a>
|
|
43
|
-
<a href="#helpers-string" class="docs-nav-link" style="font-size: 0.85em;">String</a>
|
|
44
|
-
<a href="#helpers-array" class="docs-nav-link" style="font-size: 0.85em;">Array</a>
|
|
45
|
-
<a href="#helpers-logic" class="docs-nav-link" style="font-size: 0.85em;">Logic &
|
|
46
|
-
|
|
42
|
+
<a href="/docs/cdom.html#helpers-math" class="docs-nav-link" style="font-size: 0.85em;">Math</a>
|
|
43
|
+
<a href="/docs/cdom.html#helpers-stats" class="docs-nav-link" style="font-size: 0.85em;">Stats</a>
|
|
44
|
+
<a href="/docs/cdom.html#helpers-string" class="docs-nav-link" style="font-size: 0.85em;">String</a>
|
|
45
|
+
<a href="/docs/cdom.html#helpers-array" class="docs-nav-link" style="font-size: 0.85em;">Array</a>
|
|
46
|
+
<a href="/docs/cdom.html#helpers-logic" class="docs-nav-link" style="font-size: 0.85em;">Logic &
|
|
47
|
+
Comparison</a>
|
|
48
|
+
<a href="/docs/cdom.html#helpers-conditional" class="docs-nav-link"
|
|
49
|
+
style="font-size: 0.85em;">Conditional
|
|
47
50
|
Aggregates</a>
|
|
48
|
-
<a href="#helpers-formatting" class="docs-nav-link"
|
|
49
|
-
|
|
50
|
-
<a href="#helpers-
|
|
51
|
-
<a href="#helpers-
|
|
52
|
-
<a href="#helpers-
|
|
51
|
+
<a href="/docs/cdom.html#helpers-formatting" class="docs-nav-link"
|
|
52
|
+
style="font-size: 0.85em;">Formatting</a>
|
|
53
|
+
<a href="/docs/cdom.html#helpers-datetime" class="docs-nav-link" style="font-size: 0.85em;">DateTime</a>
|
|
54
|
+
<a href="/docs/cdom.html#helpers-lookup" class="docs-nav-link" style="font-size: 0.85em;">Lookup</a>
|
|
55
|
+
<a href="/docs/cdom.html#helpers-mutation" class="docs-nav-link" style="font-size: 0.85em;">State
|
|
56
|
+
Mutation</a>
|
|
57
|
+
<a href="/docs/cdom.html#helpers-network" class="docs-nav-link" style="font-size: 0.85em;">Network</a>
|
|
58
|
+
<a href="/docs/cdom.html#helpers-dom" class="docs-nav-link" style="font-size: 0.85em;">DOM & XPath</a>
|
|
53
59
|
</div>
|
|
54
60
|
</div>
|
|
55
61
|
</div>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script src="/lightview-router.js?base=/index.html"></script>
|
|
2
|
+
|
|
3
|
+
<div class="docs-layout">
|
|
4
|
+
<aside class="docs-sidebar" src="/docs/cdom-nav.html"></aside>
|
|
5
|
+
|
|
6
|
+
<main class="docs-content">
|
|
7
|
+
<h1>XPath Navigation in cDOM</h1>
|
|
8
|
+
<p class="text-secondary" style="font-size: 1.125rem;">
|
|
9
|
+
Navigate and derive element properties from the existing DOM structure.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div class="experimental-notice"
|
|
13
|
+
style="margin: 1.5rem 0; padding: 1.25rem; border-radius: var(--site-radius); background: var(--site-accent-light); border: 1px solid var(--site-warning); color: var(--site-text);">
|
|
14
|
+
<div style="display: flex; gap: 0.75rem; align-items: flex-start;">
|
|
15
|
+
<span style="font-size: 1.5rem;">🚨</span>
|
|
16
|
+
<div>
|
|
17
|
+
<strong style="display: block; margin-bottom: 0.25rem; font-size: 1.1rem;">cDOM vs JPRX</strong>
|
|
18
|
+
<p style="margin: 0; font-size: 0.95rem; opacity: 0.9;">
|
|
19
|
+
Static XPaths are availbale to <strong>cDOM</strong> during the construction phase,
|
|
20
|
+
whereas <strong>JPRX</strong> has an <code>xpath</code> helper that returns a computed signal.
|
|
21
|
+
See <a href="/docs/cdom.html#helpers-dom">JPRX documentation</a> on the helper for more info
|
|
22
|
+
about xpath and reactivity.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<h2 id="overview">Overview</h2>
|
|
29
|
+
<p>
|
|
30
|
+
cDOM allows elements to derive their properties from the existing DOM tree. This can be done statically
|
|
31
|
+
for one-time setup or reactively for values that change over time.
|
|
32
|
+
</p>
|
|
33
|
+
<ol>
|
|
34
|
+
<li><strong>Static XPath (<code>#</code> prefix)</strong>: Available to cDOM. Evaluated once during DOM
|
|
35
|
+
construction.</li>
|
|
36
|
+
<li><strong>Reactive XPath (<code>=xpath()</code> helper)</strong>: Available to JPRX. Evaluates as an
|
|
37
|
+
expression and returns a computed signal.</li>
|
|
38
|
+
</ol>
|
|
39
|
+
|
|
40
|
+
<h2 id="static-xpath">Static XPath (<code>#</code> prefix)</h2>
|
|
41
|
+
<p>
|
|
42
|
+
Static XPath expressions are perfect for keeping your definitions <strong>DRY (Don't Repeat
|
|
43
|
+
Yourself)</strong>.
|
|
44
|
+
By referencing values like <code>id</code> or <code>class</code> from parent elements, you avoid duplicating
|
|
45
|
+
data in your cDOM structure.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
<div id="xpath-live-demo" style="margin: 2rem 0;">
|
|
50
|
+
<p class="text-sm font-bold opacity-60 uppercase mb-3">Live Interactive Example</p>
|
|
51
|
+
<pre><script>
|
|
52
|
+
examplify(document.currentScript.nextElementSibling, {
|
|
53
|
+
at: document.currentScript.parentElement,
|
|
54
|
+
scripts: ['/lightview.js', '/lightview-x.js', '/lightview-cdom.js'],
|
|
55
|
+
type: 'module',
|
|
56
|
+
height: '180px',
|
|
57
|
+
autoRun: true
|
|
58
|
+
});
|
|
59
|
+
</script><code>await import('/lightview-cdom.js');
|
|
60
|
+
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
61
|
+
const { $ } = Lightview;
|
|
62
|
+
|
|
63
|
+
const cdom = `{
|
|
64
|
+
div: {
|
|
65
|
+
id: "profile-container",
|
|
66
|
+
class: "card",
|
|
67
|
+
"data-theme": "dark",
|
|
68
|
+
children: [
|
|
69
|
+
{ h3: "User Profile" },
|
|
70
|
+
{ button: {
|
|
71
|
+
id: "7",
|
|
72
|
+
// XPath #@id gets "7" from this button's id
|
|
73
|
+
// XPath #../@id gets "profile-container" from the parent div
|
|
74
|
+
children: ["Button ", #@id, " in section ", #../@id]
|
|
75
|
+
}}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}`;
|
|
79
|
+
|
|
80
|
+
$('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<h2 id="allowed-axes">Allowed Axes (Backward-Looking Only)</h2>
|
|
84
|
+
<p>
|
|
85
|
+
To maintain high performance and safety during construction, cDOM only allows
|
|
86
|
+
<strong>backward-looking</strong>
|
|
87
|
+
XPath axes. You can reference nodes that are already constructed (parents, ancestors, earlier siblings).
|
|
88
|
+
</p>
|
|
89
|
+
<table class="api-table">
|
|
90
|
+
<thead>
|
|
91
|
+
<tr>
|
|
92
|
+
<th>Axis</th>
|
|
93
|
+
<th>Result</th>
|
|
94
|
+
</tr>
|
|
95
|
+
</thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
<tr>
|
|
98
|
+
<td><code>self::</code> / <code>.</code></td>
|
|
99
|
+
<td>The current node.</td>
|
|
100
|
+
</tr>
|
|
101
|
+
<tr>
|
|
102
|
+
<td><code>parent::</code> / <code>..</code></td>
|
|
103
|
+
<td>The parent element.</td>
|
|
104
|
+
</tr>
|
|
105
|
+
<tr>
|
|
106
|
+
<td><code>ancestor::</code></td>
|
|
107
|
+
<td>Higher-level ancestors.</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<tr>
|
|
110
|
+
<td><code>preceding-sibling::</code></td>
|
|
111
|
+
<td>Elements that appear before the current one in the same parent.</td>
|
|
112
|
+
</tr>
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
<p class="text-warning">
|
|
116
|
+
<strong>Forbidden:</strong> <code>child::</code>, <code>descendant::</code>, and <code>following::</code>
|
|
117
|
+
axes are disabled as they could create infinite loops or reference nodes that do not exist yet.
|
|
118
|
+
</p>
|
|
119
|
+
|
|
120
|
+
<h2 id="reactive-xpath">Reactive XPath (<code>=xpath()</code>)</h2>
|
|
121
|
+
<p>
|
|
122
|
+
If you need an XPath expression to update reactively if attributes elsewhere in the DOM change,
|
|
123
|
+
use the <code>=xpath()</code> helper within a JPRX expression.
|
|
124
|
+
</p>
|
|
125
|
+
<div class="code-block">
|
|
126
|
+
<pre><code>{
|
|
127
|
+
div: {
|
|
128
|
+
title: "=xpath('../@data-section')",
|
|
129
|
+
class: "=concat('item ', xpath('../@theme'))"
|
|
130
|
+
}
|
|
131
|
+
}</code></pre>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<h2 id="cdomc">Concise cDOM (cDOMC) Support</h2>
|
|
135
|
+
<p>
|
|
136
|
+
In cDOMC (the shorthand format used in <code>.cdomc</code> files), the parser is <strong>structurally
|
|
137
|
+
aware</strong>
|
|
138
|
+
of XPath expressions. You do not need to quote them even if they contain square brackets or spaces.
|
|
139
|
+
</p>
|
|
140
|
+
<div class="code-block">
|
|
141
|
+
<pre><code>// Clean unquoted syntax in .cdomc files
|
|
142
|
+
{
|
|
143
|
+
button: {
|
|
144
|
+
id: "7",
|
|
145
|
+
children: [#../@id]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Support for complex paths with predicates
|
|
150
|
+
{ span: [#ancestor::div[@data-role='container']/@title] }</code></pre>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<h2 id="safety">Security & Safety</h2>
|
|
154
|
+
<p>
|
|
155
|
+
XPath navigation in cDOM is inherently safer than using arbitrary JavaScript for DOM traversal.
|
|
156
|
+
It provides a restricted, read-only view of the existing structural tree without the risks
|
|
157
|
+
of code injection or side effects.
|
|
158
|
+
</p>
|
|
159
|
+
</main>
|
|
160
|
+
</div>
|
package/docs/cdom.html
CHANGED
|
@@ -108,8 +108,9 @@
|
|
|
108
108
|
<p>
|
|
109
109
|
cDOM uses <strong>JPRX (JSON Pointer Reactive eXpressions)</strong> as its expression language. JPRX
|
|
110
110
|
extends <a href="https://www.rfc-editor.org/rfc/rfc6901" target="_blank">JSON Pointer (RFC 6901)</a>
|
|
111
|
-
with reactivity, relative paths, and helper functions.
|
|
112
|
-
|
|
111
|
+
with reactivity, relative paths, and helper functions. cDOM also supports <strong>standard XPath</strong>
|
|
112
|
+
for powerful DOM navigation during element construction. Together with deep integration for
|
|
113
|
+
<a href="https://json-schema.org/" target="_blank">JSON Schema</a> (Standard Draft 7+), cDOM provides
|
|
113
114
|
industrial-strength data validation and automatic type coercion.
|
|
114
115
|
</p>
|
|
115
116
|
|
|
@@ -156,8 +157,57 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
|
156
157
|
<p>
|
|
157
158
|
The UI automatically updates whenever <code>count</code> changes — no manual DOM manipulation required.
|
|
158
159
|
</p>
|
|
160
|
+
|
|
161
|
+
<!-- ===== XPATH NAVIGATION ===== -->
|
|
162
|
+
<h2 id="xpath-navigation">Using XPath</h2>
|
|
159
163
|
<p>
|
|
160
|
-
|
|
164
|
+
cDOM allows elements to navigate and reference the DOM structure during construction using standard
|
|
165
|
+
<strong>XPath</strong>. This is strictly a <strong>cDOM feature</strong> (not JPRX) used for structural
|
|
166
|
+
navigation.
|
|
167
|
+
</p>
|
|
168
|
+
<p>
|
|
169
|
+
XPath is incredibly useful for keeping your definitions <strong>DRY (Don't Repeat Yourself)</strong> by
|
|
170
|
+
referencing existing attributes instead of repeating values.
|
|
171
|
+
</p>
|
|
172
|
+
<div id="xpath-demo">
|
|
173
|
+
<pre><script>
|
|
174
|
+
examplify(document.currentScript.nextElementSibling, {
|
|
175
|
+
at: document.currentScript.parentElement,
|
|
176
|
+
scripts: ['/lightview.js', '/lightview-x.js', '/lightview-cdom.js'],
|
|
177
|
+
type: 'module',
|
|
178
|
+
height: '180px',
|
|
179
|
+
autoRun: true,
|
|
180
|
+
controls: false
|
|
181
|
+
});
|
|
182
|
+
</script><code>await import('/lightview-cdom.js');
|
|
183
|
+
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
184
|
+
const { $ } = Lightview;
|
|
185
|
+
|
|
186
|
+
const cdom = `{
|
|
187
|
+
div: {
|
|
188
|
+
id: "profile-container",
|
|
189
|
+
class: "card",
|
|
190
|
+
"data-theme": "dark",
|
|
191
|
+
children: [
|
|
192
|
+
{ h3: "User Profile" },
|
|
193
|
+
{ button: {
|
|
194
|
+
id: "7",
|
|
195
|
+
// XPath #../@id gets the "7" from this button's id
|
|
196
|
+
// XPath #../../@id gets "profile-container" from the g-parent div
|
|
197
|
+
children: ["Button ", #../@id, " in section ", #../../@id]
|
|
198
|
+
}}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}`;
|
|
202
|
+
|
|
203
|
+
$('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
204
|
+
</div>
|
|
205
|
+
<p>
|
|
206
|
+
In the example above, the button's text is derived entirely from its own <code>id</code> and its
|
|
207
|
+
parent's <code>id</code> using <code>#../@id</code> and <code>#../../@id</code>.
|
|
208
|
+
</p>
|
|
209
|
+
<p>
|
|
210
|
+
For more details, see the <a href="/docs/cdom-xpath.html">Full XPath Documentation</a>.
|
|
161
211
|
</p>
|
|
162
212
|
|
|
163
213
|
<!-- ===== ADVANTAGES ===== -->
|
|
@@ -596,7 +646,7 @@ const cdomString = `{
|
|
|
596
646
|
children: [
|
|
597
647
|
{ h3: "Shopping Cart" },
|
|
598
648
|
{ ul: {
|
|
599
|
-
children: =map(/store/cart/items, { li: { children: [_/name, " - ",
|
|
649
|
+
children: =map(/store/cart/items, { li: { children: [_/name, " - ", currency(_/price)] } })
|
|
600
650
|
}},
|
|
601
651
|
{ p: {
|
|
602
652
|
style: "font-weight: bold; margin-top: 1rem;",
|
|
@@ -1007,10 +1057,23 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
1007
1057
|
|
|
1008
1058
|
<h3 id="helpers-network">Network</h3>
|
|
1009
1059
|
<p>HTTP requests.</p>
|
|
1010
|
-
<div class="code-block">
|
|
1060
|
+
<div class="code-block" style="margin-bottom: 2rem;">
|
|
1011
1061
|
<pre><code>fetch(url, options?)
|
|
1012
1062
|
<span id="helpers-mount"></span>mount(url, options?)</span></code></pre>
|
|
1013
1063
|
</div>
|
|
1064
|
+
|
|
1065
|
+
<h3 id="helpers-dom">DOM & XPath</h3>
|
|
1066
|
+
<p>Structural navigation and manipulation. For fetching and mounting remote content, see the
|
|
1067
|
+
<a href="#helpers-network">mount()</a> helper in the Network section.
|
|
1068
|
+
</p>
|
|
1069
|
+
<div class="code-block">
|
|
1070
|
+
<pre><code>move(selector, location?), xpath(expression)</code></pre>
|
|
1071
|
+
</div>
|
|
1072
|
+
<p style="margin-top: 1rem;">
|
|
1073
|
+
While <code>move</code> is for local placement, <code>mount</code> is used for remote Hypermedia updates.
|
|
1074
|
+
It fetches content (cDOM, vDOM, or oDOM) and injects it into the DOM. If the content contains a
|
|
1075
|
+
<code>=move</code> helper, it will automatically relocate itself upon mounting.
|
|
1076
|
+
</p>
|
|
1014
1077
|
<table class="api-table">
|
|
1015
1078
|
<thead>
|
|
1016
1079
|
<tr>
|
|
@@ -1049,13 +1112,10 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
1049
1112
|
</tr>
|
|
1050
1113
|
</tbody>
|
|
1051
1114
|
</table>
|
|
1052
|
-
<p style="margin-top:
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1115
|
+
<p class="text-xs italic opacity-70" style="margin-top: 0.5rem;">
|
|
1116
|
+
<strong>xpath(expression):</strong> Returns a reactive computed signal based on the DOM structure
|
|
1117
|
+
at evaluation time. Note that full MutationObserver reactivity is currently a TODO.
|
|
1118
|
+
For static DOM navigation, use the <code>#</code> prefix in cDOM.
|
|
1056
1119
|
</p>
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
1120
|
</main>
|
|
1061
1121
|
</div>
|
package/jprx/index.js
CHANGED
package/jprx/package.json
CHANGED
package/jprx/parser.js
CHANGED
|
@@ -1409,6 +1409,7 @@ export const parseExpression = (expr, context) => {
|
|
|
1409
1409
|
* Supports unquoted keys/values and strictly avoids 'eval'.
|
|
1410
1410
|
*/
|
|
1411
1411
|
export const parseCDOMC = (input) => {
|
|
1412
|
+
if (typeof input !== 'string') return input;
|
|
1412
1413
|
let i = 0;
|
|
1413
1414
|
const len = input.length;
|
|
1414
1415
|
|
|
@@ -1666,6 +1667,7 @@ export const parseCDOMC = (input) => {
|
|
|
1666
1667
|
* @returns {object} - Parsed JSON object
|
|
1667
1668
|
*/
|
|
1668
1669
|
export const parseJPRX = (input) => {
|
|
1670
|
+
if (typeof input !== 'string') return input;
|
|
1669
1671
|
let result = '';
|
|
1670
1672
|
let i = 0;
|
|
1671
1673
|
const len = input.length;
|
|
@@ -1791,9 +1793,9 @@ export const parseJPRX = (input) => {
|
|
|
1791
1793
|
}
|
|
1792
1794
|
|
|
1793
1795
|
// Handle unquoted property names, identifiers, paths, and FUNCTION CALLS
|
|
1794
|
-
if (/[a-zA-Z_
|
|
1796
|
+
if (/[a-zA-Z_$\/.\/]/.test(char)) {
|
|
1795
1797
|
let word = '';
|
|
1796
|
-
while (i < len && /[a-zA-Z0-9_
|
|
1798
|
+
while (i < len && /[a-zA-Z0-9_$\/.-]/.test(input[i])) {
|
|
1797
1799
|
word += input[i];
|
|
1798
1800
|
i++;
|
|
1799
1801
|
}
|
package/lightview-all.js
CHANGED
|
@@ -645,7 +645,6 @@
|
|
|
645
645
|
return reactiveAttrs;
|
|
646
646
|
};
|
|
647
647
|
const processChildren = (children, targetNode, clearExisting = true) => {
|
|
648
|
-
var _a2;
|
|
649
648
|
if (clearExisting && targetNode.innerHTML !== void 0) {
|
|
650
649
|
targetNode.innerHTML = "";
|
|
651
650
|
}
|
|
@@ -718,9 +717,6 @@
|
|
|
718
717
|
childElements.push(childEl);
|
|
719
718
|
}
|
|
720
719
|
}
|
|
721
|
-
if (typeof ((_a2 = globalThis.LightviewCDOM) == null ? void 0 : _a2.resolveStaticXPath) === "function") {
|
|
722
|
-
globalThis.LightviewCDOM.resolveStaticXPath(targetNode);
|
|
723
|
-
}
|
|
724
720
|
return childElements;
|
|
725
721
|
};
|
|
726
722
|
const setupChildrenInTarget = (children, targetNode) => {
|
|
@@ -3200,6 +3196,7 @@
|
|
|
3200
3196
|
return LV.computed(() => resolveExpression$1(expr, context));
|
|
3201
3197
|
};
|
|
3202
3198
|
const parseCDOMC = (input) => {
|
|
3199
|
+
if (typeof input !== "string") return input;
|
|
3203
3200
|
let i = 0;
|
|
3204
3201
|
const len2 = input.length;
|
|
3205
3202
|
const skipWhitespace = () => {
|
|
@@ -3427,6 +3424,7 @@
|
|
|
3427
3424
|
};
|
|
3428
3425
|
const parseJPRX = (input) => {
|
|
3429
3426
|
var _a2, _b2;
|
|
3427
|
+
if (typeof input !== "string") return input;
|
|
3430
3428
|
let result = "";
|
|
3431
3429
|
let i = 0;
|
|
3432
3430
|
const len2 = input.length;
|
|
@@ -3526,9 +3524,9 @@
|
|
|
3526
3524
|
result += JSON.stringify(expr);
|
|
3527
3525
|
continue;
|
|
3528
3526
|
}
|
|
3529
|
-
if (/[a-zA-Z_
|
|
3527
|
+
if (/[a-zA-Z_$\/.\/]/.test(char)) {
|
|
3530
3528
|
let word = "";
|
|
3531
|
-
while (i < len2 && /[a-zA-Z0-9_
|
|
3529
|
+
while (i < len2 && /[a-zA-Z0-9_$\/.-]/.test(input[i])) {
|
|
3532
3530
|
word += input[i];
|
|
3533
3531
|
i++;
|
|
3534
3532
|
}
|
|
@@ -5785,7 +5783,7 @@
|
|
|
5785
5783
|
const isCDOM = contentType.includes("application/cdom") || contentType.includes("application/jprx") || contentType.includes("application/vdom") || contentType.includes("application/odom") || url.endsWith(".cdom") || url.endsWith(".jprx") || url.endsWith(".vdom") || url.endsWith(".odom");
|
|
5786
5784
|
if (isCDOM || contentType.includes("application/json") && text.trim().startsWith("{")) {
|
|
5787
5785
|
try {
|
|
5788
|
-
content = hydrate(
|
|
5786
|
+
content = hydrate(parseCDOMC(text));
|
|
5789
5787
|
} catch (e) {
|
|
5790
5788
|
}
|
|
5791
5789
|
}
|
|
@@ -5961,77 +5959,89 @@
|
|
|
5961
5959
|
node[key] = hydrate(value, node);
|
|
5962
5960
|
}
|
|
5963
5961
|
}
|
|
5962
|
+
if (!parent && node.tag) {
|
|
5963
|
+
node.attributes = node.attributes || {};
|
|
5964
|
+
const originalOnMount = node.attributes.onmount;
|
|
5965
|
+
node.attributes.onmount = (el) => {
|
|
5966
|
+
if (typeof originalOnMount === "function") originalOnMount(el);
|
|
5967
|
+
resolveStaticXPath(el);
|
|
5968
|
+
};
|
|
5969
|
+
}
|
|
5964
5970
|
return node;
|
|
5965
5971
|
};
|
|
5966
5972
|
const validateXPath = (xpath) => {
|
|
5973
|
+
if (!xpath) return;
|
|
5967
5974
|
const forbiddenAxes = /\b(child|descendant|following|following-sibling)::/;
|
|
5968
5975
|
if (forbiddenAxes.test(xpath)) {
|
|
5969
5976
|
throw new Error(`XPath: Forward-looking axes not allowed during DOM construction: ${xpath}`);
|
|
5970
5977
|
}
|
|
5971
|
-
const hasShorthandChild = /\/[a-zA-Z]/.test(xpath) && !xpath.startsWith("/html");
|
|
5978
|
+
const hasShorthandChild = /\/(?![@.])(?![a-zA-Z0-9_-]+::)[a-zA-Z]/.test(xpath) && !xpath.startsWith("/html");
|
|
5972
5979
|
if (hasShorthandChild) {
|
|
5973
5980
|
throw new Error(`XPath: Shorthand child axis (/) not allowed during DOM construction: ${xpath}`);
|
|
5974
5981
|
}
|
|
5975
5982
|
};
|
|
5976
|
-
const
|
|
5977
|
-
var _a2
|
|
5978
|
-
|
|
5979
|
-
const
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
);
|
|
5983
|
-
const nodesToProcess = [];
|
|
5984
|
-
let node = walker.nextNode();
|
|
5985
|
-
while (node) {
|
|
5986
|
-
nodesToProcess.push(node);
|
|
5987
|
-
node = walker.nextNode();
|
|
5988
|
-
}
|
|
5989
|
-
for (const node2 of nodesToProcess) {
|
|
5990
|
-
if (node2.nodeType === Node.ELEMENT_NODE) {
|
|
5991
|
-
const attributes = [...node2.attributes];
|
|
5992
|
-
for (const attr of attributes) {
|
|
5993
|
-
if (attr.name.startsWith("data-xpath-")) {
|
|
5994
|
-
const realAttr = attr.name.replace("data-xpath-", "");
|
|
5995
|
-
const xpath = attr.value;
|
|
5996
|
-
try {
|
|
5997
|
-
validateXPath(xpath);
|
|
5998
|
-
const result = document.evaluate(
|
|
5999
|
-
xpath,
|
|
6000
|
-
node2,
|
|
6001
|
-
null,
|
|
6002
|
-
XPathResult.STRING_TYPE,
|
|
6003
|
-
null
|
|
6004
|
-
);
|
|
6005
|
-
node2.setAttribute(realAttr, result.stringValue);
|
|
6006
|
-
node2.removeAttribute(attr.name);
|
|
6007
|
-
} catch (e) {
|
|
6008
|
-
(_a2 = globalThis.console) == null ? void 0 : _a2.error(`[Lightview-CDOM] XPath resolution failed for attribute "${realAttr}":`, e.message);
|
|
6009
|
-
}
|
|
6010
|
-
}
|
|
6011
|
-
}
|
|
6012
|
-
}
|
|
6013
|
-
if (node2.__xpathExpr) {
|
|
6014
|
-
const xpath = node2.__xpathExpr;
|
|
5983
|
+
const resolveAttributeXPaths = (el) => {
|
|
5984
|
+
var _a2;
|
|
5985
|
+
const attributes = [...el.attributes];
|
|
5986
|
+
for (const attr of attributes) {
|
|
5987
|
+
if (attr.name.startsWith("data-xpath-")) {
|
|
5988
|
+
const realAttr = attr.name.replace("data-xpath-", "");
|
|
6015
5989
|
try {
|
|
6016
|
-
validateXPath(
|
|
6017
|
-
const
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
5990
|
+
validateXPath(attr.value);
|
|
5991
|
+
const doc = globalThis.document || el.ownerDocument;
|
|
5992
|
+
const result = doc.evaluate(
|
|
5993
|
+
attr.value,
|
|
5994
|
+
el,
|
|
6021
5995
|
null,
|
|
6022
5996
|
XPathResult.STRING_TYPE,
|
|
6023
5997
|
null
|
|
6024
5998
|
);
|
|
6025
|
-
|
|
6026
|
-
|
|
5999
|
+
el.setAttribute(realAttr, result.stringValue);
|
|
6000
|
+
el.removeAttribute(attr.name);
|
|
6027
6001
|
} catch (e) {
|
|
6028
|
-
(
|
|
6002
|
+
(_a2 = globalThis.console) == null ? void 0 : _a2.error(`[Lightview-CDOM] XPath attribute error ("${realAttr}") at <${el.tagName.toLowerCase()} id="${el.id}">:`, e.message);
|
|
6029
6003
|
}
|
|
6030
6004
|
}
|
|
6031
6005
|
}
|
|
6032
6006
|
};
|
|
6007
|
+
const resolveTextNodeXPath = (node) => {
|
|
6008
|
+
var _a2, _b2, _c;
|
|
6009
|
+
if (!node.__xpathExpr) return;
|
|
6010
|
+
const xpath = node.__xpathExpr;
|
|
6011
|
+
try {
|
|
6012
|
+
validateXPath(xpath);
|
|
6013
|
+
const doc = globalThis.document || node.ownerDocument;
|
|
6014
|
+
const contextNode = node.parentNode || node;
|
|
6015
|
+
const result = doc.evaluate(
|
|
6016
|
+
xpath,
|
|
6017
|
+
contextNode,
|
|
6018
|
+
null,
|
|
6019
|
+
XPathResult.STRING_TYPE,
|
|
6020
|
+
null
|
|
6021
|
+
);
|
|
6022
|
+
node.textContent = result.stringValue;
|
|
6023
|
+
} catch (e) {
|
|
6024
|
+
(_c = globalThis.console) == null ? void 0 : _c.error(`[Lightview-CDOM] XPath text node error on <${(_a2 = node.parentNode) == null ? void 0 : _a2.tagName.toLowerCase()} id="${(_b2 = node.parentNode) == null ? void 0 : _b2.id}">:`, e.message);
|
|
6025
|
+
} finally {
|
|
6026
|
+
delete node.__xpathExpr;
|
|
6027
|
+
}
|
|
6028
|
+
};
|
|
6029
|
+
const resolveStaticXPath = (rootNode) => {
|
|
6030
|
+
const node = rootNode instanceof Node ? rootNode : (rootNode == null ? void 0 : rootNode.domEl) || rootNode;
|
|
6031
|
+
if (!node || !node.nodeType) return;
|
|
6032
|
+
if (node.nodeType === Node.ELEMENT_NODE) resolveAttributeXPaths(node);
|
|
6033
|
+
resolveTextNodeXPath(node);
|
|
6034
|
+
const doc = globalThis.document || node.ownerDocument;
|
|
6035
|
+
const walker = doc.createTreeWalker(node, NodeFilter.SHOW_ALL);
|
|
6036
|
+
let current = walker.nextNode();
|
|
6037
|
+
while (current) {
|
|
6038
|
+
if (current.nodeType === Node.ELEMENT_NODE) resolveAttributeXPaths(current);
|
|
6039
|
+
resolveTextNodeXPath(current);
|
|
6040
|
+
current = walker.nextNode();
|
|
6041
|
+
}
|
|
6042
|
+
};
|
|
6033
6043
|
if (typeof parseCDOMC !== "function") throw new Error("parseCDOMC not found");
|
|
6034
|
-
if (typeof parseJPRX !== "function") throw new Error("
|
|
6044
|
+
if (typeof parseJPRX !== "function") throw new Error("oldParseJPRX not found");
|
|
6035
6045
|
const LightviewCDOM = {
|
|
6036
6046
|
registerHelper,
|
|
6037
6047
|
registerOperator,
|
|
@@ -6040,7 +6050,9 @@
|
|
|
6040
6050
|
resolvePathAsContext,
|
|
6041
6051
|
resolveExpression: resolveExpression$1,
|
|
6042
6052
|
parseCDOMC,
|
|
6043
|
-
parseJPRX,
|
|
6053
|
+
parseJPRX: parseCDOMC,
|
|
6054
|
+
// Alias parseJPRX to the more robust parseCDOMC
|
|
6055
|
+
oldParseJPRX: parseJPRX,
|
|
6044
6056
|
unwrapSignal,
|
|
6045
6057
|
getContext,
|
|
6046
6058
|
handleCDOMState: () => {
|
|
@@ -6050,7 +6062,7 @@
|
|
|
6050
6062
|
activate,
|
|
6051
6063
|
hydrate,
|
|
6052
6064
|
resolveStaticXPath,
|
|
6053
|
-
version: "1.
|
|
6065
|
+
version: "1.1.0"
|
|
6054
6066
|
};
|
|
6055
6067
|
if (typeof window !== "undefined") {
|
|
6056
6068
|
globalThis.LightviewCDOM = {};
|
package/lightview-cdom.js
CHANGED
|
@@ -1018,6 +1018,7 @@ var LightviewCDOM = function(exports) {
|
|
|
1018
1018
|
return LV.computed(() => resolveExpression$1(expr, context));
|
|
1019
1019
|
};
|
|
1020
1020
|
const parseCDOMC = (input) => {
|
|
1021
|
+
if (typeof input !== "string") return input;
|
|
1021
1022
|
let i = 0;
|
|
1022
1023
|
const len2 = input.length;
|
|
1023
1024
|
const skipWhitespace = () => {
|
|
@@ -1245,6 +1246,7 @@ var LightviewCDOM = function(exports) {
|
|
|
1245
1246
|
};
|
|
1246
1247
|
const parseJPRX = (input) => {
|
|
1247
1248
|
var _a, _b;
|
|
1249
|
+
if (typeof input !== "string") return input;
|
|
1248
1250
|
let result = "";
|
|
1249
1251
|
let i = 0;
|
|
1250
1252
|
const len2 = input.length;
|
|
@@ -1344,9 +1346,9 @@ var LightviewCDOM = function(exports) {
|
|
|
1344
1346
|
result += JSON.stringify(expr);
|
|
1345
1347
|
continue;
|
|
1346
1348
|
}
|
|
1347
|
-
if (/[a-zA-Z_
|
|
1349
|
+
if (/[a-zA-Z_$\/.\/]/.test(char)) {
|
|
1348
1350
|
let word = "";
|
|
1349
|
-
while (i < len2 && /[a-zA-Z0-9_
|
|
1351
|
+
while (i < len2 && /[a-zA-Z0-9_$\/.-]/.test(input[i])) {
|
|
1350
1352
|
word += input[i];
|
|
1351
1353
|
i++;
|
|
1352
1354
|
}
|
|
@@ -3627,7 +3629,7 @@ var LightviewCDOM = function(exports) {
|
|
|
3627
3629
|
const isCDOM = contentType.includes("application/cdom") || contentType.includes("application/jprx") || contentType.includes("application/vdom") || contentType.includes("application/odom") || url.endsWith(".cdom") || url.endsWith(".jprx") || url.endsWith(".vdom") || url.endsWith(".odom");
|
|
3628
3630
|
if (isCDOM || contentType.includes("application/json") && text.trim().startsWith("{")) {
|
|
3629
3631
|
try {
|
|
3630
|
-
content = hydrate(
|
|
3632
|
+
content = hydrate(parseCDOMC(text));
|
|
3631
3633
|
} catch (e) {
|
|
3632
3634
|
}
|
|
3633
3635
|
}
|
|
@@ -3803,77 +3805,89 @@ var LightviewCDOM = function(exports) {
|
|
|
3803
3805
|
node[key] = hydrate(value, node);
|
|
3804
3806
|
}
|
|
3805
3807
|
}
|
|
3808
|
+
if (!parent && node.tag) {
|
|
3809
|
+
node.attributes = node.attributes || {};
|
|
3810
|
+
const originalOnMount = node.attributes.onmount;
|
|
3811
|
+
node.attributes.onmount = (el) => {
|
|
3812
|
+
if (typeof originalOnMount === "function") originalOnMount(el);
|
|
3813
|
+
resolveStaticXPath(el);
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3806
3816
|
return node;
|
|
3807
3817
|
};
|
|
3808
3818
|
const validateXPath = (xpath) => {
|
|
3819
|
+
if (!xpath) return;
|
|
3809
3820
|
const forbiddenAxes = /\b(child|descendant|following|following-sibling)::/;
|
|
3810
3821
|
if (forbiddenAxes.test(xpath)) {
|
|
3811
3822
|
throw new Error(`XPath: Forward-looking axes not allowed during DOM construction: ${xpath}`);
|
|
3812
3823
|
}
|
|
3813
|
-
const hasShorthandChild = /\/[a-zA-Z]/.test(xpath) && !xpath.startsWith("/html");
|
|
3824
|
+
const hasShorthandChild = /\/(?![@.])(?![a-zA-Z0-9_-]+::)[a-zA-Z]/.test(xpath) && !xpath.startsWith("/html");
|
|
3814
3825
|
if (hasShorthandChild) {
|
|
3815
3826
|
throw new Error(`XPath: Shorthand child axis (/) not allowed during DOM construction: ${xpath}`);
|
|
3816
3827
|
}
|
|
3817
3828
|
};
|
|
3818
|
-
const
|
|
3819
|
-
var _a
|
|
3820
|
-
|
|
3821
|
-
const
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
);
|
|
3825
|
-
const nodesToProcess = [];
|
|
3826
|
-
let node = walker.nextNode();
|
|
3827
|
-
while (node) {
|
|
3828
|
-
nodesToProcess.push(node);
|
|
3829
|
-
node = walker.nextNode();
|
|
3830
|
-
}
|
|
3831
|
-
for (const node2 of nodesToProcess) {
|
|
3832
|
-
if (node2.nodeType === Node.ELEMENT_NODE) {
|
|
3833
|
-
const attributes = [...node2.attributes];
|
|
3834
|
-
for (const attr of attributes) {
|
|
3835
|
-
if (attr.name.startsWith("data-xpath-")) {
|
|
3836
|
-
const realAttr = attr.name.replace("data-xpath-", "");
|
|
3837
|
-
const xpath = attr.value;
|
|
3838
|
-
try {
|
|
3839
|
-
validateXPath(xpath);
|
|
3840
|
-
const result = document.evaluate(
|
|
3841
|
-
xpath,
|
|
3842
|
-
node2,
|
|
3843
|
-
null,
|
|
3844
|
-
XPathResult.STRING_TYPE,
|
|
3845
|
-
null
|
|
3846
|
-
);
|
|
3847
|
-
node2.setAttribute(realAttr, result.stringValue);
|
|
3848
|
-
node2.removeAttribute(attr.name);
|
|
3849
|
-
} catch (e) {
|
|
3850
|
-
(_a = globalThis.console) == null ? void 0 : _a.error(`[Lightview-CDOM] XPath resolution failed for attribute "${realAttr}":`, e.message);
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
}
|
|
3854
|
-
}
|
|
3855
|
-
if (node2.__xpathExpr) {
|
|
3856
|
-
const xpath = node2.__xpathExpr;
|
|
3829
|
+
const resolveAttributeXPaths = (el) => {
|
|
3830
|
+
var _a;
|
|
3831
|
+
const attributes = [...el.attributes];
|
|
3832
|
+
for (const attr of attributes) {
|
|
3833
|
+
if (attr.name.startsWith("data-xpath-")) {
|
|
3834
|
+
const realAttr = attr.name.replace("data-xpath-", "");
|
|
3857
3835
|
try {
|
|
3858
|
-
validateXPath(
|
|
3859
|
-
const
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3836
|
+
validateXPath(attr.value);
|
|
3837
|
+
const doc = globalThis.document || el.ownerDocument;
|
|
3838
|
+
const result = doc.evaluate(
|
|
3839
|
+
attr.value,
|
|
3840
|
+
el,
|
|
3863
3841
|
null,
|
|
3864
3842
|
XPathResult.STRING_TYPE,
|
|
3865
3843
|
null
|
|
3866
3844
|
);
|
|
3867
|
-
|
|
3868
|
-
|
|
3845
|
+
el.setAttribute(realAttr, result.stringValue);
|
|
3846
|
+
el.removeAttribute(attr.name);
|
|
3869
3847
|
} catch (e) {
|
|
3870
|
-
(
|
|
3848
|
+
(_a = globalThis.console) == null ? void 0 : _a.error(`[Lightview-CDOM] XPath attribute error ("${realAttr}") at <${el.tagName.toLowerCase()} id="${el.id}">:`, e.message);
|
|
3871
3849
|
}
|
|
3872
3850
|
}
|
|
3873
3851
|
}
|
|
3874
3852
|
};
|
|
3853
|
+
const resolveTextNodeXPath = (node) => {
|
|
3854
|
+
var _a, _b, _c;
|
|
3855
|
+
if (!node.__xpathExpr) return;
|
|
3856
|
+
const xpath = node.__xpathExpr;
|
|
3857
|
+
try {
|
|
3858
|
+
validateXPath(xpath);
|
|
3859
|
+
const doc = globalThis.document || node.ownerDocument;
|
|
3860
|
+
const contextNode = node.parentNode || node;
|
|
3861
|
+
const result = doc.evaluate(
|
|
3862
|
+
xpath,
|
|
3863
|
+
contextNode,
|
|
3864
|
+
null,
|
|
3865
|
+
XPathResult.STRING_TYPE,
|
|
3866
|
+
null
|
|
3867
|
+
);
|
|
3868
|
+
node.textContent = result.stringValue;
|
|
3869
|
+
} catch (e) {
|
|
3870
|
+
(_c = globalThis.console) == null ? void 0 : _c.error(`[Lightview-CDOM] XPath text node error on <${(_a = node.parentNode) == null ? void 0 : _a.tagName.toLowerCase()} id="${(_b = node.parentNode) == null ? void 0 : _b.id}">:`, e.message);
|
|
3871
|
+
} finally {
|
|
3872
|
+
delete node.__xpathExpr;
|
|
3873
|
+
}
|
|
3874
|
+
};
|
|
3875
|
+
const resolveStaticXPath = (rootNode) => {
|
|
3876
|
+
const node = rootNode instanceof Node ? rootNode : (rootNode == null ? void 0 : rootNode.domEl) || rootNode;
|
|
3877
|
+
if (!node || !node.nodeType) return;
|
|
3878
|
+
if (node.nodeType === Node.ELEMENT_NODE) resolveAttributeXPaths(node);
|
|
3879
|
+
resolveTextNodeXPath(node);
|
|
3880
|
+
const doc = globalThis.document || node.ownerDocument;
|
|
3881
|
+
const walker = doc.createTreeWalker(node, NodeFilter.SHOW_ALL);
|
|
3882
|
+
let current = walker.nextNode();
|
|
3883
|
+
while (current) {
|
|
3884
|
+
if (current.nodeType === Node.ELEMENT_NODE) resolveAttributeXPaths(current);
|
|
3885
|
+
resolveTextNodeXPath(current);
|
|
3886
|
+
current = walker.nextNode();
|
|
3887
|
+
}
|
|
3888
|
+
};
|
|
3875
3889
|
if (typeof parseCDOMC !== "function") throw new Error("parseCDOMC not found");
|
|
3876
|
-
if (typeof parseJPRX !== "function") throw new Error("
|
|
3890
|
+
if (typeof parseJPRX !== "function") throw new Error("oldParseJPRX not found");
|
|
3877
3891
|
const LightviewCDOM2 = {
|
|
3878
3892
|
registerHelper,
|
|
3879
3893
|
registerOperator,
|
|
@@ -3882,7 +3896,9 @@ var LightviewCDOM = function(exports) {
|
|
|
3882
3896
|
resolvePathAsContext,
|
|
3883
3897
|
resolveExpression: resolveExpression$1,
|
|
3884
3898
|
parseCDOMC,
|
|
3885
|
-
parseJPRX,
|
|
3899
|
+
parseJPRX: parseCDOMC,
|
|
3900
|
+
// Alias parseJPRX to the more robust parseCDOMC
|
|
3901
|
+
oldParseJPRX: parseJPRX,
|
|
3886
3902
|
unwrapSignal,
|
|
3887
3903
|
getContext,
|
|
3888
3904
|
handleCDOMState: () => {
|
|
@@ -3892,7 +3908,7 @@ var LightviewCDOM = function(exports) {
|
|
|
3892
3908
|
activate,
|
|
3893
3909
|
hydrate,
|
|
3894
3910
|
resolveStaticXPath,
|
|
3895
|
-
version: "1.
|
|
3911
|
+
version: "1.1.0"
|
|
3896
3912
|
};
|
|
3897
3913
|
if (typeof window !== "undefined") {
|
|
3898
3914
|
globalThis.LightviewCDOM = {};
|
|
@@ -3903,9 +3919,10 @@ var LightviewCDOM = function(exports) {
|
|
|
3903
3919
|
exports.default = LightviewCDOM2;
|
|
3904
3920
|
exports.getContext = getContext;
|
|
3905
3921
|
exports.hydrate = hydrate;
|
|
3922
|
+
exports.oldParseJPRX = parseJPRX;
|
|
3906
3923
|
exports.parseCDOMC = parseCDOMC;
|
|
3907
3924
|
exports.parseExpression = parseExpression;
|
|
3908
|
-
exports.parseJPRX =
|
|
3925
|
+
exports.parseJPRX = parseCDOMC;
|
|
3909
3926
|
exports.registerHelper = registerHelper;
|
|
3910
3927
|
exports.registerOperator = registerOperator;
|
|
3911
3928
|
exports.resolveExpression = resolveExpression$1;
|
package/lightview.js
CHANGED
|
@@ -644,7 +644,6 @@
|
|
|
644
644
|
return reactiveAttrs;
|
|
645
645
|
};
|
|
646
646
|
const processChildren = (children, targetNode, clearExisting = true) => {
|
|
647
|
-
var _a;
|
|
648
647
|
if (clearExisting && targetNode.innerHTML !== void 0) {
|
|
649
648
|
targetNode.innerHTML = "";
|
|
650
649
|
}
|
|
@@ -717,9 +716,6 @@
|
|
|
717
716
|
childElements.push(childEl);
|
|
718
717
|
}
|
|
719
718
|
}
|
|
720
|
-
if (typeof ((_a = globalThis.LightviewCDOM) == null ? void 0 : _a.resolveStaticXPath) === "function") {
|
|
721
|
-
globalThis.LightviewCDOM.resolveStaticXPath(targetNode);
|
|
722
|
-
}
|
|
723
719
|
return childElements;
|
|
724
720
|
};
|
|
725
721
|
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.8",
|
|
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.3.
|
|
39
|
+
"jprx": "^1.3.1",
|
|
40
40
|
"linkedom": "^0.18.12",
|
|
41
41
|
"marked": "^17.0.1"
|
|
42
42
|
}
|
package/src/lightview-cdom.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* The Reactive Path and Expression Engine for Lightview.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { registerHelper, registerOperator, parseExpression, resolvePath, resolvePathAsContext, resolveExpression, parseCDOMC, parseJPRX, unwrapSignal, BindingTarget } from '../jprx/parser.js';
|
|
6
|
+
import { registerHelper, registerOperator, parseExpression, resolvePath, resolvePathAsContext, resolveExpression, parseCDOMC, parseJPRX as oldParseJPRX, unwrapSignal, BindingTarget } from '../jprx/parser.js';
|
|
7
7
|
import { registerMathHelpers } from '../jprx/helpers/math.js';
|
|
8
8
|
import { registerLogicHelpers } from '../jprx/helpers/logic.js';
|
|
9
9
|
import { registerStringHelpers } from '../jprx/helpers/string.js';
|
|
@@ -111,7 +111,7 @@ registerHelper('mount', async (url, options = {}) => {
|
|
|
111
111
|
|
|
112
112
|
if (isCDOM || (contentType.includes('application/json') && text.trim().startsWith('{'))) {
|
|
113
113
|
try {
|
|
114
|
-
content = hydrate(
|
|
114
|
+
content = hydrate(parseCDOMC(text));
|
|
115
115
|
} catch (e) {
|
|
116
116
|
// Fail gracefully to text
|
|
117
117
|
}
|
|
@@ -352,6 +352,18 @@ const hydrate = (node, parent = null) => {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// 4. Automatic XPath Resolution
|
|
356
|
+
// If this is a top-level hydrated element, ensure it resolves its static XPaths on mount.
|
|
357
|
+
// We add it to node.attributes so Lightview's element() factory picks it up.
|
|
358
|
+
if (!parent && node.tag) {
|
|
359
|
+
node.attributes = node.attributes || {};
|
|
360
|
+
const originalOnMount = node.attributes.onmount;
|
|
361
|
+
node.attributes.onmount = (el) => {
|
|
362
|
+
if (typeof originalOnMount === 'function') originalOnMount(el);
|
|
363
|
+
resolveStaticXPath(el);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
355
367
|
return node;
|
|
356
368
|
};
|
|
357
369
|
|
|
@@ -361,15 +373,18 @@ const hydrate = (node, parent = null) => {
|
|
|
361
373
|
* @param {string} xpath - The XPath expression to validate
|
|
362
374
|
*/
|
|
363
375
|
const validateXPath = (xpath) => {
|
|
376
|
+
if (!xpath) return;
|
|
377
|
+
|
|
364
378
|
// Check for forbidden forward-looking axes
|
|
365
379
|
const forbiddenAxes = /\b(child|descendant|following|following-sibling)::/;
|
|
366
380
|
if (forbiddenAxes.test(xpath)) {
|
|
367
381
|
throw new Error(`XPath: Forward-looking axes not allowed during DOM construction: ${xpath}`);
|
|
368
382
|
}
|
|
369
383
|
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
384
|
+
// Check for shorthand forward references like /div (implies child axis)
|
|
385
|
+
// We allow '/' if it's followed by '@' (attribute), '.' (parent/self shorthand),
|
|
386
|
+
// or is the start of the document root '/html'.
|
|
387
|
+
const hasShorthandChild = /\/(?![@.])(?![a-zA-Z0-9_-]+::)[a-zA-Z]/.test(xpath) && !xpath.startsWith('/html');
|
|
373
388
|
if (hasShorthandChild) {
|
|
374
389
|
throw new Error(`XPath: Shorthand child axis (/) not allowed during DOM construction: ${xpath}`);
|
|
375
390
|
}
|
|
@@ -381,76 +396,90 @@ const validateXPath = (xpath) => {
|
|
|
381
396
|
* Walks the tree and resolves all __xpath__ markers.
|
|
382
397
|
* @param {Node} rootNode - The root DOM node to start walking from
|
|
383
398
|
*/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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;
|
|
399
|
+
/**
|
|
400
|
+
* Resolves static XPath markers for attributes on a specific element.
|
|
401
|
+
*/
|
|
402
|
+
const resolveAttributeXPaths = (el) => {
|
|
403
|
+
const attributes = [...el.attributes];
|
|
404
|
+
for (const attr of attributes) {
|
|
405
|
+
if (attr.name.startsWith('data-xpath-')) {
|
|
406
|
+
const realAttr = attr.name.replace('data-xpath-', '');
|
|
430
407
|
try {
|
|
431
|
-
validateXPath(
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
408
|
+
validateXPath(attr.value);
|
|
409
|
+
const doc = globalThis.document || el.ownerDocument;
|
|
410
|
+
const result = doc.evaluate(
|
|
411
|
+
attr.value,
|
|
412
|
+
el,
|
|
435
413
|
null,
|
|
436
414
|
XPathResult.STRING_TYPE,
|
|
437
415
|
null
|
|
438
416
|
);
|
|
439
|
-
|
|
440
|
-
|
|
417
|
+
el.setAttribute(realAttr, result.stringValue);
|
|
418
|
+
el.removeAttribute(attr.name);
|
|
441
419
|
} catch (e) {
|
|
442
|
-
globalThis.console?.error(`[Lightview-CDOM] XPath
|
|
420
|
+
globalThis.console?.error(`[Lightview-CDOM] XPath attribute error ("${realAttr}") at <${el.tagName.toLowerCase()} id="${el.id}">:`, e.message);
|
|
443
421
|
}
|
|
444
422
|
}
|
|
445
423
|
}
|
|
446
424
|
};
|
|
447
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Resolves static XPath markers for a text node.
|
|
428
|
+
*/
|
|
429
|
+
const resolveTextNodeXPath = (node) => {
|
|
430
|
+
if (!node.__xpathExpr) return;
|
|
431
|
+
const xpath = node.__xpathExpr;
|
|
432
|
+
try {
|
|
433
|
+
validateXPath(xpath);
|
|
434
|
+
const doc = globalThis.document || node.ownerDocument;
|
|
435
|
+
// Use the parent node (the element) as the context for evaluation
|
|
436
|
+
// This avoids errors in browsers that don't support Text nodes as context nodes
|
|
437
|
+
// and keeps evaluation consistent with attributes.
|
|
438
|
+
const contextNode = node.parentNode || node;
|
|
439
|
+
const result = doc.evaluate(
|
|
440
|
+
xpath,
|
|
441
|
+
contextNode,
|
|
442
|
+
null,
|
|
443
|
+
XPathResult.STRING_TYPE,
|
|
444
|
+
null
|
|
445
|
+
);
|
|
446
|
+
node.textContent = result.stringValue;
|
|
447
|
+
} catch (e) {
|
|
448
|
+
globalThis.console?.error(`[Lightview-CDOM] XPath text node error on <${node.parentNode?.tagName.toLowerCase()} id="${node.parentNode?.id}">:`, e.message);
|
|
449
|
+
} finally {
|
|
450
|
+
delete node.__xpathExpr;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Walks the tree and resolves all __xpath__ markers.
|
|
456
|
+
* @param {Node} rootNode - The root DOM node to start walking from
|
|
457
|
+
*/
|
|
458
|
+
const resolveStaticXPath = (rootNode) => {
|
|
459
|
+
const node = rootNode instanceof Node ? rootNode : (rootNode?.domEl || rootNode);
|
|
460
|
+
if (!node || !node.nodeType) return;
|
|
461
|
+
|
|
462
|
+
// Process the root node itself
|
|
463
|
+
if (node.nodeType === Node.ELEMENT_NODE) resolveAttributeXPaths(node);
|
|
464
|
+
resolveTextNodeXPath(node);
|
|
465
|
+
|
|
466
|
+
// Process all descendants
|
|
467
|
+
const doc = globalThis.document || node.ownerDocument;
|
|
468
|
+
const walker = doc.createTreeWalker(node, NodeFilter.SHOW_ALL);
|
|
469
|
+
let current = walker.nextNode();
|
|
470
|
+
while (current) {
|
|
471
|
+
if (current.nodeType === Node.ELEMENT_NODE) resolveAttributeXPaths(current);
|
|
472
|
+
resolveTextNodeXPath(current);
|
|
473
|
+
current = walker.nextNode();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
448
477
|
|
|
449
478
|
// Prevent tree-shaking of parser functions by creating a side-effect
|
|
450
479
|
// These are used externally by lightview-x.js for .cdomc file loading
|
|
451
480
|
// The typeof check creates a runtime branch the bundler can't eliminate
|
|
452
481
|
if (typeof parseCDOMC !== 'function') throw new Error('parseCDOMC not found');
|
|
453
|
-
if (typeof
|
|
482
|
+
if (typeof oldParseJPRX !== 'function') throw new Error('oldParseJPRX not found');
|
|
454
483
|
|
|
455
484
|
const LightviewCDOM = {
|
|
456
485
|
registerHelper,
|
|
@@ -460,7 +489,8 @@ const LightviewCDOM = {
|
|
|
460
489
|
resolvePathAsContext,
|
|
461
490
|
resolveExpression,
|
|
462
491
|
parseCDOMC,
|
|
463
|
-
parseJPRX,
|
|
492
|
+
parseJPRX: parseCDOMC, // Alias parseJPRX to the more robust parseCDOMC
|
|
493
|
+
oldParseJPRX,
|
|
464
494
|
unwrapSignal,
|
|
465
495
|
getContext,
|
|
466
496
|
handleCDOMState: () => { },
|
|
@@ -468,7 +498,7 @@ const LightviewCDOM = {
|
|
|
468
498
|
activate,
|
|
469
499
|
hydrate,
|
|
470
500
|
resolveStaticXPath,
|
|
471
|
-
version: '1.
|
|
501
|
+
version: '1.1.0'
|
|
472
502
|
};
|
|
473
503
|
|
|
474
504
|
// Global export for non-module usage
|
|
@@ -486,7 +516,8 @@ export {
|
|
|
486
516
|
resolvePathAsContext,
|
|
487
517
|
resolveExpression,
|
|
488
518
|
parseCDOMC,
|
|
489
|
-
parseJPRX,
|
|
519
|
+
parseCDOMC as parseJPRX,
|
|
520
|
+
oldParseJPRX,
|
|
490
521
|
unwrapSignal,
|
|
491
522
|
BindingTarget,
|
|
492
523
|
getContext,
|
package/src/lightview.js
CHANGED
|
@@ -520,10 +520,7 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
|
|
524
|
-
if (typeof globalThis.LightviewCDOM?.resolveStaticXPath === 'function') {
|
|
525
|
-
globalThis.LightviewCDOM.resolveStaticXPath(targetNode);
|
|
526
|
-
}
|
|
523
|
+
|
|
527
524
|
|
|
528
525
|
return childElements;
|
|
529
526
|
};
|
package/test-xpath-preprocess.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|