mdv-live 0.3.1 → 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 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/bin/mdv.js CHANGED
@@ -5,18 +5,20 @@
5
5
  * Compatible with the original Python mdv-live CLI
6
6
  */
7
7
 
8
- import { createMdvServer } from '../src/server.js';
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
- import { execSync, spawn } from 'child_process';
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
- // Parse command line arguments
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
- const MARP_PATTERN = /^---\s*\n[\s\S]*?marp:\s*true[\s\S]*?\n---/;
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 marp-cli with document-like settings
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
- try {
231
- await fs.access(resolved);
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
- // Marp slide: use marp-cli directly
253
- try {
254
- execSync(`npx @marp-team/marp-cli --no-stdin "${resolved}" --pdf -o "${finalOutput}"`, {
255
- encoding: 'utf-8',
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
- try {
269
- execSync(`npx md-to-pdf "${resolved}" --pdf-options '{"format":"A4","margin":{"top":"20mm","right":"20mm","bottom":"20mm","left":"20mm"}}'`, {
270
- encoding: 'utf-8',
271
- stdio: 'inherit',
272
- cwd: path.dirname(resolved)
273
- });
274
-
275
- // md-to-pdf outputs to same directory with .pdf extension
276
- const generatedPdf = resolved.replace(/\.(md|markdown)$/i, '.pdf');
277
- if (generatedPdf !== finalOutput) {
278
- await fs.rename(generatedPdf, finalOutput);
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
- console.log(`PDF saved: ${finalOutput}`);
282
- return 0;
283
- } catch (err) {
284
- console.error('Error: PDF conversion failed');
285
- console.error('Make sure md-to-pdf is available (npx md-to-pdf)');
286
- return 1;
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
- async function isPortAvailable(port) {
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 i = 0; i < maxRetries; i++) {
310
- const port = startPort + i;
311
- const available = await isPortAvailable(port);
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 (i > 0) {
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
- * Start MDV server with auto port increment
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 startViewer(targetPath, startPort, openBrowser) {
326
- let rootDir = process.cwd();
327
- let initialFile = null;
359
+ async function resolveTargetPath(targetPath) {
360
+ if (!targetPath || targetPath === '.') {
361
+ return { rootDir: process.cwd(), initialFile: null };
362
+ }
328
363
 
329
- if (targetPath && targetPath !== '.') {
330
- const resolved = path.resolve(targetPath);
331
- try {
332
- const stats = await fs.stat(resolved);
333
- if (stats.isDirectory()) {
334
- rootDir = resolved;
335
- } else if (stats.isFile()) {
336
- rootDir = path.dirname(resolved);
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
- async function main() {
379
- let args;
420
+ /**
421
+ * Parse command line arguments safely
422
+ * @returns {{values: object, positionals: string[]}}
423
+ */
424
+ function parseCommandLineArgs() {
380
425
  try {
381
- args = parseArgs({
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
- const { values, positionals } = args;
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.1');
450
+ console.log('mdv v0.3.2');
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdv-live",
3
- "version": "0.3.1",
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": {