summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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