From 4c04177c05355ddb92d3d31a4c5cfbaa86555a13 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 14 May 2022 16:14:11 -0600 Subject: Move template rendering logic into api package --- srv/src/api/api.go | 12 +++- srv/src/api/apiutil/apiutil.go | 112 ++++++++++++++++++++++++++++++++++++++ srv/src/api/apiutils/apiutils.go | 112 -------------------------------------- srv/src/api/chat.go | 36 ++++++------ srv/src/api/csrf.go | 14 ++--- srv/src/api/mailinglist.go | 24 ++++---- srv/src/api/middleware.go | 6 +- srv/src/api/posts.go | 65 ++++++++++++++++++---- srv/src/api/pow.go | 10 ++-- srv/src/api/tpl/base.html | 65 ++++++++++++++++++++++ srv/src/api/tpl/post.html | 48 ++++++++++++++++ srv/src/cmd/mediocre-blog/main.go | 1 - srv/src/post/renderer.go | 96 -------------------------------- srv/src/post/renderer_test.go | 92 ------------------------------- srv/src/tpl/html/base.html | 65 ---------------------- srv/src/tpl/html/post.html | 48 ---------------- srv/src/tpl/tpl.go | 12 ---- 17 files changed, 334 insertions(+), 484 deletions(-) create mode 100644 srv/src/api/apiutil/apiutil.go delete mode 100644 srv/src/api/apiutils/apiutils.go create mode 100644 srv/src/api/tpl/base.html create mode 100644 srv/src/api/tpl/post.html delete mode 100644 srv/src/post/renderer.go delete mode 100644 srv/src/post/renderer_test.go delete mode 100644 srv/src/tpl/html/base.html delete mode 100644 srv/src/tpl/html/post.html delete mode 100644 srv/src/tpl/tpl.go diff --git a/srv/src/api/api.go b/srv/src/api/api.go index 75147d5..92771a1 100644 --- a/srv/src/api/api.go +++ b/srv/src/api/api.go @@ -3,8 +3,10 @@ package api import ( "context" + "embed" "errors" "fmt" + "html/template" "net" "net/http" "net/http/httputil" @@ -20,14 +22,18 @@ import ( "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) +//go:embed tpl +var fs embed.FS + +var tpls = template.Must(template.ParseFS(fs, "tpl/*")) + // Params are used to instantiate a new API instance. All fields are required // unless otherwise noted. type Params struct { Logger *mlog.Logger PowManager pow.Manager - PostStore post.Store - PostHTTPRenderer post.Renderer + PostStore post.Store MailingList mailinglist.MailingList @@ -190,7 +196,7 @@ func (a *api) handler() http.Handler { mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) - mux.Handle("/posts/", a.postHandler()) + mux.Handle("/v2/posts/", a.postHandler()) return mux } diff --git a/srv/src/api/apiutil/apiutil.go b/srv/src/api/apiutil/apiutil.go new file mode 100644 index 0000000..c9f8795 --- /dev/null +++ b/srv/src/api/apiutil/apiutil.go @@ -0,0 +1,112 @@ +// Package apiutil contains utilities which are useful for implementing api +// endpoints. +package apiutil + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/mediocregopher/mediocre-go-lib/v2/mlog" +) + +type loggerCtxKey int + +// SetRequestLogger sets the given Logger onto the given Request's Context, +// returning a copy. +func SetRequestLogger(r *http.Request, logger *mlog.Logger) *http.Request { + ctx := r.Context() + ctx = context.WithValue(ctx, loggerCtxKey(0), logger) + return r.WithContext(ctx) +} + +// GetRequestLogger returns the Logger which was set by SetRequestLogger onto +// this Request, or nil. +func GetRequestLogger(r *http.Request) *mlog.Logger { + ctx := r.Context() + logger, _ := ctx.Value(loggerCtxKey(0)).(*mlog.Logger) + if logger == nil { + logger = mlog.Null + } + return logger +} + +// JSONResult writes the JSON encoding of the given value as the response body. +func JSONResult(rw http.ResponseWriter, r *http.Request, v interface{}) { + b, err := json.Marshal(v) + if err != nil { + InternalServerError(rw, r, err) + return + } + b = append(b, '\n') + + rw.Header().Set("Content-Type", "application/json") + rw.Write(b) +} + +// BadRequest writes a 400 status and a JSON encoded error struct containing the +// given error as the response body. +func BadRequest(rw http.ResponseWriter, r *http.Request, err error) { + GetRequestLogger(r).Warn(r.Context(), "bad request", err) + + rw.WriteHeader(400) + JSONResult(rw, r, struct { + Error string `json:"error"` + }{ + Error: err.Error(), + }) +} + +// InternalServerError writes a 500 status and a JSON encoded error struct +// containing a generic error as the response body (though it will log the given +// one). +func InternalServerError(rw http.ResponseWriter, r *http.Request, err error) { + GetRequestLogger(r).Error(r.Context(), "internal server error", err) + + rw.WriteHeader(500) + JSONResult(rw, r, struct { + Error string `json:"error"` + }{ + Error: "internal server error", + }) +} + +// StrToInt parses the given string as an integer, or returns the given default +// integer if the string is empty. +func StrToInt(str string, defaultVal int) (int, error) { + if str == "" { + return defaultVal, nil + } + return strconv.Atoi(str) +} + +// GetCookie returns the namd cookie's value, or the given default value if the +// cookie is not set. +// +// This will only return an error if there was an unexpected error parsing the +// Request's cookies. +func GetCookie(r *http.Request, cookieName, defaultVal string) (string, error) { + c, err := r.Cookie(cookieName) + if errors.Is(err, http.ErrNoCookie) { + return defaultVal, nil + } else if err != nil { + return "", fmt.Errorf("reading cookie %q: %w", cookieName, err) + } + + return c.Value, nil +} + +// RandStr returns a human-readable random string with the given number of bytes +// of randomness. +func RandStr(numBytes int) string { + b := make([]byte, numBytes) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return hex.EncodeToString(b) +} diff --git a/srv/src/api/apiutils/apiutils.go b/srv/src/api/apiutils/apiutils.go deleted file mode 100644 index 223c2b9..0000000 --- a/srv/src/api/apiutils/apiutils.go +++ /dev/null @@ -1,112 +0,0 @@ -// Package apiutils contains utilities which are useful for implementing api -// endpoints. -package apiutils - -import ( - "context" - "crypto/rand" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/mediocregopher/mediocre-go-lib/v2/mlog" -) - -type loggerCtxKey int - -// SetRequestLogger sets the given Logger onto the given Request's Context, -// returning a copy. -func SetRequestLogger(r *http.Request, logger *mlog.Logger) *http.Request { - ctx := r.Context() - ctx = context.WithValue(ctx, loggerCtxKey(0), logger) - return r.WithContext(ctx) -} - -// GetRequestLogger returns the Logger which was set by SetRequestLogger onto -// this Request, or nil. -func GetRequestLogger(r *http.Request) *mlog.Logger { - ctx := r.Context() - logger, _ := ctx.Value(loggerCtxKey(0)).(*mlog.Logger) - if logger == nil { - logger = mlog.Null - } - return logger -} - -// JSONResult writes the JSON encoding of the given value as the response body. -func JSONResult(rw http.ResponseWriter, r *http.Request, v interface{}) { - b, err := json.Marshal(v) - if err != nil { - InternalServerError(rw, r, err) - return - } - b = append(b, '\n') - - rw.Header().Set("Content-Type", "application/json") - rw.Write(b) -} - -// BadRequest writes a 400 status and a JSON encoded error struct containing the -// given error as the response body. -func BadRequest(rw http.ResponseWriter, r *http.Request, err error) { - GetRequestLogger(r).Warn(r.Context(), "bad request", err) - - rw.WriteHeader(400) - JSONResult(rw, r, struct { - Error string `json:"error"` - }{ - Error: err.Error(), - }) -} - -// InternalServerError writes a 500 status and a JSON encoded error struct -// containing a generic error as the response body (though it will log the given -// one). -func InternalServerError(rw http.ResponseWriter, r *http.Request, err error) { - GetRequestLogger(r).Error(r.Context(), "internal server error", err) - - rw.WriteHeader(500) - JSONResult(rw, r, struct { - Error string `json:"error"` - }{ - Error: "internal server error", - }) -} - -// StrToInt parses the given string as an integer, or returns the given default -// integer if the string is empty. -func StrToInt(str string, defaultVal int) (int, error) { - if str == "" { - return defaultVal, nil - } - return strconv.Atoi(str) -} - -// GetCookie returns the namd cookie's value, or the given default value if the -// cookie is not set. -// -// This will only return an error if there was an unexpected error parsing the -// Request's cookies. -func GetCookie(r *http.Request, cookieName, defaultVal string) (string, error) { - c, err := r.Cookie(cookieName) - if errors.Is(err, http.ErrNoCookie) { - return defaultVal, nil - } else if err != nil { - return "", fmt.Errorf("reading cookie %q: %w", cookieName, err) - } - - return c.Value, nil -} - -// RandStr returns a human-readable random string with the given number of bytes -// of randomness. -func RandStr(numBytes int) string { - b := make([]byte, numBytes) - if _, err := rand.Read(b); err != nil { - panic(err) - } - return hex.EncodeToString(b) -} diff --git a/srv/src/api/chat.go b/srv/src/api/chat.go index a1acc5a..f4b90ef 100644 --- a/srv/src/api/chat.go +++ b/srv/src/api/chat.go @@ -9,7 +9,7 @@ import ( "unicode" "github.com/gorilla/websocket" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/chat" ) @@ -44,9 +44,9 @@ func newChatHandler( func (c *chatHandler) historyHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - limit, err := apiutils.StrToInt(r.PostFormValue("limit"), 0) + limit, err := apiutil.StrToInt(r.PostFormValue("limit"), 0) if err != nil { - apiutils.BadRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err)) + apiutil.BadRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err)) return } @@ -58,13 +58,13 @@ func (c *chatHandler) historyHandler() http.Handler { }) if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) { - apiutils.BadRequest(rw, r, argErr.Err) + apiutil.BadRequest(rw, r, argErr.Err) return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) } - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { Cursor string `json:"cursor"` Messages []chat.Message `json:"messages"` }{ @@ -107,11 +107,11 @@ func (c *chatHandler) userIDHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { userID, err := c.userID(r) if err != nil { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { UserID chat.UserID `json:"userID"` }{ UserID: userID, @@ -123,18 +123,18 @@ func (c *chatHandler) appendHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { userID, err := c.userID(r) if err != nil { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } body := r.PostFormValue("body") if l := len(body); l == 0 { - apiutils.BadRequest(rw, r, errors.New("body is required")) + apiutil.BadRequest(rw, r, errors.New("body is required")) return } else if l > 300 { - apiutils.BadRequest(rw, r, errors.New("body too long")) + apiutil.BadRequest(rw, r, errors.New("body too long")) return } @@ -144,11 +144,11 @@ func (c *chatHandler) appendHandler() http.Handler { }) if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { MessageID string `json:"messageID"` }{ MessageID: msg.ID, @@ -164,7 +164,7 @@ func (c *chatHandler) listenHandler() http.Handler { conn, err := c.wsUpgrader.Upgrade(rw, r, nil) if err != nil { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } defer conn.Close() @@ -172,14 +172,14 @@ func (c *chatHandler) listenHandler() http.Handler { it, err := c.room.Listen(ctx, sinceID) if errors.As(err, new(chat.ErrInvalidArg)) { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } else if errors.Is(err, context.Canceled) { return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } @@ -192,7 +192,7 @@ func (c *chatHandler) listenHandler() http.Handler { return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } @@ -203,7 +203,7 @@ func (c *chatHandler) listenHandler() http.Handler { }) if err != nil { - apiutils.GetRequestLogger(r).Error(ctx, "couldn't write message", err) + apiutil.GetRequestLogger(r).Error(ctx, "couldn't write message", err) return } } diff --git a/srv/src/api/csrf.go b/srv/src/api/csrf.go index 13b6ec6..9717030 100644 --- a/srv/src/api/csrf.go +++ b/srv/src/api/csrf.go @@ -4,7 +4,7 @@ import ( "errors" "net/http" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" ) const ( @@ -15,16 +15,16 @@ const ( func setCSRFMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - csrfTok, err := apiutils.GetCookie(r, csrfTokenCookieName, "") + csrfTok, err := apiutil.GetCookie(r, csrfTokenCookieName, "") if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } else if csrfTok == "" { http.SetCookie(rw, &http.Cookie{ Name: csrfTokenCookieName, - Value: apiutils.RandStr(32), + Value: apiutil.RandStr(32), Secure: true, }) } @@ -36,10 +36,10 @@ func setCSRFMiddleware(h http.Handler) http.Handler { func checkCSRFMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - csrfTok, err := apiutils.GetCookie(r, csrfTokenCookieName, "") + csrfTok, err := apiutil.GetCookie(r, csrfTokenCookieName, "") if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } @@ -49,7 +49,7 @@ func checkCSRFMiddleware(h http.Handler) http.Handler { } if csrfTok == "" || givenCSRFTok != csrfTok { - apiutils.BadRequest(rw, r, errors.New("invalid CSRF token")) + apiutil.BadRequest(rw, r, errors.New("invalid CSRF token")) return } diff --git a/srv/src/api/mailinglist.go b/srv/src/api/mailinglist.go index d89fe2a..c12e75d 100644 --- a/srv/src/api/mailinglist.go +++ b/srv/src/api/mailinglist.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" ) @@ -16,7 +16,7 @@ func (a *api) mailingListSubscribeHandler() http.Handler { parts[0] == "" || parts[1] == "" || len(email) >= 512 { - apiutils.BadRequest(rw, r, errors.New("invalid email")) + apiutil.BadRequest(rw, r, errors.New("invalid email")) return } @@ -26,11 +26,11 @@ func (a *api) mailingListSubscribeHandler() http.Handler { // just eat the error, make it look to the user like the // verification email was sent. } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct{}{}) + apiutil.JSONResult(rw, r, struct{}{}) }) } @@ -40,25 +40,25 @@ func (a *api) mailingListFinalizeHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { subToken := r.PostFormValue("subToken") if l := len(subToken); l == 0 || l > 128 { - apiutils.BadRequest(rw, r, errInvalidSubToken) + apiutil.BadRequest(rw, r, errInvalidSubToken) return } err := a.params.MailingList.FinalizeSubscription(subToken) if errors.Is(err, mailinglist.ErrNotFound) { - apiutils.BadRequest(rw, r, errInvalidSubToken) + apiutil.BadRequest(rw, r, errInvalidSubToken) return } else if errors.Is(err, mailinglist.ErrAlreadyVerified) { // no problem } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct{}{}) + apiutil.JSONResult(rw, r, struct{}{}) }) } @@ -68,21 +68,21 @@ func (a *api) mailingListUnsubscribeHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { unsubToken := r.PostFormValue("unsubToken") if l := len(unsubToken); l == 0 || l > 128 { - apiutils.BadRequest(rw, r, errInvalidUnsubToken) + apiutil.BadRequest(rw, r, errInvalidUnsubToken) return } err := a.params.MailingList.Unsubscribe(unsubToken) if errors.Is(err, mailinglist.ErrNotFound) { - apiutils.BadRequest(rw, r, errInvalidUnsubToken) + apiutil.BadRequest(rw, r, errInvalidUnsubToken) return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct{}{}) + apiutil.JSONResult(rw, r, struct{}{}) }) } diff --git a/srv/src/api/middleware.go b/srv/src/api/middleware.go index 6ea0d13..0b3eec7 100644 --- a/srv/src/api/middleware.go +++ b/srv/src/api/middleware.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) @@ -61,7 +61,7 @@ func (lrw *logResponseWriter) WriteHeader(statusCode int) { func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - r = apiutils.SetRequestLogger(r, logger) + r = apiutil.SetRequestLogger(r, logger) lrw := newLogResponseWriter(rw) @@ -90,7 +90,7 @@ func postOnlyMiddleware(h http.Handler) http.Handler { return } - apiutils.GetRequestLogger(r).WarnString(r.Context(), "method not allowed") + apiutil.GetRequestLogger(r).WarnString(r.Context(), "method not allowed") rw.WriteHeader(405) }) } diff --git a/srv/src/api/posts.go b/srv/src/api/posts.go index 995f2fb..cc7a176 100644 --- a/srv/src/api/posts.go +++ b/srv/src/api/posts.go @@ -3,11 +3,15 @@ package api import ( "errors" "fmt" + "html/template" "net/http" "path/filepath" "strings" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/post" ) @@ -22,22 +26,63 @@ func (a *api) postHandler() http.Handler { http.Error(rw, "Post not found", 404) return } else if err != nil { - apiutils.InternalServerError( + apiutil.InternalServerError( rw, r, fmt.Errorf("fetching post with id %q: %w", id, err), ) return } - renderablePost, err := post.NewRenderablePost(a.params.PostStore, storedPost) - if err != nil { - apiutils.InternalServerError( - rw, r, fmt.Errorf("constructing renderable post with id %q: %w", id, err), - ) - return + parserExt := parser.CommonExtensions | parser.AutoHeadingIDs + parser := parser.NewWithExtensions(parserExt) + + htmlFlags := html.CommonFlags | html.HrefTargetBlank + htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags}) + + renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer) + + tplData := struct { + post.StoredPost + SeriesPrevious, SeriesNext *post.StoredPost + Body template.HTML + }{ + StoredPost: storedPost, + Body: template.HTML(renderedBody), + } + + if series := storedPost.Series; series != "" { + + seriesPosts, err := a.params.PostStore.GetBySeries(series) + if err != nil { + apiutil.InternalServerError( + rw, r, + fmt.Errorf("fetching posts for series %q: %w", series, err), + ) + return + } + + var foundThis bool + + for i := range seriesPosts { + + seriesPost := seriesPosts[i] + + if seriesPost.ID == storedPost.ID { + foundThis = true + continue + } + + if !foundThis { + tplData.SeriesPrevious = &seriesPost + continue + } + + tplData.SeriesNext = &seriesPost + break + } } - if err := a.params.PostHTTPRenderer.Render(rw, renderablePost); err != nil { - apiutils.InternalServerError( + if err := tpls.ExecuteTemplate(rw, "post.html", tplData); err != nil { + apiutil.InternalServerError( rw, r, fmt.Errorf("rendering post with id %q: %w", id, err), ) return diff --git a/srv/src/api/pow.go b/srv/src/api/pow.go index 1b232b1..ae2d2f1 100644 --- a/srv/src/api/pow.go +++ b/srv/src/api/pow.go @@ -6,7 +6,7 @@ import ( "fmt" "net/http" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" ) func (a *api) newPowChallengeHandler() http.Handler { @@ -14,7 +14,7 @@ func (a *api) newPowChallengeHandler() http.Handler { challenge := a.params.PowManager.NewChallenge() - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { Seed string `json:"seed"` Target uint32 `json:"target"` }{ @@ -30,21 +30,21 @@ func (a *api) requirePowMiddleware(h http.Handler) http.Handler { seedHex := r.FormValue("powSeed") seed, err := hex.DecodeString(seedHex) if err != nil || len(seed) == 0 { - apiutils.BadRequest(rw, r, errors.New("invalid powSeed")) + apiutil.BadRequest(rw, r, errors.New("invalid powSeed")) return } solutionHex := r.FormValue("powSolution") solution, err := hex.DecodeString(solutionHex) if err != nil || len(seed) == 0 { - apiutils.BadRequest(rw, r, errors.New("invalid powSolution")) + apiutil.BadRequest(rw, r, errors.New("invalid powSolution")) return } err = a.params.PowManager.CheckSolution(seed, solution) if err != nil { - apiutils.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) + apiutil.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) return } diff --git a/srv/src/api/tpl/base.html b/srv/src/api/tpl/base.html new file mode 100644 index 0000000..bf81032 --- /dev/null +++ b/srv/src/api/tpl/base.html @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + +
+ + + + {{ template "body" . }} + +
+

+ Unless otherwised specified, all works are licensed under the + WTFPL. +

+
+ +
+ + + + + diff --git a/srv/src/api/tpl/post.html b/srv/src/api/tpl/post.html new file mode 100644 index 0000000..22a5b97 --- /dev/null +++ b/srv/src/api/tpl/post.html @@ -0,0 +1,48 @@ +{{ define "body" }} + +
+

+ {{ .Title }} +

+
+ {{ .PublishedAt.Format "2006-01-02" }} +  •  + {{ if not .LastUpdatedAt.IsZero }} + (Updated {{ .LastUpdatedAt.Format "2006-01-02" }}) +  •  + {{ end }} + {{ .Description }} +
+
+ +{{ if (or .SeriesPrevious .SeriesNext) }} +

+ This post is part of a series:
+ {{ if .SeriesPrevious }} + Previously: {{ .SeriesPrevious.Title }}
+ {{ end }} + {{ if .SeriesNext }} + Next: {{ .SeriesNext.Title }}
+ {{ end }} +

+{{ end }} + +
+ {{ .Body }} +
+ +{{ if (or .SeriesPrevious .SeriesNext) }} +

+ If you liked this post, consider checking out other posts in the series:
+ {{ if .SeriesPrevious }} + Previously: {{ .SeriesPrevious.Title }}
+ {{ end }} + {{ if .SeriesNext }} + Next: {{ .SeriesNext.Title }}
+ {{ end }} +

+{{ end }} + +{{ end }} + +{{ template "base.html" . }} diff --git a/srv/src/cmd/mediocre-blog/main.go b/srv/src/cmd/mediocre-blog/main.go index 28571a3..bdcd1b9 100644 --- a/srv/src/cmd/mediocre-blog/main.go +++ b/srv/src/cmd/mediocre-blog/main.go @@ -124,7 +124,6 @@ func main() { apiParams.Logger = logger.WithNamespace("api") apiParams.PowManager = powMgr apiParams.PostStore = postStore - apiParams.PostHTTPRenderer = post.NewMarkdownToHTMLRenderer() apiParams.MailingList = ml apiParams.GlobalRoom = chatGlobalRoom apiParams.UserIDCalculator = chatUserIDCalc diff --git a/srv/src/post/renderer.go b/srv/src/post/renderer.go deleted file mode 100644 index 74acc25..0000000 --- a/srv/src/post/renderer.go +++ /dev/null @@ -1,96 +0,0 @@ -package post - -import ( - _ "embed" - "fmt" - "html/template" - "io" - - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" - "github.com/mediocregopher/blog.mediocregopher.com/srv/tpl" -) - -// RenderablePost is a Post wrapped with extra information necessary for -// rendering. -type RenderablePost struct { - StoredPost - SeriesPrevious, SeriesNext *StoredPost -} - -// NewRenderablePost wraps an existing Post such that it can be rendered. -func NewRenderablePost(store Store, post StoredPost) (RenderablePost, error) { - - renderablePost := RenderablePost{ - StoredPost: post, - } - - if post.Series != "" { - - seriesPosts, err := store.GetBySeries(post.Series) - if err != nil { - return RenderablePost{}, fmt.Errorf( - "fetching posts for series %q: %w", - post.Series, err, - ) - } - - var foundThis bool - - for i := range seriesPosts { - - seriesPost := seriesPosts[i] - - if seriesPost.ID == post.ID { - foundThis = true - continue - } - - if !foundThis { - renderablePost.SeriesPrevious = &seriesPost - continue - } - - renderablePost.SeriesNext = &seriesPost - break - } - } - - return renderablePost, nil -} - -// Renderer takes a Post and renders it to some encoding. -type Renderer interface { - Render(io.Writer, RenderablePost) error -} - -func mdBodyToHTML(body []byte) []byte { - parserExt := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(parserExt) - - htmlFlags := html.CommonFlags | html.HrefTargetBlank - htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags}) - - return markdown.ToHTML(body, parser, htmlRenderer) -} - -type mdHTMLRenderer struct{} - -// NewMarkdownToHTMLRenderer renders Posts from markdown to HTML. -func NewMarkdownToHTMLRenderer() Renderer { - return mdHTMLRenderer{} -} - -func (r mdHTMLRenderer) Render(into io.Writer, post RenderablePost) error { - - data := struct { - RenderablePost - Body template.HTML - }{ - RenderablePost: post, - Body: template.HTML(mdBodyToHTML([]byte(post.Body))), - } - - return tpl.HTML.ExecuteTemplate(into, "post.html", data) -} diff --git a/srv/src/post/renderer_test.go b/srv/src/post/renderer_test.go deleted file mode 100644 index 5c01cd2..0000000 --- a/srv/src/post/renderer_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package post - -import ( - "bytes" - "strconv" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestMarkdownBodyToHTML(t *testing.T) { - - tests := []struct { - body string - exp string - }{ - { - body: ` -# Foo -`, - exp: `

Foo

`, - }, - { - body: ` -this is a body - -this is another -`, - exp: ` -

this is a body

- -

this is another

`, - }, - { - body: `this is a [link](somewhere.html)`, - exp: `

this is a link

`, - }, - } - - for i, test := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - - outB := mdBodyToHTML([]byte(test.body)) - out := string(outB) - - // just to make the tests nicer - out = strings.TrimSpace(out) - test.exp = strings.TrimSpace(test.exp) - - assert.Equal(t, test.exp, out) - }) - } -} - -func TestMarkdownToHTMLRenderer(t *testing.T) { - - r := NewMarkdownToHTMLRenderer() - - post := RenderablePost{ - StoredPost: StoredPost{ - Post: Post{ - ID: "foo", - Title: "Foo", - Description: "Bar.", - Body: "This is the body.", - Series: "baz", - }, - PublishedAt: time.Now(), - }, - - SeriesPrevious: &StoredPost{ - Post: Post{ - ID: "foo-prev", - Title: "Foo Prev", - }, - }, - - SeriesNext: &StoredPost{ - Post: Post{ - ID: "foo-next", - Title: "Foo Next", - }, - }, - } - - buf := new(bytes.Buffer) - err := r.Render(buf, post) - assert.NoError(t, err) - t.Log(buf.String()) -} diff --git a/srv/src/tpl/html/base.html b/srv/src/tpl/html/base.html deleted file mode 100644 index bf81032..0000000 --- a/srv/src/tpl/html/base.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - -
- - - - {{ template "body" . }} - -
-

- Unless otherwised specified, all works are licensed under the - WTFPL. -

-
- -
- - - - - diff --git a/srv/src/tpl/html/post.html b/srv/src/tpl/html/post.html deleted file mode 100644 index 22a5b97..0000000 --- a/srv/src/tpl/html/post.html +++ /dev/null @@ -1,48 +0,0 @@ -{{ define "body" }} - -
-

- {{ .Title }} -

-
- {{ .PublishedAt.Format "2006-01-02" }} -  •  - {{ if not .LastUpdatedAt.IsZero }} - (Updated {{ .LastUpdatedAt.Format "2006-01-02" }}) -  •  - {{ end }} - {{ .Description }} -
-
- -{{ if (or .SeriesPrevious .SeriesNext) }} -

- This post is part of a series:
- {{ if .SeriesPrevious }} - Previously: {{ .SeriesPrevious.Title }}
- {{ end }} - {{ if .SeriesNext }} - Next: {{ .SeriesNext.Title }}
- {{ end }} -

-{{ end }} - -
- {{ .Body }} -
- -{{ if (or .SeriesPrevious .SeriesNext) }} -

- If you liked this post, consider checking out other posts in the series:
- {{ if .SeriesPrevious }} - Previously: {{ .SeriesPrevious.Title }}
- {{ end }} - {{ if .SeriesNext }} - Next: {{ .SeriesNext.Title }}
- {{ end }} -

-{{ end }} - -{{ end }} - -{{ template "base.html" . }} diff --git a/srv/src/tpl/tpl.go b/srv/src/tpl/tpl.go deleted file mode 100644 index 1dd98ba..0000000 --- a/srv/src/tpl/tpl.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package tpl contains template files which are used to render the blog. -package tpl - -import ( - "embed" - html_tpl "html/template" -) - -//go:embed * -var fs embed.FS - -var HTML = html_tpl.Must(html_tpl.ParseFS(fs, "html/*")) -- cgit v1.2.3