Setup
This commit is contained in:
commit
d96bed7368
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.
|
7
jsconfig.json
Normal file
7
jsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"checkJs": true,
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["dom", "es6"]
|
||||||
|
}
|
||||||
|
}
|
72
src/answer.js
Normal file
72
src/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
src/fetch.js
Normal file
144
src/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;
|
38
src/main.user.js
Normal file
38
src/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