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.
- package/README.md +45 -37
- package/cli/commands/add-themes-cli.js +85 -0
- package/cli/commands/create-project.js +199 -0
- package/cli/utils/file-utils.js +106 -0
- package/cli/utils/prompt-utils.js +89 -0
- package/docs/plans/2025-02-19-marp-template-design.md +207 -0
- package/docs/plans/2025-02-19-marp-template-implementation.md +848 -0
- package/docs/plans/2026-02-20-example-slides-design.md +179 -0
- package/docs/plans/2026-02-20-example-slides-implementation.md +811 -0
- package/docs/plans/2026-02-23-theme-management-design.md +836 -0
- package/docs/plans/2026-02-23-theme-management-implementation.md +3585 -0
- package/docs/plans/2026-02-26-theme-addition-refactoring-design.md +172 -0
- package/docs/plans/2026-02-26-theme-addition-refactoring.md +456 -0
- package/docs/plans/2026-02-27-theme-add-fix-design.md +136 -0
- package/docs/plans/2026-02-27-theme-add-fix.md +353 -0
- package/docs/plans/TODO.md +5 -0
- package/docs/reqs/themes-requirements.md +49 -0
- package/docs/theme-management.md +261 -0
- 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 +15 -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,836 @@
|
|
|
1
|
+
# Theme Management System Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-02-23
|
|
4
|
+
**Author:** Design session with user
|
|
5
|
+
**Status:** Approved
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The theme management system enables users to create, manage, and switch between Marp themes in their presentation projects. It provides CLI commands for theme operations and integrates with VSCode for live preview.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
See [docs/reqs/themes-requirements.md](../reqs/themes-requirements.md) for detailed requirements.
|
|
14
|
+
|
|
15
|
+
### Key Requirements Summary
|
|
16
|
+
|
|
17
|
+
- **System themes:** `default`, `gaia`, `uncover` (built into Marp)
|
|
18
|
+
- **User themes:** CSS files that can import other themes via `@import`
|
|
19
|
+
- **Theme identification:** via `/* @theme name */` directive in CSS
|
|
20
|
+
- **Interactive CLI:** using `@inquirer/prompts` (Inquirer.js v3+)
|
|
21
|
+
- **VSCode integration:** dynamic updates to `.vscode/settings.json`
|
|
22
|
+
- **Testing:** full coverage with Jest
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
### Monolithic Approach
|
|
27
|
+
|
|
28
|
+
Selected approach: **Monolithic ThemeManager** with focused helper classes.
|
|
29
|
+
|
|
30
|
+
### File Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
create-marp-presentation/ # Мета-пакет (npm published)
|
|
34
|
+
├── index.js # CLI entry point
|
|
35
|
+
├── lib/ # Библиотечный код
|
|
36
|
+
│ ├── add-themes-command.js # addThemesCommand функция
|
|
37
|
+
│ ├── theme-manager.js # Main ThemeManager class
|
|
38
|
+
│ ├── theme-resolver.js # Parse CSS for @theme and @import
|
|
39
|
+
│ ├── prompts.js # Wrappers around @inquirer/prompts
|
|
40
|
+
│ ├── vscode-integration.js # Update .vscode/settings.json
|
|
41
|
+
│ ├── frontmatter.js # Parse/edit markdown frontmatter
|
|
42
|
+
│ └── errors.js # Custom error classes
|
|
43
|
+
│
|
|
44
|
+
├── template/ # Копируется в создаваемый проект
|
|
45
|
+
│ ├── themes/ # Flat structure: themes/{name}/
|
|
46
|
+
│ │ ├── beam/
|
|
47
|
+
│ │ │ └── beam.css
|
|
48
|
+
│ │ ├── marpx/
|
|
49
|
+
│ │ │ ├── marpx.css
|
|
50
|
+
│ │ │ └── socrates.css
|
|
51
|
+
│ │ ├── gaia-dark/
|
|
52
|
+
│ │ │ └── dark.css
|
|
53
|
+
│ │ ├── uncover-minimal/
|
|
54
|
+
│ │ │ └── minimal.css
|
|
55
|
+
│ │ └── default-clean/
|
|
56
|
+
│ │ └── clean.css
|
|
57
|
+
│ │
|
|
58
|
+
│ └── scripts/ # Копируется в project/scripts/
|
|
59
|
+
│ ├── theme-cli.js # CLI entry point for npm scripts
|
|
60
|
+
│ └── lib/ # Копия lib/ для theme-cli.js
|
|
61
|
+
│ ├── theme-manager.js
|
|
62
|
+
│ ├── theme-resolver.js
|
|
63
|
+
│ ├── prompts.js
|
|
64
|
+
│ ├── vscode-integration.js
|
|
65
|
+
│ ├── frontmatter.js
|
|
66
|
+
│ └── errors.js
|
|
67
|
+
│
|
|
68
|
+
└── tests/
|
|
69
|
+
├── unit/
|
|
70
|
+
│ ├── theme-resolver.test.js
|
|
71
|
+
│ ├── vscode-integration.test.js
|
|
72
|
+
│ ├── theme-manager.test.js
|
|
73
|
+
│ └── add-themes-command.test.js
|
|
74
|
+
├── integration/
|
|
75
|
+
│ └── theme-cli.test.js
|
|
76
|
+
└── fixtures/
|
|
77
|
+
├── themes/
|
|
78
|
+
└── presentations/
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Key Architectural Decisions
|
|
82
|
+
|
|
83
|
+
1. **lib/ копируется в project/scripts/lib/** — theme-cli.js в созданном проекте использует локальную копию модулей
|
|
84
|
+
|
|
85
|
+
2. **addThemesCommand в мета-пакете** — функция находится в create-marp-presentation/lib/add-themes-command.js, используется:
|
|
86
|
+
- index.js при создании проекта
|
|
87
|
+
- CLI команда `add-themes` для theme:add-from-template
|
|
88
|
+
|
|
89
|
+
3. **npx для theme:add-from-template** — вызывается через `npx create-marp-presentation add-themes`, код выполняется из установленного пакета
|
|
90
|
+
|
|
91
|
+
## Components
|
|
92
|
+
|
|
93
|
+
### ThemeResolver
|
|
94
|
+
|
|
95
|
+
Parses CSS files to extract theme metadata.
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
class ThemeResolver {
|
|
99
|
+
/**
|
|
100
|
+
* Extract theme name from CSS: /* @theme beam *\/
|
|
101
|
+
* Falls back to filename if directive not found
|
|
102
|
+
*/
|
|
103
|
+
static extractThemeName(cssContent) → string
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extract @import dependencies: @import "gaia"
|
|
107
|
+
* Ignores url() imports
|
|
108
|
+
*/
|
|
109
|
+
static extractDependencies(cssContent) → string[]
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Full theme resolution from single CSS file
|
|
113
|
+
*/
|
|
114
|
+
static resolveTheme(cssPath) → Theme
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Scan directory recursively for all themes
|
|
118
|
+
* Returns flat array of Theme objects
|
|
119
|
+
* Used by: index.js (project creation) and theme:list
|
|
120
|
+
*/
|
|
121
|
+
static scanDirectory(themesPath) → Theme[]
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve all dependencies for selected themes
|
|
125
|
+
* Returns complete set of themes to copy (including parents)
|
|
126
|
+
* Excludes system themes (default, gaia, uncover)
|
|
127
|
+
*
|
|
128
|
+
* @param {Theme[]} selectedThemes - themes user selected
|
|
129
|
+
* @param {Theme[]} allThemes - all available themes in template
|
|
130
|
+
* @returns {Theme[]} - themes to copy (selected + dependencies)
|
|
131
|
+
*
|
|
132
|
+
* Example:
|
|
133
|
+
* selected: [socrates]
|
|
134
|
+
* socrates.dependencies: [marpx]
|
|
135
|
+
* marpx.dependencies: [default] ← system, not copied
|
|
136
|
+
* returns: [socrates, marpx]
|
|
137
|
+
*/
|
|
138
|
+
static resolveDependencies(selectedThemes, allThemes) → Theme[]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Theme
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
class Theme {
|
|
146
|
+
constructor(name, path, css, dependencies = [])
|
|
147
|
+
|
|
148
|
+
name: string // From /* @theme */ or filename
|
|
149
|
+
path: string // Full path to CSS file
|
|
150
|
+
css: string // CSS content
|
|
151
|
+
dependencies: string[] // Parent themes
|
|
152
|
+
isSystem: boolean // false for user themes
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### VSCodeIntegration
|
|
157
|
+
|
|
158
|
+
Manages VSCode settings for Marp extension.
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
class VSCodeIntegration {
|
|
162
|
+
constructor(projectPath)
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sync themes to markdown.marp.themes in settings.json
|
|
166
|
+
* Creates settings.json if not exists
|
|
167
|
+
* Backs up corrupted JSON and recreates
|
|
168
|
+
*/
|
|
169
|
+
syncThemes(themes: Theme[]) → void
|
|
170
|
+
|
|
171
|
+
readSettings() → object
|
|
172
|
+
writeSettings(settings: object) → void
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**VSCode settings format:**
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"markdown.marp.themes": [
|
|
180
|
+
"themes/beam/beam.css",
|
|
181
|
+
"themes/marpx/marpx.css"
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### ThemeManager
|
|
187
|
+
|
|
188
|
+
Main class for theme operations. **Delegates scanning to ThemeResolver.**
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
class ThemeManager {
|
|
192
|
+
constructor(projectPath)
|
|
193
|
+
|
|
194
|
+
// Properties
|
|
195
|
+
get themesPath() → string // path/to/project/themes
|
|
196
|
+
|
|
197
|
+
// Reading
|
|
198
|
+
/**
|
|
199
|
+
* Scan themes in project
|
|
200
|
+
* IMPLEMENTATION: delegates to ThemeResolver.scanDirectory(this.themesPath)
|
|
201
|
+
*/
|
|
202
|
+
scanThemes() → Theme[]
|
|
203
|
+
getTheme(name) → Theme | null
|
|
204
|
+
getActiveTheme() → string | null
|
|
205
|
+
|
|
206
|
+
// Modification
|
|
207
|
+
createTheme(name, parent, directory) → void
|
|
208
|
+
setActiveTheme(themeName) → void
|
|
209
|
+
|
|
210
|
+
// Integration
|
|
211
|
+
updateVSCodeSettings() → void
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Single Source of Truth:** `ThemeResolver.scanDirectory()` — единственная реализация сканирования тем.
|
|
216
|
+
- `ThemeManager.scanThemes()` → вызывает `ThemeResolver.scanDirectory(this.themesPath)`
|
|
217
|
+
- `index.js` (project creation) → вызывает `ThemeResolver.scanDirectory(template/themes/)`
|
|
218
|
+
- Это обеспечивает консистентность логики сканирования везде.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Prompts
|
|
222
|
+
|
|
223
|
+
Interactive prompts using `@inquirer/prompts`.
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
const inquirer = require('@inquirer/prompts');
|
|
227
|
+
|
|
228
|
+
async function promptThemes(availableThemes) → string[]
|
|
229
|
+
async function promptActiveTheme(selectedThemes) → string
|
|
230
|
+
async function promptNewThemeName() → string
|
|
231
|
+
async function promptParentTheme(availableThemes) → string | null
|
|
232
|
+
async function promptDirectoryLocation(existingDirs) → string
|
|
233
|
+
async function promptNewFolderName() → string
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## CLI Commands
|
|
237
|
+
|
|
238
|
+
### create-marp-presentation Commands
|
|
239
|
+
|
|
240
|
+
#### `npx create-marp-presentation add-themes <target-dir>`
|
|
241
|
+
|
|
242
|
+
**Общая команда для добавления тем из шаблона.** Используется:
|
|
243
|
+
- При создании проекта (Flow 1)
|
|
244
|
+
- В theme:add-from-template (вызывается через npx)
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
# Интерактивный режим
|
|
248
|
+
$ npx create-marp-presentation add-themes ./my-project
|
|
249
|
+
|
|
250
|
+
? Select themes to add:
|
|
251
|
+
☁ beam
|
|
252
|
+
☁ socrates
|
|
253
|
+
☐ gaia-dark
|
|
254
|
+
|
|
255
|
+
✓ Added 2 themes with dependencies
|
|
256
|
+
- beam (themes/beam/)
|
|
257
|
+
- socrates (themes/marpx/socrates.css)
|
|
258
|
+
- marpx (dependency of socrates)
|
|
259
|
+
✓ VSCode settings updated
|
|
260
|
+
|
|
261
|
+
# Неинтерактивный режим (CI/CD)
|
|
262
|
+
$ npx create-marp-presentation add-themes ./my-project --themes=beam,socrates
|
|
263
|
+
|
|
264
|
+
✓ Added themes: beam, socrates (with dependencies)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Аргументы:**
|
|
268
|
+
- `<target-dir>` — путь к директории проекта (должен существовать)
|
|
269
|
+
- `--themes=name1,name2` — список тем (опционально, для CI/CD)
|
|
270
|
+
|
|
271
|
+
**Логика:**
|
|
272
|
+
1. `ThemeResolver.scanDirectory(template/themes/)` → все доступные темы
|
|
273
|
+
2. Если `--themes` не указан → `@inquirer/checkbox` для выбора
|
|
274
|
+
3. `ThemeResolver.resolveDependencies()` → темы + родители
|
|
275
|
+
4. Копирование в `<target-dir>/themes/`
|
|
276
|
+
5. Обновление `<target-dir>/.vscode/settings.json`
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Project CLI Commands (npm scripts)
|
|
281
|
+
|
|
282
|
+
| Command | Description |
|
|
283
|
+
|---------|-------------|
|
|
284
|
+
| `theme:list` | List all themes in project |
|
|
285
|
+
| `theme:add` | Create new theme with parent selection |
|
|
286
|
+
| `theme:add-from-template` | Add themes from create-marp-presentation template |
|
|
287
|
+
| `theme:switch -- <name>` | Switch active theme in presentation.md |
|
|
288
|
+
|
|
289
|
+
### npm scripts
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"scripts": {
|
|
294
|
+
"theme:list": "node scripts/theme-cli.js list",
|
|
295
|
+
"theme:add": "node scripts/theme-cli.js add",
|
|
296
|
+
"theme:add-from-template": "node scripts/theme-cli.js add-from-template",
|
|
297
|
+
"theme:switch": "node scripts/theme-cli.js switch"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Command Details
|
|
303
|
+
|
|
304
|
+
#### theme:list
|
|
305
|
+
|
|
306
|
+
Shows all themes in project with dependencies.
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
$ npm run theme:list
|
|
310
|
+
|
|
311
|
+
Themes in project:
|
|
312
|
+
✓ beam (themes/beam/beam.css) extends: default
|
|
313
|
+
✓ marpx (themes/marpx/marpx.css) extends: default
|
|
314
|
+
✓ socrates (themes/marpx/socrates.css) extends: marpx
|
|
315
|
+
|
|
316
|
+
Active theme: beam
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### theme:add
|
|
320
|
+
|
|
321
|
+
Creates new theme with parent and directory selection.
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
$ npm run theme:add
|
|
325
|
+
|
|
326
|
+
? Theme name: my-dark
|
|
327
|
+
? Parent theme:
|
|
328
|
+
❯ none (create from scratch)
|
|
329
|
+
default (system built-in)
|
|
330
|
+
gaia (system built-in)
|
|
331
|
+
marpx (custom)
|
|
332
|
+
|
|
333
|
+
? Where to create the theme CSS file?
|
|
334
|
+
❯ In root (themes/my-dark.css)
|
|
335
|
+
In existing folder: themes/marpx/
|
|
336
|
+
In new folder (enter name)
|
|
337
|
+
|
|
338
|
+
? Folder name: gaia-variants
|
|
339
|
+
|
|
340
|
+
✓ Created themes/gaia-variants/my-dark.css
|
|
341
|
+
✓ Theme registered in VSCode settings
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### theme:add-from-template
|
|
345
|
+
|
|
346
|
+
**Делегирует к `npx create-marp-presentation add-themes`**
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
// template/scripts/theme-cli.js
|
|
350
|
+
case 'add-from-template':
|
|
351
|
+
// Просто вызываем общую команду через npx
|
|
352
|
+
const { execSync } = require('child_process');
|
|
353
|
+
execSync(`npx create-marp-presentation add-themes ${process.cwd()}`, {
|
|
354
|
+
stdio: 'inherit'
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
$ npm run theme:add-from-template
|
|
361
|
+
# → npx create-marp-presentation add-themes $(pwd)
|
|
362
|
+
|
|
363
|
+
? Select themes to add:
|
|
364
|
+
☁ beam
|
|
365
|
+
☁ marpx
|
|
366
|
+
☐ socrates
|
|
367
|
+
|
|
368
|
+
✓ Added 2 themes with dependencies
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Преимущества:**
|
|
372
|
+
- Код добавления тем в одном месте (create-marp-presentation)
|
|
373
|
+
- Обновления автоматически доступны во всех проектах
|
|
374
|
+
- Консистентное поведение при создании проекта и добавлении тем позже
|
|
375
|
+
|
|
376
|
+
#### theme:switch
|
|
377
|
+
|
|
378
|
+
Switch active theme in presentation.md.
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
$ npm run theme:switch beam
|
|
382
|
+
|
|
383
|
+
✓ Active theme changed to 'beam'
|
|
384
|
+
Updated presentation.md frontmatter
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Interactive mode (no argument):
|
|
388
|
+
```
|
|
389
|
+
$ npm run theme:switch
|
|
390
|
+
|
|
391
|
+
? Select active theme:
|
|
392
|
+
❯ beam
|
|
393
|
+
marpx
|
|
394
|
+
socrates
|
|
395
|
+
|
|
396
|
+
✓ Active theme changed to 'beam'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**IMPORTANT:** Only edits `theme` attribute in frontmatter, preserves all other content.
|
|
400
|
+
|
|
401
|
+
## Data Flows
|
|
402
|
+
|
|
403
|
+
### Flow 1: Project Creation with Theme Selection
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
npx create-marp-presentation my-project
|
|
407
|
+
↓
|
|
408
|
+
[index.js]
|
|
409
|
+
↓
|
|
410
|
+
1. Создать директорию проекта
|
|
411
|
+
2. Скопировать template/* → my-project/
|
|
412
|
+
(кроме template/themes/)
|
|
413
|
+
↓
|
|
414
|
+
3. Вызвать addThemesCommand(projectPath)
|
|
415
|
+
┌─────────────────────────────────────────────────────────┐
|
|
416
|
+
│ ThemeResolver.scanDirectory(template/themes/) │
|
|
417
|
+
│ ├── Найти все *.css файлы рекурсивно │
|
|
418
|
+
│ ├── extractThemeName() + extractDependencies() │
|
|
419
|
+
│ └── Вернуть плоский список Theme[] │
|
|
420
|
+
│ │
|
|
421
|
+
│ @inquirer/checkbox: Select themes to add │
|
|
422
|
+
│ Пример: │
|
|
423
|
+
│ ☐ beam │
|
|
424
|
+
│ ☐ marpx │
|
|
425
|
+
│ ☁ socrates ← выбрал пользователь │
|
|
426
|
+
│ ☐ gaia-dark │
|
|
427
|
+
│ │
|
|
428
|
+
│ ThemeResolver.resolveDependencies(selected, all) │
|
|
429
|
+
│ └── socrates → marpx → [socrates, marpx] │
|
|
430
|
+
│ │
|
|
431
|
+
│ Copy themes to project/themes/ │
|
|
432
|
+
│ ├── marpx/* → project/themes/marpx/ │
|
|
433
|
+
│ └── (default НЕ копируется - системная) │
|
|
434
|
+
│ │
|
|
435
|
+
│ VSCodeIntegration.syncThemes() │
|
|
436
|
+
│ └── Обновить .vscode/settings.json │
|
|
437
|
+
└─────────────────────────────────────────────────────────┘
|
|
438
|
+
↓
|
|
439
|
+
4. @inquirer/select: Select active theme
|
|
440
|
+
Choices: только скопированные темы
|
|
441
|
+
↓
|
|
442
|
+
5. ThemeManager.setActiveTheme(themeName)
|
|
443
|
+
└── Обновить presentation.md frontmatter
|
|
444
|
+
↓
|
|
445
|
+
6. npm install
|
|
446
|
+
↓
|
|
447
|
+
Done
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Важно:**
|
|
451
|
+
- Шаг 3 — это та же логика, что в `add-themes` команде
|
|
452
|
+
- `addThemesCommand()` экспортируется и используется:
|
|
453
|
+
- index.js (создание проекта)
|
|
454
|
+
- CLI команда `add-themes` (для theme:add-from-template)
|
|
455
|
+
- **Single Source of Truth** для логики добавления тем
|
|
456
|
+
|
|
457
|
+
### Общая функция: addThemesCommand
|
|
458
|
+
|
|
459
|
+
Используется в Flow 1 (создание проекта) и Flow 3 (theme:add-from-template).
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
addThemesCommand(targetPath, options = {})
|
|
463
|
+
↓
|
|
464
|
+
ThemeResolver.scanDirectory(template/themes/) → Theme[]
|
|
465
|
+
↓
|
|
466
|
+
[Если options.themes не задан]
|
|
467
|
+
@inquirer/checkbox: Select themes → selectedThemes[]
|
|
468
|
+
[Иначе]
|
|
469
|
+
selectedThemes = options.themes
|
|
470
|
+
↓
|
|
471
|
+
ThemeResolver.resolveDependencies(selectedThemes, allThemes) → Theme[]
|
|
472
|
+
↓
|
|
473
|
+
[Проверка конфликтов]
|
|
474
|
+
existingThemes = ThemeResolver.scanDirectory(targetPath/themes/)
|
|
475
|
+
conflicts = findConflicts(themesToCopy, existingThemes)
|
|
476
|
+
↓
|
|
477
|
+
[Если есть конфликты]
|
|
478
|
+
@inquirer/select: "Theme X already exists. Action?"
|
|
479
|
+
- Skip (пропустить)
|
|
480
|
+
- Overwrite (перезаписать)
|
|
481
|
+
- Cancel (отменить всё)
|
|
482
|
+
↓
|
|
483
|
+
Copy to targetPath/themes/
|
|
484
|
+
├── Исключить системные темы (default, gaia, uncover)
|
|
485
|
+
├── Исключить skipped темы
|
|
486
|
+
└── Сохранить структуру папок
|
|
487
|
+
↓
|
|
488
|
+
VSCodeIntegration.syncThemes(themesToCopy)
|
|
489
|
+
↓
|
|
490
|
+
Return: { added: Theme[], skipped: Theme[], dependencies: Theme[] }
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Conflict Handling
|
|
494
|
+
|
|
495
|
+
When a theme already exists in the project:
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
? Theme "beam" already exists at themes/beam/beam.css
|
|
499
|
+
What would you like to do?
|
|
500
|
+
|
|
501
|
+
❯ Skip (keep existing)
|
|
502
|
+
Overwrite (replace with template version)
|
|
503
|
+
Cancel (stop adding themes)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Options:**
|
|
507
|
+
- **Skip:** Keep existing theme, don't copy from template
|
|
508
|
+
- **Overwrite:** Replace existing theme with template version
|
|
509
|
+
- **Cancel:** Stop the entire operation
|
|
510
|
+
|
|
511
|
+
**For multiple conflicts:**
|
|
512
|
+
```
|
|
513
|
+
? 3 themes already exist in your project:
|
|
514
|
+
- beam (themes/beam/)
|
|
515
|
+
- marpx (themes/marpx/)
|
|
516
|
+
- socrates (themes/marpx/socrates.css)
|
|
517
|
+
|
|
518
|
+
? Apply to all conflicts?
|
|
519
|
+
❯ Skip all
|
|
520
|
+
Overwrite all
|
|
521
|
+
Choose for each
|
|
522
|
+
Cancel
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Flow 2: theme:list
|
|
526
|
+
|
|
527
|
+
```
|
|
528
|
+
npm run theme:list
|
|
529
|
+
↓
|
|
530
|
+
ThemeManager.scanThemes()
|
|
531
|
+
↓
|
|
532
|
+
Parse all CSS files in themes/
|
|
533
|
+
↓
|
|
534
|
+
Display theme list with dependencies
|
|
535
|
+
↓
|
|
536
|
+
Show active theme from presentation.md
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Flow 3: theme:add-from-template
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
npm run theme:add-from-template
|
|
543
|
+
↓
|
|
544
|
+
theme-cli.js
|
|
545
|
+
↓
|
|
546
|
+
execSync('npx create-marp-presentation add-themes $(pwd)')
|
|
547
|
+
↓
|
|
548
|
+
[addThemesCommand выполняется]
|
|
549
|
+
↓
|
|
550
|
+
Done
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Реализация в theme-cli.js:**
|
|
554
|
+
```javascript
|
|
555
|
+
case 'add-from-template':
|
|
556
|
+
const { execSync } = require('child_process');
|
|
557
|
+
const projectPath = process.cwd();
|
|
558
|
+
execSync(`npx create-marp-presentation add-themes "${projectPath}"`, {
|
|
559
|
+
stdio: 'inherit'
|
|
560
|
+
});
|
|
561
|
+
break;
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Flow 4: theme:add
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
npm run theme:add
|
|
568
|
+
↓
|
|
569
|
+
promptNewThemeName()
|
|
570
|
+
↓
|
|
571
|
+
ThemeManager.scanThemes() → existing themes
|
|
572
|
+
↓
|
|
573
|
+
promptParentTheme(existing themes)
|
|
574
|
+
↓
|
|
575
|
+
promptDirectoryLocation()
|
|
576
|
+
↓
|
|
577
|
+
[If new folder] promptNewFolderName()
|
|
578
|
+
↓
|
|
579
|
+
Create directory if needed
|
|
580
|
+
↓
|
|
581
|
+
Generate CSS template with @import if parent selected
|
|
582
|
+
↓
|
|
583
|
+
VSCodeIntegration.syncThemes()
|
|
584
|
+
↓
|
|
585
|
+
Done
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Flow 5: theme:switch
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
npm run theme:switch [theme-name]
|
|
592
|
+
↓
|
|
593
|
+
ThemeManager.scanThemes() → available themes
|
|
594
|
+
↓
|
|
595
|
+
[If no arg] @inquirer/select: Choose theme
|
|
596
|
+
↓
|
|
597
|
+
Validate theme exists
|
|
598
|
+
↓
|
|
599
|
+
ThemeManager.setThemeInFrontmatter(themeName)
|
|
600
|
+
↓
|
|
601
|
+
Parse presentation.md frontmatter
|
|
602
|
+
↓
|
|
603
|
+
Find/replace only: theme: old → theme: new
|
|
604
|
+
↓
|
|
605
|
+
Write updated presentation.md
|
|
606
|
+
↓
|
|
607
|
+
Done
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Error Handling
|
|
611
|
+
|
|
612
|
+
### Error Classes
|
|
613
|
+
|
|
614
|
+
```javascript
|
|
615
|
+
class ThemeError extends Error
|
|
616
|
+
|
|
617
|
+
class ThemeNotFoundError extends ThemeError
|
|
618
|
+
class ThemeAlreadyExistsError extends ThemeError
|
|
619
|
+
class PresentationNotFoundError extends ThemeError
|
|
620
|
+
class InvalidCSSError extends ThemeError
|
|
621
|
+
class VSCodeIntegrationError extends ThemeError
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Error Handling Strategy
|
|
625
|
+
|
|
626
|
+
| Component | Error Handling |
|
|
627
|
+
|-----------|----------------|
|
|
628
|
+
| ThemeResolver | Fallback to filename if no /* @theme */ |
|
|
629
|
+
| VSCodeIntegration | Create if missing, backup + recreate if corrupted |
|
|
630
|
+
| ThemeManager | Validate before operations, clear error messages |
|
|
631
|
+
| CLI | User-friendly messages with actionable suggestions |
|
|
632
|
+
|
|
633
|
+
## Testing
|
|
634
|
+
|
|
635
|
+
### Test Structure
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
tests/
|
|
639
|
+
├── unit/
|
|
640
|
+
│ ├── theme-resolver.test.js
|
|
641
|
+
│ ├── vscode-integration.test.js
|
|
642
|
+
│ └── theme-manager.test.js
|
|
643
|
+
├── integration/
|
|
644
|
+
│ └── theme-cli.test.js
|
|
645
|
+
└── fixtures/
|
|
646
|
+
├── themes/
|
|
647
|
+
└── presentations/
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Coverage Targets
|
|
651
|
+
|
|
652
|
+
| Component | Target |
|
|
653
|
+
|-----------|--------|
|
|
654
|
+
| ThemeResolver | 90%+ |
|
|
655
|
+
| VSCodeIntegration | 85%+ |
|
|
656
|
+
| ThemeManager | 90%+ |
|
|
657
|
+
| theme-cli | 80%+ |
|
|
658
|
+
| **Overall** | **85%+** |
|
|
659
|
+
|
|
660
|
+
### Key Test Cases
|
|
661
|
+
|
|
662
|
+
1. **ThemeResolver**
|
|
663
|
+
- Extract theme name from CSS comment
|
|
664
|
+
- Fallback to filename
|
|
665
|
+
- Extract @import dependencies
|
|
666
|
+
- Ignore url() imports
|
|
667
|
+
|
|
668
|
+
2. **VSCodeIntegration**
|
|
669
|
+
- Create settings.json if not exists
|
|
670
|
+
- Merge with existing settings
|
|
671
|
+
- Backup corrupted JSON
|
|
672
|
+
|
|
673
|
+
3. **ThemeManager**
|
|
674
|
+
- Scan all themes in directory
|
|
675
|
+
- Set active theme in frontmatter
|
|
676
|
+
- Create frontmatter if not exists
|
|
677
|
+
- Preserve content when updating theme
|
|
678
|
+
|
|
679
|
+
4. **CLI**
|
|
680
|
+
- All commands execute correctly
|
|
681
|
+
- Interactive mode works
|
|
682
|
+
- Proper error messages
|
|
683
|
+
|
|
684
|
+
5. **addThemesCommand**
|
|
685
|
+
- Select themes from template
|
|
686
|
+
- Resolve dependencies
|
|
687
|
+
- Handle conflicts (skip/overwrite/cancel)
|
|
688
|
+
- Copy themes to project
|
|
689
|
+
- Update VSCode settings
|
|
690
|
+
|
|
691
|
+
6. **Conflict Resolution**
|
|
692
|
+
- Detect existing themes
|
|
693
|
+
- Prompt for action
|
|
694
|
+
- Skip selected themes
|
|
695
|
+
- Overwrite selected themes
|
|
696
|
+
- Cancel operation
|
|
697
|
+
|
|
698
|
+
## Implementation Notes
|
|
699
|
+
|
|
700
|
+
### Dependencies
|
|
701
|
+
|
|
702
|
+
**Root package.json (create-marp-presentation):**
|
|
703
|
+
```json
|
|
704
|
+
{
|
|
705
|
+
"dependencies": {
|
|
706
|
+
"@inquirer/prompts": "^7.0.0"
|
|
707
|
+
},
|
|
708
|
+
"devDependencies": {
|
|
709
|
+
"jest": "^29.7.0"
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**template/package.json (generated project):**
|
|
715
|
+
```json
|
|
716
|
+
{
|
|
717
|
+
"dependencies": {
|
|
718
|
+
"@inquirer/prompts": "^7.0.0",
|
|
719
|
+
"gray-matter": "^4.0.3"
|
|
720
|
+
},
|
|
721
|
+
"devDependencies": {
|
|
722
|
+
"@marp-team/marp-cli": "^4.1.2"
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### File Copying Mechanism
|
|
728
|
+
|
|
729
|
+
Use Node.js built-in `fs.cpSync()` for recursive directory copying:
|
|
730
|
+
|
|
731
|
+
```javascript
|
|
732
|
+
import fs from 'fs';
|
|
733
|
+
import path from 'path';
|
|
734
|
+
|
|
735
|
+
function copyThemeToProject(theme, projectPath) {
|
|
736
|
+
const sourceDir = path.dirname(theme.path);
|
|
737
|
+
const themeDirName = path.basename(sourceDir);
|
|
738
|
+
const targetDir = path.join(projectPath, 'themes', themeDirName);
|
|
739
|
+
|
|
740
|
+
// Copy entire theme directory
|
|
741
|
+
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**For lib/ copying during project creation:**
|
|
746
|
+
```javascript
|
|
747
|
+
function copyLibToProject(projectPath) {
|
|
748
|
+
const libSource = path.join(__dirname, 'lib');
|
|
749
|
+
const libTarget = path.join(projectPath, 'scripts', 'lib');
|
|
750
|
+
fs.cpSync(libSource, libTarget, { recursive: true });
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Finding Template Themes Path
|
|
755
|
+
|
|
756
|
+
```javascript
|
|
757
|
+
// In addThemesCommand
|
|
758
|
+
function getTemplateThemesPath() {
|
|
759
|
+
// When running from index.js
|
|
760
|
+
return path.join(__dirname, '..', 'template', 'themes');
|
|
761
|
+
|
|
762
|
+
// When installed as npm package and run via npx
|
|
763
|
+
// __dirname points to lib/ directory in installed package
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Theme Template CSS
|
|
768
|
+
|
|
769
|
+
**No parent (from scratch):**
|
|
770
|
+
```css
|
|
771
|
+
/* @theme my-theme */
|
|
772
|
+
|
|
773
|
+
:root {
|
|
774
|
+
/* Your theme variables */
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
**With parent:**
|
|
779
|
+
```css
|
|
780
|
+
/* @theme my-theme */
|
|
781
|
+
|
|
782
|
+
@import "default";
|
|
783
|
+
|
|
784
|
+
:root {
|
|
785
|
+
/* Your overrides */
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Custom parent:**
|
|
790
|
+
```css
|
|
791
|
+
/* @theme my-theme */
|
|
792
|
+
|
|
793
|
+
@import "../marpx/marpx.css";
|
|
794
|
+
|
|
795
|
+
:root {
|
|
796
|
+
/* Your overrides */
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Theme Sources
|
|
801
|
+
|
|
802
|
+
Initial themes from `docs/reqs/theme-templates/`:
|
|
803
|
+
- `beam/beam.css` - Beamer-inspired theme
|
|
804
|
+
- `marpx/marpx.css` - Academic theme base
|
|
805
|
+
- `marpx/socrates.css` - Socrates theme extending marpx
|
|
806
|
+
|
|
807
|
+
Plus one theme per system theme:
|
|
808
|
+
- `gaia-dark/dark.css` - Dark variant of gaia
|
|
809
|
+
- `uncover-minimal/minimal.css` - Minimal variant of uncover
|
|
810
|
+
- `default-clean/clean.css` - Clean variant of default
|
|
811
|
+
|
|
812
|
+
## Manual Operations
|
|
813
|
+
|
|
814
|
+
Users can manually:
|
|
815
|
+
|
|
816
|
+
**Add CSS file to theme:**
|
|
817
|
+
```bash
|
|
818
|
+
cp ~/my-styles.css themes/marpx/
|
|
819
|
+
# Then manually add @import to appropriate CSS file
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Remove theme:**
|
|
823
|
+
```bash
|
|
824
|
+
rm -rf themes/beam
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
## Next Steps
|
|
828
|
+
|
|
829
|
+
1. Create implementation plan via `writing-plans` skill
|
|
830
|
+
2. Implement ThemeResolver
|
|
831
|
+
3. Implement VSCodeIntegration
|
|
832
|
+
4. Implement ThemeManager
|
|
833
|
+
5. Implement prompts
|
|
834
|
+
6. Implement theme-cli
|
|
835
|
+
7. Add tests
|
|
836
|
+
8. Update main index.js for project creation flow
|