@walkthru-earth/objex 0.1.0 → 1.0.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 +1 -1
- package/dist/components/browser/FileBrowser.svelte +37 -1
- package/dist/components/browser/FileRow.svelte +8 -3
- package/dist/components/browser/FileTreeSidebar.svelte +1 -3
- package/dist/components/layout/AboutSheet.svelte +126 -0
- package/dist/components/layout/AboutSheet.svelte.d.ts +6 -0
- package/dist/components/layout/ConnectionDialog.svelte +186 -138
- package/dist/components/layout/ConnectionDialog.svelte.d.ts +1 -0
- package/dist/components/layout/Sidebar.svelte +19 -3
- package/dist/components/layout/TabBar.svelte +4 -7
- package/dist/components/viewers/CodeViewer.svelte +17 -9
- package/dist/components/viewers/ImageViewer.svelte +6 -16
- package/dist/components/viewers/MarkdownViewer.svelte +8 -16
- package/dist/components/viewers/MediaViewer.svelte +6 -17
- package/dist/components/viewers/ModelViewer.svelte +4 -2
- package/dist/components/viewers/NotebookViewer.svelte +90 -40
- package/dist/components/viewers/PdfViewer.svelte +5 -3
- package/dist/components/viewers/RawViewer.svelte +4 -2
- package/dist/components/viewers/TableGrid.svelte +3 -2
- package/dist/components/viewers/ZarrMapViewer.svelte +334 -40
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +3 -8
- package/dist/components/viewers/ZarrViewer.svelte +459 -178
- package/dist/components/viewers/map/AttributeTable.svelte +1 -6
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +2 -6
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +96 -22
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +34 -0
- package/dist/file-icons/index.js +6 -0
- package/dist/i18n/ar.js +34 -0
- package/dist/i18n/en.js +34 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/query/wasm.js +5 -4
- package/dist/storage/browser-cloud.d.ts +7 -0
- package/dist/storage/browser-cloud.js +74 -7
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +318 -0
- package/dist/stores/connections.svelte.js +5 -5
- package/dist/stores/query-history.svelte.js +4 -5
- package/dist/stores/settings.svelte.js +4 -4
- package/dist/types.d.ts +2 -2
- package/dist/utils/clipboard.d.ts +13 -0
- package/dist/utils/clipboard.js +38 -0
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.js +12 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/format.js +22 -0
- package/dist/utils/host-detection.js +78 -18
- package/dist/utils/notebook.d.ts +59 -0
- package/dist/utils/notebook.js +211 -0
- package/dist/utils/parquet-metadata.js +1 -1
- package/dist/utils/pmtiles-tile.js +2 -1
- package/dist/utils/pmtiles.js +2 -1
- package/dist/utils/storage-url.d.ts +1 -1
- package/dist/utils/storage-url.js +82 -24
- package/dist/utils/url-state.js +2 -7
- package/dist/utils/url.d.ts +15 -1
- package/dist/utils/url.js +45 -19
- package/dist/utils/zarr.d.ts +60 -20
- package/dist/utils/zarr.js +450 -103
- package/package.json +64 -52
- package/dist/assets/favicon.svg +0 -17
- package/dist/components/CLAUDE.md +0 -44
- package/dist/components/viewers/CLAUDE.md +0 -60
- package/dist/file-icons/CLAUDE.md +0 -21
- package/dist/i18n/CLAUDE.md +0 -19
- package/dist/query/CLAUDE.md +0 -22
- package/dist/storage/CLAUDE.md +0 -23
- package/dist/stores/CLAUDE.md +0 -29
- package/dist/types/notebookjs.d.ts +0 -14
- package/dist/utils/CLAUDE.md +0 -54
- package/dist/utils/analytics.d.ts +0 -10
- package/dist/utils/analytics.js +0 -38
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Also extracts `rootPrefix` when the app is hosted inside a subfolder.
|
|
9
9
|
*/
|
|
10
|
+
import { buildProviderBaseUrl } from '../storage/providers.js';
|
|
10
11
|
import { parseStorageUrl } from './storage-url.js';
|
|
11
12
|
/**
|
|
12
13
|
* Extract root prefix from pathname.
|
|
@@ -28,20 +29,8 @@ function extractRootPrefix(pathname) {
|
|
|
28
29
|
/**
|
|
29
30
|
* Build a normalized API endpoint URL for a detected provider.
|
|
30
31
|
*/
|
|
31
|
-
function buildBucketUrl(provider, endpoint, bucket) {
|
|
32
|
-
|
|
33
|
-
return `${endpoint.replace(/\/$/, '')}/${bucket}`;
|
|
34
|
-
}
|
|
35
|
-
switch (provider) {
|
|
36
|
-
case 'gcs':
|
|
37
|
-
return `https://storage.googleapis.com/${bucket}`;
|
|
38
|
-
case 'azure':
|
|
39
|
-
return `${endpoint}/${bucket}`;
|
|
40
|
-
case 'storj':
|
|
41
|
-
return `https://gateway.storjshare.io/${bucket}`;
|
|
42
|
-
default:
|
|
43
|
-
return `https://s3.us-east-1.amazonaws.com/${bucket}`;
|
|
44
|
-
}
|
|
32
|
+
function buildBucketUrl(provider, endpoint, bucket, region) {
|
|
33
|
+
return buildProviderBaseUrl((provider === 'unknown' ? 's3' : provider), endpoint, bucket, region || '');
|
|
45
34
|
}
|
|
46
35
|
/**
|
|
47
36
|
* Detect hosting bucket from current URL.
|
|
@@ -178,7 +167,7 @@ export function detectHostBucket() {
|
|
|
178
167
|
const doSpaces = host.match(/^(.+)\.([a-z0-9-]+)\.digitaloceanspaces\.com$/);
|
|
179
168
|
if (doSpaces) {
|
|
180
169
|
return {
|
|
181
|
-
provider: '
|
|
170
|
+
provider: 'digitalocean',
|
|
182
171
|
bucket: doSpaces[1],
|
|
183
172
|
region: doSpaces[2],
|
|
184
173
|
endpoint: `https://${doSpaces[2]}.digitaloceanspaces.com`,
|
|
@@ -190,7 +179,7 @@ export function detectHostBucket() {
|
|
|
190
179
|
const doCdn = host.match(/^(.+)\.([a-z0-9-]+)\.cdn\.digitaloceanspaces\.com$/);
|
|
191
180
|
if (doCdn) {
|
|
192
181
|
return {
|
|
193
|
-
provider: '
|
|
182
|
+
provider: 'digitalocean',
|
|
194
183
|
bucket: doCdn[1],
|
|
195
184
|
region: doCdn[2],
|
|
196
185
|
endpoint: `https://${doCdn[2]}.digitaloceanspaces.com`,
|
|
@@ -204,7 +193,7 @@ export function detectHostBucket() {
|
|
|
204
193
|
const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
|
|
205
194
|
if (parts.length > 0) {
|
|
206
195
|
return {
|
|
207
|
-
provider: '
|
|
196
|
+
provider: 'wasabi',
|
|
208
197
|
bucket: parts[0],
|
|
209
198
|
region: wasabi[1],
|
|
210
199
|
endpoint: `https://${host}`,
|
|
@@ -217,7 +206,7 @@ export function detectHostBucket() {
|
|
|
217
206
|
const b2s3 = host.match(/^(.+)\.s3\.([a-z0-9-]+)\.backblazeb2\.com$/);
|
|
218
207
|
if (b2s3) {
|
|
219
208
|
return {
|
|
220
|
-
provider: '
|
|
209
|
+
provider: 'b2',
|
|
221
210
|
bucket: b2s3[1],
|
|
222
211
|
region: b2s3[2],
|
|
223
212
|
endpoint: `https://s3.${b2s3[2]}.backblazeb2.com`,
|
|
@@ -267,6 +256,77 @@ export function detectHostBucket() {
|
|
|
267
256
|
};
|
|
268
257
|
}
|
|
269
258
|
}
|
|
259
|
+
// Contabo: <region>.contabostorage.com/<bucket>
|
|
260
|
+
const contabo = host.match(/^([a-z0-9]+)\.contabostorage\.com$/);
|
|
261
|
+
if (contabo) {
|
|
262
|
+
const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
|
|
263
|
+
if (parts.length > 0) {
|
|
264
|
+
return {
|
|
265
|
+
provider: 'contabo',
|
|
266
|
+
bucket: parts[0],
|
|
267
|
+
region: contabo[1],
|
|
268
|
+
endpoint: `${url.protocol}//${url.host}`,
|
|
269
|
+
rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
|
|
270
|
+
bucketUrl: `${url.protocol}//${url.host}/${parts[0]}`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Hetzner: <region>.your-objectstorage.com/<bucket>
|
|
275
|
+
const hetzner = host.match(/^([a-z0-9]+)\.your-objectstorage\.com$/);
|
|
276
|
+
if (hetzner) {
|
|
277
|
+
const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
|
|
278
|
+
if (parts.length > 0) {
|
|
279
|
+
return {
|
|
280
|
+
provider: 'hetzner',
|
|
281
|
+
bucket: parts[0],
|
|
282
|
+
region: hetzner[1],
|
|
283
|
+
endpoint: `${url.protocol}//${url.host}`,
|
|
284
|
+
rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
|
|
285
|
+
bucketUrl: `${url.protocol}//${url.host}/${parts[0]}`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Linode / Akamai: <bucket>.<region>.linodeobjects.com or <region>.linodeobjects.com/<bucket>
|
|
290
|
+
const linodeVhost = host.match(/^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/);
|
|
291
|
+
if (linodeVhost) {
|
|
292
|
+
return {
|
|
293
|
+
provider: 'linode',
|
|
294
|
+
bucket: linodeVhost[1],
|
|
295
|
+
region: linodeVhost[2],
|
|
296
|
+
endpoint: `https://${linodeVhost[2]}.linodeobjects.com`,
|
|
297
|
+
rootPrefix: extractRootPrefix(pathname),
|
|
298
|
+
bucketUrl: `https://${linodeVhost[2]}.linodeobjects.com/${linodeVhost[1]}`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const linodePath = host.match(/^([a-z0-9-]+)\.linodeobjects\.com$/);
|
|
302
|
+
if (linodePath) {
|
|
303
|
+
const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
|
|
304
|
+
if (parts.length > 0) {
|
|
305
|
+
return {
|
|
306
|
+
provider: 'linode',
|
|
307
|
+
bucket: parts[0],
|
|
308
|
+
region: linodePath[1],
|
|
309
|
+
endpoint: `https://${url.host}`,
|
|
310
|
+
rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
|
|
311
|
+
bucketUrl: `https://${url.host}/${parts[0]}`
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// OVHcloud: s3.<region>.io.cloud.ovh.net/<bucket>
|
|
316
|
+
const ovh = host.match(/^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/);
|
|
317
|
+
if (ovh) {
|
|
318
|
+
const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
|
|
319
|
+
if (parts.length > 0) {
|
|
320
|
+
return {
|
|
321
|
+
provider: 'ovhcloud',
|
|
322
|
+
bucket: parts[0],
|
|
323
|
+
region: ovh[1],
|
|
324
|
+
endpoint: `https://${url.host}`,
|
|
325
|
+
rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
|
|
326
|
+
bucketUrl: `https://${url.host}/${parts[0]}`
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
270
330
|
// MinIO / localhost / private IPs
|
|
271
331
|
const isLocal = host === 'localhost' ||
|
|
272
332
|
host === '127.0.0.1' ||
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight browser-native Jupyter notebook renderer.
|
|
3
|
+
* Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
|
|
4
|
+
* Handles nbformat 2, 3, 4, and 5.
|
|
5
|
+
*/
|
|
6
|
+
export interface NotebookConfig {
|
|
7
|
+
markdown: (md: string) => string;
|
|
8
|
+
ansi: (text: string) => string;
|
|
9
|
+
highlighter: (code: string, lang: string) => string;
|
|
10
|
+
}
|
|
11
|
+
interface RawNotebook {
|
|
12
|
+
nbformat: number;
|
|
13
|
+
nbformat_minor?: number;
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
cells?: RawCell[];
|
|
16
|
+
worksheets?: {
|
|
17
|
+
cells: RawCell[];
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
interface RawCell {
|
|
21
|
+
cell_type: string;
|
|
22
|
+
source?: string | string[];
|
|
23
|
+
input?: string | string[];
|
|
24
|
+
outputs?: RawOutput[];
|
|
25
|
+
prompt_number?: number;
|
|
26
|
+
execution_count?: number | null;
|
|
27
|
+
level?: number;
|
|
28
|
+
language?: string;
|
|
29
|
+
}
|
|
30
|
+
interface RawOutput {
|
|
31
|
+
output_type: string;
|
|
32
|
+
data?: Record<string, string | string[]>;
|
|
33
|
+
text?: string | string[];
|
|
34
|
+
stream?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
png?: string;
|
|
37
|
+
jpeg?: string;
|
|
38
|
+
svg?: string;
|
|
39
|
+
html?: string;
|
|
40
|
+
latex?: string;
|
|
41
|
+
traceback?: string[];
|
|
42
|
+
ename?: string;
|
|
43
|
+
evalue?: string;
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
}
|
|
46
|
+
export interface NotebookMeta {
|
|
47
|
+
kernelName: string;
|
|
48
|
+
language: string;
|
|
49
|
+
cellCount: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse and render a Jupyter notebook JSON to a DOM element.
|
|
53
|
+
* Returns the rendered element and metadata.
|
|
54
|
+
*/
|
|
55
|
+
export declare function renderNotebook(raw: RawNotebook, config: NotebookConfig): {
|
|
56
|
+
element: HTMLElement;
|
|
57
|
+
meta: NotebookMeta;
|
|
58
|
+
};
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight browser-native Jupyter notebook renderer.
|
|
3
|
+
* Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
|
|
4
|
+
* Handles nbformat 2, 3, 4, and 5.
|
|
5
|
+
*/
|
|
6
|
+
const PREFIX = 'nb-';
|
|
7
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
8
|
+
function el(tag, classNames = []) {
|
|
9
|
+
const e = document.createElement(tag);
|
|
10
|
+
if (classNames.length)
|
|
11
|
+
e.className = classNames.map((c) => PREFIX + c).join(' ');
|
|
12
|
+
return e;
|
|
13
|
+
}
|
|
14
|
+
function escapeHTML(raw) {
|
|
15
|
+
return raw.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
16
|
+
}
|
|
17
|
+
function joinText(text) {
|
|
18
|
+
if (Array.isArray(text))
|
|
19
|
+
return text.join('');
|
|
20
|
+
return text ?? '';
|
|
21
|
+
}
|
|
22
|
+
// ── Display renderers ────────────────────────────────────────────────────────
|
|
23
|
+
const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
24
|
+
function renderImage(format, data) {
|
|
25
|
+
const img = el('img', ['image-output']);
|
|
26
|
+
img.src = `data:${format};base64,${joinText(data).replace(/\n/g, '')}`;
|
|
27
|
+
return img;
|
|
28
|
+
}
|
|
29
|
+
function renderDisplayData(output, config) {
|
|
30
|
+
// Resolve data from v4 `data` field or v3 flat fields
|
|
31
|
+
const getData = (mime) => output.data?.[mime] ?? output[mime];
|
|
32
|
+
// Priority order (richest first)
|
|
33
|
+
for (const fmt of IMAGE_FORMATS) {
|
|
34
|
+
const d = getData(fmt);
|
|
35
|
+
if (d)
|
|
36
|
+
return renderImage(fmt, d);
|
|
37
|
+
}
|
|
38
|
+
const svg = getData('image/svg+xml') ?? getData('text/svg+xml');
|
|
39
|
+
if (svg) {
|
|
40
|
+
const wrapper = el('div', ['svg-output']);
|
|
41
|
+
wrapper.innerHTML = joinText(svg);
|
|
42
|
+
return wrapper;
|
|
43
|
+
}
|
|
44
|
+
const html = getData('text/html');
|
|
45
|
+
if (html) {
|
|
46
|
+
const wrapper = el('div', ['html-output']);
|
|
47
|
+
wrapper.innerHTML = joinText(html);
|
|
48
|
+
return wrapper;
|
|
49
|
+
}
|
|
50
|
+
const md = getData('text/markdown');
|
|
51
|
+
if (md) {
|
|
52
|
+
const wrapper = el('div', ['html-output']);
|
|
53
|
+
wrapper.innerHTML = config.markdown(joinText(md));
|
|
54
|
+
return wrapper;
|
|
55
|
+
}
|
|
56
|
+
const latex = getData('text/latex');
|
|
57
|
+
if (latex) {
|
|
58
|
+
const wrapper = el('div', ['latex-output']);
|
|
59
|
+
wrapper.textContent = joinText(latex);
|
|
60
|
+
return wrapper;
|
|
61
|
+
}
|
|
62
|
+
const plain = getData('text/plain');
|
|
63
|
+
if (plain) {
|
|
64
|
+
const pre = el('pre', ['text-output']);
|
|
65
|
+
pre.innerHTML = config.ansi(escapeHTML(joinText(plain)));
|
|
66
|
+
return pre;
|
|
67
|
+
}
|
|
68
|
+
return el('div', ['empty-output']);
|
|
69
|
+
}
|
|
70
|
+
function renderStream(output, config) {
|
|
71
|
+
const streamName = output.stream ?? output.name ?? 'stdout';
|
|
72
|
+
const pre = el('pre', [streamName]);
|
|
73
|
+
pre.innerHTML = config.ansi(escapeHTML(joinText(output.text ?? '')));
|
|
74
|
+
return pre;
|
|
75
|
+
}
|
|
76
|
+
function renderError(output, config) {
|
|
77
|
+
const pre = el('pre', ['pyerr']);
|
|
78
|
+
const raw = (output.traceback ?? []).join('\n');
|
|
79
|
+
pre.innerHTML = config.ansi(escapeHTML(raw));
|
|
80
|
+
return pre;
|
|
81
|
+
}
|
|
82
|
+
// ── Output coalescing (merge consecutive same-name streams) ──────────────────
|
|
83
|
+
function coalesceStreams(outputs) {
|
|
84
|
+
if (!outputs.length)
|
|
85
|
+
return outputs;
|
|
86
|
+
const result = [{ ...outputs[0] }];
|
|
87
|
+
for (let i = 1; i < outputs.length; i++) {
|
|
88
|
+
const o = outputs[i];
|
|
89
|
+
const last = result[result.length - 1];
|
|
90
|
+
if (o.output_type === 'stream' &&
|
|
91
|
+
last.output_type === 'stream' &&
|
|
92
|
+
(o.stream ?? o.name) === (last.stream ?? last.name)) {
|
|
93
|
+
// Merge text
|
|
94
|
+
const lastText = Array.isArray(last.text) ? last.text : [last.text ?? ''];
|
|
95
|
+
const oText = Array.isArray(o.text) ? o.text : [o.text ?? ''];
|
|
96
|
+
last.text = lastText.concat(oText);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
result.push({ ...o });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
// ── Cell renderers ───────────────────────────────────────────────────────────
|
|
105
|
+
function renderCodeCell(cell, lang, config) {
|
|
106
|
+
const cellEl = el('div', ['cell', 'code-cell']);
|
|
107
|
+
// Input
|
|
108
|
+
const source = joinText(cell.source ?? cell.input ?? '');
|
|
109
|
+
if (source) {
|
|
110
|
+
const holder = el('div', ['input']);
|
|
111
|
+
const num = cell.prompt_number ?? cell.execution_count;
|
|
112
|
+
if (typeof num === 'number' && num > -1) {
|
|
113
|
+
holder.setAttribute('data-prompt-number', String(num));
|
|
114
|
+
}
|
|
115
|
+
const pre = el('pre');
|
|
116
|
+
const code = document.createElement('code');
|
|
117
|
+
code.setAttribute('data-language', lang);
|
|
118
|
+
code.className = `lang-${lang}`;
|
|
119
|
+
code.innerHTML = config.highlighter(escapeHTML(source), lang);
|
|
120
|
+
pre.appendChild(code);
|
|
121
|
+
holder.appendChild(pre);
|
|
122
|
+
cellEl.appendChild(holder);
|
|
123
|
+
}
|
|
124
|
+
// Outputs
|
|
125
|
+
const rawOutputs = coalesceStreams(cell.outputs ?? []);
|
|
126
|
+
for (const output of rawOutputs) {
|
|
127
|
+
const outer = el('div', ['output']);
|
|
128
|
+
let inner;
|
|
129
|
+
switch (output.output_type) {
|
|
130
|
+
case 'display_data':
|
|
131
|
+
case 'execute_result':
|
|
132
|
+
case 'pyout':
|
|
133
|
+
inner = renderDisplayData(output, config);
|
|
134
|
+
break;
|
|
135
|
+
case 'stream':
|
|
136
|
+
inner = renderStream(output, config);
|
|
137
|
+
break;
|
|
138
|
+
case 'error':
|
|
139
|
+
case 'pyerr':
|
|
140
|
+
inner = renderError(output, config);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
inner = el('div', ['empty-output']);
|
|
144
|
+
}
|
|
145
|
+
outer.appendChild(inner);
|
|
146
|
+
cellEl.appendChild(outer);
|
|
147
|
+
}
|
|
148
|
+
return cellEl;
|
|
149
|
+
}
|
|
150
|
+
function renderMarkdownCell(cell, config) {
|
|
151
|
+
const cellEl = el('div', ['cell', 'markdown-cell']);
|
|
152
|
+
const source = joinText(cell.source ?? cell.input ?? '');
|
|
153
|
+
cellEl.innerHTML = config.markdown(source);
|
|
154
|
+
return cellEl;
|
|
155
|
+
}
|
|
156
|
+
function renderHeadingCell(cell) {
|
|
157
|
+
const level = Math.min(Math.max(cell.level ?? 1, 1), 6);
|
|
158
|
+
const heading = el(`h${level}`, ['cell', 'heading-cell']);
|
|
159
|
+
heading.innerHTML = escapeHTML(joinText(cell.source ?? cell.input ?? ''));
|
|
160
|
+
return heading;
|
|
161
|
+
}
|
|
162
|
+
function renderRawCell(cell) {
|
|
163
|
+
const cellEl = el('div', ['cell', 'raw-cell']);
|
|
164
|
+
cellEl.innerHTML = escapeHTML(joinText(cell.source ?? cell.input ?? ''));
|
|
165
|
+
return cellEl;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse and render a Jupyter notebook JSON to a DOM element.
|
|
169
|
+
* Returns the rendered element and metadata.
|
|
170
|
+
*/
|
|
171
|
+
export function renderNotebook(raw, config) {
|
|
172
|
+
const meta = raw.metadata ?? {};
|
|
173
|
+
const lang = meta.kernelspec?.language ?? meta.language_info?.name ?? meta.language ?? 'python';
|
|
174
|
+
const kernelName = meta.kernelspec?.display_name ?? meta.language_info?.name ?? '';
|
|
175
|
+
// Normalize cells: v4+ has top-level `cells`, v2/v3 uses `worksheets`
|
|
176
|
+
let allCells = [];
|
|
177
|
+
if (Array.isArray(raw.cells)) {
|
|
178
|
+
allCells = raw.cells;
|
|
179
|
+
}
|
|
180
|
+
else if (Array.isArray(raw.worksheets)) {
|
|
181
|
+
for (const ws of raw.worksheets) {
|
|
182
|
+
if (Array.isArray(ws.cells))
|
|
183
|
+
allCells.push(...ws.cells);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const notebook = el('div', ['notebook']);
|
|
187
|
+
for (const cell of allCells) {
|
|
188
|
+
let rendered;
|
|
189
|
+
switch (cell.cell_type) {
|
|
190
|
+
case 'code':
|
|
191
|
+
rendered = renderCodeCell(cell, cell.language ?? lang, config);
|
|
192
|
+
break;
|
|
193
|
+
case 'markdown':
|
|
194
|
+
rendered = renderMarkdownCell(cell, config);
|
|
195
|
+
break;
|
|
196
|
+
case 'heading':
|
|
197
|
+
rendered = renderHeadingCell(cell);
|
|
198
|
+
break;
|
|
199
|
+
case 'raw':
|
|
200
|
+
rendered = renderRawCell(cell);
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
rendered = renderRawCell(cell);
|
|
204
|
+
}
|
|
205
|
+
notebook.appendChild(rendered);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
element: notebook,
|
|
209
|
+
meta: { kernelName, language: lang, cellCount: allCells.length }
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This provides instant metadata display before DuckDB-WASM finishes loading.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import { WGS84_CODES } from '../constants.js';
|
|
10
10
|
/** Map hyparquet schema element types to DuckDB-like type strings. */
|
|
11
11
|
function mapParquetType(col) {
|
|
12
12
|
const lt = col.logical_type;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* vector tiles into a structure suitable for SVG rendering and
|
|
6
6
|
* feature inspection.
|
|
7
7
|
*/
|
|
8
|
+
import { LAYER_HUE_MULTIPLIER } from '../constants.js';
|
|
8
9
|
const GEOM_TYPES = ['Unknown', 'Point', 'LineString', 'Polygon'];
|
|
9
10
|
/**
|
|
10
11
|
* Fetch and decode an MVT tile from a PMTiles archive.
|
|
@@ -60,5 +61,5 @@ export function tileMimeType(format) {
|
|
|
60
61
|
}
|
|
61
62
|
/** Compute the hue for layer index i (same palette as buildPmtilesLayers). */
|
|
62
63
|
export function layerHue(i) {
|
|
63
|
-
return (i *
|
|
64
|
+
return (i * LAYER_HUE_MULTIPLIER) % 360;
|
|
64
65
|
}
|
package/dist/utils/pmtiles.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PMTiles, Protocol } from 'pmtiles';
|
|
2
|
+
import { LAYER_HUE_MULTIPLIER } from '../constants.js';
|
|
2
3
|
let protocol = null;
|
|
3
4
|
export function getPmtilesProtocol() {
|
|
4
5
|
if (!protocol) {
|
|
@@ -96,7 +97,7 @@ export function buildPmtilesLayers(sourceId, metadata) {
|
|
|
96
97
|
const layers = [];
|
|
97
98
|
for (let i = 0; i < metadata.layers.length; i++) {
|
|
98
99
|
const layerId = metadata.layers[i];
|
|
99
|
-
const hue = (i *
|
|
100
|
+
const hue = (i * LAYER_HUE_MULTIPLIER) % 360;
|
|
100
101
|
layers.push({
|
|
101
102
|
id: `${layerId}-fill`,
|
|
102
103
|
type: 'fill',
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*
|
|
36
36
|
* Also handles plain bucket names (no protocol).
|
|
37
37
|
*/
|
|
38
|
-
export type StorageProvider =
|
|
38
|
+
export type StorageProvider = string;
|
|
39
39
|
export interface ParsedStorageUrl {
|
|
40
40
|
bucket: string;
|
|
41
41
|
region: string;
|
|
@@ -35,25 +35,25 @@
|
|
|
35
35
|
*
|
|
36
36
|
* Also handles plain bucket names (no protocol).
|
|
37
37
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
import { PROVIDERS } from '../storage/providers.js';
|
|
39
|
+
/**
|
|
40
|
+
* Build SCHEME_MAP from the provider registry's `schemes` arrays.
|
|
41
|
+
* Each scheme like "s3" generates an entry `"s3://": { provider: "s3", strip: 5 }`.
|
|
42
|
+
*/
|
|
43
|
+
function buildSchemeMap() {
|
|
44
|
+
const map = {};
|
|
45
|
+
for (const [id, def] of Object.entries(PROVIDERS)) {
|
|
46
|
+
for (const scheme of def.schemes) {
|
|
47
|
+
const key = `${scheme}://`;
|
|
48
|
+
map[key] = { provider: id, strip: key.length };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Non-registry schemes (no corresponding provider)
|
|
52
|
+
map['swift://'] = { provider: 'unknown', strip: 8 };
|
|
53
|
+
return map;
|
|
54
|
+
}
|
|
55
|
+
/** All recognized URI scheme prefixes (lowercase), derived from provider registry */
|
|
56
|
+
const SCHEME_MAP = buildSchemeMap();
|
|
57
57
|
function defaultResult(defaults) {
|
|
58
58
|
return {
|
|
59
59
|
bucket: '',
|
|
@@ -174,7 +174,7 @@ export function parseStorageUrl(input, defaults = {}) {
|
|
|
174
174
|
bucket: doVhost[1],
|
|
175
175
|
region: doVhost[2],
|
|
176
176
|
endpoint: `${url.protocol}//${doVhost[2]}.digitaloceanspaces.com`,
|
|
177
|
-
provider: '
|
|
177
|
+
provider: 'digitalocean',
|
|
178
178
|
prefix: pathParts.join('/')
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -185,7 +185,7 @@ export function parseStorageUrl(input, defaults = {}) {
|
|
|
185
185
|
bucket: pathParts[0],
|
|
186
186
|
region: doPath[1],
|
|
187
187
|
endpoint: `${url.protocol}//${url.host}`,
|
|
188
|
-
provider: '
|
|
188
|
+
provider: 'digitalocean',
|
|
189
189
|
prefix: pathParts.slice(1).join('/')
|
|
190
190
|
};
|
|
191
191
|
}
|
|
@@ -197,7 +197,7 @@ export function parseStorageUrl(input, defaults = {}) {
|
|
|
197
197
|
bucket: pathParts[0],
|
|
198
198
|
region: wasabiMatch[1],
|
|
199
199
|
endpoint: `${url.protocol}//${url.host}`,
|
|
200
|
-
provider: '
|
|
200
|
+
provider: 'wasabi',
|
|
201
201
|
prefix: pathParts.slice(1).join('/')
|
|
202
202
|
};
|
|
203
203
|
}
|
|
@@ -209,7 +209,7 @@ export function parseStorageUrl(input, defaults = {}) {
|
|
|
209
209
|
bucket: b2S3[1],
|
|
210
210
|
region: b2S3[2],
|
|
211
211
|
endpoint: `${url.protocol}//s3.${b2S3[2]}.backblazeb2.com`,
|
|
212
|
-
provider: '
|
|
212
|
+
provider: 'b2',
|
|
213
213
|
prefix: pathParts.join('/')
|
|
214
214
|
};
|
|
215
215
|
}
|
|
@@ -220,7 +220,7 @@ export function parseStorageUrl(input, defaults = {}) {
|
|
|
220
220
|
bucket: pathParts[1],
|
|
221
221
|
region: defaults.region || 'us-west-000',
|
|
222
222
|
endpoint: `${url.protocol}//${url.host}`,
|
|
223
|
-
provider: '
|
|
223
|
+
provider: 'b2',
|
|
224
224
|
prefix: pathParts.slice(2).join('/')
|
|
225
225
|
};
|
|
226
226
|
}
|
|
@@ -259,6 +259,64 @@ export function parseStorageUrl(input, defaults = {}) {
|
|
|
259
259
|
prefix: pathParts.slice(1).join('/')
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
|
+
// --- Contabo ---
|
|
263
|
+
// <region>.contabostorage.com/<bucket>
|
|
264
|
+
const contaboMatch = host.match(/^([a-z0-9]+)\.contabostorage\.com$/);
|
|
265
|
+
if (contaboMatch && pathParts.length > 0) {
|
|
266
|
+
return {
|
|
267
|
+
bucket: pathParts[0],
|
|
268
|
+
region: contaboMatch[1],
|
|
269
|
+
endpoint: `${url.protocol}//${url.host}`,
|
|
270
|
+
provider: 'contabo',
|
|
271
|
+
prefix: pathParts.slice(1).join('/')
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// --- Hetzner ---
|
|
275
|
+
// <region>.your-objectstorage.com/<bucket>
|
|
276
|
+
const hetznerMatch = host.match(/^([a-z0-9]+)\.your-objectstorage\.com$/);
|
|
277
|
+
if (hetznerMatch && pathParts.length > 0) {
|
|
278
|
+
return {
|
|
279
|
+
bucket: pathParts[0],
|
|
280
|
+
region: hetznerMatch[1],
|
|
281
|
+
endpoint: `${url.protocol}//${url.host}`,
|
|
282
|
+
provider: 'hetzner',
|
|
283
|
+
prefix: pathParts.slice(1).join('/')
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// --- Linode / Akamai ---
|
|
287
|
+
// <bucket>.<region>.linodeobjects.com or <region>.linodeobjects.com/<bucket>
|
|
288
|
+
const linodeVhost = host.match(/^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/);
|
|
289
|
+
if (linodeVhost) {
|
|
290
|
+
return {
|
|
291
|
+
bucket: linodeVhost[1],
|
|
292
|
+
region: linodeVhost[2],
|
|
293
|
+
endpoint: `${url.protocol}//${linodeVhost[2]}.linodeobjects.com`,
|
|
294
|
+
provider: 'linode',
|
|
295
|
+
prefix: pathParts.join('/')
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const linodePath = host.match(/^([a-z0-9-]+)\.linodeobjects\.com$/);
|
|
299
|
+
if (linodePath && pathParts.length > 0) {
|
|
300
|
+
return {
|
|
301
|
+
bucket: pathParts[0],
|
|
302
|
+
region: linodePath[1],
|
|
303
|
+
endpoint: `${url.protocol}//${url.host}`,
|
|
304
|
+
provider: 'linode',
|
|
305
|
+
prefix: pathParts.slice(1).join('/')
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// --- OVHcloud ---
|
|
309
|
+
// s3.<region>.io.cloud.ovh.net/<bucket>
|
|
310
|
+
const ovhMatch = host.match(/^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/);
|
|
311
|
+
if (ovhMatch && pathParts.length > 0) {
|
|
312
|
+
return {
|
|
313
|
+
bucket: pathParts[0],
|
|
314
|
+
region: ovhMatch[1],
|
|
315
|
+
endpoint: `${url.protocol}//${url.host}`,
|
|
316
|
+
provider: 'ovhcloud',
|
|
317
|
+
prefix: pathParts.slice(1).join('/')
|
|
318
|
+
};
|
|
319
|
+
}
|
|
262
320
|
// --- MinIO ---
|
|
263
321
|
// Common patterns: minio.<domain>, localhost with port
|
|
264
322
|
const isMinioLike = host.includes('minio') ||
|
package/dist/utils/url-state.js
CHANGED
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
* Uses SvelteKit's replaceState to avoid conflicts with the router.
|
|
11
11
|
*/
|
|
12
12
|
import { replaceState } from '$app/navigation';
|
|
13
|
+
import { buildProviderBaseUrl } from '../storage/providers.js';
|
|
13
14
|
import { parseStorageUrl } from './storage-url.js';
|
|
14
15
|
/**
|
|
15
16
|
* Build the base HTTPS URL for a connection (endpoint + bucket).
|
|
16
17
|
*/
|
|
17
18
|
function buildBaseUrl(conn) {
|
|
18
|
-
|
|
19
|
-
return `${conn.endpoint.replace(/\/$/, '')}/${conn.bucket}`;
|
|
20
|
-
}
|
|
21
|
-
if (conn.provider === 'gcs') {
|
|
22
|
-
return `https://storage.googleapis.com/${conn.bucket}`;
|
|
23
|
-
}
|
|
24
|
-
return `https://s3.${conn.region || 'us-east-1'}.amazonaws.com/${conn.bucket}`;
|
|
19
|
+
return buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region);
|
|
25
20
|
}
|
|
26
21
|
/**
|
|
27
22
|
* Build a full storage URL from a Connection + optional object prefix.
|
package/dist/utils/url.d.ts
CHANGED
|
@@ -4,7 +4,11 @@ import type { Tab } from '../types.js';
|
|
|
4
4
|
* Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
|
|
5
5
|
*/
|
|
6
6
|
export declare function buildHttpsUrl(tab: Tab): string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Map provider to its native URI scheme prefix.
|
|
9
|
+
* Derived from the registry's `schemes` array (first entry is the primary scheme).
|
|
10
|
+
* Falls back to 's3' for providers without a scheme (S3-compatible).
|
|
11
|
+
*/
|
|
8
12
|
export declare function getNativeScheme(provider: string): string;
|
|
9
13
|
/**
|
|
10
14
|
* Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
|
|
@@ -25,3 +29,13 @@ export declare function buildDuckDbUrl(tab: Tab): string;
|
|
|
25
29
|
* False for authenticated S3 (needs signed URLs or blob download via adapter).
|
|
26
30
|
*/
|
|
27
31
|
export declare function canStreamDirectly(tab: Tab): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
|
|
34
|
+
* for browser access. Returns the original URL if already HTTP(S) or unknown.
|
|
35
|
+
*
|
|
36
|
+
* Supported:
|
|
37
|
+
* - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
|
|
38
|
+
* (region auto-detected from bucket name when possible, e.g. "us-west-2.opendata.source.coop")
|
|
39
|
+
* - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveCloudUrl(url: string): string;
|