diff options
| -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.")  		} | 
