added escape support on matching text

This commit is contained in:
2025-01-22 08:59:53 -07:00
parent d581569c7a
commit 4005c85d60
7 changed files with 117 additions and 28 deletions

28
pnpm-lock.yaml generated
View File

@@ -38,6 +38,9 @@ importers:
jsdom: jsdom:
specifier: ^25.0.0 specifier: ^25.0.0
version: 25.0.1 version: 25.0.1
marked-katex-extension:
specifier: ^5.1.4
version: 5.1.4(katex@0.16.20)(marked@14.1.4)
next: next:
specifier: ^15.0.2 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) 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==} resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1833,6 +1840,10 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
katex@0.16.20:
resolution: {integrity: sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==}
hasBin: true
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1881,6 +1892,12 @@ packages:
magic-string@0.30.17: magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 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: marked@14.1.4:
resolution: {integrity: sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==} resolution: {integrity: sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
@@ -3647,6 +3664,8 @@ snapshots:
commander@4.1.1: {} commander@4.1.1: {}
commander@8.3.0: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
@@ -4570,6 +4589,10 @@ snapshots:
object.assign: 4.1.5 object.assign: 4.1.5
object.values: 1.2.0 object.values: 1.2.0
katex@0.16.20:
dependencies:
commander: 8.3.0
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@@ -4613,6 +4636,11 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@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: {} marked@14.1.4: {}
math-intrinsics@1.0.0: {} math-intrinsics@1.0.0: {}

View File

@@ -6,6 +6,7 @@ import {
QuestionType, QuestionType,
} from "@/models/local/quiz/localQuizQuestion"; } from "@/models/local/quiz/localQuizQuestion";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils"; import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
export default function QuizPreview({ export default function QuizPreview({
moduleName, moduleName,
@@ -78,6 +79,8 @@ export default function QuizPreview({
function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) { function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
const [settings] = useLocalCourseSettingsQuery(); const [settings] = useLocalCourseSettingsQuery();
question.answers.map(a => console.log(escapeMatchingText(a.text)))
return ( return (
<div className="rounded bg-slate-900 px-2"> <div className="rounded bg-slate-900 px-2">
<div className="flex flex-row justify-between text-slate-400"> <div className="flex flex-row justify-between text-slate-400">
@@ -89,7 +92,9 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
<div <div
className="ms-4 mb-2 markdownPreview" className="ms-4 mb-2 markdownPreview"
dangerouslySetInnerHTML={{ __html: markdownToHTMLSafe(question.text, settings) }} dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(question.text, settings),
}}
></div> ></div>
{question.questionType === QuestionType.MATCHING && ( {question.questionType === QuestionType.MATCHING && (
<div> <div>
@@ -98,8 +103,10 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
key={JSON.stringify(answer)} key={JSON.stringify(answer)}
className="mx-3 mb-1 bg-dark rounded border border-slate-600 flex flex-row" className="mx-3 mb-1 bg-dark rounded border border-slate-600 flex flex-row"
> >
<div className="text-right my-auto">{answer.text} - </div> <div className="text-right my-auto flex-1 pe-3">
<div className="">{answer.matchedText}</div> {escapeMatchingText(answer.text)}
</div>
<div className=" flex-1">{answer.matchedText}</div>
</div> </div>
))} ))}
{question.matchDistractors.map((distractor) => ( {question.matchDistractors.map((distractor) => (

View File

@@ -1,6 +1,18 @@
import { QuestionType } from "../localQuizQuestion"; import { QuestionType } from "../localQuizQuestion";
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer"; 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 = { export const quizQuestionAnswerMarkdownUtils = {
// getHtmlText(): string { // getHtmlText(): string {
// return MarkdownService.render(this.text); // return MarkdownService.render(this.text);
@@ -10,17 +22,7 @@ export const quizQuestionAnswerMarkdownUtils = {
const isCorrect = input.startsWith("*") || input[1] === "*"; const isCorrect = input.startsWith("*") || input[1] === "*";
if (questionType === QuestionType.MATCHING) { if (questionType === QuestionType.MATCHING) {
const matchingPattern = /^\^ ?/; return parseMatchingAnswer(input);
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;
} }
const startingQuestionPattern = /^(\*?[a-z]?\))|\[\s*\]|\[\*\]|\^ /; const startingQuestionPattern = /^(\*?[a-z]?\))|\[\s*\]|\[\*\]|\^ /;

View File

@@ -71,9 +71,9 @@ const getQuestionType = (
return QuestionType.SHORT_ANSWER; return QuestionType.SHORT_ANSWER;
if ( if (
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() === linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
"short_answer=" "short_answer="
) )
return QuestionType.SHORT_ANSWER_WITH_ANSWERS; return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
const answerLines = getAnswerStringsWithMultilineSupport( const answerLines = getAnswerStringsWithMultilineSupport(
linesWithoutPoints, linesWithoutPoints,
@@ -102,7 +102,11 @@ const getAnswers = (
questionIndex: number, questionIndex: number,
questionType: string questionType: string
): LocalQuizQuestionAnswer[] => { ): 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( const answerLines = getAnswerStringsWithMultilineSupport(
linesWithoutPoints, linesWithoutPoints,
questionIndex questionIndex

View File

@@ -131,4 +131,36 @@ Match the following terms & definitions
"^ statement - a single command to be executed\n^ - this is the distractor" "^ 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"
);
});
}); });

View File

@@ -11,16 +11,24 @@ import {
} from "@/models/local/quiz/localQuizQuestion"; } from "@/models/local/quiz/localQuizQuestion";
import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel"; import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel";
import { LocalCourseSettings } from "@/models/local/localCourseSettings"; import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { escapeMatchingText } from "../utils/questionHtmlUtils";
export const getAnswers = ( export const getAnswers = (
question: LocalQuizQuestion, question: LocalQuizQuestion,
settings: LocalCourseSettings settings: LocalCourseSettings
) => { ) => {
if (question.questionType === QuestionType.MATCHING) if (question.questionType === QuestionType.MATCHING)
return question.answers.map((a) => ({ return question.answers.map((a) => {
answer_match_left: a.text, const text =
answer_match_right: a.matchedText, question.questionType === QuestionType.MATCHING
})); ? escapeMatchingText(a.text)
: a.text;
return {
answer_match_left: text,
answer_match_right: a.matchedText,
};
});
return question.answers.map((answer) => ({ return question.answers.map((answer) => ({
answer_html: markdownToHTMLSafe(answer.text, settings), answer_html: markdownToHTMLSafe(answer.text, settings),
@@ -29,11 +37,9 @@ export const getAnswers = (
})); }));
}; };
export const getQuestionType = ( export const getQuestionType = (question: LocalQuizQuestion) => {
question: LocalQuizQuestion
) => {
return `${question.questionType.replace("=", "")}_question`; return `${question.questionType.replace("=", "")}_question`;
} };
const createQuestionOnly = async ( const createQuestionOnly = async (
canvasCourseId: number, canvasCourseId: number,
@@ -45,6 +51,7 @@ const createQuestionOnly = async (
console.log("Creating individual question"); //, question); console.log("Creating individual question"); //, question);
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`; const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`;
const body = { const body = {
question: { question: {
question_text: markdownToHTMLSafe(question.text, settings), question_text: markdownToHTMLSafe(question.text, settings),
@@ -179,7 +186,12 @@ export const canvasQuizService = {
}; };
const { data: canvasQuiz } = await axiosClient.post<CanvasQuiz>(url, body); const { data: canvasQuiz } = await axiosClient.post<CanvasQuiz>(url, body);
await createQuizQuestions(canvasCourseId, canvasQuiz.id, localQuiz, settings); await createQuizQuestions(
canvasCourseId,
canvasQuiz.id,
localQuiz,
settings
);
return canvasQuiz.id; return canvasQuiz.id;
}, },
async delete(canvasCourseId: number, canvasQuizId: number) { async delete(canvasCourseId: number, canvasQuizId: number) {

View File

@@ -0,0 +1,4 @@
export function escapeMatchingText(input: string){
return input.replaceAll("\\-", "-");
}