|
| 1 | +// jspod folder-aware data browser. JSS embeds the resource as JSON-LD |
| 2 | +// in #dataisland; this script: |
| 3 | +// 1. If the resource is an LDP container, render breadcrumb + table |
| 4 | +// (NAME / TYPE / SIZE / MODIFIED) with folder-first sorting and |
| 5 | +// friendly type labels. |
| 6 | +// 2. Otherwise, fall back to a pretty-printed JSON-LD dump with |
| 7 | +// clickable URIs (same shape as data-browser.js). |
| 8 | +// |
| 9 | +// Single file, no build, no framework. Container-parsing logic mirrors |
| 10 | +// solid-apps/chrome's Files pane (the proven version). |
| 11 | + |
| 12 | +document.head.insertAdjacentHTML('beforeend', `<style> |
| 13 | +body{font:14px/1.55 system-ui,-apple-system,sans-serif;margin:0;color:#222;background:#f3eee5} |
| 14 | +.db{max-width:880px;margin:2em auto;padding:0 1em} |
| 15 | +.db-nav{display:flex;gap:1.2em;margin:0 0 .75em;padding:.6em 1em;background:#fff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.05);font-size:.9em} |
| 16 | +.db-card{background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.08);padding:1.5em 2em} |
| 17 | +.db-bc{display:flex;align-items:center;flex-wrap:wrap;gap:.25em;font-size:.95em;margin-bottom:.5em} |
| 18 | +.db-bc a{color:#7a4ed8;font-weight:500} |
| 19 | +.db-bc .sep{color:#888} |
| 20 | +.db-bc .here{color:#222;font-weight:600} |
| 21 | +.db-title{margin:.2em 0 .15em;font-size:1.6em;font-weight:500} |
| 22 | +.db-url{color:#888;font-size:.85em;font-family:"SFMono-Regular",Consolas,monospace;margin-bottom:.6em;word-break:break-all} |
| 23 | +.db-count{color:#666;font-size:.9em;margin:.6em 0 1em} |
| 24 | +.db-table{width:100%;border-collapse:collapse} |
| 25 | +.db-table th{text-align:left;padding:9px 8px;font-weight:600;font-size:11px;color:#666;text-transform:uppercase;letter-spacing:.06em;border-bottom:2px solid #7a4ed8} |
| 26 | +.db-table td{padding:11px 8px;border-bottom:1px solid #eee;font-size:14px} |
| 27 | +.db-table tr:hover td{background:#faf7f0} |
| 28 | +.db-name{display:flex;align-items:center;gap:.6em} |
| 29 | +.db-name a{color:#7a4ed8;text-decoration:none} |
| 30 | +.db-name a:hover{text-decoration:underline} |
| 31 | +.db-icon{font-size:18px;width:24px;text-align:center;flex-shrink:0} |
| 32 | +.db-meta{color:#666;white-space:nowrap;font-size:13px} |
| 33 | +.db-num{text-align:right} |
| 34 | +.db-empty{padding:2em;text-align:center;color:#888} |
| 35 | +.db-pre{padding:1.5em;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.08);overflow:auto;white-space:pre-wrap;word-break:break-all;margin:0} |
| 36 | +.db-pre a{color:#0a66c2;text-decoration:none} |
| 37 | +.db-pre a:hover{text-decoration:underline} |
| 38 | +</style>`); |
| 39 | + |
| 40 | +(function () { |
| 41 | + const here = window.location.pathname; |
| 42 | + const up = (here === '/' || !here) |
| 43 | + ? null |
| 44 | + : (s => { const i = s.lastIndexOf('/'); return i === 0 ? '/' : s.slice(0, i) + '/'; })(here.replace(/\/$/, '')); |
| 45 | + const navHTML = `<nav class="db-nav">${ |
| 46 | + up ? `<a href="${up}" title="One level up">↑ Up</a>` : '' |
| 47 | + }<a href="/">Home</a><a href="/account.html">Account</a><a href="/docs.html">Docs</a></nav>`; |
| 48 | + |
| 49 | + let doc = null; |
| 50 | + try { doc = JSON.parse(document.getElementById('dataisland').textContent); } catch (e) {} |
| 51 | + |
| 52 | + const items = doc ? parseContainer(doc, window.location.href) : null; |
| 53 | + const isContainer = Array.isArray(items); |
| 54 | + |
| 55 | + const target = document.getElementById('mashlib'); |
| 56 | + if (isContainer) { |
| 57 | + target.innerHTML = `<div class="db">${navHTML}<div class="db-card">${renderFolder(items, here)}</div></div>`; |
| 58 | + } else { |
| 59 | + target.innerHTML = `<div class="db">${navHTML}${renderJson(doc)}</div>`; |
| 60 | + } |
| 61 | + |
| 62 | + function renderFolder(items, path) { |
| 63 | + const folders = items.filter(i => i.type === 'container').length; |
| 64 | + const files = items.length - folders; |
| 65 | + items.sort((a, b) => { |
| 66 | + if (a.type !== b.type) return a.type === 'container' ? -1 : 1; |
| 67 | + return nameOf(a.url).localeCompare(nameOf(b.url)); |
| 68 | + }); |
| 69 | + |
| 70 | + const segments = path.replace(/\/$/, '').split('/').filter(Boolean); |
| 71 | + const here = segments[segments.length - 1] || window.location.host; |
| 72 | + let acc = '/'; |
| 73 | + const crumbs = [`<a href="/">${escape(window.location.host)}</a>`].concat( |
| 74 | + segments.map((s, i) => { |
| 75 | + acc += s + '/'; |
| 76 | + return i === segments.length - 1 |
| 77 | + ? `<span class="sep">/</span><span class="here">${escape(decodeURIComponent(s))}</span>` |
| 78 | + : `<span class="sep">/</span><a href="${escape(acc)}">${escape(decodeURIComponent(s))}</a>`; |
| 79 | + }) |
| 80 | + ).join(''); |
| 81 | + |
| 82 | + const rows = items.length |
| 83 | + ? `<table class="db-table"> |
| 84 | + <thead><tr><th>NAME</th><th>TYPE</th><th class="db-num">SIZE</th><th>MODIFIED</th></tr></thead> |
| 85 | + <tbody>${items.map(rowHTML).join('')}</tbody> |
| 86 | + </table>` |
| 87 | + : `<div class="db-empty">Empty container</div>`; |
| 88 | + |
| 89 | + return ` |
| 90 | + <div class="db-bc">${crumbs}</div> |
| 91 | + <div class="db-title">${escape(decodeURIComponent(here))}</div> |
| 92 | + <div class="db-url">${escape(window.location.href)}</div> |
| 93 | + <div class="db-count">${folders} folder${folders === 1 ? '' : 's'}, ${files} file${files === 1 ? '' : 's'}</div> |
| 94 | + ${rows} |
| 95 | + `; |
| 96 | + } |
| 97 | + |
| 98 | + function rowHTML(it) { |
| 99 | + const name = nameOf(it.url); |
| 100 | + const tl = typeLabel(it); |
| 101 | + const icon = iconFor(it); |
| 102 | + return `<tr> |
| 103 | + <td class="db-name"><span class="db-icon">${icon}</span><a href="${escape(it.url)}">${escape(name)}${it.type === 'container' ? '/' : ''}</a></td> |
| 104 | + <td class="db-meta">${escape(tl)}</td> |
| 105 | + <td class="db-meta db-num">${it.size != null ? fmtBytes(it.size) : '—'}</td> |
| 106 | + <td class="db-meta">${it.modified ? fmtDate(it.modified) : '—'}</td> |
| 107 | + </tr>`; |
| 108 | + } |
| 109 | + |
| 110 | + function renderJson(d) { |
| 111 | + if (!d) return `<div class="db-pre">No data.</div>`; |
| 112 | + const pretty = JSON.stringify(d, null, 2) |
| 113 | + .replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c])) |
| 114 | + .replace(/https?:\/\/[^"\s]+/g, '<a href="$&">$&</a>'); |
| 115 | + return `<pre class="db-pre">${pretty}</pre>`; |
| 116 | + } |
| 117 | + |
| 118 | + // ---- container parsing (JSON-LD with ldp:contains) ---- |
| 119 | + |
| 120 | + function parseContainer(doc, baseUrl) { |
| 121 | + const nodes = Array.isArray(doc?.['@graph']) ? doc['@graph'] : [doc]; |
| 122 | + const container = nodes.find(n => |
| 123 | + n['@id'] === baseUrl || |
| 124 | + (typeof n['@id'] === 'string' && resolveUrl(n['@id'], baseUrl) === baseUrl) |
| 125 | + ) || nodes[0]; |
| 126 | + if (!container) return null; |
| 127 | + const raw = container['contains'] |
| 128 | + ?? container['ldp:contains'] |
| 129 | + ?? container['http://www.w3.org/ns/ldp#contains']; |
| 130 | + if (raw == null) return null; |
| 131 | + const arr = Array.isArray(raw) ? raw : [raw]; |
| 132 | + return arr.map(c => { |
| 133 | + if (typeof c === 'string') return { url: resolveUrl(c, baseUrl), type: c.endsWith('/') ? 'container' : 'resource' }; |
| 134 | + const url = resolveUrl(c['@id'], baseUrl); |
| 135 | + const types = [].concat(c['@type'] || []); |
| 136 | + const isC = types.some(t => /(?:#|\/)(?:BasicContainer|Container)$/.test(t)) || url.endsWith('/'); |
| 137 | + return { |
| 138 | + url, |
| 139 | + type: isC ? 'container' : 'resource', |
| 140 | + size: c['stat:size'] ?? c['http://www.w3.org/ns/posix/stat#size'], |
| 141 | + modified: c['dcterms:modified'] ?? c['http://purl.org/dc/terms/modified'] ?? c['dc:modified'], |
| 142 | + }; |
| 143 | + }).filter(x => x.url && x.url !== baseUrl); |
| 144 | + } |
| 145 | + |
| 146 | + // ---- formatting helpers ---- |
| 147 | + |
| 148 | + function nameOf(url) { |
| 149 | + return decodeURIComponent(url.replace(/\/$/, '').split('/').pop() || url); |
| 150 | + } |
| 151 | + function typeLabel(it) { |
| 152 | + if (it.type === 'container') return 'Folder'; |
| 153 | + const ext = nameOf(it.url).split('.').pop()?.toLowerCase() || ''; |
| 154 | + const map = { |
| 155 | + html: 'HTML', htm: 'HTML', |
| 156 | + md: 'Markdown', markdown: 'Markdown', |
| 157 | + json: 'JSON', jsonld: 'JSON-LD', |
| 158 | + ttl: 'Turtle', n3: 'N3', rdf: 'RDF', |
| 159 | + css: 'CSS', js: 'JavaScript', ts: 'TypeScript', |
| 160 | + png: 'Image', jpg: 'Image', jpeg: 'Image', gif: 'Image', webp: 'Image', svg: 'SVG', avif: 'Image', |
| 161 | + mp3: 'Audio', ogg: 'Audio', wav: 'Audio', flac: 'Audio', |
| 162 | + mp4: 'Video', webm: 'Video', mov: 'Video', |
| 163 | + pdf: 'PDF', |
| 164 | + txt: 'Text', text: 'Text', |
| 165 | + acl: 'ACL', |
| 166 | + }; |
| 167 | + return map[ext] || (ext ? ext.toUpperCase() : '—'); |
| 168 | + } |
| 169 | + function iconFor(it) { |
| 170 | + if (it.type === 'container') return '📁'; |
| 171 | + const ext = nameOf(it.url).split('.').pop()?.toLowerCase() || ''; |
| 172 | + if (/^(html|htm)$/.test(ext)) return '🌐'; |
| 173 | + if (/^(json|jsonld|ttl|n3|rdf)$/.test(ext)) return '🔗'; |
| 174 | + if (/^(md|markdown|txt|text)$/.test(ext)) return '📝'; |
| 175 | + if (/^(png|jpg|jpeg|gif|webp|svg|avif)$/.test(ext)) return '🖼'; |
| 176 | + if (/^(mp3|ogg|wav|flac)$/.test(ext)) return '🎵'; |
| 177 | + if (/^(mp4|webm|mov)$/.test(ext)) return '🎬'; |
| 178 | + if (ext === 'pdf') return '📕'; |
| 179 | + if (ext === 'acl') return '🔒'; |
| 180 | + return '📄'; |
| 181 | + } |
| 182 | + function fmtBytes(n) { |
| 183 | + if (n < 1024) return n + ' B'; |
| 184 | + if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'; |
| 185 | + if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB'; |
| 186 | + return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB'; |
| 187 | + } |
| 188 | + function fmtDate(iso) { |
| 189 | + try { |
| 190 | + return new Date(iso).toLocaleString(undefined, { |
| 191 | + month: 'short', day: 'numeric', year: 'numeric', |
| 192 | + hour: 'numeric', minute: '2-digit' |
| 193 | + }); |
| 194 | + } catch { return String(iso); } |
| 195 | + } |
| 196 | + function resolveUrl(href, base) { try { return new URL(href, base).href; } catch { return href; } } |
| 197 | + function escape(s) { |
| 198 | + return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); |
| 199 | + } |
| 200 | +})(); |
0 commit comments