Move template rendering logic into api package
This commit is contained in:
parent
dd354bc323
commit
4c04177c05
@ -3,8 +3,10 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -20,6 +22,11 @@ import (
|
|||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"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
|
// Params are used to instantiate a new API instance. All fields are required
|
||||||
// unless otherwise noted.
|
// unless otherwise noted.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
@ -27,7 +34,6 @@ type Params struct {
|
|||||||
PowManager pow.Manager
|
PowManager pow.Manager
|
||||||
|
|
||||||
PostStore post.Store
|
PostStore post.Store
|
||||||
PostHTTPRenderer post.Renderer
|
|
||||||
|
|
||||||
MailingList mailinglist.MailingList
|
MailingList mailinglist.MailingList
|
||||||
|
|
||||||
@ -190,7 +196,7 @@ func (a *api) handler() http.Handler {
|
|||||||
|
|
||||||
mux.Handle("/api/", http.StripPrefix("/api", apiHandler))
|
mux.Handle("/api/", http.StripPrefix("/api", apiHandler))
|
||||||
|
|
||||||
mux.Handle("/posts/", a.postHandler())
|
mux.Handle("/v2/posts/", a.postHandler())
|
||||||
|
|
||||||
return mux
|
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.
|
// endpoints.
|
||||||
package apiutils
|
package apiutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -9,7 +9,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"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"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,9 +44,9 @@ func newChatHandler(
|
|||||||
|
|
||||||
func (c *chatHandler) historyHandler() http.Handler {
|
func (c *chatHandler) historyHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,13 +58,13 @@ func (c *chatHandler) historyHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) {
|
if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) {
|
||||||
apiutils.BadRequest(rw, r, argErr.Err)
|
apiutil.BadRequest(rw, r, argErr.Err)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} 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"`
|
Cursor string `json:"cursor"`
|
||||||
Messages []chat.Message `json:"messages"`
|
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) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
userID, err := c.userID(r)
|
userID, err := c.userID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiutils.BadRequest(rw, r, err)
|
apiutil.BadRequest(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiutils.JSONResult(rw, r, struct {
|
apiutil.JSONResult(rw, r, struct {
|
||||||
UserID chat.UserID `json:"userID"`
|
UserID chat.UserID `json:"userID"`
|
||||||
}{
|
}{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@ -123,18 +123,18 @@ func (c *chatHandler) appendHandler() http.Handler {
|
|||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
userID, err := c.userID(r)
|
userID, err := c.userID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiutils.BadRequest(rw, r, err)
|
apiutil.BadRequest(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body := r.PostFormValue("body")
|
body := r.PostFormValue("body")
|
||||||
|
|
||||||
if l := len(body); l == 0 {
|
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
|
return
|
||||||
|
|
||||||
} else if l > 300 {
|
} else if l > 300 {
|
||||||
apiutils.BadRequest(rw, r, errors.New("body too long"))
|
apiutil.BadRequest(rw, r, errors.New("body too long"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,11 +144,11 @@ func (c *chatHandler) appendHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiutils.JSONResult(rw, r, struct {
|
apiutil.JSONResult(rw, r, struct {
|
||||||
MessageID string `json:"messageID"`
|
MessageID string `json:"messageID"`
|
||||||
}{
|
}{
|
||||||
MessageID: msg.ID,
|
MessageID: msg.ID,
|
||||||
@ -164,7 +164,7 @@ func (c *chatHandler) listenHandler() http.Handler {
|
|||||||
|
|
||||||
conn, err := c.wsUpgrader.Upgrade(rw, r, nil)
|
conn, err := c.wsUpgrader.Upgrade(rw, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiutils.BadRequest(rw, r, err)
|
apiutil.BadRequest(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
@ -172,14 +172,14 @@ func (c *chatHandler) listenHandler() http.Handler {
|
|||||||
it, err := c.room.Listen(ctx, sinceID)
|
it, err := c.room.Listen(ctx, sinceID)
|
||||||
|
|
||||||
if errors.As(err, new(chat.ErrInvalidArg)) {
|
if errors.As(err, new(chat.ErrInvalidArg)) {
|
||||||
apiutils.BadRequest(rw, r, err)
|
apiutil.BadRequest(rw, r, err)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if errors.Is(err, context.Canceled) {
|
} else if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ func (c *chatHandler) listenHandler() http.Handler {
|
|||||||
return
|
return
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ func (c *chatHandler) listenHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiutils.GetRequestLogger(r).Error(ctx, "couldn't write message", err)
|
apiutil.GetRequestLogger(r).Error(ctx, "couldn't write message", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -15,16 +15,16 @@ const (
|
|||||||
func setCSRFMiddleware(h http.Handler) http.Handler {
|
func setCSRFMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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 {
|
if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if csrfTok == "" {
|
} else if csrfTok == "" {
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: csrfTokenCookieName,
|
Name: csrfTokenCookieName,
|
||||||
Value: apiutils.RandStr(32),
|
Value: apiutil.RandStr(32),
|
||||||
Secure: true,
|
Secure: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -36,10 +36,10 @@ func setCSRFMiddleware(h http.Handler) http.Handler {
|
|||||||
func checkCSRFMiddleware(h http.Handler) http.Handler {
|
func checkCSRFMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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 {
|
if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ func checkCSRFMiddleware(h http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if csrfTok == "" || givenCSRFTok != csrfTok {
|
if csrfTok == "" || givenCSRFTok != csrfTok {
|
||||||
apiutils.BadRequest(rw, r, errors.New("invalid CSRF token"))
|
apiutil.BadRequest(rw, r, errors.New("invalid CSRF token"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"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"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ func (a *api) mailingListSubscribeHandler() http.Handler {
|
|||||||
parts[0] == "" ||
|
parts[0] == "" ||
|
||||||
parts[1] == "" ||
|
parts[1] == "" ||
|
||||||
len(email) >= 512 {
|
len(email) >= 512 {
|
||||||
apiutils.BadRequest(rw, r, errors.New("invalid email"))
|
apiutil.BadRequest(rw, r, errors.New("invalid email"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,11 +26,11 @@ func (a *api) mailingListSubscribeHandler() http.Handler {
|
|||||||
// just eat the error, make it look to the user like the
|
// just eat the error, make it look to the user like the
|
||||||
// verification email was sent.
|
// verification email was sent.
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
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) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
subToken := r.PostFormValue("subToken")
|
subToken := r.PostFormValue("subToken")
|
||||||
if l := len(subToken); l == 0 || l > 128 {
|
if l := len(subToken); l == 0 || l > 128 {
|
||||||
apiutils.BadRequest(rw, r, errInvalidSubToken)
|
apiutil.BadRequest(rw, r, errInvalidSubToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.params.MailingList.FinalizeSubscription(subToken)
|
err := a.params.MailingList.FinalizeSubscription(subToken)
|
||||||
|
|
||||||
if errors.Is(err, mailinglist.ErrNotFound) {
|
if errors.Is(err, mailinglist.ErrNotFound) {
|
||||||
apiutils.BadRequest(rw, r, errInvalidSubToken)
|
apiutil.BadRequest(rw, r, errInvalidSubToken)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if errors.Is(err, mailinglist.ErrAlreadyVerified) {
|
} else if errors.Is(err, mailinglist.ErrAlreadyVerified) {
|
||||||
// no problem
|
// no problem
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
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) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
unsubToken := r.PostFormValue("unsubToken")
|
unsubToken := r.PostFormValue("unsubToken")
|
||||||
if l := len(unsubToken); l == 0 || l > 128 {
|
if l := len(unsubToken); l == 0 || l > 128 {
|
||||||
apiutils.BadRequest(rw, r, errInvalidUnsubToken)
|
apiutil.BadRequest(rw, r, errInvalidUnsubToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.params.MailingList.Unsubscribe(unsubToken)
|
err := a.params.MailingList.Unsubscribe(unsubToken)
|
||||||
|
|
||||||
if errors.Is(err, mailinglist.ErrNotFound) {
|
if errors.Is(err, mailinglist.ErrNotFound) {
|
||||||
apiutils.BadRequest(rw, r, errInvalidUnsubToken)
|
apiutil.BadRequest(rw, r, errInvalidUnsubToken)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutils.InternalServerError(rw, r, err)
|
apiutil.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiutils.JSONResult(rw, r, struct{}{})
|
apiutil.JSONResult(rw, r, struct{}{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"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 {
|
func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
r = apiutils.SetRequestLogger(r, logger)
|
r = apiutil.SetRequestLogger(r, logger)
|
||||||
|
|
||||||
lrw := newLogResponseWriter(rw)
|
lrw := newLogResponseWriter(rw)
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ func postOnlyMiddleware(h http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiutils.GetRequestLogger(r).WarnString(r.Context(), "method not allowed")
|
apiutil.GetRequestLogger(r).WarnString(r.Context(), "method not allowed")
|
||||||
rw.WriteHeader(405)
|
rw.WriteHeader(405)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,15 @@ package api
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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"
|
"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)
|
http.Error(rw, "Post not found", 404)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutils.InternalServerError(
|
apiutil.InternalServerError(
|
||||||
rw, r, fmt.Errorf("fetching post with id %q: %w", id, err),
|
rw, r, fmt.Errorf("fetching post with id %q: %w", id, err),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
renderablePost, err := post.NewRenderablePost(a.params.PostStore, storedPost)
|
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 {
|
if err != nil {
|
||||||
apiutils.InternalServerError(
|
apiutil.InternalServerError(
|
||||||
rw, r, fmt.Errorf("constructing renderable post with id %q: %w", id, err),
|
rw, r,
|
||||||
|
fmt.Errorf("fetching posts for series %q: %w", series, err),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.params.PostHTTPRenderer.Render(rw, renderablePost); err != nil {
|
var foundThis bool
|
||||||
apiutils.InternalServerError(
|
|
||||||
|
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),
|
rw, r, fmt.Errorf("rendering post with id %q: %w", id, err),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"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 {
|
func (a *api) newPowChallengeHandler() http.Handler {
|
||||||
@ -14,7 +14,7 @@ func (a *api) newPowChallengeHandler() http.Handler {
|
|||||||
|
|
||||||
challenge := a.params.PowManager.NewChallenge()
|
challenge := a.params.PowManager.NewChallenge()
|
||||||
|
|
||||||
apiutils.JSONResult(rw, r, struct {
|
apiutil.JSONResult(rw, r, struct {
|
||||||
Seed string `json:"seed"`
|
Seed string `json:"seed"`
|
||||||
Target uint32 `json:"target"`
|
Target uint32 `json:"target"`
|
||||||
}{
|
}{
|
||||||
@ -30,21 +30,21 @@ func (a *api) requirePowMiddleware(h http.Handler) http.Handler {
|
|||||||
seedHex := r.FormValue("powSeed")
|
seedHex := r.FormValue("powSeed")
|
||||||
seed, err := hex.DecodeString(seedHex)
|
seed, err := hex.DecodeString(seedHex)
|
||||||
if err != nil || len(seed) == 0 {
|
if err != nil || len(seed) == 0 {
|
||||||
apiutils.BadRequest(rw, r, errors.New("invalid powSeed"))
|
apiutil.BadRequest(rw, r, errors.New("invalid powSeed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
solutionHex := r.FormValue("powSolution")
|
solutionHex := r.FormValue("powSolution")
|
||||||
solution, err := hex.DecodeString(solutionHex)
|
solution, err := hex.DecodeString(solutionHex)
|
||||||
if err != nil || len(seed) == 0 {
|
if err != nil || len(seed) == 0 {
|
||||||
apiutils.BadRequest(rw, r, errors.New("invalid powSolution"))
|
apiutil.BadRequest(rw, r, errors.New("invalid powSolution"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.params.PowManager.CheckSolution(seed, solution)
|
err = a.params.PowManager.CheckSolution(seed, solution)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +124,6 @@ func main() {
|
|||||||
apiParams.Logger = logger.WithNamespace("api")
|
apiParams.Logger = logger.WithNamespace("api")
|
||||||
apiParams.PowManager = powMgr
|
apiParams.PowManager = powMgr
|
||||||
apiParams.PostStore = postStore
|
apiParams.PostStore = postStore
|
||||||
apiParams.PostHTTPRenderer = post.NewMarkdownToHTMLRenderer()
|
|
||||||
apiParams.MailingList = ml
|
apiParams.MailingList = ml
|
||||||
apiParams.GlobalRoom = chatGlobalRoom
|
apiParams.GlobalRoom = chatGlobalRoom
|
||||||
apiParams.UserIDCalculator = chatUserIDCalc
|
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