package main import ( "bytes" "fmt" "io" "io/ioutil" "log" "os" "os/exec" pathpkg "path" "sort" "strings" "time" "gopkg.in/yaml.v3" ) // Page represents a page. type Page struct { Title string Date time.Time Weight int Params map[string]interface{} FilePath string `yaml:"-"` Permalink string `yaml:"-"` Content string `yaml:"-"` Prev *Page `yaml:"-"` Next *Page `yaml:"-"` Pages []*Page `yaml:"-"` Dirs []*Page `yaml:"-"` feeds map[string][]byte index bool } // read reads from a directory and indexes the files and directories within it. func (p *Page) read(srcDir string, task *Task, cfg *Site) error { return p._read(srcDir, "", task, cfg) } func (p *Page) _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 := &Page{Permalink: "/" + path + "/", FilePath: path} if err := dir._read(srcDir, path, task, cfg); err != nil { return err } p.Dirs = append(p.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{ FilePath: path, } if namePrefix == "_index" { p.index = true page = p } // 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 if err := execute(cmd, bytes.NewReader(content), &buf); err != nil { return err } content = buf.Bytes() } page.Content = string(content) if !page.index { 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[p.Permalink]; ok { var b strings.Builder permalink.Execute(&b, page) page.Permalink = b.String() } p.Pages = append(p.Pages, page) } } } return nil } // process processes the directory's contents. func (p *Page) process(cfg *Site, task *Task) error { // Build feeds before templates are applied to the page contents for _, feed := range task.feeds[p.FilePath] { b, err := p.buildFeed(cfg, feed) if err != nil { return err } p.addFeed(feed.Output, b) } if task.TemplateExt != "" { // Create index if p.index { tmpl, ok := cfg.templates.FindTemplate(p.Permalink, "index"+task.TemplateExt) if ok { var b strings.Builder if err := tmpl.Execute(&b, p); err != nil { return err } p.Content = b.String() } } // Process pages for i := range p.Pages { var b strings.Builder tmpl, ok := cfg.templates.FindTemplate(p.Permalink, "page"+task.TemplateExt) if ok { if err := tmpl.Execute(&b, p.Pages[i]); err != nil { return err } p.Pages[i].Content = b.String() } } } // Process subdirectories for _, d := range p.Dirs { if err := d.process(cfg, task); err != nil { return err } } return nil } // buildFeed build the feed of the directory func (p *Page) buildFeed(cfg *Site, feed Feed) ([]byte, error) { // Feed represents a feed. type Feed struct { Title string Permalink string Pages []*Page } tmpl, ok := cfg.templates.FindTemplate(p.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 data := Feed{ Title: feed.Title, Permalink: p.Permalink, Pages: p.Pages, } if err := tmpl.Execute(&b, data); err != nil { return nil, err } return b.Bytes(), nil } func (p *Page) addFeed(name string, content []byte) { if p.feeds == nil { p.feeds = map[string][]byte{} } p.feeds[name] = content } // write writes the directory's contents to the provided destination path. func (p *Page) write(dstDir string, task *Task) error { dirPath := pathpkg.Join(dstDir, p.Permalink) // Write pages for _, page := range p.Pages { dstPath := pathpkg.Join(dstDir, page.Permalink) if !task.UglyURLs { dstPath = pathpkg.Join(dstPath, "index"+task.OutputExt) } if err := page.writeTo(dstPath, task); err != nil { return err } } // Write index page if p.index { dstPath := pathpkg.Join(dirPath, p.Permalink, "index"+task.OutputExt) if err := p.writeTo(dstPath, task); err != nil { return err } } // Write feeds for name, content := range p.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 p.Dirs { dir.write(dstDir, task) } return nil } func (p *Page) writeTo(dstPath string, task *Task) error { var content []byte if cmd := task.Postprocess; cmd != "" { var buf bytes.Buffer if err := execute(cmd, strings.NewReader(p.Content), &buf); err != nil { return err } content = buf.Bytes() } else { content = []byte(p.Content) } dir := pathpkg.Dir(dstPath) os.MkdirAll(dir, 0755) if err := os.WriteFile(dstPath, content, 0644); err != nil { return err } return nil } // sort sorts the directory's pages by weight, then date, then filepath. func (p *Page) sort() { sort.Slice(p.Pages, func(i, j int) bool { pi, pj := p.Pages[i], p.Pages[j] return pi.FilePath < pj.FilePath }) sort.SliceStable(p.Pages, func(i, j int) bool { pi, pj := p.Pages[i], p.Pages[j] return pi.Date.After(pj.Date) }) sort.SliceStable(p.Pages, func(i, j int) bool { pi, pj := p.Pages[i], p.Pages[j] return pi.Weight < pj.Weight }) for i := range p.Pages { if i-1 >= 0 { p.Pages[i].Prev = p.Pages[i-1] } if i+1 < len(p.Pages) { p.Pages[i].Next = p.Pages[i+1] } } // Sort subdirectories for _, d := range p.Dirs { d.sort() } } // execute runs a command. func execute(command string, input io.Reader, output io.Writer) error { 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 { return err } return nil } func (p *Page) getPage(path string) *Page { // XXX: This is inefficient if p.Permalink == path { return p } for _, page := range p.Pages { if page.FilePath == path { return page } } for _, dir := range p.Dirs { if dir.Permalink == path { return dir } } for _, dir := range p.Dirs { if page := dir.getPage(path); page != nil { return page } } return nil }