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. feed []byte // Atom feed. } // 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 + "/"} 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 { 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() } } } // Feed represents a feed. type Feed struct { Title string // Feed title. Permalink string // Feed permalink. Updated time.Time // Last updated time. Entries []*Page // Feed entries. } // Create feeds if title, ok := cfg.Feeds[d.Permalink]; ok { var b bytes.Buffer feed := &Feed{ Title: title, Permalink: d.Permalink, Updated: time.Now(), Entries: d.Pages, } tmpl, ok := cfg.templates.FindTemplate(d.Permalink, "atom.xml") if ok { if err := tmpl.Execute(&b, feed); err != nil { return err } d.feed = b.Bytes() } else { fmt.Printf("Warning: failed to generate feed %q: missing template \"atom.xml\"\n", title) } } // Process subdirectories for _, d := range d.Dirs { if err := d.process(cfg, task); err != nil { return err } } return nil } // 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 the atom feed if d.feed != nil { const path = "atom.xml" dstPath := pathpkg.Join(dirPath, path) os.MkdirAll(dirPath, 0755) if err := os.WriteFile(dstPath, d.feed, 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 }