diff --git a/parsers.js b/parsers.js
new file mode 100644
index 0000000..756b14d
--- /dev/null
+++ b/parsers.js
@@ -0,0 +1,50 @@
+(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(/ /gi, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ function extractRows(html) {
+ const rows = [];
+ const trRe = /
]*>([\s\S]*?)<\/tr>/gi;
+ let trMatch;
+ while ((trMatch = trRe.exec(html)) !== null) {
+ const rowHtml = trMatch[1];
+ if (/| ]*>([\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]
+ }));
+ }
+
+ return { parseStudyLoadTable };
+});
diff --git a/test/parsers.test.js b/test/parsers.test.js
new file mode 100644
index 0000000..fbb9f21
--- /dev/null
+++ b/test/parsers.test.js
@@ -0,0 +1,28 @@
+const { test } = require('node:test');
+const assert = require('node:assert');
+const fs = require('node:fs');
+const path = require('node:path');
+const { parseStudyLoadTable } = require('../parsers.js');
+
+const fixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'study-load-table.html'), 'utf8');
+
+test('parseStudyLoadTable extracts all course rows', () => {
+ const rows = parseStudyLoadTable(fixture);
+ assert.strictEqual(rows.length, 5);
+});
+
+test('parseStudyLoadTable maps columns correctly', () => {
+ const rows = parseStudyLoadTable(fixture);
+ const first = rows[0];
+ assert.strictEqual(first.group, '1');
+ assert.strictEqual(first.courseCode, 'CS 5105');
+ assert.strictEqual(first.units, '3.00');
+ assert.strictEqual(first.status, 'DISSOLVED');
+ assert.ok(first.schedule.includes('F 01:30 PM'));
+});
+
+test('parseStudyLoadTable handles empty faculty', () => {
+ const rows = parseStudyLoadTable(fixture);
+ const it4102 = rows.find(r => r.courseCode === 'IT 4102N');
+ assert.strictEqual(it4102.faculty, '');
+});
|