+
+ {t('learn.exam.results-header', { title })}
+
{examResultsMessage}
+
-
Time: {timeInSeconds}
- {correctAnswers.length} of {results.length} correct answers |{' '}
- {correctPercent}%
+ {t('learn.exam.question-results', {
+ n: numberOfCorrectAnswers,
+ q: numberOfQuestionsInExam
+ })}
+
+
|
+
+ {t('learn.exam.percent-results', {
+ p: percentCorrect
+ })}
+
+
|
+
+ {t('learn.exam.time', { t: formatSecondsToTime(examTimeInSeconds) })}
-
- {results.map((result, index) => (
- <>
-
-
- {result.correct ? : }
-
-
-
-
- Question {index + 1}
-
-
{result.question}
-
Your Answer:
-
{result.answer}
-
-
-
- >
- ))}
-
+
+
+
@@ -106,4 +111,4 @@ function ExamResults({
ExamResults.displayName = 'ExamResults';
-export default connect(mapStateToProps, mapDispatchToProps)(ExamResults);
+export default ExamResults;
diff --git a/client/src/templates/Challenges/exam/components/exit-exam-modal.tsx b/client/src/templates/Challenges/exam/components/exit-exam-modal.tsx
new file mode 100644
index 00000000000..d8cd3676e36
--- /dev/null
+++ b/client/src/templates/Challenges/exam/components/exit-exam-modal.tsx
@@ -0,0 +1,84 @@
+// Package Utilities
+import { Button, Modal } from '@freecodecamp/react-bootstrap';
+import React from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators, Dispatch } from 'redux';
+import { createSelector } from 'reselect';
+import { useTranslation } from 'react-i18next';
+
+// Local Utilities
+import { closeModal } from '../../redux/actions';
+import { isExitExamModalOpenSelector } from '../../redux/selectors';
+
+// Types
+interface ExitExamModalProps {
+ closeExitExamModal: () => void;
+ isExitExamModalOpen: boolean;
+ exitExam: () => void;
+}
+
+// Redux Setup
+const mapStateToProps = createSelector(
+ isExitExamModalOpenSelector,
+ (isExitExamModalOpen: boolean) => ({
+ isExitExamModalOpen
+ })
+);
+
+const mapDispatchToProps = (dispatch: Dispatch) =>
+ bindActionCreators(
+ {
+ closeExitExamModal: () => closeModal('exitExam')
+ },
+ dispatch
+ );
+
+// Component
+function ExitExamModal({
+ closeExitExamModal,
+ isExitExamModalOpen,
+ exitExam
+}: ExitExamModalProps): JSX.Element {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t('learn.exam.exit-header')}
+
+
+
+ {t('learn.exam.exit')}
+
+
+
+
+
+
+ );
+}
+
+ExitExamModal.displayName = 'ExitExamModal';
+
+export default connect(mapStateToProps, mapDispatchToProps)(ExitExamModal);
diff --git a/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx b/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx
index 0590ca724a5..294d698c2fd 100644
--- a/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx
+++ b/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx
@@ -4,6 +4,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
+import { useTranslation } from 'react-i18next';
// Local Utilities
import { closeModal } from '../../redux/actions';
@@ -38,6 +39,8 @@ function FinishExamModal({
isFinishExamModalOpen,
finishExam
}: FinishExamModalProps): JSX.Element {
+ const { t } = useTranslation();
+
return (
- Finish Exam
+
+ {t('learn.exam.finish-header')}
+
-
- Are you sure? You will not be able to change any answers. Your results
- will be final.
-
+ {t('learn.exam.finish')}
diff --git a/client/src/templates/Challenges/exam/exam.css b/client/src/templates/Challenges/exam/exam.css
index a0709d3c599..c2b551f8f81 100644
--- a/client/src/templates/Challenges/exam/exam.css
+++ b/client/src/templates/Challenges/exam/exam.css
@@ -1,4 +1,5 @@
-.exam-wrapper {
+.exam-wrapper,
+.exam-results-wrapper {
margin-top: 60px;
padding: 25px;
background-color: var(--primary-background);
@@ -6,12 +7,14 @@
}
@media (max-width: 600px) {
- .exam-wrapper {
+ .exam-wrapper,
+ .exam-results-wrapper {
margin-top: 80px;
}
}
-.exam-header {
+.exam-header,
+.exam-results-header {
width: 100%;
display: flex;
justify-content: space-evenly;
@@ -28,12 +31,30 @@
.exam-answer-label {
display: flex;
- margin-bottom: 20px;
+ align-items: center;
+ margin-bottom: 15px;
padding: 20px;
cursor: pointer;
font-weight: normal;
}
+.exam-answer-label > .line-numbers,
+.exam-questions > .line-numbers {
+ display: inline-block;
+}
+
+.exam-answers pre,
+.exam-questions pre {
+ color: var(--tertiary-color);
+ border: 1px solid var(--secondary-color);
+ background-color: var(--quaternary-background);
+ padding: 10px;
+}
+
+.exam-answer-label p {
+ margin: 0;
+}
+
.exam-answer-input-hidden {
position: absolute;
left: -9999px;
@@ -74,17 +95,11 @@
max-width: 40%;
}
-.exam-result {
- display: flex;
+.exam-results-message {
+ text-align: center;
}
-.exam-result-icon > svg {
- width: 40px;
- height: 40px;
-}
-
-.exam-result-questions {
+.exam-results {
display: flex;
- flex-direction: column;
- padding-inline-start: 30px;
+ justify-content: space-evenly;
}
diff --git a/client/src/templates/Challenges/exam/show.tsx b/client/src/templates/Challenges/exam/show.tsx
index 45ac3f773f1..54f962c8a88 100644
--- a/client/src/templates/Challenges/exam/show.tsx
+++ b/client/src/templates/Challenges/exam/show.tsx
@@ -1,6 +1,6 @@
// Package Utilities
import { Alert, Grid, Col, Row, Button } from '@freecodecamp/react-bootstrap';
-import { graphql } from 'gatsby';
+import { graphql, navigate } from 'gatsby';
import React, { Component, RefObject } from 'react';
import Helmet from 'react-helmet';
import type { TFunction } from 'i18next';
@@ -9,6 +9,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
+import { micromark } from 'micromark';
// Local Utilities
import Spacer from '../../../components/helpers/spacer';
@@ -18,11 +19,12 @@ import PrismFormatted from '../components/prism-formatted';
import CompletionModal from '../components/completion-modal';
import HelpModal from '../components/help-modal';
import Hotkeys from '../components/hotkeys';
-import { startExam, stopExam } from '../../../redux/actions';
+import { clearExamResults, startExam, stopExam } from '../../../redux/actions';
import {
completedChallengesSelector,
isSignedInSelector,
- examInProgressSelector
+ examInProgressSelector,
+ examResultsSelector
} from '../../../redux/selectors';
import {
challengeMounted,
@@ -30,19 +32,26 @@ import {
openModal,
closeModal,
submitChallenge,
- setExamResults,
+ setUserCompletedExam,
updateSolutionFormValues
} from '../redux/actions';
+import { getGenerateExam } from '../../../utils/ajax';
import { isChallengeCompletedSelector } from '../redux/selectors';
import { createFlashMessage } from '../../../components/Flash/redux';
import {
ChallengeNode,
ChallengeMeta,
- CompletedChallenge
+ CompletedChallenge,
+ UserExamQuestion,
+ UserExam,
+ GeneratedExamResults,
+ GeneratedExamQuestion
} from '../../../redux/prop-types';
+import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
+import { formatSecondsToTime } from '../../../utils/format-seconds';
+import ExitExamModal from './components/exit-exam-modal';
import FinishExamModal from './components/finish-exam-modal';
import ExamResults from './components/exam-results';
-
import './exam.css';
// Redux
@@ -51,16 +60,19 @@ const mapStateToProps = createSelector(
isChallengeCompletedSelector,
isSignedInSelector,
examInProgressSelector,
+ examResultsSelector,
(
completedChallenges: CompletedChallenge[],
isChallengeCompleted: boolean,
isSignedIn: boolean,
- examInProgress: boolean
+ examInProgress: boolean,
+ examResults: GeneratedExamResults | null
) => ({
completedChallenges,
isChallengeCompleted,
isSignedIn,
- examInProgress
+ examInProgress,
+ examResults
})
);
@@ -69,11 +81,14 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
{
challengeMounted,
createFlashMessage,
+ openExitExamModal: () => openModal('exitExam'),
+ closeExitExamModal: () => closeModal('exitExam'),
openFinishExamModal: () => openModal('finishExam'),
closeFinishExamModal: () => closeModal('finishExam'),
startExam,
stopExam,
- setExamResults,
+ setUserCompletedExam,
+ clearExamResults,
submitChallenge,
updateChallengeMeta,
updateSolutionFormValues
@@ -85,11 +100,15 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
interface ShowExamProps {
challengeMounted: (arg0: string) => void;
completedChallenges: CompletedChallenge[];
+ clearExamResults: () => void;
createFlashMessage: typeof createFlashMessage;
data: { challengeNode: ChallengeNode };
examInProgress: boolean;
+ examResults: GeneratedExamResults | null;
isChallengeCompleted: boolean;
isSignedIn: boolean;
+ openExitExamModal: () => void;
+ closeExitExamModal: () => void;
openFinishExamModal: () => void;
closeFinishExamModal: () => void;
pageContext: {
@@ -99,199 +118,22 @@ interface ShowExamProps {
startExam: () => void;
stopExam: () => void;
submitChallenge: () => void;
- setExamResults: (arg0: ExamResults) => void;
+ setUserCompletedExam: (arg0: UserExam) => void;
updateChallengeMeta: (arg0: ChallengeMeta) => void;
}
interface ShowExamState {
currentQuestionIndex: number;
examTimeInSeconds: number;
- generatedExam: GeneratedExamQuestion[];
- userExam: UserExamQuestion[];
+ generatedExamQuestions: GeneratedExamQuestion[];
+ userExamQuestions: UserExamQuestion[];
showResults: boolean;
}
-interface GeneratedExamQuestion {
- question: string;
- answers: string[];
+function convertMd(md: string): string {
+ return micromark(md);
}
-interface UserExamQuestion {
- question: string;
- answer: string | null;
-}
-
-interface ExamResultQuestion {
- question: string;
- answer: string;
- correct: boolean;
-}
-
-interface ExamResults {
- timeInSeconds: number;
- results: ExamResultQuestion[];
-}
-
-const examInDatabase = [
- {
- question: 'Which of the following is a programming language?',
- wrongAnswers: ['Apple', 'Orange', 'Banana', 'Mango'],
- correctAnswer: 'Python'
- },
- {
- question: 'What does CSS stand for?',
- wrongAnswers: [
- 'Computer Style Sheets',
- 'Complete Style Sheets',
- 'Cool Style Sheets',
- 'Creative Style Sheets'
- ],
- correctAnswer: 'Cascading Style Sheets'
- },
- {
- question: 'What is the extension for a JavaScript file?',
- wrongAnswers: ['.txt', '.doc', '.html', '.css'],
- correctAnswer: '.js'
- },
- {
- question: 'What is the purpose of the "if" statement in programming?',
- wrongAnswers: [
- 'To repeat a set of instructions',
- 'To define a function',
- 'To assign a value to a variable',
- 'To declare a loop'
- ],
- correctAnswer: 'To check a condition'
- },
- {
- question:
- 'What is the symbol used to represent addition in most programming languages?',
- wrongAnswers: ['-', '*', '=', '%'],
- correctAnswer: '+'
- },
- {
- question: 'Which of the following is NOT a programming language?',
- wrongAnswers: ['Java', 'Ruby', 'Swift', 'PHP'],
- correctAnswer: 'Spanish'
- }
-];
-
-Object.freeze(examInDatabase);
-
-// TODO: move helper functions to utility file
-// helper functions
-function shuffleArray(
- array: string[] | GeneratedExamQuestion[]
-): string[] | GeneratedExamQuestion[] {
- let currentIndex = array.length,
- randomIndex;
-
- while (currentIndex != 0) {
- randomIndex = Math.floor(Math.random() * currentIndex);
- currentIndex--;
- [array[currentIndex], array[randomIndex]] = [
- array[randomIndex],
- array[currentIndex]
- ];
- }
-
- return array;
-}
-
-function formatSecondsToTime(s: number) {
- const hourInSeconds = 60 * 60;
- const minuteInSeconds = 60;
- const h = Math.floor(s / hourInSeconds);
- s -= h * hourInSeconds;
-
- const minutes = Math.floor(s / minuteInSeconds);
- s -= minutes * minuteInSeconds;
-
- const mm = minutes < 10 && h >= 1 ? `0${minutes}` : minutes;
- const seconds = s % 60;
- const ss = seconds < 10 ? `0${seconds}` : seconds;
-
- return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
-}
-
-// TODO: generate exam on server
-function generateExam(): GeneratedExamQuestion[] {
- const NUMBER_OF_ANSWERS_PER_QUESTION = 4;
- const NUMBER_OF_QUESTIONS_IN_EXAM = 5;
- const generatedExam: GeneratedExamQuestion[] = [];
- const examFromDatabase = Array.from(examInDatabase);
-
- while (generatedExam.length < NUMBER_OF_QUESTIONS_IN_EXAM) {
- const randomIndex = Math.floor(
- Math.random() * (examFromDatabase.length - 1)
- );
- const randomQuestion = examFromDatabase.splice(randomIndex, 1)[0];
- const wrongAnswers = randomQuestion.wrongAnswers;
- const answers = [randomQuestion.correctAnswer];
-
- while (answers.length < NUMBER_OF_ANSWERS_PER_QUESTION) {
- const index = Math.floor(Math.random() * (wrongAnswers.length - 1));
- const randomAnswer = wrongAnswers.splice(index, 1)[0];
- answers.push(randomAnswer);
- }
-
- const newExamQuestion: GeneratedExamQuestion = {
- question: randomQuestion.question,
- answers: shuffleArray(answers) as string[]
- };
-
- generatedExam.push(newExamQuestion);
- }
-
- return shuffleArray(generatedExam) as GeneratedExamQuestion[];
-}
-
-/* const exampleGeneratedExam = [
- {
- "question": "What is the extension for a JavaScript file?",
- "answers": ['.txt', '.js', '.html', '.css']
- },
- ...rest_of_questions
-]*/
-
-/* const exampleUserExam = [
- {
- "question": "What is the extension for a JavaScript file?",
- "answer": ".doc"
- },
- ...rest_of_questions
-]*/
-
-/* const exampleExamResults = [
- {
- "question": "What is the extension for a JavaScript file?",
- "answer": ".doc",
- "correct": false
- }
- ...rest_of_questions
- ]
- */
-
-/* example item added to completedChalleges array
- {
- "id": "645147516c245de4d11eb7ba",
- "completedDate": 1644532946064,
- "exam": {
- "completionTimeInSeconds": number,
- "results": [
- {
- "question": "What is the extension for a JavaScript file?",
- "answer": ".doc",
- "correct": false
- },
- ...rest_of_questions
- ]
- }
- }*/
-
-const generatedExam = generateExam();
-Object.freeze(generatedExam);
-
class ShowExam extends Component
{
static displayName: string;
private _container: RefObject | undefined;
@@ -301,9 +143,9 @@ class ShowExam extends Component {
super(props);
this.state = {
currentQuestionIndex: 0,
- generatedExam: generatedExam,
+ generatedExamQuestions: [],
examTimeInSeconds: 0,
- userExam: [],
+ userExamQuestions: [],
showResults: false
};
@@ -312,8 +154,8 @@ class ShowExam extends Component {
this.goToNextQuestion = this.goToNextQuestion.bind(this);
this.selectAnswer = this.selectAnswer.bind(this);
this.finishExam = this.finishExam.bind(this);
- this.createExamResults = this.createExamResults.bind(this);
- this.submitExamResults = this.submitExamResults.bind(this);
+ this.exitExam = this.exitExam.bind(this);
+ this.cleanUp = this.cleanUp.bind(this);
}
componentDidMount(): void {
@@ -339,11 +181,8 @@ class ShowExam extends Component {
}
componentWillUnmount() {
+ this.cleanUp();
this.props.stopExam();
- clearInterval(this.timerInterval);
- window.removeEventListener('beforeunload', this.stopWindowClose);
- window.removeEventListener('unload', this.stopWindowClose);
- window.removeEventListener('popstate', this.stopBrowserBack);
}
stopWindowClose = (event: Event) => {
@@ -357,36 +196,63 @@ class ShowExam extends Component {
alert(this.props.t('misc.navigation-warning'));
};
- runExam = () => {
+ runExam = async () => {
// TODO: show loader
- // TODO: fetch exam from server/database
- const newExam = this.state.generatedExam.map(q => {
- return { question: q.question, answer: null };
- });
+ const {
+ createFlashMessage,
+ data: {
+ challengeNode: {
+ challenge: { id: challengeId }
+ }
+ }
+ } = this.props;
- this.timerInterval = setInterval(() => {
- this.setState(state => ({
- examTimeInSeconds: state.examTimeInSeconds + 1
- }));
- }, 1000);
+ const generateExamResponse = await getGenerateExam(challengeId);
+ const { response, data } = generateExamResponse;
- this.setState(
- {
- userExam: newExam
- },
- this.props.startExam
- );
+ if (response.status === 200) {
+ const { generatedExam = [] } = data;
+ const emptyUserExamQuestions = generatedExam.map(q => {
+ return {
+ id: q.id,
+ question: q.question,
+ answer: { id: null, answer: null }
+ };
+ }) as UserExamQuestion[];
- window.addEventListener('beforeunload', this.stopWindowClose);
- window.addEventListener('unload', this.stopWindowClose);
- window.addEventListener('popstate', this.stopBrowserBack);
+ this.setState(
+ {
+ generatedExamQuestions: generatedExam,
+ userExamQuestions: emptyUserExamQuestions
+ },
+ () => {
+ this.timerInterval = setInterval(() => {
+ this.setState({
+ examTimeInSeconds: this.state.examTimeInSeconds + 1
+ });
+ }, 1000);
+
+ this.props.startExam();
+
+ window.addEventListener('beforeunload', this.stopWindowClose);
+ window.addEventListener('unload', this.stopWindowClose);
+ window.addEventListener('popstate', this.stopBrowserBack);
+ }
+ );
+ } else {
+ createFlashMessage({
+ type: 'danger',
+ message: FlashMessages.GenerateExamError
+ });
+ }
};
- selectAnswer = (index: number, option: string): void => {
- const newExam = Array.from(this.state.userExam);
- newExam[index].answer = option;
+ selectAnswer = (index: number, id: string, answer: string): void => {
+ const newUserExamQuestions = Array.from(this.state.userExamQuestions);
+ newUserExamQuestions[index].answer.id = id;
+ newUserExamQuestions[index].answer.answer = answer;
this.setState({
- userExam: newExam
+ userExamQuestions: newUserExamQuestions
});
};
@@ -402,51 +268,48 @@ class ShowExam extends Component {
});
};
- // TODO: have server check the exam
- createExamResults = () => {
- // TODO: show loader
- const { setExamResults } = this.props;
- const { userExam, examTimeInSeconds } = this.state;
-
- const results: ExamResultQuestion[] = [];
-
- userExam.forEach(userQuestion => {
- const questionInDb = examInDatabase.find(
- dbQuestion => userQuestion.question === dbQuestion.question
- );
-
- const questionResult = {
- question: userQuestion.question,
- answer: userQuestion.answer,
- correct: questionInDb?.correctAnswer === userQuestion.answer
- };
-
- results.push(questionResult as ExamResultQuestion);
- });
-
- const examResults = {
- timeInSeconds: examTimeInSeconds,
- results
- };
-
- setExamResults(examResults);
- };
-
- finishExam = () => {
+ cleanUp = () => {
clearInterval(this.timerInterval);
- this.props.closeFinishExamModal();
- this.createExamResults();
this.setState({
- showResults: true
+ examTimeInSeconds: 0,
+ currentQuestionIndex: 0
});
- };
- submitExamResults = () => {
window.removeEventListener('beforeunload', this.stopWindowClose);
window.removeEventListener('unload', this.stopWindowClose);
window.removeEventListener('popstate', this.stopBrowserBack);
- this.props.submitChallenge();
- this.props.stopExam();
+
+ this.props.clearExamResults();
+ this.props.closeExitExamModal();
+ this.props.closeFinishExamModal();
+ };
+
+ finishExam = () => {
+ // TODO: show loader
+ this.cleanUp();
+
+ const { setUserCompletedExam, submitChallenge } = this.props;
+ const { userExamQuestions, examTimeInSeconds } = this.state;
+
+ setUserCompletedExam({ userExamQuestions, examTimeInSeconds });
+ submitChallenge();
+ };
+
+ exitExam = () => {
+ this.cleanUp();
+
+ const {
+ data: {
+ challengeNode: {
+ challenge: {
+ fields: { blockHashSlug }
+ }
+ }
+ },
+ stopExam
+ } = this.props;
+ stopExam();
+ void navigate(blockHashSlug);
};
render() {
@@ -455,6 +318,7 @@ class ShowExam extends Component {
challengeNode: {
challenge: {
block,
+ dashedName,
description,
fields: { blockName },
instructions,
@@ -466,8 +330,10 @@ class ShowExam extends Component {
}
},
examInProgress,
+ examResults,
completedChallenges,
isChallengeCompleted,
+ openExitExamModal,
openFinishExamModal,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
@@ -478,15 +344,22 @@ class ShowExam extends Component {
const {
examTimeInSeconds,
currentQuestionIndex,
- generatedExam,
- userExam,
- showResults
+ generatedExamQuestions,
+ userExamQuestions
} = this.state;
- const missingPrequisites = prerequisites.filter(
- prerequisite =>
- !completedChallenges.find(({ id }) => prerequisite.id === id)
- );
+ type Prerequisite = {
+ id: string;
+ title: string;
+ };
+
+ let missingPrequisites: Prerequisite[] = [];
+ if (prerequisites) {
+ missingPrequisites = prerequisites?.filter(
+ prerequisite =>
+ !completedChallenges.find(({ id }) => prerequisite.id === id)
+ );
+ }
const qualifiedForExam = missingPrequisites.length === 0;
@@ -496,61 +369,78 @@ class ShowExam extends Component {
const windowTitle = `${blockNameTitle} | freeCodeCamp.org`;
const ariaLabel = t('aria.answer');
+ // TODO: If already taken exam, show different messages
+
return examInProgress ? (
- {showResults ? (
+ {examResults ? (
) : (
{title}
|
-
Time: {formatSecondsToTime(examTimeInSeconds)}
+
+ {t('learn.exam.time', {
+ t: formatSecondsToTime(examTimeInSeconds)
+ })}
+
|
- Question {currentQuestionIndex + 1} of{' '}
- {generatedExam.length}
+ {t('learn.exam.questions', {
+ n: currentQuestionIndex + 1,
+ t: generatedExamQuestions.length
+ })}
-
{generatedExam[currentQuestionIndex].question}
+
+
+
+
+
+
+
+
)}
+
@@ -621,11 +531,11 @@ class ShowExam extends Component
{
{qualifiedForExam ? (
- {t('learn.qualified-for-exam')}
+ {t('learn.exam.qualified')}
) : (
- {t('learn.not-qualified-for-exam')}
+ {t('learn.exam.not-qualified')}
{missingPrequisites.map(({ title, id }) => (
@@ -673,8 +583,10 @@ export const query = graphql`
challenge {
block
challengeType
+ dashedName
description
fields {
+ blockHashSlug
blockName
}
helpCategory
diff --git a/client/src/templates/Challenges/redux/action-types.js b/client/src/templates/Challenges/redux/action-types.js
index 6ffbb0ab6ab..81509aba9af 100644
--- a/client/src/templates/Challenges/redux/action-types.js
+++ b/client/src/templates/Challenges/redux/action-types.js
@@ -32,7 +32,7 @@ export const actionTypes = createTypes(
'openModal',
'setIsAdvancing',
'setChapterSlug',
- 'setExamResults',
+ 'setUserCompletedExam',
'previewMounted',
'projectPreviewMounted',
'storePortalWindow',
diff --git a/client/src/templates/Challenges/redux/actions.js b/client/src/templates/Challenges/redux/actions.js
index f5796c2ed9c..41dd9104e4e 100644
--- a/client/src/templates/Challenges/redux/actions.js
+++ b/client/src/templates/Challenges/redux/actions.js
@@ -51,7 +51,9 @@ export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound);
export const saveEditorContent = createAction(actionTypes.saveEditorContent);
export const setIsAdvancing = createAction(actionTypes.setIsAdvancing);
export const setChapterSlug = createAction(actionTypes.setChapterSlug);
-export const setExamResults = createAction(actionTypes.setExamResults);
+export const setUserCompletedExam = createAction(
+ actionTypes.setUserCompletedExam
+);
export const closeModal = createAction(actionTypes.closeModal);
export const openModal = createAction(actionTypes.openModal);
diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js
index 9d8c70ab8fa..97e6281c966 100644
--- a/client/src/templates/Challenges/redux/completion-epic.js
+++ b/client/src/templates/Challenges/redux/completion-epic.js
@@ -44,7 +44,7 @@ import {
challengeFilesSelector,
challengeMetaSelector,
challengeTestsSelector,
- examResultsSelector,
+ userCompletedExamSelector,
projectFormValuesSelector,
isBlockNewlyCompletedSelector
} from './selectors';
@@ -53,7 +53,12 @@ function postChallenge(update, username) {
const saveChallenge = postUpdate$(update).pipe(
retry(3),
switchMap(({ data }) => {
- const { savedChallenges, points, isTrophyMissing } = data;
+ const {
+ savedChallenges,
+ points,
+ isTrophyMissing,
+ examResults = {}
+ } = data;
const payloadWithClientProperties = {
...omit(update.payload, ['files'])
};
@@ -73,7 +78,8 @@ function postChallenge(update, username) {
points,
...payloadWithClientProperties
},
- savedChallenges: mapFilesToChallengeFiles(savedChallenges)
+ savedChallenges: mapFilesToChallengeFiles(savedChallenges),
+ examResults
}),
updateComplete(),
submitChallengeComplete()
@@ -181,10 +187,11 @@ const submitters = {
function submitExam(type, state) {
// TODO: verify shape of examResults?
if (type === actionTypes.submitChallenge) {
- const { id } = challengeMetaSelector(state);
- const examResults = examResultsSelector(state);
+ const { id, challengeType } = challengeMetaSelector(state);
+ const userCompletedExam = userCompletedExamSelector(state);
+
const { username } = userSelector(state);
- const challengeInfo = { id, examResults };
+ const challengeInfo = { id, challengeType, userCompletedExam };
const update = {
endpoint: '/exam-challenge-completed',
@@ -209,7 +216,6 @@ export default function completionEpic(action$, state$) {
block,
blockHashSlug
} = challengeMetaSelector(state);
-
let submitter = () => of({ type: 'no-user-signed-in' });
if (
!(challengeType in submitTypes) ||
@@ -244,7 +250,9 @@ export default function completionEpic(action$, state$) {
mergeMap(x => of(x, setRenderStartTime(Date.now()))),
tap(res => {
if (res.type !== submitActionTypes.updateFailed) {
- navigate(pathToNavigateTo);
+ if (challengeType !== challengeTypes.exam) {
+ navigate(pathToNavigateTo);
+ }
} else {
createFlashMessage(standardErrorMessage);
}
diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js
index 8d4f058b936..d2b0025bf14 100644
--- a/client/src/templates/Challenges/redux/index.js
+++ b/client/src/templates/Challenges/redux/index.js
@@ -29,10 +29,7 @@ const initialState = {
},
challengeTests: [],
consoleOut: [],
- examResults: {
- timeInSeconds: 0,
- results: []
- },
+ userCompletedExam: null,
hasCompletedBlock: false,
isBuildEnabled: true,
isResetting: false,
@@ -42,6 +39,7 @@ const initialState = {
help: false,
video: false,
reset: false,
+ exitExam: false,
finishExam: false,
projectPreview: false,
shortcuts: false
@@ -214,9 +212,9 @@ export const reducer = handleActions(
...state,
chapterSlug: payload
}),
- [actionTypes.setExamResults]: (state, { payload }) => ({
+ [actionTypes.setUserCompletedExam]: (state, { payload }) => ({
...state,
- examResults: payload
+ userCompletedExam: payload
}),
[actionTypes.closeModal]: (state, { payload }) => ({
...state,
diff --git a/client/src/templates/Challenges/redux/selectors.js b/client/src/templates/Challenges/redux/selectors.js
index fc389b8712b..b476e1ba0f5 100644
--- a/client/src/templates/Challenges/redux/selectors.js
+++ b/client/src/templates/Challenges/redux/selectors.js
@@ -28,6 +28,7 @@ export const isCompletionModalOpenSelector = state =>
export const isHelpModalOpenSelector = state => state[ns].modal.help;
export const isVideoModalOpenSelector = state => state[ns].modal.video;
export const isResetModalOpenSelector = state => state[ns].modal.reset;
+export const isExitExamModalOpenSelector = state => state[ns].modal.exitExam;
export const isFinishExamModalOpenSelector = state =>
state[ns].modal.finishExam;
export const isProjectPreviewModalOpenSelector = state =>
@@ -46,7 +47,7 @@ export const chapterSlugSelector = state => state[ns].chapterSlug;
export const portalDocumentSelector = state => state[ns].portalWindow?.document;
export const portalWindowSelector = state => state[ns].portalWindow;
-export const examResultsSelector = state => state[ns].examResults;
+export const userCompletedExamSelector = state => state[ns].userCompletedExam;
export const challengeDataSelector = state => {
const { challengeType } = challengeMetaSelector(state);
let challengeData = { challengeType };
diff --git a/client/src/utils/ajax.ts b/client/src/utils/ajax.ts
index 3ed2d7e673e..7e92fbc1f74 100644
--- a/client/src/utils/ajax.ts
+++ b/client/src/utils/ajax.ts
@@ -5,6 +5,7 @@ import type {
ChallengeFile,
ChallengeFiles,
CompletedChallenge,
+ GenerateExamResponseWithData,
SavedChallenge,
SavedChallengeFile,
User
@@ -213,6 +214,12 @@ export function getUsernameExists(
return get(`/api/users/exists?username=${username}`);
}
+export function getGenerateExam(
+ challengeId: string
+): Promise {
+ return get(`/exam/${challengeId}`);
+}
+
/** POST **/
interface Donation {
diff --git a/client/src/utils/format-seconds.ts b/client/src/utils/format-seconds.ts
new file mode 100644
index 00000000000..e931c43f336
--- /dev/null
+++ b/client/src/utils/format-seconds.ts
@@ -0,0 +1,15 @@
+export function formatSecondsToTime(s: number) {
+ const hourInSeconds = 60 * 60;
+ const minuteInSeconds = 60;
+ const h = Math.floor(s / hourInSeconds);
+ s -= h * hourInSeconds;
+
+ const minutes = Math.floor(s / minuteInSeconds);
+ s -= minutes * minuteInSeconds;
+
+ const mm = minutes < 10 && h >= 1 ? `0${minutes}` : minutes;
+ const seconds = s % 60;
+ const ss = seconds < 10 ? `0${seconds}` : seconds;
+
+ return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
+}
diff --git a/client/src/utils/tone/index.ts b/client/src/utils/tone/index.ts
index 6f7b8d855f8..7cf38103f97 100644
--- a/client/src/utils/tone/index.ts
+++ b/client/src/utils/tone/index.ts
@@ -28,6 +28,7 @@ const toneUrls = {
[FlashMessages.CompleteProjectFirst]: TRY_AGAIN,
[FlashMessages.DeleteTokenErr]: TRY_AGAIN,
[FlashMessages.EmailValid]: CHAL_COMP,
+ [FlashMessages.GenerateExamError]: TRY_AGAIN,
[FlashMessages.HonestFirst]: TRY_AGAIN,
[FlashMessages.IncompleteSteps]: TRY_AGAIN,
[FlashMessages.LocalCodeSaved]: CHAL_COMP,
diff --git a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md
index b3419db1cb2..ad5acd4c259 100644
--- a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md
+++ b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md
@@ -22,8 +22,12 @@ When is an `ArgumentOutOfRangeException` exception thrown?
An `ArgumentOutOfRangeException` exception is thrown when an attempt is made to index an array outside the bounds of the array.
+---
+
An `ArgumentOutOfRangeException` exception is thrown when the value of an argument is outside the allowable range of values as defined by the method.
+---
+
An `ArgumentOutOfRangeException` exception is thrown when an attempt is made to store a value of one type in an array of another type.
## --video-solution--
diff --git a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md
index 54119ff1976..dae16becaac 100644
--- a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md
+++ b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md
@@ -24,9 +24,9 @@ Pass this exam to earn your Foundational C# with Microsoft Certification. Before
- You must complete the entire exam in one session.
- Exiting the exam at any point before you are finished, will result in the loss of your progress.
-- The exam consists of multiple-choice questions.
- There is no time limit, but your total time taken will be recorded.
-- Complete the exam and answer a sufficent number of question correctly to earn your certification.
+- The exam consists of 80 multiple-choice questions.
+- Complete the exam and correctly answer at least 70% of the questions to earn your certification.
# --instructions--
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 74586163390..a02fa95e77a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -629,6 +629,9 @@ importers:
lodash-es:
specifier: 4.17.21
version: 4.17.21
+ micromark:
+ specifier: 4.0.0
+ version: 4.0.0
monaco-editor:
specifier: 0.28.1
version: 0.28.1
@@ -16763,6 +16766,7 @@ packages:
dependencies:
ms: 2.1.3
supports-color: 8.1.1
+ dev: true
/debug@4.3.1:
resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==}
@@ -17093,7 +17097,7 @@ packages:
'@types/tmp': 0.0.33
application-config-path: 0.1.1
command-exists: 1.2.9
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
eol: 0.9.1
get-port: 3.2.0
glob: 7.2.3
@@ -17108,6 +17112,12 @@ packages:
transitivePeerDependencies:
- supports-color
+ /devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+ dependencies:
+ dequal: 2.0.3
+ dev: false
+
/devtools-protocol@0.0.901419:
resolution: {integrity: sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==}
dev: true
@@ -18012,7 +18022,7 @@ packages:
/eslint-import-resolver-node@0.3.7:
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
is-core-module: 2.12.1
resolve: 1.22.2
transitivePeerDependencies:
@@ -18063,7 +18073,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
eslint: 7.32.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.28.0)(eslint@8.46.0)
@@ -18092,7 +18102,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.5)
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
eslint: 8.46.0
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.28.0)(eslint@8.46.0)
transitivePeerDependencies:
@@ -18120,7 +18130,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.5)
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
eslint: 8.46.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.28.0)(eslint@8.46.0)
@@ -18194,7 +18204,7 @@ packages:
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
doctrine: 2.1.0
eslint: 7.32.0
eslint-import-resolver-node: 0.3.7
@@ -18227,7 +18237,7 @@ packages:
array.prototype.findlastindex: 1.2.2
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
doctrine: 2.1.0
eslint: 8.46.0
eslint-import-resolver-node: 0.3.7
@@ -18322,7 +18332,7 @@ packages:
minimatch: 3.1.2
object.entries: 1.1.6
object.fromentries: 2.0.6
- semver: 6.3.1
+ semver: 6.3.0
/eslint-plugin-jsx-a11y@6.7.1(eslint@8.46.0):
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
@@ -19523,7 +19533,6 @@ packages:
optional: true
dependencies:
debug: 2.2.0
- dev: false
/follow-redirects@1.15.2(debug@3.2.7):
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
@@ -19534,7 +19543,7 @@ packages:
debug:
optional: true
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
/follow-redirects@1.15.2(debug@4.3.4):
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
@@ -20356,7 +20365,7 @@ packages:
css-minimizer-webpack-plugin: 2.0.0(webpack@5.88.2)
css.escape: 1.5.1
date-fns: 2.30.0
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
deepmerge: 4.3.0
del: 5.1.0
detect-port: 1.5.1
@@ -21577,7 +21586,7 @@ packages:
engines: {node: '>=8.0.0'}
dependencies:
eventemitter3: 4.0.7
- follow-redirects: 1.15.2(debug@4.3.4)
+ follow-redirects: 1.15.2(debug@2.2.0)
requires-port: 1.0.0
transitivePeerDependencies:
- debug
@@ -24553,7 +24562,7 @@ packages:
dependencies:
async: 0.9.2
commondir: 1.0.1
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
lodash: 4.17.21
semver: 5.7.1
strong-globalize: 4.1.3
@@ -24566,7 +24575,7 @@ packages:
resolution: {integrity: sha512-vDRR4gqkvGOEXh5yL383xGuGxUW9xtF+NCY6/lJu1VAgupKltZxEx3Vw+L3nsGvQrlkJTSmiK3jk72qxkoBtbw==}
engines: {node: '>=6'}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
lodash: 4.17.21
loopback-swagger: 5.9.0
strong-globalize: 4.1.3
@@ -24581,7 +24590,7 @@ packages:
dependencies:
async: 2.6.4
bson: 1.1.6
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
loopback-connector: 4.11.1
mongodb: 3.6.9
strong-globalize: 4.1.3
@@ -24625,7 +24634,7 @@ packages:
dependencies:
async: 2.6.4
bluebird: 3.7.2
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
depd: 1.1.2
inflection: 1.13.4
lodash: 4.17.21
@@ -24649,7 +24658,7 @@ packages:
resolution: {integrity: sha512-p0qSzuuX7eATe5Bxy+RqCj3vSfSFfdCtqyf3yuC+DpchMvgal33XlhEi2UmywyK/Ym28oVnZxxWmfrwFMzSwLQ==}
engines: {node: '>=4.0.0'}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
dev: false
@@ -24659,7 +24668,7 @@ packages:
engines: {node: '>=8.9'}
dependencies:
async: 2.6.4
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
strong-globalize: 4.1.3
transitivePeerDependencies:
- supports-color
@@ -24670,7 +24679,7 @@ packages:
engines: {node: '>=8'}
dependencies:
async: 2.6.4
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
ejs: 2.7.4
lodash: 4.17.21
strong-globalize: 4.1.3
@@ -25366,6 +25375,27 @@ packages:
micromark-util-types: 1.0.2
uvu: 0.5.6
+ /micromark-core-commonmark@2.0.0:
+ resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==}
+ dependencies:
+ decode-named-character-reference: 1.0.2
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.0
+ micromark-factory-label: 2.0.0
+ micromark-factory-space: 2.0.0
+ micromark-factory-title: 2.0.0
+ micromark-factory-whitespace: 2.0.0
+ micromark-util-character: 2.0.1
+ micromark-util-chunked: 2.0.0
+ micromark-util-classify-character: 2.0.0
+ micromark-util-html-tag-name: 2.0.0
+ micromark-util-normalize-identifier: 2.0.0
+ micromark-util-resolve-all: 2.0.0
+ micromark-util-subtokenize: 2.0.0
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-extension-directive@1.4.0:
resolution: {integrity: sha512-8uJN4N2hfhxc0I2Mdya+HZ35D0fyBnHn66aVnHawLj0Nd22Poqgqw3N0vTdYOsNwwrshfMLlPDKtLfEeq4lxgw==}
dependencies:
@@ -25498,6 +25528,14 @@ packages:
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
+ /micromark-factory-destination@2.0.0:
+ resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==}
+ dependencies:
+ micromark-util-character: 2.0.1
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-factory-label@1.0.2:
resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==}
dependencies:
@@ -25506,6 +25544,15 @@ packages:
micromark-util-types: 1.0.2
uvu: 0.5.6
+ /micromark-factory-label@2.0.0:
+ resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==}
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.0.1
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-factory-mdx-expression@1.0.7:
resolution: {integrity: sha512-QAdFbkQagTZ/eKb8zDGqmjvgevgJH3+aQpvvKrXWxNJp3o8/l2cAbbrBd0E04r0Gx6nssPpqWIjnbHFvZu5qsQ==}
dependencies:
@@ -25524,6 +25571,13 @@ packages:
micromark-util-character: 1.1.0
micromark-util-types: 1.0.2
+ /micromark-factory-space@2.0.0:
+ resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==}
+ dependencies:
+ micromark-util-character: 2.0.1
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-factory-title@1.0.2:
resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==}
dependencies:
@@ -25533,6 +25587,15 @@ packages:
micromark-util-types: 1.0.2
uvu: 0.5.6
+ /micromark-factory-title@2.0.0:
+ resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==}
+ dependencies:
+ micromark-factory-space: 2.0.0
+ micromark-util-character: 2.0.1
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-factory-whitespace@1.0.0:
resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==}
dependencies:
@@ -25541,17 +25604,39 @@ packages:
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
+ /micromark-factory-whitespace@2.0.0:
+ resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==}
+ dependencies:
+ micromark-factory-space: 2.0.0
+ micromark-util-character: 2.0.1
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-util-character@1.1.0:
resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==}
dependencies:
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
+ /micromark-util-character@2.0.1:
+ resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==}
+ dependencies:
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-util-chunked@1.0.0:
resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==}
dependencies:
micromark-util-symbol: 1.0.1
+ /micromark-util-chunked@2.0.0:
+ resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==}
+ dependencies:
+ micromark-util-symbol: 2.0.0
+ dev: false
+
/micromark-util-classify-character@1.0.0:
resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==}
dependencies:
@@ -25559,17 +25644,38 @@ packages:
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
+ /micromark-util-classify-character@2.0.0:
+ resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==}
+ dependencies:
+ micromark-util-character: 2.0.1
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-util-combine-extensions@1.0.0:
resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==}
dependencies:
micromark-util-chunked: 1.0.0
micromark-util-types: 1.0.2
+ /micromark-util-combine-extensions@2.0.0:
+ resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==}
+ dependencies:
+ micromark-util-chunked: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-util-decode-numeric-character-reference@1.0.0:
resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==}
dependencies:
micromark-util-symbol: 1.0.1
+ /micromark-util-decode-numeric-character-reference@2.0.0:
+ resolution: {integrity: sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q==}
+ dependencies:
+ micromark-util-symbol: 2.0.0
+ dev: false
+
/micromark-util-decode-string@1.0.2:
resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==}
dependencies:
@@ -25581,6 +25687,10 @@ packages:
/micromark-util-encode@1.0.1:
resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==}
+ /micromark-util-encode@2.0.0:
+ resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==}
+ dev: false
+
/micromark-util-events-to-acorn@1.2.1:
resolution: {integrity: sha512-mkg3BaWlw6ZTkQORrKVBW4o9ICXPxLtGz51vml5mQpKFdo9vqIX68CAx5JhTOdjQyAHH7JFmm4rh8toSPQZUmg==}
dependencies:
@@ -25595,16 +25705,32 @@ packages:
/micromark-util-html-tag-name@1.1.0:
resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==}
+ /micromark-util-html-tag-name@2.0.0:
+ resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==}
+ dev: false
+
/micromark-util-normalize-identifier@1.0.0:
resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==}
dependencies:
micromark-util-symbol: 1.0.1
+ /micromark-util-normalize-identifier@2.0.0:
+ resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==}
+ dependencies:
+ micromark-util-symbol: 2.0.0
+ dev: false
+
/micromark-util-resolve-all@1.0.0:
resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==}
dependencies:
micromark-util-types: 1.0.2
+ /micromark-util-resolve-all@2.0.0:
+ resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==}
+ dependencies:
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-util-sanitize-uri@1.1.0:
resolution: {integrity: sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==}
dependencies:
@@ -25612,6 +25738,14 @@ packages:
micromark-util-encode: 1.0.1
micromark-util-symbol: 1.0.1
+ /micromark-util-sanitize-uri@2.0.0:
+ resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==}
+ dependencies:
+ micromark-util-character: 2.0.1
+ micromark-util-encode: 2.0.0
+ micromark-util-symbol: 2.0.0
+ dev: false
+
/micromark-util-subtokenize@1.0.2:
resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==}
dependencies:
@@ -25620,12 +25754,29 @@ packages:
micromark-util-types: 1.0.2
uvu: 0.5.6
+ /micromark-util-subtokenize@2.0.0:
+ resolution: {integrity: sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==}
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.0
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ dev: false
+
/micromark-util-symbol@1.0.1:
resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==}
+ /micromark-util-symbol@2.0.0:
+ resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==}
+ dev: false
+
/micromark-util-types@1.0.2:
resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==}
+ /micromark-util-types@2.0.0:
+ resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==}
+ dev: false
+
/micromark@2.11.4:
resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==}
dependencies:
@@ -25658,6 +25809,30 @@ packages:
transitivePeerDependencies:
- supports-color
+ /micromark@4.0.0:
+ resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
+ dependencies:
+ '@types/debug': 4.1.7
+ debug: 4.3.4(supports-color@8.1.1)
+ decode-named-character-reference: 1.0.2
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.0
+ micromark-factory-space: 2.0.0
+ micromark-util-character: 2.0.1
+ micromark-util-chunked: 2.0.0
+ micromark-util-combine-extensions: 2.0.0
+ micromark-util-decode-numeric-character-reference: 2.0.0
+ micromark-util-encode: 2.0.0
+ micromark-util-normalize-identifier: 2.0.0
+ micromark-util-resolve-all: 2.0.0
+ micromark-util-sanitize-uri: 2.0.0
+ micromark-util-subtokenize: 2.0.0
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/micromatch@3.1.10:
resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
engines: {node: '>=0.10.0'}
@@ -34272,7 +34447,7 @@ packages:
/webpack-virtual-modules@0.2.2:
resolution: {integrity: sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA==}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
dev: true
@@ -34280,7 +34455,7 @@ packages:
/webpack-virtual-modules@0.3.2:
resolution: {integrity: sha512-RXQXioY6MhzM4CNQwmBwKXYgBs6ulaiQ8bkNQEl2J6Z+V+s7lgl/wGvaI/I0dLnYKB8cKsxQc17QOAVIphPLDw==}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color