Move template rendering logic into api package
This commit is contained in:
parent
dd354bc323
commit
4c04177c05
@ -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
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Package apiutils contains utilities which are useful for implementing api
|
||||
// Package apiutil contains utilities which are useful for implementing api
|
||||
// endpoints.
|
||||
package apiutils
|
||||
package apiutil
|
||||
|
||||
import (
|
||||
"context"
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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{}{})
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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 err := a.params.PostHTTPRenderer.Render(rw, renderablePost); err != nil {
|
||||
apiutils.InternalServerError(
|
||||
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 := tpls.ExecuteTemplate(rw, "post.html", tplData); err != nil {
|
||||
apiutil.InternalServerError(
|
||||
rw, r, fmt.Errorf("rendering post with id %q: %w", id, err),
|
||||
)
|
||||
return
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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: `<h1 id="foo">Foo</h1>`,
|
||||
},
|
||||
{
|
||||
body: `
|
||||
this is a body
|
||||
|
||||
this is another
|
||||
`,
|
||||
exp: `
|
||||
<p>this is a body</p>
|
||||
|
||||
<p>this is another</p>`,
|
||||
},
|
||||
{
|
||||
body: `this is a [link](somewhere.html)`,
|
||||
exp: `<p>this is a <a href="somewhere.html" target="_blank">link</a></p>`,
|
||||
},
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
@ -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/*"))
|
Loading…
Reference in New Issue
Block a user