Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Format UNSW timetable for conversion to PDF
let printoutHeight = 18; // In centimetres
function xpath(node, path) {
let elems = document.evaluate(path, node, null, XPathResult.ANY_TYPE, null);
let elem;
let result = [];
while (elem = elems.iterateNext())
result.push(elem);
return result;
}
function removeElem(e) {
e.parentNode.removeChild(e);
}
let body = xpath(document, '//*[text() = "Instructor"]/ancestor::*[@class = "formBody"]')[0];
// Remove all elements outside of the body (including the pesky "-?-" at the end)
xpath(body, "preceding::*[count(. | //body/descendant::*) = count(//body/descendant::*)]").forEach(removeElem);
xpath(body, "following::*[count(. | //body/descendant::*) = count(//body/descendant::*)]").forEach(removeElem);
xpath(body, "following::text()").forEach(removeElem);
// Extract the tables we want and remove the rest
let subjectsMatcher = Array.from(new Set(body.textContent.match(/[A-Z]{4}[0-9]{4}/g))).map((code) => `contains(text(), "${code}")`).join(' or ');
let tables = xpath(body, `*[.//*[${subjectsMatcher} or contains(text(), "Activity")]]`);
Array.from(body.children).forEach(removeElem);
tables.forEach((elem) => body.appendChild(elem));
// Find and remove empty columns from main timetable
let timetable = xpath(body, './/td[text() = "Monday"]/parent::*/parent::*')[0];
let headers = Array.from(timetable.children[0].children);
let columns = Array.from(timetable.children[1].children);
for (let c = 1; c < columns.length; ++c) {
if (!columns[c].textContent.match(/[A-Z]{4}[0-9]{4}/)) {
removeElem(headers[c]);
removeElem(columns[c]);
}
}
// Remove the unneeded "description" rows from the subject tables
xpath(body, `.//tr[td[${subjectsMatcher}]]/following-sibling::*`).forEach(removeElem);
// Remove class listing spacers
xpath(body, '//td[@class = "rowSpacer"]/parent::tr').forEach(removeElem);
// Merge identical instructor cells adjacent to each other
for (let list of xpath(body, '//td[text() = "Instructor"]/parent::*/parent::*')) {
let instructors = xpath(list, 'tr[position() > 1]/td[last()]');
let prev = null;
let count = 0;
for (let instructor of instructors.concat(null)) {
if (instructor && prev && instructor.textContent && instructor.textContent === prev.textContent) {
++count;
removeElem(instructor);
} else {
if (count > 1 && prev.textContent.trim())
prev.setAttribute("rowspan", count);
prev = instructor;
count = 1;
}
}
}
// Fix styles
let styles = new Map();
for (let sheet of document.styleSheets) {
// Lovely same origin rule
try {
if (!sheet.cssRules)
continue;
} catch (e) {
continue;
}
for (let rule of sheet.cssRules) {
let style = styles.get(rule.selectorText);
if (!style)
styles.set(rule.selectorText, style = []);
style.push(rule.style);
}
}
// Remove excess padding
styles.get(".bsdsModule").forEach((style) => style.paddingBottom = "");
styles.get(".container-fluid").forEach((style) => {style.paddingLeft = ""; style.paddingRight = "";});
styles.get(".formBody").forEach((style) => style.paddingTop = "");
styles.get("td, th").forEach((style) => style.lineHeight = "");
styles.get(".rowHighlight td").forEach((style) => {style.paddingTop = ""; style.paddingBottom = "";});
styles.get(".rowLowlight td").forEach((style) => {style.paddingTop = ""; style.paddingBottom = "";});
styles.get(".tableHeading").forEach((style) => style.padding = "");
// Fix table widths
xpath(document, "//table").forEach((table) => table.style.width = "100%");
// Make timetable columns' width and height uniform
let rowCount = columns[0].getElementsByTagName("tr").length;
xpath(timetable, ".//table").forEach((elem) => elem.style.height = "100%");
xpath(timetable, "tr//tr/td").forEach((elem) => elem.style.height = (parseInt(elem.getAttribute("height"), 10) / 72 / rowCount * printoutHeight) + "cm");
headers[0].style.width = (100 / (2 * columns.length - 1)) + "%";
headers.slice(1).forEach((elem) => elem.style.width = (100 / (columns.length - 0.5)) + "%");
// Make class listings' columns' width uniform
xpath(body, '//td[text() = "Activity"]').forEach((elem) => elem.style.width = "20%");
xpath(body, '//td[text() = "Section"]').forEach((elem) => elem.style.width = "10%");
xpath(body, '//td[text() = "Weeks"]').forEach((elem) => elem.style.width = "10%");
xpath(body, '//td[text() = "Location"]').forEach((elem) => elem.style.width = "25%");
xpath(body, '//td[text() = "Instructor"]').forEach((elem) => elem.style.width = "15%");
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=332740
xpath(body, '//td[text() = "Activity"]/preceding::table[1]').forEach((elem) => elem.style.borderCollapse = "separate");
xpath(body, '//td[text() = "Activity"]/ancestor::table[1]').forEach((elem) => elem.style.borderCollapse = "separate");
styles.get(".rowHighlight").forEach((style) => style.borderTop = "");
styles.get(".rowLowlight").forEach((style) => style.borderTop = "");
xpath(body, '//td[text() = "Activity"]/parent::*/parent::*/child::*/td').forEach((elem) => elem.style.borderBottom = "1px solid #ddd");
// Add a bit of spacing
xpath(timetable, "ancestor-or-self::table[1]")[0].style.pageBreakAfter = "always";
tables.forEach((table) => table.style.pageBreakInside = "avoid");
xpath(body, `.//tr[td[${subjectsMatcher}]]/ancestor::table[1]`).slice(1).forEach((elem) => elem.style.marginTop = "1em");
// Remove width and height properties
xpath(document, "//*[@width]").forEach((elem) => elem.removeAttribute("width"));
xpath(document, "//*[@height]").forEach((elem) => elem.removeAttribute("height"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.