chuvsu-js 2.8.2 → 3.0.1
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 +3 -2
- package/dist/browser.d.ts +1 -5
- package/dist/browser.js +1 -2
- package/dist/index.d.ts +1 -8
- package/dist/index.js +2 -3
- package/dist/shared.d.ts +9 -0
- package/dist/shared.js +7 -0
- package/dist/tt/client.js +1 -1
- package/dist/tt/parse/audience.d.ts +3 -0
- package/dist/tt/parse/audience.js +150 -0
- package/dist/tt/parse/full-schedule.d.ts +4 -0
- package/dist/tt/parse/full-schedule.js +190 -0
- package/dist/tt/parse/groups.d.ts +15 -0
- package/dist/tt/parse/groups.js +28 -0
- package/dist/tt/parse/index.d.ts +5 -0
- package/dist/tt/parse/index.js +5 -0
- package/dist/tt/parse/lists.d.ts +11 -0
- package/dist/tt/parse/lists.js +80 -0
- package/dist/tt/parse/overlays.d.ts +10 -0
- package/dist/tt/parse/overlays.js +104 -0
- package/dist/tt/parse/teacher.d.ts +4 -0
- package/dist/tt/parse/teacher.js +166 -0
- package/dist/tt/schedule.d.ts +1 -2
- package/dist/tt/schedule.js +2 -8
- package/dist/tt/types.d.ts +9 -4
- package/dist/tt/utils/date.d.ts +5 -0
- package/dist/tt/utils/date.js +27 -0
- package/dist/tt/{utils.d.ts → utils/holidays.d.ts} +5 -55
- package/dist/tt/utils/holidays.js +197 -0
- package/dist/tt/utils/index.d.ts +7 -0
- package/dist/tt/utils/index.js +6 -0
- package/dist/tt/utils/lessons.d.ts +14 -0
- package/dist/tt/utils/lessons.js +123 -0
- package/dist/tt/utils/period.d.ts +6 -0
- package/dist/tt/utils/period.js +24 -0
- package/dist/tt/utils/semester.d.ts +30 -0
- package/dist/tt/utils/semester.js +64 -0
- package/dist/tt/utils/time-slots.d.ts +5 -0
- package/dist/tt/utils/time-slots.js +41 -0
- package/package.json +1 -1
- package/dist/tt/parse.d.ts +0 -16
- package/dist/tt/parse.js +0 -671
- package/dist/tt/utils.js +0 -521
package/dist/tt/parse.js
DELETED
|
@@ -1,671 +0,0 @@
|
|
|
1
|
-
import { parseHtml, text, parseTime, parseWeeks, parseTeacher, parseWeekParity, } from "../common/parse.js";
|
|
2
|
-
import { getLessonNumber } from "./utils.js";
|
|
3
|
-
const PERIOD_LABELS = {
|
|
4
|
-
"осенний семестр": 1,
|
|
5
|
-
"зимняя сессия": 2,
|
|
6
|
-
"весенний семестр": 3,
|
|
7
|
-
"летняя сессия": 4,
|
|
8
|
-
};
|
|
9
|
-
export function parsePeriodFromPage(html) {
|
|
10
|
-
const match = html.match(/идет\s+(.+?)\s*</i);
|
|
11
|
-
if (!match)
|
|
12
|
-
return null;
|
|
13
|
-
const label = match[1].toLowerCase().trim();
|
|
14
|
-
return PERIOD_LABELS[label] ?? null;
|
|
15
|
-
}
|
|
16
|
-
export function parseGroupButtons(html) {
|
|
17
|
-
const doc = parseHtml(html);
|
|
18
|
-
const groups = [];
|
|
19
|
-
for (const btn of doc.querySelectorAll("button[id^='gr']")) {
|
|
20
|
-
const onclick = btn.getAttribute("onClick") ?? "";
|
|
21
|
-
const idMatch = onclick.match(/val\((\d+)\)/);
|
|
22
|
-
if (idMatch) {
|
|
23
|
-
groups.push({
|
|
24
|
-
id: parseInt(idMatch[1]),
|
|
25
|
-
name: btn.getAttribute("value") ?? text(btn),
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return groups;
|
|
30
|
-
}
|
|
31
|
-
export function parseFacultyButtons(html) {
|
|
32
|
-
const doc = parseHtml(html);
|
|
33
|
-
const faculties = [];
|
|
34
|
-
for (const btn of doc.querySelectorAll(".facbut")) {
|
|
35
|
-
const onclick = btn.getAttribute("onClick") ?? "";
|
|
36
|
-
const idMatch = onclick.match(/val\((\d+)\)/);
|
|
37
|
-
if (idMatch) {
|
|
38
|
-
faculties.push({ id: parseInt(idMatch[1]), name: text(btn) });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return faculties;
|
|
42
|
-
}
|
|
43
|
-
export function parseAudienceButtons(html) {
|
|
44
|
-
const results = [];
|
|
45
|
-
const seen = new Set();
|
|
46
|
-
const re = /<button[^>]*\bname="aud(\d+)"[^>]*\bvalue="([^"]*)"/g;
|
|
47
|
-
let m;
|
|
48
|
-
while ((m = re.exec(html)) !== null) {
|
|
49
|
-
const id = parseInt(m[1]);
|
|
50
|
-
if (seen.has(id))
|
|
51
|
-
continue;
|
|
52
|
-
seen.add(id);
|
|
53
|
-
results.push({ id, name: m[2] });
|
|
54
|
-
}
|
|
55
|
-
return results;
|
|
56
|
-
}
|
|
57
|
-
export function parseAudienceName(html) {
|
|
58
|
-
const m = html.match(/id="path"[\s\S]*?findaud[^>]*>[^<]*<\/a>([\s\S]*?)<\/div>/);
|
|
59
|
-
if (!m)
|
|
60
|
-
return null;
|
|
61
|
-
const tail = m[1]
|
|
62
|
-
.replace(/ /g, " ")
|
|
63
|
-
.replace(/<[^>]*>/g, "")
|
|
64
|
-
.replace(/^[\s/]+/, "")
|
|
65
|
-
.trim();
|
|
66
|
-
return tail || null;
|
|
67
|
-
}
|
|
68
|
-
export function parseTeacherButtons(html) {
|
|
69
|
-
const doc = parseHtml(html);
|
|
70
|
-
const results = [];
|
|
71
|
-
for (const btn of doc.querySelectorAll(".techbut")) {
|
|
72
|
-
const onclick = btn.getAttribute("onClick") ?? "";
|
|
73
|
-
const idMatch = onclick.match(/val\((\d+)\)/);
|
|
74
|
-
if (idMatch) {
|
|
75
|
-
results.push({
|
|
76
|
-
id: parseInt(idMatch[1]),
|
|
77
|
-
name: btn.getAttribute("value") ?? text(btn),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return results;
|
|
82
|
-
}
|
|
83
|
-
export function parseFullSchedule(html, educationType) {
|
|
84
|
-
const doc = parseHtml(html);
|
|
85
|
-
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
86
|
-
// Session layout has date-based cells with ids like "trd20251224"
|
|
87
|
-
if (doc.querySelector('td[id^="trd2"]')) {
|
|
88
|
-
return parseSessionSchedule(doc, edType);
|
|
89
|
-
}
|
|
90
|
-
return parseSemesterSchedule(doc);
|
|
91
|
-
}
|
|
92
|
-
// --- Semester schedule parsing (weekday-based, repeating weekly) ---
|
|
93
|
-
function parseSemesterScheduleWith(doc, entryParser) {
|
|
94
|
-
const days = [];
|
|
95
|
-
const rows = doc.querySelectorAll("tr");
|
|
96
|
-
let currentDay = null;
|
|
97
|
-
for (const row of rows) {
|
|
98
|
-
const style = row.getAttribute("style") ?? "";
|
|
99
|
-
const cls = row.getAttribute("class") ?? "";
|
|
100
|
-
if (style.includes("lightgray") && cls.includes("trfd")) {
|
|
101
|
-
const dayName = text(row.querySelector("td"));
|
|
102
|
-
if (dayName) {
|
|
103
|
-
currentDay = { weekday: dayName, slots: [] };
|
|
104
|
-
days.push(currentDay);
|
|
105
|
-
}
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (!currentDay)
|
|
109
|
-
continue;
|
|
110
|
-
const timeCell = row.querySelector("td.trf");
|
|
111
|
-
const dataCell = row.querySelector("td.trdata:not(.trf)");
|
|
112
|
-
if (!timeCell || !dataCell)
|
|
113
|
-
continue;
|
|
114
|
-
const timeDiv = timeCell.querySelector(".trfd");
|
|
115
|
-
if (!timeDiv)
|
|
116
|
-
continue;
|
|
117
|
-
const timeText = text(timeDiv);
|
|
118
|
-
const numberMatch = timeText.match(/(\d+)\s*пара/);
|
|
119
|
-
const timeMatch = timeText.match(/\((\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})\)/);
|
|
120
|
-
if (!numberMatch)
|
|
121
|
-
continue;
|
|
122
|
-
const entries = [];
|
|
123
|
-
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
124
|
-
const entry = entryParser(entryRow);
|
|
125
|
-
if (entry)
|
|
126
|
-
entries.push(entry);
|
|
127
|
-
}
|
|
128
|
-
currentDay.slots.push({
|
|
129
|
-
number: parseInt(numberMatch[1]),
|
|
130
|
-
timeStart: parseTime(timeMatch?.[1] ?? "00:00"),
|
|
131
|
-
timeEnd: parseTime(timeMatch?.[2] ?? "00:00"),
|
|
132
|
-
entries,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
return days;
|
|
136
|
-
}
|
|
137
|
-
function parseSemesterSchedule(doc) {
|
|
138
|
-
return parseSemesterScheduleWith(doc, parseSemesterEntry);
|
|
139
|
-
}
|
|
140
|
-
function parseDate(dd, mm, yyyy) {
|
|
141
|
-
return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
142
|
-
}
|
|
143
|
-
function parseTransferDiv(div) {
|
|
144
|
-
const divText = text(div);
|
|
145
|
-
const divHtml = div.innerHTML ?? "";
|
|
146
|
-
const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*перенос\s*c\s*(\d{2})\.(\d{2})\.(\d{4})\s*\((\d+)\s*пара\)/);
|
|
147
|
-
if (!m)
|
|
148
|
-
return null;
|
|
149
|
-
const targetDate = parseDate(m[1], m[2], m[3]);
|
|
150
|
-
const fromDate = parseDate(m[4], m[5], m[6]);
|
|
151
|
-
const fromSlot = parseInt(m[7]);
|
|
152
|
-
const subjectEl = div.querySelector('span[style*="color: blue"]');
|
|
153
|
-
const subject = subjectEl ? text(subjectEl) : "";
|
|
154
|
-
if (!subject)
|
|
155
|
-
return null;
|
|
156
|
-
const roomMatch = divHtml.match(/([А-Яа-яA-Za-z]-\d+)/);
|
|
157
|
-
const typeMatch = divText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
158
|
-
// Teacher: last text line that isn't a subgroup marker
|
|
159
|
-
const parts = divHtml.split(/<br\s*\/?>/);
|
|
160
|
-
let teacherPart = "";
|
|
161
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
162
|
-
const clean = parts[i].replace(/<[^>]*>/g, "").trim();
|
|
163
|
-
if (clean && !/подгруппа/.test(clean)) {
|
|
164
|
-
teacherPart = clean;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const transfer = { targetDate, fromDate, fromSlot, subject };
|
|
169
|
-
const subgroupMatch = divText.match(/(\d+)\s*подгруппа/);
|
|
170
|
-
return {
|
|
171
|
-
transfer,
|
|
172
|
-
entry: {
|
|
173
|
-
room: roomMatch?.[1] ?? "",
|
|
174
|
-
subject,
|
|
175
|
-
type: typeMatch?.[1] ?? "",
|
|
176
|
-
weeks: { from: 0, to: 0 },
|
|
177
|
-
teacher: parseTeacher(teacherPart),
|
|
178
|
-
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
179
|
-
transfer,
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
function parseSubstitutionDiv(div) {
|
|
184
|
-
const divText = text(div);
|
|
185
|
-
const divHtml = div.innerHTML ?? "";
|
|
186
|
-
const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*замена\s*на:/);
|
|
187
|
-
if (!m)
|
|
188
|
-
return null;
|
|
189
|
-
const date = parseDate(m[1], m[2], m[3]);
|
|
190
|
-
let room;
|
|
191
|
-
let teacher;
|
|
192
|
-
const roomMatch = divHtml.match(/Аудитория:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
193
|
-
if (roomMatch)
|
|
194
|
-
room = roomMatch[1].trim();
|
|
195
|
-
const teacherMatch = divHtml.match(/Преподаватель:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
196
|
-
if (teacherMatch)
|
|
197
|
-
teacher = parseTeacher(teacherMatch[1].trim());
|
|
198
|
-
return { date, room, teacher };
|
|
199
|
-
}
|
|
200
|
-
function parseSubstituteForDiv(div) {
|
|
201
|
-
const divText = text(div);
|
|
202
|
-
const divHtml = div.innerHTML ?? "";
|
|
203
|
-
const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*замена\s*вместо:/);
|
|
204
|
-
if (!m)
|
|
205
|
-
return null;
|
|
206
|
-
const date = parseDate(m[1], m[2], m[3]);
|
|
207
|
-
// Original teacher: first blue span (right after "замена вместо:")
|
|
208
|
-
const origTeacherMatch = divHtml.match(/замена\s*вместо:\s*<\/b><\/span>\s*<span[^>]*>([^<]+)<\/span>/);
|
|
209
|
-
const originalTeacher = origTeacherMatch
|
|
210
|
-
? parseTeacher(origTeacherMatch[1].trim())
|
|
211
|
-
: { name: "" };
|
|
212
|
-
// Subject: second blue span
|
|
213
|
-
const subjectEl = div.querySelectorAll('span[style*="color: blue"]');
|
|
214
|
-
let subject = "";
|
|
215
|
-
for (const el of subjectEl) {
|
|
216
|
-
const t = text(el);
|
|
217
|
-
if (t && t !== origTeacherMatch?.[1]?.trim()) {
|
|
218
|
-
subject = t;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (!subject)
|
|
223
|
-
return null;
|
|
224
|
-
const roomMatch = divHtml.match(/(?:<br\s*\/?>)\s*([А-Яа-яA-Za-z]-\d+)/);
|
|
225
|
-
const typeMatch = divText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
226
|
-
const groupsMatch = divHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс)\)\s*(?:<br\s*\/?>)\s*([^<]+?)(?:\s*<i|$)/);
|
|
227
|
-
const subgroupMatch = divText.match(/(\d+)\s*подгруппа/);
|
|
228
|
-
return {
|
|
229
|
-
entry: {
|
|
230
|
-
room: roomMatch?.[1] ?? "",
|
|
231
|
-
subject,
|
|
232
|
-
type: typeMatch?.[1] ?? "",
|
|
233
|
-
weeks: { from: 0, to: 0 },
|
|
234
|
-
teacher: { name: "" },
|
|
235
|
-
groups: groupsMatch?.[1]?.trim() ?? "",
|
|
236
|
-
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
237
|
-
substituteFor: { date, originalTeacher },
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function parseSemesterEntry(el) {
|
|
242
|
-
const td = el.querySelector("td") ?? el;
|
|
243
|
-
const fullHtml = td.innerHTML ?? "";
|
|
244
|
-
const plainText = text(td);
|
|
245
|
-
if (!plainText)
|
|
246
|
-
return null;
|
|
247
|
-
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
248
|
-
// Detect red-bordered divs (transfers / substitutions)
|
|
249
|
-
const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
|
|
250
|
-
// Check for transfer (перенос) — the whole entry is the transferred lesson
|
|
251
|
-
for (const div of redDivs) {
|
|
252
|
-
const result = parseTransferDiv(div);
|
|
253
|
-
if (result) {
|
|
254
|
-
if (possibleChanges)
|
|
255
|
-
result.entry.possibleChanges = true;
|
|
256
|
-
return result.entry;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// Collect substitutions (замена на)
|
|
260
|
-
const substitutions = [];
|
|
261
|
-
for (const div of redDivs) {
|
|
262
|
-
const sub = parseSubstitutionDiv(div);
|
|
263
|
-
if (sub)
|
|
264
|
-
substitutions.push(sub);
|
|
265
|
-
}
|
|
266
|
-
// Strip red divs from HTML/text before parsing the regular entry
|
|
267
|
-
let cleanHtml = fullHtml;
|
|
268
|
-
let cleanText = plainText;
|
|
269
|
-
for (const div of redDivs) {
|
|
270
|
-
cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
|
|
271
|
-
cleanText = cleanText.replace(text(div), "");
|
|
272
|
-
}
|
|
273
|
-
// Parse regular entry
|
|
274
|
-
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
275
|
-
const subject = subjectEl ? text(subjectEl) : "";
|
|
276
|
-
if (!subject)
|
|
277
|
-
return null;
|
|
278
|
-
const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
279
|
-
const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
|
|
280
|
-
const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
|
|
281
|
-
const teacherMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
|
|
282
|
-
const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
|
|
283
|
-
const weekParity = parseWeekParity(cleanHtml);
|
|
284
|
-
return {
|
|
285
|
-
room: roomMatch?.[1] ?? "",
|
|
286
|
-
subject,
|
|
287
|
-
type: typeMatch?.[1] ?? "",
|
|
288
|
-
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
289
|
-
teacher: parseTeacher(teacherMatch?.[1] ?? ""),
|
|
290
|
-
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
291
|
-
weekParity,
|
|
292
|
-
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
293
|
-
possibleChanges,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
// --- Session schedule parsing (date-based, specific dates) ---
|
|
297
|
-
function parseSessionSchedule(doc, educationType) {
|
|
298
|
-
const days = [];
|
|
299
|
-
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
300
|
-
// Parse date from cell id: trd20251224 -> 2025-12-24
|
|
301
|
-
const id = dateCell.getAttribute("id") ?? "";
|
|
302
|
-
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
303
|
-
if (!dateMatch)
|
|
304
|
-
continue;
|
|
305
|
-
const year = parseInt(dateMatch[1]);
|
|
306
|
-
const month = parseInt(dateMatch[2]) - 1;
|
|
307
|
-
const dayNum = parseInt(dateMatch[3]);
|
|
308
|
-
const date = new Date(year, month, dayNum);
|
|
309
|
-
// Extract weekday from after <br>
|
|
310
|
-
const cellHtml = dateCell.innerHTML ?? "";
|
|
311
|
-
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
312
|
-
const weekday = brMatch ? brMatch[1].trim() : "";
|
|
313
|
-
// Data cell is the next td.trdata sibling in the same row
|
|
314
|
-
const row = dateCell.parentElement;
|
|
315
|
-
if (!row)
|
|
316
|
-
continue;
|
|
317
|
-
const dataCell = row.querySelector("td.trdata:not(.trfd)");
|
|
318
|
-
if (!dataCell)
|
|
319
|
-
continue;
|
|
320
|
-
// Parse entries
|
|
321
|
-
const slots = [];
|
|
322
|
-
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
323
|
-
const td = entryRow.querySelector("td") ?? entryRow;
|
|
324
|
-
const entry = parseSessionEntry(td);
|
|
325
|
-
if (!entry)
|
|
326
|
-
continue;
|
|
327
|
-
slots.push({
|
|
328
|
-
number: getLessonNumber(entry.timeStart, educationType),
|
|
329
|
-
timeStart: entry.timeStart,
|
|
330
|
-
timeEnd: entry.timeEnd,
|
|
331
|
-
entries: [entry.entry],
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
if (slots.length > 0) {
|
|
335
|
-
days.push({ weekday, date, slots });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return days;
|
|
339
|
-
}
|
|
340
|
-
function parseSessionEntry(td) {
|
|
341
|
-
const fullHtml = td.innerHTML ?? "";
|
|
342
|
-
const plainText = text(td);
|
|
343
|
-
if (!plainText)
|
|
344
|
-
return null;
|
|
345
|
-
// Subject from blue span
|
|
346
|
-
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
347
|
-
const subject = subjectEl ? text(subjectEl) : "";
|
|
348
|
-
if (!subject)
|
|
349
|
-
return null;
|
|
350
|
-
// Room: text before the first <span
|
|
351
|
-
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
352
|
-
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
353
|
-
// Type: parenthesized text after </span>, case-insensitive
|
|
354
|
-
const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
|
|
355
|
-
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
356
|
-
// Time: after <br>, format HH:MM - HH:MM
|
|
357
|
-
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
358
|
-
if (!timeMatch)
|
|
359
|
-
return null;
|
|
360
|
-
return {
|
|
361
|
-
entry: {
|
|
362
|
-
room,
|
|
363
|
-
subject,
|
|
364
|
-
type,
|
|
365
|
-
weeks: { from: 0, to: 0 },
|
|
366
|
-
teacher: { name: "" },
|
|
367
|
-
},
|
|
368
|
-
timeStart: parseTime(timeMatch[1]),
|
|
369
|
-
timeEnd: parseTime(timeMatch[2]),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
// --- Audience schedule & info parsing ---
|
|
373
|
-
export function parseAudienceInfo(html) {
|
|
374
|
-
const doc = parseHtml(html);
|
|
375
|
-
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
376
|
-
const nameEl = doc.querySelector('.htext span[style*="color: blue"]');
|
|
377
|
-
const name = nameEl ? text(nameEl).trim() : "";
|
|
378
|
-
if (!name)
|
|
379
|
-
return null;
|
|
380
|
-
// Details: <span class="htextb"> (Корпус Б; 3 этаж - Учебная лаборатория)</span>
|
|
381
|
-
const detailsEl = doc.querySelector(".htextb");
|
|
382
|
-
const details = detailsEl ? text(detailsEl).trim() : "";
|
|
383
|
-
let building;
|
|
384
|
-
let floor;
|
|
385
|
-
let usage;
|
|
386
|
-
if (details) {
|
|
387
|
-
const buildingMatch = details.match(/Корпус\s+([^\s;,)]+)/i);
|
|
388
|
-
if (buildingMatch)
|
|
389
|
-
building = buildingMatch[1];
|
|
390
|
-
const floorMatch = details.match(/(\d+)\s*этаж/i);
|
|
391
|
-
if (floorMatch)
|
|
392
|
-
floor = parseInt(floorMatch[1]);
|
|
393
|
-
const usageMatch = details.match(/этаж\s*-\s*([^)]+?)\s*\)?\s*$/i);
|
|
394
|
-
if (usageMatch)
|
|
395
|
-
usage = usageMatch[1].trim();
|
|
396
|
-
}
|
|
397
|
-
const audImg = doc.querySelector("#audsrc");
|
|
398
|
-
const blockImg = doc.querySelector("#blocksrc");
|
|
399
|
-
const floorImg = doc.querySelector("#floorsrc");
|
|
400
|
-
// Highlight rect from the image map: prefer the <area> whose id matches
|
|
401
|
-
// the current audience (planaudNNNN); fall back to the first rect area.
|
|
402
|
-
let floorplanRect;
|
|
403
|
-
const areas = doc.querySelectorAll('map[name="flooraud"] area[shape="rect"]');
|
|
404
|
-
let chosen = undefined;
|
|
405
|
-
for (const a of areas) {
|
|
406
|
-
if (a.getAttribute("alt")?.trim() === name) {
|
|
407
|
-
chosen = a;
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (!chosen && areas.length > 0)
|
|
412
|
-
chosen = areas[0];
|
|
413
|
-
if (chosen) {
|
|
414
|
-
const coords = chosen.getAttribute("coords") ?? "";
|
|
415
|
-
const parts = coords.split(",").map((s) => parseInt(s.trim(), 10));
|
|
416
|
-
if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
|
|
417
|
-
floorplanRect = { x1: parts[0], y1: parts[1], x2: parts[2], y2: parts[3] };
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
name,
|
|
422
|
-
building,
|
|
423
|
-
floor,
|
|
424
|
-
usage,
|
|
425
|
-
audImageUrl: audImg?.getAttribute("src") || undefined,
|
|
426
|
-
blockImageUrl: blockImg?.getAttribute("src") || undefined,
|
|
427
|
-
floorplanUrl: floorImg?.getAttribute("src") || undefined,
|
|
428
|
-
floorplanRect,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
function parseAudienceSemesterEntry(el) {
|
|
432
|
-
const td = el.querySelector("td") ?? el;
|
|
433
|
-
const fullHtml = td.innerHTML ?? "";
|
|
434
|
-
const plainText = text(td);
|
|
435
|
-
if (!plainText)
|
|
436
|
-
return null;
|
|
437
|
-
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
438
|
-
const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
|
|
439
|
-
for (const div of redDivs) {
|
|
440
|
-
const result = parseTransferDiv(div);
|
|
441
|
-
if (result) {
|
|
442
|
-
if (possibleChanges)
|
|
443
|
-
result.entry.possibleChanges = true;
|
|
444
|
-
return result.entry;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
for (const div of redDivs) {
|
|
448
|
-
const result = parseSubstituteForDiv(div);
|
|
449
|
-
if (result) {
|
|
450
|
-
if (possibleChanges)
|
|
451
|
-
result.entry.possibleChanges = true;
|
|
452
|
-
return result.entry;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
const substitutions = [];
|
|
456
|
-
for (const div of redDivs) {
|
|
457
|
-
const sub = parseSubstitutionDiv(div);
|
|
458
|
-
if (sub)
|
|
459
|
-
substitutions.push(sub);
|
|
460
|
-
}
|
|
461
|
-
let cleanHtml = fullHtml;
|
|
462
|
-
let cleanText = plainText;
|
|
463
|
-
for (const div of redDivs) {
|
|
464
|
-
cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
|
|
465
|
-
cleanText = cleanText.replace(text(div), "");
|
|
466
|
-
}
|
|
467
|
-
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
468
|
-
const subject = subjectEl ? text(subjectEl) : "";
|
|
469
|
-
if (!subject)
|
|
470
|
-
return null;
|
|
471
|
-
const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
472
|
-
const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
|
|
473
|
-
const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
|
|
474
|
-
const weekParity = parseWeekParity(cleanHtml);
|
|
475
|
-
// Audience entries layout:
|
|
476
|
-
// <span blue>SUBJ</span> (TYPE) (WEEKS) <br>TEACHER<br>GROUPS
|
|
477
|
-
// Teacher is the first line after </span>...<br>, groups is the next line.
|
|
478
|
-
// We split on <br> after the blue subject span.
|
|
479
|
-
const afterSubject = cleanHtml.split(/<\/span>/i).slice(1).join("</span>");
|
|
480
|
-
const parts = afterSubject
|
|
481
|
-
.split(/<br\s*\/?>/i)
|
|
482
|
-
.map((p) => p.replace(/<[^>]*>/g, "").trim())
|
|
483
|
-
.filter((p) => p.length > 0);
|
|
484
|
-
// parts[0] = " (лк) (1 - 16 нед.) " — trailing metadata; drop tokens that
|
|
485
|
-
// look like (type)/(weeks)/(N подгруппа). First real text line = teacher.
|
|
486
|
-
const textLines = [];
|
|
487
|
-
for (const p of parts) {
|
|
488
|
-
const cleaned = p
|
|
489
|
-
.replace(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/g, "")
|
|
490
|
-
.replace(/\([^)]*нед\.?[^)]*\)/g, "")
|
|
491
|
-
.replace(/\(\d+\s*подгруппа\)/g, "")
|
|
492
|
-
.trim();
|
|
493
|
-
if (cleaned)
|
|
494
|
-
textLines.push(cleaned);
|
|
495
|
-
}
|
|
496
|
-
const teacherLine = textLines[0] ?? "";
|
|
497
|
-
const groupsLine = textLines.slice(1).join(" ").trim();
|
|
498
|
-
return {
|
|
499
|
-
room: "",
|
|
500
|
-
subject,
|
|
501
|
-
type: typeMatch?.[1] ?? "",
|
|
502
|
-
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
503
|
-
teacher: parseTeacher(teacherLine),
|
|
504
|
-
groups: groupsLine || undefined,
|
|
505
|
-
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
506
|
-
weekParity,
|
|
507
|
-
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
508
|
-
possibleChanges,
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
export function parseAudienceFullSchedule(html) {
|
|
512
|
-
const doc = parseHtml(html);
|
|
513
|
-
return parseSemesterScheduleWith(doc, parseAudienceSemesterEntry);
|
|
514
|
-
}
|
|
515
|
-
// --- Teacher schedule parsing ---
|
|
516
|
-
export function parseTeacherFullSchedule(html, educationType) {
|
|
517
|
-
const doc = parseHtml(html);
|
|
518
|
-
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
519
|
-
if (doc.querySelector('td[id^="trd2"]')) {
|
|
520
|
-
return parseTeacherSessionSchedule(doc, edType);
|
|
521
|
-
}
|
|
522
|
-
return parseSemesterScheduleWith(doc, parseTeacherSemesterEntry);
|
|
523
|
-
}
|
|
524
|
-
function parseTeacherSemesterEntry(el) {
|
|
525
|
-
const td = el.querySelector("td") ?? el;
|
|
526
|
-
const fullHtml = td.innerHTML ?? "";
|
|
527
|
-
const plainText = text(td);
|
|
528
|
-
if (!plainText)
|
|
529
|
-
return null;
|
|
530
|
-
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
531
|
-
const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
|
|
532
|
-
for (const div of redDivs) {
|
|
533
|
-
const result = parseTransferDiv(div);
|
|
534
|
-
if (result) {
|
|
535
|
-
if (possibleChanges)
|
|
536
|
-
result.entry.possibleChanges = true;
|
|
537
|
-
return result.entry;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
// Check for "замена вместо:" (substitute lesson for another teacher)
|
|
541
|
-
for (const div of redDivs) {
|
|
542
|
-
const result = parseSubstituteForDiv(div);
|
|
543
|
-
if (result) {
|
|
544
|
-
if (possibleChanges)
|
|
545
|
-
result.entry.possibleChanges = true;
|
|
546
|
-
return result.entry;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
const substitutions = [];
|
|
550
|
-
for (const div of redDivs) {
|
|
551
|
-
const sub = parseSubstitutionDiv(div);
|
|
552
|
-
if (sub)
|
|
553
|
-
substitutions.push(sub);
|
|
554
|
-
}
|
|
555
|
-
let cleanHtml = fullHtml;
|
|
556
|
-
let cleanText = plainText;
|
|
557
|
-
for (const div of redDivs) {
|
|
558
|
-
cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
|
|
559
|
-
cleanText = cleanText.replace(text(div), "");
|
|
560
|
-
}
|
|
561
|
-
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
562
|
-
const subject = subjectEl ? text(subjectEl) : "";
|
|
563
|
-
if (!subject)
|
|
564
|
-
return null;
|
|
565
|
-
const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
566
|
-
const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
|
|
567
|
-
const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
|
|
568
|
-
const groupsMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
|
|
569
|
-
const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
|
|
570
|
-
const weekParity = parseWeekParity(cleanHtml);
|
|
571
|
-
return {
|
|
572
|
-
room: roomMatch?.[1] ?? "",
|
|
573
|
-
subject,
|
|
574
|
-
type: typeMatch?.[1] ?? "",
|
|
575
|
-
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
576
|
-
teacher: { name: "" },
|
|
577
|
-
groups: groupsMatch?.[1]?.trim().replace(/\s*\(\d+\s*подгруппа\)/, "") ?? "",
|
|
578
|
-
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
579
|
-
weekParity,
|
|
580
|
-
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
581
|
-
possibleChanges,
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
function parseTeacherSessionSchedule(doc, educationType) {
|
|
585
|
-
const days = [];
|
|
586
|
-
for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
|
|
587
|
-
const id = dateCell.getAttribute("id") ?? "";
|
|
588
|
-
const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
|
|
589
|
-
if (!dateMatch)
|
|
590
|
-
continue;
|
|
591
|
-
const year = parseInt(dateMatch[1]);
|
|
592
|
-
const month = parseInt(dateMatch[2]) - 1;
|
|
593
|
-
const dayNum = parseInt(dateMatch[3]);
|
|
594
|
-
const date = new Date(year, month, dayNum);
|
|
595
|
-
const cellHtml = dateCell.innerHTML ?? "";
|
|
596
|
-
const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
|
|
597
|
-
const weekday = brMatch ? brMatch[1].trim() : "";
|
|
598
|
-
const row = dateCell.parentElement;
|
|
599
|
-
if (!row)
|
|
600
|
-
continue;
|
|
601
|
-
const dataCell = row.querySelector("td.trdata:not(.trfd)");
|
|
602
|
-
if (!dataCell)
|
|
603
|
-
continue;
|
|
604
|
-
const slots = [];
|
|
605
|
-
for (const entryRow of dataCell.querySelectorAll("table tr")) {
|
|
606
|
-
const td = entryRow.querySelector("td") ?? entryRow;
|
|
607
|
-
const entry = parseTeacherSessionEntry(td);
|
|
608
|
-
if (!entry)
|
|
609
|
-
continue;
|
|
610
|
-
slots.push({
|
|
611
|
-
number: getLessonNumber(entry.timeStart, educationType),
|
|
612
|
-
timeStart: entry.timeStart,
|
|
613
|
-
timeEnd: entry.timeEnd,
|
|
614
|
-
entries: [entry.entry],
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
if (slots.length > 0) {
|
|
618
|
-
days.push({ weekday, date, slots });
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return days;
|
|
622
|
-
}
|
|
623
|
-
function parseTeacherSessionEntry(td) {
|
|
624
|
-
const fullHtml = td.innerHTML ?? "";
|
|
625
|
-
const plainText = text(td);
|
|
626
|
-
if (!plainText)
|
|
627
|
-
return null;
|
|
628
|
-
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
629
|
-
const subject = subjectEl ? text(subjectEl) : "";
|
|
630
|
-
if (!subject)
|
|
631
|
-
return null;
|
|
632
|
-
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
633
|
-
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
634
|
-
const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
|
|
635
|
-
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
636
|
-
// Groups: text between </span> type and <br>time
|
|
637
|
-
const groupsMatch = fullHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)\s*([^<]+?)\s*<br/i);
|
|
638
|
-
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
639
|
-
if (!timeMatch)
|
|
640
|
-
return null;
|
|
641
|
-
return {
|
|
642
|
-
entry: {
|
|
643
|
-
room,
|
|
644
|
-
subject,
|
|
645
|
-
type,
|
|
646
|
-
weeks: { from: 0, to: 0 },
|
|
647
|
-
teacher: { name: "" },
|
|
648
|
-
groups: groupsMatch?.[1]?.trim() ?? "",
|
|
649
|
-
},
|
|
650
|
-
timeStart: parseTime(timeMatch[1]),
|
|
651
|
-
timeEnd: parseTime(timeMatch[2]),
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
export function parseTeacherInfo(html) {
|
|
655
|
-
const doc = parseHtml(html);
|
|
656
|
-
const nameEl = doc.querySelector(".htextb");
|
|
657
|
-
if (!nameEl)
|
|
658
|
-
return null;
|
|
659
|
-
const nameHtml = nameEl.innerHTML ?? "";
|
|
660
|
-
const nameMatch = nameHtml.match(/^([^<]+)/);
|
|
661
|
-
const name = nameMatch?.[1]?.trim() ?? "";
|
|
662
|
-
if (!name)
|
|
663
|
-
return null;
|
|
664
|
-
const degreeEl = nameEl.querySelector('span[style*="color: blue"]');
|
|
665
|
-
const degree = degreeEl ? text(degreeEl).trim() : undefined;
|
|
666
|
-
const deptEl = doc.querySelector(".htext");
|
|
667
|
-
const department = deptEl ? text(deptEl).trim() : undefined;
|
|
668
|
-
const photoImg = doc.querySelector("#photosrc");
|
|
669
|
-
const photoUrl = photoImg?.getAttribute("src") || undefined;
|
|
670
|
-
return { name, degree: degree || undefined, department: department || undefined, photoUrl };
|
|
671
|
-
}
|