Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,5 @@ In addition to these plugins, Interactive book also inherit's all the plugins us
---
### Create, Contribute, Learn, and succeed with CircuitVerse!!!

Interactive-Book is © 2024 by [CircuitVerse](https://circuitverse.org/)
Interactive-Book is © 2025 by [CircuitVerse](https://circuitverse.org/)

158 changes: 158 additions & 0 deletions _sass/quiz-feedback.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* ===============================================
Pop Quiz Feedback Styles
=============================================== */

/* Disable pointer events on answered questions */
.quiz-question.answered .quiz-answer {
cursor: not-allowed;
}

.quiz-question.answered .quiz-answer:not(.quiz-show-answer) {
opacity: 0.5;
}

/* Disabled answers */
.quiz-answer.quiz-disabled {
pointer-events: none;
opacity: 0.5;
}

/* Enhanced correct answer styling */
.quiz-answer.quiz-show-answer.quiz-true {
background-color: #d4edda !important;
border-color: #28a745 !important;
animation: correctPulse 0.5s ease;
}

/* Enhanced incorrect answer styling */
.quiz-answer.quiz-show-answer.quiz-false {
background-color: #f8d7da !important;
border-color: #dc3545 !important;
animation: incorrectShake 0.5s ease;
}

/* Animations */
@keyframes correctPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}

@keyframes incorrectShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}

/* Feedback Container */
.quiz-feedback-container {
margin-top: 1.5rem;
}

/* Feedback Box */
.quiz-feedback {
padding: 1.25rem;
border-radius: 8px;
animation: fadeInUp 0.4s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
}

@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.feedback-correct {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}

.feedback-incorrect {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}

.feedback-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
font-size: 1.1rem;
}

.feedback-icon {
font-size: 1.5rem;
font-weight: bold;
}

.feedback-correct .feedback-icon {
color: #28a745;
}

.feedback-incorrect .feedback-icon {
color: #dc3545;
}

.feedback-message {
line-height: 1.6;
font-size: 0.95rem;
margin-bottom: 1rem;
}

/* Reset button */
.quiz-reset-btn {
padding: 0.5rem 1rem;
background-color: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;

&:hover {
background-color: #5a6268;
}

&:active {
transform: scale(0.98);
}
}

