chuvsu-js 4.1.5 → 5.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/README.md +165 -205
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/common/cache.js +30 -9
- package/dist/common/http.d.ts +6 -0
- package/dist/common/http.js +8 -1
- package/dist/common/parse.d.ts +1 -1
- package/dist/common/parse.js +3 -3
- package/dist/common/types.d.ts +8 -2
- package/dist/common/types.js +12 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -4
- package/dist/lk/client.d.ts +8 -6
- package/dist/lk/client.js +51 -23
- package/dist/lk/types.d.ts +7 -7
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/parsers.d.ts +3 -0
- package/dist/parsers.js +2 -0
- package/dist/tt/client.d.ts +55 -63
- package/dist/tt/client.js +477 -226
- package/dist/tt/domain/directory.d.ts +14 -0
- package/dist/tt/domain/directory.js +116 -0
- package/dist/tt/domain/ids.d.ts +7 -0
- package/dist/tt/domain/ids.js +20 -0
- package/dist/tt/domain/index.d.ts +7 -0
- package/dist/tt/domain/index.js +5 -0
- package/dist/tt/domain/normalize.d.ts +7 -0
- package/dist/tt/domain/normalize.js +65 -0
- package/dist/tt/domain/repository.d.ts +51 -0
- package/dist/tt/domain/repository.js +805 -0
- package/dist/tt/domain/schedule.d.ts +37 -0
- package/dist/tt/domain/schedule.js +292 -0
- package/dist/tt/domain/types.d.ts +175 -0
- package/dist/tt/domain/types.js +1 -0
- package/dist/tt/observations.d.ts +11 -0
- package/dist/tt/observations.js +131 -0
- package/dist/tt/parse/audience.d.ts +3 -3
- package/dist/tt/parse/audience.js +58 -34
- package/dist/tt/parse/entry-parts.d.ts +8 -0
- package/dist/tt/parse/entry-parts.js +52 -0
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +49 -51
- package/dist/tt/parse/groups.d.ts +1 -1
- package/dist/tt/parse/groups.js +1 -1
- package/dist/tt/parse/index.d.ts +4 -4
- package/dist/tt/parse/index.js +4 -4
- package/dist/tt/parse/lists.d.ts +6 -5
- package/dist/tt/parse/lists.js +7 -2
- package/dist/tt/parse/overlays.d.ts +5 -4
- package/dist/tt/parse/overlays.js +13 -14
- package/dist/tt/parse/patterns.d.ts +2 -2
- package/dist/tt/parse/patterns.js +3 -3
- package/dist/tt/parse/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +21 -65
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +56 -69
- package/dist/tt/utils/date.d.ts +4 -0
- package/dist/tt/utils/date.js +30 -1
- package/dist/tt/utils/index.d.ts +2 -3
- package/dist/tt/utils/index.js +2 -3
- package/dist/tt/utils/period.d.ts +4 -4
- package/dist/tt/utils/period.js +9 -8
- package/dist/tt/utils/semester.d.ts +4 -4
- package/dist/tt/utils/semester.js +25 -5
- package/dist/tt/utils/time-slots.d.ts +3 -5
- package/dist/tt/utils/time-slots.js +19 -34
- package/dist/tt/webinars.d.ts +7 -0
- package/dist/tt/webinars.js +38 -0
- package/docs/fixture-review.md +56 -0
- package/docs/testing.md +23 -0
- package/docs/v5-architecture.md +144 -0
- package/docs/v5-migration.md +451 -0
- package/package.json +19 -7
- package/dist/shared.d.ts +0 -9
- package/dist/shared.js +0 -7
- package/dist/tt/schedule.d.ts +0 -55
- package/dist/tt/schedule.js +0 -233
- package/dist/tt/utils/lessons.d.ts +0 -16
- package/dist/tt/utils/lessons.js +0 -173
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { parseHtml, parseTeacher, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
|
-
import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
1
|
+
import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
|
+
import { parseSemesterScheduleWith, parseSessionScheduleWith, } from "./full-schedule.js";
|
|
3
3
|
import { parseGroupsString } from "./groups.js";
|
|
4
4
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
5
|
-
import { LESSON_TYPE_GLOBAL_RE, LESSON_TYPE_RE, SUBGROUP_ANNOTATION_RE, SUBGROUP_RE, WEEKS_GLOBAL_RE, WEEKS_RE, } from "./patterns.js";
|
|
6
|
-
|
|
7
|
-
export function
|
|
5
|
+
import { LESSON_TYPE_GLOBAL_RE, LESSON_TYPE_RE, FLEXIBLE_LESSON_TYPE_RE_I, SUBGROUP_ANNOTATION_RE, SUBGROUP_RE, WEEKS_GLOBAL_RE, WEEKS_RE, } from "./patterns.js";
|
|
6
|
+
import { containsGroupCode, hasDistanceMarker, linesAfterSubject, stripDistanceMarker, } from "./entry-parts.js";
|
|
7
|
+
export function parseRoomInfo(html) {
|
|
8
8
|
const doc = parseHtml(html);
|
|
9
9
|
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
10
10
|
const nameEl = doc.querySelector('.htext span[style*="color: blue"]');
|
|
@@ -67,7 +67,7 @@ export function parseAudienceInfo(html) {
|
|
|
67
67
|
floorplanRect,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
function
|
|
70
|
+
function parseRoomSemesterEntry(el) {
|
|
71
71
|
const td = el.querySelector("td") ?? el;
|
|
72
72
|
const fullHtml = td.innerHTML ?? "";
|
|
73
73
|
const plainText = text(td);
|
|
@@ -111,43 +111,67 @@ function parseAudienceSemesterEntry(el) {
|
|
|
111
111
|
const weeksMatch = cleanText.match(WEEKS_RE);
|
|
112
112
|
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
113
113
|
const weekParity = parseWeekParity(cleanHtml);
|
|
114
|
-
// Audience entries
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
.
|
|
120
|
-
.map((
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.replace(WEEKS_GLOBAL_RE, "")
|
|
129
|
-
.replace(SUBGROUP_ANNOTATION_RE, "")
|
|
130
|
-
.trim();
|
|
131
|
-
if (cleaned)
|
|
132
|
-
textLines.push(cleaned);
|
|
133
|
-
}
|
|
134
|
-
const teacherLine = textLines[0] ?? "";
|
|
135
|
-
const groupsLine = textLines.slice(1).join(" ").trim();
|
|
114
|
+
// Audience entries: subject line, then teacher line, then group line(s).
|
|
115
|
+
const parts = linesAfterSubject(cleanHtml, subject);
|
|
116
|
+
const teacherLine = stripDistanceMarker(parts.find((line) => !containsGroupCode(stripDistanceMarker(line))) ?? "");
|
|
117
|
+
const groupsLine = parts
|
|
118
|
+
.filter((line) => containsGroupCode(stripDistanceMarker(line)))
|
|
119
|
+
.map(stripDistanceMarker)
|
|
120
|
+
.map((line) => line
|
|
121
|
+
.replace(LESSON_TYPE_GLOBAL_RE, "")
|
|
122
|
+
.replace(WEEKS_GLOBAL_RE, "")
|
|
123
|
+
.replace(SUBGROUP_ANNOTATION_RE, "")
|
|
124
|
+
.trim())
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(" ");
|
|
127
|
+
const groups = parseGroupsString(groupsLine);
|
|
136
128
|
return {
|
|
137
|
-
room: "",
|
|
138
129
|
subject,
|
|
139
130
|
type: typeMatch?.[1] ?? "",
|
|
140
131
|
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
141
|
-
teacher: parseTeacher(teacherLine),
|
|
142
|
-
groups:
|
|
132
|
+
teacher: teacherLine ? parseTeacher(teacherLine) : undefined,
|
|
133
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
143
134
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
144
135
|
weekParity,
|
|
145
|
-
isDistance:
|
|
136
|
+
isDistance: hasDistanceMarker(cleanText),
|
|
146
137
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
147
138
|
possibleChanges,
|
|
148
139
|
};
|
|
149
140
|
}
|
|
150
|
-
export function
|
|
141
|
+
export function parseRoomSchedule(html) {
|
|
151
142
|
const doc = parseHtml(html);
|
|
152
|
-
|
|
143
|
+
if (doc.querySelector('td[id^="trd2"]')) {
|
|
144
|
+
return parseSessionScheduleWith(doc, parseRoomSessionEntry);
|
|
145
|
+
}
|
|
146
|
+
return parseSemesterScheduleWith(doc, parseRoomSemesterEntry);
|
|
147
|
+
}
|
|
148
|
+
function parseRoomSessionEntry(td) {
|
|
149
|
+
const fullHtml = td.innerHTML ?? "";
|
|
150
|
+
const plainText = text(td);
|
|
151
|
+
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
152
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
153
|
+
if (!plainText || !subject)
|
|
154
|
+
return null;
|
|
155
|
+
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
156
|
+
if (!timeMatch)
|
|
157
|
+
return null;
|
|
158
|
+
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
159
|
+
const parts = linesAfterSubject(fullHtml, subject);
|
|
160
|
+
const teacherLine = parts.find((part) => !/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part) &&
|
|
161
|
+
!containsGroupCode(stripDistanceMarker(part)));
|
|
162
|
+
const groupsLine = parts.find((part) => containsGroupCode(stripDistanceMarker(part)));
|
|
163
|
+
const groups = parseGroupsString(stripDistanceMarker(groupsLine ?? ""));
|
|
164
|
+
const subgroupMatch = plainText.match(SUBGROUP_RE);
|
|
165
|
+
return {
|
|
166
|
+
lesson: {
|
|
167
|
+
subject,
|
|
168
|
+
type: typeMatch?.[1].replace(/\.$/, "").toLowerCase() ?? "",
|
|
169
|
+
teacher: teacherLine ? parseTeacher(stripDistanceMarker(teacherLine)) : undefined,
|
|
170
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
171
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
172
|
+
isDistance: hasDistanceMarker(plainText),
|
|
173
|
+
possibleChanges: (td.getAttribute("class") ?? "").includes("want") || undefined,
|
|
174
|
+
},
|
|
175
|
+
time: { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) },
|
|
176
|
+
};
|
|
153
177
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function entryHtmlLines(html: string): string[];
|
|
2
|
+
export declare function entryTextLines(html: string): string[];
|
|
3
|
+
export declare function stripHtml(html: string): string;
|
|
4
|
+
export declare function hasDistanceMarker(value: string): boolean;
|
|
5
|
+
export declare function linesAfterSubject(html: string, subject: string): string[];
|
|
6
|
+
export declare function parseEntryRoom(html: string, subject: string): string;
|
|
7
|
+
export declare function stripDistanceMarker(value: string): string;
|
|
8
|
+
export declare function containsGroupCode(value: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const BR_RE = /<br\s*\/?>/i;
|
|
2
|
+
const TAG_RE = /<[^>]*>/g;
|
|
3
|
+
const DISTANCE_RE = /(?:^|[^\p{L}\p{N}])(?:дистанционно|ДОТ)(?=$|[^\p{L}\p{N}])/iu;
|
|
4
|
+
const GROUP_TOKEN_RE = /^[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+$/u;
|
|
5
|
+
export function entryHtmlLines(html) {
|
|
6
|
+
return html.split(BR_RE).map((line) => line.trim());
|
|
7
|
+
}
|
|
8
|
+
export function entryTextLines(html) {
|
|
9
|
+
return entryHtmlLines(html)
|
|
10
|
+
.map(stripHtml)
|
|
11
|
+
.filter((line) => line.length > 0);
|
|
12
|
+
}
|
|
13
|
+
export function stripHtml(html) {
|
|
14
|
+
return html.replace(TAG_RE, " ").replace(/\s+/g, " ").trim();
|
|
15
|
+
}
|
|
16
|
+
export function hasDistanceMarker(value) {
|
|
17
|
+
return DISTANCE_RE.test(value);
|
|
18
|
+
}
|
|
19
|
+
export function linesAfterSubject(html, subject) {
|
|
20
|
+
const lines = entryTextLines(html);
|
|
21
|
+
const index = lines.findIndex((line) => line.includes(subject));
|
|
22
|
+
return index < 0 ? [] : lines.slice(index + 1);
|
|
23
|
+
}
|
|
24
|
+
export function parseEntryRoom(html, subject) {
|
|
25
|
+
const lines = entryHtmlLines(html);
|
|
26
|
+
const line = lines.find((part) => stripHtml(part).includes(subject));
|
|
27
|
+
if (!line)
|
|
28
|
+
return "";
|
|
29
|
+
const lineText = stripHtml(line);
|
|
30
|
+
const subjectIndex = lineText.indexOf(subject);
|
|
31
|
+
if (subjectIndex < 0)
|
|
32
|
+
return "";
|
|
33
|
+
const beforeSubject = lineText
|
|
34
|
+
.slice(0, subjectIndex)
|
|
35
|
+
.replace(/^\*+\s*/, "")
|
|
36
|
+
.trim();
|
|
37
|
+
if (hasDistanceMarker(stripHtml(html))) {
|
|
38
|
+
return "Дистанционно (ДОТ)";
|
|
39
|
+
}
|
|
40
|
+
if (!beforeSubject)
|
|
41
|
+
return "";
|
|
42
|
+
return beforeSubject;
|
|
43
|
+
}
|
|
44
|
+
export function stripDistanceMarker(value) {
|
|
45
|
+
return value
|
|
46
|
+
.replace(/\(\s*ДОТ\s*\)/giu, " ")
|
|
47
|
+
.replace(/\s+/g, " ")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
export function containsGroupCode(value) {
|
|
51
|
+
return value.split(/\s+/).some((token) => GROUP_TOKEN_RE.test(token));
|
|
52
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
4
|
-
|
|
1
|
+
import type { ParsedScheduleDay, ParsedScheduleBlock, ParsedLesson } from "../types.js";
|
|
2
|
+
export declare function parseGroupSchedule(html: string): ParsedScheduleDay[];
|
|
3
|
+
export declare function parseSemesterScheduleWith(doc: Document, entryParser: (el: Element) => ParsedLesson | null): ParsedScheduleDay[];
|
|
4
|
+
interface ParsedSessionEntry {
|
|
5
|
+
lesson: ParsedLesson;
|
|
6
|
+
time: NonNullable<ParsedScheduleBlock["time"]>;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseSessionScheduleWith(doc: Document, entryParser: (element: Element) => ParsedSessionEntry | null): ParsedScheduleDay[];
|
|
9
|
+
export {};
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
|
-
import { getLessonNumber } from "../utils/index.js";
|
|
3
2
|
import { parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
4
3
|
import { FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
5
|
-
|
|
6
|
-
export function
|
|
4
|
+
import { hasDistanceMarker, containsGroupCode, linesAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
5
|
+
export function parseGroupSchedule(html) {
|
|
7
6
|
const doc = parseHtml(html);
|
|
8
|
-
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
9
7
|
// Session layout has date-based cells with ids like "trd20251224"
|
|
10
8
|
if (doc.querySelector('td[id^="trd2"]')) {
|
|
11
|
-
return parseSessionSchedule(doc
|
|
9
|
+
return parseSessionSchedule(doc);
|
|
12
10
|
}
|
|
13
11
|
return parseSemesterSchedule(doc);
|
|
14
12
|
}
|
|
@@ -23,13 +21,20 @@ export function parseSemesterScheduleWith(doc, entryParser) {
|
|
|
23
21
|
if (style.includes("lightgray") && cls.includes("trfd")) {
|
|
24
22
|
const dayName = text(row.querySelector("td"));
|
|
25
23
|
if (dayName) {
|
|
26
|
-
currentDay = { weekday: dayName,
|
|
24
|
+
currentDay = { weekday: dayName, blocks: [] };
|
|
27
25
|
days.push(currentDay);
|
|
28
26
|
}
|
|
29
27
|
continue;
|
|
30
28
|
}
|
|
31
29
|
if (!currentDay)
|
|
32
30
|
continue;
|
|
31
|
+
const selfStudyMarker = row.querySelector('span[style*="color: blue"]');
|
|
32
|
+
if (selfStudyMarker &&
|
|
33
|
+
/^День самостоятельной работы$/i.test(text(selfStudyMarker)) &&
|
|
34
|
+
selfStudyMarker.closest("tr") === row) {
|
|
35
|
+
currentDay.isSelfStudyDay = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
33
38
|
const timeCell = row.querySelector("td.trf");
|
|
34
39
|
const dataCell = row.querySelector("td.trdata:not(.trf)");
|
|
35
40
|
if (!timeCell || !dataCell)
|
|
@@ -42,17 +47,18 @@ export function parseSemesterScheduleWith(doc, entryParser) {
|
|
|
42
47
|
const timeMatch = timeText.match(/\((\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})\)/);
|
|
43
48
|
if (!numberMatch)
|
|
44
49
|
continue;
|
|
45
|
-
const
|
|
50
|
+
const lessons = [];
|
|
46
51
|
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
47
52
|
const entry = entryParser(entryRow);
|
|
48
53
|
if (entry)
|
|
49
|
-
|
|
54
|
+
lessons.push(entry);
|
|
50
55
|
}
|
|
51
|
-
currentDay.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
currentDay.blocks.push({
|
|
57
|
+
slotNumber: parseInt(numberMatch[1]),
|
|
58
|
+
time: timeMatch
|
|
59
|
+
? { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) }
|
|
60
|
+
: undefined,
|
|
61
|
+
lessons,
|
|
56
62
|
});
|
|
57
63
|
}
|
|
58
64
|
return days;
|
|
@@ -98,26 +104,27 @@ function parseSemesterEntry(el) {
|
|
|
98
104
|
return null;
|
|
99
105
|
const typeMatch = cleanText.match(LESSON_TYPE_RE);
|
|
100
106
|
const weeksMatch = cleanText.match(WEEKS_RE);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
107
|
+
const room = parseEntryRoom(cleanHtml, subject);
|
|
108
|
+
const teacherLine = linesAfterSubject(cleanHtml, subject).find((line) => {
|
|
109
|
+
const candidate = stripDistanceMarker(line);
|
|
110
|
+
return candidate.length > 0 && !SUBGROUP_RE.test(candidate);
|
|
111
|
+
}) ?? "";
|
|
103
112
|
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
104
113
|
const weekParity = parseWeekParity(cleanHtml);
|
|
105
114
|
return {
|
|
106
|
-
room:
|
|
115
|
+
room: room || undefined,
|
|
107
116
|
subject,
|
|
108
117
|
type: typeMatch?.[1] ?? "",
|
|
109
118
|
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
110
|
-
teacher: parseTeacher(
|
|
111
|
-
groups: [],
|
|
119
|
+
teacher: teacherLine ? parseTeacher(teacherLine) : undefined,
|
|
112
120
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
113
121
|
weekParity,
|
|
114
|
-
isDistance:
|
|
122
|
+
isDistance: hasDistanceMarker(cleanText) || hasDistanceMarker(room ?? ""),
|
|
115
123
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
116
124
|
possibleChanges,
|
|
117
125
|
};
|
|
118
126
|
}
|
|
119
|
-
|
|
120
|
-
function parseSessionSchedule(doc, educationType) {
|
|
127
|
+
export function parseSessionScheduleWith(doc, entryParser) {
|
|
121
128
|
const days = [];
|
|
122
129
|
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
123
130
|
// Parse date from cell id: trd20251224 -> 2025-12-24
|
|
@@ -125,10 +132,7 @@ function parseSessionSchedule(doc, educationType) {
|
|
|
125
132
|
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
126
133
|
if (!dateMatch)
|
|
127
134
|
continue;
|
|
128
|
-
const
|
|
129
|
-
const month = parseInt(dateMatch[2]) - 1;
|
|
130
|
-
const dayNum = parseInt(dateMatch[3]);
|
|
131
|
-
const date = new Date(year, month, dayNum);
|
|
135
|
+
const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
|
|
132
136
|
// Extract weekday from after <br>
|
|
133
137
|
const cellHtml = dateCell.innerHTML ?? "";
|
|
134
138
|
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
@@ -139,25 +143,26 @@ function parseSessionSchedule(doc, educationType) {
|
|
|
139
143
|
if (!dataCell?.matches("td.trdata:not(.trfd)")) {
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
142
|
-
const
|
|
146
|
+
const blocks = [];
|
|
143
147
|
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
144
148
|
const td = entryRow.querySelector("td") ?? entryRow;
|
|
145
|
-
const entry =
|
|
149
|
+
const entry = entryParser(td);
|
|
146
150
|
if (!entry)
|
|
147
151
|
continue;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
timeEnd: entry.timeEnd,
|
|
152
|
-
entries: [entry.entry],
|
|
152
|
+
blocks.push({
|
|
153
|
+
time: entry.time,
|
|
154
|
+
lessons: [entry.lesson],
|
|
153
155
|
});
|
|
154
156
|
}
|
|
155
|
-
if (
|
|
156
|
-
days.push({ weekday, date,
|
|
157
|
+
if (blocks.length > 0) {
|
|
158
|
+
days.push({ weekday, date, blocks });
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
161
|
return days;
|
|
160
162
|
}
|
|
163
|
+
function parseSessionSchedule(doc) {
|
|
164
|
+
return parseSessionScheduleWith(doc, parseSessionEntry);
|
|
165
|
+
}
|
|
161
166
|
function parseSessionEntry(td) {
|
|
162
167
|
const fullHtml = td.innerHTML ?? "";
|
|
163
168
|
const plainText = text(td);
|
|
@@ -168,37 +173,30 @@ function parseSessionEntry(td) {
|
|
|
168
173
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
169
174
|
if (!subject)
|
|
170
175
|
return null;
|
|
171
|
-
|
|
172
|
-
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
173
|
-
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
176
|
+
const room = parseEntryRoom(fullHtml, subject);
|
|
174
177
|
// Type: parenthesized text after </span>, case-insensitive
|
|
175
178
|
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
176
179
|
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
177
180
|
const subgroupMatch = plainText.match(SUBGROUP_RE);
|
|
178
|
-
const parts = fullHtml
|
|
179
|
-
|
|
180
|
-
.map((part) => part.replace(/<[^>]*>/g, "").trim())
|
|
181
|
-
.filter((part) => part.length > 0);
|
|
182
|
-
const teacherPart = parts.find((part) => !part.includes(subject) &&
|
|
181
|
+
const parts = linesAfterSubject(fullHtml, subject);
|
|
182
|
+
const teacherPart = parts.find((part) => stripDistanceMarker(part).length > 0 &&
|
|
183
183
|
!/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part) &&
|
|
184
|
-
!SUBGROUP_RE.test(part)
|
|
184
|
+
!SUBGROUP_RE.test(part) &&
|
|
185
|
+
!containsGroupCode(stripDistanceMarker(part))) ?? "";
|
|
185
186
|
// Time: after <br>, format HH:MM - HH:MM
|
|
186
187
|
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
187
188
|
if (!timeMatch)
|
|
188
189
|
return null;
|
|
189
190
|
return {
|
|
190
|
-
|
|
191
|
-
room,
|
|
191
|
+
lesson: {
|
|
192
|
+
room: room || undefined,
|
|
192
193
|
subject,
|
|
193
194
|
type,
|
|
194
|
-
|
|
195
|
-
teacher: parseTeacher(teacherPart),
|
|
196
|
-
groups: [],
|
|
195
|
+
teacher: teacherPart ? parseTeacher(teacherPart) : undefined,
|
|
197
196
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
198
|
-
isDistance:
|
|
197
|
+
isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
|
|
199
198
|
possibleChanges,
|
|
200
199
|
},
|
|
201
|
-
|
|
202
|
-
timeEnd: parseTime(timeMatch[2]),
|
|
200
|
+
time: { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) },
|
|
203
201
|
};
|
|
204
202
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Preserves in-name annotations (e.g. "КТ-42-25 (АихС)") and strips service
|
|
5
5
|
* markers like "(N подгруппа)" — the subgroup number is carried separately on
|
|
6
|
-
* {@link import("../types.js").
|
|
6
|
+
* {@link import("../types.js").ParsedLesson.subgroup}.
|
|
7
7
|
*
|
|
8
8
|
* Examples:
|
|
9
9
|
* "КТ-42-25" -> ["КТ-42-25"]
|
package/dist/tt/parse/groups.js
CHANGED
|
@@ -4,7 +4,7 @@ import { SUBGROUP_ANNOTATION_RE_I } from "./patterns.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Preserves in-name annotations (e.g. "КТ-42-25 (АихС)") and strips service
|
|
6
6
|
* markers like "(N подгруппа)" — the subgroup number is carried separately on
|
|
7
|
-
* {@link import("../types.js").
|
|
7
|
+
* {@link import("../types.js").ParsedLesson.subgroup}.
|
|
8
8
|
*
|
|
9
9
|
* Examples:
|
|
10
10
|
* "КТ-42-25" -> ["КТ-42-25"]
|
package/dist/tt/parse/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { parseGroupsString } from "./groups.js";
|
|
2
|
-
export { parseAcademicYearFromPage,
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
2
|
+
export { parseAcademicYearFromPage, parseRoomButtons, parseRoomName, parseFacultyButtons, parseGroupButtons, parseGroupName, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
|
|
3
|
+
export { parseGroupSchedule } from "./full-schedule.js";
|
|
4
|
+
export { parseRoomSchedule, parseRoomInfo, } from "./audience.js";
|
|
5
|
+
export { parseTeacherSchedule, parseTeacherInfo, } from "./teacher.js";
|
|
6
6
|
export { parseWebinars } from "./webinars.js";
|
package/dist/tt/parse/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { parseGroupsString } from "./groups.js";
|
|
2
|
-
export { parseAcademicYearFromPage,
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
2
|
+
export { parseAcademicYearFromPage, parseRoomButtons, parseRoomName, parseFacultyButtons, parseGroupButtons, parseGroupName, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
|
|
3
|
+
export { parseGroupSchedule } from "./full-schedule.js";
|
|
4
|
+
export { parseRoomSchedule, parseRoomInfo, } from "./audience.js";
|
|
5
|
+
export { parseTeacherSchedule, parseTeacherInfo, } from "./teacher.js";
|
|
6
6
|
export { parseWebinars } from "./webinars.js";
|
package/dist/tt/parse/lists.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
export declare function parsePeriodFromPage(html: string):
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
|
+
import type { Faculty, Group, Room } from "../types.js";
|
|
3
|
+
export declare function parsePeriodFromPage(html: string): AcademicPeriod | null;
|
|
4
4
|
/** Parse the start year from labels like "2026/2027 учебный год". */
|
|
5
5
|
export declare function parseAcademicYearFromPage(html: string): number | null;
|
|
6
6
|
export declare function parseGroupButtons(html: string): Group[];
|
|
7
|
+
export declare function parseGroupName(html: string): string | null;
|
|
7
8
|
export declare function parseFacultyButtons(html: string): Faculty[];
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function parseRoomButtons(html: string): Room[];
|
|
10
|
+
export declare function parseRoomName(html: string): string | null;
|
|
10
11
|
export declare function parseTeacherButtons(html: string): {
|
|
11
12
|
id: number;
|
|
12
13
|
name: string;
|
package/dist/tt/parse/lists.js
CHANGED
|
@@ -49,6 +49,11 @@ export function parseGroupButtons(html) {
|
|
|
49
49
|
}
|
|
50
50
|
return groups;
|
|
51
51
|
}
|
|
52
|
+
export function parseGroupName(html) {
|
|
53
|
+
const doc = parseHtml(html);
|
|
54
|
+
const name = text(doc.querySelector('span.htext span[style*="color: blue"]'));
|
|
55
|
+
return name || null;
|
|
56
|
+
}
|
|
52
57
|
export function parseFacultyButtons(html) {
|
|
53
58
|
const doc = parseHtml(html);
|
|
54
59
|
const faculties = [];
|
|
@@ -61,7 +66,7 @@ export function parseFacultyButtons(html) {
|
|
|
61
66
|
}
|
|
62
67
|
return faculties;
|
|
63
68
|
}
|
|
64
|
-
export function
|
|
69
|
+
export function parseRoomButtons(html) {
|
|
65
70
|
const results = [];
|
|
66
71
|
const seen = new Set();
|
|
67
72
|
const re = /<button[^>]*\bname="aud(\d+)"[^>]*\bvalue="([^"]*)"/g;
|
|
@@ -74,7 +79,7 @@ export function parseAudienceButtons(html) {
|
|
|
74
79
|
}
|
|
75
80
|
return results;
|
|
76
81
|
}
|
|
77
|
-
export function
|
|
82
|
+
export function parseRoomName(html) {
|
|
78
83
|
const m = html.match(/id="path"[\s\S]*?findaud[^>]*>[^<]*<\/a>([\s\S]*?)<\/div>/);
|
|
79
84
|
if (!m)
|
|
80
85
|
return null;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { LocalDate } from "../../common/types.js";
|
|
2
|
+
import type { ParsedLesson, Substitution, TransferInfo } from "../types.js";
|
|
3
|
+
export declare function parseDate(dd: string, mm: string, yyyy: string): LocalDate;
|
|
3
4
|
export declare function parseTransferDiv(div: Element): {
|
|
4
5
|
transfer: TransferInfo;
|
|
5
|
-
entry:
|
|
6
|
+
entry: ParsedLesson;
|
|
6
7
|
} | null;
|
|
7
8
|
export declare function parseSubstitutionDiv(div: Element): Substitution | null;
|
|
8
9
|
export declare function parseSubstituteForDiv(div: Element): {
|
|
9
|
-
entry:
|
|
10
|
+
entry: ParsedLesson;
|
|
10
11
|
} | null;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { parseTeacher, text } from "../../common/parse.js";
|
|
2
2
|
import { parseGroupsString } from "./groups.js";
|
|
3
|
+
import { hasDistanceMarker } from "./entry-parts.js";
|
|
3
4
|
import { LESSON_TYPE_PATTERN, LESSON_TYPE_RE, LESSON_TYPE_RE_I, SUBGROUP_RE, } from "./patterns.js";
|
|
4
5
|
const GROUP_CODE_RE = /[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+/u;
|
|
5
|
-
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
6
6
|
const BLUE_SPAN_RE = /<span\b(?=[^>]*(?:style=["'][^"']*color:\s*blue|class=["'][^"']*\bblue\b))[^>]*>([^<]+)<\/span>/i;
|
|
7
7
|
export function parseDate(dd, mm, yyyy) {
|
|
8
|
-
return
|
|
8
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
9
9
|
}
|
|
10
10
|
export function parseTransferDiv(div) {
|
|
11
11
|
const divText = text(div);
|
|
@@ -53,17 +53,17 @@ export function parseTransferDiv(div) {
|
|
|
53
53
|
}
|
|
54
54
|
const transfer = { targetDate, fromDate, fromSlot, subject };
|
|
55
55
|
const subgroupMatch = divText.match(SUBGROUP_RE);
|
|
56
|
+
const groups = parseGroupsString(groupsPart);
|
|
56
57
|
return {
|
|
57
58
|
transfer,
|
|
58
59
|
entry: {
|
|
59
|
-
room: roomMatch?.[1]
|
|
60
|
+
room: roomMatch?.[1] || undefined,
|
|
60
61
|
subject,
|
|
61
62
|
type: typeMatch?.[1] ?? "",
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
groups: parseGroupsString(groupsPart),
|
|
63
|
+
teacher: teacherPart ? parseTeacher(teacherPart) : undefined,
|
|
64
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
65
65
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
66
|
-
isDistance:
|
|
66
|
+
isDistance: hasDistanceMarker(divText) || hasDistanceMarker(roomMatch?.[1] ?? ""),
|
|
67
67
|
transfer,
|
|
68
68
|
},
|
|
69
69
|
};
|
|
@@ -83,7 +83,7 @@ export function parseSubstitutionDiv(div) {
|
|
|
83
83
|
const teacherMatch = divHtml.match(/Преподаватель:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
84
84
|
if (teacherMatch)
|
|
85
85
|
teacher = parseTeacher(teacherMatch[1].trim());
|
|
86
|
-
return { date, room, teacher, isDistance:
|
|
86
|
+
return { date, room, teacher, isDistance: hasDistanceMarker(room ?? divText) };
|
|
87
87
|
}
|
|
88
88
|
export function parseSubstituteForDiv(div) {
|
|
89
89
|
const divText = text(div);
|
|
@@ -96,7 +96,7 @@ export function parseSubstituteForDiv(div) {
|
|
|
96
96
|
const origTeacherMatch = divHtml.match(/замена\s*вместо:\s*<\/b><\/span>\s*<span[^>]*>([^<]+)<\/span>/);
|
|
97
97
|
const originalTeacher = origTeacherMatch
|
|
98
98
|
? parseTeacher(origTeacherMatch[1].trim())
|
|
99
|
-
:
|
|
99
|
+
: undefined;
|
|
100
100
|
// Subject: second blue span
|
|
101
101
|
const subjectEl = div.querySelectorAll('span[style*="color: blue"]');
|
|
102
102
|
let subject = "";
|
|
@@ -113,16 +113,15 @@ export function parseSubstituteForDiv(div) {
|
|
|
113
113
|
const typeMatch = divText.match(LESSON_TYPE_RE);
|
|
114
114
|
const groupsMatch = divHtml.match(new RegExp(`\\((?:${LESSON_TYPE_PATTERN})\\)\\s*(?:<br\\s*\\/?>)\\s*([^<]+?)(?:\\s*<i|$)`));
|
|
115
115
|
const subgroupMatch = divText.match(SUBGROUP_RE);
|
|
116
|
+
const groups = parseGroupsString(groupsMatch?.[1]);
|
|
116
117
|
return {
|
|
117
118
|
entry: {
|
|
118
|
-
room: roomMatch?.[1]
|
|
119
|
+
room: roomMatch?.[1] || undefined,
|
|
119
120
|
subject,
|
|
120
121
|
type: typeMatch?.[1] ?? "",
|
|
121
|
-
|
|
122
|
-
teacher: { name: "" },
|
|
123
|
-
groups: parseGroupsString(groupsMatch?.[1]),
|
|
122
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
124
123
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
125
|
-
isDistance:
|
|
124
|
+
isDistance: hasDistanceMarker(divText) || hasDistanceMarker(roomMatch?.[1] ?? ""),
|
|
126
125
|
substituteFor: { date, originalTeacher },
|
|
127
126
|
},
|
|
128
127
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const LESSON_TYPE_PATTERN = "\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447\u043E|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441|\u043A\u043F";
|
|
2
|
-
export declare const FLEXIBLE_LESSON_TYPE_PATTERN = "(?:\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447\u043E|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441|\u043A\u043F)\\.?|\u042D\u043A\u0437";
|
|
1
|
+
export declare const LESSON_TYPE_PATTERN = "\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447\u043E|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441|\u043A\u043F|\u0438\u0437|\u0433\u0437|\u043A\u0440\u043F";
|
|
2
|
+
export declare const FLEXIBLE_LESSON_TYPE_PATTERN = "(?:\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447\u043E|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441|\u043A\u043F|\u0438\u0437|\u0433\u0437|\u043A\u0440\u043F)\\.?|\u042D\u043A\u0437";
|
|
3
3
|
export declare const LESSON_TYPE_RE: RegExp;
|
|
4
4
|
export declare const LESSON_TYPE_RE_I: RegExp;
|
|
5
5
|
export declare const FLEXIBLE_LESSON_TYPE_RE_I: RegExp;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export const LESSON_TYPE_PATTERN = "
|
|
1
|
+
export const LESSON_TYPE_PATTERN = "лк|пр|лб|зачо|зач|экз|конс|кп|из|гз|крп";
|
|
2
2
|
export const FLEXIBLE_LESSON_TYPE_PATTERN = `(?:${LESSON_TYPE_PATTERN})\\.?|Экз`;
|
|
3
|
-
export const LESSON_TYPE_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)
|
|
3
|
+
export const LESSON_TYPE_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "i");
|
|
4
4
|
export const LESSON_TYPE_RE_I = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "i");
|
|
5
5
|
export const FLEXIBLE_LESSON_TYPE_RE_I = new RegExp(`\\((${FLEXIBLE_LESSON_TYPE_PATTERN})\\)`, "i");
|
|
6
|
-
export const LESSON_TYPE_GLOBAL_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "
|
|
6
|
+
export const LESSON_TYPE_GLOBAL_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "gi");
|
|
7
7
|
export const WEEKS_RE = /\(([^)]*нед\.?[^)]*)\)/;
|
|
8
8
|
export const WEEKS_GLOBAL_RE = /\([^)]*нед\.?[^)]*\)/g;
|
|
9
9
|
export const SUBGROUP_RE = /(\d+)\s*подгруппа/;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare function parseTeacherFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
|
|
1
|
+
import type { ParsedScheduleDay, TeacherInfo } from "../types.js";
|
|
2
|
+
export declare function parseTeacherSchedule(html: string): ParsedScheduleDay[];
|
|
4
3
|
export declare function parseTeacherInfo(html: string): TeacherInfo | null;
|