marp-dev-preview 0.1.4 → 0.1.6
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/.babelrc +3 -0
- package/README.md +106 -43
- package/jest.config.mjs +11 -0
- package/package.json +12 -4
- package/src/args.mjs +32 -0
- package/{client.js → src/client.js} +18 -0
- package/src/marp-dev-preview.mjs +158 -0
- package/src/marp-utils.mjs +33 -0
- package/src/server.mjs +99 -0
- package/test/args.test.mjs +90 -0
- package/test/marp-dev-preview.test.mjs +24 -0
- package/test/marp-utils.test.mjs +29 -0
- package/marp-dev-preview.mjs +0 -314
package/.babelrc
ADDED
package/README.md
CHANGED
|
@@ -1,52 +1,62 @@
|
|
|
1
1
|
# marp-dev-preview
|
|
2
2
|
|
|
3
|
-
A CLI tool
|
|
3
|
+
A lightweight CLI tool for previewing **Marp markdown slide decks** with live reloading, navigation, and development-focused features.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Unlike the `marp-cli -s` option—which is primarily meant for presenting—this tool is designed specifically for **authoring and iterating** on slides. It provides a live preview server similar to the Marp VS Code extension, but in a terminal-friendly, editor-agnostic way.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Originally built as a dependency for the [marp-dev-preview.nvim](https://github.com/boborbt/marp-dev-preview.nvim) NeoVim plugin, it can also be used **standalone** or as a dependency in other projects.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* API to reload the slides using incremental updates.
|
|
11
|
-
* Automatic browser reload on file changes.
|
|
12
|
-
* Custom theme support.
|
|
13
|
-
* Keyboard navigation for slides.
|
|
14
|
-
* Also installs and uses the following markdown-it plugins (it's easy to add more if needed):
|
|
15
|
-
* `markdown-it-container`
|
|
16
|
-
* `markdown-it-mark`
|
|
17
|
-
* `markdown-it-footnote`
|
|
18
|
-
|
|
19
|
-
## Usage via npx
|
|
9
|
+
---
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- **Live preview** of Marp markdown files, with position syncing
|
|
14
|
+
- **Incremental updates API** for fast reloads
|
|
15
|
+
- **Automatic browser reload** on file changes
|
|
16
|
+
- **Custom theme support** (CSS-based themes)
|
|
17
|
+
- **Keyboard navigation** for slides
|
|
18
|
+
- Includes several **markdown-it plugins** (easy to extend):
|
|
19
|
+
- `markdown-it-container`
|
|
20
|
+
- `markdown-it-mark`
|
|
21
|
+
- `markdown-it-footnote`
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚦 Quick Start (via `npx`)
|
|
26
|
+
|
|
27
|
+
The simplest way to run the previewer:
|
|
22
28
|
|
|
23
29
|
```bash
|
|
24
|
-
npx marp-dev-preview --theme-dir <dir
|
|
30
|
+
npx marp-dev-preview --theme-dir <themes-dir> <presentation.md>
|
|
25
31
|
```
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📦 Installation
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
Global install (recommended for CLI use):
|
|
30
38
|
|
|
31
39
|
```bash
|
|
32
40
|
npm install -g marp-dev-preview
|
|
33
41
|
```
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
Or as a local project dependency:
|
|
36
44
|
|
|
37
45
|
```bash
|
|
38
46
|
npm install marp-dev-preview
|
|
39
47
|
```
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ▶️ Usage
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
Start the preview server with:
|
|
44
54
|
|
|
45
55
|
```bash
|
|
46
|
-
mdp <path-to-
|
|
56
|
+
mdp <path-to-markdown-file> [options]
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
**Example:**
|
|
59
|
+
**Example:**
|
|
50
60
|
|
|
51
61
|
```bash
|
|
52
62
|
mdp my-slides/presentation.md --port 3000 --theme-dir my-themes
|
|
@@ -54,34 +64,87 @@ mdp my-slides/presentation.md --port 3000 --theme-dir my-themes
|
|
|
54
64
|
|
|
55
65
|
### Options
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
- `-t, --theme-dir <path>` — Path to custom Marp themes (CSS files)
|
|
68
|
+
- `-p, --port <number>` — Port for the preview server (default: `8080`)
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
---
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
## ⌨️ Keyboard Shortcuts
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
* <kbd>G</kbd>: Go to the last slide.
|
|
66
|
-
* <kbd>:<number></kbd>: Go to the specified slide number.
|
|
67
|
-
* <kbd>?</kbd>: Toggle the help box displaying key bindings.
|
|
74
|
+
In addition to normal browser navigation keys (`Page Up`, `Page Down`, `Home`, `End`), the following bindings are available:
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
- **Ctrl+f** — Forward one page
|
|
77
|
+
- **Ctrl+b** — Backward one page
|
|
78
|
+
- **Ctrl+d** — Forward half a page
|
|
79
|
+
- **Ctrl+u** — Backward half a page
|
|
80
|
+
- **gg** — First slide
|
|
81
|
+
- **G** — Last slide
|
|
82
|
+
- **:number** — Jump to slide `{number}`
|
|
83
|
+
- **?** — Toggle help overlay
|
|
70
84
|
|
|
71
|
-
|
|
85
|
+
---
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
curl -X POST -H "Content-Type: application/json" -d '{"command": "find", "string": "my awesome text"}' http://localhost:8080/api/command
|
|
75
|
-
```
|
|
76
|
-
would scroll to the first slide containing "my awesome text".
|
|
87
|
+
## 🔗 API Endpoints
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
The preview server exposes a simple **HTTP API** for controlling slides and content.
|
|
90
|
+
|
|
91
|
+
### `POST /api/reload`
|
|
92
|
+
|
|
93
|
+
Reloads the presentation with new markdown content. The server parses the received markdown, renders it into HTML, and broadcasts the changes to all connected clients. This is ideal for tools that need to push updates without requiring a full page refresh.
|
|
94
|
+
|
|
95
|
+
- **Request**:
|
|
96
|
+
- **Headers**: `Content-Type: text/markdown`
|
|
97
|
+
- **Body**: Raw markdown content.
|
|
98
|
+
|
|
99
|
+
- **Response**:
|
|
100
|
+
- `200 OK`: `{ "status": "ok" }` on success.
|
|
101
|
+
- `500 Internal Server Error`: `{ "status": "error", "message": "..." }` on failure.
|
|
102
|
+
|
|
103
|
+
- **Example**:
|
|
104
|
+
```bash
|
|
105
|
+
curl -X POST \
|
|
106
|
+
-H "Content-Type: text/markdown" \
|
|
107
|
+
-d "$(cat path/to/your/slides.md)" \
|
|
108
|
+
http://localhost:8080/api/reload
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `POST /api/command`
|
|
112
|
+
|
|
113
|
+
Sends a command to the browser to control the presentation's navigation. This allows external tools to programmatically change the visible slide.
|
|
114
|
+
|
|
115
|
+
- **Request**:
|
|
116
|
+
- **Headers**: `Content-Type: application/json`
|
|
117
|
+
- **Body**: A JSON object describing the command.
|
|
118
|
+
|
|
119
|
+
- **Response**:
|
|
120
|
+
- `200 OK`: `{ "status": "ok", "command": { ... } }` on success.
|
|
121
|
+
- `400 Bad Request`: `{ "status": "error", "message": "Invalid JSON" }` if the body is malformed.
|
|
122
|
+
|
|
123
|
+
#### Supported Commands
|
|
124
|
+
|
|
125
|
+
1. **`goto`**: Jumps to a specific slide number.
|
|
126
|
+
- **Payload**: `{ "command": "goto", "slide": <number> }`
|
|
127
|
+
- **Example**:
|
|
128
|
+
```bash
|
|
129
|
+
curl -X POST \
|
|
130
|
+
-H "Content-Type: application/json" \
|
|
131
|
+
-d '{"command": "goto", "slide": 5}' \
|
|
132
|
+
http://localhost:8080/api/command
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
2. **`find`**: Searches for a string and jumps to the first slide containing it. The search is case-insensitive.
|
|
136
|
+
- **Payload**: `{ "command": "find", "string": "<search-term>" }`
|
|
137
|
+
- **Example**:
|
|
138
|
+
```bash
|
|
139
|
+
curl -X POST \
|
|
140
|
+
-H "Content-Type: application/json" \
|
|
141
|
+
-d '{"command": "find", "string": "My Awesome Slide"}' \
|
|
142
|
+
http://localhost:8080/api/command
|
|
143
|
+
```
|
|
81
144
|
|
|
82
|
-
would go to slide 3.
|
|
83
145
|
|
|
146
|
+
---
|
|
84
147
|
|
|
85
|
-
## License
|
|
148
|
+
## 📄 License
|
|
86
149
|
|
|
87
|
-
|
|
150
|
+
Licensed under the [MIT License](./LICENSE).
|
package/jest.config.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marp-dev-preview",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A CLI tool to preview Marp markdown files.",
|
|
5
|
-
"main": "marp-dev-preview.mjs",
|
|
5
|
+
"main": "src/marp-dev-preview.mjs",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mdp": "marp-dev-preview.mjs"
|
|
8
|
+
"mdp": "src/marp-dev-preview.mjs"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "node marp-dev-preview.mjs"
|
|
11
|
+
"start": "node src/marp-dev-preview.mjs",
|
|
12
|
+
"test": "jest"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"marp",
|
|
@@ -32,5 +33,12 @@
|
|
|
32
33
|
"morphdom": "^2.7.7",
|
|
33
34
|
"ws": "^8.18.3",
|
|
34
35
|
"yargs": "^18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@babel/preset-env": "^7.28.3",
|
|
39
|
+
"babel-jest": "^30.1.2",
|
|
40
|
+
"jest": "^30.1.3",
|
|
41
|
+
"jest-esm-transformer": "^1.0.0",
|
|
42
|
+
"ts-jest": "^29.4.1"
|
|
35
43
|
}
|
|
36
44
|
}
|
package/src/args.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
|
|
4
|
+
export function parseArgs() {
|
|
5
|
+
return yargs(hideBin(process.argv))
|
|
6
|
+
.usage('Usage: $0 <markdown-file> [options]')
|
|
7
|
+
.positional('markdown-file', {
|
|
8
|
+
describe: 'Path to the markdown file to preview',
|
|
9
|
+
type: 'string'
|
|
10
|
+
})
|
|
11
|
+
.option('theme-dir', {
|
|
12
|
+
alias: 't',
|
|
13
|
+
describe: 'Directory for custom themes',
|
|
14
|
+
type: 'string'
|
|
15
|
+
})
|
|
16
|
+
.option('port', {
|
|
17
|
+
alias: 'p',
|
|
18
|
+
describe: 'Port to listen on',
|
|
19
|
+
type: 'number',
|
|
20
|
+
default: 8080
|
|
21
|
+
})
|
|
22
|
+
.option('verbose', {
|
|
23
|
+
alias: 'v',
|
|
24
|
+
describe: 'Enable verbose logging',
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: false
|
|
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
|
+
}
|
|
@@ -133,6 +133,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
133
133
|
command = '';
|
|
134
134
|
lastKey = '';
|
|
135
135
|
updatePrompt(':' + command);
|
|
136
|
+
} else if (e.key === 'j') {
|
|
137
|
+
window.scrollBy({ top: window.innerHeight * 0.1, behavior: 'smooth' });
|
|
138
|
+
lastKey = '';
|
|
139
|
+
} else if (e.key === 'k') {
|
|
140
|
+
window.scrollBy({ top: -window.innerHeight * 0.1, behavior: 'smooth' });
|
|
141
|
+
lastKey = '';
|
|
142
|
+
} else if (e.key === 'u' && e.ctrlKey) {
|
|
143
|
+
window.scrollBy({ top: -window.innerHeight * 0.5, behavior: 'smooth' });
|
|
144
|
+
lastKey = '';
|
|
145
|
+
} else if (e.key === 'd' && e.ctrlKey) {
|
|
146
|
+
window.scrollBy({ top: window.innerHeight * 0.5, behavior: 'smooth' });
|
|
147
|
+
lastKey = '';
|
|
148
|
+
} else if (e.key === 'f' && e.ctrlKey) {
|
|
149
|
+
window.scrollBy({ top: window.innerHeight * 0.9, behavior: 'smooth' });
|
|
150
|
+
lastKey = '';
|
|
151
|
+
} else if (e.key === 'b' && e.ctrlKey) {
|
|
152
|
+
window.scrollBy({ top: -window.innerHeight * 0.9, behavior: 'smooth' });
|
|
153
|
+
lastKey = '';
|
|
136
154
|
} else if (e.key === '?') {
|
|
137
155
|
helpBox.style.display = helpBox.style.display === 'none' ? 'block' : 'none';
|
|
138
156
|
lastKey = ''; // Reset lastKey to prevent unintended 'gg'
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
import { WebSocketServer } from 'ws';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
/* Sub-modules */
|
|
9
|
+
import { createServer } from './server.mjs';
|
|
10
|
+
import { initializeMarp, renderMarp as renderMarpInternal } from './marp-utils.mjs';
|
|
11
|
+
import { parseArgs } from './args.mjs';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const argv = parseArgs();
|
|
17
|
+
|
|
18
|
+
const markdownFile = argv._[0]
|
|
19
|
+
const themeDir = argv.themeDir;
|
|
20
|
+
const port = argv.port;
|
|
21
|
+
const verbose = argv.verbose;
|
|
22
|
+
|
|
23
|
+
if (argv.version) {
|
|
24
|
+
const pkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
25
|
+
console.log(`marp-dev-preview version ${pkg.version}`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (verbose) {
|
|
30
|
+
console.debug = console.log;
|
|
31
|
+
} else {
|
|
32
|
+
console.debug = () => { };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!markdownFile) {
|
|
36
|
+
console.error('Error: You must provide a path to a markdown file.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const markdownDir = path.dirname(markdownFile);
|
|
41
|
+
|
|
42
|
+
const wss = new WebSocketServer({ port: port + 1 });
|
|
43
|
+
|
|
44
|
+
async function renderMarp() {
|
|
45
|
+
const md = await fs.readFile(markdownFile, 'utf8');
|
|
46
|
+
const { html, css } = renderMarpInternal(md);
|
|
47
|
+
const customCss = `
|
|
48
|
+
svg[data-marpit-svg] {
|
|
49
|
+
margin-bottom:20px !important;
|
|
50
|
+
border: 1px solid gray;
|
|
51
|
+
border-radius: 10px;
|
|
52
|
+
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
|
53
|
+
}
|
|
54
|
+
#help-box table td, #help-box table th {
|
|
55
|
+
padding: 0 15px 0 15px;
|
|
56
|
+
}
|
|
57
|
+
#help-box {
|
|
58
|
+
position: fixed;
|
|
59
|
+
top: 50%;
|
|
60
|
+
left: 50%;
|
|
61
|
+
transform: translate(-50%, -50%);
|
|
62
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
63
|
+
color: white;
|
|
64
|
+
padding: 20px;
|
|
65
|
+
border-radius: 10px;
|
|
66
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
67
|
+
z-index: 1001;
|
|
68
|
+
display: none; /* Initially hidden */
|
|
69
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
|
70
|
+
max-width: 400px;
|
|
71
|
+
text-align: left;
|
|
72
|
+
}
|
|
73
|
+
#command-prompt {
|
|
74
|
+
position: fixed;
|
|
75
|
+
bottom: 20px;
|
|
76
|
+
left: 50%;
|
|
77
|
+
transform: translateX(-50%);
|
|
78
|
+
background-color: #333;
|
|
79
|
+
color: white;
|
|
80
|
+
padding: 10px 20px;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
83
|
+
font-size: 1.2em;
|
|
84
|
+
z-index: 1000;
|
|
85
|
+
display: none;
|
|
86
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
return `
|
|
91
|
+
<!DOCTYPE html>
|
|
92
|
+
<html>
|
|
93
|
+
<head>
|
|
94
|
+
<meta name="ws-port" content="${port + 1}">
|
|
95
|
+
<style id="marp-style">${css}</style>
|
|
96
|
+
<style id="custom-style">${customCss}</style>
|
|
97
|
+
<script src="https://unpkg.com/morphdom@2.7.0/dist/morphdom-umd.min.js"></script>
|
|
98
|
+
<script src="/client.js"></script>
|
|
99
|
+
<meta charset="UTF-8">
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div id="marp-container">
|
|
103
|
+
${html}
|
|
104
|
+
</div>
|
|
105
|
+
<div id="help-box">
|
|
106
|
+
<h3>Key Bindings</h3>
|
|
107
|
+
<p>In addition to standard key bindings (e.g., Space, Page Down, Arrow Down, Page Up, Arrow Up), the following key bindings are available:</p>
|
|
108
|
+
<table>
|
|
109
|
+
<tr><th>Key</th><th>Action</th></tr>
|
|
110
|
+
<tr><td><kbd>gg</kbd> or <kbd>Home</kbd></td><td>Go to first slide</td></tr>
|
|
111
|
+
<tr><td><kbd>G</kbd> or <kbd>End</kbd></td><td>Go to last slide</td></tr>
|
|
112
|
+
<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>
|
|
115
|
+
<tr><td><kbd>^d</kbd></td><td>Forward half a page</td></tr>
|
|
116
|
+
<tr><td><kbd>^u</kbd></td><td>Back half a page</td></tr>
|
|
117
|
+
<tr><td><kbd>?</kbd></td><td>Show/hide help</td></tr>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
<div id="command-prompt"></div>
|
|
121
|
+
</body>
|
|
122
|
+
</html>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function reload(markdown) {
|
|
127
|
+
try {
|
|
128
|
+
const { html, css } = renderMarpInternal(markdown);
|
|
129
|
+
const message = JSON.stringify({
|
|
130
|
+
type: 'update',
|
|
131
|
+
html: html,
|
|
132
|
+
css: css
|
|
133
|
+
});
|
|
134
|
+
for (const ws of wss.clients) {
|
|
135
|
+
ws.send(message);
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Error rendering or sending update:', error);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
chokidar.watch(markdownFile).on('change', async () => {
|
|
145
|
+
console.debug(`File ${markdownFile} changed, updating...`);
|
|
146
|
+
const md = await fs.readFile(markdownFile, 'utf8');
|
|
147
|
+
await reload(md);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
initializeMarp(themeDir).then(() => {
|
|
151
|
+
createServer(port, markdownFile, markdownDir, renderMarp, reload, wss, __dirname);
|
|
152
|
+
if (themeDir) {
|
|
153
|
+
console.log(`Using custom themes from ${themeDir}`);
|
|
154
|
+
}
|
|
155
|
+
}).catch(error => {
|
|
156
|
+
console.error("Failed to initialize Marp:", error);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
import { Marp } from '@marp-team/marp-core';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import markdownItFootnote from 'markdown-it-footnote';
|
|
6
|
+
import markdownItMark from 'markdown-it-mark';
|
|
7
|
+
import markdownItContainer from 'markdown-it-container';
|
|
8
|
+
|
|
9
|
+
let marp;
|
|
10
|
+
|
|
11
|
+
export async function initializeMarp(themeDir) {
|
|
12
|
+
const options = { html: true, linkify: true, };
|
|
13
|
+
marp = new Marp(options)
|
|
14
|
+
.use(markdownItFootnote)
|
|
15
|
+
.use(markdownItMark)
|
|
16
|
+
.use(markdownItContainer, 'note');
|
|
17
|
+
|
|
18
|
+
if (themeDir) {
|
|
19
|
+
const themeFiles = await fs.readdir(themeDir);
|
|
20
|
+
for (const file of themeFiles) {
|
|
21
|
+
if (path.extname(file) === '.css') {
|
|
22
|
+
const css = await fs.readFile(path.join(themeDir, file), 'utf8');
|
|
23
|
+
marp.themeSet.add(css);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return marp;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderMarp(markdown) {
|
|
31
|
+
const { html, css } = marp.render(markdown);
|
|
32
|
+
return { html, css };
|
|
33
|
+
}
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
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
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createServer(port, markdownFile, markdownDir, renderMarp, reload, wss, __dirname) {
|
|
25
|
+
const server = http.createServer(async (req, res) => {
|
|
26
|
+
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';
|
|
73
|
+
|
|
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
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(error);
|
|
89
|
+
res.writeHead(500);
|
|
90
|
+
res.end('Internal Server Error');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
server.listen(port, () => {
|
|
95
|
+
console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return server;
|
|
99
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { parseArgs } from '../src/args.mjs';
|
|
2
|
+
|
|
3
|
+
const mockArgv = {};
|
|
4
|
+
const defaultValues = {};
|
|
5
|
+
|
|
6
|
+
jest.mock('yargs', () => {
|
|
7
|
+
const yargsMock = jest.fn(() => yargsMock);
|
|
8
|
+
yargsMock.usage = jest.fn().mockReturnThis();
|
|
9
|
+
yargsMock.positional = jest.fn().mockReturnThis();
|
|
10
|
+
yargsMock.option = jest.fn().mockImplementation((name, options) => {
|
|
11
|
+
if (options.default !== undefined) {
|
|
12
|
+
defaultValues[name] = options.default;
|
|
13
|
+
}
|
|
14
|
+
return yargsMock;
|
|
15
|
+
});
|
|
16
|
+
yargsMock.config = jest.fn().mockReturnThis();
|
|
17
|
+
yargsMock.default = jest.fn().mockImplementation((name, value) => {
|
|
18
|
+
defaultValues[name] = value;
|
|
19
|
+
return yargsMock;
|
|
20
|
+
});
|
|
21
|
+
yargsMock.demandCommand = jest.fn().mockReturnThis();
|
|
22
|
+
Object.defineProperty(yargsMock, 'argv', {
|
|
23
|
+
get: () => ({
|
|
24
|
+
...defaultValues,
|
|
25
|
+
...mockArgv,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
return yargsMock;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
jest.mock('yargs/helpers', () => ({
|
|
32
|
+
hideBin: jest.fn(() => ['node', 'marp-dev-preview.mjs']),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
describe('Args', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// Reset mockArgv before each test
|
|
38
|
+
for (const key in mockArgv) {
|
|
39
|
+
delete mockArgv[key];
|
|
40
|
+
}
|
|
41
|
+
// Reset hideBin mock
|
|
42
|
+
require('yargs/helpers').hideBin.mockReturnValue(['node', 'marp-dev-preview.mjs']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return default port if not specified', () => {
|
|
46
|
+
mockArgv._ = ['test.md'];
|
|
47
|
+
const argv = parseArgs();
|
|
48
|
+
expect(argv.port).toBe(8080);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return specified port', () => {
|
|
52
|
+
mockArgv._ = ['test.md'];
|
|
53
|
+
mockArgv.port = 3000;
|
|
54
|
+
const argv = parseArgs();
|
|
55
|
+
expect(argv.port).toBe(3000);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return default verbose as false', () => {
|
|
59
|
+
mockArgv._ = ['test.md'];
|
|
60
|
+
const argv = parseArgs();
|
|
61
|
+
expect(argv.verbose).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return verbose as true if specified', () => {
|
|
65
|
+
mockArgv._ = ['test.md'];
|
|
66
|
+
mockArgv.verbose = true;
|
|
67
|
+
const argv = parseArgs();
|
|
68
|
+
expect(argv.verbose).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should capture markdown file positional argument', () => {
|
|
72
|
+
mockArgv._ = ['my-presentation.md'];
|
|
73
|
+
const argv = parseArgs();
|
|
74
|
+
expect(argv._[0]).toBe('my-presentation.md');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return theme-dir if specified', () => {
|
|
78
|
+
mockArgv._ = ['test.md'];
|
|
79
|
+
mockArgv.themeDir = '/path/to/themes';
|
|
80
|
+
const argv = parseArgs();
|
|
81
|
+
expect(argv.themeDir).toBe('/path/to/themes');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle config file path', () => {
|
|
85
|
+
mockArgv._ = ['test.md'];
|
|
86
|
+
mockArgv.config = './my-config.json';
|
|
87
|
+
const argv = parseArgs();
|
|
88
|
+
expect(argv.config).toBe('./my-config.json');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
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
|
+
|
|
10
|
+
describe('Server', () => {
|
|
11
|
+
it('should create a server', () => {
|
|
12
|
+
const port = 8080;
|
|
13
|
+
const markdownFile = 'test.md';
|
|
14
|
+
const markdownDir = '.';
|
|
15
|
+
const renderMarp = jest.fn();
|
|
16
|
+
const reload = jest.fn();
|
|
17
|
+
const wss = { clients: [] };
|
|
18
|
+
const __dirname = '.';
|
|
19
|
+
|
|
20
|
+
createServer(port, markdownFile, markdownDir, renderMarp, reload, wss, __dirname);
|
|
21
|
+
|
|
22
|
+
expect(http.createServer).toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { initializeMarp, renderMarp } from '../src/marp-utils.mjs';
|
|
2
|
+
import { Marp } from '@marp-team/marp-core';
|
|
3
|
+
|
|
4
|
+
jest.mock('@marp-team/marp-core', () => ({
|
|
5
|
+
Marp: jest.fn(() => ({
|
|
6
|
+
use: jest.fn().mockReturnThis(),
|
|
7
|
+
render: jest.fn(() => ({ html: '', css: '' })),
|
|
8
|
+
themeSet: {
|
|
9
|
+
add: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
})),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('Marp Utils', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should initialize Marp', async () => {
|
|
20
|
+
await initializeMarp();
|
|
21
|
+
expect(Marp).toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should render markdown', async () => {
|
|
25
|
+
const marp = await initializeMarp();
|
|
26
|
+
renderMarp('# Hello');
|
|
27
|
+
expect(marp.render).toHaveBeenCalledWith('# Hello');
|
|
28
|
+
});
|
|
29
|
+
});
|
package/marp-dev-preview.mjs
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
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
|
-
import markdownItFootnote from 'markdown-it-footnote';
|
|
11
|
-
import markdownItMark from 'markdown-it-mark';
|
|
12
|
-
import markdownItContainer from 'markdown-it-container';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
-
const __dirname = path.dirname(__filename);
|
|
18
|
-
|
|
19
|
-
const argv = yargs(hideBin(process.argv))
|
|
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
|
-
.option('verbose', {
|
|
37
|
-
alias: 'v',
|
|
38
|
-
describe: 'Enable verbose logging',
|
|
39
|
-
type: 'boolean',
|
|
40
|
-
default: false
|
|
41
|
-
})
|
|
42
|
-
.config('config', 'Path to a JSON config file')
|
|
43
|
-
.default('config', '.mp-config.json')
|
|
44
|
-
.demandCommand(1, 'You must provide a markdown file.')
|
|
45
|
-
.argv;
|
|
46
|
-
|
|
47
|
-
const markdownFile = argv._[0]
|
|
48
|
-
const themeDir = argv.themeDir;
|
|
49
|
-
const port = argv.port;
|
|
50
|
-
const verbose = argv.verbose;
|
|
51
|
-
|
|
52
|
-
async function findPackageJson(startDir) {
|
|
53
|
-
let dir = startDir;
|
|
54
|
-
while (dir !== path.parse(dir).root) {
|
|
55
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
56
|
-
try {
|
|
57
|
-
await fs.access(pkgPath);
|
|
58
|
-
return pkgPath;
|
|
59
|
-
} catch {
|
|
60
|
-
dir = path.dirname(dir);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Version reporting block (replace your current version block)
|
|
67
|
-
if (argv.version || argv.v) {
|
|
68
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
69
|
-
const __dirname = dirname(__filename);
|
|
70
|
-
const pkgPath = await findPackageJson(__dirname);
|
|
71
|
-
if (pkgPath) {
|
|
72
|
-
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
73
|
-
console.log(`marp-dev-preview version ${pkg.version}`);
|
|
74
|
-
} else {
|
|
75
|
-
console.error('Could not find package.json for version info.');
|
|
76
|
-
}
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (verbose) {
|
|
81
|
-
console.debug = console.log;
|
|
82
|
-
} else {
|
|
83
|
-
console.debug = () => { };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!markdownFile) {
|
|
87
|
-
console.error('Error: You must provide a path to a markdown file.');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const markdownDir = path.dirname(markdownFile);
|
|
92
|
-
|
|
93
|
-
const mimeTypes = {
|
|
94
|
-
'.html': 'text/html',
|
|
95
|
-
'.js': 'text/javascript',
|
|
96
|
-
'.css': 'text/css',
|
|
97
|
-
'.json': 'application/json',
|
|
98
|
-
'.png': 'image/png',
|
|
99
|
-
'.jpg': 'image/jpeg',
|
|
100
|
-
'.gif': 'image/gif',
|
|
101
|
-
'.svg': 'image/svg+xml',
|
|
102
|
-
'.wav': 'audio/wav',
|
|
103
|
-
'.mp4': 'video/mp4',
|
|
104
|
-
'.woff': 'application/font-woff',
|
|
105
|
-
'.ttf': 'application/font-ttf',
|
|
106
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
107
|
-
'.otf': 'application/font-otf',
|
|
108
|
-
'.wasm': 'application/wasm'
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const wss = new WebSocketServer({ port: port + 1 });
|
|
112
|
-
|
|
113
|
-
let marp;
|
|
114
|
-
|
|
115
|
-
async function initializeMarp() {
|
|
116
|
-
const options = { html: true, linkify: true, };
|
|
117
|
-
marp = new Marp(options)
|
|
118
|
-
.use(markdownItFootnote)
|
|
119
|
-
.use(markdownItMark)
|
|
120
|
-
.use(markdownItContainer, 'note');
|
|
121
|
-
|
|
122
|
-
if (themeDir) {
|
|
123
|
-
const themeFiles = await fs.readdir(themeDir);
|
|
124
|
-
for (const file of themeFiles) {
|
|
125
|
-
if (path.extname(file) === '.css') {
|
|
126
|
-
const css = await fs.readFile(path.join(themeDir, file), 'utf8');
|
|
127
|
-
marp.themeSet.add(css);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
async function renderMarp() {
|
|
135
|
-
const md = await fs.readFile(markdownFile, 'utf8');
|
|
136
|
-
const { html, css } = marp.render(md);
|
|
137
|
-
const customCss = `
|
|
138
|
-
svg[data-marpit-svg] {
|
|
139
|
-
margin-bottom:20px !important;
|
|
140
|
-
border: 1px solid gray;
|
|
141
|
-
border-radius: 10px;
|
|
142
|
-
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
|
143
|
-
}
|
|
144
|
-
#help-box table td, #help-box table th {
|
|
145
|
-
padding: 0 15px 0 15px;
|
|
146
|
-
}
|
|
147
|
-
#help-box {
|
|
148
|
-
position: fixed;
|
|
149
|
-
top: 50%;
|
|
150
|
-
left: 50%;
|
|
151
|
-
transform: translate(-50%, -50%);
|
|
152
|
-
background-color: rgba(0, 0, 0, 0.8);
|
|
153
|
-
color: white;
|
|
154
|
-
padding: 20px;
|
|
155
|
-
border-radius: 10px;
|
|
156
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
157
|
-
z-index: 1001;
|
|
158
|
-
display: none; /* Initially hidden */
|
|
159
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
|
160
|
-
max-width: 400px;
|
|
161
|
-
text-align: left;
|
|
162
|
-
}
|
|
163
|
-
#command-prompt {
|
|
164
|
-
position: fixed;
|
|
165
|
-
bottom: 20px;
|
|
166
|
-
left: 50%;
|
|
167
|
-
transform: translateX(-50%);
|
|
168
|
-
background-color: #333;
|
|
169
|
-
color: white;
|
|
170
|
-
padding: 10px 20px;
|
|
171
|
-
border-radius: 8px;
|
|
172
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
173
|
-
font-size: 1.2em;
|
|
174
|
-
z-index: 1000;
|
|
175
|
-
display: none;
|
|
176
|
-
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
177
|
-
}
|
|
178
|
-
`;
|
|
179
|
-
|
|
180
|
-
return `
|
|
181
|
-
<!DOCTYPE html>
|
|
182
|
-
<html>
|
|
183
|
-
<head>
|
|
184
|
-
<meta name="ws-port" content="${port + 1}">
|
|
185
|
-
<style id="marp-style">${css}</style>
|
|
186
|
-
<style id="custom-style">${customCss}</style>
|
|
187
|
-
<script src="https://unpkg.com/morphdom@2.7.0/dist/morphdom-umd.min.js"></script>
|
|
188
|
-
<script src="/client.js"></script>
|
|
189
|
-
<meta charset="UTF-8">
|
|
190
|
-
</head>
|
|
191
|
-
<body>
|
|
192
|
-
<div id="marp-container">
|
|
193
|
-
${html}
|
|
194
|
-
</div>
|
|
195
|
-
<div id="help-box">
|
|
196
|
-
<h3>Key Bindings</h3>
|
|
197
|
-
<table>
|
|
198
|
-
<tr><th>Key</th><th>Action</th></tr>
|
|
199
|
-
<tr><td><kbd>gg</kbd> or <kbd>Home</kbd></td><td>Go to first slide</td></tr>
|
|
200
|
-
<tr><td><kbd>G</kbd> or <kbd>End</kbd></td><td>Go to last slide</td></tr>
|
|
201
|
-
<tr><td><kbd>:<number></kbd></td><td>Go to the given slide number</td></tr>
|
|
202
|
-
<tr><td><kbd>?</kbd></td><td>Show/hide help</td></tr>
|
|
203
|
-
</table>
|
|
204
|
-
</div>
|
|
205
|
-
<div id="command-prompt"></div>
|
|
206
|
-
</body>
|
|
207
|
-
</html>
|
|
208
|
-
`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const server = http.createServer(async (req, res) => {
|
|
212
|
-
try {
|
|
213
|
-
if (req.url === '/') {
|
|
214
|
-
const html = await renderMarp();
|
|
215
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
216
|
-
res.end(html);
|
|
217
|
-
} else if (req.url === '/client.js') {
|
|
218
|
-
const clientJs = await fs.readFile(path.join(__dirname, 'client.js'), 'utf8');
|
|
219
|
-
res.writeHead(200, { 'Content-Type': 'text/javascript' });
|
|
220
|
-
res.end(clientJs);
|
|
221
|
-
} else if (req.url === '/api/reload' && req.method === 'POST') {
|
|
222
|
-
let body = '';
|
|
223
|
-
req.on('data', chunk => {
|
|
224
|
-
body += chunk.toString();
|
|
225
|
-
});
|
|
226
|
-
req.on('end', async () => {
|
|
227
|
-
console.debug("Reload request received");
|
|
228
|
-
const success = await reload(body);
|
|
229
|
-
if (success) {
|
|
230
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
231
|
-
res.end(JSON.stringify({ status: 'ok' }));
|
|
232
|
-
} else {
|
|
233
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
234
|
-
res.end(JSON.stringify({ status: 'error', message: 'Failed to render markdown' }));
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
} else if (req.url === '/api/command' && req.method === 'POST') {
|
|
238
|
-
let body = '';
|
|
239
|
-
req.on('data', chunk => {
|
|
240
|
-
body += chunk.toString();
|
|
241
|
-
});
|
|
242
|
-
req.on('end', () => {
|
|
243
|
-
try {
|
|
244
|
-
const command = JSON.parse(body);
|
|
245
|
-
for (const ws of wss.clients) {
|
|
246
|
-
ws.send(JSON.stringify(command));
|
|
247
|
-
}
|
|
248
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
249
|
-
res.end(JSON.stringify({ status: 'ok', command }));
|
|
250
|
-
} catch (e) {
|
|
251
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
252
|
-
res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON' }));
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
} else {
|
|
256
|
-
const assetPath = path.join(markdownDir, req.url);
|
|
257
|
-
const ext = path.extname(assetPath);
|
|
258
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const content = await fs.readFile(assetPath);
|
|
262
|
-
res.writeHead(200, { 'Content-Type': contentType });
|
|
263
|
-
res.end(content);
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (error.code === 'ENOENT') {
|
|
266
|
-
res.writeHead(404);
|
|
267
|
-
res.end('Not Found');
|
|
268
|
-
} else {
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
console.error(error);
|
|
275
|
-
res.writeHead(500);
|
|
276
|
-
res.end('Internal Server Error');
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
async function reload(markdown) {
|
|
281
|
-
try {
|
|
282
|
-
const { html, css } = marp.render(markdown);
|
|
283
|
-
const message = JSON.stringify({
|
|
284
|
-
type: 'update',
|
|
285
|
-
html: html,
|
|
286
|
-
css: css
|
|
287
|
-
});
|
|
288
|
-
for (const ws of wss.clients) {
|
|
289
|
-
ws.send(message);
|
|
290
|
-
}
|
|
291
|
-
return true;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
console.error('Error rendering or sending update:', error);
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
chokidar.watch(markdownFile).on('change', async () => {
|
|
299
|
-
console.log(`File ${markdownFile} changed, updating...`);
|
|
300
|
-
const md = await fs.readFile(markdownFile, 'utf8');
|
|
301
|
-
await reload(md);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
initializeMarp().then(() => {
|
|
305
|
-
server.listen(port, () => {
|
|
306
|
-
console.log(`Server listening on http://localhost:${port} for ${markdownFile}`);
|
|
307
|
-
if (themeDir) {
|
|
308
|
-
console.log(`Using custom themes from ${themeDir}`);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}).catch(error => {
|
|
312
|
-
console.error("Failed to initialize Marp:", error);
|
|
313
|
-
process.exit(1);
|
|
314
|
-
});
|