mdv-live 0.3.0 → 0.3.2
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/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/bin/mdv.js +140 -57
- package/package.json +4 -4
- package/scripts/setup-macos-app.sh +3 -3
- package/src/api/file.js +140 -111
- package/src/api/pdf.js +24 -27
- package/src/api/tree.js +35 -28
- package/src/api/upload.js +26 -25
- package/src/rendering/index.js +26 -25
- package/src/rendering/markdown.js +27 -40
- package/src/server.js +53 -66
- package/src/static/app.js +107 -140
- package/src/static/index.html +48 -25
- package/src/static/styles.css +95 -169
- package/src/utils/fileTypes.js +99 -90
- package/src/utils/path.js +11 -14
- package/src/watcher.js +38 -48
- package/src/websocket.js +17 -13
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.2] - 2026-01-31
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Refactored codebase with extracted helper functions and constants
|
|
13
|
+
- Simplified CSS with consolidated variables (--success, --warning, --danger)
|
|
14
|
+
- Reduced styles.css by 84 lines, app.js by 45 lines
|
|
15
|
+
- Added ARIA attributes for improved accessibility
|
|
16
|
+
- Standardized test helpers and imports
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- File tree not updating on file structure changes (add/delete/rename)
|
|
21
|
+
|
|
22
|
+
## [0.3.1] - 2026-01-31
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Minor code cleanup and organization
|
|
27
|
+
|
|
8
28
|
## [0.3.0] - 2026-01-31
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ macOSで`.md`ファイルをダブルクリックしてMDVで開けるように
|
|
|
87
87
|
which mdv
|
|
88
88
|
|
|
89
89
|
# セットアップスクリプトを実行
|
|
90
|
-
curl -fsSL https://raw.githubusercontent.com/panhouse/mdv/main/scripts/setup-macos-app.sh | bash
|
|
90
|
+
curl -fsSL https://raw.githubusercontent.com/panhouse/mdv-live/main/scripts/setup-macos-app.sh | bash
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
または、リポジトリをクローンしている場合:
|
|
@@ -175,8 +175,8 @@ paginate: true
|
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
177
|
# Clone repository
|
|
178
|
-
git clone https://github.com/panhouse/mdv.git
|
|
179
|
-
cd mdv
|
|
178
|
+
git clone https://github.com/panhouse/mdv-live.git
|
|
179
|
+
cd mdv-live
|
|
180
180
|
|
|
181
181
|
# Install dependencies
|
|
182
182
|
npm install
|
package/bin/mdv.js
CHANGED
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
* Compatible with the original Python mdv-live CLI
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import { createServer as createNetServer } from 'node:net';
|
|
11
|
+
import path from 'node:path';
|
|
9
12
|
import { parseArgs } from 'node:util';
|
|
10
|
-
|
|
11
|
-
import { createServer as createNetServer } from 'net';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import fs from 'fs/promises';
|
|
13
|
+
|
|
14
14
|
import open from 'open';
|
|
15
15
|
|
|
16
|
+
import { createMdvServer } from '../src/server.js';
|
|
17
|
+
|
|
16
18
|
const DEFAULT_PORT = 8642;
|
|
19
|
+
const MARP_FRONTMATTER_PATTERN = /^---\s*\n[\s\S]*?marp:\s*true[\s\S]*?\n---/;
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
const options = {
|
|
21
|
+
const OPTIONS = {
|
|
20
22
|
port: {
|
|
21
23
|
type: 'string',
|
|
22
24
|
short: 'p',
|
|
@@ -60,6 +62,9 @@ const options = {
|
|
|
60
62
|
}
|
|
61
63
|
};
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Display help message
|
|
67
|
+
*/
|
|
63
68
|
function showHelp() {
|
|
64
69
|
console.log(`
|
|
65
70
|
MDV - Markdown Viewer with file tree + live preview + Marp support
|
|
@@ -99,6 +104,7 @@ Examples:
|
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
106
|
* Get running MDV server processes
|
|
107
|
+
* @returns {{pid: string, port: string, command: string}[]} Array of process info
|
|
102
108
|
*/
|
|
103
109
|
function getMdvProcesses() {
|
|
104
110
|
try {
|
|
@@ -139,7 +145,8 @@ function getMdvProcesses() {
|
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
/**
|
|
142
|
-
* List running MDV servers
|
|
148
|
+
* List running MDV servers to console
|
|
149
|
+
* @returns {number} Exit code (0 = success)
|
|
143
150
|
*/
|
|
144
151
|
function listServers() {
|
|
145
152
|
const processes = getMdvProcesses();
|
|
@@ -165,6 +172,9 @@ function listServers() {
|
|
|
165
172
|
|
|
166
173
|
/**
|
|
167
174
|
* Kill MDV server(s)
|
|
175
|
+
* @param {string|null} target - Specific PID to kill, or null for all
|
|
176
|
+
* @param {boolean} killAll - Whether to kill all servers
|
|
177
|
+
* @returns {number} Exit code (0 = success, 1 = error)
|
|
168
178
|
*/
|
|
169
179
|
function killServers(target, killAll) {
|
|
170
180
|
if (target) {
|
|
@@ -212,14 +222,27 @@ function killServers(target, killAll) {
|
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
/**
|
|
215
|
-
*
|
|
225
|
+
* Check if markdown content is a Marp presentation
|
|
226
|
+
* @param {string} content - Markdown file content
|
|
227
|
+
* @returns {boolean} True if content has Marp frontmatter
|
|
228
|
+
*/
|
|
229
|
+
function isMarpFile(content) {
|
|
230
|
+
return MARP_FRONTMATTER_PATTERN.test(content);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Convert markdown to PDF using appropriate tool
|
|
235
|
+
* - Marp slides: use marp-cli
|
|
236
|
+
* - Regular markdown: use md-to-pdf for A4 document format
|
|
237
|
+
* @param {string} inputPath - Input markdown file path
|
|
238
|
+
* @param {string} [outputPath] - Output PDF file path
|
|
239
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = error)
|
|
216
240
|
*/
|
|
217
241
|
async function convertToPdf(inputPath, outputPath) {
|
|
218
242
|
const resolved = path.resolve(inputPath);
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
} catch {
|
|
244
|
+
const fileExists = await fs.access(resolved).then(() => true).catch(() => false);
|
|
245
|
+
if (!fileExists) {
|
|
223
246
|
console.error(`Error: File not found: ${inputPath}`);
|
|
224
247
|
return 1;
|
|
225
248
|
}
|
|
@@ -230,51 +253,98 @@ async function convertToPdf(inputPath, outputPath) {
|
|
|
230
253
|
return 1;
|
|
231
254
|
}
|
|
232
255
|
|
|
256
|
+
const content = await fs.readFile(resolved, 'utf-8');
|
|
257
|
+
const isMarp = isMarpFile(content);
|
|
233
258
|
const defaultOutput = resolved.replace(/\.(md|markdown)$/i, '.pdf');
|
|
234
259
|
const finalOutput = outputPath ? path.resolve(outputPath) : defaultOutput;
|
|
235
260
|
|
|
236
261
|
console.log(`Converting ${inputPath} to PDF...`);
|
|
237
262
|
|
|
263
|
+
if (isMarp) {
|
|
264
|
+
return convertMarpToPdf(resolved, finalOutput);
|
|
265
|
+
}
|
|
266
|
+
return convertMarkdownToPdf(resolved, finalOutput);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Convert Marp presentation to PDF using marp-cli
|
|
271
|
+
* @param {string} inputPath - Resolved input file path
|
|
272
|
+
* @param {string} outputPath - Resolved output file path
|
|
273
|
+
* @returns {Promise<number>} Exit code
|
|
274
|
+
*/
|
|
275
|
+
async function convertMarpToPdf(inputPath, outputPath) {
|
|
238
276
|
try {
|
|
239
|
-
|
|
240
|
-
execSync(`npx @marp-team/marp-cli "${resolved}" --pdf -o "${finalOutput}"`, {
|
|
277
|
+
execSync(`npx @marp-team/marp-cli --no-stdin "${inputPath}" --pdf -o "${outputPath}"`, {
|
|
241
278
|
encoding: 'utf-8',
|
|
242
279
|
stdio: 'inherit'
|
|
243
280
|
});
|
|
244
|
-
console.log(`PDF saved: ${
|
|
281
|
+
console.log(`PDF saved: ${outputPath}`);
|
|
245
282
|
return 0;
|
|
246
|
-
} catch
|
|
283
|
+
} catch {
|
|
284
|
+
console.error('Error: PDF conversion failed');
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Convert regular markdown to PDF using md-to-pdf (A4 format)
|
|
291
|
+
* @param {string} inputPath - Resolved input file path
|
|
292
|
+
* @param {string} outputPath - Resolved output file path
|
|
293
|
+
* @returns {Promise<number>} Exit code
|
|
294
|
+
*/
|
|
295
|
+
async function convertMarkdownToPdf(inputPath, outputPath) {
|
|
296
|
+
console.log('Converting as document (A4 portrait)...');
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const pdfOptions = '{"format":"A4","margin":{"top":"20mm","right":"20mm","bottom":"20mm","left":"20mm"}}';
|
|
300
|
+
execSync(`npx md-to-pdf "${inputPath}" --pdf-options '${pdfOptions}'`, {
|
|
301
|
+
encoding: 'utf-8',
|
|
302
|
+
stdio: 'inherit',
|
|
303
|
+
cwd: path.dirname(inputPath)
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// md-to-pdf outputs to same directory with .pdf extension
|
|
307
|
+
const generatedPdf = inputPath.replace(/\.(md|markdown)$/i, '.pdf');
|
|
308
|
+
if (generatedPdf !== outputPath) {
|
|
309
|
+
await fs.rename(generatedPdf, outputPath);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log(`PDF saved: ${outputPath}`);
|
|
313
|
+
return 0;
|
|
314
|
+
} catch {
|
|
247
315
|
console.error('Error: PDF conversion failed');
|
|
248
|
-
console.error('Make sure
|
|
316
|
+
console.error('Make sure md-to-pdf is available (npx md-to-pdf)');
|
|
249
317
|
return 1;
|
|
250
318
|
}
|
|
251
319
|
}
|
|
252
320
|
|
|
253
321
|
/**
|
|
254
|
-
* Check if a port is available
|
|
322
|
+
* Check if a port is available for binding
|
|
323
|
+
* @param {number} port - Port number to check
|
|
324
|
+
* @returns {Promise<boolean>} True if port is available
|
|
255
325
|
*/
|
|
256
|
-
|
|
326
|
+
function isPortAvailable(port) {
|
|
257
327
|
return new Promise((resolve) => {
|
|
258
328
|
const server = createNetServer();
|
|
259
329
|
server.once('error', () => resolve(false));
|
|
260
|
-
server.once('listening', () =>
|
|
261
|
-
server.close(() => resolve(true));
|
|
262
|
-
});
|
|
330
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
263
331
|
server.listen(port);
|
|
264
332
|
});
|
|
265
333
|
}
|
|
266
334
|
|
|
267
335
|
/**
|
|
268
336
|
* Find an available port starting from the given port
|
|
337
|
+
* @param {number} startPort - Starting port number
|
|
338
|
+
* @param {number} [maxRetries=100] - Maximum ports to try
|
|
339
|
+
* @returns {Promise<number|null>} Available port or null if none found
|
|
269
340
|
*/
|
|
270
341
|
async function findAvailablePort(startPort, maxRetries = 100) {
|
|
271
|
-
for (let
|
|
272
|
-
const port = startPort +
|
|
273
|
-
|
|
274
|
-
if (available) {
|
|
342
|
+
for (let offset = 0; offset < maxRetries; offset++) {
|
|
343
|
+
const port = startPort + offset;
|
|
344
|
+
if (await isPortAvailable(port)) {
|
|
275
345
|
return port;
|
|
276
346
|
}
|
|
277
|
-
if (
|
|
347
|
+
if (offset > 0) {
|
|
278
348
|
console.log(`ポート ${port - 1} は使用中です。${port} を試します...`);
|
|
279
349
|
}
|
|
280
350
|
}
|
|
@@ -282,29 +352,40 @@ async function findAvailablePort(startPort, maxRetries = 100) {
|
|
|
282
352
|
}
|
|
283
353
|
|
|
284
354
|
/**
|
|
285
|
-
*
|
|
355
|
+
* Resolve target path to root directory and optional initial file
|
|
356
|
+
* @param {string} targetPath - User-provided path
|
|
357
|
+
* @returns {Promise<{rootDir: string, initialFile: string|null}>}
|
|
286
358
|
*/
|
|
287
|
-
async function
|
|
288
|
-
|
|
289
|
-
|
|
359
|
+
async function resolveTargetPath(targetPath) {
|
|
360
|
+
if (!targetPath || targetPath === '.') {
|
|
361
|
+
return { rootDir: process.cwd(), initialFile: null };
|
|
362
|
+
}
|
|
290
363
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
initialFile = path.basename(resolved);
|
|
300
|
-
}
|
|
301
|
-
} catch {
|
|
302
|
-
console.error(`Error: Path not found: ${targetPath}`);
|
|
303
|
-
process.exit(1);
|
|
364
|
+
const resolved = path.resolve(targetPath);
|
|
365
|
+
try {
|
|
366
|
+
const stats = await fs.stat(resolved);
|
|
367
|
+
if (stats.isDirectory()) {
|
|
368
|
+
return { rootDir: resolved, initialFile: null };
|
|
369
|
+
}
|
|
370
|
+
if (stats.isFile()) {
|
|
371
|
+
return { rootDir: path.dirname(resolved), initialFile: path.basename(resolved) };
|
|
304
372
|
}
|
|
373
|
+
} catch {
|
|
374
|
+
console.error(`Error: Path not found: ${targetPath}`);
|
|
375
|
+
process.exit(1);
|
|
305
376
|
}
|
|
377
|
+
return { rootDir: process.cwd(), initialFile: null };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Start MDV server with auto port increment
|
|
382
|
+
* @param {string} targetPath - Target directory or file path
|
|
383
|
+
* @param {number} startPort - Starting port number
|
|
384
|
+
* @param {boolean} openBrowser - Whether to open browser automatically
|
|
385
|
+
*/
|
|
386
|
+
async function startViewer(targetPath, startPort, openBrowser) {
|
|
387
|
+
const { rootDir, initialFile } = await resolveTargetPath(targetPath);
|
|
306
388
|
|
|
307
|
-
// Find available port
|
|
308
389
|
const port = await findAvailablePort(startPort);
|
|
309
390
|
if (!port) {
|
|
310
391
|
console.error('Error: 利用可能なポートが見つかりませんでした');
|
|
@@ -315,7 +396,6 @@ async function startViewer(targetPath, startPort, openBrowser) {
|
|
|
315
396
|
console.log(`ポート ${startPort} は使用中のため、${port} で起動します`);
|
|
316
397
|
}
|
|
317
398
|
|
|
318
|
-
// Create and start server
|
|
319
399
|
const mdv = createMdvServer({ rootDir, port });
|
|
320
400
|
await mdv.start();
|
|
321
401
|
|
|
@@ -337,11 +417,14 @@ async function startViewer(targetPath, startPort, openBrowser) {
|
|
|
337
417
|
}
|
|
338
418
|
}
|
|
339
419
|
|
|
340
|
-
|
|
341
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Parse command line arguments safely
|
|
422
|
+
* @returns {{values: object, positionals: string[]}}
|
|
423
|
+
*/
|
|
424
|
+
function parseCommandLineArgs() {
|
|
342
425
|
try {
|
|
343
|
-
|
|
344
|
-
options,
|
|
426
|
+
return parseArgs({
|
|
427
|
+
options: OPTIONS,
|
|
345
428
|
allowPositionals: true,
|
|
346
429
|
strict: false
|
|
347
430
|
});
|
|
@@ -350,33 +433,33 @@ async function main() {
|
|
|
350
433
|
showHelp();
|
|
351
434
|
process.exit(1);
|
|
352
435
|
}
|
|
436
|
+
}
|
|
353
437
|
|
|
354
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Main entry point
|
|
440
|
+
*/
|
|
441
|
+
async function main() {
|
|
442
|
+
const { values, positionals } = parseCommandLineArgs();
|
|
355
443
|
|
|
356
|
-
// Help
|
|
357
444
|
if (values.help) {
|
|
358
445
|
showHelp();
|
|
359
446
|
process.exit(0);
|
|
360
447
|
}
|
|
361
448
|
|
|
362
|
-
// Version
|
|
363
449
|
if (values.version) {
|
|
364
|
-
console.log('mdv v0.3.
|
|
450
|
+
console.log('mdv v0.3.2');
|
|
365
451
|
process.exit(0);
|
|
366
452
|
}
|
|
367
453
|
|
|
368
|
-
// List servers
|
|
369
454
|
if (values.list) {
|
|
370
455
|
process.exit(listServers());
|
|
371
456
|
}
|
|
372
457
|
|
|
373
|
-
// Kill servers
|
|
374
458
|
if (values.kill) {
|
|
375
459
|
const pid = positionals[0] || null;
|
|
376
460
|
process.exit(killServers(pid, values.all));
|
|
377
461
|
}
|
|
378
462
|
|
|
379
|
-
// PDF conversion
|
|
380
463
|
if (values.pdf) {
|
|
381
464
|
const inputPath = positionals[0];
|
|
382
465
|
if (!inputPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdv-live",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/panhouse/mdv.git"
|
|
33
|
+
"url": "git+https://github.com/panhouse/mdv-live.git"
|
|
34
34
|
},
|
|
35
|
-
"homepage": "https://github.com/panhouse/mdv#readme",
|
|
35
|
+
"homepage": "https://github.com/panhouse/mdv-live#readme",
|
|
36
36
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/panhouse/mdv/issues"
|
|
37
|
+
"url": "https://github.com/panhouse/mdv-live/issues"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"bin/",
|
|
@@ -43,7 +43,7 @@ Usage:
|
|
|
43
43
|
- Double-click any .md file
|
|
44
44
|
- Drag & drop .md files onto this app
|
|
45
45
|
|
|
46
|
-
Version: 0.3.
|
|
46
|
+
Version: 0.3.1 (Node.js)" buttons {"OK"} default button "OK" with title "MDV"
|
|
47
47
|
end run
|
|
48
48
|
EOF
|
|
49
49
|
|
|
@@ -127,9 +127,9 @@ cat << 'EOF' > "$TEMP_DIR/$APP_NAME/Contents/Info.plist"
|
|
|
127
127
|
<key>CFBundlePackageType</key>
|
|
128
128
|
<string>APPL</string>
|
|
129
129
|
<key>CFBundleShortVersionString</key>
|
|
130
|
-
<string>0.3.
|
|
130
|
+
<string>0.3.1</string>
|
|
131
131
|
<key>CFBundleVersion</key>
|
|
132
|
-
<string>0.3.
|
|
132
|
+
<string>0.3.1</string>
|
|
133
133
|
<key>LSMinimumSystemVersion</key>
|
|
134
134
|
<string>10.13</string>
|
|
135
135
|
</dict>
|