@ranzenjs/client 0.2.1 → 0.3.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.
@@ -1,39 +1,36 @@
1
1
  import { layoutChanged } from '../../errors.js';
2
- import { all, attr, cells, hasClass, one, ownText, rows, text } from '../../html.js';
2
+ import { all, attr, cells, hasClass, one, ownText, rawText, rows, text } from '../../html.js';
3
3
  import { ignoreDrift, isoDate, parseCount, queryParam } from '../../util.js';
4
4
  export function parseOverview(document, drift = ignoreDrift) {
5
5
  const folders = one('#mappen', document);
6
6
  if (!folders)
7
7
  throw layoutChanged('#mappen on the lessons page');
8
8
  const cards = all('.thumbnail', folders);
9
- // An empty #mappen is a valid account without courses; course links outside cards are not.
9
+ // An empty #mappen is a valid account without courses; a skipped course would read as gone.
10
+ const links = all('a[href*="sus_view"]', folders);
10
11
  const carded = new Set(cards.flatMap((card) => all('a[href*="sus_view"]', card)));
11
- if (all('a[href*="sus_view"]', folders).some((link) => !carded.has(link))) {
12
- drift('The lessons page lists courses outside known cards.');
12
+ if (links.some((link) => !carded.has(link))) {
13
+ throw layoutChanged('.thumbnail cards around every course in #mappen');
13
14
  }
14
15
  const latest = one('#aktuellTable', document);
15
16
  if (!latest && one('tr[data-entry]', document)) {
16
17
  drift('The lessons page lists entries outside #aktuellTable.');
17
18
  }
19
+ const courses = cards.map((card) => {
20
+ const id = queryParam(attr(one('a[href*="sus_view"]', card), 'href'), 'id');
21
+ if (!id)
22
+ throw layoutChanged('a course link in every .thumbnail card');
23
+ return {
24
+ id,
25
+ name: attr(one('h2 [title]', card), 'title') ?? text(one('h2', card)),
26
+ teachers: all('button.dropdown-toggle[title]', card).map(teacher),
27
+ marks: count(one('a.btn-info[href*="#leistungen"]', card)),
28
+ undoneHomework: count(one('a.btn-danger', card)),
29
+ upcomingEntries: count(one('a.btn-warning', card))
30
+ };
31
+ });
18
32
  return {
19
- courses: cards.flatMap((card) => {
20
- const id = queryParam(attr(one('a[href*="sus_view"]', card), 'href'), 'id');
21
- if (!id) {
22
- drift('A course card has no course link.');
23
- return [];
24
- }
25
- const name = attr(one('h2 [title]', card), 'title') ?? text(one('h2', card));
26
- return [
27
- {
28
- id,
29
- name,
30
- teachers: all('button.dropdown-toggle[title]', card).map(teacher),
31
- marks: count(one('a.btn-info[href*="#leistungen"]', card)),
32
- undoneHomework: count(one('a.btn-danger', card)),
33
- upcomingEntries: count(one('a.btn-warning', card))
34
- }
35
- ];
36
- }),
33
+ courses,
37
34
  latest: entryRows(rows(latest), 'A latest entry', drift).map((row) => ({
38
35
  course: { id: attr(row, 'data-book') ?? '', name: text(one('.name', row)) },
39
36
  teachers: all('.teacher button[title]', row).map(teacher),
@@ -111,7 +108,7 @@ function historyEntry(row, drift) {
111
108
  return {
112
109
  ...emptyEntry(attr(row, 'data-entry') ?? ''),
113
110
  date: entryDate(text(when), 'A history row', drift),
114
- periods: periods(text(when && one('small', when)), drift),
111
+ periods: periods(rawText(when && one('small', when)), drift),
115
112
  topic: text(body && one(':scope > b', body)) || null,
116
113
  content: text(contentOf(body), true) || null,
117
114
  homework: historyHomework(body, drift),
@@ -171,7 +168,7 @@ function submission(group, drift) {
171
168
  return [
172
169
  {
173
170
  id,
174
- name: ownText(button),
171
+ name: rawText(button, true),
175
172
  open: hasClass(button, 'btn-warning'),
176
173
  uploaded: text(button && one('.badge', button)) || null,
177
174
  due: day && time ? `${day}T${time[1].padStart(2, '0')}:${time[2]}` : day
@@ -185,7 +182,7 @@ function parseMarks(document, drift) {
185
182
  const [name, date, value, ...rest] = cells(row);
186
183
  const last = marks.at(-1);
187
184
  if (name && date && value && !rest.length) {
188
- marks.push({ name: text(name), date: text(date), value: text(value), comment: null });
185
+ marks.push({ name: rawText(name), date: rawText(date), value: text(value), comment: null });
189
186
  }
190
187
  else if (last &&
191
188
  name &&
@@ -300,7 +297,7 @@ function periods(label, drift) {
300
297
  const match = /^(\d+)\.?(?:\s*[-–]\s*(\d+)\.?)?(?:\s*Stunden?)?$/.exec(label.trim());
301
298
  if (!match) {
302
299
  drift('A history row has unreadable periods.');
303
- return null;
300
+ return { from: null, to: null, label };
304
301
  }
305
302
  const from = Number(match[1]);
306
303
  return { from, to: match[2] ? Number(match[2]) : from, label };
@@ -5,8 +5,9 @@ export interface Teacher {
5
5
  lastName: string | null;
6
6
  }
7
7
  export interface Periods {
8
- from: number;
9
- to: number;
8
+ /** Null with `to` when the label names no period numbers, e.g. "08:00 - 09:30". */
9
+ from: number | null;
10
+ to: number | null;
10
11
  /** The portal's wording, unchanged, e.g. "1. - 2. Stunde". */
11
12
  label: string;
12
13
  }
@@ -22,6 +23,7 @@ export interface EntryFile {
22
23
  }
23
24
  export interface Submission {
24
25
  id: string;
26
+ /** As written, so it can serve as a key. */
25
27
  name: string;
26
28
  open: boolean;
27
29
  /** The portal's badge, e.g. "2" files handed in; null without one. */
@@ -70,6 +72,7 @@ export interface Overview {
70
72
  attendance: AttendanceSummary[];
71
73
  }
72
74
  export interface Mark {
75
+ /** As written, like `date`, so it can serve as a key. */
73
76
  name: string;
74
77
  /** As shown by the portal, which omits the year, e.g. "Do, 17.09.". */
75
78
  date: string;
package/dist/html.d.ts CHANGED
@@ -14,6 +14,8 @@ export declare function rows(table: Element | null | undefined): Element[];
14
14
  * Multiline keeps <br> and block breaks, for free text such as lesson content and homework.
15
15
  */
16
16
  export declare function text(node: Root | null | undefined, multiline?: boolean): string;
17
+ /** Visible text with its whitespace as written, only trimmed, for names callers store as keys. */
18
+ export declare function rawText(node: Element | null | undefined, own?: boolean): string;
17
19
  /** Only the element's own text, without that of badges or labels nested in it. */
18
20
  export declare function ownText(node: Element | null | undefined): string;
19
21
  export declare function attr(node: Element | null | undefined, name: string): string | undefined;
package/dist/html.js CHANGED
@@ -73,6 +73,19 @@ export function text(node, multiline = false) {
73
73
  .replace(/\n{3,}/g, '\n\n')
74
74
  .trim();
75
75
  }
76
+ /** Visible text with its whitespace as written, only trimmed, for names callers store as keys. */
77
+ export function rawText(node, own = false) {
78
+ const parts = [];
79
+ const walk = (current) => {
80
+ if (isText(current))
81
+ parts.push(current.data);
82
+ else if (!own && isTag(current) && !hasClass(current, 'hidden_encoded')) {
83
+ current.children.forEach(walk);
84
+ }
85
+ };
86
+ node?.children.forEach(walk);
87
+ return parts.join('').trim();
88
+ }
76
89
  /** Only the element's own text, without that of badges or labels nested in it. */
77
90
  export function ownText(node) {
78
91
  return (node?.children ?? [])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranzenjs/client",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Typed client for Schulportal Hessen (SPH, LANIS)",
5
5
  "license": "MIT",
6
6
  "author": "Suko",