2015-07-27 23:21:09 -05:00
|
|
|
package markdown
|
|
|
|
|
|
|
|
import (
|
2015-07-29 18:41:52 -05:00
|
|
|
"bytes"
|
2015-07-27 23:21:09 -05:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/russross/blackfriday"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2015-07-29 06:15:02 -05:00
|
|
|
// Date format YYYY-MM-DD HH:MM:SS
|
2015-07-27 23:21:09 -05:00
|
|
|
timeLayout = `2006-01-02 15:04:05`
|
2015-07-29 06:15:02 -05:00
|
|
|
|
2015-07-29 18:41:52 -05:00
|
|
|
// Maximum length of page summary.
|
|
|
|
summaryLen = 500
|
2015-07-27 23:21:09 -05:00
|
|
|
)
|
|
|
|
|
2015-07-29 06:15:02 -05:00
|
|
|
// PageLink represents a statically generated markdown page.
|
2015-07-27 23:21:09 -05:00
|
|
|
type PageLink struct {
|
|
|
|
Title string
|
|
|
|
Summary string
|
|
|
|
Date time.Time
|
2015-07-29 18:41:52 -05:00
|
|
|
URL string
|
2015-07-27 23:21:09 -05:00
|
|
|
}
|
|
|
|
|
2015-07-29 12:06:53 -05:00
|
|
|
// byDate sorts PageLink by newest date to oldest.
|
|
|
|
type byDate []PageLink
|
2015-07-27 23:21:09 -05:00
|
|
|
|
2015-07-29 12:06:53 -05:00
|
|
|
func (p byDate) Len() int { return len(p) }
|
|
|
|
func (p byDate) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
|
|
func (p byDate) Less(i, j int) bool { return p[i].Date.After(p[j].Date) }
|
2015-07-27 23:21:09 -05:00
|
|
|
|
2015-07-29 06:15:02 -05:00
|
|
|
type linkGen struct {
|
|
|
|
generating bool
|
|
|
|
waiters int
|
|
|
|
lastErr error
|
|
|
|
sync.RWMutex
|
|
|
|
sync.WaitGroup
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *linkGen) addWaiter() {
|
|
|
|
l.WaitGroup.Add(1)
|
|
|
|
l.waiters++
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *linkGen) discardWaiters() {
|
|
|
|
l.Lock()
|
|
|
|
defer l.Unlock()
|
|
|
|
for i := 0; i < l.waiters; i++ {
|
|
|
|
l.Done()
|
2015-07-27 23:21:09 -05:00
|
|
|
}
|
2015-07-29 06:15:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *linkGen) started() bool {
|
|
|
|
l.RLock()
|
|
|
|
defer l.RUnlock()
|
|
|
|
return l.generating
|
|
|
|
}
|
2015-07-27 23:21:09 -05:00
|
|
|
|
2015-07-29 06:15:02 -05:00
|
|
|
func (l *linkGen) generateLinks(md Markdown, cfg *Config) {
|
|
|
|
l.Lock()
|
|
|
|
l.generating = true
|
|
|
|
l.Unlock()
|
2015-07-27 23:21:09 -05:00
|
|
|
|
|
|
|
fp := filepath.Join(md.Root, cfg.PathScope)
|
|
|
|
|
|
|
|
cfg.Links = []PageLink{}
|
2015-07-29 06:15:02 -05:00
|
|
|
|
|
|
|
cfg.Lock()
|
|
|
|
l.lastErr = filepath.Walk(fp, func(path string, info os.FileInfo, err error) error {
|
2015-07-27 23:21:09 -05:00
|
|
|
for _, ext := range cfg.Extensions {
|
|
|
|
if !info.IsDir() && strings.HasSuffix(info.Name(), ext) {
|
|
|
|
// Load the file
|
|
|
|
body, err := ioutil.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the relative path as if it were a HTTP request,
|
|
|
|
// then prepend with "/" (like a real HTTP request)
|
|
|
|
reqPath, err := filepath.Rel(md.Root, path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
reqPath = "/" + reqPath
|
|
|
|
|
|
|
|
parser := findParser(body)
|
|
|
|
if parser == nil {
|
|
|
|
// no metadata, ignore.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
summary, err := parser.Parse(body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-07-29 18:41:52 -05:00
|
|
|
// truncate summary to maximum length
|
2015-07-27 23:21:09 -05:00
|
|
|
if len(summary) > summaryLen {
|
|
|
|
summary = summary[:summaryLen]
|
2015-07-29 18:41:52 -05:00
|
|
|
|
|
|
|
// trim to nearest word
|
|
|
|
lastSpace := bytes.LastIndex(summary, []byte(" "))
|
|
|
|
if lastSpace != -1 {
|
|
|
|
summary = summary[:lastSpace]
|
|
|
|
}
|
2015-07-27 23:21:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
metadata := parser.Metadata()
|
|
|
|
|
|
|
|
cfg.Links = append(cfg.Links, PageLink{
|
|
|
|
Title: metadata.Title,
|
2015-07-29 18:41:52 -05:00
|
|
|
URL: reqPath,
|
2015-07-27 23:21:09 -05:00
|
|
|
Date: metadata.Date,
|
|
|
|
Summary: string(blackfriday.Markdown(summary, PlaintextRenderer{}, 0)),
|
|
|
|
})
|
|
|
|
|
|
|
|
break // don't try other file extensions
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
2015-07-29 06:15:02 -05:00
|
|
|
// sort by newest date
|
2015-07-29 12:06:53 -05:00
|
|
|
sort.Sort(byDate(cfg.Links))
|
2015-07-29 06:15:02 -05:00
|
|
|
cfg.Unlock()
|
|
|
|
|
|
|
|
l.Lock()
|
|
|
|
l.generating = false
|
|
|
|
l.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
type linkGenerator struct {
|
|
|
|
gens map[*Config]*linkGen
|
|
|
|
sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
var generator = linkGenerator{gens: make(map[*Config]*linkGen)}
|
|
|
|
|
|
|
|
// GenerateLinks generates links to all markdown files ordered by newest date.
|
|
|
|
// This blocks until link generation is done. When called by multiple goroutines,
|
|
|
|
// the first caller starts the generation and others only wait.
|
|
|
|
func GenerateLinks(md Markdown, cfg *Config) error {
|
|
|
|
generator.Lock()
|
|
|
|
|
|
|
|
// if link generator exists for config and running, wait.
|
|
|
|
if g, ok := generator.gens[cfg]; ok {
|
|
|
|
if g.started() {
|
|
|
|
g.addWaiter()
|
|
|
|
generator.Unlock()
|
|
|
|
g.Wait()
|
|
|
|
return g.lastErr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
g := &linkGen{}
|
|
|
|
generator.gens[cfg] = g
|
|
|
|
generator.Unlock()
|
|
|
|
|
|
|
|
g.generateLinks(md, cfg)
|
|
|
|
g.discardWaiters()
|
|
|
|
return g.lastErr
|
2015-07-27 23:21:09 -05:00
|
|
|
}
|