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.
- package/README.md +7 -0
- package/core/activate.js +27 -0
- package/core/adopt.js +71 -0
- package/core/auth-core.js +73 -0
- package/core/auth-fetch.js +154 -0
- package/core/component-mount.js +110 -0
- package/core/defaults.js +48 -0
- package/core/define.js +15 -0
- package/core/display-target.js +166 -0
- package/core/edit-placements.js +28 -0
- package/core/editor-self.js +127 -0
- package/core/editor.js +162 -0
- package/core/events.js +27 -0
- package/core/extension-points.js +189 -0
- package/core/form-utils.js +210 -0
- package/core/from-query.js +138 -0
- package/core/from-rdf.js +52 -0
- package/core/here.js +33 -0
- package/core/include-core.js +73 -0
- package/core/inrupt-global.js +18 -0
- package/core/menu-consumer.js +41 -0
- package/core/menu-rdf.js +154 -0
- package/core/pod-ops.js +392 -0
- package/core/pod-registry.js +82 -0
- package/core/popup-proxy.js +255 -0
- package/core/rdf-core.js +280 -0
- package/core/rdf-render.js +136 -0
- package/core/rdf-utils.js +411 -0
- package/core/rdf.js +154 -0
- package/core/services.js +106 -0
- package/core/shape-to-form.js +741 -0
- package/core/sparql-safety.js +20 -0
- package/core/utils.js +196 -0
- package/dist/importmap-cdn.json +49 -0
- package/dist/importmap-local.json +49 -0
- package/dist/sol-loader.manifest.json +140 -0
- package/dist/vendor/@comunica-query-sparql.js +137851 -0
- package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
- package/dist/vendor/dompurify.js +1476 -0
- package/dist/vendor/ical.js.js +9739 -0
- package/dist/vendor/marked.js +85 -0
- package/dist/vendor/n3.js +14670 -0
- package/dist/vendor/rdf-validate-shacl.js +6970 -0
- package/dist/vendor/rdflib.js +35172 -0
- package/dist/vendor/solid-logic.js +6819 -0
- package/dist/vendor/solid-ui.js +21945 -0
- package/node/sol-form.js +133 -0
- package/node/sol-include.js +55 -0
- package/node/sol-login.js +632 -0
- package/node/sol-menu.js +639 -0
- package/node/sol-query.js +116 -0
- package/package.json +133 -0
- package/web/menu-from-rdf.js +23 -0
- package/web/scripts/prefs.js +25 -0
- package/web/sol-accordion.js +114 -0
- package/web/sol-basic.js +50 -0
- package/web/sol-breadcrumb.js +131 -0
- package/web/sol-button.js +244 -0
- package/web/sol-calendar.js +465 -0
- package/web/sol-default.js +118 -0
- package/web/sol-dropdown-button.js +222 -0
- package/web/sol-feed.js +1336 -0
- package/web/sol-form.js +949 -0
- package/web/sol-full.js +43 -0
- package/web/sol-gallery.js +303 -0
- package/web/sol-include.js +246 -0
- package/web/sol-live-edit.js +415 -0
- package/web/sol-login.js +856 -0
- package/web/sol-menu.js +593 -0
- package/web/sol-modal.js +377 -0
- package/web/sol-pod-extras.js +17 -0
- package/web/sol-pod-ops.js +680 -0
- package/web/sol-pod.js +1039 -0
- package/web/sol-query.js +546 -0
- package/web/sol-rolodex.js +95 -0
- package/web/sol-search.js +402 -0
- package/web/sol-settings.js +199 -0
- package/web/sol-solidos.js +93 -0
- package/web/sol-tabs.js +445 -0
- package/web/sol-time.js +194 -0
- package/web/sol-tree-edit.js +492 -0
- package/web/sol-wac.js +456 -0
- package/web/sol-weather.js +337 -0
- package/web/sol-window.js +142 -0
- package/web/styles/buttons-css.js +108 -0
- package/web/styles/help.css +242 -0
- package/web/styles/root.css +112 -0
- package/web/styles/sol-accordion-css.js +97 -0
- package/web/styles/sol-calendar-css.js +154 -0
- package/web/styles/sol-feed-css.js +475 -0
- package/web/styles/sol-form-css.js +471 -0
- package/web/styles/sol-gallery-css.js +181 -0
- package/web/styles/sol-include-css.js +95 -0
- package/web/styles/sol-live-edit-css.js +84 -0
- package/web/styles/sol-live-edit.css +101 -0
- package/web/styles/sol-login-css.js +116 -0
- package/web/styles/sol-menu-css.js +145 -0
- package/web/styles/sol-modal-css.js +134 -0
- package/web/styles/sol-pod-css.js +187 -0
- package/web/styles/sol-pod-modal-css.js +203 -0
- package/web/styles/sol-query-css.js +140 -0
- package/web/styles/sol-query-help.css +267 -0
- package/web/styles/sol-query-one-pager.css +67 -0
- package/web/styles/sol-search-css.js +157 -0
- package/web/styles/sol-solidos-css.js +7 -0
- package/web/styles/sol-tabs-css.js +114 -0
- package/web/styles/sol-time-css.js +30 -0
- package/web/styles/sol-wac-css.js +73 -0
- package/web/styles/sol-weather-css.js +59 -0
- package/web/styles/solid-logo.svg +9 -0
- package/web/styles/view-accordion-css.js +66 -0
- package/web/styles/view-anchorlist-css.js +22 -0
- package/web/styles/view-autocomplete-css.js +59 -0
- package/web/styles/view-rolodex-css.js +102 -0
- package/web/styles/view-select-css.js +21 -0
- package/web/utils/calendar-fetch.js +388 -0
- package/web/utils/code-mirror-editor.js +82 -0
- package/web/utils/commons-fetch.js +108 -0
- package/web/utils/feed-edit.js +159 -0
- package/web/utils/feed-edit.smoke.mjs +74 -0
- package/web/utils/feed-fetch.js +573 -0
- package/web/utils/live-edit-help/csv.js +64 -0
- package/web/utils/live-edit-help/graphviz.js +41 -0
- package/web/utils/live-edit-help/jsonld.js +55 -0
- package/web/utils/live-edit-help/markdown.js +52 -0
- package/web/utils/live-edit-help/mermaid.js +48 -0
- package/web/utils/live-edit-help/turtle.js +85 -0
- package/web/utils/rdf-config.js +125 -0
- package/web/utils/renderers/csv.js +124 -0
- package/web/utils/renderers/d3-force.js +82 -0
- package/web/utils/renderers/graphviz.js +13 -0
- package/web/utils/renderers/html.js +10 -0
- package/web/utils/renderers/jsonld.js +63 -0
- package/web/utils/renderers/markdown.js +19 -0
- package/web/utils/renderers/mermaid.js +54 -0
- package/web/utils/renderers/turtle.js +51 -0
- package/web/utils/sol-query-triple-patterns.js +151 -0
- package/web/utils/sol-query-ui.js +250 -0
- package/web/utils/sol-query-views.js +32 -0
- package/web/views/_helpers.js +34 -0
- package/web/views/accordion.js +133 -0
- package/web/views/anchorlist.js +59 -0
- package/web/views/auto-complete.js +183 -0
- package/web/views/dl.js +38 -0
- package/web/views/list.js +19 -0
- package/web/views/menu.js +56 -0
- package/web/views/rolodex.js +126 -0
- package/web/views/select.js +79 -0
- package/web/views/table.js +73 -0
- package/web/views/tabs.js +57 -0
package/web/sol-query.js
ADDED
|
@@ -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);
|