marp-dev-preview 0.0.4 → 0.1.0
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 +2 -1
- package/client.js +143 -0
- package/marp-dev-preview.mjs +168 -241
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ The tool is mainly intended for slide deck authors who want to preview their sli
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
* Live preview of Marp markdown files.
|
|
9
|
+
* Live preview of Marp markdown files, with position syncing.
|
|
10
|
+
* API to reload the slides using incremental updates.
|
|
10
11
|
* Automatic browser reload on file changes.
|
|
11
12
|
* Custom theme support.
|
|
12
13
|
* Keyboard navigation for slides.
|
package/client.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
const wsPort = document.querySelector('meta[name="ws-port"]').content;
|
|
3
|
+
const ws = new WebSocket(`ws://localhost:${wsPort}`);
|
|
4
|
+
|
|
5
|
+
let slides = Array.from(document.querySelectorAll('section[id]'));
|
|
6
|
+
const commandPrompt = document.getElementById('command-prompt');
|
|
7
|
+
const helpBox = document.getElementById('help-box');
|
|
8
|
+
|
|
9
|
+
let lastKey = '';
|
|
10
|
+
let command = '';
|
|
11
|
+
let commandMode = false;
|
|
12
|
+
|
|
13
|
+
function goToSlide(slideNumber) {
|
|
14
|
+
if (isNaN(slideNumber)) {
|
|
15
|
+
console.error('Invalid slide number: ' + slideNumber);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (slideNumber <= 0) {
|
|
20
|
+
console.error('Slide number must be greater than 0: ' + slideNumber);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (slideNumber > slides.length) {
|
|
25
|
+
console.error('Slide number exceeds total slides: ' + slideNumber);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.info('Navigating to slide: ' + slideNumber);
|
|
30
|
+
|
|
31
|
+
slides[slideNumber - 1].scrollIntoView({ behavior: 'smooth' });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findSlideByString(string) {
|
|
36
|
+
const lowerString = string.toLowerCase();
|
|
37
|
+
let found = false;
|
|
38
|
+
for (let i = 0; i < slides.length && !found; i++) {
|
|
39
|
+
if (slides[i].textContent.toLowerCase().includes(lowerString)) {
|
|
40
|
+
slides[i].scrollIntoView({ behavior: 'smooth' });
|
|
41
|
+
found = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!found) {
|
|
45
|
+
console.error('No slide contains the string: ' + string);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ws.onmessage = (event) => {
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.parse(event.data);
|
|
55
|
+
if (data.type === 'update') {
|
|
56
|
+
const marpContainer = document.getElementById('marp-container');
|
|
57
|
+
if (marpContainer) {
|
|
58
|
+
morphdom(marpContainer, `<div id="marp-container">${data.html}</div>`);
|
|
59
|
+
}
|
|
60
|
+
if (document.getElementById('marp-style').innerHTML !== data.css) {
|
|
61
|
+
document.getElementById('marp-style').innerHTML = data.css;
|
|
62
|
+
}
|
|
63
|
+
slides = Array.from(document.querySelectorAll('section[id]'));
|
|
64
|
+
} else if (data.command === 'goto' && data.slide) {
|
|
65
|
+
goToSlide(parseInt(data.slide, 10));
|
|
66
|
+
} else if (data.command === 'find' && data.string) {
|
|
67
|
+
findSlideByString(data.string);
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('Failed to parse WebSocket message:', e);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function updatePrompt(text, isError = false) {
|
|
75
|
+
if (commandMode) {
|
|
76
|
+
commandPrompt.style.display = 'block';
|
|
77
|
+
commandPrompt.textContent = text;
|
|
78
|
+
commandPrompt.style.color = isError ? 'red' : 'white';
|
|
79
|
+
} else {
|
|
80
|
+
commandPrompt.style.display = 'none';
|
|
81
|
+
commandPrompt.style.color = 'white'; // Reset color when hidden
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
document.addEventListener('keydown', (e) => {
|
|
86
|
+
if (commandMode) {
|
|
87
|
+
if (e.key === 'Enter') {
|
|
88
|
+
const slideNumber = parseInt(command, 10);
|
|
89
|
+
if (goToSlide(slideNumber)) {
|
|
90
|
+
commandMode = false;
|
|
91
|
+
command = '';
|
|
92
|
+
updatePrompt(':' + command);
|
|
93
|
+
} else {
|
|
94
|
+
updatePrompt(`Error: Slide not found.`, true); // Pass message and error flag
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
commandMode = false;
|
|
97
|
+
command = '';
|
|
98
|
+
updatePrompt(':' + command); // Reset to normal prompt
|
|
99
|
+
}, 2000);
|
|
100
|
+
}
|
|
101
|
+
} else if (e.key === 'Backspace') {
|
|
102
|
+
command = command.slice(0, -1);
|
|
103
|
+
updatePrompt(':' + command);
|
|
104
|
+
} else if (e.key.length === 1 && !isNaN(parseInt(e.key, 10))) {
|
|
105
|
+
command += e.key;
|
|
106
|
+
updatePrompt(':' + command);
|
|
107
|
+
} else if (e.key === 'Escape') {
|
|
108
|
+
commandMode = false;
|
|
109
|
+
command = '';
|
|
110
|
+
updatePrompt(':' + command);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (e.key === 'g') {
|
|
116
|
+
if (lastKey === 'g') {
|
|
117
|
+
// gg
|
|
118
|
+
if (slides.length > 0) {
|
|
119
|
+
slides[0].scrollIntoView({ behavior: 'smooth' });
|
|
120
|
+
}
|
|
121
|
+
lastKey = '';
|
|
122
|
+
} else {
|
|
123
|
+
lastKey = 'g';
|
|
124
|
+
setTimeout(() => { lastKey = '' }, 500); // reset after 500ms
|
|
125
|
+
}
|
|
126
|
+
} else if (e.key === 'G') {
|
|
127
|
+
if (slides.length > 0) {
|
|
128
|
+
slides[slides.length - 1].scrollIntoView({ behavior: 'smooth' });
|
|
129
|
+
}
|
|
130
|
+
lastKey = '';
|
|
131
|
+
} else if (e.key === ':') {
|
|
132
|
+
commandMode = true;
|
|
133
|
+
command = '';
|
|
134
|
+
lastKey = '';
|
|
135
|
+
updatePrompt(':' + command);
|
|
136
|
+
} else if (e.key === '?') {
|
|
137
|
+
helpBox.style.display = helpBox.style.display === 'none' ? 'block' : 'none';
|
|
138
|
+
lastKey = ''; // Reset lastKey to prevent unintended 'gg'
|
|
139
|
+
} else {
|
|
140
|
+
lastKey = '';
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
package/marp-dev-preview.mjs
CHANGED
|
@@ -10,56 +10,61 @@ import { hideBin } from 'yargs/helpers';
|
|
|
10
10
|
import markdownItFootnote from 'markdown-it-footnote';
|
|
11
11
|
import markdownItMark from 'markdown-it-mark';
|
|
12
12
|
import markdownItContainer from 'markdown-it-container';
|
|
13
|
+
import morphdom from 'morphdom';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
13
18
|
|
|
14
19
|
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
|
-
|
|
20
|
+
.usage('Usage: $0 <markdown-file> [options]')
|
|
21
|
+
.positional('markdown-file', {
|
|
22
|
+
describe: 'Path to the markdown file to preview',
|
|
23
|
+
type: 'string'
|
|
24
|
+
})
|
|
25
|
+
.option('theme-dir', {
|
|
26
|
+
alias: 't',
|
|
27
|
+
describe: 'Directory for custom themes',
|
|
28
|
+
type: 'string'
|
|
29
|
+
})
|
|
30
|
+
.option('port', {
|
|
31
|
+
alias: 'p',
|
|
32
|
+
describe: 'Port to listen on',
|
|
33
|
+
type: 'number',
|
|
34
|
+
default: 8080
|
|
35
|
+
})
|
|
36
|
+
.config('config', 'Path to a JSON config file')
|
|
37
|
+
.default('config', '.mp-config.json')
|
|
38
|
+
.demandCommand(1, 'You must provide a markdown file.')
|
|
39
|
+
.argv;
|
|
35
40
|
|
|
36
41
|
const markdownFile = argv._[0]
|
|
37
42
|
const themeDir = argv.themeDir;
|
|
38
43
|
const port = argv.port;
|
|
39
44
|
|
|
40
45
|
if (!markdownFile) {
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
console.error('Error: You must provide a path to a markdown file.');
|
|
47
|
+
process.exit(1);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
const markdownDir = path.dirname(markdownFile);
|
|
46
51
|
|
|
47
52
|
const mimeTypes = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
'.html': 'text/html',
|
|
54
|
+
'.js': 'text/javascript',
|
|
55
|
+
'.css': 'text/css',
|
|
56
|
+
'.json': 'application/json',
|
|
57
|
+
'.png': 'image/png',
|
|
58
|
+
'.jpg': 'image/jpeg',
|
|
59
|
+
'.gif': 'image/gif',
|
|
60
|
+
'.svg': 'image/svg+xml',
|
|
61
|
+
'.wav': 'audio/wav',
|
|
62
|
+
'.mp4': 'video/mp4',
|
|
63
|
+
'.woff': 'application/font-woff',
|
|
64
|
+
'.ttf': 'application/font-ttf',
|
|
65
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
66
|
+
'.otf': 'application/font-otf',
|
|
67
|
+
'.wasm': 'application/wasm'
|
|
63
68
|
};
|
|
64
69
|
|
|
65
70
|
const wss = new WebSocketServer({ port: port + 1 });
|
|
@@ -67,33 +72,28 @@ const wss = new WebSocketServer({ port: port + 1 });
|
|
|
67
72
|
let marp;
|
|
68
73
|
|
|
69
74
|
async function initializeMarp() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const options = { html: true, linkify: true, };
|
|
76
|
+
marp = new Marp(options)
|
|
77
|
+
.use(markdownItFootnote)
|
|
78
|
+
.use(markdownItMark)
|
|
79
|
+
.use(markdownItContainer, 'note');
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if (themeDir) {
|
|
82
|
+
const themeFiles = await fs.readdir(themeDir);
|
|
83
|
+
for (const file of themeFiles) {
|
|
84
|
+
if (path.extname(file) === '.css') {
|
|
85
|
+
const css = await fs.readFile(path.join(themeDir, file), 'utf8');
|
|
86
|
+
marp.themeSet.add(css);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
async function renderMarp() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<!DOCTYPE html>
|
|
93
|
-
<html>
|
|
94
|
-
<head>
|
|
95
|
-
<style>
|
|
96
|
-
${css}
|
|
94
|
+
const md = await fs.readFile(markdownFile, 'utf8');
|
|
95
|
+
const { html, css } = marp.render(md);
|
|
96
|
+
const customCss = `
|
|
97
97
|
svg[data-marpit-svg] {
|
|
98
98
|
margin-bottom:20px !important;
|
|
99
99
|
border: 1px solid gray;
|
|
@@ -134,133 +134,23 @@ async function renderMarp() {
|
|
|
134
134
|
display: none;
|
|
135
135
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
<script>
|
|
139
|
-
const ws = new WebSocket('ws://localhost:${port + 1}');
|
|
140
|
-
|
|
141
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
142
|
-
const slides = Array.from(document.querySelectorAll('section[id]'));
|
|
143
|
-
const commandPrompt = document.getElementById('command-prompt');
|
|
144
|
-
const helpBox = document.getElementById('help-box');
|
|
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; 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
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
ws.onmessage = (event) => {
|
|
173
|
-
if (event.data === 'reload') {
|
|
174
|
-
window.location.reload();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
const data = JSON.parse(event.data);
|
|
179
|
-
if (data.command === 'goto' && data.slide) {
|
|
180
|
-
goToSlide(parseInt(data.slide, 10));
|
|
181
|
-
} else if (data.command === 'find' && data.string) {
|
|
182
|
-
findSlideByString(data.string);
|
|
183
|
-
}
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.error('Failed to parse WebSocket message:', e);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
function updatePrompt(text, isError = false) {
|
|
190
|
-
if (commandMode) {
|
|
191
|
-
commandPrompt.style.display = 'block';
|
|
192
|
-
commandPrompt.textContent = text;
|
|
193
|
-
commandPrompt.style.color = isError ? 'red' : 'white';
|
|
194
|
-
} else {
|
|
195
|
-
commandPrompt.style.display = 'none';
|
|
196
|
-
commandPrompt.style.color = 'white'; // Reset color when hidden
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
document.addEventListener('keydown', (e) => {
|
|
201
|
-
if (commandMode) {
|
|
202
|
-
if (e.key === 'Enter') {
|
|
203
|
-
const slideNumber = parseInt(command, 10);
|
|
204
|
-
if (goToSlide(slideNumber)) {
|
|
205
|
-
commandMode = false;
|
|
206
|
-
command = '';
|
|
207
|
-
updatePrompt(':' + command);
|
|
208
|
-
} else {
|
|
209
|
-
updatePrompt(\`Error: Slide not found.\`, true); // Pass message and error flag
|
|
210
|
-
setTimeout(() => {
|
|
211
|
-
commandMode = false;
|
|
212
|
-
command = '';
|
|
213
|
-
updatePrompt(':' + command); // Reset to normal prompt
|
|
214
|
-
}, 2000);
|
|
215
|
-
}
|
|
216
|
-
} else if (e.key === 'Backspace') {
|
|
217
|
-
command = command.slice(0, -1);
|
|
218
|
-
updatePrompt(':' + command);
|
|
219
|
-
} else if (e.key.length === 1 && !isNaN(parseInt(e.key,10))) {
|
|
220
|
-
command += e.key;
|
|
221
|
-
updatePrompt(':' + command);
|
|
222
|
-
} else if (e.key === 'Escape') {
|
|
223
|
-
commandMode = false;
|
|
224
|
-
command = '';
|
|
225
|
-
updatePrompt(':' + command);
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
137
|
+
`;
|
|
229
138
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
} else if (e.key === 'G') {
|
|
242
|
-
if (slides.length > 0) {
|
|
243
|
-
slides[slides.length - 1].scrollIntoView({ behavior: 'smooth' });
|
|
244
|
-
}
|
|
245
|
-
lastKey = '';
|
|
246
|
-
} else if (e.key === ':') {
|
|
247
|
-
commandMode = true;
|
|
248
|
-
command = '';
|
|
249
|
-
lastKey = '';
|
|
250
|
-
updatePrompt(':' + command);
|
|
251
|
-
} else if (e.key === '?') {
|
|
252
|
-
helpBox.style.display = helpBox.style.display === 'none' ? 'block' : 'none';
|
|
253
|
-
lastKey = ''; // Reset lastKey to prevent unintended 'gg'
|
|
254
|
-
} else {
|
|
255
|
-
lastKey = '';
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
</script>
|
|
260
|
-
<meta charset="UTF-8">
|
|
139
|
+
return `
|
|
140
|
+
<!DOCTYPE html>
|
|
141
|
+
<html>
|
|
142
|
+
<head>
|
|
143
|
+
<meta name="ws-port" content="${port + 1}">
|
|
144
|
+
<style id="marp-style">${css}</style>
|
|
145
|
+
<style id="custom-style">${customCss}</style>
|
|
146
|
+
<script src="https://unpkg.com/morphdom@2.7.0/dist/morphdom-umd.min.js"></script>
|
|
147
|
+
<script src="/client.js"></script>
|
|
148
|
+
<meta charset="UTF-8">
|
|
261
149
|
</head>
|
|
262
150
|
<body>
|
|
263
|
-
|
|
151
|
+
<div id="marp-container">
|
|
152
|
+
${html}
|
|
153
|
+
</div>
|
|
264
154
|
<div id="help-box">
|
|
265
155
|
<h3>Key Bindings</h3>
|
|
266
156
|
<table>
|
|
@@ -278,69 +168,106 @@ async function renderMarp() {
|
|
|
278
168
|
}
|
|
279
169
|
|
|
280
170
|
const server = http.createServer(async (req, res) => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
171
|
+
try {
|
|
172
|
+
if (req.url === '/') {
|
|
173
|
+
const html = await renderMarp();
|
|
174
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
175
|
+
res.end(html);
|
|
176
|
+
} else if (req.url === '/client.js') {
|
|
177
|
+
const clientJs = await fs.readFile(path.join(__dirname, 'client.js'), 'utf8');
|
|
178
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript' });
|
|
179
|
+
res.end(clientJs);
|
|
180
|
+
} else if (req.url === '/api/reload' && req.method === 'POST') {
|
|
181
|
+
let body = '';
|
|
182
|
+
req.on('data', chunk => {
|
|
183
|
+
body += chunk.toString();
|
|
184
|
+
});
|
|
185
|
+
req.on('end', async () => {
|
|
186
|
+
console.debug("Reload request received");
|
|
187
|
+
const success = await reload(body);
|
|
188
|
+
if (success) {
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
191
|
+
} else {
|
|
192
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
193
|
+
res.end(JSON.stringify({ status: 'error', message: 'Failed to render markdown' }));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
} else if (req.url === '/api/command' && req.method === 'POST') {
|
|
197
|
+
let body = '';
|
|
198
|
+
req.on('data', chunk => {
|
|
199
|
+
body += chunk.toString();
|
|
200
|
+
});
|
|
201
|
+
req.on('end', () => {
|
|
202
|
+
try {
|
|
203
|
+
const command = JSON.parse(body);
|
|
204
|
+
for (const ws of wss.clients) {
|
|
205
|
+
ws.send(JSON.stringify(command));
|
|
206
|
+
}
|
|
207
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
208
|
+
res.end(JSON.stringify({ status: 'ok', command }));
|
|
209
|
+
} catch (e) {
|
|
210
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
211
|
+
res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON' }));
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
const assetPath = path.join(markdownDir, req.url);
|
|
216
|
+
const ext = path.extname(assetPath);
|
|
217
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
308
218
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
219
|
+
try {
|
|
220
|
+
const content = await fs.readFile(assetPath);
|
|
221
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
222
|
+
res.end(content);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error.code === 'ENOENT') {
|
|
225
|
+
res.writeHead(404);
|
|
226
|
+
res.end('Not Found');
|
|
227
|
+
} else {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(error);
|
|
234
|
+
res.writeHead(500);
|
|
235
|
+
res.end('Internal Server Error');
|
|
236
|
+
}
|
|
327
237
|
});
|
|
328
238
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
239
|
+
async function reload(markdown) {
|
|
240
|
+
try {
|
|
241
|
+
const { html, css } = marp.render(markdown);
|
|
242
|
+
const message = JSON.stringify({
|
|
243
|
+
type: 'update',
|
|
244
|
+
html: html,
|
|
245
|
+
css: css
|
|
246
|
+
});
|
|
247
|
+
for (const ws of wss.clients) {
|
|
248
|
+
ws.send(message);
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('Error rendering or sending update:', error);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
chokidar.watch(markdownFile).on('change', async () => {
|
|
258
|
+
console.log(`File ${markdownFile} changed, updating...`);
|
|
259
|
+
const md = await fs.readFile(markdownFile, 'utf8');
|
|
260
|
+
await reload(md);
|
|
334
261
|
});
|
|
335
262
|
|
|
336
263
|
initializeMarp().then(() => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
264
|
+
server.listen(port, () => {
|
|
265
|
+
console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
|
|
266
|
+
if (themeDir) {
|
|
267
|
+
console.log(`Using custom themes from ${themeDir}`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
343
270
|
}).catch(error => {
|
|
344
|
-
|
|
345
|
-
|
|
271
|
+
console.error("Failed to initialize Marp:", error);
|
|
272
|
+
process.exit(1);
|
|
346
273
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marp-dev-preview",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A CLI tool to preview Marp markdown files.",
|
|
5
5
|
"main": "marp-dev-preview.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"markdown-it-container": "^4.0.0",
|
|
30
30
|
"markdown-it-footnote": "^4.0.0",
|
|
31
31
|
"markdown-it-mark": "^4.0.0",
|
|
32
|
+
"morphdom": "^2.7.7",
|
|
32
33
|
"ws": "^8.18.3",
|
|
33
34
|
"yargs": "^18.0.0"
|
|
34
35
|
}
|