moth/cmd/transpile/puzzle.go

218 lines
4.4 KiB
Go
Raw Normal View History

package main
import (
"bufio"
"bytes"
2020-08-28 17:41:17 -06:00
"context"
"encoding/json"
"fmt"
"io"
"net/mail"
2020-08-28 17:41:17 -06:00
"os/exec"
"strconv"
"strings"
2020-08-28 17:41:17 -06:00
"time"
2020-08-14 20:26:04 -06:00
"github.com/russross/blackfriday"
2020-08-28 17:41:17 -06:00
"github.com/spf13/afero"
2020-08-14 20:26:04 -06:00
"gopkg.in/yaml.v2"
)
2020-08-28 17:41:17 -06:00
// NewPuzzle returns a new Puzzle for points.
func NewPuzzle(fs afero.Fs, points int) (*Puzzle, error) {
p := &Puzzle{
fs: afero.NewBasePathFs(fs, strconv.Itoa(points)),
}
if err := p.parseMoth(); err != nil {
return p, err
}
// BUG(neale): Doesn't yet handle "puzzle.py" or "mkpuzzle"
return p, nil
2019-08-17 13:09:09 -06:00
}
2020-08-28 17:41:17 -06:00
// Puzzle contains everything about a puzzle.
2019-08-17 13:09:09 -06:00
type Puzzle struct {
Pre struct {
2019-08-17 13:09:09 -06:00
Authors []string
Attachments []Attachment
2019-08-17 16:00:15 -06:00
Scripts []Attachment
2019-08-17 13:09:09 -06:00
AnswerPattern string
Body string
}
Post struct {
Objective string
2019-08-17 13:09:09 -06:00
Success struct {
Acceptable string
Mastery string
}
KSAs []string
}
Debug struct {
2019-08-17 13:09:09 -06:00
Log []string
Errors []string
Hints []string
Summary string
}
2019-08-17 13:09:09 -06:00
Answers []string
2020-08-28 17:41:17 -06:00
fs afero.Fs
}
2020-08-28 17:41:17 -06:00
// Attachment carries information about an attached file.
type Attachment struct {
Filename string // Filename presented as part of puzzle
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
Listed bool // Whether this file is listed as an attachment
}
2020-08-28 17:41:17 -06:00
func legacyAttachmentParser(val []string) []Attachment {
2019-08-17 16:00:15 -06:00
ret := make([]Attachment, len(val))
for idx, txt := range val {
parts := strings.SplitN(txt, " ", 3)
cur := Attachment{}
cur.FilesystemPath = parts[0]
if len(parts) > 1 {
cur.Filename = parts[1]
} else {
cur.Filename = cur.FilesystemPath
}
if (len(parts) > 2) && (parts[2] == "hidden") {
cur.Listed = false
} else {
cur.Listed = true
}
ret[idx] = cur
}
return ret
}
2020-08-28 17:41:17 -06:00
func (p *Puzzle) yamlHeaderParser(r io.Reader) error {
decoder := yaml.NewDecoder(r)
decoder.SetStrict(true)
return decoder.Decode(p)
}
func (p *Puzzle) rfc822HeaderParser(r io.Reader) error {
m, err := mail.ReadMessage(r)
if err != nil {
2020-08-28 17:41:17 -06:00
return fmt.Errorf("Parsing RFC822 headers: %v", err)
}
2019-08-17 13:09:09 -06:00
for key, val := range m.Header {
key = strings.ToLower(key)
switch key {
2019-08-17 13:09:09 -06:00
case "author":
2020-08-28 17:41:17 -06:00
p.Pre.Authors = val
2019-08-17 13:09:09 -06:00
case "pattern":
2020-08-28 17:41:17 -06:00
p.Pre.AnswerPattern = val[0]
2019-08-17 16:00:15 -06:00
case "script":
2020-08-28 17:41:17 -06:00
p.Pre.Scripts = legacyAttachmentParser(val)
2019-08-17 16:00:15 -06:00
case "file":
2020-08-28 17:41:17 -06:00
p.Pre.Attachments = legacyAttachmentParser(val)
2019-08-17 13:09:09 -06:00
case "answer":
2020-08-28 17:41:17 -06:00
p.Answers = val
2019-08-17 13:09:09 -06:00
case "summary":
2020-08-28 17:41:17 -06:00
p.Debug.Summary = val[0]
2019-08-17 13:09:09 -06:00
case "hint":
2020-08-28 17:41:17 -06:00
p.Debug.Hints = val
2019-08-17 13:09:09 -06:00
case "ksa":
2020-08-28 17:41:17 -06:00
p.Post.KSAs = val
2019-08-17 13:09:09 -06:00
default:
2020-08-28 17:41:17 -06:00
return fmt.Errorf("Unknown header field: %s", key)
}
}
2020-08-28 17:41:17 -06:00
return nil
}
2020-08-28 17:41:17 -06:00
func (p *Puzzle) parseMoth() error {
2020-08-31 16:37:51 -06:00
r, err := p.fs.Open("puzzle.md")
2020-08-28 17:41:17 -06:00
if err != nil {
2020-08-31 16:37:51 -06:00
var err2 error
if r, err2 = p.fs.Open("puzzle.moth"); err2 != nil {
return err
}
2020-08-28 17:41:17 -06:00
}
defer r.Close()
headerBuf := new(bytes.Buffer)
2020-08-28 17:41:17 -06:00
headerParser := p.rfc822HeaderParser
headerEnd := ""
scanner := bufio.NewScanner(r)
lineNo := 0
for scanner.Scan() {
line := scanner.Text()
2020-08-28 17:41:17 -06:00
lineNo++
if lineNo == 1 {
if line == "---" {
2020-08-28 17:41:17 -06:00
headerParser = p.yamlHeaderParser
headerEnd = "---"
continue
}
}
if line == headerEnd {
2020-08-28 17:41:17 -06:00
headerBuf.WriteRune('\n')
break
}
headerBuf.WriteString(line)
headerBuf.WriteRune('\n')
}
2019-08-17 13:09:09 -06:00
bodyBuf := new(bytes.Buffer)
for scanner.Scan() {
line := scanner.Text()
2020-08-28 17:41:17 -06:00
lineNo++
bodyBuf.WriteString(line)
bodyBuf.WriteRune('\n')
}
2019-08-17 13:09:09 -06:00
2020-08-28 17:41:17 -06:00
if err := headerParser(headerBuf); err != nil {
return err
}
2020-08-14 20:26:04 -06:00
2019-08-17 16:00:15 -06:00
// Markdownify the body
2020-08-28 17:41:17 -06:00
if (p.Pre.Body != "") && (bodyBuf.Len() > 0) {
return fmt.Errorf("Puzzle body present in header and in moth body")
}
p.Pre.Body = string(blackfriday.Run(bodyBuf.Bytes()))
return nil
}
func (p *Puzzle) mkpuzzle() error {
bfs, ok := p.fs.(*afero.BasePathFs)
if !ok {
return fmt.Errorf("Fs won't resolve real paths for %v", p)
}
mkpuzzlePath, err := bfs.RealPath("mkpuzzle")
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, mkpuzzlePath)
stdout, err := cmd.Output()
if err != nil {
return err
2019-08-17 13:09:09 -06:00
}
2020-08-28 17:41:17 -06:00
jsdec := json.NewDecoder(bytes.NewReader(stdout))
jsdec.DisallowUnknownFields()
puzzle := new(Puzzle)
if err := jsdec.Decode(puzzle); err != nil {
return err
}
return nil
}
// Open returns a newly-opened file.
func (p *Puzzle) Open(name string) (io.ReadCloser, error) {
// BUG(neale): You cannot open generated files in puzzles, only files actually on the disk
return p.fs.Open(name)
}