marp-dev-preview 0.0.3 → 0.0.5

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
@@ -65,6 +65,22 @@ While viewing the presentation in your browser, in addition to the usual browser
65
65
  * <kbd>:&lt;number&gt;</kbd>: Go to the specified slide number.
66
66
  * <kbd>?</kbd>: Toggle the help box displaying key bindings.
67
67
 
68
+ ## Integration with other tools
69
+
70
+ The tool can respond to http requests to change slides and to scroll to a slide containing a given text. Any http client can be used to send such requests, e.g.:
71
+
72
+ ```bash
73
+ curl -X POST -H "Content-Type: application/json" -d '{"command": "find", "string": "my awesome text"}' http://localhost:8080/api/command
74
+ ```
75
+ would scroll to the first slide containing "my awesome text".
76
+
77
+ ```bash
78
+ curl -X POST -H "Content-Type: application/json" -d '{"command": "goto", "slide": 3}' http://localhost:8080/api/command
79
+ ````
80
+
81
+ would go to slide 3.
82
+
83
+
68
84
  ## License
69
85
 
70
86
  This project is licensed under the MIT License - see the `LICENSE` file for details.
@@ -12,54 +12,54 @@ import markdownItMark from 'markdown-it-mark';
12
12
  import markdownItContainer from 'markdown-it-container';
13
13
 
14
14
  const argv = yargs(hideBin(process.argv))
15
- .usage('Usage: $0 <markdown-file> [options]')
16
- .positional('markdown-file', {
17
- describe: 'Path to the markdown file to preview',
18
- type: 'string'
19
- })
20
- .option('theme-dir', {
21
- alias: 't',
22
- describe: 'Directory for custom themes',
23
- type: 'string'
24
- })
25
- .option('port', {
26
- alias: 'p',
27
- describe: 'Port to listen on',
28
- type: 'number',
29
- default: 8080
30
- })
31
- .config('config', 'Path to a JSON config file')
32
- .default('config', '.mp-config.json')
33
- .demandCommand(1, 'You must provide a markdown file.')
34
- .argv;
15
+ .usage('Usage: $0 <markdown-file> [options]')
16
+ .positional('markdown-file', {
17
+ describe: 'Path to the markdown file to preview',
18
+ type: 'string'
19
+ })
20
+ .option('theme-dir', {
21
+ alias: 't',
22
+ describe: 'Directory for custom themes',
23
+ type: 'string'
24
+ })
25
+ .option('port', {
26
+ alias: 'p',
27
+ describe: 'Port to listen on',
28
+ type: 'number',
29
+ default: 8080
30
+ })
31
+ .config('config', 'Path to a JSON config file')
32
+ .default('config', '.mp-config.json')
33
+ .demandCommand(1, 'You must provide a markdown file.')
34
+ .argv;
35
35
 
36
36
  const markdownFile = argv._[0]
37
37
  const themeDir = argv.themeDir;
38
38
  const port = argv.port;
39
39
 
40
40
  if (!markdownFile) {
41
- console.error('Error: You must provide a path to a markdown file.');
42
- process.exit(1);
41
+ console.error('Error: You must provide a path to a markdown file.');
42
+ process.exit(1);
43
43
  }
44
44
 
45
45
  const markdownDir = path.dirname(markdownFile);
46
46
 
47
47
  const mimeTypes = {
48
- '.html': 'text/html',
49
- '.js': 'text/javascript',
50
- '.css': 'text/css',
51
- '.json': 'application/json',
52
- '.png': 'image/png',
53
- '.jpg': 'image/jpeg',
54
- '.gif': 'image/gif',
55
- '.svg': 'image/svg+xml',
56
- '.wav': 'audio/wav',
57
- '.mp4': 'video/mp4',
58
- '.woff': 'application/font-woff',
59
- '.ttf': 'application/font-ttf',
60
- '.eot': 'application/vnd.ms-fontobject',
61
- '.otf': 'application/font-otf',
62
- '.wasm': 'application/wasm'
48
+ '.html': 'text/html',
49
+ '.js': 'text/javascript',
50
+ '.css': 'text/css',
51
+ '.json': 'application/json',
52
+ '.png': 'image/png',
53
+ '.jpg': 'image/jpeg',
54
+ '.gif': 'image/gif',
55
+ '.svg': 'image/svg+xml',
56
+ '.wav': 'audio/wav',
57
+ '.mp4': 'video/mp4',
58
+ '.woff': 'application/font-woff',
59
+ '.ttf': 'application/font-ttf',
60
+ '.eot': 'application/vnd.ms-fontobject',
61
+ '.otf': 'application/font-otf',
62
+ '.wasm': 'application/wasm'
63
63
  };
64
64
 
65
65
  const wss = new WebSocketServer({ port: port + 1 });
@@ -67,28 +67,28 @@ const wss = new WebSocketServer({ port: port + 1 });
67
67
  let marp;
68
68
 
69
69
  async function initializeMarp() {
70
- const options = { html: true, linkify: true, };
71
- marp = new Marp(options)
72
- .use(markdownItFootnote)
73
- .use(markdownItMark)
74
- .use(markdownItContainer, 'note');
70
+ const options = { html: true, linkify: true, };
71
+ marp = new Marp(options)
72
+ .use(markdownItFootnote)
73
+ .use(markdownItMark)
74
+ .use(markdownItContainer, 'note');
75
75
 
76
- if (themeDir) {
77
- const themeFiles = await fs.readdir(themeDir);
78
- for (const file of themeFiles) {
79
- if (path.extname(file) === '.css') {
80
- const css = await fs.readFile(path.join(themeDir, file), 'utf8');
81
- marp.themeSet.add(css);
82
- }
83
- }
84
- }
76
+ if (themeDir) {
77
+ const themeFiles = await fs.readdir(themeDir);
78
+ for (const file of themeFiles) {
79
+ if (path.extname(file) === '.css') {
80
+ const css = await fs.readFile(path.join(themeDir, file), 'utf8');
81
+ marp.themeSet.add(css);
82
+ }
83
+ }
84
+ }
85
85
  }
86
86
 
87
87
 
88
88
  async function renderMarp() {
89
- const md = await fs.readFile(markdownFile, 'utf8');
90
- const { html, css } = marp.render(md);
91
- return `
89
+ const md = await fs.readFile(markdownFile, 'utf8');
90
+ const { html, css } = marp.render(md);
91
+ return `
92
92
  <!DOCTYPE html>
93
93
  <html>
94
94
  <head>
@@ -137,21 +137,58 @@ async function renderMarp() {
137
137
  </style>
138
138
  <script>
139
139
  const ws = new WebSocket('ws://localhost:${port + 1}');
140
- ws.onmessage = (event) => {
141
- if (event.data === 'reload') {
142
- window.location.reload();
143
- }
144
- };
145
-
146
- let lastKey = '';
147
- let command = '';
148
- let commandMode = false;
149
140
 
150
141
  document.addEventListener('DOMContentLoaded', () => {
151
142
  const slides = Array.from(document.querySelectorAll('section[id]'));
152
143
  const commandPrompt = document.getElementById('command-prompt');
153
144
  const helpBox = document.getElementById('help-box');
154
145
 
146
+ let lastKey = '';
147
+ let command = '';
148
+ let commandMode = false;
149
+
150
+ function goToSlide(slideNumber) {
151
+ if (!isNaN(slideNumber) && slideNumber > 0 && slideNumber <= slides.length) {
152
+ slides[slideNumber - 1].scrollIntoView({ behavior: 'smooth' });
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+
158
+ function findSlideByString(string) {
159
+ const lowerString = string.toLowerCase();
160
+ let found = false;
161
+ for (let i = 0; i < slides.length && !found; i++) {
162
+ if (slides[i].textContent.toLowerCase().includes(lowerString)) {
163
+ slides[i].scrollIntoView({ behavior: 'smooth' });
164
+ found = true;
165
+ }
166
+ }
167
+ if (!found) {
168
+ console.error('No slide contains the string: ' + string);
169
+ return false;
170
+ }
171
+
172
+ return true;
173
+ }
174
+
175
+ ws.onmessage = (event) => {
176
+ if (event.data === 'reload') {
177
+ window.location.reload();
178
+ return;
179
+ }
180
+ try {
181
+ const data = JSON.parse(event.data);
182
+ if (data.command === 'goto' && data.slide) {
183
+ goToSlide(parseInt(data.slide, 10));
184
+ } else if (data.command === 'find' && data.string) {
185
+ findSlideByString(data.string);
186
+ }
187
+ } catch (e) {
188
+ console.error('Failed to parse WebSocket message:', e);
189
+ }
190
+ };
191
+
155
192
  function updatePrompt(text, isError = false) {
156
193
  if (commandMode) {
157
194
  commandPrompt.style.display = 'block';
@@ -167,11 +204,10 @@ async function renderMarp() {
167
204
  if (commandMode) {
168
205
  if (e.key === 'Enter') {
169
206
  const slideNumber = parseInt(command, 10);
170
- if (!isNaN(slideNumber) && slideNumber > 0 && slideNumber <= slides.length) {
171
- slides[slideNumber - 1].scrollIntoView({ behavior: 'smooth' });
207
+ if (goToSlide(slideNumber)) {
172
208
  commandMode = false;
173
209
  command = '';
174
- updatePrompt(':' + command); // Reset to normal prompt
210
+ updatePrompt(':' + command);
175
211
  } else {
176
212
  updatePrompt(\`Error: Slide not found.\`, true); // Pass message and error flag
177
213
  setTimeout(() => {
@@ -245,51 +281,69 @@ async function renderMarp() {
245
281
  }
246
282
 
247
283
  const server = http.createServer(async (req, res) => {
284
+ try {
285
+ if (req.url === '/') {
286
+ const html = await renderMarp();
287
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
288
+ res.end(html);
289
+ } else if (req.url === '/api/command' && req.method === 'POST') {
290
+ let body = '';
291
+ req.on('data', chunk => {
292
+ body += chunk.toString();
293
+ });
294
+ req.on('end', () => {
248
295
  try {
249
- if (req.url === '/') {
250
- const html = await renderMarp();
251
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
252
- res.end(html);
253
- } else {
254
- const assetPath = path.join(markdownDir, req.url);
255
- const ext = path.extname(assetPath);
256
- const contentType = mimeTypes[ext] || 'application/octet-stream';
296
+ const command = JSON.parse(body);
297
+ for (const ws of wss.clients) {
298
+ ws.send(JSON.stringify(command));
299
+ }
300
+ res.writeHead(200, { 'Content-Type': 'application/json' });
301
+ res.end(JSON.stringify({ status: 'ok', command }));
302
+ } catch (e) {
303
+ res.writeHead(400, { 'Content-Type': 'application/json' });
304
+ res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON' }));
305
+ }
306
+ });
307
+ } else {
308
+ const assetPath = path.join(markdownDir, req.url);
309
+ const ext = path.extname(assetPath);
310
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
257
311
 
258
- try {
259
- const content = await fs.readFile(assetPath);
260
- res.writeHead(200, { 'Content-Type': contentType });
261
- res.end(content);
262
- } catch (error) {
263
- if (error.code === 'ENOENT') {
264
- res.writeHead(404);
265
- res.end('Not Found');
266
- } else {
267
- throw error;
268
- }
269
- }
270
- }
271
- } catch (error) {
272
- console.error(error);
273
- res.writeHead(500);
274
- res.end('Internal Server Error');
312
+ try {
313
+ const content = await fs.readFile(assetPath);
314
+ res.writeHead(200, { 'Content-Type': contentType });
315
+ res.end(content);
316
+ } catch (error) {
317
+ if (error.code === 'ENOENT') {
318
+ res.writeHead(404);
319
+ res.end('Not Found');
320
+ } else {
321
+ throw error;
275
322
  }
323
+ }
324
+ }
325
+ } catch (error) {
326
+ console.error(error);
327
+ res.writeHead(500);
328
+ res.end('Internal Server Error');
329
+ }
276
330
  });
277
331
 
278
332
  chokidar.watch(markdownFile).on('change', () => {
279
- console.log(`File ${markdownFile} changed, reloading...`);
280
- for (const ws of wss.clients) {
281
- ws.send('reload');
282
- }
333
+ console.log(`File ${markdownFile} changed, reloading...`);
334
+ for (const ws of wss.clients) {
335
+ ws.send('reload');
336
+ }
283
337
  });
284
338
 
285
339
  initializeMarp().then(() => {
286
- server.listen(port, () => {
287
- console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
288
- if (themeDir) {
289
- console.log(`Using custom themes from ${themeDir}`);
290
- }
291
- });
340
+ server.listen(port, () => {
341
+ console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
342
+ if (themeDir) {
343
+ console.log(`Using custom themes from ${themeDir}`);
344
+ }
345
+ });
292
346
  }).catch(error => {
293
- console.error("Failed to initialize Marp:", error);
294
- process.exit(1);
347
+ console.error("Failed to initialize Marp:", error);
348
+ process.exit(1);
295
349
  });
package/package.json CHANGED
@@ -1,24 +1,27 @@
1
1
  {
2
2
  "name": "marp-dev-preview",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A CLI tool to preview Marp markdown files.",
5
5
  "main": "marp-dev-preview.mjs",
6
6
  "type": "module",
7
7
  "bin": {
8
- "mdp":"./marp-dev-preview.mjs"
8
+ "mdp": "marp-dev-preview.mjs"
9
9
  },
10
10
  "scripts": {
11
- "start": "node marp-dev-preview.mjs"
11
+ "start": "node marp-dev-preview.mjs"
12
12
  },
13
13
  "keywords": [
14
- "marp",
15
- "markdown",
16
- "preview",
17
- "cli"
18
- ],
14
+ "marp",
15
+ "markdown",
16
+ "preview",
17
+ "cli"
18
+ ],
19
19
  "author": "Roberto Esposito",
20
20
  "license": "MIT",
21
- "repository": "git@github.com:boborbt/marp-dev-preview.git",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+ssh://git@github.com/boborbt/marp-dev-preview.git"
24
+ },
22
25
  "bugs": "https://github.com/boborbt/marp-dev-preview/issues",
23
26
  "dependencies": {
24
27
  "@marp-team/marp-core": "^4.1.0",