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 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
@@ -0,0 +1,12 @@
1
+ const express = () => ({
2
+ use: jest.fn(),
3
+ get: jest.fn(),
4
+ post: jest.fn(),
5
+ listen: jest.fn(),
6
+ });
7
+
8
+ express.static = jest.fn();
9
+ express.text = jest.fn();
10
+ express.json = jest.fn();
11
+
12
+ export default express;
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.11",
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 wsPort = document.querySelector('meta[name="ws-port"]').content;
3
- const ws = new WebSocket(`ws://localhost:${wsPort}`);
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 === '?') {
@@ -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({ port: port + 1 });
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 + 1}">
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>:&lt;number&gt</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(port, markdownFile, markdownDir, renderMarp, reload, wss, __dirname);
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
- const mimeTypes = {
7
- '.html': 'text/html',
8
- '.js': 'text/javascript',
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
- export function createServer(port, markdownFile, markdownDir, renderMarp, reload, wss, __dirname) {
25
- const server = http.createServer(async (req, res) => {
12
+ app.get('/', async (req, res) => {
26
13
  try {
27
- if (req.url === '/') {
28
- const html = await renderMarp();
29
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
30
- res.end(html);
31
- } else if (req.url === '/client.js') {
32
- const clientJs = await fs.readFile(path.join(__dirname, 'client.js'), 'utf8');
33
- res.writeHead(200, { 'Content-Type': 'text/javascript' });
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
- try {
75
- const content = await fs.readFile(assetPath);
76
- res.writeHead(200, { 'Content-Type': contentType });
77
- res.end(content);
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.writeHead(500);
90
- res.end('Internal Server Error');
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
- server.listen(port, () => {
95
- console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
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 server;
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(port, markdownFile, markdownDir, renderMarp, reload, wss, __dirname);
15
+ const app = createServer(markdownDir, renderMarp, reload, wss, __dirname);
21
16
 
22
- expect(http.createServer).toHaveBeenCalled();
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
  });