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 out.RenderMarkdown(fb)
75 cfg := config.ConfigYaml{}
76 if err := cfg.ParseConfig(); err != nil {
77 return err
78 }
79 if err = out.RenderHTML(
80 htmlFile,
81 TEMPLATES,
82 struct {
83 Cfg config.ConfigYaml
84 Meta markdown.Matter
85 Body string
86 }{cfg, out.Meta, string(out.HTML)},
87 ); err != nil {
88 return err
89 }
90 } else {
91 src := filepath.Join(PAGES, f)
92 dst := filepath.Join(BUILD, f)
93 if err := util.CopyFile(src, dst); err != nil {
94 return err
95 }
96 }
97 }
98 return nil
99}
100
101func (pgs *Pages) processDirs() error {
102 for _, d := range pgs.Dirs {
103 // ex: build/blog
104 dstDir := filepath.Join(BUILD, d)
105 // ex: pages/blog
106 srcDir := filepath.Join(PAGES, d)
107 os.Mkdir(dstDir, 0755)
108
109 entries, err := os.ReadDir(srcDir)
110 if err != nil {
111 return err
112 }
113
114 posts := []markdown.Output{}
115 // Collect all posts
116 for _, e := range entries {
117 // foo-bar.md -> foo-bar
118 slug := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name()))
119
120 // ex: build/blog/foo-bar/
121 os.Mkdir(filepath.Join(dstDir, slug), 0755)
122 // ex: build/blog/foo-bar/index.html
123 htmlFile := filepath.Join(dstDir, slug, "index.html")
124
125 if e.Name() != "_index.md" {
126 ePath := filepath.Join(srcDir, e.Name())
127 fb, err := os.ReadFile(ePath)
128 if err != nil {
129 return err
130 }
131
132 out := markdown.Output{}
133 out.RenderMarkdown(fb)
134 cfg := config.ConfigYaml{}
135 if err := cfg.ParseConfig(); err != nil {
136 return err
137 }
138
139 if err = out.RenderHTML(
140 htmlFile,
141 TEMPLATES,
142 struct {
143 Cfg config.ConfigYaml
144 Meta markdown.Matter
145 Body string
146 }{cfg, out.Meta, string(out.HTML)},
147 ); err != nil {
148 return err
149 }
150 posts = append(posts, out)
151 }
152
153 // Sort posts slice by date
154 sort.Slice(posts, func(i, j int) bool {
155 dateStr1 := posts[j].Meta["date"]
156 dateStr2 := posts[i].Meta["date"]
157 date1, _ := time.Parse("2006-01-02", dateStr1)
158 date2, _ := time.Parse("2006-01-02", dateStr2)
159 return date1.Before(date2)
160 })
161 }
162
163 // Render index using posts slice.
164 // ex: build/blog/index.html
165 indexHTML := filepath.Join(dstDir, "index.html")
166 // ex: pages/blog/_index.md
167 indexMd, err := os.ReadFile(filepath.Join(srcDir, "_index.md"))
168 if err != nil {
169 return err
170 }
171 out := markdown.Output{}
172 out.RenderMarkdown(indexMd)
173 cfg := config.ConfigYaml{}
174 if err := cfg.ParseConfig(); err != nil {
175 return err
176 }
177
178 out.RenderHTML(indexHTML, TEMPLATES, struct {
179 Cfg config.ConfigYaml
180 Meta markdown.Matter
181 Body string
182 Posts []markdown.Output
183 }{cfg, out.Meta, string(out.HTML), posts})
184
185 // Create feeds
186 // ex: build/blog/feed.xml
187 xml, err := atom.NewAtomFeed(d, posts)
188 if err != nil {
189 return err
190 }
191 feedFile := filepath.Join(dstDir, "feed.xml")
192 os.WriteFile(feedFile, xml, 0755)
193 }
194 return nil
195}
196
197// Core builder function. Converts markdown to html,
198// copies over non .md files, etc.
199func Build() error {
200 fmt.Print("vite: building... ")
201 pages := Pages{}
202 if err := pages.initPages(); err != nil {
203 return err
204 }
205
206 // Clean the build directory.
207 if err := util.Clean(BUILD); err != nil {
208 return err
209 }
210
211 wg := sync.WaitGroup{}
212 wg.Add(2)
213 wgDone := make(chan bool)
214
215 ec := make(chan error)
216
217 // Deal with files.
218 // ex: pages/{_index,about,etc}.md
219 go func() {
220 err := pages.processFiles()
221 if err != nil {
222 ec <- err
223 }
224 wg.Done()
225 }()
226
227 // Deal with dirs -- i.e. dirs of markdown files.
228 // ex: pages/{blog,travel}/*.md
229 go func() {
230 err := pages.processDirs()
231 if err != nil {
232 ec <- err
233 }
234 wg.Done()
235 }()
236
237 go func() {
238 wg.Wait()
239 close(wgDone)
240 }()
241
242 select {
243 case <-wgDone:
244 break
245 case err := <-ec:
246 close(ec)
247 return err
248 }
249
250 // Copy the static directory into build
251 // ex: build/static/
252 buildStatic := filepath.Join(BUILD, STATIC)
253 os.Mkdir(buildStatic, 0755)
254 if err := util.CopyDir(STATIC, buildStatic); err != nil {
255 return err
256 }
257
258 fmt.Print("done\n")
259 return nil
260}