mdv-live 0.3.1 → 0.3.3
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 +26 -0
- package/bin/mdv.js +134 -89
- package/package.json +1 -1
- 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/README.pdf +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ 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.3] - 2026-01-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Print preview missing padding on markdown content
|
|
13
|
+
|
|
14
|
+
## [0.3.2] - 2026-01-31
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Refactored codebase with extracted helper functions and constants
|
|
19
|
+
- Simplified CSS with consolidated variables (--success, --warning, --danger)
|
|
20
|
+
- Reduced styles.css by 84 lines, app.js by 45 lines
|
|
21
|
+
- Added ARIA attributes for improved accessibility
|
|
22
|
+
- Standardized test helpers and imports
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- File tree not updating on file structure changes (add/delete/rename)
|
|
27
|
+
|
|
28
|
+
## [0.3.1] - 2026-01-31
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Minor code cleanup and organization
|
|
33
|
+
|
|
8
34
|
## [0.3.0] - 2026-01-31
|
|
9
35
|
|
|
10
36
|
### Added
|
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) {
|
|
@@ -213,23 +223,26 @@ function killServers(target, killAll) {
|
|
|
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
|
|
216
228
|
*/
|
|
217
229
|
function isMarpFile(content) {
|
|
218
|
-
|
|
219
|
-
return MARP_PATTERN.test(content);
|
|
230
|
+
return MARP_FRONTMATTER_PATTERN.test(content);
|
|
220
231
|
}
|
|
221
232
|
|
|
222
233
|
/**
|
|
223
|
-
* Convert markdown to PDF
|
|
234
|
+
* Convert markdown to PDF using appropriate tool
|
|
224
235
|
* - Marp slides: use marp-cli
|
|
225
|
-
* - Regular markdown: use
|
|
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)
|
|
226
240
|
*/
|
|
227
241
|
async function convertToPdf(inputPath, outputPath) {
|
|
228
242
|
const resolved = path.resolve(inputPath);
|
|
229
243
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} catch {
|
|
244
|
+
const fileExists = await fs.access(resolved).then(() => true).catch(() => false);
|
|
245
|
+
if (!fileExists) {
|
|
233
246
|
console.error(`Error: File not found: ${inputPath}`);
|
|
234
247
|
return 1;
|
|
235
248
|
}
|
|
@@ -242,77 +255,96 @@ async function convertToPdf(inputPath, outputPath) {
|
|
|
242
255
|
|
|
243
256
|
const content = await fs.readFile(resolved, 'utf-8');
|
|
244
257
|
const isMarp = isMarpFile(content);
|
|
245
|
-
|
|
246
258
|
const defaultOutput = resolved.replace(/\.(md|markdown)$/i, '.pdf');
|
|
247
259
|
const finalOutput = outputPath ? path.resolve(outputPath) : defaultOutput;
|
|
248
260
|
|
|
249
261
|
console.log(`Converting ${inputPath} to PDF...`);
|
|
250
262
|
|
|
251
263
|
if (isMarp) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
stdio: 'inherit'
|
|
257
|
-
});
|
|
258
|
-
console.log(`PDF saved: ${finalOutput}`);
|
|
259
|
-
return 0;
|
|
260
|
-
} catch (err) {
|
|
261
|
-
console.error('Error: PDF conversion failed');
|
|
262
|
-
return 1;
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
// Regular markdown: use md-to-pdf for proper A4 document format
|
|
266
|
-
console.log('Converting as document (A4 portrait)...');
|
|
264
|
+
return convertMarpToPdf(resolved, finalOutput);
|
|
265
|
+
}
|
|
266
|
+
return convertMarkdownToPdf(resolved, finalOutput);
|
|
267
|
+
}
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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) {
|
|
276
|
+
try {
|
|
277
|
+
execSync(`npx @marp-team/marp-cli --no-stdin "${inputPath}" --pdf -o "${outputPath}"`, {
|
|
278
|
+
encoding: 'utf-8',
|
|
279
|
+
stdio: 'inherit'
|
|
280
|
+
});
|
|
281
|
+
console.log(`PDF saved: ${outputPath}`);
|
|
282
|
+
return 0;
|
|
283
|
+
} catch {
|
|
284
|
+
console.error('Error: PDF conversion failed');
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
280
288
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
287
310
|
}
|
|
311
|
+
|
|
312
|
+
console.log(`PDF saved: ${outputPath}`);
|
|
313
|
+
return 0;
|
|
314
|
+
} catch {
|
|
315
|
+
console.error('Error: PDF conversion failed');
|
|
316
|
+
console.error('Make sure md-to-pdf is available (npx md-to-pdf)');
|
|
317
|
+
return 1;
|
|
288
318
|
}
|
|
289
319
|
}
|
|
290
320
|
|
|
291
321
|
/**
|
|
292
|
-
* 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
|
|
293
325
|
*/
|
|
294
|
-
|
|
326
|
+
function isPortAvailable(port) {
|
|
295
327
|
return new Promise((resolve) => {
|
|
296
328
|
const server = createNetServer();
|
|
297
329
|
server.once('error', () => resolve(false));
|
|
298
|
-
server.once('listening', () =>
|
|
299
|
-
server.close(() => resolve(true));
|
|
300
|
-
});
|
|
330
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
301
331
|
server.listen(port);
|
|
302
332
|
});
|
|
303
333
|
}
|
|
304
334
|
|
|
305
335
|
/**
|
|
306
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
|
|
307
340
|
*/
|
|
308
341
|
async function findAvailablePort(startPort, maxRetries = 100) {
|
|
309
|
-
for (let
|
|
310
|
-
const port = startPort +
|
|
311
|
-
|
|
312
|
-
if (available) {
|
|
342
|
+
for (let offset = 0; offset < maxRetries; offset++) {
|
|
343
|
+
const port = startPort + offset;
|
|
344
|
+
if (await isPortAvailable(port)) {
|
|
313
345
|
return port;
|
|
314
346
|
}
|
|
315
|
-
if (
|
|
347
|
+
if (offset > 0) {
|
|
316
348
|
console.log(`ポート ${port - 1} は使用中です。${port} を試します...`);
|
|
317
349
|
}
|
|
318
350
|
}
|
|
@@ -320,29 +352,40 @@ async function findAvailablePort(startPort, maxRetries = 100) {
|
|
|
320
352
|
}
|
|
321
353
|
|
|
322
354
|
/**
|
|
323
|
-
*
|
|
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}>}
|
|
324
358
|
*/
|
|
325
|
-
async function
|
|
326
|
-
|
|
327
|
-
|
|
359
|
+
async function resolveTargetPath(targetPath) {
|
|
360
|
+
if (!targetPath || targetPath === '.') {
|
|
361
|
+
return { rootDir: process.cwd(), initialFile: null };
|
|
362
|
+
}
|
|
328
363
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
initialFile = path.basename(resolved);
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
340
|
-
console.error(`Error: Path not found: ${targetPath}`);
|
|
341
|
-
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) };
|
|
342
372
|
}
|
|
373
|
+
} catch {
|
|
374
|
+
console.error(`Error: Path not found: ${targetPath}`);
|
|
375
|
+
process.exit(1);
|
|
343
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);
|
|
344
388
|
|
|
345
|
-
// Find available port
|
|
346
389
|
const port = await findAvailablePort(startPort);
|
|
347
390
|
if (!port) {
|
|
348
391
|
console.error('Error: 利用可能なポートが見つかりませんでした');
|
|
@@ -353,7 +396,6 @@ async function startViewer(targetPath, startPort, openBrowser) {
|
|
|
353
396
|
console.log(`ポート ${startPort} は使用中のため、${port} で起動します`);
|
|
354
397
|
}
|
|
355
398
|
|
|
356
|
-
// Create and start server
|
|
357
399
|
const mdv = createMdvServer({ rootDir, port });
|
|
358
400
|
await mdv.start();
|
|
359
401
|
|
|
@@ -375,11 +417,14 @@ async function startViewer(targetPath, startPort, openBrowser) {
|
|
|
375
417
|
}
|
|
376
418
|
}
|
|
377
419
|
|
|
378
|
-
|
|
379
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Parse command line arguments safely
|
|
422
|
+
* @returns {{values: object, positionals: string[]}}
|
|
423
|
+
*/
|
|
424
|
+
function parseCommandLineArgs() {
|
|
380
425
|
try {
|
|
381
|
-
|
|
382
|
-
options,
|
|
426
|
+
return parseArgs({
|
|
427
|
+
options: OPTIONS,
|
|
383
428
|
allowPositionals: true,
|
|
384
429
|
strict: false
|
|
385
430
|
});
|
|
@@ -388,33 +433,33 @@ async function main() {
|
|
|
388
433
|
showHelp();
|
|
389
434
|
process.exit(1);
|
|
390
435
|
}
|
|
436
|
+
}
|
|
391
437
|
|
|
392
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Main entry point
|
|
440
|
+
*/
|
|
441
|
+
async function main() {
|
|
442
|
+
const { values, positionals } = parseCommandLineArgs();
|
|
393
443
|
|
|
394
|
-
// Help
|
|
395
444
|
if (values.help) {
|
|
396
445
|
showHelp();
|
|
397
446
|
process.exit(0);
|
|
398
447
|
}
|
|
399
448
|
|
|
400
|
-
// Version
|
|
401
449
|
if (values.version) {
|
|
402
|
-
console.log('mdv v0.3.
|
|
450
|
+
console.log('mdv v0.3.3');
|
|
403
451
|
process.exit(0);
|
|
404
452
|
}
|
|
405
453
|
|
|
406
|
-
// List servers
|
|
407
454
|
if (values.list) {
|
|
408
455
|
process.exit(listServers());
|
|
409
456
|
}
|
|
410
457
|
|
|
411
|
-
// Kill servers
|
|
412
458
|
if (values.kill) {
|
|
413
459
|
const pid = positionals[0] || null;
|
|
414
460
|
process.exit(killServers(pid, values.all));
|
|
415
461
|
}
|
|
416
462
|
|
|
417
|
-
// PDF conversion
|
|
418
463
|
if (values.pdf) {
|
|
419
464
|
const inputPath = positionals[0];
|
|
420
465
|
if (!inputPath) {
|