diff --git a/awnser.js b/answer.js similarity index 70% rename from awnser.js rename to answer.js index cd65643..f62d117 100644 --- a/awnser.js +++ b/answer.js @@ -1,4 +1,4 @@ -function awnserQuestion(awnserData){ +function answerQuestion(answerData){ const question = document.querySelector(".question:not(.hidden)"); if (!question) { return; @@ -12,15 +12,14 @@ function awnserQuestion(awnserData){ if (!answersDom) return; const answers = answersDom.children; - for (let answer of answers) { + for (let answer of Array.from(answers)) { const input = answer.querySelector("input"); if (!input) continue; input.checked = false; } - const correctAnswers = findAnswers(awnserData, questionText, answers); + const correctAnswers = findAnswers(answerData, questionText, answers); if (correctAnswers.length === 0) { - GM_log("no awnser") return; } @@ -31,18 +30,17 @@ function awnserQuestion(awnserData){ } } -function findAnswers(awnserData, questionText, answers) { - if (awnserData === null) { - alert("No chapter data loaded. Maybe the fetch failed?!"); +function findAnswers(answerData, questionText, answers) { + if (answerData === null) { return []; } const correctAnswers = []; - for (let entry of awnserData) { - if (matchAwnser(questionText.trim(), entry.question.trim())) { + for (let entry of answerData) { + if (matchAnswer(questionText.trim(), entry.question.trim())) { for (let availableAnswer of answers) { for (let possibleAnswer of entry.answers) { - if (matchAwnser(availableAnswer.textContent.trim(), possibleAnswer)) { + if (matchAnswer(availableAnswer.textContent.trim(), possibleAnswer)) { correctAnswers.push(availableAnswer); } } @@ -53,11 +51,11 @@ function findAnswers(awnserData, questionText, answers) { return correctAnswers; } -function matchAwnser(textA, textB) { +function matchAnswer(textA, textB) { const replaceRegex = /[^\w]/gi; textA = textA.replace(replaceRegex, ""); textB = textB.replace(replaceRegex, ""); return (textA === textB); } -window.awnserQuestion = awnserQuestion; \ No newline at end of file +window.answerQuestion = answerQuestion; \ No newline at end of file diff --git a/fetch.js b/fetch.js index aa27f54..3b89ab6 100644 --- a/fetch.js +++ b/fetch.js @@ -1,79 +1,139 @@ -function fetchAwnsers(awnserURL = "") { - return new Promise((resolve, reject) => { - GM_xmlhttpRequest({ - method: "GET", - url: awnserURL, - headers: { - "Content-Type": "text/html", - }, - onload: function (response) { - awnserImgs = new Map(); - const results = []; - const parser = new DOMParser(); - const virtDom = parser.parseFromString( - response.responseText, - "text/html" - ); - - let answersDom = virtDom.querySelector(".pf-content"); - if (!answersDom) { - answersDom = virtDom.querySelector(".thecontent"); - } - - let index = -1; - for (let childDom of answersDom.children) { - index++; - - if (childDom.tagName === "P" || childDom.tagName === "STRONG") { - // maybe a question question - let innerDom = childDom.querySelector("strong"); - if (innerDom === null) { - if (!childDom.textContent) { - continue; - } - innerDom = childDom; - } - - const textContent = innerDom.textContent.trim(); - const matches = textContent.match(/^[0-9]+\. (.*)$/); - if (matches !== null) { - const questionText = matches[1]; - - // most likely a question - let nextChild = answersDom.children[index + 1]; - - if (nextChild.tagName === "P") { - nextChild = answersDom.children[index + 2]; - } - - if (nextChild === null) continue; - - if (nextChild.tagName === "UL") { - // most likely the awnser - const answers = []; - for (let answerDom of nextChild.querySelectorAll("strong")) { - let answerText = answerDom.textContent.trim(); - if (answerText.endsWith("*")) { - answerText = answerText.substring(0, answerText.length - 1); - } - answers.push(answerText); - } - - results.push({ - question: questionText, - answers: answers, - }); - } - } - } - } - resolve(results); - }, - onerror: function(error) { - reject(error) - } - }); +const IS_QUESTION_REGEX = /^[0-9]+\. (.*)$/; + +/** + * Fetches answers from the specified URL. + * @param {string} [answerURL=""] - The URL to fetch answers from. + * @returns {Promise>} A Promise that resolves with the fetched answers. + */ +function fetchAnswers(answerURL = "") { + return new Promise((resolve, reject) => { + GM_xmlhttpRequest({ + method: "GET", + url: answerURL, + headers: { + "Content-Type": "text/html", + }, + onload: function (response) { + parseAnswers(response, resolve); + }, + onerror: function (error) { + reject(error); + }, }); + }); +} + +/** + * + * @param {GMXMLHttpRequestResponse} response + * @param {(value: Answer[] | PromiseLike) => void} resolve + */ +function parseAnswers(response, resolve) { + const results = []; + const allAnswersElement = getAllAnswersElement(response); + + let index = -1; + for (let child of Array.from(allAnswersElement.children)) { + index++; + const result = parseAnswerElement(index, child, allAnswersElement); + results.push(result); } - - window.fetchAwnsers = fetchAwnsers; \ No newline at end of file + + resolve(results); +} + +/** + * @param {GMXMLHttpRequestResponse} response + * @returns {Element} + */ +function getAllAnswersElement(response) { + const parser = new DOMParser(); + const virtualDOM = parser.parseFromString(response.responseText, "text/html"); + + let answersElement = virtualDOM.querySelector(".pf-content"); + if (!answersElement) { + answersElement = virtualDOM.querySelector(".thecontent"); + } + return answersElement; +} + +/** + * @param {number} index + * @param {Element} allAnswersElement + * @param {Element} element + * @returns {Answer} + */ +function parseAnswerElement(index, element, allAnswersElement) { + // Check for Possible Tags + if (!(element.tagName === "P" || element.tagName === "STRONG")) { + return; + } + + // Get Question Element + /** @type {Element} */ + let questionElement = element.querySelector("strong"); + if (questionElement === null) { + if (!element.textContent) { + return; + } + questionElement = element; + } + + // Get Question + const questionText = parseQuestion(questionElement); + if (questionText === null) { + return; + } + + // Get Awsners + const answersElement = getAnswersElement(index, allAnswersElement); + if (answersElement === null || answersElement.tagName === "UL") return; + + return { + question: questionText, + answers: getAnswers(answersElement), + }; +} + +/** + * @param {Element} questionElement + * @returns {String} + */ +function parseQuestion(questionElement) { + const textContent = questionElement.textContent.trim(); + const matches = textContent.match(IS_QUESTION_REGEX); + return matches !== null ? matches[1] : null; +} + +/** + * @param {number} index + * @param {Element} allAnswersElement + * @returns {Element} + */ +function getAnswersElement(index, allAnswersElement) { + let answersElement = allAnswersElement.children[index + 1]; + + if (answersElement.tagName === "P") { + answersElement = allAnswersElement.children[index + 2]; + } + return answersElement; +} + +/** + * @param {Element} answersElement + * @returns {Array} + */ +function getAnswers(answersElement) { + const answers = []; + for (let answerDom of Array.from(answersElement.querySelectorAll("strong"))) { + let answerText = answerDom.textContent.trim(); + if (answerText.endsWith("*")) { + answerText = answerText.substring(0, answerText.length - 1); + } + answers.push(answerText); + } + + return answers; +} + +window.fetchAnswers = fetchAnswers; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..f90ecb7 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "checkJs": true, + "target": "es6", + "lib": ["dom", "es6"] + } +} diff --git a/main.user.js b/main.user.js index 4e8efdf..1abf615 100644 --- a/main.user.js +++ b/main.user.js @@ -6,27 +6,24 @@ // @match *://www.google.com/* // @match *://www.google.de/* // @require https://git.euph.dev/SZUT-Dominik/CCNA_Autofill_Userscript/raw/branch/main/fetch.js -// @require https://git.euph.dev/SZUT-Dominik/CCNA_Autofill_Userscript/raw/branch/main/awnser.js +// @require https://git.euph.dev/SZUT-Dominik/CCNA_Autofill_Userscript/raw/branch/main/answer.js // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest -// @grant GM_log -// @version 0.0.11 +// @version 0.0.12 // @author Dominik Säume // ==/UserScript== const URL_STORAGE_KEY = "itexamanswers.net URL"; -let awnserData; - -console.log = console.__proto__.log; +let answerData; window.addEventListener("keydown", async (event) => { switch(event.key){ case "p": - const oldAwnsersURL = GM_getValue(URL_STORAGE_KEY); - let newAwnsersURL = prompt("Please input the answer url (itexamanswers.net)", oldAwnsersURL); - GM_setValue(URL_STORAGE_KEY, newAwnsersURL); - awnserData = await window.fetchAwnsers(newAwnsersURL); + const oldAnswersURL = GM_getValue(URL_STORAGE_KEY); + const newAnswersURL = prompt("Please input the answer url (itexamanswers.net)", oldAnswersURL); + GM_setValue(URL_STORAGE_KEY, newAnswersURL); + answerData = await window.fetchAnswers(newAnswersURL); break; case "n": @@ -34,7 +31,7 @@ window.addEventListener("keydown", async (event) => { break; case "a": - window.awnserQuestion(awnserData); + window.answerQuestion(answerData); break; } }); \ No newline at end of file diff --git a/types/GM_xmlhttpRequest.d.js b/types/GM_xmlhttpRequest.d.js new file mode 100644 index 0000000..a9a9e06 --- /dev/null +++ b/types/GM_xmlhttpRequest.d.js @@ -0,0 +1,37 @@ +/** + * Makes an HTTP request using GM.xmlHttpRequest. + * @param {Object} details - Details of the HTTP request. + * @param {string} details.method - The HTTP request method (e.g., 'GET', 'POST'). + * @param {string} details.url - The URL to send the request to. + * @param {Object} [details.headers] - Additional headers to include in the request. + * @param {string|FormData|Document|Blob|ArrayBuffer} [details.data] - The data to send with the request. + * @param {string} [details.overrideMimeType] - A MIME type to specify with the request (e.g., "text/html; charset=ISO-8859-1"). + * @param {string} [details.password] - Password to use for authentication purposes. + * @param {string} [details.responseType] - Decode the response as specified type. Accepted values are "", "arraybuffer", "blob", "document", "json", "text", "ms-stream". Default value is "text". See XMLHttpRequest responseType. + * @param {boolean} [details.synchronous] - Defaults to false. When true, this is a synchronous request. Be careful: The entire Firefox UI will be locked and frozen until the request completes. In this mode, more data will be available in the return value. + * @param {number} [details.timeout] - The number of milliseconds to wait before terminating the call; zero (the default) means wait forever. + * @param {Object} [details.upload] - Object containing optional function callbacks (onabort, onerror, onload, onprogress) to monitor the upload of data. Each is passed one argument, the Response Object. + * @param {string} details.url - Required. The URL to make the request to. Must be an absolute URL, beginning with the scheme. May be relative to the current page. + * @param {string} [details.user] - User name to use for authentication purposes. + * @param {function(GMXMLHttpRequestResponse):void} [details.onload] - Optional. Will be called when the request has completed successfully. Passed one argument, the Response Object. + * @param {Function} [details.onerror] - Optional. Will be called if an error occurs while processing the request. Passed one argument, the Response Object. + * @param {Function} [details.onabort] - Optional. Will be called when the request is aborted. Passed one argument, the Response Object. + * @param {Function} [details.ontimeout] - Optional. Will be called if/when the request times out. Passed one argument, the Response Object. + * @param {Function} [details.onprogress] - Optional. Will be called when the request progress changes. Passed one argument, the Response Object. + * @param {Function} [details.onreadystatechange] - Optional. Will be called repeatedly while the request is in progress. Passed one argument, the Response Object. + * @returns {undefined} + */ +function GM_xmlhttpRequest(details) { + // Implementation is not necessary for a declaration file +} + +/** + * Response object for the HTTP request. + * @typedef {Object} GMXMLHttpRequestResponse + * @property {number} readyState - The state of the request. + * @property {string} responseHeaders - The response headers. + * @property {string} responseText - The response body as text. + * @property {number} status - The HTTP status code of the response. + * @property {string} statusText - The status message corresponding to the status code. + * @property {Object} responseHeaders - The response headers. + */ diff --git a/types/awnser.d.js b/types/awnser.d.js new file mode 100644 index 0000000..d6d11c0 --- /dev/null +++ b/types/awnser.d.js @@ -0,0 +1,6 @@ +/** + * Object representing a question with answers. + * @typedef {Object} Answer + * @property {string} question - The question. + * @property {Array} answers - An array of answers. + */