@learnpack/learnpack 5.0.348 → 5.0.350

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/src/ui/app.tar.gz CHANGED
Binary file
@@ -10,6 +10,7 @@ import {
10
10
  downloadS3Folder,
11
11
  getFilesList,
12
12
  getCourseMetadata,
13
+ escapeXml,
13
14
  } from "./shared";
14
15
 
15
16
  export async function exportToScorm(options: ExportOptions): Promise<string> {
@@ -29,24 +30,28 @@ export async function exportToScorm(options: ExportOptions): Promise<string> {
29
30
  const appDir = path.resolve(__dirname, "../../ui/_app");
30
31
  const configDir = path.join(scormOutDir, "config");
31
32
 
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
- }
33
+ // Copy the IDE bundle. app.js/app.css are declared as required files in the
34
+ // manifest, so their absence must be a hard error (otherwise we would ship a
35
+ // broken, blank package that references non-existent files).
36
+ for (const file of ["app.css", "app.js"]) {
37
+ const src = path.join(appDir, file);
38
+ const dest = path.join(configDir, file);
39
+ if (!fs.existsSync(src)) {
40
+ throw new Error(
41
+ `SCORM export failed: IDE bundle (${file}) not found in ${appDir}. ` +
42
+ "Ensure the editor was downloaded/extracted before exporting."
43
+ );
41
44
  }
42
45
 
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
- }
46
+ fs.copyFileSync(src, dest);
47
+ }
48
+
49
+ // Copy logo-192.png and logo-512.png to SCORM build root (optional assets)
50
+ for (const file of ["logo-192.png", "logo-512.png"]) {
51
+ const src = path.join(appDir, file);
52
+ const dest = path.join(scormOutDir, file);
53
+ if (fs.existsSync(src)) {
54
+ fs.copyFileSync(src, dest);
50
55
  }
51
56
  }
52
57
 
