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.
Files changed (43) hide show
  1. package/README.md +3 -2
  2. package/dist/browser.d.ts +1 -5
  3. package/dist/browser.js +1 -2
  4. package/dist/index.d.ts +1 -8
  5. package/dist/index.js +2 -3
  6. package/dist/shared.d.ts +9 -0
  7. package/dist/shared.js +7 -0
  8. package/dist/tt/client.js +1 -1
  9. package/dist/tt/parse/audience.d.ts +3 -0
  10. package/dist/tt/parse/audience.js +150 -0
  11. package/dist/tt/parse/full-schedule.d.ts +4 -0
  12. package/dist/tt/parse/full-schedule.js +190 -0
  13. package/dist/tt/parse/groups.d.ts +15 -0
  14. package/dist/tt/parse/groups.js +28 -0
  15. package/dist/tt/parse/index.d.ts +5 -0
  16. package/dist/tt/parse/index.js +5 -0
  17. package/dist/tt/parse/lists.d.ts +11 -0
  18. package/dist/tt/parse/lists.js +80 -0
  19. package/dist/tt/parse/overlays.d.ts +10 -0
  20. package/dist/tt/parse/overlays.js +104 -0
  21. package/dist/tt/parse/teacher.d.ts +4 -0
  22. package/dist/tt/parse/teacher.js +166 -0
  23. package/dist/tt/schedule.d.ts +1 -2
  24. package/dist/tt/schedule.js +2 -8
  25. package/dist/tt/types.d.ts +9 -4
  26. package/dist/tt/utils/date.d.ts +5 -0
  27. package/dist/tt/utils/date.js +27 -0
  28. package/dist/tt/{utils.d.ts → utils/holidays.d.ts} +5 -55
  29. package/dist/tt/utils/holidays.js +197 -0
  30. package/dist/tt/utils/index.d.ts +7 -0
  31. package/dist/tt/utils/index.js +6 -0
  32. package/dist/tt/utils/lessons.d.ts +14 -0
  33. package/dist/tt/utils/lessons.js +123 -0
  34. package/dist/tt/utils/period.d.ts +6 -0
  35. package/dist/tt/utils/period.js +24 -0
  36. package/dist/tt/utils/semester.d.ts +30 -0
  37. package/dist/tt/utils/semester.js +64 -0
  38. package/dist/tt/utils/time-slots.d.ts +5 -0
  39. package/dist/tt/utils/time-slots.js +41 -0
  40. package/package.json +1 -1
  41. package/dist/tt/parse.d.ts +0 -16
  42. package/dist/tt/parse.js +0 -671
  43. package/dist/tt/utils.js +0 -521
package/README.md CHANGED
@@ -6,8 +6,9 @@ Node.js библиотека для работы с порталами ЧувГ
6
6
  - **lk.chuvsu.ru** — личный кабинет студента (персональные данные)
7
7
 
8
8
  > [!WARNING]
9
- > Пока не доработана, код и архитектура говно и надо бы его 10 раз переписать.
10
- > Не надейтесь на правильный вывод расписания, но впринципе я не замечал пока расхождений.
9
+ > Пока не доработана, код и архитектура говно (написано Claude) и надо бы его 10 раз переписать.
10
+ > Не надейтесь на правильный вывод расписания, возможны расхождения но я их фикшу оперативно,
11
+ > как появляется больше информации.
11
12
 
12
13
  ## Установка
13
14
 
package/dist/browser.d.ts CHANGED
@@ -1,5 +1 @@
1
- export { Schedule } from "./tt/schedule.js";
2
- export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, } from "./tt/utils.js";
3
- export { Period, EducationType } from "./common/types.js";
4
- export type { Time, WeekRange, Teacher } from "./common/types.js";
5
- export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
1
+ export * from "./shared.js";
package/dist/browser.js CHANGED
@@ -1,2 +1 @@
1
- export { Schedule } from "./tt/schedule.js";
2
- export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, } from "./tt/utils.js";
1
+ export * from "./shared.js";
package/dist/index.d.ts CHANGED
@@ -1,10 +1,3 @@
1
+ export * from "./shared.js";
1
2
  export { LkClient } from "./lk/client.js";
