skimmd 1.0.0 → 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/README.md +93 -55
- package/bin/skimmd.js +139 -2
- package/darkMode.png +0 -0
- package/lightMode.png +0 -0
- package/package.json +1 -1
- package/public/index.html +1105 -67
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
### skimmd
|
|
2
2
|
|
|
3
3
|
**One command. Zero install. Your markdown, beautifully rendered.**
|
|
4
4
|
|
|
@@ -8,20 +8,20 @@ npx skimmd
|
|
|
8
8
|
|
|
9
9
|
That's it. Browser opens. Your markdown files look like GitHub. Edit them live.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
* * *
|
|
12
12
|
|
|
13
13
|
## The Problem
|
|
14
14
|
|
|
15
15
|
You're writing a README. You want to see how it looks on GitHub. Your options:
|
|
16
16
|
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
17
|
+
1. Push to GitHub, refresh, fix typo, push again, refresh... (we've all been there)
|
|
18
|
+
2. Install a 200MB Electron app
|
|
19
|
+
3. Use VS Code's preview that looks nothing like GitHub
|
|
20
|
+
4. Use `grip` but hit GitHub API rate limits
|
|
21
21
|
|
|
22
22
|
**skimmd**: One command, works offline, looks like GitHub, updates as you type.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
* * *
|
|
25
25
|
|
|
26
26
|
## 30-Second Demo
|
|
27
27
|
|
|
@@ -34,64 +34,102 @@ Your browser opens. You see your README rendered beautifully. Now open that file
|
|
|
34
34
|
|
|
35
35
|
No reload button. No refresh. Just save and see.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
### Dark Mode
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
### Light Mode
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
* * *
|
|
38
46
|
|
|
39
47
|
## Features
|
|
40
48
|
|
|
41
49
|
### Live Reload
|
|
50
|
+
|
|
42
51
|
Edit in VS Code, Vim, or any editor. Save. Browser updates. No plugins, no extensions, no configuration.
|
|
43
52
|
|
|
44
53
|
### Inline Editing
|
|
54
|
+
|
|
45
55
|
Click "Edit" in the browser. Make changes. Hit Ctrl+S. **Saves directly to your .md file.** Your editor picks up the change. Round-trip editing.
|
|
46
56
|
|
|
47
57
|
### GitHub-Flavored Markdown
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
58
|
+
|
|
59
|
+
- Tables render correctly
|
|
60
|
+
- Task lists work (`- [x] like this`)
|
|
61
|
+
- Syntax highlighting for 190+ languages
|
|
62
|
+
- Fenced code blocks
|
|
63
|
+
- Strikethrough, autolinks, all of it
|
|
53
64
|
|
|
54
65
|
### Dark Mode
|
|
66
|
+
|
|
55
67
|
Light, Dark, or Auto (follows your system). Because it's 2024 and we're not animals.
|
|
56
68
|
|
|
57
69
|
### Browse Entire Folders
|
|
70
|
+
|
|
58
71
|
```bash
|
|
59
72
|
npx skimmd ./docs
|
|
60
73
|
```
|
|
74
|
+
|
|
61
75
|
Sidebar shows all your markdown files. Filter them. Jump between them. Table of contents auto-generated from headings.
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
* * *
|
|
64
78
|
|
|
65
79
|
## Why Not Just Use...
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
Tool
|
|
82
|
+
|
|
83
|
+
The Problem
|
|
84
|
+
|
|
85
|
+
**VS Code Preview**
|
|
86
|
+
|
|
87
|
+
Doesn't look like GitHub. No live reload in browser.
|
|
88
|
+
|
|
89
|
+
**grip**
|
|
90
|
+
|
|
91
|
+
Hits GitHub API. Rate limited. Requires auth for private repos.
|
|
92
|
+
|
|
93
|
+
**Typora**
|
|
94
|
+
|
|
95
|
+
$15. Electron app. Doesn't look like GitHub.
|
|
96
|
+
|
|
97
|
+
**MacDown**
|
|
98
|
+
|
|
99
|
+
macOS only. No live reload. Dated UI.
|
|
100
|
+
|
|
101
|
+
**Obsidian**
|
|
102
|
+
|
|
103
|
+
Overkill for previewing a README. Different styling.
|
|
104
|
+
|
|
105
|
+
**glow**
|
|
106
|
+
|
|
107
|
+
Terminal only. Can't share screen with non-terminal people.
|
|
108
|
+
|
|
109
|
+
**peekmd**
|
|
110
|
+
|
|
111
|
+
Requires Bun. No editing. No live reload.
|
|
76
112
|
|
|
77
113
|
**skimmd**: Works with Node (you already have it). Zero config. Live reload. Inline editing. Looks like GitHub.
|
|
78
114
|
|
|
79
|
-
|
|
115
|
+
* * *
|
|
80
116
|
|
|
81
117
|
## Installation
|
|
82
118
|
|
|
83
119
|
**Option 1: No install (recommended)**
|
|
120
|
+
|
|
84
121
|
```bash
|
|
85
122
|
npx skimmd
|
|
86
123
|
```
|
|
87
124
|
|
|
88
125
|
**Option 2: Global install**
|
|
126
|
+
|
|
89
127
|
```bash
|
|
90
128
|
npm install -g skimmd
|
|
91
129
|
skimmd
|
|
92
130
|
```
|
|
93
131
|
|
|
94
|
-
|
|
132
|
+
* * *
|
|
95
133
|
|
|
96
134
|
## Usage
|
|
97
135
|
|
|
@@ -112,48 +150,50 @@ skimmd --port 8080
|
|
|
112
150
|
skimmd --no-open
|
|
113
151
|
```
|
|
114
152
|
|
|
115
|
-
|
|
153
|
+
* * *
|
|
116
154
|
|
|
117
155
|
## Keyboard Shortcuts
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
Shortcut
|
|
158
|
+
|
|
159
|
+
Action
|
|
160
|
+
|
|
161
|
+
`Ctrl/Cmd + S`
|
|
162
|
+
|
|
163
|
+
Save (when editing)
|
|
164
|
+
|
|
165
|
+
`Ctrl/Cmd + K`
|
|
166
|
+
|
|
167
|
+
Focus file filter
|
|
123
168
|
|
|
124
|
-
|
|
169
|
+
* * *
|
|
125
170
|
|
|
126
171
|
## How It Works
|
|
127
172
|
|
|
128
|
-
1.
|
|
129
|
-
2.
|
|
130
|
-
3.
|
|
131
|
-
4.
|
|
132
|
-
5.
|
|
133
|
-
6.
|
|
173
|
+
1. Starts a local Express server
|
|
174
|
+
2. Watches your markdown files with chokidar
|
|
175
|
+
3. Renders with `marked` (GitHub-flavored)
|
|
176
|
+
4. Syntax highlighting with `highlight.js`
|
|
177
|
+
5. Sends file changes via Server-Sent Events
|
|
178
|
+
6. Browser updates without refresh
|
|
134
179
|
|
|
135
180
|
All local. Nothing leaves your machine. Works on a plane.
|
|
136
181
|
|
|
137
|
-
|
|
182
|
+
* * *
|
|
138
183
|
|
|
139
184
|
## FAQ
|
|
140
185
|
|
|
141
|
-
**Does it work offline?**
|
|
142
|
-
Yes. Everything runs locally. No API calls.
|
|
186
|
+
**Does it work offline?** Yes. Everything runs locally. No API calls.
|
|
143
187
|
|
|
144
|
-
**Can I use it for a presentation?**
|
|
145
|
-
Yes. Dark mode + clean UI + live reload = great for live coding demos.
|
|
188
|
+
**Can I use it for a presentation?** Yes. Dark mode + clean UI + live reload = great for live coding demos.
|
|
146
189
|
|
|
147
|
-
**Does it support Mermaid diagrams?**
|
|
148
|
-
Not yet. PRs welcome.
|
|
190
|
+
**Does it support Mermaid diagrams?** Not yet. PRs welcome.
|
|
149
191
|
|
|
150
|
-
**What about MDX?**
|
|
151
|
-
Just markdown for now. Keep it simple.
|
|
192
|
+
**What about MDX?** Just markdown for now. Keep it simple.
|
|
152
193
|
|
|
153
|
-
**Windows support?**
|
|
154
|
-
Yes. Works anywhere Node.js runs.
|
|
194
|
+
**Windows support?** Yes. Works anywhere Node.js runs.
|
|
155
195
|
|
|
156
|
-
|
|
196
|
+
* * *
|
|
157
197
|
|
|
158
198
|
## Contributing
|
|
159
199
|
|
|
@@ -167,15 +207,13 @@ npm link
|
|
|
167
207
|
skimmd ./docs
|
|
168
208
|
```
|
|
169
209
|
|
|
170
|
-
|
|
210
|
+
* * *
|
|
171
211
|
|
|
172
212
|
## License
|
|
173
213
|
|
|
174
214
|
MIT
|
|
175
215
|
|
|
176
|
-
|
|
216
|
+
* * *
|
|
177
217
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<code>npx skimmd</code>
|
|
181
|
-
</p>
|
|
218
|
+
**Stop pushing to GitHub just to preview your README.**
|
|
219
|
+
`npx skimmd`
|
package/bin/skimmd.js
CHANGED
|
@@ -297,6 +297,129 @@ async function createServer(options) {
|
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
// Browse directories and files
|
|
301
|
+
app.get("/api/browse", async (req, res) => {
|
|
302
|
+
const browsePath = req.query.path || process.env.HOME || "/";
|
|
303
|
+
try {
|
|
304
|
+
const resolvedPath = path.resolve(browsePath);
|
|
305
|
+
const stats = await fsp.stat(resolvedPath);
|
|
306
|
+
|
|
307
|
+
if (!stats.isDirectory()) {
|
|
308
|
+
return res.status(400).json({ error: "Path is not a directory" });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const entries = await fsp.readdir(resolvedPath, { withFileTypes: true });
|
|
312
|
+
const items = [];
|
|
313
|
+
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
// Skip hidden files/folders
|
|
316
|
+
if (entry.name.startsWith(".")) continue;
|
|
317
|
+
|
|
318
|
+
const fullPath = path.join(resolvedPath, entry.name);
|
|
319
|
+
const isDir = entry.isDirectory();
|
|
320
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
321
|
+
const isMarkdown = [".md", ".markdown", ".txt"].includes(ext);
|
|
322
|
+
|
|
323
|
+
// Only include directories and markdown files
|
|
324
|
+
if (isDir || isMarkdown) {
|
|
325
|
+
items.push({
|
|
326
|
+
name: entry.name,
|
|
327
|
+
path: fullPath,
|
|
328
|
+
isDirectory: isDir,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Sort: directories first, then files, alphabetically
|
|
334
|
+
items.sort((a, b) => {
|
|
335
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
336
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
337
|
+
return a.name.localeCompare(b.name);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
res.json({
|
|
341
|
+
current: resolvedPath,
|
|
342
|
+
parent: path.dirname(resolvedPath),
|
|
343
|
+
items,
|
|
344
|
+
});
|
|
345
|
+
} catch (error) {
|
|
346
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to browse" });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Open any markdown file by absolute path
|
|
351
|
+
app.get("/api/open-file", async (req, res) => {
|
|
352
|
+
const filePath = req.query.path;
|
|
353
|
+
if (!filePath || typeof filePath !== "string") {
|
|
354
|
+
return res.status(400).json({ error: "Missing file path" });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const resolvedPath = path.resolve(filePath);
|
|
359
|
+
const stats = await fsp.stat(resolvedPath);
|
|
360
|
+
|
|
361
|
+
if (!stats.isFile()) {
|
|
362
|
+
return res.status(400).json({ error: "Path is not a file" });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const content = await fsp.readFile(resolvedPath, "utf-8");
|
|
366
|
+
const html = marked.parse(content);
|
|
367
|
+
|
|
368
|
+
res.json({
|
|
369
|
+
path: resolvedPath,
|
|
370
|
+
name: path.basename(resolvedPath),
|
|
371
|
+
content,
|
|
372
|
+
html,
|
|
373
|
+
metadata: {
|
|
374
|
+
size: stats.size,
|
|
375
|
+
modifiedAt: stats.mtime,
|
|
376
|
+
createdAt: stats.birthtime,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
res.status(404).json({ error: error instanceof Error ? error.message : "File not found" });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Save any markdown file by absolute path
|
|
385
|
+
app.put("/api/open-file", async (req, res) => {
|
|
386
|
+
const filePath = req.query.path;
|
|
387
|
+
const { content, html } = req.body || {};
|
|
388
|
+
|
|
389
|
+
if (!filePath || typeof filePath !== "string") {
|
|
390
|
+
return res.status(400).json({ error: "Missing file path" });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let finalContent = content;
|
|
394
|
+
if (typeof html === "string") {
|
|
395
|
+
finalContent = turndownService.turndown(html);
|
|
396
|
+
}
|
|
397
|
+
if (typeof finalContent !== "string") {
|
|
398
|
+
return res.status(400).json({ error: "Missing content" });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const resolvedPath = path.resolve(filePath);
|
|
403
|
+
await fsp.writeFile(resolvedPath, finalContent, "utf-8");
|
|
404
|
+
const stats = await fsp.stat(resolvedPath);
|
|
405
|
+
const renderedHtml = marked.parse(finalContent);
|
|
406
|
+
|
|
407
|
+
res.json({
|
|
408
|
+
path: resolvedPath,
|
|
409
|
+
name: path.basename(resolvedPath),
|
|
410
|
+
content: finalContent,
|
|
411
|
+
html: renderedHtml,
|
|
412
|
+
metadata: {
|
|
413
|
+
size: stats.size,
|
|
414
|
+
modifiedAt: stats.mtime,
|
|
415
|
+
createdAt: stats.birthtime,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
} catch (error) {
|
|
419
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to save file" });
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
300
423
|
app.use(express.static(path.join(__dirname, "..", "public")));
|
|
301
424
|
|
|
302
425
|
app.get("*", (_req, res) => {
|
|
@@ -363,9 +486,23 @@ program
|
|
|
363
486
|
}
|
|
364
487
|
});
|
|
365
488
|
|
|
366
|
-
const shutdown = () => {
|
|
367
|
-
|
|
489
|
+
const shutdown = async () => {
|
|
490
|
+
console.log("\nShutting down...");
|
|
491
|
+
|
|
492
|
+
// Close all SSE connections first
|
|
493
|
+
for (const client of sseClients) {
|
|
494
|
+
client.end();
|
|
495
|
+
}
|
|
496
|
+
sseClients.clear();
|
|
497
|
+
|
|
498
|
+
// Close file watcher (returns Promise in chokidar v5)
|
|
499
|
+
await watcher.close();
|
|
500
|
+
|
|
501
|
+
// Close server with timeout fallback
|
|
368
502
|
server.close(() => process.exit(0));
|
|
503
|
+
|
|
504
|
+
// Force exit if server doesn't close within 2 seconds
|
|
505
|
+
setTimeout(() => process.exit(0), 2000);
|
|
369
506
|
};
|
|
370
507
|
|
|
371
508
|
process.on("SIGINT", shutdown);
|
package/darkMode.png
ADDED
|
Binary file
|
package/lightMode.png
ADDED
|
Binary file
|