all repos — vite @ fbcc6c93a3aa3282d34c7c90a223c9535e8d2b94

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	"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}