Files
ismis-helper/parsers.js
T
adrianbonpin 0cb70ecee0
ci / test (push) Successful in 43s
build-extension / build (push) Successful in 6m7s
feat(v1.4.0): exports + smart year level + progress + encrypted passwords
v1.4.0: CSV/Excel/JSON/NextUp exports, units-first smart year standing, live export progress bar, AES-GCM at-rest password encryption.
2026-08-20 04:39:08 +00:00

205 lines
7.4 KiB
JavaScript

(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
}
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function cleanText(s) {
return String(s == null ? '' : s)
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function extractRows(html) {
const rows = [];
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let trMatch;
while ((trMatch = trRe.exec(html)) !== null) {
const rowHtml = trMatch[1];
if (/<th/i.test(rowHtml)) continue;
const tds = [];
const tdRe = /<td[^>]*>([\s\S]*?)<\/td>/gi;
let tdMatch;
while ((tdMatch = tdRe.exec(rowHtml)) !== null) {
tds.push(cleanText(tdMatch[1]));
}
rows.push(tds);
}
return rows;
}
function parseStudyLoadTable(html) {
return extractRows(html)
.filter((tds) => tds.length >= 6)
.map((tds) => ({
group: tds[0],
courseCode: tds[1],
schedule: tds[2],
faculty: tds[3],
units: tds[4],
status: tds[5]
}));
}
function parseProfilePage(html) {
const result = {};
const h3Match = /<h3[^>]*>([\s\S]*?)<\/h3>/i.exec(html);
const bMatch = (h3Match && /<b>([\s\S]*?)<\/b>/i.exec(h3Match[1])) ||
/<b>([\s\S]*?)<\/b>/i.exec(html);
if (bMatch) result.program = cleanText(bMatch[1]);
const smallMatch = /<small>([\s\S]*?)<\/small>/i.exec(html);
if (smallMatch) {
const lines = smallMatch[1].split(/<br\s*\/?>/i).map(cleanText).filter(Boolean);
result.semester = lines[lines.length - 1] || '';
}
const kvRe = /<td[^>]*>([\s\S]*?)<\/td>\s*<td[^>]*class=["'][^"']*\bbold\b[^"']*["'][^>]*>([\s\S]*?)<\/td>/gi;
let m;
while ((m = kvRe.exec(html)) !== null) {
const key = cleanText(m[1]);
const val = cleanText(m[2]);
if (/full name/i.test(key)) result.fullName = val;
else if (/student id/i.test(key)) result.studentId = val;
else if (/year level/i.test(key)) result.yearLevel = val;
else if (/student type/i.test(key)) result.studentType = val;
}
return result;
}
function parseOfferedCourses(html) {
return extractRows(html)
.filter((tds) => tds.length >= 2)
.map((tds) => ({
courseCode: tds[0].replace(/\s*-\s*Group\s*\d+$/i, '').trim(),
description: tds[1]
}));
}
// ---- Year standing helpers ----
// Parse the View Grades page into an ordered list of academic terms:
// [{ semester: 'FIRST'|'SECOND'|'SUMMER', year: 2024 }, ...]
// Terms appear in chronological order in the page. Regular semesters appear
// as "FIRST SEMESTER - YYYY"; summer appears as "SUMMER - YYYY".
function parseGradeHistory(html) {
const history = [];
const re = /\b(FIRST|SECOND)\s+SEMESTER\s*-\s*(\d{4})\b|\bSUMMER\s*-\s*(\d{4})\b/gi;
let m;
while ((m = re.exec(html)) !== null) {
if (m[1]) {
history.push({ semester: m[1].toUpperCase(), year: parseInt(m[2], 10) });
} else {
history.push({ semester: 'SUMMER', year: parseInt(m[3], 10) });
}
}
return history;
}
// Standing for a study load printed in `year` from the View Grades history.
// It is the number of distinct academic years strictly before that year + 1
// (FIRST/SECOND/SUMMER of a year share the same standing), clamped to at most
// the number of distinct years in history. NOTE: this is only an
// approximation for the printed semester — it counts calendar years present,
// not units passed — so a repeated/failed year can over-advance. It is used
// only as a fallback when the units-based standing is unavailable.
function yearLevelForSemester(history, year) {
if (!history || !history.length || !Number.isFinite(year)) return null;
const distinctYears = [...new Set(history.map((t) => t.year))].sort((a, b) => a - b);
const yearsBefore = distinctYears.filter((y) => y < year).length;
const level = Math.min(yearsBefore + 1, distinctYears.length);
return Number.isFinite(level) && level >= 1 ? level : null;
}
// Parse the Student Prospectus into per-year unit totals. Each table is
// preceded by a "Year Level: N" heading. Returns [{ year, units }].
function parseProspectusYearUnits(html) {
const segs = String(html || '').split(/(Year Level:\s*\d)/i);
const yearUnits = {};
let cur = null;
for (let i = 0; i < segs.length; i++) {
const s = segs[i];
const ym = /Year Level:\s*(\d+)/i.exec(s);
if (ym) { cur = parseInt(ym[1], 10); continue; }
if (cur == null) continue;
const tm = /<table[^>]*>([\s\S]*?)<\/table>/i.exec(s);
if (!tm) continue;
const rows = extractRows(tm[1]);
let units = 0;
for (const r of rows) {
if (r.length >= 3 && /^[A-Z]/.test(r[0]) && /[\d.]+/.test(r[2])) {
const u = parseFloat(String(r[2]).replace(/[^\d.]/g, ''));
if (Number.isFinite(u)) units += u;
}
}
yearUnits[cur] = (yearUnits[cur] || 0) + units;
cur = null; // only the immediately-following table belongs to this heading
}
const years = Object.keys(yearUnits).map(Number).sort((a, b) => a - b);
return years.map((y) => ({ year: y, units: yearUnits[y] }));
}
// Parse the Lacking Subjects page for total remaining units.
function parseLackingSubjectsUnits(html) {
return parseLackingSubjects(html).units;
}
function parseLackingSubjects(html) {
const rows = extractRows(String(html || ''));
let units = 0;
const courses = [];
for (const r of rows) {
if (r.length >= 3 && /^[A-Z]/.test(r[0]) && /[\d.]+/.test(r[2])) {
const u = parseFloat(String(r[2]).replace(/[^\d.]/g, ''));
if (Number.isFinite(u)) {
units += u;
courses.push({ courseCode: r[0], description: r[1], units: String(u) });
}
}
}
return { units: units.toFixed(2), courses };
}
// Year standing from units: finished = totalProspectusUnits - lackingUnits;
// standing = first N in [1..maxYear] where finished < cumulativeUnits(N);
// if finished passes every threshold, standing = maxYear (program length).
// yearUnits is the array [{year, units}] from parseProspectusYearUnits.
function computeYearStandingFromUnits(yearUnits, lackingUnits) {
const yrs = (yearUnits || []).slice().sort((a, b) => a.year - b.year);
if (!yrs.length) return null;
const total = yrs.reduce((s, y) => s + y.units, 0);
const finished = total - (parseFloat(lackingUnits) || 0);
if (finished < 0) return yrs[0].year;
let cum = 0;
for (const y of yrs) {
cum += y.units;
if (finished < cum) return y.year;
}
return yrs[yrs.length - 1].year; // passed everything -> final level
}
function filterStudyLoadCourses(courses) {
return (courses || []).filter(
(c) => String(c.status || '').trim().toUpperCase() !== 'DISSOLVED'
);
}
function sumUnits(courses) {
return (courses || [])
.reduce((sum, c) => sum + (parseFloat(c.units) || 0), 0)
.toFixed(2);
}
return {
parseStudyLoadTable, parseProfilePage, parseOfferedCourses,
filterStudyLoadCourses, sumUnits,
parseGradeHistory, yearLevelForSemester,
parseProspectusYearUnits, parseLackingSubjects, parseLackingSubjectsUnits,
computeYearStandingFromUnits
};
});