1
1
Fork 0
This commit is contained in:
Dominik Säume 2024-05-23 14:01:51 +00:00 committed by Dominik Säume
commit 69fb65deb7
Signed by: SZUT-Dominik
GPG key ID: 67D15BB250B41E7C
9 changed files with 315 additions and 0 deletions

19
README.md Normal file
View file

@ -0,0 +1,19 @@
# CCNA Autofill Userscript
A Userscript for autofilling the Answers on the **CCNA**.
inspired by [Merlinfuchs/ccna-extension](https://github.com/merlinfuchs/ccna-extension).
## Installation
Use a userscript client like [ViolentMonkey](https://violentmonkey.github.io/), [Tampermonkey](https://www.tampermonkey.net/).
[![Install USerscript](https://img.shields.io/badge/Install_Userscript-Install_Userscript?style=for-the-badge&logo=tampermonkey&logoColor=brown&color=lightgray)](https://git.euph.dev/SZUT-Dominik/CCNA_Autofill_Userscript/raw/branch/main/src/main.user.js)
## Status
This Userscript is only tested with [ViolentMonkey](https://violentmonkey.github.io/) on the **CCNAv7**.
## Usage
- **p**: Paste your answer-URL from [ItexamAnswers](https://itexamanswers.net) for the **CCNAv7** use [these](https://itexamanswers.net/ccna-1-v7-modules-1-3-basic-network-connectivity-and-communications-exam-answers.html).
- **a**: Try autofilling the current answer. There won't be any visual feedback when it fails.
- **n**: Go to the next Question.

7
jsconfig.json Normal file
View file

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

72
src/answer.js Normal file
View file

@ -0,0 +1,72 @@
/**
*
* @param {Array<Answer>} answerData
* @returns
*/
function answerQuestion(answerData){
const question = document.querySelector(".question:not(.hidden)");
if (!question) {
return;
}
const questionTextDom = question.querySelector(".questionText .mattext");
if (!questionTextDom) return;
const questionText = questionTextDom.textContent.trim();
const answersDom = question.querySelector("ul.coreContent");
if (!answersDom) return;
const answers = answersDom.children;
for (let answer of Array.from(answers)) {
const input = answer.querySelector("input");
if (!input) continue;
input.checked = false;
}
const correctAnswers = findAnswers(answerData, questionText, answers);
if (correctAnswers.length === 0) {
return;
}
for (const answer of correctAnswers) {
const input = answer.querySelector("input");
if (!input) continue;
input.checked = true;
}
}
/**
*
* @param {Array<Answer>} answerData
* @param {string} questionText
* @returns
*/
function findAnswers(answerData, questionText, answers) {
if (answerData === null) {
return [];
}
const correctAnswers = [];
for (let entry of answerData) {
if (matchAnswer(questionText.trim(), entry.question.trim())) {
for (let availableAnswer of answers) {
for (let possibleAnswer of entry.answers) {
if (matchAnswer(availableAnswer.textContent.trim(), possibleAnswer)) {
correctAnswers.push(availableAnswer);
}
}
}
}
}
return correctAnswers;
}
function matchAnswer(textA, textB) {
const replaceRegex = /[^\w]/gi;
textA = textA.replace(replaceRegex, "");
textB = textB.replace(replaceRegex, "");
return (textA === textB);
}
window.answerQuestion = answerQuestion;

144
src/fetch.js Normal file
View file

@ -0,0 +1,144 @@
const QUESTION_REGEX = /^[0-9]+\. (.*)$/;
/**
* Fetches answers from the specified URL.
* @param {string} [answerURL=""] - The URL to fetch answers from.
* @returns {Promise<Array<Answer>>} 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<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);
if (result != undefined) {
results.push(result);
}
}
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") ||
!element.innerHTML
) {
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(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;

38
src/main.user.js Normal file
View file

@ -0,0 +1,38 @@
// ==UserScript==
// @name CCNA Autofill
// @namespace Violentmonkey Scripts
// @match *://assessment.netacad.net/*
// @match *://www.assessment.netacad.net/*
// @match *://www.google.com/*
// @match *://www.google.de/*
// @require https://git.euph.dev/SZUT-Dominik/CCNA_Autofill_Userscript/raw/branch/main/src/fetch.js
// @require https://git.euph.dev/SZUT-Dominik/CCNA_Autofill_Userscript/raw/branch/main/src/answer.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @version 1.0.0
// @author Dominik Säume
// ==/UserScript==
const URL_STORAGE_KEY = "itexamanswers.net URL";
/** @type {Array<Answer>} */
let answerData;
window.addEventListener("keydown", async (event) => {
switch(event.key){
case "p":
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":
document.getElementById("next").click();
break;
case "a":
window.answerQuestion(answerData);
break;
}
});

6
types/Answer.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.
*/

7
types/GM_getValue.d.js Normal file
View file

@ -0,0 +1,7 @@
/**
* @param {string} storageKey
* @returns {string}
*/
function GM_getValue(storageKey) {
return "";
}

5
types/GM_setValue.d.js Normal file
View file

@ -0,0 +1,5 @@
/**
* @param {string} storageKey
* @param {string} value
*/
function GM_setValue(storageKey, value) {}

View file

@ -0,0 +1,17 @@
/**
* 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 {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.
* @returns {undefined}
*/
function GM_xmlhttpRequest(details) {}
/**
* Response object for the HTTP request.
* @typedef {Object} GMXMLHttpRequestResponse
* @property {string} responseText - The response body as text.
*/