package gemtext
import (
"bytes"
"html/template"
"io"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
// GmisubToAtom converts a gemini document to Atom format.
//
// It identifies feed fields and entries according to the specification at
// gemini://gemini.circumlunar.space/docs/companion/subscription.gmi
func GmisubToAtom(doc Document, location url.URL, out io.Writer) error {
if _, err := out.Write([]byte(``)); err != nil {
return err
}
if _, err := out.Write([]byte{'\n'}); err != nil {
return err
}
if err := atomTmpl.Execute(out, parseGemSub(doc, &location)); err != nil {
return err
}
return nil
}
type gmiSub struct {
ID template.URL
Title string
Subtitle string
Updated string
Entries []gmiSubEntry
}
type gmiSubEntry struct {
ID template.URL
Updated string
Title string
}
var linkElemRE = regexp.MustCompile(`(\d{4})-([0-1]\d)-([0-3]\d)`)
func parseGemSub(doc Document, location *url.URL) *gmiSub {
sub := &gmiSub{ID: template.URL(location.String())}
updated := time.Time{}
for i, line := range doc {
switch line.Type() {
case LineTypeHeading1:
if sub.Title != "" {
continue
}
sub.Title = line.(HeadingLine).Body()
for { // skip any empty lines
i += 1
if i >= len(doc) || strings.TrimPrefix(doc[i].String(), "\r") != "\n" {
break
}
}
if i < len(doc) && doc[i].Type() == LineTypeHeading2 {
sub.Subtitle = doc[i].(HeadingLine).Body()
}
case LineTypeLink:
label := line.(LinkLine).Label()
if len(label) < 10 {
continue
}
match := linkElemRE.FindStringSubmatch(label[:10])
if match == nil {
continue
}
year, err := strconv.Atoi(match[1])
if err != nil {
continue
}
month, err := strconv.Atoi(match[2])
if err != nil || month > 12 {
continue
}
day, err := strconv.Atoi(match[3])
if err != nil || day > 31 {
continue
}
entryUpdated := time.Date(year, time.Month(month), day, 12, 0, 0, 0, time.UTC)
entryTitle := strings.TrimLeft(strings.TrimPrefix(strings.TrimLeft(label[10:], " \t"), "-"), " \t")
sub.Entries = append(sub.Entries, gmiSubEntry{
ID: template.URL(line.(LinkLine).URL()),
Updated: entryUpdated.Format(time.RFC3339),
Title: entryTitle,
})
if entryUpdated.After(updated) {
updated = entryUpdated
sub.Updated = updated.Format(time.RFC3339)
}
}
}
return sub
}
var atomTmpl = template.Must(template.New("atom").Parse(`
{{.ID}}
{{.Title}}
{{- if .Subtitle }}
{{.Subtitle}}
{{- end }}
{{.Updated}}
{{- range .Entries }}
{{.ID}}
{{.Title}}
{{.Updated}}
{{- end }}
`[1:]))