commands/build.go (view raw)
1package commands
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "sort"
8 "strings"
9 "sync"
10 "time"
11
12 "git.icyphox.sh/vite/atom"
13 "git.icyphox.sh/vite/config"
14 "git.icyphox.sh/vite/markdown"
15 "git.icyphox.sh/vite/util"
16)
17
18const (
19 BUILD = "build"
20 PAGES = "pages"
21 TEMPLATES = "templates"
22 STATIC = "static"
23)
24
25type Pages struct {
26 Dirs []string
27 Files []string
28}
29
30// Populates a Pages object with dirs and files
31// found in 'pages/'.
32func (pgs *Pages) initPages() error {
33 files, err := os.ReadDir("./pages")
34 if err != nil {
35 return err
36 }
37
38 for _, f := range files {
39 if f.IsDir() {
40 pgs.Dirs = append(pgs.Dirs, f.Name())
41 } else {
42 pgs.Files = append(pgs.Files, f.Name())
43 }
44 }
45
46 return nil
47}
48
49func (pgs *Pages) processFiles() error {
50 for _, f := range pgs.Files {
51 if filepath.Ext(f) == ".md" {
52 // ex: pages/about.md
53 mdFile := filepath.Join(PAGES, f)
54 var htmlDir string
55 // ex: build/index.html (root index)
56 if f == "_index.md" {
57 htmlDir = BUILD
58 } else {
59 htmlDir = filepath.Join(
60 BUILD,
61 strings.TrimSuffix(f, ".md"),
62 )
63 }
64 os.Mkdir(htmlDir, 0755)
65 // ex: build/about/index.html
66 htmlFile := filepath.Join(htmlDir, "index.html")
67
68 fb, err := os.ReadFile(mdFile)
69 if err != nil {
70 return err
71 }
72
73 out := markdown.Output{}
74 if err = out.RenderMarkdown(fb); err != nil {
75 return err
76 }
77 if err = out.RenderHTML(
78 htmlFile,
79 TEMPLATES,
80 struct {
81 Cfg config.ConfigYaml
82 Meta markdown.Matter
83 Body string
84 }{config.Config, out.Meta, string(out.HTML)},
85 ); err != nil {
86 return err
87 }
88 } else {
89 src := filepath.Join(PAGES, f)
90 dst := filepath.Join(BUILD, f)
91 if err := util.CopyFile(src, dst); err != nil {
92 return err
93 }
94 }
95 }
96 return nil
97}
98
99func (pgs *Pages) processDirs() error {
100 for _, d := range pgs.Dirs {
101 // ex: build/blog
102 dstDir := filepath.Join(BUILD, d)
103 // ex: pages/blog
104 srcDir := filepath.Join(PAGES, d)
105 os.Mkdir(dstDir, 0755)
106
107 entries, err := os.ReadDir(srcDir)
108 if err != nil {
109 return err
110 }
111
112 posts := []markdown.Output{}
113 // Collect all posts
114 for _, e := range entries {
115 // foo-bar.md -> foo-bar
116 slug := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name()))
117
118 // ex: build/blog/foo-bar/
119 os.Mkdir(filepath.Join(dstDir, slug), 0755)
120 // ex: build/blog/foo-bar/index.html
121 htmlFile := filepath.Join(dstDir, slug, "index.html")
122
123 if e.Name() != "_index.md" {
124 ePath := filepath.Join(srcDir, e.Name())
125 fb, err := os.ReadFile(ePath)
126 if err != nil {
127 return err
128 }
129
130 out := markdown.Output{}
131 if err := out.RenderMarkdown(fb); err != nil {
132 return err
133 }
134 if err = out.RenderHTML(
135 htmlFile,
136 TEMPLATES,
137 struct {
138 Cfg config.ConfigYaml
139 Meta markdown.Matter
140 Body string
141 }{config.Config, out.Meta, string(out.HTML)},
142 ); err != nil {
143 return err
144 }
145 posts = append(posts, out)
146 }
147
148 // Sort posts slice by date
149 sort.Slice(posts, func(i, j int) bool {
150 dateStr1 := posts[j].Meta["date"]
151 dateStr2 := posts[i].Meta["date"]
152 date1, _ := time.Parse("2006-01-02", dateStr1)
153 date2, _ := time.Parse("2006-01-02", dateStr2)
154 return date1.Before(date2)
155 })
156 }
157
158 // Render index using posts slice.
159 // ex: build/blog/index.html
160 indexHTML := filepath.Join(dstDir, "index.html")
161 // ex: pages/blog/_index.md
162 indexMd, err := os.ReadFile(filepath.Join(srcDir, "_index.md"))
163 if err != nil {
164 return err
165 }
166 out := markdown.Output{}
167 if err := out.RenderMarkdown(indexMd); err != nil {
168 return err
169 }
170
171 out.RenderHTML(indexHTML, TEMPLATES, struct {
172 Cfg config.ConfigYaml
173 Meta markdown.Matter
174 Body string
175 Posts []markdown.Output
176 }{config.Config, out.Meta, string(out.HTML), posts})
177
178 // Create feeds
179 // ex: build/blog/feed.xml
180 xml, err := atom.NewAtomFeed(d, posts)
181 if err != nil {
182 return err
183 }
184 feedFile := filepath.Join(dstDir, "feed.xml")
185 os.WriteFile(feedFile, xml, 0755)
186 }
187 return nil
188}
189
190// Core builder function. Converts markdown to html,
191// copies over non .md files, etc.
192func Build() error {
193 fmt.Print("vite: building... ")
194 pages := Pages{}
195 if err := pages.initPages(); err != nil {
196 return err
197 }
198
199 // Clean the build directory.
200 if err := util.Clean(BUILD); err != nil {
201 return err
202 }
203
204 wg := sync.WaitGroup{}
205 wg.Add(2)
206 wgDone := make(chan bool)
207
208 ec := make(chan error)
209
210 // Deal with files.
211 // ex: pages/{_index,about,etc}.md
212 go func() {
213 err := pages.processFiles()
214 if err != nil {
215 ec <- err
216 }
217 wg.Done()
218 }()
219
220 // Deal with dirs -- i.e. dirs of markdown files.
221 // ex: pages/{blog,travel}/*.md
222 go func() {
223 err := pages.processDirs()
224 if err != nil {
225 ec <- err
226 }
227 wg.Done()
228 }()
229
230 go func() {
231 wg.Wait()
232 close(wgDone)
233 }()
234
235 select {
236 case <-wgDone:
237 break
238 case err := <-ec:
239 close(ec)
240 return err
241 }
242
243 // Copy the static directory into build
244 // ex: build/static/
245 buildStatic := filepath.Join(BUILD, STATIC)
246 os.Mkdir(buildStatic, 0755)
247 if err := util.CopyDir(STATIC, buildStatic); err != nil {
248 return err
249 }
250
251 fmt.Print("done\n")
252 return nil
253}