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
5 changed files with 57 additions and 4 deletions
Showing only changes of commit 106900860a - Show all commits
+2 -1
View File
@@ -168,9 +168,10 @@
const included = ISMIStools.filterStudyLoadCourses(courses); const included = ISMIStools.filterStudyLoadCourses(courses);
if (!included.length) throw new Error('No enrolled courses found.'); if (!included.length) throw new Error('No enrolled courses found.');
const totalUnits = ISMIStools.sumUnits(included); const totalUnits = ISMIStools.sumUnits(included);
const { truncateDescription } = await browser.storage.local.get('truncateDescription');
const res = await browser.runtime.sendMessage({ const res = await browser.runtime.sendMessage({
type: 'generate-pdf', type: 'generate-pdf',
data: { header, courses: included, totalUnits } data: { header, courses: included, totalUnits, truncateDescription: truncateDescription !== false }
}); });
if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed'); if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed');
showToast('PDF downloaded.'); showToast('PDF downloaded.');
+26 -2
View File
@@ -64,6 +64,22 @@
return out.length ? out : ['']; return out.length ? out : [''];
} }
// Truncate a string to the longest prefix that fits within maxWidth at the
// given size, hard-cutting mid-word with no ellipsis. Mirrors the official
// PDF which cuts off course descriptions that exceed the column width.
function truncateToWidth(text, font, size, maxWidth) {
const s = String(text == null ? '' : text).trim();
if (!s) return '';
if (font.widthOfTextAtSize(s, size) <= maxWidth) return s;
let lo = 0, hi = s.length;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (font.widthOfTextAtSize(s.slice(0, mid), size) <= maxWidth) lo = mid;
else hi = mid - 1;
}
return s.slice(0, lo);
}
// Compact a degree name for display: // Compact a degree name for display:
// "BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY" -> "BS IT". // "BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY" -> "BS IT".
// Multi-word fields become initials; single-word fields stay title-cased. // Multi-word fields become initials; single-word fields stay title-cased.
@@ -177,6 +193,9 @@
const { PDFDocument, StandardFonts, rgb } = PDFLib; const { PDFDocument, StandardFonts, rgb } = PDFLib;
const header = data.header || {}; const header = data.header || {};
const courses = data.courses || []; const courses = data.courses || [];
// Truncate long course descriptions to a single line matching the official
// PDF (default ON). Disabling allows descriptions to wrap.
const truncateDescription = data.truncateDescription !== false;
const doc = await PDFDocument.create(); const doc = await PDFDocument.create();
let page = doc.addPage([612, 792]); // Letter let page = doc.addPage([612, 792]); // Letter
@@ -278,7 +297,12 @@
return cols.map((c) => { return cols.map((c) => {
let lines; let lines;
if (c.key === 'description') { if (c.key === 'description') {
lines = wrapLines(cells.description, font, cellSize, c.w - 6); if (truncateDescription) {
// Single line, hard-truncated to fit the column (official PDF style).
lines = [truncateToWidth(cells.description, font, cellSize, c.w - 3)];
} else {
lines = wrapLines(cells.description, font, cellSize, c.w - 6);
}
const sched = String(cells.schedule || ''); const sched = String(cells.schedule || '');
if (sched) lines.push(...wrapLines(sched, font, cellSize, c.w - 6)); if (sched) lines.push(...wrapLines(sched, font, cellSize, c.w - 6));
} else { } else {
@@ -408,5 +432,5 @@
return doc.save(); // returns Promise<Uint8Array> return doc.save(); // returns Promise<Uint8Array>
} }
return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName }; return { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, truncateToWidth };
}); });
+5
View File
@@ -32,6 +32,11 @@
</div> </div>
</section> </section>
<section class="tab">
<h2>PDF Settings</h2>
<label class="row"><input type="checkbox" id="truncateToggle"> Truncate long course descriptions (official PDF style)</label>
</section>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>
+10
View File
@@ -18,6 +18,15 @@ $('#masterToggle').addEventListener('change', async (e) => {
$('#masterLabel').textContent = e.target.checked ? 'Enabled' : 'Disabled'; $('#masterLabel').textContent = e.target.checked ? 'Enabled' : 'Disabled';
}); });
// ---- PDF settings: truncate long course descriptions (default ON) ----
async function loadTruncate() {
const { truncateDescription } = await browser.storage.local.get('truncateDescription');
$('#truncateToggle').checked = truncateDescription !== false;
}
$('#truncateToggle').addEventListener('change', async (e) => {
await browser.storage.local.set({ truncateDescription: e.target.checked });
});
// ---- Profiles ---- // ---- Profiles ----
async function getProfiles() { async function getProfiles() {
const { profiles } = await browser.storage.local.get(PROFILES_KEY); const { profiles } = await browser.storage.local.get(PROFILES_KEY);
@@ -104,5 +113,6 @@ $('#saveProfile').addEventListener('click', async () => {
// ---- Init ---- // ---- Init ----
(async function init() { (async function init() {
await loadMaster(); await loadMaster();
await loadTruncate();
renderProfiles(await getProfiles()); renderProfiles(await getProfiles());
})(); })();
+14 -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, compactProgram, deriveDateRange, formatSchedule, formatFullName } = require('../pdf.js'); const { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, truncateToWidth } = require('../pdf.js');
const sampleData = { const sampleData = {
header: { header: {
@@ -88,3 +88,16 @@ test('deriveDateRange maps semesters', () => {
assert.strictEqual(deriveDateRange('Summer 2026'), 'JUNE - JULY'); assert.strictEqual(deriveDateRange('Summer 2026'), 'JUNE - JULY');
assert.strictEqual(deriveDateRange(''), ''); assert.strictEqual(deriveDateRange(''), '');
}); });
test('truncateToWidth hard-cuts text to fit within maxWidth', async () => {
const doc = await global.PDFLib.PDFDocument.create();
const font = doc.embedStandardFont(global.PDFLib.StandardFonts.Helvetica);
const longText = 'APPLICATION DEVELOPMENT AND EMERGING TECHNOLOGIES';
const tr = truncateToWidth(longText, font, 8, 100);
assert.ok(tr.length < longText.length, 'long text should be cut down');
assert.ok(longText.startsWith(tr), 'result must be a prefix of the input');
assert.ok(font.widthOfTextAtSize(tr, 8) <= 100, 'truncated line must fit maxWidth');
const short = 'DATA STRUCTURES';
assert.strictEqual(truncateToWidth(short, font, 8, 200), short);
assert.strictEqual(truncateToWidth('', font, 8, 200), '');
});