diff --git a/content.js b/content.js index 6bd3eb0..0da0d55 100644 --- a/content.js +++ b/content.js @@ -164,14 +164,25 @@ try { btn.disabled = true; btn.innerHTML = ' 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.'); diff --git a/pdf.js b/pdf.js index 127b5bd..604389e 100644 --- a/pdf.js +++ b/pdf.js @@ -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 } - return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, truncateToWidth }; + return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, formatTeacher, truncateToWidth }; }); diff --git a/popup.css b/popup.css index b4318f9..999728a 100644 --- a/popup.css +++ b/popup.css @@ -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; } diff --git a/popup.html b/popup.html index f28c2f4..f820df7 100644 --- a/popup.html +++ b/popup.html @@ -35,6 +35,11 @@

PDF Settings

+ + +

Leave blank to use the scraped value (auto-adjusted in Summer).

diff --git a/popup.js b/popup.js index 5de4e9e..c601f31 100644 --- a/popup.js +++ b/popup.js @@ -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()); })(); diff --git a/test/pdf.test.js b/test/pdf.test.js index be95d7b..b02362b 100644 --- a/test/pdf.test.js +++ b/test/pdf.test.js @@ -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');