create-marp-presentation 1.1.0 → 1.2.1

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.
Files changed (36) hide show
  1. package/README.md +45 -37
  2. package/cli/commands/add-themes-cli.js +85 -0
  3. package/cli/commands/create-project.js +199 -0
  4. package/cli/utils/file-utils.js +106 -0
  5. package/cli/utils/prompt-utils.js +89 -0
  6. package/docs/plans/2025-02-19-marp-template-design.md +207 -0
  7. package/docs/plans/2025-02-19-marp-template-implementation.md +848 -0
  8. package/docs/plans/2026-02-20-example-slides-design.md +179 -0
  9. package/docs/plans/2026-02-20-example-slides-implementation.md +811 -0
  10. package/docs/plans/2026-02-23-theme-management-design.md +836 -0
  11. package/docs/plans/2026-02-23-theme-management-implementation.md +3585 -0
  12. package/docs/plans/2026-02-26-theme-addition-refactoring-design.md +172 -0
  13. package/docs/plans/2026-02-26-theme-addition-refactoring.md +456 -0
  14. package/docs/plans/2026-02-27-theme-add-fix-design.md +136 -0
  15. package/docs/plans/2026-02-27-theme-add-fix.md +353 -0
  16. package/docs/plans/TODO.md +5 -0
  17. package/docs/reqs/themes-requirements.md +49 -0
  18. package/docs/theme-management.md +261 -0
  19. package/index.js +111 -164
  20. package/lib/add-themes-command.js +381 -0
  21. package/lib/errors.js +62 -0
  22. package/lib/frontmatter.js +71 -0
  23. package/lib/prompts.js +222 -0
  24. package/lib/theme-manager.js +238 -0
  25. package/lib/theme-resolver.js +227 -0
  26. package/lib/vscode-integration.js +198 -0
  27. package/package.json +15 -5
  28. package/template/README.md +28 -17
  29. package/template/package.json +13 -1
  30. package/template/scripts/theme-cli.js +248 -0
  31. package/themes/beam/beam.css +141 -0
  32. package/themes/default-clean/default-clean.css +57 -0
  33. package/themes/gaia-dark/gaia-dark.css +27 -0
  34. package/themes/marpx/marpx.css +1735 -0
  35. package/themes/marpx/socrates.css +105 -0
  36. package/themes/uncover-minimal/uncover-minimal.css +22 -0
package/README.md CHANGED
@@ -1,64 +1,72 @@
1
1
  # Create Marp Presentation
2
2
 
3
- Create a new Marp presentation with a single command.
3
+ Create beautiful presentations in Markdown. Zero setup.
4
4
 
5
- ## Installation
5
+ ## Quick Start
6
6
 
7
7
  ```bash
8
8
  npx create-marp-presentation my-presentation
9
+ cd my-presentation
10
+ npm run dev
9
11
  ```
10
12
 
11
- ## Usage
13
+ That's it! Edit `presentation.md` and see changes live at `http://localhost:8080`.
12
14
 
13
- ```bash
14
- cd my-presentation
15
- npm run dev # Live preview
16
- npm run build:all # Build all formats
17
- ```
15
+ ## VSCode Setup
18
16
 
19
- ## Features
17
+ For the best editing experience, install [Marp for VSCode](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode):
20
18
 
21
- - 🚀 One-command setup
22
- - 📝 Markdown slides
23
- - 🎨 Marp themes
24
- - 📦 HTML, PDF, PPTX export
25
- - 📁 Static files support
26
- - 🔥 Live preview
19
+ 1. Open VSCode
20
+ 2. Press `Ctrl+Shift+X` (or `Cmd+Shift+X` on Mac)
21
+ 3. Search "Marp for VSCode"
22
+ 4. Click Install
27
23
 
28
- ## Local Testing
24
+ After installing, open any `.md` file and click the **Marp: Preview** button in the top-right corner.
29
25
 
30
- To test project generation without publishing to npm:
26
+ ## What You Get
31
27
 
