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