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.
@@ -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
- **Example:**
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: [#../@aria-label]
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": ["#../@aria-label"]
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: [#../@id]`
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.
@@ -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="#advantages" class="docs-nav-link">Advantages</a>
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 & Comparison</a>
46
- <a href="#helpers-conditional" class="docs-nav-link" style="font-size: 0.85em;">Conditional
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" style="font-size: 0.85em;">Formatting</a>
49
- <a href="#helpers-datetime" class="docs-nav-link" style="font-size: 0.85em;">DateTime</a>
50
- <a href="#helpers-lookup" class="docs-nav-link" style="font-size: 0.85em;">Lookup</a>
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</a>
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. It also provides deep integration with
112
- <a href="https://json-schema.org/" target="_blank">JSON Schema</a> (Standard Draft 7+) for
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
- The UI automatically updates whenever <code>count</code> changes no manual DOM manipulation required.
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, " - ", =currency(_/price)] } })
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: 1rem;">
1053
- The <code>mount</code> helper is the primary mechanism for **Hypermedia updates**. It fetches content
1054
- (cDOM, vDOM, or oDOM) and injects it into the DOM. If the content contains a <code>=move</code>
1055
- helper, it will automatically relocate itself upon mounting.
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
@@ -19,7 +19,8 @@ export {
19
19
  resolvePathAsContext,
20
20
  resolveExpression,
21
21
  parseCDOMC,
22
- parseJPRX,
22
+ parseCDOMC as parseJPRX,
23
+ parseJPRX as oldParseJPRX,
23
24
  unwrapSignal,
24
25
  getRegistry,
25
26
  BindingTarget
package/jprx/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jprx",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "JSON Path Reactive eXpressions - A reactive expression language for JSON data",
5
5
  "main": "index.js",
6
6
  "type": "module",
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_$/./]/.test(char)) {
1796
+ if (/[a-zA-Z_$\/.\/]/.test(char)) {
1795
1797
  let word = '';
1796
- while (i < len && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
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_$/./]/.test(char)) {
3527
+ if (/[a-zA-Z_$\/.\/]/.test(char)) {
3530
3528
  let word = "";
3531
- while (i < len2 && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
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(parseJPRX(text));
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 resolveStaticXPath = (rootNode) => {
5977
- var _a2, _b2;
5978
- if (!rootNode || !rootNode.nodeType) return;
5979
- const walker = document.createTreeWalker(
5980
- rootNode,
5981
- NodeFilter.SHOW_ALL
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(xpath);
6017
- const result = document.evaluate(
6018
- xpath,
6019
- node2,
6020
- // Use text node as context, not its parent!
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
- node2.textContent = result.stringValue;
6026
- delete node2.__xpathExpr;
5999
+ el.setAttribute(realAttr, result.stringValue);
6000
+ el.removeAttribute(attr.name);
6027
6001
  } catch (e) {
6028
- (_b2 = globalThis.console) == null ? void 0 : _b2.error(`[Lightview-CDOM] XPath resolution failed for text node:`, e.message);
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("parseJPRX not found");
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.0.0"
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_$/./]/.test(char)) {
1349
+ if (/[a-zA-Z_$\/.\/]/.test(char)) {
1348
1350
  let word = "";
1349
- while (i < len2 && /[a-zA-Z0-9_$/.-]/.test(input[i])) {
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(parseJPRX(text));
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 resolveStaticXPath = (rootNode) => {
3819
- var _a, _b;
3820
- if (!rootNode || !rootNode.nodeType) return;
3821
- const walker = document.createTreeWalker(
3822
- rootNode,
3823
- NodeFilter.SHOW_ALL
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(xpath);
3859
- const result = document.evaluate(
3860
- xpath,
3861
- node2,
3862
- // Use text node as context, not its parent!
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
- node2.textContent = result.stringValue;
3868
- delete node2.__xpathExpr;
3845
+ el.setAttribute(realAttr, result.stringValue);
3846
+ el.removeAttribute(attr.name);
3869
3847
  } catch (e) {
3870
- (_b = globalThis.console) == null ? void 0 : _b.error(`[Lightview-CDOM] XPath resolution failed for text node:`, e.message);
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("parseJPRX not found");
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.0.0"
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 = 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.6",
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.0",
39
+ "jprx": "^1.3.1",
40
40
  "linkedom": "^0.18.12",
41
41
  "marked": "^17.0.1"
42
42
  }
@@ -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(parseJPRX(text));
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
- // Also check for shorthand forward references like /div (implies child axis)
371
- // But allow / for document root like /html
372
- const hasShorthandChild = /\/[a-zA-Z]/.test(xpath) && !xpath.startsWith('/html');
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
- const resolveStaticXPath = (rootNode) => {
385
- if (!rootNode || !rootNode.nodeType) return;
386
-
387
- const walker = document.createTreeWalker(
388
- rootNode,
389
- NodeFilter.SHOW_ALL
390
- );
391
-
392
- const nodesToProcess = [];
393
- let node = walker.nextNode();
394
- while (node) {
395
- nodesToProcess.push(node);
396
- node = walker.nextNode();
397
- }
398
-
399
- // Process all nodes
400
- for (const node of nodesToProcess) {
401
- // Check for XPath markers in attributes
402
- if (node.nodeType === Node.ELEMENT_NODE) {
403
- const attributes = [...node.attributes];
404
- for (const attr of attributes) {
405
- if (attr.name.startsWith('data-xpath-')) {
406
- const realAttr = attr.name.replace('data-xpath-', '');
407
- const xpath = attr.value;
408
-
409
- try {
410
- validateXPath(xpath);
411
- const result = document.evaluate(
412
- xpath,
413
- node,
414
- null,
415
- XPathResult.STRING_TYPE,
416
- null
417
- );
418
- node.setAttribute(realAttr, result.stringValue);
419
- node.removeAttribute(attr.name);
420
- } catch (e) {
421
- globalThis.console?.error(`[Lightview-CDOM] XPath resolution failed for attribute "${realAttr}":`, e.message);
422
- }
423
- }
424
- }
425
- }
426
-
427
- // Check for XPath markers in text nodes
428
- if (node.__xpathExpr) {
429
- const xpath = node.__xpathExpr;
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(xpath);
432
- const result = document.evaluate(
433
- xpath,
434
- node, // Use text node as context, not its parent!
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
- node.textContent = result.stringValue;
440
- delete node.__xpathExpr;
417
+ el.setAttribute(realAttr, result.stringValue);
418
+ el.removeAttribute(attr.name);
441
419
  } catch (e) {
442
- globalThis.console?.error(`[Lightview-CDOM] XPath resolution failed for text node:`, e.message);
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 parseJPRX !== 'function') throw new Error('parseJPRX not found');
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.0.0'
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
- // Resolve static XPath expressions after DOM tree is constructed
524
- if (typeof globalThis.LightviewCDOM?.resolveStaticXPath === 'function') {
525
- globalThis.LightviewCDOM.resolveStaticXPath(targetNode);
526
- }
523
+
527
524
 
528
525
  return childElements;
529
526
  };
@@ -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
- }