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