md-slides 1.0.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/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "md-slides",
3
+ "version": "1.0.0",
4
+ "description": "Convert Markdown to beautiful presentation slides. Zero config, developer-friendly.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "slides": "src/cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node test/test.js"
12
+ },
13
+ "keywords": [
14
+ "markdown",
15
+ "slides",
16
+ "presentation",
17
+ "cli",
18
+ "reveal",
19
+ "deck"
20
+ ],
21
+ "author": "Deepankar Rawat <dprrwt>",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/dprrwt/md-slides"
26
+ },
27
+ "dependencies": {
28
+ "chalk": "^5.3.0",
29
+ "chokidar": "^3.6.0",
30
+ "commander": "^12.1.0",
31
+ "highlight.js": "^11.10.0",
32
+ "marked": "^14.1.0",
33
+ "open": "^10.1.0",
34
+ "ws": "^8.18.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }
package/src/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { initProject } from './commands/init.js';
5
+ import { buildSlides } from './commands/build.js';
6
+ import { previewSlides } from './commands/preview.js';
7
+ import { exportSlides } from './commands/export.js';
8
+ import { createRequire } from 'module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('../package.json');
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('slides')
17
+ .description('Convert Markdown to beautiful presentation slides')
18
+ .version(pkg.version);
19
+
20
+ program
21
+ .command('init [name]')
22
+ .description('Create a new presentation from template')
23
+ .option('-t, --theme <theme>', 'Theme: dark, light, minimal, neon', 'dark')
24
+ .action((name, options) => {
25
+ initProject(name || 'presentation', options);
26
+ });
27
+
28
+ program
29
+ .command('build [file]')
30
+ .description('Build slides to static HTML')
31
+ .option('-o, --output <dir>', 'Output directory', 'dist')
32
+ .option('-t, --theme <theme>', 'Theme override')
33
+ .action((file, options) => {
34
+ buildSlides(file || 'slides.md', options);
35
+ });
36
+
37
+ program
38
+ .command('preview [file]')
39
+ .alias('dev')
40
+ .description('Live preview with hot reload')
41
+ .option('-p, --port <port>', 'Port number', '3000')
42
+ .option('-t, --theme <theme>', 'Theme override')
43
+ .action((file, options) => {
44
+ previewSlides(file || 'slides.md', options);
45
+ });
46
+
47
+ program
48
+ .command('export [file]')
49
+ .description('Export to PDF')
50
+ .option('-o, --output <file>', 'Output file', 'slides.pdf')
51
+ .action((file, options) => {
52
+ exportSlides(file || 'slides.md', options);
53
+ });
54
+
55
+ program.parse();
@@ -0,0 +1,42 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join, resolve, basename } from 'path';
3
+ import { parseSlides } from '../parser.js';
4
+ import { renderHTML } from '../renderer.js';
5
+
6
+ export function buildSlides(file, options = {}) {
7
+ const { output = 'dist', theme } = options;
8
+ const filePath = resolve(file);
9
+
10
+ if (!existsSync(filePath)) {
11
+ console.log(`\n ❌ File not found: ${file}\n`);
12
+ process.exit(1);
13
+ }
14
+
15
+ const content = readFileSync(filePath, 'utf-8');
16
+ const slidesData = parseSlides(content);
17
+
18
+ const buildOptions = {
19
+ theme: theme || slidesData.metadata.theme || 'dark',
20
+ title: slidesData.metadata.title || basename(file, '.md'),
21
+ author: slidesData.metadata.author || '',
22
+ liveReload: false,
23
+ };
24
+
25
+ const html = renderHTML(slidesData, buildOptions);
26
+
27
+ // Create output directory
28
+ const outputDir = resolve(output);
29
+ mkdirSync(outputDir, { recursive: true });
30
+
31
+ const outputFile = join(outputDir, 'index.html');
32
+ writeFileSync(outputFile, html);
33
+
34
+ console.log(`
35
+ ✅ Built ${slidesData.slides.length} slides → ${outputFile}
36
+
37
+ Theme: ${buildOptions.theme}
38
+ Title: ${buildOptions.title}
39
+
40
+ Open in browser or deploy to GitHub Pages.
41
+ `);
42
+ }
@@ -0,0 +1,18 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { resolve, basename } from 'path';
3
+
4
+ export function exportSlides(file, options = {}) {
5
+ console.log(`
6
+ 📄 PDF Export
7
+
8
+ For now, use the browser's Print to PDF:
9
+
10
+ 1. Run: slides preview ${file}
11
+ 2. Open the URL in Chrome
12
+ 3. Press Ctrl+P (or Cmd+P)
13
+ 4. Select "Save as PDF"
14
+ 5. Set Layout to "Landscape"
15
+
16
+ Native PDF export coming in a future version.
17
+ `);
18
+ }
@@ -0,0 +1,100 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ function getSampleSlides(theme) {
5
+ return [
6
+ '---',
7
+ 'title: My Presentation',
8
+ 'author: Your Name',
9
+ 'theme: ' + theme,
10
+ '---',
11
+ '',
12
+ '# Hello, World 👋',
13
+ '',
14
+ 'Welcome to **md-slides**',
15
+ '',
16
+ '---',
17
+ '',
18
+ '## What is md-slides?',
19
+ '',
20
+ 'A Markdown-to-slides converter that just works.',
21
+ '',
22
+ '- Write in **Markdown**',
23
+ '- Present in the **browser**',
24
+ '- Deploy anywhere as **static HTML**',
25
+ '',
26
+ '<!-- notes: This is the intro slide. Mention how easy it is to get started. -->',
27
+ '',
28
+ '---',
29
+ '',
30
+ '## Code Highlighting',
31
+ '',
32
+ '```javascript',
33
+ '// Syntax highlighting works out of the box',
34
+ 'function fibonacci(n) {',
35
+ ' if (n <= 1) return n;',
36
+ ' return fibonacci(n - 1) + fibonacci(n - 2);',
37
+ '}',
38
+ '',
39
+ 'console.log(fibonacci(10)); // 55',
40
+ '```',
41
+ '',
42
+ '---',
43
+ '',
44
+ '## Keyboard Shortcuts',
45
+ '',
46
+ '| Key | Action |',
47
+ '|-----|--------|',
48
+ '| ← → | Navigate slides |',
49
+ '| S | Toggle speaker notes |',
50
+ '| F | Fullscreen |',
51
+ '| Home / End | First / Last slide |',
52
+ '',
53
+ '---',
54
+ '',
55
+ '> "The best presentations are the ones you barely have to think about making."',
56
+ '',
57
+ '---',
58
+ '',
59
+ '<!-- layout: center -->',
60
+ '',
61
+ '# Thank You! 🎉',
62
+ '',
63
+ '**@yourhandle** · yourwebsite.com',
64
+ ].join('\n');
65
+ }
66
+
67
+ export function initProject(name, options = {}) {
68
+ const dir = join(process.cwd(), name);
69
+ const theme = options.theme || 'dark';
70
+
71
+ if (existsSync(dir)) {
72
+ console.log('\n Warning: Directory "' + name + '" already exists.\n');
73
+ process.exit(1);
74
+ }
75
+
76
+ mkdirSync(dir, { recursive: true });
77
+
78
+ // Create slides.md
79
+ writeFileSync(join(dir, 'slides.md'), getSampleSlides(theme));
80
+
81
+ // Create .gitignore
82
+ writeFileSync(join(dir, '.gitignore'), 'dist/\nnode_modules/\n');
83
+
84
+ console.log([
85
+ '',
86
+ ' ✨ Created presentation: ' + name + '/',
87
+ '',
88
+ ' Files:',
89
+ ' slides.md Your presentation',
90
+ ' .gitignore Ignores dist/',
91
+ '',
92
+ ' Next steps:',
93
+ ' cd ' + name,
94
+ ' slides preview Live preview with hot reload',
95
+ ' slides build Build to dist/',
96
+ '',
97
+ ' Edit slides.md and separate slides with ---',
98
+ '',
99
+ ].join('\n'));
100
+ }
@@ -0,0 +1,85 @@
1
+ import { readFileSync, existsSync, watch } from 'fs';
2
+ import { resolve, basename } from 'path';
3
+ import { createServer } from 'http';
4
+ import { WebSocketServer } from 'ws';
5
+ import { parseSlides } from '../parser.js';
6
+ import { renderHTML } from '../renderer.js';
7
+
8
+ export function previewSlides(file, options = {}) {
9
+ const { port = '3000', theme } = options;
10
+ const filePath = resolve(file);
11
+ const httpPort = parseInt(port);
12
+ const wsPort = httpPort + 1;
13
+
14
+ if (!existsSync(filePath)) {
15
+ console.log(`\n ❌ File not found: ${file}\n`);
16
+ process.exit(1);
17
+ }
18
+
19
+ function buildHTML() {
20
+ const content = readFileSync(filePath, 'utf-8');
21
+ const slidesData = parseSlides(content);
22
+ return renderHTML(slidesData, {
23
+ theme: theme || slidesData.metadata.theme || 'dark',
24
+ title: slidesData.metadata.title || basename(file, '.md'),
25
+ author: slidesData.metadata.author || '',
26
+ liveReload: true,
27
+ });
28
+ }
29
+
30
+ let html = buildHTML();
31
+
32
+ // HTTP server
33
+ const server = createServer((req, res) => {
34
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
35
+ res.end(html);
36
+ });
37
+
38
+ // WebSocket for live reload
39
+ const wss = new WebSocketServer({ port: wsPort });
40
+
41
+ // Watch for file changes
42
+ let debounce = null;
43
+ watch(filePath, () => {
44
+ if (debounce) clearTimeout(debounce);
45
+ debounce = setTimeout(() => {
46
+ try {
47
+ html = buildHTML();
48
+ wss.clients.forEach(client => {
49
+ if (client.readyState === 1) {
50
+ client.send('reload');
51
+ }
52
+ });
53
+ console.log(' 🔄 Rebuilt — reloading...');
54
+ } catch (err) {
55
+ console.log(` ⚠️ Build error: ${err.message}`);
56
+ }
57
+ }, 150);
58
+ });
59
+
60
+ server.listen(httpPort, () => {
61
+ const url = `http://localhost:${httpPort}`;
62
+ console.log(`
63
+ 🎬 Live preview running
64
+
65
+ URL: ${url}
66
+ File: ${filePath}
67
+
68
+ Watching for changes...
69
+ Press Ctrl+C to stop.
70
+ `);
71
+
72
+ // Try to open browser
73
+ import('open').then(({ default: open }) => {
74
+ open(url).catch(() => {});
75
+ });
76
+ });
77
+
78
+ server.on('error', (err) => {
79
+ if (err.code === 'EADDRINUSE') {
80
+ console.log(`\n ❌ Port ${httpPort} is in use. Try: slides preview -p ${httpPort + 10}\n`);
81
+ process.exit(1);
82
+ }
83
+ throw err;
84
+ });
85
+ }
package/src/parser.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Markdown Slides Parser
3
+ *
4
+ * Parses markdown into slide objects with frontmatter support.
5
+ * Slide separator: --- (horizontal rule)
6
+ * Speaker notes: <!--notes: ... -->
7
+ */
8
+
9
+ import { marked } from 'marked';
10
+ import hljs from 'highlight.js';
11
+
12
+ // Configure marked with syntax highlighting
13
+ marked.setOptions({
14
+ highlight: (code, lang) => {
15
+ if (lang && hljs.getLanguage(lang)) {
16
+ return hljs.highlight(code, { language: lang }).value;
17
+ }
18
+ return hljs.highlightAuto(code).value;
19
+ },
20
+ gfm: true,
21
+ breaks: false,
22
+ });
23
+
24
+ /**
25
+ * Parse frontmatter from markdown content
26
+ * Supports YAML-like key: value pairs between --- delimiters at the start
27
+ */
28
+ export function parseFrontmatter(content) {
29
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
30
+ const match = content.match(frontmatterRegex);
31
+
32
+ if (!match) {
33
+ return { metadata: {}, body: content };
34
+ }
35
+
36
+ const metadata = {};
37
+ const lines = match[1].split('\n');
38
+
39
+ for (const line of lines) {
40
+ const colonIndex = line.indexOf(':');
41
+ if (colonIndex > 0) {
42
+ const key = line.slice(0, colonIndex).trim();
43
+ const value = line.slice(colonIndex + 1).trim();
44
+ metadata[key] = value;
45
+ }
46
+ }
47
+
48
+ return {
49
+ metadata,
50
+ body: content.slice(match[0].length),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Parse slide-level directives from comment syntax
56
+ * <!-- class: classname -->
57
+ * <!-- background: #hex or url -->
58
+ * <!-- transition: fade|slide|zoom -->
59
+ */
60
+ function parseSlideDirectives(slideContent) {
61
+ const directives = {};
62
+ const directiveRegex = /<!--\s*(class|background|transition|layout|align):\s*(.*?)\s*-->/g;
63
+ let match;
64
+
65
+ while ((match = directiveRegex.exec(slideContent)) !== null) {
66
+ directives[match[1]] = match[2];
67
+ }
68
+
69
+ // Remove directive comments from content
70
+ const cleanContent = slideContent.replace(directiveRegex, '').trim();
71
+
72
+ return { directives, cleanContent };
73
+ }
74
+
75
+ /**
76
+ * Extract speaker notes from <!-- notes: ... --> blocks
77
+ */
78
+ function extractNotes(slideContent) {
79
+ const notesRegex = /<!--\s*notes:\s*([\s\S]*?)\s*-->/;
80
+ const match = slideContent.match(notesRegex);
81
+
82
+ if (!match) {
83
+ return { notes: '', content: slideContent };
84
+ }
85
+
86
+ return {
87
+ notes: match[1].trim(),
88
+ content: slideContent.replace(notesRegex, '').trim(),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Detect slide layout from content structure
94
+ */
95
+ function detectLayout(htmlContent) {
96
+ const hasH1 = /<h1[^>]*>/.test(htmlContent);
97
+ const hasH2 = /<h2[^>]*>/.test(htmlContent);
98
+ const hasOnlyHeading = /^<h[12][^>]*>.*<\/h[12]>\s*$/s.test(htmlContent.trim());
99
+ const hasImage = /<img[^>]*>/.test(htmlContent);
100
+ const hasCode = /<pre><code/.test(htmlContent);
101
+ const hasList = /<[uo]l>/.test(htmlContent);
102
+ const hasBlockquote = /<blockquote>/.test(htmlContent);
103
+
104
+ if (hasOnlyHeading) return 'center';
105
+ if (hasH1 && !hasH2) return 'title';
106
+ if (hasBlockquote && !hasCode && !hasList) return 'quote';
107
+ if (hasCode) return 'code';
108
+ if (hasImage) return 'image';
109
+ return 'default';
110
+ }
111
+
112
+ /**
113
+ * Parse markdown content into an array of slide objects
114
+ */
115
+ export function parseSlides(content) {
116
+ const { metadata, body } = parseFrontmatter(content);
117
+
118
+ // Split on --- that's on its own line (not inside code blocks)
119
+ const rawSlides = splitSlides(body);
120
+
121
+ const slides = rawSlides.map((raw, index) => {
122
+ const { notes, content: withoutNotes } = extractNotes(raw);
123
+ const { directives, cleanContent } = parseSlideDirectives(withoutNotes);
124
+ const html = marked.parse(cleanContent);
125
+ const layout = directives.layout || detectLayout(html);
126
+
127
+ return {
128
+ index,
129
+ raw: cleanContent,
130
+ html,
131
+ notes,
132
+ layout,
133
+ directives,
134
+ };
135
+ });
136
+
137
+ return { metadata, slides };
138
+ }
139
+
140
+ /**
141
+ * Split markdown into slides by --- separator
142
+ * Respects code blocks (doesn't split inside ```)
143
+ */
144
+ function splitSlides(content) {
145
+ const lines = content.split('\n');
146
+ const slides = [];
147
+ let current = [];
148
+ let inCodeBlock = false;
149
+
150
+ for (const line of lines) {
151
+ // Track code block state
152
+ if (line.trim().startsWith('```')) {
153
+ inCodeBlock = !inCodeBlock;
154
+ }
155
+
156
+ // Check for slide separator (--- on its own line, not in code block)
157
+ if (!inCodeBlock && /^---\s*$/.test(line.trim()) && current.length > 0) {
158
+ const slideContent = current.join('\n').trim();
159
+ if (slideContent) {
160
+ slides.push(slideContent);
161
+ }
162
+ current = [];
163
+ continue;
164
+ }
165
+
166
+ current.push(line);
167
+ }
168
+
169
+ // Don't forget the last slide
170
+ const lastSlide = current.join('\n').trim();
171
+ if (lastSlide) {
172
+ slides.push(lastSlide);
173
+ }
174
+
175
+ return slides;
176
+ }
177
+
178
+ export default { parseSlides, parseFrontmatter };