lightview 2.3.6 → 2.3.7

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.
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "lightview",
3
+ "version": "2.3.7",
4
+ "description": "A lightweight reactive UI library with features of Bau, Juris, and HTMX",
5
+ "main": "lightview.js",
6
+ "workspaces": [
7
+ "jprx"
8
+ ],
9
+ "directories": {
10
+ "doc": "docs"
11
+ },
12
+ "scripts": {
13
+ "dev": "wrangler pages dev . --port 3000",
14
+ "preview": "npm run build && wrangler pages dev ./dist --port 8788",
15
+ "build": "node build-bundles.mjs && node build.js",
16
+ "watch": "node build-bundles.mjs && node build.js --watch --env=dev",
17
+ "deploy": "npm run build && wrangler pages deploy dist",
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "keywords": [],
21
+ "author": "Simon Y. Blackwell, AnyWhichWay LLC",
22
+ "license": "MIT",
23
+ "type": "commonjs",
24
+ "devDependencies": {
25
+ "@grucloud/bau": "^0.106.0",
26
+ "acorn": "^8.15.0",
27
+ "acorn-walk": "^8.3.4",
28
+ "htmx.org": "^2.0.8",
29
+ "jsdom": "^27.4.0",
30
+ "juris": "^0.9.0",
31
+ "react": "^19.2.3",
32
+ "terser": "^5.24.0",
33
+ "vite": "^5.0.0",
34
+ "vitest": "^4.0.16",
35
+ "wrangler": "^4.54.0"
36
+ },
37
+ "dependencies": {
38
+ "expr-eval": "^2.0.2",
39
+ "jprx": "^1.3.1",
40
+ "linkedom": "^0.18.12",
41
+ "marked": "^17.0.1"
42
+ }
43
+ }
@@ -0,0 +1,185 @@
1
+ (function() {
2
+ "use strict";
3
+ (() => {
4
+ const base = (shellPath) => {
5
+ if (typeof window === "undefined" || document.getElementById("content")) return;
6
+ const url = new URL(shellPath, globalThis.location.href);
7
+ url.searchParams.set("load", globalThis.location.pathname);
8
+ globalThis.location.href = url.toString();
9
+ };
10
+ const router = (options = {}) => {
11
+ const { base: base2 = "", contentEl, notFound, debug, onResponse, onStart } = options;
12
+ const chains = [];
13
+ const normalizePath = (p) => {
14
+ if (!p) return "/";
15
+ let hash = "";
16
+ if (p.includes("#")) {
17
+ [p, hash] = p.split("#");
18
+ hash = "#" + hash;
19
+ }
20
+ try {
21
+ if (p.startsWith("http") || p.startsWith("//")) p = new URL(p, globalThis.location.origin).pathname;
22
+ } catch (e) {
23
+ }
24
+ if (base2 && p.startsWith(base2)) p = p.slice(base2.length);
25
+ return (p.replace(/\/+$/, "").replace(/^([^/])/, "/$1") || "/") + hash;
26
+ };
27
+ const createMatcher = (pattern) => {
28
+ if (typeof pattern === "function") return pattern;
29
+ return (ctx) => {
30
+ const { path } = ctx;
31
+ const pathOnly = path.split("#")[0];
32
+ if (pattern instanceof RegExp) {
33
+ const m2 = pathOnly.match(pattern);
34
+ return m2 ? { ...ctx, match: m2 } : null;
35
+ }
36
+ if (pattern === "*" || pattern === pathOnly) return { ...ctx, wildcard: pathOnly };
37
+ const keys = [];
38
+ const regexStr = "^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "(.*)").replace(/:([^/]+)/g, (_, k) => (keys.push(k), "([^/]+)")) + "$";
39
+ const m = pathOnly.match(new RegExp(regexStr));
40
+ if (m) {
41
+ const params = {};
42
+ keys.forEach((k, i) => params[k] = m[i + 1]);
43
+ return { ...ctx, params, wildcard: m[1] };
44
+ }
45
+ return null;
46
+ };
47
+ };
48
+ const createReplacer = (pat) => (ctx) => {
49
+ const [path, hash] = ctx.path.split("#");
50
+ return {
51
+ ...ctx,
52
+ path: pat.replace(/\*|:([^/]+)/g, (m, k) => {
53
+ var _a;
54
+ return (k ? (_a = ctx.params) == null ? void 0 : _a[k] : ctx.wildcard) || m;
55
+ }) + (hash ? "#" + hash : "")
56
+ };
57
+ };
58
+ const fetchHandler = async (ctx) => {
59
+ try {
60
+ const pathOnly = ctx.path.split("#")[0];
61
+ const res = await fetch(pathOnly);
62
+ if (res.ok) return res;
63
+ } catch (e) {
64
+ if (debug) console.error("[Router] Fetch error:", e);
65
+ }
66
+ return null;
67
+ };
68
+ const use = (...args) => {
69
+ const chain = args.map((arg, i) => i === 0 && typeof arg !== "function" ? createMatcher(arg) : typeof arg === "string" ? createReplacer(arg) : arg);
70
+ if (contentEl && !chain.some((f) => f.name === "fetchHandler" || args.some((a) => typeof a === "function"))) chain.push(fetchHandler);
71
+ chains.push(chain);
72
+ return routerInstance;
73
+ };
74
+ const route = async (raw) => {
75
+ let ctx = { path: normalizePath(raw), contentEl };
76
+ if (debug) console.log(`[Router] Routing: ${ctx.path}`);
77
+ for (const chain of chains) {
78
+ let res = ctx, failed = false;
79
+ for (const fn of chain) {
80
+ try {
81
+ res = await fn(res);
82
+ if (res instanceof Response) return res;
83
+ if (!res) {
84
+ failed = true;
85
+ break;
86
+ }
87
+ } catch (e) {
88
+ console.error("[Router] Chain error:", e);
89
+ failed = true;
90
+ break;
91
+ }
92
+ }
93
+ if (!failed) ctx = typeof res === "string" ? { ...ctx, path: res } : { ...ctx, ...res };
94
+ }
95
+ return notFound ? notFound(ctx) : null;
96
+ };
97
+ const handleRequest = async (path) => {
98
+ var _a, _b;
99
+ if (onStart) onStart(path);
100
+ const internals = (_a = globalThis.Lightview) == null ? void 0 : _a.internals;
101
+ const scrollMap = (_b = internals == null ? void 0 : internals.saveScrolls) == null ? void 0 : _b.call(internals);
102
+ const res = await route(path);
103
+ if (!res) return console.warn(`[Router] No route: ${path}`);
104
+ if (res.ok && contentEl) {
105
+ contentEl.innerHTML = await res.text();
106
+ contentEl.querySelectorAll("script").forEach((s) => {
107
+ const n = document.createElement("script");
108
+ [...s.attributes].forEach((a) => n.setAttribute(a.name, a.value));
109
+ n.textContent = s.textContent;
110
+ s.replaceWith(n);
111
+ });
112
+ if ((internals == null ? void 0 : internals.restoreScrolls) && scrollMap) {
113
+ internals.restoreScrolls(scrollMap);
114
+ }
115
+ const urlParts = path.split("#");
116
+ const hash = urlParts.length > 1 ? "#" + urlParts[1] : "";
117
+ if (hash) {
118
+ requestAnimationFrame(() => {
119
+ requestAnimationFrame(() => {
120
+ const id = hash.slice(1);
121
+ const target = document.getElementById(id);
122
+ if (target) {
123
+ target.style.scrollMarginTop = "calc(var(--site-nav-height, 0px) + 2rem)";
124
+ target.scrollIntoView({ behavior: "smooth", block: "start", inline: "start" });
125
+ }
126
+ });
127
+ });
128
+ }
129
+ }
130
+ if (onResponse) await onResponse(res, path);
131
+ return res;
132
+ };
133
+ const navigate = (path) => {
134
+ const p = normalizePath(path);
135
+ return handleRequest(base2 + p).then((r) => {
136
+ let dest = (r == null ? void 0 : r.url) ? new URL(r.url, globalThis.location.origin).pathname : base2 + p;
137
+ if (p.includes("#") && !dest.includes("#")) {
138
+ dest += "#" + p.split("#")[1];
139
+ }
140
+ globalThis.history.pushState({ path: dest }, "", dest);
141
+ }).catch((e) => console.error("[Router] Nav error:", e));
142
+ };
143
+ const start = async () => {
144
+ const load = new URLSearchParams(globalThis.location.search).get("load");
145
+ globalThis.onpopstate = (e) => {
146
+ var _a;
147
+ return handleRequest(((_a = e.state) == null ? void 0 : _a.path) || normalizePath(globalThis.location.pathname + globalThis.location.hash));
148
+ };
149
+ document.onclick = (e) => {
150
+ const path = e.composedPath();
151
+ const a = path.find((el) => {
152
+ var _a;
153
+ return el.tagName === "A" && ((_a = el.hasAttribute) == null ? void 0 : _a.call(el, "href"));
154
+ });
155
+ if (!a || a.target === "_blank" || /^(http|#|mailto|tel)/.test(a.getAttribute("href"))) return;
156
+ const url = new URL(a.href, document.baseURI);
157
+ if (url.origin === globalThis.location.origin) {
158
+ e.preventDefault();
159
+ const fullPath = url.pathname + url.search + url.hash;
160
+ navigate(normalizePath(fullPath));
161
+ }
162
+ };
163
+ const init = load || normalizePath(globalThis.location.pathname + globalThis.location.hash);
164
+ globalThis.history.replaceState({ path: init }, "", base2 + init);
165
+ return handleRequest(init).then(() => routerInstance);
166
+ };
167
+ const routerInstance = { use, navigate, start };
168
+ return routerInstance;
169
+ };
170
+ const LightviewRouter = { base, router };
171
+ if (typeof module !== "undefined" && module.exports) module.exports = LightviewRouter;
172
+ else if (typeof window !== "undefined") {
173
+ globalThis.LightviewRouter = LightviewRouter;
174
+ try {
175
+ const script = document.currentScript;
176
+ if (script && script.src.includes("?")) {
177
+ const params = new URL(script.src).searchParams;
178
+ const b = params.get("base");
179
+ if (b) LightviewRouter.base(b);
180
+ }
181
+ } catch (e) {
182
+ }
183
+ }
184
+ })();
185
+ })();