vimd 0.5.4 → 0.5.6

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.
@@ -9,6 +9,9 @@
9
9
  var STORAGE_KEY_SIDEBAR_WIDTH = 'vimd-sidebar-width';
10
10
  var STORAGE_KEY_STATE = 'vimd-folder-mode-state';
11
11
 
12
+ // Supported file extensions for link navigation
13
+ var SUPPORTED_EXTENSIONS = ['.md', '.tex', '.latex'];
14
+
12
15
  // State
13
16
  var fileTree = [];
14
17
  var ws = null;
@@ -274,6 +277,95 @@
274
277
  return search(fileTree);
275
278
  }
276
279
 
280
+ /**
281
+ * Get file extension from path
282
+ */
283
+ function getExtension(path) {
284
+ // Remove query string and hash
285
+ var cleanPath = path.split('?')[0].split('#')[0];
286
+ var lastDot = cleanPath.lastIndexOf('.');
287
+ if (lastDot === -1) {
288
+ return '';
289
+ }
290
+ return cleanPath.substring(lastDot).toLowerCase();
291
+ }
292
+
293
+ /**
294
+ * Check if href is an internal link to a supported file
295
+ */
296
+ function isInternalLink(href) {
297
+ // Ignore external links (http://, https://, //, mailto:, etc.)
298
+ if (/^(https?:)?\/\/|^mailto:|^tel:|^javascript:/i.test(href)) {
299
+ return false;
300
+ }
301
+
302
+ // Ignore anchor links
303
+ if (href.startsWith('#')) {
304
+ return false;
305
+ }
306
+
307
+ // Check if it's a supported file extension
308
+ var ext = getExtension(href);
309
+ return SUPPORTED_EXTENSIONS.indexOf(ext) !== -1;
310
+ }
311
+
312
+ /**
313
+ * Resolve relative path
314
+ */
315
+ function resolvePath(basePath, relativePath) {
316
+ // Handle absolute paths (starting with /)
317
+ if (relativePath.startsWith('/')) {
318
+ return relativePath.substring(1);
319
+ }
320
+
321
+ // Combine base and relative
322
+ var combined = basePath + relativePath;
323
+
324
+ // Normalize path (handle ../ and ./)
325
+ var parts = combined.split('/');
326
+ var result = [];
327
+
328
+ for (var i = 0; i < parts.length; i++) {
329
+ var part = parts[i];
330
+ if (part === '..') {
331
+ result.pop();
332
+ } else if (part !== '.' && part !== '') {
333
+ result.push(part);
334
+ }
335
+ }
336
+
337
+ return result.join('/');
338
+ }
339
+
340
+ /**
341
+ * Handle internal link click
342
+ */
343
+ function handleInternalLink(href) {
344
+ // Get current file's directory
345
+ var currentFile = state.panels[state.activePanel].file;
346
+ var currentDir = '';
347
+
348
+ if (currentFile) {
349
+ var lastSlash = currentFile.lastIndexOf('/');
350
+ if (lastSlash !== -1) {
351
+ currentDir = currentFile.substring(0, lastSlash + 1);
352
+ }
353
+ }
354
+
355
+ // Resolve relative path
356
+ var targetPath = resolvePath(currentDir, href);
357
+
358
+ // Remove query string and hash for file selection
359
+ targetPath = targetPath.split('?')[0].split('#')[0];
360
+
361
+ // Check if file exists in tree
362
+ if (fileExists(targetPath)) {
363
+ selectFile(targetPath);
364
+ } else {
365
+ console.warn('[vimd] File not found:', targetPath);
366
+ }
367
+ }
368
+
277
369
  /**
278
370
  * Handle file deleted event
279
371
  */
@@ -714,6 +806,24 @@
714
806
  }
715
807
  }
716
808
  });
