@learnpack/learnpack 5.0.348 → 5.0.349
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/lib/utils/export/scorm.js +29 -27
- package/lib/utils/export/shared.d.ts +1 -0
- package/lib/utils/export/shared.js +11 -1
- package/lib/utils/templates/scorm/config/api.js +29 -0
- package/lib/utils/templates/scorm/imsmanifest.xml +1 -2
- package/package.json +1 -1
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2254 -2159
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/export/scorm.ts +36 -30
- package/src/utils/export/shared.ts +14 -1
- package/src/utils/templates/scorm/config/api.js +29 -0
- package/src/utils/templates/scorm/imsmanifest.xml +1 -2
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
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
/{{
|
|
74
|
-
|
|
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
|
-
//
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
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, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'");
|
|
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="${
|
|
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
|
|
|
@@ -25,13 +25,12 @@
|
|
|
25
25
|
|
|
26
26
|
{{exercises_list}}
|
|
27
27
|
|
|
28
|
-
{{
|
|
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>
|