prettymdviewer 1.0.2
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/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/cli.js +81 -0
- package/index.js +4 -0
- package/lib/renderer.js +526 -0
- package/lib/server.js +49 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yogesh swami
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# mdpretty
|
|
2
|
+
|
|
3
|
+
Beautiful markdown viewer — render `.md` files in a styled browser UI with TOC, dark/light themes, and scroll spy.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g mdpretty
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## CLI Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Open in browser (default port 3333)
|
|
15
|
+
mdpretty README.md
|
|
16
|
+
|
|
17
|
+
# Output rendered HTML to stdout
|
|
18
|
+
mdpretty README.md --html
|
|
19
|
+
|
|
20
|
+
# Export to an HTML file
|
|
21
|
+
mdpretty README.md --out=output.html
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Programmatic Usage
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const { renderHTML, serve } = require("mdpretty");
|
|
28
|
+
|
|
29
|
+
// Get a self-contained HTML string
|
|
30
|
+
const html = renderHTML("# Hello\n\nWorld", "example.md");
|
|
31
|
+
|
|
32
|
+
// Start a live-reload server
|
|
33
|
+
serve("./README.md", { port: 3333 });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- Sidebar table of contents generated from headings
|
|
39
|
+
- Dark / light theme toggle
|
|
40
|
+
- Scroll spy highlights current section
|
|
41
|
+
- Single self-contained HTML output (no external assets)
|
|
42
|
+
- Live reload on browser refresh in server mode
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const flags = {};
|
|
8
|
+
const positional = [];
|
|
9
|
+
|
|
10
|
+
for (const arg of args) {
|
|
11
|
+
if (arg === "--help" || arg === "-h") {
|
|
12
|
+
flags.help = true;
|
|
13
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
14
|
+
flags.version = true;
|
|
15
|
+
} else if (arg === "--no-browser") {
|
|
16
|
+
flags.noBrowser = true;
|
|
17
|
+
} else if (arg.startsWith("--port=")) {
|
|
18
|
+
flags.port = parseInt(arg.split("=")[1], 10);
|
|
19
|
+
} else if (arg === "--html") {
|
|
20
|
+
flags.html = true;
|
|
21
|
+
} else if (arg.startsWith("--out=")) {
|
|
22
|
+
flags.out = arg.split("=")[1];
|
|
23
|
+
} else {
|
|
24
|
+
positional.push(arg);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (flags.version) {
|
|
29
|
+
const pkg = require("../package.json");
|
|
30
|
+
console.log(pkg.version);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (flags.help || positional.length === 0) {
|
|
35
|
+
console.log(`
|
|
36
|
+
mdpretty - Beautiful markdown viewer
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
mdpretty <file.md> Open in browser
|
|
40
|
+
mdpretty <file.md> --html Output rendered HTML to stdout
|
|
41
|
+
mdpretty <file.md> --out=f.html Export to HTML file
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--port=<n> Server port (default: 3333)
|
|
45
|
+
--no-browser Start server without opening browser
|
|
46
|
+
--html Print rendered HTML to stdout (no server)
|
|
47
|
+
--out=<file> Write rendered HTML to file (no server)
|
|
48
|
+
-v, --version Show version
|
|
49
|
+
-h, --help Show this help
|
|
50
|
+
`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const filePath = path.resolve(positional[0]);
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(filePath)) {
|
|
57
|
+
console.error(`File not found: ${filePath}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- HTML output mode (no server) ---
|
|
62
|
+
if (flags.html || flags.out) {
|
|
63
|
+
const { renderHTML } = require("../lib/renderer");
|
|
64
|
+
const md = fs.readFileSync(filePath, "utf-8");
|
|
65
|
+
const html = renderHTML(md, path.basename(filePath));
|
|
66
|
+
|
|
67
|
+
if (flags.out) {
|
|
68
|
+
fs.writeFileSync(flags.out, html, "utf-8");
|
|
69
|
+
console.log(`Written to ${flags.out}`);
|
|
70
|
+
} else {
|
|
71
|
+
process.stdout.write(html);
|
|
72
|
+
}
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Server mode (default) ---
|
|
77
|
+
const { serve } = require("../lib/server");
|
|
78
|
+
serve(filePath, {
|
|
79
|
+
port: flags.port,
|
|
80
|
+
noBrowser: flags.noBrowser,
|
|
81
|
+
});
|
package/index.js
ADDED
package/lib/renderer.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
const { marked } = require("marked");
|
|
2
|
+
|
|
3
|
+
marked.setOptions({
|
|
4
|
+
gfm: true,
|
|
5
|
+
breaks: true,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
function renderHTML(mdContent, fileName) {
|
|
9
|
+
const rendered = marked.parse(mdContent);
|
|
10
|
+
|
|
11
|
+
return `<!DOCTYPE html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
+
<title>${fileName}</title>
|
|
17
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
18
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
19
|
+
<style>
|
|
20
|
+
:root {
|
|
21
|
+
--bg: #0d1117;
|
|
22
|
+
--surface: #161b22;
|
|
23
|
+
--surface-2: #1c2129;
|
|
24
|
+
--border: #30363d;
|
|
25
|
+
--border-light: #3d444d;
|
|
26
|
+
--text: #e6edf3;
|
|
27
|
+
--text-muted: #8b949e;
|
|
28
|
+
--text-subtle: #6e7681;
|
|
29
|
+
--accent: #58a6ff;
|
|
30
|
+
--accent-soft: #1f6feb33;
|
|
31
|
+
--green: #3fb950;
|
|
32
|
+
--green-soft: #23863633;
|
|
33
|
+
--purple: #bc8cff;
|
|
34
|
+
--orange: #f0883e;
|
|
35
|
+
--red: #f85149;
|
|
36
|
+
--yellow: #d29922;
|
|
37
|
+
--code-bg: #0d1117;
|
|
38
|
+
--inline-code-bg: #262c36;
|
|
39
|
+
--scrollbar: #30363d;
|
|
40
|
+
--scrollbar-hover: #484f58;
|
|
41
|
+
--gradient-1: #58a6ff;
|
|
42
|
+
--gradient-2: #bc8cff;
|
|
43
|
+
--gradient-3: #3fb950;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* {
|
|
47
|
+
margin: 0;
|
|
48
|
+
padding: 0;
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
html {
|
|
53
|
+
scroll-behavior: smooth;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
body {
|
|
57
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
58
|
+
background: var(--bg);
|
|
59
|
+
color: var(--text);
|
|
60
|
+
line-height: 1.7;
|
|
61
|
+
-webkit-font-smoothing: antialiased;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
65
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
66
|
+
::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 4px; }
|
|
67
|
+
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
|
|
68
|
+
|
|
69
|
+
/* --- Top bar --- */
|
|
70
|
+
.topbar {
|
|
71
|
+
position: fixed;
|
|
72
|
+
top: 0;
|
|
73
|
+
left: 0;
|
|
74
|
+
right: 0;
|
|
75
|
+
height: 48px;
|
|
76
|
+
background: var(--surface);
|
|
77
|
+
border-bottom: 1px solid var(--border);
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
padding: 0 24px;
|
|
81
|
+
z-index: 100;
|
|
82
|
+
backdrop-filter: blur(12px);
|
|
83
|
+
background: rgba(22, 27, 34, 0.85);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.topbar-dot {
|
|
87
|
+
width: 12px;
|
|
88
|
+
height: 12px;
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
margin-right: 8px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.topbar-dot.red { background: #f85149; }
|
|
94
|
+
.topbar-dot.yellow { background: #d29922; }
|
|
95
|
+
.topbar-dot.green { background: #3fb950; }
|
|
96
|
+
|
|
97
|
+
.topbar-title {
|
|
98
|
+
margin-left: 16px;
|
|
99
|
+
font-size: 13px;
|
|
100
|
+
color: var(--text-muted);
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
letter-spacing: 0.3px;
|
|
103
|
+
font-family: 'JetBrains Mono', monospace;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.topbar-actions {
|
|
107
|
+
margin-left: auto;
|
|
108
|
+
display: flex;
|
|
109
|
+
gap: 8px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.topbar-btn {
|
|
113
|
+
background: var(--surface-2);
|
|
114
|
+
border: 1px solid var(--border);
|
|
115
|
+
color: var(--text-muted);
|
|
116
|
+
padding: 4px 12px;
|
|
117
|
+
border-radius: 6px;
|
|
118
|
+
font-size: 12px;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
font-family: 'Inter', sans-serif;
|
|
121
|
+
transition: all 0.15s ease;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.topbar-btn:hover {
|
|
125
|
+
background: var(--border);
|
|
126
|
+
color: var(--text);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* --- Sidebar TOC --- */
|
|
130
|
+
.layout {
|
|
131
|
+
display: flex;
|
|
132
|
+
padding-top: 48px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.sidebar {
|
|
136
|
+
position: fixed;
|
|
137
|
+
top: 48px;
|
|
138
|
+
left: 0;
|
|
139
|
+
width: 280px;
|
|
140
|
+
height: calc(100vh - 48px);
|
|
141
|
+
background: var(--surface);
|
|
142
|
+
border-right: 1px solid var(--border);
|
|
143
|
+
overflow-y: auto;
|
|
144
|
+
padding: 20px 0;
|
|
145
|
+
transition: transform 0.25s ease;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.sidebar-header {
|
|
149
|
+
padding: 0 20px 16px;
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
text-transform: uppercase;
|
|
153
|
+
letter-spacing: 1.2px;
|
|
154
|
+
color: var(--text-subtle);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.toc-item {
|
|
158
|
+
display: block;
|
|
159
|
+
padding: 6px 20px;
|
|
160
|
+
color: var(--text-muted);
|
|
161
|
+
text-decoration: none;
|
|
162
|
+
font-size: 13px;
|
|
163
|
+
transition: all 0.15s ease;
|
|
164
|
+
border-left: 2px solid transparent;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.toc-item:hover {
|
|
168
|
+
color: var(--text);
|
|
169
|
+
background: var(--surface-2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.toc-item.active {
|
|
173
|
+
color: var(--accent);
|
|
174
|
+
border-left-color: var(--accent);
|
|
175
|
+
background: var(--accent-soft);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.toc-item.depth-2 { padding-left: 20px; }
|
|
179
|
+
.toc-item.depth-3 { padding-left: 36px; font-size: 12px; }
|
|
180
|
+
|
|
181
|
+
/* --- Main content --- */
|
|
182
|
+
.content {
|
|
183
|
+
margin-left: 280px;
|
|
184
|
+
max-width: 860px;
|
|
185
|
+
padding: 48px 56px 120px;
|
|
186
|
+
width: 100%;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* --- Typography --- */
|
|
190
|
+
.markdown-body h1 {
|
|
191
|
+
font-size: 2em;
|
|
192
|
+
font-weight: 700;
|
|
193
|
+
margin: 48px 0 16px;
|
|
194
|
+
padding-bottom: 12px;
|
|
195
|
+
border-bottom: 1px solid var(--border);
|
|
196
|
+
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
197
|
+
-webkit-background-clip: text;
|
|
198
|
+
-webkit-text-fill-color: transparent;
|
|
199
|
+
background-clip: text;
|
|
200
|
+
letter-spacing: -0.5px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.markdown-body h1:first-child {
|
|
204
|
+
margin-top: 0;
|
|
205
|
+
font-size: 2.4em;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.markdown-body h2 {
|
|
209
|
+
font-size: 1.5em;
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
margin: 40px 0 16px;
|
|
212
|
+
padding-bottom: 8px;
|
|
213
|
+
border-bottom: 1px solid var(--border);
|
|
214
|
+
color: var(--text);
|
|
215
|
+
letter-spacing: -0.3px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.markdown-body h3 {
|
|
219
|
+
font-size: 1.2em;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
margin: 32px 0 12px;
|
|
222
|
+
color: var(--accent);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.markdown-body h4 {
|
|
226
|
+
font-size: 1em;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
margin: 24px 0 8px;
|
|
229
|
+
color: var(--purple);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.markdown-body p {
|
|
233
|
+
margin: 0 0 16px;
|
|
234
|
+
color: var(--text-muted);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.markdown-body strong {
|
|
238
|
+
color: var(--text);
|
|
239
|
+
font-weight: 600;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.markdown-body a {
|
|
243
|
+
color: var(--accent);
|
|
244
|
+
text-decoration: none;
|
|
245
|
+
border-bottom: 1px solid transparent;
|
|
246
|
+
transition: border-color 0.15s ease;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.markdown-body a:hover {
|
|
250
|
+
border-bottom-color: var(--accent);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* --- Lists --- */
|
|
254
|
+
.markdown-body ul,
|
|
255
|
+
.markdown-body ol {
|
|
256
|
+
padding-left: 24px;
|
|
257
|
+
margin: 0 0 16px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.markdown-body li {
|
|
261
|
+
margin: 4px 0;
|
|
262
|
+
color: var(--text-muted);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.markdown-body li::marker {
|
|
266
|
+
color: var(--border-light);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* --- Blockquotes --- */
|
|
270
|
+
.markdown-body blockquote {
|
|
271
|
+
margin: 0 0 16px;
|
|
272
|
+
padding: 12px 20px;
|
|
273
|
+
border-left: 3px solid var(--accent);
|
|
274
|
+
background: var(--accent-soft);
|
|
275
|
+
border-radius: 0 8px 8px 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.markdown-body blockquote p {
|
|
279
|
+
margin: 0;
|
|
280
|
+
color: var(--text);
|
|
281
|
+
font-size: 14px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* --- Code blocks --- */
|
|
285
|
+
.markdown-body pre {
|
|
286
|
+
background: var(--code-bg);
|
|
287
|
+
border: 1px solid var(--border);
|
|
288
|
+
border-radius: 8px;
|
|
289
|
+
padding: 16px 20px;
|
|
290
|
+
margin: 0 0 16px;
|
|
291
|
+
overflow-x: auto;
|
|
292
|
+
position: relative;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.markdown-body pre code {
|
|
296
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
297
|
+
font-size: 13px;
|
|
298
|
+
line-height: 1.6;
|
|
299
|
+
color: var(--text);
|
|
300
|
+
background: none;
|
|
301
|
+
padding: 0;
|
|
302
|
+
border: none;
|
|
303
|
+
border-radius: 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.markdown-body code {
|
|
307
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
308
|
+
font-size: 0.875em;
|
|
309
|
+
background: var(--inline-code-bg);
|
|
310
|
+
color: var(--orange);
|
|
311
|
+
padding: 2px 6px;
|
|
312
|
+
border-radius: 4px;
|
|
313
|
+
border: 1px solid var(--border);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* --- Tables --- */
|
|
317
|
+
.markdown-body table {
|
|
318
|
+
width: 100%;
|
|
319
|
+
border-collapse: collapse;
|
|
320
|
+
margin: 0 0 16px;
|
|
321
|
+
font-size: 14px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.markdown-body thead th {
|
|
325
|
+
background: var(--surface);
|
|
326
|
+
color: var(--text);
|
|
327
|
+
font-weight: 600;
|
|
328
|
+
text-align: left;
|
|
329
|
+
padding: 10px 16px;
|
|
330
|
+
border: 1px solid var(--border);
|
|
331
|
+
font-size: 13px;
|
|
332
|
+
text-transform: uppercase;
|
|
333
|
+
letter-spacing: 0.5px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.markdown-body tbody td {
|
|
337
|
+
padding: 10px 16px;
|
|
338
|
+
border: 1px solid var(--border);
|
|
339
|
+
color: var(--text-muted);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.markdown-body tbody tr:nth-child(even) {
|
|
343
|
+
background: var(--surface);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.markdown-body tbody tr:hover {
|
|
347
|
+
background: var(--surface-2);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* --- Horizontal rules --- */
|
|
351
|
+
.markdown-body hr {
|
|
352
|
+
border: none;
|
|
353
|
+
height: 1px;
|
|
354
|
+
background: var(--border);
|
|
355
|
+
margin: 32px 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* --- Images --- */
|
|
359
|
+
.markdown-body img {
|
|
360
|
+
max-width: 100%;
|
|
361
|
+
border-radius: 8px;
|
|
362
|
+
border: 1px solid var(--border);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* --- Scroll spy active heading --- */
|
|
366
|
+
.heading-anchor {
|
|
367
|
+
scroll-margin-top: 72px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* --- Responsive --- */
|
|
371
|
+
@media (max-width: 1024px) {
|
|
372
|
+
.sidebar { transform: translateX(-100%); }
|
|
373
|
+
.sidebar.open { transform: translateX(0); }
|
|
374
|
+
.content { margin-left: 0; padding: 32px 24px 80px; }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* --- Print --- */
|
|
378
|
+
@media print {
|
|
379
|
+
.topbar, .sidebar { display: none; }
|
|
380
|
+
.content { margin-left: 0; max-width: 100%; }
|
|
381
|
+
body { background: white; color: #1a1a1a; }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* --- Syntax-like coloring for code (basic) --- */
|
|
385
|
+
.markdown-body pre code .keyword { color: var(--purple); }
|
|
386
|
+
.markdown-body pre code .string { color: var(--green); }
|
|
387
|
+
.markdown-body pre code .comment { color: var(--text-subtle); }
|
|
388
|
+
|
|
389
|
+
/* --- Fade-in animation --- */
|
|
390
|
+
.markdown-body > * {
|
|
391
|
+
animation: fadeUp 0.4s ease both;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@keyframes fadeUp {
|
|
395
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
396
|
+
to { opacity: 1; transform: translateY(0); }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.markdown-body > *:nth-child(1) { animation-delay: 0.02s; }
|
|
400
|
+
.markdown-body > *:nth-child(2) { animation-delay: 0.04s; }
|
|
401
|
+
.markdown-body > *:nth-child(3) { animation-delay: 0.06s; }
|
|
402
|
+
.markdown-body > *:nth-child(4) { animation-delay: 0.08s; }
|
|
403
|
+
.markdown-body > *:nth-child(5) { animation-delay: 0.10s; }
|
|
404
|
+
.markdown-body > *:nth-child(6) { animation-delay: 0.12s; }
|
|
405
|
+
.markdown-body > *:nth-child(7) { animation-delay: 0.14s; }
|
|
406
|
+
.markdown-body > *:nth-child(8) { animation-delay: 0.16s; }
|
|
407
|
+
.markdown-body > *:nth-child(9) { animation-delay: 0.18s; }
|
|
408
|
+
.markdown-body > *:nth-child(10) { animation-delay: 0.20s; }
|
|
409
|
+
</style>
|
|
410
|
+
</head>
|
|
411
|
+
<body>
|
|
412
|
+
|
|
413
|
+
<!-- Top Bar -->
|
|
414
|
+
<div class="topbar">
|
|
415
|
+
<span class="topbar-dot red"></span>
|
|
416
|
+
<span class="topbar-dot yellow"></span>
|
|
417
|
+
<span class="topbar-dot green"></span>
|
|
418
|
+
<span class="topbar-title">${fileName}</span>
|
|
419
|
+
<div class="topbar-actions">
|
|
420
|
+
<button class="topbar-btn" onclick="toggleSidebar()">TOC</button>
|
|
421
|
+
<button class="topbar-btn" onclick="toggleTheme()">Light</button>
|
|
422
|
+
<button class="topbar-btn" onclick="scrollToTop()">Top</button>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="layout">
|
|
427
|
+
<!-- Sidebar (generated by JS) -->
|
|
428
|
+
<nav class="sidebar" id="sidebar"></nav>
|
|
429
|
+
|
|
430
|
+
<!-- Content -->
|
|
431
|
+
<main class="content">
|
|
432
|
+
<article class="markdown-body" id="content">
|
|
433
|
+
${rendered}
|
|
434
|
+
</article>
|
|
435
|
+
</main>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<script>
|
|
439
|
+
(function() {
|
|
440
|
+
// --- Build TOC from headings ---
|
|
441
|
+
const content = document.getElementById('content');
|
|
442
|
+
const sidebar = document.getElementById('sidebar');
|
|
443
|
+
const headings = content.querySelectorAll('h1, h2, h3');
|
|
444
|
+
let tocHTML = '<div class="sidebar-header">On this page</div>';
|
|
445
|
+
|
|
446
|
+
headings.forEach((h, i) => {
|
|
447
|
+
const id = 'heading-' + i;
|
|
448
|
+
h.id = id;
|
|
449
|
+
h.classList.add('heading-anchor');
|
|
450
|
+
const depth = parseInt(h.tagName[1]);
|
|
451
|
+
tocHTML += '<a class="toc-item depth-' + depth + '" href="#' + id + '">' + h.textContent + '</a>';
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
sidebar.innerHTML = tocHTML;
|
|
455
|
+
|
|
456
|
+
// --- Scroll spy ---
|
|
457
|
+
const tocLinks = sidebar.querySelectorAll('.toc-item');
|
|
458
|
+
|
|
459
|
+
function updateActive() {
|
|
460
|
+
let current = 0;
|
|
461
|
+
headings.forEach((h, i) => {
|
|
462
|
+
if (h.getBoundingClientRect().top <= 100) current = i;
|
|
463
|
+
});
|
|
464
|
+
tocLinks.forEach(l => l.classList.remove('active'));
|
|
465
|
+
if (tocLinks[current]) tocLinks[current].classList.add('active');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
window.addEventListener('scroll', updateActive, { passive: true });
|
|
469
|
+
updateActive();
|
|
470
|
+
|
|
471
|
+
// --- Sidebar toggle (mobile) ---
|
|
472
|
+
window.toggleSidebar = function() {
|
|
473
|
+
sidebar.classList.toggle('open');
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// --- Scroll to top ---
|
|
477
|
+
window.scrollToTop = function() {
|
|
478
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// --- Light/Dark toggle ---
|
|
482
|
+
const lightVars = {
|
|
483
|
+
'--bg': '#ffffff',
|
|
484
|
+
'--surface': '#f6f8fa',
|
|
485
|
+
'--surface-2': '#eef1f5',
|
|
486
|
+
'--border': '#d0d7de',
|
|
487
|
+
'--border-light': '#c5ccd3',
|
|
488
|
+
'--text': '#1f2328',
|
|
489
|
+
'--text-muted': '#57606a',
|
|
490
|
+
'--text-subtle': '#8b949e',
|
|
491
|
+
'--accent': '#0969da',
|
|
492
|
+
'--accent-soft': '#0969da1a',
|
|
493
|
+
'--green': '#1a7f37',
|
|
494
|
+
'--green-soft': '#1a7f371a',
|
|
495
|
+
'--purple': '#8250df',
|
|
496
|
+
'--orange': '#bc4c00',
|
|
497
|
+
'--red': '#cf222e',
|
|
498
|
+
'--yellow': '#9a6700',
|
|
499
|
+
'--code-bg': '#f6f8fa',
|
|
500
|
+
'--inline-code-bg': '#eff1f3',
|
|
501
|
+
'--scrollbar': '#d0d7de',
|
|
502
|
+
'--scrollbar-hover': '#afb8c1',
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const darkVars = {};
|
|
506
|
+
for (const key in lightVars) {
|
|
507
|
+
darkVars[key] = getComputedStyle(document.documentElement).getPropertyValue(key).trim();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let isDark = true;
|
|
511
|
+
window.toggleTheme = function() {
|
|
512
|
+
isDark = !isDark;
|
|
513
|
+
const vars = isDark ? darkVars : lightVars;
|
|
514
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
515
|
+
document.documentElement.style.setProperty(k, v);
|
|
516
|
+
}
|
|
517
|
+
document.querySelector('.topbar-btn:nth-child(2)').textContent = isDark ? 'Light' : 'Dark';
|
|
518
|
+
};
|
|
519
|
+
})();
|
|
520
|
+
</script>
|
|
521
|
+
|
|
522
|
+
</body>
|
|
523
|
+
</html>`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
module.exports = { renderHTML };
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { renderHTML } = require("./renderer");
|
|
5
|
+
|
|
6
|
+
function serve(filePath, options = {}) {
|
|
7
|
+
const port = options.port || 3333;
|
|
8
|
+
const absolutePath = path.resolve(filePath);
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(absolutePath)) {
|
|
11
|
+
console.error(`File not found: ${absolutePath}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const server = http.createServer((req, res) => {
|
|
16
|
+
if (req.url === "/" || req.url === "/index.html" || req.url === "/reload") {
|
|
17
|
+
const md = fs.readFileSync(absolutePath, "utf-8");
|
|
18
|
+
const html = renderHTML(md, path.basename(absolutePath));
|
|
19
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
20
|
+
res.end(html);
|
|
21
|
+
} else {
|
|
22
|
+
res.writeHead(404);
|
|
23
|
+
res.end("Not found");
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
server.listen(port, async () => {
|
|
28
|
+
const url = `http://localhost:${port}`;
|
|
29
|
+
console.log(`\n mdpretty`);
|
|
30
|
+
console.log(` ────────`);
|
|
31
|
+
console.log(` File: ${absolutePath}`);
|
|
32
|
+
console.log(` Server: ${url}\n`);
|
|
33
|
+
|
|
34
|
+
if (!options.noBrowser) {
|
|
35
|
+
const { default: open } = await import("open");
|
|
36
|
+
await open(url);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
process.on("SIGINT", () => {
|
|
41
|
+
console.log("\n Server stopped.");
|
|
42
|
+
server.close();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return server;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { serve };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prettymdviewer",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Beautiful markdown viewer — render .md files in a styled browser UI with TOC, dark/light themes, and scroll spy",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mdpretty": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"index.js"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node bin/cli.js",
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"markdown",
|
|
20
|
+
"viewer",
|
|
21
|
+
"preview",
|
|
22
|
+
"renderer",
|
|
23
|
+
"cli",
|
|
24
|
+
"pretty",
|
|
25
|
+
"dark-theme"
|
|
26
|
+
],
|
|
27
|
+
"author": "yogesh swami",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"type": "commonjs",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/yogesh-79/mdpretty"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"marked": "^17.0.6",
|
|
36
|
+
"open": "^11.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|