Setup
This commit is contained in:
commit
7b54b1b20a
9 changed files with 315 additions and 0 deletions
19
README.md
Normal file
19
README.md
Normal 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/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.
|
72
answer.js
Normal file
72
answer.js
Normal 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
fetch.js
Normal file
144
fetch.js
Normal 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;
|
7
jsconfig.json
Normal file
7
jsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"target": "es6",
|
||||
"lib": ["dom", "es6"]
|
||||
}
|
||||
}
|
38
main.user.js
Normal file
38
main.user.js
Normal 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/fetch.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
|
||||
// @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.js
Normal file
6
types/Answer.js
Normal 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
7
types/GM_getValue.d.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* @param {string} storageKey
|
||||
* @returns {string}
|
||||
*/
|
||||
function GM_getValue(storageKey) {
|
||||
return "";
|
||||
}
|
5
types/GM_setValue.d.js
Normal file
5
types/GM_setValue.d.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* @param {string} storageKey
|
||||
* @param {string} value
|
||||
*/
|
||||
function GM_setValue(storageKey, value) {}
|
17
types/GM_xmlhttpRequest.d.js
Normal file
17
types/GM_xmlhttpRequest.d.js
Normal 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.
|
||||
*/
|
Loading…
Reference in a new issue