32
- ```bash
33
- # Clone the repository
34
- git clone https://github.com/echernyshev/marp-presentation-template.git
35
- cd marp-presentation-template
28
+ - **Markdown slides** - Write presentations in plain text
29
+ - **Live preview** - See changes instantly in browser
30
+ - **Export anywhere** - HTML, PDF, PPTX with one command
31
+ - **Themes** - Beautiful built-in themes, add more anytime
32
+ - **Static files** - Images and assets copied automatically
36
33
 
37
- # Install dependencies for tests
38
- npm install
34
+ ## Commands
39
35
 
40
- # Run tests
41
- npm test
36
+ | Command | What it does |
37
+ |---------|--------------|
38
+ | `npm run dev` | Live preview at http://localhost:8080 |
39
+ | `npm run build:all` | Export to HTML, PDF, and PPTX |
40
+ | `npm run clean` | Remove output folder |
42
41
 
43
- # Create a test project (interactive mode)
44
- node index.js test-project
42
+ ## Theme Management
45
43
 
46
- # Check the contents
47
- cd test-project
48
- ls -la
49
- npm run dev
50
- ```
44
+ Add and switch themes anytime.
51
45
 
52
- Other example commands:
46
+ ### Quick Theme Commands
53
47
 
54
48
  ```bash
55
- # Create a test project without examples
56
- echo "n" | node index.js test-project-minimal
49
+ npm run theme:add # Interactive: select themes to add
50
+ npm run theme:add beam # Add specific themes
51
+ npm run theme:list # See what's available
52
+ npm run theme:set marpx # Change active theme
57
53
  ```
58
54
 
59
- ## Documentation
55
+ When you create a project, you'll be prompted to add themes. Add more later with `npm run theme:add`.
56
+
57
+ [See full theme documentation →](docs/theme-management.md)
58
+
59
+ ## Local Development
60
60
 
61
- After creating a project, read the README in the project folder.
61
+ To test without publishing to npm:
62
+
63
+ ```bash
64
+ git clone https://github.com/echernyshev/marp-presentation-template.git
65
+ cd marp-presentation-template
66
+ npm install
67
+ npm test
68
+ node index.js test-project
69
+ ```
62
70
 
63
71
  ## License
64
72
 
