LearnedLeague Utility

Utility for learnedleague.com: load your answers inline with questions, link to questions from forum threads, copy OP forum post template

目前为 2024-11-21 提交的版本。查看 最新版本

// ==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();
}