create-openclass-uniminuto 1.4.0 → 1.4.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.
@@ -1,279 +1,632 @@
1
- /**
2
- * Genera o actualiza una Open Class Slidev a partir de openclass.config.json.
3
- *
4
- * Uso principal:
5
- * npm run config
6
- * node scripts/generar-desde-config.mjs --config config/openclass.config.iot-ejemplo.json --force
7
- */
8
- import fs from "node:fs";
9
- import path from "node:path";
10
-
11
- const args = process.argv.slice(2);
12
-
13
- function valueAfter(flag, fallback) {
14
- const i = args.indexOf(flag);
15
- if (i >= 0 && args[i + 1]) return args[i + 1];
16
- const pair = args.find((arg) => arg.startsWith(`${flag}=`));
17
- if (pair) return pair.slice(flag.length + 1);
18
- return fallback;
19
- }
20
-
21
- const configPath = valueAfter("--config", "openclass.config.json");
22
- const force = args.includes("--force") || args.includes("-f");
23
- const dryRun = args.includes("--dry-run");
24
-
25
- function fail(message) {
26
- console.error(`\n❌ ${message}\n`);
27
- process.exit(1);
28
- }
29
-
30
- function readJson(filePath) {
31
- if (!fs.existsSync(filePath)) fail(`No se encontró el archivo de configuración: ${filePath}`);
32
- try {
33
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
34
- } catch (error) {
35
- fail(`No se pudo leer ${filePath}. Revisa que sea JSON válido.\n${error.message}`);
36
- }
37
- }
38
-
39
- function ensureDir(dir) {
40
- if (!dryRun) fs.mkdirSync(dir, { recursive: true });
41
- }
42
-
43
- function writeFileSafe(filePath, content, { overwrite = true, label = filePath } = {}) {
44
- ensureDir(path.dirname(filePath));
45
-
46
- if (fs.existsSync(filePath) && !overwrite) {
47
- console.log(`⏭ Conservado: ${label}`);
48
- return false;
49
- }
50
-
51
- if (!dryRun) fs.writeFileSync(filePath, content, "utf-8");
52
- console.log(`${dryRun ? "🔎" : "✓"} ${label}`);
53
- return true;
54
- }
55
-
56
- function cleanShortName(value) {
57
- return String(value || "")
58
- .normalize("NFD")
59
- .replace(/[\u0300-\u036f]/g, "")
60
- .toLowerCase()
61
- .replace(/[^a-z0-9]+/g, "")
62
- .trim();
63
- }
64
-
65
- function asArray(value) {
66
- return Array.isArray(value) ? value : [];
67
- }
68
-
69
- function replaceTokens(template, tokens) {
70
- return Object.entries(tokens).reduce(
71
- (content, [key, value]) => content.replaceAll(`{{${key}}}`, String(value ?? "")),
72
- template,
73
- );
74
- }
75
-
76
- function readTemplate(filePath, fallback) {
77
- return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : fallback;
78
- }
79
-
80
- function normalizeBase(value) {
81
- let base = value || "/";
82
- if (!base.startsWith("/")) base = `/${base}`;
83
- if (!base.endsWith("/")) base = `${base}/`;
84
- return base;
85
- }
86
-
87
- const config = readJson(configPath);
88
- const course = config.course || {};
89
- const generation = config.generation || {};
90
-
91
- const courseShort = cleanShortName(course.shortName || config.shortName || config.courseCode || "curso");
92
- const courseName = String(course.fullName || course.name || config.courseName || "Nombre del curso").trim();
93
- const courseYear = String(course.year || config.year || new Date().getFullYear()).trim();
94
- const description = String(course.description || config.description || "Agrega aquí la descripción general del curso.").trim();
95
- const openClassLabel = String(course.openClassLabel || "Open Class").trim();
96
- const weeksTotal = Number(generation.weeksTotal || config.weeksTotal || config.totalWeeks || asArray(config.weeks).length || 8);
97
-
98
- if (!courseShort) fail("El campo course.shortName es obligatorio.");
99
- if (!courseName) fail("El campo course.fullName es obligatorio.");
100
- if (!Number.isInteger(weeksTotal) || weeksTotal < 1) fail("generation.weeksTotal debe ser un número entero mayor o igual a 1.");
101
-
102
- const configuredWeeks = new Map(asArray(config.weeks).map((week) => [Number(week.number), week]));
103
-
104
- function weekInfo(number) {
105
- const week = configuredWeeks.get(number) || {};
106
- const title = String(week.title || `Título semana ${number}`).trim();
107
- return {
108
- number,
109
- title,
110
- date: String(week.date || courseYear).trim(),
111
- centralTheme: String(week.centralTheme || week.theme || title).trim(),
112
- activity: String(week.activity || "Actividad o evaluación relacionada").trim(),
113
- duration: String(week.duration || "90 minutos").trim(),
114
- status: String(week.status || "active").trim(),
115
- };
116
- }
117
-
118
- const allWeeks = Array.from({ length: weeksTotal }, (_, i) => weekInfo(i + 1));
119
- const activeWeeksRaw = asArray(generation.activeWeeks || config.activeWeeks);
120
- const activeWeekNumbers = activeWeeksRaw.length
121
- ? activeWeeksRaw.map(Number).filter((n) => Number.isInteger(n) && n >= 1 && n <= weeksTotal)
122
- : allWeeks.filter((week) => week.status !== "draft" && week.status !== "inactive").map((week) => week.number);
123
-
124
- const activeWeeks = allWeeks.filter((week) => activeWeekNumbers.includes(week.number));
125
- const overwriteLaunchers = generation.overwriteLaunchers !== false || force;
126
- const overwritePortal = generation.overwritePortal !== false || force;
127
- const overwriteDecks = generation.overwriteDecks !== false || force;
128
- const overwritePackageScripts = generation.overwritePackageScripts !== false || force;
129
- const overwriteWeekContent = generation.overwriteWeekContent === true || force;
130
- const exportPortal = generation.exportPortal === true;
131
-
132
- const semanaTemplate = readTemplate(
133
- "plantillas/semana.md",
134
- `---\nlayout: slide-01-portada\n---\n\n::title::\n{{COURSE_NAME}}\n\n::week::\nSemana {{WEEK_NUMBER}} — {{WEEK_TITLE}}\n\n::date::\n{{WEEK_DATE}}\n\n---\nlayout: slide-12-cierre\n---\n`,
135
- );
136
-
137
- const launcherTemplate = readTemplate(
138
- "plantillas/launcher.md",
139
- `---\ntheme: ./theme/uniminuto\ntitle: {{COURSE_NAME}} — Semana {{WEEK_NUMBER}} — {{WEEK_TITLE}}\nfavicon: /favicon.png\ncodeCopy: true\ntransition: fade\nrouterMode: hash\ndrawings:\n persist: false\nsrc: ./semanas/{{COURSE_SHORT}}_semana{{WEEK_NUMBER}}.md\n---\n`,
140
- );
141
-
142
- function weekTokens(week) {
143
- return {
144
- COURSE_SHORT: courseShort,
145
- COURSE_NAME: courseName,
146
- COURSE_YEAR: courseYear,
147
- OPEN_CLASS_LABEL: openClassLabel,
148
- WEEK_NUMBER: week.number,
149
- WEEK_TITLE: week.title,
150
- WEEK_DATE: week.date,
151
- WEEK_THEME: week.centralTheme,
152
- WEEK_ACTIVITY: week.activity,
153
- WEEK_DURATION: week.duration,
154
- };
155
- }
156
-
157
- function portalWeekItem(week) {
158
- const slug = `${courseShort}_semana${week.number}`;
159
- return `### **Semana ${week.number}**\n\n<a href="./semanas/${slug}/#/1" target="_self">${week.title}</a>\n\n<a href="./descargas/${slug}.pdf" download>Descargar PDF</a> · <a href="./descargas/${slug}.pptx" download>Descargar PPTX</a>`;
160
- }
161
-
162
- function buildPortal() {
163
- const left = activeWeeks.length
164
- ? activeWeeks.filter((_, index) => index < Math.ceil(activeWeeks.length / 2)).map(portalWeekItem).join("\n\n")
165
- : "### Sin semanas activas\n\nEjecuta `npm run semana -- 1` para generar la primera semana.";
166
- const right = activeWeeks.filter((_, index) => index >= Math.ceil(activeWeeks.length / 2)).map(portalWeekItem).join("\n\n") || "### Próximamente\n\nActiva más semanas con `npm run semana -- 2`, `npm run semana -- 3` y así sucesivamente.";
167
-
168
- return `---\ntheme: ./theme/uniminuto\ntitle: ${courseName} — ${openClassLabel}\nfavicon: /favicon.png\ncodeCopy: true\ntransition: fade\nrouterMode: hash\ndrawings:\n persist: false\nlayout: slide-01-portada\n---\n\n::title::\n${courseName}\n\n::week::\n${openClassLabel}\n\n::date::\n${courseYear}\n\n---\nlayout: slide-08-titulo-texto\n---\n\n::title::\nDescripción general del curso\n\n::content::\n${description}\n\n---\nlayout: slide-08-titulo-texto\n---\n\n::title::\nRuta de aprendizaje\n\n::content::\nEl curso se organiza en semanas. Cada semana cuenta con un lanzador raíz y un archivo interno en la carpeta \`semanas/\`.\n\nLas presentaciones activas se controlan desde \`openclass.config.json\` mediante el campo \`generation.activeWeeks\`.\n\n---\nlayout: slide-10-titulo-dos-columnas\n---\n\n::title::\nPresentaciones disponibles\n\n::left::\n${left}\n\n::right::\n${right}\n\n---\nlayout: slide-12-cierre\n---\n`;
169
- }
170
-
171
- function buildDecks() {
172
- const entries = [
173
- `function normalizeBase(value) {\n let base = value || "/";\n if (!base.startsWith("/")) base = \`/\${base}\`;\n if (!base.endsWith("/")) base = \`\${base}/\`;\n return base;\n}\n\nconst SITE_BASE = normalizeBase(process.env.SITE_BASE || "/");\n\nfunction withBase(path = "") {\n return \`\${SITE_BASE}\${path.replace(/^\\/+/, "")}\`;\n}\n\nexport const decks = [\n {\n name: "openclass-${courseShort}",\n entry: "slides.md",\n out: "dist",\n base: SITE_BASE,\n exportable: ${exportPortal ? "true" : "false"},\n },`,
174
- ];
175
-
176
- for (const week of activeWeeks) {
177
- const slug = `${courseShort}_semana${week.number}`;
178
- entries.push(` {\n name: "${slug}",\n entry: "${slug}.md",\n out: "dist/semanas/${slug}",\n base: withBase("semanas/${slug}/"),\n exportable: true,\n },`);
179
- }
180
-
181
- entries.push(`];\n`);
182
- return entries.join("\n");
183
- }
184
-
185
- function buildScripts() {
186
- const scripts = {
187
- dev: "slidev slides.md --open --port 3000",
188
- config: "node scripts/generar-desde-config.mjs",
189
- semana: "node scripts/semana.mjs",
190
- "pages:check": "node scripts/preparar-github-pages.mjs",
191
- };
192
-
193
- for (let i = 1; i <= weeksTotal; i++) {
194
- const slug = `${courseShort}_semana${i}`;
195
- scripts[`dev:s${i}`] = `slidev ${slug}.md --open --port ${3000 + i}`;
196
- }
197
-
198
- scripts.clean = "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"";
199
- scripts["clean:downloads"] = "node -e \"require('fs').rmSync('public/descargas',{recursive:true,force:true})\"";
200
- scripts["clean:cache"] = "node -e \"require('fs').rmSync('.slidev',{recursive:true,force:true}); require('fs').rmSync('node_modules/.vite',{recursive:true,force:true})\"";
201
- scripts["build:index"] = "slidev build slides.md --out dist --base / --without-notes";
202
-
203
- for (let i = 1; i <= weeksTotal; i++) {
204
- const slug = `${courseShort}_semana${i}`;
205
- scripts[`build:s${i}`] = `slidev build ${slug}.md --out dist/semanas/${slug} --base /semanas/${slug}/ --without-notes`;
206
- }
207
-
208
- scripts["build:all"] = "node scripts/build-site.mjs";
209
- scripts["build:incremental"] = "node scripts/build-incremental.mjs";
210
- scripts["export:downloads"] = "node scripts/export-downloads.mjs";
211
- scripts["export:incremental"] = "node scripts/export-incremental.mjs";
212
- scripts.vista = "npm run clean && npm run export:downloads && npm run build:all && http-server dist -p 4173 -c-1 -o";
213
- scripts.publicar = "node scripts/publicar.mjs";
214
- scripts["dev:todo"] = "node scripts/dev-all.mjs";
215
- scripts.nuevo = "node scripts/nuevo-curso.mjs";
216
- scripts["zip:template"] = "node scripts/zip-template.mjs";
217
- scripts["preview:static"] = "npm run build:all && http-server dist -p 4173 -c-1";
218
- scripts["preview:pages"] = "npm run export:downloads && npm run build:all && http-server dist -p 4173 -c-1";
219
- scripts["pages:build"] = "npm run export:downloads && npm run build:all";
220
- scripts["pages:preview"] = "npm run preview:pages";
221
-
222
- return scripts;
223
- }
224
-
225
- console.log("\n┌──────────────────────────────────────────────┐");
226
- console.log("│ Generador Open Class UNIMINUTO desde config │");
227
- console.log("└──────────────────────────────────────────────┘\n");
228
- console.log(`Configuración : ${configPath}`);
229
- console.log(`Curso : ${courseName}`);
230
- console.log(`Nombre corto : ${courseShort}`);
231
- console.log(`Semanas : ${weeksTotal}`);
232
- console.log(`Activas : ${activeWeeks.map((w) => w.number).join(", ") || "ninguna"}\n`);
233
-
234
- ensureDir("semanas");
235
- ensureDir("public/descargas");
236
- ensureDir("public/imagenes");
237
- ensureDir("public/videos");
238
-
239
- writeFileSafe("slides.md", buildPortal(), { overwrite: overwritePortal, label: "slides.md" });
240
- writeFileSafe("scripts/decks.mjs", buildDecks(), { overwrite: overwriteDecks, label: "scripts/decks.mjs" });
241
-
242
- for (const week of allWeeks) {
243
- const slug = `${courseShort}_semana${week.number}`;
244
- const tokens = weekTokens(week);
245
- writeFileSafe(`${slug}.md`, replaceTokens(launcherTemplate, tokens), {
246
- overwrite: overwriteLaunchers,
247
- label: `${slug}.md`,
248
- });
249
- writeFileSafe(path.join("semanas", `${slug}.md`), replaceTokens(semanaTemplate, tokens), {
250
- overwrite: overwriteWeekContent,
251
- label: path.join("semanas", `${slug}.md`),
252
- });
253
- }
254
-
255
- if (overwritePackageScripts && fs.existsSync("package.json")) {
256
- const pkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
257
- pkg.name = `openclass-${courseShort}`;
258
- pkg.description = `Presentaciones Open Class del curso ${courseName}.`;
259
- pkg.scripts = buildScripts();
260
- writeFileSafe("package.json", JSON.stringify(pkg, null, 2) + "\n", { overwrite: true, label: "package.json" });
261
- }
262
-
263
- if (fs.existsSync("package-lock.json")) {
264
- try {
265
- const lock = JSON.parse(fs.readFileSync("package-lock.json", "utf-8"));
266
- lock.name = `openclass-${courseShort}`;
267
- if (lock.packages && lock.packages[""]) {
268
- lock.packages[""].name = `openclass-${courseShort}`;
269
- }
270
- writeFileSafe("package-lock.json", JSON.stringify(lock, null, 2) + "\n", { overwrite: true, label: "package-lock.json" });
271
- } catch {
272
- console.log("⚠️ package-lock.json no se actualizó porque no parece ser JSON válido.");
273
- }
274
- }
275
-
276
- console.log("\n✅ Configuración generada.");
277
- console.log(" Próximo paso sugerido: npm install");
278
- console.log(" Vista de una semana: npm run dev:s1");
279
- console.log(" Vista completa: npm run vista\n");
1
+ /**
2
+ * Genera o actualiza una Open Class Slidev a partir de openclass.config.json.
3
+ *
4
+ * Uso principal:
5
+ * npm run config
6
+ <<<<<<< HEAD
7
+ * npm run semana -- 6
8
+ * node scripts/generar-desde-config.mjs --config config/openclass.config.iot-ejemplo.json --force
9
+ *
10
+ * Mejoras:
11
+ * - Mantiene el curso en 8 semanas.
12
+ * - Repara public/favicon.png si no existe.
13
+ * - Elimina demo_semanaX.md cuando el curso ya tiene un nombre corto diferente de "demo".
14
+ * - No sobrescribe el contenido académico ya editado en semanas/*.md salvo con --force.
15
+ =======
16
+ * node scripts/generar-desde-config.mjs --config config/openclass.config.iot-ejemplo.json --force
17
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
18
+ */
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+
22
+ const args = process.argv.slice(2);
23
+
24
+ function valueAfter(flag, fallback) {
25
+ const i = args.indexOf(flag);
26
+ if (i >= 0 && args[i + 1]) return args[i + 1];
27
+ <<<<<<< HEAD
28
+
29
+ const pair = args.find((arg) => arg.startsWith(`${flag}=`));
30
+ if (pair) return pair.slice(flag.length + 1);
31
+
32
+ =======
33
+ const pair = args.find((arg) => arg.startsWith(`${flag}=`));
34
+ if (pair) return pair.slice(flag.length + 1);
35
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
36
+ return fallback;
37
+ }
38
+
39
+ const configPath = valueAfter("--config", "openclass.config.json");
40
+ const force = args.includes("--force") || args.includes("-f");
41
+ const dryRun = args.includes("--dry-run");
42
+ <<<<<<< HEAD
43
+ const forceCleanDemo = args.includes("--clean-demo");
44
+
45
+ /**
46
+ * Favicon PNG mínimo de respaldo.
47
+ *
48
+ * No reemplaza un favicon existente.
49
+ * Solo se usa si public/favicon.png no existe, para evitar que Slidev o GitHub Pages
50
+ * queden apuntando a un recurso inexistente.
51
+ */
52
+ const FALLBACK_FAVICON_PNG_BASE64 =
53
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
54
+ =======
55
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
56
+
57
+ function fail(message) {
58
+ console.error(`\n❌ ${message}\n`);
59
+ process.exit(1);
60
+ }
61
+
62
+ function readJson(filePath) {
63
+ <<<<<<< HEAD
64
+ if (!fs.existsSync(filePath)) {
65
+ fail(`No se encontró el archivo de configuración: ${filePath}`);
66
+ }
67
+
68
+ =======
69
+ if (!fs.existsSync(filePath)) fail(`No se encontró el archivo de configuración: ${filePath}`);
70
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
71
+ try {
72
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
73
+ } catch (error) {
74
+ fail(`No se pudo leer ${filePath}. Revisa que sea JSON válido.\n${error.message}`);
75
+ }
76
+ }
77
+
78
+ function ensureDir(dir) {
79
+ if (!dryRun) fs.mkdirSync(dir, { recursive: true });
80
+ }
81
+
82
+ function writeFileSafe(filePath, content, { overwrite = true, label = filePath } = {}) {
83
+ ensureDir(path.dirname(filePath));
84
+
85
+ if (fs.existsSync(filePath) && !overwrite) {
86
+ console.log(`⏭ Conservado: ${label}`);
87
+ return false;
88
+ }
89
+
90
+ if (!dryRun) fs.writeFileSync(filePath, content, "utf-8");
91
+ console.log(`${dryRun ? "🔎" : ""} ${label}`);
92
+ return true;
93
+ }
94
+
95
+ <<<<<<< HEAD
96
+ function writeBinaryFileIfMissing(filePath, base64Content, { label = filePath } = {}) {
97
+ ensureDir(path.dirname(filePath));
98
+
99
+ if (fs.existsSync(filePath)) {
100
+ console.log(`⏭ Conservado: ${label}`);
101
+ return false;
102
+ }
103
+
104
+ if (!dryRun) {
105
+ fs.writeFileSync(filePath, Buffer.from(base64Content, "base64"));
106
+ }
107
+
108
+ console.log(`${dryRun ? "🔎" : "✓"} ${label} creado como respaldo`);
109
+ return true;
110
+ }
111
+
112
+ function removeFileIfExists(filePath, { label = filePath } = {}) {
113
+ if (!fs.existsSync(filePath)) return false;
114
+
115
+ if (!dryRun) fs.rmSync(filePath, { force: true });
116
+ console.log(`${dryRun ? "🔎" : "🧹"} Eliminado: ${label}`);
117
+ return true;
118
+ }
119
+
120
+ function cleanShortName(value) {
121
+ return (
122
+ String(value || "")
123
+ .normalize("NFD")
124
+ .replace(/[\u0300-\u036f]/g, "")
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9]+/g, "")
127
+ .trim() || "curso"
128
+ );
129
+ =======
130
+ function cleanShortName(value) {
131
+ return String(value || "")
132
+ .normalize("NFD")
133
+ .replace(/[\u0300-\u036f]/g, "")
134
+ .toLowerCase()
135
+ .replace(/[^a-z0-9]+/g, "")
136
+ .trim();
137
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
138
+ }
139
+
140
+ function asArray(value) {
141
+ return Array.isArray(value) ? value : [];
142
+ }
143
+
144
+ function replaceTokens(template, tokens) {
145
+ return Object.entries(tokens).reduce(
146
+ (content, [key, value]) => content.replaceAll(`{{${key}}}`, String(value ?? "")),
147
+ template,
148
+ );
149
+ }
150
+
151
+ function readTemplate(filePath, fallback) {
152
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : fallback;
153
+ }
154
+
155
+ <<<<<<< HEAD
156
+ =======
157
+ function normalizeBase(value) {
158
+ let base = value || "/";
159
+ if (!base.startsWith("/")) base = `/${base}`;
160
+ if (!base.endsWith("/")) base = `${base}/`;
161
+ return base;
162
+ }
163
+
164
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
165
+ const config = readJson(configPath);
166
+ const course = config.course || {};
167
+ const generation = config.generation || {};
168
+
169
+ const courseShort = cleanShortName(course.shortName || config.shortName || config.courseCode || "curso");
170
+ const courseName = String(course.fullName || course.name || config.courseName || "Nombre del curso").trim();
171
+ const courseYear = String(course.year || config.year || new Date().getFullYear()).trim();
172
+ const description = String(course.description || config.description || "Agrega aquí la descripción general del curso.").trim();
173
+ const openClassLabel = String(course.openClassLabel || "Open Class").trim();
174
+ const weeksTotal = Number(generation.weeksTotal || config.weeksTotal || config.totalWeeks || asArray(config.weeks).length || 8);
175
+
176
+ if (!courseShort) fail("El campo course.shortName es obligatorio.");
177
+ if (!courseName) fail("El campo course.fullName es obligatorio.");
178
+ <<<<<<< HEAD
179
+ if (!Number.isInteger(weeksTotal) || weeksTotal < 1) {
180
+ fail("generation.weeksTotal debe ser un número entero mayor o igual a 1.");
181
+ }
182
+ =======
183
+ if (!Number.isInteger(weeksTotal) || weeksTotal < 1) fail("generation.weeksTotal debe ser un número entero mayor o igual a 1.");
184
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
185
+
186
+ const configuredWeeks = new Map(asArray(config.weeks).map((week) => [Number(week.number), week]));
187
+
188
+ function weekInfo(number) {
189
+ const week = configuredWeeks.get(number) || {};
190
+ const title = String(week.title || `Título semana ${number}`).trim();
191
+ <<<<<<< HEAD
192
+
193
+ =======
194
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
195
+ return {
196
+ number,
197
+ title,
198
+ date: String(week.date || courseYear).trim(),
199
+ centralTheme: String(week.centralTheme || week.theme || title).trim(),
200
+ activity: String(week.activity || "Actividad o evaluación relacionada").trim(),
201
+ duration: String(week.duration || "90 minutos").trim(),
202
+ status: String(week.status || "active").trim(),
203
+ };
204
+ }
205
+
206
+ const allWeeks = Array.from({ length: weeksTotal }, (_, i) => weekInfo(i + 1));
207
+ <<<<<<< HEAD
208
+
209
+ =======
210
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
211
+ const activeWeeksRaw = asArray(generation.activeWeeks || config.activeWeeks);
212
+ const activeWeekNumbers = activeWeeksRaw.length
213
+ ? activeWeeksRaw.map(Number).filter((n) => Number.isInteger(n) && n >= 1 && n <= weeksTotal)
214
+ : allWeeks.filter((week) => week.status !== "draft" && week.status !== "inactive").map((week) => week.number);
215
+
216
+ const activeWeeks = allWeeks.filter((week) => activeWeekNumbers.includes(week.number));
217
+ <<<<<<< HEAD
218
+
219
+ =======
220
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
221
+ const overwriteLaunchers = generation.overwriteLaunchers !== false || force;
222
+ const overwritePortal = generation.overwritePortal !== false || force;
223
+ const overwriteDecks = generation.overwriteDecks !== false || force;
224
+ const overwritePackageScripts = generation.overwritePackageScripts !== false || force;
225
+ const overwriteWeekContent = generation.overwriteWeekContent === true || force;
226
+ const exportPortal = generation.exportPortal === true;
227
+
228
+ <<<<<<< HEAD
229
+ /**
230
+ * Por defecto se eliminan los archivos demo cuando el curso ya no se llama "demo".
231
+ * Puede desactivarse desde openclass.config.json:
232
+ *
233
+ * "generation": {
234
+ * "cleanDemoFiles": false
235
+ * }
236
+ */
237
+ const cleanDemoFiles = generation.cleanDemoFiles !== false;
238
+
239
+ const semanaTemplate = readTemplate(
240
+ "plantillas/semana.md",
241
+ `---
242
+ layout: slide-01-portada
243
+ ---
244
+
245
+ ::title::
246
+ {{COURSE_NAME}}
247
+
248
+ ::week::
249
+ Semana {{WEEK_NUMBER}} {{WEEK_TITLE}}
250
+
251
+ ::date::
252
+ {{WEEK_DATE}}
253
+
254
+ ---
255
+ layout: slide-12-cierre
256
+ ---
257
+ `,
258
+ =======
259
+ const semanaTemplate = readTemplate(
260
+ "plantillas/semana.md",
261
+ `---\nlayout: slide-01-portada\n---\n\n::title::\n{{COURSE_NAME}}\n\n::week::\nSemana {{WEEK_NUMBER}} — {{WEEK_TITLE}}\n\n::date::\n{{WEEK_DATE}}\n\n---\nlayout: slide-12-cierre\n---\n`,
262
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
263
+ );
264
+
265
+ const launcherTemplate = readTemplate(
266
+ "plantillas/launcher.md",
267
+ <<<<<<< HEAD
268
+ `---
269
+ theme: ./theme/uniminuto
270
+ title: {{COURSE_NAME}} Semana {{WEEK_NUMBER}} {{WEEK_TITLE}}
271
+ favicon: /favicon.png
272
+ codeCopy: true
273
+ transition: fade
274
+ routerMode: hash
275
+ drawings:
276
+ persist: false
277
+ src: ./semanas/{{COURSE_SHORT}}_semana{{WEEK_NUMBER}}.md
278
+ ---
279
+ `,
280
+ =======
281
+ `---\ntheme: ./theme/uniminuto\ntitle: {{COURSE_NAME}} — Semana {{WEEK_NUMBER}} — {{WEEK_TITLE}}\nfavicon: /favicon.png\ncodeCopy: true\ntransition: fade\nrouterMode: hash\ndrawings:\n persist: false\nsrc: ./semanas/{{COURSE_SHORT}}_semana{{WEEK_NUMBER}}.md\n---\n`,
282
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
283
+ );
284
+
285
+ function weekTokens(week) {
286
+ return {
287
+ COURSE_SHORT: courseShort,
288
+ COURSE_NAME: courseName,
289
+ COURSE_YEAR: courseYear,
290
+ OPEN_CLASS_LABEL: openClassLabel,
291
+ WEEK_NUMBER: week.number,
292
+ WEEK_TITLE: week.title,
293
+ WEEK_DATE: week.date,
294
+ WEEK_THEME: week.centralTheme,
295
+ WEEK_ACTIVITY: week.activity,
296
+ WEEK_DURATION: week.duration,
297
+ };
298
+ }
299
+
300
+ function portalWeekItem(week) {
301
+ const slug = `${courseShort}_semana${week.number}`;
302
+ <<<<<<< HEAD
303
+
304
+ return `### **Semana ${week.number}**
305
+
306
+ <a href="./semanas/${slug}/#/1" target="_self">${week.title}</a>
307
+
308
+ <a href="./descargas/${slug}.pdf" download>Descargar PDF</a> · <a href="./descargas/${slug}.pptx" download>Descargar PPTX</a>`;
309
+ =======
310
+ return `### **Semana ${week.number}**\n\n<a href="./semanas/${slug}/#/1" target="_self">${week.title}</a>\n\n<a href="./descargas/${slug}.pdf" download>Descargar PDF</a> · <a href="./descargas/${slug}.pptx" download>Descargar PPTX</a>`;
311
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
312
+ }
313
+
314
+ function buildPortal() {
315
+ const left = activeWeeks.length
316
+ <<<<<<< HEAD
317
+ ? activeWeeks
318
+ .filter((_, index) => index < Math.ceil(activeWeeks.length / 2))
319
+ .map(portalWeekItem)
320
+ .join("\n\n")
321
+ : "### Sin semanas activas\n\nEjecuta `npm run semana -- 1` para generar la primera semana.";
322
+
323
+ const right =
324
+ activeWeeks
325
+ .filter((_, index) => index >= Math.ceil(activeWeeks.length / 2))
326
+ .map(portalWeekItem)
327
+ .join("\n\n") ||
328
+ "### Próximamente\n\nActiva más semanas con `npm run semana -- 2`, `npm run semana -- 3` y así sucesivamente.";
329
+
330
+ return `---
331
+ theme: ./theme/uniminuto
332
+ title: ${courseName} — ${openClassLabel}
333
+ favicon: /favicon.png
334
+ codeCopy: true
335
+ transition: fade
336
+ routerMode: hash
337
+ drawings:
338
+ persist: false
339
+ layout: slide-01-portada
340
+ ---
341
+
342
+ ::title::
343
+ ${courseName}
344
+
345
+ ::week::
346
+ ${openClassLabel}
347
+
348
+ ::date::
349
+ ${courseYear}
350
+
351
+ ---
352
+ layout: slide-08-titulo-texto
353
+ ---
354
+
355
+ ::title::
356
+ Descripción general del curso
357
+
358
+ ::content::
359
+ ${description}
360
+
361
+ ---
362
+ layout: slide-08-titulo-texto
363
+ ---
364
+
365
+ ::title::
366
+ Ruta de aprendizaje
367
+
368
+ ::content::
369
+ El curso se organiza en semanas. Cada semana cuenta con un lanzador raíz y un archivo interno en la carpeta \`semanas/\`.
370
+
371
+ Las presentaciones activas se controlan desde \`openclass.config.json\` mediante el campo \`generation.activeWeeks\`.
372
+
373
+ ---
374
+ layout: slide-10-titulo-dos-columnas
375
+ ---
376
+
377
+ ::title::
378
+ Presentaciones disponibles
379
+
380
+ ::left::
381
+ ${left}
382
+
383
+ ::right::
384
+ ${right}
385
+
386
+ ---
387
+ layout: slide-12-cierre
388
+ ---
389
+ `;
390
+ =======
391
+ ? activeWeeks.filter((_, index) => index < Math.ceil(activeWeeks.length / 2)).map(portalWeekItem).join("\n\n")
392
+ : "### Sin semanas activas\n\nEjecuta `npm run semana -- 1` para generar la primera semana.";
393
+ const right = activeWeeks.filter((_, index) => index >= Math.ceil(activeWeeks.length / 2)).map(portalWeekItem).join("\n\n") || "### Próximamente\n\nActiva más semanas con `npm run semana -- 2`, `npm run semana -- 3` y así sucesivamente.";
394
+
395
+ return `---\ntheme: ./theme/uniminuto\ntitle: ${courseName} — ${openClassLabel}\nfavicon: /favicon.png\ncodeCopy: true\ntransition: fade\nrouterMode: hash\ndrawings:\n persist: false\nlayout: slide-01-portada\n---\n\n::title::\n${courseName}\n\n::week::\n${openClassLabel}\n\n::date::\n${courseYear}\n\n---\nlayout: slide-08-titulo-texto\n---\n\n::title::\nDescripción general del curso\n\n::content::\n${description}\n\n---\nlayout: slide-08-titulo-texto\n---\n\n::title::\nRuta de aprendizaje\n\n::content::\nEl curso se organiza en semanas. Cada semana cuenta con un lanzador raíz y un archivo interno en la carpeta \`semanas/\`.\n\nLas presentaciones activas se controlan desde \`openclass.config.json\` mediante el campo \`generation.activeWeeks\`.\n\n---\nlayout: slide-10-titulo-dos-columnas\n---\n\n::title::\nPresentaciones disponibles\n\n::left::\n${left}\n\n::right::\n${right}\n\n---\nlayout: slide-12-cierre\n---\n`;
396
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
397
+ }
398
+
399
+ function buildDecks() {
400
+ const entries = [
401
+ <<<<<<< HEAD
402
+ `function normalizeBase(value) {
403
+ let base = value || "/";
404
+ if (!base.startsWith("/")) base = \`/\${base}\`;
405
+ if (!base.endsWith("/")) base = \`\${base}/\`;
406
+ return base;
407
+ }
408
+
409
+ const SITE_BASE = normalizeBase(process.env.SITE_BASE || "/");
410
+
411
+ function withBase(path = "") {
412
+ return \`\${SITE_BASE}\${path.replace(/^\\/+/, "")}\`;
413
+ }
414
+
415
+ export const decks = [
416
+ {
417
+ name: "openclass-${courseShort}",
418
+ entry: "slides.md",
419
+ out: "dist",
420
+ base: SITE_BASE,
421
+ exportable: ${exportPortal ? "true" : "false"},
422
+ },`,
423
+ =======
424
+ `function normalizeBase(value) {\n let base = value || "/";\n if (!base.startsWith("/")) base = \`/\${base}\`;\n if (!base.endsWith("/")) base = \`\${base}/\`;\n return base;\n}\n\nconst SITE_BASE = normalizeBase(process.env.SITE_BASE || "/");\n\nfunction withBase(path = "") {\n return \`\${SITE_BASE}\${path.replace(/^\\/+/, "")}\`;\n}\n\nexport const decks = [\n {\n name: "openclass-${courseShort}",\n entry: "slides.md",\n out: "dist",\n base: SITE_BASE,\n exportable: ${exportPortal ? "true" : "false"},\n },`,
425
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
426
+ ];
427
+
428
+ for (const week of activeWeeks) {
429
+ const slug = `${courseShort}_semana${week.number}`;
430
+ <<<<<<< HEAD
431
+
432
+ entries.push(` {
433
+ name: "${slug}",
434
+ entry: "${slug}.md",
435
+ out: "dist/semanas/${slug}",
436
+ base: withBase("semanas/${slug}/"),
437
+ exportable: true,
438
+ },`);
439
+ =======
440
+ entries.push(` {\n name: "${slug}",\n entry: "${slug}.md",\n out: "dist/semanas/${slug}",\n base: withBase("semanas/${slug}/"),\n exportable: true,\n },`);
441
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
442
+ }
443
+
444
+ entries.push(`];\n`);
445
+ return entries.join("\n");
446
+ }
447
+
448
+ function buildScripts() {
449
+ const scripts = {
450
+ dev: "slidev slides.md --open --port 3000",
451
+ config: "node scripts/generar-desde-config.mjs",
452
+ semana: "node scripts/semana.mjs",
453
+ "pages:check": "node scripts/preparar-github-pages.mjs",
454
+ };
455
+
456
+ for (let i = 1; i <= weeksTotal; i++) {
457
+ const slug = `${courseShort}_semana${i}`;
458
+ scripts[`dev:s${i}`] = `slidev ${slug}.md --open --port ${3000 + i}`;
459
+ }
460
+
461
+ scripts.clean = "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"";
462
+ <<<<<<< HEAD
463
+ scripts["clean:downloads"] = "node -e \"require('fs').rmSync('public/descargas',{recursive:true,force:true}); require('fs').mkdirSync('public/descargas',{recursive:true})\"";
464
+ =======
465
+ scripts["clean:downloads"] = "node -e \"require('fs').rmSync('public/descargas',{recursive:true,force:true})\"";
466
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
467
+ scripts["clean:cache"] = "node -e \"require('fs').rmSync('.slidev',{recursive:true,force:true}); require('fs').rmSync('node_modules/.vite',{recursive:true,force:true})\"";
468
+ scripts["build:index"] = "slidev build slides.md --out dist --base / --without-notes";
469
+
470
+ for (let i = 1; i <= weeksTotal; i++) {
471
+ const slug = `${courseShort}_semana${i}`;
472
+ scripts[`build:s${i}`] = `slidev build ${slug}.md --out dist/semanas/${slug} --base /semanas/${slug}/ --without-notes`;
473
+ }
474
+
475
+ scripts["build:all"] = "node scripts/build-site.mjs";
476
+ scripts["build:incremental"] = "node scripts/build-incremental.mjs";
477
+ scripts["export:downloads"] = "node scripts/export-downloads.mjs";
478
+ scripts["export:incremental"] = "node scripts/export-incremental.mjs";
479
+ scripts.vista = "npm run clean && npm run export:downloads && npm run build:all && http-server dist -p 4173 -c-1 -o";
480
+ scripts.publicar = "node scripts/publicar.mjs";
481
+ scripts["dev:todo"] = "node scripts/dev-all.mjs";
482
+ scripts.nuevo = "node scripts/nuevo-curso.mjs";
483
+ scripts["zip:template"] = "node scripts/zip-template.mjs";
484
+ scripts["preview:static"] = "npm run build:all && http-server dist -p 4173 -c-1";
485
+ scripts["preview:pages"] = "npm run export:downloads && npm run build:all && http-server dist -p 4173 -c-1";
486
+ scripts["pages:build"] = "npm run export:downloads && npm run build:all";
487
+ scripts["pages:preview"] = "npm run preview:pages";
488
+
489
+ return scripts;
490
+ }
491
+
492
+ <<<<<<< HEAD
493
+ function ensureProjectAssets() {
494
+ ensureDir("public");
495
+ ensureDir("public/descargas");
496
+ ensureDir("public/imagenes");
497
+ ensureDir("public/videos");
498
+
499
+ writeBinaryFileIfMissing("public/favicon.png", FALLBACK_FAVICON_PNG_BASE64, {
500
+ label: "public/favicon.png",
501
+ });
502
+ }
503
+
504
+ function cleanLegacyDemoFiles() {
505
+ const shouldClean = forceCleanDemo || (cleanDemoFiles && courseShort !== "demo");
506
+
507
+ if (!shouldClean) {
508
+ console.log("⏭ Archivos demo conservados.");
509
+ return;
510
+ }
511
+
512
+ for (let i = 1; i <= 8; i++) {
513
+ removeFileIfExists(`demo_semana${i}.md`, {
514
+ label: `demo_semana${i}.md`,
515
+ });
516
+
517
+ removeFileIfExists(path.join("semanas", `demo_semana${i}.md`), {
518
+ label: path.join("semanas", `demo_semana${i}.md`),
519
+ });
520
+ }
521
+ }
522
+
523
+ =======
524
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
525
+ console.log("\n┌──────────────────────────────────────────────┐");
526
+ console.log("│ Generador Open Class UNIMINUTO desde config │");
527
+ console.log("└──────────────────────────────────────────────┘\n");
528
+ console.log(`Configuración : ${configPath}`);
529
+ console.log(`Curso : ${courseName}`);
530
+ console.log(`Nombre corto : ${courseShort}`);
531
+ console.log(`Semanas : ${weeksTotal}`);
532
+ console.log(`Activas : ${activeWeeks.map((w) => w.number).join(", ") || "ninguna"}\n`);
533
+
534
+ ensureDir("semanas");
535
+ <<<<<<< HEAD
536
+ ensureProjectAssets();
537
+ cleanLegacyDemoFiles();
538
+
539
+ writeFileSafe("slides.md", buildPortal(), {
540
+ overwrite: overwritePortal,
541
+ label: "slides.md",
542
+ });
543
+
544
+ writeFileSafe("scripts/decks.mjs", buildDecks(), {
545
+ overwrite: overwriteDecks,
546
+ label: "scripts/decks.mjs",
547
+ });
548
+ =======
549
+ ensureDir("public/descargas");
550
+ ensureDir("public/imagenes");
551
+ ensureDir("public/videos");
552
+
553
+ writeFileSafe("slides.md", buildPortal(), { overwrite: overwritePortal, label: "slides.md" });
554
+ writeFileSafe("scripts/decks.mjs", buildDecks(), { overwrite: overwriteDecks, label: "scripts/decks.mjs" });
555
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
556
+
557
+ for (const week of allWeeks) {
558
+ const slug = `${courseShort}_semana${week.number}`;
559
+ const tokens = weekTokens(week);
560
+ <<<<<<< HEAD
561
+
562
+ =======
563
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
564
+ writeFileSafe(`${slug}.md`, replaceTokens(launcherTemplate, tokens), {
565
+ overwrite: overwriteLaunchers,
566
+ label: `${slug}.md`,
567
+ });
568
+ <<<<<<< HEAD
569
+
570
+ =======
571
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
572
+ writeFileSafe(path.join("semanas", `${slug}.md`), replaceTokens(semanaTemplate, tokens), {
573
+ overwrite: overwriteWeekContent,
574
+ label: path.join("semanas", `${slug}.md`),
575
+ });
576
+ }
577
+
578
+ if (overwritePackageScripts && fs.existsSync("package.json")) {
579
+ const pkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
580
+ <<<<<<< HEAD
581
+
582
+ pkg.name = `openclass-${courseShort}`;
583
+ pkg.description = `Presentaciones Open Class del curso ${courseName}.`;
584
+ pkg.scripts = buildScripts();
585
+
586
+ writeFileSafe("package.json", JSON.stringify(pkg, null, 2) + "\n", {
587
+ overwrite: true,
588
+ label: "package.json",
589
+ });
590
+ =======
591
+ pkg.name = `openclass-${courseShort}`;
592
+ pkg.description = `Presentaciones Open Class del curso ${courseName}.`;
593
+ pkg.scripts = buildScripts();
594
+ writeFileSafe("package.json", JSON.stringify(pkg, null, 2) + "\n", { overwrite: true, label: "package.json" });
595
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
596
+ }
597
+
598
+ if (fs.existsSync("package-lock.json")) {
599
+ try {
600
+ const lock = JSON.parse(fs.readFileSync("package-lock.json", "utf-8"));
601
+ <<<<<<< HEAD
602
+
603
+ =======
604
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
605
+ lock.name = `openclass-${courseShort}`;
606
+ if (lock.packages && lock.packages[""]) {
607
+ lock.packages[""].name = `openclass-${courseShort}`;
608
+ }
609
+ <<<<<<< HEAD
610
+
611
+ writeFileSafe("package-lock.json", JSON.stringify(lock, null, 2) + "\n", {
612
+ overwrite: true,
613
+ label: "package-lock.json",
614
+ });
615
+ =======
616
+ writeFileSafe("package-lock.json", JSON.stringify(lock, null, 2) + "\n", { overwrite: true, label: "package-lock.json" });
617
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33
618
+ } catch {
619
+ console.log("⚠️ package-lock.json no se actualizó porque no parece ser JSON válido.");
620
+ }
621
+ }
622
+
623
+ console.log("\n✅ Configuración generada.");
624
+ <<<<<<< HEAD
625
+ console.log(" Vista de una semana: npm run dev:s1");
626
+ console.log(" Vista completa: npm run vista");
627
+ console.log(" Exportar PDF/PPTX: npm run export:downloads\n");
628
+ =======
629
+ console.log(" Próximo paso sugerido: npm install");
630
+ console.log(" Vista de una semana: npm run dev:s1");
631
+ console.log(" Vista completa: npm run vista\n");
632
+ >>>>>>> 55a9d4cb0e83dc9640a84aea0dc007a5379aba33