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}