marp-dev-preview 0.1.11 → 0.2.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/README.md +2 -2
- package/__mocks__/express.js +12 -0
- package/jest.config.mjs +5 -3
- package/package.json +2 -1
- package/src/client.js +5 -4
- package/src/marp-dev-preview.mjs +17 -5
- package/src/server.mjs +43 -86
- package/test/marp-dev-preview.test.mjs +9 -11
package/README.md
CHANGED
|
@@ -73,8 +73,8 @@ mdp my-slides/presentation.md --port 3000 --theme-dir my-themes
|
|
|
73
73
|
|
|
74
74
|
In addition to normal browser navigation keys (`Page Up`, `Page Down`, `Home`, `End`), the following bindings are available:
|
|
75
75
|
|
|
76
|
-
- **Ctrl+f** — Forward one page
|
|
77
|
-
- **Ctrl+b** — Backward one page
|
|
76
|
+
- **Ctrl+f** or **Ctrl+n** — Forward one page
|
|
77
|
+
- **Ctrl+b** or **Ctrl+p** — Backward one page
|
|
78
78
|
- **Ctrl+d** — Forward half a page
|
|
79
79
|
- **Ctrl+u** — Backward half a page
|
|
80
80
|
- **gg** — First slide
|
package/jest.config.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
transform: {
|
|
3
|
-
'^.+\.m?js'
|
|
4
|
-
: 'babel-jest',
|
|
3
|
+
'^.+\.m?js': 'babel-jest',
|
|
5
4
|
},
|
|
6
5
|
testEnvironment: 'node',
|
|
7
6
|
moduleFileExtensions: ['js', 'mjs'],
|
|
8
7
|
transformIgnorePatterns: [
|
|
9
8
|
'/node_modules/(?!yargs|yargs-parser)/',
|
|
10
9
|
],
|
|
11
|
-
|
|
10
|
+
moduleNameMapper: {
|
|
11
|
+
'^express$': '<rootDir>/__mocks__/express.js',
|
|
12
|
+
},
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marp-dev-preview",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI tool to preview Marp markdown files.",
|
|
5
5
|
"main": "src/marp-dev-preview.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@marp-team/marp-core": "^4.1.0",
|
|
29
29
|
"chokidar": "^4.0.3",
|
|
30
|
+
"express": "^5.1.0",
|
|
30
31
|
"markdown-it-container": "^4.0.0",
|
|
31
32
|
"markdown-it-footnote": "^4.0.0",
|
|
32
33
|
"markdown-it-mark": "^4.0.0",
|
package/src/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const wsHost = window.location.host;
|
|
3
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
4
|
+
const ws = new WebSocket(`${wsProtocol}//${wsHost}`);
|
|
4
5
|
|
|
5
6
|
let slides = Array.from(document.querySelectorAll('section[id]'));
|
|
6
7
|
const commandPrompt = document.getElementById('command-prompt');
|
|
@@ -152,10 +153,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
152
153
|
} else if (e.key === 'd' && e.ctrlKey) {
|
|
153
154
|
window.scrollBy({ top: window.innerHeight * 0.5, behavior: 'smooth' });
|
|
154
155
|
lastKey = '';
|
|
155
|
-
} else if (e.key === 'f' && e.ctrlKey) {
|
|
156
|
+
} else if ((e.key === 'f' || e.key === 'n') && e.ctrlKey) {
|
|
156
157
|
window.scrollBy({ top: window.innerHeight * 0.9, behavior: 'smooth' });
|
|
157
158
|
lastKey = '';
|
|
158
|
-
} else if (e.key === 'b' && e.ctrlKey) {
|
|
159
|
+
} else if ((e.key === 'b' || e.key === 'p') && e.ctrlKey) {
|
|
159
160
|
window.scrollBy({ top: -window.innerHeight * 0.9, behavior: 'smooth' });
|
|
160
161
|
lastKey = '';
|
|
161
162
|
} else if (e.key === '?') {
|
package/src/marp-dev-preview.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
|
+
import http from 'http';
|
|
5
6
|
import { WebSocketServer } from 'ws';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
8
|
|
|
@@ -39,7 +40,7 @@ if (!markdownFile) {
|
|
|
39
40
|
|
|
40
41
|
const markdownDir = path.dirname(markdownFile);
|
|
41
42
|
|
|
42
|
-
const wss = new WebSocketServer({
|
|
43
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
43
44
|
|
|
44
45
|
async function renderMarp() {
|
|
45
46
|
const md = await fs.readFile(markdownFile, 'utf8');
|
|
@@ -91,7 +92,7 @@ async function renderMarp() {
|
|
|
91
92
|
<!DOCTYPE html>
|
|
92
93
|
<html>
|
|
93
94
|
<head>
|
|
94
|
-
<meta name="ws-port" content="${port
|
|
95
|
+
<meta name="ws-port" content="${port}">
|
|
95
96
|
<style id="marp-style">${css}</style>
|
|
96
97
|
<style id="custom-style">${customCss}</style>
|
|
97
98
|
<script src="https://unpkg.com/morphdom@2.7.0/dist/morphdom-umd.min.js"></script>
|
|
@@ -110,8 +111,8 @@ async function renderMarp() {
|
|
|
110
111
|
<tr><td><kbd>gg</kbd> or <kbd>Home</kbd></td><td>Go to first slide</td></tr>
|
|
111
112
|
<tr><td><kbd>G</kbd> or <kbd>End</kbd></td><td>Go to last slide</td></tr>
|
|
112
113
|
<tr><td><kbd>:<number></kbd></td><td>Go to the given slide number</td></tr>
|
|
113
|
-
<tr><td><kbd>^f</kbd></td><td>Forward one page</td></tr>
|
|
114
|
-
<tr><td><kbd>^b</kbd></td><td>Back one page</td></tr>
|
|
114
|
+
<tr><td><kbd>^f</kbd> or <kbd>^n</kbd></td><td>Forward one page</td></tr>
|
|
115
|
+
<tr><td><kbd>^b</kbd> or <kbd>^p</kbd></td><td>Back one page</td></tr>
|
|
115
116
|
<tr><td><kbd>^d</kbd></td><td>Forward half a page</td></tr>
|
|
116
117
|
<tr><td><kbd>^u</kbd></td><td>Back half a page</td></tr>
|
|
117
118
|
<tr><td><kbd>?</kbd></td><td>Show/hide help</td></tr>
|
|
@@ -166,7 +167,18 @@ if (themeSet) {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
initializeMarp(themeSet).then(() => {
|
|
169
|
-
createServer(
|
|
170
|
+
const app = createServer(markdownDir, renderMarp, reload, wss, __dirname);
|
|
171
|
+
const server = http.createServer(app);
|
|
172
|
+
|
|
173
|
+
server.on('upgrade', (request, socket, head) => {
|
|
174
|
+
wss.handleUpgrade(request, socket, head, ws => {
|
|
175
|
+
wss.emit('connection', ws, request);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
server.listen(port, () => {
|
|
180
|
+
console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
|
|
181
|
+
});
|
|
170
182
|
}).catch(error => {
|
|
171
183
|
console.error("Failed to initialize Marp:", error);
|
|
172
184
|
process.exit(1);
|
package/src/server.mjs
CHANGED
|
@@ -1,99 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import { promises as fs } from 'fs';
|
|
1
|
+
import express from 'express';
|
|
4
2
|
import path from 'path';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
|
|
5
|
+
export function createServer(markdownDir, renderMarp, reload, wss, __dirname) {
|
|
6
|
+
const app = express();
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'.css': 'text/css',
|
|
10
|
-
'.json': 'application/json',
|
|
11
|
-
'.png': 'image/png',
|
|
12
|
-
'.jpg': 'image/jpeg',
|
|
13
|
-
'.gif': 'image/gif',
|
|
14
|
-
'.svg': 'image/svg+xml',
|
|
15
|
-
'.wav': 'audio/wav',
|
|
16
|
-
'.mp4': 'video/mp4',
|
|
17
|
-
'.woff': 'application/font-woff',
|
|
18
|
-
'.ttf': 'application/font-ttf',
|
|
19
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
20
|
-
'.otf': 'application/font-otf',
|
|
21
|
-
'.wasm': 'application/wasm'
|
|
22
|
-
};
|
|
8
|
+
app.use(express.static(markdownDir));
|
|
9
|
+
app.use(express.text({ type: 'text/markdown' }));
|
|
10
|
+
app.use(express.json());
|
|
23
11
|
|
|
24
|
-
|
|
25
|
-
const server = http.createServer(async (req, res) => {
|
|
12
|
+
app.get('/', async (req, res) => {
|
|
26
13
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
res.end(clientJs);
|
|
35
|
-
} else if (req.url === '/api/reload' && req.method === 'POST') {
|
|
36
|
-
let body = '';
|
|
37
|
-
req.on('data', chunk => {
|
|
38
|
-
body += chunk.toString();
|
|
39
|
-
});
|
|
40
|
-
req.on('end', async () => {
|
|
41
|
-
console.debug("Reload request received");
|
|
42
|
-
const success = await reload(body);
|
|
43
|
-
if (success) {
|
|
44
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
45
|
-
res.end(JSON.stringify({ status: 'ok' }));
|
|
46
|
-
} else {
|
|
47
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
48
|
-
res.end(JSON.stringify({ status: 'error', message: 'Failed to render markdown' }));
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
} else if (req.url === '/api/command' && req.method === 'POST') {
|
|
52
|
-
let body = '';
|
|
53
|
-
req.on('data', chunk => {
|
|
54
|
-
body += chunk.toString();
|
|
55
|
-
});
|
|
56
|
-
req.on('end', () => {
|
|
57
|
-
try {
|
|
58
|
-
const command = JSON.parse(body);
|
|
59
|
-
for (const ws of wss.clients) {
|
|
60
|
-
ws.send(JSON.stringify(command));
|
|
61
|
-
}
|
|
62
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
63
|
-
res.end(JSON.stringify({ status: 'ok', command }));
|
|
64
|
-
} catch (e) {
|
|
65
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
66
|
-
res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON' }));
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
} else {
|
|
70
|
-
const assetPath = path.join(markdownDir, req.url);
|
|
71
|
-
const ext = path.extname(assetPath);
|
|
72
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
14
|
+
const html = await renderMarp();
|
|
15
|
+
res.send(html);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(error);
|
|
18
|
+
res.status(500).send('Internal Server Error');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
73
21
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (error.code === 'ENOENT') {
|
|
80
|
-
res.writeHead(404);
|
|
81
|
-
res.end('Not Found');
|
|
82
|
-
} else {
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
22
|
+
app.get('/client.js', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const clientJs = await fs.readFile(path.join(__dirname, 'client.js'), 'utf8');
|
|
25
|
+
res.type('js').send(clientJs);
|
|
87
26
|
} catch (error) {
|
|
88
27
|
console.error(error);
|
|
89
|
-
res.
|
|
90
|
-
|
|
28
|
+
res.status(500).send('Internal Server Error');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.post('/api/reload', async (req, res) => {
|
|
33
|
+
console.debug("Reload request received");
|
|
34
|
+
console.debug(req.body)
|
|
35
|
+
const success = await reload(req.body);
|
|
36
|
+
if (success) {
|
|
37
|
+
res.json({ status: 'ok' });
|
|
38
|
+
} else {
|
|
39
|
+
res.status(500).json({ status: 'error', message: 'Failed to render markdown' });
|
|
91
40
|
}
|
|
92
41
|
});
|
|
93
42
|
|
|
94
|
-
|
|
95
|
-
|
|
43
|
+
app.post('/api/command', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const command = req.body;
|
|
46
|
+
for (const ws of wss.clients) {
|
|
47
|
+
ws.send(JSON.stringify(command));
|
|
48
|
+
}
|
|
49
|
+
res.json({ status: 'ok', command });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
res.status(400).json({ status: 'error', message: 'Invalid JSON' });
|
|
52
|
+
}
|
|
96
53
|
});
|
|
97
54
|
|
|
98
|
-
return
|
|
55
|
+
return app;
|
|
99
56
|
}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
+
// This test uses a mock for the express module.
|
|
2
|
+
// The mock is defined in __mocks__/express.js and configured in jest.config.mjs.
|
|
3
|
+
// This is necessary because jest has issues with importing express, which is a CJS module,
|
|
4
|
+
// in a project that uses ES modules ("type": "module" in package.json).
|
|
1
5
|
import { createServer } from '../src/server.mjs';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
|
|
4
|
-
jest.mock('http', () => ({
|
|
5
|
-
createServer: jest.fn(() => ({
|
|
6
|
-
listen: jest.fn(),
|
|
7
|
-
})),
|
|
8
|
-
}));
|
|
9
6
|
|
|
10
7
|
describe('Server', () => {
|
|
11
8
|
it('should create a server', () => {
|
|
12
|
-
const port = 8080;
|
|
13
|
-
const markdownFile = 'test.md';
|
|
14
9
|
const markdownDir = '.';
|
|
15
10
|
const renderMarp = jest.fn();
|
|
16
11
|
const reload = jest.fn();
|
|
17
12
|
const wss = { clients: [] };
|
|
18
13
|
const __dirname = '.';
|
|
19
14
|
|
|
20
|
-
createServer(
|
|
15
|
+
const app = createServer(markdownDir, renderMarp, reload, wss, __dirname);
|
|
21
16
|
|
|
22
|
-
expect(
|
|
17
|
+
expect(app).toBeDefined();
|
|
18
|
+
expect(typeof app.use).toBe('function');
|
|
19
|
+
expect(typeof app.get).toBe('function');
|
|
20
|
+
expect(typeof app.post).toBe('function');
|
|
23
21
|
});
|
|
24
22
|
});
|