Compare commits
10
Commits
631ff4e9d2
...
2fa4064c7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fa4064c7c | ||
|
|
4ef392423a | ||
|
|
e6249ae330 | ||
|
|
51d467fbe7 | ||
|
|
52360ab559 | ||
|
|
c75850da2e | ||
|
|
17aeab9592 | ||
|
|
053c6aaa98 | ||
|
|
57c4b6f204 | ||
|
|
792974bd32 |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
+28
-3
@@ -13,14 +13,39 @@ browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
}
|
||||
});
|
||||
|
||||
function formatPeriod(period) {
|
||||
if (!period) return '';
|
||||
return String(period)
|
||||
.replace(/\b1ST\b/g, '1st')
|
||||
.replace(/\b2ND\b/g, '2nd')
|
||||
.replace(/\b3RD\b/g, '3rd')
|
||||
.replace(/\b(\d+)TH\b/g, '$1th')
|
||||
.toLowerCase()
|
||||
.replace(/^./, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
async function generatePdf(data) {
|
||||
const bytes = await ISMIStools.buildStudyLoadPdf(data);
|
||||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const name = (data.header && data.header.fullName) || 'Student';
|
||||
const filename = 'Study Load - ' + name + '.pdf';
|
||||
const header = data.header || {};
|
||||
const year = parseInt(data.year, 10);
|
||||
const period = formatPeriod(data.period || header.semester || '');
|
||||
const compactProgram = ISMIStools.compactProgram(header.program || '');
|
||||
const schoolYear = Number.isFinite(year) ? `${year}-${year + 1}` : '';
|
||||
const parts = [
|
||||
header.studentId || (header.fullName ? header.fullName.replace(/\s+/g, ' ') : ''),
|
||||
compactProgram,
|
||||
schoolYear ? 'S.Y. ' + schoolYear : '',
|
||||
period,
|
||||
'Study Load'
|
||||
].filter(Boolean);
|
||||
const filename = parts.join(' - ') + '.pdf';
|
||||
const fallback = 'Study Load - ' + (header.fullName || 'Student') + '.pdf';
|
||||
const safeFilename = filename || fallback;
|
||||
|
||||
try {
|
||||
const id = await browser.downloads.download({ url, filename, saveAs: true });
|
||||
const id = await browser.downloads.download({ url, filename: safeFilename, saveAs: true });
|
||||
const onChanged = (delta) => {
|
||||
if (delta.id === id && delta.state && (delta.state.current === 'complete' || delta.state.current === 'interrupted')) {
|
||||
browser.downloads.onChanged.removeListener(onChanged);
|
||||
|
||||
+14
-1
@@ -102,6 +102,14 @@
|
||||
const academicYear = yearEl ? yearEl.value : '';
|
||||
const token = tokenEl ? tokenEl.value : '';
|
||||
|
||||
// Semester/year come from the search dropdowns the user picked.
|
||||
// AcademicPeriod's selected option label (e.g. "1ST Semester") + year.
|
||||
const periodLabel = (periodEl && periodEl.selectedOptions && periodEl.selectedOptions[0])
|
||||
? periodEl.selectedOptions[0].textContent.trim()
|
||||
: '';
|
||||
const selectedPeriod = periodLabel || academicPeriod;
|
||||
const selectedYear = academicYear;
|
||||
|
||||
const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))];
|
||||
const descMap = {};
|
||||
for (const code of uniqueCodes) {
|
||||
@@ -123,7 +131,12 @@
|
||||
header = {};
|
||||
}
|
||||
|
||||
return { header, courses };
|
||||
// Semester goes into the header so the PDF shows the dropdown's term/year.
|
||||
header.semester = (selectedPeriod && selectedYear)
|
||||
? selectedPeriod + ' ' + selectedYear
|
||||
: (header.semester || '');
|
||||
|
||||
return { header, courses, period: selectedPeriod, year: selectedYear };
|
||||
}
|
||||
|
||||
// ---- On-page Download PDF button ----
|
||||
|
||||
@@ -29,7 +29,33 @@
|
||||
return lines.length ? lines : [''];
|
||||
}
|
||||
|
||||
// Wraps multi-line text (segments split on \n) into wrapped lines.
|
||||
// Format the raw schedule string to match the official PDF style:
|
||||
// comma-separate day codes, trim duplicated room suffixes, and split
|
||||
// multiple meeting segments onto separate lines.
|
||||
function formatSchedule(schedule) {
|
||||
let s = String(schedule || '').replace(/\s+/g, ' ').trim();
|
||||
if (!s) return s;
|
||||
// Collapse duplicated TC anywhere in the string.
|
||||
while (/TC\s+TC/.test(s)) s = s.replace(/TC\s+TC/g, 'TC');
|
||||
// Join adjacent day codes with commas.
|
||||
s = s.replace(/\b(M|Tu|T|W|Th|F|Sa|Su)\s+(M|Tu|T|W|Th|F|Sa|Su)\b/g, '$1,$2');
|
||||
// Split concatenated meeting segments (room ending in TC followed by a
|
||||
// day code) onto separate lines.
|
||||
s = s.replace(/(TC)\s+(?=(?:Su|Sa|Th|Tu|M|W|F|T)\b)/g, '$1\n');
|
||||
return s;
|
||||
}
|
||||
|
||||
// Reformat a full name to "Lastname, Firstname Middle" when the source gives
|
||||
// it as "Firstname Middle Lastname" (no comma). If a comma is already
|
||||
// present the name is returned unchanged.
|
||||
function formatFullName(name) {
|
||||
const s = String(name || '').trim();
|
||||
if (!s || /,/.test(s)) return s;
|
||||
const parts = s.split(/\s+/);
|
||||
if (parts.length < 2) return s;
|
||||
const last = parts.pop();
|
||||
return last + ', ' + parts.join(' ');
|
||||
}
|
||||
function wrapLines(text, font, size, maxWidth) {
|
||||
const out = [];
|
||||
String(text == null ? '' : text).split(/\n/).forEach((seg) => {
|
||||
@@ -38,12 +64,58 @@
|
||||
return out.length ? out : [''];
|
||||
}
|
||||
|
||||
// Compact a degree name for display:
|
||||
// "BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY" -> "BS IT".
|
||||
// Multi-word fields become initials; single-word fields stay title-cased.
|
||||
function compactProgram(program) {
|
||||
const s = String(program || '').trim().toUpperCase();
|
||||
let m = /^BACHELOR OF SCIENCE IN (.+)$/.exec(s);
|
||||
let prefix = 'BS';
|
||||
if (!m) {
|
||||
m = /^BACHELOR OF ARTS IN (.+)$/.exec(s);
|
||||
if (m) prefix = 'AB';
|
||||
}
|
||||
if (!m) return String(program || '').trim();
|
||||
const field = m[1];
|
||||
const words = field.split(/\s+/).filter((w) => !/^(AND|OF|THE|IN)$/.test(w));
|
||||
if (words.length <= 1) {
|
||||
const single = words[0] || field;
|
||||
return prefix + ' ' + single.charAt(0).toUpperCase() + single.slice(1).toLowerCase();
|
||||
}
|
||||
return prefix + ' ' + words.map((w) => w[0]).join('');
|
||||
}
|
||||
|
||||
// Map a semester to the official date range label.
|
||||
function deriveDateRange(semester) {
|
||||
const s = String(semester || '').toUpperCase();
|
||||
if (/1ST|FIRST/.test(s)) return 'AUGUST - JANUARY';
|
||||
if (/2ND|SECOND/.test(s)) return 'JANUARY - MAY';
|
||||
if (/SUMMER/.test(s)) return 'JUNE - JULY';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Load the bundled header logo (JPEG) in the extension background page.
|
||||
// Returns null in Node (tests) or if the asset is unavailable.
|
||||
async function loadLogo() {
|
||||
try {
|
||||
const url = (typeof browser !== 'undefined' && browser.runtime && browser.runtime.getURL)
|
||||
? browser.runtime.getURL('assets/logo.jpeg')
|
||||
: null;
|
||||
if (!url) return null;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) return null;
|
||||
return new Uint8Array(await resp.arrayBuffer());
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a Letter-size PDF replicating the official ISMIS Study Load layout:
|
||||
// title block (STUDENT'S STUDY LOAD, semester, date range, date enrolled),
|
||||
// a student info row (ID, name, program, Year Level), the course table
|
||||
// (GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer
|
||||
// (total units, printed-by). NOTE is the status; UNITS/NOTE are right-aligned;
|
||||
// schedule is stacked under the course description.
|
||||
// header logo, title, semester + date range, date enrolled, a student info
|
||||
// row (ID, name, program, Year Level), the course table (GROUP, COURSE,
|
||||
// COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer (total under the
|
||||
// UNITS column, printed-by). NOTE is the status; schedule is stacked under
|
||||
// the course description.
|
||||
async function buildStudyLoadPdf(data) {
|
||||
const PDFLib = getPDFLib();
|
||||
const { PDFDocument, StandardFonts, rgb } = PDFLib;
|
||||
@@ -55,13 +127,19 @@
|
||||
const font = doc.embedStandardFont(StandardFonts.Helvetica);
|
||||
const bold = doc.embedStandardFont(StandardFonts.HelveticaBold);
|
||||
const italic = doc.embedStandardFont(StandardFonts.HelveticaOblique);
|
||||
const margin = 50;
|
||||
const contentWidth = 612 - margin * 2;
|
||||
let y = 792 - 50;
|
||||
const margin = 36;
|
||||
const contentWidth = 539.3; // matches reference right edge 575.3
|
||||
let y = 792 - 10; // near the very top, matching the reference logo position
|
||||
|
||||
const black = rgb(0, 0, 0);
|
||||
const gray = rgb(0.3, 0.3, 0.3);
|
||||
|
||||
const logoBytes = await loadLogo();
|
||||
let logoImg = null;
|
||||
if (logoBytes) {
|
||||
try { logoImg = await doc.embedJpg(logoBytes); } catch (e) { logoImg = null; }
|
||||
}
|
||||
|
||||
function center(text, size, f, dy) {
|
||||
const w = f.widthOfTextAtSize(text, size);
|
||||
page.drawText(text, { x: (612 - w) / 2, y, size, font: f, color: black });
|
||||
@@ -76,39 +154,62 @@
|
||||
}
|
||||
|
||||
// ---- Header ----
|
||||
center("STUDENT'S STUDY LOAD", 9.5, bold, 18);
|
||||
if (header.semester) center(header.semester, 9, font, 15);
|
||||
if (header.dateRange) center(header.dateRange, 9, font, 15);
|
||||
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13);
|
||||
if (logoImg) {
|
||||
const logoW = 292.5;
|
||||
const logoH = logoW * (logoImg.height / logoImg.width);
|
||||
page.drawImage(logoImg, { x: (612 - logoW) / 2, y: y - logoH, width: logoW, height: logoH });
|
||||
y -= logoH + 8;
|
||||
}
|
||||
center("STUDENT'S STUDY LOAD", 9.5, bold, 14);
|
||||
if (header.semester) center(String(header.semester).toUpperCase(), 9, font, 13.5);
|
||||
const dateRange = header.dateRange || deriveDateRange(header.semester);
|
||||
if (dateRange) center(dateRange, 9, font, 13.5);
|
||||
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13.5);
|
||||
|
||||
// Student info row: ID (bold), name, program, "Year Level: N"
|
||||
const studentParts = [];
|
||||
if (header.studentId) studentParts.push({ text: header.studentId, f: bold });
|
||||
if (header.fullName) studentParts.push({ text: header.fullName, f: font });
|
||||
if (header.program) studentParts.push({ text: header.program, f: font });
|
||||
if (header.yearLevel) studentParts.push({ text: 'Year Level: ' + header.yearLevel, f: font });
|
||||
if (studentParts.length) {
|
||||
let sx = margin;
|
||||
for (const part of studentParts) {
|
||||
const w = part.f.widthOfTextAtSize(part.text, 9);
|
||||
page.drawText(part.text, { x: sx, y, size: 9, font: part.f, color: black });
|
||||
sx += w + 18;
|
||||
}
|
||||
y -= 16;
|
||||
// Student info row, laid out in fixed zones across the page (matching the
|
||||
// reference): ID (bold) left, name at ~100, program at ~338, and
|
||||
// "Year Level: N" right-aligned to the right content edge (575.3).
|
||||
const yearLevelText = header.yearLevel ? 'Year Level: ' + header.yearLevel : '';
|
||||
if (header.studentId) page.drawText(header.studentId, { x: margin, y, size: 9, font: bold, color: black });
|
||||
if (header.fullName) page.drawText(formatFullName(header.fullName), { x: 100, y, size: 9, font, color: black });
|
||||
if (header.program) page.drawText(compactProgram(header.program), { x: 338, y, size: 9, font, color: black });
|
||||
if (yearLevelText) {
|
||||
const w = font.widthOfTextAtSize(yearLevelText, 9);
|
||||
page.drawText(yearLevelText, { x: margin + contentWidth - w, y, size: 9, font, color: black });
|
||||
}
|
||||
if (header.studentId || header.fullName || header.program || yearLevelText) y -= 14.5;
|
||||
|
||||
// ---- Table: GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE ----
|
||||
// Column boundaries tuned to the reference PDF (left edge 36, right edge 575.3).
|
||||
const cols = [
|
||||
{ key: 'group', label: 'GROUP', x: margin, w: 26, align: 'left' },
|
||||
{ key: 'courseCode', label: 'COURSE', x: margin + 26, w: 46, align: 'left' },
|
||||
{ key: 'description', label: 'COURSE DESCRIPTION', x: margin + 72, w: 189, align: 'left' },
|
||||
{ key: 'teacher', label: 'TEACHER', x: margin + 261, w: 123, align: 'left' },
|
||||
{ key: 'units', label: 'UNITS', x: margin + 384, w: 31, align: 'right' },
|
||||
{ key: 'status', label: 'NOTE', x: margin + 415, w: 97, align: 'right' }
|
||||
{ key: 'group', label: 'GROUP', x: 36.0, w: 28.8, align: 'left' },
|
||||
{ key: 'courseCode', label: 'COURSE', x: 64.8, w: 47.6, align: 'left' },
|
||||
{ key: 'description', label: 'COURSE DESCRIPTION', x: 112.4, w: 201.4, align: 'left' },
|
||||
{ key: 'teacher', label: 'TEACHER', x: 313.8, w: 127.6, align: 'left' },
|
||||
{ key: 'units', label: 'UNITS', x: 441.4, w: 29.5, align: 'right' },
|
||||
{ key: 'status', label: 'NOTE', x: 470.9, w: 104.4, align: 'right' }
|
||||
];
|
||||
const cellSize = 8;
|
||||
const headerSize = 8;
|
||||
const lineH = 10;
|
||||
const lineH = 12;
|
||||
|
||||
// Header label positions tuned for Helvetica: left/center aligned within
|
||||
// each column so labels never overlap.
|
||||
function headerLabelXFor() {
|
||||
const widths = {};
|
||||
for (const c of cols) {
|
||||
const key = c.label.toUpperCase().replace(/\s+/g, '_');
|
||||
widths[key] = bold.widthOfTextAtSize(c.label, headerSize);
|
||||
}
|
||||
return {
|
||||
GROUP: cols[0].x,
|
||||
COURSE: cols[1].x + 5,
|
||||
COURSE_DESCRIPTION: cols[2].x + (cols[2].w - widths.COURSE_DESCRIPTION) / 2,
|
||||
TEACHER: cols[3].x + (cols[3].w - widths.TEACHER) / 2,
|
||||
UNITS: cols[4].x + (cols[4].w - widths.UNITS) / 2,
|
||||
NOTE: cols[5].x + (cols[5].w - widths.NOTE) / 2
|
||||
};
|
||||
}
|
||||
|
||||
function rowLines(cells) {
|
||||
return cols.map((c) => {
|
||||
@@ -116,7 +217,7 @@
|
||||
if (c.key === 'description') {
|
||||
lines = wrapLines(cells.description, font, cellSize, c.w - 6);
|
||||
const sched = String(cells.schedule || '');
|
||||
if (sched) lines.push(...wrapText(sched, font, cellSize, c.w - 6));
|
||||
if (sched) lines.push(...wrapLines(sched, font, cellSize, c.w - 6));
|
||||
} else {
|
||||
lines = wrapLines(cells[c.key], font, cellSize, c.w - 6);
|
||||
}
|
||||
@@ -128,40 +229,72 @@
|
||||
const f = isHeader ? bold : font;
|
||||
const size = isHeader ? headerSize : cellSize;
|
||||
const linesPerCol = rowLines(cells);
|
||||
const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * lineH + 8;
|
||||
|
||||
if (y - rowHeight < 50) {
|
||||
page = doc.addPage([612, 792]);
|
||||
y = 792 - 50;
|
||||
drawHeaderRow();
|
||||
}
|
||||
|
||||
if (isHeader) {
|
||||
// top line above the header row
|
||||
hline(margin, margin + contentWidth, y);
|
||||
// Reference header row: only a top and bottom horizontal rule; no
|
||||
// vertical dividers. Text baseline at y; top rule 1.8pt above the
|
||||
// text bbox top; bottom rule 11.5pt below the text bbox top. For
|
||||
// 8pt Helvetica the text bbox top sits ~8.6pt above the baseline,
|
||||
// so the lines are offset by +9.6 (top) and -3.7 (bottom) relative
|
||||
// to y. Data rows start 13pt below the header text bbox top.
|
||||
const topY = y + 9.6;
|
||||
const botY = y - 3.7;
|
||||
if (botY < 50) {
|
||||
page = doc.addPage([612, 792]);
|
||||
y = 792 - 10;
|
||||
return drawHeaderRow();
|
||||
}
|
||||
|
||||
hline(margin, margin + contentWidth, topY);
|
||||
|
||||
const labelXs = headerLabelXFor();
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
const c = cols[i];
|
||||
const labelKey = c.label.toUpperCase().replace(/\s+/g, '_');
|
||||
const lx = labelXs[labelKey];
|
||||
const lines = linesPerCol[i];
|
||||
page.drawText(lines[0], { x: lx, y, size, font: f, color: black });
|
||||
}
|
||||
|
||||
hline(margin, margin + contentWidth, botY);
|
||||
y -= 13.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Data rows: fixed 24.5pt baseline-to-baseline spacing. Description +
|
||||
// schedule stack with 12pt line spacing. Expand the row height when
|
||||
// the schedule or wrapped description needs more lines.
|
||||
const maxLines = Math.max(...linesPerCol.map((l) => l.length));
|
||||
const rowHeight = Math.max(24.5, maxLines * lineH + 0.5);
|
||||
if (y - rowHeight < 50) {
|
||||
page = doc.addPage([612, 792]);
|
||||
y = 792 - 10;
|
||||
drawHeaderRow();
|
||||
}
|
||||
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
const c = cols[i];
|
||||
const lines = linesPerCol[i];
|
||||
let ty = y - 9;
|
||||
let ty = y;
|
||||
for (const line of lines) {
|
||||
if (c.align === 'right') {
|
||||
if (c.key === 'units') {
|
||||
const w = f.widthOfTextAtSize(line, size);
|
||||
page.drawText(line, { x: c.x + c.w - 3 - w, y: ty, size, font: f, color: black });
|
||||
page.drawText(line, { x: c.x + c.w - w, y: ty, size, font: f, color: black });
|
||||
} else if (c.key === 'status') {
|
||||
page.drawText(line, { x: c.x + 35, y: ty, size, font: f, color: black });
|
||||
} else {
|
||||
page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black });
|
||||
let dx = 3;
|
||||
if (c.key === 'group') dx = 12;
|
||||
else if (c.key === 'courseCode') dx = 8;
|
||||
else if (c.key === 'teacher') dx = 3.5;
|
||||
else if (c.key === 'description') dx = 3.5;
|
||||
page.drawText(line, { x: c.x + dx, y: ty, size, font: f, color: black });
|
||||
}
|
||||
ty -= lineH;
|
||||
}
|
||||
}
|
||||
|
||||
y -= rowHeight;
|
||||
|
||||
if (isHeader) {
|
||||
// line below the header row
|
||||
hline(margin, margin + contentWidth, y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -178,8 +311,8 @@
|
||||
group: course.group,
|
||||
courseCode: course.courseCode,
|
||||
description: course.description,
|
||||
schedule: course.schedule,
|
||||
teacher: course.teacher,
|
||||
schedule: formatSchedule(course.schedule),
|
||||
teacher: course.teacher || course.faculty,
|
||||
units: course.units,
|
||||
status: course.status
|
||||
};
|
||||
@@ -189,22 +322,26 @@
|
||||
// ---- Footer ----
|
||||
if (y < 90) {
|
||||
page = doc.addPage([612, 792]);
|
||||
y = 792 - 50;
|
||||
y = 792 - 10;
|
||||
}
|
||||
y -= 8;
|
||||
y -= 12;
|
||||
if (data.totalUnits) {
|
||||
const w = bold.widthOfTextAtSize(data.totalUnits, 9);
|
||||
page.drawText(data.totalUnits, { x: margin + contentWidth - w, y, size: 9, font: bold, color: black });
|
||||
y -= 14;
|
||||
// Total sits under the UNITS column, right-aligned within it.
|
||||
const unitsCol = cols[4];
|
||||
const w = bold.widthOfTextAtSize(data.totalUnits, 8);
|
||||
// Short underline above the total, spanning the UNITS column.
|
||||
hline(unitsCol.x, unitsCol.x + unitsCol.w, y + 9);
|
||||
page.drawText(data.totalUnits, { x: unitsCol.x + unitsCol.w - w, y, size: 8, font: bold, color: black });
|
||||
y -= 12;
|
||||
}
|
||||
hline(margin, margin + contentWidth, y);
|
||||
y -= 12;
|
||||
const printedBy = 'Printed by: ' + (header.studentId || '') + ' on ' + new Date().toLocaleDateString();
|
||||
const pw = font.widthOfTextAtSize(printedBy, 8);
|
||||
page.drawText(printedBy, { x: margin + contentWidth - pw, y, size: 8, font, color: gray });
|
||||
page.drawText(printedBy, { x: margin + contentWidth - pw, y, size: 8, font, color: black });
|
||||
|
||||
return doc.save(); // returns Promise<Uint8Array>
|
||||
}
|
||||
|
||||
return { buildStudyLoadPdf };
|
||||
return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName };
|
||||
});
|
||||
|
||||
+39
-1
@@ -5,7 +5,7 @@ const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
global.PDFLib = require('pdf-lib');
|
||||
const { buildStudyLoadPdf } = require('../pdf.js');
|
||||
const { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName } = require('../pdf.js');
|
||||
|
||||
const sampleData = {
|
||||
header: {
|
||||
@@ -49,4 +49,42 @@ test('buildStudyLoadPdf returns valid PDF bytes', async () => {
|
||||
const text = extractPdfText(bytes);
|
||||
assert.ok(text.includes('CIS 2101'), 'PDF text should contain course code CIS 2101');
|
||||
assert.ok(text.includes("STUDENT'S STUDY LOAD"), 'PDF text should contain the title');
|
||||
assert.ok(text.includes('CHRISTIAN MADERAZO'), 'PDF text should contain the teacher');
|
||||
assert.ok(text.includes('BS IT'), 'PDF text should contain the compacted program');
|
||||
});
|
||||
|
||||
test('compactProgram shortens degree names', () => {
|
||||
assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY'), 'BS IT');
|
||||
assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN COMPUTER SCIENCE'), 'BS CS');
|
||||
assert.strictEqual(compactProgram('BACHELOR OF ARTS IN POLITICAL SCIENCE'), 'AB PS');
|
||||
assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN ACCOUNTANCY'), 'BS Accountancy');
|
||||
assert.strictEqual(compactProgram('UNKNOWN PROGRAM'), 'UNKNOWN PROGRAM');
|
||||
});
|
||||
|
||||
test('formatSchedule normalizes schedule text', () => {
|
||||
assert.strictEqual(formatSchedule('M W 07:30 AM - 10:00 AM LB447TC TC'), 'M,W 07:30 AM - 10:00 AM LB447TC');
|
||||
assert.strictEqual(formatSchedule('T Th 09:00 AM - 10:30 AM LB443TC TC'), 'T,Th 09:00 AM - 10:30 AM LB443TC');
|
||||
assert.strictEqual(formatSchedule('M,W 03:00 PM - 05:30 PM LB443TC'), 'M,W 03:00 PM - 05:30 PM LB443TC');
|
||||
assert.strictEqual(
|
||||
formatSchedule('T 07:30 AM - 10:30 AM LB448TC TC Th 07:30 AM - 10:30 AM LB448TC'),
|
||||
'T 07:30 AM - 10:30 AM LB448TC\nTh 07:30 AM - 10:30 AM LB448TC'
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatSchedule('W 01:30 PM - 04:30 PM LB448TC TC M 01:30 PM - 04:30 PM LB448TC'),
|
||||
'W 01:30 PM - 04:30 PM LB448TC\nM 01:30 PM - 04:30 PM LB448TC'
|
||||
);
|
||||
});
|
||||
|
||||
test('formatFullName reorders first-last names to last-first', () => {
|
||||
assert.strictEqual(formatFullName('ADRIAN ALFRED CHIONG BONPIN'), 'BONPIN, ADRIAN ALFRED CHIONG');
|
||||
assert.strictEqual(formatFullName('BONPIN, ADRIAN ALFRED CHIONG'), 'BONPIN, ADRIAN ALFRED CHIONG');
|
||||
assert.strictEqual(formatFullName('JOHN DOE'), 'DOE, JOHN');
|
||||
assert.strictEqual(formatFullName(''), '');
|
||||
});
|
||||
|
||||
test('deriveDateRange maps semesters', () => {
|
||||
assert.strictEqual(deriveDateRange('1ST Semester 2026'), 'AUGUST - JANUARY');
|
||||
assert.strictEqual(deriveDateRange('2ND Semester 2026'), 'JANUARY - MAY');
|
||||
assert.strictEqual(deriveDateRange('Summer 2026'), 'JUNE - JULY');
|
||||
assert.strictEqual(deriveDateRange(''), '');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user