diff options
Diffstat (limited to 'srv/src/post')
-rw-r--r-- | srv/src/post/post.go | 20 | ||||
-rw-r--r-- | srv/src/post/renderer.go | 96 | ||||
-rw-r--r-- | srv/src/post/renderer_test.go | 92 |
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()) +} |