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.
Files changed (239) hide show
  1. package/.idea/edu-format-ts.iml +10 -0
  2. package/.idea/inspectionProfiles/Project_Default.xml +28 -0
  3. package/.idea/misc.xml +4 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/AGENTS.md +1 -0
  7. package/dist/courseFormat/AnswerPlaceholder.d.ts +37 -0
  8. package/dist/courseFormat/AnswerPlaceholder.js +101 -0
  9. package/dist/courseFormat/AnswerPlaceholderComparator.d.ts +4 -0
  10. package/dist/courseFormat/AnswerPlaceholderComparator.js +8 -0
  11. package/dist/courseFormat/AnswerPlaceholderDependency.d.ts +19 -0
  12. package/dist/courseFormat/AnswerPlaceholderDependency.js +91 -0
  13. package/dist/courseFormat/CheckFeedback.d.ts +20 -0
  14. package/dist/courseFormat/CheckFeedback.js +58 -0
  15. package/dist/courseFormat/CheckResult.d.ts +33 -0
  16. package/dist/courseFormat/CheckResult.js +58 -0
  17. package/dist/courseFormat/CheckResultSeverity.d.ts +7 -0
  18. package/dist/courseFormat/CheckResultSeverity.js +17 -0
  19. package/dist/courseFormat/CheckStatus.d.ts +5 -0
  20. package/dist/courseFormat/CheckStatus.js +9 -0
  21. package/dist/courseFormat/Course.d.ts +68 -0
  22. package/dist/courseFormat/Course.js +165 -0
  23. package/dist/courseFormat/CourseMode.d.ts +4 -0
  24. package/dist/courseFormat/CourseMode.js +8 -0
  25. package/dist/courseFormat/CourseVisibility.d.ts +4 -0
  26. package/dist/courseFormat/CourseVisibility.js +8 -0
  27. package/dist/courseFormat/CourseraCourse.d.ts +5 -0
  28. package/dist/courseFormat/CourseraCourse.js +15 -0
  29. package/dist/courseFormat/DescriptionFormat.d.ts +5 -0
  30. package/dist/courseFormat/DescriptionFormat.js +9 -0
  31. package/dist/courseFormat/EduCourse.d.ts +17 -0
  32. package/dist/courseFormat/EduCourse.js +42 -0
  33. package/dist/courseFormat/EduFile.d.ts +22 -0
  34. package/dist/courseFormat/EduFile.js +110 -0
  35. package/dist/courseFormat/EduFileErrorHighlightLevel.d.ts +5 -0
  36. package/dist/courseFormat/EduFileErrorHighlightLevel.js +9 -0
  37. package/dist/courseFormat/EduFormatNames.d.ts +75 -0
  38. package/dist/courseFormat/EduFormatNames.js +80 -0
  39. package/dist/courseFormat/EduTestInfo.d.ts +28 -0
  40. package/dist/courseFormat/EduTestInfo.js +68 -0
  41. package/dist/courseFormat/EduVersions.d.ts +2 -0
  42. package/dist/courseFormat/EduVersions.js +5 -0
  43. package/dist/courseFormat/FileContents.d.ts +36 -0
  44. package/dist/courseFormat/FileContents.js +67 -0
  45. package/dist/courseFormat/FileContentsFactory.d.ts +17 -0
  46. package/dist/courseFormat/FileContentsFactory.js +2 -0
  47. package/dist/courseFormat/FrameworkLesson.d.ts +9 -0
  48. package/dist/courseFormat/FrameworkLesson.js +28 -0
  49. package/dist/courseFormat/ItemContainer.d.ts +13 -0
  50. package/dist/courseFormat/ItemContainer.js +45 -0
  51. package/dist/courseFormat/JBAccountUserInfo.d.ts +9 -0
  52. package/dist/courseFormat/JBAccountUserInfo.js +20 -0
  53. package/dist/courseFormat/Language.d.ts +4 -0
  54. package/dist/courseFormat/Language.js +33 -0
  55. package/dist/courseFormat/Lesson.d.ts +20 -0
  56. package/dist/courseFormat/Lesson.js +56 -0
  57. package/dist/courseFormat/LessonContainer.d.ts +16 -0
  58. package/dist/courseFormat/LessonContainer.js +54 -0
  59. package/dist/courseFormat/PluginInfo.d.ts +7 -0
  60. package/dist/courseFormat/PluginInfo.js +15 -0
  61. package/dist/courseFormat/Section.d.ts +8 -0
  62. package/dist/courseFormat/Section.js +27 -0
  63. package/dist/courseFormat/StudyItem.d.ts +20 -0
  64. package/dist/courseFormat/StudyItem.js +47 -0
  65. package/dist/courseFormat/Tags.d.ts +16 -0
  66. package/dist/courseFormat/Tags.js +42 -0
  67. package/dist/courseFormat/TaskFile.d.ts +26 -0
  68. package/dist/courseFormat/TaskFile.js +72 -0
  69. package/dist/courseFormat/UserInfo.d.ts +3 -0
  70. package/dist/courseFormat/UserInfo.js +2 -0
  71. package/dist/courseFormat/Vendor.d.ts +7 -0
  72. package/dist/courseFormat/Vendor.js +14 -0
  73. package/dist/courseFormat/attempts/Attempt.d.ts +12 -0
  74. package/dist/courseFormat/attempts/Attempt.js +25 -0
  75. package/dist/courseFormat/attempts/AttemptBase.d.ts +9 -0
  76. package/dist/courseFormat/attempts/AttemptBase.js +28 -0
  77. package/dist/courseFormat/attempts/DataTaskAttempt.d.ts +6 -0
  78. package/dist/courseFormat/attempts/DataTaskAttempt.js +24 -0
  79. package/dist/courseFormat/attempts/Dataset.d.ts +12 -0
  80. package/dist/courseFormat/attempts/Dataset.js +21 -0
  81. package/dist/courseFormat/fileUtils.d.ts +5 -0
  82. package/dist/courseFormat/fileUtils.js +32 -0
  83. package/dist/courseFormat/hyperskill/HyperskillCourse.d.ts +13 -0
  84. package/dist/courseFormat/hyperskill/HyperskillCourse.js +25 -0
  85. package/dist/courseFormat/hyperskill/HyperskillProject.d.ts +10 -0
  86. package/dist/courseFormat/hyperskill/HyperskillProject.js +16 -0
  87. package/dist/courseFormat/hyperskill/HyperskillStage.d.ts +8 -0
  88. package/dist/courseFormat/hyperskill/HyperskillStage.js +20 -0
  89. package/dist/courseFormat/hyperskill/HyperskillTaskType.d.ts +4 -0
  90. package/dist/courseFormat/hyperskill/HyperskillTaskType.js +26 -0
  91. package/dist/courseFormat/hyperskill/HyperskillTopic.d.ts +5 -0
  92. package/dist/courseFormat/hyperskill/HyperskillTopic.js +11 -0
  93. package/dist/courseFormat/loggerUtils.d.ts +1 -0
  94. package/dist/courseFormat/loggerUtils.js +6 -0
  95. package/dist/courseFormat/stepik/StepikCourse.d.ts +5 -0
  96. package/dist/courseFormat/stepik/StepikCourse.js +15 -0
  97. package/dist/courseFormat/stepik/StepikLesson.d.ts +6 -0
  98. package/dist/courseFormat/stepik/StepikLesson.js +16 -0
  99. package/dist/courseFormat/tasks/AnswerTask.d.ts +8 -0
  100. package/dist/courseFormat/tasks/AnswerTask.js +11 -0
  101. package/dist/courseFormat/tasks/CodeTask.d.ts +12 -0
  102. package/dist/courseFormat/tasks/CodeTask.js +21 -0
  103. package/dist/courseFormat/tasks/DataTask.d.ts +18 -0
  104. package/dist/courseFormat/tasks/DataTask.js +32 -0
  105. package/dist/courseFormat/tasks/EduTask.d.ts +12 -0
  106. package/dist/courseFormat/tasks/EduTask.js +22 -0
  107. package/dist/courseFormat/tasks/IdeTask.d.ts +9 -0
  108. package/dist/courseFormat/tasks/IdeTask.js +14 -0
  109. package/dist/courseFormat/tasks/NumberTask.d.ts +9 -0
  110. package/dist/courseFormat/tasks/NumberTask.js +14 -0
  111. package/dist/courseFormat/tasks/OutputTask.d.ts +10 -0
  112. package/dist/courseFormat/tasks/OutputTask.js +18 -0
  113. package/dist/courseFormat/tasks/OutputTaskBase.d.ts +14 -0
  114. package/dist/courseFormat/tasks/OutputTaskBase.js +19 -0
  115. package/dist/courseFormat/tasks/RemoteEduTask.d.ts +9 -0
  116. package/dist/courseFormat/tasks/RemoteEduTask.js +15 -0
  117. package/dist/courseFormat/tasks/StringTask.d.ts +9 -0
  118. package/dist/courseFormat/tasks/StringTask.js +14 -0
  119. package/dist/courseFormat/tasks/TableTask.d.ts +17 -0
  120. package/dist/courseFormat/tasks/TableTask.js +43 -0
  121. package/dist/courseFormat/tasks/Task.d.ts +45 -0
  122. package/dist/courseFormat/tasks/Task.js +155 -0
  123. package/dist/courseFormat/tasks/TheoryTask.d.ts +10 -0
  124. package/dist/courseFormat/tasks/TheoryTask.js +15 -0
  125. package/dist/courseFormat/tasks/UnsupportedTask.d.ts +9 -0
  126. package/dist/courseFormat/tasks/UnsupportedTask.js +14 -0
  127. package/dist/courseFormat/tasks/choice/ChoiceOption.d.ts +10 -0
  128. package/dist/courseFormat/tasks/choice/ChoiceOption.js +33 -0
  129. package/dist/courseFormat/tasks/choice/ChoiceOptionStatus.d.ts +5 -0
  130. package/dist/courseFormat/tasks/choice/ChoiceOptionStatus.js +9 -0
  131. package/dist/courseFormat/tasks/choice/ChoiceTask.d.ts +23 -0
  132. package/dist/courseFormat/tasks/choice/ChoiceTask.js +47 -0
  133. package/dist/courseFormat/tasks/matching/MatchingTask.d.ts +10 -0
  134. package/dist/courseFormat/tasks/matching/MatchingTask.js +15 -0
  135. package/dist/courseFormat/tasks/matching/SortingBasedTask.d.ts +16 -0
  136. package/dist/courseFormat/tasks/matching/SortingBasedTask.js +50 -0
  137. package/dist/courseFormat/tasks/matching/SortingTask.d.ts +9 -0
  138. package/dist/courseFormat/tasks/matching/SortingTask.js +14 -0
  139. package/dist/courseFormat/uiMessages.d.ts +3 -0
  140. package/dist/courseFormat/uiMessages.js +14 -0
  141. package/dist/disk-loader.d.ts +4 -0
  142. package/dist/disk-loader.js +389 -0
  143. package/dist/index.d.ts +31 -0
  144. package/dist/index.js +64 -0
  145. package/dist/loader.d.ts +7 -0
  146. package/dist/loader.js +435 -0
  147. package/dist/models.d.ts +49 -0
  148. package/dist/models.js +2 -0
  149. package/dist/zip-loader.d.ts +4 -0
  150. package/dist/zip-loader.js +431 -0
  151. package/example-course-project/course-info.yaml +15 -0
  152. package/example-course-project/lesson1/lesson-info.yaml +3 -0
  153. package/example-course-project/lesson1/lesson-remote-info.yaml +1 -0
  154. package/example-course-project/lesson1/task1/Task.txt +1 -0
  155. package/example-course-project/lesson1/task1/task-info.yaml +12 -0
  156. package/example-course-project/lesson1/task1/task-remote-info.yaml +1 -0
  157. package/example-course-project/lesson1/task1/task.md +47 -0
  158. package/example-course-project/lesson1/task1/tests/Tests.txt +0 -0
  159. package/example-course-project/lesson1/task2/Task.txt +1 -0
  160. package/example-course-project/lesson1/task2/task-info.yaml +12 -0
  161. package/example-course-project/lesson1/task2/task-remote-info.yaml +1 -0
  162. package/example-course-project/lesson1/task2/task.md +47 -0
  163. package/example-course-project/lesson1/task2/tests/Tests.txt +0 -0
  164. package/package.json +19 -0
  165. package/src/@types/mime-types.d.ts +3 -0
  166. package/src/courseFormat/AnswerPlaceholder.ts +121 -0
  167. package/src/courseFormat/AnswerPlaceholderComparator.ts +7 -0
  168. package/src/courseFormat/AnswerPlaceholderDependency.ts +122 -0
  169. package/src/courseFormat/CheckFeedback.ts +71 -0
  170. package/src/courseFormat/CheckResult.ts +92 -0
  171. package/src/courseFormat/CheckResultSeverity.ts +13 -0
  172. package/src/courseFormat/CheckStatus.ts +5 -0
  173. package/src/courseFormat/Course.ts +201 -0
  174. package/src/courseFormat/CourseMode.ts +4 -0
  175. package/src/courseFormat/CourseVisibility.ts +4 -0
  176. package/src/courseFormat/CourseraCourse.ts +10 -0
  177. package/src/courseFormat/DescriptionFormat.ts +5 -0
  178. package/src/courseFormat/EduCourse.ts +41 -0
  179. package/src/courseFormat/EduFile.ts +133 -0
  180. package/src/courseFormat/EduFileErrorHighlightLevel.ts +5 -0
  181. package/src/courseFormat/EduFormatNames.ts +95 -0
  182. package/src/courseFormat/EduTestInfo.ts +87 -0
  183. package/src/courseFormat/EduVersions.ts +2 -0
  184. package/src/courseFormat/FileContents.ts +97 -0
  185. package/src/courseFormat/FileContentsFactory.ts +19 -0
  186. package/src/courseFormat/FrameworkLesson.ts +29 -0
  187. package/src/courseFormat/ItemContainer.ts +47 -0
  188. package/src/courseFormat/JBAccountUserInfo.ts +21 -0
  189. package/src/courseFormat/Language.ts +31 -0
  190. package/src/courseFormat/Lesson.ts +69 -0
  191. package/src/courseFormat/LessonContainer.ts +65 -0
  192. package/src/courseFormat/PluginInfo.ts +15 -0
  193. package/src/courseFormat/Section.ts +29 -0
  194. package/src/courseFormat/StudyItem.ts +55 -0
  195. package/src/courseFormat/Tags.ts +45 -0
  196. package/src/courseFormat/TaskFile.ts +88 -0
  197. package/src/courseFormat/UserInfo.ts +3 -0
  198. package/src/courseFormat/Vendor.ts +15 -0
  199. package/src/courseFormat/attempts/Attempt.ts +28 -0
  200. package/src/courseFormat/attempts/AttemptBase.ts +24 -0
  201. package/src/courseFormat/attempts/DataTaskAttempt.ts +19 -0
  202. package/src/courseFormat/attempts/Dataset.ts +13 -0
  203. package/src/courseFormat/fileUtils.ts +31 -0
  204. package/src/courseFormat/hyperskill/HyperskillCourse.ts +24 -0
  205. package/src/courseFormat/hyperskill/HyperskillProject.ts +10 -0
  206. package/src/courseFormat/hyperskill/HyperskillStage.ts +15 -0
  207. package/src/courseFormat/hyperskill/HyperskillTaskType.ts +23 -0
  208. package/src/courseFormat/hyperskill/HyperskillTopic.ts +5 -0
  209. package/src/courseFormat/loggerUtils.ts +3 -0
  210. package/src/courseFormat/stepik/StepikCourse.ts +10 -0
  211. package/src/courseFormat/stepik/StepikLesson.ts +11 -0
  212. package/src/courseFormat/tasks/AnswerTask.ts +13 -0
  213. package/src/courseFormat/tasks/CodeTask.ts +42 -0
  214. package/src/courseFormat/tasks/DataTask.ts +37 -0
  215. package/src/courseFormat/tasks/EduTask.ts +26 -0
  216. package/src/courseFormat/tasks/IdeTask.ts +17 -0
  217. package/src/courseFormat/tasks/NumberTask.ts +17 -0
  218. package/src/courseFormat/tasks/OutputTask.ts +21 -0
  219. package/src/courseFormat/tasks/OutputTaskBase.ts +23 -0
  220. package/src/courseFormat/tasks/RemoteEduTask.ts +18 -0
  221. package/src/courseFormat/tasks/StringTask.ts +17 -0
  222. package/src/courseFormat/tasks/TableTask.ts +51 -0
  223. package/src/courseFormat/tasks/Task.ts +181 -0
  224. package/src/courseFormat/tasks/TheoryTask.ts +19 -0
  225. package/src/courseFormat/tasks/UnsupportedTask.ts +17 -0
  226. package/src/courseFormat/tasks/choice/ChoiceOption.ts +37 -0
  227. package/src/courseFormat/tasks/choice/ChoiceOptionStatus.ts +5 -0
  228. package/src/courseFormat/tasks/choice/ChoiceTask.ts +57 -0
  229. package/src/courseFormat/tasks/matching/MatchingTask.ts +19 -0
  230. package/src/courseFormat/tasks/matching/SortingBasedTask.ts +59 -0
  231. package/src/courseFormat/tasks/matching/SortingTask.ts +17 -0
  232. package/src/courseFormat/uiMessages.ts +12 -0
  233. package/src/disk-loader.ts +463 -0
  234. package/src/index.ts +33 -0
  235. package/src/models.ts +54 -0
  236. package/src/zip-loader.ts +583 -0
  237. package/test/load-course.test.js +279 -0
  238. package/test/load-zip-course.test.js +73 -0
  239. 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
+ }