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/node/sol-menu.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as $rdf from 'rdflib';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
|
|
9
|
+
const UI = 'http://www.w3.org/ns/ui#';
|
|
10
|
+
const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
11
|
+
const SCH = 'http://schema.org/';
|
|
12
|
+
|
|
13
|
+
// ─── RDF parsing (mirrors sol-menu.js) ───────────────────────────
|
|
14
|
+
|
|
15
|
+
function loadStoreLocal(filePath) {
|
|
16
|
+
const abs = resolve(filePath);
|
|
17
|
+
const text = readFileSync(abs, 'utf-8');
|
|
18
|
+
const store = $rdf.graph();
|
|
19
|
+
const base = pathToFileURL(abs).href;
|
|
20
|
+
$rdf.parse(text, store, base, 'text/turtle');
|
|
21
|
+
return { store, base };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function loadStoreRemote(url) {
|
|
25
|
+
const resp = await fetch(url, { headers: { Accept: 'text/turtle, application/rdf+xml;q=0.9, */*;q=0.1' } });
|
|
26
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching ${url}`);
|
|
27
|
+
const text = await resp.text();
|
|
28
|
+
const store = $rdf.graph();
|
|
29
|
+
const ct = (resp.headers.get('content-type') || '').split(';')[0].trim();
|
|
30
|
+
const mime = ct === 'application/rdf+xml' ? 'application/rdf+xml' : 'text/turtle';
|
|
31
|
+
$rdf.parse(text, store, url, mime);
|
|
32
|
+
return { store, base: url };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function val(store, s, local) {
|
|
36
|
+
const n = store.any(s, $rdf.sym(UI + local));
|
|
37
|
+
return n ? n.value : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listEls(store, node) {
|
|
41
|
+
if (node.elements) return node.elements;
|
|
42
|
+
const out = [];
|
|
43
|
+
const nil = $rdf.sym(RDF + 'nil');
|
|
44
|
+
const first = $rdf.sym(RDF + 'first');
|
|
45
|
+
const rest = $rdf.sym(RDF + 'rest');
|
|
46
|
+
let cur = node;
|
|
47
|
+
while (cur && cur.value !== nil.value) {
|
|
48
|
+
const el = store.any(cur, first);
|
|
49
|
+
if (el) out.push(el);
|
|
50
|
+
cur = store.any(cur, rest);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rdfComponent(store, node) {
|
|
56
|
+
if (!node) return { tag: null, attrs: [] };
|
|
57
|
+
const tag = val(store, node, 'name') || val(store, node, 'label');
|
|
58
|
+
const an = store.each(node, $rdf.sym(UI + 'attribute'));
|
|
59
|
+
const pn = store.each(node, $rdf.sym(UI + 'parameter'));
|
|
60
|
+
const attrs = [...an, ...pn].map(p => [
|
|
61
|
+
(store.any(p, $rdf.sym(SCH + 'name')) || {}).value || '',
|
|
62
|
+
(store.any(p, $rdf.sym(SCH + 'value')) || {}).value || '',
|
|
63
|
+
]).filter(([k]) => k);
|
|
64
|
+
return { tag, attrs };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseItems(store, menuNode) {
|
|
68
|
+
const pn = store.any(menuNode, $rdf.sym(UI + 'parts'));
|
|
69
|
+
if (!pn) return [];
|
|
70
|
+
const parts = listEls(store, pn);
|
|
71
|
+
const menuT = $rdf.sym(UI + 'Menu');
|
|
72
|
+
const compT = $rdf.sym(UI + 'Component');
|
|
73
|
+
const typeP = $rdf.sym(RDF + 'type');
|
|
74
|
+
const items = [];
|
|
75
|
+
for (const part of parts) {
|
|
76
|
+
const t = store.any(part, typeP);
|
|
77
|
+
const label = val(store, part, 'label') || part.value;
|
|
78
|
+
if (t?.value === menuT.value) {
|
|
79
|
+
items.push({ kind: 'menu', name: label, children: parseItems(store, part) });
|
|
80
|
+
} else if (t?.value === compT.value) {
|
|
81
|
+
const c = rdfComponent(store, part);
|
|
82
|
+
items.push({ kind: 'component', name: label, tag: c.tag, attrs: c.attrs });
|
|
83
|
+
} else {
|
|
84
|
+
items.push({
|
|
85
|
+
kind: 'link',
|
|
86
|
+
name: label,
|
|
87
|
+
href: val(store, part, 'href'),
|
|
88
|
+
contents: val(store, part, 'contents'),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function loadMenu(uri) {
|
|
96
|
+
const hi = uri.indexOf('#');
|
|
97
|
+
const docPart = hi >= 0 ? uri.slice(0, hi) : uri;
|
|
98
|
+
const fragment = hi >= 0 ? uri.slice(hi + 1) : '';
|
|
99
|
+
const isRemote = /^https?:\/\//i.test(docPart);
|
|
100
|
+
const { store, base } = isRemote
|
|
101
|
+
? await loadStoreRemote(docPart)
|
|
102
|
+
: loadStoreLocal(docPart);
|
|
103
|
+
let root;
|
|
104
|
+
if (fragment) {
|
|
105
|
+
root = $rdf.sym(base + '#' + fragment);
|
|
106
|
+
} else {
|
|
107
|
+
root = store.each(null, $rdf.sym(RDF + 'type'), $rdf.sym(UI + 'Menu'))[0];
|
|
108
|
+
}
|
|
109
|
+
if (!root) { process.stderr.write('No ui:Menu found.\n'); process.exit(1); }
|
|
110
|
+
return {
|
|
111
|
+
title: val(store, root, 'label') || 'Menu',
|
|
112
|
+
items: parseItems(store, root),
|
|
113
|
+
base,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Content rendering ──────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function stripHtml(s) {
|
|
120
|
+
return s.replace(/<[^>]+>/g, '')
|
|
121
|
+
.replace(/</g, '<').replace(/>/g, '>')
|
|
122
|
+
.replace(/&/g, '&').replace(/"/g, '"')
|
|
123
|
+
.replace(/'/g, "'").trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function shorten(uri) {
|
|
127
|
+
if (!uri) return uri;
|
|
128
|
+
const h = uri.lastIndexOf('#');
|
|
129
|
+
if (h >= 0 && h < uri.length - 1) return uri.slice(h + 1);
|
|
130
|
+
if (uri.includes('://')) {
|
|
131
|
+
const s = uri.lastIndexOf('/');
|
|
132
|
+
if (s >= 0 && s < uri.length - 1) return uri.slice(s + 1);
|
|
133
|
+
}
|
|
134
|
+
return uri;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseSelectVars(sparql) {
|
|
138
|
+
const m = sparql.replace(/#[^\n]*/g, '')
|
|
139
|
+
.match(/SELECT\s+(?:DISTINCT\s+|REDUCED\s+)?(.*?)\s+WHERE/is);
|
|
140
|
+
if (!m) return null;
|
|
141
|
+
const clause = m[1].trim();
|
|
142
|
+
if (clause === '*') return null;
|
|
143
|
+
const vars = clause.match(/\?\w+/g);
|
|
144
|
+
return vars ? vars.map(v => v.slice(1)) : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveEndpoint(endpoint, base) {
|
|
148
|
+
try {
|
|
149
|
+
const url = new URL(endpoint, base);
|
|
150
|
+
if (url.protocol === 'file:') return { type: 'file', path: fileURLToPath(url) };
|
|
151
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') return { type: 'remote', url: url.href };
|
|
152
|
+
} catch { /* fall through */ }
|
|
153
|
+
try { return { type: 'file', path: resolve(endpoint) }; } catch { return null; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function loadStoreFrom(endpoint, base) {
|
|
157
|
+
const target = resolveEndpoint(endpoint, base);
|
|
158
|
+
if (!target) throw new Error('Cannot resolve endpoint');
|
|
159
|
+
|
|
160
|
+
const store = $rdf.graph();
|
|
161
|
+
let storeBase;
|
|
162
|
+
|
|
163
|
+
if (target.type === 'file') {
|
|
164
|
+
const text = readFileSync(target.path, 'utf-8');
|
|
165
|
+
storeBase = pathToFileURL(resolve(target.path)).href;
|
|
166
|
+
$rdf.parse(text, store, storeBase, 'text/turtle');
|
|
167
|
+
} else {
|
|
168
|
+
const resp = await fetch(target.url, { headers: { Accept: 'text/turtle, application/rdf+xml;q=0.9, */*;q=0.1' } });
|
|
169
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching ${target.url}`);
|
|
170
|
+
const text = await resp.text();
|
|
171
|
+
storeBase = target.url;
|
|
172
|
+
const ct = (resp.headers.get('content-type') || '').split(';')[0].trim();
|
|
173
|
+
const mime = ct === 'application/rdf+xml' ? 'application/rdf+xml' : 'text/turtle';
|
|
174
|
+
$rdf.parse(text, store, storeBase, mime);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { store, base: storeBase };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function runPatternQuery(store, pattern, storeBase) {
|
|
181
|
+
const tokens = pattern.trim().split(/\s+/);
|
|
182
|
+
if (tokens.length !== 3) throw new Error('Pattern must have exactly 3 parts');
|
|
183
|
+
|
|
184
|
+
function toNode(tok) {
|
|
185
|
+
if (tok === '?' || tok.startsWith('?')) return undefined;
|
|
186
|
+
if (tok.startsWith('<') && tok.endsWith('>')) return $rdf.sym(tok.slice(1, -1));
|
|
187
|
+
if (tok.startsWith('"')) return $rdf.lit(tok.replace(/^"|"$/g, ''));
|
|
188
|
+
return $rdf.sym(new URL(tok, storeBase).href);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const s = toNode(tokens[0]), p = toNode(tokens[1]), o = toNode(tokens[2]);
|
|
192
|
+
const stmts = store.match(s, p, o, null);
|
|
193
|
+
|
|
194
|
+
const varNames = [];
|
|
195
|
+
const slots = ['s', 'p', 'o'];
|
|
196
|
+
tokens.forEach((tok, i) => {
|
|
197
|
+
if (tok === '?' || tok.startsWith('?')) varNames.push(tok === '?' ? slots[i] : tok.slice(1));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const rows = stmts.map(st => {
|
|
201
|
+
const row = {};
|
|
202
|
+
let vi = 0;
|
|
203
|
+
if (s === undefined) row[varNames[vi++]] = st.subject.value;
|
|
204
|
+
if (p === undefined) row[varNames[vi++]] = st.predicate.value;
|
|
205
|
+
if (o === undefined) row[varNames[vi++]] = st.object.value;
|
|
206
|
+
return row;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return { vars: varNames, rows };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function runQuery(endpoint, sparql, pattern, base) {
|
|
213
|
+
const { store, base: storeBase } = await loadStoreFrom(endpoint, base);
|
|
214
|
+
|
|
215
|
+
if (pattern) return runPatternQuery(store, pattern, storeBase);
|
|
216
|
+
|
|
217
|
+
const parsed = $rdf.SPARQLToQuery(sparql, false, store);
|
|
218
|
+
if (!parsed) throw new Error('Cannot parse SPARQL');
|
|
219
|
+
|
|
220
|
+
const declaredVars = parseSelectVars(sparql);
|
|
221
|
+
const fetcher = new $rdf.Fetcher(store);
|
|
222
|
+
|
|
223
|
+
return new Promise((res) => {
|
|
224
|
+
const bindings = [];
|
|
225
|
+
store.query(parsed, b => bindings.push(b), fetcher, () => {
|
|
226
|
+
const vars = declaredVars
|
|
227
|
+
|| (bindings.length
|
|
228
|
+
? Object.keys(bindings[0]).filter(k => k.startsWith('?')).map(k => k.slice(1))
|
|
229
|
+
: []);
|
|
230
|
+
const rows = bindings.map(b => {
|
|
231
|
+
const row = {};
|
|
232
|
+
for (const v of vars) {
|
|
233
|
+
const node = b[`?${v}`];
|
|
234
|
+
row[v] = node ? node.value : '';
|
|
235
|
+
}
|
|
236
|
+
return row;
|
|
237
|
+
});
|
|
238
|
+
res({ vars, rows });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function preloadQueries(items, base) {
|
|
244
|
+
for (const item of items) {
|
|
245
|
+
if (item.kind === 'menu' && item.children) {
|
|
246
|
+
await preloadQueries(item.children, base);
|
|
247
|
+
} else if (item.kind === 'component' && item.tag === 'sol-query') {
|
|
248
|
+
const a = Object.fromEntries(item.attrs);
|
|
249
|
+
const endpoint = a.endpoint || a.source;
|
|
250
|
+
const sparql = a.sparql || a.query;
|
|
251
|
+
const pattern = a.pattern || a.wanted;
|
|
252
|
+
if (endpoint && (sparql || pattern)) {
|
|
253
|
+
try {
|
|
254
|
+
item._queryResult = await runQuery(endpoint, sparql, pattern, base);
|
|
255
|
+
} catch (e) { item._queryError = e.message; }
|
|
256
|
+
}
|
|
257
|
+
} else if (item.kind === 'link' && item.href) {
|
|
258
|
+
try {
|
|
259
|
+
const url = new URL(item.href, base);
|
|
260
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
261
|
+
const resp = await fetch(url.href);
|
|
262
|
+
if (resp.ok) item._remoteContent = await resp.text();
|
|
263
|
+
else item._remoteContent = `[HTTP ${resp.status}]`;
|
|
264
|
+
}
|
|
265
|
+
} catch { /* local files handled synchronously in contentFor */ }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function pivotTriples(data) {
|
|
271
|
+
const { vars, rows } = data;
|
|
272
|
+
const hasSPO = vars.length === 3 && vars[0] === 's' && vars[1] === 'p' && vars[2] === 'o';
|
|
273
|
+
const hasPO = vars.length === 2 && vars[0] === 'p' && vars[1] === 'o';
|
|
274
|
+
if (!hasSPO && !hasPO) return null;
|
|
275
|
+
|
|
276
|
+
const subjectOrder = [];
|
|
277
|
+
const subjects = new Map();
|
|
278
|
+
const predOrder = [];
|
|
279
|
+
const predSet = new Set();
|
|
280
|
+
|
|
281
|
+
for (const row of rows) {
|
|
282
|
+
const sKey = hasSPO ? (row.s || '') : '';
|
|
283
|
+
const pURI = row.p || '';
|
|
284
|
+
if (!subjects.has(sKey)) { subjectOrder.push(sKey); subjects.set(sKey, new Map()); }
|
|
285
|
+
if (!predSet.has(pURI)) { predSet.add(pURI); predOrder.push(pURI); }
|
|
286
|
+
const predMap = subjects.get(sKey);
|
|
287
|
+
if (!predMap.has(pURI)) predMap.set(pURI, []);
|
|
288
|
+
predMap.get(pURI).push(row.o || '');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const names = predOrder.map(shorten);
|
|
292
|
+
const seen = {};
|
|
293
|
+
for (let i = 0; i < names.length; i++) {
|
|
294
|
+
if (seen[names[i]] !== undefined) {
|
|
295
|
+
names[seen[names[i]]] = predOrder[seen[names[i]]];
|
|
296
|
+
names[i] = predOrder[i];
|
|
297
|
+
} else { seen[names[i]] = i; }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const INDEX = ['name', 'label', 'title'];
|
|
301
|
+
const ii = names.findIndex(n => INDEX.includes(n.toLowerCase()));
|
|
302
|
+
if (ii > 0) {
|
|
303
|
+
const [n] = names.splice(ii, 1);
|
|
304
|
+
const [p] = predOrder.splice(ii, 1);
|
|
305
|
+
names.unshift(n);
|
|
306
|
+
predOrder.unshift(p);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const pivotedRows = subjectOrder.map(sKey => {
|
|
310
|
+
const predMap = subjects.get(sKey);
|
|
311
|
+
const row = {};
|
|
312
|
+
for (let i = 0; i < predOrder.length; i++) {
|
|
313
|
+
const vals = predMap.get(predOrder[i]);
|
|
314
|
+
row[names[i]] = vals?.length ? vals.join(', ') : '';
|
|
315
|
+
}
|
|
316
|
+
return row;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return { vars: names, rows: pivotedRows };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function reorderColumns(data) {
|
|
323
|
+
const INDEX = ['name', 'label', 'title'];
|
|
324
|
+
const idx = data.vars.findIndex(v => INDEX.includes(v.toLowerCase()));
|
|
325
|
+
if (idx <= 0) return data;
|
|
326
|
+
const vars = [data.vars[idx], ...data.vars.filter((_, i) => i !== idx)];
|
|
327
|
+
return { vars, rows: data.rows };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatTable(data, maxW) {
|
|
331
|
+
if (!data || !data.vars.length) return 'No results.';
|
|
332
|
+
|
|
333
|
+
const src = pivotTriples(data) || reorderColumns(data);
|
|
334
|
+
const { vars, rows } = src;
|
|
335
|
+
if (!rows.length) return `Columns: ${vars.join(', ')}\n\nNo rows.`;
|
|
336
|
+
|
|
337
|
+
const display = rows.map(row => {
|
|
338
|
+
const r = {};
|
|
339
|
+
for (const v of vars) r[v] = shorten(row[v] || '');
|
|
340
|
+
return r;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const colW = {};
|
|
344
|
+
for (const v of vars) {
|
|
345
|
+
colW[v] = v.length;
|
|
346
|
+
for (const r of display) colW[v] = Math.max(colW[v], r[v].length);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const borders = vars.length + 1;
|
|
350
|
+
const padding = vars.length * 2;
|
|
351
|
+
const totalW = Object.values(colW).reduce((a, b) => a + b, 0) + borders + padding;
|
|
352
|
+
if (totalW > maxW) {
|
|
353
|
+
const available = maxW - borders - padding;
|
|
354
|
+
const fair = Math.max(3, Math.floor(available / vars.length));
|
|
355
|
+
for (const v of vars) colW[v] = Math.min(colW[v], fair);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const cell = (s, w) => ' ' + (s.length > w ? s.slice(0, w - 1) + '…' : s + ' '.repeat(w - s.length)) + ' ';
|
|
359
|
+
const top = '┌' + vars.map(v => '─'.repeat(colW[v] + 2)).join('┬') + '┐';
|
|
360
|
+
const hdr = '│' + vars.map(v => cell(v, colW[v])).join('│') + '│';
|
|
361
|
+
const mid = '├' + vars.map(v => '─'.repeat(colW[v] + 2)).join('┼') + '┤';
|
|
362
|
+
const body = display.map(r =>
|
|
363
|
+
'│' + vars.map(v => cell(r[v], colW[v])).join('│') + '│'
|
|
364
|
+
);
|
|
365
|
+
const bot = '└' + vars.map(v => '─'.repeat(colW[v] + 2)).join('┴') + '┘';
|
|
366
|
+
|
|
367
|
+
return [top, hdr, mid, ...body, bot].join('\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formatDl(data, maxW) {
|
|
371
|
+
if (!data || !data.vars.length) return 'No results.';
|
|
372
|
+
|
|
373
|
+
const src = pivotTriples(data) || reorderColumns(data);
|
|
374
|
+
const { vars, rows } = src;
|
|
375
|
+
if (!rows.length) return 'No rows.';
|
|
376
|
+
|
|
377
|
+
const nameVar = vars[0];
|
|
378
|
+
const restVars = vars.slice(1);
|
|
379
|
+
const lines = [];
|
|
380
|
+
|
|
381
|
+
for (const row of rows) {
|
|
382
|
+
const term = shorten(row[nameVar] || '');
|
|
383
|
+
lines.push(term);
|
|
384
|
+
lines.push('─'.repeat(Math.min(term.length, maxW)));
|
|
385
|
+
for (const v of restVars) {
|
|
386
|
+
const val = shorten(row[v] || '');
|
|
387
|
+
lines.push(` ${v}: ${val}`);
|
|
388
|
+
}
|
|
389
|
+
lines.push('');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return lines.join('\n');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function formatList(data, maxW) {
|
|
396
|
+
if (!data || !data.vars.length) return 'No results.';
|
|
397
|
+
|
|
398
|
+
const src = pivotTriples(data) || reorderColumns(data);
|
|
399
|
+
const { vars, rows } = src;
|
|
400
|
+
if (!rows.length) return 'No rows.';
|
|
401
|
+
|
|
402
|
+
if (vars.length === 1) {
|
|
403
|
+
return rows.map(r => ` • ${shorten(r[vars[0]] || '')}`).join('\n');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return formatTable(data, maxW);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function formatQuery(data, view, maxW) {
|
|
410
|
+
if (!data) return 'No results.';
|
|
411
|
+
if (view === 'dl') return formatDl(data, maxW);
|
|
412
|
+
if (view === 'list') return formatList(data, maxW);
|
|
413
|
+
return formatTable(data, maxW);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function contentFor(item, base, maxW) {
|
|
417
|
+
if (item.kind === 'component' && item.tag === 'sol-query') {
|
|
418
|
+
if (item._queryError) return `[Query error: ${item._queryError}]`;
|
|
419
|
+
if (item._queryResult) {
|
|
420
|
+
const view = (Object.fromEntries(item.attrs).view || '').toLowerCase();
|
|
421
|
+
return formatQuery(item._queryResult, view, maxW);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (item.kind === 'component') {
|
|
425
|
+
const lines = [`<${item.tag}>`];
|
|
426
|
+
for (const [k, v] of item.attrs) lines.push(` ${k}="${v}"`);
|
|
427
|
+
return lines.join('\n');
|
|
428
|
+
}
|
|
429
|
+
if (item.contents) return stripHtml(item.contents);
|
|
430
|
+
if (item.href) {
|
|
431
|
+
try {
|
|
432
|
+
const url = new URL(item.href, base);
|
|
433
|
+
if (url.protocol === 'file:') return readFileSync(fileURLToPath(url), 'utf-8');
|
|
434
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
435
|
+
return item._remoteContent || `[Loading: ${item.href}]`;
|
|
436
|
+
}
|
|
437
|
+
} catch { return `[Could not load: ${item.href}]`; }
|
|
438
|
+
}
|
|
439
|
+
return '';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Terminal UI ────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
const CSI = '\x1b[';
|
|
445
|
+
const CLEAR = CSI + '2J' + CSI + 'H';
|
|
446
|
+
const HIDE = CSI + '?25l';
|
|
447
|
+
const SHOW = CSI + '?25h';
|
|
448
|
+
const RESET = CSI + '0m';
|
|
449
|
+
const BOLD = CSI + '1m';
|
|
450
|
+
const DIM = CSI + '2m';
|
|
451
|
+
const REV = CSI + '7m';
|
|
452
|
+
const CYAN = CSI + '36m';
|
|
453
|
+
const YELLOW = CSI + '33m';
|
|
454
|
+
|
|
455
|
+
function pad(s, w) {
|
|
456
|
+
return s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function wordWrap(text, w) {
|
|
460
|
+
const out = [];
|
|
461
|
+
for (const raw of text.split('\n')) {
|
|
462
|
+
if (raw.length <= w) { out.push(raw); continue; }
|
|
463
|
+
let rem = raw;
|
|
464
|
+
while (rem.length > w) {
|
|
465
|
+
let br = rem.lastIndexOf(' ', w);
|
|
466
|
+
if (br <= 0) br = w;
|
|
467
|
+
out.push(rem.slice(0, br));
|
|
468
|
+
rem = rem.slice(br).trimStart();
|
|
469
|
+
}
|
|
470
|
+
if (rem) out.push(rem);
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function buildVisible(items, expanded, depth = 0) {
|
|
476
|
+
const out = [];
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
if (item.kind === 'menu') {
|
|
479
|
+
const exp = expanded.has(item);
|
|
480
|
+
out.push({ item, depth, group: true, expanded: exp });
|
|
481
|
+
if (exp) out.push(...buildVisible(item.children, expanded, depth + 1));
|
|
482
|
+
} else {
|
|
483
|
+
out.push({ item, depth, group: false });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function draw(title, vis, sel, lines, navScr, cScr) {
|
|
490
|
+
const W = process.stdout.columns || 80;
|
|
491
|
+
const H = process.stdout.rows || 24;
|
|
492
|
+
const navW = Math.max(18, Math.min(36, Math.floor(W * 0.32)));
|
|
493
|
+
const cW = W - navW - 4;
|
|
494
|
+
const bodyH = H - 5;
|
|
495
|
+
|
|
496
|
+
let buf = CLEAR;
|
|
497
|
+
|
|
498
|
+
// top border
|
|
499
|
+
const tLabel = `─ ${title} `;
|
|
500
|
+
const tFill = Math.max(0, navW - tLabel.length);
|
|
501
|
+
buf += `${BOLD}┌${tLabel}${'─'.repeat(tFill)}┬${'─'.repeat(cW + 1)}┐${RESET}\n`;
|
|
502
|
+
|
|
503
|
+
// body
|
|
504
|
+
for (let r = 0; r < bodyH; r++) {
|
|
505
|
+
const vi = r + navScr;
|
|
506
|
+
let navTxt = '';
|
|
507
|
+
if (vi < vis.length) {
|
|
508
|
+
const e = vis[vi];
|
|
509
|
+
const indent = ' '.repeat(e.depth);
|
|
510
|
+
let mark = ' ';
|
|
511
|
+
if (e.group) mark = e.expanded ? '▼ ' : '▶ ';
|
|
512
|
+
else if (vi === sel) mark = '► ';
|
|
513
|
+
navTxt = indent + mark + e.item.name;
|
|
514
|
+
}
|
|
515
|
+
const navPlain = pad(navTxt, navW);
|
|
516
|
+
let navStyled;
|
|
517
|
+
if (vi === sel) navStyled = `${REV}${CYAN}${navPlain}${RESET}`;
|
|
518
|
+
else if (vi < vis.length && vis[vi].group) navStyled = `${YELLOW}${navPlain}${RESET}`;
|
|
519
|
+
else navStyled = navPlain;
|
|
520
|
+
|
|
521
|
+
const ci = r + cScr;
|
|
522
|
+
const cTxt = ci < lines.length ? pad(lines[ci], cW) : ' '.repeat(cW);
|
|
523
|
+
|
|
524
|
+
buf += `│${navStyled}│ ${cTxt}│\n`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// divider + status + bottom
|
|
528
|
+
const bW = navW + cW + 2;
|
|
529
|
+
const help = ' ↑↓ navigate Enter/→ expand ← collapse PgUp/Dn scroll q quit ';
|
|
530
|
+
const hPad = Math.max(0, bW - help.length);
|
|
531
|
+
const hL = Math.floor(hPad / 2);
|
|
532
|
+
const hR = hPad - hL;
|
|
533
|
+
buf += `├${'─'.repeat(navW)}┴${'─'.repeat(cW + 1)}┤\n`;
|
|
534
|
+
buf += `│${'─'.repeat(hL)}${DIM}${help}${RESET}${'─'.repeat(hR)}│\n`;
|
|
535
|
+
buf += `└${'─'.repeat(bW)}┘`;
|
|
536
|
+
|
|
537
|
+
process.stdout.write(buf);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ─── Interactive loop ───────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
async function run(uri) {
|
|
543
|
+
const { title, items, base } = await loadMenu(uri);
|
|
544
|
+
if (!items.length) { console.log('Empty menu.'); return; }
|
|
545
|
+
|
|
546
|
+
process.stdout.write('Loading data...\r');
|
|
547
|
+
await preloadQueries(items, base);
|
|
548
|
+
|
|
549
|
+
const expanded = new Set();
|
|
550
|
+
let sel = 0, navScr = 0, cScr = 0;
|
|
551
|
+
|
|
552
|
+
function refresh() {
|
|
553
|
+
const vis = buildVisible(items, expanded);
|
|
554
|
+
if (sel >= vis.length) sel = vis.length - 1;
|
|
555
|
+
if (sel < 0) sel = 0;
|
|
556
|
+
|
|
557
|
+
const H = process.stdout.rows || 24;
|
|
558
|
+
const W = process.stdout.columns || 80;
|
|
559
|
+
const navW = Math.max(18, Math.min(36, Math.floor(W * 0.32)));
|
|
560
|
+
const cW = W - navW - 4;
|
|
561
|
+
const bodyH = H - 5;
|
|
562
|
+
|
|
563
|
+
if (sel < navScr) navScr = sel;
|
|
564
|
+
if (sel >= navScr + bodyH) navScr = sel - bodyH + 1;
|
|
565
|
+
|
|
566
|
+
const entry = vis[sel];
|
|
567
|
+
let text = '';
|
|
568
|
+
if (entry && !entry.group) text = contentFor(entry.item, base, cW - 1);
|
|
569
|
+
else if (entry?.group) text = `Submenu: ${entry.item.name}\n\nPress Enter or → to expand.`;
|
|
570
|
+
|
|
571
|
+
const lines = wordWrap(text, Math.max(1, cW - 1));
|
|
572
|
+
const maxCS = Math.max(0, lines.length - bodyH);
|
|
573
|
+
if (cScr > maxCS) cScr = maxCS;
|
|
574
|
+
|
|
575
|
+
draw(title, vis, sel, lines, navScr, cScr);
|
|
576
|
+
return vis;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
process.stdout.write(HIDE);
|
|
580
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
581
|
+
readline.emitKeypressEvents(process.stdin);
|
|
582
|
+
|
|
583
|
+
let vis = refresh();
|
|
584
|
+
const fl = vis.findIndex(v => !v.group);
|
|
585
|
+
if (fl >= 0) { sel = fl; vis = refresh(); }
|
|
586
|
+
|
|
587
|
+
function cleanup() {
|
|
588
|
+
process.stdout.write(SHOW + '\n');
|
|
589
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
process.stdin.on('keypress', (_str, key) => {
|
|
593
|
+
if (!key) return;
|
|
594
|
+
const bodyH = (process.stdout.rows || 24) - 5;
|
|
595
|
+
|
|
596
|
+
if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
597
|
+
cleanup();
|
|
598
|
+
process.exit(0);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (key.name === 'up' && sel > 0) { sel--; cScr = 0; }
|
|
602
|
+
else if (key.name === 'down' && sel < vis.length-1) { sel++; cScr = 0; }
|
|
603
|
+
else if (key.name === 'right' || key.name === 'return') {
|
|
604
|
+
const e = vis[sel];
|
|
605
|
+
if (e?.group && !expanded.has(e.item)) expanded.add(e.item);
|
|
606
|
+
}
|
|
607
|
+
else if (key.name === 'left') {
|
|
608
|
+
const e = vis[sel];
|
|
609
|
+
if (e?.group && expanded.has(e.item)) {
|
|
610
|
+
expanded.delete(e.item);
|
|
611
|
+
} else if (e && e.depth > 0) {
|
|
612
|
+
for (let i = sel - 1; i >= 0; i--) {
|
|
613
|
+
if (vis[i].group && vis[i].depth < e.depth) { sel = i; break; }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (key.name === 'pagedown') { cScr += (process.stdout.rows || 24) - 5; }
|
|
618
|
+
else if (key.name === 'pageup') { cScr = Math.max(0, cScr - ((process.stdout.rows || 24) - 5)); }
|
|
619
|
+
|
|
620
|
+
vis = refresh();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
process.on('SIGWINCH', () => { vis = refresh(); });
|
|
624
|
+
process.on('exit', cleanup);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─── Entry ──────────────────────────────────────────────────────
|
|
628
|
+
|
|
629
|
+
export { loadMenu, parseItems };
|
|
630
|
+
|
|
631
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
632
|
+
if (process.argv[1] && resolve(process.argv[1]) === __filename) {
|
|
633
|
+
const uri = process.argv[2];
|
|
634
|
+
if (!uri) {
|
|
635
|
+
console.error('Usage: node sol-menu.js <menu.ttl[#MenuId]>');
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
run(uri);
|
|
639
|
+
}
|