summaryrefslogtreecommitdiff
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
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.
-rw-r--r--flake.nix2
-rw-r--r--src/cmd/mediocre-blog/main.go1
-rw-r--r--src/gmi/gemtext/gemtext.go (renamed from src/gmi/gemtext.go)8
-rw-r--r--src/gmi/gemtext/gemtext_test.go (renamed from src/gmi/gemtext_test.go)6
-rw-r--r--src/gmi/gmi.go14
-rw-r--r--src/gmi/tpl.go150
-rw-r--r--src/gmi/tpl/posts/post.gmi4
-rw-r--r--src/http/assets.go2
-rw-r--r--src/http/drafts.go2
-rw-r--r--src/http/http.go2
-rw-r--r--src/http/index.go2
-rw-r--r--src/http/posts.go129
-rw-r--r--src/http/tpl.go84
-rw-r--r--src/http/tpl/post.html30
-rw-r--r--src/render/methods.go226
-rw-r--r--src/render/render.go2
16 files changed, 352 insertions, 312 deletions
diff --git a/flake.nix b/flake.nix
index cbc08a7..fe98f8d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -43,7 +43,7 @@
export MEDIOCRE_BLOG_HTTP_PUBLIC_URL="$MEDIOCRE_BLOG_ML_PUBLIC_URL"
export MEDIOCRE_BLOG_HTTP_LISTEN_PROTO="tcp"
export MEDIOCRE_BLOG_HTTP_LISTEN_ADDR=":4000"
- export MEDIOCRE_BLOG_HTTP_GEMINI_GATEWAY_URL="https://nightfall.city/x/"
+ export MEDIOCRE_BLOG_HTTP_GEMINI_GATEWAY_URL="https://gemini.tildeverse.org/?gemini://"
# http auth
# (password is "bar". This should definitely be changed for prod.)
diff --git a/src/cmd/mediocre-blog/main.go b/src/cmd/mediocre-blog/main.go
index 835b2d0..996b769 100644
--- a/src/cmd/mediocre-blog/main.go
+++ b/src/cmd/mediocre-blog/main.go
@@ -94,6 +94,7 @@ func main() {
gmiParams.PostStore = postStore
gmiParams.PostAssetLoader = postAssetLoader
gmiParams.HTTPPublicURL = httpParams.PublicURL
+ gmiParams.HTTPGeminiGatewayURL = httpParams.GeminiGatewayURL
logger.Info(ctx, "starting gmi api")
gmiAPI, err := gmi.New(gmiParams)
diff --git a/src/gmi/gemtext.go b/src/gmi/gemtext/gemtext.go
index 884635c..5c8f594 100644
--- a/src/gmi/gemtext.go
+++ b/src/gmi/gemtext/gemtext.go
@@ -1,4 +1,6 @@
-package gmi
+// Package gemtext contains code related to processing and producing gemtext
+// documents.
+package gemtext
import (
"bufio"
@@ -23,12 +25,12 @@ func hasImgExt(p string) bool {
// matches `=> dstURL [optional description]`
var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`)
-// GemtextToMarkdown reads a gemtext formatted body from the Reader and writes
+// 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 GemtextToMarkdown(dst io.Writer, src io.Reader, gmiGateway *url.URL) error {
+func ToMarkdown(dst io.Writer, src io.Reader, gmiGateway *url.URL) error {
bufSrc := bufio.NewReader(src)
diff --git a/src/gmi/gemtext_test.go b/src/gmi/gemtext/gemtext_test.go
index 75da9df..fe58a64 100644
--- a/src/gmi/gemtext_test.go
+++ b/src/gmi/gemtext/gemtext_test.go
@@ -1,4 +1,4 @@
-package gmi
+package gemtext
import (
"bytes"
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestGemtextToMarkdown(t *testing.T) {
+func TestToMarkdown(t *testing.T) {
gmiGateway, _ := url.Parse("https://gateway.com/x/")
@@ -58,7 +58,7 @@ func TestGemtextToMarkdown(t *testing.T) {
t.Run(strconv.Itoa(i), func(t *testing.T) {
got := new(bytes.Buffer)
- err := GemtextToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway)
+ err := ToMarkdown(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 }}
========================================
diff --git a/src/http/assets.go b/src/http/assets.go
index 79ab98c..5a47152 100644
--- a/src/http/assets.go
+++ b/src/http/assets.go
@@ -34,7 +34,7 @@ func (a *api) managePostAssetsHandler() http.Handler {
IDs: ids,
}
- executeTemplate(rw, r, tpl, tplPayload)
+ a.executeTemplate(rw, r, tpl, tplPayload)
})
}
diff --git a/src/http/drafts.go b/src/http/drafts.go
index f8e4c8a..b0550ce 100644
--- a/src/http/drafts.go
+++ b/src/http/drafts.go
@@ -48,7 +48,7 @@ func (a *api) manageDraftPostsHandler() http.Handler {
tplPayload.NextPage = page + 1
}
- executeTemplate(rw, r, tpl, tplPayload)
+ a.executeTemplate(rw, r, tpl, tplPayload)
})
}
diff --git a/src/http/http.go b/src/http/http.go
index fd0ea16..9bfed59 100644
--- a/src/http/http.go
+++ b/src/http/http.go
@@ -146,7 +146,7 @@ func New(params Params) (API, error) {
auther: NewAuther(params.AuthUsers, params.AuthRatelimit),
}
- a.redirectTpl = a.mustParseTpl("redirect.html")
+ a.redirectTpl = mustParseTpl(a.emptyTpl(), "redirect.html")
a.srv = &http.Server{Handler: a.handler()}
diff --git a/src/http/index.go b/src/http/index.go
index 21c6c16..48d0d33 100644
--- a/src/http/index.go
+++ b/src/http/index.go
@@ -31,6 +31,6 @@ func (a *api) renderIndexHandler() http.Handler {
return
}
- executeTemplate(rw, r, tpl, nil)
+ a.executeTemplate(rw, r, tpl, nil)
})
}
diff --git a/src/http/posts.go b/src/http/posts.go
index ab3a18a..939b811 100644
--- a/src/http/posts.go
+++ b/src/http/posts.go
@@ -12,13 +12,10 @@ import (
txttpl "text/template"
"time"
- "github.com/gomarkdown/markdown"
- "github.com/gomarkdown/markdown/html"
- "github.com/gomarkdown/markdown/parser"
- "dev.mediocregopher.com/mediocre-blog.git/src/gmi"
"dev.mediocregopher.com/mediocre-blog.git/src/http/apiutil"
"dev.mediocregopher.com/mediocre-blog.git/src/post"
"dev.mediocregopher.com/mediocre-blog.git/src/post/asset"
+ "dev.mediocregopher.com/mediocre-blog.git/src/render"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
)
@@ -92,80 +89,6 @@ func (a *api) postPreprocessFuncs() post.PreprocessFunctions {
}
}
-func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload, error) {
-
- preprocessFuncs := a.postPreprocessFuncs()
-
- bodyBuf := new(bytes.Buffer)
-
- if err := storedPost.PreprocessBody(bodyBuf, preprocessFuncs); err != nil {
- return postTplPayload{}, fmt.Errorf("preprocessing post body: %w", err)
- }
-
- if storedPost.Format == post.FormatGemtext {
-
- prevBodyBuf := bodyBuf
- bodyBuf = new(bytes.Buffer)
-
- err := gmi.GemtextToMarkdown(
- bodyBuf, prevBodyBuf, a.params.GeminiGatewayURL,
- )
-
- if err != nil {
- return postTplPayload{}, fmt.Errorf("converting gemtext to markdown: %w", err)
- }
- }
-
- // this helps the markdown renderer properly parse pages which end in a
- // `</script>` tag... I don't know why.
- _, _ = bodyBuf.WriteString("\n")
-
- parserExt := parser.CommonExtensions | parser.AutoHeadingIDs
- parser := parser.NewWithExtensions(parserExt)
-
- htmlFlags := html.HrefTargetBlank
- htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags})
-
- renderedBody := markdown.ToHTML(bodyBuf.Bytes(), parser, htmlRenderer)
-
- tplPayload := postTplPayload{
- StoredPost: storedPost,
- Body: template.HTML(renderedBody),
- }
-
- if series := storedPost.Series; series != "" {
-
- seriesPosts, err := a.params.PostStore.GetBySeries(series)
- if err != nil {
- return postTplPayload{}, fmt.Errorf(
- "fetching posts for series %q: %w", series, err,
- )
- }
-
- var foundThis bool
-
- for i := range seriesPosts {
-
- seriesPost := seriesPosts[i]
-
- if seriesPost.ID == storedPost.ID {
- foundThis = true
- continue
- }
-
- if !foundThis {
- tplPayload.SeriesNext = &seriesPost
- continue
- }
-
- tplPayload.SeriesPrevious = &seriesPost
- break
- }
- }
-
- return tplPayload, nil
-}
-
func (a *api) getPostsHandler() http.Handler {
tpl := a.mustParseBasedTpl("posts.html")
@@ -236,7 +159,7 @@ func (a *api) getPostsHandler() http.Handler {
tplPayload.NextPage = page + 1
}
- executeTemplate(rw, r, tpl, tplPayload)
+ a.executeTemplate(rw, r, tpl, tplPayload)
})
}
@@ -245,37 +168,7 @@ func (a *api) getPostHandler() http.Handler {
tpl := a.mustParseBasedTpl("post.html")
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
-
- id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html")
-
- storedPost, err := a.params.PostStore.GetByID(id)
-
- if errors.Is(err, post.ErrPostNotFound) {
- http.Error(rw, "Post not found", 404)
- return
- } else if err != nil {
- apiutil.InternalServerError(
- rw, r, fmt.Errorf("fetching post with id %q: %w", id, err),
- )
- return
- }
-
- tplPayload, err := a.postToPostTplPayload(storedPost)
-
- if err != nil {
- apiutil.InternalServerError(
- rw, r, fmt.Errorf(
- "generating template payload for post with id %q: %w",
- id, err,
- ),
- )
- return
- }
-
- executeTemplate(
- rw, r, tpl, tplPayload,
- executeTemplateWithTitlePrefix(storedPost.Title),
- )
+ a.executeTemplate(rw, r, tpl, nil)
})
}
@@ -319,7 +212,7 @@ func (a *api) managePostsHandler() http.Handler {
tplPayload.NextPage = page + 1
}
- executeTemplate(rw, r, tpl, tplPayload)
+ a.executeTemplate(rw, r, tpl, tplPayload)
})
}
@@ -378,7 +271,7 @@ func (a *api) editPostHandler(isDraft bool) http.Handler {
Formats: post.Formats,
}
- executeTemplate(rw, r, tpl, tplPayload)
+ a.executeTemplate(rw, r, tpl, tplPayload)
})
}
@@ -516,15 +409,7 @@ func (a *api) previewPostHandler() http.Handler {
PublishedAt: time.Now(),
}
- tplPayload, err := a.postToPostTplPayload(storedPost)
-
- if err != nil {
- apiutil.InternalServerError(
- rw, r, fmt.Errorf("generating template payload: %w", err),
- )
- return
- }
-
- executeTemplate(rw, r, tpl, tplPayload)
+ r = r.WithContext(render.WithPost(r.Context(), storedPost))
+ a.executeTemplate(rw, r, tpl, nil)
})
}
diff --git a/src/http/tpl.go b/src/http/tpl.go
index 24f2453..c623f2e 100644
--- a/src/http/tpl.go
+++ b/src/http/tpl.go
@@ -1,9 +1,12 @@
package http
import (
+ "bytes"
"embed"
+ "errors"
"fmt"
"html/template"
+ "io"
"io/fs"
"net/http"
"net/url"
@@ -12,6 +15,8 @@ import (
"time"
"dev.mediocregopher.com/mediocre-blog.git/src/http/apiutil"
+ "dev.mediocregopher.com/mediocre-blog.git/src/post"
+ "dev.mediocregopher.com/mediocre-blog.git/src/render"
)
//go:embed tpl
@@ -105,77 +110,84 @@ func (a *api) tplFuncs() template.FuncMap {
}
}
-func (a *api) parseTpl(name, tplBody string) (*template.Template, error) {
-
- tpl := template.New(name)
+func (a *api) emptyTpl() *template.Template {
+ tpl := template.New("")
tpl = tpl.Funcs(a.tplFuncs())
tpl = tpl.Funcs(template.FuncMap(a.postPreprocessFuncs().ToFuncMap()))
-
- var err error
-
- if tpl, err = tpl.Parse(tplBody); err != nil {
- return nil, err
- }
-
- return tpl, nil
+ return tpl
}
-func (a *api) mustParseTpl(name string) *template.Template {
- return template.Must(a.parseTpl(name, mustReadTplFile(name)))
+func mustParseTpl(tpl *template.Template, name string) *template.Template {
+ return template.Must(tpl.New(name).Parse(mustReadTplFile(name)))
}
func (a *api) mustParseBasedTpl(name string) *template.Template {
- tpl := a.mustParseTpl(name)
- tpl = template.Must(tpl.New("gemini-cta.html").Parse(mustReadTplFile("gemini-cta.html")))
- tpl = template.Must(tpl.New("base.html").Parse(mustReadTplFile("base.html")))
+ tpl := a.emptyTpl()
+ tpl = mustParseTpl(tpl, "gemini-cta.html")
+ tpl = mustParseTpl(tpl, "base.html")
+ tpl = mustParseTpl(tpl, name)
return tpl
}
type tplData struct {
+ *render.Methods
Payload interface{}
Title string
}
-func newTPLData(r *http.Request, payload interface{}) tplData {
+// WithTitlePrefix returns a copy of tplData but with the given string prefixed
+// to the page title. This is intended for use within templates, when nesting
+// the base template.
+func (d tplData) WithTitlePrefix(prefix string) tplData {
+ d.Title = prefix + " - " + d.Title
+ return d
+}
+
+func (a *api) newTPLData(r *http.Request, payload interface{}) tplData {
return tplData{
+ Methods: render.NewMethods(
+ r.Context(),
+ r.URL,
+ a.params.PublicURL,
+ a.params.GeminiGatewayURL,
+ a.params.PostStore,
+ a.postPreprocessFuncs(),
+ ),
Payload: payload,
Title: "mediocregopher's lil web corner",
}
}
-type executeTemplateOpt func(*tplData)
-
-func executeTemplateWithTitlePrefix(prefix string) executeTemplateOpt {
- return func(d *tplData) {
- d.Title = prefix + " - " + d.Title
- }
-}
-
// executeTemplate expects to be the final action in an http.Handler
-func executeTemplate(
- rw http.ResponseWriter, r *http.Request,
- tpl *template.Template, payload interface{},
- opts ...executeTemplateOpt,
+func (a *api) executeTemplate(
+ rw http.ResponseWriter,
+ r *http.Request,
+ tpl *template.Template,
+ payload interface{},
) {
- tplData := newTPLData(r, payload)
+ tplData := a.newTPLData(r, payload)
- for _, opt := range opts {
- opt(&tplData)
- }
+ buf := new(bytes.Buffer)
- if err := tpl.Execute(rw, tplData); err != nil {
+ err := tpl.Execute(buf, tplData)
+ if errors.Is(err, post.ErrPostNotFound) {
+ http.Error(rw, "Post not found", 404)
+ return
+ } else if err != nil {
apiutil.InternalServerError(
rw, r, fmt.Errorf("rendering template: %w", err),
)
return
}
+
+ io.Copy(rw, buf)
}
func (a *api) executeRedirectTpl(
rw http.ResponseWriter, r *http.Request, url string,
) {
- executeTemplate(rw, r, a.redirectTpl, struct {
+ a.executeTemplate(rw, r, a.redirectTpl, struct {
URL string
}{
URL: url,
@@ -188,7 +200,7 @@ func (a *api) renderDumbTplHandler(tplName string) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- tplData := newTPLData(r, nil)
+ tplData := a.newTPLData(r, nil)
if err := tpl.Execute(rw, tplData); err != nil {
apiutil.InternalServerError(
diff --git a/src/http/tpl/post.html b/src/http/tpl/post.html
index db69302..0cf3622 100644
--- a/src/http/tpl/post.html
+++ b/src/http/tpl/post.html
@@ -1,45 +1,51 @@
{{ define "body" }}
+{{ $post := .GetThisPost -}}
<h1 id="post-headline">
- {{ .Payload.Title }}
+ {{ $post.Title }}
</h1>
-{{ if ne .Payload.Description "" }}
+{{ if ne $post.Description "" }}
<p>
- <em>- {{ .Payload.Description }}</em>
+ <em>- {{ $post.Description }}</em>
</p>
{{ end }}
<hr/>
-{{ .Payload.Body }}
+{{ .PostHTMLBody $post }}
<p><em>
- Published {{ DateTimeFormat .Payload.PublishedAt }}
+ Published {{ DateTimeFormat $post.PublishedAt }}
</em></p>
-{{ if (or .Payload.SeriesPrevious .Payload.SeriesNext) }}
+{{- if $post.Series }}
+{{ $seriesNextPrev := .GetPostSeriesNextPrevious $post -}}
+{{ if or $seriesNextPrev.Next $seriesNextPrev.Previous }}
<hr/>
<p><em>
This post is part of a series.<br/>
- {{ if .Payload.SeriesPrevious }}
- Previously: <a href="{{ PostURL .Payload.SeriesPrevious.ID }}">{{ .Payload.SeriesPrevious.Title }}</a>
+ {{ if $seriesNextPrev.Next }}
+ Next: <a href="{{ PostURL $seriesNextPrev.Next.ID }}">{{ $seriesNextPrev.Next.Title }}</a>
{{ end }}
- {{ if (and .Payload.SeriesNext .Payload.SeriesPrevious) }}
+ {{ if and $seriesNextPrev.Next $seriesNextPrev.Previous }}
</br>
{{ end }}
- {{ if .Payload.SeriesNext }}
- Next: <a href="{{ PostURL .Payload.SeriesNext.ID }}">{{ .Payload.SeriesNext.Title }}</a></br>
+ {{ if $seriesNextPrev.Previous }}
+ Previously: <a href="{{ PostURL $seriesNextPrev.Previous.ID }}">{{ $seriesNextPrev.Previous.Title }}</a>
+ <br/>
{{ end }}
</em></p>
{{ end }}
+{{ end }}
{{ template "gemini-cta.html" . }}
{{ end }}
-{{ template "base.html" . }}
+{{ $post := .GetThisPost -}}
+{{ template "base.html" (.WithTitlePrefix $post.Title) }}
diff --git a/src/render/methods.go b/src/render/methods.go
new file mode 100644
index 0000000..6dd9332
--- /dev/null
+++ b/src/render/methods.go
@@ -0,0 +1,226 @@
+package render
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "html/template"
+ "net/url"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "dev.mediocregopher.com/mediocre-blog.git/src/gmi/gemtext"
+ "dev.mediocregopher.com/mediocre-blog.git/src/post"
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+ gmnhg "github.com/tdemin/gmnhg"
+)
+
+type ctxKey string
+
+const (
+ ctxKeyPost ctxKey = "post"
+)
+
+// WithPost sets the post on a Context, such that it can be retrieved using
+// the GetThisPost method.
+func WithPost(ctx context.Context, p post.StoredPost) context.Context {
+ return context.WithValue(ctx, ctxKeyPost, p)
+}
+
+// GetPostsRes are the fields returned from the GetPosts method.
+type GetPostsRes struct {
+ Posts []post.StoredPost
+ HasMore bool
+}
+
+// GetPostSeriesNextPreviousRes are the fields returned from the
+// GetPostSeriesNextPreviousRes method.
+type GetPostSeriesNextPreviousRes struct {
+ Next *post.StoredPost
+ Previous *post.StoredPost
+}
+
+// Methods carries methods which are designed to be accessible from a template.
+type Methods struct {
+ ctx context.Context
+ url *url.URL
+ publicURL *url.URL
+ geminiGatewayURL *url.URL
+ postStore post.Store
+ preprocessFuncs post.PreprocessFunctions
+
+ thisPost *post.StoredPost // cache
+}
+
+// NewMethods initializes a Methods using its required dependencies.
+func NewMethods(
+ ctx context.Context,
+ url *url.URL,
+ publicURL *url.URL,
+ geminiGatewayURL *url.URL,
+ postStore post.Store,
+ preprocessFuncs post.PreprocessFunctions,
+) *Methods {
+ return &Methods{
+ ctx,
+ url,
+ publicURL,
+ geminiGatewayURL,
+ postStore,
+ preprocessFuncs,
+ nil, // thisPost
+ }
+}
+
+func (m *Methods) GetPosts(page, count int) (GetPostsRes, error) {
+ posts, hasMore, err := m.postStore.Get(page, count)
+ return GetPostsRes{posts, hasMore}, err
+}
+
+func (m *Methods) GetThisPost() (p post.StoredPost, err error) {
+ if m.thisPost != nil {
+ return *m.thisPost, nil
+ }
+
+ defer func() {
+ m.thisPost = &p
+ }()
+
+ if p, ok := m.ctx.Value(ctxKeyPost).(post.StoredPost); ok {
+ return p, nil
+ }
+
+ id := path.Base(m.url.Path)
+ id = strings.TrimSuffix(id, path.Ext(id))
+
+ return m.postStore.GetByID(id)
+}
+
+func (m *Methods) GetPostByID(id string) (post.StoredPost, error) {
+ p, err := m.postStore.GetByID(id)
+ if err != nil {
+ return post.StoredPost{}, fmt.Errorf("fetching post %q: %w", id, err)
+ }
+ return p, nil
+}
+
+func (m *Methods) GetPostSeriesNextPrevious(
+ p post.StoredPost,
+) (
+ GetPostSeriesNextPreviousRes, error,
+) {
+
+ seriesPosts, err := m.postStore.GetBySeries(p.Series)
+ if err != nil {
+ return GetPostSeriesNextPreviousRes{}, fmt.Errorf(
+ "fetching posts for series %q: %w", p.Series, err,
+ )
+ }
+
+ var (
+ res GetPostSeriesNextPreviousRes
+ 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 (m *Methods) PostGemtextBody(p post.StoredPost) (string, error) {
+
+ buf := new(bytes.Buffer)
+
+ if err := p.PreprocessBody(buf, m.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
+}
+
+func (m *Methods) PostHTMLBody(p post.StoredPost) (template.HTML, error) {
+ bodyBuf := new(bytes.Buffer)
+
+ if err := p.PreprocessBody(bodyBuf, m.preprocessFuncs); err != nil {
+ return "", fmt.Errorf("preprocessing post body: %w", err)
+ }
+
+ if p.Format == post.FormatGemtext {
+
+ prevBodyBuf := bodyBuf
+ bodyBuf = new(bytes.Buffer)
+
+ err := gemtext.ToMarkdown(
+ bodyBuf, prevBodyBuf, m.geminiGatewayURL,
+ )
+
+ if err != nil {
+ return "", fmt.Errorf("converting gemtext to markdown: %w", err)
+ }
+ }
+
+ // this helps the markdown renderer properly parse pages which end in a
+ // `</script>` tag... I don't know why.
+ _, _ = bodyBuf.WriteString("\n")
+
+ parserExt := parser.CommonExtensions | parser.AutoHeadingIDs
+ parser := parser.NewWithExtensions(parserExt)
+
+ htmlFlags := html.HrefTargetBlank
+ htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags})
+
+ renderedBody := markdown.ToHTML(bodyBuf.Bytes(), parser, htmlRenderer)
+ return template.HTML(renderedBody), nil
+}
+
+func (m *Methods) GetQueryValue(key, def string) string {
+ v := m.url.Query().Get(key)
+ if v == "" {
+ v = def
+ }
+ return v
+}
+
+func (m *Methods) GetQueryIntValue(key string, def int) (int, error) {
+ vStr := m.GetQueryValue(key, strconv.Itoa(def))
+ return strconv.Atoi(vStr)
+}
+
+func (m *Methods) GetPath() (string, error) {
+ basePath := filepath.Join("/", m.publicURL.Path) // in case it's empty
+ return filepath.Rel(basePath, m.url.Path)
+}
+
+func (m *Methods) Add(a, b int) int { return a + b }
diff --git a/src/render/render.go b/src/render/render.go
new file mode 100644
index 0000000..28e60df
--- /dev/null
+++ b/src/render/render.go
@@ -0,0 +1,2 @@
+// Package render implements shared utilities used when rendering templates.
+package render