809
+
810
+ // Preview content link handler (event delegation)
811
+ preview.addEventListener('click', function(e) {
812
+ var target = e.target;
813
+
814
+ // Find closest anchor element
815
+ while (target && target !== preview) {
816
+ if (target.tagName === 'A') {
817
+ var href = target.getAttribute('href');
818
+ if (href && isInternalLink(href)) {
819
+ e.preventDefault();
820
+ handleInternalLink(href);
821
+ }
822
+ return;
823
+ }
824
+ target = target.parentElement;
825
+ }
826
+ });
717
827
  }
718
828
 
719
829
  /**
@@ -19,6 +19,8 @@ export declare class FolderModeServer {
19
19
  private _port;
20
20
  private fileTree;
21
21
  private renderedHtml;
22
+ private folderWatcher;
23
+ private treeUpdateTimer;
22
24
  constructor(options: FolderModeOptions);
23
25
  /**
24
26
  * Get the actual port the server is running on
@@ -84,5 +86,17 @@ export declare class FolderModeServer {
84
86
  * Count total files in tree
85
87
  */
86
88
  private countFiles;
89
+ /**
90
+ * Start watching the folder for file changes
91
+ */
92
+ private startFolderWatcher;
93
+ /**
94
+ * Handle file system changes (add, unlink, etc.)
95
+ */
96
+ private handleFileSystemChange;
97
+ /**
98
+ * Update file tree and broadcast to all clients
99
+ */
100
+ private updateFileTree;
87
101
  }
88
102
  //# sourceMappingURL=folder-mode-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"folder-mode-server.d.ts","sourceRoot":"","sources":["../../../src/core/folder-mode/folder-mode-server.ts"],"names":[],"mappings":"AAcA,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;IAiGzC;;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,cAAc;IAYtB;;OAEG;IACH,OAAO,CAAC,UAAU;CAWnB"}
1
+ {"version":3,"file":"folder-mode-server.d.ts","sourceRoot":"","sources":["../../../src/core/folder-mode/folder-mode-server.ts"],"names":[],"mappings":"AAeA,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;AAeD;;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;IAC3C,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,eAAe,CAA+B;gBAE1C,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;IAoGzC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C3B;;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,cAAc;IAYtB;;OAEG;IACH,OAAO,CAAC,UAAU;IAYlB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuC1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAsB9B;;OAEG;YACW,cAAc;CAgB7B"}
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
4
4
  import { fileURLToPath } from 'url';
5
5
  import polka from 'polka';
6
6
  import sirv from 'sirv';
7
+ import chokidar from 'chokidar';
7
8
  import { WebSocketServer as WSServer, WebSocket } from 'ws';
8
9
  import { Logger } from '../../utils/logger.js';
9
10
  import { SessionManager } from '../../utils/session-manager.js';
@@ -14,6 +15,10 @@ import { FileWatcher } from '../watcher.js';
14
15
  import { FolderScanner } from './folder-scanner.js';
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
18
+ /**
19
+ * Supported extensions for tree updates
20
+ */
21
+ const TREE_EXTENSIONS = ['.md', '.tex', '.latex'];
17
22
  /**
18
23
  * Folder mode server for multi-file preview
19
24
  */
