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
6 changed files with 97 additions and 15 deletions
Showing only changes of commit cb47d6faac - Show all commits
+14 -3
View File
@@ -164,14 +164,25 @@
try {
btn.disabled = true;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Downloading...';
const { header, courses } = await scrapeStudyLoad();
const { header, courses, period: selectedPeriod } = await scrapeStudyLoad();
const included = ISMIStools.filterStudyLoadCourses(courses);
if (!included.length) throw new Error('No enrolled courses found.');
const totalUnits = ISMIStools.sumUnits(included);
const { truncateDescription } = await browser.storage.local.get('truncateDescription');
const settings = await browser.storage.local.get(['truncateDescription', 'showYearLevel', 'customYearLevel']);
// Resolve the Year Level: custom value wins; otherwise prefer the scraped
// value, smart-adjusted (minus one) during SUMMER when the year level has
// not yet advanced.
header.showYearLevel = settings.showYearLevel !== false;
const custom = String(settings.customYearLevel || '').trim();
if (custom) {
header.yearLevel = custom;
} else if (/summer/i.test(selectedPeriod || '')) {
const n = parseInt(header.yearLevel, 10);
if (Number.isFinite(n) && n > 1) header.yearLevel = String(n - 1);
}
const res = await browser.runtime.sendMessage({
type: 'generate-pdf',
data: { header, courses: included, totalUnits, truncateDescription: truncateDescription !== false }
data: { header, courses: included, totalUnits, truncateDescription: settings.truncateDescription !== false }
});
if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed');
showToast('PDF downloaded.');
+49 -7
View File
@@ -56,6 +56,44 @@
const last = parts.pop();
return last + ', ' + parts.join(' ');
}
// Format a faculty/teacher name to match the official PDF's style:
// "First [MiddleInitial] Last" (no comma). Handles a source like
// "Surname, First Middle" by dropping full middle names while keeping
// honorifics/titles (e.g. FR.) and single-letter middle initials.
const HONORIFICS = new Set(['FR', 'REV', 'DR', 'SR', 'BR', 'PROF', 'ENGR', 'ATTY', 'HON', 'MR', 'MS', 'MRS', 'BRO', 'SIS']);
function formatTeacher(name) {
let v = String(name || '').trim();
if (!v) return v;
let surname = '', given = [];
if (/,/.test(v)) {
const idx = v.lastIndexOf(',');
surname = v.slice(0, idx).trim();
given = v.slice(idx + 1).trim().split(/\s+/).filter(Boolean);
} else {
const tok = v.split(/\s+/).filter(Boolean);
if (tok.length === 0) return v;
if (tok.length === 1) return v;
given = tok.slice(0, -1);
surname = tok[tok.length - 1];
}
const out = [];
let seenFirstName = false;
for (const tok of given) {
const bare = tok.replace(/\.$/, '').toUpperCase();
const isTitle = HONORIFICS.has(bare);
const isInitial = tok.length === 1 || (bare.length === 1 && /\.$/.test(tok));
if (isTitle || isInitial) {
out.push(tok);
} else if (!seenFirstName) {
out.push(tok);
seenFirstName = true;
}
// full middle names (after the first name) are dropped
}
if (surname) out.push(surname);
return out.join(' ');
}
function wrapLines(text, font, size, maxWidth) {
const out = [];
String(text == null ? '' : text).split(/\n/).forEach((seg) => {
@@ -250,7 +288,11 @@
// 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 : '';
// Year Level is shown by default; the extension can hide it or override the
// value via settings (header.showYearLevel / header.yearLevel).
const yearLevelText = (header.showYearLevel !== false && 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 });
@@ -401,7 +443,7 @@
courseCode: course.courseCode,
description: course.description,
schedule: formatSchedule(course.schedule),
teacher: course.teacher || course.faculty,
teacher: formatTeacher(course.teacher || course.faculty),
units: course.units,
status: course.status
};
@@ -413,14 +455,14 @@
page = doc.addPage([612, 792]);
y = 792 - 10;
}
y -= 12;
if (data.totalUnits) {
// Total sits under the UNITS column, centered within it (matches reference).
// Total sits one standard row height below the last course row (matching
// the reference, which has a ~24.5pt gap), centered within the UNITS col.
const unitsCol = cols[4];
const w = bold.widthOfTextAtSize(data.totalUnits, 8);
const w = font.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) / 2, y, size: 8, font: bold, color: black });
page.drawText(data.totalUnits, { x: unitsCol.x + (unitsCol.w - w) / 2, y, size: 8, font, color: black });
y -= 12;
}
hline(margin, margin + contentWidth, y);
@@ -432,5 +474,5 @@
return doc.save(); // returns Promise<Uint8Array>
}
return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, truncateToWidth };
return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, formatTeacher, truncateToWidth };
});
+6
View File
@@ -48,3 +48,9 @@ body {
border: 1px solid #ccc; border-radius: 4px;
}
.form .row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.tab .row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.tab .row input[type="text"] {
flex: 1; min-width: 0; padding: 4px 6px;
border: 1px solid #ccc; border-radius: 4px;
}
.hint { font-size: 11px; color: #777; margin-top: 4px; }
+5
View File
@@ -35,6 +35,11 @@
<section class="tab">
<h2>PDF Settings</h2>
<label class="row"><input type="checkbox" id="truncateToggle"> Truncate long course descriptions (official PDF style)</label>
<label class="row"><input type="checkbox" id="showYearToggle"> Show Year Level on the PDF</label>
<label class="row">Custom Year Level (optional):
<input type="text" id="customYear" placeholder="e.g. 3">
</label>
<p class="hint">Leave blank to use the scraped value (auto-adjusted in Summer).</p>
</section>
<script src="popup.js"></script>
+12 -4
View File
@@ -19,13 +19,21 @@ $('#masterToggle').addEventListener('change', async (e) => {
});
// ---- PDF settings: truncate long course descriptions (default ON) ----
async function loadTruncate() {
const { truncateDescription } = await browser.storage.local.get('truncateDescription');
$('#truncateToggle').checked = truncateDescription !== false;
async function loadPdfSettings() {
const s = await browser.storage.local.get(['truncateDescription', 'showYearLevel', 'customYearLevel']);
$('#truncateToggle').checked = s.truncateDescription !== false;
$('#showYearToggle').checked = s.showYearLevel !== false;
$('#customYear').value = s.customYearLevel || '';
}
$('#truncateToggle').addEventListener('change', async (e) => {
await browser.storage.local.set({ truncateDescription: e.target.checked });
});
$('#showYearToggle').addEventListener('change', async (e) => {
await browser.storage.local.set({ showYearLevel: e.target.checked });
});
$('#customYear').addEventListener('change', async (e) => {
await browser.storage.local.set({ customYearLevel: e.target.value.trim() });
});
// ---- Profiles ----
async function getProfiles() {
@@ -113,6 +121,6 @@ $('#saveProfile').addEventListener('click', async () => {
// ---- Init ----
(async function init() {
await loadMaster();
await loadTruncate();
await loadPdfSettings();
renderProfiles(await getProfiles());
})();
+11 -1
View File
@@ -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, compactProgram, deriveDateRange, formatSchedule, formatFullName, truncateToWidth } = require('../pdf.js');
const { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, formatTeacher, truncateToWidth } = require('../pdf.js');
const sampleData = {
header: {
@@ -82,6 +82,16 @@ test('formatFullName reorders first-last names to last-first', () => {
assert.strictEqual(formatFullName(''), '');
});
test('formatTeacher renders names First [MiddleInitial] Last like the reference', () => {
assert.strictEqual(formatTeacher('FILLO, FR. PETER'), 'FR. PETER FILLO');
assert.strictEqual(formatTeacher('BACALSO, REUELLA J. AWAYAN'), 'REUELLA J. BACALSO');
assert.strictEqual(formatTeacher('PEPITO, GLENN BULLECER'), 'GLENN PEPITO');
assert.strictEqual(formatTeacher('MADERAZO, CHRISTIAN'), 'CHRISTIAN MADERAZO');
assert.strictEqual(formatTeacher('DELA PAZ, KHENT'), 'KHENT DELA PAZ');
assert.strictEqual(formatTeacher('CHRISTIAN MADERAZO'), 'CHRISTIAN MADERAZO');
assert.strictEqual(formatTeacher(''), '');
});
test('deriveDateRange maps semesters', () => {
assert.strictEqual(deriveDateRange('1ST Semester 2026'), 'AUGUST - JANUARY');
assert.strictEqual(deriveDateRange('2ND Semester 2026'), 'JANUARY - MAY');