@wilm-ai/wilma-client 1.6.0 → 1.6.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.
@@ -12,8 +12,13 @@ import type { LessonNote, LessonNoteSummary } from "../types.js";
12
12
  * three grid columns but consumes one index, so by the third or fourth cell
13
13
  * the index has drifted from the true grid column.
14
14
  *
15
- * Title attribute on event cells: "TypeLabel /TeacherName" or
16
- * "SubjectCode; TypeLabel /TeacherName".
15
+ * Two kinds of event cells: absence-type marks (`<td class="at-tpN">`, whose
16
+ * text is the teacher's code) and free-text remarks such as praise or
17
+ * behaviour notes (`<td class="at-tp-other">`, whose text is a `<small>`
18
+ * label plus an optional `<sup>` footnote marker pointing at the
19
+ * "Huomioita" column). The title attribute carries the details:
20
+ * "TypeLabel /TeacherName", "SubjectCode; TypeLabel /TeacherName" or
21
+ * "SubjectCode; TypeLabel; teacher's note /TeacherName".
17
22
  */
18
23
  export declare function parseAttendanceHtml(html: string, date?: string): LessonNote[];
19
24
  /** Count lesson notes by kind (absences, lateness, feedback…), most common first. */
@@ -12,8 +12,13 @@ import * as cheerio from "cheerio";
12
12
  * three grid columns but consumes one index, so by the third or fourth cell
13
13
  * the index has drifted from the true grid column.
14
14
  *
15
- * Title attribute on event cells: "TypeLabel /TeacherName" or
16
- * "SubjectCode; TypeLabel /TeacherName".
15
+ * Two kinds of event cells: absence-type marks (`<td class="at-tpN">`, whose
16
+ * text is the teacher's code) and free-text remarks such as praise or
17
+ * behaviour notes (`<td class="at-tp-other">`, whose text is a `<small>`
18
+ * label plus an optional `<sup>` footnote marker pointing at the
19
+ * "Huomioita" column). The title attribute carries the details:
20
+ * "TypeLabel /TeacherName", "SubjectCode; TypeLabel /TeacherName" or
21
+ * "SubjectCode; TypeLabel; teacher's note /TeacherName".
17
22
  */
18
23
  export function parseAttendanceHtml(html, date) {
19
24
  const $ = cheerio.load(html);
@@ -62,10 +67,14 @@ export function parseAttendanceHtml(html, date) {
62
67
  for (let i = 2; i < cells.length; i++) {
63
68
  const $cell = $(cells[i]);
64
69
  const colspan = parseInt($cell.attr("colspan") ?? "1", 10) || 1;
65
- const tpClass = (($cell.attr("class") ?? "").match(/\bat-tp\d+\b/) ?? [])[0];
70
+ const tpClass = (($cell.attr("class") ?? "").match(/\bat-tp(?:\d+|-other)\b/) ?? [])[0];
66
71
  if (tpClass) {
72
+ const isRemark = tpClass === "at-tp-other";
67
73
  const title = ($cell.attr("title") ?? "").trim();
68
- const cellText = $cell.text().trim();
74
+ // The visible text: a remark's <small> label, or an absence mark's
75
+ // teacher code; a <sup> footnote marker is never part of either.
76
+ const $small = $cell.find("small");
77
+ const cellText = ($small.length ? $small.text() : $cell.clone().find("sup").remove().end().text()).trim();
69
78
  // Title formats observed:
70
79
  // "TypeLabel /TeacherFullName"
71
80
  // "SubjectCode; TypeLabel /TeacherFullName"
@@ -76,7 +85,8 @@ export function parseAttendanceHtml(html, date) {
76
85
  let subject = "";
77
86
  let typeLabel = "";
78
87
  let note = null;
79
- let teacher = cellText;
88
+ // A remark's cell text is its label, never a teacher.
89
+ let teacher = isRemark ? "" : cellText;
80
90
  if (title) {
81
91
  let rest = title;
82
92
  const slashIdx = rest.lastIndexOf(" /");
@@ -113,7 +123,7 @@ export function parseAttendanceHtml(html, date) {
113
123
  start,
114
124
  end,
115
125
  subject,
116
- typeLabel: typeLabel || tpClass.replace("at-tp", "Type "),
126
+ typeLabel: typeLabel || (isRemark ? cellText : tpClass.replace("at-tp", "Type ")),
117
127
  typeClass: tpClass,
118
128
  teacher,
119
129
  note,
@@ -98,7 +98,12 @@ export function parseTimetableJson(payload) {
98
98
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
99
99
  continue;
100
100
  for (const module of lesson.modules ?? []) {
101
- const subject = (module.caption ?? "").trim();
101
+ // The caption is the course code, sometimes followed by the name
102
+ // ("BI_ Biologia", or just "yRUB1.1" in upper secondary schools); the
103
+ // name alone is in courseName. Lessons without a course (lunch, a
104
+ // class teacher's hour) have only a caption.
105
+ const caption = (module.caption ?? "").trim();
106
+ const subject = (module.courseName ?? "").trim() || caption;
102
107
  const teachers = module.teachers ?? [];
103
108
  const rooms = (module.rooms ?? []).map((room) => (room.abbreviation ?? room.caption ?? "").trim()).filter(Boolean);
104
109
  lessons.push({
@@ -107,7 +112,7 @@ export function parseTimetableJson(payload) {
107
112
  start,
108
113
  end,
109
114
  subject,
110
- subjectCode: firstSubjectToken(subject),
115
+ subjectCode: firstSubjectToken(caption),
111
116
  teacher: teachers.map((t) => [t.lastname, t.firstname].filter(Boolean).join(" ")).join(", "),
112
117
  teacherCode: teachers.map((t) => t.abbreviation ?? "").filter(Boolean).join(", "),
113
118
  groupId: Number(module.id) || 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wilm-ai/wilma-client",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Unofficial client for Wilma, the Finnish school information system: login (with two-step verification), schedule, homework, exams, messages and news.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",
33
33
  "lint": "tsc -p tsconfig.json --noEmit",
34
- "test": "pnpm run build && node test/login.mjs && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/parser-students.mjs && node test/client-students.mjs && node test/client-schedule.mjs && node test/client-news-resources.mjs && node test/network-error.mjs && node test/audit.mjs && node test/real-pages.mjs && node test/client-features.mjs",
34
+ "test": "pnpm run build && node test/login.mjs && node test/parser-attendance.mjs && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/parser-students.mjs && node test/client-students.mjs && node test/client-schedule.mjs && node test/client-attendance.mjs && node test/client-news-resources.mjs && node test/network-error.mjs && node test/audit.mjs && node test/real-pages.mjs && node test/client-features.mjs",
35
35
  "test:live": "tsc -p tsconfig.json && node test/live.spec.mjs",
36
36
  "fixtures:anonymize": "node scripts/anonymize-fixture.mjs"
37
37
  },