mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
can get exact answers
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { quizMarkdownUtils } from "../../quizzes/models/utils/quizMarkdownUtils";
|
||||||
|
import { QuestionType } from "../../quizzes/models/localQuizQuestion";
|
||||||
|
|
||||||
|
describe("numerical answer questions", () => {
|
||||||
|
it("can parse question with numerical answers", () => {
|
||||||
|
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: quiz description
|
||||||
|
---
|
||||||
|
What is 2+3?
|
||||||
|
= 5
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.text).toBe("What is 2+3?");
|
||||||
|
expect(question.questionType).toBe(QuestionType.NUMERICAL);
|
||||||
|
expect(question.answers[0].numericAnswer).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -208,12 +208,6 @@ short_answer=
|
|||||||
expect(firstQuestion.answers[1].text).toBe("other");
|
expect(firstQuestion.answers[1].text).toBe("other");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Has short_answer= type at the same position in types and zod types", () => {
|
|
||||||
expect(Object.values(zodQuestionType.Enum)).toEqual(
|
|
||||||
Object.values(QuestionType)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Associates short_answer= questions with short_answer_question canvas question type", () => {
|
it("Associates short_answer= questions with short_answer_question canvas question type", () => {
|
||||||
const name = "Test Quiz";
|
const name = "Test Quiz";
|
||||||
const rawMarkdownQuiz = `
|
const rawMarkdownQuiz = `
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export enum QuestionType {
|
|||||||
MATCHING = "matching",
|
MATCHING = "matching",
|
||||||
NONE = "",
|
NONE = "",
|
||||||
SHORT_ANSWER_WITH_ANSWERS = "short_answer=",
|
SHORT_ANSWER_WITH_ANSWERS = "short_answer=",
|
||||||
|
NUMERICAL = "numerical",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zodQuestionType = z.enum([
|
export const zodQuestionType = z.enum([
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export interface LocalQuizQuestionAnswer {
|
|
||||||
correct: boolean;
|
|
||||||
text: string;
|
|
||||||
matchedText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const zodLocalQuizQuestionAnswer = z.object({
|
export const zodLocalQuizQuestionAnswer = z.object({
|
||||||
correct: z.boolean(),
|
correct: z.boolean(),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
matchedText: z.string().optional(),
|
matchedText: z.string().optional(),
|
||||||
|
numericalAnswerType: z
|
||||||
|
.enum(["exact_answer", "range_answer", "precision_answer"])
|
||||||
|
.optional(),
|
||||||
|
numericAnswer: z.number().optional(),
|
||||||
|
numericAnswerRangeMin: z.number().optional(),
|
||||||
|
numericAnswerRangeMax: z.number().optional(),
|
||||||
|
numericAnswerMargin: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type LocalQuizQuestionAnswer = z.infer<
|
||||||
|
typeof zodLocalQuizQuestionAnswer
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,5 +1,29 @@
|
|||||||
import { QuestionType } from "../localQuizQuestion";
|
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
||||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
||||||
|
const _validFirstAnswerDelimiters = [
|
||||||
|
"*a)",
|
||||||
|
"a)",
|
||||||
|
"*)",
|
||||||
|
")",
|
||||||
|
"[ ]",
|
||||||
|
"[]",
|
||||||
|
"[*]",
|
||||||
|
"^",
|
||||||
|
"=",
|
||||||
|
];
|
||||||
|
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
||||||
|
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
||||||
|
|
||||||
|
const parseNumericalAnswer = (input: string): LocalQuizQuestionAnswer => {
|
||||||
|
const numericValue = parseFloat(input.replace(/^=\s*/, "").trim());
|
||||||
|
const answer: LocalQuizQuestionAnswer = {
|
||||||
|
correct: true,
|
||||||
|
text: input.trim(),
|
||||||
|
numericalAnswerType: "exact_answer",
|
||||||
|
numericAnswer: numericValue,
|
||||||
|
};
|
||||||
|
return answer;
|
||||||
|
};
|
||||||
|
|
||||||
const parseMatchingAnswer = (input: string) => {
|
const parseMatchingAnswer = (input: string) => {
|
||||||
const matchingPattern = /^\^?/;
|
const matchingPattern = /^\^?/;
|
||||||
@@ -13,11 +37,51 @@ const parseMatchingAnswer = (input: string) => {
|
|||||||
return answer;
|
return answer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAnswerStringsWithMultilineSupport = (
|
||||||
|
linesWithoutPoints: string[],
|
||||||
|
questionIndex: number
|
||||||
|
) => {
|
||||||
|
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
||||||
|
_validFirstAnswerDelimiters.some((prefix) =>
|
||||||
|
l.trimStart().startsWith(prefix)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (indexOfAnswerStart === -1) {
|
||||||
|
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
||||||
|
throw Error(
|
||||||
|
`question ${
|
||||||
|
questionIndex + 1
|
||||||
|
}: no answers when detecting question type on ${debugLine}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
||||||
|
|
||||||
|
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
||||||
|
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
||||||
|
const isNewAnswer = answerStartPattern.test(line);
|
||||||
|
if (isNewAnswer) {
|
||||||
|
acc.push(line);
|
||||||
|
} else if (acc.length !== 0) {
|
||||||
|
acc[acc.length - 1] += "\n" + line;
|
||||||
|
} else {
|
||||||
|
acc.push(line);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
return answerLines;
|
||||||
|
};
|
||||||
|
|
||||||
export const quizQuestionAnswerMarkdownUtils = {
|
export const quizQuestionAnswerMarkdownUtils = {
|
||||||
|
parseMarkdown(
|
||||||
|
input: string,
|
||||||
|
questionType: QuestionType
|
||||||
|
): LocalQuizQuestionAnswer {
|
||||||
|
if (questionType === QuestionType.NUMERICAL) {
|
||||||
|
return parseNumericalAnswer(input);
|
||||||
|
}
|
||||||
|
|
||||||
parseMarkdown(input: string, questionType: string): LocalQuizQuestionAnswer {
|
|
||||||
const isCorrect = input.startsWith("*") || input[1] === "*";
|
const isCorrect = input.startsWith("*") || input[1] === "*";
|
||||||
|
|
||||||
if (questionType === QuestionType.MATCHING) {
|
if (questionType === QuestionType.MATCHING) {
|
||||||
return parseMatchingAnswer(input);
|
return parseMatchingAnswer(input);
|
||||||
}
|
}
|
||||||
@@ -35,4 +99,112 @@ export const quizQuestionAnswerMarkdownUtils = {
|
|||||||
};
|
};
|
||||||
return answer;
|
return answer;
|
||||||
},
|
},
|
||||||
|
isAnswerLine: (trimmedLine: string): boolean => {
|
||||||
|
return _validFirstAnswerDelimiters.some((prefix) =>
|
||||||
|
trimmedLine.startsWith(prefix)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getQuestionType: (
|
||||||
|
linesWithoutPoints: string[],
|
||||||
|
questionIndex: number // needed for debug logging
|
||||||
|
): QuestionType => {
|
||||||
|
const lastLine = linesWithoutPoints[linesWithoutPoints.length - 1]
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
||||||
|
if (lastLine === "essay") return QuestionType.ESSAY;
|
||||||
|
if (lastLine === "short answer") return QuestionType.SHORT_ANSWER;
|
||||||
|
if (lastLine === "short_answer") return QuestionType.SHORT_ANSWER;
|
||||||
|
if (lastLine === "short_answer=")
|
||||||
|
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
||||||
|
if (lastLine.startsWith("=")) return QuestionType.NUMERICAL;
|
||||||
|
|
||||||
|
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||||
|
linesWithoutPoints,
|
||||||
|
questionIndex
|
||||||
|
);
|
||||||
|
const firstAnswerLine = answerLines[0];
|
||||||
|
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
||||||
|
firstAnswerLine.startsWith(prefix)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
||||||
|
|
||||||
|
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
||||||
|
firstAnswerLine.startsWith(prefix)
|
||||||
|
);
|
||||||
|
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
||||||
|
|
||||||
|
const isMatching = firstAnswerLine.startsWith("^");
|
||||||
|
if (isMatching) return QuestionType.MATCHING;
|
||||||
|
|
||||||
|
return QuestionType.NONE;
|
||||||
|
},
|
||||||
|
getAnswers: (
|
||||||
|
linesWithoutPoints: string[],
|
||||||
|
questionIndex: number,
|
||||||
|
questionType: QuestionType
|
||||||
|
): { answers: LocalQuizQuestionAnswer[]; distractors: string[] } => {
|
||||||
|
const typesWithAnswers: QuestionType[] = [
|
||||||
|
QuestionType.MULTIPLE_CHOICE,
|
||||||
|
QuestionType.MULTIPLE_ANSWERS,
|
||||||
|
QuestionType.MATCHING,
|
||||||
|
QuestionType.SHORT_ANSWER_WITH_ANSWERS,
|
||||||
|
QuestionType.NUMERICAL,
|
||||||
|
];
|
||||||
|
if (!typesWithAnswers.includes(questionType)) {
|
||||||
|
return { answers: [], distractors: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||||
|
linesWithoutPoints = linesWithoutPoints.slice(
|
||||||
|
0,
|
||||||
|
linesWithoutPoints.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||||
|
linesWithoutPoints,
|
||||||
|
questionIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const allAnswers = answerLines.map((a) =>
|
||||||
|
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
||||||
|
);
|
||||||
|
|
||||||
|
// For matching questions, separate answers from distractors
|
||||||
|
if (questionType === QuestionType.MATCHING) {
|
||||||
|
const answers = allAnswers.filter((a) => a.text);
|
||||||
|
const distractors = allAnswers
|
||||||
|
.filter((a) => !a.text)
|
||||||
|
.map((a) => a.matchedText ?? "");
|
||||||
|
return { answers, distractors };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { answers: allAnswers, distractors: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
getAnswerMarkdown: (
|
||||||
|
question: LocalQuizQuestion,
|
||||||
|
answer: LocalQuizQuestionAnswer,
|
||||||
|
index: number
|
||||||
|
): string => {
|
||||||
|
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
||||||
|
? "\n" + answer.text
|
||||||
|
: answer.text;
|
||||||
|
|
||||||
|
if (question.questionType === "multiple_answers") {
|
||||||
|
const correctIndicator = answer.correct ? "*" : " ";
|
||||||
|
const questionTypeIndicator = `[${correctIndicator}] `;
|
||||||
|
|
||||||
|
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||||
|
} else if (question.questionType === "matching") {
|
||||||
|
return `^ ${answer.text} - ${answer.matchedText}`;
|
||||||
|
} else {
|
||||||
|
const questionLetter = String.fromCharCode(97 + index);
|
||||||
|
const correctIndicator = answer.correct ? "*" : "";
|
||||||
|
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
||||||
|
|
||||||
|
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import { QuestionType, LocalQuizQuestion } from "../localQuizQuestion";
|
|
||||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
|
||||||
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
|
||||||
|
|
||||||
const _validFirstAnswerDelimiters = [
|
|
||||||
"*a)",
|
|
||||||
"a)",
|
|
||||||
"*)",
|
|
||||||
")",
|
|
||||||
"[ ]",
|
|
||||||
"[]",
|
|
||||||
"[*]",
|
|
||||||
"^",
|
|
||||||
];
|
|
||||||
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
|
||||||
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
|
||||||
|
|
||||||
export const isAnswerLine = (trimmedLine: string): boolean => {
|
|
||||||
return _validFirstAnswerDelimiters.some((prefix) =>
|
|
||||||
trimmedLine.startsWith(prefix)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAnswerStringsWithMultilineSupport = (
|
|
||||||
linesWithoutPoints: string[],
|
|
||||||
questionIndex: number
|
|
||||||
) => {
|
|
||||||
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
|
||||||
_validFirstAnswerDelimiters.some((prefix) =>
|
|
||||||
l.trimStart().startsWith(prefix)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (indexOfAnswerStart === -1) {
|
|
||||||
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
|
||||||
throw Error(
|
|
||||||
`question ${
|
|
||||||
questionIndex + 1
|
|
||||||
}: no answers when detecting question type on ${debugLine}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
|
||||||
|
|
||||||
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
|
||||||
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
|
||||||
const isNewAnswer = answerStartPattern.test(line);
|
|
||||||
if (isNewAnswer) {
|
|
||||||
acc.push(line);
|
|
||||||
} else if (acc.length !== 0) {
|
|
||||||
acc[acc.length - 1] += "\n" + line;
|
|
||||||
} else {
|
|
||||||
acc.push(line);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
return answerLines;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getQuestionType = (
|
|
||||||
linesWithoutPoints: string[],
|
|
||||||
questionIndex: number
|
|
||||||
): QuestionType => {
|
|
||||||
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() === "essay"
|
|
||||||
)
|
|
||||||
return QuestionType.ESSAY;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
|
||||||
"short answer"
|
|
||||||
)
|
|
||||||
return QuestionType.SHORT_ANSWER;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
|
||||||
"short_answer"
|
|
||||||
)
|
|
||||||
return QuestionType.SHORT_ANSWER;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
|
|
||||||
"short_answer="
|
|
||||||
)
|
|
||||||
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
|
||||||
|
|
||||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
|
||||||
linesWithoutPoints,
|
|
||||||
questionIndex
|
|
||||||
);
|
|
||||||
const firstAnswerLine = answerLines[0];
|
|
||||||
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
|
||||||
firstAnswerLine.startsWith(prefix)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
|
||||||
|
|
||||||
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
|
||||||
firstAnswerLine.startsWith(prefix)
|
|
||||||
);
|
|
||||||
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
|
||||||
|
|
||||||
const isMatching = firstAnswerLine.startsWith("^");
|
|
||||||
if (isMatching) return QuestionType.MATCHING;
|
|
||||||
|
|
||||||
return QuestionType.NONE;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAnswers = (
|
|
||||||
linesWithoutPoints: string[],
|
|
||||||
questionIndex: number,
|
|
||||||
questionType: string
|
|
||||||
): LocalQuizQuestionAnswer[] => {
|
|
||||||
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
|
||||||
linesWithoutPoints = linesWithoutPoints.slice(
|
|
||||||
0,
|
|
||||||
linesWithoutPoints.length - 1
|
|
||||||
);
|
|
||||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
|
||||||
linesWithoutPoints,
|
|
||||||
questionIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
const answers = answerLines.map((a) =>
|
|
||||||
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
|
||||||
);
|
|
||||||
return answers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAnswerMarkdown = (
|
|
||||||
question: LocalQuizQuestion,
|
|
||||||
answer: LocalQuizQuestionAnswer,
|
|
||||||
index: number
|
|
||||||
): string => {
|
|
||||||
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
|
||||||
? "\n" + answer.text
|
|
||||||
: answer.text;
|
|
||||||
|
|
||||||
if (question.questionType === "multiple_answers") {
|
|
||||||
const correctIndicator = answer.correct ? "*" : " ";
|
|
||||||
const questionTypeIndicator = `[${correctIndicator}] `;
|
|
||||||
|
|
||||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
|
||||||
} else if (question.questionType === "matching") {
|
|
||||||
return `^ ${answer.text} - ${answer.matchedText}`;
|
|
||||||
} else {
|
|
||||||
const questionLetter = String.fromCharCode(97 + index);
|
|
||||||
const correctIndicator = answer.correct ? "*" : "";
|
|
||||||
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
|
||||||
|
|
||||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,16 +1,66 @@
|
|||||||
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
||||||
import { quizFeedbackMarkdownUtils } from "./quizFeedbackMarkdownUtils";
|
import { quizFeedbackMarkdownUtils } from "./quizFeedbackMarkdownUtils";
|
||||||
import {
|
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
||||||
getAnswerMarkdown,
|
|
||||||
getAnswers,
|
const splitLinesAndPoints = (input: string[]) => {
|
||||||
getQuestionType,
|
const firstLineIsPoints = input[0].toLowerCase().includes("points: ");
|
||||||
isAnswerLine,
|
|
||||||
} from "./quizQuestionAnswerParsingUtils";
|
const textHasPointsLine =
|
||||||
|
input.length > 0 &&
|
||||||
|
input[0].includes(": ") &&
|
||||||
|
input[0].split(": ").length > 1 &&
|
||||||
|
!isNaN(parseFloat(input[0].split(": ")[1]));
|
||||||
|
|
||||||
|
const points =
|
||||||
|
firstLineIsPoints && textHasPointsLine
|
||||||
|
? parseFloat(input[0].split(": ")[1])
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const linesWithoutPoints = firstLineIsPoints ? input.slice(1) : input;
|
||||||
|
|
||||||
|
return { points, lines: linesWithoutPoints };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLinesBeforeAnswerLines = (lines: string[]): string[] => {
|
||||||
|
const { linesWithoutAnswers } = lines.reduce(
|
||||||
|
({ linesWithoutAnswers, taking }, currentLine) => {
|
||||||
|
if (!taking)
|
||||||
|
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||||
|
|
||||||
|
const lineIsAnswer =
|
||||||
|
quizQuestionAnswerMarkdownUtils.isAnswerLine(currentLine);
|
||||||
|
if (lineIsAnswer)
|
||||||
|
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||||
|
|
||||||
|
return {
|
||||||
|
linesWithoutAnswers: [...linesWithoutAnswers, currentLine],
|
||||||
|
taking: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ linesWithoutAnswers: [] as string[], taking: true }
|
||||||
|
);
|
||||||
|
return linesWithoutAnswers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeQuestionTypeFromDescriptionLines = (
|
||||||
|
linesWithoutAnswers: string[],
|
||||||
|
questionType: QuestionType
|
||||||
|
): string[] => {
|
||||||
|
const questionTypesWithoutAnswers = ["essay", "short answer", "short_answer"];
|
||||||
|
|
||||||
|
const descriptionLines = questionTypesWithoutAnswers.includes(questionType)
|
||||||
|
? linesWithoutAnswers.filter(
|
||||||
|
(line) => !questionTypesWithoutAnswers.includes(line.toLowerCase())
|
||||||
|
)
|
||||||
|
: linesWithoutAnswers;
|
||||||
|
|
||||||
|
return descriptionLines;
|
||||||
|
};
|
||||||
|
|
||||||
export const quizQuestionMarkdownUtils = {
|
export const quizQuestionMarkdownUtils = {
|
||||||
toMarkdown(question: LocalQuizQuestion): string {
|
toMarkdown(question: LocalQuizQuestion): string {
|
||||||
const answerArray = question.answers.map((a, i) =>
|
const answerArray = question.answers.map((a, i) =>
|
||||||
getAnswerMarkdown(question, a, i)
|
quizQuestionAnswerMarkdownUtils.getAnswerMarkdown(question, a, i)
|
||||||
);
|
);
|
||||||
|
|
||||||
const distractorText =
|
const distractorText =
|
||||||
@@ -38,89 +88,38 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseMarkdown(input: string, questionIndex: number): LocalQuizQuestion {
|
parseMarkdown(input: string, questionIndex: number): LocalQuizQuestion {
|
||||||
const lines = input
|
const { points, lines } = splitLinesAndPoints(input.trim().split("\n"));
|
||||||
.trim()
|
|
||||||
.split("\n");
|
|
||||||
const firstLineIsPoints = lines[0].toLowerCase().includes("points: ");
|
|
||||||
|
|
||||||
const textHasPoints =
|
const linesWithoutAnswers = getLinesBeforeAnswerLines(lines);
|
||||||
lines.length > 0 &&
|
|
||||||
lines[0].includes(": ") &&
|
|
||||||
lines[0].split(": ").length > 1 &&
|
|
||||||
!isNaN(parseFloat(lines[0].split(": ")[1]));
|
|
||||||
|
|
||||||
const points =
|
const questionType = quizQuestionAnswerMarkdownUtils.getQuestionType(
|
||||||
firstLineIsPoints && textHasPoints
|
lines,
|
||||||
? parseFloat(lines[0].split(": ")[1])
|
questionIndex
|
||||||
: 1;
|
|
||||||
|
|
||||||
const linesWithoutPoints = firstLineIsPoints ? lines.slice(1) : lines;
|
|
||||||
|
|
||||||
const { linesWithoutAnswers } = linesWithoutPoints.reduce(
|
|
||||||
({ linesWithoutAnswers, taking }, currentLine) => {
|
|
||||||
if (!taking)
|
|
||||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
|
||||||
|
|
||||||
const lineIsAnswer = isAnswerLine(currentLine);
|
|
||||||
if (lineIsAnswer)
|
|
||||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
|
||||||
|
|
||||||
return {
|
|
||||||
linesWithoutAnswers: [...linesWithoutAnswers, currentLine],
|
|
||||||
taking: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ linesWithoutAnswers: [] as string[], taking: true }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const questionType = getQuestionType(lines, questionIndex);
|
const linesWithoutAnswersAndTypes = removeQuestionTypeFromDescriptionLines(
|
||||||
|
linesWithoutAnswers,
|
||||||
const questionTypesWithoutAnswers = [
|
questionType
|
||||||
"essay",
|
);
|
||||||
"short answer",
|
|
||||||
"short_answer",
|
|
||||||
];
|
|
||||||
|
|
||||||
const descriptionLines = questionTypesWithoutAnswers.includes(questionType)
|
|
||||||
? linesWithoutAnswers
|
|
||||||
.slice(0, linesWithoutPoints.length)
|
|
||||||
.filter(
|
|
||||||
(line) => !questionTypesWithoutAnswers.includes(line.toLowerCase())
|
|
||||||
)
|
|
||||||
: linesWithoutAnswers;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
correctComments,
|
correctComments,
|
||||||
incorrectComments,
|
incorrectComments,
|
||||||
neutralComments,
|
neutralComments,
|
||||||
otherLines: descriptionWithoutFeedback,
|
otherLines: descriptionLines,
|
||||||
} = quizFeedbackMarkdownUtils.extractFeedback(descriptionLines);
|
} = quizFeedbackMarkdownUtils.extractFeedback(linesWithoutAnswersAndTypes);
|
||||||
|
|
||||||
const typesWithAnswers = [
|
const { answers, distractors } = quizQuestionAnswerMarkdownUtils.getAnswers(
|
||||||
"multiple_choice",
|
lines,
|
||||||
"multiple_answers",
|
questionIndex,
|
||||||
"matching",
|
questionType
|
||||||
"short_answer=",
|
);
|
||||||
];
|
|
||||||
const answers = typesWithAnswers.includes(questionType)
|
|
||||||
? getAnswers(lines, questionIndex, questionType)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const answersWithoutDistractors =
|
|
||||||
questionType === QuestionType.MATCHING
|
|
||||||
? answers.filter((a) => a.text)
|
|
||||||
: answers;
|
|
||||||
|
|
||||||
const distractors =
|
|
||||||
questionType === QuestionType.MATCHING
|
|
||||||
? answers.filter((a) => !a.text).map((a) => a.matchedText ?? "")
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const question: LocalQuizQuestion = {
|
const question: LocalQuizQuestion = {
|
||||||
text: descriptionWithoutFeedback.join("\n"),
|
text: descriptionLines.join("\n"),
|
||||||
questionType,
|
questionType,
|
||||||
points,
|
points,
|
||||||
answers: answersWithoutDistractors,
|
answers,
|
||||||
matchDistractors: distractors,
|
matchDistractors: distractors,
|
||||||
correctComments,
|
correctComments,
|
||||||
incorrectComments,
|
incorrectComments,
|
||||||
|
|||||||
Reference in New Issue
Block a user