ISMIS Helper: cross-browser build, CI, PDF fixes, Chrome download fix #1

Merged
adrianbonpin merged 55 commits from feat/ismis-helper into prod 2026-08-18 19:13:50 +00:00
3 changed files with 92 additions and 16 deletions
Showing only changes of commit 792974bd32 - Show all commits
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+74 -15
View File
@@ -38,12 +38,58 @@
return out.length ? out : ['']; 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: // Builds a Letter-size PDF replicating the official ISMIS Study Load layout:
// title block (STUDENT'S STUDY LOAD, semester, date range, date enrolled), // header logo, title, semester + date range, date enrolled, a student info
// a student info row (ID, name, program, Year Level), the course table // row (ID, name, program, Year Level), the course table (GROUP, COURSE,
// (GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer // COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer (total under the
// (total units, printed-by). NOTE is the status; UNITS/NOTE are right-aligned; // UNITS column, printed-by). NOTE is the status; schedule is stacked under
// schedule is stacked under the course description. // the course description.
async function buildStudyLoadPdf(data) { async function buildStudyLoadPdf(data) {
const PDFLib = getPDFLib(); const PDFLib = getPDFLib();
const { PDFDocument, StandardFonts, rgb } = PDFLib; const { PDFDocument, StandardFonts, rgb } = PDFLib;
@@ -62,6 +108,12 @@
const black = rgb(0, 0, 0); const black = rgb(0, 0, 0);
const gray = rgb(0.3, 0.3, 0.3); 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) { function center(text, size, f, dy) {
const w = f.widthOfTextAtSize(text, size); const w = f.widthOfTextAtSize(text, size);
page.drawText(text, { x: (612 - w) / 2, y, size, font: f, color: black }); page.drawText(text, { x: (612 - w) / 2, y, size, font: f, color: black });
@@ -76,16 +128,23 @@
} }
// ---- Header ---- // ---- Header ----
center("STUDENT'S STUDY LOAD", 9.5, bold, 18); if (logoImg) {
if (header.semester) center(header.semester, 9, font, 15); const logoW = 292.5;
if (header.dateRange) center(header.dateRange, 9, font, 15); const logoH = logoW * (logoImg.height / logoImg.width);
page.drawImage(logoImg, { x: (612 - logoW) / 2, y: y - logoH, width: logoW, height: logoH });
y -= logoH + 14;
}
center("STUDENT'S STUDY LOAD", 9.5, bold, 16);
if (header.semester) center(String(header.semester).toUpperCase(), 9, font, 14);
const dateRange = header.dateRange || deriveDateRange(header.semester);
if (dateRange) center(dateRange, 9, font, 14);
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13); if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13);
// Student info row: ID (bold), name, program, "Year Level: N" // Student info row: ID (bold), name, program (compacted), "Year Level: N"
const studentParts = []; const studentParts = [];
if (header.studentId) studentParts.push({ text: header.studentId, f: bold }); if (header.studentId) studentParts.push({ text: header.studentId, f: bold });
if (header.fullName) studentParts.push({ text: header.fullName, f: font }); if (header.fullName) studentParts.push({ text: header.fullName, f: font });
if (header.program) studentParts.push({ text: header.program, f: font }); if (header.program) studentParts.push({ text: compactProgram(header.program), f: font });
if (header.yearLevel) studentParts.push({ text: 'Year Level: ' + header.yearLevel, f: font }); if (header.yearLevel) studentParts.push({ text: 'Year Level: ' + header.yearLevel, f: font });
if (studentParts.length) { if (studentParts.length) {
let sx = margin; let sx = margin;
@@ -137,7 +196,6 @@
} }
if (isHeader) { if (isHeader) {
// top line above the header row
hline(margin, margin + contentWidth, y); hline(margin, margin + contentWidth, y);
} }
@@ -159,7 +217,6 @@
y -= rowHeight; y -= rowHeight;
if (isHeader) { if (isHeader) {
// line below the header row
hline(margin, margin + contentWidth, y); hline(margin, margin + contentWidth, y);
} }
return true; return true;
@@ -179,7 +236,7 @@
courseCode: course.courseCode, courseCode: course.courseCode,
description: course.description, description: course.description,
schedule: course.schedule, schedule: course.schedule,
teacher: course.teacher, teacher: course.teacher || course.faculty,
units: course.units, units: course.units,
status: course.status status: course.status
}; };
@@ -193,8 +250,10 @@
} }
y -= 8; y -= 8;
if (data.totalUnits) { if (data.totalUnits) {
// Total sits under the UNITS column, right-aligned within it.
const unitsCol = cols[4];
const w = bold.widthOfTextAtSize(data.totalUnits, 9); const w = bold.widthOfTextAtSize(data.totalUnits, 9);
page.drawText(data.totalUnits, { x: margin + contentWidth - w, y, size: 9, font: bold, color: black }); page.drawText(data.totalUnits, { x: unitsCol.x + unitsCol.w - 3 - w, y, size: 9, font: bold, color: black });
y -= 14; y -= 14;
} }
hline(margin, margin + contentWidth, y); hline(margin, margin + contentWidth, y);
@@ -206,5 +265,5 @@
return doc.save(); // returns Promise<Uint8Array> return doc.save(); // returns Promise<Uint8Array>
} }
return { buildStudyLoadPdf }; return { buildStudyLoadPdf, compactProgram, deriveDateRange };
}); });
+18 -1
View File
@@ -5,7 +5,7 @@ const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const { execFileSync } = require('node:child_process'); const { execFileSync } = require('node:child_process');
global.PDFLib = require('pdf-lib'); global.PDFLib = require('pdf-lib');
const { buildStudyLoadPdf } = require('../pdf.js'); const { buildStudyLoadPdf, compactProgram, deriveDateRange } = require('../pdf.js');
const sampleData = { const sampleData = {
header: { header: {
@@ -49,4 +49,21 @@ test('buildStudyLoadPdf returns valid PDF bytes', async () => {
const text = extractPdfText(bytes); const text = extractPdfText(bytes);
assert.ok(text.includes('CIS 2101'), 'PDF text should contain course code CIS 2101'); 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("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('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(''), '');
}); });