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.
Files changed (80) hide show
  1. package/README.md +165 -193
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/common/cache.js +30 -9
  5. package/dist/common/http.d.ts +6 -0
  6. package/dist/common/http.js +8 -1
  7. package/dist/common/parse.d.ts +1 -1
  8. package/dist/common/parse.js +2 -2
  9. package/dist/common/types.d.ts +8 -2
  10. package/dist/common/types.js +12 -0
  11. package/dist/core.d.ts +10 -0
  12. package/dist/core.js +5 -0
  13. package/dist/index.d.ts +5 -3
  14. package/dist/index.js +4 -4
  15. package/dist/lk/client.d.ts +8 -6
  16. package/dist/lk/client.js +51 -23
  17. package/dist/lk/types.d.ts +7 -7
  18. package/dist/node.d.ts +1 -0
  19. package/dist/node.js +1 -0
  20. package/dist/parsers.d.ts +3 -0
  21. package/dist/parsers.js +2 -0
  22. package/dist/tt/client.d.ts +55 -63
  23. package/dist/tt/client.js +477 -226
  24. package/dist/tt/domain/directory.d.ts +14 -0
  25. package/dist/tt/domain/directory.js +116 -0
  26. package/dist/tt/domain/ids.d.ts +7 -0
  27. package/dist/tt/domain/ids.js +20 -0
  28. package/dist/tt/domain/index.d.ts +7 -0
  29. package/dist/tt/domain/index.js +5 -0
  30. package/dist/tt/domain/normalize.d.ts +7 -0
  31. package/dist/tt/domain/normalize.js +65 -0
  32. package/dist/tt/domain/repository.d.ts +51 -0
  33. package/dist/tt/domain/repository.js +805 -0
  34. package/dist/tt/domain/schedule.d.ts +37 -0
  35. package/dist/tt/domain/schedule.js +292 -0
  36. package/dist/tt/domain/types.d.ts +175 -0
  37. package/dist/tt/domain/types.js +1 -0
  38. package/dist/tt/observations.d.ts +11 -0
  39. package/dist/tt/observations.js +131 -0
  40. package/dist/tt/parse/audience.d.ts +3 -3
  41. package/dist/tt/parse/audience.js +47 -15
  42. package/dist/tt/parse/entry-parts.d.ts +1 -0
  43. package/dist/tt/parse/entry-parts.js +5 -2
  44. package/dist/tt/parse/full-schedule.d.ts +9 -4
  45. package/dist/tt/parse/full-schedule.js +32 -41
  46. package/dist/tt/parse/groups.d.ts +1 -1
  47. package/dist/tt/parse/groups.js +1 -1
  48. package/dist/tt/parse/index.d.ts +4 -4
  49. package/dist/tt/parse/index.js +4 -4
  50. package/dist/tt/parse/lists.d.ts +6 -5
  51. package/dist/tt/parse/lists.js +7 -2
  52. package/dist/tt/parse/overlays.d.ts +5 -4
  53. package/dist/tt/parse/overlays.js +13 -14
  54. package/dist/tt/parse/teacher.d.ts +2 -3
  55. package/dist/tt/parse/teacher.js +14 -56
  56. package/dist/tt/parse/webinars.js +3 -4
  57. package/dist/tt/types.d.ts +54 -69
  58. package/dist/tt/utils/date.d.ts +4 -0
  59. package/dist/tt/utils/date.js +30 -1
  60. package/dist/tt/utils/index.d.ts +2 -3
  61. package/dist/tt/utils/index.js +2 -3
  62. package/dist/tt/utils/period.d.ts +4 -4
  63. package/dist/tt/utils/period.js +9 -8
  64. package/dist/tt/utils/semester.d.ts +4 -4
  65. package/dist/tt/utils/semester.js +25 -5
  66. package/dist/tt/utils/time-slots.d.ts +3 -5
  67. package/dist/tt/utils/time-slots.js +19 -34
  68. package/dist/tt/webinars.d.ts +7 -0
  69. package/dist/tt/webinars.js +38 -0
  70. package/docs/fixture-review.md +56 -0
  71. package/docs/testing.md +23 -0
  72. package/docs/v5-architecture.md +144 -0
  73. package/docs/v5-migration.md +451 -0
  74. package/package.json +17 -7
  75. package/dist/shared.d.ts +0 -9
  76. package/dist/shared.js +0 -7
  77. package/dist/tt/schedule.d.ts +0 -55
  78. package/dist/tt/schedule.js +0 -233
  79. package/dist/tt/utils/lessons.d.ts +0 -16
  80. 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
- const DISTANCE_RE = /дистанционно|ДОТ/i;
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 parseAudienceSemesterEntry(el) {
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[0] ?? "");
116
+ const teacherLine = stripDistanceMarker(parts.find((line) => !containsGroupCode(stripDistanceMarker(line))) ?? "");
118
117
  const groupsLine = parts
119
- .slice(1)
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: parseGroupsString(groupsLine),
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: DISTANCE_RE.test(cleanText),
136
+ isDistance: hasDistanceMarker(cleanText),
138
137
  substitutions: substitutions.length > 0 ? substitutions : undefined,
139
138
  possibleChanges,
140
139
  };
141
140
  }
