From 4005c85d6071271f2afb73145783eba7daeb2bee Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Wed, 22 Jan 2025 08:59:53 -0700 Subject: [PATCH] added escape support on matching text --- pnpm-lock.yaml | 28 ++++++++++++++++ .../quiz/[quizName]/QuizPreview.tsx | 13 ++++++-- .../utils/quizQuestionAnswerMarkdownUtils.ts | 24 +++++++------- .../quiz/utils/quizQuestionMarkdownUtils.ts | 14 +++++--- .../quizMarkdown/matchingAnswers.test.ts | 32 +++++++++++++++++++ src/services/canvas/canvasQuizService.ts | 30 +++++++++++------ src/services/utils/questionHtmlUtils.ts | 4 +++ 7 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 src/services/utils/questionHtmlUtils.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b5fe48..aff4dc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: jsdom: specifier: ^25.0.0 version: 25.0.1 + marked-katex-extension: + specifier: ^5.1.4 + version: 5.1.4(katex@0.16.20)(marked@14.1.4) next: specifier: ^15.0.2 version: 15.1.0(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1105,6 +1108,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1833,6 +1840,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + katex@0.16.20: + resolution: {integrity: sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1881,6 +1892,12 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + marked-katex-extension@5.1.4: + resolution: {integrity: sha512-GQOio4vCp0laxB1IY+2oNVo5nbn82yWMDP/jILRYHmyu2WXMVlXCB+krq2/U2fQn+V9j8aqDmnNdrsgqG2AkGQ==} + peerDependencies: + katex: '>=0.16 <0.17' + marked: '>=4 <16' + marked@14.1.4: resolution: {integrity: sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==} engines: {node: '>= 18'} @@ -3647,6 +3664,8 @@ snapshots: commander@4.1.1: {} + commander@8.3.0: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -4570,6 +4589,10 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + katex@0.16.20: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4613,6 +4636,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + marked-katex-extension@5.1.4(katex@0.16.20)(marked@14.1.4): + dependencies: + katex: 0.16.20 + marked: 14.1.4 + marked@14.1.4: {} math-intrinsics@1.0.0: {} diff --git a/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx b/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx index 901aa84..ccae682 100644 --- a/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx +++ b/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx @@ -6,6 +6,7 @@ import { QuestionType, } from "@/models/local/quiz/localQuizQuestion"; import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils"; +import { escapeMatchingText } from "@/services/utils/questionHtmlUtils"; export default function QuizPreview({ moduleName, @@ -78,6 +79,8 @@ export default function QuizPreview({ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) { const [settings] = useLocalCourseSettingsQuery(); + + question.answers.map(a => console.log(escapeMatchingText(a.text))) return (
@@ -89,7 +92,9 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
{question.questionType === QuestionType.MATCHING && (
@@ -98,8 +103,10 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) { key={JSON.stringify(answer)} className="mx-3 mb-1 bg-dark rounded border border-slate-600 flex flex-row" > -
{answer.text} -
-
{answer.matchedText}
+
+ {escapeMatchingText(answer.text)} +
+
{answer.matchedText}
))} {question.matchDistractors.map((distractor) => ( diff --git a/src/models/local/quiz/utils/quizQuestionAnswerMarkdownUtils.ts b/src/models/local/quiz/utils/quizQuestionAnswerMarkdownUtils.ts index cf70862..461c563 100644 --- a/src/models/local/quiz/utils/quizQuestionAnswerMarkdownUtils.ts +++ b/src/models/local/quiz/utils/quizQuestionAnswerMarkdownUtils.ts @@ -1,6 +1,18 @@ import { QuestionType } from "../localQuizQuestion"; import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer"; +const parseMatchingAnswer = (input: string) => { + const matchingPattern = /^\^?/; + const textWithoutMatchDelimiter = input.replace(matchingPattern, ""); + const [text, ...matchedParts] = textWithoutMatchDelimiter.split(" - "); + const answer: LocalQuizQuestionAnswer = { + correct: true, + text: text.trim(), + matchedText: matchedParts.join("-").trim(), + }; + return answer; +}; + export const quizQuestionAnswerMarkdownUtils = { // getHtmlText(): string { // return MarkdownService.render(this.text); @@ -10,17 +22,7 @@ export const quizQuestionAnswerMarkdownUtils = { const isCorrect = input.startsWith("*") || input[1] === "*"; if (questionType === QuestionType.MATCHING) { - const matchingPattern = /^\^ ?/; - const textWithoutMatchDelimiter = input - .replace(matchingPattern, "") - .trim(); - const [text, ...matchedParts] = textWithoutMatchDelimiter.split("-"); - const answer: LocalQuizQuestionAnswer = { - correct: true, - text: text.trim(), - matchedText: matchedParts.join("-").trim(), - }; - return answer; + return parseMatchingAnswer(input); } const startingQuestionPattern = /^(\*?[a-z]?\))|\[\s*\]|\[\*\]|\^ /; diff --git a/src/models/local/quiz/utils/quizQuestionMarkdownUtils.ts b/src/models/local/quiz/utils/quizQuestionMarkdownUtils.ts index 1efebe7..0f4d10b 100644 --- a/src/models/local/quiz/utils/quizQuestionMarkdownUtils.ts +++ b/src/models/local/quiz/utils/quizQuestionMarkdownUtils.ts @@ -69,11 +69,11 @@ const getQuestionType = ( "short_answer" ) return QuestionType.SHORT_ANSWER; - if ( + if ( linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() === - "short_answer=" - ) - return QuestionType.SHORT_ANSWER_WITH_ANSWERS; + "short_answer=" + ) + return QuestionType.SHORT_ANSWER_WITH_ANSWERS; const answerLines = getAnswerStringsWithMultilineSupport( linesWithoutPoints, @@ -102,7 +102,11 @@ const getAnswers = ( questionIndex: number, questionType: string ): LocalQuizQuestionAnswer[] => { - if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS) linesWithoutPoints = linesWithoutPoints.slice(0, linesWithoutPoints.length - 1); + if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS) + linesWithoutPoints = linesWithoutPoints.slice( + 0, + linesWithoutPoints.length - 1 + ); const answerLines = getAnswerStringsWithMultilineSupport( linesWithoutPoints, questionIndex diff --git a/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts b/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts index a75bb3e..743347e 100644 --- a/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts +++ b/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts @@ -131,4 +131,36 @@ Match the following terms & definitions "^ statement - a single command to be executed\n^ - this is the distractor" ); }); + it("can escape - characters", () => { + const name = "Test Quiz" + const rawMarkdownQuiz = ` +ShuffleAnswers: true +OneQuestionAtATime: false +DueAt: 08/21/2023 23:59:00 +LockAt: 08/21/2023 23:59:00 +AssignmentGroup: Assignments +AllowedAttempts: -1 +Description: +--- +Match the following terms & definitions + +^ git add \-\-all - start tracking all files in the current directory and subdirectories +`; + + const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name); + + + const firstQuestion = quiz.questions[0]; + + expect(firstQuestion.answers[0].text).toBe("git add --all"); + expect(firstQuestion.answers[0].matchedText).toBe("start tracking all files in the current directory and subdirectories"); + + + + const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); + + expect(quizMarkdown).toContain( + "^ git add \-\-all - start tracking all files in the current directory and subdirectories" + ); + }); }); diff --git a/src/services/canvas/canvasQuizService.ts b/src/services/canvas/canvasQuizService.ts index 3b92f05..23092a7 100644 --- a/src/services/canvas/canvasQuizService.ts +++ b/src/services/canvas/canvasQuizService.ts @@ -11,16 +11,24 @@ import { } from "@/models/local/quiz/localQuizQuestion"; import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel"; import { LocalCourseSettings } from "@/models/local/localCourseSettings"; +import { escapeMatchingText } from "../utils/questionHtmlUtils"; + export const getAnswers = ( question: LocalQuizQuestion, settings: LocalCourseSettings ) => { if (question.questionType === QuestionType.MATCHING) - return question.answers.map((a) => ({ - answer_match_left: a.text, - answer_match_right: a.matchedText, - })); + return question.answers.map((a) => { + const text = + question.questionType === QuestionType.MATCHING + ? escapeMatchingText(a.text) + : a.text; + return { + answer_match_left: text, + answer_match_right: a.matchedText, + }; + }); return question.answers.map((answer) => ({ answer_html: markdownToHTMLSafe(answer.text, settings), @@ -29,11 +37,9 @@ export const getAnswers = ( })); }; -export const getQuestionType = ( - question: LocalQuizQuestion -) => { +export const getQuestionType = (question: LocalQuizQuestion) => { return `${question.questionType.replace("=", "")}_question`; -} +}; const createQuestionOnly = async ( canvasCourseId: number, @@ -45,6 +51,7 @@ const createQuestionOnly = async ( console.log("Creating individual question"); //, question); const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`; + const body = { question: { question_text: markdownToHTMLSafe(question.text, settings), @@ -179,7 +186,12 @@ export const canvasQuizService = { }; const { data: canvasQuiz } = await axiosClient.post(url, body); - await createQuizQuestions(canvasCourseId, canvasQuiz.id, localQuiz, settings); + await createQuizQuestions( + canvasCourseId, + canvasQuiz.id, + localQuiz, + settings + ); return canvasQuiz.id; }, async delete(canvasCourseId: number, canvasQuizId: number) { diff --git a/src/services/utils/questionHtmlUtils.ts b/src/services/utils/questionHtmlUtils.ts new file mode 100644 index 0000000..e3e35b9 --- /dev/null +++ b/src/services/utils/questionHtmlUtils.ts @@ -0,0 +1,4 @@ + +export function escapeMatchingText(input: string){ + return input.replaceAll("\\-", "-"); +} \ No newline at end of file