@learnpack/learnpack 5.0.275 → 5.0.277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/publish.js +5 -10
  8. package/lib/commands/serve.js +55 -2
  9. package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/api.d.ts +1 -1
  12. package/lib/utils/api.js +12 -9
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/lib/utils/export/epub.d.ts +2 -0
  15. package/lib/utils/export/epub.js +298 -0
  16. package/lib/utils/export/index.d.ts +3 -0
  17. package/lib/utils/export/index.js +7 -0
  18. package/lib/utils/export/scorm.d.ts +2 -0
  19. package/lib/utils/export/scorm.js +84 -0
  20. package/lib/utils/export/shared.d.ts +4 -0
  21. package/lib/utils/export/shared.js +61 -0
  22. package/lib/utils/export/types.d.ts +15 -0
  23. package/lib/utils/export/types.js +2 -0
  24. package/package.json +2 -1
  25. package/src/commands/audit.ts +487 -487
  26. package/src/commands/breakToken.ts +67 -67
  27. package/src/commands/clean.ts +30 -30
  28. package/src/commands/init.ts +650 -650
  29. package/src/commands/logout.ts +38 -38
  30. package/src/commands/publish.ts +20 -25
  31. package/src/commands/serve.ts +69 -4
  32. package/src/commands/start.ts +333 -333
  33. package/src/commands/translate.ts +123 -123
  34. package/src/creator/README.md +54 -54
  35. package/src/creator/eslint.config.js +7 -7
  36. package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
  37. package/src/creator/src/i18n.ts +28 -28
  38. package/src/creator/src/index.css +217 -217
  39. package/src/creator/src/locales/en.json +126 -126
  40. package/src/creator/src/locales/es.json +126 -126
  41. package/src/creator/src/utils/configTypes.ts +122 -122
  42. package/src/creator/src/utils/constants.ts +13 -13
  43. package/src/creator/src/utils/creatorUtils.ts +46 -46
  44. package/src/creator/src/utils/eventBus.ts +2 -2
  45. package/src/creator/src/utils/lib.ts +468 -468
  46. package/src/creator/src/utils/socket.ts +61 -61
  47. package/src/creator/src/utils/store.ts +222 -222
  48. package/src/creator/src/vite-env.d.ts +1 -1
  49. package/src/creator/vite.config.ts +13 -13
  50. package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  51. package/src/managers/config/defaults.ts +49 -49
  52. package/src/managers/config/exercise.ts +364 -364
  53. package/src/managers/config/index.ts +775 -775
  54. package/src/managers/file.ts +236 -236
  55. package/src/managers/server/routes.ts +554 -554
  56. package/src/managers/session.ts +182 -182
  57. package/src/managers/telemetry.ts +188 -188
  58. package/src/models/action.ts +13 -13
  59. package/src/models/config-manager.ts +28 -28
  60. package/src/models/config.ts +106 -106
  61. package/src/models/creator.ts +47 -47
  62. package/src/models/exercise-obj.ts +30 -30
  63. package/src/models/session.ts +39 -39
  64. package/src/models/socket.ts +61 -61
  65. package/src/models/status.ts +16 -16
  66. package/src/ui/_app/app.css +1 -1
  67. package/src/ui/_app/app.js +400 -397
  68. package/src/ui/app.tar.gz +0 -0
  69. package/src/utils/BaseCommand.ts +56 -56
  70. package/src/utils/api.ts +53 -39
  71. package/src/utils/audit.ts +392 -392
  72. package/src/utils/checkNotInstalled.ts +267 -267
  73. package/src/utils/configBuilder.ts +82 -82
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -0
  77. package/src/utils/export/epub.ts +400 -0
  78. package/src/utils/export/index.ts +3 -0
  79. package/src/utils/export/scorm.ts +121 -0
  80. package/src/utils/export/shared.ts +61 -0
  81. package/src/utils/export/types.ts +17 -0
  82. package/src/utils/incrementVersion.js +74 -74
  83. package/src/utils/misc.ts +58 -58
  84. package/src/utils/rigoActions.ts +500 -500
  85. package/src/utils/sidebarGenerator.ts +195 -195
  86. package/src/utils/templates/epub/epub.css +133 -0
  87. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  88. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  89. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -0
  90. package/src/utils/templates/scorm/config/api.js +175 -0
  91. package/src/utils/templates/scorm/config/index.html +210 -0
  92. package/src/utils/templates/scorm/ims_xml.xsd +1 -0
  93. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -0
  94. package/src/utils/templates/scorm/imsmanifest.xml +38 -0
  95. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -0
