package main import ( "bytes" "fmt" "io" "io/ioutil" "log" "os" "os/exec" pathpkg "path" "sort" "strings" "time" "gopkg.in/yaml.v3" ) // Dir represents a directory. type Dir struct { Permalink string Pages []*Page Dirs []*Dir index *Page // The index page. feeds map[string][]byte // Atom/RSS/Custom feeds. path string // relative to the content dir } // Page represents a page. type Page struct { Title string Date time.Time Weight int Permalink string `yaml:"-"` FilePath string `yaml:"-"` Content string `yaml:"-"` Params map[string]interface{} Prev *Page `yaml:"-"` Next *Page `yaml:"-"` } // read reads from a directory and indexes the files and directories within it. func (d *Dir) read(srcDir string, task *Task, cfg *Site) error { return d._read(srcDir, "", task, cfg) } func (d *Dir) _read(srcDir, path string, task *Task, cfg *Site) error { entries, err := ioutil.ReadDir(pathpkg.Join(srcDir, path)) if err != nil { return err } for _, entry := range entries { name := entry.Name() path := pathpkg.Join(path, name) if entry.IsDir() { // Ignore directories beginning with "_" if strings.HasPrefix(name, "_") { continue } // Gather directory data dir := &Dir{Permalink: "/" + path + "/", path: path} if err := dir._read(srcDir, path, task, cfg); err != nil { return err } d.Dirs = append(d.Dirs, dir) } else if ext := pathpkg.Ext(name); task.Match(ext) { // Ignore pages beginning with "_" with the exception of _index pages namePrefix := strings.TrimSuffix(name, ext) if strings.HasPrefix(name, "_") && namePrefix != "_index" { continue } srcPath := pathpkg.Join(srcDir, path) content, err := ioutil.ReadFile(srcPath) if err != nil { return err } page := &Page{} // Try to parse the date from the page filename const layout = "2006-01-02" base := pathpkg.Base(path) if len(base) >= len(layout) { dateStr := base[:len(layout)] if time, err := time.Parse(layout, dateStr); err == nil { page.Date = time // Remove the date from the path base = base[len(layout):] if len(base) > 0 { // Remove a leading dash if base[0] == '-' { base = base[1:] } if len(base) > 0 { dir := pathpkg.Dir(path) if dir == "." { dir = "" } path = pathpkg.Join(dir, base) } } } } // Extract frontmatter from content frontmatter, content := extractFrontmatter(content) if len(frontmatter) != 0 { if err := yaml.Unmarshal(frontmatter, page); err != nil { log.Printf("failed to parse frontmatter for %q: %v", path, err) } // Trim leading newlines from content content = bytes.TrimLeft(content, "\r\n") } if cmd, ok := task.Preprocess[strings.TrimPrefix(ext, ".")]; ok { var buf bytes.Buffer execute(cmd, bytes.NewReader(content), &buf) content = buf.Bytes() } page.Content = string(content) page.FilePath = path if namePrefix == "_index" { page.Permalink = d.Permalink d.index = page } else { if namePrefix == "index" { path = "/" + strings.TrimSuffix(path, name) } else { path = "/" + strings.TrimSuffix(path, ext) if task.UglyURLs { path += task.OutputExt } else { path += "/" } } page.Permalink = path if permalink, ok := cfg.permalinks[d.Permalink]; ok { var b strings.Builder permalink.Execute(&b, page) page.Permalink = b.String() } d.Pages = append(d.Pages, page) } } } return nil } // process processes the directory's contents. func (d *Dir) process(cfg *Site, task *Task) error { // Build feeds before templates are applied to the page contents for _, feed := range task.feeds[d.path] { b, err := d.buildFeed(cfg, feed) if err != nil { return err } d.addFeed(feed.Output, b) } if task.TemplateExt != "" { // Create index if d.index != nil { tmpl, ok := cfg.templates.FindTemplate(d.Permalink, "index"+task.TemplateExt) if ok { var b strings.Builder if err := tmpl.Execute(&b, d); err != nil { return err } d.index.Content = b.String() } } // Process pages for i := range d.Pages { var b strings.Builder tmpl, ok := cfg.templates.FindTemplate(d.Permalink, "page"+task.TemplateExt) if ok { if err := tmpl.Execute(&b, d.Pages[i]); err != nil { return err } d.Pages[i].Content = b.String() } } } // Process subdirectories for _, d := range d.Dirs { if err := d.process(cfg, task); err != nil { return err } } return nil } // buildFeed build the feed of the directory func (d Dir) buildFeed(cfg *Site, feed Feed) ([]byte, error) { // Feed represents a feed. type Feed struct { Title string Permalink string Updated time.Time Pages []*Page } // DeprecatedFeed represents a deprecated feed. type DeprecatedFeed struct { Title string Permalink string Updated time.Time Entries []*Page } tmpl, ok := cfg.templates.FindTemplate(d.Permalink, feed.Template) if !ok { return nil, fmt.Errorf("failed to generate feed %q: missing feed template %q", feed.Title, feed.Template) } var b bytes.Buffer var data interface{} if !feed.deprecated { data = Feed{ Title: feed.Title, Permalink: d.Permalink, Updated: time.Now(), Pages: d.Pages, } } else { data = DeprecatedFeed{ Title: feed.Title, Permalink: d.Permalink, Updated: time.Now(), Entries: d.Pages, } } if err := tmpl.Execute(&b, data); err != nil { return nil, err } return b.Bytes(), nil } func (d *Dir) addFeed(name string, content []byte) { if d.feeds == nil { d.feeds = map[string][]byte{} } d.feeds[name] = content } // write writes the directory's contents to the provided destination path. func (d *Dir) write(dstDir string, task *Task) error { dirPath := pathpkg.Join(dstDir, d.Permalink) // Write pages pages := d.Pages if d.index != nil { pages = append(pages, d.index) } for _, page := range pages { path := page.Permalink if !task.UglyURLs || page == d.index { path = pathpkg.Join(path, "index"+task.OutputExt) } var content []byte if cmd := task.Postprocess; cmd != "" { var buf bytes.Buffer execute(cmd, strings.NewReader(page.Content), &buf) content = buf.Bytes() } else { content = []byte(page.Content) } dstPath := pathpkg.Join(dstDir, path) dir := pathpkg.Dir(dstPath) os.MkdirAll(dir, 0755) if err := os.WriteFile(dstPath, content, 0644); err != nil { return err } } // Write feeds for name, content := range d.feeds { dstPath := pathpkg.Join(dstDir, name) os.MkdirAll(dirPath, 0755) if err := os.WriteFile(dstPath, content, 0644); err != nil { return err } } // Write subdirectories for _, dir := range d.Dirs { dir.write(dstDir, task) } return nil } // sort sorts the directory's pages by weight, then date, then filepath. func (d *Dir) sort() { sort.Slice(d.Pages, func(i, j int) bool { pi, pj := d.Pages[i], d.Pages[j] return pi.FilePath < pj.FilePath }) sort.SliceStable(d.Pages, func(i, j int) bool { pi, pj := d.Pages[i], d.Pages[j] return pi.Date.After(pj.Date) }) sort.SliceStable(d.Pages, func(i, j int) bool { pi, pj := d.Pages[i], d.Pages[j] return pi.Weight < pj.Weight }) for i := range d.Pages { if i-1 >= 0 { d.Pages[i].Prev = d.Pages[i-1] } if i+1 < len(d.Pages) { d.Pages[i].Next = d.Pages[i+1] } } // Sort subdirectories for _, d := range d.Dirs { d.sort() } } // execute runs a command. func execute(command string, input io.Reader, output io.Writer) { split := strings.Split(command, " ") cmd := exec.Command(split[0], split[1:]...) cmd.Stdin = input cmd.Stderr = os.Stderr cmd.Stdout = output err := cmd.Run() if err != nil { log.Fatal(err) } } func (d *Dir) Title() string { return d.index.Title } func (d *Dir) Date() time.Time { return d.index.Date } func (d *Dir) Content() string { return d.index.Content } func (d *Dir) Params() map[string]interface{} { return d.index.Params } func (d *Dir) getDir(path string) *Dir { // XXX: This is inefficient if d.Permalink == path { return d } for _, dir := range d.Dirs { if dir.Permalink == path { return dir } } for i := range d.Dirs { if dir := d.Dirs[i].getDir(path); dir != nil { return dir } } return nil } func (d *Dir) getPage(path string) *Page { // XXX: This is inefficient for _, page := range d.Pages { if page.FilePath == path { return page } } for _, dir := range d.Dirs { if page := dir.getPage(path); page != nil { return page } } return nil }