create-marp-presentation 1.1.0 → 1.2.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.
@@ -0,0 +1,198 @@
1
+ // lib/vscode-integration.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { VSCodeIntegrationError } = require('./errors');
5
+
6
+ /**
7
+ * DTO for VSCode settings structure
8
+ * Encapsulates the settings format and provides type-safe accessors
9
+ */
10
+ class VSCodeSettingsDTO {
11
+ /**
12
+ * @param {Object} settings - Raw settings object
13
+ */
14
+ constructor(settings = {}) {
15
+ this._settings = settings;
16
+ }
17
+
18
+ /**
19
+ * Get Marp themes array
20
+ * Supports both flat key "markdown.marp.themes" and nested markdown.marp.themes
21
+ * @returns {string[]} Array of theme file paths
22
+ */
23
+ getMarpThemes() {
24
+ // Support both flat key and nested structure for backward compatibility
25
+ if (this._settings['markdown.marp.themes']) {
26
+ return this._settings['markdown.marp.themes'];
27
+ }
28
+ return this._settings.markdown?.marp?.themes || [];
29
+ }
30
+
31
+ /**
32
+ * Set Marp themes array
33
+ * Always uses flat key format for VSCode
34
+ * @param {string[]} themes - Array of theme file paths
35
+ */
36
+ setMarpThemes(themes) {
37
+ // Always use flat key format for VSCode
38
+ this._settings['markdown.marp.themes'] = themes;
39
+ }
40
+
41
+ /**
42
+ * Merge another settings object into this one
43
+ * Combines themes without duplicates
44
+ * @param {VSCodeSettingsDTO} other - Other settings to merge
45
+ */
46
+ merge(other) {
47
+ const otherThemes = other.getMarpThemes();
48
+ const currentThemes = this.getMarpThemes();
49
+ const merged = [...new Set([...currentThemes, ...otherThemes])];
50
+ this.setMarpThemes(merged);
51
+ }
52
+
53
+ /**
54
+ * Convert to plain object for JSON serialization
55
+ * @returns {Object} Plain settings object
56
+ */
57
+ toObject() {
58
+ return { ...this._settings };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Manages VSCode settings integration for Marp themes
64
+ * Handles .vscode/settings.json for markdown.marp.themes configuration
65
+ */
66
+ class VSCodeIntegration {
67
+ /**
68
+ * @param {string} projectPath - Path to the project root
69
+ */
70
+ constructor(projectPath) {
71
+ if (!projectPath) {
72
+ throw new VSCodeIntegrationError('Project path is required');
73
+ }
74
+ this.projectPath = projectPath;
75
+ }
76
+
77
+ /**
78
+ * Returns the path to .vscode/settings.json
79
+ * @returns {string} Absolute path to settings.json
80
+ */
81
+ getSettingsPath() {
82
+ return path.join(this.projectPath, '.vscode', 'settings.json');
83
+ }
84
+
85
+ /**
86
+ * Reads settings.json from .vscode directory
87
+ * Returns empty object if file doesn't exist
88
+ * Creates backup of corrupted JSON files
89
+ *
90
+ * @returns {Object} Parsed settings object
91
+ * @throws {VSCodeIntegrationError} If backup creation fails
92
+ */
93
+ readSettings() {
94
+ const settingsPath = this.getSettingsPath();
95
+
96
+ // Return empty object if file doesn't exist
97
+ if (!fs.existsSync(settingsPath)) {
98
+ return {};
99
+ }
100
+
101
+ try {
102
+ const content = fs.readFileSync(settingsPath, 'utf-8');
103
+ const settings = JSON.parse(content);
104
+ return settings;
105
+ } catch (error) {
106
+ if (error instanceof SyntaxError) {
107
+ // Create backup of corrupted file
108
+ this._backupCorruptedSettings(settingsPath);
109
+ return {};
110
+ }
111
+ throw new VSCodeIntegrationError(`Failed to read settings: ${error.message}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Writes settings to .vscode/settings.json
117
+ * Creates .vscode directory if it doesn't exist
118
+ *
119
+ * @param {Object} settings - Settings object to write
120
+ * @throws {VSCodeIntegrationError} If directory creation or write fails
121
+ */
122
+ writeSettings(settings) {
123
+ const vscodeDir = path.join(this.projectPath, '.vscode');
124
+ const settingsPath = this.getSettingsPath();
125
+
126
+ try {
127
+ // Create .vscode directory if it doesn't exist
128
+ if (!fs.existsSync(vscodeDir)) {
129
+ fs.mkdirSync(vscodeDir, { recursive: true });
130
+ }
131
+
132
+ // Write settings with pretty formatting (2-space indent)
133
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
134
+ } catch (error) {
135
+ throw new VSCodeIntegrationError(`Failed to write settings: ${error.message}`);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Creates a new DTO instance from current settings
141
+ * @returns {VSCodeSettingsDTO} DTO instance
142
+ */
143
+ createSettingsDTO() {
144
+ const settings = this.readSettings();
145
+ return new VSCodeSettingsDTO(settings);
146
+ }
147
+
148
+ /**
149
+ * Syncs Marp themes to VSCode settings
150
+ * Updates markdown.marp.themes array in settings.json
151
+ * Preserves existing settings while updating theme list
152
+ *
153
+ * @param {string[]} themes - Array of theme file paths
154
+ * @throws {VSCodeIntegrationError} If settings read/write fails
155
+ */
156
+ syncThemes(themes) {
157
+ const dto = this.createSettingsDTO();
158
+ dto.setMarpThemes(themes);
159
+ this.writeSettings(dto.toObject());
160
+ }
161
+
162
+ /**
163
+ * Reads and returns settings as DTO
164
+ * @returns {VSCodeSettingsDTO} DTO instance
165
+ */
166
+ readSettingsAsDTO() {
167
+ const settings = this.readSettings();
168
+ return new VSCodeSettingsDTO(settings);
169
+ }
170
+
171
+ /**
172
+ * Creates a backup of corrupted settings.json
173
+ * Backup filename: settings.json.corrupted.YYYYMMDD-HHMMSS
174
+ *
175
+ * @private
176
+ * @param {string} settingsPath - Path to corrupted settings file
177
+ * @throws {VSCodeIntegrationError} If backup creation fails
178
+ */
179
+ _backupCorruptedSettings(settingsPath) {
180
+ try {
181
+ const timestamp = new Date()
182
+ .toISOString()
183
+ .replace(/[:.]/g, '-')
184
+ .replace('T', '-')
185
+ .slice(0, 19);
186
+ const backupPath = `${settingsPath}.corrupted.${timestamp}`;
187
+
188
+ fs.copyFileSync(settingsPath, backupPath);
189
+ console.warn(`Corrupted settings.json backed up to: ${backupPath}`);
190
+ } catch (error) {
191
+ throw new VSCodeIntegrationError(
192
+ `Failed to create backup of corrupted settings: ${error.message}`
193
+ );
194
+ }
195
+ }
196
+ }
197
+
198
+ module.exports = { VSCodeIntegration, VSCodeSettingsDTO };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "create-marp-presentation",
3
- "version": "1.1.0",
4
- "description": "Create a new Marp presentation project with one command",
3
+ "version": "1.2.0",
4
+ "description": "Create a new Marp presentation project with one command. Manage themes quickly and easily with the CLI.",
5
5
  "bin": {
6
6
  "create-marp-presentation": "index.js"
7
7
  },
8
8
  "files": [
9
9
  "index.js",
10
+ "lib/",
10
11
  "template/",
11
- "template-optional/"
12
+ "template-optional/",
13
+ "themes/"
12
14
  ],
13
15
  "scripts": {
14
16
  "test": "jest"
@@ -38,10 +40,16 @@
38
40
  "testEnvironment": "node",
39
41
  "testMatch": [
40
42
  "**/tests/**/*.test.js"
41
- ]
43
+ ],
44
+ "maxWorkers": 1,
45
+ "setupFilesAfterEnv": ["<rootDir>/jest.setup.js"]
42
46
  },
43
- "devDependencies": {
47
+ "dependencies": {
48
+ "@inquirer/prompts": "^7.10.1",
44
49
  "fast-glob": "^3.3.3",
50
+ "gray-matter": "^4.0.3"
51
+ },
52
+ "devDependencies": {
45
53
  "jest": "^29.7.0"
46
54
  }
47
55
  }
@@ -2,36 +2,47 @@
2
2
 
3
3
  A template for creating presentations with Marp.
4
4
 
5
- ## Getting Started
6
-
7
- ### Live Preview
5
+ ## Quick Start
8
6
 
9
7
  ```bash
10
8
  npm run dev
11
9
  ```
12
10
 
13
- Opens a browser with auto-reload on changes.
11
+ Opens a browser with live preview and auto-reload on changes.
12
+
13
+ Edit `presentation.md` to create your slides.
14
14
 
15
- ### Creating Slides
15
+ ## Building
16
+
17
+ ```bash
18
+ npm run build:html # HTML presentation (interactive)
19
+ npm run build:pdf # PDF file
20
+ npm run build:pptx # PowerPoint
21
+ npm run build:all # All formats at once
22
+ ```
16
23
 
17
- Edit `presentation.md` this is the main presentation file.
24
+ The output appears in the `output/` folder.
18
25
 
19
- ## Building the Presentation
26
+ ## Theme Management
20
27
 
21
28
  ```bash
22
- npm run build:html # HTML presentation (interactive)
23
- npm run build:pdf # PDF file
24
- npm run build:pptx # PowerPoint
25
- npm run build:all # All formats at once
29
+ npm run theme:add # Add themes from library
30
+ npm run theme:list # List available/installed themes
31
+ npm run theme:set <name> # Set active theme
32
+ npm run theme:create # Create custom theme
26
33
  ```
27
34
 
28
- The output will appear in the `output/` folder.
35
+ For detailed theme management guide, see [docs/theme-management.md](docs/theme-management.md).
29
36
 
30
- ## Static Files
37
+ ## VSCode Setup
31
38
 
32
- Place images and other files in the folder specified in `marp.config.js`.
39
+ 1. Install [Marp for VSCode](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode)
40
+ 2. Open this project in VSCode
41
+ 3. Click "Marp: Open Preview" in the editor toolbar
42
+
43
+ ## Static Files
33
44
 
34
- Default: `static/`
45
+ Place images and other files in `static/` folder.
35
46
 
36
47
  You can add additional folders in `marp.config.js`:
37
48
 
@@ -42,7 +53,7 @@ module.exports = {
42
53
  };
43
54
  ```
44
55
 
45
- ## Cleaning Up
56
+ ## Cleaning
46
57
 
47
58
  ```bash
48
59
  npm run clean
@@ -54,4 +65,4 @@ Removes the `output/` folder.
54
65
 
55
66
  - [Marp Documentation](https://marp.app/)
56
67
  - [Marp CLI](https://github.com/marp-team/marp-cli)
57
- - [Markdown Guide](https://www.markdownguide.org/)
68
+ - [Theme Management Guide](docs/theme-management.md)
@@ -9,7 +9,16 @@
9
9
  "build:pptx": "marp presentation.md -o output/presentation.pptx --allow-local-files && npm run copy:static",
10
10
  "build:all": "npm run build:html && npm run build:pdf && npm run build:pptx",
11
11
  "copy:static": "node scripts/copy-static.js",
12
- "clean": "rimraf output"
12
+ "clean": "rimraf output",
13
+ "theme": "node scripts/theme-cli.js",
14
+ "theme:add": "npx create-marp-presentation theme:add .",
15
+ "theme:list": "node scripts/theme-cli.js list",
16
+ "theme:create": "node scripts/theme-cli.js create",
17
+ "theme:set": "node scripts/theme-cli.js set"
18
+ },
19
+ "dependencies": {
20
+ "@inquirer/prompts": "^7.10.1",
21
+ "gray-matter": "^4.0.3"
13
22
  },
14
23
  "devDependencies": {
15
24
  "@marp-team/marp-cli": "^4.1.2",
@@ -18,5 +27,8 @@
18
27
  },
19
28
  "engines": {
20
29
  "node": ">=20.0.0"
30
+ },
31
+ "marp": {
32
+ "themeSet": "./themes"
21
33
  }
22
34
  }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * theme-cli.js - CLI for managing Marp themes in a project
5
+ *
6
+ * This script provides commands to:
7
+ * - List installed themes
8
+ * - Create new themes
9
+ * - Update presentation.md with selected theme
10
+ * - Sync themes to VSCode settings
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // Import lib modules
17
+ const {
18
+ ThemeResolver,
19
+ Theme
20
+ } = require('./lib/theme-resolver');
21
+ const { ThemeManager } = require('./lib/theme-manager');
22
+ const { Prompts } = require('./lib/prompts');
23
+ const { Frontmatter } = require('./lib/frontmatter');
24
+ const { VSCodeIntegration } = require('./lib/vscode-integration');
25
+ const {
26
+ ThemeError,
27
+ ThemeNotFoundError,
28
+ ThemeAlreadyExistsError,
29
+ PresentationNotFoundError
30
+ } = require('./lib/errors');
31
+
32
+ // Get command from arguments
33
+ const command = process.argv[2] || 'help';
34
+ const args = process.argv.slice(3);
35
+
36
+ // Paths
37
+ const projectRoot = process.cwd();
38
+ const templatePath = path.join(__dirname, '..');
39
+
40
+ /**
41
+ * Show help message
42
+ */
43
+ function showHelp() {
44
+ console.log(`
45
+ Marp Theme CLI
46
+
47
+ Usage:
48
+ npm run theme <command> [options]
49
+
50
+ Commands:
51
+ list List installed themes
52
+ create <name> Create a new theme
53
+ set <theme> Set active theme in presentation.md
54
+ switch <theme> Alias for 'set' - change active theme
55
+ sync Sync installed themes to VSCode settings
56
+ help Show this help message
57
+
58
+ Options:
59
+ --force Overwrite existing themes (for theme:add)
60
+
61
+ Examples:
62
+ npm run theme list
63
+ npm run theme create my-theme
64
+ npm run theme set beam
65
+ npm run theme sync
66
+
67
+ Note:
68
+ Run "npm run theme:add" to add themes from the theme library.
69
+ `);
70
+ }
71
+
72
+ /**
73
+ * List installed themes
74
+ */
75
+ async function listThemes() {
76
+ console.log('\n=== Installed Themes ===\n');
77
+
78
+ const projectThemesPath = path.join(projectRoot, 'themes');
79
+ let installedThemes = [];
80
+
81
+ if (fs.existsSync(projectThemesPath)) {
82
+ try {
83
+ installedThemes = ThemeResolver.scanDirectory(projectThemesPath);
84
+ } catch (error) {
85
+ // Directory exists but no themes
86
+ }
87
+ }
88
+
89
+ if (installedThemes.length === 0) {
90
+ console.log(' No themes installed.');
91
+ console.log(' Run "npm run theme:add" to install themes.\n');
92
+ } else {
93
+ for (const theme of installedThemes) {
94
+ const deps = theme.dependencies.length > 0
95
+ ? ` (depends on: ${theme.dependencies.join(', ')})`
96
+ : '';
97
+ console.log(` ${theme.name}${deps}`);
98
+ }
99
+ console.log();
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Create a new theme
105
+ */
106
+ async function createTheme(themeName) {
107
+ if (!themeName) {
108
+ console.error('\nError: Theme name is required.\n');
109
+ console.log('Usage: npm run theme create <theme-name>\n');
110
+ process.exit(1);
111
+ }
112
+
113
+ const manager = new ThemeManager(projectRoot);
114
+
115
+ try {
116
+ console.log(`\nCreating theme: ${themeName}\n`);
117
+
118
+ // Prompt for parent theme - need objects with isSystem property
119
+ const scannedThemes = manager.scanThemes();
120
+ const systemThemeObjects = ThemeManager.SYSTEM_THEMES.map(name => ({
121
+ name,
122
+ isSystem: true
123
+ }));
124
+ const allThemes = [...scannedThemes, ...systemThemeObjects];
125
+ const parentTheme = await Prompts.promptParentTheme(allThemes);
126
+
127
+ // Prompt for directory location
128
+ const existingDirs = manager.listDirectories();
129
+ const location = await Prompts.promptDirectoryLocation(existingDirs);
130
+
131
+ let newFolderName = null;
132
+ if (location === 'new') {
133
+ newFolderName = await Prompts.promptNewFolderName();
134
+ }
135
+
136
+ const result = manager.createTheme(
137
+ themeName,
138
+ parentTheme,
139
+ location,
140
+ newFolderName
141
+ );
142
+
143
+ console.log(`\n✓ Theme created: ${result.path}\n`);
144
+ console.log('Next steps:');
145
+ console.log(` 1. Edit ${result.path} to customize the theme`);
146
+ console.log(` 2. Run "npm run theme set ${themeName}" to use it in your presentation\n`);
147
+ } catch (error) {
148
+ console.error(`\nError: ${error.message}\n`);
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Sync VSCode settings with installed themes
155
+ */
156
+ async function syncThemes() {
157
+ const projectThemesPath = path.join(projectRoot, 'themes');
158
+ let installedThemes = [];
159
+
160
+ if (fs.existsSync(projectThemesPath)) {
161
+ try {
162
+ installedThemes = ThemeResolver.scanDirectory(projectThemesPath);
163
+ } catch (error) {
164
+ // Directory exists but no themes
165
+ }
166
+ }
167
+
168
+ if (installedThemes.length === 0) {
169
+ console.log('\nNo themes installed to sync.\n');
170
+ return;
171
+ }
172
+
173
+ // Build theme paths for VSCode
174
+ const themePaths = installedThemes.map(theme => {
175
+ // For themes in subdirectories, use themes/subdir/theme.css
176
+ // For themes at root level, use themes/theme.css
177
+ if (theme.path.includes(`${path.sep}themes${path.sep}`)) {
178
+ const relativePath = theme.path.split(`${path.sep}themes${path.sep}`)[1];
179
+ return `themes/${relativePath}`;
180
+ }
181
+ return `themes/${theme.name}.css`;
182
+ });
183
+
184
+ // Sync with VSCode
185
+ const vscode = new VSCodeIntegration(projectRoot);
186
+ vscode.syncThemes(themePaths);
187
+
188
+ console.log(`\n✓ Synced ${themePaths.length} theme(s) to VSCode settings:`);
189
+ themePaths.forEach(p => console.log(` - ${p}`));
190
+ console.log();
191
+ }
192
+
193
+ /**
194
+ * Main CLI entry point
195
+ */
196
+ async function main() {
197
+ switch (command) {
198
+ case 'list':
199
+ await listThemes();
200
+ break;
201
+
202
+ case 'create':
203
+ await createTheme(args[0]);
204
+ break;
205
+
206
+ case 'set':
207
+ case 'switch':
208
+ try {
209
+ const themeManager = new ThemeManager(projectRoot);
210
+ themeManager.setActiveTheme(args[0]);
211
+ console.log(`\n✓ Theme set to "${args[0]}" in presentation.md`);
212
+
213
+ // VSCode integration - sync ALL themes in project
214
+ themeManager.updateVSCodeSettings();
215
+
216
+ console.log('Next steps:');
217
+ console.log(' npm run dev # Start live preview\n');
218
+ } catch (error) {
219
+ if (error.name === 'ThemeNotFoundError') {
220
+ console.error(`\nError: ${error.message}\n`);
221
+ console.log('Run "npm run theme list" to see available themes.\n');
222
+ process.exit(1);
223
+ }
224
+ if (error.name === 'PresentationNotFoundError') {
225
+ console.error(`\nError: presentation.md not found in ${projectRoot}\n`);
226
+ process.exit(1);
227
+ }
228
+ console.error(`\nError: ${error.message}\n`);
229
+ process.exit(1);
230
+ }
231
+ break;
232
+
233
+ case 'sync':
234
+ await syncThemes();
235
+ break;
236
+
237
+ case 'help':
238
+ default:
239
+ showHelp();
240
+ break;
241
+ }
242
+ }
243
+
244
+ // Run CLI
245
+ main().catch(error => {
246
+ console.error(`\nUnexpected error: ${error.message}\n`);
247
+ process.exit(1);
248
+ });