mdv-live 0.3.8 → 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 +11 -4
- package/package.json +1 -1
- package/scripts/setup-macos-app.sh +2 -2
- package/src/api/file.js +45 -0
- package/src/server.js +3 -2
- package/src/static/app.js +79 -3
- package/src/static/styles.css +16 -0
- package/src/utils/fileTypes.js +2 -2
- package/src/watcher.js +6 -1
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}?
|
|
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
|
@@ -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?
|
|
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?
|
|
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/api/file.js
CHANGED
|
@@ -89,6 +89,32 @@ function buildBinaryFileResponse(name, fileType, downloadUrl) {
|
|
|
89
89
|
export function setupFileRoutes(app) {
|
|
90
90
|
const { rootDir } = app.locals;
|
|
91
91
|
|
|
92
|
+
// Serve raw files (for HTML preview with relative paths)
|
|
93
|
+
app.get('/raw/*', async (req, res) => {
|
|
94
|
+
const relativePath = req.params[0];
|
|
95
|
+
const { valid, fullPath } = resolveAndValidate(relativePath, rootDir);
|
|
96
|
+
|
|
97
|
+
if (!relativePath || !valid) {
|
|
98
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const stat = await fs.stat(fullPath);
|
|
103
|
+
if (!stat.isFile()) {
|
|
104
|
+
return res.status(400).json({ error: 'Not a file' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
|
108
|
+
res.setHeader('Content-Type', mimeType);
|
|
109
|
+
res.sendFile(fullPath);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err.code === 'ENOENT') {
|
|
112
|
+
return res.status(404).json({ error: 'File not found' });
|
|
113
|
+
}
|
|
114
|
+
res.status(500).json({ error: err.message });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
92
118
|
// Get file content
|
|
93
119
|
app.get('/api/file', async (req, res) => {
|
|
94
120
|
const { path: relativePath } = req.query;
|
|
@@ -115,6 +141,25 @@ export function setupFileRoutes(app) {
|
|
|
115
141
|
return res.json(buildBinaryFileResponse(name, fileType, downloadUrl));
|
|
116
142
|
}
|
|
117
143
|
|
|
144
|
+
// HTML files: return htmlUrl for iframe preview + raw content for editing
|
|
145
|
+
if (fileType.type === 'html') {
|
|
146
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
147
|
+
const escaped = content
|
|
148
|
+
.replace(/&/g, '&')
|
|
149
|
+
.replace(/</g, '<')
|
|
150
|
+
.replace(/>/g, '>')
|
|
151
|
+
.replace(/"/g, '"')
|
|
152
|
+
.replace(/'/g, ''');
|
|
153
|
+
return res.json({
|
|
154
|
+
name,
|
|
155
|
+
fileType: 'html',
|
|
156
|
+
icon: 'html',
|
|
157
|
+
htmlUrl: `/raw/${relativePath}`,
|
|
158
|
+
content: `<pre><code class="language-html">${escaped}</code></pre>`,
|
|
159
|
+
raw: content
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
118
163
|
const rendered = await renderFile(fullPath);
|
|
119
164
|
res.json({ name, ...rendered });
|
|
120
165
|
} catch (err) {
|
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);
|
|
@@ -830,6 +874,17 @@
|
|
|
830
874
|
`;
|
|
831
875
|
},
|
|
832
876
|
|
|
877
|
+
renderHTML(htmlUrl, name) {
|
|
878
|
+
elements.content.style.padding = '0';
|
|
879
|
+
elements.content.innerHTML = `
|
|
880
|
+
<div class="html-preview">
|
|
881
|
+
<iframe src="${htmlUrl}" title="${name}"
|
|
882
|
+
sandbox="allow-scripts allow-same-origin allow-forms allow-modals">
|
|
883
|
+
</iframe>
|
|
884
|
+
</div>
|
|
885
|
+
`;
|
|
886
|
+
},
|
|
887
|
+
|
|
833
888
|
renderVideo(mediaUrl, name) {
|
|
834
889
|
elements.content.innerHTML = `
|
|
835
890
|
<div class="video-preview">
|
|
@@ -908,6 +963,7 @@
|
|
|
908
963
|
css: data.css || null, // Marp CSS from marp-core
|
|
909
964
|
imageUrl: data.imageUrl,
|
|
910
965
|
pdfUrl: data.pdfUrl,
|
|
966
|
+
htmlUrl: data.htmlUrl,
|
|
911
967
|
mediaUrl: data.mediaUrl,
|
|
912
968
|
downloadUrl: data.downloadUrl,
|
|
913
969
|
scrollTop: 0
|
|
@@ -923,6 +979,7 @@
|
|
|
923
979
|
this.renderActive();
|
|
924
980
|
WebSocketManager.watchFile(path);
|
|
925
981
|
FileTreeManager.updateHighlight();
|
|
982
|
+
updateUrlPath(path);
|
|
926
983
|
},
|
|
927
984
|
|
|
928
985
|
switch(index) {
|
|
@@ -953,6 +1010,7 @@
|
|
|
953
1010
|
this.renderActive();
|
|
954
1011
|
WebSocketManager.watchFile(state.tabs[index].path);
|
|
955
1012
|
FileTreeManager.updateHighlight();
|
|
1013
|
+
updateUrlPath(state.tabs[index].path);
|
|
956
1014
|
},
|
|
957
1015
|
|
|
958
1016
|
close(index) {
|
|
@@ -963,6 +1021,7 @@
|
|
|
963
1021
|
this.render();
|
|
964
1022
|
ContentRenderer.showWelcome();
|
|
965
1023
|
FileTreeManager.updateHighlight();
|
|
1024
|
+
updateUrlPath(null);
|
|
966
1025
|
return;
|
|
967
1026
|
}
|
|
968
1027
|
|
|
@@ -974,6 +1033,7 @@
|
|
|
974
1033
|
this.render();
|
|
975
1034
|
this.renderActive();
|
|
976
1035
|
FileTreeManager.updateHighlight();
|
|
1036
|
+
updateUrlPath(state.tabs[state.activeTabIndex].path);
|
|
977
1037
|
},
|
|
978
1038
|
|
|
979
1039
|
render() {
|
|
@@ -1018,6 +1078,8 @@
|
|
|
1018
1078
|
ContentRenderer.renderImage(tab.imageUrl, tab.name);
|
|
1019
1079
|
} else if (fileType === 'pdf') {
|
|
1020
1080
|
ContentRenderer.renderPDF(tab.pdfUrl, tab.name);
|
|
1081
|
+
} else if (fileType === 'html' && tab.htmlUrl && !state.isEditMode) {
|
|
1082
|
+
ContentRenderer.renderHTML(tab.htmlUrl, tab.name);
|
|
1021
1083
|
} else if (fileType === 'video') {
|
|
1022
1084
|
ContentRenderer.renderVideo(tab.mediaUrl, tab.name);
|
|
1023
1085
|
} else if (fileType === 'audio') {
|
|
@@ -1257,6 +1319,10 @@
|
|
|
1257
1319
|
return !!elements.content.querySelector('.marpit');
|
|
1258
1320
|
},
|
|
1259
1321
|
|
|
1322
|
+
isHtmlPreview() {
|
|
1323
|
+
return !!elements.content.querySelector('.html-preview iframe');
|
|
1324
|
+
},
|
|
1325
|
+
|
|
1260
1326
|
async print() {
|
|
1261
1327
|
if (state.activeTabIndex < 0) return;
|
|
1262
1328
|
|
|
@@ -1264,6 +1330,8 @@
|
|
|
1264
1330
|
|
|
1265
1331
|
if (this.isMarpPresentation()) {
|
|
1266
1332
|
await this.exportMarpPdf(tab.path);
|
|
1333
|
+
} else if (this.isHtmlPreview()) {
|
|
1334
|
+
this.printHtmlPreview(tab.name);
|
|
1267
1335
|
} else {
|
|
1268
1336
|
this.browserPrint(tab.name);
|
|
1269
1337
|
}
|
|
@@ -1278,6 +1346,13 @@
|
|
|
1278
1346
|
document.title = originalTitle;
|
|
1279
1347
|
},
|
|
1280
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
|
+
|
|
1281
1356
|
async exportMarpPdf(filePath) {
|
|
1282
1357
|
const statusText = elements.statusText;
|
|
1283
1358
|
const originalStatus = statusText.textContent;
|
|
@@ -1931,9 +2006,10 @@
|
|
|
1931
2006
|
});
|
|
1932
2007
|
window.addEventListener('focus', handleFocusChange);
|
|
1933
2008
|
|
|
1934
|
-
const
|
|
1935
|
-
if (
|
|
1936
|
-
|
|
2009
|
+
const initialPath = new URLSearchParams(window.location.search).get('path');
|
|
2010
|
+
if (initialPath) {
|
|
2011
|
+
await FileTreeManager.expandToPath(initialPath);
|
|
2012
|
+
await TabManager.open(initialPath);
|
|
1937
2013
|
}
|
|
1938
2014
|
}
|
|
1939
2015
|
|
package/src/static/styles.css
CHANGED
|
@@ -520,6 +520,22 @@ body {
|
|
|
520
520
|
text-align: center;
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
/* ============================================================
|
|
524
|
+
HTML Preview
|
|
525
|
+
============================================================ */
|
|
526
|
+
|
|
527
|
+
.html-preview {
|
|
528
|
+
width: 100%;
|
|
529
|
+
height: 100%;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.html-preview iframe {
|
|
533
|
+
width: 100%;
|
|
534
|
+
height: 100%;
|
|
535
|
+
border: none;
|
|
536
|
+
background: white;
|
|
537
|
+
}
|
|
538
|
+
|
|
523
539
|
/* ============================================================
|
|
524
540
|
Editor Mode
|
|
525
541
|
============================================================ */
|
package/src/utils/fileTypes.js
CHANGED
|
@@ -29,8 +29,8 @@ const FILE_TYPES = {
|
|
|
29
29
|
jsx: code('react', 'jsx'),
|
|
30
30
|
|
|
31
31
|
// Code - Web
|
|
32
|
-
html:
|
|
33
|
-
htm:
|
|
32
|
+
html: { type: 'html', icon: 'html', lang: 'html', binary: false },
|
|
33
|
+
htm: { type: 'html', icon: 'html', lang: 'html', binary: false },
|
|
34
34
|
css: code('css', 'css'),
|
|
35
35
|
scss: code('css', 'scss'),
|
|
36
36
|
less: code('css', 'less'),
|
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
|