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.
- package/README.md +45 -37
- package/index.js +111 -164
- 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 +13 -5
- package/template/README.md +28 -17
- package/template/package.json +13 -1
- package/template/scripts/theme-cli.js +248 -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
|
@@ -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 };
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// lib/errors.js
|
|
2
|
+
class ThemeError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'ThemeError';
|
|
6
|
+
Error.captureStackTrace(this, this.constructor);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class ThemeNotFoundError extends ThemeError {
|
|
11
|
+
constructor(themeName) {
|
|
12
|
+
super(`Theme '${themeName}' not found`);
|
|
13
|
+
this.name = 'ThemeNotFoundError';
|
|
14
|
+
this.themeName = themeName;
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ThemeAlreadyExistsError extends ThemeError {
|
|
20
|
+
constructor(themeName) {
|
|
21
|
+
super(`Theme '${themeName}' already exists`);
|
|
22
|
+
this.name = 'ThemeAlreadyExistsError';
|
|
23
|
+
this.themeName = themeName;
|
|
24
|
+
Error.captureStackTrace(this, this.constructor);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class PresentationNotFoundError extends ThemeError {
|
|
29
|
+
constructor(presentationPath) {
|
|
30
|
+
super(`Presentation file not found: ${presentationPath}`);
|
|
31
|
+
this.name = 'PresentationNotFoundError';
|
|
32
|
+
this.presentationPath = presentationPath;
|
|
33
|
+
Error.captureStackTrace(this, this.constructor);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class InvalidCSSError extends ThemeError {
|
|
38
|
+
constructor(cssPath, reason) {
|
|
39
|
+
super(`Invalid CSS file '${cssPath}': ${reason}`);
|
|
40
|
+
this.name = 'InvalidCSSError';
|
|
41
|
+
this.cssPath = cssPath;
|
|
42
|
+
this.reason = reason;
|
|
43
|
+
Error.captureStackTrace(this, this.constructor);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class VSCodeIntegrationError extends ThemeError {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'VSCodeIntegrationError';
|
|
51
|
+
Error.captureStackTrace(this, this.constructor);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
ThemeError,
|
|
57
|
+
ThemeNotFoundError,
|
|
58
|
+
ThemeAlreadyExistsError,
|
|
59
|
+
PresentationNotFoundError,
|
|
60
|
+
InvalidCSSError,
|
|
61
|
+
VSCodeIntegrationError
|
|
62
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// lib/frontmatter.js
|
|
2
|
+
const matter = require('gray-matter');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Frontmatter class for handling Marp presentation frontmatter
|
|
8
|
+
* Uses gray-matter library for parsing and manipulating YAML frontmatter
|
|
9
|
+
*/
|
|
10
|
+
class Frontmatter {
|
|
11
|
+
/**
|
|
12
|
+
* Parse frontmatter from markdown content
|
|
13
|
+
*
|
|
14
|
+
* @param {string} content - Markdown content with frontmatter
|
|
15
|
+
* @returns {Object} Parsed frontmatter data
|
|
16
|
+
*/
|
|
17
|
+
static parse(content) {
|
|
18
|
+
const result = matter(content);
|
|
19
|
+
return result.data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract theme value from frontmatter
|
|
24
|
+
*
|
|
25
|
+
* @param {string} content - Markdown content with frontmatter
|
|
26
|
+
* @returns {string|null} Theme name or null if not found
|
|
27
|
+
*/
|
|
28
|
+
static getTheme(content) {
|
|
29
|
+
const data = this.parse(content);
|
|
30
|
+
return data.theme || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set or update theme value in frontmatter
|
|
35
|
+
* Creates frontmatter if it doesn't exist
|
|
36
|
+
*
|
|
37
|
+
* @param {string} content - Markdown content
|
|
38
|
+
* @param {string} themeName - Theme name to set
|
|
39
|
+
* @returns {string} Updated markdown content
|
|
40
|
+
*/
|
|
41
|
+
static setTheme(content, themeName) {
|
|
42
|
+
const result = matter(content);
|
|
43
|
+
|
|
44
|
+
// Set or update the theme
|
|
45
|
+
result.data.theme = themeName;
|
|
46
|
+
|
|
47
|
+
// Rebuild the markdown with updated frontmatter
|
|
48
|
+
return matter.stringify(result.content, result.data);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write content to a file
|
|
53
|
+
*
|
|
54
|
+
* @param {string} filePath - Path to the file
|
|
55
|
+
* @param {string} content - Content to write
|
|
56
|
+
* @throws {Error} If file path is invalid or write fails
|
|
57
|
+
*/
|
|
58
|
+
static writeToFile(filePath, content) {
|
|
59
|
+
const absolutePath = path.resolve(filePath);
|
|
60
|
+
|
|
61
|
+
// Ensure parent directory exists
|
|
62
|
+
const dir = path.dirname(absolutePath);
|
|
63
|
+
if (!fs.existsSync(dir)) {
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(absolutePath, content, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { Frontmatter };
|