all repos — vite @ a15978fe24a2e105d347adbc857a0af0c143bdeb

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