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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// All RDF operations for sol-query.
|
|
2
|
+
// Adapters (SPARQL execution), store loading, triple-pattern matching.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
sanitizeHtml,
|
|
6
|
+
toPlainResults,
|
|
7
|
+
ComunicaSparqlAdapter,
|
|
8
|
+
NativeSparqlAdapter,
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
import { rdf } from './rdf.js';
|
|
11
|
+
|
|
12
|
+
// Re-export pure-logic functions from rdf-core so existing importers
|
|
13
|
+
// (sol-query.js, views, tests) continue to work unchanged.
|
|
14
|
+
export {
|
|
15
|
+
KNOWN_PREFIXES,
|
|
16
|
+
ACCEPT_TYPES,
|
|
17
|
+
detectFormat,
|
|
18
|
+
termToString,
|
|
19
|
+
termToCell,
|
|
20
|
+
expandCurie,
|
|
21
|
+
knownPrefixesAsSparql,
|
|
22
|
+
w3cResults,
|
|
23
|
+
_NAMED_VAR_RE,
|
|
24
|
+
triplePatternTermToNode,
|
|
25
|
+
tokenizeTriplePattern,
|
|
26
|
+
parsePatternParts,
|
|
27
|
+
patternVarNames,
|
|
28
|
+
matchStore,
|
|
29
|
+
selectVars as _selectVars,
|
|
30
|
+
isRdfDoc as _isRdfDoc,
|
|
31
|
+
} from './rdf-core.js';
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
ACCEPT_TYPES,
|
|
35
|
+
detectFormat,
|
|
36
|
+
termToCell,
|
|
37
|
+
parsePatternParts,
|
|
38
|
+
matchStore,
|
|
39
|
+
patternVarNames,
|
|
40
|
+
selectVars as _selectVars,
|
|
41
|
+
isRdfDoc as _isRdfDoc,
|
|
42
|
+
bindingsToResults,
|
|
43
|
+
w3cResults,
|
|
44
|
+
} from './rdf-core.js';
|
|
45
|
+
|
|
46
|
+
// Re-export the rdf singleton so downstream can import either symbol
|
|
47
|
+
export { rdf };
|
|
48
|
+
|
|
49
|
+
// Re-export sanitizeHtml so downstream (sol-include) can import from here or utils
|
|
50
|
+
export { sanitizeHtml };
|
|
51
|
+
// Re-export queryHtmlWithSelector for sol-query.js
|
|
52
|
+
export { queryHtmlWithSelector } from './utils.js';
|
|
53
|
+
|
|
54
|
+
// Backwards-compatible alias (deprecated — prefer triplePatternTermToNode).
|
|
55
|
+
export { triplePatternTermToNode as miniTermToNode } from './rdf-core.js';
|
|
56
|
+
|
|
57
|
+
// ─── Store loading ───────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export async function loadRdfStore(endpoint, fetchFn = fetch, opts = {}) {
|
|
60
|
+
if (!rdf.isReady()) throw new Error('rdflib not available');
|
|
61
|
+
const rdflib = rdf;
|
|
62
|
+
|
|
63
|
+
// Parsers (notably JSON-LD) require an absolute base IRI, so resolve relative
|
|
64
|
+
// endpoints against the current document before handing them to rdflib.parse.
|
|
65
|
+
try { endpoint = new URL(endpoint, document.baseURI).href; } catch {}
|
|
66
|
+
|
|
67
|
+
// shared=true routes parsing into rdf.store (the singleton shared with
|
|
68
|
+
// solid-logic/solid-ui/mashlib) and dedupes subsequent loads of the same
|
|
69
|
+
// URL. Intended for stored-query RDF libraries where many <sol-query>
|
|
70
|
+
// elements reference the same .ttl file.
|
|
71
|
+
//
|
|
72
|
+
// Cache reads are unconditional: once a URL is in the singleton (because
|
|
73
|
+
// some earlier caller asked for shared), every subsequent load hits the
|
|
74
|
+
// cache regardless of opts. Cache writes still require shared, so callers
|
|
75
|
+
// can't pollute the singleton without opting in.
|
|
76
|
+
const shared = !!opts.shared;
|
|
77
|
+
if (rdf.isLoaded(endpoint)) return rdf.store;
|
|
78
|
+
|
|
79
|
+
const store = shared ? rdf.store : rdflib.graph();
|
|
80
|
+
const diagnostics = [];
|
|
81
|
+
let lastError = null;
|
|
82
|
+
const markOk = () => { if (shared) rdf.markLoaded(endpoint); };
|
|
83
|
+
|
|
84
|
+
// Attempt each Accept type; 405 Method-Not-Allowed is fatal (wrong HTTP method),
|
|
85
|
+
// everything else (including 501) just moves on to the next type.
|
|
86
|
+
for (const accept of ACCEPT_TYPES) {
|
|
87
|
+
try {
|
|
88
|
+
const resp = await fetchFn(endpoint, { headers: { Accept: accept } });
|
|
89
|
+
diagnostics.push({ accept, status: resp.status, contentType: resp.headers.get('content-type') });
|
|
90
|
+
|
|
91
|
+
if (!resp.ok) {
|
|
92
|
+
lastError = new Error(`HTTP ${resp.status}`);
|
|
93
|
+
if (resp.status === 405) break; // method not supported — no point retrying
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const text = await resp.text();
|
|
98
|
+
const ct = (resp.headers.get('content-type') || '').split(';')[0].trim();
|
|
99
|
+
if (ct.includes('html') || /<html/i.test(text)) {
|
|
100
|
+
// Try RDFa parsing; tag store so callers know it came from HTML
|
|
101
|
+
try {
|
|
102
|
+
const clean = await sanitizeHtml(text, { WHOLE_DOCUMENT: true, FORCE_BODY: false });
|
|
103
|
+
rdflib.parse(clean, store, endpoint, 'text/html');
|
|
104
|
+
store._isHtml = true;
|
|
105
|
+
store._rawHtml = clean;
|
|
106
|
+
store._diagnostics = diagnostics;
|
|
107
|
+
markOk();
|
|
108
|
+
return store;
|
|
109
|
+
} catch (rdfa) {
|
|
110
|
+
lastError = new Error(`Server returned HTML (${ct})`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
rdflib.parse(text, store, endpoint, detectFormat(ct, accept));
|
|
117
|
+
store._diagnostics = diagnostics;
|
|
118
|
+
markOk();
|
|
119
|
+
return store;
|
|
120
|
+
} catch (parseErr) {
|
|
121
|
+
lastError = parseErr;
|
|
122
|
+
diagnostics.push({ accept, parseError: parseErr.message });
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
diagnostics.push({ accept, error: err.message });
|
|
126
|
+
lastError = err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Last resort: no Accept header (plain fetch, let server decide content-type)
|
|
131
|
+
try {
|
|
132
|
+
const resp = await fetchFn(endpoint);
|
|
133
|
+
if (resp.ok) {
|
|
134
|
+
const text = await resp.text();
|
|
135
|
+
const ct = (resp.headers.get('content-type') || '').split(';')[0].trim();
|
|
136
|
+
if (ct.includes('html') || /<html/i.test(text)) {
|
|
137
|
+
try {
|
|
138
|
+
const clean = await sanitizeHtml(text, { WHOLE_DOCUMENT: true, FORCE_BODY: false });
|
|
139
|
+
rdflib.parse(clean, store, endpoint, 'text/html');
|
|
140
|
+
store._isHtml = true;
|
|
141
|
+
store._rawHtml = clean;
|
|
142
|
+
store._diagnostics = diagnostics;
|
|
143
|
+
markOk();
|
|
144
|
+
return store;
|
|
145
|
+
} catch {}
|
|
146
|
+
} else {
|
|
147
|
+
try {
|
|
148
|
+
rdflib.parse(text, store, endpoint, detectFormat(ct, ''));
|
|
149
|
+
store._diagnostics = diagnostics;
|
|
150
|
+
markOk();
|
|
151
|
+
return store;
|
|
152
|
+
} catch (parseErr) {
|
|
153
|
+
lastError = parseErr;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
lastError = err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const err = lastError || new Error('Failed to load or parse RDF');
|
|
162
|
+
err.diagnostics = diagnostics;
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Blank-node processing ───────────────────────────────────────────────────
|
|
167
|
+
export function expandBnodes(store, data) {
|
|
168
|
+
let vars = data.head.vars;
|
|
169
|
+
let bindings = data.results.bindings;
|
|
170
|
+
|
|
171
|
+
if (bindings.length > 0) {
|
|
172
|
+
const allBnodeCols = vars.filter(v => bindings.every(r => r[v]?.type === 'bnode'));
|
|
173
|
+
if (allBnodeCols.length) {
|
|
174
|
+
vars = vars.filter(v => !allBnodeCols.includes(v));
|
|
175
|
+
bindings = bindings.map(r => {
|
|
176
|
+
const nr = { ...r };
|
|
177
|
+
allBnodeCols.forEach(v => delete nr[v]);
|
|
178
|
+
return nr;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const objVars = vars.filter(v => v !== 's' && v !== 'p');
|
|
184
|
+
const hasBnode = bindings.some(row => objVars.some(v => row[v]?.type === 'bnode'));
|
|
185
|
+
if (!hasBnode) return w3cResults(vars, bindings);
|
|
186
|
+
|
|
187
|
+
bindings = bindings.map(row => {
|
|
188
|
+
const newRow = {};
|
|
189
|
+
for (const [k, cell] of Object.entries(row)) {
|
|
190
|
+
newRow[k] = (objVars.includes(k) && cell?.type === 'bnode')
|
|
191
|
+
? _bnodeToModal(store, cell._term)
|
|
192
|
+
: cell;
|
|
193
|
+
}
|
|
194
|
+
return newRow;
|
|
195
|
+
});
|
|
196
|
+
return w3cResults(vars, bindings);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _bnodeToModal(store, bnodeTerm) {
|
|
200
|
+
const stmts = (store.match(bnodeTerm, null, null, null) || [])
|
|
201
|
+
.filter(st => st.object.termType !== 'BlankNode');
|
|
202
|
+
const bindings = stmts.map(st => ({
|
|
203
|
+
p: termToCell(st.predicate),
|
|
204
|
+
o: termToCell(st.object),
|
|
205
|
+
}));
|
|
206
|
+
return { type: 'bnode', value: '[…]', _data: w3cResults(['p', 'o'], bindings) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Widen subjects into rows-of-properties ──────────────────────────────────
|
|
210
|
+
export function pivotSubjectsToRows(store, subjects, subjectName = 's') {
|
|
211
|
+
const shortName = uri => (uri || '').replace(/.*[/#]([^/#]+)\/?$/, '$1') || uri;
|
|
212
|
+
const cols = [subjectName];
|
|
213
|
+
const seen = new Set([subjectName]);
|
|
214
|
+
const rows = [];
|
|
215
|
+
|
|
216
|
+
for (const subj of subjects) {
|
|
217
|
+
const row = { [subjectName]: termToCell(subj) };
|
|
218
|
+
const stmts = store.match(subj, null, null, null) || [];
|
|
219
|
+
for (const st of stmts) {
|
|
220
|
+
const col = shortName(st.predicate.value);
|
|
221
|
+
const cell = termToCell(st.object);
|
|
222
|
+
if (!seen.has(col)) { cols.push(col); seen.add(col); }
|
|
223
|
+
if (row[col] === undefined) row[col] = cell;
|
|
224
|
+
else if (row[col].type === 'multi') row[col].values.push(cell);
|
|
225
|
+
else row[col] = { type: 'multi', values: [row[col], cell] };
|
|
226
|
+
}
|
|
227
|
+
rows.push(row);
|
|
228
|
+
}
|
|
229
|
+
return w3cResults(cols, rows);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function promoteDisplayColumns(data, subjectCol) {
|
|
233
|
+
let cols = data.head.vars.filter(v => v !== subjectCol);
|
|
234
|
+
const lead = cols.find(v => /^(name|label|title)$/i.test(v));
|
|
235
|
+
if (lead) cols = [lead, ...cols.filter(v => v !== lead)];
|
|
236
|
+
return w3cResults(cols, data.results.bindings);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Full store → flat renderer results ──────────────────────────────────────
|
|
240
|
+
export function storeToResults(store) {
|
|
241
|
+
const stmts = Array.isArray(store.statements)
|
|
242
|
+
? store.statements
|
|
243
|
+
: (typeof store.match === 'function' ? store.match(null, null, null) : []);
|
|
244
|
+
|
|
245
|
+
const bindings = stmts.map(st => ({
|
|
246
|
+
s: termToCell(st.subject),
|
|
247
|
+
p: termToCell(st.predicate),
|
|
248
|
+
o: termToCell(st.object),
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
return w3cResults(['s', 'p', 'o'], bindings);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── Fetch SPARQL query text from an RDF file ────────────────────────────────
|
|
255
|
+
export async function fetchQueryFromRdf(queryUrl, fetchFn = fetch) {
|
|
256
|
+
if (!rdf.isReady()) throw new Error('rdflib not available');
|
|
257
|
+
const rdflib = rdf;
|
|
258
|
+
|
|
259
|
+
try { queryUrl = new URL(queryUrl, document.baseURI).href; } catch {}
|
|
260
|
+
|
|
261
|
+
const docUrl = queryUrl.split('#')[0];
|
|
262
|
+
const store = await loadRdfStore(docUrl, fetchFn, { shared: true });
|
|
263
|
+
|
|
264
|
+
const subject = rdflib.sym(queryUrl);
|
|
265
|
+
const q = store.any(subject, rdflib.sym('http://www.w3.org/ns/sparql#query'))
|
|
266
|
+
|| store.any(subject, rdflib.sym('http://www.w3.org/2000/01/rdf-schema#comment'))
|
|
267
|
+
|| store.any(subject, rdflib.sym('http://purl.org/dc/terms/description'));
|
|
268
|
+
|
|
269
|
+
if (!q) throw new Error(`No query found at ${queryUrl}`);
|
|
270
|
+
return q.value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── runQuery: call from a script, returns plain JS values ───────────────────
|
|
274
|
+
export async function runQuery({ endpoint, sparql, pattern, wanted, vars: patternVars } = {}) {
|
|
275
|
+
const pat = pattern || wanted;
|
|
276
|
+
if (!endpoint) throw new Error('endpoint is required');
|
|
277
|
+
if (!sparql && !pat) throw new Error('sparql or pattern is required');
|
|
278
|
+
|
|
279
|
+
let data;
|
|
280
|
+
|
|
281
|
+
if (sparql) {
|
|
282
|
+
let queryText = sparql;
|
|
283
|
+
const isStoredRef = !/\s/.test(sparql) && /^https?:\/\/|^\/|^\.\.?\//.test(sparql.trim());
|
|
284
|
+
if (isStoredRef) queryText = await fetchQueryFromRdf(sparql);
|
|
285
|
+
data = await execSparql(queryText, endpoint);
|
|
286
|
+
} else {
|
|
287
|
+
const store = await loadRdfStore(endpoint);
|
|
288
|
+
const [s, p, o] = parsePatternParts(pat, rdf, {}, endpoint);
|
|
289
|
+
data = matchStore(store, s, p, o);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return toPlainResults(data, patternVars);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Run SPARQL against a pre-loaded rdflib store ────────────────────────────
|
|
296
|
+
async function _localSparql(queryText, endpoint) {
|
|
297
|
+
if (!rdf.hasSparqlEngine()) throw new Error('rdflib SPARQL engine not available');
|
|
298
|
+
|
|
299
|
+
// rdflib's local SPARQL parses LIMIT/OFFSET/ORDER BY but doesn't enforce
|
|
300
|
+
// them, so a query with these clauses silently returns the unbounded /
|
|
301
|
+
// unsorted result set. Surface it instead of letting the user wonder why
|
|
302
|
+
// their LIMIT is being ignored. To honor these clauses, load Comunica
|
|
303
|
+
// (e.g. via the vendored UMD: `<script src=".../@comunica-query-sparql.umd.js">`).
|
|
304
|
+
if (/\b(LIMIT|OFFSET|ORDER\s+BY)\b/i.test(queryText)) {
|
|
305
|
+
console.warn(
|
|
306
|
+
'[sol-query] LIMIT/OFFSET/ORDER BY present but rdflib local SPARQL ignores them. ' +
|
|
307
|
+
'Load @comunica/query-sparql to enforce these clauses against RDF documents.'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const store = await loadRdfStore(endpoint);
|
|
312
|
+
const parsed = rdf.sparqlToQuery(queryText, false, store);
|
|
313
|
+
if (!parsed) throw new Error('Could not parse SPARQL query');
|
|
314
|
+
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const bindings = [];
|
|
317
|
+
try {
|
|
318
|
+
store.query(
|
|
319
|
+
parsed,
|
|
320
|
+
b => bindings.push(b),
|
|
321
|
+
rdf.fetcher(store),
|
|
322
|
+
() => resolve(bindingsToResults(bindings, queryText)),
|
|
323
|
+
);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
reject(err);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── SPARQL execution ────────────────────────────────────────────────────────
|
|
331
|
+
// `fetchFn` is the authenticated fetch (from getAuthFetch) when one is
|
|
332
|
+
// available; passed to Comunica via its query context and to the native
|
|
333
|
+
// SPARQL adapter for endpoints that aren't RDF documents.
|
|
334
|
+
export async function execSparql(query, endpoint, fetchFn) {
|
|
335
|
+
// Multi-source federation only works through Comunica.
|
|
336
|
+
if (Array.isArray(endpoint)) {
|
|
337
|
+
const factory = ComunicaSparqlAdapter.getComunicaEngine();
|
|
338
|
+
if (!factory) throw new Error('Multiple endpoints require Comunica');
|
|
339
|
+
return await new ComunicaSparqlAdapter(factory).executeQuery(query, endpoint, fetchFn);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try { endpoint = new URL(endpoint, document.baseURI).href; } catch {}
|
|
343
|
+
|
|
344
|
+
const isRdf = _isRdfDoc(endpoint);
|
|
345
|
+
|
|
346
|
+
const comunicaFactory = ComunicaSparqlAdapter.getComunicaEngine();
|
|
347
|
+
if (comunicaFactory) {
|
|
348
|
+
try { return await new ComunicaSparqlAdapter(comunicaFactory).executeQuery(query, endpoint, fetchFn); }
|
|
349
|
+
catch (err) {
|
|
350
|
+
console.warn('[sol-query] Comunica failed, falling back to rdflib:', err);
|
|
351
|
+
}
|
|
352
|
+
} else if (typeof window !== 'undefined' && window.Comunica) {
|
|
353
|
+
console.warn('[sol-query] window.Comunica is set but no QueryEngine factory was found.',
|
|
354
|
+
'Top-level keys:', Object.keys(window.Comunica),
|
|
355
|
+
window.Comunica.default ? 'default keys: ' + Object.keys(window.Comunica.default).slice(0, 10).join(', ') : '');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (isRdf) {
|
|
359
|
+
return await _localSparql(query, endpoint);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const rdflibAdapter = new RdflibSparqlAdapter();
|
|
363
|
+
if (rdflibAdapter.isAvailable()) {
|
|
364
|
+
try { return await rdflibAdapter.executeQuery(query, endpoint, true); } catch {
|
|
365
|
+
try { return await rdflibAdapter.executeQuery(query, endpoint, false); } catch {}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return await new NativeSparqlAdapter().executeQuery(query, endpoint, fetchFn);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── SPARQL adapter (rdflib-based) ───────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
export class RdflibSparqlAdapter {
|
|
374
|
+
isAvailable() {
|
|
375
|
+
return rdf.hasRemoteSparql();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async executeQuery(query, endpoint, withCredentials = true) {
|
|
379
|
+
if (!rdf.isReady()) throw new Error('rdflib not available');
|
|
380
|
+
try {
|
|
381
|
+
return await this._attempt(query, endpoint, withCredentials);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (withCredentials) {
|
|
384
|
+
return await this._attempt(query, endpoint, false);
|
|
385
|
+
}
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async _attempt(query, endpoint, withCredentials) {
|
|
391
|
+
const raw = await rdf.sparqlQuery(query, { endpoint, withCredentials });
|
|
392
|
+
const selectVarsResult = _selectVars(query);
|
|
393
|
+
const vars = selectVarsResult
|
|
394
|
+
?? (raw.length ? Object.keys(raw[0]).filter(k => !k.startsWith('?')) : []);
|
|
395
|
+
const bindings = raw.map(binding => {
|
|
396
|
+
const row = {};
|
|
397
|
+
vars.forEach(v => {
|
|
398
|
+
const val = binding[v];
|
|
399
|
+
const cell = {
|
|
400
|
+
type: val ? (val.termType || 'literal').toLowerCase() : 'literal',
|
|
401
|
+
value: val ? (val.value || String(val)) : '',
|
|
402
|
+
};
|
|
403
|
+
if (val?.language) cell['xml:lang'] = val.language;
|
|
404
|
+
if (val?.datatype?.value) cell.datatype = val.datatype.value;
|
|
405
|
+
row[v] = cell;
|
|
406
|
+
});
|
|
407
|
+
return row;
|
|
408
|
+
});
|
|
409
|
+
return w3cResults(vars, bindings);
|
|
410
|
+
}
|
|
411
|
+
}
|
package/core/rdf.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Singleton wrapper around rdflib. The rest of the codebase goes through this
|
|
2
|
+
// class so rdflib is imported in exactly one place. Rollup treats `rdflib` as
|
|
3
|
+
// external (mapped to the `$rdf` UMD global); jest's moduleNameMapper maps it
|
|
4
|
+
// to a mock; importmaps/bundlers resolve it normally.
|
|
5
|
+
|
|
6
|
+
// Bare specifier — resolved by the consumer's importmap (CDN or local
|
|
7
|
+
// vendored copy) or by a bundler. Per-component UMD builds list `rdflib`
|
|
8
|
+
// in `external` so it stays a runtime global.
|
|
9
|
+
import * as _rdflib from 'rdflib';
|
|
10
|
+
import { register as registerService } from './services.js';
|
|
11
|
+
|
|
12
|
+
// `import * as _rdflib` exposes rdflib's named exports directly.
|
|
13
|
+
const _lib = _rdflib;
|
|
14
|
+
|
|
15
|
+
class Rdf {
|
|
16
|
+
constructor() {
|
|
17
|
+
this._store = null; // lazy shared singleton store
|
|
18
|
+
this._fetcher = null; // fetcher bound to _store
|
|
19
|
+
this._adopted = false; // a host explicitly adopted an external store (wins)
|
|
20
|
+
this._loaded = new Set(); // URLs already parsed into _store (cache key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Record that `url` has been parsed into the shared store.
|
|
24
|
+
markLoaded(url) { this._loaded.add(url); }
|
|
25
|
+
isLoaded(url) { return this._loaded.has(url); }
|
|
26
|
+
|
|
27
|
+
// Term constructors
|
|
28
|
+
sym(uri) { return _lib.sym(uri); }
|
|
29
|
+
// rdflib.literal accepts either (value, langOrDatatype) — second arg
|
|
30
|
+
// is the language tag if a string, or the datatype if a NamedNode —
|
|
31
|
+
// or (value, lang, datatype). Pass through whichever form the caller
|
|
32
|
+
// used so typed literals like "45.52"^^xsd:decimal survive.
|
|
33
|
+
literal(value, langOrDatatype, datatype) {
|
|
34
|
+
if (datatype !== undefined) return _lib.literal(value, langOrDatatype, datatype);
|
|
35
|
+
return _lib.literal(value, langOrDatatype);
|
|
36
|
+
}
|
|
37
|
+
blankNode(id) { return _lib.blankNode(id); }
|
|
38
|
+
|
|
39
|
+
// Stores & parsing
|
|
40
|
+
graph() { return _lib.graph(); }
|
|
41
|
+
parse(text, store, base, type) { return _lib.parse(text, store, base, type); }
|
|
42
|
+
st(s, p, o, g) { return _lib.st(s, p, o, g); }
|
|
43
|
+
|
|
44
|
+
// Shared singleton store — interop point with solid-logic / solid-ui / mashlib.
|
|
45
|
+
// **When solid-logic's singleton is present on `window`, that's THE store**
|
|
46
|
+
// — every sol-* component, solid-ui module, mashlib, etc. point at the same
|
|
47
|
+
// rdflib graph, so cross-component reads/writes are coherent and solid-ui's
|
|
48
|
+
// captured-at-import-time `kb` references work without any swap dance.
|
|
49
|
+
// Falls back to a freshly created graph in environments without solid-logic
|
|
50
|
+
// (unit tests, headless scripts without the singleton wired up).
|
|
51
|
+
get store() {
|
|
52
|
+
// A host that explicitly adopted a foreign store (useStore — e.g. another
|
|
53
|
+
// component library's rdflib graph handed over at runtime) wins outright,
|
|
54
|
+
// so swc components share THAT graph regardless of solid-logic's singleton.
|
|
55
|
+
if (this._adopted && this._store) return this._store;
|
|
56
|
+
// Otherwise always re-probe so consumers reach solid-logic's singleton even
|
|
57
|
+
// if an early access happened before solid-logic finished loading. Once
|
|
58
|
+
// solid-logic is up, every call returns ITS store; before then, a
|
|
59
|
+
// local fresh graph is used and persists until the singleton appears.
|
|
60
|
+
const sl = (typeof window !== 'undefined') &&
|
|
61
|
+
(window[Symbol.for('solid-logic-singleton')] || window.SolidLogic);
|
|
62
|
+
if (sl?.store) {
|
|
63
|
+
this._store = sl.store;
|
|
64
|
+
return sl.store;
|
|
65
|
+
}
|
|
66
|
+
if (!this._store) this._store = _lib.graph();
|
|
67
|
+
return this._store;
|
|
68
|
+
}
|
|
69
|
+
useStore(externalStore) {
|
|
70
|
+
if (!externalStore || typeof externalStore.match !== 'function') return false;
|
|
71
|
+
this._store = externalStore;
|
|
72
|
+
this._fetcher = externalStore.fetcher || null;
|
|
73
|
+
this._adopted = true;
|
|
74
|
+
this._loaded.clear();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
get storeFetcher() {
|
|
78
|
+
if (this._fetcher) return this._fetcher;
|
|
79
|
+
if (this.store.fetcher) { this._fetcher = this.store.fetcher; return this._fetcher; }
|
|
80
|
+
this._fetcher = new _lib.Fetcher(this.store);
|
|
81
|
+
this.store.fetcher = this._fetcher;
|
|
82
|
+
return this._fetcher;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fetch a document into the shared store via the shared (auth-aware) Fetcher,
|
|
86
|
+
// at most once per document. Returns the shared store. This is the convenience
|
|
87
|
+
// a component reaches through window.SolidWebComponents.rdf.load(url).
|
|
88
|
+
async load(url) {
|
|
89
|
+
const doc = String(url).split('#')[0];
|
|
90
|
+
if (!this.isLoaded(doc)) {
|
|
91
|
+
await this.storeFetcher.load(doc);
|
|
92
|
+
this.markLoaded(doc);
|
|
93
|
+
}
|
|
94
|
+
return this.store;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// SPARQL
|
|
98
|
+
fetcher(store, opts) { return new _lib.Fetcher(store, opts); }
|
|
99
|
+
sparqlToQuery(query, isUpdate, store) { return _lib.SPARQLToQuery(query, isUpdate, store); }
|
|
100
|
+
sparqlQuery(query, opts) { return _lib.sparqlQuery(query, opts); }
|
|
101
|
+
|
|
102
|
+
// Capability probes
|
|
103
|
+
isReady() { return !!_lib && typeof _lib.graph === 'function'; }
|
|
104
|
+
hasSparqlEngine() { return typeof _lib.SPARQLToQuery === 'function'; }
|
|
105
|
+
hasRemoteSparql() { return typeof _lib.sparqlQuery === 'function'; }
|
|
106
|
+
|
|
107
|
+
// Serialization
|
|
108
|
+
serialize(doc, store, base, contentType) {
|
|
109
|
+
return _lib.serialize(doc, store, base, contentType);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// UpdateManager — for PATCH-based edits and putBack
|
|
113
|
+
get UpdateManager() { return _lib.UpdateManager; }
|
|
114
|
+
|
|
115
|
+
// Escape hatches for the few places that need rdflib-shaped access
|
|
116
|
+
// (e.g. `new rdflib.Fetcher(...)`). Prefer the methods above.
|
|
117
|
+
get SPARQLToQuery() { return _lib.SPARQLToQuery; }
|
|
118
|
+
get Fetcher() { return _lib.Fetcher; }
|
|
119
|
+
get NamedNode() { return _lib.NamedNode; }
|
|
120
|
+
get BlankNode() { return _lib.BlankNode; }
|
|
121
|
+
get Literal() { return _lib.Literal; }
|
|
122
|
+
get Collection() { return _lib.Collection; }
|
|
123
|
+
get Statement() { return _lib.Statement; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Cross-bundle singleton. Every bundle (each UMD component, the app's own
|
|
127
|
+
// code, solid-ui's world) compiles its own copy of this module; without a
|
|
128
|
+
// shared instance each would mint its OWN store + storeFetcher, so e.g.
|
|
129
|
+
// <sol-login>'s `_integrateWithRdflib()` patch would be invisible to an app
|
|
130
|
+
// reading `rdf.store` from a different bundle. Publishing ONE instance on
|
|
131
|
+
// `window` makes the store, fetcher and loaded-set page-wide, so any app can
|
|
132
|
+
// just load components from sol-loader and share one coherent store — no
|
|
133
|
+
// bundling-from-source workaround. (Paired with the rdflib→window.$rdf shim
|
|
134
|
+
// so all bundles also share one rdflib *library*, for term `instanceof`.)
|
|
135
|
+
//
|
|
136
|
+
// Browser only: in Node / jest each module keeps its own instance (no `window`),
|
|
137
|
+
// preserving test isolation.
|
|
138
|
+
const _RDF_SINGLETON = Symbol.for('sol-components:rdf-singleton');
|
|
139
|
+
export const rdf = (typeof window !== 'undefined')
|
|
140
|
+
? (window[_RDF_SINGLETON] || (window[_RDF_SINGLETON] = new Rdf()))
|
|
141
|
+
: new Rdf();
|
|
142
|
+
export default rdf;
|
|
143
|
+
|
|
144
|
+
// Publish the shared store as the `rdf` host-service so any component can reach
|
|
145
|
+
// it via window.SolidWebComponents.rdf — no import of this module required.
|
|
146
|
+
registerService('rdf', rdf);
|
|
147
|
+
|
|
148
|
+
// Register the broker consumer that adopts a foreign rdflib store: the loader
|
|
149
|
+
// invokes it for a manifest `consumes: { rdf: { call: 'rdf.useStore' } }`. (The
|
|
150
|
+
// loader publishes registerConsumer; absent in Node/jest, so this is guarded.)
|
|
151
|
+
if (typeof window !== 'undefined' && window.SolidWebComponents
|
|
152
|
+
&& typeof window.SolidWebComponents.registerConsumer === 'function') {
|
|
153
|
+
window.SolidWebComponents.registerConsumer('rdf.useStore', function (store) { rdf.useStore(store); });
|
|
154
|
+
}
|
package/core/services.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// core/services.js — import-side accessor for the host-services registry.
|
|
2
|
+
//
|
|
3
|
+
// The ecosystem's "share resources without importing each other" surface. The
|
|
4
|
+
// component-interop loader publishes a tiny registry at `window.ComponentInterop.services`
|
|
5
|
+
// (services.js#root() also accepts a window.SolidWebComponents surface); swc capability modules import THIS file
|
|
6
|
+
// to register the shared services they provide:
|
|
7
|
+
//
|
|
8
|
+
// import { register } from '../core/services.js';
|
|
9
|
+
// register('rdf', rdf); // core/rdf.js
|
|
10
|
+
// register('auth', { fetch, manager }); // web/sol-login.js
|
|
11
|
+
// register('defaults', { get, onChange });
|
|
12
|
+
//
|
|
13
|
+
// Any component (swc's or a third party's) then reads them, import-free, via
|
|
14
|
+
// `window.SolidWebComponents.{ rdf, auth, fetch, defaults, has, services }`.
|
|
15
|
+
//
|
|
16
|
+
// Mirrors core/rdf.js: in the browser everything funnels through the one
|
|
17
|
+
// window-shared registry; in Node/jest there's no `window`, so a module-local
|
|
18
|
+
// fallback registry is used (preserving test isolation). The registry is
|
|
19
|
+
// duck-typed by its methods, so it doesn't matter whether the loader or this
|
|
20
|
+
// module created it.
|
|
21
|
+
|
|
22
|
+
import { EVENTS } from './events.js';
|
|
23
|
+
|
|
24
|
+
function makeRegistry() {
|
|
25
|
+
const map = new Map();
|
|
26
|
+
const waiters = new Map();
|
|
27
|
+
return {
|
|
28
|
+
register(name, impl) {
|
|
29
|
+
map.set(name, impl);
|
|
30
|
+
const ws = waiters.get(name);
|
|
31
|
+
if (ws) { waiters.delete(name); ws.forEach((fn) => fn(impl)); }
|
|
32
|
+
},
|
|
33
|
+
get(name) { return map.get(name); },
|
|
34
|
+
has(name) { return map.has(name); },
|
|
35
|
+
names() { return Array.from(map.keys()); },
|
|
36
|
+
whenReady(name) {
|
|
37
|
+
if (map.has(name)) return Promise.resolve(map.get(name));
|
|
38
|
+
return new Promise((res) => {
|
|
39
|
+
const a = waiters.get(name) || [];
|
|
40
|
+
a.push(res);
|
|
41
|
+
waiters.set(name, a);
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let _local = null;
|
|
48
|
+
|
|
49
|
+
/** The one shared host surface. It is the broker's `window.ComponentInterop`
|
|
50
|
+
* when component-interop's loader is on the page; `window.SolidWebComponents`
|
|
51
|
+
* is swc's historical alias for the SAME object. Unifying them here lets swc
|
|
52
|
+
* work whether the page loaded component-interop's generic loader OR swc's own
|
|
53
|
+
* (which vendors it). A Node-side stand-in is used in tests without a window. */
|
|
54
|
+
export function root() {
|
|
55
|
+
if (typeof window !== 'undefined') {
|
|
56
|
+
const surface = window.ComponentInterop || window.SolidWebComponents || {};
|
|
57
|
+
window.ComponentInterop = surface;
|
|
58
|
+
window.SolidWebComponents = surface;
|
|
59
|
+
return surface;
|
|
60
|
+
}
|
|
61
|
+
return (_local = _local || {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The shared services registry — the loader's if present, else one we create. */
|
|
65
|
+
export function services() {
|
|
66
|
+
const r = root();
|
|
67
|
+
if (!r.services) r.services = makeRegistry();
|
|
68
|
+
if (!r.EVENTS) r.EVENTS = EVENTS; // the loader doesn't bake the table
|
|
69
|
+
return r.services;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function register(name, impl) { return services().register(name, impl); }
|
|
73
|
+
export function get(name) { return services().get(name); }
|
|
74
|
+
export function has(name) { return services().has(name); }
|
|
75
|
+
export function whenReady(name) { return services().whenReady(name); }
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Adopt a foreign authenticated fetch as the page's default authenticated fetch
|
|
79
|
+
* when no <sol-login> is present. This lets swc components ride a session
|
|
80
|
+
* established by another component library (e.g. PodOS, which hands its
|
|
81
|
+
* `authenticatedFetch` out via its `pod-os:loaded` event). getAuthFetch()
|
|
82
|
+
* (core/auth-fetch.js) returns it as the fallback after the <sol-login> lookup.
|
|
83
|
+
* A logged-in <sol-login> still wins — this is the no-sol-login fallback.
|
|
84
|
+
*
|
|
85
|
+
* @param {(input: RequestInfo, init?: RequestInit) => Promise<Response>} fn
|
|
86
|
+
* @param {object} [info] e.g. { webId }
|
|
87
|
+
* @returns the adopted fetch (or null when cleared)
|
|
88
|
+
*/
|
|
89
|
+
export function adoptFetch(fn, info) {
|
|
90
|
+
const r = root();
|
|
91
|
+
r.adoptedFetch = (typeof fn === 'function') ? fn : null;
|
|
92
|
+
if (info && info.webId) r.adoptedWebId = info.webId;
|
|
93
|
+
return r.adoptedFetch;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Expose adoptFetch on the host surface so import-free host glue can call
|
|
97
|
+
// `window.SolidWebComponents.adoptFetch(fn, { webId })`.
|
|
98
|
+
if (typeof window !== 'undefined') {
|
|
99
|
+
const r = root();
|
|
100
|
+
if (!r.adoptFetch) r.adoptFetch = adoptFetch;
|
|
101
|
+
// Register the broker consumer that adopts a foreign authenticated fetch: the
|
|
102
|
+
// loader invokes it for a manifest `consumes: { auth: { call: 'adoptFetch' } }`.
|
|
103
|
+
if (typeof r.registerConsumer === 'function') r.registerConsumer('adoptFetch', (fn) => adoptFetch(fn));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { EVENTS };
|