vimd 0.4.1 → 0.5.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.
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Folder mode client-side JavaScript
3
+ */
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // Storage keys
8
+ var STORAGE_KEY_EXPANDED = 'vimd-folder-expanded';
9
+ var STORAGE_KEY_WIDTH = 'vimd-sidebar-width';
10
+
11
+ // State
12
+ var fileTree = [];
13
+ var currentPath = null;
14
+ var ws = null;
15
+ var expandedFolders = new Set();
16
+ var isResizing = false;
17
+
18
+ // DOM elements
19
+ var sidebar = document.getElementById('sidebar');
20
+ var toggleBar = document.getElementById('toggle-bar');
21
+ var toggleBtn = document.getElementById('toggle-btn');
22
+ var toggleBtnCollapsed = document.getElementById('toggle-btn-collapsed');
23
+ var fileTreeEl = document.getElementById('file-tree');
24
+ var welcome = document.getElementById('welcome');
25
+ var welcomeMessage = document.getElementById('welcome-message');
26
+ var content = document.getElementById('content');
27
+ var resizer = document.getElementById('resizer');
28
+
29
+ /**
30
+ * Initialize the application
31
+ */
32
+ function init() {
33
+ loadState();
34
+ connectWebSocket();
35
+ setupEventListeners();
36
+ handleInitialPath();
37
+ }
38
+
39
+ /**
40
+ * Load saved state from localStorage
41
+ */
42
+ function loadState() {
43
+ // Load expanded folders
44
+ try {
45
+ var saved = localStorage.getItem(STORAGE_KEY_EXPANDED);
46
+ if (saved) {
47
+ var arr = JSON.parse(saved);
48
+ expandedFolders = new Set(arr);
49
+ }
50
+ } catch (e) {
51
+ console.warn('[vimd] Failed to load expanded state:', e);
52
+ }
53
+
54
+ // Load sidebar width
55
+ try {
56
+ var width = localStorage.getItem(STORAGE_KEY_WIDTH);
57
+ if (width) {
58
+ sidebar.style.width = width + 'px';
59
+ }
60
+ } catch (e) {
61
+ console.warn('[vimd] Failed to load sidebar width:', e);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Save expanded state to localStorage
67
+ */
68
+ function saveExpandedState() {
69
+ try {
70
+ localStorage.setItem(STORAGE_KEY_EXPANDED, JSON.stringify(Array.from(expandedFolders)));
71
+ } catch (e) {
72
+ console.warn('[vimd] Failed to save expanded state:', e);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Save sidebar width to localStorage
78
+ */
79
+ function saveSidebarWidth() {
80
+ try {
81
+ var width = parseInt(sidebar.style.width, 10);
82
+ if (width) {
83
+ localStorage.setItem(STORAGE_KEY_WIDTH, width.toString());
84
+ }
85
+ } catch (e) {
86
+ console.warn('[vimd] Failed to save sidebar width:', e);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Connect to WebSocket server
92
+ */
93
+ function connectWebSocket() {
94
+ var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
95
+ ws = new WebSocket(protocol + '//' + location.host);
96
+
97
+ ws.onopen = function() {
98
+ console.log('[vimd] WebSocket connected');
99
+ };
100
+
101
+ ws.onmessage = function(event) {
102
+ try {
103
+ var msg = JSON.parse(event.data);
104
+ handleMessage(msg);
105
+ } catch (e) {
106
+ console.error('[vimd] Failed to parse message:', e);
107
+ }
108
+ };
109
+
110
+ ws.onclose = function() {
111
+ console.log('[vimd] WebSocket disconnected, reconnecting...');
112
+ setTimeout(connectWebSocket, 1000);
113
+ };
114
+
115
+ ws.onerror = function(error) {
116
+ console.error('[vimd] WebSocket error:', error);
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Handle incoming WebSocket message
122
+ */
123
+ function handleMessage(msg) {
124
+ switch (msg.type) {
125
+ case 'tree':
126
+ fileTree = msg.data;
127
+ renderTree();
128
+ updateWelcomeMessage();
129
+ break;
130
+
131
+ case 'content':
132
+ showContent(msg.data.path, msg.data.html);
133
+ break;
134
+
135
+ case 'reload':
136
+ location.reload();
137
+ break;
138
+
139
+ case 'error':
140
+ showError(msg.data.type, msg.data.message);
141
+ break;
142
+
143
+ case 'fileDeleted':
144
+ if (currentPath === msg.data.path) {
145
+ showWelcome();
146
+ currentPath = null;
147
+ updateURL('/');
148
+ }
149
+ break;
150
+
151
+ default:
152
+ console.warn('[vimd] Unknown message type:', msg.type);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Render the file tree
158
+ */
159
+ function renderTree() {
160
+ fileTreeEl.innerHTML = '';
161
+
162
+ if (fileTree.length === 0) {
163
+ return;
164
+ }
165
+
166
+ fileTree.forEach(function(node) {
167
+ var el = createTreeNode(node, 0);
168
+ fileTreeEl.appendChild(el);
169
+ });
170
+
171
+ // Restore selection if current path exists
172
+ if (currentPath) {
173
+ updateSelection(currentPath);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Create a tree node element
179
+ */
180
+ function createTreeNode(node, depth) {
181
+ var container = document.createElement('div');
182
+ container.className = 'vimd-tree-node';
183
+
184
+ var item = document.createElement('div');
185
+ item.className = 'vimd-tree-item';
186
+ item.setAttribute('data-path', node.path);
187
+ item.setAttribute('data-depth', depth.toString());
188
+
189
+ if (node.type === 'folder') {
190
+ // Folder node
191
+ var isExpanded = depth === 0 || expandedFolders.has(node.path);
192
+
193
+ // Chevron
194
+ var chevron = document.createElement('span');
195
+ chevron.className = 'vimd-tree-chevron' + (isExpanded ? '' : ' collapsed');
196
+ chevron.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>';
197
+ item.appendChild(chevron);
198
+
199
+ // Folder icon
200
+ var icon = document.createElement('span');
201
+ icon.className = 'vimd-tree-icon';
202
+ icon.innerHTML = isExpanded
203
+ ? '<svg width="16" height="16" viewBox="0 0 16 16" fill="#dcb67a"><path d="M1.5 3A1.5 1.5 0 013 1.5h3.586a1.5 1.5 0 011.06.44l.708.706a.5.5 0 00.353.147H13a1.5 1.5 0 011.5 1.5v.5H1.5V3z"/><path d="M1.5 5h13v7.5a1.5 1.5 0 01-1.5 1.5H3a1.5 1.5 0 01-1.5-1.5V5z"/></svg>'
204
+ : '<svg width="16" height="16" viewBox="0 0 16 16" fill="#dcb67a"><path d="M1.5 3A1.5 1.5 0 013 1.5h3.586a1.5 1.5 0 011.06.44l.708.706a.5.5 0 00.353.147H13a1.5 1.5 0 011.5 1.5v8a1.5 1.5 0 01-1.5 1.5H3a1.5 1.5 0 01-1.5-1.5V3z"/></svg>';
205
+ item.appendChild(icon);
206
+
207
+ // Folder name
208
+ var name = document.createElement('span');
209
+ name.className = 'vimd-tree-name';
210
+ name.textContent = node.name;
211
+ item.appendChild(name);
212
+
213
+ // Click handler for folder
214
+ item.addEventListener('click', function(e) {
215
+ e.stopPropagation();
216
+ toggleFolder(node.path, container, chevron, icon);
217
+ });
218
+
219
+ container.appendChild(item);
220
+
221
+ // Children container
222
+ var children = document.createElement('div');
223
+ children.className = 'vimd-tree-children' + (isExpanded ? '' : ' collapsed');
224
+
225
+ node.children.forEach(function(child) {
226
+ var childEl = createTreeNode(child, depth + 1);
227
+ children.appendChild(childEl);
228
+ });
229
+
230
+ container.appendChild(children);
231
+
232
+ // Initialize expanded state
233
+ if (isExpanded && depth > 0) {
234
+ expandedFolders.add(node.path);
235
+ }
236
+
237
+ } else {
238
+ // File node
239
+ // File icon
240
+ var fileIcon = document.createElement('span');
241
+ fileIcon.className = 'vimd-tree-icon';
242
+ fileIcon.innerHTML = getFileIcon(node.extension);
243
+ item.appendChild(fileIcon);
244
+
245
+ // File name
246
+ var fileName = document.createElement('span');
247
+ fileName.className = 'vimd-tree-name';
248
+ fileName.textContent = node.name;
249
+ item.appendChild(fileName);
250
+
251
+ // Click handler for file
252
+ item.addEventListener('click', function(e) {
253
+ e.stopPropagation();
254
+ selectFile(node.path);
255
+ });
256
+
257
+ container.appendChild(item);
258
+ }
259
+
260
+ return container;
261
+ }
262
+
263
+ /**
264
+ * Get file icon SVG based on extension
265
+ */
266
+ function getFileIcon(ext) {
267
+ if (ext === '.md') {
268
+ // Markdown icon
269
+ return '<svg width="16" height="16" viewBox="0 0 16 16" fill="#519aba"><path d="M2 3.5A1.5 1.5 0 013.5 2h9A1.5 1.5 0 0114 3.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 012 12.5v-9zM3.5 3a.5.5 0 00-.5.5v9a.5.5 0 00.5.5h9a.5.5 0 00.5-.5v-9a.5.5 0 00-.5-.5h-9z"/><path d="M4 6v4h1V7.5l1 1.5 1-1.5V10h1V6H7l-1 1.5L5 6H4zm5 0v4h1V8h1V7H10V6H9zm3 0v4h1V6h-1z"/></svg>';
270
+ } else {
271
+ // TeX/LaTeX icon
272
+ return '<svg width="16" height="16" viewBox="0 0 16 16" fill="#3d8137"><path d="M2 3.5A1.5 1.5 0 013.5 2h9A1.5 1.5 0 0114 3.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 012 12.5v-9zM3.5 3a.5.5 0 00-.5.5v9a.5.5 0 00.5.5h9a.5.5 0 00.5-.5v-9a.5.5 0 00-.5-.5h-9z"/><path d="M4 5h3v1H5.5v4h2V9H9v2H4V5zm5 0h3v1h-1v5h-1V6H9V5z"/></svg>';
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Toggle folder expanded/collapsed state
278
+ */
279
+ function toggleFolder(path, container, chevron, icon) {
280
+ var children = container.querySelector('.vimd-tree-children');
281
+ var isExpanded = !children.classList.contains('collapsed');
282
+
283
+ if (isExpanded) {
284
+ children.classList.add('collapsed');
285
+ chevron.classList.add('collapsed');
286
+ icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="#dcb67a"><path d="M1.5 3A1.5 1.5 0 013 1.5h3.586a1.5 1.5 0 011.06.44l.708.706a.5.5 0 00.353.147H13a1.5 1.5 0 011.5 1.5v8a1.5 1.5 0 01-1.5 1.5H3a1.5 1.5 0 01-1.5-1.5V3z"/></svg>';
287
+ expandedFolders.delete(path);
288
+ } else {
289
+ children.classList.remove('collapsed');
290
+ chevron.classList.remove('collapsed');
291
+ icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="#dcb67a"><path d="M1.5 3A1.5 1.5 0 013 1.5h3.586a1.5 1.5 0 011.06.44l.708.706a.5.5 0 00.353.147H13a1.5 1.5 0 011.5 1.5v.5H1.5V3z"/><path d="M1.5 5h13v7.5a1.5 1.5 0 01-1.5 1.5H3a1.5 1.5 0 01-1.5-1.5V5z"/></svg>';
292
+ expandedFolders.add(path);
293
+ }
294
+
295
+ saveExpandedState();
296
+ }
297
+
298
+ /**
299
+ * Select a file
300
+ */
301
+ function selectFile(path) {
302
+ if (currentPath === path) {
303
+ return;
304
+ }
305
+
306
+ // Update selection
307
+ updateSelection(path);
308
+
309
+ // Send to server
310
+ if (ws && ws.readyState === WebSocket.OPEN) {
311
+ ws.send(JSON.stringify({ type: 'selectFile', path: path }));
312
+ }
313
+
314
+ // Update URL
315
+ updateURL('/' + encodeURIComponent(path).replace(/%2F/g, '/'));
316
+
317
+ currentPath = path;
318
+ }
319
+
320
+ /**
321
+ * Update visual selection in tree
322
+ */
323
+ function updateSelection(path) {
324
+ // Remove previous selection
325
+ var selected = fileTreeEl.querySelectorAll('.vimd-tree-item.selected');
326
+ selected.forEach(function(el) {
327
+ el.classList.remove('selected');
328
+ });
329
+
330
+ // Add new selection
331
+ var item = fileTreeEl.querySelector('.vimd-tree-item[data-path="' + path + '"]');
332
+ if (item) {
333
+ item.classList.add('selected');
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Show file content
339
+ */
340
+ function showContent(path, html) {
341
+ welcome.classList.add('hidden');
342
+ content.classList.add('visible');
343
+ content.innerHTML = html;
344
+
345
+ // Update selection
346
+ updateSelection(path);
347
+ currentPath = path;
348
+
349
+ // Re-render MathJax if available
350
+ if (window.MathJax && window.MathJax.typeset) {
351
+ window.MathJax.typeset([content]);
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Show welcome screen
357
+ */
358
+ function showWelcome() {
359
+ content.classList.remove('visible');
360
+ content.innerHTML = '';
361
+ welcome.classList.remove('hidden');
362
+ }
363
+
364
+ /**
365
+ * Show error message
366
+ */
367
+ function showError(type, message) {
368
+ welcome.classList.add('hidden');
369
+ content.classList.add('visible');
370
+ content.innerHTML = '<div class="vimd-error"><h2>Error</h2><p>' + escapeHtml(message) + '</p></div>';
371
+ }
372
+
373
+ /**
374
+ * Update welcome message based on file tree
375
+ */
376
+ function updateWelcomeMessage() {
377
+ if (fileTree.length === 0) {
378
+ welcomeMessage.textContent = 'No markdown or LaTeX files found';
379
+ } else {
380
+ welcomeMessage.textContent = 'Select a file from the sidebar';
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Update URL without page reload
386
+ */
387
+ function updateURL(path) {
388
+ history.pushState({ path: path }, '', path);
389
+ }
390
+
391
+ /**
392
+ * Handle initial URL path
393
+ */
394
+ function handleInitialPath() {
395
+ var path = decodeURIComponent(location.pathname.slice(1));
396
+ if (path && path !== '') {
397
+ // Wait for tree to load then select file
398
+ var checkTree = setInterval(function() {
399
+ if (fileTree.length > 0) {
400
+ clearInterval(checkTree);
401
+ selectFile(path);
402
+ }
403
+ }, 100);
404
+
405
+ // Timeout after 5 seconds
406
+ setTimeout(function() {
407
+ clearInterval(checkTree);
408
+ }, 5000);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Setup event listeners
414
+ */
415
+ function setupEventListeners() {
416
+ // Sidebar toggle buttons
417
+ toggleBtn.addEventListener('click', function() {
418
+ collapseSidebar();
419
+ });
420
+
421
+ toggleBtnCollapsed.addEventListener('click', function() {
422
+ expandSidebar();
423
+ });
424
+
425
+ // Keyboard shortcut (Ctrl+B or Cmd+B)
426
+ document.addEventListener('keydown', function(e) {
427
+ if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
428
+ e.preventDefault();
429
+ if (sidebar.classList.contains('collapsed')) {
430
+ expandSidebar();
431
+ } else {
432
+ collapseSidebar();
433
+ }
434
+ }
435
+ });
436
+
437
+ // Resizer
438
+ resizer.addEventListener('mousedown', function(e) {
439
+ e.preventDefault();
440
+ isResizing = true;
441
+ resizer.classList.add('active');
442
+ document.body.style.cursor = 'ew-resize';
443
+ document.body.style.userSelect = 'none';
444
+ });
445
+
446
+ document.addEventListener('mousemove', function(e) {
447
+ if (!isResizing) return;
448
+
449
+ var width = e.clientX;
450
+ var minWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-min-width'), 10) || 150;
451
+ var maxWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-max-width'), 10) || 500;
452
+
453
+ if (width >= minWidth && width <= maxWidth) {
454
+ sidebar.style.width = width + 'px';
455
+ }
456
+ });
457
+
458
+ document.addEventListener('mouseup', function() {
459
+ if (isResizing) {
460
+ isResizing = false;
461
+ resizer.classList.remove('active');
462
+ document.body.style.cursor = '';
463
+ document.body.style.userSelect = '';
464
+ saveSidebarWidth();
465
+ }
466
+ });
467
+
468
+ // Browser history
469
+ window.addEventListener('popstate', function(e) {
470
+ if (e.state && e.state.path) {
471
+ var path = e.state.path.slice(1); // Remove leading /
472
+ if (path) {
473
+ selectFile(path);
474
+ } else {
475
+ showWelcome();
476
+ currentPath = null;
477
+ }
478
+ }
479
+ });
480
+ }
481
+
482
+ /**
483
+ * Collapse sidebar
484
+ */
485
+ function collapseSidebar() {
486
+ sidebar.classList.add('collapsed');
487
+ toggleBar.classList.add('visible');
488
+ }
489
+
490
+ /**
491
+ * Expand sidebar
492
+ */
493
+ function expandSidebar() {
494
+ sidebar.classList.remove('collapsed');
495
+ toggleBar.classList.remove('visible');
496
+ }
497
+
498
+ /**
499
+ * Escape HTML special characters
500
+ */
501
+ function escapeHtml(text) {
502
+ var div = document.createElement('div');
503
+ div.textContent = text;
504
+ return div.innerHTML;
505
+ }
506
+
507
+ // Initialize when DOM is ready
508
+ if (document.readyState === 'loading') {
509
+ document.addEventListener('DOMContentLoaded', init);
510
+ } else {
511
+ init();
512
+ }
513
+ })();
@@ -0,0 +1,84 @@
1
+ import type { FolderModeOptions, ServerMessage } from './types.js';
2
+ /**
3
+ * Result of server start operation
4
+ */
5
+ export interface ServerStartResult {
6
+ actualPort: number;
7
+ requestedPort: number;
8
+ portChanged: boolean;
9
+ }
10
+ /**
11
+ * Folder mode server for multi-file preview
12
+ */
13
+ export declare class FolderModeServer {
14
+ private httpServer;
15
+ private wsServer;
16
+ private clients;
17
+ private scanner;
18
+ private options;
19
+ private _port;
20
+ private fileTree;
21
+ private renderedHtml;
22
+ constructor(options: FolderModeOptions);
23
+ /**
24
+ * Get the actual port the server is running on
25
+ */
26
+ get port(): number;
27
+ /**
28
+ * Get the root path
29
+ */
30
+ get rootPath(): string;
31
+ /**
32
+ * Start the HTTP and WebSocket servers
33
+ */
34
+ start(): Promise<ServerStartResult>;
35
+ /**
36
+ * Stop the server
37
+ */
38
+ stop(): Promise<void>;
39
+ /**
40
+ * Handle new WebSocket connection
41
+ */
42
+ private handleConnection;
43
+ /**
44
+ * Handle message from client
45
+ */
46
+ private handleClientMessage;
47
+ /**
48
+ * Handle file selection
49
+ */
50
+ private handleSelectFile;
51
+ /**
52
+ * Convert a file to HTML
53
+ */
54
+ private convertFile;
55
+ /**
56
+ * Validate path and return absolute path if valid
57
+ */
58
+ private validatePath;
59
+ /**
60
+ * Send message to a specific client
61
+ */
62
+ private sendMessage;
63
+ /**
64
+ * Broadcast message to all clients
65
+ */
66
+ broadcast(message: ServerMessage): void;
67
+ /**
68
+ * Serve folder mode HTML
69
+ */
70
+ private serveFolderModeHtml;
71
+ /**
72
+ * Render the folder mode template
73
+ */
74
+ private renderTemplate;
75
+ /**
76
+ * Escape HTML special characters
77
+ */
78
+ private escapeHtml;
79
+ /**
80
+ * Count total files in tree
81
+ */
82
+ private countFiles;
83
+ }
84
+ //# sourceMappingURL=folder-mode-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folder-mode-server.d.ts","sourceRoot":"","sources":["../../../src/core/folder-mode/folder-mode-server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,iBAAiB,EAEjB,aAAa,EAEd,MAAM,YAAY,CAAC;AAKpB;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAUD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,OAAO,CAA0C;IACzD,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,YAAY,CAAuB;gBAE/B,OAAO,EAAE,iBAAiB;IAMtC;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAuEzC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAuCxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAU3B;;OAEG;YACW,gBAAgB;IAuF9B;;OAEG;YACW,WAAW;IASzB;;OAEG;IACH,OAAO,CAAC,YAAY;IAoBpB;;OAEG;IACH,OAAO,CAAC,WAAW;IAMnB;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IASvC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B;;OAEG;YACW,cAAc;IAkC5B;;OAEG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,UAAU;CAWnB"}