summaryrefslogtreecommitdiff
path: root/src/gmi/gemtext
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2024-05-17 23:37:43 +0200
committerBrian Picciano <mediocregopher@gmail.com>2024-05-18 14:47:09 +0200
commit8d7e708d98a3a46ba3ba08f9c8deeb4838bb8ca5 (patch)
tree6662c3e4c6c3baaea058a3deaba0d9cfc8e9cc40 /src/gmi/gemtext
parentfac06df97a47cda6e8989bfc5f40f2a627279b92 (diff)
Render posts completely using common rendering methods
The aim is to reduce reliance on custom logic in the handlers for every protocol, eventually outsourcing all of it into `render.Methods`, leaving each protocol to simply direct calls to the correct template.
Diffstat (limited to 'src/gmi/gemtext')
-rw-r--r--src/gmi/gemtext/gemtext.go88
-rw-r--r--src/gmi/gemtext/gemtext_test.go66
2 files changed, 154 insertions, 0 deletions
diff --git a/src/gmi/gemtext/gemtext.go b/src/gmi/gemtext/gemtext.go
new file mode 100644
index 0000000..5c8f594
--- /dev/null
+++ b/src/gmi/gemtext/gemtext.go
@@ -0,0 +1,88 @@
+// Package gemtext contains code related to processing and producing gemtext
+// documents.
+package gemtext
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "net/url"
+ "path"
+ "regexp"
+ "strings"
+)
+
+func hasImgExt(p string) bool {
+ switch path.Ext(strings.ToLower(p)) {
+ case ".jpg", ".jpeg", ".png", ".gif", ".svg":
+ return true
+ default:
+ return false
+ }
+}
+
+// matches `=> dstURL [optional description]`
+var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`)
+
+// ToMarkdown reads a gemtext formatted body from the Reader and writes
+// the markdown version of that body to the Writer.
+//
+// gmiGateway, if given, is used for all `gemini://` links. The `gemini://`
+// prefix will be stripped, and replaced with the given URL.
+func ToMarkdown(dst io.Writer, src io.Reader, gmiGateway *url.URL) error {
+
+ bufSrc := bufio.NewReader(src)
+
+ for {
+
+ line, err := bufSrc.ReadString('\n')
+ if err != nil && !errors.Is(err, io.EOF) {
+ return fmt.Errorf("reading: %w", err)
+ }
+
+ last := err == io.EOF
+
+ if match := linkRegexp.FindStringSubmatch(line); len(match) > 0 {
+
+ u, err := url.Parse(match[1])
+ if err != nil {
+ return fmt.Errorf("link to invalid url %q: %w", match[1], err)
+ }
+
+ if u.Scheme == "gemini" && gmiGateway != nil {
+ newUStr := gmiGateway.String() + u.Host + u.Path
+ if u, err = url.Parse(newUStr); err != nil {
+ return fmt.Errorf("parsing proxied URL %q: %w", newUStr, err)
+ }
+ }
+
+ isImg := hasImgExt(u.Path)
+
+ descr := match[2]
+
+ if descr != "" {
+ // ok
+ } else if isImg {
+ descr = "Image"
+ } else {
+ descr = "Link"
+ }
+
+ line = fmt.Sprintf("[%s](%s)\n", descr, u.String())
+
+ if isImg {
+ line = "!" + line
+ }
+ }
+
+ if _, err := dst.Write([]byte(line)); err != nil {
+ return fmt.Errorf("writing: %w", err)
+ }
+
+ if last {
+ return nil
+ }
+ }
+
+}
diff --git a/src/gmi/gemtext/gemtext_test.go b/src/gmi/gemtext/gemtext_test.go
new file mode 100644
index 0000000..fe58a64
--- /dev/null
+++ b/src/gmi/gemtext/gemtext_test.go
@@ -0,0 +1,66 @@
+package gemtext
+
+import (
+ "bytes"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToMarkdown(t *testing.T) {
+
+ gmiGateway, _ := url.Parse("https://gateway.com/x/")
+
+ tests := []struct {
+ in, exp string
+ }{
+ {
+ in: "",
+ exp: "",
+ },
+ {
+ in: "=> foo",
+ exp: "[Link](foo)\n",
+ },
+ {
+ in: "what\n=> foo\n=> bar",
+ exp: "what\n[Link](foo)\n[Link](bar)\n",
+ },
+ {
+ in: "=> foo description is here ",
+ exp: "[description is here](foo)\n",
+ },
+ {
+ in: "=> img.png",
+ exp: "![Image](img.png)\n",
+ },
+ {
+ in: "=> img.png description is here ",
+ exp: "![description is here](img.png)\n",
+ },
+ {
+ in: "=> gemini://somewhere.com/foo Somewhere",
+ exp: "[Somewhere](https://gateway.com/x/somewhere.com/foo)\n",
+ },
+ {
+ in: "=> gemini://somewhere.com:420/foo Somewhere",
+ exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo)\n",
+ },
+ {
+ in: "=> gemini://somewhere.com:420/foo?bar=baz Somewhere",
+ exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo?bar=baz)\n",
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+
+ got := new(bytes.Buffer)
+ err := ToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway)
+ assert.NoError(t, err)
+ assert.Equal(t, test.exp, got.String())
+ })
+ }
+}