@@ -0,0 +1,85 @@
1
+ // cli/commands/add-themes-cli.js
2
+ /**
3
+ * Add themes to existing project command for meta-package CLI
4
+ * This file is NOT copied to generated projects
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const { AddThemesCommand } = require('../../lib/add-themes-command');
10
+ const { ThemeError } = require('../../lib/errors');
11
+
12
+ /**
13
+ * Add themes to an existing project
14
+ * @param {string} targetPath - Path to the target project
15
+ * @param {Object} options - Configuration options
16
+ * @param {string} options.themesLibraryPath - Path to themes library
17
+ * @param {string[]} options.themeNames - Specific theme names to add (optional)
18
+ * @returns {Promise<void>}
19
+ */
20
+ async function addThemesToExistingProject(targetPath, options = {}) {
21
+ const {
22
+ themesLibraryPath = path.join(__dirname, '../..', 'themes'),
23
+ themeNames = null
24
+ } = options;
25
+
26
+ // Validate target path exists
27
+ const resolvedPath = path.resolve(targetPath);
28
+ if (!fs.existsSync(resolvedPath)) {
29
+ console.error(`Error: Directory does not exist: ${targetPath}`);
30
+ throw new ThemeError(`Target path does not exist: ${targetPath}`);
31
+ }
32
+
33
+ // Check if it's a valid Marp project (has presentation.md or package.json)
34
+ const hasPresentation = fs.existsSync(path.join(resolvedPath, 'presentation.md'));
35
+ const hasPackageJson = fs.existsSync(path.join(resolvedPath, 'package.json'));
36
+
37
+ if (!hasPresentation && !hasPackageJson) {
38
+ console.warn('Warning: This does not appear to be a Marp presentation project.');
39
+ console.warn(' (no presentation.md or package.json found)');
40
+ }
41
+
42
+ // Create AddThemesCommand with themes library path
43
+ const command = new AddThemesCommand({
44
+ templatePath: themesLibraryPath,
45
+ interactive: true
46
+ });
47
+
48
+ try {
49
+ console.log(`Adding themes to: ${resolvedPath}`);
50
+ console.log();
51
+
52
+ // Execute command - pass theme names if provided, otherwise prompt
53
+ const { copied, skipped, conflicts } = await command.execute(resolvedPath, {
54
+ themes: themeNames || undefined
55
+ });
56
+
57
+ // Show summary
58
+ const copiedNames = copied.map(t => t.name);
59
+ console.log(`\nCopied themes: ${copiedNames.join(', ') || 'none'}`);
60
+ if (skipped.length > 0) {
61
+ console.log(`Skipped: ${skipped.join(', ')}`);
62
+ }
63
+ if (conflicts.length > 0) {
64
+ console.log(`Conflicts: ${conflicts.join(', ')}`);
65
+ }
66
+
67
+ console.log();
68
+ console.log('Themes added successfully!');
69
+ console.log(`\nNext steps in ${targetPath}:`);
70
+ console.log(' npm run theme:list # List available themes');
71
+ console.log(' npm run theme:set <theme> # Set active theme');
72
+ console.log();
73
+
74
+ } catch (error) {
75
+ if (error instanceof ThemeError) {
76
+ console.error(`Error: ${error.message}`);
77
+ throw error;
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ module.exports = {
84
+ addThemesToExistingProject
85
+ };
@@ -0,0 +1,199 @@
1
+ // cli/commands/create-project.js
2
+ /**
3
+ * Project creation command for meta-package CLI
4
+ * This file is NOT copied to generated projects
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const { AddThemesCommand } = require('../../lib/add-themes-command');
11
+ const { Prompts } = require('../../lib/prompts');
12
+ const { ThemeManager } = require('../../lib/theme-manager');
13
+
14
+ const { copyDir, copyOptionalFiles } = require('../utils/file-utils');
15
+ const { askCreateExamples } = require('../utils/prompt-utils');
16
+
17
+ /**
18
+ * Validate project name
19
+ * @param {string} name - Project name to validate
20
+ * @returns {boolean} True if valid
21
+ */
22
+ function validateProjectName(name) {
23
+ const validName = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
24
+ return validName.test(name);
25
+ }
26
+
27
+ /**
28
+ * Parse --path argument from argv
29
+ * @param {string[]} argv - Process argv (should start from index 2)
30
+ * @returns {{outputPath: string, pathIndex: number|null}} Parsed arguments
31
+ */
32
+ function parsePathArg(argv) {
33
+ let outputPath = process.cwd();
34
+ const pathIndex = argv.indexOf('--path');
35
+ if (pathIndex !== -1 && pathIndex + 1 < argv.length) {
36
+ return { outputPath: null, pathIndex }; // Signal that validation is needed
37
+ }
38
+ return { outputPath, pathIndex: null };
39
+ }
40
+
41
+ /**
42
+ * Create a new Marp presentation project
43
+ * @param {string} projectName - Name of the project
44
+ * @param {Object} options - Configuration options
45
+ * @param {string} options.outputPath - Output directory path (default: current directory)
46
+ * @param {string} options.templatePath - Path to template directory
47
+ * @param {string} options.themesLibraryPath - Path to themes library
48
+ * @param {Function} options.validatePath - Path validation function
49
+ * @returns {Promise<void>}
50
+ */
51
+ async function createProject(projectName, options = {}) {
52
+ const {
53
+ outputPath: providedOutputPath,
54
+ templatePath = path.join(__dirname, '../..', 'template'),
55
+ themesLibraryPath = path.join(__dirname, '../..', 'themes'),
56
+ validatePath
57
+ } = options;
58
+
59
+ // Validate project name
60
+ if (!validateProjectName(projectName)) {
61
+ console.error(`Invalid project name: "${projectName}"`);
62
+ console.error('Project name must be lowercase, contain only letters, numbers, and hyphens.');
63
+ throw new Error('Invalid project name');
64
+ }
65
+
66
+ // Determine output path
67
+ let outputPath = providedOutputPath || process.cwd();
68
+
69
+ // Build final project path
70
+ const projectPath = path.join(outputPath, projectName);
71
+
72
+ // Check if project already exists
73
+ if (fs.existsSync(projectPath)) {
74
+ console.error(`Directory "${projectName}" already exists.`);
75
+ throw new Error('Project directory already exists');
76
+ }
77
+
78
+ console.log(`Creating Marp presentation: ${projectName}`);
79
+ if (outputPath !== process.cwd()) {
80
+ console.log(` Location: ${projectPath}`);
81
+ }
82
+ console.log();
83
+
84
+ // Ask about example slides
85
+ const createExamples = await askCreateExamples();
86
+
87
+ // Create project directory
88
+ fs.mkdirSync(projectPath, { recursive: true });
89
+
90
+ // Copy template completely
91
+ copyDir(templatePath, projectPath);
92
+
93
+ // Copy lib scripts to project
94
+ const rootLibPath = path.join(__dirname, '../..', 'lib');
95
+ const projectScriptsLibPath = path.join(projectPath, 'scripts', 'lib');
96
+ fs.mkdirSync(projectScriptsLibPath, { recursive: true });
97
+ copyDir(rootLibPath, projectScriptsLibPath);
98
+
99
+ // Verify marp.themeSet configuration exists in package.json
100
+ ThemeManager.ensureThemeSetConfig(projectPath);
101
+
102
+ console.log('✓ Project created');
103
+
104
+ // Copy optional files
105
+ if (createExamples) {
106
+ copyOptionalFiles(projectPath);
107
+ console.log('✓ Example slides added');
108
+ console.log('✓ Demo image added to static/');
109
+ }
110
+
111
+ // Copy theme management documentation
112
+ const themeDocsSource = path.join(__dirname, '../..', 'docs', 'theme-management.md');
113
+ const themeDocsDest = path.join(projectPath, 'docs', 'theme-management.md');
114
+ if (fs.existsSync(themeDocsSource)) {
115
+ fs.mkdirSync(path.dirname(themeDocsDest), { recursive: true });
116
+ fs.copyFileSync(themeDocsSource, themeDocsDest);
117
+ console.log('✓ Theme management guide added');
118
+ }
119
+
120
+ // Add themes to project (prompts user interactively if TTY)
121
+ let copied = [];
122
+ if (process.stdin.isTTY) {
123
+ // Interactive mode - prompt for themes
124
+ console.log();
125
+ const command = new AddThemesCommand({
126
+ templatePath: themesLibraryPath,
127
+ interactive: true
128
+ });
129
+
130
+ const result = await command.execute(projectPath, {
131
+ themes: null // Triggers built-in prompt
132
+ });
133
+ copied = result.copied;
134
+ }
135
+ // Non-interactive mode: skip theme addition entirely
136
+
137
+ // Select and set active theme from copied themes
138
+ if (copied && copied.length > 0) {
139
+ console.log();
140
+ const themeNames = copied.map(t => t.name);
141
+ const activeTheme = await Prompts.promptActiveTheme(themeNames);
142
+ const themeManager = new ThemeManager(projectPath);
143
+ try {
144
+ themeManager.setActiveTheme(activeTheme);
145
+ console.log(`✓ Set active theme to "${activeTheme}"`);
146
+ } catch (error) {
147
+ console.warn(` Warning: Could not set active theme: ${error.message}`);
148
+ }
149
+ } else {
150
+ // No themes copied, set default active theme
151
+ const themeManager = new ThemeManager(projectPath);
152
+ try {
153
+ themeManager.setActiveTheme('default');
154
+ console.log(`✓ Set active theme to "default"`);
155
+ } catch (error) {
156
+ console.warn(` Warning: Could not set active theme: ${error.message}`);
157
+ }
158
+ }
159
+
160
+ console.log();
161
+
162
+ // Run npm install
163
+ console.log('Installing dependencies...');
164
+ const installResult = spawnSync('npm', ['install'], {
165
+ cwd: projectPath,
166
+ stdio: 'inherit',
167
+ });
168
+
169
+ if (installResult.status !== 0) {
170
+ console.error();
171
+ console.error('Failed to install dependencies.');
172
+ console.error('Please run "cd ' + projectName + ' && npm install" manually.');
173
+ throw new Error('npm install failed');
174
+ }
175
+
176
+ console.log();
177
+ console.log('✓ Dependencies installed');
178
+ console.log();
179
+ console.log('Next steps:');
180
+ const cwdRelativePath = path.relative(process.cwd(), projectPath);
181
+ const cdPath = cwdRelativePath.startsWith('..') ? projectPath : cwdRelativePath;
182
+ const readmePath = path.join(cdPath, 'README.md');
183
+ console.log(` cd ${cdPath}`);
184
+ console.log(' npm run dev # Start live preview');
185
+ console.log(' npm run theme:list # List available themes');
186
+ console.log(' npm run theme:set <theme-name> # Set active theme');
187
+ console.log(' npm run theme:add # Add more themes to the project');
188
+ console.log(' npm run build:all # Build all formats');
189
+ console.log(' Open project folder in vscode with Marp extension for editing your presentation');
190
+ console.log(` Read ${readmePath} for more information`);
191
+ console.log(' Enjoy!');
192
+ console.log();
193
+ }
194
+
195
+ module.exports = {
196
+ createProject,
197
+ validateProjectName,
198
+ parsePathArg
199
+ };
@@ -0,0 +1,106 @@
1
+ // cli/utils/file-utils.js
2
+ /**
3
+ * File and directory utilities for meta-package CLI commands
4
+ * These functions are NOT copied to generated projects
5
+ */
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Expand ~ to home directory
12
+ * @param {string} input - Path that may start with ~
13
+ * @returns {string} Expanded path
14
+ */
15
+ function expandHomePath(input) {
16
+ if (input.startsWith('~')) {
17
+ return path.join(os.homedir(), input.slice(1));
18
+ }
19
+ return input;
20
+ }
21
+
22
+ /**
23
+ * Validate and normalize an output directory path
24
+ * @param {string} inputPath - Path to validate
25
+ * @returns {{valid: boolean, error?: string, resolvedPath?: string}} Validation result
26
+ */
27
+ function validateOutputPath(inputPath) {
28
+ if (!inputPath || inputPath.trim() === '') {
29
+ return { valid: false, error: 'Path cannot be empty' };
30
+ }
31
+
32
+ // Check for null bytes (security)
33
+ if (inputPath.includes('\0')) {
34
+ return { valid: false, error: 'Path contains null bytes' };
35
+ }
36
+
37
+ const expanded = expandHomePath(inputPath);
38
+ const resolved = path.resolve(expanded);
39
+ const normalizedResolved = resolved.toLowerCase();
40
+
41
+ // Block system-sensitive directories
42
+ const sensitiveDirs = ['/etc', '/sys', '/proc', '/root', '/boot'];
43
+ for (const sensitive of sensitiveDirs) {
44
+ if (normalizedResolved.startsWith(sensitive.toLowerCase() + path.sep) ||
45
+ normalizedResolved === sensitive.toLowerCase()) {
46
+ return { valid: false, error: `Cannot create project in system directory: ${sensitive}` };
47
+ }
48
+ }
49
+
50
+ // Don't allow creation inside node_modules
51
+ if (normalizedResolved.includes('node_modules')) {
52
+ return { valid: false, error: 'Cannot create project inside node_modules directory' };
53
+ }
54
+
55
+ return { valid: true, resolvedPath: resolved };
56
+ }
57
+
58
+ /**
59
+ * Recursively copy a directory
60
+ * @param {string} src - Source directory path
61
+ * @param {string} dest - Destination directory path
62
+ */
63
+ function copyDir(src, dest) {
64
+ const entries = fs.readdirSync(src, { withFileTypes: true });
65
+
66
+ for (const entry of entries) {
67
+ const srcPath = path.join(src, entry.name);
68
+ const destPath = path.join(dest, entry.name);
69
+
70
+ if (entry.isDirectory()) {
71
+ fs.mkdirSync(destPath, { recursive: true });
72
+ copyDir(srcPath, destPath);
73
+ } else {
74
+ fs.copyFileSync(srcPath, destPath);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Copy optional template files to destination
81
+ * @param {string} destPath - Destination project path
82
+ */
83
+ function copyOptionalFiles(destPath) {
84
+ const optionalPath = path.join(__dirname, '..', '..', 'template-optional');
85
+
86
+ // Copy examples.md
87
+ const examplesSrc = path.join(optionalPath, 'examples.md');
88
+ const examplesDest = path.join(destPath, 'examples.md');
89
+ if (fs.existsSync(examplesSrc)) {
90
+ fs.copyFileSync(examplesSrc, examplesDest);
91
+ }
92
+
93
+ // Copy demo image
94
+ const demoImageSrc = path.join(optionalPath, 'static', 'demo-image.png');
95
+ const demoImageDest = path.join(destPath, 'static', 'demo-image.png');
96
+ if (fs.existsSync(demoImageSrc)) {
97
+ fs.copyFileSync(demoImageSrc, demoImageDest);
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ expandHomePath,
103
+ validateOutputPath,
104
+ copyDir,
105
+ copyOptionalFiles
106
+ };
@@ -0,0 +1,89 @@
1
+ // cli/utils/prompt-utils.js
2
+ /**
3
+ * Prompt utilities for meta-package CLI commands
4
+ * These functions are NOT copied to generated projects
5
+ */
6
+ const readline = require('readline');
7
+
8
+ /**
9
+ * Ask user if they want to create example slides
10
+ * @returns {Promise<boolean>} True if user wants examples
11
+ */
12
+ async function askCreateExamples() {
13
+ return new Promise((resolve) => {
14
+ // Interactive mode - ask user
15
+ if (process.stdin.isTTY) {
16
+ const rl = readline.createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout
19
+ });
20
+
21
+ rl.question('Create example slides file? (Y/n) ', (answer) => {
22
+ rl.close();
23
+ const normalized = answer.toLowerCase().trim();
24
+ resolve(normalized !== 'n' && normalized !== 'no');
25
+ });
26
+ } else {
27
+ // Non-interactive mode - read from stdin if there's data
28
+ let input = '';
29
+ process.stdin.setEncoding('utf8');
30
+
31
+ process.stdin.on('readable', () => {
32
+ let chunk;
33
+ while ((chunk = process.stdin.read()) !== null) {
34
+ input += chunk;
35
+ }
36
+ });
37
+
38
+ process.stdin.on('end', () => {
39
+ const normalized = input.toLowerCase().trim();
40
+ // If input is empty, create examples by default
41
+ if (normalized === '') {
42
+ resolve(true);
43
+ } else {
44
+ resolve(normalized !== 'n' && normalized !== 'no');
45
+ }
46
+ });
47
+
48
+ // If stdin has no data immediately, finish
49
+ if (process.stdin.readableLength === 0) {
50
+ // Give a short time for data to appear
51
+ setTimeout(() => {
52
+ if (input === '') {
53
+ process.stdin.destroy();
54
+ resolve(true);
55
+ }
56
+ }, 10);
57
+ }
58
+ }
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Ask user if they want to add themes to the project
64
+ * @returns {Promise<boolean>} True if user wants themes
65
+ */
66
+ async function askAddThemes() {
67
+ return new Promise((resolve) => {
68
+ if (process.stdin.isTTY) {
69
+ const rl = readline.createInterface({
70
+ input: process.stdin,
71
+ output: process.stdout
72
+ });
73
+
74
+ rl.question('Add custom themes to the project? (Y/n) ', (answer) => {
75
+ rl.close();
76
+ const normalized = answer.toLowerCase().trim();
77
+ resolve(normalized !== 'n' && normalized !== 'no');
78
+ });
79
+ } else {
80
+ // Non-interactive mode: default to false (don't add themes without consent)
81
+ resolve(false);
82
+ }
83
+ });
84
+ }
85
+
86
+ module.exports = {
87
+ askCreateExamples,
88
+ askAddThemes
89
+ };