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,546 @@
1
+ /**
2
+ * <sol-query> — Query and display RDF / Linked Data from plain HTML.
3
+ *
4
+ * Supports triple patterns, inline SPARQL, stored SPARQL queries, and
5
+ * CSS-selector extraction from RDFa documents. Results render via
6
+ * pluggable views (table, dl, list, accordion, anchorlist, auto-complete,
7
+ * menu, rolodex, select, tabs).
8
+ *
9
+ * @element sol-query
10
+ * @attr {string} endpoint - URL of an RDF document or SPARQL endpoint. May be
11
+ * a comma- or whitespace-separated list of URLs; when more than one is
12
+ * given, the query is federated across all sources via Comunica.
13
+ * @attr {string} pattern - triple pattern (e.g. "?s ?p ?o") or CSS selector
14
+ * @attr {string} sparql - inline SPARQL string or URL of a stored SPARQL query
15
+ * @attr {string} query - alias for sparql
16
+ * @attr {string} view - result view: table (default), dl, list, accordion,
17
+ * anchorlist, auto-complete, menu, rolodex, select, tabs
18
+ * @attr {string} var-* - bind SPARQL variables (e.g. var-name="Alice")
19
+ *
20
+ * @fires sol-deref — detail: { uri }; cancelable, default navigates endpoint
21
+ * @fires sol-select — detail: { value, row, index }; from select/auto-complete/rolodex views
22
+ *
23
+ * @example
24
+ * <sol-query endpoint="https://example.org/data.ttl"></sol-query>
25
+ * <sol-query endpoint="data.ttl" pattern="?s foaf:name ?name"></sol-query>
26
+ * <sol-query endpoint="https://dbpedia.org/sparql"
27
+ * sparql="SELECT ?s ?label WHERE { ?s a dbo:City; rdfs:label ?label } LIMIT 10"
28
+ * view="table"></sol-query>
29
+ */
30
+ import {
31
+ fetchQueryFromRdf,
32
+ loadRdfStore,
33
+ matchStore,
34
+ parsePatternParts,
35
+ pivotSubjectsToRows,
36
+ promoteDisplayColumns,
37
+ patternVarNames,
38
+ storeToResults,
39
+ expandBnodes,
40
+ runQuery,
41
+ execSparql,
42
+ queryHtmlWithSelector,
43
+ knownPrefixesAsSparql,
44
+ } from '../core/rdf-utils.js';
45
+ import { ComunicaSparqlAdapter } from '../core/utils.js';
46
+ import { assertSafeQuery, sanitizeVarValue, substituteVariables } from '../core/sparql-safety.js';
47
+ import { getAuthFetch } from '../core/auth-fetch.js';
48
+ import { rdf } from '../core/rdf.js';
49
+ import { SparqlResultsRenderer } from './utils/sol-query-ui.js';
50
+ import { loadBuiltinView } from './utils/sol-query-views.js';
51
+ import { TriplePatternValidator, TriplePatternParser } from './utils/sol-query-triple-patterns.js';
52
+ import { define } from '../core/define.js';
53
+ import { adopt } from '../core/adopt.js';
54
+ import { CSS as QUERY_CSS, sheet as querySheet } from './styles/sol-query-css.js';
55
+
56
+ // Built-in views are loaded on demand from a module shared with the
57
+ // data-from-query activator (so the attribute and the element stay decoupled).
58
+
59
+ /**
60
+ * Query and display RDF / Linked Data from plain HTML.
61
+ *
62
+ * Supports triple patterns, inline/stored SPARQL, and CSS-selector
63
+ * extraction from RDFa documents. Results render via pluggable views.
64
+ *
65
+ * @class SolQuery
66
+ * @extends HTMLElement
67
+ * @attr {string} endpoint - URL of an RDF document or SPARQL endpoint. May be
68
+ * a comma- or whitespace-separated list of URLs; when more than one is
69
+ * given, the query is federated across all sources via Comunica.
70
+ * @attr {string} pattern - triple pattern or CSS selector (alias: wanted)
71
+ * @attr {string} sparql - inline SPARQL string or URL of a stored query
72
+ * @attr {string} query - alias for sparql
73
+ * @attr {string} view - result view: table|dl|list|accordion|anchorlist|auto-complete|menu|rolodex|select|tabs
74
+ * @fires sol-deref - detail: { uri }; cancelable, default navigates endpoint
75
+ * @fires sol-select - detail: { value, row, index }; from select/auto-complete/rolodex views
76
+ */
77
+ class SolQuery extends HTMLElement {
78
+ constructor() {
79
+ super();
80
+ this.attachShadow({ mode: 'open' });
81
+ this.render();
82
+ }
83
+
84
+ static get observedAttributes() {
85
+ return ['endpoint', 'pattern', 'wanted', 'sparql', 'query', 'view'];
86
+ }
87
+
88
+ connectedCallback() {
89
+ if (this.isConnected) this.initializeQuery();
90
+ }
91
+
92
+ attributeChangedCallback(name, oldValue, newValue) {
93
+ if (oldValue !== newValue && this.isConnected) {
94
+ if (['endpoint', 'pattern', 'wanted', 'sparql', 'query'].includes(name) || name.startsWith('var-')) {
95
+ this.initializeQuery();
96
+ }
97
+ }
98
+ }
99
+
100
+ // `query` is accepted as an alias for `sparql` so authors can write either.
101
+ _sparqlAttr() {
102
+ return this.getAttribute('sparql') ?? this.getAttribute('query');
103
+ }
104
+
105
+ // `wanted` is accepted as an alias for `pattern`.
106
+ _patternAttr() {
107
+ return this.getAttribute('pattern') ?? this.getAttribute('wanted');
108
+ }
109
+
110
+ render() {
111
+ this.shadowRoot.innerHTML = `
112
+ <div class="container" role="region" aria-live="polite" aria-label="Query results">
113
+ <div class="loading" role="status">Ready to execute query...</div>
114
+ </div>
115
+ `;
116
+ adopt(this.shadowRoot, { sheet: querySheet, css: QUERY_CSS });
117
+ const container = this.shadowRoot.querySelector('.container');
118
+ this.renderer = new SparqlResultsRenderer(container);
119
+
120
+ // ── Dereference: click a URI cell to load it as a new query (13) ──────────
121
+ // Ctrl/Cmd/Shift+click still opens the link in a new tab normally.
122
+ container.addEventListener('click', e => {
123
+ const a = e.target.closest('a[data-uri]');
124
+ if (!a || e.ctrlKey || e.metaKey || e.shiftKey) return;
125
+ e.preventDefault();
126
+ const uri = a.dataset.uri;
127
+ const ev = new CustomEvent('sol-deref', {
128
+ bubbles: true, composed: true, cancelable: true, detail: { uri },
129
+ });
130
+ if (this.dispatchEvent(ev)) {
131
+ this.removeAttribute('sparql');
132
+ this.removeAttribute('pattern');
133
+ this.removeAttribute('wanted');
134
+ this.setAttribute('endpoint', uri);
135
+ }
136
+ });
137
+ }
138
+
139
+ async initializeQuery() {
140
+ const endpoints = this._endpoints();
141
+ if (!endpoints.length) { this._reportError('config', 'No endpoint provided'); return; }
142
+
143
+ // The single-endpoint default-query path uses a #fragment to filter the
144
+ // store to one subject — an rdflib trick that doesn't carry over to
145
+ // Comunica federation. Reject rather than silently fetch the doc and
146
+ // return all of its triples.
147
+ if (endpoints.length > 1 && endpoints.some(e => e.includes('#'))) {
148
+ this._reportError('config', 'Subject fragments are not supported in federated queries');
149
+ return;
150
+ }
151
+
152
+ if (this.hasAttribute('sparql') || this.hasAttribute('query')) {
153
+ await this.handleSparqlQuery();
154
+ } else if (this.hasAttribute('pattern') || this.hasAttribute('wanted')) {
155
+ await this.handleTriplePattern();
156
+ } else {
157
+ await this.handleDefaultQuery();
158
+ }
159
+ }
160
+
161
+ // The `endpoint` attribute may carry one URL or a list separated by
162
+ // commas/whitespace. Multi-endpoint queries are federated by Comunica.
163
+ _endpoints() {
164
+ const raw = this.getAttribute('endpoint') || '';
165
+ return raw.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
166
+ }
167
+
168
+ // Render the error in the shadow DOM and dispatch a bubbling sol-error
169
+ // event so page-level orchestration can react.
170
+ _reportError(kind, message) {
171
+ this.renderer.showError(message);
172
+ this.dispatchEvent(new CustomEvent('sol-error', {
173
+ bubbles: true, composed: true,
174
+ detail: { source: 'sol-query', kind, message },
175
+ }));
176
+ }
177
+
178
+ // ─── SPARQL query (inline text or URL pointing to RDF file) ────────────────
179
+ async handleSparqlQuery() {
180
+ const sparqlAttr = this._sparqlAttr();
181
+ // A stored-query reference is a bare URL: no whitespace, starts with http(s):// or a path.
182
+ // Everything else — including all valid SPARQL text — is treated as inline.
183
+ const isStoredRef = !/\s/.test(sparqlAttr) && /^https?:\/\/|^\/|^\.\.?\//.test(sparqlAttr.trim());
184
+ if (!isStoredRef) {
185
+ this.executeQuery(sparqlAttr);
186
+ } else {
187
+ this.renderer.showLoading('Loading query from RDF file...');
188
+ try {
189
+ const query = await fetchQueryFromRdf(sparqlAttr);
190
+ if (query) {
191
+ try { assertSafeQuery(query); } catch (err) { this._reportError('sparql-unsafe', err.message); return; }
192
+ this.executeQuery(query);
193
+ } else this._reportError('sparql-empty', 'No query found in RDF file');
194
+ } catch (err) {
195
+ this._reportError('sparql-load', `Failed to load query: ${err.message}`);
196
+ }
197
+ }
198
+ }
199
+
200
+ // ─── Triple pattern / CSS selector ───────────────────────────────────────────
201
+ async handleTriplePattern() {
202
+ const pattern = this._patternAttr();
203
+ const endpoints = this._endpoints();
204
+
205
+ // Multi-endpoint federation goes through Comunica. CSS-selector / HTML-RDFa
206
+ // and the rdflib bnode-expansion / ?s-pivot features only apply to a single
207
+ // local store, so they stay on the legacy path below.
208
+ if (endpoints.length > 1) {
209
+ return this._handleTriplePatternFederated(pattern, endpoints);
210
+ }
211
+ const endpoint = endpoints[0];
212
+
213
+ this.renderer.showLoading('Loading…');
214
+ try {
215
+ const store = await loadRdfStore(endpoint, this._authFetch(endpoint));
216
+
217
+ if (store._isHtml) {
218
+ const validation = TriplePatternValidator.validate(pattern);
219
+ if (!validation.valid) {
220
+ const results = queryHtmlWithSelector(store._rawHtml, endpoint, pattern);
221
+ if (!results.results.bindings.length) { this.renderer.showError('No elements matched selector'); return; }
222
+ return this._dispatchResults(results);
223
+ }
224
+ } else {
225
+ const validation = TriplePatternValidator.validate(pattern);
226
+ if (!validation.valid) { this._reportError('pattern-invalid', validation.error); return; }
227
+ }
228
+
229
+ const rdflib = rdf;
230
+ const [s, p, o] = parsePatternParts(pattern, rdflib, {}, endpoint);
231
+ const names = patternVarNames(pattern);
232
+
233
+ // When only the subject is a variable (e.g. `?person schema:gender "female"`),
234
+ // widen each matched subject into a row with one column per predicate so
235
+ // authors get a useful property table instead of a single-column list.
236
+ let results;
237
+ if (!s && p && o) {
238
+ const stmts = store.match(null, p, o, null) || [];
239
+ const seen = new Set();
240
+ const subjects = [];
241
+ for (const st of stmts) {
242
+ if (!seen.has(st.subject.value)) {
243
+ seen.add(st.subject.value);
244
+ subjects.push(st.subject);
245
+ }
246
+ }
247
+ results = pivotSubjectsToRows(store, subjects, names.s || 's');
248
+ } else {
249
+ results = expandBnodes(store, matchStore(store, s, p, o, names));
250
+ }
251
+ results = promoteDisplayColumns(results, names.s || 's');
252
+ if (!results.results.bindings.length) { this.renderer.showError('No matching triples found'); return; }
253
+ this._dispatchResults(results);
254
+ } catch (err) {
255
+ this._reportError('query-failed', err.message);
256
+ }
257
+ }
258
+
259
+ // Federate a triple pattern across multiple sources via Comunica. The
260
+ // pattern is wrapped in a SPARQL SELECT and run with `sources: endpoints`,
261
+ // so any mix of SPARQL endpoints and RDF documents is queried as one graph.
262
+ async _handleTriplePatternFederated(pattern, endpoints) {
263
+ const validation = TriplePatternValidator.validate(pattern);
264
+ if (!validation.valid) { this._reportError('pattern-invalid', validation.error); return; }
265
+
266
+ if (!ComunicaSparqlAdapter.getComunicaEngine()) {
267
+ this._reportError('config', 'Multiple endpoints require Comunica');
268
+ return;
269
+ }
270
+
271
+ let where;
272
+ try { where = new TriplePatternParser(endpoints[0]).parse(pattern); }
273
+ catch (err) { this._reportError('pattern-invalid', err.message); return; }
274
+ const sparql = `${knownPrefixesAsSparql()}\n${where}`;
275
+
276
+ this.renderer.showLoading('Loading…');
277
+ try {
278
+ let results = await execSparql(sparql, endpoints, this._authFetch(endpoints[0]));
279
+ const names = patternVarNames(pattern);
280
+ results = promoteDisplayColumns(results, names.s || 's');
281
+ if (!results.results.bindings.length) { this.renderer.showError('No matching triples found'); return; }
282
+ this._dispatchResults(results);
283
+ } catch (err) {
284
+ this._reportError('query-failed', err.message);
285
+ }
286
+ }
287
+
288
+ // ─── Default: load store; if endpoint has a #fragment filter to that subject ──
289
+ async handleDefaultQuery() {
290
+ const endpoints = this._endpoints();
291
+ if (endpoints.length > 1) {
292
+ // Federated default: stream all triples from every source via Comunica.
293
+ // The single-source #fragment filter and bnode-expansion don't apply.
294
+ if (!ComunicaSparqlAdapter.getComunicaEngine()) {
295
+ this._reportError('config', 'Multiple endpoints require Comunica');
296
+ return;
297
+ }
298
+ this.renderer.showLoading('Loading RDF data…');
299
+ try {
300
+ const sparql = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }';
301
+ let results = await execSparql(sparql, endpoints, this._authFetch(endpoints[0]));
302
+ if (!results.results.bindings.length) { this.renderer.showError('No RDF data found at endpoints'); return; }
303
+ this._dispatchResults(promoteDisplayColumns(results, 's'));
304
+ } catch (err) {
305
+ this.showDiagnostics(err);
306
+ }
307
+ return;
308
+ }
309
+ const endpoint = endpoints[0];
310
+ this.renderer.showLoading('Loading RDF data…');
311
+ try {
312
+ const docUrl = endpoint.includes('#') ? endpoint.split('#')[0] : endpoint;
313
+ const store = await loadRdfStore(docUrl, this._authFetch(docUrl));
314
+ let results;
315
+ const isSubjectQuery = endpoint.includes('#');
316
+ if (isSubjectQuery) {
317
+ const rdflib = rdf;
318
+ results = matchStore(store, rdflib.sym(endpoint), null, null);
319
+ } else {
320
+ // Pivot by subject: one row per subject, columns per predicate —
321
+ // reads as a property table rather than a flat list of triples.
322
+ const stmts = typeof store.match === 'function' ? store.match(null, null, null) : [];
323
+ const seen = new Set();
324
+ const subjects = [];
325
+ for (const st of stmts) {
326
+ if (!seen.has(st.subject.value)) {
327
+ seen.add(st.subject.value);
328
+ subjects.push(st.subject);
329
+ }
330
+ }
331
+ results = pivotSubjectsToRows(store, subjects, 's');
332
+ }
333
+ results = expandBnodes(store, results);
334
+ if (!results.results.bindings.length) {
335
+ this.renderer.showError(store._isHtml ? 'No RDFa found in HTML page' : 'No RDF data found at endpoint');
336
+ return;
337
+ }
338
+ if (isSubjectQuery) {
339
+ this._renderSubject(results);
340
+ } else {
341
+ this._dispatchResults(promoteDisplayColumns(results, 's'));
342
+ }
343
+ } catch (err) {
344
+ this.showDiagnostics(err);
345
+ }
346
+ }
347
+
348
+ // Single-subject view: H2 banner with name/label/title, then one row per
349
+ // predicate ("field value") — rdf:type first, other properties after.
350
+ // Rendered inline so every row is styled the same — no table header,
351
+ // no promoted <dt>, no special treatment for the type row.
352
+ _renderSubject(results) {
353
+ const NAME_PREDS = [
354
+ 'http://xmlns.com/foaf/0.1/name',
355
+ 'https://schema.org/name',
356
+ 'http://schema.org/name',
357
+ 'http://purl.org/dc/terms/title',
358
+ 'http://purl.org/dc/elements/1.1/title',
359
+ 'http://www.w3.org/2000/01/rdf-schema#label',
360
+ ];
361
+ const TYPE_PRED = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
362
+ const shortName = uri => (uri || '').replace(/.*[/#]([^/#]+)\/?$/, '$1') || uri;
363
+
364
+ const bindings = results.results.bindings;
365
+ const nameRow = bindings.find(r => NAME_PREDS.includes(r.p?.value));
366
+ const rest = nameRow ? bindings.filter(r => r !== nameRow) : bindings;
367
+ const typeRows = rest.filter(r => r.p?.value === TYPE_PRED);
368
+ const others = rest.filter(r => r.p?.value !== TYPE_PRED);
369
+ const ordered = [...typeRows, ...others];
370
+
371
+ const container = this.shadowRoot.querySelector('.container');
372
+ container.innerHTML = '';
373
+
374
+ if (nameRow?.o?.value) {
375
+ const h2 = document.createElement('h2');
376
+ h2.className = 'sol-subject-header';
377
+ h2.textContent = nameRow.o.value;
378
+ container.appendChild(h2);
379
+ }
380
+
381
+ const dl = document.createElement('dl');
382
+ for (const row of ordered) {
383
+ const dd = document.createElement('dd');
384
+ const label = document.createElement('span');
385
+ label.className = 'dl-field';
386
+ label.textContent = `${shortName(row.p?.value)} `;
387
+ dd.appendChild(label);
388
+
389
+ const value = document.createElement('span');
390
+ value.className = 'dl-value';
391
+ this._appendCell(value, row.o);
392
+ dd.appendChild(value);
393
+ dl.appendChild(dd);
394
+ }
395
+ container.appendChild(dl);
396
+ }
397
+
398
+ _appendCell(parent, cell) {
399
+ if (!cell) return;
400
+ if (cell.type === 'uri') {
401
+ const shortName = uri => (uri || '').replace(/.*[/#]([^/#]+)\/?$/, '$1') || uri;
402
+ const a = document.createElement('a');
403
+ a.href = cell.value;
404
+ a.title = cell.value;
405
+ a.target = '_blank';
406
+ a.rel = 'noopener noreferrer';
407
+ a.dataset.uri = cell.value;
408
+ a.textContent = shortName(cell.value);
409
+ parent.appendChild(a);
410
+ } else if (cell.type === 'multi') {
411
+ cell.values.forEach((v, i) => {
412
+ if (i > 0) parent.appendChild(document.createTextNode(', '));
413
+ this._appendCell(parent, v);
414
+ });
415
+ } else {
416
+ parent.appendChild(document.createTextNode(cell.value ?? ''));
417
+ }
418
+ }
419
+
420
+ // ─── Dispatch results through built-in renderer or custom view module ──────
421
+ async _dispatchResults(results, options = {}) {
422
+ const view = this.getAttribute('view') || 'table';
423
+
424
+ // Custom view by URL: dynamic import, call render(container, data)
425
+ if (/^https?:\/\/|^\.\/|^\.\.\/|^\//.test(view)) {
426
+ await this._loadAndRenderView(view, results, /* byUrl */ true);
427
+ return;
428
+ }
429
+
430
+ // Built-in views are imported on demand. The all-in-one Rollup bundle
431
+ // inlines these dynamic imports at build time; ESM/importmap consumers
432
+ // pay only for the views they actually use.
433
+ let fn;
434
+ try {
435
+ fn = await loadBuiltinView(view);
436
+ } catch (err) {
437
+ this._reportError('view-load', `Failed to load view "${view}": ${err.message}`);
438
+ return;
439
+ }
440
+ if (!fn) {
441
+ this._reportError('view-unknown', `Unknown view: ${view}`);
442
+ return;
443
+ }
444
+
445
+ // Table, dl, list get preprocessing (pivot s/p/o, group predicates, scalar display)
446
+ if (view === 'table' || view === 'dl' || view === 'list') {
447
+ this.renderer.renderResults(results, fn, options);
448
+ return;
449
+ }
450
+
451
+ const container = this.shadowRoot.querySelector('.container');
452
+ container.innerHTML = '';
453
+ try { await fn(container, results, this); }
454
+ catch (err) { this._reportError('view-render', `View "${view}" error: ${err.message}`); }
455
+ }
456
+
457
+ async _loadAndRenderView(url, results, byUrl, viewName = null) {
458
+ const container = this.shadowRoot.querySelector('.container');
459
+ container.innerHTML = '<div class="loading">Loading view…</div>';
460
+ try {
461
+ const mod = await import(/* @vite-ignore */ url);
462
+ const fn = mod.render ?? mod.default;
463
+ if (typeof fn !== 'function')
464
+ throw new Error(`Module must export render(container, data)`);
465
+ container.innerHTML = '';
466
+ fn(container, results, this);
467
+ } catch (err) {
468
+ const label = byUrl ? 'Custom view' : `View "${viewName}"`;
469
+ this._reportError('view-render', `${label} error: ${err.message}`);
470
+ }
471
+ }
472
+
473
+ // ─── SPARQL execution (with adapter fallback chain) ────────────────────────
474
+ getVariables() {
475
+ const vars = {};
476
+ for (const attr of this.attributes) {
477
+ if (attr.name.startsWith('var-')) vars[attr.name.slice(4)] = attr.value;
478
+ }
479
+ return vars;
480
+ }
481
+
482
+ substituteVariables(query) {
483
+ return substituteVariables(query, this.getVariables());
484
+ }
485
+
486
+ async executeQuery(query) {
487
+ if (!query) { this._reportError('config', 'No query provided'); return; }
488
+ let processed;
489
+ try {
490
+ processed = this.substituteVariables(query);
491
+ assertSafeQuery(processed);
492
+ } catch (err) {
493
+ this._reportError('sparql-unsafe', err.message);
494
+ return;
495
+ }
496
+ const endpoints = this._endpoints();
497
+ if (!endpoints.length) { this._reportError('config', 'No endpoint provided'); return; }
498
+ const target = endpoints.length > 1 ? endpoints : endpoints[0];
499
+ this.renderer.showLoading();
500
+ try {
501
+ const results = await this.fetchResults(processed, target);
502
+ await this._dispatchResults(results);
503
+ } catch (err) {
504
+ this._reportError('query-failed', err.message);
505
+ }
506
+ }
507
+
508
+ fetchResults(query, endpoint) {
509
+ const fetchUrl = Array.isArray(endpoint) ? endpoint[0] : endpoint;
510
+ return execSparql(query, endpoint, this._authFetch(fetchUrl));
511
+ }
512
+
513
+ // Resolve an authenticated fetch for `url`. The `login` attribute, when
514
+ // set, is a CSS selector for a specific <sol-login> element (matches the
515
+ // sol-pod / sol-pod-ops / sol-wac convention). Otherwise getAuthFetch
516
+ // walks the document and uses the first <sol-login> on the page; falls
517
+ // back to the global fetch when none is logged in.
518
+ _authFetch(url) {
519
+ const sel = this.getAttribute('login');
520
+ const el = sel ? document.querySelector(sel) : null;
521
+ return getAuthFetch(url, { element: el || undefined });
522
+ }
523
+
524
+ // ─── Instance API ──────────────────────────────────────────────────────────
525
+ setEndpoint(endpoint) { this.setAttribute('endpoint', endpoint); }
526
+ setPattern(triplePattern) { this.setAttribute('pattern', triplePattern); }
527
+ setWanted(triplePattern) { this.setPattern(triplePattern); }
528
+ setSparql(sparql) { this.setAttribute('sparql', sparql); }
529
+ setVariable(n, v) { this.setAttribute(`var-${n}`, v); }
530
+ setVariables(obj) { for (const [k, v] of Object.entries(obj)) this.setVariable(k, v); }
531
+
532
+ // ─── Static API ────────────────────────────────────────────────────────────
533
+ // Returns a plain JS array of objects, or a scalar for 1×1 results.
534
+ // Example:
535
+ // const rows = await SolQuery.run({ endpoint, pattern: '?s foaf:name ?name' });
536
+ // const name = await SolQuery.run({ endpoint, sparql: 'SELECT ?n WHERE{...}', vars: ['n'] });
537
+ static run(opts) { return runQuery(opts); }
538
+
539
+ // ─── Diagnostics ───────────────────────────────────────────────────────────
540
+ showDiagnostics(err) {
541
+ this._reportError('rdf-load', `Failed to load RDF: ${err.message}`);
542
+ }
543
+ }
544
+
545
+ define('sol-query', SolQuery);
546
+ export { SolQuery };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * <sol-rolodex> — Card-by-card browser web component.
3
+ *
4
+ * Wraps child `<div>` elements into a rolodex-style card viewer with
5
+ * previous/next navigation and keyboard arrow support.
6
+ *
7
+ * @element sol-rolodex
8
+ *
9
+ * @fires sol-select — detail: { value, row, index }
10
+ *
11
+ * @example
12
+ * <sol-rolodex>
13
+ * <div>Card 1 content</div>
14
+ * <div>Card 2 content</div>
15
+ * </sol-rolodex>
16
+ */
17
+ import { render as renderRolodex } from './views/rolodex.js';
18
+ import { define } from '../core/define.js';
19
+
20
+ /**
21
+ * Card-by-card browser web component.
22
+ *
23
+ * Wraps child `<div>` elements into a rolodex-style card viewer with
24
+ * previous/next navigation and keyboard arrow support.
25
+ *
26
+ * @class SolRolodex
27
+ * @extends HTMLElement
28
+ */
29
+ class SolRolodex extends HTMLElement {
30
+ constructor() {
31
+ super();
32
+ this.attachShadow({ mode: 'open' });
33
+ }
34
+
35
+ async connectedCallback() {
36
+ const items = Array.from(this.children).filter(el => el.tagName === 'DIV');
37
+
38
+ const vars = ['content'];
39
+ const bindings = items.map(div => ({
40
+ content: {
41
+ type: 'html',
42
+ node: div.cloneNode(true)
43
+ }
44
+ }));
45
+
46
+ const container = document.createElement('div');
47
+ this.shadowRoot.appendChild(container);
48
+
49
+ const originalRenderCellInto = window._renderCellInto;
50
+
51
+ const data = { head: { vars }, results: { bindings } };
52
+
53
+ await renderRolodex(container, data, this);
54
+
55
+ const cardEl = container.querySelector('.rolodex-card');
56
+ const counterEl = container.querySelector('.rolodex-counter');
57
+ const prevBtn = container.querySelector('.rolodex-btn[aria-label="Previous record"]');
58
+ const nextBtn = container.querySelector('.rolodex-btn[aria-label="Next record"]');
59
+ const wrapper = container.querySelector('.sol-view-rolodex');
60
+
61
+ let index = 0;
62
+
63
+ const show = i => {
64
+ index = ((i % bindings.length) + bindings.length) % bindings.length;
65
+ const node = bindings[index].content.node.cloneNode(true);
66
+ cardEl.innerHTML = '';
67
+ cardEl.appendChild(node);
68
+ counterEl.textContent = `${index + 1} of ${bindings.length}`;
69
+ };
70
+
71
+ prevBtn.replaceWith(prevBtn.cloneNode(true));
72
+ nextBtn.replaceWith(nextBtn.cloneNode(true));
73
+
74
+ const newPrev = container.querySelector('.rolodex-btn[aria-label="Previous record"]');
75
+ const newNext = container.querySelector('.rolodex-btn[aria-label="Next record"]');
76
+
77
+ newPrev.addEventListener('click', () => show(index - 1));
78
+ newNext.addEventListener('click', () => show(index + 1));
79
+
80
+ const newWrapper = container.querySelector('.sol-view-rolodex');
81
+ newWrapper.addEventListener('keydown', e => {
82
+ if (e.key === 'ArrowLeft') {
83
+ e.preventDefault();
84
+ show(index - 1);
85
+ } else if (e.key === 'ArrowRight') {
86
+ e.preventDefault();
87
+ show(index + 1);
88
+ }
89
+ });
90
+
91
+ show(0);
92
+ }
93
+ }
94
+
95
+ define('sol-rolodex', SolRolodex);