create-marp-presentation 1.0.1 → 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.
- package/README.md +55 -16
- package/index.js +114 -79
- package/lib/add-themes-command.js +381 -0
- package/lib/errors.js +62 -0
- package/lib/frontmatter.js +71 -0
- package/lib/prompts.js +222 -0
- package/lib/theme-manager.js +238 -0
- package/lib/theme-resolver.js +227 -0
- package/lib/vscode-integration.js +198 -0
- package/package.json +14 -5
- package/template/README.md +32 -21
- package/template/package.json +13 -1
- package/template/presentation.md +14 -14
- package/template/scripts/theme-cli.js +248 -0
- package/template-optional/.gitkeep +0 -0
- package/template-optional/examples.md +249 -0
- package/template-optional/static/.gitkeep +0 -0
- package/template-optional/static/demo-image.png +0 -0
- package/themes/beam/beam.css +141 -0
- package/themes/default-clean/default-clean.css +57 -0
- package/themes/gaia-dark/gaia-dark.css +27 -0
- package/themes/marpx/marpx.css +1735 -0
- package/themes/marpx/socrates.css +105 -0
- package/themes/uncover-minimal/uncover-minimal.css +22 -0
package/README.md
CHANGED
|
@@ -1,34 +1,73 @@
|
|
|
1
1
|
# Create Marp Presentation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create beautiful presentations in Markdown. Zero setup.
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
-
|
|
13
|
+
That's it! Edit `presentation.md` and see changes live at `http://localhost:8080`.
|
|
14
|
+
|
|
15
|
+
## VSCode Setup
|
|
16
|
+
|
|
17
|
+
For the best editing experience, install [Marp for VSCode](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode):
|
|
18
|
+
|
|
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
|
|
23
|
+
|
|
24
|
+
After installing, open any `.md` file and click the **Marp: Preview** button in the top-right corner.
|
|
25
|
+
|
|
26
|
+
## What You Get
|
|
27
|
+
|
|
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
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
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 |
|
|
41
|
+
|
|
42
|
+
## Theme Management
|
|
43
|
+
|
|
44
|
+
Add and switch themes anytime.
|
|
45
|
+
|
|
46
|
+
### Quick Theme Commands
|
|
12
47
|
|
|
13
48
|
```bash
|
|
14
|
-
|
|
15
|
-
npm run
|
|
16
|
-
npm run
|
|
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
|
|
17
53
|
```
|
|
18
54
|
|
|
19
|
-
|
|
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)
|
|
20
58
|
|
|
21
|
-
|
|
22
|
-
- 📝 Markdown slides
|
|
23
|
-
- 🎨 Marp themes
|
|
24
|
-
- 📦 HTML, PDF, PPTX export
|
|
25
|
-
- 📁 Static files support
|
|
26
|
-
- 🔥 Live preview
|
|
59
|
+
## Local Development
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
To test without publishing to npm:
|
|
29
62
|
|
|
30
|
-
|
|
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
|
+
```
|
|
31
70
|
|
|
32
|
-
##
|
|
71
|
+
## License
|
|
33
72
|
|
|
34
73
|
MIT
|
package/index.js
CHANGED
|
@@ -1,96 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* create-marp-presentation - CLI entry point
|
|
5
|
+
* Supports dual entry points:
|
|
6
|
+
* 1. npx create-marp-presentation <name> [--path <dir>] - Create new project
|
|
7
|
+
* 2. npx create-marp-presentation theme:add <path> [themes...] - Add themes to existing project
|
|
8
|
+
*/
|
|
6
9
|
|
|
7
|
-
const
|
|
10
|
+
const path = require('path');
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
12
|
+
const { createProject, validateProjectName, parsePathArg } = require('./cli/commands/create-project');
|
|
13
|
+
const { addThemesToExistingProject } = require('./cli/commands/add-themes-cli');
|
|
14
|
+
const { validateOutputPath } = require('./cli/utils/file-utils');
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Paths
|
|
17
|
+
const templatePath = path.join(__dirname, 'template');
|
|
18
|
+
const themesLibraryPath = path.join(__dirname, 'themes');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Show usage information
|
|
22
|
+
* @param {boolean} isError - If true, write to stderr and exit with error code
|
|
23
|
+
*/
|
|
24
|
+
function showUsage(isError = false) {
|
|
25
|
+
const output = isError ? console.error : console.log;
|
|
26
|
+
output('Please provide a project name:');
|
|
27
|
+
output(' npx create-marp-presentation <project-name> [--path <output-dir>]');
|
|
28
|
+
output('');
|
|
29
|
+
output('Or use the theme:add command:');
|
|
30
|
+
output(' npx create-marp-presentation theme:add <project-path> [theme-names...]');
|
|
31
|
+
output('');
|
|
32
|
+
output('Examples:');
|
|
33
|
+
output(' npx create-marp-presentation my-project');
|
|
34
|
+
output(' npx create-marp-presentation my-project --path /tmp');
|
|
35
|
+
output(' npx create-marp-presentation my-project --path ~/projects');
|
|
36
|
+
output(' npx create-marp-presentation theme:add ./my-project');
|
|
37
|
+
output(' npx create-marp-presentation theme:add ./my-project beam marpx');
|
|
38
|
+
output('');
|
|
39
|
+
|
|
40
|
+
if (isError) {
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
21
43
|
}
|
|
22
44
|
|
|
23
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Handle theme:add command
|
|
47
|
+
* @param {string[]} args - Command arguments
|
|
48
|
+
*/
|
|
49
|
+
async function handleThemeAdd(args) {
|
|
50
|
+
const targetPath = args[0];
|
|
51
|
+
if (!targetPath) {
|
|
52
|
+
console.error('Usage: npx create-marp-presentation theme:add <project-path> [theme-names...]');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
24
55
|
|
|
25
|
-
//
|
|
26
|
-
const resolvedPath = path.resolve(projectPath);
|
|
27
|
-
if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
|
|
28
|
-
console.error('Invalid project path: path traversal detected.');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
56
|
+
const themeNames = args.slice(1); // Additional arguments are theme names
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
try {
|
|
59
|
+
await addThemesToExistingProject(targetPath, {
|
|
60
|
+
themesLibraryPath,
|
|
61
|
+
themeNames: themeNames.length > 0 ? themeNames : null
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`Error: ${error.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
36
67
|
}
|
|
37
68
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (entry.isDirectory()) {
|
|
57
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
58
|
-
copyDir(srcPath, destPath);
|
|
59
|
-
} else {
|
|
60
|
-
fs.copyFileSync(srcPath, destPath);
|
|
61
|
-
}
|
|
69
|
+
/**
|
|
70
|
+
* Handle project creation command
|
|
71
|
+
* @param {string} projectName - Name of the project
|
|
72
|
+
* @param {string[]} args - Remaining arguments (e.g., --path)
|
|
73
|
+
*/
|
|
74
|
+
async function handleProjectCreation(projectName, args = []) {
|
|
75
|
+
// Parse --path argument if present
|
|
76
|
+
const { pathIndex } = parsePathArg(args);
|
|
77
|
+
let outputPath = process.cwd();
|
|
78
|
+
|
|
79
|
+
if (pathIndex !== null) {
|
|
80
|
+
const pathArg = args[pathIndex + 1];
|
|
81
|
+
const validation = validateOutputPath(pathArg);
|
|
82
|
+
if (!validation.valid) {
|
|
83
|
+
console.error(`Invalid --path: "${pathArg}"`);
|
|
84
|
+
console.error(validation.error);
|
|
85
|
+
process.exit(1);
|
|
62
86
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
copyDir(templatePath, projectPath);
|
|
66
|
-
|
|
67
|
-
console.log('✓ Project created');
|
|
68
|
-
console.log();
|
|
69
|
-
|
|
70
|
-
// Запускаем npm install
|
|
71
|
-
console.log('Installing dependencies...');
|
|
72
|
-
const installResult = spawnSync('npm', ['install'], {
|
|
73
|
-
cwd: projectPath,
|
|
74
|
-
stdio: 'inherit',
|
|
75
|
-
});
|
|
87
|
+
outputPath = validation.resolvedPath;
|
|
88
|
+
}
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
try {
|
|
91
|
+
await createProject(projectName, {
|
|
92
|
+
outputPath,
|
|
93
|
+
templatePath,
|
|
94
|
+
themesLibraryPath
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.message === 'Invalid project name' || error.message === 'Project directory already exists') {
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
console.error('Error creating project:', error.message);
|
|
81
101
|
process.exit(1);
|
|
82
102
|
}
|
|
103
|
+
}
|
|
83
104
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Main entry point
|
|
107
|
+
*/
|
|
108
|
+
async function main() {
|
|
109
|
+
const [command, ...args] = process.argv.slice(2);
|
|
110
|
+
|
|
111
|
+
switch (command) {
|
|
112
|
+
case 'theme:add':
|
|
113
|
+
await handleThemeAdd(args);
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case undefined:
|
|
117
|
+
showUsage(true); // Exit with error code 1
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
// Treat as project name (backward compatible)
|
|
122
|
+
await handleProjectCreation(command, args);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
96
125
|
}
|
|
126
|
+
|
|
127
|
+
// Run
|
|
128
|
+
main().catch(error => {
|
|
129
|
+
console.error('Unexpected error:', error.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// lib/add-themes-command.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { ThemeResolver, Theme } = require('./theme-resolver');
|
|
5
|
+
const { VSCodeIntegration, VSCodeSettingsDTO } = require('./vscode-integration');
|
|
6
|
+
const { ThemeManager } = require('./theme-manager');
|
|
7
|
+
const { Prompts } = require('./prompts');
|
|
8
|
+
const {
|
|
9
|
+
ThemeError,
|
|
10
|
+
ThemeNotFoundError,
|
|
11
|
+
ThemeAlreadyExistsError
|
|
12
|
+
} = require('./errors');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shared function for adding Marp themes to a project
|
|
16
|
+
* Handles theme copying, dependency resolution, conflict detection, and VSCode sync
|
|
17
|
+
*/
|
|
18
|
+
class AddThemesCommand {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} options - Configuration options
|
|
21
|
+
* @param {string} options.templatePath - Path to themes library directory (optional, defaults to __dirname/../themes)
|
|
22
|
+
* @param {boolean} options.interactive - Enable interactive prompts (default: true)
|
|
23
|
+
* @param {Object} options.prompts - Prompt functions for testing (optional)
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = {
|
|
27
|
+
interactive: options.interactive !== false,
|
|
28
|
+
templatePath: options.templatePath || path.join(__dirname, '..', 'themes'),
|
|
29
|
+
...options
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the path to the themes library directory
|
|
35
|
+
* @returns {string} Absolute path to themes library
|
|
36
|
+
*/
|
|
37
|
+
getTemplateThemesPath() {
|
|
38
|
+
// templatePath now points directly to the themes library
|
|
39
|
+
return this.options.templatePath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Main execution method - adds themes to target project
|
|
44
|
+
*
|
|
45
|
+
* @param {string} targetPath - Path to target project directory
|
|
46
|
+
* @param {Object} options - Execution options
|
|
47
|
+
* @param {string[]} options.themes - Theme names to add (optional, prompts if not provided)
|
|
48
|
+
* @param {string[]} options.skip - Theme names to skip conflicts (optional)
|
|
49
|
+
* @param {boolean} options.force - Overwrite existing themes (default: false)
|
|
50
|
+
* @param {boolean} options.noVscode - Skip VSCode settings sync (default: false)
|
|
51
|
+
* @returns {Promise<Object>} Result object with copied, skipped, and conflicts arrays
|
|
52
|
+
*/
|
|
53
|
+
async execute(targetPath, options = {}) {
|
|
54
|
+
const execOptions = {
|
|
55
|
+
themes: options.themes || null,
|
|
56
|
+
skip: options.skip || [],
|
|
57
|
+
force: options.force || false,
|
|
58
|
+
noVscode: options.noVscode || false,
|
|
59
|
+
...options
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Validate target path
|
|
63
|
+
if (!fs.existsSync(targetPath)) {
|
|
64
|
+
throw new ThemeError(`Target path does not exist: ${targetPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const templateThemesPath = this.getTemplateThemesPath();
|
|
68
|
+
|
|
69
|
+
// Scan available themes from themes library
|
|
70
|
+
if (!fs.existsSync(templateThemesPath)) {
|
|
71
|
+
throw new ThemeError(`Themes library directory not found: ${templateThemesPath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const availableThemes = ThemeResolver.scanDirectory(templateThemesPath);
|
|
75
|
+
|
|
76
|
+
if (availableThemes.length === 0) {
|
|
77
|
+
throw new ThemeError('No themes found in themes library');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Determine which themes to copy
|
|
81
|
+
let themesToCopy;
|
|
82
|
+
if (execOptions.themes != null) {
|
|
83
|
+
// themes explicitly provided (even if empty array = copy nothing)
|
|
84
|
+
if (execOptions.themes.length > 0) {
|
|
85
|
+
themesToCopy = this._resolveThemeNames(execOptions.themes, availableThemes);
|
|
86
|
+
} else {
|
|
87
|
+
// Empty array means user chose no themes
|
|
88
|
+
themesToCopy = [];
|
|
89
|
+
}
|
|
90
|
+
} else if (this.options.interactive) {
|
|
91
|
+
// Interactive mode - prompt user using built-in Prompts.promptThemes()
|
|
92
|
+
const selectedNames = await this._promptThemes(availableThemes);
|
|
93
|
+
// Filter availableThemes to only include selected ones
|
|
94
|
+
themesToCopy = availableThemes.filter(t => selectedNames.includes(t.name));
|
|
95
|
+
} else {
|
|
96
|
+
// Non-interactive without explicit themes: copy all available themes
|
|
97
|
+
themesToCopy = availableThemes;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resolve dependencies
|
|
101
|
+
const themesWithDependencies = ThemeResolver.resolveDependencies(
|
|
102
|
+
themesToCopy,
|
|
103
|
+
availableThemes
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Scan existing themes in project
|
|
107
|
+
const existingThemes = this._scanProjectThemes(targetPath);
|
|
108
|
+
|
|
109
|
+
// Find conflicts
|
|
110
|
+
const conflicts = this._findConflicts(themesWithDependencies, existingThemes);
|
|
111
|
+
|
|
112
|
+
// Handle conflicts
|
|
113
|
+
let skipList = new Set(execOptions.skip);
|
|
114
|
+
|
|
115
|
+
if (conflicts.length > 0 && !execOptions.force) {
|
|
116
|
+
if (this.options.interactive) {
|
|
117
|
+
const resolvedConflicts = await this._resolveConflictsInteractive(
|
|
118
|
+
conflicts,
|
|
119
|
+
execOptions.force
|
|
120
|
+
);
|
|
121
|
+
// Add overwrite choices to skip list (inverted logic - skip if not overwriting)
|
|
122
|
+
conflicts.forEach(conflict => {
|
|
123
|
+
if (!resolvedConflicts.overwrite.includes(conflict)) {
|
|
124
|
+
skipList.add(conflict);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
// Non-interactive: skip all conflicts
|
|
129
|
+
conflicts.forEach(conflict => skipList.add(conflict));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Copy themes
|
|
134
|
+
const copyResult = this.copyThemes(
|
|
135
|
+
themesWithDependencies,
|
|
136
|
+
templateThemesPath,
|
|
137
|
+
targetPath,
|
|
138
|
+
Array.from(skipList)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Sync VSCode settings
|
|
142
|
+
if (!execOptions.noVscode && copyResult.copied.length > 0) {
|
|
143
|
+
this._syncVscodeSettings(targetPath, copyResult.copied);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Ensure marp.themeSet is configured in package.json
|
|
147
|
+
if (copyResult.copied.length > 0) {
|
|
148
|
+
ThemeManager.ensureThemeSetConfig(targetPath, { silent: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Return result with conflicts included
|
|
152
|
+
return {
|
|
153
|
+
...copyResult,
|
|
154
|
+
conflicts
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Copy themes from themes library to target project
|
|
160
|
+
*
|
|
161
|
+
* @param {Theme[]} themes - Theme objects to copy
|
|
162
|
+
* @param {string} templatePath - Path to themes library directory
|
|
163
|
+
* @param {string} targetPath - Path to target project directory
|
|
164
|
+
* @param {string[]} skipList - Theme names to skip
|
|
165
|
+
* @returns {Object} Result with copied, skipped arrays
|
|
166
|
+
*/
|
|
167
|
+
copyThemes(themes, templatePath, targetPath, skipList = []) {
|
|
168
|
+
const skipSet = new Set(skipList);
|
|
169
|
+
const copied = [];
|
|
170
|
+
const skipped = [];
|
|
171
|
+
|
|
172
|
+
// Ensure target themes directory exists
|
|
173
|
+
const targetThemesDir = path.join(targetPath, 'themes');
|
|
174
|
+
if (!fs.existsSync(targetThemesDir)) {
|
|
175
|
+
fs.mkdirSync(targetThemesDir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const theme of themes) {
|
|
179
|
+
if (skipSet.has(theme.name)) {
|
|
180
|
+
skipped.push(theme.name);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate relative path from themes library
|
|
185
|
+
const relativePath = path.relative(templatePath, theme.path);
|
|
186
|
+
const targetFilePath = path.join(targetThemesDir, relativePath);
|
|
187
|
+
|
|
188
|
+
// Ensure target directory exists
|
|
189
|
+
const targetDir = path.dirname(targetFilePath);
|
|
190
|
+
if (!fs.existsSync(targetDir)) {
|
|
191
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Copy file
|
|
195
|
+
fs.copyFileSync(theme.path, targetFilePath);
|
|
196
|
+
copied.push(theme); // Return Theme object, not just name
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { copied, skipped };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Scan project themes directory for existing theme files
|
|
204
|
+
* @private
|
|
205
|
+
*
|
|
206
|
+
* @param {string} projectPath - Path to project directory
|
|
207
|
+
* @returns {Theme[]} Array of existing theme objects
|
|
208
|
+
*/
|
|
209
|
+
_scanProjectThemes(projectPath) {
|
|
210
|
+
const themesPath = path.join(projectPath, 'themes');
|
|
211
|
+
|
|
212
|
+
if (!fs.existsSync(themesPath)) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
return ThemeResolver.scanDirectory(themesPath);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// If scan fails, return empty array
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find conflicts between themes to copy and existing themes
|
|
226
|
+
* @private
|
|
227
|
+
*
|
|
228
|
+
* @param {Theme[]} themesToCopy - Themes that will be copied
|
|
229
|
+
* @param {Theme[]} existingThemes - Themes that already exist
|
|
230
|
+
* @returns {string[]} Array of conflicting theme names
|
|
231
|
+
*/
|
|
232
|
+
_findConflicts(themesToCopy, existingThemes) {
|
|
233
|
+
const existingNames = new Set(existingThemes.map(t => t.name));
|
|
234
|
+
const conflicts = [];
|
|
235
|
+
|
|
236
|
+
for (const theme of themesToCopy) {
|
|
237
|
+
if (existingNames.has(theme.name)) {
|
|
238
|
+
conflicts.push(theme.name);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return conflicts;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Resolve theme names to Theme objects
|
|
247
|
+
* @private
|
|
248
|
+
*
|
|
249
|
+
* @param {string[]} themeNames - Theme names to resolve
|
|
250
|
+
* @param {Theme[]} availableThemes - Available themes from themes library
|
|
251
|
+
* @returns {Theme[]} Resolved theme objects
|
|
252
|
+
* @throws {ThemeNotFoundError} If a theme name is not found
|
|
253
|
+
*/
|
|
254
|
+
_resolveThemeNames(themeNames, availableThemes) {
|
|
255
|
+
const themesMap = new Map();
|
|
256
|
+
for (const theme of availableThemes) {
|
|
257
|
+
themesMap.set(theme.name, theme);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const resolved = [];
|
|
261
|
+
const notFound = [];
|
|
262
|
+
|
|
263
|
+
for (const name of themeNames) {
|
|
264
|
+
const theme = themesMap.get(name);
|
|
265
|
+
if (theme) {
|
|
266
|
+
resolved.push(theme);
|
|
267
|
+
} else {
|
|
268
|
+
notFound.push(name);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (notFound.length > 0) {
|
|
273
|
+
const message = notFound.length === 1
|
|
274
|
+
? notFound[0]
|
|
275
|
+
: `${notFound.join(', ')}`;
|
|
276
|
+
throw new ThemeNotFoundError(message);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return resolved;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Prompt user to select themes interactively
|
|
284
|
+
* Uses Prompts.promptThemes() for built-in theme selection
|
|
285
|
+
* @private
|
|
286
|
+
*
|
|
287
|
+
* @param {Theme[]} availableThemes - Available themes
|
|
288
|
+
* @returns {Promise<string[]>} Selected theme names
|
|
289
|
+
*/
|
|
290
|
+
async _promptThemes(availableThemes) {
|
|
291
|
+
// Use built-in Prompts.promptThemes() for interactive selection
|
|
292
|
+
return await Prompts.promptThemes(availableThemes);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve conflicts interactively
|
|
297
|
+
* @private
|
|
298
|
+
*
|
|
299
|
+
* @param {string[]} conflicts - Conflicting theme names
|
|
300
|
+
* @param {boolean} force - Force overwrite all
|
|
301
|
+
* @returns {Promise<Object>} Resolution result with overwrite array
|
|
302
|
+
*/
|
|
303
|
+
async _resolveConflictsInteractive(conflicts, force) {
|
|
304
|
+
if (force) {
|
|
305
|
+
return { overwrite: conflicts };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Use custom prompt if provided (backward compatibility for tests)
|
|
309
|
+
if (this.options.prompts?.resolveConflicts) {
|
|
310
|
+
return await this.options.prompts.resolveConflicts(conflicts);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Use built-in Prompts.promptConflictResolution
|
|
314
|
+
const result = await Prompts.promptConflictResolution(
|
|
315
|
+
conflicts.map(c => ({ name: c }))
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (result === 'skip-all') return { overwrite: [] };
|
|
319
|
+
if (result === 'overwrite-all') return { overwrite: conflicts };
|
|
320
|
+
if (result === 'cancel') return { overwrite: [] };
|
|
321
|
+
|
|
322
|
+
// Handle 'choose-each' or individual conflicts
|
|
323
|
+
const overwrite = [];
|
|
324
|
+
for (const conflict of conflicts) {
|
|
325
|
+
const choice = await Prompts.promptSingleConflict(conflict);
|
|
326
|
+
if (choice === 'overwrite') {
|
|
327
|
+
overwrite.push(conflict);
|
|
328
|
+
} else if (choice === 'cancel') {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { overwrite };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Sync themes to VSCode settings
|
|
337
|
+
* @private
|
|
338
|
+
*
|
|
339
|
+
* @param {string} projectPath - Path to project directory
|
|
340
|
+
* @param {string[]} copiedThemes - Names of copied themes
|
|
341
|
+
*/
|
|
342
|
+
_syncVscodeSettings(projectPath, copiedThemes) {
|
|
343
|
+
const vscode = new VSCodeIntegration(projectPath);
|
|
344
|
+
|
|
345
|
+
// Use DTO for type-safe access
|
|
346
|
+
const dto = vscode.createSettingsDTO();
|
|
347
|
+
const existingThemes = dto.getMarpThemes();
|
|
348
|
+
|
|
349
|
+
// Add new theme paths (relative to themes/)
|
|
350
|
+
// copiedThemes are Theme objects - use their paths to compute relative paths
|
|
351
|
+
const newThemePaths = copiedThemes.map(theme => {
|
|
352
|
+
// theme.path is absolute, we need relative path from themes directory
|
|
353
|
+
// e.g., /library/themes/marpx/socrates.css -> themes/marpx/socrates.css
|
|
354
|
+
const filename = path.basename(theme.path);
|
|
355
|
+
const themeName = path.basename(path.dirname(theme.path));
|
|
356
|
+
// For single-file themes: themes/themeName/themeName.css
|
|
357
|
+
// For themes in subdirs: themes/subdir/themeName.css
|
|
358
|
+
return `themes/${themeName}/${filename}`;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Merge without duplicates
|
|
362
|
+
const allThemes = [...new Set([...existingThemes, ...newThemePaths])];
|
|
363
|
+
|
|
364
|
+
dto.setMarpThemes(allThemes);
|
|
365
|
+
vscode.writeSettings(dto.toObject());
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Convenience function to create and execute AddThemesCommand
|
|
371
|
+
*
|
|
372
|
+
* @param {string} targetPath - Path to target project directory
|
|
373
|
+
* @param {Object} options - Options passed to AddThemesCommand
|
|
374
|
+
* @returns {Promise<Object>} Result object from execute()
|
|
375
|
+
*/
|
|
376
|
+
async function addThemesCommand(targetPath, options = {}) {
|
|
377
|
+
const command = new AddThemesCommand(options);
|
|
378
|
+
return await command.execute(targetPath, options);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = { AddThemesCommand, addThemesCommand };
|