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,65 @@
|
|
|
1
|
+
import { ItemContainer } from "./ItemContainer"
|
|
2
|
+
import type { Lesson } from "./Lesson"
|
|
3
|
+
import type { Section } from "./Section"
|
|
4
|
+
import { Task } from "./tasks/Task"
|
|
5
|
+
|
|
6
|
+
function getLessonModule(): { Lesson: typeof Lesson; Section: typeof Section } {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
+
const SectionModule = require("./Section")
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
10
|
+
const LessonModule = require("./Lesson")
|
|
11
|
+
return { Lesson: LessonModule.Lesson, Section: SectionModule.Section }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export abstract class LessonContainer extends ItemContainer {
|
|
15
|
+
get lessons(): Lesson[] {
|
|
16
|
+
const mod = getLessonModule()
|
|
17
|
+
return this.items.filter((item): item is Lesson => item instanceof mod.Lesson)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getLesson(name: string): Lesson | undefined
|
|
21
|
+
getLesson(id: number): Lesson | undefined
|
|
22
|
+
getLesson(check: (lesson: Lesson) => boolean): Lesson | undefined
|
|
23
|
+
getLesson(param: string | number | ((lesson: Lesson) => boolean)): Lesson | undefined {
|
|
24
|
+
if (typeof param === "string") {
|
|
25
|
+
return this.getLesson((lesson) => lesson.name === param)
|
|
26
|
+
}
|
|
27
|
+
if (typeof param === "number") {
|
|
28
|
+
return this.getLesson((lesson) => lesson.id === param)
|
|
29
|
+
}
|
|
30
|
+
return this.lessons.find(param)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addLessons(lessons: Lesson[]): void {
|
|
34
|
+
lessons.forEach((lesson) => this.addItem(lesson))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addLesson(lesson: Lesson): void {
|
|
38
|
+
this.addItem(lesson)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
removeLesson(lesson: Lesson): void {
|
|
42
|
+
this.removeItem(lesson)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
visitLessons(visit: (lesson: Lesson) => void): void {
|
|
46
|
+
const mod = getLessonModule()
|
|
47
|
+
for (const item of this.items) {
|
|
48
|
+
if (item instanceof mod.Lesson) {
|
|
49
|
+
visit(item)
|
|
50
|
+
}
|
|
51
|
+
else if (item instanceof mod.Section) {
|
|
52
|
+
item.lessons.forEach(visit)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
visitSections(visit: (section: Section) => void): void {
|
|
58
|
+
const mod = getLessonModule()
|
|
59
|
+
this.items.filter((item): item is Section => item instanceof mod.Section).forEach(visit)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
visitTasks(visit: (task: Task) => void): void {
|
|
63
|
+
this.visitLessons((lesson) => lesson.visitTasks(visit))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class PluginInfo {
|
|
2
|
+
stringId = ""
|
|
3
|
+
displayName?: string
|
|
4
|
+
minVersion?: string
|
|
5
|
+
maxVersion?: string
|
|
6
|
+
|
|
7
|
+
constructor(stringId?: string, displayName?: string | null, minVersion?: string | null, maxVersion?: string | null) {
|
|
8
|
+
if (stringId !== undefined) {
|
|
9
|
+
this.stringId = stringId
|
|
10
|
+
}
|
|
11
|
+
this.displayName = displayName ?? undefined
|
|
12
|
+
this.minVersion = minVersion ?? undefined
|
|
13
|
+
this.maxVersion = maxVersion ?? undefined
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LessonContainer } from "./LessonContainer"
|
|
2
|
+
import type { Course } from "./Course"
|
|
3
|
+
import { SECTION } from "./EduFormatNames"
|
|
4
|
+
import { ItemContainer } from "./ItemContainer"
|
|
5
|
+
|
|
6
|
+
function getCourseModule(): { Course: typeof Course } {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
+
return require("./Course")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Section extends LessonContainer {
|
|
12
|
+
override init(parentItem: ItemContainer, isRestarted: boolean): void {
|
|
13
|
+
if (!(parentItem instanceof getCourseModule().Course)) {
|
|
14
|
+
throw new Error(`Course is null for section ${this.name}`)
|
|
15
|
+
}
|
|
16
|
+
super.init(parentItem, isRestarted)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get course(): Course {
|
|
20
|
+
if (!(this.parent instanceof getCourseModule().Course)) {
|
|
21
|
+
throw new Error(`Course is null for section ${this.name}`)
|
|
22
|
+
}
|
|
23
|
+
return this.parent as Course
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get itemType(): string {
|
|
27
|
+
return SECTION
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ItemContainer } from "./ItemContainer"
|
|
2
|
+
import type { Course } from "./Course"
|
|
3
|
+
|
|
4
|
+
export abstract class StudyItem {
|
|
5
|
+
index = -1
|
|
6
|
+
name = ""
|
|
7
|
+
updateDate: Date = new Date(0)
|
|
8
|
+
id = 0
|
|
9
|
+
contentTags: string[] = []
|
|
10
|
+
customPresentableName?: string
|
|
11
|
+
protected _parent?: ItemContainer
|
|
12
|
+
|
|
13
|
+
constructor(name?: string) {
|
|
14
|
+
if (name) {
|
|
15
|
+
this.name = name
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get parent(): ItemContainer {
|
|
20
|
+
if (!this._parent) {
|
|
21
|
+
throw new Error(`Parent is null for StudyItem ${this.name}`)
|
|
22
|
+
}
|
|
23
|
+
return this._parent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set parent(value: ItemContainer) {
|
|
27
|
+
this._parent = value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get presentableName(): string {
|
|
31
|
+
return this.customPresentableName ?? this.name
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
abstract get course(): Course
|
|
35
|
+
abstract get itemType(): string
|
|
36
|
+
abstract init(parentItem: ItemContainer, isRestarted: boolean): void
|
|
37
|
+
|
|
38
|
+
getRelativePath(root: StudyItem): string {
|
|
39
|
+
if (this === root) return ""
|
|
40
|
+
const parents: string[] = []
|
|
41
|
+
let currentParent = this.parent
|
|
42
|
+
while (currentParent !== root) {
|
|
43
|
+
parents.push(currentParent.name)
|
|
44
|
+
currentParent = currentParent.parent
|
|
45
|
+
}
|
|
46
|
+
parents.reverse()
|
|
47
|
+
if (parents.length === 0) return this.name
|
|
48
|
+
parents.push(this.name)
|
|
49
|
+
return parents.join("/")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get pathInCourse(): string {
|
|
53
|
+
return this.getRelativePath(this.course)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { message } from "./uiMessages"
|
|
2
|
+
|
|
3
|
+
const PROGRAMMING_LANGUAGE_TAG_SEARCH_OPTION = "programming_language"
|
|
4
|
+
const LANGUAGE_TAG_SEARCH_OPTION = "language"
|
|
5
|
+
|
|
6
|
+
export class Tag {
|
|
7
|
+
readonly text: string
|
|
8
|
+
private readonly searchOption: string
|
|
9
|
+
|
|
10
|
+
constructor(text: string, searchOption = "tag") {
|
|
11
|
+
this.text = text
|
|
12
|
+
this.searchOption = searchOption
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getSearchText(): string {
|
|
16
|
+
return `${this.searchOption}:${this.text}`.toLowerCase()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
accept(filter: string): boolean {
|
|
20
|
+
const textInLowerCase = this.text.toLowerCase()
|
|
21
|
+
if (textInLowerCase.includes(filter)) {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
const searchPrefix = `${this.searchOption}:`
|
|
25
|
+
return filter.startsWith(searchPrefix) && textInLowerCase.includes(filter.substring(searchPrefix.length))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ProgrammingLanguageTag extends Tag {
|
|
30
|
+
constructor(language: string) {
|
|
31
|
+
super(language, PROGRAMMING_LANGUAGE_TAG_SEARCH_OPTION)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class HumanLanguageTag extends Tag {
|
|
36
|
+
constructor(languageName: string) {
|
|
37
|
+
super(languageName, LANGUAGE_TAG_SEARCH_OPTION)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class FeaturedTag extends Tag {
|
|
42
|
+
constructor() {
|
|
43
|
+
super(message("course.dialog.tags.featured"))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { EduFile } from "./EduFile"
|
|
2
|
+
import { AnswerPlaceholder } from "./AnswerPlaceholder"
|
|
3
|
+
import { AnswerPlaceholderComparator } from "./AnswerPlaceholderComparator"
|
|
4
|
+
import { CheckStatus } from "./CheckStatus"
|
|
5
|
+
import type { Task } from "./tasks/Task"
|
|
6
|
+
import { FileContents } from "./FileContents"
|
|
7
|
+
|
|
8
|
+
export class TaskFile extends EduFile {
|
|
9
|
+
private _answerPlaceholders: AnswerPlaceholder[] = []
|
|
10
|
+
private _task?: Task
|
|
11
|
+
override isVisible = true
|
|
12
|
+
|
|
13
|
+
constructor()
|
|
14
|
+
constructor(name: string, text: string)
|
|
15
|
+
constructor(name: string, contents: FileContents)
|
|
16
|
+
constructor(name: string, text: string, isVisible: boolean)
|
|
17
|
+
constructor(name: string, text: string, isVisible: boolean, isLearnerCreated: boolean)
|
|
18
|
+
constructor(name?: string, textOrContents?: string | FileContents, isVisible?: boolean, isLearnerCreated?: boolean) {
|
|
19
|
+
super()
|
|
20
|
+
if (name !== undefined) this.name = name
|
|
21
|
+
if (typeof textOrContents === "string") {
|
|
22
|
+
this.text = textOrContents
|
|
23
|
+
}
|
|
24
|
+
else if (textOrContents) {
|
|
25
|
+
this.contents = textOrContents
|
|
26
|
+
}
|
|
27
|
+
if (isVisible !== undefined) this.isVisible = isVisible
|
|
28
|
+
if (isLearnerCreated !== undefined) this.isLearnerCreated = isLearnerCreated
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get answerPlaceholders(): AnswerPlaceholder[] {
|
|
32
|
+
return this._answerPlaceholders
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
set answerPlaceholders(value: AnswerPlaceholder[]) {
|
|
36
|
+
this._answerPlaceholders = [...value]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get task(): Task {
|
|
40
|
+
if (!this._task) {
|
|
41
|
+
throw new Error(`Task is null for TaskFile ${this.name}`)
|
|
42
|
+
}
|
|
43
|
+
return this._task
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set task(value: Task) {
|
|
47
|
+
this._task = value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
initTaskFile(task: Task, isRestarted: boolean): void {
|
|
51
|
+
this.task = task
|
|
52
|
+
for (const answerPlaceholder of this._answerPlaceholders) {
|
|
53
|
+
answerPlaceholder.init(this, isRestarted)
|
|
54
|
+
}
|
|
55
|
+
this.sortAnswerPlaceholders()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addAnswerPlaceholder(answerPlaceholder: AnswerPlaceholder): void {
|
|
59
|
+
this._answerPlaceholders.push(answerPlaceholder)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeAnswerPlaceholder(answerPlaceholder: AnswerPlaceholder): void {
|
|
63
|
+
this._answerPlaceholders = this._answerPlaceholders.filter((item) => item !== answerPlaceholder)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getAnswerPlaceholder(offset: number): AnswerPlaceholder | undefined {
|
|
67
|
+
return this._answerPlaceholders.find((placeholder) => offset >= placeholder.offset && offset <= placeholder.endOffset)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
removeAllPlaceholders(): void {
|
|
71
|
+
this._answerPlaceholders = []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sortAnswerPlaceholders(): void {
|
|
75
|
+
this._answerPlaceholders.sort(AnswerPlaceholderComparator.compare)
|
|
76
|
+
this._answerPlaceholders.forEach((placeholder, index) => {
|
|
77
|
+
placeholder.index = index
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
hasFailedPlaceholders(): boolean {
|
|
82
|
+
return this._answerPlaceholders.some((placeholder) => placeholder.status === CheckStatus.Failed)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isValid(text: string): boolean {
|
|
86
|
+
return this._answerPlaceholders.every((placeholder) => placeholder.isValid(text.length))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class Vendor {
|
|
2
|
+
readonly name: string
|
|
3
|
+
readonly email?: string
|
|
4
|
+
readonly url?: string
|
|
5
|
+
|
|
6
|
+
constructor(name = "", email?: string | null, url?: string | null) {
|
|
7
|
+
this.name = name
|
|
8
|
+
this.email = email ?? undefined
|
|
9
|
+
this.url = url ?? undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
toString(): string {
|
|
13
|
+
return this.name
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AttemptBase } from "./AttemptBase"
|
|
2
|
+
import { Dataset } from "./Dataset"
|
|
3
|
+
|
|
4
|
+
export class Attempt extends AttemptBase {
|
|
5
|
+
step = 0
|
|
6
|
+
dataset: Dataset | null = null
|
|
7
|
+
status: string | null = null
|
|
8
|
+
user: string | null = null
|
|
9
|
+
|
|
10
|
+
get isActive(): boolean {
|
|
11
|
+
return this.status === "active"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor()
|
|
15
|
+
constructor(step: number)
|
|
16
|
+
constructor(id: number, time: Date, timeLeft: number)
|
|
17
|
+
constructor(stepOrId?: number, time?: Date, timeLeft?: number) {
|
|
18
|
+
super()
|
|
19
|
+
if (stepOrId !== undefined && time === undefined) {
|
|
20
|
+
this.step = stepOrId
|
|
21
|
+
}
|
|
22
|
+
else if (stepOrId !== undefined && time !== undefined) {
|
|
23
|
+
this.id = stepOrId
|
|
24
|
+
this.time = time
|
|
25
|
+
this.timeLeft = timeLeft ?? null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export abstract class AttemptBase {
|
|
2
|
+
id = 0
|
|
3
|
+
time: Date = new Date()
|
|
4
|
+
timeLeft: number | null = null
|
|
5
|
+
|
|
6
|
+
get isRunning(): boolean {
|
|
7
|
+
const endDateTime = this.calculateEndDateTime()
|
|
8
|
+
if (endDateTime === null) return true
|
|
9
|
+
return new Date() < endDateTime
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected calculateEndDateTime(): Date | null {
|
|
13
|
+
if (this.timeLeft === null) return null
|
|
14
|
+
return new Date(this.time.getTime() + this.timeLeft * 1000)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor()
|
|
18
|
+
constructor(id: number, time: Date, timeLeft: number | null)
|
|
19
|
+
constructor(id?: number, time?: Date, timeLeft?: number | null) {
|
|
20
|
+
if (id !== undefined) this.id = id
|
|
21
|
+
if (time !== undefined) this.time = time
|
|
22
|
+
if (timeLeft !== undefined) this.timeLeft = timeLeft
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AttemptBase } from "./AttemptBase"
|
|
2
|
+
|
|
3
|
+
export class DataTaskAttempt extends AttemptBase {
|
|
4
|
+
endDateTime: Date | null = null
|
|
5
|
+
|
|
6
|
+
override get isRunning(): boolean {
|
|
7
|
+
if (this.endDateTime === null) return true
|
|
8
|
+
return new Date() < this.endDateTime
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static fromAttempt(attempt: AttemptBase): DataTaskAttempt {
|
|
12
|
+
const result = new DataTaskAttempt()
|
|
13
|
+
result.id = attempt.id
|
|
14
|
+
result.time = attempt.time
|
|
15
|
+
result.timeLeft = attempt.timeLeft
|
|
16
|
+
result.endDateTime = result.calculateEndDateTime()
|
|
17
|
+
return result
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class Dataset {
|
|
2
|
+
isMultipleChoice = false
|
|
3
|
+
options: string[] | null = null
|
|
4
|
+
pairs: Pair[] | null = null
|
|
5
|
+
rows: string[] | null = null
|
|
6
|
+
columns: string[] | null = null
|
|
7
|
+
isCheckbox = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Pair {
|
|
11
|
+
first = ""
|
|
12
|
+
second = ""
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lookup } from "mime-types"
|
|
2
|
+
import { logger } from "./loggerUtils"
|
|
3
|
+
|
|
4
|
+
const mimeBinaryTypes = ["image", "audio", "video", "application", "font"].map((type) => `${type}/`)
|
|
5
|
+
|
|
6
|
+
export function isBinary(contentType: string): boolean {
|
|
7
|
+
return mimeBinaryTypes.some((prefix) => contentType.startsWith(prefix))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mimeFileType(path: string): string | false | undefined {
|
|
11
|
+
try {
|
|
12
|
+
return lookup(path)
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
logger("fileUtils").error("Failed to determine file mimetype", error)
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function exceedsBase64ContentLimit(base64text: string): boolean {
|
|
21
|
+
return Buffer.from(base64text, "utf16le").byteLength > getBinaryFileLimit()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getBinaryFileLimit(): number {
|
|
25
|
+
return 1024 * 1024
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getExtension(fileName: string): string {
|
|
29
|
+
const index = fileName.lastIndexOf(".")
|
|
30
|
+
return index < 0 ? "" : fileName.substring(index + 1)
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Course } from "../Course"
|
|
2
|
+
import { HyperskillProject } from "./HyperskillProject"
|
|
3
|
+
import { HyperskillStage } from "./HyperskillStage"
|
|
4
|
+
import { HyperskillTopic } from "./HyperskillTopic"
|
|
5
|
+
import { HYPERSKILL } from "../EduFormatNames"
|
|
6
|
+
|
|
7
|
+
export class HyperskillCourse extends Course {
|
|
8
|
+
taskToTopics: Map<number, HyperskillTopic[]> = new Map()
|
|
9
|
+
stages: HyperskillStage[] = []
|
|
10
|
+
hyperskillProject: HyperskillProject | null = null
|
|
11
|
+
selectedStage: number | null = null
|
|
12
|
+
selectedProblem: number | null = null
|
|
13
|
+
|
|
14
|
+
override get itemType(): string {
|
|
15
|
+
return HYPERSKILL
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get isTemplateBased(): boolean {
|
|
19
|
+
if (!this.hyperskillProject) {
|
|
20
|
+
throw new Error("Disconnected Hyperskill project")
|
|
21
|
+
}
|
|
22
|
+
return this.hyperskillProject.isTemplateBased
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class HyperskillStage {
|
|
2
|
+
id = -1
|
|
3
|
+
title = ""
|
|
4
|
+
stepId = -1
|
|
5
|
+
isCompleted = false
|
|
6
|
+
|
|
7
|
+
constructor()
|
|
8
|
+
constructor(stageId: number, stageTitle: string, stageStepId: number, isStageCompleted: boolean)
|
|
9
|
+
constructor(stageId?: number, stageTitle?: string, stageStepId?: number, isStageCompleted?: boolean) {
|
|
10
|
+
if (stageId !== undefined) this.id = stageId
|
|
11
|
+
if (stageTitle !== undefined) this.title = stageTitle
|
|
12
|
+
if (stageStepId !== undefined) this.stepId = stageStepId
|
|
13
|
+
if (isStageCompleted !== undefined) this.isCompleted = isStageCompleted
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps Hyperskill task type IDs to their human-readable labels.
|
|
3
|
+
*/
|
|
4
|
+
export const HYPERSKILL_TASK_TYPE_LABELS: Record<string, string> = {
|
|
5
|
+
admin: "Linux",
|
|
6
|
+
choice: "Quiz",
|
|
7
|
+
code: "Programming",
|
|
8
|
+
dataset: "Data",
|
|
9
|
+
"fill-blanks": "Fill Blanks",
|
|
10
|
+
"free-answer": "Free Response",
|
|
11
|
+
"manual-score": "Manual Score",
|
|
12
|
+
matching: "Matching",
|
|
13
|
+
math: "Math",
|
|
14
|
+
number: "Number",
|
|
15
|
+
parsons: "Parsons",
|
|
16
|
+
pycharm: "Programming",
|
|
17
|
+
remote_edu: "Programming",
|
|
18
|
+
sorting: "Sorting",
|
|
19
|
+
string: "Text",
|
|
20
|
+
table: "Table",
|
|
21
|
+
text: "Theory",
|
|
22
|
+
video: "Video",
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CheckStatus } from "../CheckStatus"
|
|
2
|
+
import { Task } from "./Task"
|
|
3
|
+
|
|
4
|
+
export abstract class AnswerTask extends Task {
|
|
5
|
+
static ANSWER_FILE_NAME = "answer.txt"
|
|
6
|
+
|
|
7
|
+
constructor()
|
|
8
|
+
constructor(name: string)
|
|
9
|
+
constructor(name: string, id: number, position: number, updateDate: Date, status: CheckStatus)
|
|
10
|
+
constructor(name?: string, id?: number, position?: number, updateDate?: Date, status?: CheckStatus) {
|
|
11
|
+
super(name as string, id as number, position as number, updateDate as Date, status as CheckStatus)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CheckStatus } from "../CheckStatus"
|
|
2
|
+
import { Task } from "./Task"
|
|
3
|
+
|
|
4
|
+
export class CodeTask extends Task {
|
|
5
|
+
submissionLanguage?: string
|
|
6
|
+
|
|
7
|
+
constructor()
|
|
8
|
+
constructor(name: string)
|
|
9
|
+
constructor(
|
|
10
|
+
name: string,
|
|
11
|
+
id: number,
|
|
12
|
+
position: number,
|
|
13
|
+
updateDate: Date,
|
|
14
|
+
status: CheckStatus,
|
|
15
|
+
submissionLanguage?: string | null
|
|
16
|
+
)
|
|
17
|
+
constructor(
|
|
18
|
+
name?: string,
|
|
19
|
+
id?: number,
|
|
20
|
+
position?: number,
|
|
21
|
+
updateDate?: Date,
|
|
22
|
+
status?: CheckStatus,
|
|
23
|
+
submissionLanguage?: string | null
|
|
24
|
+
) {
|
|
25
|
+
super(name as string, id as number, position as number, updateDate as Date, status as CheckStatus)
|
|
26
|
+
this.submissionLanguage = submissionLanguage ?? undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get itemType(): string {
|
|
30
|
+
return CodeTask.CODE_TASK_TYPE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get supportSubmissions(): boolean {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get isPluginTaskType(): boolean {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static CODE_TASK_TYPE = "code"
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CheckStatus } from "../CheckStatus"
|
|
2
|
+
import { Task } from "./Task"
|
|
3
|
+
import type { DataTaskAttempt } from "../attempts/DataTaskAttempt"
|
|
4
|
+
|
|
5
|
+
export class DataTask extends Task {
|
|
6
|
+
attempt?: DataTaskAttempt
|
|
7
|
+
|
|
8
|
+
get isTimeLimited(): boolean {
|
|
9
|
+
return this.attempt?.endDateTime !== undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor()
|
|
13
|
+
constructor(name: string)
|
|
14
|
+
constructor(name: string, id: number, position: number, updateDate: Date, status: CheckStatus)
|
|
15
|
+
constructor(name?: string, id?: number, position?: number, updateDate?: Date, status?: CheckStatus) {
|
|
16
|
+
super(name as string, id as number, position as number, updateDate as Date, status as CheckStatus)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get itemType(): string {
|
|
20
|
+
return DataTask.DATA_TASK_TYPE
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get supportSubmissions(): boolean {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isRunning(): boolean {
|
|
28
|
+
if (this.status !== CheckStatus.Unchecked) return false
|
|
29
|
+
return this.attempt?.isRunning ?? false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static DATA_TASK_TYPE = "dataset"
|
|
33
|
+
static DATA_FOLDER_NAME = "data"
|
|
34
|
+
static DATA_SAMPLE_FOLDER_NAME = "sample"
|
|
35
|
+
static DATASET_FOLDER_NAME = "dataset"
|
|
36
|
+
static INPUT_FILE_NAME = "input.txt"
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CheckStatus } from "../CheckStatus"
|
|
2
|
+
import { Task } from "./Task"
|
|
3
|
+
|
|
4
|
+
export class EduTask extends Task {
|
|
5
|
+
constructor()
|
|
6
|
+
constructor(name: string)
|
|
7
|
+
constructor(name: string, id: number, position: number, updateDate: Date, status: CheckStatus)
|
|
8
|
+
constructor(name?: string, id?: number, position?: number, updateDate?: Date, status?: CheckStatus) {
|
|
9
|
+
super(name as string, id as number, position as number, updateDate as Date, status as CheckStatus)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get itemType(): string {
|
|
13
|
+
return EduTask.EDU_TASK_TYPE
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get isToSubmitToRemote(): boolean {
|
|
17
|
+
return this.checkStatus !== CheckStatus.Unchecked
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get supportSubmissions(): boolean {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static EDU_TASK_TYPE = "edu"
|
|
25
|
+
static PYCHARM_TASK_TYPE = "pycharm"
|
|
26
|
+
}
|