marp-dev-preview 0.1.10 → 0.2.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/__mocks__/express.js +12 -0
- package/jest.config.mjs +5 -3
- package/package.json +2 -1
- package/src/client.js +3 -2
- package/src/marp-dev-preview.mjs +36 -6
- package/src/marp-utils.mjs +6 -1
- package/src/server.mjs +43 -86
- package/test/marp-dev-preview.test.mjs +9 -11
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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');
|
package/src/marp-dev-preview.mjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
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
|
|
|
8
9
|
/* Sub-modules */
|
|
9
10
|
import { createServer } from './server.mjs';
|
|
10
|
-
import { initializeMarp,
|
|
11
|
+
import { initializeMarp, getMarp } from './marp-utils.mjs';
|
|
11
12
|
import { parseArgs } from './args.mjs';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -39,11 +40,11 @@ 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');
|
|
46
|
-
const { html, css } =
|
|
47
|
+
const { html, css } = getMarp().render(md);
|
|
47
48
|
const customCss = `
|
|
48
49
|
svg[data-marpit-svg] {
|
|
49
50
|
margin-bottom:20px !important;
|
|
@@ -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>
|
|
@@ -125,7 +126,7 @@ async function renderMarp() {
|
|
|
125
126
|
|
|
126
127
|
async function reload(markdown) {
|
|
127
128
|
try {
|
|
128
|
-
const { html, css } =
|
|
129
|
+
const { html, css } = getMarp().render(markdown);
|
|
129
130
|
const message = JSON.stringify({
|
|
130
131
|
type: 'update',
|
|
131
132
|
html: html,
|
|
@@ -147,8 +148,37 @@ chokidar.watch(markdownFile).on('change', async () => {
|
|
|
147
148
|
await reload(md);
|
|
148
149
|
});
|
|
149
150
|
|
|
151
|
+
if (themeSet) {
|
|
152
|
+
for (const themeDir of themeSet) {
|
|
153
|
+
console.debug('Watching themes:', path.join(themeDir, '*.css'))
|
|
154
|
+
chokidar.watch(themeDir).on('change', async (file) => {
|
|
155
|
+
// if not a css file, ignore
|
|
156
|
+
console.debug('Detected change in file :', file);
|
|
157
|
+
if (path.extname(file) !== '.css') return;
|
|
158
|
+
|
|
159
|
+
console.debug('Reloading theme from file :', file);
|
|
160
|
+
|
|
161
|
+
console.debug(`Theme file ${file} changed, updating...`);
|
|
162
|
+
await initializeMarp(themeSet);
|
|
163
|
+
const md = await fs.readFile(markdownFile, 'utf8');
|
|
164
|
+
await reload(md);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
150
169
|
initializeMarp(themeSet).then(() => {
|
|
151
|
-
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
|
+
});
|
|
152
182
|
}).catch(error => {
|
|
153
183
|
console.error("Failed to initialize Marp:", error);
|
|
154
184
|
process.exit(1);
|
package/src/marp-utils.mjs
CHANGED
|
@@ -8,6 +8,11 @@ import markdownItContainer from 'markdown-it-container';
|
|
|
8
8
|
|
|
9
9
|
let marp;
|
|
10
10
|
|
|
11
|
+
export function getMarp() {
|
|
12
|
+
return marp;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
export async function initializeMarp(themeSet) {
|
|
12
17
|
const options = { html: true, linkify: true, };
|
|
13
18
|
marp = new Marp(options)
|
|
@@ -48,6 +53,6 @@ export async function initializeMarp(themeSet) {
|
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export function renderMarp(markdown) {
|
|
51
|
-
const { html, css } =
|
|
56
|
+
const { html, css } = getMarp().render(markdown);
|
|
52
57
|
return { html, css };
|
|
53
58
|
}
|
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
|
});
|