/* Dark mode adjustments */
body.dark-mode {
.feedback-correct {
background-color: #1e3a28;
border-color: #28a745;
color: #7fdb9f;
}

.feedback-incorrect {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line should be indented 2 spaces, but was indented 4 spaces

background-color: #3a1e1e;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color literals like #3a1e1e should only be used in variable declarations; they should be referred to via variable everywhere else.

border-color: #dc3545;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color literals like #dc3545 should only be used in variable declarations; they should be referred to via variable everywhere else.

color: #f5b7bd;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color literals like #f5b7bd should only be used in variable declarations; they should be referred to via variable everywhere else.

}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line contains trailing whitespace

.quiz-answer.quiz-show-answer.quiz-true {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line should be indented 2 spaces, but was indented 4 spaces

background-color: #1e3a28 !important;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!important should not be used
Color literals like #1e3a28 should only be used in variable declarations; they should be referred to via variable everywhere else.

border-color: #28a745 !important;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!important should not be used
Color literals like #28a745 should only be used in variable declarations; they should be referred to via variable everywhere else.

}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line contains trailing whitespace

.quiz-answer.quiz-show-answer.quiz-false {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line should be indented 2 spaces, but was indented 4 spaces

background-color: #3a1e1e !important;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!important should not be used
Color literals like #3a1e1e should only be used in variable declarations; they should be referred to via variable everywhere else.

border-color: #dc3545 !important;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!important should not be used
Color literals like #dc3545 should only be used in variable declarations; they should be referred to via variable everywhere else.

}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line contains trailing whitespace

.quiz-feedback {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line should be indented 2 spaces, but was indented 4 spaces

box-shadow: 0 2px 8px rgba(255, 255, 255, 0.1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color literals like rgba(255, 255, 255, 0.1) should only be used in variable declarations; they should be referred to via variable everywhere else.

}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files should end with a trailing newline

3 changes: 1 addition & 2 deletions about.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ disable_comments: true
description: ""
---


# About Interactive Book
Interactive-Book is © 2024 by [CircuitVerse](https://circuitverse.org/).
Interactive-Book is © 2025 by [CircuitVerse](https://circuitverse.org/).

## CircuitVerse
[CircuitVerse](https://circuitverse.org) is a digital circuit simulation platform. It aims to provide a platform where circuits can be designed and simulated using a graphical user interface. While users can design complete CPU implementations within the simulator, the software is designed primarily for educational use. CircuitVerse is an opensource project with an active community.
Expand Down
2 changes: 2 additions & 0 deletions assets/css/just-the-docs-circuitverse.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
---
---
{% include css/just-the-docs.scss.liquid color_scheme="circuitverse" %}

@import "quiz-feedback";
4 changes: 3 additions & 1 deletion assets/css/just-the-docs-circuitversedark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

.text-grey-dk-100 {
color: white!important;
}
}

@import "quiz-feedback";
132 changes: 120 additions & 12 deletions assets/js/quiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,28 @@ $(function() {
question.append('<em>Question ' + questionNo + '</em>');
question.append('<p>' + questionText + '</p>');

// Get answers
// Get answers with explanations/hints
var answers = [];
$(this).find('ul').each(function() {

// Correct answers from <ol> with explanations
$(this).find('ol').each(function() {
$(this).children('li').each(function() {
answers.push([false, $(this).contents().get(0).nodeValue.trim()]);
var text = $(this).contents().get(0).nodeValue.trim();
// Check for explanation in <em> tag, otherwise use default
var explanation = $(this).find('em').text() ||
"Correct! Well done.";
answers.push([true, text, explanation]);
});
});
$(this).find('ol').each(function() {

// Incorrect answers from <ul> with hints
$(this).find('ul').each(function() {
$(this).children('li').each(function() {
answers.push([true, $(this).contents().get(0).nodeValue.trim()]);
var text = $(this).contents().get(0).nodeValue.trim();
// Check for hint in <em> tag, otherwise use default
var hint = $(this).find('em').text() ||
"That's not correct. Try again!";
answers.push([false, text, hint]);
});
});
Comment on lines +18 to 41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden answer text extraction and escaping to avoid edge‑case crashes and XSS

The new logic for pulling answer text and explanations/hints largely meets the UX goals, but it’s brittle and slightly unsafe:

  • $(this).contents().get(0).nodeValue.trim(); will throw if the first child is not a text node (e.g., if an author wraps the answer in <strong> or there is no text node at index 0), because nodeValue may be null or get(0) may be undefined.
  • The answer text (answer[1]) is later interpolated directly into an HTML string without escaping. Since it originates from a text node, this is usually fine, but encoded sequences like &lt;img ...&gt; would become real HTML when reinserted, which can re‑open injection surface.

To make this more robust you can:

  • Derive the answer text from the <li> while stripping any <em> explanation node, then use .text().trim() on the remainder.
  • Escape the answer text when building answerDiv (similar to how you already escape the feedback).

For example:

-                var text = $(this).contents().get(0).nodeValue.trim();
+                // Extract visible answer text, excluding any <em> explanation node
+                var $li = $(this).clone();
+                $li.children('em').remove();
+                var text = $li.text().trim();
@@
-                var answerDiv = $('<div class="' + classes + '" data-feedback="' + 
-                    escapeHtml(answer[2]) + '">' +
-                    answer[1] +
-                    '</div>');
+                var answerDiv = $('<div class="' + classes + '" data-feedback="' + 
+                    escapeHtml(answer[2]) + '">' +
+                    escapeHtml(answer[1]) +
+                    '</div>');

This keeps the authoring experience the same but guards against malformed markup and unintended HTML interpretation.

Also applies to: 57-63

πŸ€– Prompt for AI Agents
In assets/js/quiz.js around lines 18 to 41 (and also apply same change at
57-63), extracting answer text using .contents().get(0).nodeValue is brittle and
unsafe and the extracted text is later interpolated into HTML unescaped;
instead, derive the answer text from the <li> itself by temporarily cloning the
<li> and removing any inner <em> explanation/hint node, then call .text().trim()
on that clone to get robust plain-text; finally, ensure the text is HTML-escaped
when you build the answerDiv (use your existing escape helper or implement an
escape function) before interpolating into the DOM to prevent unintended HTML
interpretation or XSS.


Expand All @@ -42,25 +54,121 @@ $(function() {
classes += 'quiz-false';
}

questionAnswers.append('<div class="' + classes + '" onclick="ShowQuizAnswer(this)">' +
answer[1] +
'</div>');
var answerDiv = $('<div class="' + classes + '" data-feedback="' +
escapeHtml(answer[2]) + '">' +
answer[1] +
'</div>');

questionAnswers.append(answerDiv);
});

// Add feedback container
var feedbackContainer = $('<div class="quiz-feedback-container"></div>');

question.append(questionAnswers);
question.append(feedbackContainer);

quiz.append(question);
});

quizSettings.after('<br>');
quizSettings.after(quiz);
quizSettings.remove();

// Attach click handlers after DOM insertion
$('.quiz-answer').on('click', ShowQuizAnswer);
}
});

function ShowQuizAnswer(element) {
if (!$(element).hasClass('quiz-show-answer')) {
$(element).addClass('quiz-show-answer');
function ShowQuizAnswer(event) {
var element = $(event.currentTarget);
var questionContainer = element.closest('.quiz-question');
var allAnswers = questionContainer.find('.quiz-answer');
var feedbackContainer = questionContainer.find('.quiz-feedback-container');

// Prevent re-selection
if (questionContainer.hasClass('answered')) {
return;
}

// Mark question as answered
questionContainer.addClass('answered');

// Show the selected answer
if (!element.hasClass('quiz-show-answer')) {
element.addClass('quiz-show-answer');
}

// Disable all other answers
allAnswers.not(element).addClass('quiz-disabled');

// Determine if correct or incorrect
var isCorrect = element.hasClass('quiz-true');
var feedbackText = element.data('feedback');

// Create and show feedback
var feedbackDiv = $('<div class="quiz-feedback"></div>');

if (isCorrect) {
feedbackDiv.addClass('feedback-correct');
feedbackDiv.html(
'<div class="feedback-header">' +
'<span class="feedback-icon">βœ“</span>' +
'<span class="feedback-title">Correct!</span>' +
'</div>' +
'<div class="feedback-message">' + feedbackText + '</div>'
);
} else {
feedbackDiv.addClass('feedback-incorrect');
feedbackDiv.html(
'<div class="feedback-header">' +
'<span class="feedback-icon">βœ—</span>' +
'<span class="feedback-title">Not quite right</span>' +
'</div>' +
'<div class="feedback-message">' + feedbackText + '</div>'
);
}

// Add reset button
var resetBtn = $('<button class="quiz-reset-btn">Try Again</button>');
resetBtn.on('click', function() {
ResetQuiz(questionContainer);
});
feedbackDiv.append(resetBtn);

// Show feedback with animation
feedbackContainer.html(feedbackDiv);

// Smooth scroll to feedback
setTimeout(function() {
feedbackDiv[0].scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, 100);
}

function ResetQuiz(questionContainer) {
// Remove answered state
questionContainer.removeClass('answered');

// Reset all answers
var allAnswers = questionContainer.find('.quiz-answer');
allAnswers.removeClass('quiz-show-answer quiz-disabled');

// Clear feedback
questionContainer.find('.quiz-feedback-container').empty();
}

function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}

function FilterHtml(contents) {
Expand Down Expand Up @@ -90,4 +198,4 @@ function FilterHtml(contents) {
});

return html;
}
}