pupate

No description provided
git clone https://git.woozle.org/neale/pupate.git

pupate / cmd / moth2md
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}