marp-dev-preview 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/README.md CHANGED
@@ -27,7 +27,7 @@ Originally built as a dependency for the [marp-dev-preview.nvim](https://github.
27
27
  The simplest way to run the previewer:
28
28
 
29
29
  ```bash
30
- npx marp-dev-preview --theme-dir <themes-dir> <presentation.md>
30
+ npx marp-dev-preview --markdown-file <presentation.md> --theme-set <themes-dir>
31
31
  ```
32
32
 
33
33
  ---
@@ -53,19 +53,59 @@ npm install marp-dev-preview
53
53
  Start the preview server with:
54
54
 
55
55
  ```bash
56
- mdp <path-to-markdown-file> [options]
56
+ mdp [options]
57
57
  ```
58
58
 
59
59
  **Example:**
60
60
 
61
61
  ```bash
62
- mdp my-slides/presentation.md --port 3000 --theme-dir my-themes
62
+ mdp --markdown-file my-slides/presentation.md --port 3000 --theme-set my-themes
63
63
  ```
64
64
 
65
65
  ### Options
66
66
 
67
- - `-t, --theme-dir <path>` — Path to custom Marp themes (CSS files)
67
+ - `-m, --markdown-file <path>` — Path to the markdown file to preview
68
+ - `-t, --theme-set <path...>` — One or more directories containing custom Marp themes (CSS files)
69
+ - `--theme-dir <path...>` — Alias for `--theme-set`
68
70
  - `-p, --port <number>` — Port for the preview server (default: `8080`)
71
+ - `-c, --containers <name...>` — Container names to register for `markdown-it-container`
72
+ - `-v, --verbose` — Enable verbose logging
73
+ - `--config <path>` — Path to a JSON config file (default: `.mp-config.json`)
74
+ - `--example-config` — Print an example JSON config file and exit
75
+
76
+ ### Configuration File
77
+
78
+ Options can be loaded from a JSON config file. If `.mp-config.json` is present in the current working directory, it is read automatically without passing `--config`. Command-line flags override values from that file.
79
+
80
+ Example `.mp-config.json`:
81
+
82
+ ```json
83
+ {
84
+ "markdown-file": "my-slides/presentation.md",
85
+ "theme-set": ["my-themes"],
86
+ "port": 3000,
87
+ "verbose": true,
88
+ "containers": ["note", "info", "warn", "important"]
89
+ }
90
+ ```
91
+
92
+ Run and auto-load `.mp-config.json` if it exists:
93
+
94
+ ```bash
95
+ mdp
96
+ ```
97
+
98
+ Or point to a different file:
99
+
100
+ ```bash
101
+ mdp --config ./preview-config.json
102
+ ```
103
+
104
+ Print an example config file:
105
+
106
+ ```bash
107
+ mdp --example-config
108
+ ```
69
109
 
70
110
  ---
71
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marp-dev-preview",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A CLI tool to preview Marp markdown files.",
5
5
  "main": "src/marp-dev-preview.mjs",
6
6
  "type": "module",
package/src/args.mjs CHANGED
@@ -1,9 +1,28 @@
1
1
  import yargs from 'yargs';
2
2
  import { hideBin } from 'yargs/helpers';
3
3
 
4
+ export const exampleConfig = {
5
+ 'markdown-file': 'my-slides/presentation.md',
6
+ 'theme-set': ['my-themes'],
7
+ port: 3000,
8
+ verbose: true,
9
+ containers: ['note', 'info', 'warn', 'important']
10
+ };
11
+
12
+ function toCamelCase(name) {
13
+ return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
14
+ }
15
+
4
16
  export function parseArgs() {
5
17
  return yargs(hideBin(process.argv))
6
18
  .usage('Usage: $0 [options]')
19
+ .parserConfiguration({
20
+ 'strip-aliased': true,
21
+ })
22
+ .example('$0 --markdown-file slides.md --theme-set themes', 'Preview a markdown deck with custom themes')
23
+ .example('$0', 'Automatically load .mp-config.json when it is present')
24
+ .example('$0 --config preview-config.json', 'Load options from a specific JSON config file')
25
+ .example('$0 --example-config', 'Print an example .mp-config.json and exit')
7
26
  .option('markdown-file', {
8
27
  alias: 'm',
9
28
  describe: 'Path to the markdown file to preview',
@@ -16,7 +35,7 @@ export function parseArgs() {
16
35
  default: ["note", "info", "warn", "important"]
17
36
  })
18
37
  .option('theme-set', {
19
- alias: 't',
38
+ alias: ['t', 'theme-dir'],
20
39
  describe: 'Directories for custom themes',
21
40
  type: 'array'
22
41
  })
@@ -32,7 +51,19 @@ export function parseArgs() {
32
51
  type: 'boolean',
33
52
  default: false
34
53
  })
35
- .config('config', 'Path to a JSON config file')
54
+ .option('example-config', {
55
+ describe: 'Print an example JSON config file and exit',
56
+ type: 'boolean',
57
+ default: false
58
+ })
59
+ .config('config', 'Path to a JSON config file. .mp-config.json is loaded automatically when present')
36
60
  .default('config', '.mp-config.json')
61
+ .middleware(argv => {
62
+ if (!argv.markdownFile && Array.isArray(argv._) && argv._.length > 0) {
63
+ [argv.markdownFile] = argv._;
64
+ }
65
+
66
+ argv.exampleConfig = argv.exampleConfig ?? argv[toCamelCase('example-config')] ?? argv['example-config'] ?? false;
67
+ }, true)
37
68
  .argv;
38
69
  }
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
9
9
  /* Sub-modules */
10
10
  import { createServer } from './server.mjs';
11
11
  import { initializeMarp, getMarp } from './marp-utils.mjs';
12
- import { parseArgs } from './args.mjs';
12
+ import { exampleConfig, parseArgs } from './args.mjs';
13
13
 
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
@@ -28,6 +28,11 @@ if (argv.version) {
28
28
  process.exit(0);
29
29
  }
30
30
 
31
+ if (argv.exampleConfig) {
32
+ console.log(JSON.stringify(exampleConfig, null, 2));
33
+ process.exit(0);
34
+ }
35
+
31
36
  if (verbose) {
32
37
  console.debug = console.log;
33
38
  } else {
@@ -168,7 +173,7 @@ if (themeSet) {
168
173
  }
169
174
 
170
175
  initializeMarp(themeSet, containers).then(() => {
171
- const app = createServer(markdownDir, renderMarp, reload, wss, __dirname);
176
+ const app = createServer(markdownDir, themeSet ?? [], renderMarp, reload, wss, __dirname);
172
177
  const server = http.createServer(app);
173
178
 
174
179
  server.on('upgrade', (request, socket, head) => {
@@ -13,7 +13,7 @@ export function getMarp() {
13
13
  }
14
14
 
15
15
 
16
- export async function initializeMarp(themeSet, containers) {
16
+ export async function initializeMarp(themeSet, containers = []) {
17
17
  const options = { html: true, linkify: true, };
18
18
  marp = new Marp(options)
19
19
  .use(markdownItFootnote)
package/src/server.mjs CHANGED
@@ -2,9 +2,12 @@ import express from 'express';
2
2
  import path from 'path';
3
3
  import { promises as fs } from 'fs';
4
4
 
5
- export function createServer(markdownDir, renderMarp, reload, wss, __dirname) {
5
+ export function createServer(markdownDir, themeDirs, renderMarp, reload, wss, __dirname) {
6
6
  const app = express();
7
7
 
8
+ for (const themeDir of themeDirs) {
9
+ app.use(express.static(themeDir));
10
+ }
8
11
  app.use(express.static(markdownDir));
9
12
  app.use(express.text({ type: 'text/markdown' }));
10
13
  app.use(express.json());
@@ -3,13 +3,20 @@ import { parseArgs } from '../src/args.mjs';
3
3
  const mockArgv = {};
4
4
  const defaultValues = {};
5
5
 
6
+ function mockToCamelCase(name) {
7
+ return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
8
+ }
9
+
6
10
  jest.mock('yargs', () => {
7
11
  const yargsMock = jest.fn(() => yargsMock);
8
12
  yargsMock.usage = jest.fn().mockReturnThis();
13
+ yargsMock.parserConfiguration = jest.fn().mockReturnThis();
14
+ yargsMock.example = jest.fn().mockReturnThis();
9
15
  yargsMock.positional = jest.fn().mockReturnThis();
10
16
  yargsMock.option = jest.fn().mockImplementation((name, options) => {
11
17
  if (options.default !== undefined) {
12
18
  defaultValues[name] = options.default;
19
+ defaultValues[mockToCamelCase(name)] = options.default;
13
20
  }
14
21
  return yargsMock;
15
22
  });
@@ -18,12 +25,25 @@ jest.mock('yargs', () => {
18
25
  defaultValues[name] = value;
19
26
  return yargsMock;
20
27
  });
28
+ yargsMock.middleware = jest.fn().mockImplementation((fn) => {
29
+ yargsMock.__middleware = fn;
30
+ return yargsMock;
31
+ });
21
32
  yargsMock.demandCommand = jest.fn().mockReturnThis();
22
33
  Object.defineProperty(yargsMock, 'argv', {
23
- get: () => ({
24
- ...defaultValues,
25
- ...mockArgv,
26
- }),
34
+ get: () => {
35
+ const argv = {
36
+ ...defaultValues,
37
+ ...mockArgv,
38
+ };
39
+ if (!Array.isArray(argv._)) {
40
+ argv._ = [];
41
+ }
42
+ if (yargsMock.__middleware) {
43
+ yargsMock.__middleware(argv);
44
+ }
45
+ return argv;
46
+ },
27
47
  });
28
48
  return yargsMock;
29
49
  });
@@ -71,14 +91,21 @@ describe('Args', () => {
71
91
  it('should capture markdown file positional argument', () => {
72
92
  mockArgv._ = ['my-presentation.md'];
73
93
  const argv = parseArgs();
74
- expect(argv._[0]).toBe('my-presentation.md');
94
+ expect(argv.markdownFile).toBe('my-presentation.md');
75
95
  });
76
96
 
77
- it('should return theme-dir if specified', () => {
97
+ it('should return theme-set if specified', () => {
78
98
  mockArgv._ = ['test.md'];
79
- mockArgv.themeDir = '/path/to/themes';
99
+ mockArgv.themeSet = ['/path/to/themes'];
80
100
  const argv = parseArgs();
81
- expect(argv.themeDir).toBe('/path/to/themes');
101
+ expect(argv.themeSet).toEqual(['/path/to/themes']);
102
+ });
103
+
104
+ it('should support the theme-dir alias', () => {
105
+ mockArgv._ = ['test.md'];
106
+ mockArgv.themeSet = ['/path/to/themes'];
107
+ const argv = parseArgs();
108
+ expect(argv.themeSet).toEqual(['/path/to/themes']);
82
109
  });
83
110
 
84
111
  it('should handle config file path', () => {
@@ -87,4 +114,23 @@ describe('Args', () => {
87
114
  const argv = parseArgs();
88
115
  expect(argv.config).toBe('./my-config.json');
89
116
  });
117
+
118
+ it('should keep an explicit markdown-file option over the positional argument', () => {
119
+ mockArgv._ = ['positional.md'];
120
+ mockArgv.markdownFile = 'flag.md';
121
+ const argv = parseArgs();
122
+ expect(argv.markdownFile).toBe('flag.md');
123
+ });
124
+
125
+ it('should return example-config as false by default', () => {
126
+ mockArgv._ = ['test.md'];
127
+ const argv = parseArgs();
128
+ expect(argv.exampleConfig).toBe(false);
129
+ });
130
+
131
+ it('should return example-config as true if specified', () => {
132
+ mockArgv.exampleConfig = true;
133
+ const argv = parseArgs();
134
+ expect(argv.exampleConfig).toBe(true);
135
+ });
90
136
  });
@@ -3,20 +3,41 @@
3
3
  // This is necessary because jest has issues with importing express, which is a CJS module,
4
4
  // in a project that uses ES modules ("type": "module" in package.json).
5
5
  import { createServer } from '../src/server.mjs';
6
+ import express from 'express';
6
7
 
7
8
  describe('Server', () => {
9
+ beforeEach(() => {
10
+ express.static.mockClear();
11
+ });
12
+
8
13
  it('should create a server', () => {
9
14
  const markdownDir = '.';
15
+ const themeDirs = [];
10
16
  const renderMarp = jest.fn();
11
17
  const reload = jest.fn();
12
18
  const wss = { clients: [] };
13
19
  const __dirname = '.';
14
20
 
15
- const app = createServer(markdownDir, renderMarp, reload, wss, __dirname);
21
+ const app = createServer(markdownDir, themeDirs, renderMarp, reload, wss, __dirname);
16
22
 
17
23
  expect(app).toBeDefined();
18
24
  expect(typeof app.use).toBe('function');
19
25
  expect(typeof app.get).toBe('function');
20
26
  expect(typeof app.post).toBe('function');
21
27
  });
28
+
29
+ it('should mount theme directories before the markdown directory', () => {
30
+ const markdownDir = '/slides';
31
+ const themeDirs = ['/themes/a', '/themes/b'];
32
+ const renderMarp = jest.fn();
33
+ const reload = jest.fn();
34
+ const wss = { clients: [] };
35
+ const __dirname = '.';
36
+
37
+ createServer(markdownDir, themeDirs, renderMarp, reload, wss, __dirname);
38
+
39
+ expect(express.static).toHaveBeenNthCalledWith(1, '/themes/a');
40
+ expect(express.static).toHaveBeenNthCalledWith(2, '/themes/b');
41
+ expect(express.static).toHaveBeenNthCalledWith(3, '/slides');
42
+ });
22
43
  });