From 8d7e708d98a3a46ba3ba08f9c8deeb4838bb8ca5 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Fri, 17 May 2024 23:37:43 +0200 Subject: 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. --- src/gmi/gemtext.go | 86 ----------------------- src/gmi/gemtext/gemtext.go | 88 +++++++++++++++++++++++ src/gmi/gemtext/gemtext_test.go | 66 ++++++++++++++++++ src/gmi/gemtext_test.go | 66 ------------------ src/gmi/gmi.go | 14 ++-- src/gmi/tpl.go | 150 ++++++++-------------------------------- src/gmi/tpl/posts/post.gmi | 4 +- 7 files changed, 191 insertions(+), 283 deletions(-) delete mode 100644 src/gmi/gemtext.go create mode 100644 src/gmi/gemtext/gemtext.go create mode 100644 src/gmi/gemtext/gemtext_test.go delete mode 100644 src/gmi/gemtext_test.go (limited to 'src/gmi') diff --git a/src/gmi/gemtext.go b/src/gmi/gemtext.go deleted file mode 100644 index 884635c..0000000 --- a/src/gmi/gemtext.go +++ /dev/null @@ -1,86 +0,0 @@ -package gmi - -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*$`) - -// GemtextToMarkdown 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 GemtextToMarkdown(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.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()) + }) + } +} diff --git a/src/gmi/gemtext_test.go b/src/gmi/gemtext_test.go deleted file mode 100644 index 75da9df..0000000 --- a/src/gmi/gemtext_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package gmi - -import ( - "bytes" - "net/url" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGemtextToMarkdown(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 := GemtextToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway) - assert.NoError(t, err) - assert.Equal(t, test.exp, got.String()) - }) - } -} diff --git a/src/gmi/gmi.go b/src/gmi/gmi.go index 467ab5a..e37ca74 100644 --- a/src/gmi/gmi.go +++ b/src/gmi/gmi.go @@ -14,14 +14,14 @@ import ( "path/filepath" "strings" - "git.sr.ht/~adnano/go-gemini" - "git.sr.ht/~adnano/go-gemini/certificate" "dev.mediocregopher.com/mediocre-blog.git/src/cache" "dev.mediocregopher.com/mediocre-blog.git/src/cfg" "dev.mediocregopher.com/mediocre-blog.git/src/post" "dev.mediocregopher.com/mediocre-blog.git/src/post/asset" "dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" + "git.sr.ht/~adnano/go-gemini" + "git.sr.ht/~adnano/go-gemini/certificate" ) // Params are used to instantiate a new API instance. All fields are required @@ -37,7 +37,8 @@ type Params struct { ListenAddr string CertificatesPath string - HTTPPublicURL *url.URL + HTTPPublicURL *url.URL + HTTPGeminiGatewayURL *url.URL } // SetupCfg implement the cfg.Cfger interface. @@ -193,12 +194,7 @@ func postsMiddleware(tplHandler gemini.Handler) gemini.Handler { return } - query := r.URL.Query() - query.Set("id", id) - r.URL.RawQuery = query.Encode() - - r.URL.Path = "/posts/post.gmi" - + ctx = withTplPath(ctx, "/posts/post.gmi") tplHandler.ServeGemini(ctx, rw, r) }) } diff --git a/src/gmi/tpl.go b/src/gmi/tpl.go index 03d9819..8ffa6bc 100644 --- a/src/gmi/tpl.go +++ b/src/gmi/tpl.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "embed" + "errors" "fmt" "io" "io/fs" @@ -11,126 +12,27 @@ import ( "net/url" "path" "path/filepath" - "strconv" "strings" "text/template" - "git.sr.ht/~adnano/go-gemini" "dev.mediocregopher.com/mediocre-blog.git/src/post" + "dev.mediocregopher.com/mediocre-blog.git/src/render" "dev.mediocregopher.com/mediocre-go-lib.git/mctx" - gmnhg "github.com/tdemin/gmnhg" + "git.sr.ht/~adnano/go-gemini" ) -//go:embed tpl -var tplFS embed.FS - -type rendererGetPostsRes struct { - Posts []post.StoredPost - HasMore bool -} - -type rendererGetPostSeriesNextPreviousRes struct { - Next *post.StoredPost - Previous *post.StoredPost -} - -type renderer struct { - url *url.URL - publicURL *url.URL - postStore post.Store - preprocessFuncs post.PreprocessFunctions -} - -func (r renderer) GetPosts(page, count int) (rendererGetPostsRes, error) { - posts, hasMore, err := r.postStore.Get(page, count) - return rendererGetPostsRes{posts, hasMore}, err -} - -func (r renderer) GetPostByID(id string) (post.StoredPost, error) { - p, err := r.postStore.GetByID(id) - if err != nil { - return post.StoredPost{}, fmt.Errorf("fetching post %q: %w", id, err) - } - return p, nil -} - -func (r renderer) GetPostSeriesNextPrevious(p post.StoredPost) (rendererGetPostSeriesNextPreviousRes, error) { - - seriesPosts, err := r.postStore.GetBySeries(p.Series) - if err != nil { - return rendererGetPostSeriesNextPreviousRes{}, fmt.Errorf( - "fetching posts for series %q: %w", p.Series, err, - ) - } - - var ( - res rendererGetPostSeriesNextPreviousRes - foundThis bool - ) - - for i := range seriesPosts { - - seriesPost := seriesPosts[i] - - if seriesPost.ID == p.ID { - foundThis = true - continue - } - - if !foundThis { - res.Next = &seriesPost - continue - } - - res.Previous = &seriesPost - break - } - - return res, nil -} - -func (r renderer) PostBody(p post.StoredPost) (string, error) { - - buf := new(bytes.Buffer) - - if err := p.PreprocessBody(buf, r.preprocessFuncs); err != nil { - return "", fmt.Errorf("preprocessing post body: %w", err) - } - - bodyBytes := buf.Bytes() - - if p.Format == post.FormatMarkdown { - - gemtextBodyBytes, err := gmnhg.RenderMarkdown(bodyBytes, 0) - if err != nil { - return "", fmt.Errorf("converting from markdown: %w", err) - } - - bodyBytes = gemtextBodyBytes - } - - return string(bodyBytes), nil -} +type ctxKey string -func (r renderer) GetQueryValue(key, def string) string { - v := r.url.Query().Get(key) - if v == "" { - v = def - } - return v -} - -func (r renderer) GetQueryIntValue(key string, def int) (int, error) { - vStr := r.GetQueryValue(key, strconv.Itoa(def)) - return strconv.Atoi(vStr) -} +const ( + ctxKeyTplPath ctxKey = "tplPath" +) -func (r renderer) GetPath() (string, error) { - basePath := filepath.Join("/", r.publicURL.Path) // in case it's empty - return filepath.Rel(basePath, r.url.Path) +func withTplPath(ctx context.Context, path string) context.Context { + return context.WithValue(ctx, ctxKeyTplPath, path) } -func (r renderer) Add(a, b int) int { return a + b } +//go:embed tpl +var tplFS embed.FS func (a *api) tplHandler() (gemini.Handler, error) { @@ -177,7 +79,6 @@ func (a *api) tplHandler() (gemini.Handler, error) { return blogURL(a.params.HTTPPublicURL, path, true) }, Image: func(args ...string) (string, error) { - var ( id = args[0] descr = "Image" @@ -243,9 +144,13 @@ func (a *api) tplHandler() (gemini.Handler, error) { rw gemini.ResponseWriter, r *gemini.Request, ) { + tplPath, _ := ctx.Value(ctxKeyTplPath).(string) + if tplPath == "" { + tplPath = r.URL.Path + } + tplPath = strings.TrimPrefix(tplPath, "/") - tplPath := strings.TrimPrefix(r.URL.Path, "/") - mimeType := mime.TypeByExtension(path.Ext(r.URL.Path)) + mimeType := mime.TypeByExtension(path.Ext(tplPath)) ctx = mctx.Annotate(ctx, "url", r.URL, @@ -266,14 +171,19 @@ func (a *api) tplHandler() (gemini.Handler, error) { buf := new(bytes.Buffer) - err := tpl.Execute(buf, renderer{ - url: r.URL, - publicURL: a.params.PublicURL, - postStore: a.params.PostStore, - preprocessFuncs: preprocessFuncs, - }) - - if err != nil { + err := tpl.Execute(buf, render.NewMethods( + ctx, + r.URL, + a.params.PublicURL, + a.params.HTTPGeminiGatewayURL, + a.params.PostStore, + preprocessFuncs, + )) + + if errors.Is(err, post.ErrPostNotFound) { + a.params.Logger.Warn(ctx, "post not found", err) + rw.WriteHeader(gemini.StatusNotFound, "Post not found") + } else if err != nil { a.params.Logger.Error(ctx, "rendering error", err) rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error()) return diff --git a/src/gmi/tpl/posts/post.gmi b/src/gmi/tpl/posts/post.gmi index 0234395..b568044 100644 --- a/src/gmi/tpl/posts/post.gmi +++ b/src/gmi/tpl/posts/post.gmi @@ -1,4 +1,4 @@ -{{ $post := .GetPostByID (.GetQueryValue "id" "") -}} +{{ $post := .GetThisPost -}} {{ if eq $post.Format "md" -}} This post has been translated from it's original markdown format, if it seems busted it might appear better over HTTP: @@ -14,7 +14,7 @@ This post has been translated from it's original markdown format, if it seems bu {{ end -}} -{{ .PostBody $post }} +{{ .PostGemtextBody $post }} ======================================== -- cgit v1.2.3