diff options
Diffstat (limited to 'gemini')
-rw-r--r-- | gemini/gemtext/sub.go | 122 | ||||
-rw-r--r-- | gemini/gemtext/sub_test.go | 60 | ||||
-rw-r--r-- | gemini/serve.go | 3 |
3 files changed, 183 insertions, 2 deletions
diff --git a/gemini/gemtext/sub.go b/gemini/gemtext/sub.go new file mode 100644 index 0000000..a99bed2 --- /dev/null +++ b/gemini/gemtext/sub.go @@ -0,0 +1,122 @@ +package gemtext + +import ( + "bytes" + "html/template" + "net/url" + "regexp" + "strconv" + "strings" + "time" +) + +type gemSub struct { + ID template.URL + Title string + Subtitle string + Updated string + + Entries []gemSubEntry +} + +type gemSubEntry 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) *gemSub { + sub := &gemSub{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, gemSubEntry{ + 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 +} + +func GemsubToAtom(doc Document, location url.URL) string { + buf := &bytes.Buffer{} + if err := atomTmpl.Execute(buf, parseGemSub(doc, &location)); err != nil { + panic(err) + } + return `<?xml version="1.0" encoding="utf-8"?>` + "\n" + buf.String() +} + + + +var atomTmpl = template.Must(template.New("atom").Parse(` +<feed xmlns="http://www.w3.org/2005/Atom"> + <id>{{.ID}}</id> + <link href="{{.ID}}"/> + <title>{{.Title}}</title> + {{- if .Subtitle }} + <subtitle>{{.Subtitle}}</subtitle> + {{- end }} + <updated>{{.Updated}}</updated> +{{- range .Entries }} + <entry> + <id>{{.ID}}</id> + <link rel="alternate" href="{{.ID}}"/> + <title>{{.Title}}</title> + <updated>{{.Updated}}</updated> + </entry> +{{- end }} +</feed> +`[1:])) diff --git a/gemini/gemtext/sub_test.go b/gemini/gemtext/sub_test.go new file mode 100644 index 0000000..8bba682 --- /dev/null +++ b/gemini/gemtext/sub_test.go @@ -0,0 +1,60 @@ +package gemtext + +import ( + "bytes" + "net/url" + "testing" +) + +func TestGemsubToAtom(t *testing.T) { + tests := []struct { + url string + input string + output string + }{ + { + url: "gemini://sombodys.site/a/page", + input: ` +# This is a gemlog page + + +## with a subtitle after empty lines + +=> ./first-post.gmi 2023-08-25 - This is my first post +`[1:], + output: ` +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <id>gemini://sombodys.site/a/page</id> + <link href="gemini://sombodys.site/a/page"/> + <title>This is a gemlog page</title> + <subtitle>with a subtitle after empty lines</subtitle> + <updated>2023-08-25T12:00:00Z</updated> + <entry> + <id>./first-post.gmi</id> + <link rel="alternate" href="./first-post.gmi"/> + <title>This is my first post</title> + <updated>2023-08-25T12:00:00Z</updated> + </entry> +</feed> +`[1:], + }, + } + + for _, test := range tests { + t.Run(test.url, func(t *testing.T) { + doc, err := Parse(bytes.NewBufferString(test.input)) + if err != nil { + t.Fatal(err) + } + loc, err := url.Parse(test.url) + if err != nil { + t.Fatal(err) + } + xml := GemsubToAtom(doc, *loc) + if xml != test.output { + t.Fatal("mismatched output") + } + }) + } +} diff --git a/gemini/serve.go b/gemini/serve.go index 6fee458..5c6c979 100644 --- a/gemini/serve.go +++ b/gemini/serve.go @@ -66,7 +66,6 @@ func (s *server) handleConn(conn net.Conn) { request.TLSState = &state } - ctx := s.Ctx if request.Scheme == "titan" { len, err := sizeParam(request.Path) if err == nil { @@ -81,7 +80,7 @@ func (s *server) handleConn(conn net.Conn) { _, _ = io.Copy(conn, NewResponseReader(Failure(err))) } }() - response = s.handler.Handle(ctx, request) + response = s.handler.Handle(s.Ctx, request) if response == nil { response = NotFound("Resource does not exist.") } |