package markdown import ( "bytes" "io/ioutil" "log" "os" "path/filepath" "sort" "strings" "sync" "time" "github.com/russross/blackfriday" ) const ( // Date format YYYY-MM-DD HH:MM:SS timeLayout = `2006-01-02 15:04:05` // Maximum length of page summary. summaryLen = 500 ) // PageLink represents a statically generated markdown page. type PageLink struct { Title string Summary string Date time.Time URL string } // byDate sorts PageLink by newest date to oldest. type byDate []PageLink 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) } 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() } } func (l *linkGen) started() bool { l.RLock() defer l.RUnlock() return l.generating } // generateLinks generate links to markdown files if there are file changes. // It returns true when generation is done and false otherwise. func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool { l.Lock() l.generating = true l.Unlock() fp := filepath.Join(md.Root, cfg.PathScope) // path to scan for .md files // If the file path to scan for Markdown files (fp) does // not exist, there are no markdown files to scan for. if _, err := os.Stat(fp); os.IsNotExist(err) { l.Lock() l.lastErr = err l.generating = false l.Unlock() return false } hash, err := computeDirHash(md, cfg) // same hash, return. if err == nil && hash == cfg.linksHash { l.Lock() l.generating = false l.Unlock() return false } else if err != nil { log.Printf("[ERROR] markdown: Hash error: %v", err) } cfg.Links = []PageLink{} cfg.Lock() l.lastErr = filepath.Walk(fp, func(path string, info os.FileInfo, err error) error { for _, ext := range cfg.Extensions { if info.IsDir() || !strings.HasSuffix(info.Name(), ext) { continue } // 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 = "/" + filepath.ToSlash(reqPath) // Make the summary parser := findParser(body) if parser == nil { // no metadata, ignore. continue } summaryRaw, err := parser.Parse(body) if err != nil { return err } summary := blackfriday.Markdown(summaryRaw, SummaryRenderer{}, 0) // truncate summary to maximum length if len(summary) > summaryLen { summary = summary[:summaryLen] // trim to nearest word lastSpace := bytes.LastIndex(summary, []byte(" ")) if lastSpace != -1 { summary = summary[:lastSpace] } } metadata := parser.Metadata() cfg.Links = append(cfg.Links, PageLink{ Title: metadata.Title, URL: reqPath, Date: metadata.Date, Summary: string(summary), }) break // don't try other file extensions } return nil }) // sort by newest date sort.Sort(byDate(cfg.Links)) cfg.linksHash = hash cfg.Unlock() l.Lock() l.generating = false l.Unlock() return true }