Skip to content

Instantly share code, notes, and snippets.

@JamesTheAwesomeDude
Last active March 26, 2024 00:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamesTheAwesomeDude/ad7143126e0d89d4cae096ac2cee6d78 to your computer and use it in GitHub Desktop.
Save JamesTheAwesomeDude/ad7143126e0d89d4cae096ac2cee6d78 to your computer and use it in GitHub Desktop.
UAH get machine-readable transcript
// ==UserScript==
// @name UAH Transcript to TXT
// @version 0.0.3
// @match https://ssbprod.uah.edu/PROD/bwskotrn.P_ViewTran
// @grant none
// @namespace james.edington@uah.edu
// ==/UserScript==
// To install from GitHub, click the "Raw" button on the top-right.
// Requires Userscript support:
// - https://addons.mozilla.org/firefox/addon/greasemonkey/
// - https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo
// (might work in Chrome with no extension; I haven't tested this.)
// - https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao
/*
Copyright (c) 2024 James Edington Administrator
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
function main() {
let records = Array.from(parse());
let b = new Blob([JSON.stringify(records, null, 4)], {type: 'application/json'});
let u = URL.createObjectURL(b);
console.info(u);
let e = document.createElement('iframe');
e.src = u;
document.body.appendChild(e);
};
function* parse(document = null) {
if (document === null) document = window.document;
const first_row = getElementByXPath('//*[text()="Transcript Data"]/parent::table//tr', null, document, true);
for (
const record_row of getElementsByXPath(
'./following-sibling::tr[ *[1][string-length(normalize-space(text())) > 1 and string-length(normalize-space(text())) < 5] ]',
first_row, null, true, false
)
) {
const record = new Object();
const schema_row = getElementByXPath('./preceding-sibling::*[th[1]][1]', record_row, null, true);
const schema = Array.from(schema_row.children, th => th.textContent.trim());
schema.forEach((name, i) => {
const td = record_row.children[i];
let result = undefined;
switch (name) {
case 'Title': {
let linebreak;
if (linebreak = getElementByXPath('./br', td, null, true)) {
let memo;
result = Array.from(
getElementsByXPath('./preceding-sibling::* | ./preceding-sibling::text()', linebreak, null, true),
e => e.textContent
).join('');
memo = Array.from(
getElementsByXPath('./following-sibling::* | ./following-sibling::text()', linebreak, null, true),
e => e.textContent
).join('');
record['Memo'] = memo;
} else {
result = td.textContent.trim();
}
}
break;
case 'Credit Hours':
case 'Quality Points': {
result = parseFloat(td.textContent.trim());
}
break;
default: {
result = td.textContent.trim();
}
}
record[name] = result;
});
{
let term, term_row, institution;
if (
term_row = getElementByXPath('./preceding-sibling::*[th[.//*[ starts-with(text(),"Term:") ]]][1]', record_row, null, true)
) {
// Institutional credit
let th = getElementByXPath('./th[.//*[ starts-with(text(),"Term:") ]]', term_row, null, true);
term = th.textContent.trim();
term = term.replace(/^Term: ?/, '');
institution = 'The University of Alabama in Huntsville'
} else if (
term_row = getElementByXPath('./preceding-sibling::*[th[.//*[ substring(text(),string-length(text())-string-length(":")+1,string-length(":"))=":" ]]][1]', record_row, null, true)
) {
// (XPath 1.0 Implementation of ends-with: -------------------^^^^^^^^^-^^^^^^----------------------------------------------------------------^^^)
// Transfer credit
let th1 = getElementByXPath('./th[.//*[ substring(text(),string-length(text())-string-length(":")+1,string-length(":"))=":" ]]', term_row, null, true);
let th2 = getElementByXPath('./following-sibling::*[1]', th1, null, true);
term = th1.textContent.trim();
term = term.replace(/:\s*?$/, '');
institution = th2.textContent.trim();
institution = institution.replace(/^\d+\s*/, '');// WTF is this???
} else {
console.warn({record_row, record});
throw new Error('could not tell what semester class belongs to. See console for details.');
}
term = term.replace(/ ?COVID-19 Pandemic/, '');
record['Term'] = term;
record['Institution'] = institution;
}
yield record;
}
}
/**
* Get an element by XPath.
* @param {string} expression - A string representing the XPath to be evaluated.
* @param {Element} [context] - The context node. Defaults to the current document.
* @param {Document} [document] - The document against which evaluate() will be called. Defaults to the context node's document.
* @param {boolean} [ordered=true] - Whether to return the "first" node selected by the expression. If false, allows the XPath engine to return an arbitrary node selected by the expression.
* @returns {Element|null}
*/
function getElementByXPath(expression, context = null, document = null, ordered = true) {
if (context === null) context = window.document;
if (document === null) document = context.getRootNode();
const result = document.evaluate(
expression,
context,
document,
ordered ? XPathResult.FIRST_ORDERED_NODE_TYPE : XPathResult.ANY_UNORDERED_NODE_TYPE
);
return result.singleNodeValue || null;
}
/**
* Get multiple elements by XPath.
* @param {string} expression - A string representing the XPath to be evaluated.
* @param {Element} [context] - The context node. Defaults to the current document.
* @param {Document} [document] - The document against which evaluate() will be called. Defaults to the context node's document.
* @param {boolean} [ordered=true] - Whether to return the nodes in strict order. If false, allows the XPath engine to return the elements in arbitrary order.
* @param {boolean} [snapshot=true] - If true, immediately return an Array containing all selected elements; if false, return a "lazy" Iterator yielding all selected elements.
* @returns {Array.<Element>|Iterable.<Element>}
*/
function getElementsByXPath(expression, context = null, document = null, ordered = true, snapshot = true) {
if (context === null) context = window.document;
if (document === null) document = context.getRootNode();
if (snapshot) {
// Array
const result = document.evaluate(
expression,
context,
document,
ordered ? XPathResult.ORDERED_NODE_SNAPSHOT_TYPE : XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE
);
return Array.from({
[Symbol.iterator]() {
var i = 0;
return ({
next() {
const value = result.snapshotItem(i++);
return ({ value, done: !value });
},
});
}
});
} else {
// Iterator
const result = document.evaluate(
expression,
context,
document,
ordered ? XPathResult.ORDERED_NODE_ITERATOR_TYPE : XPathResult.UNORDERED_NODE_ITERATOR_TYPE
);
return ({
next() {
const value = result.iterateNext();
return { value, done: !value };
},
[Symbol.iterator]() {
return this;
},
});
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment