summaryrefslogtreecommitdiff
path: root/src/render
diff options
context:
space:
mode:
Diffstat (limited to 'src/render')
-rw-r--r--src/render/methods.go226
-rw-r--r--src/render/render.go2
2 files changed, 228 insertions, 0 deletions
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