@@ -65,14 +70,19 @@ export async function exportToScorm(options: ExportOptions): Promise<string> {
65
70
  // 4. Read learn.json for course info
66
71
  const learnJson = await getCourseMetadata(bucket, courseSlug);
67
72
 
73
+ // Resolve the course title once, escaped for safe XML/HTML embedding
74
+ const courseTitle = escapeXml(
75
+ learnJson.title?.en || learnJson.title || courseSlug
76
+ );
77
+
68
78
  // 5. Replace imsmanifest.xml
69
79
  const manifestPath = path.join(scormOutDir, "imsmanifest.xml");
70
80
  let manifest = fs.readFileSync(manifestPath, "utf-8");
71
- manifest = manifest.replace(/{{organization_name}}/g, "FourGeeksAcademy");
72
81
  manifest = manifest.replace(
73
- /{{course_title}}/g,
74
- learnJson.title?.en || learnJson.title || courseSlug
82
+ /{{organization_name}}/g,
83
+ escapeXml("FourGeeksAcademy")
75
84
  );
85
+ manifest = manifest.replace(/{{course_title}}/g, courseTitle);
76
86
 
77
87
  // Generate exercises list
78
88
  const exercisesList = getFilesList(
@@ -81,23 +91,19 @@ export async function exportToScorm(options: ExportOptions): Promise<string> {
81
91
  ).join("\n ");
82
92
  manifest = manifest.replace(/{{exercises_list}}/g, exercisesList);
83
93
 
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);
94
+ // Declare all .learn/ files (config.json + assets) that ship in the package
95
+ const learnDir = path.join(scormOutDir, ".learn");
96
+ const learnFilesList = fs.existsSync(learnDir)
97
+ ? getFilesList(learnDir, ".learn").join("\n ")
98
+ : "";
99
+ manifest = manifest.replace(/{{learn_files_list}}/g, learnFilesList);
91
100
 
92
101
  fs.writeFileSync(manifestPath, manifest, "utf-8");
93
102
 
94
103
  // 6. Replace title in config/index.html
95
104
  const configIndexPath = path.join(scormOutDir, "config", "index.html");
96
105
  let indexHtml = fs.readFileSync(configIndexPath, "utf-8");
97
- indexHtml = indexHtml.replace(
98
- /{{title}}/g,
99
- learnJson.title?.en || learnJson.title || courseSlug
100
- );
106
+ indexHtml = indexHtml.replace(/{{title}}/g, courseTitle);
101
107
  fs.writeFileSync(configIndexPath, indexHtml, "utf-8");
102
108
 
103
109
  // 7. Compress as ZIP
@@ -2,6 +2,16 @@ import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import * as mkdirp from "mkdirp";
4
4
 
5
+ // Escape a string so it can be safely embedded inside XML text or attributes
6
+ export function escapeXml(value: string): string {
7
+ return String(value)
8
+ .replace(/&/g, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/"/g, "&quot;")
12
+ .replace(/'/g, "&apos;");
13
+ }
14
+
5
15
  // Utility function to copy directory recursively
6
16
  export function copyDir(src: string, dest: string) {
7
17
  if (!fs.existsSync(dest)) mkdirp.sync(dest);
@@ -45,10 +55,13 @@ export function getFilesList(folder: string, base: string) {
45
55
  list = list.concat(getFilesList(filePath, path.join(base, file.name)));
46
56
  } else {
47
57
  list.push(
48
- `<file href="${path.join(base, file.name).replace(/\\/g, "/")}" />`
58
+ `<file href="${escapeXml(
59
+ path.join(base, file.name).replace(/\\/g, "/")
60
+ )}" />`
49
61
  );
50
62
  }
51
63
  }
64
+
52
65
  return list;
53
66
  }
54
67
 
@@ -109,6 +109,35 @@ function ScormProcessFinish() {
109
109
  }
110
110
  }
111
111
 
112
+ function ScormProcessCommit() {
113
+ var result;
114
+
115
+ if (initialized == false || finishCalled == true) {
116
+ return;
117
+ }
118
+
119
+ result = API.LMSCommit("");
120
+
121
+ if (result == SCORM_FALSE) {
122
+ var errorNumber = API.LMSGetLastError();
123
+ var errorString = API.LMSGetErrorString(errorNumber);
124
+ var diagnostic = API.LMSGetDiagnostic(errorNumber);
125
+
126
+ var errorDescription =
127
+ "Number: " +
128
+ errorNumber +
129
+ "\nDescription: " +
130
+ errorString +
131
+ "\nDiagnostic: " +
132
+ diagnostic;
133
+
134
+ alert(
135
+ "Error - Could not persist data to the LMS.\n\nYour results may not be recorded.\n\n" +
136
+ errorDescription
137
+ );
138
+ }
139
+ }
140
+
112
141
  function ScormProcessGetValue(element) {
113
142
  var result;
114
143
 
@@ -9,43 +9,22 @@
9
9
  <script type="text/javascript">
10
10
 
11
11
 
12
- var currentPage = null;
13
12
  var startTimeStamp = null;
14
13
  var processedUnload = false;
15
14
  var reachedEnd = false;
16
- var language = 'es';
17
15
 
18
16
  function doStart() {
19
17
  startTimeStamp = new Date();
20
18
 
21
19
  ScormProcessInitialize();
22
20
 
21
+ // Mark the attempt as started as an early fallback. The IDE's
22
+ // ScormReporter re-asserts this (with a commit) and reports
23
+ // per-step progress/score once it mounts.
23
24
  var completionStatus = ScormProcessGetValue('cmi.core.lesson_status');
24
25
  if (completionStatus == 'not attempted') {
25
26
  ScormProcessSetValue('cmi.core.lesson_status', 'incomplete');
26
27
  }
27
-
28
- var bookmark = ScormProcessGetValue('cmi.core.lesson_location');
29
-
30
- if (bookmark == '') {
31
- currentPage = 0;
32
- } else {
33
- if (
34
- confirm(
35
- 'Would you like to resume from where you previously left off?'
36
- )
37
- ) {
38
- currentPage = parseInt(bookmark, 10);
39
- } else {
40
- currentPage = 0;
41
- }
42
- }
43
-
44
- goToPage();
45
- }
46
-
47
- function goToPage() {
48
- ScormProcessSetValue('cmi.core.lesson_location', currentPage);
49
28
  }
50
29
 
51
30
  function doUnload(pressedExit) {
@@ -72,20 +51,6 @@
72
51
  ScormProcessFinish();
73
52
  }
74
53
 
75
- function doPrevious() {
76
- if (currentPage > 0) {
77
- currentPage--;
78
- }
79
- goToPage();
80
- }
81
-
82
- function doNext() {
83
- if (currentPage < pageArray.length - 1) {
84
- currentPage++;
85
- }
86
- goToPage();
87
- }
88
-
89
54
  function doExit() {
90
55
  if (
91
56
  reachedEnd == false &&
@@ -25,13 +25,12 @@
25
25
 
26
26
  {{exercises_list}}
27
27
 
28
- {{images_list}}
28
+ {{learn_files_list}}
29
29
 
30
30
  <file href="config/index.html" />
31
31
  <file href="config/api.js" />
32
32
  <file href="config/app.js" />
33
33
  <file href="config/app.css" />
34
- <file href="config/config.json" />
35
34
 
36
35
  </resource>
37
36
  </resources>