@learnpack/learnpack 5.0.334 → 5.0.339

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 (48) hide show
  1. package/bin/run +17 -17
  2. package/lib/commands/init.js +41 -41
  3. package/lib/commands/serve.js +645 -129
  4. package/lib/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
  5. package/lib/creatorDist/assets/index-CjddKHB_.css +1 -1688
  6. package/lib/managers/config/exercise.js +2 -14
  7. package/lib/managers/readmeHistoryService.js +3 -1
  8. package/lib/managers/server/routes.js +2 -1
  9. package/lib/utils/configBuilder.js +2 -1
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/exerciseFileOrder.d.ts +20 -0
  12. package/lib/utils/exerciseFileOrder.js +49 -0
  13. package/lib/utils/export/epub.js +26 -26
  14. package/lib/utils/readmeSanitizer.d.ts +8 -0
  15. package/lib/utils/readmeSanitizer.js +13 -0
  16. package/lib/utils/templates/epub/epub.css +146 -146
  17. package/lib/utils/templates/scorm/config/api.js +175 -175
  18. package/package.json +1 -1
  19. package/src/commands/init.ts +655 -655
  20. package/src/commands/publish.ts +670 -670
  21. package/src/commands/serve.ts +5853 -5148
  22. package/src/creator/eslint.config.js +28 -28
  23. package/src/creator/src/index.css +227 -227
  24. package/src/creator/src/utils/lib.ts +471 -471
  25. package/src/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
  26. package/src/creatorDist/assets/index-CjddKHB_.css +1 -1688
  27. package/src/managers/config/exercise.ts +3 -15
  28. package/src/managers/readmeHistoryService.ts +3 -1
  29. package/src/managers/server/routes.ts +15 -6
  30. package/src/managers/session.ts +184 -184
  31. package/src/ui/_app/app.css +1 -1
  32. package/src/ui/_app/app.js +1950 -1878
  33. package/src/ui/app.tar.gz +0 -0
  34. package/src/utils/api.ts +675 -675
  35. package/src/utils/configBuilder.ts +102 -100
  36. package/src/utils/creatorUtilities.ts +536 -536
  37. package/src/utils/errors.ts +108 -108
  38. package/src/utils/exerciseFileOrder.ts +50 -0
  39. package/src/utils/export/epub.ts +553 -553
  40. package/src/utils/export/index.ts +4 -4
  41. package/src/utils/export/scorm.ts +121 -121
  42. package/src/utils/export/shared.ts +61 -61
  43. package/src/utils/export/types.ts +25 -25
  44. package/src/utils/export/zip.ts +55 -55
  45. package/src/utils/readmeSanitizer.ts +10 -0
  46. package/src/utils/rigoActions.ts +642 -642
  47. package/src/utils/templates/epub/epub.css +146 -146
  48. package/src/utils/templates/scorm/config/api.js +175 -175
@@ -6,6 +6,7 @@ const p = require("path");
6
6
  const fs = require("fs");
7
7
  const console_1 = require("../../utils/console");
8
8
  const allowed_files_1 = require("./allowed_files");
9
+ const exerciseFileOrder_1 = require("../../utils/exerciseFileOrder");
9
10
  // function processQuestions(markdown: string): {
10
11
  // updatedMarkdown: string
11
12
  // questions: { id: string; examples: string[] }[]
@@ -272,20 +273,7 @@ const filterFiles = (files, basePath = ".") => files
272
273
  path: basePath + "/" + ex,
273
274
  }),
274
275
  }))
275
- .sort((f1, f2) => {
276
- const score = {
277
- // sorting priority
278
- "index.html": 1,
279
- "styles.css": 2,
280
- "styles.scss": 2,
281
- "style.css": 2,
282
- "style.scss": 2,
283
- "index.css": 2,
284
- "index.scss": 2,
285
- "index.js": 3,
286
- };
287
- return score[f1.name] < score[f2.name] ? -1 : 1;
288
- });
276
+ .sort((f1, f2) => (0, exerciseFileOrder_1.compareExerciseFileNames)(f1.name, f2.name));
289
277
  exports.filterFiles = filterFiles;
