summaryrefslogtreecommitdiff
path: root/srv/src/post
diff options
context:
space:
mode:
Diffstat (limited to 'srv/src/post')
-rw-r--r--srv/src/post/post.go20
-rw-r--r--srv/src/post/renderer.go96
-rw-r--r--srv/src/post/renderer_test.go92
3 files changed, 194 insertions, 14 deletions
diff --git a/srv/src/post/post.go b/srv/src/post/post.go
index bdc48af..5835995 100644
--- a/srv/src/post/post.go
+++ b/srv/src/post/post.go
@@ -5,7 +5,6 @@ import (
"database/sql"
"errors"
"fmt"
- "path"
"regexp"
"strings"
"time"
@@ -37,6 +36,12 @@ type Post struct {
Body string
}
+// HTTPPath returns the relative URL path of the StoredPost, when querying it
+// over HTTP.
+func (p Post) HTTPPath() string {
+ return fmt.Sprintf("%s.html", p.ID)
+}
+
// StoredPost is a Post which has been stored in a Store, and has been given
// some extra fields as a result.
type StoredPost struct {
@@ -46,19 +51,6 @@ type StoredPost struct {
LastUpdatedAt time.Time
}
-// URL returns the relative URL of the StoredPost.
-func (p StoredPost) URL() string {
- return path.Join(
- fmt.Sprintf(
- "%d/%0d/%0d",
- p.PublishedAt.Year(),
- p.PublishedAt.Month(),
- p.PublishedAt.Day(),
- ),
- p.ID+".html",
- )
-}
-
// Store is used for storing posts to a persistent storage.
type Store interface {
diff --git a/srv/src/post/renderer.go b/srv/src/post/renderer.go
new file mode 100644
index 0000000..74acc25
--- /dev/null
+++ b/srv/src/post/renderer.go
@@ -0,0 +1,96 @@
+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
new file mode 100644
index 0000000..5c01cd2
--- /dev/null
+++ b/srv/src/post/renderer_test.go
@@ -0,0 +1,92 @@
+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())
+}