Last active
September 10, 2022 16:44
-
-
Save IvanaGyro/e24d8da3ede07f39ed24670e03b15f9b to your computer and use it in GitHub Desktop.
Create the Google Calendar event links of the courses on the NTU "My Schedule" page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
javascript: (() => { | |
/** | |
* This script is used to create the Google Calendar event links of the | |
* courses on the NTU "My Schedule" page. | |
*/ | |
if ( | |
location.href !== "https://nol.ntu.edu.tw/nol/coursesearch/myschedule.php" | |
) { | |
return; | |
} | |
let locationTimeIdx = null; | |
let table = null; | |
/** | |
* Get course name and the information link from the row of the course table. | |
* | |
* @param {HTMLElement} row | |
* @returns {[String, String]} | |
*/ | |
function getCourseNameAndLinkFromRow(row) { | |
/* assume the first link in the row is the course name */ | |
const courseNameElm = row.getElementsByTagName("a")[0]; | |
const url = new URL(courseNameElm.href); | |
const keys = Array.from(url.searchParams.keys()); | |
keys.forEach((key) => { | |
if (!["course_id", "semester"].includes(key)) { | |
url.searchParams.delete(key); | |
} | |
}); | |
return [courseNameElm.innerText, url.href]; | |
} | |
/** | |
* Get the link of COOL platform from the row of the course table. | |
* | |
* @param {HTMLElement} row | |
* @returns {String=} | |
*/ | |
function getCoolLink(row) { | |
return row.querySelector('a[href*="cool.ntu.edu.tw"]')?.href; | |
} | |
/** | |
* Parse the locations and time from the row of the course table. | |
* | |
* @param {HTMLElement} row | |
* @returns {[String, String[], String][]} | |
*/ | |
function getLocationAndTime(row) { | |
const timeAndLocationPattern = | |
/([一二三四五六日])(([0-9]|10|[A-D])(,([0-9]|10|[A-D]))*)\((.+?)\)/g; | |
if (locationTimeIdx == null) { | |
locationTimeIdx = -1; | |
Array.from(row.getElementsByTagName("td")).some((elm) => { | |
locationTimeIdx += 1; | |
const istimeAndLocation = timeAndLocationPattern.test(elm.innerText); | |
timeAndLocationPattern.lastIndex = 0; | |
return istimeAndLocation; | |
}); | |
} | |
return [ | |
...row | |
.getElementsByTagName("td") | |
[locationTimeIdx].innerText.matchAll(timeAndLocationPattern), | |
].map((res) => [res[1], res[2].split(","), res[6]]); | |
} | |
/** | |
* Convert the orders of the class to numbers. | |
* E.g. "1" => 1, "A" => 11 | |
* | |
* @param {String[]} classNo | |
* @returns {Number[]} | |
*/ | |
function parseClassNo(classNo) { | |
const parsedClassNo = classNo.map((c) => { | |
if (/^[ABCD]$/.test(c)) { | |
return c.charCodeAt(0) - "A".charCodeAt(0) + 11; | |
} | |
return parseInt(c); | |
}); | |
for (let i = 1; i < parsedClassNo.length; ++i) { | |
if (parsedClassNo[i] !== parsedClassNo[i - 1] + 1) { | |
throw new Error( | |
`Class No. are not strictly increasing: [${parsedClassNo}]` | |
); | |
} | |
} | |
return parsedClassNo; | |
} | |
/** | |
* Compose Google Calendar Event link from the information of the course. | |
* | |
* @param {String} courseName | |
* @param {String} link | |
* @param {String=} coolLink | |
* @param {[String, String[], String][]} locationAndTimePairs | |
* @param {String=} calendarID | |
* @returns {URL[]} | |
*/ | |
function composeUrl(courseName, link, coolLink, locationAndTimePairs, calendarID) { | |
return locationAndTimePairs.map(([weekDay, classNo, location]) => { | |
const setToStartOfClass = (dateObj, classNoIdx) => { | |
const classStartTimeTable = [ | |
[7, 10], | |
[8, 10], | |
[9, 10], | |
[10, 20], | |
[11, 20], | |
[12, 20], | |
[13, 20], | |
[14, 20], | |
[15, 30], | |
[16, 30], | |
[17, 30], | |
[18, 25], | |
[19, 20], | |
[20, 15], | |
[21, 00], | |
]; | |
dateObj.setHours(classStartTimeTable[classNoIdx][0]); | |
dateObj.setMinutes(classStartTimeTable[classNoIdx][1]); | |
}; | |
const parsedClassNo = parseClassNo(classNo); | |
const classStartTime = new Date(firstDate); | |
classStartTime.setDate(classStartTime.getDate() + dayOffsetMap[weekDay]); | |
setToStartOfClass(classStartTime, parsedClassNo[0]); | |
const classEndTime = new Date(classStartTime); | |
setToStartOfClass(classEndTime, parsedClassNo[parsedClassNo.length - 1]); | |
classEndTime.setMinutes(classEndTime.getMinutes() + 50); | |
const url = new URL( | |
"https://www.google.com/calendar/event?action=TEMPLATE" | |
); | |
url.searchParams.set("text", courseName); | |
const toGoogleCalendarDateString = (time) => { | |
const year = time.getUTCFullYear(); | |
const month = `${time.getUTCMonth() + 1}`.padStart(2, "0"); | |
const date = `${time.getUTCDate()}`.padStart(2, "0"); | |
const hour = `${time.getUTCHours()}`.padStart(2, "0"); | |
const minute = `${time.getUTCMinutes()}`.padStart(2, "0"); | |
return `${year}${month}${date}T${hour}${minute}00Z`; | |
}; | |
url.searchParams.set( | |
"dates", | |
`${toGoogleCalendarDateString( | |
classStartTime | |
)}/${toGoogleCalendarDateString(classEndTime)}` | |
); | |
url.searchParams.set("location", location); | |
let description = link; | |
if (coolLink != null) { | |
description += `\n\ncool: ${coolLink}`; | |
} | |
url.searchParams.set("details", description); | |
url.searchParams.set("recur", "RRULE:FREQ=WEEKLY;COUNT=16"); | |
if (calendarID !== null) { | |
url.searchParams.set("src", calendarID); | |
} | |
return url; | |
}); | |
} | |
/** | |
* @param {String} html representing a single element | |
* @return {Element} | |
*/ | |
function htmlToElement(html) { | |
const template = document.createElement("template"); | |
html = html.trim(); /* Never return a text node of whitespace as the result */ | |
template.innerHTML = html; | |
return template.content.firstChild; | |
} | |
if (window.googleCalendarLinksHaveAdded != null) return; | |
let firstDate = prompt( | |
"Please enter the first day of this semester:", | |
"20220905" | |
); | |
if (firstDate == null || firstDate == "") { | |
return; | |
} | |
firstDate = new Date( | |
firstDate.substring(0, 4), | |
firstDate.substring(4, 6) - 1, | |
firstDate.substring(6, 8) | |
); | |
if (isNaN(firstDate)) { | |
/* invalid input */ | |
return; | |
} | |
const firstDay = firstDate.getDay(); | |
const dayOffsetMap = { | |
日: (7 - firstDay) % 7, | |
一: (8 - firstDay) % 7, | |
二: (9 - firstDay) % 7, | |
三: (10 - firstDay) % 7, | |
四: (11 - firstDay) % 7, | |
五: (12 - firstDay) % 7, | |
六: (13 - firstDay) % 7, | |
}; | |
const calendarID = prompt( | |
"Target calendar ID (optional):", | |
"" | |
); | |
if (calendarID == null) { | |
return; | |
} | |
table = document.getElementsByTagName("table")[0]; | |
let rows = table.getElementsByTagName("tr"); | |
/* Update header */ | |
const header = rows[0]; | |
const addToGoogleCalendarCell = ` | |
<td> | |
加到 | |
<br> | |
<br> | |
日曆 | |
</td> | |
`; | |
header.insertBefore( | |
htmlToElement(addToGoogleCalendarCell), | |
header.children[header.children.length - 1] | |
); | |
/* Update foot */ | |
const foot = rows[rows.length - 1]; | |
const footCell = foot.getElementsByTagName("td")[0]; | |
footCell.setAttribute("colspan", footCell.getAttribute("colspan") - -1); | |
rows = Array.from(rows); | |
rows.shift(); | |
rows.pop(); | |
rows.forEach((row) => { | |
const [courseName, link] = getCourseNameAndLinkFromRow(row); | |
const coolLink = getCoolLink(row); | |
const locationAndTimePairs = getLocationAndTime(row); | |
console.debug(courseName, link, coolLink, locationAndTimePairs); | |
const urls = composeUrl(courseName, link, coolLink, locationAndTimePairs, calendarID); | |
const newCell = document.createElement("td"); | |
if (urls.length === 1) { | |
newCell.append( | |
htmlToElement(`<a target="_blank" href="${urls[0].href}">活動</a>`) | |
); | |
} else { | |
urls.forEach((url, idx) => { | |
if (idx > 0) { | |
newCell.append(document.createElement("br")); | |
} | |
newCell.append( | |
htmlToElement( | |
`<a target="_blank" href="${urls[idx].href}">活動${idx + 1}</a>` | |
) | |
); | |
}); | |
} | |
row.insertBefore(newCell, row.children[row.children.length - 1]); | |
}); | |
window.googleCalendarLinksHaveAdded = true; | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment