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,463 @@
1
+ import { promises as fs } from "node:fs"
2
+ import { readFileSync } from "node:fs"
3
+ import path from "node:path"
4
+ import crypto from "node:crypto"
5
+ import { parse } from "yaml"
6
+ import type { Vendor } from "./models"
7
+ import { CheckStatus } from "./courseFormat/CheckStatus"
8
+ import { CourseMode } from "./courseFormat/CourseMode"
9
+ import { Lesson } from "./courseFormat/Lesson"
10
+ import { FrameworkLesson } from "./courseFormat/FrameworkLesson"
11
+ import { Section } from "./courseFormat/Section"
12
+ import { Course } from "./courseFormat/Course"
13
+ import { EduCourse } from "./courseFormat/EduCourse"
14
+ import { CourseraCourse } from "./courseFormat/CourseraCourse"
15
+ import { HyperskillCourse } from "./courseFormat/hyperskill/HyperskillCourse"
16
+ import { StepikCourse } from "./courseFormat/stepik/StepikCourse"
17
+ import { StepikLesson } from "./courseFormat/stepik/StepikLesson"
18
+ import { Task } from "./courseFormat/tasks/Task"
19
+ import { EduTask } from "./courseFormat/tasks/EduTask"
20
+ import { CodeTask } from "./courseFormat/tasks/CodeTask"
21
+ import { NumberTask } from "./courseFormat/tasks/NumberTask"
22
+ import { StringTask } from "./courseFormat/tasks/StringTask"
23
+ import { OutputTask } from "./courseFormat/tasks/OutputTask"
24
+ import { DataTask } from "./courseFormat/tasks/DataTask"
25
+ import { TableTask } from "./courseFormat/tasks/TableTask"
26
+ import { TheoryTask } from "./courseFormat/tasks/TheoryTask"
27
+ import { IdeTask } from "./courseFormat/tasks/IdeTask"
28
+ import { UnsupportedTask } from "./courseFormat/tasks/UnsupportedTask"
29
+ import { RemoteEduTask } from "./courseFormat/tasks/RemoteEduTask"
30
+ import { ChoiceTask } from "./courseFormat/tasks/choice/ChoiceTask"
31
+ import { ChoiceOption } from "./courseFormat/tasks/choice/ChoiceOption"
32
+ import { ChoiceOptionStatus } from "./courseFormat/tasks/choice/ChoiceOptionStatus"
33
+ import { MatchingTask } from "./courseFormat/tasks/matching/MatchingTask"
34
+ import { SortingTask } from "./courseFormat/tasks/matching/SortingTask"
35
+ import { TaskFile } from "./courseFormat/TaskFile"
36
+
37
+ const COURSE_CONFIG = "course-info.yaml"
38
+ const LESSON_CONFIG = "lesson-info.yaml"
39
+ const TASK_CONFIG = "task-info.yaml"
40
+
41
+ const TEST_AES_KEY = "DFC929E375655998A34E56A21C98651C"
42
+
43
+ type RawYaml = Record<string, unknown>
44
+
45
+ type CourseYaml = {
46
+ type?: string
47
+ title?: string
48
+ summary?: string
49
+ language?: string
50
+ programming_language?: string
51
+ programming_language_version?: string
52
+ environment?: string
53
+ solutions_hidden?: boolean
54
+ environment_settings?: Record<string, string>
55
+ additional_files?: TaskFileYaml[]
56
+ custom_content_path?: string
57
+ disabled_features?: string[]
58
+ vendor?: Vendor
59
+ is_private?: boolean
60
+ mode?: string
61
+ content?: Array<string | null>
62
+ }
63
+
64
+ type LessonYaml = {
65
+ custom_name?: string
66
+ tags?: string[]
67
+ content?: Array<string | null>
68
+ }
69
+
70
+ type TaskFileYaml = {
71
+ name?: string
72
+ text?: string
73
+ encrypted_text?: string
74
+ visible?: boolean
75
+ editable?: boolean
76
+ propagatable?: boolean
77
+ is_binary?: boolean
78
+ learner_created?: boolean
79
+ }
80
+
81
+ function createCourse(type: string | undefined): Course {
82
+ switch (type) {
83
+ case "hyperskill":
84
+ return new HyperskillCourse()
85
+ case "coursera":
86
+ return new CourseraCourse()
87
+ case "stepik":
88
+ return new StepikCourse()
89
+ default:
90
+ const course = new EduCourse()
91
+ return course
92
+ }
93
+ }
94
+
95
+ function hydrateCourse(course: Course, yaml: CourseYaml): void {
96
+ course.name = yaml.title ?? ""
97
+ course.description = yaml.summary ?? ""
98
+ course.programmingLanguage = yaml.programming_language ?? ""
99
+ course.languageVersion = yaml.programming_language_version
100
+ course.environment = yaml.environment ?? course.environment
101
+ course.solutionsHidden = yaml.solutions_hidden ?? course.solutionsHidden
102
+ course.environmentSettings = (yaml.environment_settings as Record<string, string>) ?? course.environmentSettings
103
+ course.customContentPath = yaml.custom_content_path ?? course.customContentPath
104
+ course.disabledFeatures = (yaml.disabled_features as string[]) ?? course.disabledFeatures
105
+ course.vendor = (yaml.vendor as Vendor) ?? course.vendor
106
+ if (yaml.is_private !== undefined) course.isMarketplacePrivate = yaml.is_private
107
+ if (yaml.mode) {
108
+ course.courseMode = CourseMode.STUDENT
109
+ }
110
+ else {
111
+ course.courseMode = CourseMode.EDUCATOR
112
+ }
113
+ }
114
+
115
+ export async function loadCourseProject(
116
+ projectPath: string,
117
+ options: { aesKey?: string } = {}
118
+ ): Promise<Course> {
119
+ const courseConfigPath = path.join(projectPath, COURSE_CONFIG)
120
+ const courseYaml = await readRawYamlFile<CourseYaml>(courseConfigPath)
121
+
122
+ if (!courseYaml.title) {
123
+ throw new Error(`Missing 'title' in ${courseConfigPath}`)
124
+ }
125
+ if (!courseYaml.language) {
126
+ throw new Error(`Missing 'language' in ${courseConfigPath}`)
127
+ }
128
+ if (!courseYaml.programming_language) {
129
+ throw new Error(`Missing 'programming_language' in ${courseConfigPath}`)
130
+ }
131
+
132
+ const contentNames = ensureNamedContent(courseYaml.content, courseConfigPath)
133
+ const aesKey = options.aesKey ?? getAesKey()
134
+
135
+ const course = createCourse(courseYaml.type)
136
+ hydrateCourse(course, courseYaml)
137
+ course.languageCode = courseYaml.language
138
+
139
+ const lessons = await Promise.all(
140
+ contentNames.map((lessonName) => loadLesson(projectPath, lessonName, aesKey, courseYaml.type))
141
+ )
142
+
143
+ for (const lesson of lessons) {
144
+ course.addLesson(lesson)
145
+ }
146
+
147
+ return course
148
+ }
149
+
150
+ function createLesson(courseType: string | undefined): Lesson {
151
+ if (courseType === "stepik") {
152
+ return new StepikLesson()
153
+ }
154
+ return new Lesson()
155
+ }
156
+
157
+ function hydrateLesson(lesson: Lesson, lessonName: string, yaml: LessonYaml): void {
158
+ lesson.name = lessonName
159
+ if (yaml.custom_name) {
160
+ lesson.customPresentableName = yaml.custom_name
161
+ }
162
+ if (yaml.tags) {
163
+ lesson.contentTags = yaml.tags
164
+ }
165
+ }
166
+
167
+ async function loadLesson(coursePath: string, lessonName: string, aesKey: string, courseType?: string): Promise<Lesson> {
168
+ const lessonPath = path.join(coursePath, lessonName)
169
+ const lessonConfigPath = path.join(lessonPath, LESSON_CONFIG)
170
+ const lessonYaml = await readRawYamlFile<LessonYaml>(lessonConfigPath)
171
+
172
+ const taskNames = ensureNamedContent(lessonYaml.content, lessonConfigPath)
173
+ const tasks = await Promise.all(
174
+ taskNames.map((taskName) => loadTask(lessonPath, taskName, aesKey))
175
+ )
176
+
177
+ const lesson = createLesson(courseType)
178
+ hydrateLesson(lesson, lessonName, lessonYaml)
179
+
180
+ for (const task of tasks) {
181
+ lesson.addTask(task)
182
+ }
183
+
184
+ return lesson
185
+ }
186
+
187
+ function createTask(taskType: string): Task | null {
188
+ switch (taskType) {
189
+ case "edu":
190
+ case "pycharm":
191
+ return new EduTask()
192
+ case "code":
193
+ return new CodeTask()
194
+ case "number":
195
+ return new NumberTask()
196
+ case "string":
197
+ return new StringTask()
198
+ case "output":
199
+ return new OutputTask()
200
+ case "dataset":
201
+ return new DataTask()
202
+ case "table":
203
+ return new TableTask()
204
+ case "theory":
205
+ return new TheoryTask()
206
+ case "ide":
207
+ return new IdeTask()
208
+ case "unsupported":
209
+ return new UnsupportedTask()
210
+ case "remote_edu":
211
+ return new RemoteEduTask()
212
+ case "choice":
213
+ return new ChoiceTask()
214
+ case "matching":
215
+ return new MatchingTask()
216
+ case "sorting":
217
+ return new SortingTask()
218
+ default:
219
+ return null
220
+ }
221
+ }
222
+
223
+ type TaskYaml = {
224
+ type?: string
225
+ [key: string]: unknown
226
+ }
227
+
228
+ function hydrateTask(task: Task, taskName: string, yaml: TaskYaml): void {
229
+ task.name = taskName
230
+ if (yaml.status !== undefined && yaml.status !== null) {
231
+ const statusStr = String(yaml.status)
232
+ const statusMap: Record<string, CheckStatus> = {
233
+ Unchecked: CheckStatus.Unchecked,
234
+ Solved: CheckStatus.Solved,
235
+ Failed: CheckStatus.Failed,
236
+ }
237
+ task.status = statusMap[statusStr] ?? CheckStatus.Unchecked
238
+ }
239
+ if (typeof yaml.record === "number") {
240
+ task.record = yaml.record
241
+ }
242
+ if (yaml.feedback_link !== undefined) {
243
+ task.feedbackLink = String(yaml.feedback_link)
244
+ }
245
+ if (yaml.solution_hidden !== undefined) {
246
+ task.solutionHidden = Boolean(yaml.solution_hidden)
247
+ }
248
+ if (Array.isArray(yaml.tags)) {
249
+ task.contentTags = yaml.tags.map(String)
250
+ }
251
+ }
252
+
253
+ function hydrateChoiceTask(task: ChoiceTask, yaml: TaskYaml): void {
254
+ if (yaml.is_multiple_choice !== undefined) {
255
+ task.isMultipleChoice = Boolean(yaml.is_multiple_choice)
256
+ }
257
+ if (Array.isArray(yaml.options)) {
258
+ task.choiceOptions = yaml.options.map((opt: Record<string, unknown>) => {
259
+ const option = new ChoiceOption()
260
+ option.text = String(opt.text ?? "")
261
+ if (opt.is_correct === true) {
262
+ option.status = ChoiceOptionStatus.CORRECT
263
+ }
264
+ else if (opt.is_correct === false) {
265
+ option.status = ChoiceOptionStatus.INCORRECT
266
+ }
267
+ return option
268
+ })
269
+ }
270
+ }
271
+
272
+ function hydrateMatchingTask(task: MatchingTask, yaml: TaskYaml): void {
273
+ if (Array.isArray(yaml.captions)) {
274
+ const captions = yaml.captions as Array<Record<string, string>>
275
+ task.captions = captions.map((c) => {
276
+ const first = String(c.first ?? "")
277
+ const second = String(c.second ?? "")
278
+ return `${first} : ${second}`
279
+ })
280
+ }
281
+ if (Array.isArray(yaml.options)) {
282
+ const opts = yaml.options as string[]
283
+ task.options = opts.map(String)
284
+ }
285
+ }
286
+
287
+ function hydrateSortingTask(task: SortingTask, yaml: TaskYaml): void {
288
+ if (Array.isArray(yaml.options)) {
289
+ const opts = yaml.options as string[]
290
+ task.options = opts.map(String)
291
+ }
292
+ }
293
+
294
+ function hydrateTableTask(task: TableTask, yaml: TaskYaml): void {
295
+ if (yaml.is_multiple_choice !== undefined) {
296
+ task.isMultipleChoice = Boolean(yaml.is_multiple_choice)
297
+ }
298
+ if (Array.isArray(yaml.rows)) {
299
+ task.rows = yaml.rows.map(String)
300
+ }
301
+ if (Array.isArray(yaml.columns)) {
302
+ task.columns = yaml.columns.map(String)
303
+ }
304
+ if (Array.isArray(yaml.selected)) {
305
+ const sel = yaml.selected as number[][]
306
+ task.selected = sel.map((row) => row.map(Boolean))
307
+ }
308
+ }
309
+
310
+ async function loadTask(lessonPath: string, taskName: string, aesKey: string): Promise<Task> {
311
+ const taskPath = path.join(lessonPath, taskName)
312
+ const taskConfigPath = path.join(taskPath, TASK_CONFIG)
313
+ const rawYaml = await readRawYamlFile(taskConfigPath) as TaskYaml
314
+
315
+ if (!rawYaml.type) {
316
+ throw new Error(`Missing 'type' in ${taskConfigPath}`)
317
+ }
318
+
319
+ const task = createTask(rawYaml.type)
320
+ if (!task) {
321
+ throw new Error(`Unknown task type '${rawYaml.type}' in ${taskConfigPath}`)
322
+ }
323
+
324
+ hydrateTask(task, taskName, rawYaml)
325
+
326
+ const files = rawYaml.files
327
+ ? await Promise.all((rawYaml.files as TaskFileYaml[]).map((file) => loadTaskFile(taskPath, file, aesKey)))
328
+ : []
329
+
330
+ for (const taskFile of files) {
331
+ task.addTaskFileInstance(taskFile)
332
+ }
333
+
334
+ // Hydrate task-specific fields
335
+ if (task instanceof ChoiceTask) {
336
+ hydrateChoiceTask(task, rawYaml)
337
+ }
338
+ else if (task instanceof MatchingTask) {
339
+ hydrateMatchingTask(task, rawYaml)
340
+ }
341
+ else if (task instanceof SortingTask) {
342
+ hydrateSortingTask(task, rawYaml)
343
+ }
344
+ else if (task instanceof TableTask) {
345
+ hydrateTableTask(task, rawYaml)
346
+ }
347
+
348
+ return task
349
+ }
350
+
351
+ async function loadTaskFile(
352
+ ownerPath: string,
353
+ fileYaml: TaskFileYaml,
354
+ aesKey: string
355
+ ): Promise<TaskFile> {
356
+ if (!fileYaml.name) {
357
+ throw new Error(`Task file without name in ${ownerPath}`)
358
+ }
359
+
360
+ const isBinary = fileYaml.is_binary ?? false
361
+ const text = await resolveTaskFileText(ownerPath, fileYaml, aesKey, isBinary)
362
+
363
+ const taskFile = new TaskFile()
364
+ taskFile.name = fileYaml.name
365
+ taskFile.text = text ?? ""
366
+ taskFile.isVisible = fileYaml.visible ?? true
367
+ taskFile.isEditable = fileYaml.editable ?? true
368
+ taskFile.isPropagatable = fileYaml.propagatable ?? true
369
+ taskFile.isLearnerCreated = fileYaml.learner_created ?? false
370
+ return taskFile
371
+ }
372
+
373
+ async function resolveTaskFileText(
374
+ ownerPath: string,
375
+ fileYaml: TaskFileYaml,
376
+ aesKey: string,
377
+ isBinary: boolean
378
+ ): Promise<string | undefined> {
379
+ if (fileYaml.encrypted_text) {
380
+ let decryptedText: string | undefined
381
+ try {
382
+ decryptedText = decryptText(fileYaml.encrypted_text, aesKey)
383
+ }
384
+ catch {
385
+ decryptedText = undefined
386
+ }
387
+
388
+ if (decryptedText && decryptedText.length > 0) {
389
+ return decryptedText
390
+ }
391
+
392
+ const fileText = await readTaskFileFromDisk(ownerPath, fileYaml.name, isBinary)
393
+ return fileText ?? decryptedText
394
+ }
395
+ if (fileYaml.text !== undefined) {
396
+ return fileYaml.text
397
+ }
398
+
399
+ return readTaskFileFromDisk(ownerPath, fileYaml.name, isBinary)
400
+ }
401
+
402
+ async function readTaskFileFromDisk(
403
+ ownerPath: string,
404
+ name: string | undefined,
405
+ isBinary: boolean
406
+ ): Promise<string | undefined> {
407
+ if (!name) return undefined
408
+ const filePath = path.join(ownerPath, name)
409
+ try {
410
+ const buffer = await fs.readFile(filePath)
411
+ return isBinary ? buffer.toString("base64") : buffer.toString("utf8")
412
+ }
413
+ catch {
414
+ return undefined
415
+ }
416
+ }
417
+
418
+ function ensureNamedContent(content: Array<string | null> | undefined, configPath: string): string[] {
419
+ if (!content) return []
420
+ return content.map((name, index) => {
421
+ if (!name) {
422
+ throw new Error(`Unnamed item at position ${index + 1} in ${configPath}`)
423
+ }
424
+ return name
425
+ })
426
+ }
427
+
428
+ async function readRawYamlFile<T = Record<string, unknown>>(filePath: string): Promise<T> {
429
+ const text = await fs.readFile(filePath, "utf8")
430
+ return parse(text) as T
431
+ }
432
+
433
+ function decryptText(encryptedText: string, aesKey: string): string {
434
+ const key = Buffer.from(aesKey, "utf8")
435
+ const iv = Buffer.from(aesKey.slice(0, 16), "utf8")
436
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv)
437
+ const decrypted = Buffer.concat([
438
+ decipher.update(Buffer.from(encryptedText, "base64")),
439
+ decipher.final(),
440
+ ])
441
+ return decrypted.toString("utf8")
442
+ }
443
+
444
+ function getAesKey(): string {
445
+ const keyFromFile = readAesKeyFromResources()
446
+ if (keyFromFile && keyFromFile.length === 32) {
447
+ return keyFromFile
448
+ }
449
+ return TEST_AES_KEY
450
+ }
451
+
452
+ function readAesKeyFromResources(): string | null {
453
+ const resourcePath = path.resolve(process.cwd(), "edu-format", "resources", "aes", "aes.properties")
454
+ try {
455
+ const data = readFileSync(resourcePath, "utf8")
456
+ const match = data.split(/\r?\n/).find((line: string) => line.startsWith("aesKey="))
457
+ if (!match) return null
458
+ return match.slice("aesKey=".length).trim() || null
459
+ }
460
+ catch {
461
+ return null
462
+ }
463
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export { loadCourseProject } from "./disk-loader"
2
+ export { loadCourseProjectFromZip } from "./zip-loader"
3
+
4
+ // Core models
5
+ export { EduCourse } from "./courseFormat/EduCourse"
6
+ export { CourseraCourse } from "./courseFormat/CourseraCourse"
7
+ export { HyperskillCourse } from "./courseFormat/hyperskill/HyperskillCourse"
8
+ export { StepikCourse } from "./courseFormat/stepik/StepikCourse"
9
+ export { Course } from "./courseFormat/Course"
10
+ export { CourseMode } from "./courseFormat/CourseMode"
11
+ export { Lesson } from "./courseFormat/Lesson"
12
+ export { StepikLesson } from "./courseFormat/stepik/StepikLesson"
13
+ export { FrameworkLesson } from "./courseFormat/FrameworkLesson"
14
+ export { Section } from "./courseFormat/Section"
15
+ export { TaskFile } from "./courseFormat/TaskFile"
16
+ export { Task } from "./courseFormat/tasks/Task"
17
+ export { EduTask } from "./courseFormat/tasks/EduTask"
18
+ export { CodeTask } from "./courseFormat/tasks/CodeTask"
19
+ export { AnswerTask } from "./courseFormat/tasks/AnswerTask"
20
+ export { NumberTask } from "./courseFormat/tasks/NumberTask"
21
+ export { StringTask } from "./courseFormat/tasks/StringTask"
22
+ export { OutputTask } from "./courseFormat/tasks/OutputTask"
23
+ export { DataTask } from "./courseFormat/tasks/DataTask"
24
+ export { TableTask } from "./courseFormat/tasks/TableTask"
25
+ export { TheoryTask } from "./courseFormat/tasks/TheoryTask"
26
+ export { IdeTask } from "./courseFormat/tasks/IdeTask"
27
+ export { UnsupportedTask } from "./courseFormat/tasks/UnsupportedTask"
28
+ export { RemoteEduTask } from "./courseFormat/tasks/RemoteEduTask"
29
+ export { ChoiceTask } from "./courseFormat/tasks/choice/ChoiceTask"
30
+ export { MatchingTask } from "./courseFormat/tasks/matching/MatchingTask"
31
+ export { SortingTask } from "./courseFormat/tasks/matching/SortingTask"
32
+ export { Vendor } from "./models"
33
+ export { CheckStatus } from "./courseFormat/CheckStatus"
package/src/models.ts ADDED
@@ -0,0 +1,54 @@
1
+ export interface Vendor {
2
+ name: string
3
+ url?: string
4
+ email?: string
5
+ }
6
+
7
+ export type CourseMode = "STUDENT" | "EDUCATOR"
8
+
9
+ export interface TaskFile {
10
+ name: string
11
+ text?: string
12
+ visible: boolean
13
+ editable: boolean
14
+ propagatable: boolean
15
+ isBinary: boolean
16
+ learnerCreated: boolean
17
+ }
18
+
19
+ export interface Task {
20
+ name: string
21
+ type: string
22
+ status?: string
23
+ record?: number
24
+ files: TaskFile[]
25
+ feedbackLink?: string
26
+ solutionHidden?: boolean
27
+ tags?: string[]
28
+ }
29
+
30
+ export interface Lesson {
31
+ name: string
32
+ customName?: string
33
+ tags?: string[]
34
+ content: Task[]
35
+ }
36
+
37
+ export interface Course {
38
+ type: string
39
+ title: string
40
+ summary: string
41
+ language: string
42
+ programmingLanguage: string
43
+ programmingLanguageVersion?: string
44
+ environment?: string
45
+ solutionsHidden?: boolean
46
+ environmentSettings: Record<string, string>
47
+ additionalFiles: TaskFile[]
48
+ customContentPath?: string
49
+ disabledFeatures: string[]
50
+ vendor?: Vendor
51
+ isPrivate?: boolean
52
+ courseMode: CourseMode
53
+ content: Lesson[]
54
+ }