probo/pkg/models/quiz.go
2024-04-12 13:39:49 +02:00

232 lines
4.6 KiB
Go

package models
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"sort"
"strings"
"gopkg.in/yaml.v2"
)
type Quiz struct {
Meta
Hash string `json:"hash"`
Question *Question `json:"question"`
Answers []*Answer `json:"answers"`
Tags []string `json:"tags" yaml:"-"`
Correct *Answer `json:"correct"`
CorrectPos uint // Position of the correct answer during quiz creation
Type int `json:"type"`
}
func MarkdownToQuiz(quiz *Quiz, markdown string) error {
meta, remainingMarkdown, err := ParseMetaHeaderFromMarkdown(markdown)
if err != nil {
return err
}
lines := strings.Split(remainingMarkdown, "\n")
questionText := ""
answers := []*Answer{}
tags := make([]string, 0)
for _, line := range lines {
if strings.HasPrefix(line, "*") {
answerText := strings.TrimPrefix(line, "* ")
answer := &Answer{Text: answerText}
answers = append(answers, answer)
} else {
if questionText != "" {
questionText += "\n"
}
questionText += line
}
parseTags(&tags, line)
}
questionText = strings.TrimRight(questionText, "\n")
if questionText == "" {
return fmt.Errorf("Question text should not be empty.")
}
if len(answers) < 2 {
return fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers))
}
question := &Question{Text: questionText}
quiz.Question = question
quiz.Answers = answers
quiz.Tags = tags
if meta != nil {
quiz.Meta = *meta
}
return nil
}
func QuizToMarkdown(quiz *Quiz) (string, error) {
if quiz.Question == nil {
return "", errors.New("Quiz should contain a question but it wasn't provided.")
}
if len(quiz.Answers) < 2 {
return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
}
quiz.Correct = quiz.Answers[quiz.CorrectPos]
if quiz.Correct == nil {
return "", errors.New("Quiz should contain a correct answer but not was provided.")
}
correctAnswer := "* " + quiz.Correct.Text
var otherAnswers string
for pos, answer := range quiz.Answers {
if quiz.CorrectPos != uint(pos) {
otherAnswers += "* " + answer.Text + "\n"
}
}
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
return markdown, nil
}
func (q *Quiz) GetHash() string {
return q.calculateHash()
}
func (q *Quiz) Marshal() ([]byte, error) {
result, err := QuizToMarkdown(q)
return []byte(result), err
}
func (q *Quiz) Unmarshal(data []byte) error {
return MarkdownToQuiz(q, string(data))
}
func (q *Quiz) calculateHash() string {
result := make([]string, 0)
result = append(result, q.Question.GetHash())
for _, a := range q.Answers {
result = append(result, a.GetHash())
}
orderedHashes := make([]string, len(result))
copy(orderedHashes, result)
sort.Strings(orderedHashes)
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(orderedHashes, ""))))
}
func ParseMetaHeaderFromMarkdown(markdown string) (*Meta, string, error) {
reader := strings.NewReader(markdown)
var sb strings.Builder
var line string
var err error
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
if sb.String() == "" {
return nil, markdown, nil
}
var meta Meta
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, markdown, err
}
remainingMarkdown := markdown[strings.Index(markdown, "---\n"+sb.String()+"---\n")+len("---\n"+sb.String()+"---\n"):]
return &meta, remainingMarkdown, nil
}
func readLine(reader *strings.Reader) (string, error) {
var sb strings.Builder
for {
r, _, err := reader.ReadRune()
if err != nil {
if err == io.EOF {
return sb.String(), io.EOF
}
return "", err
}
sb.WriteRune(r)
if r == '\n' {
break
}
}
return sb.String(), nil
}
func parseTags(tags *[]string, text string) {
// Trim the following chars
trimChars := "*:.,/\\@()[]{}<>"
// Split the text into words
words := strings.Fields(text)
for _, word := range words {
// If the word starts with '#', it is considered as a tag
if strings.HasPrefix(word, "#") {
// Check if the tag already exists in the tags slice
exists := false
for _, tag := range *tags {
if tag == word {
exists = true
break
}
}
// If the tag does not exist in the tags slice, add it
if !exists {
*tags = append(*tags, strings.TrimRight(word, trimChars))
}
}
}
}