all repos — vite @ d3cb74b6d1df9cca082c7fab84325b36c0bce969

a fast (this time, actually) and minimal static site generator

commands/build.go (view raw)

  1package commands
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"sort"
  7	"strings"
  8	"time"
  9
 10	"git.icyphox.sh/vite/atom"
 11	"git.icyphox.sh/vite/config"
 12	"git.icyphox.sh/vite/markdown"
 13	"git.icyphox.sh/vite/util"
 14)
 15
 16const (
 17	BUILD     = "build"
 18	PAGES     = "pages"
 19	TEMPLATES = "templates"
 20	STATIC    = "static"
 21)
 22
 23type Pages struct {
 24	Dirs  []string
 25	Files []string
 26}
 27
 28// Populates a Pages object with dirs and files
 29// found in 'pages/'.
 30func (pgs *Pages) initPages() error {
 31	files, err := os.ReadDir("./pages")
 32	if err != nil {
 33		return err
 34	}
 35
 36	for _, f := range files {
 37		if f.IsDir() {
 38			pgs.Dirs = append(pgs.Dirs, f.Name())
 39		} else {
 40			pgs.Files = append(pgs.Files, f.Name())
 41		}
 42	}
 43
 44	return nil
 45}
 46
 47func (pgs *Pages) processFiles() error {
 48	for _, f := range pgs.Files {
 49		if filepath.Ext(f) == ".md" {
 50			// ex: pages/about.md
 51			mdFile := filepath.Join(PAGES, f)
 52			var htmlDir string
 53			// ex: build/index.html (root index)
 54			if f == "_index.md" {
 55				htmlDir = BUILD
 56			} else {
 57				htmlDir = filepath.Join(
 58					BUILD,
 59					strings.TrimSuffix(f, ".md"),
 60				)
 61			}
 62			os.Mkdir(htmlDir, 0755)
 63			// ex: build/about/index.html
 64			htmlFile := filepath.Join(htmlDir, "index.html")
 65
 66			fb, err := os.ReadFile(mdFile)
 67			if err != nil {
 68				return err
 69			}
 70
 71			out := markdown.Output{}
 72			out.RenderMarkdown(fb)
 73			if err = out.RenderHTML(
 74				htmlFile,
 75				TEMPLATES,
 76				struct {
 77					Cfg  config.ConfigYaml
 78					Meta markdown.Matter
 79					Body string
 80				}{config.Config, out.Meta, string(out.HTML)},
 81			); err != nil {
 82				return err
 83			}
 84		} else {
 85			src := filepath.Join(PAGES, f)
 86			dst := filepath.Join(BUILD, f)
 87			if err := util.CopyFile(src, dst); err != nil {
 88				return err
 89			}
 90		}
 91	}
 92	return nil
 93}
 94
 95func (pgs *Pages) processDirs() error {
 96	for _, d := range pgs.Dirs {
 97		// ex: build/blog
 98		dstDir := filepath.Join(BUILD, d)
 99		// ex: pages/blog
100		srcDir := filepath.Join(PAGES, d)
101		os.Mkdir(dstDir, 0755)
102
103		entries, err := os.ReadDir(srcDir)
104		if err != nil {
105			return err
106		}
107
108		posts := []markdown.Output{}
109		// Collect all posts
110		for _, e := range entries {
111			// foo-bar.md -> foo-bar
112			slug := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name()))
113
114			// ex: build/blog/foo-bar/
115			os.Mkdir(filepath.Join(dstDir, slug), 0755)
116			// ex: build/blog/foo-bar/index.html
117			htmlFile := filepath.Join(dstDir, slug, "index.html")
118
119			if e.Name() != "_index.md" {
120				ePath := filepath.Join(srcDir, e.Name())
121				fb, err := os.ReadFile(ePath)
122				if err != nil {
123					return err
124				}
125
126				out := markdown.Output{}
127				out.RenderMarkdown(fb)
128
129				if err = out.RenderHTML(
130					htmlFile,
131					TEMPLATES,
132					struct {
133						Cfg  config.ConfigYaml
134						Meta markdown.Matter
135						Body string
136					}{config.Config, out.Meta, string(out.HTML)},
137				); err != nil {
138					return err
139				}
140				posts = append(posts, out)
141			}
142
143			// Sort posts slice by date
144			sort.Slice(posts, func(i, j int) bool {
145				dateStr1 := posts[j].Meta["date"]
146				dateStr2 := posts[i].Meta["date"]
147				date1, _ := time.Parse("2006-01-02", dateStr1)
148				date2, _ := time.Parse("2006-01-02", dateStr2)
149				return date1.Before(date2)
150			})
151		}
152
153		// Render index using posts slice.
154		// ex: build/blog/index.html
155		indexHTML := filepath.Join(dstDir, "index.html")
156		// ex: pages/blog/_index.md
157		indexMd, err := os.ReadFile(filepath.Join(srcDir, "_index.md"))
158		if err != nil {
159			return err
160		}
161		out := markdown.Output{}
162		out.RenderMarkdown(indexMd)
163
164		out.RenderHTML(indexHTML, TEMPLATES, struct {
165			Cfg   config.ConfigYaml
166			Meta  markdown.Matter
167			Body  string
168			Posts []markdown.Output
169		}{config.Config, out.Meta, string(out.HTML), posts})
170
171		// Create feeds
172		// ex: build/blog/feed.xml
173		xml, err := atom.NewAtomFeed(d, posts)
174		if err != nil {
175			return err
176		}
177		feedFile := filepath.Join(dstDir, "feed.xml")
178		os.WriteFile(feedFile, xml, 0755)
179	}
180	return nil
181}
182
183// Core builder function. Converts markdown to html,
184// copies over non .md files, etc.
185func Build() error {
186	pages := Pages{}
187	if err := pages.initPages(); err != nil {
188		return err
189	}
190
191	// Clean the build directory.
192	if err := util.Clean(BUILD); err != nil {
193		return err
194	}
195
196	// Deal with files.
197	// ex: pages/{_index,about,etc}.md
198	if err := pages.processFiles(); err != nil {
199		return err
200	}
201
202	// Deal with dirs -- i.e. dirs of markdown files.
203	// ex: pages/{blog,travel}/*.md
204	if err := pages.processDirs(); err != nil {
205		return err
206	}
207
208	// Copy the static directory into build
209	// ex: build/static/
210	buildStatic := filepath.Join(BUILD, STATIC)
211	os.Mkdir(buildStatic, 0755)
212	if err := util.CopyDir(STATIC, buildStatic); err != nil {
213		return err
214	}
215
216	return nil
217}