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/lib/prompts.js ADDED
@@ -0,0 +1,222 @@
1
+ // lib/prompts.js
2
+ const inquirer = require('@inquirer/prompts');
3
+ const { THEME_NOT_FOUND } = require('./errors');
4
+
5
+ /**
6
+ * Interactive prompts for theme management
7
+ */
8
+ class Prompts {
9
+ /**
10
+ * Prompt user to select themes from available options
11
+ *
12
+ * @param {Array} availableThemes - Array of {name, description} objects
13
+ * @returns {Promise<string[]>} Array of selected theme names
14
+ */
15
+ static async promptThemes(availableThemes) {
16
+ if (availableThemes.length === 0) {
17
+ return [];
18
+ }
19
+
20
+ const choices = availableThemes.map(theme => ({
21
+ name: theme.name,
22
+ value: theme.name,
23
+ checked: false
24
+ }));
25
+
26
+ return await inquirer.checkbox({
27
+ message: 'Select themes to add:',
28
+ choices
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Prompt user to select active theme from available themes
34
+ *
35
+ * @param {string[]} selectedThemes - Array of available theme names
36
+ * @returns {Promise<string>} Selected theme name
37
+ * @throws {Error} If no themes available
38
+ */
39
+ static async promptActiveTheme(selectedThemes) {
40
+ if (selectedThemes.length === 0) {
41
+ throw new Error('No themes available');
42
+ }
43
+
44
+ // Return single theme if only one available
45
+ if (selectedThemes.length === 1) {
46
+ return selectedThemes[0];
47
+ }
48
+
49
+ return await inquirer.select({
50
+ message: 'Select active theme:',
51
+ choices: selectedThemes
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Prompt user for new theme name with validation
57
+ *
58
+ * @returns {Promise<string>} Validated theme name
59
+ */
60
+ static async promptNewThemeName() {
61
+ return await inquirer.input({
62
+ message: 'Theme name:',
63
+ validate: (input) => {
64
+ if (!input || input.trim().length === 0) {
65
+ return 'Theme name is required';
66
+ }
67
+ if (!/^[a-z0-9-]+$/.test(input)) {
68
+ return 'Theme name must contain only lowercase letters, numbers, and hyphens';
69
+ }
70
+ return true;
71
+ }
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Prompt user to select parent theme
77
+ *
78
+ * @param {Array} existingThemes - Array of {name, isSystem} objects
79
+ * @returns {Promise<string|null>} Selected parent theme name or null for none
80
+ */
81
+ static async promptParentTheme(existingThemes) {
82
+ const choices = [
83
+ { name: 'none (create from scratch)', value: null },
84
+ new inquirer.Separator('--- System Themes ---'),
85
+ { name: 'default (system built-in)', value: 'default' },
86
+ { name: 'gaia (system built-in)', value: 'gaia' },
87
+ { name: 'uncover (system built-in)', value: 'uncover' }
88
+ ];
89
+
90
+ // Add custom themes
91
+ const customThemes = existingThemes.filter(t => !t.isSystem);
92
+ if (customThemes.length > 0) {
93
+ choices.push(new inquirer.Separator('--- Custom Themes ---'));
94
+ customThemes.forEach(theme => {
95
+ choices.push({ name: theme.name, value: theme.name });
96
+ });
97
+ }
98
+
99
+ return await inquirer.select({
100
+ message: 'Parent theme:',
101
+ choices
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Prompt user for directory location for new theme
107
+ *
108
+ * @param {Array} existingDirs - Array of existing directory names
109
+ * @returns {Promise<string>} Selected option: 'root', 'existing', or 'new'
110
+ */
111
+ static async promptDirectoryLocation(existingDirs) {
112
+ const choices = [
113
+ { name: 'In root (themes/<name>.css)', value: 'root' }
114
+ ];
115
+
116
+ if (existingDirs.length > 0) {
117
+ choices.push(new inquirer.Separator('--- Existing Folders ---'));
118
+ existingDirs.forEach(dir => {
119
+ choices.push({
120
+ name: `In existing folder: themes/${dir}/`,
121
+ value: dir
122
+ });
123
+ });
124
+ }
125
+
126
+ choices.push(new inquirer.Separator('--- New Folder ---'));
127
+ choices.push({ name: 'In new folder (enter name)', value: 'new' });
128
+
129
+ return await inquirer.select({
130
+ message: 'Where to create the theme CSS file?',
131
+ choices
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Prompt user for new folder name
137
+ *
138
+ * @returns {Promise<string>} Folder name
139
+ */
140
+ static async promptNewFolderName() {
141
+ return await inquirer.input({
142
+ message: 'Folder name:',
143
+ validate: (input) => {
144
+ if (!input || input.trim().length === 0) {
145
+ return 'Folder name is required';
146
+ }
147
+ if (!/^[a-z0-9-]+$/.test(input)) {
148
+ return 'Folder name must contain only lowercase letters, numbers, and hyphens';
149
+ }
150
+ return true;
151
+ }
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Prompt user for conflict resolution
157
+ *
158
+ * @param {Array} conflicts - Array of conflicting theme names
159
+ * @returns {Promise<string>} Selected action: 'skip', 'overwrite', 'skip-all', 'overwrite-all', or 'cancel'
160
+ */
161
+ static async promptConflictResolution(conflicts) {
162
+ const isMultiple = conflicts.length > 1;
163
+
164
+ if (isMultiple) {
165
+ const conflictList = conflicts.map(c => ` - ${c.name}`).join('\n');
166
+ console.log(`\n${conflicts.length} themes already exist in your project:\n${conflictList}\n`);
167
+
168
+ return await inquirer.select({
169
+ message: 'Apply to all conflicts?',
170
+ choices: [
171
+ { name: 'Skip all', value: 'skip-all' },
172
+ { name: 'Overwrite all', value: 'overwrite-all' },
173
+ { name: 'Choose for each', value: 'choose-each' },
174
+ { name: 'Cancel', value: 'cancel' }
175
+ ]
176
+ });
177
+ }
178
+
179
+ const conflict = conflicts[0];
180
+ return await inquirer.select({
181
+ message: `Theme "${conflict.name}" already exists. What would you like to do?`,
182
+ choices: [
183
+ { name: 'Skip (keep existing)', value: 'skip' },
184
+ { name: 'Overwrite (replace with template version)', value: 'overwrite' },
185
+ { name: 'Cancel (stop adding themes)', value: 'cancel' }
186
+ ]
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Prompt user for single theme conflict resolution
192
+ *
193
+ * @param {string} themeName - Name of conflicting theme
194
+ * @returns {Promise<string>} Selected action: 'skip', 'overwrite', or 'cancel'
195
+ */
196
+ static async promptSingleConflict(themeName) {
197
+ return await inquirer.select({
198
+ message: `Theme "${themeName}" already exists. What would you like to do?`,
199
+ choices: [
200
+ { name: 'Skip (keep existing)', value: 'skip' },
201
+ { name: 'Overwrite (replace with template version)', value: 'overwrite' },
202
+ { name: 'Cancel (stop adding themes)', value: 'cancel' }
203
+ ]
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Confirm action with user
209
+ *
210
+ * @param {string} message - Confirmation message
211
+ * @param {boolean} defaultValue - Default value (true: yes, false: no)
212
+ * @returns {Promise<boolean>} User's choice
213
+ */
214
+ static async confirm(message, defaultValue = true) {
215
+ return await inquirer.confirm({
216
+ message,
217
+ default: defaultValue
218
+ });
219
+ }
220
+ }
221
+
222
+ module.exports = { Prompts };
@@ -0,0 +1,238 @@
1
+ // lib/theme-manager.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { ThemeResolver } = require('./theme-resolver');
5
+ const { VSCodeIntegration } = require('./vscode-integration');
6
+ const { Frontmatter } = require('./frontmatter');
7
+ const {
8
+ ThemeNotFoundError,
9
+ ThemeAlreadyExistsError,
10
+ PresentationNotFoundError
11
+ } = require('./errors');
12
+
13
+ /**
14
+ * Main class for theme operations
15
+ * Manages Marp themes in a project, including scanning, creating, and updating themes
16
+ */
17
+ class ThemeManager {
18
+ // System themes that are always available
19
+ static SYSTEM_THEMES = ['default', 'gaia', 'uncover'];
20
+
21
+ /**
22
+ * Ensure marp.themeSet is configured in package.json
23
+ * This is a static utility that can be called without instantiating ThemeManager
24
+ *
25
+ * @param {string} projectPath - Path to the project directory
26
+ * @param {Object} options - Optional configuration
27
+ * @param {boolean} options.silent - If true, don't log messages (default: false)
28
+ * @returns {boolean} - True if configuration was added, false if already present or error occurred
29
+ */
30
+ static ensureThemeSetConfig(projectPath, options = {}) {
31
+ const { silent = false } = options;
32
+ const pkgPath = path.join(projectPath, 'package.json');
33
+
34
+ if (!fs.existsSync(pkgPath)) {
35
+ if (!silent) console.warn(' Warning: package.json not found');
36
+ return false;
37
+ }
38
+
39
+ try {
40
+ const pkgContent = fs.readFileSync(pkgPath, 'utf-8');
41
+ const pkg = JSON.parse(pkgContent);
42
+
43
+ // Check if marp.themeSet is already configured
44
+ if (pkg.marp && pkg.marp.themeSet) {
45
+ return false; // Already configured
46
+ }
47
+
48
+ // Add marp.themeSet configuration
49
+ if (!pkg.marp) {
50
+ pkg.marp = {};
51
+ }
52
+ pkg.marp.themeSet = './themes';
53
+
54
+ // Write back to package.json with proper formatting
55
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
56
+
57
+ if (!silent) {
58
+ console.log('✓ Added marp.themeSet configuration to package.json');
59
+ }
60
+ return true; // Configuration was added
61
+ } catch (error) {
62
+ if (!silent) {
63
+ console.warn(` Warning: Could not ensure marp.themeSet: ${error.message}`);
64
+ }
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Create a new ThemeManager instance
71
+ *
72
+ * @param {string} projectPath - Path to the project root
73
+ */
74
+ constructor(projectPath) {
75
+ if (!projectPath) {
76
+ throw new Error('Project path is required');
77
+ }
78
+ this.projectPath = projectPath;
79
+ this.themesPath = path.join(projectPath, 'themes');
80
+ this.presentationPath = path.join(projectPath, 'presentation.md');
81
+ }
82
+
83
+ /**
84
+ * Scan themes in project
85
+ * Delegates to ThemeResolver.scanDirectory
86
+ *
87
+ * @returns {Theme[]} Array of Theme objects found in themes directory
88
+ */
89
+ scanThemes() {
90
+ return ThemeResolver.scanDirectory(this.themesPath);
91
+ }
92
+
93
+ /**
94
+ * List theme names (for theme-cli.js compatibility)
95
+ *
96
+ * @returns {string[]} Array of theme names including system themes
97
+ */
98
+ listThemes() {
99
+ const themes = this.scanThemes().map(t => t.name);
100
+ return [...themes, ...ThemeManager.SYSTEM_THEMES];
101
+ }
102
+
103
+ /**
104
+ * List theme directory names (for theme-cli.js compatibility)
105
+ *
106
+ * @returns {string[]} Array of directory names in themes folder
107
+ */
108
+ listDirectories() {
109
+ if (!fs.existsSync(this.themesPath)) {
110
+ return [];
111
+ }
112
+
113
+ const entries = fs.readdirSync(this.themesPath, { withFileTypes: true });
114
+ return entries
115
+ .filter(entry => entry.isDirectory())
116
+ .map(entry => entry.name);
117
+ }
118
+
119
+ /**
120
+ * Get theme by name
121
+ *
122
+ * @param {string} name - Theme name
123
+ * @returns {Theme|null} Theme object or null if not found
124
+ */
125
+ getTheme(name) {
126
+ const themes = this.scanThemes();
127
+ return themes.find((theme) => theme.name === name) || null;
128
+ }
129
+
130
+ /**
131
+ * Get active theme from presentation.md
132
+ *
133
+ * @returns {string|null} Theme name or null if not found
134
+ */
135
+ getActiveTheme() {
136
+ if (!fs.existsSync(this.presentationPath)) {
137
+ return null;
138
+ }
139
+
140
+ const content = fs.readFileSync(this.presentationPath, 'utf-8');
141
+ return Frontmatter.getTheme(content);
142
+ }
143
+
144
+ /**
145
+ * Set active theme in presentation.md
146
+ *
147
+ * @param {string} themeName - Theme name to set
148
+ * @throws {ThemeNotFoundError} If theme does not exist
149
+ * @throws {PresentationNotFoundError} If presentation.md does not exist
150
+ */
151
+ setActiveTheme(themeName) {
152
+ if (!fs.existsSync(this.presentationPath)) {
153
+ throw new PresentationNotFoundError(this.presentationPath);
154
+ }
155
+
156
+ // Validate theme exists
157
+ const availableThemes = this.scanThemes().map((t) => t.name);
158
+ const allThemes = [...availableThemes, ...ThemeManager.SYSTEM_THEMES];
159
+
160
+ if (!allThemes.includes(themeName)) {
161
+ throw new ThemeNotFoundError(themeName);
162
+ }
163
+
164
+ const content = fs.readFileSync(this.presentationPath, 'utf-8');
165
+ const updated = Frontmatter.setTheme(content, themeName);
166
+ Frontmatter.writeToFile(this.presentationPath, updated);
167
+ }
168
+
169
+ /**
170
+ * Create new theme
171
+ *
172
+ * @param {string} name - Theme name
173
+ * @param {string|null} parent - Parent theme name or null
174
+ * @param {string} location - 'root', 'existing' folder name, or 'new'
175
+ * @param {string} newFolderName - Required if location is 'new'
176
+ * @throws {ThemeAlreadyExistsError} If theme with same name already exists
177
+ * @throws {Error} If location is 'new' but newFolderName not provided
178
+ */
179
+ createTheme(name, parent, location, newFolderName = null) {
180
+ // Check for duplicate
181
+ if (this.getTheme(name)) {
182
+ throw new ThemeAlreadyExistsError(name);
183
+ }
184
+
185
+ let cssPath;
186
+ if (location === 'root') {
187
+ cssPath = path.join(this.themesPath, `${name}.css`);
188
+ } else if (location === 'new') {
189
+ if (!newFolderName) {
190
+ throw new Error('newFolderName required when location is "new"');
191
+ }
192
+ const folderPath = path.join(this.themesPath, newFolderName);
193
+ fs.mkdirSync(folderPath, { recursive: true });
194
+ cssPath = path.join(folderPath, `${name}.css`);
195
+ } else {
196
+ // Existing folder
197
+ cssPath = path.join(this.themesPath, location, `${name}.css`);
198
+ }
199
+
200
+ // Generate CSS content
201
+ let css = `/* @theme ${name} */\n\n`;
202
+
203
+ if (parent) {
204
+ css += `@import "${parent}";\n\n`;
205
+ }
206
+
207
+ css += `:root {\n /* Your theme variables */\n}\n`;
208
+
209
+ fs.writeFileSync(cssPath, css, 'utf-8');
210
+
211
+ // Update VSCode settings
212
+ this.updateVSCodeSettings();
213
+
214
+ return { path: cssPath };
215
+ }
216
+
217
+ /**
218
+ * Update VSCode settings with current themes
219
+ * Syncs all discovered themes plus system themes to .vscode/settings.json
220
+ */
221
+ updateVSCodeSettings() {
222
+ const themes = this.scanThemes();
223
+ const vscode = new VSCodeIntegration(this.projectPath);
224
+
225
+ // Convert theme objects to relative paths for VSCode
226
+ const themePaths = themes.map((theme) => {
227
+ return path.relative(this.projectPath, theme.path);
228
+ });
229
+
230
+ // Add system theme paths for VSCode (system themes use flat structure)
231
+ const systemThemePaths = ThemeManager.SYSTEM_THEMES.map(name => `themes/${name}.css`);
232
+ const allThemePaths = [...themePaths, ...systemThemePaths];
233
+
234
+ vscode.syncThemes(allThemePaths);
235
+ }
236
+ }
237
+
238
+ module.exports = { ThemeManager };
@@ -0,0 +1,227 @@
1
+ // lib/theme-resolver.js
2
+ const fs = require('fs');
3
+ const { readdirSync } = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Represents a Marp theme
8
+ */
9
+ class Theme {
10
+ constructor(name, cssPath, css, dependencies = []) {
11
+ this.name = name;
12
+ this.path = cssPath;
13
+ this.css = css;
14
+ this.dependencies = dependencies;
15
+ this.isSystem = ['default', 'gaia', 'uncover'].includes(name);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Resolves theme information from CSS files
21
+ */
22
+ class ThemeResolver {
23
+ static SYSTEM_THEMES = ['default', 'gaia', 'uncover'];
24
+
25
+ /**
26
+ * Extract theme name from CSS comment directive
27
+ * Pattern: /* @theme name *\/
28
+ *
29
+ * @param {string} cssContent - CSS file content
30
+ * @param {string} fallbackFilename - Filename to use as fallback
31
+ * @returns {string|null} Theme name or null if not found
32
+ */
33
+ static extractThemeName(cssContent, fallbackFilename = null) {
34
+ // Match /* @theme name */ - supports multi-line comments
35
+ // The [\s\S]*? allows matching across newlines (non-greedy)
36
+ const themeDirectiveRegex = /\/\*[\s\S]*?@theme\s+([\w-]+)[\s\S]*?\*\//;
37
+ const match = cssContent.match(themeDirectiveRegex);
38
+
39
+ if (match && match[1]) {
40
+ return match[1].trim();
41
+ }
42
+
43
+ // Fallback to filename (with or without .css extension)
44
+ if (fallbackFilename) {
45
+ const basename = path.basename(fallbackFilename, '.css');
46
+ return basename;
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Extract @import dependencies from CSS
54
+ * Ignores url() imports
55
+ *
56
+ * @param {string} cssContent - CSS file content
57
+ * @returns {string[]} Array of theme names from @import statements
58
+ */
59
+ static extractDependencies(cssContent) {
60
+ // Match @import "theme" or @import 'theme' - exclude url() imports using negative lookahead
61
+ const importRegex = /@import\s+(?!url\()['"]([^'"]+)['"]\s*;/g;
62
+ const dependencies = [];
63
+
64
+ let match;
65
+ while ((match = importRegex.exec(cssContent)) !== null) {
66
+ const importPath = match[1];
67
+
68
+ // Skip URL imports (data:, http:, https:, etc.)
69
+ if (importPath.includes('://') || importPath.startsWith('data:')) {
70
+ continue;
71
+ }
72
+
73
+ // Skip if it looks like inline CSS content
74
+ if (importPath.includes('{') || importPath.includes('}')) {
75
+ continue;
76
+ }
77
+
78
+ dependencies.push(importPath);
79
+ }
80
+
81
+ return dependencies;
82
+ }
83
+
84
+ /**
85
+ * Resolve theme from a CSS file
86
+ *
87
+ * @param {string} cssPath - Path to CSS file
88
+ * @returns {Theme} Theme object
89
+ * @throws {Error} If file does not exist
90
+ */
91
+ static resolveTheme(cssPath) {
92
+ if (!fs.existsSync(cssPath)) {
93
+ throw new Error(`CSS file not found: ${cssPath}`);
94
+ }
95
+
96
+ const css = fs.readFileSync(cssPath, 'utf-8');
97
+ const filename = path.basename(cssPath);
98
+ const name = this.extractThemeName(css, filename) || filename;
99
+ const dependencies = this.extractDependencies(css);
100
+
101
+ return new Theme(name, cssPath, css, dependencies);
102
+ }
103
+
104
+ /**
105
+ * Scan directory recursively for CSS theme files
106
+ *
107
+ * @param {string} themesPath - Path to themes directory
108
+ * @returns {Theme[]} Array of Theme objects (empty if directory doesn't exist)
109
+ */
110
+ static scanDirectory(themesPath) {
111
+ if (!fs.existsSync(themesPath)) {
112
+ return [];
113
+ }
114
+
115
+ const themes = [];
116
+ const cssFiles = this._findCssFiles(themesPath);
117
+
118
+ for (const cssPath of cssFiles) {
119
+ try {
120
+ const theme = this.resolveTheme(cssPath);
121
+ themes.push(theme);
122
+ } catch (error) {
123
+ // Skip files that can't be resolved
124
+ console.warn(`Warning: Could not resolve theme from ${cssPath}: ${error.message}`);
125
+ }
126
+ }
127
+
128
+ return themes;
129
+ }
130
+
131
+ /**
132
+ * Find all CSS files in directory recursively
133
+ * @private
134
+ */
135
+ static _findCssFiles(dirPath, results = []) {
136
+ const entries = readdirSync(dirPath, { withFileTypes: true });
137
+
138
+ for (const entry of entries) {
139
+ const fullPath = path.join(dirPath, entry.name);
140
+
141
+ if (entry.isDirectory()) {
142
+ this._findCssFiles(fullPath, results);
143
+ } else if (entry.isFile() && entry.name.endsWith('.css')) {
144
+ results.push(fullPath);
145
+ }
146
+ }
147
+
148
+ return results;
149
+ }
150
+
151
+ /**
152
+ * Resolve all dependencies for selected themes
153
+ * Returns complete set of themes to copy (including parents)
154
+ * Excludes system themes (default, gaia, uncover)
155
+ *
156
+ * @param {Theme[]} selectedThemes - Themes user selected
157
+ * @param {Theme[]} allThemes - All available themes
158
+ * @returns {Theme[]} Themes to copy (selected + dependencies)
159
+ */
160
+ static resolveDependencies(selectedThemes, allThemes) {
161
+ const toCopy = new Map(); // Use Map for deduplication by name
162
+ const visited = new Set();
163
+ const visiting = new Set();
164
+
165
+ // Create a map of all available themes for quick lookup
166
+ // Also include selected themes in case they're not in allThemes
167
+ const allThemesMap = new Map();
168
+ for (const theme of allThemes) {
169
+ allThemesMap.set(theme.name, theme);
170
+ }
171
+ for (const theme of selectedThemes) {
172
+ if (!allThemesMap.has(theme.name)) {
173
+ allThemesMap.set(theme.name, theme);
174
+ }
175
+ }
176
+
177
+ const addTheme = (themeName) => {
178
+ if (visited.has(themeName)) {
179
+ return;
180
+ }
181
+
182
+ // Check for circular dependency
183
+ if (visiting.has(themeName)) {
184
+ console.warn(`Warning: Circular dependency detected for theme '${themeName}'`);
185
+ return;
186
+ }
187
+
188
+ visiting.add(themeName);
189
+
190
+ // Find theme in allThemesMap
191
+ const theme = allThemesMap.get(themeName);
192
+
193
+ if (!theme) {
194
+ console.warn(`Warning: Dependency '${themeName}' not found in available themes`);
195
+ visiting.delete(themeName);
196
+ return;
197
+ }
198
+
199
+ // Skip system themes
200
+ if (theme.isSystem) {
201
+ visiting.delete(themeName);
202
+ return;
203
+ }
204
+
205
+ // First resolve dependencies
206
+ for (const depName of theme.dependencies) {
207
+ // Extract just the theme name from path (e.g., "../marpx/marpx.css" -> "marpx")
208
+ const simpleDepName = depName.replace(/\.css$/g, '').split('/').pop();
209
+ addTheme(simpleDepName);
210
+ }
211
+
212
+ // Then add this theme
213
+ toCopy.set(theme.name, theme);
214
+ visited.add(theme.name);
215
+ visiting.delete(themeName);
216
+ };
217
+
218
+ // Start with selected themes
219
+ for (const theme of selectedThemes) {
220
+ addTheme(theme.name);
221
+ }
222
+
223
+ return Array.from(toCopy.values());
224
+ }
225
+ }
226
+
227
+ module.exports = { ThemeResolver, Theme };