rename 'pages' to 'posts' and move functionality into own file

Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
Naman Sood 2021-03-15 22:47:07 -04:00
parent 00f27f0cbe
commit ef112be9bd
3 changed files with 198 additions and 187 deletions

125
page.go
View file

@ -1,125 +0,0 @@
package main
import (
"bytes"
"fmt"
"os"
"sort"
"github.com/aymerick/raymond"
"github.com/mitchellh/mapstructure"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting"
meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
// Metadata stores the data about a page that needs to be visible
// at the home page.
type Metadata struct {
Title string
Summary string
Time int64 // unix timestamp
}
// Page stores the contents of a blog post.
type Page struct {
Slug string
Metadata Metadata
Contents string
}
func newPage(slug string) (*Page, error) {
data, err := os.ReadFile("posts/" + slug + ".md")
if err != nil {
return nil, fmt.Errorf("could not read file: %s", err)
}
md := goldmark.New(
goldmark.WithExtensions(
extension.Linkify,
extension.Strikethrough,
extension.Typographer,
meta.Meta,
highlighting.Highlighting,
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)
var converted bytes.Buffer
ctx := parser.NewContext()
err = md.Convert(data, &converted, parser.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("could not parse markdown: %s", err)
}
mdMap, err := meta.TryGet(ctx)
if err != nil {
return nil, fmt.Errorf("could not parse metadata: %s", err)
}
var metadata Metadata
err = mapstructure.Decode(mdMap, &metadata)
if err != nil {
return nil, fmt.Errorf("could not destructure metadata: %s", err)
}
page := &Page{
Slug: slug,
Metadata: metadata,
Contents: converted.String(),
}
return page, nil
}
func (p *Page) render(tpl *raymond.Template) (string, error) {
return tpl.Exec(p)
}
func (p *Page) String() string {
return p.Slug
}
type pages []*Page
func insertOrUpdate(ps pages, p *Page) pages {
defer sort.Sort(ps)
for i, pg := range ps {
if pg.Slug == p.Slug {
ps[i] = p
return ps
}
}
ps = append(ps, p)
return ps
}
func remove(ps pages, slug string) pages {
for i, pg := range ps {
if pg.Slug == slug {
ps = append(ps[:i], ps[i+1:]...)
break
}
}
fmt.Println(ps)
return ps
}
// Len implements sort.Interface
func (ps pages) Len() int {
return len(ps)
}
// Less implements sort.Interface
func (ps pages) Less(i, j int) bool {
return ps[i].Metadata.Time > ps[j].Metadata.Time
}
// Swap implements sort.Interface
func (ps pages) Swap(i, j int) {
temp := ps[i]
ps[i] = ps[j]
ps[j] = temp
}

174
post.go Normal file
View file

@ -0,0 +1,174 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
"sort"
"strings"
"github.com/aymerick/raymond"
"github.com/mitchellh/mapstructure"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting"
meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
// Metadata stores the data about a post that needs to be visible
// at the home page.
type Metadata struct {
Title string
Summary string
Time int64 // unix timestamp
}
// Post stores the contents of a blog post.
type Post struct {
Slug string
Metadata Metadata
Contents string
}
func newPost(slug string) (*Post, error) {
data, err := os.ReadFile("posts/" + slug + ".md")
if err != nil {
return nil, fmt.Errorf("could not read file: %s", err)
}
md := goldmark.New(
goldmark.WithExtensions(
extension.Linkify,
extension.Strikethrough,
extension.Typographer,
meta.Meta,
highlighting.Highlighting,
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)
var converted bytes.Buffer
ctx := parser.NewContext()
err = md.Convert(data, &converted, parser.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("could not parse markdown: %s", err)
}
mdMap, err := meta.TryGet(ctx)
if err != nil {
return nil, fmt.Errorf("could not parse metadata: %s", err)
}
var metadata Metadata
err = mapstructure.Decode(mdMap, &metadata)
if err != nil {
return nil, fmt.Errorf("could not destructure metadata: %s", err)
}
post := &Post{
Slug: slug,
Metadata: metadata,
Contents: converted.String(),
}
return post, nil
}
func (p *Post) render(tpl *raymond.Template) (string, error) {
return tpl.Exec(p)
}
func (p *Post) String() string {
return p.Slug
}
type postList []*Post
func newPostList() (postList, error) {
files, err := os.ReadDir("posts/")
if err != nil {
return nil, err
}
pl := make(postList, 0, len(files))
for _, f := range files {
filename := f.Name()
if strings.HasSuffix(filename, ".md") {
post, err := newPost(strings.TrimSuffix(filename, ".md"))
if err != nil {
return nil, fmt.Errorf("could not render %s: %s", filename, err)
}
pl = append(pl, post)
log.Printf("Loaded post %s", filename)
}
}
sort.Sort(pl)
return pl, nil
}
func insertOrUpdatePost(pl postList, p *Post) postList {
defer sort.Sort(pl)
for i, post := range pl {
if post.Slug == p.Slug {
pl[i] = p
return pl
}
}
pl = append(pl, p)
return pl
}
func removePost(pl postList, slug string) postList {
for i, post := range pl {
if post.Slug == slug {
pl = append(pl[:i], pl[i+1:]...)
break
}
}
fmt.Println(pl)
return pl
}
// Len implements sort.Interface
func (pl postList) Len() int {
return len(pl)
}
// Less implements sort.Interface
func (pl postList) Less(i, j int) bool {
return pl[i].Metadata.Time > pl[j].Metadata.Time
}
// Swap implements sort.Interface
func (pl postList) Swap(i, j int) {
temp := pl[i]
pl[i] = pl[j]
pl[j] = temp
}
func newPostListener(update func(func(postList) postList)) *listener {
ln := &listener{
folder: "posts/",
update: func(file string) error {
post, err := newPost(strings.TrimSuffix(file, ".md"))
if err != nil {
return err
}
update(func(oldList postList) postList {
return insertOrUpdatePost(oldList, post)
})
return nil
},
clean: func(file string) error {
update(func(oldList postList) postList {
return removePost(oldList, strings.TrimSuffix(file, ".md"))
})
return nil
},
}
return ln
}

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -24,8 +23,8 @@ type server struct {
tplMutex sync.RWMutex tplMutex sync.RWMutex
templates map[string]*raymond.Template templates map[string]*raymond.Template
pgMutex sync.RWMutex postsMutex sync.RWMutex
pages postList
} }
func newServer() (*server, error) { func newServer() (*server, error) {
@ -33,10 +32,13 @@ func newServer() (*server, error) {
staticHandler: http.FileServer(http.Dir("static/")), staticHandler: http.FileServer(http.Dir("static/")),
} }
err := s.refreshPages() posts, err := newPostList()
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.postsMutex.Lock()
s.postList = posts
s.postsMutex.Unlock()
err = s.refreshTemplates() err = s.refreshTemplates()
if err != nil { if err != nil {
@ -48,26 +50,12 @@ func newServer() (*server, error) {
return nil, err return nil, err
} }
pagesLn := &listener{ postsLn := newPostListener(func(updateFn func(postList) postList) {
folder: "posts/", s.postsMutex.Lock()
update: func(file string) error { defer s.postsMutex.Unlock()
s.pgMutex.Lock() s.postList = updateFn(s.postList)
defer s.pgMutex.Unlock() })
page, err := newPage(strings.TrimSuffix(file, ".md")) go postsLn.listen()
if err != nil {
return err
}
s.pages = insertOrUpdate(s.pages, page)
return nil
},
clean: func(file string) error {
s.pgMutex.Lock()
defer s.pgMutex.Unlock()
s.pages = remove(s.pages, strings.TrimSuffix(file, ".md"))
return nil
},
}
go pagesLn.listen()
templatesLn := &listener{ templatesLn := &listener{
folder: "templates/", folder: "templates/",
@ -119,32 +107,6 @@ func newServer() (*server, error) {
return s, nil return s, nil
} }
func (s *server) refreshPages() error {
files, err := os.ReadDir("posts/")
if err != nil {
return err
}
s.pgMutex.Lock()
defer s.pgMutex.Unlock()
s.pages = make(pages, 0, len(files))
for _, f := range files {
filename := f.Name()
if strings.HasSuffix(filename, ".md") {
page, err := newPage(strings.TrimSuffix(filename, ".md"))
if err != nil {
return fmt.Errorf("could not render %s: %s", filename, err)
}
s.pages = append(s.pages, page)
log.Printf("Loaded page %s", filename)
}
}
sort.Sort(s.pages)
return nil
}
func loadTemplate(file string) (*raymond.Template, error) { func loadTemplate(file string) (*raymond.Template, error) {
tpl, err := raymond.ParseFile("templates/" + file + ".html") tpl, err := raymond.ParseFile("templates/" + file + ".html")
if err != nil { if err != nil {
@ -340,11 +302,11 @@ func (s *server) router(res http.ResponseWriter, req *http.Request) {
return return
} }
s.pgMutex.RLock() s.postsMutex.RLock()
defer s.pgMutex.RUnlock() defer s.postsMutex.RUnlock()
for _, p := range s.pages { for _, p := range s.postList {
if p.Slug == slug { if p.Slug == slug {
s.renderPage(p, res, req) s.postPage(p, res, req)
return return
} }
} }
@ -358,7 +320,7 @@ func (s *server) errorInRequest(res http.ResponseWriter, req *http.Request, err
log.Printf("ERR %s: %s", req.URL.Path, err) log.Printf("ERR %s: %s", req.URL.Path, err)
} }
func (s *server) createPage(title, contents string) (string, error) { func (s *server) createWebPage(title, contents string) (string, error) {
ctx := map[string]interface{}{ ctx := map[string]interface{}{
"title": title, "title": title,
"contents": contents, "contents": contents,
@ -366,13 +328,13 @@ func (s *server) createPage(title, contents string) (string, error) {
return s.templates["page"].Exec(ctx) return s.templates["page"].Exec(ctx)
} }
func (s *server) renderPage(p *Page, res http.ResponseWriter, req *http.Request) { func (s *server) postPage(p *Post, res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html") res.Header().Add("content-type", "text/html")
contents, err := p.render(s.templates["fullpost"]) contents, err := p.render(s.templates["fullpost"])
if err != nil { if err != nil {
s.errorInRequest(res, req, err) s.errorInRequest(res, req, err)
} }
page, err := s.createPage(p.Metadata.Title, contents) page, err := s.createWebPage(p.Metadata.Title, contents)
if err != nil { if err != nil {
s.errorInRequest(res, req, err) s.errorInRequest(res, req, err)
} }
@ -384,17 +346,17 @@ func (s *server) homePage(res http.ResponseWriter, req *http.Request) {
var posts string var posts string
s.pgMutex.RLock() s.postsMutex.RLock()
defer s.pgMutex.RUnlock() defer s.postsMutex.RUnlock()
for _, p := range s.pages { for _, p := range s.postList {
summary, err := p.render(s.templates["summary"]) summary, err := p.render(s.templates["summary"])
if err != nil { if err != nil {
log.Printf("could not render page summary for %s", p.Slug) log.Printf("could not render post summary for %s", p.Slug)
} }
posts = posts + summary posts = posts + summary
} }
page, err := s.createPage("Home", posts) page, err := s.createWebPage("Home", posts)
if err != nil { if err != nil {
s.errorInRequest(res, req, err) s.errorInRequest(res, req, err)