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/core/menu-rdf.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Pure RDF→menu-item parsing helpers used by <sol-menu>'s `from-rdf`
|
|
2
|
+
// attribute. No DOM dependencies — `parseMenuItems` and friends return
|
|
3
|
+
// plain JS descriptions that the host element wraps with render closures.
|
|
4
|
+
|
|
5
|
+
import { rdf } from './rdf.js';
|
|
6
|
+
import { loadRdfStore } from './rdf-utils.js';
|
|
7
|
+
|
|
8
|
+
const UI = 'http://www.w3.org/ns/ui#';
|
|
9
|
+
const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
10
|
+
const SCHEMA = 'http://schema.org/';
|
|
11
|
+
const ACL = 'http://www.w3.org/ns/auth/acl#';
|
|
12
|
+
|
|
13
|
+
// An item may declare the access mode it needs via the standard WAC vocab,
|
|
14
|
+
// e.g. `acl:mode acl:Write`. We surface a `requiresWrite` flag; the host app
|
|
15
|
+
// decides what to do with it (hide / disable / …) — the menu takes no policy.
|
|
16
|
+
function requiresWriteMode(store, subject) {
|
|
17
|
+
return store.each(subject, rdf.sym(ACL + 'mode'), null)
|
|
18
|
+
.some(m => m.value === ACL + 'Write');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Read a single ui:<localName> property of `subject` from `store`.
|
|
22
|
+
export function rdfVal(store, subject, localName) {
|
|
23
|
+
const node = store.any(subject, rdf.sym(UI + localName));
|
|
24
|
+
return node ? node.value : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Walk an rdf:List, returning its elements as an array.
|
|
28
|
+
export function rdfListElements(store, listNode) {
|
|
29
|
+
if (listNode.elements) return listNode.elements;
|
|
30
|
+
const items = [];
|
|
31
|
+
let cur = listNode;
|
|
32
|
+
const nil = rdf.sym(RDF + 'nil');
|
|
33
|
+
const first = rdf.sym(RDF + 'first');
|
|
34
|
+
const rest = rdf.sym(RDF + 'rest');
|
|
35
|
+
while (cur && cur.value !== nil.value) {
|
|
36
|
+
const el = store.any(cur, first);
|
|
37
|
+
if (el) items.push(el);
|
|
38
|
+
cur = store.any(cur, rest);
|
|
39
|
+
}
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read a ui:Component (or handler) node into { tag, params } where
|
|
44
|
+
// params is [[name, value], ...] from ui:attribute / ui:parameter blanks.
|
|
45
|
+
export function rdfComponent(store, node) {
|
|
46
|
+
if (!node) return { tag: null, params: [] };
|
|
47
|
+
const tag = rdfVal(store, node, 'name') || rdfVal(store, node, 'label');
|
|
48
|
+
const attrNodes = store.each(node, rdf.sym(UI + 'attribute'), null);
|
|
49
|
+
const paramNodes = store.each(node, rdf.sym(UI + 'parameter'), null);
|
|
50
|
+
const params = [...attrNodes, ...paramNodes].map(p => [
|
|
51
|
+
(store.any(p, rdf.sym(SCHEMA + 'name')) || {}).value || '',
|
|
52
|
+
(store.any(p, rdf.sym(SCHEMA + 'value')) || {}).value || '',
|
|
53
|
+
]).filter(([k]) => k);
|
|
54
|
+
return { tag, params };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The fragment of a subject IRI (e.g. ".../menu.ttl#Settings" → "Settings"),
|
|
58
|
+
// used as the item's stable id so an HTML region can claim it via data-for.
|
|
59
|
+
function fragmentOf(node) {
|
|
60
|
+
const v = (node && node.value) || '';
|
|
61
|
+
const i = v.indexOf('#');
|
|
62
|
+
return i >= 0 ? v.slice(i + 1) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Normalize a ui:orientation value to the "horizontal"/"vertical" token used
|
|
66
|
+
// by the HTML attribute layer. Accepts a ui:Orientation instance IRI
|
|
67
|
+
// (ui:Horizontal → "horizontal") or a legacy literal ("horizontal").
|
|
68
|
+
function orientationToken(v) {
|
|
69
|
+
if (!v) return null;
|
|
70
|
+
const local = v.includes('#') ? v.slice(v.indexOf('#') + 1) : v;
|
|
71
|
+
return local.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a ui:Menu's parts into a tree of plain item descriptions.
|
|
76
|
+
*
|
|
77
|
+
* Each description has one of these shapes (no functions, no DOM):
|
|
78
|
+
*
|
|
79
|
+
* { type: 'submenu', id, name, children: [...] }
|
|
80
|
+
* { type: 'component', id, name, icon, tag, params }
|
|
81
|
+
* { type: 'link', id, name, icon, href, contents }
|
|
82
|
+
*
|
|
83
|
+
* No display info lives in the RDF — `where/how/lifetime` are resolved from
|
|
84
|
+
* the HTML at render time (region= cascade, data-for, surface keywords). `id`
|
|
85
|
+
* is the item's IRI fragment, the join key an HTML region uses to claim it.
|
|
86
|
+
*/
|
|
87
|
+
export function parseMenuItems(store, menuNode) {
|
|
88
|
+
const partsNode = store.any(menuNode, rdf.sym(UI + 'parts'));
|
|
89
|
+
if (!partsNode) return [];
|
|
90
|
+
const parts = rdfListElements(store, partsNode);
|
|
91
|
+
const menuType = rdf.sym(UI + 'Menu');
|
|
92
|
+
const componentType = rdf.sym(UI + 'Component');
|
|
93
|
+
const typeNode = rdf.sym(RDF + 'type');
|
|
94
|
+
const items = [];
|
|
95
|
+
|
|
96
|
+
for (const part of parts) {
|
|
97
|
+
const partType = store.any(part, typeNode);
|
|
98
|
+
const id = fragmentOf(part);
|
|
99
|
+
const label = rdfVal(store, part, 'label') || part.value;
|
|
100
|
+
const icon = rdfVal(store, part, 'icon');
|
|
101
|
+
const requiresWrite = requiresWriteMode(store, part);
|
|
102
|
+
|
|
103
|
+
if (partType && partType.value === menuType.value) {
|
|
104
|
+
items.push({ type: 'submenu', id, name: label, requiresWrite, children: parseMenuItems(store, part) });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (partType && partType.value === componentType.value) {
|
|
109
|
+
const { tag, params } = rdfComponent(store, part);
|
|
110
|
+
items.push({ type: 'component', id, name: label, icon, requiresWrite, tag, params });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const href = rdfVal(store, part, 'href');
|
|
115
|
+
const contents = rdfVal(store, part, 'contents');
|
|
116
|
+
items.push({ type: 'link', id, name: label, icon, requiresWrite, href, contents });
|
|
117
|
+
}
|
|
118
|
+
return items;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve `uri` (optionally relative to `baseUri`), fetch the RDF doc,
|
|
123
|
+
* locate the menu root (by fragment or by ui:Menu type), and parse it.
|
|
124
|
+
*
|
|
125
|
+
* @returns {Promise<null | { items, orientation }>}
|
|
126
|
+
* `null` if no ui:Menu is found in the document.
|
|
127
|
+
*/
|
|
128
|
+
export async function loadMenuFromUri(uri, baseUri = null) {
|
|
129
|
+
let docUrl, fragment;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = new URL(uri, baseUri || undefined);
|
|
132
|
+
fragment = parsed.hash.slice(1);
|
|
133
|
+
parsed.hash = '';
|
|
134
|
+
docUrl = parsed.href;
|
|
135
|
+
} catch {
|
|
136
|
+
docUrl = uri;
|
|
137
|
+
fragment = '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const store = await loadRdfStore(docUrl);
|
|
141
|
+
let root;
|
|
142
|
+
if (fragment) {
|
|
143
|
+
root = rdf.sym(docUrl + '#' + fragment);
|
|
144
|
+
} else {
|
|
145
|
+
const menuType = rdf.sym(UI + 'Menu');
|
|
146
|
+
const typeNode = rdf.sym(RDF + 'type');
|
|
147
|
+
root = store.each(null, typeNode, menuType)[0];
|
|
148
|
+
}
|
|
149
|
+
if (!root) return null;
|
|
150
|
+
|
|
151
|
+
const orientation = orientationToken(rdfVal(store, root, 'orientation')) || 'horizontal';
|
|
152
|
+
const items = parseMenuItems(store, root);
|
|
153
|
+
return { items, orientation };
|
|
154
|
+
}
|
package/core/pod-ops.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pod operations — container listing, file operations, discovery.
|
|
3
|
+
* Shared utility used by <sol-pod>.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { rdf } from './rdf.js';
|
|
7
|
+
|
|
8
|
+
const LDP_CONTAINS = 'http://www.w3.org/ns/ldp#contains';
|
|
9
|
+
const OWL_SAME_AS = 'http://www.w3.org/2002/07/owl#sameAs';
|
|
10
|
+
const PIM_STORAGE = 'http://www.w3.org/ns/pim/space#storage';
|
|
11
|
+
// Per-resource metadata predicates emitted by Solid LDP servers in
|
|
12
|
+
// container listings. POSIX_* is the canonical pair (CSS, and NSS via
|
|
13
|
+
// the same URI even though it imports as `stat:`). DCT_MODIFIED is
|
|
14
|
+
// the human-readable ISO-datetime mirror.
|
|
15
|
+
const POSIX_SIZE = 'http://www.w3.org/ns/posix/stat#size';
|
|
16
|
+
const POSIX_MTIME = 'http://www.w3.org/ns/posix/stat#mtime';
|
|
17
|
+
const DCT_MODIFIED = 'http://purl.org/dc/terms/modified';
|
|
18
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
19
|
+
|
|
20
|
+
// ── MIME types ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export const MIME_TYPES = {
|
|
23
|
+
ttl:'text/turtle', n3:'text/n3', jsonld:'application/ld+json',
|
|
24
|
+
json:'application/json', html:'text/html', xml:'application/xml',
|
|
25
|
+
txt:'text/plain', md:'text/markdown', csv:'text/csv',
|
|
26
|
+
png:'image/png', jpg:'image/jpeg', jpeg:'image/jpeg', gif:'image/gif',
|
|
27
|
+
svg:'image/svg+xml', pdf:'application/pdf',
|
|
28
|
+
js:'application/javascript', css:'text/css',
|
|
29
|
+
acl:'text/turtle',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Extension extraction supporting $.ext convention. */
|
|
33
|
+
export function extOf(name) {
|
|
34
|
+
const m = name.match(/\$\.([^.]+)$/);
|
|
35
|
+
if (m) return m[1].toLowerCase();
|
|
36
|
+
return name.includes('.') ? name.split('.').pop().toLowerCase() : '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function contentTypeFor(name, blobType) {
|
|
40
|
+
if (blobType) return blobType;
|
|
41
|
+
return MIME_TYPES[extOf(name)] || 'application/octet-stream';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Fetch with timeout ────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function withTimeout(fetchFn, ms = 30000) {
|
|
47
|
+
return async (url, options = {}) => {
|
|
48
|
+
const ctrl = new AbortController();
|
|
49
|
+
const id = setTimeout(() => ctrl.abort(), ms);
|
|
50
|
+
try {
|
|
51
|
+
return await fetchFn(url, { ...options, signal: ctrl.signal });
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (e.name === 'AbortError') throw new Error(`Request timed out after ${ms / 1000}s: ${url}`);
|
|
54
|
+
throw e;
|
|
55
|
+
} finally { clearTimeout(id); }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Container listing ─────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async function fetchContainerRaw(url, fetchFn) {
|
|
62
|
+
const timedFetch = withTimeout(fetchFn, 30000);
|
|
63
|
+
const resp = await timedFetch(url, { headers: { Accept: 'text/turtle' } });
|
|
64
|
+
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
|
|
65
|
+
const text = await resp.text();
|
|
66
|
+
|
|
67
|
+
const store = rdf.graph();
|
|
68
|
+
rdf.parse(text, store, url, 'text/turtle');
|
|
69
|
+
const container = rdf.sym(url);
|
|
70
|
+
return store.each(container, rdf.sym(LDP_CONTAINS), null, null).map(n => {
|
|
71
|
+
const sub = rdf.sym(n.value);
|
|
72
|
+
const num = (pred) => {
|
|
73
|
+
const v = store.any(sub, rdf.sym(pred), null, null);
|
|
74
|
+
if (!v) return null;
|
|
75
|
+
const x = Number(v.value);
|
|
76
|
+
return Number.isFinite(x) ? x : null;
|
|
77
|
+
};
|
|
78
|
+
const str = (pred) => {
|
|
79
|
+
const v = store.any(sub, rdf.sym(pred), null, null);
|
|
80
|
+
return v ? v.value : null;
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
url: n.value,
|
|
84
|
+
size: num(POSIX_SIZE),
|
|
85
|
+
mtime: num(POSIX_MTIME),
|
|
86
|
+
modified: str(DCT_MODIFIED),
|
|
87
|
+
types: store.each(sub, rdf.sym(RDF_TYPE), null, null).map(t => t.value),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tryDecode(s) {
|
|
93
|
+
try { return decodeURIComponent(s); } catch { return s; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mapResources(resources) {
|
|
97
|
+
return resources.map(r => {
|
|
98
|
+
const url = r.url;
|
|
99
|
+
const isContainer = url.endsWith('/');
|
|
100
|
+
const name = isContainer ? url.split('/').slice(-2)[0] : url.split('/').pop();
|
|
101
|
+
const displayName = tryDecode(name);
|
|
102
|
+
// Inferred from the extension (containers are turtle). It's a guess —
|
|
103
|
+
// an extensionless file is 'application/octet-stream'; HEAD the url for
|
|
104
|
+
// the server's authoritative Content-Type.
|
|
105
|
+
const contentType = isContainer ? 'text/turtle' : contentTypeFor(name);
|
|
106
|
+
return {
|
|
107
|
+
url, name, displayName, isContainer, contentType,
|
|
108
|
+
size: r.size, mtime: r.mtime, modified: r.modified, types: r.types,
|
|
109
|
+
};
|
|
110
|
+
}).sort((a, b) => {
|
|
111
|
+
if (a.isContainer === b.isContainer) return a.displayName.localeCompare(b.displayName);
|
|
112
|
+
return a.isContainer ? -1 : 1;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function fetchContainer(url, fetchFn) {
|
|
117
|
+
const resources = await fetchContainerRaw(url, fetchFn);
|
|
118
|
+
return mapResources(resources);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── File operations ───────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export async function copyFile(sourceUrl, targetContainerUrl, fileName, sourceFetchFn, targetFetchFn) {
|
|
124
|
+
const sf = withTimeout(sourceFetchFn, 60000);
|
|
125
|
+
const resp = await sf(sourceUrl);
|
|
126
|
+
if (!resp.ok) throw new Error(`GET ${sourceUrl} failed: ${resp.status}`);
|
|
127
|
+
const blob = await resp.blob();
|
|
128
|
+
|
|
129
|
+
const targetUrl = targetContainerUrl + fileName;
|
|
130
|
+
const tf = withTimeout(targetFetchFn, 60000);
|
|
131
|
+
const put = await tf(targetUrl, {
|
|
132
|
+
method: 'PUT',
|
|
133
|
+
headers: { 'Content-Type': contentTypeFor(fileName, blob.type) },
|
|
134
|
+
body: blob
|
|
135
|
+
});
|
|
136
|
+
if (!put.ok) {
|
|
137
|
+
const err = new Error(`PUT failed: ${put.status}`);
|
|
138
|
+
err.needsAuth = put.status === 401 || put.status === 403;
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
return { success: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function copyFolder(sourceUrl, targetContainerUrl, folderName, fetchFnForUrl, onProgress) {
|
|
145
|
+
const targetFolderUrl = targetContainerUrl + folderName + '/';
|
|
146
|
+
if (onProgress) onProgress(`Copying folder ${folderName}...`);
|
|
147
|
+
|
|
148
|
+
const sourceFetch = fetchFnForUrl(sourceUrl);
|
|
149
|
+
let children;
|
|
150
|
+
try {
|
|
151
|
+
children = await fetchContainerRaw(sourceUrl, sourceFetch);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return { success: false, error: e.message };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create target folder
|
|
157
|
+
const tf = withTimeout(fetchFnForUrl(targetContainerUrl), 30000);
|
|
158
|
+
const mkdir = await tf(targetFolderUrl, {
|
|
159
|
+
method: 'PUT', headers: { 'Content-Type': 'text/turtle' }, body: ''
|
|
160
|
+
});
|
|
161
|
+
if (!mkdir.ok && mkdir.status !== 409) {
|
|
162
|
+
return { success: false, error: `Failed to create folder: ${mkdir.status}` };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let failed = 0;
|
|
166
|
+
for (const child of children) {
|
|
167
|
+
const childUrl = child.url;
|
|
168
|
+
const isContainer = childUrl.endsWith('/');
|
|
169
|
+
const name = isContainer ? childUrl.split('/').slice(-2)[0] : childUrl.split('/').pop();
|
|
170
|
+
if (isContainer) {
|
|
171
|
+
const r = await copyFolder(childUrl, targetFolderUrl, name, fetchFnForUrl, onProgress);
|
|
172
|
+
if (!r.success) failed++;
|
|
173
|
+
} else {
|
|
174
|
+
if (onProgress) onProgress(`Copying ${name}...`);
|
|
175
|
+
try {
|
|
176
|
+
await copyFile(childUrl, targetFolderUrl, name, fetchFnForUrl(childUrl), fetchFnForUrl(targetFolderUrl));
|
|
177
|
+
} catch (e) { failed++; }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { success: failed === 0, failed };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function deleteFolder(url, fetchFnForUrl) {
|
|
184
|
+
let children = [];
|
|
185
|
+
try { children = await fetchContainer(url, fetchFnForUrl(url)); } catch (e) {}
|
|
186
|
+
for (const child of children) {
|
|
187
|
+
if (child.isContainer) {
|
|
188
|
+
await deleteFolder(child.url, fetchFnForUrl);
|
|
189
|
+
} else {
|
|
190
|
+
await fetchFnForUrl(child.url)(child.url, { method: 'DELETE' });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await fetchFnForUrl(url)(url, { method: 'DELETE' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// ── WebID / storage discovery ─────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
export async function discoverOwnerWebIds(origin) {
|
|
200
|
+
origin = origin || window.location.origin;
|
|
201
|
+
const webIds = new Set();
|
|
202
|
+
|
|
203
|
+
const addFromTurtle = (text, base) => {
|
|
204
|
+
let m, re;
|
|
205
|
+
re = /<([^>]+)>\s+(?:solid:account|<[^>]*solid[^>]*#account>)\s+<[^>]+>/g;
|
|
206
|
+
while ((m = re.exec(text)) !== null) webIds.add(new URL(m[1], base).href);
|
|
207
|
+
re = /(?:solid:owner|<[^>]*solid[^>]*#owner>)\s+<([^>]+)>/g;
|
|
208
|
+
while ((m = re.exec(text)) !== null) webIds.add(new URL(m[1], base).href);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const addFromAcl = (text, base) => {
|
|
212
|
+
const blocks = text.split(/\n\s*\n/);
|
|
213
|
+
for (const block of blocks) {
|
|
214
|
+
if (/acl:Control/.test(block)) {
|
|
215
|
+
const re = /acl:agent\s+<([^>]+)>/g;
|
|
216
|
+
let m;
|
|
217
|
+
while ((m = re.exec(block)) !== null) webIds.add(new URL(m[1], base).href);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let metaUrl = origin + '/.meta';
|
|
223
|
+
let aclUrl = origin + '/.acl';
|
|
224
|
+
try {
|
|
225
|
+
const resp = await fetch(origin + '/');
|
|
226
|
+
const link = resp.headers.get('Link') || '';
|
|
227
|
+
const mm = link.match(/<([^>]+)>\s*;\s*rel="describedby"/);
|
|
228
|
+
if (mm) metaUrl = new URL(mm[1], origin).href;
|
|
229
|
+
const am = link.match(/<([^>]+)>\s*;\s*rel="acl"/);
|
|
230
|
+
if (am) aclUrl = new URL(am[1], origin).href;
|
|
231
|
+
} catch (e) {}
|
|
232
|
+
|
|
233
|
+
if (webIds.size === 0) {
|
|
234
|
+
try {
|
|
235
|
+
const resp = await fetch(metaUrl, { headers: { Accept: 'text/turtle' } });
|
|
236
|
+
if (resp.ok) addFromTurtle(await resp.text(), metaUrl);
|
|
237
|
+
} catch (e) {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (webIds.size === 0) {
|
|
241
|
+
try {
|
|
242
|
+
const resp = await fetch(origin + '/.well-known/solid', {
|
|
243
|
+
headers: { Accept: 'text/turtle, application/ld+json' }
|
|
244
|
+
});
|
|
245
|
+
if (resp.ok) {
|
|
246
|
+
const text = await resp.text();
|
|
247
|
+
addFromTurtle(text, origin + '/.well-known/solid');
|
|
248
|
+
if (webIds.size === 0) {
|
|
249
|
+
try {
|
|
250
|
+
const json = JSON.parse(text);
|
|
251
|
+
for (const key of ['http://www.w3.org/ns/solid/terms#owner', 'http://www.w3.org/ns/solid/terms#account']) {
|
|
252
|
+
const val = json[key];
|
|
253
|
+
if (Array.isArray(val)) val.forEach(v => webIds.add(v['@id'] || v));
|
|
254
|
+
else if (val) webIds.add(val['@id'] || val);
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (webIds.size === 0) {
|
|
263
|
+
try {
|
|
264
|
+
const resp = await fetch(aclUrl, { headers: { Accept: 'text/turtle' } });
|
|
265
|
+
if (resp.ok) addFromAcl(await resp.text(), aclUrl);
|
|
266
|
+
} catch (e) {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return [...webIds];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function getStoragesFromWebIds(webIds) {
|
|
273
|
+
const storages = new Set();
|
|
274
|
+
const visited = new Set();
|
|
275
|
+
|
|
276
|
+
async function processWebId(webId) {
|
|
277
|
+
if (visited.has(webId)) return;
|
|
278
|
+
visited.add(webId);
|
|
279
|
+
const profileDoc = webId.split('#')[0];
|
|
280
|
+
try {
|
|
281
|
+
const store = rdf.graph();
|
|
282
|
+
const fetcher = rdf.fetcher(store);
|
|
283
|
+
await fetcher.load(profileDoc);
|
|
284
|
+
const subj = rdf.sym(webId);
|
|
285
|
+
const found = store.each(subj, rdf.sym(PIM_STORAGE), null, null).map(n => n.value);
|
|
286
|
+
found.forEach(u => storages.add(u));
|
|
287
|
+
// owl:sameAs is symmetric — follow it in BOTH directions so a
|
|
288
|
+
// profile that asserts `<remote> owl:sameAs <local>` chains the
|
|
289
|
+
// same as one that asserts `<local> owl:sameAs <remote>`.
|
|
290
|
+
const forward = store.each(subj, rdf.sym(OWL_SAME_AS), null, null);
|
|
291
|
+
const inverse = store.each(null, rdf.sym(OWL_SAME_AS), subj, null);
|
|
292
|
+
const linked = [...forward, ...inverse].map(n => n.value);
|
|
293
|
+
console.info(`[pod-ops] ${webId} → ${found.length} storage(s), ${linked.length} sameAs link(s)`,
|
|
294
|
+
{ storages: found, sameAs: linked });
|
|
295
|
+
for (const next of linked) await processWebId(next);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn(`[pod-ops] Failed to load profile ${profileDoc} for ${webId}:`, e?.message || e);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const webId of webIds) await processWebId(webId);
|
|
302
|
+
console.info(`[pod-ops] discovery: ${visited.size} WebID(s) walked, ${storages.size} pod(s) found`,
|
|
303
|
+
{ webIds: [...visited], storages: [...storages] });
|
|
304
|
+
return [...storages].sort();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── File-type classification ─────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
const TEXT_VIEWABLE = ['txt','md','csv','json','jsonld','ttl','n3','html','xml','svg','js','css'];
|
|
310
|
+
const EDITABLE = ['txt','md','csv','json','jsonld','ttl','n3','html','htm','xml','svg','js','css'];
|
|
311
|
+
const IMAGE_TYPES = ['png','jpg','jpeg','gif','webp','svg','bmp','ico','avif'];
|
|
312
|
+
const VIDEO_TYPES = ['mp4','webm','ogg','mov','m4v'];
|
|
313
|
+
const AUDIO_TYPES = ['mp3','ogg','wav','flac','aac','m4a','opus'];
|
|
314
|
+
const PDF_TYPES = ['pdf'];
|
|
315
|
+
const RDF_EXTS = ['ttl','n3','trig','nq','nt','rdf','jsonld'];
|
|
316
|
+
|
|
317
|
+
export const isTextViewable = n => TEXT_VIEWABLE.includes(extOf(n));
|
|
318
|
+
export const isEditable = n => EDITABLE.includes(extOf(n));
|
|
319
|
+
export const isImage = n => IMAGE_TYPES.includes(extOf(n));
|
|
320
|
+
export const isVideo = n => VIDEO_TYPES.includes(extOf(n));
|
|
321
|
+
export const isAudio = n => AUDIO_TYPES.includes(extOf(n));
|
|
322
|
+
export const isPDF = n => PDF_TYPES.includes(extOf(n));
|
|
323
|
+
export const isViewable = n => isTextViewable(n) || isImage(n) || isVideo(n) || isAudio(n) || isPDF(n);
|
|
324
|
+
export const isRdf = n => RDF_EXTS.includes(extOf(n));
|
|
325
|
+
|
|
326
|
+
export const CT_TO_EXT = {
|
|
327
|
+
'text/turtle':'ttl','text/n3':'n3','application/ld+json':'jsonld',
|
|
328
|
+
'application/json':'json','text/html':'html','text/plain':'txt',
|
|
329
|
+
'text/markdown':'md','text/csv':'csv','application/xml':'xml',
|
|
330
|
+
'text/xml':'xml','application/javascript':'js','text/css':'css',
|
|
331
|
+
'image/png':'png','image/jpeg':'jpg','image/svg+xml':'svg',
|
|
332
|
+
'audio/mpeg':'mp3','video/mp4':'mp4','application/pdf':'pdf',
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export function fileIcon(name) {
|
|
336
|
+
const ext = extOf(name);
|
|
337
|
+
if (!ext && name.endsWith('$')) return '\u{1F517}';
|
|
338
|
+
if (['ttl','n3','trig','nq','nt','rdf','jsonld'].includes(ext)) return '\u{1F537}';
|
|
339
|
+
if (ext === 'json') return '\u{1F4CB}';
|
|
340
|
+
if (['csv','tsv'].includes(ext)) return '\u{1F4CA}';
|
|
341
|
+
if (['md','markdown'].includes(ext)) return '\u{1F4DD}';
|
|
342
|
+
if (ext === 'txt') return '\u{1F4C4}';
|
|
343
|
+
if (ext === 'pdf') return '\u{1F4D5}';
|
|
344
|
+
if (['html','htm'].includes(ext)) return '\u{1F310}';
|
|
345
|
+
if (['js','mjs','cjs','ts'].includes(ext)) return '\u26A1';
|
|
346
|
+
if (['css','scss'].includes(ext)) return '\u{1F3A8}';
|
|
347
|
+
if (['png','jpg','jpeg','gif','webp','avif','bmp','ico'].includes(ext)) return '\u{1F5BC}';
|
|
348
|
+
if (ext === 'svg') return '\u{1F3AD}';
|
|
349
|
+
if (['mp3','ogg','wav','flac','aac','m4a','opus'].includes(ext)) return '\u{1F3B5}';
|
|
350
|
+
if (['mp4','webm','mov','m4v'].includes(ext)) return '\u{1F3AC}';
|
|
351
|
+
if (['zip','tar','gz','bz2','xz','7z'].includes(ext)) return '\u{1F4E6}';
|
|
352
|
+
if (['yaml','yml','toml','ini','env'].includes(ext)) return '\u2699';
|
|
353
|
+
if (ext === 'acl') return '\u{1F512}';
|
|
354
|
+
if (name.startsWith('.')) return '\u{1F527}';
|
|
355
|
+
return '\u{1F4C4}';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── Live-editor format detection ──────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
const EXT_TO_LIVE_FORMAT = {
|
|
361
|
+
ttl:'turtle', n3:'turtle', jsonld:'jsonld', csv:'csv', tsv:'csv',
|
|
362
|
+
md:'markdown', markdown:'markdown', mmd:'mermaid', mermaid:'mermaid',
|
|
363
|
+
html:'html', htm:'html', dot:'graphviz', gv:'graphviz',
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const MIME_TO_LIVE_FORMAT = {
|
|
367
|
+
'text/turtle':'turtle', 'text/n3':'turtle',
|
|
368
|
+
'application/ld+json':'jsonld',
|
|
369
|
+
'text/csv':'csv', 'text/tab-separated-values':'csv',
|
|
370
|
+
'text/markdown':'markdown', 'text/x-markdown':'markdown',
|
|
371
|
+
'text/html':'html',
|
|
372
|
+
'text/x-mermaid':'mermaid',
|
|
373
|
+
'text/vnd.graphviz':'graphviz', 'text/x-dot':'graphviz',
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export function liveFormatFor(url, mime) {
|
|
377
|
+
if (mime) {
|
|
378
|
+
const f = MIME_TO_LIVE_FORMAT[mime.split(';')[0].trim()];
|
|
379
|
+
if (f) return f;
|
|
380
|
+
}
|
|
381
|
+
if (url) {
|
|
382
|
+
const ext = url.split('?')[0].split('.').pop().toLowerCase();
|
|
383
|
+
const f = EXT_TO_LIVE_FORMAT[ext];
|
|
384
|
+
if (f) return f;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function isLiveFormat(url, mime) {
|
|
390
|
+
return !!liveFormatFor(url, mime);
|
|
391
|
+
}
|
|
392
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/pod-registry.js — a group-keyed registry of known pod storage URLs.
|
|
3
|
+
*
|
|
4
|
+
* Several <sol-pod>s that share a `pods-group` (or all use the default
|
|
5
|
+
* group) draw from one list, so discovering or adding a pod in one
|
|
6
|
+
* surfaces it in every sibling's selector.
|
|
7
|
+
*
|
|
8
|
+
* The registry is in-memory only. Persistence is left to the host —
|
|
9
|
+
* see <sol-pod>'s seedPods() method and sol-pod-pods-changed event.
|
|
10
|
+
*
|
|
11
|
+
* The reserved group key 'none' yields a fresh, unshared registry on
|
|
12
|
+
* every call — for a <sol-pod> that must stand entirely alone.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const NONE_GROUP = 'none';
|
|
16
|
+
const DEFAULT_GROUP = '__default__';
|
|
17
|
+
|
|
18
|
+
/** Normalise a pod URL to a trailing-slash form, or null if unusable. */
|
|
19
|
+
function normalize(url) {
|
|
20
|
+
if (typeof url !== 'string') return null;
|
|
21
|
+
const u = url.trim();
|
|
22
|
+
if (!u) return null;
|
|
23
|
+
return u.endsWith('/') ? u : u + '/';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class PodRegistry {
|
|
27
|
+
constructor() {
|
|
28
|
+
this._pods = new Set();
|
|
29
|
+
this._subs = new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Snapshot of known pod URLs, in insertion order. */
|
|
33
|
+
list() { return [...this._pods]; }
|
|
34
|
+
|
|
35
|
+
subscribe(fn) { if (typeof fn === 'function') this._subs.add(fn); }
|
|
36
|
+
unsubscribe(fn) { this._subs.delete(fn); }
|
|
37
|
+
|
|
38
|
+
/** Add one URL. See addAll. */
|
|
39
|
+
add(url, opts) { return this.addAll([url], opts); }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add URLs to the registry. Returns true if anything new was added.
|
|
43
|
+
* Subscribers are notified on a change and passed (snapshot, silent);
|
|
44
|
+
* `silent` (default false) is for host-driven seeding that should not
|
|
45
|
+
* echo back out as a persist-worthy change.
|
|
46
|
+
*/
|
|
47
|
+
addAll(urls, { silent = false } = {}) {
|
|
48
|
+
let changed = false;
|
|
49
|
+
for (const raw of urls || []) {
|
|
50
|
+
const u = normalize(raw);
|
|
51
|
+
if (u && !this._pods.has(u)) { this._pods.add(u); changed = true; }
|
|
52
|
+
}
|
|
53
|
+
if (changed) this._notify(silent);
|
|
54
|
+
return changed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_notify(silent) {
|
|
58
|
+
const snapshot = this.list();
|
|
59
|
+
for (const fn of this._subs) {
|
|
60
|
+
// A misbehaving subscriber must not stop the others.
|
|
61
|
+
try { fn(snapshot, silent); } catch (e) { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const registries = new Map();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The shared registry for a group key. `'none'` (reserved) returns a
|
|
70
|
+
* brand-new unshared registry every call; any other key — or none —
|
|
71
|
+
* returns the one persistent registry for that group.
|
|
72
|
+
*/
|
|
73
|
+
export function getRegistry(group) {
|
|
74
|
+
if (group === NONE_GROUP) return new PodRegistry();
|
|
75
|
+
const key = group || DEFAULT_GROUP;
|
|
76
|
+
let reg = registries.get(key);
|
|
77
|
+
if (!reg) { reg = new PodRegistry(); registries.set(key, reg); }
|
|
78
|
+
return reg;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Test helper — drop every shared registry. */
|
|
82
|
+
export function _resetRegistries() { registries.clear(); }
|