290
278
  exports.default = {
291
279
  exercise: exports.exercise,
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReadmeHistoryService = void 0;
4
+ const readmeSanitizer_1 = require("../utils/readmeSanitizer");
4
5
  /**
5
6
  * Service to handle README file updates with automatic history tracking
6
7
  * Encapsulates the logic of saving state to history before updating files
@@ -169,9 +170,10 @@ class ReadmeHistoryService {
169
170
  * @returns void
170
171
  */
171
172
  async saveToGCS(courseSlug, exerciseSlug, fileName, content) {
173
+ const sanitizedContent = (0, readmeSanitizer_1.sanitizeReadmeNewlines)(content);
172
174
  const filePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${fileName}`;
173
175
  const file = this.bucket.file(filePath);
174
- await file.save(content, {
176
+ await file.save(sanitizedContent, {
175
177
  resumable: false,
176
178
  });
177
179
  }
@@ -13,6 +13,7 @@ const session_1 = require("../../managers/session");
13
13
  const telemetry_1 = require("../telemetry");
14
14
  const creatorUtilities_1 = require("../../utils/creatorUtilities");
15
15
  const rigoActions_1 = require("../../utils/rigoActions");
16
+ const readmeSanitizer_1 = require("../../utils/readmeSanitizer");
16
17
  const sidebarGenerator_1 = require("../../utils/sidebarGenerator");
17
18
  const path = require("path");
18
19
  // import { eventManager } from "../../utils/osOperations"
@@ -303,7 +304,7 @@ async function default_1(app, configObject, configManager) {
303
304
  const readme = exercise.getReadme(null);
304
305
  await Promise.all(languagesToTranslate.map(async (language) => {
305
306
  const response = await (0, rigoActions_1.translateExercise)(rigoToken, {
306
- text_to_translate: readme.body,
307
+ text_to_translate: (0, readmeSanitizer_1.sanitizeReadmeNewlines)(readme.body),
307
308
  output_language: language,
308
309
  }, `${process.env.HOST}/webhooks/translate-exercise`);
309
310
  await (0, creatorUtilities_1.saveTranslatedReadme)(slug, response.parsed.output_language_code, response.parsed.translation);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildConfig = buildConfig;
4
+ const exerciseFileOrder_1 = require("./exerciseFileOrder");
4
5
  function naturalCompare(a, b) {
5
6
  // Split by dots and hyphens, compare numbers as numbers
6
7
  const regex = /(\d+|\D+)/g;
@@ -64,7 +65,7 @@ async function buildConfig(bucket, courseSlug) {
64
65
  }
65
66
  const exercises = Object.values(map)
66
67
  .sort((a, b) => naturalCompare(a.slug, b.slug))
67
- .map((ex, i) => (Object.assign(Object.assign({}, ex), { position: i })));
68
+ .map((ex, i) => (Object.assign(Object.assign({}, ex), { files: (0, exerciseFileOrder_1.sortExerciseFiles)(ex.files), position: i })));
68
69
  return {
69
70
  config: Object.assign({}, learnJson),
70
71
  exercises,
@@ -334,13 +334,13 @@ function countSentences(text) {
334
334
  function howManyDifficultParagraphs(paragraphs, maxFKGL) {
335
335
  return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length;
336
336
  }
337
- const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
338
-
339
- Introduction to AI: Explain what is AI and its applications
340
- Introduction to Machine Learning: Explain what is machine learning and its applications
341
- What is an AI Model: Explain what is an AI model and its applications
342
- How to use an AI Model: Different APIs, local models, etc.
343
- How to build an AI Model: Fine-tuning, data collection, cleaning and more.
337
+ const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
338
+
339
+ Introduction to AI: Explain what is AI and its applications
340
+ Introduction to Machine Learning: Explain what is machine learning and its applications
341
+ What is an AI Model: Explain what is an AI model and its applications
342
+ How to use an AI Model: Different APIs, local models, etc.
343
+ How to build an AI Model: Fine-tuning, data collection, cleaning and more.
344
344
  `;
345
345
  const appendContentIndex = async () => {
346
346
  const choices = await prompts([
@@ -370,13 +370,13 @@ const appendContentIndex = async () => {
370
370
  return null;
371
371
  };
372
372
  exports.appendContentIndex = appendContentIndex;
373
- const example_airules = `
374
- Write with an engaging tone, use simple words and avoid complex sentences.
375
- Write in first person, as if you are talking to the reader.
376
- Add mental maps to help the reader understand the content.
377
- Add diagrams to help the reader understand the content.
378
- No code exercises required
379
-
373
+ const example_airules = `
374
+ Write with an engaging tone, use simple words and avoid complex sentences.
375
+ Write in first person, as if you are talking to the reader.
376
+ Add mental maps to help the reader understand the content.
377
+ Add diagrams to help the reader understand the content.
378
+ No code exercises required
379
+
380
380
  `;
381
381
  const appendAIRules = async () => {
382
382
  const choices = await prompts([
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared ordering logic for exercise file lists (e.g. editor tabs).
3
+ * Ensures solution files (*.solution.hide.*) always appear after their base file.
4
+ */
5
+ /**
6
+ * Comparator for two file names. Use with Array.prototype.sort.
7
+ * Solution files are ordered after their base file; then by priority; then alphabetically.
8
+ * @param nameA - First file name.
9
+ * @param nameB - Second file name.
10
+ * @returns Negative if nameA < nameB, 0 if equal, positive if nameA > nameB.
11
+ */
12
+ export declare function compareExerciseFileNames(nameA: string, nameB: string): number;
13
+ /**
14
+ * Sorts an array of file-like objects by name using exercise file ordering rules.
15
+ * @param files - Array of objects with a `name` property.
16
+ * @returns A new sorted array (original is not mutated).
17
+ */
18
+ export declare function sortExerciseFiles<T extends {
19
+ name: string;
20
+ }>(files: T[]): T[];
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Shared ordering logic for exercise file lists (e.g. editor tabs).
4
+ * Ensures solution files (*.solution.hide.*) always appear after their base file.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.compareExerciseFileNames = compareExerciseFileNames;
8
+ exports.sortExerciseFiles = sortExerciseFiles;
9
+ const SOLUTION_HIDE = ".solution.hide";
10
+ const isSolutionFile = (name) => name.includes(SOLUTION_HIDE);
11
+ const getBaseFileName = (name) => name.replace(SOLUTION_HIDE, "");
12
+ const FILE_PRIORITY = {
13
+ "index.html": 1,
14
+ "styles.css": 2,
15
+ "styles.scss": 2,
16
+ "style.css": 2,
17
+ "style.scss": 2,
18
+ "index.css": 2,
19
+ "index.scss": 2,
20
+ "index.js": 3,
21
+ };
22
+ const DEFAULT_PRIORITY = Infinity;
23
+ /**
24
+ * Comparator for two file names. Use with Array.prototype.sort.
25
+ * Solution files are ordered after their base file; then by priority; then alphabetically.
26
+ * @param nameA - First file name.
27
+ * @param nameB - Second file name.
28
+ * @returns Negative if nameA < nameB, 0 if equal, positive if nameA > nameB.
29
+ */
30
+ function compareExerciseFileNames(nameA, nameB) {
31
+ var _a, _b;
32
+ if (isSolutionFile(nameA) && getBaseFileName(nameA) === nameB)
33
+ return 1;
34
+ if (isSolutionFile(nameB) && getBaseFileName(nameB) === nameA)
35
+ return -1;
36
+ const s1 = (_a = FILE_PRIORITY[nameA]) !== null && _a !== void 0 ? _a : DEFAULT_PRIORITY;
37
+ const s2 = (_b = FILE_PRIORITY[nameB]) !== null && _b !== void 0 ? _b : DEFAULT_PRIORITY;
38
+ if (s1 !== s2)
39
+ return s1 - s2;
40
+ return nameA.localeCompare(nameB);
41
+ }
42
+ /**
43
+ * Sorts an array of file-like objects by name using exercise file ordering rules.
44
+ * @param files - Array of objects with a `name` property.
45
+ * @returns A new sorted array (original is not mutated).
46
+ */
47
+ function sortExerciseFiles(files) {
48
+ return [...files].sort((a, b) => compareExerciseFileNames(a.name, b.name));
49
+ }
@@ -75,14 +75,14 @@ async function generateEpub(markdownFiles, outputPath, metadata, epubMetadata, c
75
75
  return "";
76
76
  return str.replace(/"/g, '\\"').replace(/\n/g, "\\n");
77
77
  };
78
- const yamlContent = `---
79
- title: "${epubMetadata.title}"
80
- creator: "${epubMetadata.creator}"
81
- publisher: "${epubMetadata.publisher}"
82
- rights: "${epubMetadata.rights}"
83
- lang: "${epubMetadata.lang}"
84
- description: "${escapeYaml(metadata.description || "")}"
85
- subject: "${((_a = metadata.technologies) === null || _a === void 0 ? void 0 : _a.join(", ")) || "Programming"}"
78
+ const yamlContent = `---
79
+ title: "${epubMetadata.title}"
80
+ creator: "${epubMetadata.creator}"
81
+ publisher: "${epubMetadata.publisher}"
82
+ rights: "${epubMetadata.rights}"
83
+ lang: "${epubMetadata.lang}"
84
+ description: "${escapeYaml(metadata.description || "")}"
85
+ subject: "${((_a = metadata.technologies) === null || _a === void 0 ? void 0 : _a.join(", ")) || "Programming"}"
86
86
  ---`;
87
87
  fs.writeFileSync(yamlPath, yamlContent, "utf-8");
88
88
  // Include all markdown files in the EPUB
@@ -348,20 +348,20 @@ async function exportToEpub(options, epubMetadata) {
348
348
  const sidebarData = readSidebarData(path.join(epubOutDir, "learn"));
349
349
  console.log("📋 Sidebar data loaded:", sidebarData ? "Success" : "Not found");
350
350
  // 7. Create main content file as markdown
351
- const mainContent = `# ${metadata.title}
352
-
353
- ${metadata.description || ""}
354
-
355
- **Technologies:** ${((_c = metadata.technologies) === null || _c === void 0 ? void 0 : _c.join(", ")) || "Programming"}
356
-
357
- **Difficulty:** ${metadata.difficulty}
358
-
359
- **Language:** ${language}
360
-
361
- ---
362
-
363
- ## Table of Contents
364
-
351
+ const mainContent = `# ${metadata.title}
352
+
353
+ ${metadata.description || ""}
354
+
355
+ **Technologies:** ${((_c = metadata.technologies) === null || _c === void 0 ? void 0 : _c.join(", ")) || "Programming"}
356
+
357
+ **Difficulty:** ${metadata.difficulty}
358
+
359
+ **Language:** ${language}
360
+
361
+ ---
362
+
363
+ ## Table of Contents
364
+
365
365
  ${processedMarkdownFiles
366
366
  .map((file, index) => {
367
367
  const fileName = path.basename(file, ".processed.md");
@@ -379,10 +379,10 @@ ${processedMarkdownFiles
379
379
  }
380
380
  return `${index + 1}. ${displayName}`;
381
381
  })
382
- .join("\n")}
383
-
384
- ---
385
-
382
+ .join("\n")}
383
+
384
+ ---
385
+
386
386
  `;
387
387
  const mainContentPath = path.join(epubOutDir, "processed", "main.md");
388
388
  mkdirp.sync(path.dirname(mainContentPath));
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Replaces sequences of 3+ newlines (with optional spaces in between)
3
+ * with a single double newline. Prevents excessive whitespace that can
4
+ * cause translation API token limit issues.
5
+ * @param content - Raw README string
6
+ * @returns Sanitized string
7
+ */
8
+ export declare function sanitizeReadmeNewlines(content: string): string;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeReadmeNewlines = sanitizeReadmeNewlines;
4
+ /**
5
+ * Replaces sequences of 3+ newlines (with optional spaces in between)
6
+ * with a single double newline. Prevents excessive whitespace that can
7
+ * cause translation API token limit issues.
8
+ * @param content - Raw README string
9
+ * @returns Sanitized string
10
+ */
11
+ function sanitizeReadmeNewlines(content) {
12
+ return content.replace(/(\n\s*){3,}/g, "\n\n");
13
+ }
@@ -1,146 +1,146 @@
1
- /* Basic EPUB styling */
2
- body {
3
- font-family: "Georgia", serif;
4
- line-height: 1.6;
5
- margin: 2em;
6
- color: #333;
7
- }
8
-
9
- h1,
10
- h2,
11
- h3,
12
- h4,
13
- h5,
14
- h6 {
15
- color: #2c3e50;
16
- margin-top: 1.5em;
17
- margin-bottom: 0.5em;
18
- }
19
-
20
- h1 {
21
- font-size: 2em;
22
- border-bottom: 2px solid #3498db;
23
- padding-bottom: 0.3em;
24
- }
25
-
26
- h2 {
27
- font-size: 1.5em;
28
- border-bottom: 1px solid #bdc3c7;
29
- padding-bottom: 0.2em;
30
- }
31
-
32
- p {
33
- margin-bottom: 1em;
34
- text-align: justify;
35
- }
36
-
37
- code {
38
- background-color: #f8f9fa;
39
- padding: 0.2em 0.4em;
40
- border-radius: 3px;
41
- font-family: "Courier New", monospace;
42
- font-size: 0.9em;
43
- }
44
-
45
- pre {
46
- background-color: #f8f9fa;
47
- padding: 1em;
48
- border-radius: 5px;
49
- overflow-x: auto;
50
- border-left: 4px solid #3498db;
51
- }
52
-
53
- pre code {
54
- background-color: transparent;
55
- padding: 0;
56
- }
57
-
58
- blockquote {
59
- border-left: 4px solid #bdc3c7;
60
- margin: 1em 0;
61
- padding-left: 1em;
62
- font-style: italic;
63
- color: #7f8c8d;
64
- }
65
-
66
- ul,
67
- ol {
68
- margin-bottom: 1em;
69
- padding-left: 2em;
70
- }
71
-
72
- li {
73
- margin-bottom: 0.5em;
74
- }
75
-
76
- a {
77
- color: #3498db;
78
- text-decoration: none;
79
- }
80
-
81
- a:hover {
82
- text-decoration: underline;
83
- }
84
-
85
- img {
86
- max-width: 100%;
87
- height: auto;
88
- display: block;
89
- margin: 1em auto;
90
- }
91
-
92
- table {
93
- border-collapse: collapse;
94
- width: 100%;
95
- margin: 1em 0;
96
- }
97
-
98
- th,
99
- td {
100
- border: 1px solid #ddd;
101
- padding: 8px;
102
- text-align: left;
103
- }
104
-
105
- th {
106
- background-color: #f2f2f2;
107
- font-weight: bold;
108
- }
109
-
110
- #toc {
111
- background-color: #f8f9fa;
112
- padding: 1em;
113
- border-radius: 5px;
114
- margin-bottom: 2em;
115
- }
116
-
117
- #toc h2 {
118
- border-bottom: none;
119
- margin-top: 0;
120
- }
121
-
122
- #toc ol {
123
- padding-left: 1em;
124
- }
125
-
126
- #toc li {
127
- margin-bottom: 0.3em;
128
- }
129
-
130
- #toc a {
131
- color: #2c3e50;
132
- font-weight: 500;
133
- }
134
-
135
- /* Open question styles */
136
- .open-question {
137
- border: 1px solid #c2c2c2;
138
- padding: 1em;
139
- background-color: #fafafa;
140
- margin: 1em 0;
141
- width: 100%;
142
- height: 150px;
143
- color: #c2c2c2;
144
- border-radius: 5px;
145
- font-style: italic;
146
- }
1
+ /* Basic EPUB styling */
2
+ body {
3
+ font-family: "Georgia", serif;
4
+ line-height: 1.6;
5
+ margin: 2em;
6
+ color: #333;
7
+ }
8
+
9
+ h1,
10
+ h2,
11
+ h3,
12
+ h4,
13
+ h5,
14
+ h6 {
15
+ color: #2c3e50;
16
+ margin-top: 1.5em;
17
+ margin-bottom: 0.5em;
18
+ }
19
+
20
+ h1 {
21
+ font-size: 2em;
22
+ border-bottom: 2px solid #3498db;
23
+ padding-bottom: 0.3em;
24
+ }
25
+
26
+ h2 {
27
+ font-size: 1.5em;
28
+ border-bottom: 1px solid #bdc3c7;
29
+ padding-bottom: 0.2em;
30
+ }
31
+
32
+ p {
33
+ margin-bottom: 1em;
34
+ text-align: justify;
35
+ }
36
+
37
+ code {
38
+ background-color: #f8f9fa;
39
+ padding: 0.2em 0.4em;
40
+ border-radius: 3px;
41
+ font-family: "Courier New", monospace;
42
+ font-size: 0.9em;
43
+ }
44
+
45
+ pre {
46
+ background-color: #f8f9fa;
47
+ padding: 1em;
48
+ border-radius: 5px;
49
+ overflow-x: auto;
50
+ border-left: 4px solid #3498db;
51
+ }
52
+
53
+ pre code {
54
+ background-color: transparent;
55
+ padding: 0;
56
+ }
57
+
58
+ blockquote {
59
+ border-left: 4px solid #bdc3c7;
60
+ margin: 1em 0;
61
+ padding-left: 1em;
62
+ font-style: italic;
63
+ color: #7f8c8d;
64
+ }
65
+
66
+ ul,
67
+ ol {
68
+ margin-bottom: 1em;
69
+ padding-left: 2em;
70
+ }
71
+
72
+ li {
73
+ margin-bottom: 0.5em;
74
+ }
75
+
76
+ a {
77
+ color: #3498db;
78
+ text-decoration: none;
79
+ }
80
+
81
+ a:hover {
82
+ text-decoration: underline;
83
+ }
84
+
85
+ img {
86
+ max-width: 100%;
87
+ height: auto;
88
+ display: block;
89
+ margin: 1em auto;
90
+ }
91
+
92
+ table {
93
+ border-collapse: collapse;
94
+ width: 100%;
95
+ margin: 1em 0;
96
+ }
97
+
98
+ th,
99
+ td {
100
+ border: 1px solid #ddd;
101
+ padding: 8px;
102
+ text-align: left;
103
+ }
104
+
105
+ th {
106
+ background-color: #f2f2f2;
107
+ font-weight: bold;
108
+ }
109
+
110
+ #toc {
111
+ background-color: #f8f9fa;
112
+ padding: 1em;
113
+ border-radius: 5px;
114
+ margin-bottom: 2em;
115
+ }
116
+
117
+ #toc h2 {
118
+ border-bottom: none;
119
+ margin-top: 0;
120
+ }
121
+
122
+ #toc ol {
123
+ padding-left: 1em;
124
+ }
125
+
126
+ #toc li {
127
+ margin-bottom: 0.3em;
128
+ }
129
+
130
+ #toc a {
131
+ color: #2c3e50;
132
+ font-weight: 500;
133
+ }
134
+
135
+ /* Open question styles */
136
+ .open-question {
137
+ border: 1px solid #c2c2c2;
138
+ padding: 1em;
139
+ background-color: #fafafa;
140
+ margin: 1em 0;
141
+ width: 100%;
142
+ height: 150px;
143
+ color: #c2c2c2;
144
+ border-radius: 5px;
145
+ font-style: italic;
146
+ }