all repos — vite @ cf6acc3b2905bd905bd44805a159e4bb3b6072e6

a fast (this time, actually) and minimal static site generator

commands/build.go (view raw)

  1package commands
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"sort"
  8	"strings"
  9	"time"
 10
 11	"git.icyphox.sh/vite/atom"
 12	"git.icyphox.sh/vite/config"
 13	"git.icyphox.sh/vite/formats"
 14	"git.icyphox.sh/vite/formats/markdown"
 15	"git.icyphox.sh/vite/formats/yaml"
 16	"git.icyphox.sh/vite/types"
 17	"git.icyphox.sh/vite/util"
 18)
 19
 20type Dir struct {
 21	Name     string
 22	HasIndex bool
 23	Files    []types.File
 24}
 25
 26type Pages struct {
 27	Dirs  []Dir
 28	Files []types.File
 29}
 30
 31func NewPages() (*Pages, error) {
 32	pages := &Pages{}
 33
 34	entries, err := os.ReadDir(types.PagesDir)
 35	if err != nil {
 36		return nil, err
 37	}
 38
 39	for _, entry := range entries {
 40		if entry.IsDir() {
 41			thingsDir := filepath.Join(types.PagesDir, entry.Name())
 42			dir := Dir{Name: entry.Name()}
 43			things, err := os.ReadDir(thingsDir)
 44			if err != nil {
 45				return nil, err
 46			}
 47
 48			for _, thing := range things {
 49				if thing.Name() == "_index.md" {
 50					dir.HasIndex = true
 51					continue
 52				}
 53				switch filepath.Ext(thing.Name()) {
 54				case ".md":
 55					path := filepath.Join(thingsDir, thing.Name())
 56					dir.Files = append(dir.Files, &markdown.Markdown{Path: path})
 57				case ".yaml":
 58					path := filepath.Join(thingsDir, thing.Name())
 59					dir.Files = append(dir.Files, &yaml.YAML{Path: path})
 60				default:
 61					fmt.Printf("warn: unrecognized filetype for file: %s\n", thing.Name())
 62				}
 63			}
 64
 65			pages.Dirs = append(pages.Dirs, dir)
 66		} else {
 67			path := filepath.Join(types.PagesDir, entry.Name())
 68			switch filepath.Ext(entry.Name()) {
 69			case ".md":
 70				pages.Files = append(pages.Files, &markdown.Markdown{Path: path})
 71			case ".yaml":
 72				pages.Files = append(pages.Files, &yaml.YAML{Path: path})
 73			default:
 74				pages.Files = append(pages.Files, formats.Anything{Path: path})
 75			}
 76		}
 77	}
 78
 79	return pages, nil
 80}
 81
 82// Build is the core builder function. Converts markdown/yaml
 83// to html, copies over non-.md/.yaml files, etc.
 84func Build() error {
 85	if err := preBuild(); err != nil {
 86		return err
 87	}
 88	fmt.Println("vite: building")
 89
 90	pages, err := NewPages()
 91	if err != nil {
 92		return fmt.Errorf("error: reading 'pages/' %w", err)
 93	}
 94
 95	if err := util.Clean(types.BuildDir); err != nil {
 96		return err
 97	}
 98
 99	if err := pages.ProcessFiles(); err != nil {
100		return err
101	}
102
103	if err := pages.ProcessDirectories(); err != nil {
104		return err
105	}
106
107	buildStatic := filepath.Join(types.BuildDir, types.StaticDir)
108	if err := os.MkdirAll(buildStatic, 0755); err != nil {
109		return err
110	}
111	if err := util.CopyDir(types.StaticDir, buildStatic); err != nil {
112		return err
113	}
114
115	return nil
116}
117
118// ProcessFiles handles root level files under 'pages',
119// for example: 'pages/_index.md' or 'pages/about.md'.
120func (p *Pages) ProcessFiles() error {
121	for _, f := range p.Files {
122		var htmlDir string
123		if f.Basename() == "_index.md" {
124			htmlDir = types.BuildDir
125		} else {
126			htmlDir = filepath.Join(types.BuildDir, strings.TrimSuffix(f.Basename(), f.Ext()))
127		}
128
129		destFile := filepath.Join(htmlDir, "index.html")
130		if f.Ext() == "" {
131			destFile = filepath.Join(types.BuildDir, f.Basename())
132		} else {
133			if err := os.MkdirAll(htmlDir, 0755); err != nil {
134				return err
135			}
136		}
137		if err := f.Render(destFile, nil); err != nil {
138			return fmt.Errorf("error: failed to render %s: %w", destFile, err)
139		}
140	}
141	return nil
142}
143
144// ProcessDirectories handles directories of posts under 'pages',
145// for example: 'pages/photos/foo.md' or 'pages/blog/bar.md'.
146func (p *Pages) ProcessDirectories() error {
147	for _, dir := range p.Dirs {
148		dstDir := filepath.Join(types.BuildDir, dir.Name)
149		if err := os.MkdirAll(dstDir, 0755); err != nil {
150			return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err)
151		}
152
153		posts := []types.Post{}
154
155		for _, file := range dir.Files {
156			post := types.Post{}
157			// foo-bar.md -> foo-bar
158			slug := strings.TrimSuffix(file.Basename(), file.Ext())
159			dstFile := filepath.Join(dstDir, slug, "index.html")
160
161			// ex: build/blog/foo-bar/
162			if err := os.MkdirAll(filepath.Join(dstDir, slug), 0755); err != nil {
163				return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err)
164			}
165
166			if err := file.Render(dstFile, nil); err != nil {
167				return fmt.Errorf("error: failed to render %s: %w", dstFile, err)
168			}
169
170			post.Meta = file.Frontmatter()
171			post.Body = file.Body()
172			posts = append(posts, post)
173		}
174
175		sort.Slice(posts, func(i, j int) bool {
176			dateStr1 := posts[j].Meta["date"]
177			dateStr2 := posts[i].Meta["date"]
178			date1, _ := time.Parse("2006-01-02", dateStr1)
179			date2, _ := time.Parse("2006-01-02", dateStr2)
180			return date1.Before(date2)
181		})
182
183		if dir.HasIndex {
184			indexMd := filepath.Join(types.PagesDir, dir.Name, "_index.md")
185			index := markdown.Markdown{Path: indexMd}
186			dstFile := filepath.Join(dstDir, "index.html")
187			if err := index.Render(dstFile, posts); err != nil {
188				return fmt.Errorf("error: failed to render index %s: %w", dstFile, err)
189			}
190		}
191
192		xml, err := atom.NewAtomFeed(filepath.Join(types.PagesDir, dir.Name), posts)
193		if err != nil {
194			return fmt.Errorf("error: failed to create atom feed for: %s: %w", dir.Name, err)
195		}
196		feedFile := filepath.Join(dstDir, "feed.xml")
197		os.WriteFile(feedFile, xml, 0755)
198	}
199
200	return nil
201}
202
203func postBuild() error {
204	for _, cmd := range config.Config.PostBuild {
205		fmt.Println("vite: running post-build command:", cmd)
206		if err := util.RunCmd(cmd); err != nil {
207			return err
208		}
209	}
210	return nil
211}
212
213func preBuild() error {
214	for _, cmd := range config.Config.PreBuild {
215		fmt.Println("vite: running pre-build command:", cmd)
216		if err := util.RunCmd(cmd); err != nil {
217			return err
218		}
219	}
220	return nil
221}