2
3
  export { TtClient } from "./tt/client.js";
3
- export { Schedule } from "./tt/schedule.js";
4
- export type { CacheEntry } from "./common/cache.js";
5
- export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, getEffectiveHolidays, getHolidayTransfers, getCompensatingWorkDays, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
6
- export type { Holiday, HolidayTransfer } from "./tt/utils.js";
7
- export { Period, EducationType, AuthError, ParseError, } from "./common/types.js";
8
- export type { Time, WeekRange, Teacher } from "./common/types.js";
9
- export type { PersonalData } from "./lk/types.js";
10
- export type { Audience, AudienceInfo, Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, SubstituteForInfo, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
+ export * from "./shared.js";
2
+ // Node-only clients (depend on `undici`).
1
3
  export { LkClient } from "./lk/client.js";
2
4
  export { TtClient } from "./tt/client.js";
3
- export { Schedule } from "./tt/schedule.js";
4
- export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, isHoliday, getEffectiveHolidays, getHolidayTransfers, getCompensatingWorkDays, RUSSIAN_HOLIDAYS, } from "./tt/utils.js";
5
- export { AuthError, ParseError, } from "./common/types.js";
@@ -0,0 +1,9 @@
1
+ export { Schedule } from "./tt/schedule.js";
2
+ export { parseGroupsString } from "./tt/parse/index.js";
3
+ export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
4
+ export type { Holiday, HolidayTransfer } from "./tt/utils/index.js";
5
+ export { AuthError, EducationType, ParseError, Period, } from "./common/types.js";
6
+ export type { Teacher, Time, WeekRange } from "./common/types.js";
7
+ export type { CacheEntry } from "./common/cache.js";
8
+ export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, } from "./tt/types.js";
9
+ export type { PersonalData } from "./lk/types.js";
package/dist/shared.js ADDED
@@ -0,0 +1,7 @@
1
+ // Shared exports that work in any environment (Node, browser, Deno).
2
+ // Both ./index.ts and ./browser.ts re-export everything from here and only
3
+ // add their platform-specific extras on top.
4
+ export { Schedule } from "./tt/schedule.js";
5
+ export { parseGroupsString } from "./tt/parse/index.js";
6
+ export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
7
+ export { AuthError, ParseError, } from "./common/types.js";
package/dist/tt/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { HttpClient } from "../common/http.js";
2
2
  import { Cache } from "../common/cache.js";
3
3
  import { AuthError } from "../common/types.js";
4
- import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse.js";
4
+ import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse/index.js";
5
5
  import { Schedule } from "./schedule.js";
6
6
  const BASE = "https://tt.chuvsu.ru";
7
7
  const AUTH_URL = `${BASE}/auth`;
