sol-components 2.1.0

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.
Files changed (150) hide show
  1. package/README.md +7 -0
  2. package/core/activate.js +27 -0
  3. package/core/adopt.js +71 -0
  4. package/core/auth-core.js +73 -0
  5. package/core/auth-fetch.js +154 -0
  6. package/core/component-mount.js +110 -0
  7. package/core/defaults.js +48 -0
  8. package/core/define.js +15 -0
  9. package/core/display-target.js +166 -0
  10. package/core/edit-placements.js +28 -0
  11. package/core/editor-self.js +127 -0
  12. package/core/editor.js +162 -0
  13. package/core/events.js +27 -0
  14. package/core/extension-points.js +189 -0
  15. package/core/form-utils.js +210 -0
  16. package/core/from-query.js +138 -0
  17. package/core/from-rdf.js +52 -0
  18. package/core/here.js +33 -0
  19. package/core/include-core.js +73 -0
  20. package/core/inrupt-global.js +18 -0
  21. package/core/menu-consumer.js +41 -0
  22. package/core/menu-rdf.js +154 -0
  23. package/core/pod-ops.js +392 -0
  24. package/core/pod-registry.js +82 -0
  25. package/core/popup-proxy.js +255 -0
  26. package/core/rdf-core.js +280 -0
  27. package/core/rdf-render.js +136 -0
  28. package/core/rdf-utils.js +411 -0
  29. package/core/rdf.js +154 -0
  30. package/core/services.js +106 -0
  31. package/core/shape-to-form.js +741 -0
  32. package/core/sparql-safety.js +20 -0
  33. package/core/utils.js +196 -0
  34. package/dist/importmap-cdn.json +49 -0
  35. package/dist/importmap-local.json +49 -0
  36. package/dist/sol-loader.manifest.json +140 -0
  37. package/dist/vendor/@comunica-query-sparql.js +137851 -0
  38. package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
  39. package/dist/vendor/dompurify.js +1476 -0
  40. package/dist/vendor/ical.js.js +9739 -0
  41. package/dist/vendor/marked.js +85 -0
  42. package/dist/vendor/n3.js +14670 -0
  43. package/dist/vendor/rdf-validate-shacl.js +6970 -0
  44. package/dist/vendor/rdflib.js +35172 -0
  45. package/dist/vendor/solid-logic.js +6819 -0
  46. package/dist/vendor/solid-ui.js +21945 -0
  47. package/node/sol-form.js +133 -0
  48. package/node/sol-include.js +55 -0
  49. package/node/sol-login.js +632 -0
  50. package/node/sol-menu.js +639 -0
  51. package/node/sol-query.js +116 -0
  52. package/package.json +133 -0
  53. package/web/menu-from-rdf.js +23 -0
  54. package/web/scripts/prefs.js +25 -0
  55. package/web/sol-accordion.js +114 -0
  56. package/web/sol-basic.js +50 -0
  57. package/web/sol-breadcrumb.js +131 -0
  58. package/web/sol-button.js +244 -0
  59. package/web/sol-calendar.js +465 -0
  60. package/web/sol-default.js +118 -0
  61. package/web/sol-dropdown-button.js +222 -0
  62. package/web/sol-feed.js +1336 -0
  63. package/web/sol-form.js +949 -0
  64. package/web/sol-full.js +43 -0
  65. package/web/sol-gallery.js +303 -0
  66. package/web/sol-include.js +246 -0
  67. package/web/sol-live-edit.js +415 -0
  68. package/web/sol-login.js +856 -0
  69. package/web/sol-menu.js +593 -0
  70. package/web/sol-modal.js +377 -0
  71. package/web/sol-pod-extras.js +17 -0
  72. package/web/sol-pod-ops.js +680 -0
  73. package/web/sol-pod.js +1039 -0
  74. package/web/sol-query.js +546 -0
  75. package/web/sol-rolodex.js +95 -0
  76. package/web/sol-search.js +402 -0
  77. package/web/sol-settings.js +199 -0
  78. package/web/sol-solidos.js +93 -0
  79. package/web/sol-tabs.js +445 -0
  80. package/web/sol-time.js +194 -0
  81. package/web/sol-tree-edit.js +492 -0
  82. package/web/sol-wac.js +456 -0
  83. package/web/sol-weather.js +337 -0
  84. package/web/sol-window.js +142 -0
  85. package/web/styles/buttons-css.js +108 -0
  86. package/web/styles/help.css +242 -0
  87. package/web/styles/root.css +112 -0
  88. package/web/styles/sol-accordion-css.js +97 -0
  89. package/web/styles/sol-calendar-css.js +154 -0
  90. package/web/styles/sol-feed-css.js +475 -0
  91. package/web/styles/sol-form-css.js +471 -0
  92. package/web/styles/sol-gallery-css.js +181 -0
  93. package/web/styles/sol-include-css.js +95 -0
  94. package/web/styles/sol-live-edit-css.js +84 -0
  95. package/web/styles/sol-live-edit.css +101 -0
  96. package/web/styles/sol-login-css.js +116 -0
  97. package/web/styles/sol-menu-css.js +145 -0
  98. package/web/styles/sol-modal-css.js +134 -0
  99. package/web/styles/sol-pod-css.js +187 -0
  100. package/web/styles/sol-pod-modal-css.js +203 -0
  101. package/web/styles/sol-query-css.js +140 -0
  102. package/web/styles/sol-query-help.css +267 -0
  103. package/web/styles/sol-query-one-pager.css +67 -0
  104. package/web/styles/sol-search-css.js +157 -0
  105. package/web/styles/sol-solidos-css.js +7 -0
  106. package/web/styles/sol-tabs-css.js +114 -0
  107. package/web/styles/sol-time-css.js +30 -0
  108. package/web/styles/sol-wac-css.js +73 -0
  109. package/web/styles/sol-weather-css.js +59 -0
  110. package/web/styles/solid-logo.svg +9 -0
  111. package/web/styles/view-accordion-css.js +66 -0
  112. package/web/styles/view-anchorlist-css.js +22 -0
  113. package/web/styles/view-autocomplete-css.js +59 -0
  114. package/web/styles/view-rolodex-css.js +102 -0
  115. package/web/styles/view-select-css.js +21 -0
  116. package/web/utils/calendar-fetch.js +388 -0
  117. package/web/utils/code-mirror-editor.js +82 -0
  118. package/web/utils/commons-fetch.js +108 -0
  119. package/web/utils/feed-edit.js +159 -0
  120. package/web/utils/feed-edit.smoke.mjs +74 -0
  121. package/web/utils/feed-fetch.js +573 -0
  122. package/web/utils/live-edit-help/csv.js +64 -0
  123. package/web/utils/live-edit-help/graphviz.js +41 -0
  124. package/web/utils/live-edit-help/jsonld.js +55 -0
  125. package/web/utils/live-edit-help/markdown.js +52 -0
  126. package/web/utils/live-edit-help/mermaid.js +48 -0
  127. package/web/utils/live-edit-help/turtle.js +85 -0
  128. package/web/utils/rdf-config.js +125 -0
  129. package/web/utils/renderers/csv.js +124 -0
  130. package/web/utils/renderers/d3-force.js +82 -0
  131. package/web/utils/renderers/graphviz.js +13 -0
  132. package/web/utils/renderers/html.js +10 -0
  133. package/web/utils/renderers/jsonld.js +63 -0
  134. package/web/utils/renderers/markdown.js +19 -0
  135. package/web/utils/renderers/mermaid.js +54 -0
  136. package/web/utils/renderers/turtle.js +51 -0
  137. package/web/utils/sol-query-triple-patterns.js +151 -0
  138. package/web/utils/sol-query-ui.js +250 -0
  139. package/web/utils/sol-query-views.js +32 -0
  140. package/web/views/_helpers.js +34 -0
  141. package/web/views/accordion.js +133 -0
  142. package/web/views/anchorlist.js +59 -0
  143. package/web/views/auto-complete.js +183 -0
  144. package/web/views/dl.js +38 -0
  145. package/web/views/list.js +19 -0
  146. package/web/views/menu.js +56 -0
  147. package/web/views/rolodex.js +126 -0
  148. package/web/views/select.js +79 -0
  149. package/web/views/table.js +73 -0
  150. package/web/views/tabs.js +57 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * popup-proxy — parent-side proxy for an OIDC session that lives in a
