@learnpack/learnpack 5.0.349 → 5.0.351
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/commands/serve.js +6 -0
- package/lib/utils/export/scorm.js +5 -0
- package/lib/utils/export/zip.js +4 -0
- package/lib/utils/packageManifest.d.ts +54 -0
- package/lib/utils/packageManifest.js +220 -0
- package/lib/utils/templates/scorm/config/index.html +3 -38
- package/lib/utils/titlefy.d.ts +2 -0
- package/lib/utils/titlefy.js +25 -0
- package/package.json +1 -1
- package/src/commands/serve.ts +7 -0
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2097 -2097
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/export/README.md +5 -2
- package/src/utils/export/scorm.ts +6 -0
- package/src/utils/export/zip.ts +6 -1
- package/src/utils/packageManifest.ts +392 -0
- package/src/utils/templates/scorm/config/index.html +3 -38
- package/src/utils/titlefy.ts +26 -0
package/src/ui/app.tar.gz
CHANGED
|
Binary file
|
|
@@ -4,8 +4,11 @@ This directory contains utilities for exporting courses to different formats.
|
|
|
4
4
|
|
|
5
5
|
## Supported Formats
|
|
6
6
|
|
|
7
|
-
- **SCORM**: Learning Management System compatible package
|
|
8
|
-
- **
|
|
7
|
+
- **SCORM**: Learning Management System compatible package (includes `.learn/package-manifest.json`)
|
|
8
|
+
- **ZIP**: Full course mirror from GCS (includes `.learn/package-manifest.json`)
|
|
9
|
+
- **EPUB**: E-book format for offline reading (does not include `package-manifest.json`)
|
|
10
|
+
|
|
11
|
+
`package-manifest.json` is regenerated on each ZIP/SCORM export with `publishedAt: null`. See [`docs/package-manifest.md`](../../../../docs/package-manifest.md) in the monorepo.
|
|
9
12
|
|
|
10
13
|
## Usage
|
|
11
14
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getCourseMetadata,
|
|
13
13
|
escapeXml,
|
|
14
14
|
} from "./shared";
|
|
15
|
+
import { generateAndPersistPackageManifest } from "../packageManifest";
|
|
15
16
|
|
|
16
17
|
export async function exportToScorm(options: ExportOptions): Promise<string> {
|
|
17
18
|
const { courseSlug, bucket, outDir } = options;
|
|
@@ -67,6 +68,11 @@ export async function exportToScorm(options: ExportOptions): Promise<string> {
|
|
|
67
68
|
path.join(scormOutDir, ".learn")
|
|
68
69
|
);
|
|
69
70
|
|
|
71
|
+
await generateAndPersistPackageManifest(bucket, courseSlug, {
|
|
72
|
+
publishedAt: null,
|
|
73
|
+
localDir: scormOutDir,
|
|
74
|
+
});
|
|
75
|
+
|
|
70
76
|
// 4. Read learn.json for course info
|
|
71
77
|
const learnJson = await getCourseMetadata(bucket, courseSlug);
|
|
72
78
|
|
package/src/utils/export/zip.ts
CHANGED
|
@@ -5,7 +5,8 @@ import * as mkdirp from "mkdirp";
|
|
|
5
5
|
import * as rimraf from "rimraf";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
import { ExportOptions } from "./types";
|
|
8
|
-
import { downloadS3Folder
|
|
8
|
+
import { downloadS3Folder } from "./shared";
|
|
9
|
+
import { generateAndPersistPackageManifest } from "../packageManifest";
|
|
9
10
|
|
|
10
11
|
export async function exportToZip(options: ExportOptions): Promise<string> {
|
|
11
12
|
const { courseSlug, bucket, outDir } = options;
|
|
@@ -16,6 +17,10 @@ export async function exportToZip(options: ExportOptions): Promise<string> {
|
|
|
16
17
|
rimraf.sync(zipOutDir);
|
|
17
18
|
mkdirp.sync(zipOutDir);
|
|
18
19
|
|
|
20
|
+
await generateAndPersistPackageManifest(bucket, courseSlug, {
|
|
21
|
+
publishedAt: null,
|
|
22
|
+
});
|
|
23
|
+
|
|
19
24
|
// 2. Download all course files from bucket to temporary directory
|
|
20
25
|
await downloadS3Folder(bucket, `courses/${courseSlug}/`, zipOutDir);
|
|
21
26
|
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { Bucket } from "@google-cloud/storage"
|
|
2
|
+
import * as fs from "fs"
|
|
3
|
+
import * as mkdirp from "mkdirp"
|
|
4
|
+
import * as path from "path"
|
|
5
|
+
import { Lesson, Syllabus } from "../models/creator"
|
|
6
|
+
import { buildConfig, Exercise } from "./configBuilder"
|
|
7
|
+
import { getReadmeExtension, slugify } from "./creatorUtilities"
|
|
8
|
+
import { formatSidebarTitle } from "./titlefy"
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
const frontMatter = require("front-matter")
|
|
12
|
+
|
|
13
|
+
export const PACKAGE_MANIFEST_FILENAME = "package-manifest.json"
|
|
14
|
+
export const PACKAGE_MANIFEST_REL_PATH = ".learn/package-manifest.json"
|
|
15
|
+
export const SCHEMA_VERSION = 1
|
|
16
|
+
|
|
17
|
+
export type LessonType = "READ" | "CODE" | "QUIZ";
|
|
18
|
+
|
|
19
|
+
export type PackageManifestLesson = {
|
|
20
|
+
id: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
position: number;
|
|
23
|
+
type: LessonType;
|
|
24
|
+
title: Record<string, string>;
|
|
25
|
+
video: Record<string, string | null>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PackageManifest = {
|
|
29
|
+
schemaVersion: number;
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
publishedAt: string | null;
|
|
32
|
+
slug: string;
|
|
33
|
+
title: Record<string, string>;
|
|
34
|
+
description: Record<string, string>;
|
|
35
|
+
preview: string;
|
|
36
|
+
technologies?: string[];
|
|
37
|
+
difficulty?: string;
|
|
38
|
+
duration?: number;
|
|
39
|
+
lessons: PackageManifestLesson[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ReadmeFrontmatter = {
|
|
43
|
+
tutorial?: string;
|
|
44
|
+
intro?: string;
|
|
45
|
+
title?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type PackageManifestBuildInput = {
|
|
49
|
+
learnJson: Record<string, unknown>;
|
|
50
|
+
syllabus: Syllabus | null;
|
|
51
|
+
sidebar: Record<string, Record<string, string>>;
|
|
52
|
+
exercises: Exercise[];
|
|
53
|
+
readmeFrontmatters: Record<string, Record<string, ReadmeFrontmatter>>;
|
|
54
|
+
options: {
|
|
55
|
+
slug: string;
|
|
56
|
+
generatedAt: string;
|
|
57
|
+
publishedAt: string | null;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const VALID_LESSON_TYPES = new Set<LessonType>(["READ", "CODE", "QUIZ"])
|
|
62
|
+
|
|
63
|
+
export function packageManifestGcsPath(courseSlug: string): string {
|
|
64
|
+
return `courses/${courseSlug}/${PACKAGE_MANIFEST_REL_PATH}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolvePreviewUrl(slug: string, preview?: string): string {
|
|
68
|
+
if (preview && preview.startsWith("http")) {
|
|
69
|
+
return preview
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `https://${slug}.learn-pack.com/preview.png`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findSyllabusLesson(
|
|
76
|
+
exerciseSlug: string,
|
|
77
|
+
lessons: Lesson[]
|
|
78
|
+
): Lesson | undefined {
|
|
79
|
+
return lessons.find(lesson => {
|
|
80
|
+
const candidates = [
|
|
81
|
+
slugify(lesson.id + "-" + lesson.title),
|
|
82
|
+
lesson.uid,
|
|
83
|
+
].filter(Boolean)
|
|
84
|
+
return candidates.includes(exerciseSlug)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deriveLessonId(exerciseSlug: string): string {
|
|
89
|
+
const match = exerciseSlug.match(/^(\d+(?:\.\d+)?)/)
|
|
90
|
+
return match ? match[1] : exerciseSlug
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function coerceLessonType(type: string | undefined): LessonType {
|
|
94
|
+
if (type && VALID_LESSON_TYPES.has(type as LessonType)) {
|
|
95
|
+
return type as LessonType
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (type) {
|
|
99
|
+
console.warn(
|
|
100
|
+
`[package-manifest] Invalid lesson type "${type}", defaulting to READ`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return "READ"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function extractReadmeH1(body: string): string | null {
|
|
108
|
+
const match = body.match(/^#\s+(.+)$/m)
|
|
109
|
+
if (!match) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const title = match[1].replace(/\r/g, "").trim()
|
|
114
|
+
return title || null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function normalizeReadmeTitle(raw: string): string | null {
|
|
118
|
+
const normalized = raw
|
|
119
|
+
.trim()
|
|
120
|
+
.replace(/^\d+(?:\.\d+)?\s*[.:-]?\s*/, "")
|
|
121
|
+
.trim()
|
|
122
|
+
return normalized || null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function firstNonEmpty(
|
|
126
|
+
...candidates: Array<string | null | undefined>
|
|
127
|
+
): string | null {
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const value = candidate?.trim()
|
|
130
|
+
if (value) {
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveLessonTitle(
|
|
139
|
+
exercise: Exercise,
|
|
140
|
+
sidebar: Record<string, Record<string, string>>,
|
|
141
|
+
syllabusLesson: Lesson | undefined,
|
|
142
|
+
courseLang: string,
|
|
143
|
+
readmeByLang: Record<string, ReadmeFrontmatter>
|
|
144
|
+
): Record<string, string> {
|
|
145
|
+
const sidebarEntry = sidebar[exercise.slug] ?? {}
|
|
146
|
+
const languages = new Set([
|
|
147
|
+
...Object.keys(exercise.translations),
|
|
148
|
+
...Object.keys(sidebarEntry),
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
const titles: Record<string, string> = {}
|
|
152
|
+
|
|
153
|
+
for (const lang of languages) {
|
|
154
|
+
const title = firstNonEmpty(
|
|
155
|
+
readmeByLang[lang]?.title,
|
|
156
|
+
sidebarEntry[lang] ? formatSidebarTitle(sidebarEntry[lang]) : null,
|
|
157
|
+
lang === courseLang ? syllabusLesson?.title : null
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if (title) {
|
|
161
|
+
titles[lang] = title
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return titles
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveLessonVideos(
|
|
169
|
+
exercise: Exercise,
|
|
170
|
+
readmeFrontmatters: Record<string, Record<string, ReadmeFrontmatter>>
|
|
171
|
+
): Record<string, string | null> {
|
|
172
|
+
const video: Record<string, string | null> = {}
|
|
173
|
+
const frontmatters = readmeFrontmatters[exercise.slug] || {}
|
|
174
|
+
|
|
175
|
+
for (const lang of Object.keys(exercise.translations)) {
|
|
176
|
+
const attrs = frontmatters[lang] || {}
|
|
177
|
+
const url = attrs.tutorial || attrs.intro || null
|
|
178
|
+
video[lang] = url && url.trim().length > 0 ? url.trim() : null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return video
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function buildPackageManifestFromSources(
|
|
185
|
+
input: PackageManifestBuildInput
|
|
186
|
+
): PackageManifest {
|
|
187
|
+
const {
|
|
188
|
+
learnJson,
|
|
189
|
+
syllabus,
|
|
190
|
+
sidebar,
|
|
191
|
+
exercises,
|
|
192
|
+
readmeFrontmatters,
|
|
193
|
+
options,
|
|
194
|
+
} = input
|
|
195
|
+
|
|
196
|
+
const syllabusLessons = syllabus?.lessons ?? []
|
|
197
|
+
const courseLang = syllabus?.courseInfo?.language || "en"
|
|
198
|
+
|
|
199
|
+
const sortedExercises = [...exercises].sort(
|
|
200
|
+
(a, b) => a.position - b.position
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const lessons: PackageManifestLesson[] = sortedExercises.map(exercise => {
|
|
204
|
+
const syllabusLesson = findSyllabusLesson(exercise.slug, syllabusLessons)
|
|
205
|
+
|
|
206
|
+
const id = syllabusLesson?.id ?? deriveLessonId(exercise.slug)
|
|
207
|
+
const type = syllabusLesson ?
|
|
208
|
+
coerceLessonType(syllabusLesson.type) :
|
|
209
|
+
"READ"
|
|
210
|
+
|
|
211
|
+
if (!syllabusLesson) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`[package-manifest] No syllabus match for exercise "${exercise.slug}", using derived id "${id}"`
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
id,
|
|
219
|
+
slug: exercise.slug,
|
|
220
|
+
position: exercise.position,
|
|
221
|
+
type,
|
|
222
|
+
title: resolveLessonTitle(
|
|
223
|
+
exercise,
|
|
224
|
+
sidebar,
|
|
225
|
+
syllabusLesson,
|
|
226
|
+
courseLang,
|
|
227
|
+
readmeFrontmatters[exercise.slug] ?? {}
|
|
228
|
+
),
|
|
229
|
+
video: resolveLessonVideos(exercise, readmeFrontmatters),
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const title = (learnJson.title as Record<string, string>) || {}
|
|
234
|
+
const description = (learnJson.description as Record<string, string>) || {}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
schemaVersion: SCHEMA_VERSION,
|
|
238
|
+
generatedAt: options.generatedAt,
|
|
239
|
+
publishedAt: options.publishedAt,
|
|
240
|
+
slug: options.slug,
|
|
241
|
+
title,
|
|
242
|
+
description,
|
|
243
|
+
preview: resolvePreviewUrl(
|
|
244
|
+
options.slug,
|
|
245
|
+
learnJson.preview as string | undefined
|
|
246
|
+
),
|
|
247
|
+
technologies: learnJson.technologies as string[] | undefined,
|
|
248
|
+
difficulty: learnJson.difficulty as string | undefined,
|
|
249
|
+
duration: learnJson.duration as number | undefined,
|
|
250
|
+
lessons,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function downloadJsonFile<T>(
|
|
255
|
+
bucket: Bucket,
|
|
256
|
+
filePath: string
|
|
257
|
+
): Promise<T | null> {
|
|
258
|
+
try {
|
|
259
|
+
const [buf] = await bucket.file(filePath).download()
|
|
260
|
+
return JSON.parse(buf.toString()) as T
|
|
261
|
+
} catch {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function readReadmeFrontmatter(
|
|
267
|
+
bucket: Bucket,
|
|
268
|
+
exerciseSlug: string,
|
|
269
|
+
lang: string,
|
|
270
|
+
readmePath: string
|
|
271
|
+
): Promise<{
|
|
272
|
+
exerciseSlug: string;
|
|
273
|
+
lang: string;
|
|
274
|
+
frontmatter: ReadmeFrontmatter;
|
|
275
|
+
}> {
|
|
276
|
+
try {
|
|
277
|
+
const [buf] = await bucket.file(readmePath).download()
|
|
278
|
+
const parsed = frontMatter(buf.toString())
|
|
279
|
+
const attributes = parsed.attributes || {}
|
|
280
|
+
const h1 = extractReadmeH1(parsed.body || "")
|
|
281
|
+
const title = h1 ? normalizeReadmeTitle(h1) : null
|
|
282
|
+
return {
|
|
283
|
+
exerciseSlug,
|
|
284
|
+
lang,
|
|
285
|
+
frontmatter: {
|
|
286
|
+
tutorial: attributes.tutorial,
|
|
287
|
+
intro: attributes.intro,
|
|
288
|
+
...(title ? { title } : {}),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.warn(
|
|
293
|
+
`[package-manifest] Could not read README for ${exerciseSlug} (${lang}):`,
|
|
294
|
+
(error as Error).message
|
|
295
|
+
)
|
|
296
|
+
return { exerciseSlug, lang, frontmatter: {} }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function collectReadmeFrontmatters(
|
|
301
|
+
bucket: Bucket,
|
|
302
|
+
courseSlug: string,
|
|
303
|
+
exercises: Exercise[]
|
|
304
|
+
): Promise<Record<string, Record<string, ReadmeFrontmatter>>> {
|
|
305
|
+
const readmeTasks = exercises.flatMap(exercise =>
|
|
306
|
+
Object.keys(exercise.translations).map(lang => {
|
|
307
|
+
const readmeFilename = `README${getReadmeExtension(lang)}`
|
|
308
|
+
const readmePath = `courses/${courseSlug}/exercises/${exercise.slug}/${readmeFilename}`
|
|
309
|
+
return readReadmeFrontmatter(bucket, exercise.slug, lang, readmePath)
|
|
310
|
+
})
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const readmeEntries = await Promise.all(readmeTasks)
|
|
314
|
+
const result: Record<string, Record<string, ReadmeFrontmatter>> = {}
|
|
315
|
+
|
|
316
|
+
for (const { exerciseSlug, lang, frontmatter } of readmeEntries) {
|
|
317
|
+
if (!result[exerciseSlug]) {
|
|
318
|
+
result[exerciseSlug] = {}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
result[exerciseSlug][lang] = frontmatter
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return result
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function writeManifestJson(
|
|
328
|
+
manifest: PackageManifest,
|
|
329
|
+
localDir?: string
|
|
330
|
+
): string {
|
|
331
|
+
const json = JSON.stringify(manifest, null, 2)
|
|
332
|
+
|
|
333
|
+
if (localDir) {
|
|
334
|
+
const learnDir = path.join(localDir, ".learn")
|
|
335
|
+
mkdirp.sync(learnDir)
|
|
336
|
+
const localPath = path.join(learnDir, PACKAGE_MANIFEST_FILENAME)
|
|
337
|
+
fs.writeFileSync(localPath, json, "utf-8")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return json
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function generateAndPersistPackageManifest(
|
|
344
|
+
bucket: Bucket,
|
|
345
|
+
courseSlug: string,
|
|
346
|
+
options: { publishedAt: string | null; localDir?: string }
|
|
347
|
+
): Promise<PackageManifest> {
|
|
348
|
+
const generatedAt = new Date().toISOString()
|
|
349
|
+
|
|
350
|
+
const { config: learnJson, exercises } = await buildConfig(
|
|
351
|
+
bucket,
|
|
352
|
+
courseSlug
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const syllabus = await downloadJsonFile<Syllabus>(
|
|
356
|
+
bucket,
|
|
357
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
const sidebar =
|
|
361
|
+
(await downloadJsonFile<Record<string, Record<string, string>>>(
|
|
362
|
+
bucket,
|
|
363
|
+
`courses/${courseSlug}/.learn/sidebar.json`
|
|
364
|
+
)) || {}
|
|
365
|
+
|
|
366
|
+
const readmeFrontmatters = await collectReadmeFrontmatters(
|
|
367
|
+
bucket,
|
|
368
|
+
courseSlug,
|
|
369
|
+
exercises
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
const manifest = buildPackageManifestFromSources({
|
|
373
|
+
learnJson,
|
|
374
|
+
syllabus,
|
|
375
|
+
sidebar,
|
|
376
|
+
exercises,
|
|
377
|
+
readmeFrontmatters,
|
|
378
|
+
options: {
|
|
379
|
+
slug: courseSlug,
|
|
380
|
+
generatedAt,
|
|
381
|
+
publishedAt: options.publishedAt,
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const json = writeManifestJson(manifest, options.localDir)
|
|
386
|
+
|
|
387
|
+
await bucket
|
|
388
|
+
.file(packageManifestGcsPath(courseSlug))
|
|
389
|
+
.save(Buffer.from(json, "utf8"))
|
|
390
|
+
|
|
391
|
+
return manifest
|
|
392
|
+
}
|
|
@@ -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 &&
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const SLUG_PATTERN = /^\d+(?:\.\d+)?-/
|
|
2
|
+
|
|
3
|
+
export function titlefy(str: string): string {
|
|
4
|
+
const arr = str.split("-")
|
|
5
|
+
arr.shift()
|
|
6
|
+
const result = arr.join(" ")
|
|
7
|
+
if (!result) {
|
|
8
|
+
return ""
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return result.charAt(0).toUpperCase() + result.slice(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatSidebarTitle(value: string): string | null {
|
|
15
|
+
const trimmed = value.trim()
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const formatted = SLUG_PATTERN.test(trimmed) ? titlefy(trimmed) : trimmed
|
|
25
|
+
return formatted.trim() || null
|
|
26
|
+
}
|