@@ -0,0 +1,3 @@
1
+ import type { AudienceInfo, FullScheduleDay } from "../types.js";
2
+ export declare function parseAudienceInfo(html: string): AudienceInfo | null;
3
+ export declare function parseAudienceFullSchedule(html: string): FullScheduleDay[];
@@ -0,0 +1,150 @@
1
+ import { parseHtml, parseTeacher, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
2
+ import { parseSemesterScheduleWith } from "./full-schedule.js";
3
+ import { parseGroupsString } from "./groups.js";
4
+ import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
5
+ export function parseAudienceInfo(html) {
6
+ const doc = parseHtml(html);
7
+ // Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
8
+ const nameEl = doc.querySelector('.htext span[style*="color: blue"]');
9
+ const name = nameEl ? text(nameEl).trim() : "";
10
+ if (!name)
11
+ return null;
12
+ // Details: <span class="htextb"> (Корпус Б; 3 этаж - Учебная лаборатория)</span>
13
+ const detailsEl = doc.querySelector(".htextb");
14
+ const details = detailsEl ? text(detailsEl).trim() : "";
15
+ let building;
16
+ let floor;
17
+ let usage;
18
+ if (details) {
19
+ const buildingMatch = details.match(/Корпус\s+([^\s;,)]+)/i);
20
+ if (buildingMatch)
21
+ building = buildingMatch[1];
22
+ const floorMatch = details.match(/(\d+)\s*этаж/i);
23
+ if (floorMatch)
24
+ floor = parseInt(floorMatch[1]);
25
+ const usageMatch = details.match(/этаж\s*-\s*([^)]+?)\s*\)?\s*$/i);
26
+ if (usageMatch)
27
+ usage = usageMatch[1].trim();
28
+ }
29
+ const audImg = doc.querySelector("#audsrc");
30
+ const blockImg = doc.querySelector("#blocksrc");
31
+ const floorImg = doc.querySelector("#floorsrc");
32
+ // Highlight rect from the image map: prefer the <area> whose id matches
33
+ // the current audience (planaudNNNN); fall back to the first rect area.
34
+ let floorplanRect;
35
+ const areas = doc.querySelectorAll('map[name="flooraud"] area[shape="rect"]');
36
+ let chosen = undefined;
37
+ for (const a of areas) {
38
+ if (a.getAttribute("alt")?.trim() === name) {
39
+ chosen = a;
40
+ break;
41
+ }
42
+ }
43
+ if (!chosen && areas.length > 0)
44
+ chosen = areas[0];
45
+ if (chosen) {
46
+ const coords = chosen.getAttribute("coords") ?? "";
47
+ const parts = coords.split(",").map((s) => parseInt(s.trim(), 10));
48
+ if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
49
+ floorplanRect = {
50
+ x1: parts[0],
51
+ y1: parts[1],
52
+ x2: parts[2],
53
+ y2: parts[3],
54
+ };
55
+ }
56
+ }
57
+ return {
58
+ name,
59
+ building,
60
+ floor,
61
+ usage,
62
+ audImageUrl: audImg?.getAttribute("src") || undefined,
63
+ blockImageUrl: blockImg?.getAttribute("src") || undefined,
64
+ floorplanUrl: floorImg?.getAttribute("src") || undefined,
65
+ floorplanRect,
66
+ };
67
+ }
68
+ function parseAudienceSemesterEntry(el) {
69
+ const td = el.querySelector("td") ?? el;
70
+ const fullHtml = td.innerHTML ?? "";
71
+ const plainText = text(td);
72
+ if (!plainText)
73
+ return null;
74
+ const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
75
+ const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
76
+ for (const div of redDivs) {
77
+ const result = parseTransferDiv(div);
78
+ if (result) {
79
+ if (possibleChanges)
80
+ result.entry.possibleChanges = true;
81
+ return result.entry;
82
+ }
83
+ }
84
+ for (const div of redDivs) {
85
+ const result = parseSubstituteForDiv(div);
86
+ if (result) {
87
+ if (possibleChanges)
88
+ result.entry.possibleChanges = true;
89
+ return result.entry;
90
+ }
91
+ }
92
+ const substitutions = [];
93
+ for (const div of redDivs) {
94
+ const sub = parseSubstitutionDiv(div);
95
+ if (sub)
96
+ substitutions.push(sub);
97
+ }
98
+ let cleanHtml = fullHtml;
99
+ let cleanText = plainText;
100
+ for (const div of redDivs) {
101
+ cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
102
+ cleanText = cleanText.replace(text(div), "");
103
+ }
104
+ const subjectEl = td.querySelector('span[style*="color: blue"]');
105
+ const subject = subjectEl ? text(subjectEl) : "";
106
+ if (!subject)
107
+ return null;
108
+ const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
109
+ const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
110
+ const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
111
+ const weekParity = parseWeekParity(cleanHtml);
112
+ // Audience entries layout:
113
+ // <span blue>SUBJ</span> (TYPE) (WEEKS) <br>TEACHER<br>GROUPS
114
+ // Teacher is the first line after </span>...<br>, groups is the next line.
115
+ const afterSubject = cleanHtml.split(/<\/span>/i).slice(1).join("</span>");
116
+ const parts = afterSubject
117
+ .split(/<br\s*\/?>/i)
118
+ .map((p) => p.replace(/<[^>]*>/g, "").trim())
119
+ .filter((p) => p.length > 0);
120
+ // parts[0] = " (лк) (1 - 16 нед.) " — trailing metadata; drop tokens that
121
+ // look like (type)/(weeks)/(N подгруппа). First real text line = teacher.
122
+ const textLines = [];
123
+ for (const p of parts) {
124
+ const cleaned = p
125
+ .replace(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/g, "")
126
+ .replace(/\([^)]*нед\.?[^)]*\)/g, "")
127
+ .replace(/\(\d+\s*подгруппа\)/g, "")
128
+ .trim();
129
+ if (cleaned)
130
+ textLines.push(cleaned);
131
+ }
132
+ const teacherLine = textLines[0] ?? "";
133
+ const groupsLine = textLines.slice(1).join(" ").trim();
134
+ return {
135
+ room: "",
136
+ subject,
137
+ type: typeMatch?.[1] ?? "",
138
+ weeks: parseWeeks(weeksMatch?.[1] ?? ""),
139
+ teacher: parseTeacher(teacherLine),
140
+ groups: parseGroupsString(groupsLine),
141
+ subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
142
+ weekParity,
143
+ substitutions: substitutions.length > 0 ? substitutions : undefined,
144
+ possibleChanges,
145
+ };
146
+ }
147
+ export function parseAudienceFullSchedule(html) {
148
+ const doc = parseHtml(html);
149
+ return parseSemesterScheduleWith(doc, parseAudienceSemesterEntry);
150
+ }
@@ -0,0 +1,4 @@
1
+ import { EducationType } from "../../common/types.js";
2
+ import type { FullScheduleDay, ScheduleEntry } from "../types.js";
3
+ export declare function parseFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
4
+ export declare function parseSemesterScheduleWith(doc: Document, entryParser: (el: Element) => ScheduleEntry | null): FullScheduleDay[];
@@ -0,0 +1,190 @@
1
+ import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
2
+ import { getLessonNumber } from "../utils/index.js";
3
+ import { parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
4
+ export function parseFullSchedule(html, educationType) {
5
+ const doc = parseHtml(html);
6
+ const edType = educationType ?? 1 /* EducationType.HigherEducation */;
7
+ // Session layout has date-based cells with ids like "trd20251224"
8
+ if (doc.querySelector('td[id^="trd2"]')) {
9
+ return parseSessionSchedule(doc, edType);
10
+ }
11
+ return parseSemesterSchedule(doc);
12
+ }
13
+ // --- Semester schedule parsing (weekday-based, repeating weekly) ---
14
+ export function parseSemesterScheduleWith(doc, entryParser) {
15
+ const days = [];
16
+ const rows = doc.querySelectorAll("tr");
17
+ let currentDay = null;
18
+ for (const row of rows) {
19
+ const style = row.getAttribute("style") ?? "";
20
+ const cls = row.getAttribute("class") ?? "";
21
+ if (style.includes("lightgray") && cls.includes("trfd")) {
22
+ const dayName = text(row.querySelector("td"));
23
+ if (dayName) {
24
+ currentDay = { weekday: dayName, slots: [] };
25
+ days.push(currentDay);
26
+ }
27
+ continue;
28
+ }
29
+ if (!currentDay)
30
+ continue;
31
+ const timeCell = row.querySelector("td.trf");
32
+ const dataCell = row.querySelector("td.trdata:not(.trf)");
33
+ if (!timeCell || !dataCell)
34
+ continue;
35
+ const timeDiv = timeCell.querySelector(".trfd");
36
+ if (!timeDiv)
37
+ continue;
38
+ const timeText = text(timeDiv);
39
+ const numberMatch = timeText.match(/(\d+)\s*пара/);
40
+ const timeMatch = timeText.match(/\((\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})\)/);
41
+ if (!numberMatch)
42
+ continue;
43
+ const entries = [];
44
+ for (const entryRow of dataCell.querySelectorAll("table tr")) {
45
+ const entry = entryParser(entryRow);
46
+ if (entry)
47
+ entries.push(entry);
48
+ }
49
+ currentDay.slots.push({
50
+ number: parseInt(numberMatch[1]),
51
+ timeStart: parseTime(timeMatch?.[1] ?? "00:00"),
52
+ timeEnd: parseTime(timeMatch?.[2] ?? "00:00"),
53
+ entries,
54
+ });
55
+ }
56
+ return days;
57
+ }
58
+ function parseSemesterSchedule(doc) {
59
+ return parseSemesterScheduleWith(doc, parseSemesterEntry);
60
+ }
61
+ function parseSemesterEntry(el) {
62
+ const td = el.querySelector("td") ?? el;
63
+ const fullHtml = td.innerHTML ?? "";
64
+ const plainText = text(td);
65
+ if (!plainText)
66
+ return null;
67
+ const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
68
+ // Detect red-bordered divs (transfers / substitutions)
69
+ const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
70
+ // Transfer (перенос) — the whole entry is the transferred lesson
71
+ for (const div of redDivs) {
72
+ const result = parseTransferDiv(div);
73
+ if (result) {
74
+ if (possibleChanges)
75
+ result.entry.possibleChanges = true;
76
+ return result.entry;
77
+ }
78
+ }
79
+ // Collect substitutions (замена на)
80
+ const substitutions = [];
81
+ for (const div of redDivs) {
82
+ const sub = parseSubstitutionDiv(div);
83
+ if (sub)
84
+ substitutions.push(sub);
85
+ }
86
+ // Strip red divs from HTML/text before parsing the regular entry
87
+ let cleanHtml = fullHtml;
88
+ let cleanText = plainText;
89
+ for (const div of redDivs) {
90
+ cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
91
+ cleanText = cleanText.replace(text(div), "");
92
+ }
93
+ const subjectEl = td.querySelector('span[style*="color: blue"]');
94
+ const subject = subjectEl ? text(subjectEl) : "";
95
+ if (!subject)
96
+ return null;
97
+ const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
98
+ const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
99
+ const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
100
+ const teacherMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
101
+ const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
102
+ const weekParity = parseWeekParity(cleanHtml);
103
+ return {
104
+ room: roomMatch?.[1] ?? "",
105
+ subject,
106
+ type: typeMatch?.[1] ?? "",
107
+ weeks: parseWeeks(weeksMatch?.[1] ?? ""),
108
+ teacher: parseTeacher(teacherMatch?.[1] ?? ""),
109
+ groups: [],
110
+ subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
111
+ weekParity,
112
+ substitutions: substitutions.length > 0 ? substitutions : undefined,
113
+ possibleChanges,
114
+ };
115
+ }
116
+ // --- Session schedule parsing (date-based, specific dates) ---
117
+ function parseSessionSchedule(doc, educationType) {
118
+ const days = [];
119
+ for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
120
+ // Parse date from cell id: trd20251224 -> 2025-12-24
121
+ const id = dateCell.getAttribute("id") ?? "";
122
+ const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
123
+ if (!dateMatch)
124
+ continue;
125
+ const year = parseInt(dateMatch[1]);
126
+ const month = parseInt(dateMatch[2]) - 1;
127
+ const dayNum = parseInt(dateMatch[3]);
128
+ const date = new Date(year, month, dayNum);
129
+ // Extract weekday from after <br>
130
+ const cellHtml = dateCell.innerHTML ?? "";
131
+ const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
132
+ const weekday = brMatch ? brMatch[1].trim() : "";
133
+ // Data cell is the next td.trdata sibling in the same row
134
+ const row = dateCell.parentElement;
135
+ if (!row)
136
+ continue;
137
+ const dataCell = row.querySelector("td.trdata:not(.trfd)");
138
+ if (!dataCell)
139
+ continue;
140
+ const slots = [];
141
+ for (const entryRow of dataCell.querySelectorAll("table tr")) {
142
+ const td = entryRow.querySelector("td") ?? entryRow;
143
+ const entry = parseSessionEntry(td);
144
+ if (!entry)
145
+ continue;
146
+ slots.push({
147
+ number: getLessonNumber(entry.timeStart, educationType),
148
+ timeStart: entry.timeStart,
149
+ timeEnd: entry.timeEnd,
150
+ entries: [entry.entry],
151
+ });
152
+ }
153
+ if (slots.length > 0) {
154
+ days.push({ weekday, date, slots });
155
+ }
156
+ }
157
+ return days;
158
+ }
159
+ function parseSessionEntry(td) {
160
+ const fullHtml = td.innerHTML ?? "";
161
+ const plainText = text(td);
162
+ if (!plainText)
163
+ return null;
164
+ const subjectEl = td.querySelector('span[style*="color: blue"]');
165
+ const subject = subjectEl ? text(subjectEl) : "";
166
+ if (!subject)
167
+ return null;
168
+ // Room: text before the first <span
169
+ const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
170
+ const room = roomMatch ? roomMatch[1].trim() : "";
171
+ // Type: parenthesized text after </span>, case-insensitive
172
+ const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
173
+ const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
174
+ // Time: after <br>, format HH:MM - HH:MM
175
+ const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
176
+ if (!timeMatch)
177
+ return null;
178
+ return {
179
+ entry: {
180
+ room,
181
+ subject,
182
+ type,
183
+ weeks: { from: 0, to: 0 },
184
+ teacher: { name: "" },
185
+ groups: [],
186
+ },
187
+ timeStart: parseTime(timeMatch[1]),
188
+ timeEnd: parseTime(timeMatch[2]),
189
+ };
190
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Split a raw "groups" string from the schedule HTML into individual group names.
3
+ *
4
+ * Preserves in-name annotations (e.g. "КТ-42-25 (АихС)") and strips service
5
+ * markers like "(N подгруппа)" — the subgroup number is carried separately on
6
+ * {@link import("../types.js").ScheduleEntry.subgroup}.
7
+ *
8
+ * Examples:
9
+ * "КТ-42-25" -> ["КТ-42-25"]
10
+ * "КТ-42-25 (АихС) КТ-41-25" -> ["КТ-42-25 (АихС)", "КТ-41-25"]
11
+ * "КТ-42-25 (1 подгруппа)" -> ["КТ-42-25"]
12
+ * "КТ-42-25 (АихС) (1 подгруппа)" -> ["КТ-42-25 (АихС)"]
13
+ * "" -> []
14
+ */
15
+ export declare function parseGroupsString(raw: string | undefined | null): string[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Split a raw "groups" string from the schedule HTML into individual group names.
3
+ *
4
+ * Preserves in-name annotations (e.g. "КТ-42-25 (АихС)") and strips service
5
+ * markers like "(N подгруппа)" — the subgroup number is carried separately on
6
+ * {@link import("../types.js").ScheduleEntry.subgroup}.
7
+ *
8
+ * Examples:
9
+ * "КТ-42-25" -> ["КТ-42-25"]
10
+ * "КТ-42-25 (АихС) КТ-41-25" -> ["КТ-42-25 (АихС)", "КТ-41-25"]
11
+ * "КТ-42-25 (1 подгруппа)" -> ["КТ-42-25"]
12
+ * "КТ-42-25 (АихС) (1 подгруппа)" -> ["КТ-42-25 (АихС)"]
13
+ * "" -> []
14
+ */
15
+ export function parseGroupsString(raw) {
16
+ if (!raw)
17
+ return [];
18
+ const cleaned = raw
19
+ .replace(/\s*\(\s*\d+\s*подгруппа\s*\)\s*/gi, " ")
20
+ .trim();
21
+ if (!cleaned)
22
+ return [];
23
+ const out = [];
24
+ for (const m of cleaned.matchAll(/(\S+)(?:\s+\(([^)]+)\))?/g)) {
25
+ out.push(m[2] ? `${m[1]} (${m[2]})` : m[1]);
26
+ }
27
+ return out;
28
+ }
@@ -0,0 +1,5 @@
1
+ export { parseGroupsString } from "./groups.js";
2
+ export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGroupButtons, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
3
+ export { parseFullSchedule } from "./full-schedule.js";
4
+ export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
5
+ export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
@@ -0,0 +1,5 @@
1
+ export { parseGroupsString } from "./groups.js";
2
+ export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGroupButtons, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
3
+ export { parseFullSchedule } from "./full-schedule.js";
4
+ export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
5
+ export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
@@ -0,0 +1,11 @@
1
+ import { Period } from "../../common/types.js";
2
+ import type { Audience, Faculty, Group } from "../types.js";
3
+ export declare function parsePeriodFromPage(html: string): Period | null;
4
+ export declare function parseGroupButtons(html: string): Group[];
5
+ export declare function parseFacultyButtons(html: string): Faculty[];
6
+ export declare function parseAudienceButtons(html: string): Audience[];
7
+ export declare function parseAudienceName(html: string): string | null;
8
+ export declare function parseTeacherButtons(html: string): {
9
+ id: number;
10
+ name: string;
11
+ }[];
@@ -0,0 +1,80 @@
1
+ import { parseHtml, text } from "../../common/parse.js";
2
+ const PERIOD_LABELS = {
3
+ "осенний семестр": 1,
4
+ "зимняя сессия": 2,
5
+ "весенний семестр": 3,
6
+ "летняя сессия": 4,
7
+ };
8
+ export function parsePeriodFromPage(html) {
9
+ const match = html.match(/идет\s+(.+?)\s*</i);
10
+ if (!match)
11
+ return null;
12
+ const label = match[1].toLowerCase().trim();
13
+ return PERIOD_LABELS[label] ?? null;
14
+ }
15
+ export function parseGroupButtons(html) {
16
+ const doc = parseHtml(html);
17
+ const groups = [];
18
+ for (const btn of doc.querySelectorAll("button[id^='gr']")) {
19
+ const onclick = btn.getAttribute("onClick") ?? "";
20
+ const idMatch = onclick.match(/val\((\d+)\)/);
21
+ if (idMatch) {
22
+ groups.push({
23
+ id: parseInt(idMatch[1]),
24
+ name: btn.getAttribute("value") ?? text(btn),
25
+ });
26
+ }
27
+ }
28
+ return groups;
29
+ }
30
+ export function parseFacultyButtons(html) {
31
+ const doc = parseHtml(html);
32
+ const faculties = [];
33
+ for (const btn of doc.querySelectorAll(".facbut")) {
34
+ const onclick = btn.getAttribute("onClick") ?? "";
35
+ const idMatch = onclick.match(/val\((\d+)\)/);
36
+ if (idMatch) {
37
+ faculties.push({ id: parseInt(idMatch[1]), name: text(btn) });
38
+ }
39
+ }
40
+ return faculties;
41
+ }
42
+ export function parseAudienceButtons(html) {
43
+ const results = [];
44
+ const seen = new Set();
45
+ const re = /<button[^>]*\bname="aud(\d+)"[^>]*\bvalue="([^"]*)"/g;
46
+ for (const m of html.matchAll(re)) {
47
+ const id = parseInt(m[1]);
48
+ if (seen.has(id))
49
+ continue;
50
+ seen.add(id);
51
+ results.push({ id, name: m[2] });
52
+ }
53
+ return results;
54
+ }
55
+ export function parseAudienceName(html) {
56
+ const m = html.match(/id="path"[\s\S]*?findaud[^>]*>[^<]*<\/a>([\s\S]*?)<\/div>/);
57
+ if (!m)
58
+ return null;
59
+ const tail = m[1]
60
+ .replace(/&nbsp;/g, " ")
61
+ .replace(/<[^>]*>/g, "")
62
+ .replace(/^[\s/]+/, "")
63
+ .trim();
64
+ return tail || null;
65
+ }
66
+ export function parseTeacherButtons(html) {
67
+ const doc = parseHtml(html);
68
+ const results = [];
69
+ for (const btn of doc.querySelectorAll(".techbut")) {
70
+ const onclick = btn.getAttribute("onClick") ?? "";
71
+ const idMatch = onclick.match(/val\((\d+)\)/);
72
+ if (idMatch) {
73
+ results.push({
74
+ id: parseInt(idMatch[1]),
75
+ name: btn.getAttribute("value") ?? text(btn),
76
+ });
77
+ }
78
+ }
79
+ return results;
80
+ }
@@ -0,0 +1,10 @@
1
+ import type { ScheduleEntry, Substitution, TransferInfo } from "../types.js";
2
+ export declare function parseDate(dd: string, mm: string, yyyy: string): Date;
3
+ export declare function parseTransferDiv(div: Element): {
4
+ transfer: TransferInfo;
5
+ entry: ScheduleEntry;
6
+ } | null;
7
+ export declare function parseSubstitutionDiv(div: Element): Substitution | null;
8
+ export declare function parseSubstituteForDiv(div: Element): {
9
+ entry: ScheduleEntry;
10
+ } | null;