@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.
- package/dist/apps/lessons/parse.js +23 -26
- package/dist/apps/lessons/types.d.ts +5 -2
- package/dist/html.d.ts +2 -0
- package/dist/html.js +13 -0
- package/package.json +1 -1
|
@@ -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
|
|
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 (
|
|
12
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
9
|
-
|
|
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 ?? [])
|