summaryrefslogtreecommitdiff
path: root/src/gmi/gemtext
diff options
context:
space:
mode:
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())
+ })
+ }
+}