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:]))