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,19 @@
1
+ export async function renderMarkdown(content, outputEl) {
2
+ const { marked } = await import('marked');
3
+
4
+ marked.setOptions({ gfm: true, breaks: false });
5
+ const html = marked.parse(content);
6
+
7
+ outputEl.innerHTML = '';
8
+ const wrapper = document.createElement('div');
9
+ wrapper.className = 'sle-md-preview';
10
+ wrapper.innerHTML = html;
11
+
12
+ // Make links open in a new tab safely
13
+ wrapper.querySelectorAll('a').forEach(a => {
14
+ a.target = '_blank';
15
+ a.rel = 'noopener noreferrer';
16
+ });
17
+
18
+ outputEl.appendChild(wrapper);
19
+ }
@@ -0,0 +1,54 @@
1
+ let mermaidReady = false;
2
+ let _mermaid = null;
3
+ let _mmdSeq = 0;
4
+
5
+ async function ensureMermaid() {
6
+ if (mermaidReady) return _mermaid;
7
+ const m = await import('https://esm.sh/mermaid@10');
8
+ const mermaid = m.default || m.mermaid || m;
9
+ mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' });
10
+ _mermaid = mermaid;
11
+ mermaidReady = true;
12
+ return mermaid;
13
+ }
14
+
15
+ export async function renderMermaid(content, outputEl) {
16
+ // Strip %% comment lines ourselves before handing the source to
17
+ // Mermaid — its built-in comment handling is unreliable when a
18
+ // diagram opens with a comment block. %%{…}%% init directives are
19
+ // NOT comments and are kept.
20
+ const trimmed = content
21
+ .split(/\r?\n/)
22
+ .filter(line => !/^\s*%%(?!\{)/.test(line))
23
+ .join('\n')
24
+ .trim();
25
+ if (!trimmed) {
26
+ outputEl.innerHTML = '<p style="padding:1rem;color:#888">Enter a Mermaid diagram above.</p>';
27
+ return;
28
+ }
29
+
30
+ let mermaid;
31
+ try {
32
+ mermaid = await ensureMermaid();
33
+ } catch (e) {
34
+ outputEl.innerHTML = '<p style="padding:1rem;color:#c0392b">Failed to load Mermaid library.</p>';
35
+ return;
36
+ }
37
+
38
+ const id = 'mmd-' + (++_mmdSeq);
39
+ try {
40
+ const { svg } = await mermaid.render(id, trimmed);
41
+ outputEl.innerHTML = svg;
42
+ // Make the diagram responsive — fit the preview pane as it is
43
+ // resized / zoomed, instead of Mermaid's fixed pixel max-width.
44
+ const svgEl = outputEl.querySelector('svg');
45
+ if (svgEl) { svgEl.style.maxWidth = '100%'; svgEl.style.height = 'auto'; }
46
+ } catch (e) {
47
+ // Mermaid leaves orphan elements on parse errors — clean up
48
+ const orphan = document.getElementById('d' + id);
49
+ if (orphan) orphan.remove();
50
+ outputEl.innerHTML = `<pre style="padding:1rem;color:#c0392b;white-space:pre-wrap;font-size:.85em">Diagram error: ${
51
+ (e.message || String(e)).replace(/<[^>]*>/g, '')
52
+ }</pre>`;
53
+ }
54
+ }
@@ -0,0 +1,51 @@
1
+ import { drawForceGraph } from './d3-force.js';
2
+ import { rdf } from '../../../core/rdf.js';
3
+
4
+ export async function renderTurtle(content, outputEl) {
5
+ if (!rdf.isReady()) throw new Error('rdflib not available');
6
+
7
+ const store = rdf.graph();
8
+ try {
9
+ rdf.parse(content, store, 'http://example.org/', 'text/turtle');
10
+ } catch (e) {
11
+ throw new Error(`Turtle parse error: ${e.message}`);
12
+ }
13
+
14
+ const labelPredicates = new Set([
15
+ 'http://www.w3.org/2000/01/rdf-schema#label',
16
+ 'http://xmlns.com/foaf/0.1/name',
17
+ 'http://www.w3.org/2004/02/skos/core#prefLabel',
18
+ 'http://purl.org/dc/elements/1.1/title',
19
+ 'http://purl.org/dc/terms/title',
20
+ 'https://schema.org/name',
21
+ ]);
22
+
23
+ // Just the local term: everything after the last '/' or '#'.
24
+ const shortId = uri => uri.split(/[/#]/).pop() || uri;
25
+
26
+ const nodes = new Map();
27
+ const links = [];
28
+
29
+ store.statements.forEach(({ subject: s, predicate: p, object: o }) => {
30
+ if (!nodes.has(s.value)) nodes.set(s.value, { id: s.value, label: shortId(s.value), displayLabel: null, properties: [] });
31
+
32
+ if (o.termType === 'NamedNode') {
33
+ if (!nodes.has(o.value)) nodes.set(o.value, { id: o.value, label: shortId(o.value), displayLabel: null, properties: [] });
34
+ links.push({ source: s.value, target: o.value, label: shortId(p.value) });
35
+ } else if (o.termType === 'Literal') {
36
+ const node = nodes.get(s.value);
37
+ if (labelPredicates.has(p.value)) {
38
+ node.displayLabel = o.value;
39
+ } else {
40
+ node.properties.push(`${shortId(p.value)} ${o.value}`);
41
+ }
42
+ }
43
+ });
44
+
45
+ if (nodes.size === 0) {
46
+ outputEl.innerHTML = '<p style="padding:1rem;color:#888">No triples to display.</p>';
47
+ return;
48
+ }
49
+
50
+ await drawForceGraph(outputEl, { nodes: [...nodes.values()], links });
51
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Triple-pattern parser and SPARQL converter.
3
+ * Converts a strict 3-part triple pattern (subject predicate object) to SPARQL.
4
+ *
5
+ * Grammar (strict — matches the W3C triple-pattern spec):
6
+ * subject: named-var (?x) | <uri> | prefix:local
7
+ * predicate: named-var | <uri> | prefix:local
8
+ * object: named-var | <uri> | prefix:local | "literal"[@lang|^^<datatype>]
9
+ *
10
+ * Rejected: bare `?` (all variables must be named), bare words, unquoted
11
+ * literals, numeric literals without quotes.
12
+ */
13
+
14
+ const NAMED_VAR_RE = /^\?[A-Za-z_][A-Za-z0-9_]*$/;
15
+
16
+ export class TriplePatternParser {
17
+ constructor(endpoint) {
18
+ this.endpoint = endpoint;
19
+ }
20
+
21
+ /**
22
+ * Parse a triple-pattern string into a SPARQL SELECT query.
23
+ * @param {string} pattern - "subject predicate object"
24
+ * @returns {string} A valid SPARQL SELECT query
25
+ * @throws {Error} If the pattern is invalid
26
+ */
27
+ parse(pattern) {
28
+ const tokens = _tokenizeTriplePattern(pattern);
29
+ if (tokens.length !== 3) {
30
+ throw new Error(`Triple pattern must have exactly 3 parts: subject predicate object (got ${tokens.length})`);
31
+ }
32
+ const [s, p, o] = tokens.map(t => this.expandTerm(t));
33
+ return `SELECT * WHERE { ${s} ${p} ${o} }`;
34
+ }
35
+
36
+ expandTerm(term) {
37
+ if (term === '?') {
38
+ throw new Error('Bare "?" is not allowed — use a named variable like "?x"');
39
+ }
40
+ if (term.startsWith('?')) {
41
+ if (!NAMED_VAR_RE.test(term)) {
42
+ throw new Error(`Invalid variable "${term}" — must match ?[A-Za-z_][A-Za-z0-9_]*`);
43
+ }
44
+ return term;
45
+ }
46
+ if (term.startsWith('<') && term.endsWith('>')) return term;
47
+ if (term.startsWith('"')) {
48
+ if (!/^"([^"\\]|\\.)*"(@[A-Za-z-]+|\^\^<[^>]+>)?$/.test(term)) {
49
+ throw new Error(`Malformed literal "${term}" — must be "value" with optional @lang or ^^<datatype>`);
50
+ }
51
+ return term;
52
+ }
53
+ if (_isCurie(term)) return term;
54
+ throw new Error(
55
+ `Unrecognized term "${term}" — must be a named variable (?x), <uri>, prefix:local, or quoted "literal"`
56
+ );
57
+ }
58
+ }
59
+
60
+ /** Validator for triple-pattern syntax. */
61
+ export class TriplePatternValidator {
62
+ static validate(input) {
63
+ if (!input || typeof input !== 'string') {
64
+ return { valid: false, error: 'Input must be a non-empty string' };
65
+ }
66
+ let tokens;
67
+ try { tokens = _tokenizeTriplePattern(input); }
68
+ catch (e) { return { valid: false, error: e.message }; }
69
+
70
+ if (tokens.length !== 3) {
71
+ return { valid: false, error: `Triple pattern needs exactly 3 parts — got ${tokens.length}` };
72
+ }
73
+ const p = new TriplePatternParser('');
74
+ try {
75
+ p.expandTerm(tokens[0]);
76
+ p.expandTerm(tokens[1]);
77
+ p.expandTerm(tokens[2]);
78
+ } catch (e) {
79
+ return { valid: false, error: e.message };
80
+ }
81
+ return { valid: true };
82
+ }
83
+
84
+ static getHelpMessage() {
85
+ return `
86
+ Triple-pattern format: subject predicate object
87
+
88
+ Examples:
89
+ ?person foaf:name "Alice" — People named Alice
90
+ <http://example.org/me> ?p ?o — All properties of a subject
91
+ ?s ?p ?o — All triples
92
+ ex:alice ex:knows ?friend — Alice's friends
93
+
94
+ Term types:
95
+ ?name — Named variable (bare "?" is NOT allowed)
96
+ <uri> — Full URI
97
+ prefix:local — Prefixed URI (CURIE)
98
+ "literal" — String literal (bare words and numbers are NOT accepted)
99
+ "x"@en — Literal with language tag
100
+ "1"^^<http://www.w3.org/2001/XMLSchema#integer> — Typed literal
101
+ `.trim();
102
+ }
103
+ }
104
+
105
+ // ── Internal helpers ─────────────────────────────────────────────────────────
106
+ function _isCurie(term) {
107
+ const m = term.match(/^([A-Za-z_][A-Za-z0-9_-]*):([^\s]*)$/);
108
+ if (!m) return false;
109
+ if (/^(https?|ftp|file|urn|mailto|data|tel|news|gopher|ldap|about):/i.test(term)) return false;
110
+ return true;
111
+ }
112
+
113
+ function _tokenizeTriplePattern(input) {
114
+ const out = [];
115
+ const s = String(input).trim();
116
+ let i = 0;
117
+ while (i < s.length) {
118
+ while (i < s.length && /\s/.test(s[i])) i++;
119
+ if (i >= s.length) break;
120
+ if (s[i] === '"') {
121
+ let j = i + 1;
122
+ while (j < s.length && s[j] !== '"') {
123
+ if (s[j] === '\\' && j + 1 < s.length) j += 2;
124
+ else j++;
125
+ }
126
+ if (j >= s.length) throw new Error(`Unterminated literal starting at position ${i}`);
127
+ j++;
128
+ if (s[j] === '@') {
129
+ j++;
130
+ while (j < s.length && /[A-Za-z-]/.test(s[j])) j++;
131
+ } else if (s[j] === '^' && s[j + 1] === '^' && s[j + 2] === '<') {
132
+ j += 3;
133
+ while (j < s.length && s[j] !== '>') j++;
134
+ if (j >= s.length) throw new Error('Unterminated datatype IRI');
135
+ j++;
136
+ }
137
+ out.push(s.slice(i, j));
138
+ i = j;
139
+ } else {
140
+ let j = i;
141
+ while (j < s.length && !/\s/.test(s[j])) j++;
142
+ out.push(s.slice(i, j));
143
+ i = j;
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ // Back-compat aliases (deprecated — prefer the new names).
150
+ export const MiniQueryParser = TriplePatternParser;
151
+ export const MiniQueryValidator = TriplePatternValidator;
@@ -0,0 +1,250 @@
1
+ import { mkLink } from '../views/_helpers.js';
2
+ import { render as renderTable } from '../views/table.js';
3
+
4
+ // W3C SPARQL Query Results JSON envelope helper.
5
+ function w3c(vars, bindings) { return { head: { vars }, results: { bindings } }; }
6
+
7
+ export class SparqlResultsRenderer {
8
+ constructor(container) {
9
+ this.container = container;
10
+ this._bnodeData = new Map();
11
+ this._modal = null;
12
+
13
+ // Bnode-link clicks: open modal with the node's properties
14
+ container.addEventListener('click', e => {
15
+ const a = e.target.closest('a.bnode-link');
16
+ if (!a) return;
17
+ e.preventDefault();
18
+ const idx = parseInt(a.dataset.bnodeIdx, 10);
19
+ const data = this._bnodeData.get(idx);
20
+ if (data) this._showBnodeModal(data);
21
+ });
22
+ }
23
+
24
+ showLoading(message = 'Loading results...') {
25
+ this.container.innerHTML = `<div class="loading" role="status" aria-live="polite">${message}</div>`;
26
+ }
27
+
28
+ showError(message) {
29
+ this.container.innerHTML = `<div class="error" role="alert">${message}</div>`;
30
+ }
31
+
32
+ showNoResults() {
33
+ this.container.innerHTML = '<div class="loading" role="status" aria-live="polite">No results found</div>';
34
+ }
35
+
36
+ // ── Main entry point ────────────────────────────────────────────────────────
37
+ // viewFn: a free function with signature render(container, data, host, options)
38
+ // `data` is the W3C SPARQL Query Results JSON envelope:
39
+ // { head: { vars: string[] }, results: { bindings: Row[] } }
40
+ renderResults(data, viewFn, options = {}) {
41
+ this._bnodeData.clear();
42
+
43
+ const vars = data.head.vars;
44
+ const bindings = data.results.bindings;
45
+
46
+ if (!bindings || bindings.length === 0) {
47
+ this.showNoResults();
48
+ return;
49
+ }
50
+
51
+ // 1 row × 1 column → scalar value, no table
52
+ if (vars.length === 1 && bindings.length === 1) {
53
+ const val = bindings[0][vars[0]];
54
+ this.container.innerHTML = '';
55
+ if (val && val.type === 'uri') {
56
+ this.container.appendChild(mkLink(val));
57
+ } else {
58
+ const span = document.createElement('span');
59
+ span.className = 'single-value';
60
+ span.textContent = val ? val.value : '';
61
+ this.container.appendChild(span);
62
+ }
63
+ return;
64
+ }
65
+
66
+ const pivoted = this._pivotSPO(data);
67
+ if (pivoted) {
68
+ this.container.innerHTML = '';
69
+ this._renderView(pivoted, viewFn, options);
70
+ return;
71
+ }
72
+
73
+ const grouped = this._groupByPredicate(data);
74
+
75
+ this.container.innerHTML = '';
76
+ this._renderView(grouped, viewFn, options);
77
+ }
78
+
79
+ _renderView(data, viewFn, options) {
80
+ const mkBnodeLink = v => this._mkBnodeLink(v);
81
+ viewFn(this.container, data, null, { ...options, mkBnodeLink });
82
+ }
83
+
84
+ // ── Pivot s,p,o → predicates as columns, subjects as rows ─────────────────
85
+ _pivotSPO(data) {
86
+ const v = data.head.vars;
87
+ const hasSPO = v.length === 3 && v[0]==='s' && v[1]==='p' && v[2]==='o';
88
+ const hasPO = v.length === 2 && v[0]==='p' && v[1]==='o';
89
+ if (!hasSPO && !hasPO) return null;
90
+
91
+ const subjectOrder = [];
92
+ const subjects = new Map();
93
+ const predOrder = [];
94
+ const predSet = new Set();
95
+
96
+ for (const row of data.results.bindings) {
97
+ const sKey = hasSPO ? (row.s?.value ?? '') : '';
98
+ const pURI = row.p?.value ?? '';
99
+
100
+ if (!subjects.has(sKey)) {
101
+ subjectOrder.push(sKey);
102
+ subjects.set(sKey, new Map());
103
+ }
104
+ if (!predSet.has(pURI)) { predSet.add(pURI); predOrder.push(pURI); }
105
+
106
+ const predMap = subjects.get(sKey);
107
+ if (!predMap.has(pURI)) predMap.set(pURI, []);
108
+ if (row.o) predMap.get(pURI).push(row.o);
109
+ }
110
+
111
+ const _short = uri => uri.replace(/.*[/#]([^/#]+)\/?$/, '$1') || uri;
112
+ const names = predOrder.map(_short);
113
+ const seen = {};
114
+ for (let i = 0; i < names.length; i++) {
115
+ const n = names[i];
116
+ if (seen[n] !== undefined) {
117
+ names[seen[n]] = predOrder[seen[n]];
118
+ names[i] = predOrder[i];
119
+ } else { seen[n] = i; }
120
+ }
121
+
122
+ const bindings = subjectOrder.map(sKey => {
123
+ const predMap = subjects.get(sKey);
124
+ const row = {};
125
+ for (let i = 0; i < predOrder.length; i++) {
126
+ const vals = predMap.get(predOrder[i]);
127
+ if (!vals || vals.length === 0) {
128
+ row[names[i]] = { type: 'literal', value: '' };
129
+ } else if (vals.length === 1) {
130
+ row[names[i]] = vals[0];
131
+ } else {
132
+ row[names[i]] = { type: 'multi', values: vals };
133
+ }
134
+ }
135
+ return row;
136
+ });
137
+
138
+ return w3c(names, bindings);
139
+ }
140
+
141
+ // ── Group rows by non-object columns, collect 'o' values ──────────────────
142
+ _groupByPredicate(data) {
143
+ const vars = data.head.vars;
144
+ if (!vars.includes('o')) return data;
145
+
146
+ const keyVars = vars.filter(v => v !== 'o');
147
+ const order = [];
148
+ const map = new Map();
149
+
150
+ for (const row of data.results.bindings) {
151
+ const key = keyVars.map(v => row[v]?.value ?? '').join('\x00');
152
+ if (!map.has(key)) {
153
+ order.push(key);
154
+ const newRow = {};
155
+ keyVars.forEach(v => newRow[v] = row[v]);
156
+ newRow._oVals = row.o ? [row.o] : [];
157
+ map.set(key, newRow);
158
+ } else if (row.o) {
159
+ map.get(key)._oVals.push(row.o);
160
+ }
161
+ }
162
+
163
+ const bindings = order.map(key => {
164
+ const row = map.get(key);
165
+ const vals = row._oVals;
166
+ delete row._oVals;
167
+ row.o = vals.length === 0 ? { type: 'literal', value: '' }
168
+ : vals.length === 1 ? vals[0]
169
+ : { type: 'multi', values: vals };
170
+ return row;
171
+ });
172
+
173
+ return w3c(vars, bindings);
174
+ }
175
+
176
+ // ── Blank-node link + modal ────────────────────────────────────────────────
177
+ _mkBnodeLink(value) {
178
+ const a = document.createElement('a');
179
+ a.href = '#';
180
+ a.className = 'bnode-link';
181
+ a.textContent = '[…]';
182
+ a.title = 'Click to view blank node properties';
183
+ const idx = this._bnodeData.size;
184
+ this._bnodeData.set(idx, value._data);
185
+ a.dataset.bnodeIdx = String(idx);
186
+ return a;
187
+ }
188
+
189
+ _showBnodeModal(data) {
190
+ const modal = this._getOrCreateModal();
191
+ const body = modal.querySelector('.bnode-modal-body');
192
+ body.innerHTML = '';
193
+ const sub = new SparqlResultsRenderer(body);
194
+ sub.renderResults(data, renderTable, { hideHeader: true });
195
+ this._lastFocus = (this.container.getRootNode().activeElement) || document.activeElement;
196
+ modal.classList.add('active');
197
+ const closeBtn = modal.querySelector('.bnode-modal-close');
198
+ if (closeBtn) closeBtn.focus();
199
+ }
200
+
201
+ _closeModal() {
202
+ if (!this._modal) return;
203
+ this._modal.classList.remove('active');
204
+ if (this._lastFocus && typeof this._lastFocus.focus === 'function') {
205
+ try { this._lastFocus.focus(); } catch { /* element may be gone */ }
206
+ }
207
+ this._lastFocus = null;
208
+ }
209
+
210
+ _getOrCreateModal() {
211
+ if (this._modal) return this._modal;
212
+ const modal = document.createElement('div');
213
+ modal.className = 'bnode-modal';
214
+ modal.setAttribute('role', 'dialog');
215
+ modal.setAttribute('aria-modal', 'true');
216
+ modal.setAttribute('aria-label', 'Blank node properties');
217
+ modal.innerHTML = `
218
+ <div class="bnode-modal-inner" tabindex="-1">
219
+ <button class="sol-btn sol-btn-danger bnode-modal-close" aria-label="Close">×</button>
220
+ <div class="bnode-modal-body"></div>
221
+ </div>`;
222
+ modal.querySelector('.bnode-modal-close')
223
+ .addEventListener('click', () => this._closeModal());
224
+ modal.addEventListener('click', e => { if (e.target === modal) this._closeModal(); });
225
+ // Focus trap: Tab cycles within the dialog while it's open.
226
+ modal.addEventListener('keydown', e => {
227
+ if (e.key !== 'Tab' || !modal.classList.contains('active')) return;
228
+ const focusables = modal.querySelectorAll(
229
+ 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input, select, textarea'
230
+ );
231
+ if (!focusables.length) return;
232
+ const first = focusables[0];
233
+ const last = focusables[focusables.length - 1];
234
+ const root = modal.getRootNode();
235
+ const active = root.activeElement === modal ? document.activeElement : root.activeElement;
236
+ if (e.shiftKey && active === first) { last.focus(); e.preventDefault(); }
237
+ else if (!e.shiftKey && active === last) { first.focus(); e.preventDefault(); }
238
+ });
239
+ (this.container.parentNode ?? this.container).appendChild(modal);
240
+ this._modal = modal;
241
+ this._escHandler = e => {
242
+ if (e.key === 'Escape' && modal.classList.contains('active')) this._closeModal();
243
+ };
244
+ document.addEventListener('keydown', this._escHandler);
245
+ return modal;
246
+ }
247
+ }
248
+
249
+ // CSS now lives in ../styles/sol-query-css.js — import { CSS, sheet } from there.
250
+ export { CSS as getDefaultStylesCSS, sheet as defaultStylesSheet } from '../styles/sol-query-css.js';
@@ -0,0 +1,32 @@
1
+ // Built-in result views, shared by the <sol-query> element (web/sol-query.js)
2
+ // and the data-from-query activator (core/from-query.js) so neither pulls in the
3
+ // other. Each entry returns the view's render function on demand; the all-in-one
4
+ // Rollup bundle inlines these `import()`s, ESM consumers fetch only what they use.
5
+ const BUILTIN_VIEW_LOADERS = {
6
+ table: () => import('../views/table.js'),
7
+ dl: () => import('../views/dl.js'),
8
+ list: () => import('../views/list.js'),
9
+ accordion: () => import('../views/accordion.js'),
10
+ anchorlist: () => import('../views/anchorlist.js'),
11
+ 'auto-complete': () => import('../views/auto-complete.js'),
12
+ menu: () => import('../views/menu.js'),
13
+ rolodex: () => import('../views/rolodex.js'),
14
+ select: () => import('../views/select.js'),
15
+ tabs: () => import('../views/tabs.js'),
16
+ };
17
+
18
+ // Views whose results go through SparqlResultsRenderer preprocessing (pivot
19
+ // s/p/o, group predicates, scalar display); others are called as fn(container,
20
+ // data, host).
21
+ export const PREPROCESS_VIEWS = new Set(['table', 'dl', 'list']);
22
+
23
+ const _viewCache = new Map();
24
+ export async function loadBuiltinView(name) {
25
+ if (_viewCache.has(name)) return _viewCache.get(name);
26
+ const loader = BUILTIN_VIEW_LOADERS[name];
27
+ if (!loader) return null;
28
+ const mod = await loader();
29
+ const fn = mod.render ?? mod.default;
30
+ _viewCache.set(name, fn);
31
+ return fn;
32
+ }
@@ -0,0 +1,34 @@
1
+ // Cell-rendering helpers shared across view renderers.
2
+
3
+ export function mkLink(val) {
4
+ const a = document.createElement('a');
5
+ a.href = val.value;
6
+ a.textContent = val.value.replace(/.*[/#]([^/#]+)\/?$/, '$1') || val.value;
7
+ a.title = val.value;
8
+ a.dataset.uri = val.value;
9
+ a.target = '_blank';
10
+ a.rel = 'noopener noreferrer';
11
+ return a;
12
+ }
13
+
14
+ export function termText(val) {
15
+ if (!val) return '';
16
+ if (val.type === 'uri') return val.value.replace(/.*[/#]([^/#]+)\/?$/, '$1') || val.value;
17
+ return val.value;
18
+ }
19
+
20
+ export function appendCell(parent, cell, mkBnodeLink) {
21
+ if (!cell) { parent.appendChild(document.createTextNode('')); return; }
22
+ if (cell.type === 'multi') {
23
+ cell.values.forEach((v, i) => {
24
+ if (i > 0) parent.appendChild(document.createTextNode(', '));
25
+ appendCell(parent, v, mkBnodeLink);
26
+ });
27
+ } else if (cell.type === 'bnode' && mkBnodeLink) {
28
+ parent.appendChild(mkBnodeLink(cell));
29
+ } else if (cell.type === 'uri') {
30
+ parent.appendChild(mkLink(cell));
31
+ } else {
32
+ parent.appendChild(document.createTextNode(cell.value ?? ''));
33
+ }
34
+ }