marp-dev-preview 0.0.1
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/LICENCE +7 -0
- package/README.md +70 -0
- package/marp-preview.mjs +287 -0
- package/package.json +31 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) <year> <copyright holders>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# marp-dev-preview
|
|
2
|
+
|
|
3
|
+
A CLI tool to preview Marp markdown files with live reloading and navigation features.
|
|
4
|
+
|
|
5
|
+
The tool is mainly intended for slide deck authors who want to preview their slides in a web browser during development. In fact, while marp-cli provides a `-s` option to serve the slides, it is mainly intended for presenting the slides, not for development. This tool fills that gap by providing a live preview server presenting the slides similarly to how they are presented in the marp vs-code extension.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Live preview of Marp markdown files.
|
|
10
|
+
* Automatic browser reload on file changes.
|
|
11
|
+
* Custom theme support.
|
|
12
|
+
* Keyboard navigation for slides.
|
|
13
|
+
* Also installs and uses the following markdown-it plugins:
|
|
14
|
+
* `markdown-it-container`
|
|
15
|
+
* `markdown-it-mark`
|
|
16
|
+
* `markdown-it-footnote`
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
To install the package globally (recommended for CLI tools):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g marp-dev-preview
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Alternatively, you can install it as a local dependency in your project:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install marp-dev-preview
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
To start the preview server, run the `mp` command followed by your markdown file path:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
mp <path-to-your-markdown-file.md> [options]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Example:**
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
mp my-slides/presentation.md --port 3000 --theme-dir my-themes
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Options
|
|
47
|
+
|
|
48
|
+
* `-t, --theme-dir <path>`: Specify a directory for custom Marp themes (e.g., CSS files).
|
|
49
|
+
* `-p, --port <number>`: Specify the port for the preview server to listen on (default: `8080`).
|
|
50
|
+
|
|
51
|
+
## Keyboard Shortcuts
|
|
52
|
+
|
|
53
|
+
While viewing the presentation in your browser, in addition to the usual browser controls (page-up, page-down, home, end, etc.), you can use the following key bindings:
|
|
54
|
+
|
|
55
|
+
* <kbd>gg</kbd>: Go to the first slide.
|
|
56
|
+
* <kbd>G</kbd>: Go to the last slide.
|
|
57
|
+
* <kbd>:<number></kbd>: Go to the specified slide number.
|
|
58
|
+
* <kbd>?</kbd>: Toggle the help box displaying key bindings.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
If you are contributing to the development of this tool, you can run it locally using:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run preview -- <path-to-your-markdown-file.md>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
package/marp-preview.mjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Marp } from '@marp-team/marp-core';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import chokidar from 'chokidar';
|
|
7
|
+
import { WebSocketServer } from 'ws';
|
|
8
|
+
import yargs from 'yargs';
|
|
9
|
+
import { hideBin } from 'yargs/helpers';
|
|
10
|
+
|
|
11
|
+
const argv = yargs(hideBin(process.argv))
|
|
12
|
+
.usage('Usage: $0 <markdown-file> [options]')
|
|
13
|
+
.positional('markdown-file', {
|
|
14
|
+
describe: 'Path to the markdown file to preview',
|
|
15
|
+
type: 'string'
|
|
16
|
+
})
|
|
17
|
+
.option('theme-dir', {
|
|
18
|
+
alias: 't',
|
|
19
|
+
describe: 'Directory for custom themes',
|
|
20
|
+
type: 'string'
|
|
21
|
+
})
|
|
22
|
+
.option('port', {
|
|
23
|
+
alias: 'p',
|
|
24
|
+
describe: 'Port to listen on',
|
|
25
|
+
type: 'number',
|
|
26
|
+
default: 8080
|
|
27
|
+
})
|
|
28
|
+
.config('config', 'Path to a JSON config file')
|
|
29
|
+
.default('config', '.mp-config.json')
|
|
30
|
+
.demandCommand(1, 'You must provide a markdown file.')
|
|
31
|
+
.argv;
|
|
32
|
+
|
|
33
|
+
const markdownFile = argv._[0]
|
|
34
|
+
const themeDir = argv.themeDir;
|
|
35
|
+
const port = argv.port;
|
|
36
|
+
|
|
37
|
+
if (!markdownFile) {
|
|
38
|
+
console.error('Error: You must provide a path to a markdown file.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const markdownDir = path.dirname(markdownFile);
|
|
43
|
+
|
|
44
|
+
const mimeTypes = {
|
|
45
|
+
'.html': 'text/html',
|
|
46
|
+
'.js': 'text/javascript',
|
|
47
|
+
'.css': 'text/css',
|
|
48
|
+
'.json': 'application/json',
|
|
49
|
+
'.png': 'image/png',
|
|
50
|
+
'.jpg': 'image/jpeg',
|
|
51
|
+
'.gif': 'image/gif',
|
|
52
|
+
'.svg': 'image/svg+xml',
|
|
53
|
+
'.wav': 'audio/wav',
|
|
54
|
+
'.mp4': 'video/mp4',
|
|
55
|
+
'.woff': 'application/font-woff',
|
|
56
|
+
'.ttf': 'application/font-ttf',
|
|
57
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
58
|
+
'.otf': 'application/font-otf',
|
|
59
|
+
'.wasm': 'application/wasm'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const wss = new WebSocketServer({ port: port + 1 });
|
|
63
|
+
|
|
64
|
+
let marp;
|
|
65
|
+
|
|
66
|
+
async function initializeMarp() {
|
|
67
|
+
const options = { html: true, linkify: true, };
|
|
68
|
+
marp = new Marp(options);
|
|
69
|
+
if (themeDir) {
|
|
70
|
+
const themeFiles = await fs.readdir(themeDir);
|
|
71
|
+
for (const file of themeFiles) {
|
|
72
|
+
if (path.extname(file) === '.css') {
|
|
73
|
+
const css = await fs.readFile(path.join(themeDir, file), 'utf8');
|
|
74
|
+
marp.themeSet.add(css);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async function renderMarp() {
|
|
82
|
+
const md = await fs.readFile(markdownFile, 'utf8');
|
|
83
|
+
const { html, css } = marp.render(md);
|
|
84
|
+
return `
|
|
85
|
+
<!DOCTYPE html>
|
|
86
|
+
<html>
|
|
87
|
+
<head>
|
|
88
|
+
<style>
|
|
89
|
+
${css}
|
|
90
|
+
svg[data-marpit-svg] {
|
|
91
|
+
margin-bottom:20px !important;
|
|
92
|
+
border: 1px solid gray;
|
|
93
|
+
border-radius: 10px;
|
|
94
|
+
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
|
95
|
+
}
|
|
96
|
+
#help-box table td, #help-box table th {
|
|
97
|
+
padding: 0 15px 0 15px;
|
|
98
|
+
}
|
|
99
|
+
#help-box {
|
|
100
|
+
position: fixed;
|
|
101
|
+
top: 50%;
|
|
102
|
+
left: 50%;
|
|
103
|
+
transform: translate(-50%, -50%);
|
|
104
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
105
|
+
color: white;
|
|
106
|
+
padding: 20px;
|
|
107
|
+
border-radius: 10px;
|
|
108
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
109
|
+
z-index: 1001;
|
|
110
|
+
display: none; /* Initially hidden */
|
|
111
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
|
112
|
+
max-width: 400px;
|
|
113
|
+
text-align: left;
|
|
114
|
+
}
|
|
115
|
+
#command-prompt {
|
|
116
|
+
position: fixed;
|
|
117
|
+
bottom: 20px;
|
|
118
|
+
left: 50%;
|
|
119
|
+
transform: translateX(-50%);
|
|
120
|
+
background-color: #333;
|
|
121
|
+
color: white;
|
|
122
|
+
padding: 10px 20px;
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
125
|
+
font-size: 1.2em;
|
|
126
|
+
z-index: 1000;
|
|
127
|
+
display: none;
|
|
128
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
131
|
+
<script>
|
|
132
|
+
const ws = new WebSocket('ws://localhost:${port + 1}');
|
|
133
|
+
ws.onmessage = (event) => {
|
|
134
|
+
if (event.data === 'reload') {
|
|
135
|
+
window.location.reload();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
let lastKey = '';
|
|
140
|
+
let command = '';
|
|
141
|
+
let commandMode = false;
|
|
142
|
+
|
|
143
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
144
|
+
const slides = Array.from(document.querySelectorAll('section[id]'));
|
|
145
|
+
const commandPrompt = document.getElementById('command-prompt');
|
|
146
|
+
const helpBox = document.getElementById('help-box');
|
|
147
|
+
|
|
148
|
+
function updatePrompt(text, isError = false) {
|
|
149
|
+
if (commandMode) {
|
|
150
|
+
commandPrompt.style.display = 'block';
|
|
151
|
+
commandPrompt.textContent = text;
|
|
152
|
+
commandPrompt.style.color = isError ? 'red' : 'white';
|
|
153
|
+
} else {
|
|
154
|
+
commandPrompt.style.display = 'none';
|
|
155
|
+
commandPrompt.style.color = 'white'; // Reset color when hidden
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
document.addEventListener('keydown', (e) => {
|
|
160
|
+
if (commandMode) {
|
|
161
|
+
if (e.key === 'Enter') {
|
|
162
|
+
const slideNumber = parseInt(command, 10);
|
|
163
|
+
if (!isNaN(slideNumber) && slideNumber > 0 && slideNumber <= slides.length) {
|
|
164
|
+
slides[slideNumber - 1].scrollIntoView({ behavior: 'smooth' });
|
|
165
|
+
commandMode = false;
|
|
166
|
+
command = '';
|
|
167
|
+
updatePrompt(':' + command); // Reset to normal prompt
|
|
168
|
+
} else {
|
|
169
|
+
updatePrompt(\`Error: Slide not found.\`, true); // Pass message and error flag
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
commandMode = false;
|
|
172
|
+
command = '';
|
|
173
|
+
updatePrompt(':' + command); // Reset to normal prompt
|
|
174
|
+
}, 2000);
|
|
175
|
+
}
|
|
176
|
+
} else if (e.key === 'Backspace') {
|
|
177
|
+
command = command.slice(0, -1);
|
|
178
|
+
updatePrompt(':' + command);
|
|
179
|
+
} else if (e.key.length === 1 && !isNaN(parseInt(e.key,10))) {
|
|
180
|
+
command += e.key;
|
|
181
|
+
updatePrompt(':' + command);
|
|
182
|
+
} else if (e.key === 'Escape') {
|
|
183
|
+
commandMode = false;
|
|
184
|
+
command = '';
|
|
185
|
+
updatePrompt(':' + command);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (e.key === 'g') {
|
|
191
|
+
if (lastKey === 'g') {
|
|
192
|
+
// gg
|
|
193
|
+
if (slides.length > 0) {
|
|
194
|
+
slides[0].scrollIntoView({ behavior: 'smooth' });
|
|
195
|
+
}
|
|
196
|
+
lastKey = '';
|
|
197
|
+
} else {
|
|
198
|
+
lastKey = 'g';
|
|
199
|
+
setTimeout(() => { lastKey = '' }, 500); // reset after 500ms
|
|
200
|
+
}
|
|
201
|
+
} else if (e.key === 'G') {
|
|
202
|
+
if (slides.length > 0) {
|
|
203
|
+
slides[slides.length - 1].scrollIntoView({ behavior: 'smooth' });
|
|
204
|
+
}
|
|
205
|
+
lastKey = '';
|
|
206
|
+
} else if (e.key === ':') {
|
|
207
|
+
commandMode = true;
|
|
208
|
+
command = '';
|
|
209
|
+
lastKey = '';
|
|
210
|
+
updatePrompt(':' + command);
|
|
211
|
+
} else if (e.key === '?') {
|
|
212
|
+
helpBox.style.display = helpBox.style.display === 'none' ? 'block' : 'none';
|
|
213
|
+
lastKey = ''; // Reset lastKey to prevent unintended 'gg'
|
|
214
|
+
} else {
|
|
215
|
+
lastKey = '';
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
</script>
|
|
220
|
+
</head>
|
|
221
|
+
<body>
|
|
222
|
+
${html}
|
|
223
|
+
<div id="help-box">
|
|
224
|
+
<h3>Key Bindings</h3>
|
|
225
|
+
<table>
|
|
226
|
+
<tr><th>Key</th><th>Action</th></tr>
|
|
227
|
+
<tr><td><kbd>gg</kbd> or <kbd>Home</kbd></td><td>Go to first slide</td></tr>
|
|
228
|
+
<tr><td><kbd>G</kbd> or <kbd>End</kbd></td><td>Go to last slide</td></tr>
|
|
229
|
+
<tr><td><kbd>:<number></kbd></td><td>Go to the given slide number</td></tr>
|
|
230
|
+
<tr><td><kbd>?</kbd></td><td>Show/hide help</td></tr>
|
|
231
|
+
</table>
|
|
232
|
+
</div>
|
|
233
|
+
<div id="command-prompt"></div>
|
|
234
|
+
</body>
|
|
235
|
+
</html>
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const server = http.createServer(async (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
if (req.url === '/') {
|
|
242
|
+
const html = await renderMarp();
|
|
243
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
244
|
+
res.end(html);
|
|
245
|
+
} else {
|
|
246
|
+
const assetPath = path.join(markdownDir, req.url);
|
|
247
|
+
const ext = path.extname(assetPath);
|
|
248
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const content = await fs.readFile(assetPath);
|
|
252
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
253
|
+
res.end(content);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error.code === 'ENOENT') {
|
|
256
|
+
res.writeHead(404);
|
|
257
|
+
res.end('Not Found');
|
|
258
|
+
} else {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(error);
|
|
265
|
+
res.writeHead(500);
|
|
266
|
+
res.end('Internal Server Error');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
chokidar.watch(markdownFile).on('change', () => {
|
|
271
|
+
console.log(`File ${markdownFile} changed, reloading...`);
|
|
272
|
+
for (const ws of wss.clients) {
|
|
273
|
+
ws.send('reload');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
initializeMarp().then(() => {
|
|
278
|
+
server.listen(port, () => {
|
|
279
|
+
console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
|
|
280
|
+
if (themeDir) {
|
|
281
|
+
console.log(`Using custom themes from ${themeDir}`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}).catch(error => {
|
|
285
|
+
console.error("Failed to initialize Marp:", error);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "marp-dev-preview",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A CLI tool to preview Marp markdown files.",
|
|
5
|
+
"main": "marp-preview.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mp":"./marp-preview.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node marp-preview.mjs",
|
|
12
|
+
"preview": "node marp-preview.mjs"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"marp",
|
|
16
|
+
"markdown",
|
|
17
|
+
"preview",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "Roberto Esposito",
|
|
21
|
+
"licence": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@marp-team/marp-core": "^4.1.0",
|
|
24
|
+
"chokidar": "^4.0.3",
|
|
25
|
+
"markdown-it-container": "^4.0.0",
|
|
26
|
+
"markdown-it-footnote": "^4.0.0",
|
|
27
|
+
"markdown-it-mark": "^4.0.0",
|
|
28
|
+
"ws": "^8.18.3",
|
|
29
|
+
"yargs": "^18.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|