chuvsu-js 4.2.0 → 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 -193
- 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 +2 -2
- 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 +47 -15
- package/dist/tt/parse/entry-parts.d.ts +1 -0
- package/dist/tt/parse/entry-parts.js +5 -2
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +32 -41
- 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/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +14 -56
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +54 -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 +17 -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,11 +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
|
-
import { linesAfterSubject, stripDistanceMarker } from "./entry-parts.js";
|
|
7
|
-
|
|
8
|
-
export function parseAudienceInfo(html) {
|
|
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) {
|
|
9
8
|
const doc = parseHtml(html);
|
|
10
9
|
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
11
10
|
const nameEl = doc.querySelector('.htext span[style*="color: blue"]');
|
|
@@ -68,7 +67,7 @@ export function parseAudienceInfo(html) {
|
|
|
68
67
|
floorplanRect,
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
|
-
function
|
|
70
|
+
function parseRoomSemesterEntry(el) {
|
|
72
71
|
const td = el.querySelector("td") ?? el;
|
|
73
72
|
const fullHtml = td.innerHTML ?? "";
|
|
74
73
|
const plainText = text(td);
|
|
@@ -114,9 +113,9 @@ function parseAudienceSemesterEntry(el) {
|
|
|
114
113
|
const weekParity = parseWeekParity(cleanHtml);
|
|
115
114
|
// Audience entries: subject line, then teacher line, then group line(s).
|
|
116
115
|
const parts = linesAfterSubject(cleanHtml, subject);
|
|
117
|
-
const teacherLine = stripDistanceMarker(parts
|
|
116
|
+
const teacherLine = stripDistanceMarker(parts.find((line) => !containsGroupCode(stripDistanceMarker(line))) ?? "");
|
|
118
117
|
const groupsLine = parts
|
|
119
|
-
.
|
|
118
|
+
.filter((line) => containsGroupCode(stripDistanceMarker(line)))
|
|
120
119
|
.map(stripDistanceMarker)
|
|
121
120
|
.map((line) => line
|
|
122
121
|
.replace(LESSON_TYPE_GLOBAL_RE, "")
|
|
@@ -125,21 +124,54 @@ function parseAudienceSemesterEntry(el) {
|
|
|
125
124
|
.trim())
|
|
126
125
|
.filter(Boolean)
|
|
127
126
|
.join(" ");
|
|
127
|
+
const groups = parseGroupsString(groupsLine);
|
|
128
128
|
return {
|
|
129
|
-
room: "",
|
|
130
129
|
subject,
|
|
131
130
|
type: typeMatch?.[1] ?? "",
|
|
132
131
|
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
133
|
-
teacher: parseTeacher(teacherLine),
|
|
134
|
-
groups:
|
|
132
|
+
teacher: teacherLine ? parseTeacher(teacherLine) : undefined,
|
|
133
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
135
134
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
136
135
|
weekParity,
|
|
137
|
-
isDistance:
|
|
136
|
+
isDistance: hasDistanceMarker(cleanText),
|
|
138
137
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
139
138
|
possibleChanges,
|
|
140
139
|
};
|
|
141
140
|
}
|
|
142
|
-
export function
|
|
141
|
+
export function parseRoomSchedule(html) {
|
|
143
142
|
const doc = parseHtml(html);
|
|
144
|
-
|
|
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
|
+
};
|
|
145
177
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function entryHtmlLines(html: string): string[];
|
|
2
2
|
export declare function entryTextLines(html: string): string[];
|
|
3
3
|
export declare function stripHtml(html: string): string;
|
|
4
|
+
export declare function hasDistanceMarker(value: string): boolean;
|
|
4
5
|
export declare function linesAfterSubject(html: string, subject: string): string[];
|
|
5
6
|
export declare function parseEntryRoom(html: string, subject: string): string;
|
|
6
7
|
export declare function stripDistanceMarker(value: string): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const BR_RE = /<br\s*\/?>/i;
|
|
2
2
|
const TAG_RE = /<[^>]*>/g;
|
|
3
|
-
const DISTANCE_RE =
|
|
3
|
+
const DISTANCE_RE = /(?:^|[^\p{L}\p{N}])(?:дистанционно|ДОТ)(?=$|[^\p{L}\p{N}])/iu;
|
|
4
4
|
const GROUP_TOKEN_RE = /^[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+$/u;
|
|
5
5
|
export function entryHtmlLines(html) {
|
|
6
6
|
return html.split(BR_RE).map((line) => line.trim());
|
|
@@ -13,6 +13,9 @@ export function entryTextLines(html) {
|
|
|
13
13
|
export function stripHtml(html) {
|
|
14
14
|
return html.replace(TAG_RE, " ").replace(/\s+/g, " ").trim();
|
|
15
15
|
}
|
|
16
|
+
export function hasDistanceMarker(value) {
|
|
17
|
+
return DISTANCE_RE.test(value);
|
|
18
|
+
}
|
|
16
19
|
export function linesAfterSubject(html, subject) {
|
|
17
20
|
const lines = entryTextLines(html);
|
|
18
21
|
const index = lines.findIndex((line) => line.includes(subject));
|
|
@@ -31,7 +34,7 @@ export function parseEntryRoom(html, subject) {
|
|
|
31
34
|
.slice(0, subjectIndex)
|
|
32
35
|
.replace(/^\*+\s*/, "")
|
|
33
36
|
.trim();
|
|
34
|
-
if (
|
|
37
|
+
if (hasDistanceMarker(stripHtml(html))) {
|
|
35
38
|
return "Дистанционно (ДОТ)";
|
|
36
39
|
}
|
|
37
40
|
if (!beforeSubject)
|
|
@@ -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,15 +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
|
-
import { containsGroupCode, linesAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
6
|
-
|
|
7
|
-
export function parseFullSchedule(html, educationType) {
|
|
4
|
+
import { hasDistanceMarker, containsGroupCode, linesAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
5
|
+
export function parseGroupSchedule(html) {
|
|
8
6
|
const doc = parseHtml(html);
|
|
9
|
-
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
10
7
|
// Session layout has date-based cells with ids like "trd20251224"
|
|
11
8
|
if (doc.querySelector('td[id^="trd2"]')) {
|
|
12
|
-
return parseSessionSchedule(doc
|
|
9
|
+
return parseSessionSchedule(doc);
|
|
13
10
|
}
|
|
14
11
|
return parseSemesterSchedule(doc);
|
|
15
12
|
}
|
|
@@ -24,7 +21,7 @@ export function parseSemesterScheduleWith(doc, entryParser) {
|
|
|
24
21
|
if (style.includes("lightgray") && cls.includes("trfd")) {
|
|
25
22
|
const dayName = text(row.querySelector("td"));
|
|
26
23
|
if (dayName) {
|
|
27
|
-
currentDay = { weekday: dayName,
|
|
24
|
+
currentDay = { weekday: dayName, blocks: [] };
|
|
28
25
|
days.push(currentDay);
|
|
29
26
|
}
|
|
30
27
|
continue;
|
|
@@ -50,17 +47,18 @@ export function parseSemesterScheduleWith(doc, entryParser) {
|
|
|
50
47
|
const timeMatch = timeText.match(/\((\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})\)/);
|
|
51
48
|
if (!numberMatch)
|
|
52
49
|
continue;
|
|
53
|
-
const
|
|
50
|
+
const lessons = [];
|
|
54
51
|
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
55
52
|
const entry = entryParser(entryRow);
|
|
56
53
|
if (entry)
|
|
57
|
-
|
|
54
|
+
lessons.push(entry);
|
|
58
55
|
}
|
|
59
|
-
currentDay.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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,
|
|
64
62
|
});
|
|
65
63
|
}
|
|
66
64
|
return days;
|
|
@@ -114,21 +112,19 @@ function parseSemesterEntry(el) {
|
|
|
114
112
|
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
115
113
|
const weekParity = parseWeekParity(cleanHtml);
|
|
116
114
|
return {
|
|
117
|
-
room,
|
|
115
|
+
room: room || undefined,
|
|
118
116
|
subject,
|
|
119
117
|
type: typeMatch?.[1] ?? "",
|
|
120
118
|
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
121
|
-
teacher: parseTeacher(teacherLine),
|
|
122
|
-
groups: [],
|
|
119
|
+
teacher: teacherLine ? parseTeacher(teacherLine) : undefined,
|
|
123
120
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
124
121
|
weekParity,
|
|
125
|
-
isDistance:
|
|
122
|
+
isDistance: hasDistanceMarker(cleanText) || hasDistanceMarker(room ?? ""),
|
|
126
123
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
127
124
|
possibleChanges,
|
|
128
125
|
};
|
|
129
126
|
}
|
|
130
|
-
|
|
131
|
-
function parseSessionSchedule(doc, educationType) {
|
|
127
|
+
export function parseSessionScheduleWith(doc, entryParser) {
|
|
132
128
|
const days = [];
|
|
133
129
|
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
134
130
|
// Parse date from cell id: trd20251224 -> 2025-12-24
|
|
@@ -136,10 +132,7 @@ function parseSessionSchedule(doc, educationType) {
|
|
|
136
132
|
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
137
133
|
if (!dateMatch)
|
|
138
134
|
continue;
|
|
139
|
-
const
|
|
140
|
-
const month = parseInt(dateMatch[2]) - 1;
|
|
141
|
-
const dayNum = parseInt(dateMatch[3]);
|
|
142
|
-
const date = new Date(year, month, dayNum);
|
|
135
|
+
const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
|
|
143
136
|
// Extract weekday from after <br>
|
|
144
137
|
const cellHtml = dateCell.innerHTML ?? "";
|
|
145
138
|
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
@@ -150,25 +143,26 @@ function parseSessionSchedule(doc, educationType) {
|
|
|
150
143
|
if (!dataCell?.matches("td.trdata:not(.trfd)")) {
|
|
151
144
|
continue;
|
|
152
145
|
}
|
|
153
|
-
const
|
|
146
|
+
const blocks = [];
|
|
154
147
|
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
155
148
|
const td = entryRow.querySelector("td") ?? entryRow;
|
|
156
|
-
const entry =
|
|
149
|
+
const entry = entryParser(td);
|
|
157
150
|
if (!entry)
|
|
158
151
|
continue;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
timeEnd: entry.timeEnd,
|
|
163
|
-
entries: [entry.entry],
|
|
152
|
+
blocks.push({
|
|
153
|
+
time: entry.time,
|
|
154
|
+
lessons: [entry.lesson],
|
|
164
155
|
});
|
|
165
156
|
}
|
|
166
|
-
if (
|
|
167
|
-
days.push({ weekday, date,
|
|
157
|
+
if (blocks.length > 0) {
|
|
158
|
+
days.push({ weekday, date, blocks });
|
|
168
159
|
}
|
|
169
160
|
}
|
|
170
161
|
return days;
|
|
171
162
|
}
|
|
163
|
+
function parseSessionSchedule(doc) {
|
|
164
|
+
return parseSessionScheduleWith(doc, parseSessionEntry);
|
|
165
|
+
}
|
|
172
166
|
function parseSessionEntry(td) {
|
|
173
167
|
const fullHtml = td.innerHTML ?? "";
|
|
174
168
|
const plainText = text(td);
|
|
@@ -194,18 +188,15 @@ function parseSessionEntry(td) {
|
|
|
194
188
|
if (!timeMatch)
|
|
195
189
|
return null;
|
|
196
190
|
return {
|
|
197
|
-
|
|
198
|
-
room,
|
|
191
|
+
lesson: {
|
|
192
|
+
room: room || undefined,
|
|
199
193
|
subject,
|
|
200
194
|
type,
|
|
201
|
-
|
|
202
|
-
teacher: parseTeacher(teacherPart),
|
|
203
|
-
groups: [],
|
|
195
|
+
teacher: teacherPart ? parseTeacher(teacherPart) : undefined,
|
|
204
196
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
205
|
-
isDistance:
|
|
197
|
+
isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
|
|
206
198
|
possibleChanges,
|
|
207
199
|
},
|
|
208
|
-
|
|
209
|
-
timeEnd: parseTime(timeMatch[2]),
|
|
200
|
+
time: { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) },
|
|
210
201
|
};
|
|
211
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,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;
|
package/dist/tt/parse/teacher.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { parseHtml, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
|
-
import {
|
|
3
|
-
import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
2
|
+
import { parseSemesterScheduleWith, parseSessionScheduleWith, } from "./full-schedule.js";
|
|
4
3
|
import { parseGroupsString } from "./groups.js";
|
|
5
4
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
6
5
|
import { FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
7
|
-
import { containsGroupCode, linesAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
8
|
-
|
|
9
|
-
export function parseTeacherFullSchedule(html, educationType) {
|
|
6
|
+
import { containsGroupCode, hasDistanceMarker, linesAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
7
|
+
export function parseTeacherSchedule(html) {
|
|
10
8
|
const doc = parseHtml(html);
|
|
11
|
-
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
12
9
|
if (doc.querySelector('td[id^="trd2"]')) {
|
|
13
|
-
return
|
|
10
|
+
return parseSessionScheduleWith(doc, parseTeacherSessionEntry);
|
|
14
11
|
}
|
|
15
12
|
return parseSemesterScheduleWith(doc, parseTeacherSemesterEntry);
|
|
16
13
|
}
|
|
@@ -61,57 +58,20 @@ function parseTeacherSemesterEntry(el) {
|
|
|
61
58
|
const groupsLine = linesAfterSubject(cleanHtml, subject).find((line) => containsGroupCode(stripDistanceMarker(line))) ?? "";
|
|
62
59
|
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
63
60
|
const weekParity = parseWeekParity(cleanHtml);
|
|
61
|
+
const groups = parseGroupsString(stripDistanceMarker(groupsLine));
|
|
64
62
|
return {
|
|
65
|
-
room,
|
|
63
|
+
room: room || undefined,
|
|
66
64
|
subject,
|
|
67
65
|
type: typeMatch?.[1] ?? "",
|
|
68
66
|
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
69
|
-
|
|
70
|
-
groups: parseGroupsString(stripDistanceMarker(groupsLine)),
|
|
67
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
71
68
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
72
69
|
weekParity,
|
|
73
|
-
isDistance:
|
|
70
|
+
isDistance: hasDistanceMarker(cleanText) || hasDistanceMarker(room ?? ""),
|
|
74
71
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
75
72
|
possibleChanges,
|
|
76
73
|
};
|
|
77
74
|
}
|
|
78
|
-
function parseTeacherSessionSchedule(doc, educationType) {
|
|
79
|
-
const days = [];
|
|
80
|
-
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
81
|
-
const id = dateCell.getAttribute("id") ?? "";
|
|
82
|
-
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
83
|
-
if (!dateMatch)
|
|
84
|
-
continue;
|
|
85
|
-
const year = parseInt(dateMatch[1]);
|
|
86
|
-
const month = parseInt(dateMatch[2]) - 1;
|
|
87
|
-
const dayNum = parseInt(dateMatch[3]);
|
|
88
|
-
const date = new Date(year, month, dayNum);
|
|
89
|
-
const cellHtml = dateCell.innerHTML ?? "";
|
|
90
|
-
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
91
|
-
const weekday = brMatch ? brMatch[1].trim() : "";
|
|
92
|
-
const dataCell = dateCell.nextElementSibling;
|
|
93
|
-
if (!dataCell?.matches("td.trdata:not(.trfd)")) {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const slots = [];
|
|
97
|
-
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
98
|
-
const td = entryRow.querySelector("td") ?? entryRow;
|
|
99
|
-
const entry = parseTeacherSessionEntry(td);
|
|
100
|
-
if (!entry)
|
|
101
|
-
continue;
|
|
102
|
-
slots.push({
|
|
103
|
-
number: getLessonNumber(entry.timeStart, educationType),
|
|
104
|
-
timeStart: entry.timeStart,
|
|
105
|
-
timeEnd: entry.timeEnd,
|
|
106
|
-
entries: [entry.entry],
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
if (slots.length > 0) {
|
|
110
|
-
days.push({ weekday, date, slots });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return days;
|
|
114
|
-
}
|
|
115
75
|
function parseTeacherSessionEntry(td) {
|
|
116
76
|
const fullHtml = td.innerHTML ?? "";
|
|
117
77
|
const plainText = text(td);
|
|
@@ -133,20 +93,18 @@ function parseTeacherSessionEntry(td) {
|
|
|
133
93
|
const groupsPart = parts.find((part) => stripDistanceMarker(part).length > 0 &&
|
|
134
94
|
!/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part) &&
|
|
135
95
|
containsGroupCode(stripDistanceMarker(part))) ?? "";
|
|
96
|
+
const groups = parseGroupsString(stripDistanceMarker(groupsPart));
|
|
136
97
|
return {
|
|
137
|
-
|
|
138
|
-
room,
|
|
98
|
+
lesson: {
|
|
99
|
+
room: room || undefined,
|
|
139
100
|
subject,
|
|
140
101
|
type,
|
|
141
|
-
|
|
142
|
-
teacher: { name: "" },
|
|
143
|
-
groups: parseGroupsString(stripDistanceMarker(groupsPart)),
|
|
102
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
144
103
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
145
|
-
isDistance:
|
|
104
|
+
isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
|
|
146
105
|
possibleChanges,
|
|
147
106
|
},
|
|
148
|
-
|
|
149
|
-
timeEnd: parseTime(timeMatch[2]),
|
|
107
|
+
time: { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) },
|
|
150
108
|
};
|
|
151
109
|
}
|
|
152
110
|
export function parseTeacherInfo(html) {
|
|
@@ -6,7 +6,7 @@ function parseDateValue(value) {
|
|
|
6
6
|
const match = value?.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
7
7
|
if (!match)
|
|
8
8
|
return undefined;
|
|
9
|
-
return
|
|
9
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
10
10
|
}
|
|
11
11
|
function parseTimeRange(raw) {
|
|
12
12
|
const match = raw.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
@@ -71,10 +71,9 @@ function parseWebinarRows(doc, tableSelector, scheduled, fallbackDate) {
|
|
|
71
71
|
id: joinMatch?.[1] ?? "",
|
|
72
72
|
idType: joinMatch ? parseInt(joinMatch[2]) : 1,
|
|
73
73
|
scheduled,
|
|
74
|
-
date,
|
|
74
|
+
scheduledDate: date,
|
|
75
75
|
slotNumber,
|
|
76
|
-
|
|
77
|
-
timeEnd: timeRange.end,
|
|
76
|
+
time: timeRange,
|
|
78
77
|
subject: parsed.subject,
|
|
79
78
|
type: parsed.type,
|
|
80
79
|
teacher: parseTeacher(parsed.teacherRaw),
|