@bendyline/docblocks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/chunk-GUNM43XZ.js +63 -0
  4. package/dist/chunk-GUNM43XZ.js.map +1 -0
  5. package/dist/chunk-NSVTXALR.js +559 -0
  6. package/dist/chunk-NSVTXALR.js.map +1 -0
  7. package/dist/filesystem/index.d.ts +5 -0
  8. package/dist/filesystem/index.d.ts.map +1 -0
  9. package/dist/filesystem/index.js +4 -0
  10. package/dist/filesystem/index.js.map +1 -0
  11. package/dist/filesystem/indexeddb-content-container.d.ts +20 -0
  12. package/dist/filesystem/indexeddb-content-container.d.ts.map +1 -0
  13. package/dist/filesystem/indexeddb-content-container.js +93 -0
  14. package/dist/filesystem/indexeddb-content-container.js.map +1 -0
  15. package/dist/filesystem/indexeddb-provider.d.ts +32 -0
  16. package/dist/filesystem/indexeddb-provider.d.ts.map +1 -0
  17. package/dist/filesystem/indexeddb-provider.js +253 -0
  18. package/dist/filesystem/indexeddb-provider.js.map +1 -0
  19. package/dist/filesystem/native-provider.d.ts +44 -0
  20. package/dist/filesystem/native-provider.d.ts.map +1 -0
  21. package/dist/filesystem/native-provider.js +302 -0
  22. package/dist/filesystem/native-provider.js.map +1 -0
  23. package/dist/filesystem/types.d.ts +50 -0
  24. package/dist/filesystem/types.d.ts.map +1 -0
  25. package/dist/filesystem/types.js +8 -0
  26. package/dist/filesystem/types.js.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/workspace/index.d.ts +3 -0
  32. package/dist/workspace/index.d.ts.map +1 -0
  33. package/dist/workspace/index.js +2 -0
  34. package/dist/workspace/index.js.map +1 -0
  35. package/dist/workspace/types.d.ts +15 -0
  36. package/dist/workspace/types.d.ts.map +1 -0
  37. package/dist/workspace/types.js +6 -0
  38. package/dist/workspace/types.js.map +1 -0
  39. package/dist/workspace/workspace-manager.d.ts +35 -0
  40. package/dist/workspace/workspace-manager.d.ts.map +1 -0
  41. package/dist/workspace/workspace-manager.js +82 -0
  42. package/dist/workspace/workspace-manager.js.map +1 -0
  43. package/package.json +59 -0
  44. package/src/filesystem/index.ts +20 -0
  45. package/src/filesystem/indexeddb-content-container.ts +104 -0
  46. package/src/filesystem/indexeddb-provider.ts +299 -0
  47. package/src/filesystem/native-provider.ts +348 -0
  48. package/src/filesystem/types.ts +69 -0
  49. package/src/index.ts +8 -0
  50. package/src/workspace/index.ts +10 -0
  51. package/src/workspace/types.ts +15 -0
  52. package/src/workspace/workspace-manager.ts +90 -0