142
- export function parseAudienceFullSchedule(html) {
141
+ export function parseRoomSchedule(html) {
143
142
  const doc = parseHtml(html);
144
- return parseSemesterScheduleWith(doc, parseAudienceSemesterEntry);
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 = /дистанционно|ДОТ/i;
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 (DISTANCE_RE.test(stripHtml(html))) {
37
+ if (hasDistanceMarker(stripHtml(html))) {
35
38
  return "Дистанционно (ДОТ)";
36
39
  }
37
40
  if (!beforeSubject)
@@ -1,4 +1,9 @@
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[];
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
- const DISTANCE_RE = /дистанционно|ДОТ/i;
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, edType);
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, slots: [] };
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 entries = [];
50
+ const lessons = [];
54
51
  for (const entryRow of dataCell.querySelectorAll("table tr")) {
55
52
  const entry = entryParser(entryRow);
56
53
  if (entry)
57
- entries.push(entry);
54
+ lessons.push(entry);
58
55
  }
59
- currentDay.slots.push({
60
- number: parseInt(numberMatch[1]),
61
- timeStart: parseTime(timeMatch?.[1] ?? "00:00"),
62
- timeEnd: parseTime(timeMatch?.[2] ?? "00:00"),
63
- entries,
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: DISTANCE_RE.test(cleanText) || DISTANCE_RE.test(room),
122
+ isDistance: hasDistanceMarker(cleanText) || hasDistanceMarker(room ?? ""),
126
123
  substitutions: substitutions.length > 0 ? substitutions : undefined,
127
124
  possibleChanges,
128
125
  };
129
126
  }
130
- // --- Session schedule parsing (date-based, specific dates) ---
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 year = parseInt(dateMatch[1]);
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 slots = [];
146
+ const blocks = [];
154
147
  for (const entryRow of dataCell.querySelectorAll("table tr")) {
155
148
  const td = entryRow.querySelector("td") ?? entryRow;
156
- const entry = parseSessionEntry(td);
149
+ const entry = entryParser(td);
157
150
  if (!entry)
158
151
  continue;
159
- slots.push({
160
- number: getLessonNumber(entry.timeStart, educationType),
161
- timeStart: entry.timeStart,
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 (slots.length > 0) {
167
- days.push({ weekday, date, slots });
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
- entry: {
198
- room,
191
+ lesson: {
192
+ room: room || undefined,
199
193
  subject,
200
194
  type,
201
- weeks: { from: 0, to: 0 },
202
- teacher: parseTeacher(teacherPart),
203
- groups: [],
195
+ teacher: teacherPart ? parseTeacher(teacherPart) : undefined,
204
196
  subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
205
- isDistance: DISTANCE_RE.test(plainText) || DISTANCE_RE.test(room),
197
+ isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
206
198
  possibleChanges,
207
199
  },
208
- timeStart: parseTime(timeMatch[1]),
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").ScheduleEntry.subgroup}.
6
+ * {@link import("../types.js").ParsedLesson.subgroup}.
7
7
  *
8
8
  * Examples:
9
9
  * "КТ-42-25" -> ["КТ-42-25"]
@@ -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").ScheduleEntry.subgroup}.
7
+ * {@link import("../types.js").ParsedLesson.subgroup}.
8
8
  *
9
9
  * Examples:
10
10
  * "КТ-42-25" -> ["КТ-42-25"]
@@ -1,6 +1,6 @@
1
1
  export { parseGroupsString } from "./groups.js";
2
- export { parseAcademicYearFromPage, 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";
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";
@@ -1,6 +1,6 @@
1
1
  export { parseGroupsString } from "./groups.js";
2
- export { parseAcademicYearFromPage, 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";
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";
@@ -1,12 +1,13 @@
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;
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 parseAudienceButtons(html: string): Audience[];
9
- export declare function parseAudienceName(html: string): string | null;
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;
@@ -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 parseAudienceButtons(html) {
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 parseAudienceName(html) {
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 { ScheduleEntry, Substitution, TransferInfo } from "../types.js";
2
- export declare function parseDate(dd: string, mm: string, yyyy: string): Date;
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: ScheduleEntry;
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: ScheduleEntry;
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 new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
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
- weeks: { from: 0, to: 0 },
63
- teacher: parseTeacher(teacherPart),
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: DISTANCE_RE.test(divText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
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: DISTANCE_RE.test(room ?? divText) };
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
- : { name: "" };
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
- weeks: { from: 0, to: 0 },
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: DISTANCE_RE.test(divText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
124
+ isDistance: hasDistanceMarker(divText) || hasDistanceMarker(roomMatch?.[1] ?? ""),
126
125
  substituteFor: { date, originalTeacher },
127
126
  },
128
127
  };
@@ -1,4 +1,3 @@
1
- import { EducationType } from "../../common/types.js";
2
- import type { FullScheduleDay, TeacherInfo } from "../types.js";
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;
@@ -1,16 +1,13 @@
1
1
  import { parseHtml, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
2
- import { getLessonNumber } from "../utils/index.js";
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
- const DISTANCE_RE = /дистанционно|ДОТ/i;
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 parseTeacherSessionSchedule(doc, edType);
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
- teacher: { name: "" },
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: DISTANCE_RE.test(cleanText) || DISTANCE_RE.test(room),
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
- entry: {
138
- room,
98
+ lesson: {
99
+ room: room || undefined,
139
100
  subject,
140
101
  type,
141
- weeks: { from: 0, to: 0 },
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: DISTANCE_RE.test(plainText) || DISTANCE_RE.test(room),
104
+ isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
146
105
  possibleChanges,
147
106
  },
148
- timeStart: parseTime(timeMatch[1]),
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 new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]));
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
- timeStart: timeRange.start,
77
- timeEnd: timeRange.end,
76
+ time: timeRange,
78
77
  subject: parsed.subject,
79
78
  type: parsed.type,
80
79
  teacher: parseTeacher(parsed.teacherRaw),