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 +16 -0
- package/marp-dev-preview.mjs +157 -103
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -65,6 +65,22 @@ While viewing the presentation in your browser, in addition to the usual browser
|
|
|
65
65
|
* <kbd>:<number></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.
|
package/marp-dev-preview.mjs
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 (
|
|
171
|
-
slides[slideNumber - 1].scrollIntoView({ behavior: 'smooth' });
|
|
207
|
+
if (goToSlide(slideNumber)) {
|
|
172
208
|
commandMode = false;
|
|
173
209
|
command = '';
|
|
174
|
-
updatePrompt(':' + command);
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
+
"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
|
-
|
|
8
|
+
"mdp": "marp-dev-preview.mjs"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
|
|
11
|
+
"start": "node marp-dev-preview.mjs"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
"marp",
|
|
15
|
+
"markdown",
|
|
16
|
+
"preview",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
19
|
"author": "Roberto Esposito",
|
|
20
20
|
"license": "MIT",
|
|
21
|
-
"repository":
|
|
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",
|