@@ -0,0 +1,20 @@
1
+ export type {
2
+ FileSystemProvider,
3
+ FileSystemEntry,
4
+ FileEntry,
5
+ FolderEntry,
6
+ FileMeta,
7
+ } from './types.js';
8
+
9
+ export { IndexedDBFileSystemProvider } from './indexeddb-provider.js';
10
+ export { IndexedDBContentContainer } from './indexeddb-content-container.js';
11
+
12
+ export {
13
+ NativeFileSystemProvider,
14
+ isNativeFileSystemSupported,
15
+ openNativeFolder,
16
+ restoreNativeFolder,
17
+ storeDirectoryHandle,
18
+ loadDirectoryHandle,
19
+ removeDirectoryHandle,
20
+ } from './native-provider.js';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * IndexedDBContentContainer — a ContentContainer backed by IndexedDB.
3
+ *
4
+ * Uses a dedicated IndexedDBFileSystemProvider instance (separate store)
5
+ * to persist media files across page refreshes.
6
+ */
7
+
8
+ import type { ContentContainer, ContentEntry } from '@bendyline/squisq/storage';
9
+ import { findDocumentPath } from '@bendyline/squisq/storage';
10
+ import { IndexedDBFileSystemProvider } from './indexeddb-provider.js';
11
+
12
+ // ── MIME type guessing ─────────────────────────────────────────────
13
+
14
+ const EXTENSION_MIME_MAP: Record<string, string> = {
15
+ '.md': 'text/markdown',
16
+ '.txt': 'text/plain',
17
+ '.json': 'application/json',
18
+ '.jpg': 'image/jpeg',
19
+ '.jpeg': 'image/jpeg',
20
+ '.png': 'image/png',
21
+ '.gif': 'image/gif',
22
+ '.svg': 'image/svg+xml',
23
+ '.webp': 'image/webp',
24
+ '.avif': 'image/avif',
25
+ '.mp4': 'video/mp4',
26
+ '.webm': 'video/webm',
27
+ '.mp3': 'audio/mpeg',
28
+ '.wav': 'audio/wav',
29
+ '.ogg': 'audio/ogg',
30
+ };
31
+
32
+ function guessMimeType(path: string): string {
33
+ const dot = path.lastIndexOf('.');
34
+ if (dot === -1) return 'application/octet-stream';
35
+ const ext = path.slice(dot).toLowerCase();
36
+ return EXTENSION_MIME_MAP[ext] ?? 'application/octet-stream';
37
+ }
38
+
39
+ // ── Implementation ─────────────────────────────────────────────────
40
+
41
+ export class IndexedDBContentContainer implements ContentContainer {
42
+ private provider: IndexedDBFileSystemProvider;
43
+
44
+ constructor(workspaceId: string) {
45
+ this.provider = new IndexedDBFileSystemProvider(`${workspaceId}-media`, 'Media Storage');
46
+ }
47
+
48
+ async readFile(path: string): Promise<ArrayBuffer | null> {
49
+ return this.provider.readBinary(path);
50
+ }
51
+
52
+ async writeFile(path: string, data: ArrayBuffer | Uint8Array, _mimeType?: string): Promise<void> {
53
+ await this.provider.writeBinary(path, data);
54
+ }
55
+
56
+ async removeFile(path: string): Promise<void> {
57
+ await this.provider.delete(path);
58
+ }
59
+
60
+ async listFiles(prefix?: string): Promise<ContentEntry[]> {
61
+ const entries: ContentEntry[] = [];
62
+ const walk = async (dir: string) => {
63
+ const children = await this.provider.readDirectory(dir);
64
+ for (const child of children) {
65
+ if (child.kind === 'directory') {
66
+ await walk(child.path);
67
+ } else {
68
+ const filePath = child.path.replace(/^\//, '');
69
+ if (prefix && !filePath.startsWith(prefix)) continue;
70
+ const meta = await this.provider.stat(child.path);
71
+ entries.push({
72
+ path: filePath,
73
+ mimeType: guessMimeType(filePath),
74
+ size: meta?.size ?? 0,
75
+ });
76
+ }
77
+ }
78
+ };
79
+ await walk('/');
80
+ return entries;
81
+ }
82
+
83
+ async exists(path: string): Promise<boolean> {
84
+ return this.provider.exists(path);
85
+ }
86
+
87
+ async getDocumentPath(): Promise<string | null> {
88
+ return findDocumentPath(await this.listFiles());
89
+ }
90
+
91
+ async readDocument(): Promise<string | null> {
92
+ const docPath = await this.getDocumentPath();
93
+ if (!docPath) return null;
94
+ const data = await this.readFile(docPath);
95
+ if (!data) return null;
96
+ return new TextDecoder().decode(data);
97
+ }
98
+
99
+ async writeDocument(markdown: string, filename?: string): Promise<void> {
100
+ const name = filename ?? 'index.md';
101
+ const data = new TextEncoder().encode(markdown);
102
+ await this.writeFile(name, data, 'text/markdown');
103
+ }
104
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * IndexedDBFileSystemProvider — virtualises a filesystem on top of IndexedDB
3
+ * using the LocalForageAdapter from @bendyline/squisq/storage.
4
+ *
5
+ * Key schema:
6
+ * fs:{path}:content → string (text file contents)
7
+ * fs:{path}:binary → ArrayBuffer (binary file contents)
8
+ * fs:{path}:meta → FileMeta object
9
+ * fs:dirs → Set<string> of known directory paths
10
+ */
11
+
12
+ import { LocalForageAdapter } from '@bendyline/squisq/storage';
13
+ import type { FileSystemProvider, FileSystemEntry, FileMeta } from './types.js';
14
+
15
+ // ── Helpers ────────────────────────────────────────────────────────
16
+
17
+ /** Normalise a path: strip leading/trailing slashes, collapse doubles. */
18
+ function normalisePath(p: string): string {
19
+ return p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, '');
20
+ }
21
+
22
+ /** Get the parent directory of a path, or empty string for root-level. */
23
+ function parentDir(p: string): string {
24
+ const idx = p.lastIndexOf('/');
25
+ return idx === -1 ? '' : p.slice(0, idx);
26
+ }
27
+
28
+ /** Get the filename component of a path. */
29
+ function baseName(p: string): string {
30
+ const idx = p.lastIndexOf('/');
31
+ return idx === -1 ? p : p.slice(idx + 1);
32
+ }
33
+
34
+ // ── Key helpers ────────────────────────────────────────────────────
35
+
36
+ function contentKey(path: string): string {
37
+ return `fs:${path}:content`;
38
+ }
39
+
40
+ function binaryKey(path: string): string {
41
+ return `fs:${path}:binary`;
42
+ }
43
+
44
+ function metaKey(path: string): string {
45
+ return `fs:${path}:meta`;
46
+ }
47
+
48
+ const DIRS_KEY = 'fs:dirs';
49
+
50
+ // ── Implementation ─────────────────────────────────────────────────
51
+
52
+ export class IndexedDBFileSystemProvider implements FileSystemProvider {
53
+ readonly id: string;
54
+ readonly label: string;
55
+
56
+ private store: LocalForageAdapter;
57
+
58
+ constructor(id: string, label: string) {
59
+ this.id = id;
60
+ this.label = label;
61
+ this.store = new LocalForageAdapter({
62
+ name: `docblocks-fs-${id}`,
63
+ storeName: 'files',
64
+ });
65
+ }
66
+
67
+ // ── Directory tracking ──────────────────────────────────────────
68
+
69
+ private async getDirs(): Promise<Set<string>> {
70
+ const raw = await this.store.get<string[]>(DIRS_KEY);
71
+ return new Set(raw ?? []);
72
+ }
73
+
74
+ private async saveDirs(dirs: Set<string>): Promise<void> {
75
+ await this.store.set(DIRS_KEY, [...dirs]);
76
+ }
77
+
78
+ /** Ensure a directory (and all ancestors) are tracked. */
79
+ private async ensureDir(dirPath: string): Promise<void> {
80
+ if (!dirPath) return;
81
+ const dirs = await this.getDirs();
82
+ const parts = dirPath.split('/');
83
+ let current = '';
84
+ let changed = false;
85
+ for (const part of parts) {
86
+ current = current ? `${current}/${part}` : part;
87
+ if (!dirs.has(current)) {
88
+ dirs.add(current);
89
+ changed = true;
90
+ }
91
+ }
92
+ if (changed) {
93
+ await this.saveDirs(dirs);
94
+ }
95
+ }
96
+
97
+ // ── FileSystemProvider implementation ───────────────────────────
98
+
99
+ async readFile(path: string): Promise<string | null> {
100
+ const p = normalisePath(path);
101
+ return this.store.get<string>(contentKey(p));
102
+ }
103
+
104
+ async writeFile(path: string, content: string): Promise<void> {
105
+ const p = normalisePath(path);
106
+ const parent = parentDir(p);
107
+ if (parent) {
108
+ await this.ensureDir(parent);
109
+ }
110
+
111
+ const meta: FileMeta = {
112
+ name: baseName(p),
113
+ path: p,
114
+ size: new Blob([content]).size,
115
+ lastModified: new Date().toISOString(),
116
+ };
117
+
118
+ await this.store.set(contentKey(p), content);
119
+ await this.store.set(metaKey(p), meta);
120
+ }
121
+
122
+ async delete(path: string): Promise<void> {
123
+ const p = normalisePath(path);
124
+
125
+ // Remove file keys
126
+ await this.store.remove(contentKey(p));
127
+ await this.store.remove(binaryKey(p));
128
+ await this.store.remove(metaKey(p));
129
+
130
+ // If it was a directory, remove it and all children
131
+ const dirs = await this.getDirs();
132
+ if (dirs.has(p)) {
133
+ const prefix = p + '/';
134
+ const toRemove: string[] = [p];
135
+ for (const d of dirs) {
136
+ if (d.startsWith(prefix)) {
137
+ toRemove.push(d);
138
+ }
139
+ }
140
+ for (const d of toRemove) {
141
+ dirs.delete(d);
142
+ }
143
+ await this.saveDirs(dirs);
144
+
145
+ // Remove all file keys under this directory
146
+ const allKeys = await this.store.keys();
147
+ const filePrefix = `fs:${p}/`;
148
+ const keysToRemove = allKeys.filter((k) => k.startsWith(filePrefix));
149
+ await Promise.all(keysToRemove.map((k) => this.store.remove(k)));
150
+ }
151
+ }
152
+
153
+ async rename(oldPath: string, newPath: string): Promise<void> {
154
+ const op = normalisePath(oldPath);
155
+ const np = normalisePath(newPath);
156
+
157
+ // Read existing data
158
+ const content = await this.store.get<string>(contentKey(op));
159
+ const binary = await this.store.get<ArrayBuffer>(binaryKey(op));
160
+ const meta = await this.store.get<FileMeta>(metaKey(op));
161
+
162
+ // Write to new location
163
+ if (content !== null) {
164
+ await this.store.set(contentKey(np), content);
165
+ }
166
+ if (binary !== null) {
167
+ await this.store.set(binaryKey(np), binary);
168
+ }
169
+ if (meta) {
170
+ meta.name = baseName(np);
171
+ meta.path = np;
172
+ await this.store.set(metaKey(np), meta);
173
+ }
174
+
175
+ // Ensure parent dir of new path exists
176
+ const newParent = parentDir(np);
177
+ if (newParent) {
178
+ await this.ensureDir(newParent);
179
+ }
180
+
181
+ // Delete old
182
+ await this.store.remove(contentKey(op));
183
+ await this.store.remove(binaryKey(op));
184
+ await this.store.remove(metaKey(op));
185
+
186
+ // Handle directory rename
187
+ const dirs = await this.getDirs();
188
+ if (dirs.has(op)) {
189
+ dirs.delete(op);
190
+ dirs.add(np);
191
+ const oldPrefix = op + '/';
192
+ const newPrefix = np + '/';
193
+ for (const d of [...dirs]) {
194
+ if (d.startsWith(oldPrefix)) {
195
+ dirs.delete(d);
196
+ dirs.add(newPrefix + d.slice(oldPrefix.length));
197
+ }
198
+ }
199
+ await this.saveDirs(dirs);
200
+ }
201
+ }
202
+
203
+ async readDirectory(path: string): Promise<FileSystemEntry[]> {
204
+ const p = normalisePath(path);
205
+ const dirs = await this.getDirs();
206
+ const entries: FileSystemEntry[] = [];
207
+ const seen = new Set<string>();
208
+
209
+ // Find child directories
210
+ const prefix = p ? p + '/' : '';
211
+ for (const d of dirs) {
212
+ if (!p && !d.includes('/')) {
213
+ // Root-level directory
214
+ if (!seen.has(d)) {
215
+ seen.add(d);
216
+ entries.push({ kind: 'directory', name: d, path: d });
217
+ }
218
+ } else if (p && d.startsWith(prefix)) {
219
+ const rest = d.slice(prefix.length);
220
+ if (!rest.includes('/')) {
221
+ // Direct child directory
222
+ if (!seen.has(rest)) {
223
+ seen.add(rest);
224
+ entries.push({ kind: 'directory', name: rest, path: d });
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ // Find child files by scanning meta keys
231
+ const allKeys = await this.store.keys();
232
+ const metaPrefix = p ? `fs:${p}/` : 'fs:';
233
+ const metaSuffix = ':meta';
234
+
235
+ for (const key of allKeys) {
236
+ if (!key.startsWith(metaPrefix) || !key.endsWith(metaSuffix)) continue;
237
+
238
+ const filePath = key.slice(3, -metaSuffix.length); // strip "fs:" and ":meta"
239
+ const rel = p ? filePath.slice(prefix.length) : filePath;
240
+
241
+ // Only direct children (no further slashes)
242
+ if (rel.includes('/')) continue;
243
+
244
+ if (!seen.has(rel)) {
245
+ seen.add(rel);
246
+ entries.push({ kind: 'file', name: rel, path: filePath });
247
+ }
248
+ }
249
+
250
+ // Sort: directories first, then alphabetical
251
+ entries.sort((a, b) => {
252
+ if (a.kind !== b.kind) return a.kind === 'directory' ? -1 : 1;
253
+ return a.name.localeCompare(b.name);
254
+ });
255
+
256
+ return entries;
257
+ }
258
+
259
+ async exists(path: string): Promise<boolean> {
260
+ const p = normalisePath(path);
261
+ const dirs = await this.getDirs();
262
+ if (dirs.has(p)) return true;
263
+ const meta = await this.store.get(metaKey(p));
264
+ return meta !== null;
265
+ }
266
+
267
+ async createDirectory(path: string): Promise<void> {
268
+ const p = normalisePath(path);
269
+ await this.ensureDir(p);
270
+ }
271
+
272
+ async stat(path: string): Promise<FileMeta | null> {
273
+ const p = normalisePath(path);
274
+ return this.store.get<FileMeta>(metaKey(p));
275
+ }
276
+
277
+ async readBinary(path: string): Promise<ArrayBuffer | null> {
278
+ const p = normalisePath(path);
279
+ return this.store.get<ArrayBuffer>(binaryKey(p));
280
+ }
281
+
282
+ async writeBinary(path: string, data: ArrayBuffer | Uint8Array): Promise<void> {
283
+ const p = normalisePath(path);
284
+ const parent = parentDir(p);
285
+ if (parent) {
286
+ await this.ensureDir(parent);
287
+ }
288
+
289
+ const meta: FileMeta = {
290
+ name: baseName(p),
291
+ path: p,
292
+ size: data.byteLength,
293
+ lastModified: new Date().toISOString(),
294
+ };
295
+
296
+ await this.store.set(binaryKey(p), data);
297
+ await this.store.set(metaKey(p), meta);
298
+ }
299
+ }