// ==UserScript==
// @name LearnedLeague Utility
// @namespace Violentmonkey Scripts
// @match https://*.learnedleague.com/viewtopic.php*
// @match https://*.learnedleague.com/question.php*
// @match https://*.learnedleague.com/match.php*
// @match https://*.learnedleague.com/mini/match.php*
// @grant none
// @version 1.4.1
// @author BlumE
// @license MIT
// @description Utility for learnedleague.com: load your answers inline with questions, link to questions from forum threads, copy OP forum post template
// ==/UserScript==
let processFunction;
if (window.location.pathname === "/question.php") {
processFunction = handleQuestionPage;
} else if (window.location.pathname === "/viewtopic.php") {
processFunction = handleMessageBoardTopicPage;
} else if (window.location.pathname === "/match.php") {
processFunction = handleMatchPage;
} else if (window.location.pathname === "/mini/match.php") {
processFunction = handleMiniMatchPage;
} else {
return;
}
function handleQuestionPage() {
let answerDiv = document.querySelector('#xyz > span');
if (!answerDiv) return;
const statsDiv = document.querySelector('#main > div > div.indivqContent > div > div:nth-child(5) > div');
if (!statsDiv) return;
const [leagueNumber, matchDayNumber, questionNumber] = window.location.search.slice(1).split('&');
if (!(leagueNumber && matchDayNumber && questionNumber)) return;
if (document.getElementsByClassName('res_table').length > 0) {
// Only add "Your Answer" section if page has the results box that indicates you participated
appendYourAnswerDiv(statsDiv, leagueNumber, matchDayNumber, questionNumber);
}
appendCopyTemplateDiv(statsDiv, leagueNumber, matchDayNumber, questionNumber, answerDiv)
}
function handleMessageBoardTopicPage() {
addLinkToQuestionPage();
}
/**
* Adds a button to load your answers and includes them next to the correct answer
* Handles the different kind of match pages
*/
function handleMatchPage() {
const queryTerm = window.location.search.substring(1);
if (queryTerm.startsWith('id=')){
addLinkToLoadMatchAnswersSpecificMatch();
} else if (queryTerm.indexOf('_Div_') !== -1) {
addLinkToLoadMatchAnswersSpecificDivision();
} else {
addLinkToLoadMatchAnswers();
}
}
/**
* Adds a button to load your answers and includes them next to the correct answer
* Handles the different kind of mini match pages
*/
function handleMiniMatchPage() {
const queryTerm = window.location.search.substring(1);
if (queryTerm.startsWith('id=')){
addLinkToLoadMiniMatchAnswersSpecificMatch();
} else {
let [mini_league_name, match_day_number, group_number] = queryTerm.split('&');
if (group_number) {
addLinkToLoadMiniMatchAnswersSpecificGroup();
} else {
addLinkToLoadMiniMatchAnswers();
}
}
}
// If page is still loading, queue function for when it is done
if (document.readyState !== 'loading') {
processFunction();
} else {
document.addEventListener('DOMContentLoaded', processFunction);
}
/**
* Adds a link on forum topics that include the proper LL question header.
* e.g. LL83 MD9Q6 (KINGSTON) to the question page.
*/
function addLinkToQuestionPage() {
const reLeagueNumber = /[Ll][Ll](\d+)/;
const reMatchDayNumber = /[Mm][Dd](\d+)/;
const reQuestionNumber = /[Qq](\d+)/;
let postHeader = document.querySelector("#pageheader a");
if (postHeader) {
let postName = postHeader.innerHTML;
let leagueNumber = reLeagueNumber.exec(postName);
let matchDayNumber = reMatchDayNumber.exec(postName);
let questionNumber = reQuestionNumber.exec(postName);
if (leagueNumber && matchDayNumber && questionNumber) {
let linkURL = `/question.php?${leagueNumber[1]}&${matchDayNumber[1]}&${questionNumber[1]}`
let newLink = document.createElement('a');
newLink.href = linkURL;
newLink.title = "Link";
newLink.text = " Link >>";
newLink.style = "border-bottom: dotted 1px;color: #336666;"
postHeader.parentElement.appendChild(newLink);
}
}
}
/**
* Adds a button to load your answer for the given question
*/
function appendYourAnswerDiv(statsDiv, leagueNumber, matchDayNumber, questionNumber) {
let answerHeader = document.createElement('h3');
answerHeader.innerHTML = "<i>Your</i> Answer";
let answerLinkDiv = document.createElement('div');
answerLinkDiv.style = "margin-bottom:1.5em;margin-left:0.5em;";
let answerToggle = document.createElement('a');
answerToggle.id = "answerToggle";
answerToggle.innerHTML = 'Click here to reveal';
answerToggle.href = `javascript:RemoveContent('answerToggle');javascript:InsertContent('answerValue')`;
answerToggle.onclick = function () {fillInMyAnswer(leagueNumber, matchDayNumber, questionNumber);};
let answerValue = document.createElement('p');
answerValue.id = "answerValue";
answerValue.style = "display:none";
answerValue.innerHTML = `<i>Loading...</i>`;
answerLinkDiv.appendChild(answerToggle);
answerLinkDiv.appendChild(answerValue);
statsDiv.appendChild(answerHeader);
statsDiv.appendChild(answerLinkDiv);
}
/**
* Adds a button to copy a template for a question to your clipboard for use in a new
* forum post including the post subject and a nicely formatted body with the question text
* included.
*/
function appendCopyTemplateDiv(statsDiv, leagueNumber, matchDayNumber, questionNumber, answerDiv) {
let answer = answerDiv.textContent.trim();
let postSubject = `LL${leagueNumber} MD${matchDayNumber}Q${questionNumber} (${answer})`;
let postBody = templateBody();
let postContent = postSubject + "\\n" + postBody;
let templateHeader = document.createElement('h3');
templateHeader.innerHTML = "Forum Post Template";
let templateCopyDiv = document.createElement('div');
templateCopyDiv.style = "margin-bottom:1.5em;margin-left:0.5em;";
let templateCopy = document.createElement('a');
templateCopy.id = "subjectToggle";
templateCopy.innerHTML = "Click here to copy template to clipboard";
templateCopy.href = `javascript:navigator.clipboard.writeText("${postContent}")`;
templateCopyDiv.appendChild(templateCopy);
statsDiv.appendChild(templateHeader);
statsDiv.appendChild(templateCopyDiv);
}
/**
* Processes the body of the question to transform any special symbols or formatting to something the message board
* system recognizes
*/
function templateBody() {
let questionDiv = document.querySelector('.indivqQuestion');
let answerDiv = document.querySelector('#xyz > span');
let answer = answerDiv.textContent.trim();
let postBody = `[quote="Question"]${questionDiv.innerHTML.trim()}[/quote] Answer: [spoiler]${answer}[/spoiler]`;
let template = document.createElement('template');
template.innerHTML = postBody;
let content = [...template.content.childNodes].map(node => processNode(node)).join("");
content = content.replace(/"/g, '\\\"');
return content;
}
function processNode(node) {
let val;
if (node.hasChildNodes()) {
val = [...node.childNodes].map(node => processNode(node)).join("");
} else {
val = node.textContent;
}
if (node.nodeType === node.TEXT_NODE) {
return val
} else {
const equivalentElements = ['b', 'i', 'u'];
if (equivalentElements.indexOf(node.localName) !== -1) {
return `[${node.localName}]${val}[/${node.localName}]`;
} else if (node.localName === 'em') {
return `[i]${val}[/i]`;
} else if (node.localName === 'a') {
return `[url=${node.href}]${val}[/url]`;
} else if (node.localName === 'br') {
return '\\n'
} else if (node.localName === 'sup') {
const superscriptMap = new Map([['0', '⁰'], ['1', '¹'], ['2', '²'], ['3', '³'], ['4', '⁴'], ['5', '⁵'], ['6', '⁶'], ['7', '⁷'], ['8', '⁸'],
['9', '⁹'], ['+', '⁺'], ['-', '⁻'], ['=', '⁼'], ['(', '⁽'], [')', '⁾'], ['n', 'ⁿ'], ['i', 'ⁱ']]);
if ([...val].every(char => superscriptMap.has(char))) {
return [...val].map(char => superscriptMap.get(char)).join('');
} else if (
node.children.length === 1
&& equivalentElements.indexOf(node.firstChild.localName) !== -1
&& node.firstChild.children.length === 0
&& [...node.firstChild.textContent].every(char => superscriptMap.has(char))
) {
// Handle edge case of <sup><i>123</i></sup>
return `[${node.firstChild.localName}]${[...node.firstChild.textContent].map(char => superscriptMap.get(char)).join('')}[/${node.firstChild.localName}]`;
} else {
return "^" + val;
}
} else if (node.localName === 'sub') {
const subscriptMap = new Map([['0', '₀'], ['1', '₁'], ['2', '₂'], ['3', '₃'], ['4', '₄'], ['5', '₅'], ['6', '₆'], ['7', '₇'], ['8', '₈'],
['9', '₉'], ['+', '₊'], ['-', '₋'], ['=', '₌'], ['(', '₍'], [')', '₎'], ['a', 'ₐ'], ['e', 'ₑ'], ['o', 'ₒ'],
['x', 'ₓ'], ['h', 'ₕ'], ['k', 'ₖ'], ['l', 'ₗ'], ['m', 'ₘ'], ['n', 'ₙ'], ['p', 'ₚ'], ['s', 'ₛ'], ['t', 'ₜ'], ['ə', 'ₔ']]);
if ([...val].every(char => subscriptMap.has(char))) {
return [...val].map(char => subscriptMap.get(char)).join('');
} else if (
node.children.length === 1
&& equivalentElements.indexOf(node.firstChild.localName) !== -1
&& node.firstChild.children.length === 0
&& [...node.firstChild.textContent].every(char => superscriptMap.has(char))
) {
// Handle edge case of <sub><i>123</i></sub>
return `[${node.firstChild.localName}]${[...node.firstChild.textContent].map(char => subscriptMap.get(char)).join('')}[/${node.firstChild.localName}]`;
} else {
return "_" + val + "_";
}
} else {
console.log('unknown node ', node);
return val;
}
}
}
function fillInMyAnswer(seasonNumber, matchDayNumber, questionNumber) {
queryPastAnswers(seasonNumber, matchDayNumber).then(function (response) {
const parser = new DOMParser();
const doc = parser.parseFromString(response, "text/html");
// Hopefully this remains consistent
const answerQuery = `table.qtable > tbody > tr:nth-child(${parseInt(questionNumber) + 1}) > td:nth-child(3)`;
const answerNode = doc.querySelector(answerQuery);
let answerValue = document.getElementById('answerValue');
if (!answerNode) {
answerValue.innerHTML = 'Failed to retrieve';
} else {
answerValue.innerHTML = answerNode.innerHTML;
}
}).catch(function (err) {
let answerValue = document.getElementById('answerValue');
answerValue.innerHTML = 'Failed to retrieve ' + err;
})
}
function queryPastAnswers(leagueNumber, matchDayNumber) {
return new Promise(function (resolve, reject) {
const req = new XMLHttpRequest();
req.addEventListener("load", onload);
req.open("POST", "/thorsten/pastanswers.php");
req.onload = function () {
if (req.status >= 200 && req.status < 300) {
resolve(req.response);
} else {
reject({
status: req.status,
statusText: req.statusText
});
}
};
req.onerror = function () {
reject({
status: req.status,
statusText: req.statusText
});
};
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
req.send(`season=${leagueNumber}&matchday=${matchDayNumber}`);
})
}
function addLinkToLoadMatchAnswersSpecificMatch() {
let parentDiv = document.querySelector('.QTable').parentElement
let answerLinkDiv = document.createElement('div');
answerLinkDiv.style = "margin-bottom:1.0em";
let answerToggle = document.createElement('a');
answerToggle.id = "answerToggle";
answerToggle.innerHTML = 'Click here to reveal your answers';
answerToggle.href = `javascript:RemoveContent('answerToggle')`;
const questionLink = new URL(document.querySelector('.ind-Numb2 a').href);
const [leagueNumber, matchDayNumber, questionNumber] = questionLink.search.slice(1).split('&');
answerToggle.onclick = function () {fillInMyMatchAnswersSpecificMatch(leagueNumber, matchDayNumber);};
let answerLoadingProgress = document.createElement('p');
answerLoadingProgress.id = "answerLoadingProgress";
answerLoadingProgress.hidden = true;
answerLoadingProgress.innerHTML = `<i>Loading...</i>`;
answerLinkDiv.appendChild(answerToggle);
answerLinkDiv.appendChild(answerLoadingProgress);
parentDiv.prepend(answerLinkDiv);
}
function fillInMyMatchAnswersSpecificMatch(leagueNumber, matchDayNumber) {
const answerLoadingProgress = document.querySelector('#answerLoadingProgress')
answerLoadingProgress.hidden = false;
queryPastAnswers(leagueNumber, matchDayNumber).then(function (response) {
const parser = new DOMParser();
const doc = parser.parseFromString(response, "text/html");
const answerTable = doc.querySelector(`table.qtable`);
if (!answerTable) {
answerLoadingProgress.innerHTML = '<i>Failed to retrieve</i>';
} else {
let matchTable = document.querySelector(`table.QTable`);
// Fill in your answers next to actual answers
for (let i = 1; i <= 6; i++) {
let yourAnswer = answerTable.rows[i].cells[2].innerHTML;
let resultImgSrc = answerTable.rows[i].cells[3].firstChild.src;
matchTable.rows[i].cells[1].innerHTML += `<div><i>${yourAnswer}</i> <img src="${resultImgSrc}" style="height: 12px"></img></div>`;
}
answerLoadingProgress.hidden = true;
}
}).catch(function (err) {
console.log(err);
})
}
function addLinkToLoadMatchAnswersSpecificDivision() {
let parentDiv = document.querySelector('.qacontainer').parentElement
let answerLinkDiv = document.createElement('div');
answerLinkDiv.style = "margin-bottom:1.0em";
let answerToggle = document.createElement('a');
answerToggle.id = "answerToggle";
answerToggle.innerHTML = 'Click here to reveal your answers';
answerToggle.href = `javascript:RemoveContent('answerToggle')`;
const questionLink = new URL(document.querySelector('.qaqnumber a').href);
const [leagueNumber, matchDayNumber, questionNumber] = questionLink.search.slice(1).split('&');
answerToggle.onclick = function () {fillInMyMatchAnswersSpecificDivision(leagueNumber, matchDayNumber);};
let answerLoadingProgress = document.createElement('p');
answerLoadingProgress.id = "answerLoadingProgress";
answerLoadingProgress.hidden = true;
answerLoadingProgress.innerHTML = `<i>Loading...</i>`;
answerLinkDiv.appendChild(answerToggle);
answerLinkDiv.appendChild(answerLoadingProgress);
parentDiv.insertBefore(answerLinkDiv, document.querySelector('#lft > div:nth-child(3)'));
}
function fillInMyMatchAnswersSpecificDivision(leagueNumber, matchDayNumber) {
const answerLoadingProgress = document.querySelector('#answerLoadingProgress')
answerLoadingProgress.hidden = false;
queryPastAnswers(leagueNumber, matchDayNumber).then(function (response) {
const parser = new DOMParser();
const doc = parser.parseFromString(response, "text/html");
const answerTable = doc.querySelector(`table.qtable`);
const answerDivs = document.querySelector(`#lft > div:nth-child(5)`);
if (!answerTable) {
answerLoadingProgress.innerHTML = '<i>Failed to retrieve</i>';
} else {
// Fill in your answers next to actual answers
for (let i = 1; i <= 6; i++) {
let yourAnswer = answerTable.rows[i].cells[2].innerHTML;
let resultImgSrc = answerTable.rows[i].cells[3].firstChild.src;
answerDivs.children[i-1].innerHTML += `<div><i>${yourAnswer}</i> <img src="${resultImgSrc}" style="height: 12px"></img></div>`;
}
answerLoadingProgress.hidden = true;
}
}).catch(function (err) {
console.log(err);
})
}
function addLinkToLoadMatchAnswers() {
let parentDiv = document.querySelector('#lft > div.yellolbl').parentElement
let answerLinkDiv = document.createElement('div');
answerLinkDiv.style = "margin-bottom:1.0em";
let answerToggle = document.createElement('a');
answerToggle.id = "answerToggle";
answerToggle.innerHTML = 'Click here to include your answers';
answerToggle.href = `javascript:RemoveContent('answerToggle')`;
const questionLink = new URL(document.querySelector('#lft > div.ind-boxATbl > div:nth-child(1) > span > a').href);
const [leagueNumber, matchDayNumber, questionNumber] = questionLink.search.slice(1).split('&');
answerToggle.onclick = function () {fillInMyMatchAnswers(leagueNumber, matchDayNumber);};
let answerLoadingProgress = document.createElement('p');
answerLoadingProgress.id = "answerLoadingProgress";
answerLoadingProgress.hidden = true;
answerLoadingProgress.innerHTML = `<i>Loading...</i>`;
answerLinkDiv.appendChild(answerToggle);
answerLinkDiv.appendChild(answerLoadingProgress);
parentDiv.insertBefore(answerLinkDiv, document.querySelector('#lft > div.yellolbl'));
}
function fillInMyMatchAnswers(leagueNumber, matchDayNumber) {
const answerLoadingProgress = document.querySelector('#answerLoadingProgress')
answerLoadingProgress.hidden = false;
queryPastAnswers(leagueNumber, matchDayNumber).then(function (response) {
const parser = new DOMParser();
const doc = parser.parseFromString(response, "text/html");
const answerTable = doc.querySelector(`table.qtable`);
const answerDivs = document.querySelectorAll('.indivqAnswerwrapper');
if (!answerTable) {
answerLoadingProgress.innerHTML = '<i>Failed to retrieve</i>';
} else {
let matchTable = document.querySelector(`table.QTable`);
// Fill in your answers next to actual answers
for (let i = 1; i <= 6; i++) {
let yourAnswer = answerTable.rows[i].cells[2].innerHTML;
let resultImgSrc = answerTable.rows[i].cells[3].firstChild.src;
answerDivs[i-1].children[1].innerHTML += `<div style="color: #293A55; font-weight: 400"><i>${yourAnswer}</i> <img src="${resultImgSrc}" style="height: 12px"></img></div>`;
}
answerLoadingProgress.hidden = true;
}
}).catch(function (err) {
console.log(err);
})
}
function addLinkToLoadMiniMatchAnswersSpecificMatch() {
addLinkToLoadMatchAnswersSpecificMatch();
}
function addLinkToLoadMiniMatchAnswersSpecificGroup() {
addLinkToLoadMatchAnswersSpecificDivision();
}
function addLinkToLoadMiniMatchAnswers() {
addLinkToLoadMatchAnswers();
}