diff --git a/README.md b/README.md index b7c793e..21a643d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Prose is a blogging platform written in Go, which I am building to serve my own ## Usage -Blog posts should be created in the format `DATE-title-slug.md`. Work in progress posts should be stored as `WIP-title-slug.md`. Static content should be stored in the `static/` folder, appropriately arranged. +Blog posts should be created in the format `title-slug.md`. Work in progress posts should be stored as `WIP-title-slug.md`. Static content should be stored in the `static/` folder, appropriately arranged. Posts will be served as `/title-slug`, and files like `static/random/file/structure.txt` will be served as `/random/file/structure.txt`. When title slugs and static files conflict, slugs will have higher precdence. diff --git a/go.mod b/go.mod index e90b934..43db5bf 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,12 @@ module prose go 1.15 -require github.com/yuin/goldmark v1.3.1 +require ( + github.com/aymerick/raymond v2.0.2+incompatible + github.com/mitchellh/mapstructure v1.4.1 + github.com/wellington/go-libsass v0.9.2 + github.com/yuin/goldmark v1.3.1 + github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 + github.com/yuin/goldmark-meta v1.0.0 + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect +) diff --git a/go.sum b/go.sum index de9bccb..2de3205 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,60 @@ +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a h1:3v1NrYWWqp2S72e4HLgxKt83B3l0lnORDholH/ihoMM= +github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/aymerick/raymond v1.1.0 h1:phuNN2s67eI/HtO8CrvqFcdR2JP+BtkGJZ9n692Hr2Y= +github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= +github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/wellington/go-libsass v0.9.2 h1:6Ims04UDdBs6/CGSVK5JC8FNikR5ssrsMMKE/uaO5Q8= +github.com/wellington/go-libsass v0.9.2/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs= +github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.1 h1:eVwehsLsZlCJCwXyGLgg+Q4iFWE/eTIMG0e8waCmm/I= github.com/yuin/goldmark v1.3.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 h1:VWSxtAiQNh3zgHJpdpkpVYjTPqRE3P6UZCOPa1nRDio= +github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691/go.mod h1:YLF3kDffRfUH/bTxOxHhV6lxwIB3Vfj91rEwNMS9MXo= +github.com/yuin/goldmark-meta v1.0.0 h1:ScsatUIT2gFS6azqzLGUjgOnELsBOxMXerM3ogdJhAM= +github.com/yuin/goldmark-meta v1.0.0/go.mod h1:zsNNOrZ4nLuyHAJeLQEZcQat8dm70SmB2kHbls092Gc= gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA= gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow= gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g= @@ -17,8 +68,17 @@ gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1: gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI= gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs= gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/page.go b/page.go index 56da3bf..c3ae948 100644 --- a/page.go +++ b/page.go @@ -5,33 +5,74 @@ import ( "fmt" "io/ioutil" + "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" ) -type page struct { - slug string +// Metadata stores the data about a page that needs to be visible +// at the home page. +type Metadata struct { + Title string + Summary string + Date string // TODO: better representation? time.Time might cause timezone issues... } -func (p *page) render() ([]byte, error) { - data, err := ioutil.ReadFile("posts/" + p.slug + ".md") +// 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 := ioutil.ReadFile("posts/" + slug + ".md") if err != nil { - return nil, fmt.Errorf("Could not read from %s.md: %s", p.slug, err) + return nil, fmt.Errorf("could not read file: %s", err) } md := goldmark.New( - goldmark.WithExtensions(extension.Linkify), + goldmark.WithExtensions( + extension.Linkify, + extension.Strikethrough, + extension.Typographer, + meta.Meta, + highlighting.Highlighting, + ), goldmark.WithRendererOptions( html.WithUnsafe(), ), ) - var converted bytes.Buffer - err = md.Convert(data, &converted) + ctx := parser.NewContext() + err = md.Convert(data, &converted, parser.WithContext(ctx)) if err != nil { - return nil, fmt.Errorf("Could not parse markdown from %s.md: %s", p.slug, err) + 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) } - return converted.Bytes(), nil + 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) } diff --git a/server.go b/server.go index 06441f7..d67efab 100644 --- a/server.go +++ b/server.go @@ -2,15 +2,23 @@ package main import ( "fmt" + "io" "io/ioutil" "log" "net/http" + "os" "strings" + + "github.com/aymerick/raymond" + "github.com/wellington/go-libsass" ) type server struct { - pages []page + pages []*Page staticHandler http.Handler + pageTpl *raymond.Template + fullPostTpl *raymond.Template + summaryTpl *raymond.Template } func newServer() (*server, error) { @@ -21,7 +29,7 @@ func newServer() (*server, error) { } s := &server{ - pages: make([]page, 0, len(files)), + pages: make([]*Page, 0, len(files)), staticHandler: http.FileServer(http.Dir("static/")), } @@ -29,10 +37,73 @@ func newServer() (*server, error) { filename := f.Name() if strings.HasSuffix(filename, ".md") { - s.pages = append(s.pages, page{slug: strings.TrimSuffix(filename, ".md")}) + page, err := newPage(strings.TrimSuffix(filename, ".md")) + if err != nil { + return nil, fmt.Errorf("could not render %s: %s", filename, err) + } + s.pages = append(s.pages, page) + log.Printf("Loaded page %s", filename) } } + s.pageTpl, err = raymond.ParseFile("templates/page.html") + if err != nil { + return nil, fmt.Errorf("could not parse page template") + } + log.Printf("Loaded page template") + + s.fullPostTpl, err = raymond.ParseFile("templates/fullpost.html") + if err != nil { + return nil, fmt.Errorf("could not parse full post template") + } + log.Printf("Loaded full post template") + + s.summaryTpl, err = raymond.ParseFile("templates/summary.html") + if err != nil { + return nil, fmt.Errorf("could not parse summary template") + } + log.Printf("Loaded summary template") + + styles, err := ioutil.ReadDir("styles/") + if err != nil { + return nil, fmt.Errorf("Could not load styles directory: %s", err) + } + + for _, s := range styles { + filename := s.Name() + in, err := os.Open("styles/" + filename) + if err != nil { + return nil, fmt.Errorf("Could not open style infile %s: %s", filename, err) + } + if strings.HasSuffix(filename, ".scss") { + outFilename := strings.TrimSuffix(filename, ".scss") + ".css" + out, err := os.Create("static/css/" + outFilename) + if err != nil { + return nil, fmt.Errorf("Could not open style outfile %s: %s", outFilename, err) + } + comp, err := libsass.New(out, in) + if err != nil { + return nil, fmt.Errorf("Could not start sass compiler for file %s: %s", filename, err) + } + if err = comp.Run(); err != nil { + return nil, fmt.Errorf("Could not generate stylesheet %s: %s", filename, err) + } + } else if strings.HasSuffix(filename, ".css") { + out, err := os.Create("static/css/" + filename) + if err != nil { + return nil, fmt.Errorf("Could not open style outfile %s: %s", filename, err) + } + _, err = io.Copy(out, in) + if err != nil { + return nil, fmt.Errorf("Could not copy stylesheet %s: %s", filename, err) + } + } else { + log.Printf("Skipping stylesheet %s, don't know how to handle", filename) + continue + } + log.Printf("Loaded stylesheet %s", filename) + } + return s, nil } @@ -50,16 +121,20 @@ func (s *server) router(res http.ResponseWriter, req *http.Request) { } for _, p := range s.pages { - if p.slug == slug { - buf, err := p.render() + if p.Slug == slug { + res.Header().Add("content-type", "text/html") + contents, err := p.render(s.fullPostTpl) if err != nil { - res.WriteHeader(http.StatusInternalServerError) - res.Write([]byte("oh no")) + s.errorInRequest(res, req, err) } - res.Write(buf) + page, err := s.createPage(p.Metadata.Title, contents) + if err != nil { + s.errorInRequest(res, req, err) + } + res.Write([]byte(page)) return } } @@ -67,12 +142,38 @@ func (s *server) router(res http.ResponseWriter, req *http.Request) { s.staticHandler.ServeHTTP(res, req) } +func (s *server) errorInRequest(res http.ResponseWriter, req *http.Request, err error) { + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("oh no")) + log.Printf("ERR %s: %s", req.URL.Path, err) +} + +func (s *server) createPage(title, contents string) (string, error) { + ctx := map[string]interface{}{ + "title": title, + "contents": contents, + } + return s.pageTpl.Exec(ctx) +} + func (s *server) homePage(res http.ResponseWriter, req *http.Request) { res.Header().Add("content-type", "text/html") - res.Write([]byte("