all repos — vite @ 678fa38599dd50bf47e04e6440675b8e133b543c

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