我正在尝试向一个半流行的 JavaScript-Markdown 程序添加一个额外的功能来创建名为 Quizdown 的测验,这是免费回答问题,您所拥有的只是一个输入栏,供用户输入和输入将检查传递的字符串是否与我指定为答案的字符串匹配,但是我不确定如何更新代码以执行此操作,到目前为止我已经完成了。我创建了一种名为“FreeResponse”的新型问题,并使用已提供的现有代码为其添加了一个类。然后在 Parser 中,我只是将它添加到 parseQuestion() 函数中,我不确定从这里去哪里。我知道我必须更新 determineQuestionType() 函数以确定问题类型是否为自由回答,而且我很可能必须更新 App.svelte 以包含一个新的文本栏组件。我将在这里提供这两个文件: 小测验:
import { writable, get, Writable } from 'svelte/store';
import autoBind from 'auto-bind';
import type { Config } from './config.js';
//Modify this file to support a FreeResponse question type
function isEqual(a1: Array<number>, a2: Array<number>): boolean {
return JSON.stringify(a1) === JSON.stringify(a2);
}
function shuffle(array: Array<any>, n: number | undefined): Array<any> {
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
let currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array.slice(0, n);
}
// we need to reference the classes in the svelte app despite minifaction of class names
export type QuestionType = 'MultipleChoice' | 'SingleChoice' | 'Sequence' | 'FreeResponse';
export abstract class BaseQuestion {
readonly text: string;
answers: Array<Answer>;
readonly explanation: string;
selected: Array<number>;
solved: boolean;
readonly hint: string;
readonly questionType: QuestionType;
readonly options: Config;
showHint: Writable<boolean>;
visited: boolean;
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
questionType: QuestionType,
options: Config
) {
if (answers.length === 0) {
throw 'no answers for question provided';
}
this.text = text;
this.explanation = explanation;
this.hint = hint;
this.solved = false;
this.showHint = writable(false);
this.options = options;
this.answers = answers;
this.questionType = questionType;
this.visited = false;
autoBind(this);
this.reset();
}
enableHint() {
this.showHint.update((val) => true);
}
reset() {
this.selected = [];
this.solved = false;
this.visited = false;
this.showHint.set(false);
if (this.options.shuffleAnswers) {
this.answers = shuffle(this.answers, this.answers.length);
}
}
abstract isCorrect(): boolean;
}
class Blanks extends BaseQuestion {
isCorrect() {
this.solved = false;
return this.solved;
}
}
class Pairs extends BaseQuestion {
isCorrect() {
this.solved = false;
return this.solved;
}
}
export class Sequence extends BaseQuestion {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
// always enable shuffling for sequence questions
options.shuffleAnswers = true;
super(text, explanation, hint, answers, 'Sequence', options);
}
isCorrect() {
// extract answer ids from answers
let trueAnswerIds = this.answers.map((answer) => answer.id);
this.solved = isEqual(trueAnswerIds.sort(), this.selected);
return this.solved;
}
}
//Create a FreeResponse class that extends BaseQuestion
class FreeResponse extends BaseQuestion {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
super(text, explanation, hint, answers, 'FreeResponse', options);
}
isCorrect() {
let trueAnswerIds = this.answers
.filter((answer) => answer.correct)
.map((answer) => answer.id);
let selectedAnswerIds = this.selected.map((i) => this.answers[i].id);
this.solved = isEqual(trueAnswerIds.sort(), selectedAnswerIds.sort());
return this.solved;
}
}
class Choice extends BaseQuestion {
isCorrect() {
let trueAnswerIds = this.answers
.filter((answer) => answer.correct)
.map((answer) => answer.id);
let selectedAnswerIds = this.selected.map((i) => this.answers[i].id);
this.solved = isEqual(trueAnswerIds.sort(), selectedAnswerIds.sort());
return this.solved;
}
}
export class MultipleChoice extends Choice {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
super(text, explanation, hint, answers, 'MultipleChoice', options);
}
}
export class SingleChoice extends Choice {
constructor(
text: string,
explanation: string,
hint: string,
answers: Array<Answer>,
options: Config
) {
super(text, explanation, hint, answers, 'SingleChoice', options);
let nCorrect = this.answers.filter((answer) => answer.correct).length;
if (nCorrect > 1) {
throw 'Single Choice questions can not have more than one correct answer.';
}
}
}
export class Answer {
html: string;
correct: boolean;
id: number;
comment: string;
constructor(id: number, html: string, correct: boolean, comment: string) {
this.html = html;
this.correct = correct;
this.id = id;
this.comment = comment;
autoBind(this);
}
}
export class Quiz {
questions: Array<BaseQuestion>;
active: Writable<BaseQuestion>;
index: Writable<number>;
config: Config;
onLast: Writable<boolean>;
onResults: Writable<boolean>;
onFirst: Writable<boolean>;
isEvaluated: Writable<boolean>;
allVisited: Writable<boolean>;
constructor(questions: Array<BaseQuestion>, config: Config) {
this.index = writable(0);
this.questions = questions;
this.config = config;
if (this.config.shuffleQuestions) {
this.questions = shuffle(this.questions, this.config.nQuestions);
}
if (this.questions.length == 0) {
throw 'No questions for quiz provided';
}
// setup first question
this.active = writable(this.questions[0]);
this.questions[0].visited = true;
this.onLast = writable(this.questions.length == 1);
this.onResults = writable(false);
this.onFirst = writable(true);
this.allVisited = writable(this.questions.length == 1);
this.isEvaluated = writable(false);
autoBind(this);
}
private setActive() {
let idx = get(this.index);
this.active.update((act) => this.questions[idx]);
this.questions[idx].visited = true;
}
private checkAllVisited(): boolean {
for (let question of this.questions) {
if (!question.visited) {
return false;
}
}
return true;
}
jump(index: number): boolean {
if (index <= this.questions.length - 1 && index >= 0) {
// on a question
this.index.set(index);
this.setActive();
this.allVisited.set(this.checkAllVisited());
this.onResults.set(false);
this.onLast.set(index == this.questions.length - 1);
this.onFirst.set(index == 0);
return true;
} else if (index == this.questions.length) {
// on results page
this.onResults.set(true);
this.onLast.set(false);
this.index.set(index);
return true;
} else {
return false;
}
}
next(): boolean {
return this.jump(get(this.index) + 1);
}
previous(): boolean {
return this.jump(get(this.index) - 1);
}
reset(): Boolean {
this.onLast.set(false);
this.onResults.set(false);
this.allVisited.set(false);
this.isEvaluated.set(false);
this.questions.forEach((q) => q.reset());
return this.jump(0);
}
evaluate(): number {
var points = 0;
for (var q of this.questions) {
if (q.isCorrect()) {
points += 1;
}
}
this.isEvaluated.set(true);
return points;
}
}
//Export the FreeResponse class
export { FreeResponse };
解析器.ts:
import DOMPurify from 'dompurify';
import stripIndent from 'strip-indent';
import {
Quiz,
BaseQuestion,
MultipleChoice,
SingleChoice,
FreeResponse,
Sequence,
Answer,
QuestionType,
} from './quiz';
import { Config, mergeAttributes } from './config';
import marked from './customizedMarked';
function parseQuizdown(rawQuizdown: string, globalConfig: Config): Quiz {
let tokens = tokenize(rawQuizdown);
// globalConfig < quizConfig < questionConfig
let quizConfig = new Config(globalConfig);
if (hasQuizOptions(tokens)) {
quizConfig = parseOptions(tokens, quizConfig);
}
let firstHeadingIdx = findFirstHeadingIdx(tokens);
let questions = extractQuestions(tokens.slice(firstHeadingIdx), quizConfig);
return new Quiz(questions, quizConfig);
}
function tokenize(rawQuizdown: string): marked.TokensList {
return marked.lexer(htmlDecode(stripIndent(rawQuizdown)));
}
function hasQuizOptions(tokens: marked.TokensList) {
// type definition does not allow custom token types
// @ts-ignore
let optionsIdx = tokens.findIndex((token) => token['type'] == 'options');
let headingIdx = tokens.findIndex((token) => token['type'] == 'heading');
// quiz options appear at the top before the first heading
return optionsIdx !== -1 && headingIdx > optionsIdx;
}
function parseOptions(tokens: marked.Token[], quizConfig: Config): Config {
// type definition does not allow custom token types
// @ts-ignore
let options = tokens.find((token) => token.type == 'options');
return mergeAttributes(quizConfig, options['data']);
}
function extractQuestions(tokens: marked.Token[], config: Config) {
let questions: BaseQuestion[] = [];
let nextQuestion = 0;
while (tokens.length !== 0) {
nextQuestion = findFirstHeadingIdx(tokens.slice(1));
if (nextQuestion === -1) {
// no next question on last question
nextQuestion = tokens.length;
}
let question = parseQuestion(
tokens.splice(0, nextQuestion + 1),
config
);
questions.push(question);
}
return questions;
}
function parseQuestion(tokens: marked.Token[], config: Config): BaseQuestion {
let explanation = parseExplanation(tokens);
let hint = parseHint(tokens);
let heading = parseHeading(tokens);
let answers = parseAnswers(tokens);
let questionType = determineQuestionType(tokens);
let questionConfig = new Config(config);
const args = [heading, explanation, hint, answers, questionConfig] as const;
switch (questionType) {
case 'SingleChoice':
return new SingleChoice(...args);
case 'MultipleChoice':
return new MultipleChoice(...args);
case 'Sequence':
return new Sequence(...args);
case 'FreeResponse':
return new FreeResponse(...args);
}
}
function findFirstHeadingIdx(tokens: marked.Token[]): number {
return tokens.findIndex((token) => token['type'] == 'heading');
}
function parseHint(tokens: marked.Token[]): string {
let blockquotes = tokens.filter((token) => token['type'] == 'blockquote');
return parseTokens(blockquotes);
}
function parseExplanation(tokens: marked.Token[]): string {
let explanations = tokens.filter(
(token) => token['type'] == 'paragraph' || token['type'] == 'code'
);
return parseTokens(explanations);
}
function parseHeading(tokens: marked.Token[]): string {
let headings = tokens.filter((token) => token['type'] == 'heading');
return parseTokens(headings);
}
function parseAnswers(tokens: marked.Token[]): Array<Answer> {
let list = tokens.find(
(token) => token.type == 'list'
) as marked.Tokens.List;
let answers: Array<Answer> = [];
list.items.forEach(function (item, i) {
let answer = parseAnswer(item);
answers.push(
new Answer(i, answer['text'], item['checked'], answer['comment'])
);
});
return answers;
}
function parseAnswer(item: marked.Tokens.ListItem) {
let comments = item['tokens'].filter((token) => token.type == 'blockquote');
let texts = item['tokens'].filter((token) => token.type != 'blockquote');
return { text: parseTokens(texts), comment: parseTokens(comments) };
}
//Modify this function to check for FreeResponse
function determineQuestionType(tokens: marked.Token[]): QuestionType {
let list = tokens.find(
(token) => token.type == 'list'
) as marked.Tokens.List;
if (list.ordered) {
if (list.items[0].task) {
return 'SingleChoice';
} else {
return 'Sequence';
}
} else {
return 'MultipleChoice';
}
}
function parseTokens(tokens: marked.Token[]): string {
return DOMPurify.sanitize(marked.parser(tokens as marked.TokensList));
}
function htmlDecode(text: string) {
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&');
}
export default parseQuizdown;
App.svelte:
<script lang="ts">
import type { Quiz } from './quiz';
import ProgressBar from './components/ProgressBar.svelte';
import { onMount } from 'svelte';
import registerLanguages from './languages/i18n';
import Card from './components/Card.svelte';
import Credits from './components/Credits.svelte';
import SmoothResize from './components/SmoothResize.svelte';
import QuestionView from './components/QuestionView.svelte';
import Row from './components/Row.svelte';
import Button from './components/Button.svelte';
import { _ } from 'svelte-i18n';
import ResultsView from './components/ResultsView.svelte';
// import { Linear, CheckFirst } from './progressModes.js';
import Animated from './components/Animated.svelte';
import registerIcons from './registerIcons.js';
import Icon from './components/Icon.svelte';
import Hint from './components/Hint.svelte';
import { fly } from 'svelte/transition';
import Container from './components/Container.svelte';
import Loading from './components/Loading.svelte';
// import Modal from './components/Modal.svelte';
export let quiz: Quiz;
// https://github.com/sveltejs/svelte/issues/4079
$: question = quiz.active;
$: showHint = $question.showHint;
$: index = quiz.index;
$: onLast = quiz.onLast;
$: onFirst = quiz.onFirst;
$: onResults = quiz.onResults;
$: isEvaluated = quiz.isEvaluated;
$: allVisited = quiz.allVisited;
//let game = new Linear(quiz);
registerLanguages(quiz.config.locale);
registerIcons();
let node: HTMLElement;
let minHeight = 150;
let reloaded = false;
// let showModal = false;
// set global options
onMount(async () => {
let primaryColor: string = quiz.config.primaryColor;
let secondaryColor: string = quiz.config.secondaryColor;
let textColor: string = quiz.config.textColor;
node.style.setProperty('--quizdown-color-primary', primaryColor);
node.style.setProperty('--quizdown-color-secondary', secondaryColor);
node.style.setProperty('--quizdown-color-text', textColor);
node.style.minHeight = `${minHeight}px`;
});
</script>
<div class="quizdown-content" bind:this="{node}">
<Card>
<ProgressBar value="{$index}" max="{quiz.questions.length - 1}" />
<Loading update="{reloaded}" ms="{800}" minHeight="{minHeight}">
<Container>
<SmoothResize minHeight="{minHeight}">
<Animated update="{$index}">
{#if $onResults}
<ResultsView quiz="{quiz}" />
{:else}
<QuestionView
question="{$question}"
n="{$index + 1}"
/>
<Hint hint="{$question.hint}" show="{$showHint}" />
{/if}
</Animated>
</SmoothResize>
<!-- <Modal show="{showModal}">Are you sure?</Modal> -->
<Row>
<Button
slot="left"
title="{$_('hint')}"
disabled="{!$question.hint || $showHint || $onResults}"
buttonAction="{$question.enableHint}"
><Icon name="lightbulb" solid="{false}" /></Button
>
<svelte:fragment slot="center">
<Button
title="{$_('previous')}"
disabled="{$onFirst || $onResults || $isEvaluated}"
buttonAction="{quiz.previous}"
><Icon name="arrow-left" size="lg" /></Button
>
<Button
disabled="{$onLast || $onResults || $isEvaluated}"
buttonAction="{quiz.next}"
title="{$_('next')}"
><Icon name="arrow-right" size="lg" /></Button
>
{#if $onLast || $allVisited}
<div in:fly="{{ x: 200, duration: 500 }}">
<Button
disabled="{!($onLast || $allVisited) ||
$onResults}"
title="{$_('evaluate')}"
buttonAction="{() =>
quiz.jump(quiz.questions.length)}"
><Icon
name="check-double"
size="lg"
/></Button
>
</div>
{/if}
</svelte:fragment>
<Button
slot="right"
title="{$_('reset')}"
buttonAction="{() => {
reloaded = !reloaded;
quiz.reset();
}}"><Icon name="redo" /></Button
>
</Row>
<Credits />
</Container>
</Loading>
</Card>
</div>
<!-- global styles applied to all elements in the app -->
<style type="text/scss" global>
@import 'highlight.js/styles/github';
@import 'katex/dist/katex';
@import '@fortawesome/fontawesome-svg-core/styles';
img {
max-height: 400px;
border-radius: 4px;
max-width: 100%;
height: auto;
}
code {
padding: 0 0.4rem;
font-size: 85%;
color: #333;
white-space: pre-wrap;
border-radius: 4px;
padding: 0.2em 0.4em;
background-color: #f8f8f8;
font-family: Consolas, Monaco, monospace;
}
a {
color: var(--quizdown-color-primary);
}
.quizdown-content {
padding: 1rem;
max-width: 900px;
margin: auto;
}
</style>
在我的 Quizdown 功能更新方面的任何帮助。