@@ -24,6 +29,8 @@ export class FolderModeServer {
24
29
  this.clients = new Map();
25
30
  this.fileTree = [];
26
31
  this.renderedHtml = null;
32
+ this.folderWatcher = null;
33
+ this.treeUpdateTimer = null;
27
34
  this.options = options;
28
35
  this._port = options.port;
29
36
  this.scanner = new FolderScanner({ rootPath: options.rootPath });
@@ -55,6 +62,8 @@ export class FolderModeServer {
55
62
  // Initial scan
56
63
  this.fileTree = await this.scanner.scan();
57
64
  Logger.info(`Found ${this.countFiles(this.fileTree)} files in folder`);
65
+ // Start watching for file changes
66
+ this.startFolderWatcher();
58
67
  // Render template
59
68
  this.renderedHtml = await this.renderTemplate();
60
69
  // Create polka app
@@ -123,6 +132,16 @@ export class FolderModeServer {
123
132
  * Stop the server
124
133
  */
125
134
  async stop() {
135
+ // Stop folder watcher
136
+ if (this.folderWatcher) {
137
+ await this.folderWatcher.close();
138
+ this.folderWatcher = null;
139
+ }
140
+ // Clear tree update timer
141
+ if (this.treeUpdateTimer) {
142
+ clearTimeout(this.treeUpdateTimer);
143
+ this.treeUpdateTimer = null;
144
+ }
126
145
  // Terminate all WebSocket clients
127
146
  for (const [client] of this.clients) {
128
147
  try {
@@ -408,4 +427,78 @@ export class FolderModeServer {
408
427
  }
409
428
  return count;
410
429
  }
430
+ /**
431
+ * Start watching the folder for file changes
432
+ */
433
+ startFolderWatcher() {
434
+ const watchOptions = {
435
+ ignored: [
436
+ '**/node_modules/**',
437
+ '**/.git/**',
438
+ '**/dist/**',
439
+ '**/build/**',
440
+ '**/vimd-preview-*.html',
441
+ ],
442
+ persistent: true,
443
+ ignoreInitial: true,
444
+ depth: 99,
445
+ };
446
+ this.folderWatcher = chokidar.watch(this.options.rootPath, watchOptions);
447
+ // File added
448
+ this.folderWatcher.on('add', (filePath) => {
449
+ this.handleFileSystemChange(filePath, 'add');
450
+ });
451
+ // File removed
452
+ this.folderWatcher.on('unlink', (filePath) => {
453
+ this.handleFileSystemChange(filePath, 'unlink');
454
+ });
455
+ // Directory added
456
+ this.folderWatcher.on('addDir', (dirPath) => {
457
+ this.handleFileSystemChange(dirPath, 'addDir');
458
+ });
459
+ // Directory removed
460
+ this.folderWatcher.on('unlinkDir', (dirPath) => {
461
+ this.handleFileSystemChange(dirPath, 'unlinkDir');
462
+ });
463
+ Logger.info('Folder watcher started');
464
+ }
465
+ /**
466
+ * Handle file system changes (add, unlink, etc.)
467
+ */
468
+ handleFileSystemChange(filePath, eventType) {
469
+ const ext = path.extname(filePath).toLowerCase();
470
+ // Only react to supported extensions or directory changes
471
+ const isDirectory = eventType.includes('Dir');
472
+ const isSupportedFile = TREE_EXTENSIONS.includes(ext);
473
+ if (!isDirectory && !isSupportedFile) {
474
+ return;
475
+ }
476
+ // Debounce tree updates (300ms)
477
+ if (this.treeUpdateTimer) {
478
+ clearTimeout(this.treeUpdateTimer);
479
+ }
480
+ this.treeUpdateTimer = setTimeout(async () => {
481
+ await this.updateFileTree();
482
+ this.treeUpdateTimer = null;
483
+ }, 300);
484
+ }
485
+ /**
486
+ * Update file tree and broadcast to all clients
487
+ */
488
+ async updateFileTree() {
489
+ try {
490
+ const newTree = await this.scanner.scan();
491
+ const fileCount = this.countFiles(newTree);
492
+ // Only update if tree actually changed
493
+ if (JSON.stringify(newTree) !== JSON.stringify(this.fileTree)) {
494
+ this.fileTree = newTree;
495
+ this.broadcast({ type: 'tree', data: this.getWrappedTree() });
496
+ Logger.info(`File tree updated: ${fileCount} files`);
497
+ }
498
+ }
499
+ catch (error) {
500
+ const message = error instanceof Error ? error.message : 'Unknown error';
501
+ Logger.error(`Failed to update file tree: ${message}`);
502
+ }
503
+ }
411
504
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimd",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Real-time Markdown preview tool with pandoc (view markdown)",
5
5
  "type": "module",
6
6
  "keywords": [