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 { Path 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 Path string `yaml:"-"` Content string `yaml:"-"` Params map[string]string } // NewDir returns a new Dir with the given path. func NewDir(path string) *Dir { if path == "" { path = "/" } else { path = "/" + path + "/" } return &Dir{ Path: path, } } // read reads from a directory and indexes the files and directories within it. func (d *Dir) read(srcDir string, task *Task, cfg *Config) error { return d._read(srcDir, "", task, cfg) } func (d *Dir) _read(srcDir, path string, task *Task, cfg *Config) error { entries, err := ioutil.ReadDir(pathpkg.Join(srcDir, path)) if err != nil { return err } for _, entry := range entries { name := entry.Name() // Ignore names that start with "_" if strings.HasPrefix(name, "_") { continue } path := pathpkg.Join(path, name) if entry.IsDir() { // Gather directory data dir := NewDir(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) { 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 { content = process(cmd, bytes.NewReader(content)) } page.Content = string(content) if strings.TrimSuffix(name, ext) == "index" { page.Path = d.Path d.index = page } else { path = "/" + strings.TrimSuffix(path, ext) if task.UglyURLs { path += task.OutputExt } else { path += "/" } page.Path = path if permalink, ok := cfg.permalinks[d.Path]; ok { var b strings.Builder permalink.Execute(&b, page) page.Path = b.String() } d.Pages = append(d.Pages, page) } } } return nil } // process processes the directory's contents. func (d *Dir) process(cfg *Config, task *Task) error { if task.TemplateExt != "" { // Create index if d.index != nil { tmpl, ok := cfg.templates.FindTemplate(d.Path, "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.Path, "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. Path string // Feed path. Updated time.Time // Last updated time. Entries []*Page // Feed entries. } // Create feeds if title, ok := cfg.Feeds[d.Path]; ok { var b bytes.Buffer feed := &Feed{ Title: title, Path: d.Path, Updated: time.Now(), Entries: d.Pages, } tmpl, ok := cfg.templates.FindTemplate(d.Path, "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 { // Create the directory dirPath := pathpkg.Join(dstDir, d.Path) if err := os.MkdirAll(dirPath, 0755); err != nil { return err } // Write pages pages := d.Pages if d.index != nil { pages = append(pages, d.index) } for _, page := range pages { path := page.Path if !task.UglyURLs || page == d.index { path = pathpkg.Join(path, "index"+task.OutputExt) } var content []byte if cmd := task.Postprocess; cmd != "" { content = process(cmd, strings.NewReader(page.Content)) } 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(dstDir, path) os.MkdirAll(dstDir, 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 date. func (d *Dir) sort() { sort.Slice(d.Pages, func(i, j int) bool { return d.Pages[i].Date.After(d.Pages[j].Date) }) // Sort subdirectories for _, d := range d.Dirs { d.sort() } } // process runs a process command. func process(command string, input io.Reader) []byte { split := strings.Split(command, " ") cmd := exec.Command(split[0], split[1:]...) cmd.Stdin = input cmd.Stderr = os.Stderr output, err := cmd.Output() if err != nil { log.Fatal(err) } return output } 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 }