diff --git a/models/posts/index.go b/models/posts/index.go index 33d5c10..99b7a9e 100644 --- a/models/posts/index.go +++ b/models/posts/index.go @@ -15,8 +15,9 @@ func UpdateIndex(p *Post) error { // Index caches high level metadata about the blog's contents for fast access. type Index struct { - Posts map[int]Post `json:"posts"` - Fragments map[string]int `json:"fragments"` + Posts map[int]Post `json:"posts"` + Fragments map[string]int `json:"fragments"` + Thumbnails map[int]string `json:"thumbnails"` } // GetIndex loads the index, or rebuilds it first if it doesn't exist. @@ -33,8 +34,9 @@ func GetIndex() (*Index, error) { // RebuildIndex builds the index from scratch. func RebuildIndex() (*Index, error) { idx := &Index{ - Posts: map[int]Post{}, - Fragments: map[string]int{}, + Posts: map[int]Post{}, + Fragments: map[string]int{}, + Thumbnails: map[int]string{}, } entries, _ := DB.List("blog/posts") for _, doc := range entries { @@ -65,6 +67,12 @@ func (idx *Index) Update(p *Post) error { Updated: p.Updated, } idx.Fragments[p.Fragment] = p.ID + + // Find a thumbnail image if possible. + if thumb, ok := p.ExtractThumbnail(); ok { + idx.Thumbnails[p.ID] = thumb + } + err := DB.Commit("blog/index", idx) return err } diff --git a/models/posts/posts.go b/models/posts/posts.go index 625242e..7867012 100644 --- a/models/posts/posts.go +++ b/models/posts/posts.go @@ -18,6 +18,12 @@ var DB *jsondb.DB var log *golog.Logger +// Regexp used to parse a thumbnail image from a blog post. Looks for the first +// URI component ending with an image extension. +var ( + ThumbnailImageRegexp = regexp.MustCompile(`['"(]([a-zA-Z0-9-_:/?.=&]+\.(?:jpe?g|png|gif))['")]`) +) + func init() { log = golog.GetLogger("blog") } @@ -191,6 +197,16 @@ func (p *Post) Delete() error { return idx.Delete(p) } +// ExtractThumbnail searches and returns a thumbnail image to represent the +// post. This will be the first image embedded in the post, or nothing. +func (p *Post) ExtractThumbnail() (string, bool) { + result := ThumbnailImageRegexp.FindStringSubmatch(p.Body) + if len(result) < 2 { + return "", false + } + return result[1], true +} + // getNextID gets the next blog post ID. func (p *Post) nextID() int { // Highest ID seen so far. diff --git a/models/posts/thumbnail_test.go b/models/posts/thumbnail_test.go new file mode 100644 index 0000000..4e25b59 --- /dev/null +++ b/models/posts/thumbnail_test.go @@ -0,0 +1,63 @@ +package posts_test + +import ( + "testing" + + "github.com/kirsle/blog/models/posts" +) + +func TestThumbnailRegexp(t *testing.T) { + type testCase struct { + Text string + Expect string + ExpectFail bool + } + + var tests = []testCase{ + { + Text: "Hello world", + ExpectFail: true, + }, + { + Text: "Some text.\n\n![An image](/static/photos/Image-1.jpg)\n" + + "![Another image](/static/photos/Image-2.jpg)", + Expect: "/static/photos/Image-1.jpg", + }, + { + Text: `` + + ``, + Expect: "/static/photos/12Abc456.jpg", + }, + { + Text: `A markdown image: ![With text](/test1.gif) and an HTML ` + + `image: `, + Expect: "/test1.gif", + }, + { + Text: ``, + Expect: "https://example.com/logo.gif?query=string.jpg", + }, + } + for _, test := range tests { + p := &posts.Post{ + Body: test.Text, + } + + result, ok := p.ExtractThumbnail() + if !ok && !test.ExpectFail { + t.Errorf("Text: %s\nExpected to fail, but did not!\nGot: %s", + test.Text, + result, + ) + continue + } + + if result != test.Expect { + t.Errorf("Text: %s\nExpect: %s\nGot: %s", + test.Text, + test.Expect, + result, + ) + } + } +} diff --git a/root/blog/archive.gohtml b/root/blog/archive.gohtml index 8264797..13e9e60 100644 --- a/root/blog/archive.gohtml +++ b/root/blog/archive.gohtml @@ -3,22 +3,40 @@

Archive

+{{ $thumbs := .Data.Thumbnails }} + {{ range .Data.Archive }} -

{{ .Date.Format "January, 2006" }}

+
+
+

{{ .Date.Format "January, 2006" }}

+
+
- +
+ +
+ {{ end }} {{ end }} diff --git a/src/controllers/posts/archive.go b/src/controllers/posts/archive.go index c99a69d..377091b 100644 --- a/src/controllers/posts/archive.go +++ b/src/controllers/posts/archive.go @@ -5,8 +5,8 @@ import ( "sort" "time" - "github.com/kirsle/blog/src/middleware/auth" "github.com/kirsle/blog/models/posts" + "github.com/kirsle/blog/src/middleware/auth" "github.com/kirsle/blog/src/render" "github.com/kirsle/blog/src/responses" "github.com/kirsle/blog/src/types" @@ -54,7 +54,8 @@ func archiveHandler(w http.ResponseWriter, r *http.Request) { } v := map[string]interface{}{ - "Archive": result, + "Archive": result, + "Thumbnails": idx.Thumbnails, } render.Template(w, r, "blog/archive", v) }