@@ -0,0 +1,178 @@
1
+ # Export Utilities
2
+
3
+ This directory contains utilities for exporting courses to different formats.
4
+
5
+ ## Supported Formats
6
+
7
+ - **SCORM**: Learning Management System compatible package
8
+ - **EPUB**: E-book format for offline reading
9
+
10
+ ## Usage
11
+
12
+ ### API Endpoints
13
+
14
+ The export functionality is available through the `/export/:course_slug/:format` endpoint:
15
+
16
+ - `GET /export/{course_slug}/scorm` - Export course to SCORM format
17
+ - `GET /export/{course_slug}/epub` - Export course to EPUB format
18
+
19
+ ### Example Usage
20
+
21
+ ```bash
22
+ # Export course to SCORM
23
+ curl -O "http://localhost:3000/export/my-course/scorm"
24
+
25
+ # Export course to EPUB (default English)
26
+ curl -O "http://localhost:3000/export/my-course/epub"
27
+
28
+ # Export course to EPUB in Spanish
29
+ curl -O "http://localhost:3000/export/my-course/epub?language=es"
30
+
31
+ # Export course to EPUB in French
32
+ curl -O "http://localhost:3000/export/my-course/epub?language=fr"
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ ### SCORM Export
38
+
39
+ - No additional requirements
40
+
41
+ ### EPUB Export
42
+
43
+ - **Pandoc** must be installed on the system
44
+ - Pandoc is used to convert markdown to HTML and generate EPUB files
45
+ - **Language Support**: Use the `language` query parameter to specify the language
46
+ - Default: `en` (English)
47
+ - The system will prioritize language-specific files (e.g., `README.es.md` for Spanish)
48
+ - Falls back to `README.md` if no language-specific file is found
49
+
50
+ ### Installing Pandoc
51
+
52
+ #### Windows
53
+
54
+ ```bash
55
+ # Using Chocolatey
56
+ choco install pandoc
57
+
58
+ # Using Scoop
59
+ scoop install pandoc
60
+
61
+ # Or download from https://pandoc.org/installing.html
62
+ ```
63
+
64
+ #### macOS
65
+
66
+ ```bash
67
+ # Using Homebrew
68
+ brew install pandoc
69
+
70
+ # Using MacPorts
71
+ sudo port install pandoc
72
+ ```
73
+
74
+ #### Linux
75
+
76
+ ```bash
77
+ # Ubuntu/Debian
78
+ sudo apt-get install pandoc
79
+
80
+ # CentOS/RHEL
81
+ sudo yum install pandoc
82
+
83
+ # Arch Linux
84
+ sudo pacman -S pandoc
85
+ ```
86
+
87
+ ## Architecture
88
+
89
+ The export system is modular and consists of:
90
+
91
+ - **`types.ts`**: Type definitions for export options and course metadata
92
+ - **`shared.ts`**: Common utility functions used by all export formats
93
+ - **`scorm.ts`**: SCORM export implementation
94
+ - **`epub.ts`**: EPUB export implementation using Pandoc
95
+ - **`index.ts`**: Main export interface
96
+
97
+ ## Adding New Export Formats
98
+
99
+ To add a new export format:
100
+
101
+ 1. Create a new file in the export directory (e.g., `pdf.ts`)
102
+ 2. Implement the export function following the same pattern
103
+ 3. Add the new format to the `ExportFormat` type in `types.ts`
104
+ 4. Update the main export endpoint in `serve.ts`
105
+ 5. Add the new format to the export index
106
+
107
+ ### Example New Format
108
+
109
+ ```typescript
110
+ // pdf.ts
111
+ export async function exportToPdf(options: ExportOptions): Promise<string> {
112
+ // Implementation here
113
+ return outputPath;
114
+ }
115
+
116
+ // types.ts
117
+ export type ExportFormat = 'scorm' | 'epub' | 'pdf';
118
+
119
+ // serve.ts
120
+ } else if (format === "pdf") {
121
+ outputPath = await exportToPdf({
122
+ courseSlug: course_slug,
123
+ format: "pdf",
124
+ bucket,
125
+ outDir: path.join(__dirname, "../output/directory")
126
+ });
127
+ filename = `${course_slug}.pdf`;
128
+ }
129
+ ```
130
+
131
+ ## File Structure
132
+
133
+ ```
134
+ src/utils/export/
135
+ ├── index.ts # Main export interface
136
+ ├── types.ts # Type definitions
137
+ ├── shared.ts # Shared utilities
138
+ ├── scorm.ts # SCORM export
139
+ ├── epub.ts # EPUB export
140
+ ├── templates/ # Export templates
141
+ │ ├── scorm/ # SCORM template files
142
+ │ └── epub/ # EPUB template files
143
+ └── README.md # This file
144
+ ```
145
+
146
+ ## Templates
147
+
148
+ ### SCORM Template
149
+
150
+ Contains the necessary files for a SCORM package:
151
+
152
+ - `imsmanifest.xml` - Course manifest
153
+ - `config/` - Configuration files
154
+ - XSD schema files for validation
155
+
156
+ ### EPUB Template
157
+
158
+ Contains styling and assets for EPUB generation:
159
+
160
+ - `epub.css` - Basic EPUB styling
161
+ - `cover.png` - Default cover image (placeholder)
162
+
163
+ ## Error Handling
164
+
165
+ The export system includes comprehensive error handling:
166
+
167
+ - Invalid format validation
168
+ - File system operation errors
169
+ - Pandoc execution errors (for EPUB)
170
+ - Network/download errors
171
+ - Cleanup on failure
172
+
173
+ ## Performance Considerations
174
+
175
+ - Temporary files are automatically cleaned up
176
+ - Large courses may take time to process
177
+ - Consider implementing progress indicators for long-running exports
178
+ - Memory usage scales with course size
@@ -0,0 +1,400 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as mkdirp from "mkdirp";
4
+ import * as rimraf from "rimraf";
5
+ import { v4 as uuidv4 } from "uuid";
6
+ import { spawn } from "child_process";
7
+ import { ExportOptions, CourseMetadata } from "./types";
8
+ import { downloadS3Folder, getCourseMetadata } from "./shared";
9
+
10
+ // Convert markdown to HTML using pandoc
11
+ async function convertMarkdownToHtml(
12
+ markdownPath: string,
13
+ outputPath: string
14
+ ): Promise<void> {
15
+ return new Promise((resolve, reject) => {
16
+ let stdout = "";
17
+ let stderr = "";
18
+
19
+ const pandocArgs = [
20
+ markdownPath,
21
+ "-o",
22
+ outputPath,
23
+ "--from",
24
+ "markdown",
25
+ "--to",
26
+ "html",
27
+ "--standalone",
28
+ "--css",
29
+ path.join(__dirname, "../templates/epub/epub.css"),
30
+ ];
31
+
32
+ console.log("Executing pandoc command:", "pandoc", pandocArgs.join(" "));
33
+ console.log("Input file exists:", fs.existsSync(markdownPath));
34
+ console.log(
35
+ "Output directory exists:",
36
+ fs.existsSync(path.dirname(outputPath))
37
+ );
38
+
39
+ const pandoc = spawn("pandoc", pandocArgs);
40
+
41
+ pandoc.stdout.on("data", (data) => {
42
+ stdout += data.toString();
43
+ });
44
+
45
+ pandoc.stderr.on("data", (data) => {
46
+ stderr += data.toString();
47
+ });
48
+
49
+ pandoc.on("close", (code) => {
50
+ if (code === 0) {
51
+ resolve();
52
+ } else {
53
+ const errorMessage = `Pandoc process exited with code ${code}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
54
+ console.error("Pandoc Error Details:", errorMessage);
55
+ reject(new Error(errorMessage));
56
+ }
57
+ });
58
+
59
+ pandoc.on("error", (err) => {
60
+ console.error("Pandoc spawn error:", err);
61
+ reject(err);
62
+ });
63
+ });
64
+ }
65
+
66
+ // Generate EPUB using pandoc
67
+ async function generateEpub(
68
+ htmlFiles: string[],
69
+ outputPath: string,
70
+ metadata: CourseMetadata
71
+ ): Promise<void> {
72
+ return new Promise((resolve, reject) => {
73
+ let stdout = "";
74
+ let stderr = "";
75
+
76
+ // Include all HTML files in the EPUB
77
+ const pandocArgs = [
78
+ ...htmlFiles, // All HTML files
79
+ "-o",
80
+ outputPath,
81
+ "--from",
82
+ "html",
83
+ "--to",
84
+ "epub",
85
+ "--metadata",
86
+ `title=${metadata.title}`,
87
+ "--metadata",
88
+ `language=${metadata.language || "en"}`,
89
+ "--metadata",
90
+ `creator=FourGeeksAcademy`,
91
+ "--metadata",
92
+ `description=${metadata.description || ""}`,
93
+ "--metadata",
94
+ `subject=${metadata.technologies?.join(", ") || "Programming"}`,
95
+ "--css",
96
+ path.join(__dirname, "../templates/epub/epub.css"),
97
+ ];
98
+
99
+ console.log(
100
+ "Executing pandoc EPUB command:",
101
+ "pandoc",
102
+ pandocArgs.join(" ")
103
+ );
104
+ console.log("HTML files to include:", htmlFiles);
105
+ console.log(
106
+ "All input files exist:",
107
+ htmlFiles.every((file) => fs.existsSync(file))
108
+ );
109
+ console.log(
110
+ "Output directory exists:",
111
+ fs.existsSync(path.dirname(outputPath))
112
+ );
113
+
114
+ const pandoc = spawn("pandoc", pandocArgs);
115
+
116
+ pandoc.stdout.on("data", (data) => {
117
+ stdout += data.toString();
118
+ });
119
+
120
+ pandoc.stderr.on("data", (data) => {
121
+ stderr += data.toString();
122
+ });
123
+
124
+ pandoc.on("close", (code) => {
125
+ if (code === 0) {
126
+ resolve();
127
+ } else {
128
+ const errorMessage = `Pandoc process exited with code ${code}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
129
+ console.error("Pandoc Error Details:", errorMessage);
130
+ reject(new Error(errorMessage));
131
+ }
132
+ });
133
+
134
+ pandoc.on("error", (err) => {
135
+ console.error("Pandoc spawn error:", err);
136
+ reject(err);
137
+ });
138
+ });
139
+ }
140
+
141
+ // Process exercises and convert to HTML
142
+ async function processExercises(
143
+ exercisesDir: string,
144
+ outputDir: string,
145
+ language: string = "en"
146
+ ): Promise<string[]> {
147
+ const htmlFiles: string[] = [];
148
+
149
+ console.log("Processing exercises directory:", exercisesDir);
150
+ console.log("Output directory:", outputDir);
151
+ console.log("Language:", language);
152
+
153
+ if (!fs.existsSync(exercisesDir)) {
154
+ console.log("Exercises directory does not exist:", exercisesDir);
155
+ return htmlFiles;
156
+ }
157
+
158
+ console.log("Exercises directory exists, scanning for files...");
159
+
160
+ const processDirectory = async (dir: string, relativePath: string = "") => {
161
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
162
+
163
+ // Sort entries to prioritize README files first
164
+ entries.sort((a, b) => {
165
+ const aName = a.name.toLowerCase();
166
+ const bName = b.name.toLowerCase();
167
+
168
+ // Prioritize README files
169
+ if (aName === "readme.md" && bName !== "readme.md") return -1;
170
+ if (bName === "readme.md" && aName !== "readme.md") return 1;
171
+
172
+ return aName.localeCompare(bName);
173
+ });
174
+
175
+ for (const entry of entries) {
176
+ const fullPath = path.join(dir, entry.name);
177
+ const relativeFilePath = path.join(relativePath, entry.name);
178
+
179
+ if (entry.isDirectory()) {
180
+ console.log("Processing subdirectory:", relativeFilePath);
181
+ // esling-disable-next-line
182
+ await processDirectory(fullPath, relativeFilePath);
183
+ } else if (
184
+ entry.name.endsWith(".md") ||
185
+ entry.name.endsWith(".markdown")
186
+ ) {
187
+ // Apply language filtering for all markdown files
188
+ const baseName = entry.name.replace(/\.(md|markdown)$/, "");
189
+
190
+ if (language === "en") {
191
+ // For English: exclude files with language suffixes
192
+ if (
193
+ baseName.includes(".") &&
194
+ /\.(es|fr|de|it|pt|ja|ko|zh|ar|ru)$/i.test(baseName)
195
+ ) {
196
+ console.log(
197
+ "Skipping language-specific file for English:",
198
+ relativeFilePath
199
+ );
200
+ continue;
201
+ }
202
+ } else {
203
+ // For other languages: only include files with the correct language suffix
204
+ if (!baseName.endsWith(`.${language}`)) {
205
+ console.log(
206
+ "Skipping file with different language:",
207
+ relativeFilePath
208
+ );
209
+ continue;
210
+ }
211
+ }
212
+
213
+ // Convert markdown to HTML
214
+ console.log("Converting markdown file:", relativeFilePath);
215
+ const htmlFileName = entry.name.replace(/\.(md|markdown)$/, ".html");
216
+ const htmlPath = path.join(
217
+ outputDir,
218
+ relativeFilePath.replace(/\.(md|markdown)$/, ".html")
219
+ );
220
+
221
+ console.log("HTML output path:", htmlPath);
222
+ mkdirp.sync(path.dirname(htmlPath));
223
+ // esling-disable-next-line
224
+ await convertMarkdownToHtml(fullPath, htmlPath);
225
+
226
+ htmlFiles.push(htmlPath);
227
+ console.log(
228
+ "Successfully converted:",
229
+ relativeFilePath,
230
+ "->",
231
+ htmlPath
232
+ );
233
+ } else if (entry.name.endsWith(".html")) {
234
+ // Copy HTML files as-is
235
+ console.log("Copying HTML file:", relativeFilePath);
236
+ const destPath = path.join(outputDir, relativeFilePath);
237
+ mkdirp.sync(path.dirname(destPath));
238
+ // esling-disable-next-line
239
+ fs.copyFileSync(fullPath, destPath);
240
+ htmlFiles.push(destPath);
241
+ console.log("Successfully copied:", relativeFilePath, "->", destPath);
242
+ } else {
243
+ console.log(
244
+ "Skipping file:",
245
+ relativeFilePath,
246
+ "(not markdown or HTML)"
247
+ );
248
+ }
249
+ }
250
+ };
251
+
252
+ await processDirectory(exercisesDir);
253
+ return htmlFiles;
254
+ }
255
+
256
+ // Create EPUB table of contents
257
+ function createTocHtml(htmlFiles: string[], metadata: CourseMetadata): string {
258
+ let tocContent = `
259
+ <!DOCTYPE html>
260
+ <html>
261
+ <head>
262
+ <meta charset="UTF-8">
263
+ <title>Table of Contents - ${metadata.title}</title>
264
+ <link rel="stylesheet" href="epub.css">
265
+ </head>
266
+ <body>
267
+ <h1>${metadata.title}</h1>
268
+ <nav id="toc">
269
+ <h2>Table of Contents</h2>
270
+ <ol>
271
+ <li><a href="main.html">Course Overview</a></li>
272
+ `;
273
+
274
+ htmlFiles.forEach((file) => {
275
+ const fileName = path.basename(file, ".html");
276
+ const displayName = fileName
277
+ .replace(/[-_]/g, " ")
278
+ .replace(/\b\w/g, (l) => l.toUpperCase());
279
+ // Use the full relative path for the href to ensure proper navigation
280
+ const relativePath = file.replace(/.*\/html\//, "").replace(/\\/g, "/");
281
+ tocContent += ` <li><a href="${relativePath}">${displayName}</a></li>\n`;
282
+ });
283
+
284
+ tocContent += `
285
+ </ol>
286
+ </nav>
287
+ </body>
288
+ </html>`;
289
+
290
+ return tocContent;
291
+ }
292
+
293
+ export async function exportToEpub(options: ExportOptions): Promise<string> {
294
+ const { courseSlug, bucket, outDir, language = "en" } = options;
295
+
296
+ console.log("Starting EPUB export for course:", courseSlug);
297
+ console.log("Output directory:", outDir);
298
+ console.log("Language:", language);
299
+
300
+ // 1. Create temporary folder
301
+ const tmpName = uuidv4();
302
+ const epubOutDir = path.join(outDir, tmpName);
303
+ rimraf.sync(epubOutDir);
304
+ mkdirp.sync(epubOutDir);
305
+
306
+ console.log("Created temporary directory:", epubOutDir);
307
+
308
+ // 2. Create EPUB template directory
309
+ const epubTemplateDir = path.join(epubOutDir, "template");
310
+ mkdirp.sync(epubTemplateDir);
311
+
312
+ // 3. Download exercises and .learn from bucket
313
+ console.log("Downloading exercises...");
314
+ await downloadS3Folder(
315
+ bucket,
316
+ `courses/${courseSlug}/exercises/`,
317
+ path.join(epubOutDir, "exercises")
318
+ );
319
+ console.log("Downloading .learn files...");
320
+ await downloadS3Folder(
321
+ bucket,
322
+ `courses/${courseSlug}/.learn/`,
323
+ path.join(epubOutDir, ".learn")
324
+ );
325
+
326
+ // 4. Read learn.json for course info
327
+ console.log("Reading course metadata...");
328
+ const learnJson = await getCourseMetadata(bucket, courseSlug);
329
+
330
+ const metadata: CourseMetadata = {
331
+ title: learnJson.title?.en || learnJson.title || courseSlug,
332
+ description: learnJson.description?.en || learnJson.description,
333
+ language: learnJson.language || "en",
334
+ technologies: learnJson.technologies || [],
335
+ difficulty: learnJson.difficulty || "beginner",
336
+ };
337
+
338
+ console.log("Course metadata:", metadata);
339
+
340
+ // 5. Process exercises and convert to HTML
341
+ console.log("Processing exercises and converting to HTML...");
342
+ const htmlFiles = await processExercises(
343
+ path.join(epubOutDir, "exercises"),
344
+ path.join(epubOutDir, "html"),
345
+ language
346
+ );
347
+
348
+ console.log("Generated HTML files:", htmlFiles.length);
349
+ console.log("HTML files:", htmlFiles);
350
+
351
+ // 6. Create table of contents
352
+ const tocHtml = createTocHtml(htmlFiles, metadata);
353
+ const tocPath = path.join(epubOutDir, "html", "toc.html");
354
+ mkdirp.sync(path.dirname(tocPath));
355
+ fs.writeFileSync(tocPath, tocHtml, "utf-8");
356
+
357
+ // 7. Create main content file
358
+ const mainContent = `
359
+ <!DOCTYPE html>
360
+ <html>
361
+ <head>
362
+ <meta charset="UTF-8">
363
+ <title>${metadata.title}</title>
364
+ <link rel="stylesheet" href="epub.css">
365
+ </head>
366
+ <body>
367
+ <h1>${metadata.title}</h1>
368
+ <p>${metadata.description || ""}</p>
369
+ <p><strong>Technologies:</strong> ${
370
+ metadata.technologies?.join(", ") || "Programming"
371
+ }</p>
372
+ <p><strong>Difficulty:</strong> ${metadata.difficulty}</p>
373
+ <p><strong>Language:</strong> ${language}</p>
374
+ <p><a href="toc.html">Go to Table of Contents</a></p>
375
+ </body>
376
+ </html>`;
377
+
378
+ const mainContentPath = path.join(epubOutDir, "html", "main.html");
379
+ fs.writeFileSync(mainContentPath, mainContent, "utf-8");
380
+
381
+ // 8. Prepare all HTML files for EPUB generation
382
+ const allHtmlFiles = [mainContentPath, tocPath, ...htmlFiles];
383
+ console.log("All HTML files for EPUB:", allHtmlFiles);
384
+
385
+ // 9. Generate EPUB using pandoc
386
+ console.log("Generating EPUB file...");
387
+ const epubPath = path.join(epubOutDir, `${courseSlug}.epub`);
388
+ await generateEpub(allHtmlFiles, epubPath, metadata);
389
+
390
+ console.log("EPUB generated successfully:", epubPath);
391
+
392
+ // Clean up temporary files
393
+ console.log("Cleaning up temporary files...");
394
+ rimraf.sync(path.join(epubOutDir, "html"));
395
+ rimraf.sync(path.join(epubOutDir, "exercises"));
396
+ rimraf.sync(path.join(epubOutDir, ".learn"));
397
+
398
+ console.log("EPUB export completed successfully");
399
+ return epubPath;
400
+ }
@@ -0,0 +1,3 @@
1
+ export { exportToScorm } from "./scorm";
2
+ export { exportToEpub } from "./epub";
3
+ export { ExportFormat, ExportOptions } from "./types";
@@ -0,0 +1,121 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as archiver from "archiver";
4
+ import * as mkdirp from "mkdirp";
5
+ import * as rimraf from "rimraf";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { ExportOptions, CourseMetadata } from "./types";
8
+ import {
9
+ copyDir,
10
+ downloadS3Folder,
11
+ getFilesList,
12
+ getCourseMetadata,
13
+ } from "./shared";
14
+
15
+ export async function exportToScorm(options: ExportOptions): Promise<string> {
16
+ const { courseSlug, bucket, outDir } = options;
17
+
18
+ // 1. Create temporary folder
19
+ const tmpName = uuidv4();
20
+ const scormOutDir = path.join(outDir, tmpName);
21
+ rimraf.sync(scormOutDir);
22
+ mkdirp.sync(scormOutDir);
23
+
24
+ // 2. Copy SCORM template
25
+ const scormTpl = path.join(__dirname, "../templates/scorm");
26
+ copyDir(scormTpl, scormOutDir);
27
+
28
+ // Paths
29
+ const appDir = path.resolve(__dirname, "../../ui/_app");
30
+ const configDir = path.join(scormOutDir, "config");
31
+
32
+ // Check if _app exists and has content
33
+ if (fs.existsSync(appDir) && fs.readdirSync(appDir).length > 0) {
34
+ // Copy app.css and app.js to config/
35
+ for (const file of ["app.css", "app.js"]) {
36
+ const src = path.join(appDir, file);
37
+ const dest = path.join(configDir, file);
38
+ if (fs.existsSync(src)) {
39
+ fs.copyFileSync(src, dest);
40
+ }
41
+ }
42
+
43
+ // Copy logo-192.png and logo-512.png to SCORM build root
44
+ for (const file of ["logo-192.png", "logo-512.png"]) {
45
+ const src = path.join(appDir, file);
46
+ const dest = path.join(scormOutDir, file);
47
+ if (fs.existsSync(src)) {
48
+ fs.copyFileSync(src, dest);
49
+ }
50
+ }
51
+ }
52
+
53
+ // 3. Download exercises and .learn from bucket
54
+ await downloadS3Folder(
55
+ bucket,
56
+ `courses/${courseSlug}/exercises/`,
57
+ path.join(scormOutDir, "exercises")
58
+ );
59
+ await downloadS3Folder(
60
+ bucket,
61
+ `courses/${courseSlug}/.learn/`,
62
+ path.join(scormOutDir, ".learn")
63
+ );
64
+
65
+ // 4. Read learn.json for course info
66
+ const learnJson = await getCourseMetadata(bucket, courseSlug);
67
+
68
+ // 5. Replace imsmanifest.xml
69
+ const manifestPath = path.join(scormOutDir, "imsmanifest.xml");
70
+ let manifest = fs.readFileSync(manifestPath, "utf-8");
71
+ manifest = manifest.replace(/{{organization_name}}/g, "FourGeeksAcademy");
72
+ manifest = manifest.replace(
73
+ /{{course_title}}/g,
74
+ learnJson.title?.en || learnJson.title || courseSlug
75
+ );
76
+
77
+ // Generate exercises list
78
+ const exercisesList = getFilesList(
79
+ path.join(scormOutDir, "exercises"),
80
+ "exercises"
81
+ ).join("\n ");
82
+ manifest = manifest.replace(/{{exercises_list}}/g, exercisesList);
83
+
84
+ // Generate images list (optional, if you have assets)
85
+ let imagesList = "";
86
+ const assetsDir = path.join(scormOutDir, ".learn", "assets");
87
+ if (fs.existsSync(assetsDir)) {
88
+ imagesList = getFilesList(assetsDir, ".learn/assets").join("\n ");
89
+ }
90
+ manifest = manifest.replace(/{{images_list}}/g, imagesList);
91
+
92
+ fs.writeFileSync(manifestPath, manifest, "utf-8");
93
+
94
+ // 6. Replace title in config/index.html
95
+ const configIndexPath = path.join(scormOutDir, "config", "index.html");
96
+ let indexHtml = fs.readFileSync(configIndexPath, "utf-8");
97
+ indexHtml = indexHtml.replace(
98
+ /{{title}}/g,
99
+ learnJson.title?.en || learnJson.title || courseSlug
100
+ );
101
+ fs.writeFileSync(configIndexPath, indexHtml, "utf-8");
102
+
103
+ // 7. Compress as ZIP
104
+ const zipPath = `${scormOutDir}.zip`;
105
+ await new Promise((resolve: any, reject: any) => {
106
+ const output = fs.createWriteStream(zipPath);
107
+ const archive = archiver("zip", { zlib: { level: 9 } });
108
+
109
+ output.on("close", resolve);
110
+ archive.on("error", reject);
111
+
112
+ archive.pipe(output);
113
+ archive.directory(scormOutDir, false);
114
+ archive.finalize();
115
+ });
116
+
117
+ // Clean up temporary directory
118
+ rimraf.sync(scormOutDir);
119
+
120
+ return zipPath;
121
+ }