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)) } } } }