3
+ * popup window.
4
+ *
5
+ * Phase 0 established that two Inrupt `Session`s cannot coexist in one
6
+ * window (storage gets wiped; restoration redirects top-level). The
7
+ * workaround: each session lives in its own popup window, which holds
8
+ * the real `Session`. The parent never navigates, so multiple popups'
9
+ * sessions coexist. The parent talks to each popup over postMessage.
10
+ *
11
+ * `PopupProxySession` is shape-compatible with the bits of Inrupt's
12
+ * `Session` that callers use — `.info`, `.fetch`, `.logout` — so it can
13
+ * be stored in `AuthManager.sessions` alongside (or instead of) real
14
+ * `Session`s.
15
+ *
16
+ * Wire protocol (both directions use `source` to disambiguate):
17
+ *
18
+ * parent → popup { source:'sol-popup-parent', type, id, side, ... }
19
+ * popup → parent { source:'sol-popup-auth', type, id, side, ... }
20
+ *
21
+ * type 'fetch' parent→popup { url, init: SerializedInit }
22
+ * type 'fetch-reply' popup→parent { ok,status,statusText,headers,body } | { error }
23
+ * type 'logout' parent→popup {}
24
+ * type 'logout-reply' popup→parent {}
25
+ * type 'logged-in' popup→parent { webId, sessionId, issuer }
26
+ * type 'login-failed' popup→parent { error }
27
+ * type 'popup-ready' popup→parent {}
28
+ *
29
+ * Request/response bodies are buffered (not streamed) — fine for pod
30
+ * file ops, which are not huge. Blob and ArrayBuffer survive structured
31
+ * clone; ReadableStream does not, so it is buffered first.
32
+ */
33
+
34
+ const PARENT_SRC = 'sol-popup-parent';
35
+ const POPUP_SRC = 'sol-popup-auth';
36
+
37
+ /** Statuses whose Response MUST have a null body. */
38
+ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
39
+
40
+ /**
41
+ * Serialize a fetch `init` (and a Request-ish input) into a structured-
42
+ * cloneable object. Headers → [[k,v]]; a streaming body is buffered to
43
+ * a Blob.
44
+ */
45
+ export async function serializeRequest(input, init = {}) {
46
+ const url = typeof input === 'string'
47
+ ? input
48
+ : (input && input.url) || String(input);
49
+
50
+ const method = (init.method
51
+ || (typeof input !== 'string' && input && input.method)
52
+ || 'GET').toUpperCase();
53
+
54
+ // Merge headers from a Request input and the init.
55
+ const headers = [];
56
+ const collect = (h) => {
57
+ if (!h) return;
58
+ if (typeof h.forEach === 'function' && !(Array.isArray(h))) {
59
+ h.forEach((v, k) => headers.push([k, v]));
60
+ } else if (Array.isArray(h)) {
61
+ for (const [k, v] of h) headers.push([k, v]);
62
+ } else {
63
+ for (const k of Object.keys(h)) headers.push([k, h[k]]);
64
+ }
65
+ };
66
+ if (typeof input !== 'string' && input && input.headers) collect(input.headers);
67
+ if (init.headers) collect(init.headers);
68
+
69
+ let body = init.body;
70
+ if (body == null && typeof input !== 'string' && input && input.body) {
71
+ // Request input with a body — buffer it.
72
+ body = await input.clone().blob();
73
+ }
74
+ if (body instanceof ReadableStream) {
75
+ body = await new Response(body).blob();
76
+ }
77
+ // Blob, ArrayBuffer, ArrayBufferView, string, URLSearchParams all
78
+ // survive structured clone. FormData does too. Leave as-is.
79
+
80
+ return { url, init: { method, headers, body } };
81
+ }
82
+
83
+ /** Reconstruct a fetch call inside the popup from a serialized request. */
84
+ export function deserializeRequest(msg) {
85
+ const init = {
86
+ method: msg.init.method,
87
+ headers: new Headers(msg.init.headers || []),
88
+ };
89
+ if (msg.init.body != null && msg.init.method !== 'GET' && msg.init.method !== 'HEAD') {
90
+ init.body = msg.init.body;
91
+ }
92
+ return { url: msg.url, init };
93
+ }
94
+
95
+ /** Serialize a Response (popup side) into a cloneable object. */
96
+ export async function serializeResponse(res) {
97
+ const headers = [];
98
+ res.headers.forEach((v, k) => headers.push([k, v]));
99
+ let body = null;
100
+ if (!NULL_BODY_STATUS.has(res.status)) {
101
+ body = await res.blob();
102
+ }
103
+ return {
104
+ ok: res.ok,
105
+ status: res.status,
106
+ statusText: res.statusText,
107
+ headers,
108
+ body,
109
+ };
110
+ }
111
+
112
+ /** Reconstruct a Response (parent side) from a serialized reply. */
113
+ export function deserializeResponse(reply) {
114
+ const init = {
115
+ status: reply.status,
116
+ statusText: reply.statusText,
117
+ headers: new Headers(reply.headers || []),
118
+ };
119
+ const body = NULL_BODY_STATUS.has(reply.status) ? null : reply.body;
120
+ return new Response(body, init);
121
+ }
122
+
123
+ /**
124
+ * Parent-side proxy. Holds a reference to the popup window and forwards
125
+ * `fetch` to it.
126
+ */
127
+ export class PopupProxySession extends EventTarget {
128
+ /**
129
+ * @param {Window} popupWindow the open popup running the callback page
130
+ * @param {Object} loginInfo { webId, sessionId, issuer, side }
131
+ * @param {string} popupOrigin origin to postMessage to (same-origin app)
132
+ */
133
+ constructor(popupWindow, loginInfo, popupOrigin) {
134
+ super();
135
+ this._popup = popupWindow;
136
+ this._origin = popupOrigin || (typeof window !== 'undefined' ? window.location.origin : '*');
137
+ this._side = loginInfo.side || null;
138
+ this._reqId = 0;
139
+ this._pending = new Map();
140
+
141
+ this.info = {
142
+ isLoggedIn: true,
143
+ sessionId: loginInfo.sessionId || null,
144
+ webId: loginInfo.webId || null,
145
+ issuer: loginInfo.issuer || null,
146
+ clientAppId: loginInfo.clientId || null,
147
+ };
148
+
149
+ this._onMessage = (e) => this._handleMessage(e);
150
+ window.addEventListener('message', this._onMessage);
151
+
152
+ // Close the popup when the app page goes away (tab close, navigation,
153
+ // browser quit) so we don't leave orphaned login windows behind.
154
+ // Skip when the page is going into the back/forward cache
155
+ // (event.persisted) — it may be restored, and the popup should
156
+ // survive with it.
157
+ this._onPageHide = (e) => {
158
+ if (e && e.persisted) return;
159
+ if (this._popup && !this._popup.closed) {
160
+ try { this._popup.close(); } catch (_) { /* ignore */ }
161
+ }
162
+ };
163
+ window.addEventListener('pagehide', this._onPageHide);
164
+
165
+ // Notice if the popup is closed out from under us.
166
+ this._closeWatch = setInterval(() => {
167
+ if (this._popup && this._popup.closed) this._handlePopupClosed();
168
+ }, 1500);
169
+ }
170
+
171
+ get side() { return this._side; }
172
+ get popupClosed() { return !this._popup || this._popup.closed; }
173
+
174
+ _handleMessage(e) {
175
+ const d = e.data;
176
+ if (!d || d.source !== POPUP_SRC) return;
177
+ if (this._side && d.side && d.side !== this._side) return;
178
+ if (d.type === 'fetch-reply' || d.type === 'logout-reply') {
179
+ const p = this._pending.get(d.id);
180
+ if (p) {
181
+ this._pending.delete(d.id);
182
+ if (d.error) p.reject(new Error(d.error));
183
+ else p.resolve(d);
184
+ }
185
+ }
186
+ }
187
+
188
+ _handlePopupClosed() {
189
+ clearInterval(this._closeWatch);
190
+ if (!this.info.isLoggedIn) return;
191
+ this.info.isLoggedIn = false;
192
+ for (const [, p] of this._pending) p.reject(new Error('popup closed'));
193
+ this._pending.clear();
194
+ this.dispatchEvent(new CustomEvent('logout', {
195
+ detail: { reason: 'popup-closed', side: this._side },
196
+ }));
197
+ }
198
+
199
+ _post(msg, transfer) {
200
+ if (this.popupClosed) throw new Error('popup is closed');
201
+ msg.source = PARENT_SRC;
202
+ msg.side = this._side;
203
+ this._popup.postMessage(msg, this._origin, transfer || []);
204
+ }
205
+
206
+ _request(msg, timeoutMs = 60000) {
207
+ const id = String(++this._reqId);
208
+ msg.id = id;
209
+ return new Promise((resolve, reject) => {
210
+ this._pending.set(id, { resolve, reject });
211
+ try {
212
+ this._post(msg);
213
+ } catch (err) {
214
+ this._pending.delete(id);
215
+ reject(err);
216
+ return;
217
+ }
218
+ setTimeout(() => {
219
+ if (this._pending.has(id)) {
220
+ this._pending.delete(id);
221
+ reject(new Error('popup request timed out'));
222
+ }
223
+ }, timeoutMs);
224
+ });
225
+ }
226
+
227
+ /** Authenticated fetch — proxied to the popup's real Session. */
228
+ fetch = async (input, init) => {
229
+ if (!this.info.isLoggedIn) throw new Error('PopupProxySession: not logged in');
230
+ const serialized = await serializeRequest(input, init);
231
+ const reply = await this._request({ type: 'fetch', ...serialized });
232
+ if (reply.error) throw new Error(reply.error);
233
+ return deserializeResponse(reply);
234
+ };
235
+
236
+ async logout() {
237
+ if (!this.popupClosed) {
238
+ try { await this._request({ type: 'logout' }, 5000); } catch (_) { /* ignore */ }
239
+ }
240
+ this.info.isLoggedIn = false;
241
+ this.dispatchEvent(new CustomEvent('logout', { detail: { reason: 'explicit', side: this._side } }));
242
+ this.destroy();
243
+ }
244
+
245
+ /** Tear down listeners and close the popup. */
246
+ destroy() {
247
+ clearInterval(this._closeWatch);
248
+ window.removeEventListener('message', this._onMessage);
249
+ window.removeEventListener('pagehide', this._onPageHide);
250
+ if (this._popup && !this._popup.closed) {
251
+ try { this._popup.close(); } catch (_) { /* ignore */ }
252
+ }
253
+ this._popup = null;
254
+ }
255
+ }
@@ -0,0 +1,280 @@
1
+ // Pure RDF utility functions shared between browser (rdf-utils.js) and
2
+ // Node (sol-query-node.js). All rdflib-dependent functions accept an
3
+ // rdflib-like object ({ sym, literal }) as a parameter.
4
+
5
+ // ─── Namespace prefixes ──────────────────────────────────────────────────────
6
+ export const KNOWN_PREFIXES = {
7
+ acl: 'http://www.w3.org/ns/auth/acl#',
8
+ arg: 'http://www.w3.org/ns/pim/arg#',
9
+ as: 'https://www.w3.org/ns/activitystreams#',
10
+ bookmark: 'http://www.w3.org/2002/01/bookmark#',
11
+ cal: 'http://www.w3.org/2002/12/cal/ical#',
12
+ cco: 'http://www.ontologyrepository.com/CommonCoreOntologies/',
13
+ cert: 'http://www.w3.org/ns/auth/cert#',
14
+ contact: 'http://www.w3.org/2000/10/swap/pim/contact#',
15
+ dc: 'http://purl.org/dc/elements/1.1/',
16
+ dct: 'http://purl.org/dc/terms/',
17
+ doap: 'http://usefulinc.com/ns/doap#',
18
+ foaf: 'http://xmlns.com/foaf/0.1/',
19
+ geo: 'http://www.w3.org/2003/01/geo/wgs84_pos#',
20
+ gpx: 'http://www.w3.org/ns/pim/gpx#',
21
+ gr: 'http://purl.org/goodrelations/v1#',
22
+ http: 'http://www.w3.org/2007/ont/http#',
23
+ httph: 'http://www.w3.org/2007/ont/httph#',
24
+ icalTZ: 'http://www.w3.org/2002/12/cal/icaltzd#',
25
+ ldp: 'http://www.w3.org/ns/ldp#',
26
+ link: 'http://www.w3.org/2007/ont/link#',
27
+ log: 'http://www.w3.org/2000/10/swap/log#',
28
+ meeting: 'http://www.w3.org/ns/pim/meeting#',
29
+ mo: 'http://purl.org/ontology/mo/',
30
+ org: 'http://www.w3.org/ns/org#',
31
+ owl: 'http://www.w3.org/2002/07/owl#',
32
+ pad: 'http://www.w3.org/ns/pim/pad#',
33
+ patch: 'http://www.w3.org/ns/pim/patch#',
34
+ prov: 'http://www.w3.org/ns/prov#',
35
+ pto: 'http://www.productontology.org/id/',
36
+ qu: 'http://www.w3.org/2000/10/swap/pim/qif#',
37
+ trip: 'http://www.w3.org/ns/pim/trip#',
38
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
39
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
40
+ rss: 'http://purl.org/rss/1.0/',
41
+ sched: 'http://www.w3.org/ns/pim/schedule#',
42
+ schema: 'http://schema.org/',
43
+ sioc: 'http://rdfs.org/sioc/ns#',
44
+ skos: 'http://www.w3.org/2004/02/skos/core#',
45
+ solid: 'http://www.w3.org/ns/solid/terms#',
46
+ space: 'http://www.w3.org/ns/pim/space#',
47
+ stat: 'http://www.w3.org/ns/posix/stat#',
48
+ ui: 'http://www.w3.org/ns/ui#',
49
+ vann: 'http://purl.org/vocab/vann/',
50
+ vcard: 'http://www.w3.org/2006/vcard/ns#',
51
+ wf: 'http://www.w3.org/2005/01/wf/flow#',
52
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
53
+ };
54
+
55
+ // ─── Accept types & format detection ─────────────────────────────────────────
56
+ export const ACCEPT_TYPES = [
57
+ 'text/turtle',
58
+ 'application/ld+json',
59
+ 'application/rdf+xml',
60
+ 'text/n3',
61
+ 'application/n-triples',
62
+ ];
63
+
64
+ export function detectFormat(ct, accept) {
65
+ if (ct.includes('turtle') || accept.includes('turtle')) return 'text/turtle';
66
+ if (ct.includes('ld+json') || accept.includes('ld+json')) return 'application/ld+json';
67
+ if (ct.includes('rdf+xml')) return 'application/rdf+xml';
68
+ if (ct.includes('n-triples') || accept.includes('n-triples')) return 'application/n-triples';
69
+ if (ct.includes('n3') || accept.includes('n3')) return 'text/n3';
70
+ return ct || 'text/turtle';
71
+ }
72
+
73
+ // ─── Term utilities ──────────────────────────────────────────────────────────
74
+ export function termToString(term) {
75
+ if (!term) return '';
76
+ if (typeof term === 'string') return term;
77
+ if (typeof term.toNT === 'function') return term.toNT();
78
+ if (typeof term.value === 'string') return term.value;
79
+ return String(term);
80
+ }
81
+
82
+ export function termToCell(term) {
83
+ if (!term) return { type: 'literal', value: '' };
84
+ if (term.termType === 'NamedNode') return { type: 'uri', value: term.value };
85
+ if (term.termType === 'BlankNode') return { type: 'bnode', value: term.value, _term: term };
86
+ // Preserve W3C SPARQL Query Results JSON literal extras when present.
87
+ const cell = { type: 'literal', value: term.value ?? String(term) };
88
+ if (term.language) cell['xml:lang'] = term.language;
89
+ if (term.datatype?.value) cell.datatype = term.datatype.value;
90
+ return cell;
91
+ }
92
+
93
+ // Wrap (vars, bindings) as the W3C SPARQL Query Results JSON envelope.
94
+ // All producers in this codebase use this so consumers see one shape.
95
+ export function w3cResults(vars, bindings) {
96
+ return { head: { vars }, results: { bindings } };
97
+ }
98
+
99
+ // Build a SPARQL PREFIX block from KNOWN_PREFIXES (plus optional extras).
100
+ // Used to make Comunica accept CURIE-style triple patterns ("?s foaf:name ?n")
101
+ // without forcing callers to declare every prefix themselves.
102
+ export function knownPrefixesAsSparql(extra = {}) {
103
+ const all = { ...KNOWN_PREFIXES, ...extra };
104
+ return Object.entries(all).map(([k, v]) => `PREFIX ${k}: <${v}>`).join('\n');
105
+ }
106
+
107
+ // ─── CURIE expansion ─────────────────────────────────────────────────────────
108
+ export function expandCurie(curie, extraPrefixes = {}) {
109
+ const colon = curie.indexOf(':');
110
+ if (colon < 0) return null;
111
+ const prefix = curie.slice(0, colon);
112
+ const local = curie.slice(colon + 1);
113
+ const ns = extraPrefixes[prefix] || KNOWN_PREFIXES[prefix];
114
+ return ns ? ns + local : null;
115
+ }
116
+
117
+ function _isCurie(token) {
118
+ return /^[a-zA-Z][a-zA-Z0-9]*:[^/\s]/.test(token);
119
+ }
120
+
121
+ function _resolveUri(raw, baseUri) {
122
+ if (!baseUri) return raw;
123
+ const docBase = baseUri.split('#')[0];
124
+ if (raw.startsWith('#')) return docBase + raw;
125
+ try { return new URL(raw, baseUri).href; } catch { return raw; }
126
+ }
127
+
128
+ export const _NAMED_VAR_RE = /^\?[A-Za-z_][A-Za-z0-9_]*$/;
129
+
130
+ // ─── Triple-pattern term parser ──────────────────────────────────────────────
131
+ // rdflib: object with sym(uri) and literal(value, langOrType) methods.
132
+ export function triplePatternTermToNode(term, rdflib, extraPrefixes = {}, baseUri = '') {
133
+ if (!term) throw new Error('Triple-pattern term is empty');
134
+ if (term === '?') {
135
+ throw new Error('Bare "?" is not allowed — use a named variable like "?x"');
136
+ }
137
+ if (term.startsWith('?')) {
138
+ if (!_NAMED_VAR_RE.test(term)) {
139
+ throw new Error(`Invalid variable "${term}" — must match ?[A-Za-z_][A-Za-z0-9_]*`);
140
+ }
141
+ return null;
142
+ }
143
+ if (term.startsWith('<') && term.endsWith('>')) {
144
+ const inner = term.slice(1, -1);
145
+ const resolved = (inner.startsWith('#') || inner.startsWith('.') || !/^[a-z][a-z0-9+\-.]*:/i.test(inner))
146
+ ? _resolveUri(inner, baseUri)
147
+ : inner;
148
+ return rdflib.sym(resolved);
149
+ }
150
+ if (term.startsWith('"')) {
151
+ const m = term.match(/^"([^"]*)"(?:\^\^<([^>]+)>|@([a-z-]+))?$/i);
152
+ if (!m) throw new Error(`Malformed literal "${term}" — must be "value" with optional @lang or ^^<datatype>`);
153
+ return rdflib.literal(m[1], m[3] || (m[2] ? rdflib.sym(m[2]) : undefined));
154
+ }
155
+ if (term.startsWith('#')) {
156
+ if (!baseUri) throw new Error(`Fragment "${term}" requires a baseUri to resolve`);
157
+ return rdflib.sym(_resolveUri(term, baseUri));
158
+ }
159
+ if (_isCurie(term)) {
160
+ const expanded = expandCurie(term, extraPrefixes);
161
+ return rdflib.sym(expanded || term);
162
+ }
163
+ throw new Error(
164
+ `Unrecognized term "${term}" — must be a named variable (?x), <uri>, prefix:local, #fragment, or quoted "literal"`
165
+ );
166
+ }
167
+
168
+ // ─── Tokenize a triple pattern ───────────────────────────────────────────────
169
+ export function tokenizeTriplePattern(input) {
170
+ const out = [];
171
+ const s = input.trim();
172
+ let i = 0;
173
+ while (i < s.length) {
174
+ while (i < s.length && /\s/.test(s[i])) i++;
175
+ if (i >= s.length) break;
176
+ if (s[i] === '"') {
177
+ let j = i + 1;
178
+ while (j < s.length && s[j] !== '"') {
179
+ if (s[j] === '\\' && j + 1 < s.length) j += 2;
180
+ else j++;
181
+ }
182
+ if (j >= s.length) throw new Error(`Unterminated literal starting at position ${i}`);
183
+ j++;
184
+ if (s[j] === '@') {
185
+ j++;
186
+ while (j < s.length && /[A-Za-z-]/.test(s[j])) j++;
187
+ } else if (s[j] === '^' && s[j + 1] === '^' && s[j + 2] === '<') {
188
+ j += 3;
189
+ while (j < s.length && s[j] !== '>') j++;
190
+ if (j >= s.length) throw new Error(`Unterminated datatype IRI`);
191
+ j++;
192
+ }
193
+ out.push(s.slice(i, j));
194
+ i = j;
195
+ } else {
196
+ let j = i;
197
+ while (j < s.length && !/\s/.test(s[j])) j++;
198
+ out.push(s.slice(i, j));
199
+ i = j;
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ // ─── Parse a triple-pattern string into [sNode, pNode, oNode] ───────────────
206
+ export function parsePatternParts(pattern, rdflib, extraPrefixes = {}, baseUri = '') {
207
+ const tokens = tokenizeTriplePattern(pattern);
208
+ if (tokens.length !== 3) {
209
+ throw new Error(`Triple pattern must have exactly 3 parts (subject predicate object) — got ${tokens.length}`);
210
+ }
211
+ const s = triplePatternTermToNode(tokens[0], rdflib, extraPrefixes, baseUri);
212
+ const p = triplePatternTermToNode(tokens[1], rdflib, extraPrefixes, baseUri);
213
+ const o = triplePatternTermToNode(tokens[2], rdflib, extraPrefixes, baseUri);
214
+ return [s, p, o];
215
+ }
216
+
217
+ // ─── Extract variable names from a triple pattern ───────────────────────────
218
+ export function patternVarNames(pattern) {
219
+ const tokens = tokenizeTriplePattern(pattern);
220
+ if (tokens.length !== 3) return {};
221
+ const out = {};
222
+ const slots = ['s', 'p', 'o'];
223
+ tokens.forEach((tok, i) => {
224
+ if (_NAMED_VAR_RE.test(tok)) out[slots[i]] = tok.slice(1);
225
+ });
226
+ return out;
227
+ }
228
+
229
+ // ─── store.match → W3C SPARQL Query Results JSON envelope ──────────────────
230
+ export function matchStore(store, s, p, o, names = {}) {
231
+ const stmts = store.match(s, p, o, null);
232
+ const slots = [];
233
+ if (!s) slots.push('s');
234
+ if (!p) slots.push('p');
235
+ if (!o) slots.push('o');
236
+ if (!slots.length) slots.push('s', 'p', 'o');
237
+
238
+ const cols = slots.map(slot => names[slot] || slot);
239
+ const bindings = stmts.map(st => {
240
+ const row = {};
241
+ slots.forEach((slot, i) => {
242
+ const node = slot === 's' ? st.subject : slot === 'p' ? st.predicate : st.object;
243
+ row[cols[i]] = termToCell(node);
244
+ });
245
+ return row;
246
+ });
247
+ return w3cResults(cols, bindings);
248
+ }
249
+
250
+ // ─── SPARQL helpers ──────────────────────────────────────────────────────────
251
+ export function selectVars(queryText) {
252
+ const stripped = queryText.replace(/#[^\n]*/g, '');
253
+ const m = stripped.match(/\bSELECT\s+(?:DISTINCT\s+|REDUCED\s+)?(.*?)\s+WHERE\b/is);
254
+ if (!m) return null;
255
+ const clause = m[1].trim();
256
+ if (clause === '*') return null;
257
+ const vars = clause.match(/\?\w+/g);
258
+ return vars ? vars.map(v => v.slice(1)) : null;
259
+ }
260
+
261
+ export function isRdfDoc(url) {
262
+ return /\.(ttl|rdf|n3|jsonld|nt|nq|owl|trig)(\?|#|$)/i.test(url);
263
+ }
264
+
265
+ export function bindingsToResults(bindings, queryText) {
266
+ const selectVarsResult = selectVars(queryText);
267
+ const allKeys = bindings.length
268
+ ? Object.keys(bindings[0]).map(k => k.replace(/^\?/, ''))
269
+ : [];
270
+ const vars = selectVarsResult ?? allKeys;
271
+ const out = bindings.map(b => {
272
+ const row = {};
273
+ for (const v of vars) {
274
+ const node = b[`?${v}`];
275
+ row[v] = node ? termToCell(node) : { type: 'literal', value: '' };
276
+ }
277
+ return row;
278
+ });
279
+ return w3cResults(vars, out);
280
+ }
@@ -0,0 +1,136 @@
1
+ // Turns the plain item descriptions produced by core/menu-rdf.js
2
+ // (parseMenuItems) into DOM render closures. Shared by <sol-menu> and
3
+ // <sol-tabs> so both render the identical ui:Menu RDF shape the same way.
4
+ //
5
+ // The descriptions are component-agnostic; only the closures below touch
6
+ // the DOM. A host element supplies a `ctx`:
7
+ //
8
+ // { host, baseUrl, sourceName, embedClass }
9
+ //
10
+ // host — element used for getAttribute('handler') and sol-error
11
+ // baseUrl — the host module's import.meta.url, for handler resolution
12
+ // sourceName — host tag name, used in error messages / event detail
13
+ // embedClass — CSS class added to each embedded element
14
+ // ('sol-menu-embed' / 'sol-tab-embed')
15
+
16
+ import { siblingUrl } from './here.js';
17
+ import { displayItem, contentForHref } from './display-target.js';
18
+
19
+ /**
20
+ * A ui:Component's `ui:name` is either a custom-element tag (render that
21
+ * component) or a *command* — an opaque registry key the host app resolves.
22
+ * Custom-element names must contain a hyphen (HTML spec), so a bare name that
23
+ * isn't a registered element is a command. The name is NOT a tag, a global, or
24
+ * a script: clicking it dispatches `sol-command` for the app to map; an
25
+ * unregistered key is a no-op. Bounded entirely by the app's registry.
26
+ *
27
+ * @param {string} name a ui:Component ui:name value
28
+ * @returns {boolean} true when it should be treated as a command
29
+ */
30
+ export function isCommandName(name) {
31
+ if (!name) return false;
32
+ return !name.includes('-') && !customElements.get(name);
33
+ }
34
+
35
+ /** ui:attribute/ui:parameter pairs [[k,v],…] → { k: v, … } command args. */
36
+ export function paramsToObject(params) {
37
+ return Object.fromEntries(params || []);
38
+ }
39
+
40
+ /**
41
+ * Dispatch a menu command. `command` is the registry key (from a ui:Component
42
+ * `ui:name`); `params` is the args object. Bubbling + composed so one
43
+ * document-level listener in the host app catches it.
44
+ *
45
+ * @param {HTMLElement} host
46
+ * @param {string} command
47
+ * @param {object} [params]
48
+ */
49
+ export function dispatchCommand(host, command, params) {
50
+ host.dispatchEvent(new CustomEvent('sol-command', {
51
+ bubbles: true, composed: true,
52
+ detail: { command, params: params || {}, source: host },
53
+ }));
54
+ }
55
+
56
+ /**
57
+ * Lazy-import a sibling sol-* handler module on first use, so authors
58
+ * don't have to <script> every component a declared item references.
59
+ *
60
+ * @param {string} tag custom-element tag, e.g. "sol-query"
61
+ * @param {HTMLElement} host element that emits sol-error on failure
62
+ * @param {string} baseUrl importing component's import.meta.url
63
+ * @param {string} sourceName host tag name, for the warning / event
64
+ */
65
+ export function ensureHandler(tag, host, baseUrl, sourceName) {
66
+ if (!/^sol-[a-z-]+$/.test(tag)) return;
67
+ if (customElements.get(tag)) return;
68
+ import(siblingUrl(`./${tag}.js`, baseUrl)).catch(err => {
69
+ const msg = `<${sourceName}> could not auto-load handler "${tag}" — make sure its module is reachable and any externals are in the importmap (${err.message})`;
70
+ console.warn(msg);
71
+ if (host) host.dispatchEvent(new CustomEvent('sol-error', {
72
+ bubbles: true, composed: true,
73
+ detail: { source: sourceName, kind: 'handler-load', tag, message: err.message },
74
+ }));
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Build a render closure for a ui:Component part. Placement is resolved from
80
+ * the HTML at click time by the dispatcher (region= cascade off the host,
81
+ * `data-for` claim by this item's id, or the host's own body as fallback).
82
+ * Components default to keep-alive.
83
+ *
84
+ * @param {object} desc { id, name, tag, params }
85
+ * @param {object} ctx { host, baseUrl, sourceName, embedClass }
86
+ * @returns {(body: HTMLElement) => void}
87
+ */
88
+ export function renderComponentItem(desc, ctx) {
89
+ return (body) => {
90
+ const { id, name, tag, params } = desc;
91
+ if (!tag) return;
92
+ const ensure = (t) => ensureHandler(t, ctx.host, ctx.baseUrl, ctx.sourceName);
93
+ ensure(tag);
94
+ displayItem({
95
+ launcher: ctx.host, id, name: name || id,
96
+ tag, attrs: params, replace: false,
97
+ embedClass: ctx.embedClass, fallbackEl: body, ensure,
98
+ });
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Build a render closure for a ui:Link part. A `ui:contents` literal is
104
+ * injected as HTML; otherwise the `ui:href` is rendered by the origin-inferred
105
+ * element (same-origin → trusted `sol-include`, external → `iframe`). A
106
+ * non-default viewer is expressed as a `ui:Component`, not a handler.
107
+ * Placement is resolved from the HTML by the dispatcher (region= / data-for).
108
+ *
109
+ * @param {object} desc { id, name, href, contents }
110
+ * @param {object} ctx { host, baseUrl, sourceName, embedClass }
111
+ * @returns {(body: HTMLElement) => void}
112
+ */
113
+ export function renderLinkItem(desc, ctx) {
114
+ return (body) => {
115
+ const { id, name, href, contents } = desc;
116
+ const ensure = (t) => ensureHandler(t, ctx.host, ctx.baseUrl, ctx.sourceName);
117
+
118
+ if (contents != null) {
119
+ displayItem({
120
+ launcher: ctx.host, id, name: name || id, contents,
121
+ embedClass: ctx.embedClass, fallbackEl: body, ensure,
122
+ });
123
+ return;
124
+ }
125
+ if (!href) return;
126
+
127
+ const { tag, attrs, replace } = contentForHref(href);
128
+ ensure(tag);
129
+
130
+ displayItem({
131
+ launcher: ctx.host, id, name: name || id,
132
+ tag, attrs, href, replace,
133
+ embedClass: ctx.embedClass, fallbackEl: body, ensure,
134
+ });
135
+ };
136
+ }