Neale Pickett
·
2024-12-20
main.go
1package main
2
3import (
4 "flag"
5 "fmt"
6 "io"
7 "log"
8 "net/mail"
9 "os"
10 "strings"
11 "text/template"
12)
13
14var OutputTemplate = template.Must(template.New("md").Parse(`---
15{{- with .Extra.Title}}
16title: "{{.}}"
17{{- end}}
18
19{{- with .Authors}}
20authors: {{- range .}}
21 - {{.}}
22 {{- end}}
23{{- end}}
24
25{{- with .Answers}}
26answers: {{- range .}}
27 - "{{.}}"
28 {{- end}}
29{{- end}}
30
31{{- with .AnswerPattern}}
32answerpattern: "{{.}}"
33{{- end}}
34
35{{- with .KSAs}}
36ksas: {{- range .}}
37 - "{{.}}"
38 {{- end}}
39{{- end}}
40
41{{- with .Attachments}}
42attachments:
43 {{- range .}}
44 - {{.Filename}}
45 {{- end}}
46{{- end}}
47
48{{- with .Scripts}}
49scripts:
50 {{- range .}}
51 - {{.Filename}}
52 {{- end}}
53{{- end}}
54
55{{- with .Objective}}
56objective: "{{.}}"
57{{- end}}
58
59{{- with .Debug}}
60debug:
61 {{- with .Log}}
62 log:
63 {{- range .}}
64 - "{{.}}"
65 {{- end}}
66 {{- end}}
67 {{- with .Errors}}
68 errors:
69 {{- range .}}
70 - "{{.}}"
71 {{- end}}
72 {{- end}}
73 {{- with .Hints}}
74 hints:
75 {{- range .}}
76 - "{{.}}"
77 {{- end}}
78 {{- end}}
79 {{- with .Notes}}
80 notes:
81 {{- range .}}
82 - "{{.}}"
83 {{- end}}
84 {{- end}}
85 {{- with .Summary}}
86 summary: "{{.}}"
87 {{- end}}
88{{- end}}
89
90{{- with .Success}}
91success:
92 {{- with .Acceptable}}
93 acceptable: "{{.}}"
94 {{- end}}
95 {{- with .Mastery}}
96 mastery: "{{.}}"
97 {{- end}}
98{{- end}}
99
100---
101
102{{.Body}}
103`))
104
105// Puzzle contains everything a static puzzle might tell us.
106type Puzzle struct {
107 Authors []string
108 Attachments []StaticAttachment
109 Scripts []StaticAttachment
110 AnswerPattern string
111 Answers []string
112 Debug PuzzleDebug
113 Extra map[string]any
114 Objective string
115 Success struct {
116 Acceptable string
117 Mastery string
118 }
119 KSAs []string
120 Body string
121}
122
123// StaticAttachment carries information about an attached file.
124type StaticAttachment struct {
125 Filename string // Filename presented as part of puzzle
126 FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
127}
128
129// PuzzleDebug is the full suite of debug fields in a puzzle
130type PuzzleDebug struct {
131 Log []string
132 Errors []string
133 Hints []string
134 Notes string
135 Summary string
136}
137
138func legacyAttachmentParser(val []string) []StaticAttachment {
139 ret := make([]StaticAttachment, len(val))
140 for idx, txt := range val {
141 parts := strings.SplitN(txt, " ", 3)
142 cur := StaticAttachment{}
143 cur.FilesystemPath = parts[0]
144 if len(parts) > 1 {
145 cur.Filename = parts[1]
146 } else {
147 cur.Filename = cur.FilesystemPath
148 }
149 ret[idx] = cur
150 }
151 return ret
152}
153
154func rfc822HeaderParser(r io.Reader) (Puzzle, error) {
155 p := Puzzle{}
156 m, err := mail.ReadMessage(r)
157 if err != nil {
158 return p, fmt.Errorf("parsing RFC822 headers: %v", err)
159 }
160
161 for key, val := range m.Header {
162 key = strings.ToLower(key)
163 switch key {
164 case "author":
165 p.Authors = val
166 case "pattern":
167 p.AnswerPattern = val[0]
168 case "script":
169 p.Scripts = legacyAttachmentParser(val)
170 case "file":
171 p.Attachments = legacyAttachmentParser(val)
172 case "answer":
173 p.Answers = val
174 case "summary":
175 p.Debug.Summary = val[0]
176 case "hint":
177 p.Debug.Hints = val
178 case "solution":
179 p.Debug.Hints = val
180 case "ksa":
181 p.KSAs = val
182 case "objective":
183 p.Objective = val[0]
184 case "success.acceptable":
185 p.Success.Acceptable = val[0]
186 case "success.mastery":
187 p.Success.Mastery = val[0]
188 default:
189 return p, fmt.Errorf("unknown header field: %s", key)
190 }
191 }
192
193 body, err := io.ReadAll(m.Body)
194 if err != nil {
195 return p, err
196 }
197 p.Body = string(body)
198 return p, nil
199}
200
201func Usage() {
202 fmt.Fprintf(os.Stderr, "Usage: %s [FLAGS] FILENAME [FILENAME...]\n", os.Args[0])
203 flag.PrintDefaults()
204}
205
206func convert(filename string) error {
207 outfn := strings.TrimSuffix(filename, ".moth") + ".md"
208
209 w, err := os.Create(outfn)
210 if err != nil {
211 return err
212 }
213 defer w.Close()
214
215 r, err := os.Open(filename)
216 if err != nil {
217 return err
218 }
219 defer r.Close()
220
221 puzzle, err := rfc822HeaderParser(r)
222 if err != nil {
223 return err
224 }
225
226
227 if err := OutputTemplate.Execute(w, puzzle); err != nil {
228 return err
229 }
230
231 return nil
232}
233
234func main() {
235 flag.Usage = Usage
236 flag.Parse()
237 if (flag.NArg() < 1) {
238 flag.Usage()
239 os.Exit(1)
240 }
241
242 for _, filename := range flag.Args() {
243 log.Println("Converting", filename)
244 if err := convert(filename); err != nil {
245 log.Fatal(err)
246 }
247 }
248}