ISMIS Helper: cross-browser build, CI, PDF fixes, Chrome download fix #1
+14
-3
@@ -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.');
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user