course-format-ts 1.0.0
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/.idea/edu-format-ts.iml +10 -0
- package/.idea/inspectionProfiles/Project_Default.xml +28 -0
- package/.idea/misc.xml +4 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/AGENTS.md +1 -0
- package/dist/courseFormat/AnswerPlaceholder.d.ts +37 -0
- package/dist/courseFormat/AnswerPlaceholder.js +101 -0
- package/dist/courseFormat/AnswerPlaceholderComparator.d.ts +4 -0
- package/dist/courseFormat/AnswerPlaceholderComparator.js +8 -0
- package/dist/courseFormat/AnswerPlaceholderDependency.d.ts +19 -0
- package/dist/courseFormat/AnswerPlaceholderDependency.js +91 -0
- package/dist/courseFormat/CheckFeedback.d.ts +20 -0
- package/dist/courseFormat/CheckFeedback.js +58 -0
- package/dist/courseFormat/CheckResult.d.ts +33 -0
- package/dist/courseFormat/CheckResult.js +58 -0
- package/dist/courseFormat/CheckResultSeverity.d.ts +7 -0
- package/dist/courseFormat/CheckResultSeverity.js +17 -0
- package/dist/courseFormat/CheckStatus.d.ts +5 -0
- package/dist/courseFormat/CheckStatus.js +9 -0
- package/dist/courseFormat/Course.d.ts +68 -0
- package/dist/courseFormat/Course.js +165 -0
- package/dist/courseFormat/CourseMode.d.ts +4 -0
- package/dist/courseFormat/CourseMode.js +8 -0
- package/dist/courseFormat/CourseVisibility.d.ts +4 -0
- package/dist/courseFormat/CourseVisibility.js +8 -0
- package/dist/courseFormat/CourseraCourse.d.ts +5 -0
- package/dist/courseFormat/CourseraCourse.js +15 -0
- package/dist/courseFormat/DescriptionFormat.d.ts +5 -0
- package/dist/courseFormat/DescriptionFormat.js +9 -0
- package/dist/courseFormat/EduCourse.d.ts +17 -0
- package/dist/courseFormat/EduCourse.js +42 -0
- package/dist/courseFormat/EduFile.d.ts +22 -0
- package/dist/courseFormat/EduFile.js +110 -0
- package/dist/courseFormat/EduFileErrorHighlightLevel.d.ts +5 -0
- package/dist/courseFormat/EduFileErrorHighlightLevel.js +9 -0
- package/dist/courseFormat/EduFormatNames.d.ts +75 -0
- package/dist/courseFormat/EduFormatNames.js +80 -0
- package/dist/courseFormat/EduTestInfo.d.ts +28 -0
- package/dist/courseFormat/EduTestInfo.js +68 -0
- package/dist/courseFormat/EduVersions.d.ts +2 -0
- package/dist/courseFormat/EduVersions.js +5 -0
- package/dist/courseFormat/FileContents.d.ts +36 -0
- package/dist/courseFormat/FileContents.js +67 -0
- package/dist/courseFormat/FileContentsFactory.d.ts +17 -0
- package/dist/courseFormat/FileContentsFactory.js +2 -0
- package/dist/courseFormat/FrameworkLesson.d.ts +9 -0
- package/dist/courseFormat/FrameworkLesson.js +28 -0
- package/dist/courseFormat/ItemContainer.d.ts +13 -0
- package/dist/courseFormat/ItemContainer.js +45 -0
- package/dist/courseFormat/JBAccountUserInfo.d.ts +9 -0
- package/dist/courseFormat/JBAccountUserInfo.js +20 -0
- package/dist/courseFormat/Language.d.ts +4 -0
- package/dist/courseFormat/Language.js +33 -0
- package/dist/courseFormat/Lesson.d.ts +20 -0
- package/dist/courseFormat/Lesson.js +56 -0
- package/dist/courseFormat/LessonContainer.d.ts +16 -0
- package/dist/courseFormat/LessonContainer.js +54 -0
- package/dist/courseFormat/PluginInfo.d.ts +7 -0
- package/dist/courseFormat/PluginInfo.js +15 -0
- package/dist/courseFormat/Section.d.ts +8 -0
- package/dist/courseFormat/Section.js +27 -0
- package/dist/courseFormat/StudyItem.d.ts +20 -0
- package/dist/courseFormat/StudyItem.js +47 -0
- package/dist/courseFormat/Tags.d.ts +16 -0
- package/dist/courseFormat/Tags.js +42 -0
- package/dist/courseFormat/TaskFile.d.ts +26 -0
- package/dist/courseFormat/TaskFile.js +72 -0
- package/dist/courseFormat/UserInfo.d.ts +3 -0
- package/dist/courseFormat/UserInfo.js +2 -0
- package/dist/courseFormat/Vendor.d.ts +7 -0
- package/dist/courseFormat/Vendor.js +14 -0
- package/dist/courseFormat/attempts/Attempt.d.ts +12 -0
- package/dist/courseFormat/attempts/Attempt.js +25 -0
- package/dist/courseFormat/attempts/AttemptBase.d.ts +9 -0
- package/dist/courseFormat/attempts/AttemptBase.js +28 -0
- package/dist/courseFormat/attempts/DataTaskAttempt.d.ts +6 -0
- package/dist/courseFormat/attempts/DataTaskAttempt.js +24 -0
- package/dist/courseFormat/attempts/Dataset.d.ts +12 -0
- package/dist/courseFormat/attempts/Dataset.js +21 -0
- package/dist/courseFormat/fileUtils.d.ts +5 -0
- package/dist/courseFormat/fileUtils.js +32 -0
- package/dist/courseFormat/hyperskill/HyperskillCourse.d.ts +13 -0
- package/dist/courseFormat/hyperskill/HyperskillCourse.js +25 -0
- package/dist/courseFormat/hyperskill/HyperskillProject.d.ts +10 -0
- package/dist/courseFormat/hyperskill/HyperskillProject.js +16 -0
- package/dist/courseFormat/hyperskill/HyperskillStage.d.ts +8 -0
- package/dist/courseFormat/hyperskill/HyperskillStage.js +20 -0
- package/dist/courseFormat/hyperskill/HyperskillTaskType.d.ts +4 -0
- package/dist/courseFormat/hyperskill/HyperskillTaskType.js +26 -0
- package/dist/courseFormat/hyperskill/HyperskillTopic.d.ts +5 -0
- package/dist/courseFormat/hyperskill/HyperskillTopic.js +11 -0
- package/dist/courseFormat/loggerUtils.d.ts +1 -0
- package/dist/courseFormat/loggerUtils.js +6 -0
- package/dist/courseFormat/stepik/StepikCourse.d.ts +5 -0
- package/dist/courseFormat/stepik/StepikCourse.js +15 -0
- package/dist/courseFormat/stepik/StepikLesson.d.ts +6 -0
- package/dist/courseFormat/stepik/StepikLesson.js +16 -0
- package/dist/courseFormat/tasks/AnswerTask.d.ts +8 -0
- package/dist/courseFormat/tasks/AnswerTask.js +11 -0
- package/dist/courseFormat/tasks/CodeTask.d.ts +12 -0
- package/dist/courseFormat/tasks/CodeTask.js +21 -0
- package/dist/courseFormat/tasks/DataTask.d.ts +18 -0
- package/dist/courseFormat/tasks/DataTask.js +32 -0
- package/dist/courseFormat/tasks/EduTask.d.ts +12 -0
- package/dist/courseFormat/tasks/EduTask.js +22 -0
- package/dist/courseFormat/tasks/IdeTask.d.ts +9 -0
- package/dist/courseFormat/tasks/IdeTask.js +14 -0
- package/dist/courseFormat/tasks/NumberTask.d.ts +9 -0
- package/dist/courseFormat/tasks/NumberTask.js +14 -0
- package/dist/courseFormat/tasks/OutputTask.d.ts +10 -0
- package/dist/courseFormat/tasks/OutputTask.js +18 -0
- package/dist/courseFormat/tasks/OutputTaskBase.d.ts +14 -0
- package/dist/courseFormat/tasks/OutputTaskBase.js +19 -0
- package/dist/courseFormat/tasks/RemoteEduTask.d.ts +9 -0
- package/dist/courseFormat/tasks/RemoteEduTask.js +15 -0
- package/dist/courseFormat/tasks/StringTask.d.ts +9 -0
- package/dist/courseFormat/tasks/StringTask.js +14 -0
- package/dist/courseFormat/tasks/TableTask.d.ts +17 -0
- package/dist/courseFormat/tasks/TableTask.js +43 -0
- package/dist/courseFormat/tasks/Task.d.ts +45 -0
- package/dist/courseFormat/tasks/Task.js +155 -0
- package/dist/courseFormat/tasks/TheoryTask.d.ts +10 -0
- package/dist/courseFormat/tasks/TheoryTask.js +15 -0
- package/dist/courseFormat/tasks/UnsupportedTask.d.ts +9 -0
- package/dist/courseFormat/tasks/UnsupportedTask.js +14 -0
- package/dist/courseFormat/tasks/choice/ChoiceOption.d.ts +10 -0
- package/dist/courseFormat/tasks/choice/ChoiceOption.js +33 -0
- package/dist/courseFormat/tasks/choice/ChoiceOptionStatus.d.ts +5 -0
- package/dist/courseFormat/tasks/choice/ChoiceOptionStatus.js +9 -0
- package/dist/courseFormat/tasks/choice/ChoiceTask.d.ts +23 -0
- package/dist/courseFormat/tasks/choice/ChoiceTask.js +47 -0
- package/dist/courseFormat/tasks/matching/MatchingTask.d.ts +10 -0
- package/dist/courseFormat/tasks/matching/MatchingTask.js +15 -0
- package/dist/courseFormat/tasks/matching/SortingBasedTask.d.ts +16 -0
- package/dist/courseFormat/tasks/matching/SortingBasedTask.js +50 -0
- package/dist/courseFormat/tasks/matching/SortingTask.d.ts +9 -0
- package/dist/courseFormat/tasks/matching/SortingTask.js +14 -0
- package/dist/courseFormat/uiMessages.d.ts +3 -0
- package/dist/courseFormat/uiMessages.js +14 -0
- package/dist/disk-loader.d.ts +4 -0
- package/dist/disk-loader.js +389 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +64 -0
- package/dist/loader.d.ts +7 -0
- package/dist/loader.js +435 -0
- package/dist/models.d.ts +49 -0
- package/dist/models.js +2 -0
- package/dist/zip-loader.d.ts +4 -0
- package/dist/zip-loader.js +431 -0
- package/example-course-project/course-info.yaml +15 -0
- package/example-course-project/lesson1/lesson-info.yaml +3 -0
- package/example-course-project/lesson1/lesson-remote-info.yaml +1 -0
- package/example-course-project/lesson1/task1/Task.txt +1 -0
- package/example-course-project/lesson1/task1/task-info.yaml +12 -0
- package/example-course-project/lesson1/task1/task-remote-info.yaml +1 -0
- package/example-course-project/lesson1/task1/task.md +47 -0
- package/example-course-project/lesson1/task1/tests/Tests.txt +0 -0
- package/example-course-project/lesson1/task2/Task.txt +1 -0
- package/example-course-project/lesson1/task2/task-info.yaml +12 -0
- package/example-course-project/lesson1/task2/task-remote-info.yaml +1 -0
- package/example-course-project/lesson1/task2/task.md +47 -0
- package/example-course-project/lesson1/task2/tests/Tests.txt +0 -0
- package/package.json +19 -0
- package/src/@types/mime-types.d.ts +3 -0
- package/src/courseFormat/AnswerPlaceholder.ts +121 -0
- package/src/courseFormat/AnswerPlaceholderComparator.ts +7 -0
- package/src/courseFormat/AnswerPlaceholderDependency.ts +122 -0
- package/src/courseFormat/CheckFeedback.ts +71 -0
- package/src/courseFormat/CheckResult.ts +92 -0
- package/src/courseFormat/CheckResultSeverity.ts +13 -0
- package/src/courseFormat/CheckStatus.ts +5 -0
- package/src/courseFormat/Course.ts +201 -0
- package/src/courseFormat/CourseMode.ts +4 -0
- package/src/courseFormat/CourseVisibility.ts +4 -0
- package/src/courseFormat/CourseraCourse.ts +10 -0
- package/src/courseFormat/DescriptionFormat.ts +5 -0
- package/src/courseFormat/EduCourse.ts +41 -0
- package/src/courseFormat/EduFile.ts +133 -0
- package/src/courseFormat/EduFileErrorHighlightLevel.ts +5 -0
- package/src/courseFormat/EduFormatNames.ts +95 -0
- package/src/courseFormat/EduTestInfo.ts +87 -0
- package/src/courseFormat/EduVersions.ts +2 -0
- package/src/courseFormat/FileContents.ts +97 -0
- package/src/courseFormat/FileContentsFactory.ts +19 -0
- package/src/courseFormat/FrameworkLesson.ts +29 -0
- package/src/courseFormat/ItemContainer.ts +47 -0
- package/src/courseFormat/JBAccountUserInfo.ts +21 -0
- package/src/courseFormat/Language.ts +31 -0
- package/src/courseFormat/Lesson.ts +69 -0
- package/src/courseFormat/LessonContainer.ts +65 -0
- package/src/courseFormat/PluginInfo.ts +15 -0
- package/src/courseFormat/Section.ts +29 -0
- package/src/courseFormat/StudyItem.ts +55 -0
- package/src/courseFormat/Tags.ts +45 -0
- package/src/courseFormat/TaskFile.ts +88 -0
- package/src/courseFormat/UserInfo.ts +3 -0
- package/src/courseFormat/Vendor.ts +15 -0
- package/src/courseFormat/attempts/Attempt.ts +28 -0
- package/src/courseFormat/attempts/AttemptBase.ts +24 -0
- package/src/courseFormat/attempts/DataTaskAttempt.ts +19 -0
- package/src/courseFormat/attempts/Dataset.ts +13 -0
- package/src/courseFormat/fileUtils.ts +31 -0
- package/src/courseFormat/hyperskill/HyperskillCourse.ts +24 -0
- package/src/courseFormat/hyperskill/HyperskillProject.ts +10 -0
- package/src/courseFormat/hyperskill/HyperskillStage.ts +15 -0
- package/src/courseFormat/hyperskill/HyperskillTaskType.ts +23 -0
- package/src/courseFormat/hyperskill/HyperskillTopic.ts +5 -0
- package/src/courseFormat/loggerUtils.ts +3 -0
- package/src/courseFormat/stepik/StepikCourse.ts +10 -0
- package/src/courseFormat/stepik/StepikLesson.ts +11 -0
- package/src/courseFormat/tasks/AnswerTask.ts +13 -0
- package/src/courseFormat/tasks/CodeTask.ts +42 -0
- package/src/courseFormat/tasks/DataTask.ts +37 -0
- package/src/courseFormat/tasks/EduTask.ts +26 -0
- package/src/courseFormat/tasks/IdeTask.ts +17 -0
- package/src/courseFormat/tasks/NumberTask.ts +17 -0
- package/src/courseFormat/tasks/OutputTask.ts +21 -0
- package/src/courseFormat/tasks/OutputTaskBase.ts +23 -0
- package/src/courseFormat/tasks/RemoteEduTask.ts +18 -0
- package/src/courseFormat/tasks/StringTask.ts +17 -0
- package/src/courseFormat/tasks/TableTask.ts +51 -0
- package/src/courseFormat/tasks/Task.ts +181 -0
- package/src/courseFormat/tasks/TheoryTask.ts +19 -0
- package/src/courseFormat/tasks/UnsupportedTask.ts +17 -0
- package/src/courseFormat/tasks/choice/ChoiceOption.ts +37 -0
- package/src/courseFormat/tasks/choice/ChoiceOptionStatus.ts +5 -0
- package/src/courseFormat/tasks/choice/ChoiceTask.ts +57 -0
- package/src/courseFormat/tasks/matching/MatchingTask.ts +19 -0
- package/src/courseFormat/tasks/matching/SortingBasedTask.ts +59 -0
- package/src/courseFormat/tasks/matching/SortingTask.ts +17 -0
- package/src/courseFormat/uiMessages.ts +12 -0
- package/src/disk-loader.ts +463 -0
- package/src/index.ts +33 -0
- package/src/models.ts +54 -0
- package/src/zip-loader.ts +583 -0
- package/test/load-course.test.js +279 -0
- package/test/load-zip-course.test.js +73 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { unzipSync } from "fflate"
|
|
2
|
+
import type { Vendor } from "./models"
|
|
3
|
+
import { Vendor as VendorClass } from "./courseFormat/Vendor"
|
|
4
|
+
import { Course } from "./courseFormat/Course"
|
|
5
|
+
import { EduCourse } from "./courseFormat/EduCourse"
|
|
6
|
+
import { CourseraCourse } from "./courseFormat/CourseraCourse"
|
|
7
|
+
import { HyperskillCourse } from "./courseFormat/hyperskill/HyperskillCourse"
|
|
8
|
+
import { StepikCourse } from "./courseFormat/stepik/StepikCourse"
|
|
9
|
+
import { Section } from "./courseFormat/Section"
|
|
10
|
+
import { Lesson } from "./courseFormat/Lesson"
|
|
11
|
+
import { LessonContainer } from "./courseFormat/LessonContainer"
|
|
12
|
+
import { Task } from "./courseFormat/tasks/Task"
|
|
13
|
+
import { EduTask } from "./courseFormat/tasks/EduTask"
|
|
14
|
+
import { CodeTask } from "./courseFormat/tasks/CodeTask"
|
|
15
|
+
import { NumberTask } from "./courseFormat/tasks/NumberTask"
|
|
16
|
+
import { StringTask } from "./courseFormat/tasks/StringTask"
|
|
17
|
+
import { OutputTask } from "./courseFormat/tasks/OutputTask"
|
|
18
|
+
import { DataTask } from "./courseFormat/tasks/DataTask"
|
|
19
|
+
import { TableTask } from "./courseFormat/tasks/TableTask"
|
|
20
|
+
import { TheoryTask } from "./courseFormat/tasks/TheoryTask"
|
|
21
|
+
import { IdeTask } from "./courseFormat/tasks/IdeTask"
|
|
22
|
+
import { UnsupportedTask } from "./courseFormat/tasks/UnsupportedTask"
|
|
23
|
+
import { RemoteEduTask } from "./courseFormat/tasks/RemoteEduTask"
|
|
24
|
+
import { ChoiceTask } from "./courseFormat/tasks/choice/ChoiceTask"
|
|
25
|
+
import { ChoiceOption } from "./courseFormat/tasks/choice/ChoiceOption"
|
|
26
|
+
import { ChoiceOptionStatus } from "./courseFormat/tasks/choice/ChoiceOptionStatus"
|
|
27
|
+
import { MatchingTask } from "./courseFormat/tasks/matching/MatchingTask"
|
|
28
|
+
import { SortingTask } from "./courseFormat/tasks/matching/SortingTask"
|
|
29
|
+
import { TaskFile } from "./courseFormat/TaskFile"
|
|
30
|
+
import { AnswerPlaceholder } from "./courseFormat/AnswerPlaceholder"
|
|
31
|
+
import { CheckStatus } from "./courseFormat/CheckStatus"
|
|
32
|
+
import { CourseMode } from "./courseFormat/CourseMode"
|
|
33
|
+
import { DescriptionFormat } from "./courseFormat/DescriptionFormat"
|
|
34
|
+
import { EduFile } from "./courseFormat/EduFile"
|
|
35
|
+
|
|
36
|
+
const TEST_AES_KEY = "DFC929E375655998A34E56A21C98651C"
|
|
37
|
+
|
|
38
|
+
// ── Types ────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
type ZipData = Record<string, Uint8Array>
|
|
41
|
+
|
|
42
|
+
type CourseJson = {
|
|
43
|
+
title?: string
|
|
44
|
+
summary?: string
|
|
45
|
+
language?: string
|
|
46
|
+
programming_language_id?: string
|
|
47
|
+
environment?: string
|
|
48
|
+
course_type?: string
|
|
49
|
+
vendor?: { name?: string; url?: string; email?: string }
|
|
50
|
+
items?: ItemJson[]
|
|
51
|
+
additional_files?: AdditionalFileJson[]
|
|
52
|
+
version?: number
|
|
53
|
+
course_version?: number
|
|
54
|
+
id?: number
|
|
55
|
+
generated_edu_id?: number
|
|
56
|
+
edu_plugin_version?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type ItemJson = {
|
|
60
|
+
type?: string
|
|
61
|
+
id?: number
|
|
62
|
+
title?: string
|
|
63
|
+
custom_name?: string
|
|
64
|
+
items?: ItemJson[]
|
|
65
|
+
task_list?: TaskJson[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type TaskJson = {
|
|
69
|
+
id?: number
|
|
70
|
+
name?: string
|
|
71
|
+
custom_name?: string
|
|
72
|
+
task_type?: string
|
|
73
|
+
description_text?: string
|
|
74
|
+
description_format?: string
|
|
75
|
+
feedback_link?: string
|
|
76
|
+
solution_hidden?: boolean
|
|
77
|
+
status?: string
|
|
78
|
+
record?: number
|
|
79
|
+
tags?: string[]
|
|
80
|
+
files?: Record<string, FileConfigJson>
|
|
81
|
+
// Choice-specific
|
|
82
|
+
choiceOptions?: ChoiceOptionJson[]
|
|
83
|
+
isMultipleChoice?: boolean
|
|
84
|
+
messageIncorrect?: string
|
|
85
|
+
// Matching-specific
|
|
86
|
+
captions?: CaptionJson[]
|
|
87
|
+
options?: string[]
|
|
88
|
+
// Table-specific
|
|
89
|
+
rows?: string[]
|
|
90
|
+
columns?: string[]
|
|
91
|
+
selected?: number[][]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type FileConfigJson = {
|
|
95
|
+
name?: string
|
|
96
|
+
placeholders?: PlaceholderJson[]
|
|
97
|
+
is_visible?: boolean
|
|
98
|
+
is_binary?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type PlaceholderJson = {
|
|
102
|
+
offset?: number
|
|
103
|
+
length?: number
|
|
104
|
+
possible_answer?: string
|
|
105
|
+
placeholder_text?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type ChoiceOptionJson = {
|
|
109
|
+
text?: string
|
|
110
|
+
status?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type CaptionJson = {
|
|
114
|
+
first?: string
|
|
115
|
+
second?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type AdditionalFileJson = {
|
|
119
|
+
name?: string
|
|
120
|
+
is_binary?: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Main exported function ──────────────────────────
|
|
124
|
+
|
|
125
|
+
export async function loadCourseProjectFromZip(
|
|
126
|
+
data: Uint8Array,
|
|
127
|
+
options?: { aesKey?: string }
|
|
128
|
+
): Promise<Course> {
|
|
129
|
+
const zip = unzipSync(data) as ZipData
|
|
130
|
+
const aesKey = options?.aesKey ?? TEST_AES_KEY
|
|
131
|
+
|
|
132
|
+
const courseJsonRaw = zip["course.json"]
|
|
133
|
+
if (!courseJsonRaw) {
|
|
134
|
+
throw new Error("Missing course.json in zip archive")
|
|
135
|
+
}
|
|
136
|
+
const courseJson: CourseJson = JSON.parse(new TextDecoder().decode(courseJsonRaw))
|
|
137
|
+
|
|
138
|
+
if (!courseJson.title) {
|
|
139
|
+
throw new Error("Missing 'title' in course.json")
|
|
140
|
+
}
|
|
141
|
+
if (!courseJson.language) {
|
|
142
|
+
throw new Error("Missing 'language' in course.json")
|
|
143
|
+
}
|
|
144
|
+
if (!courseJson.programming_language_id) {
|
|
145
|
+
throw new Error("Missing 'programming_language_id' in course.json")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const course = createZipCourse(courseJson.course_type)
|
|
149
|
+
hydrateZipCourse(course, courseJson)
|
|
150
|
+
|
|
151
|
+
if (courseJson.items) {
|
|
152
|
+
for (const item of courseJson.items) {
|
|
153
|
+
if (item.type === "section" && item.items) {
|
|
154
|
+
await loadZipSection(course, item, zip, aesKey)
|
|
155
|
+
} else if (item.task_list) {
|
|
156
|
+
// Direct lesson at top level (uncommon for this zip but handle it)
|
|
157
|
+
await loadZipLesson(course, item, "", zip, aesKey)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (courseJson.additional_files) {
|
|
163
|
+
loadZipAdditionalFiles(course, courseJson.additional_files, zip)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return course
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Course creation and hydration ────────────────────
|
|
170
|
+
|
|
171
|
+
function createZipCourse(type: string | undefined): Course {
|
|
172
|
+
switch (type) {
|
|
173
|
+
case "hyperskill":
|
|
174
|
+
return new HyperskillCourse()
|
|
175
|
+
case "coursera":
|
|
176
|
+
return new CourseraCourse()
|
|
177
|
+
case "stepik":
|
|
178
|
+
return new StepikCourse()
|
|
179
|
+
default:
|
|
180
|
+
return new EduCourse()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hydrateZipCourse(course: Course, json: CourseJson): void {
|
|
185
|
+
course.name = json.title ?? ""
|
|
186
|
+
course.description = json.summary ?? ""
|
|
187
|
+
course.programmingLanguage = json.programming_language_id ?? ""
|
|
188
|
+
course.environment = json.environment ?? course.environment
|
|
189
|
+
course.languageCode = json.language ?? "en"
|
|
190
|
+
|
|
191
|
+
if (json.vendor) {
|
|
192
|
+
course.vendor = new VendorClass(json.vendor.name, json.vendor.email, json.vendor.url)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (json.course_type === "Marketplace") {
|
|
196
|
+
course.courseMode = CourseMode.STUDENT
|
|
197
|
+
} else {
|
|
198
|
+
course.courseMode = CourseMode.EDUCATOR
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Section loading ──────────────────────────────────
|
|
203
|
+
|
|
204
|
+
async function loadZipSection(
|
|
205
|
+
course: Course,
|
|
206
|
+
sectionJson: ItemJson,
|
|
207
|
+
zip: ZipData,
|
|
208
|
+
aesKey: string
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const section = new Section()
|
|
211
|
+
section.name = sectionJson.title ?? ""
|
|
212
|
+
if (sectionJson.custom_name) {
|
|
213
|
+
section.customPresentableName = sectionJson.custom_name
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sectionTitle = sectionJson.title ?? ""
|
|
217
|
+
const nestedItems = sectionJson.items ?? []
|
|
218
|
+
for (const item of nestedItems) {
|
|
219
|
+
if (item.type === "lesson" && item.task_list) {
|
|
220
|
+
await loadZipLesson(section, item, sectionTitle, zip, aesKey)
|
|
221
|
+
} else if (item.task_list) {
|
|
222
|
+
// Also treat items with task_list as lessons even without explicit type
|
|
223
|
+
await loadZipLesson(section, item, sectionTitle, zip, aesKey)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
course.addSection(section)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Lesson loading ───────────────────────────────────
|
|
231
|
+
|
|
232
|
+
async function loadZipLesson(
|
|
233
|
+
parent: LessonContainer,
|
|
234
|
+
lessonJson: ItemJson,
|
|
235
|
+
sectionTitle: string,
|
|
236
|
+
zip: ZipData,
|
|
237
|
+
aesKey: string
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const lesson = new Lesson()
|
|
240
|
+
lesson.name = lessonJson.title ?? ""
|
|
241
|
+
if (lessonJson.custom_name) {
|
|
242
|
+
lesson.customPresentableName = lessonJson.custom_name
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const taskList = lessonJson.task_list ?? []
|
|
246
|
+
for (const taskJson of taskList) {
|
|
247
|
+
const task = await loadZipTask(taskJson, lessonJson, sectionTitle, zip, aesKey)
|
|
248
|
+
if (task) {
|
|
249
|
+
lesson.addTask(task)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
parent.addItem(lesson)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Task loading ─────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
async function loadZipTask(
|
|
259
|
+
taskJson: TaskJson,
|
|
260
|
+
lessonJson: ItemJson,
|
|
261
|
+
sectionTitle: string,
|
|
262
|
+
zip: ZipData,
|
|
263
|
+
aesKey: string
|
|
264
|
+
): Promise<Task | null> {
|
|
265
|
+
if (!taskJson.task_type) {
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const task = createZipTask(taskJson.task_type)
|
|
270
|
+
if (!task) {
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Basic fields
|
|
275
|
+
task.name = taskJson.name ?? ""
|
|
276
|
+
task.id = taskJson.id ?? 0
|
|
277
|
+
if (taskJson.custom_name) {
|
|
278
|
+
task.customPresentableName = taskJson.custom_name
|
|
279
|
+
}
|
|
280
|
+
task.descriptionText = taskJson.description_text ?? ""
|
|
281
|
+
if (taskJson.description_format) {
|
|
282
|
+
task.descriptionFormat = taskJson.description_format as DescriptionFormat
|
|
283
|
+
}
|
|
284
|
+
if (taskJson.feedback_link !== undefined) {
|
|
285
|
+
task.feedbackLink = taskJson.feedback_link
|
|
286
|
+
}
|
|
287
|
+
if (taskJson.solution_hidden !== undefined) {
|
|
288
|
+
task.solutionHidden = taskJson.solution_hidden
|
|
289
|
+
}
|
|
290
|
+
if (taskJson.status !== undefined && taskJson.status !== null) {
|
|
291
|
+
const statusMap: Record<string, CheckStatus> = {
|
|
292
|
+
Unchecked: CheckStatus.Unchecked,
|
|
293
|
+
Solved: CheckStatus.Solved,
|
|
294
|
+
Failed: CheckStatus.Failed,
|
|
295
|
+
}
|
|
296
|
+
task.status = statusMap[taskJson.status] ?? CheckStatus.Unchecked
|
|
297
|
+
}
|
|
298
|
+
if (typeof taskJson.record === "number") {
|
|
299
|
+
task.record = taskJson.record
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(taskJson.tags)) {
|
|
302
|
+
task.contentTags = taskJson.tags.map(String)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Load task files
|
|
306
|
+
const lessonTitle = lessonJson.title ?? ""
|
|
307
|
+
const files = taskJson.files ?? {}
|
|
308
|
+
for (const filename of Object.keys(files)) {
|
|
309
|
+
const fileConfig = files[filename]
|
|
310
|
+
const taskFile = await loadZipTaskFile(filename, fileConfig, task.name, sectionTitle, lessonTitle, zip, aesKey)
|
|
311
|
+
if (taskFile) {
|
|
312
|
+
task.addTaskFileInstance(taskFile)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Hydrate type-specific fields
|
|
317
|
+
if (task instanceof ChoiceTask) {
|
|
318
|
+
hydrateZipChoiceTask(task, taskJson)
|
|
319
|
+
} else if (task instanceof MatchingTask) {
|
|
320
|
+
hydrateZipMatchingTask(task, taskJson)
|
|
321
|
+
} else if (task instanceof SortingTask) {
|
|
322
|
+
hydrateZipSortingTask(task, taskJson)
|
|
323
|
+
} else if (task instanceof TableTask) {
|
|
324
|
+
hydrateZipTableTask(task, taskJson)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return task
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function createZipTask(taskType: string): Task | null {
|
|
331
|
+
switch (taskType) {
|
|
332
|
+
case "edu":
|
|
333
|
+
case "pycharm":
|
|
334
|
+
return new EduTask()
|
|
335
|
+
case "code":
|
|
336
|
+
return new CodeTask()
|
|
337
|
+
case "number":
|
|
338
|
+
return new NumberTask()
|
|
339
|
+
case "string":
|
|
340
|
+
return new StringTask()
|
|
341
|
+
case "output":
|
|
342
|
+
return new OutputTask()
|
|
343
|
+
case "dataset":
|
|
344
|
+
return new DataTask()
|
|
345
|
+
case "table":
|
|
346
|
+
return new TableTask()
|
|
347
|
+
case "theory":
|
|
348
|
+
return new TheoryTask()
|
|
349
|
+
case "ide":
|
|
350
|
+
return new IdeTask()
|
|
351
|
+
case "unsupported":
|
|
352
|
+
return new UnsupportedTask()
|
|
353
|
+
case "remote_edu":
|
|
354
|
+
return new RemoteEduTask()
|
|
355
|
+
case "choice":
|
|
356
|
+
return new ChoiceTask()
|
|
357
|
+
case "matching":
|
|
358
|
+
return new MatchingTask()
|
|
359
|
+
case "sorting":
|
|
360
|
+
return new SortingTask()
|
|
361
|
+
default:
|
|
362
|
+
return null
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Task file loading ────────────────────────────────
|
|
367
|
+
|
|
368
|
+
async function loadZipTaskFile(
|
|
369
|
+
filename: string,
|
|
370
|
+
config: FileConfigJson,
|
|
371
|
+
taskName: string,
|
|
372
|
+
sectionTitle: string,
|
|
373
|
+
lessonTitle: string,
|
|
374
|
+
zip: ZipData,
|
|
375
|
+
aesKey: string
|
|
376
|
+
): Promise<TaskFile | null> {
|
|
377
|
+
if (!config.name) {
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const taskFile = new TaskFile()
|
|
382
|
+
taskFile.name = config.name ?? filename
|
|
383
|
+
taskFile.isVisible = config.is_visible ?? true
|
|
384
|
+
|
|
385
|
+
const isBinary = config.is_binary ?? false
|
|
386
|
+
|
|
387
|
+
// Read file content from contents/ path
|
|
388
|
+
// Path: contents/{sectionTitle}/{lessonTitle}/{taskName}/{filename}
|
|
389
|
+
const contentPath = buildContentPath(sectionTitle, lessonTitle, taskName, config.name ?? filename)
|
|
390
|
+
let fileContent: string | undefined
|
|
391
|
+
|
|
392
|
+
if (isBinary) {
|
|
393
|
+
const rawData = zip[contentPath]
|
|
394
|
+
if (rawData) {
|
|
395
|
+
fileContent = bufferToBase64(rawData)
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
const rawData = zip[contentPath]
|
|
399
|
+
if (rawData) {
|
|
400
|
+
fileContent = new TextDecoder().decode(rawData)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (fileContent !== undefined) {
|
|
405
|
+
taskFile.text = fileContent
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Handle placeholders
|
|
409
|
+
const placeholders = config.placeholders ?? []
|
|
410
|
+
for (const ph of placeholders) {
|
|
411
|
+
if (ph.offset !== undefined && ph.placeholder_text !== undefined) {
|
|
412
|
+
const placeholder = new AnswerPlaceholder(ph.offset, ph.placeholder_text)
|
|
413
|
+
placeholder.length = ph.length ?? ph.placeholder_text.length
|
|
414
|
+
|
|
415
|
+
// Decrypt possible_answer if present
|
|
416
|
+
if (ph.possible_answer) {
|
|
417
|
+
const decrypted = await decryptAesCbc(ph.possible_answer, aesKey)
|
|
418
|
+
if (decrypted !== undefined && decrypted.length > 0) {
|
|
419
|
+
placeholder.possibleAnswer = decrypted
|
|
420
|
+
} else {
|
|
421
|
+
placeholder.possibleAnswer = ph.possible_answer
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
taskFile.addAnswerPlaceholder(placeholder)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return taskFile
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildContentPath(
|
|
433
|
+
sectionTitle: string,
|
|
434
|
+
lessonTitle: string,
|
|
435
|
+
taskName: string,
|
|
436
|
+
filename: string
|
|
437
|
+
): string {
|
|
438
|
+
const parts = ["contents"]
|
|
439
|
+
if (sectionTitle) {
|
|
440
|
+
parts.push(sectionTitle)
|
|
441
|
+
}
|
|
442
|
+
parts.push(lessonTitle)
|
|
443
|
+
parts.push(taskName)
|
|
444
|
+
parts.push(filename)
|
|
445
|
+
return parts.join("/")
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── Type-specific hydration ──────────────────────────
|
|
449
|
+
|
|
450
|
+
function hydrateZipChoiceTask(task: ChoiceTask, json: TaskJson): void {
|
|
451
|
+
if (json.isMultipleChoice !== undefined) {
|
|
452
|
+
task.isMultipleChoice = json.isMultipleChoice
|
|
453
|
+
}
|
|
454
|
+
if (json.messageIncorrect !== undefined) {
|
|
455
|
+
task.messageIncorrect = json.messageIncorrect
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(json.choiceOptions)) {
|
|
458
|
+
task.choiceOptions = json.choiceOptions.map((opt: ChoiceOptionJson) => {
|
|
459
|
+
const option = new ChoiceOption()
|
|
460
|
+
option.text = opt.text ?? ""
|
|
461
|
+
if (opt.status === "CORRECT") {
|
|
462
|
+
option.status = ChoiceOptionStatus.CORRECT
|
|
463
|
+
} else if (opt.status === "INCORRECT") {
|
|
464
|
+
option.status = ChoiceOptionStatus.INCORRECT
|
|
465
|
+
}
|
|
466
|
+
return option
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function hydrateZipMatchingTask(task: MatchingTask, json: TaskJson): void {
|
|
472
|
+
if (Array.isArray(json.captions)) {
|
|
473
|
+
task.captions = json.captions.map((c: CaptionJson) => {
|
|
474
|
+
const first = String(c.first ?? "")
|
|
475
|
+
const second = String(c.second ?? "")
|
|
476
|
+
return `${first} : ${second}`
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(json.options)) {
|
|
480
|
+
task.options = json.options.map(String)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function hydrateZipSortingTask(task: SortingTask, json: TaskJson): void {
|
|
485
|
+
if (Array.isArray(json.options)) {
|
|
486
|
+
task.options = json.options.map(String)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function hydrateZipTableTask(task: TableTask, json: TaskJson): void {
|
|
491
|
+
if (json.isMultipleChoice !== undefined) {
|
|
492
|
+
task.isMultipleChoice = json.isMultipleChoice
|
|
493
|
+
}
|
|
494
|
+
if (Array.isArray(json.rows)) {
|
|
495
|
+
task.rows = json.rows.map(String)
|
|
496
|
+
}
|
|
497
|
+
if (Array.isArray(json.columns)) {
|
|
498
|
+
task.columns = json.columns.map(String)
|
|
499
|
+
}
|
|
500
|
+
if (Array.isArray(json.selected)) {
|
|
501
|
+
task.selected = json.selected.map((row) => row.map(Boolean))
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ── Additional files ─────────────────────────────────
|
|
506
|
+
|
|
507
|
+
function loadZipAdditionalFiles(
|
|
508
|
+
course: Course,
|
|
509
|
+
additionalFiles: AdditionalFileJson[],
|
|
510
|
+
zip: ZipData
|
|
511
|
+
): void {
|
|
512
|
+
for (const af of additionalFiles) {
|
|
513
|
+
if (!af.name) continue
|
|
514
|
+
|
|
515
|
+
const contentPath = `contents/${af.name}`
|
|
516
|
+
const rawData = zip[contentPath]
|
|
517
|
+
if (!rawData) continue
|
|
518
|
+
|
|
519
|
+
const eduFile = new EduFile()
|
|
520
|
+
eduFile.name = af.name
|
|
521
|
+
|
|
522
|
+
if (af.is_binary) {
|
|
523
|
+
eduFile.text = bufferToBase64(rawData)
|
|
524
|
+
} else {
|
|
525
|
+
eduFile.text = new TextDecoder().decode(rawData)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
course.additionalFiles.push(eduFile)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── AES-256-CBC decryption using Web Crypto API ──────
|
|
533
|
+
|
|
534
|
+
async function decryptAesCbc(encryptedBase64: string, aesKey: string): Promise<string | undefined> {
|
|
535
|
+
try {
|
|
536
|
+
const keyBytes = new TextEncoder().encode(aesKey)
|
|
537
|
+
const ivBytes = new TextEncoder().encode(aesKey.slice(0, 16))
|
|
538
|
+
const encryptedBytes = base64ToBuffer(encryptedBase64)
|
|
539
|
+
|
|
540
|
+
let cryptoKey: CryptoKey
|
|
541
|
+
try {
|
|
542
|
+
cryptoKey = await crypto.subtle.importKey(
|
|
543
|
+
"raw",
|
|
544
|
+
keyBytes,
|
|
545
|
+
{ name: "AES-CBC" },
|
|
546
|
+
false,
|
|
547
|
+
["decrypt"]
|
|
548
|
+
)
|
|
549
|
+
} catch {
|
|
550
|
+
// crypto.subtle may not be available
|
|
551
|
+
return undefined
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
555
|
+
{ name: "AES-CBC", iv: ivBytes },
|
|
556
|
+
cryptoKey,
|
|
557
|
+
encryptedBytes.buffer as ArrayBuffer
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
return new TextDecoder().decode(decryptedBuffer)
|
|
561
|
+
} catch {
|
|
562
|
+
return undefined
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── Utility functions ────────────────────────────────
|
|
567
|
+
|
|
568
|
+
function base64ToBuffer(base64: string): Uint8Array {
|
|
569
|
+
const binaryStr = atob(base64)
|
|
570
|
+
const bytes = new Uint8Array(binaryStr.length)
|
|
571
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
572
|
+
bytes[i] = binaryStr.charCodeAt(i)
|
|
573
|
+
}
|
|
574
|
+
return bytes
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function bufferToBase64(buffer: Uint8Array): string {
|
|
578
|
+
let binary = ""
|
|
579
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
580
|
+
binary += String.fromCharCode(buffer[i])
|
|
581
|
+
}
|
|
582
|
+
return btoa(binary)
|
|
583
|
+
}
|