mdv-live 0.3.9 → 0.4.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/bin/mdv.js CHANGED
@@ -23,6 +23,10 @@ const OPTIONS = {
23
23
  type: 'string',
24
24
  short: 'p',
25
25
  },
26
+ depth: {
27
+ type: 'string',
28
+ short: 'd',
29
+ },
26
30
  'no-browser': {
27
31
  type: 'boolean',
28
32
  default: false
@@ -76,6 +80,7 @@ Arguments:
76
80
 
77
81
  Server Options:
78
82
  -p, --port <n> Server port (default: ${DEFAULT_PORT})
83
+ -d, --depth <n> Directory watch depth (default: 3, prevents EMFILE errors)
79
84
  --no-browser Don't open browser automatically
80
85
 
81
86
  Server Management:
@@ -382,8 +387,9 @@ async function resolveTargetPath(targetPath) {
382
387
  * @param {string} targetPath - Target directory or file path
383
388
  * @param {number} startPort - Starting port number
384
389
  * @param {boolean} openBrowser - Whether to open browser automatically
390
+ * @param {number} depth - Directory watch depth
385
391
  */
386
- async function startViewer(targetPath, startPort, openBrowser) {
392
+ async function startViewer(targetPath, startPort, openBrowser, depth) {
387
393
  const { rootDir, initialFile } = await resolveTargetPath(targetPath);
388
394
 
389
395
  const port = await findAvailablePort(startPort);
@@ -396,11 +402,11 @@ async function startViewer(targetPath, startPort, openBrowser) {
396
402
  console.log(`ポート ${startPort} は使用中のため、${port} で起動します`);
397
403
  }
398
404
 
399
- const mdv = createMdvServer({ rootDir, port });
405
+ const mdv = createMdvServer({ rootDir, port, depth });
400
406
  await mdv.start();
401
407
 
402
408
  const url = initialFile
403
- ? `http://localhost:${port}?file=${encodeURIComponent(initialFile)}`
409
+ ? `http://localhost:${port}?path=${encodeURIComponent(initialFile)}`
404
410
  : `http://localhost:${port}`;
405
411
 
406
412
  console.log(`
@@ -472,9 +478,10 @@ async function main() {
472
478
  // Default: start viewer
473
479
  const targetPath = positionals[0] || '.';
474
480
  const port = parseInt(values.port, 10) || DEFAULT_PORT;
481
+ const depth = parseInt(values.depth, 10) || 3;
475
482
  const openBrowser = !values['no-browser'];
476
483
 
477
- await startViewer(targetPath, port, openBrowser);
484
+ await startViewer(targetPath, port, openBrowser, depth);
478
485
  }
479
486
 
480
487
  main().catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdv-live",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -77,7 +77,7 @@ for i in {1..25}; do
77
77
  echo "Found port: $PORT"
78
78
  echo "Opening browser..."
79
79
  # Use osascript to open URL (works better from AppleScript context)
80
- osascript -e "open location \"http://localhost:$PORT?file=$FILE_NAME\""
80
+ osascript -e "open location \"http://localhost:$PORT?path=$FILE_NAME\""
81
81
  echo "Done"
82
82
  exit 0
83
83
  fi
@@ -85,7 +85,7 @@ done
85
85
 
86
86
  echo "Timeout - using fallback"
87
87
  # Fallback
88
- osascript -e "open location \"http://localhost:8642?file=$FILE_NAME\""
88
+ osascript -e "open location \"http://localhost:8642?path=$FILE_NAME\""
89
89
  LAUNCHSCRIPT
90
90
 
91
91
  # Replace placeholder with actual path
package/src/server.js CHANGED
@@ -47,10 +47,11 @@ function setupApiRoutes(app) {
47
47
  * @param {Object} options - Server options
48
48
  * @param {string} options.rootDir - Root directory to serve
49
49
  * @param {number} [options.port=8080] - Port to listen on
50
+ * @param {number} [options.depth=3] - Directory watch depth (prevents EMFILE errors)
50
51
  * @returns {{ app: express.Application, server: http.Server, watcher: FSWatcher, wss: WebSocketServer, port: number, start: () => Promise<{port: number}>, stop: () => Promise<void> }}
51
52
  */
52
53
  export function createMdvServer(options) {
53
- const { rootDir, port = 8080 } = options;
54
+ const { rootDir, port = 8080, depth = 3 } = options;
54
55
 
55
56
  const app = express();
56
57
  const server = createServer(app);
@@ -68,7 +69,7 @@ export function createMdvServer(options) {
68
69
  });
69
70
 
70
71
  const wss = setupWebSocket(server);
71
- const watcher = setupWatcher(app.locals.rootDir, wss);
72
+ const watcher = setupWatcher(app.locals.rootDir, wss, { depth });
72
73
 
73
74
  app.locals.watcher = watcher;
74
75
  app.locals.wss = wss;
package/src/static/app.js CHANGED
@@ -88,6 +88,21 @@
88
88
  rootPath: ''
89
89
  };
90
90
 
91
+ // ============================================================
92
+ // URL State Management
93
+ // ============================================================
94
+
95
+ function updateUrlPath(path) {
96
+ const url = new URL(window.location);
97
+ if (path) {
98
+ // パスの/をエンコードせずに表示
99
+ url.search = '?path=' + encodeURIComponent(path).replace(/%2F/g, '/');
100
+ } else {
101
+ url.search = '';
102
+ }
103
+ history.replaceState(null, '', url);
104
+ }
105
+
91
106
  // ============================================================
92
107
  // DOM Elements
93
108
  // ============================================================
@@ -477,6 +492,35 @@
477
492
  }
478
493
  },
479
494
 
495
+ async expandToPath(filePath) {
496
+ // パスを分割して親フォルダのリストを作成
497
+ const parts = filePath.split('/');
498
+ parts.pop(); // ファイル名を除外
499
+
500
+ let currentPath = '';
501
+ for (const part of parts) {
502
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
503
+
504
+ const item = document.querySelector(`.tree-item[data-path="${currentPath}"]`);
505
+ if (!item) continue;
506
+
507
+ const children = item.querySelector('.tree-children');
508
+ const chevron = item.querySelector('.chevron');
509
+
510
+ if (children && children.classList.contains('collapsed')) {
511
+ // 未読み込みの場合は子要素を取得
512
+ if (item.dataset.loaded !== 'true') {
513
+ await this.expandDirectory(currentPath, children);
514
+ }
515
+ children.classList.remove('collapsed');
516
+ if (chevron) chevron.classList.add('expanded');
517
+ }
518
+ }
519
+
520
+ // ファイルをハイライト
521
+ this.updateHighlight();
522
+ },
523
+
480
524
  renderFile(item) {
481
525
  const iconClass = item.icon ? `icon-${item.icon}` : '';
482
526
  const iconSvg = getFileIcon(item.icon);
@@ -835,7 +879,7 @@
835
879
  elements.content.innerHTML = `
836
880
  <div class="html-preview">
837
881
  <iframe src="${htmlUrl}" title="${name}"
838
- sandbox="allow-scripts allow-same-origin allow-forms">
882
+ sandbox="allow-scripts allow-same-origin allow-forms allow-modals">
839
883
  </iframe>
840
884
  </div>
841
885
  `;
@@ -935,6 +979,7 @@
935
979
  this.renderActive();
936
980
  WebSocketManager.watchFile(path);
937
981
  FileTreeManager.updateHighlight();
982
+ updateUrlPath(path);
938
983
  },
939
984
 
940
985
  switch(index) {
@@ -965,6 +1010,7 @@
965
1010
  this.renderActive();
966
1011
  WebSocketManager.watchFile(state.tabs[index].path);
967
1012
  FileTreeManager.updateHighlight();
1013
+ updateUrlPath(state.tabs[index].path);
968
1014
  },
969
1015
 
970
1016
  close(index) {
@@ -975,6 +1021,7 @@
975
1021
  this.render();
976
1022
  ContentRenderer.showWelcome();
977
1023
  FileTreeManager.updateHighlight();
1024
+ updateUrlPath(null);
978
1025
  return;
979
1026
  }
980
1027
 
@@ -986,6 +1033,7 @@
986
1033
  this.render();
987
1034
  this.renderActive();
988
1035
  FileTreeManager.updateHighlight();
1036
+ updateUrlPath(state.tabs[state.activeTabIndex].path);
989
1037
  },
990
1038
 
991
1039
  render() {
@@ -1271,6 +1319,10 @@
1271
1319
  return !!elements.content.querySelector('.marpit');
1272
1320
  },
1273
1321
 
1322
+ isHtmlPreview() {
1323
+ return !!elements.content.querySelector('.html-preview iframe');
1324
+ },
1325
+
1274
1326
  async print() {
1275
1327
  if (state.activeTabIndex < 0) return;
1276
1328
 
@@ -1278,6 +1330,8 @@
1278
1330
 
1279
1331
  if (this.isMarpPresentation()) {
1280
1332
  await this.exportMarpPdf(tab.path);
1333
+ } else if (this.isHtmlPreview()) {
1334
+ this.printHtmlPreview(tab.name);
1281
1335
  } else {
1282
1336
  this.browserPrint(tab.name);
1283
1337
  }
@@ -1292,6 +1346,13 @@
1292
1346
  document.title = originalTitle;
1293
1347
  },
1294
1348
 
1349
+ printHtmlPreview(fileName) {
1350
+ const iframe = elements.content.querySelector('.html-preview iframe');
1351
+ if (iframe && iframe.contentWindow) {
1352
+ iframe.contentWindow.print();
1353
+ }
1354
+ },
1355
+
1295
1356
  async exportMarpPdf(filePath) {
1296
1357
  const statusText = elements.statusText;
1297
1358
  const originalStatus = statusText.textContent;
@@ -1945,9 +2006,10 @@
1945
2006
  });
1946
2007
  window.addEventListener('focus', handleFocusChange);
1947
2008
 
1948
- const initialFile = new URLSearchParams(window.location.search).get('file');
1949
- if (initialFile) {
1950
- TabManager.open(initialFile);
2009
+ const initialPath = new URLSearchParams(window.location.search).get('path');
2010
+ if (initialPath) {
2011
+ await FileTreeManager.expandToPath(initialPath);
2012
+ await TabManager.open(initialPath);
1951
2013
  }
1952
2014
  }
1953
2015
 
package/src/watcher.js CHANGED
@@ -34,13 +34,18 @@ const TREE_CHANGE_EVENTS = ['add', 'unlink', 'addDir', 'unlinkDir'];
34
34
  * Setup file watcher
35
35
  * @param {string} rootDir - Root directory to watch
36
36
  * @param {WebSocketServer} wss - WebSocket server for broadcasting
37
+ * @param {Object} [options] - Watcher options
38
+ * @param {number} [options.depth=3] - Directory depth to watch (prevents EMFILE errors)
37
39
  * @returns {FSWatcher} Chokidar watcher instance
38
40
  */
39
- export function setupWatcher(rootDir, wss) {
41
+ export function setupWatcher(rootDir, wss, options = {}) {
42
+ const { depth = 3 } = options;
43
+
40
44
  const watcher = chokidar.watch(rootDir, {
41
45
  ignored: IGNORED_PATTERNS,
42
46
  persistent: true,
43
47
  ignoreInitial: true,
48
+ depth,
44
49
  awaitWriteFinish: {
45
50
  stabilityThreshold: 100,
46
51
  pollInterval: 50