1
1
Fork 0
This commit is contained in:
Dominik Säume 2024-05-23 17:34:40 +02:00
parent d7a20264fb
commit f9383a99c9
Signed by: SZUT-Dominik
GPG key ID: 67D15BB250B41E7C
6 changed files with 205 additions and 100 deletions

View file

@ -1,4 +1,4 @@
function awnserQuestion(awnserData){ function answerQuestion(answerData){
const question = document.querySelector(".question:not(.hidden)"); const question = document.querySelector(".question:not(.hidden)");
if (!question) { if (!question) {
return; return;
@ -12,15 +12,14 @@ function awnserQuestion(awnserData){
if (!answersDom) return; if (!answersDom) return;
const answers = answersDom.children; const answers = answersDom.children;
for (let answer of answers) { for (let answer of Array.from(answers)) {
const input = answer.querySelector("input"); const input = answer.querySelector("input");
if (!input) continue; if (!input) continue;
input.checked = false; input.checked = false;
} }
const correctAnswers = findAnswers(awnserData, questionText, answers); const correctAnswers = findAnswers(answerData, questionText, answers);
if (correctAnswers.length === 0) { if (correctAnswers.length === 0) {
GM_log("no awnser")
return; return;
} }
@ -31,18 +30,17 @@ function awnserQuestion(awnserData){
} }
} }
function findAnswers(awnserData, questionText, answers) { function findAnswers(answerData, questionText, answers) {
if (awnserData === null) { if (answerData === null) {
alert("No chapter data loaded. Maybe the fetch failed?!");
return []; return [];
} }
const correctAnswers = []; const correctAnswers = [];
for (let entry of awnserData) { for (let entry of answerData) {
if (matchAwnser(questionText.trim(), entry.question.trim())) { if (matchAnswer(questionText.trim(), entry.question.trim())) {
for (let availableAnswer of answers) { for (let availableAnswer of answers) {
for (let possibleAnswer of entry.answers) { for (let possibleAnswer of entry.answers) {
if (matchAwnser(availableAnswer.textContent.trim(), possibleAnswer)) { if (matchAnswer(availableAnswer.textContent.trim(), possibleAnswer)) {
correctAnswers.push(availableAnswer); correctAnswers.push(availableAnswer);
} }
} }
@ -53,11 +51,11 @@ function findAnswers(awnserData, questionText, answers) {
return correctAnswers; return correctAnswers;
} }
function matchAwnser(textA, textB) { function matchAnswer(textA, textB) {
const replaceRegex = /[^\w]/gi; const replaceRegex = /[^\w]/gi;
textA = textA.replace(replaceRegex, ""); textA = textA.replace(replaceRegex, "");
textB = textB.replace(replaceRegex, ""); textB = textB.replace(replaceRegex, "");
return (textA === textB); return (textA === textB);
} }
window.awnserQuestion = awnserQuestion; window.answerQuestion = answerQuestion;

214
fetch.js
View file

@ -1,79 +1,139 @@
function fetchAwnsers(awnserURL = "") { const IS_QUESTION_REGEX = /^[0-9]+\. (.*)$/;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({ /**
method: "GET", * Fetches answers from the specified URL.
url: awnserURL, * @param {string} [answerURL=""] - The URL to fetch answers from.
headers: { * @returns {Promise<Array<Answer>>} A Promise that resolves with the fetched answers.
"Content-Type": "text/html", */
}, function fetchAnswers(answerURL = "") {
onload: function (response) { return new Promise((resolve, reject) => {
awnserImgs = new Map(); GM_xmlhttpRequest({
const results = []; method: "GET",
const parser = new DOMParser(); url: answerURL,
const virtDom = parser.parseFromString( headers: {
response.responseText, "Content-Type": "text/html",
"text/html" },
); onload: function (response) {
parseAnswers(response, resolve);
let answersDom = virtDom.querySelector(".pf-content"); },
if (!answersDom) { onerror: function (error) {
answersDom = virtDom.querySelector(".thecontent"); reject(error);
} },
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)
}
});
}); });
});
}
/**
*
* @param {GMXMLHttpRequestResponse} response
* @param {(value: Answer[] | PromiseLike<Answer[]>) => 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; 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<string>}
*/
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;

7
jsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"checkJs": true,
"target": "es6",
"lib": ["dom", "es6"]
}
}

View file

@ -6,27 +6,24 @@
// @match *://www.google.com/* // @match *://www.google.com/*
// @match *://www.google.de/* // @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/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_setValue
// @grant GM_getValue // @grant GM_getValue
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_log // @version 0.0.12
// @version 0.0.11
// @author Dominik Säume // @author Dominik Säume
// ==/UserScript== // ==/UserScript==
const URL_STORAGE_KEY = "itexamanswers.net URL"; const URL_STORAGE_KEY = "itexamanswers.net URL";
let awnserData; let answerData;
console.log = console.__proto__.log;
window.addEventListener("keydown", async (event) => { window.addEventListener("keydown", async (event) => {
switch(event.key){ switch(event.key){
case "p": case "p":
const oldAwnsersURL = GM_getValue(URL_STORAGE_KEY); const oldAnswersURL = GM_getValue(URL_STORAGE_KEY);
let newAwnsersURL = prompt("Please input the answer url (itexamanswers.net)", oldAwnsersURL); const newAnswersURL = prompt("Please input the answer url (itexamanswers.net)", oldAnswersURL);
GM_setValue(URL_STORAGE_KEY, newAwnsersURL); GM_setValue(URL_STORAGE_KEY, newAnswersURL);
awnserData = await window.fetchAwnsers(newAwnsersURL); answerData = await window.fetchAnswers(newAnswersURL);
break; break;
case "n": case "n":
@ -34,7 +31,7 @@ window.addEventListener("keydown", async (event) => {
break; break;
case "a": case "a":
window.awnserQuestion(awnserData); window.answerQuestion(answerData);
break; break;
} }
}); });

View file

@ -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<string, string>} [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<string, string>} responseHeaders - The response headers.
*/

6
types/awnser.d.js Normal file
View file

@ -0,0 +1,6 @@
/**
* Object representing a question with answers.
* @typedef {Object} Answer
* @property {string} question - The question.
* @property {Array<string>} answers - An array of answers.
*/