all repos — vite @ e0dc7bd326a18d8004efd5b706b941b2265ad64b

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			cfg := config.ConfigYaml{}
 74			if err := cfg.ParseConfig(); 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				}{cfg, 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				out.RenderMarkdown(fb)
132				cfg := config.ConfigYaml{}
133				if err := cfg.ParseConfig(); err != nil {
134					return err
135				}
136
137				if err = out.RenderHTML(
138					htmlFile,
139					TEMPLATES,
140					struct {
141						Cfg  config.ConfigYaml
142						Meta markdown.Matter
143						Body string
144					}{cfg, out.Meta, string(out.HTML)},
145				); err != nil {
146					return err
147				}
148				posts = append(posts, out)
149			}
150
151			// Sort posts slice by date
152			sort.Slice(posts, func(i, j int) bool {
153				dateStr1 := posts[j].Meta["date"]
154				dateStr2 := posts[i].Meta["date"]
155				date1, _ := time.Parse("2006-01-02", dateStr1)
156				date2, _ := time.Parse("2006-01-02", dateStr2)
157				return date1.Before(date2)
158			})
159		}
160
161		// Render index using posts slice.
162		// ex: build/blog/index.html
163		indexHTML := filepath.Join(dstDir, "index.html")
164		// ex: pages/blog/_index.md
165		indexMd, err := os.ReadFile(filepath.Join(srcDir, "_index.md"))
166		if err != nil {
167			return err
168		}
169		out := markdown.Output{}
170		out.RenderMarkdown(indexMd)
171		cfg := config.ConfigYaml{}
172		if err := cfg.ParseConfig(); err != nil {
173			return err
174		}
175
176		out.RenderHTML(indexHTML, TEMPLATES, struct {
177			Cfg   config.ConfigYaml
178			Meta  markdown.Matter
179			Body  string
180			Posts []markdown.Output
181		}{cfg, out.Meta, string(out.HTML), posts})
182
183		// Create feeds
184		// ex: build/blog/feed.xml
185		xml, err := atom.NewAtomFeed(d, posts)
186		if err != nil {
187			return err
188		}
189		feedFile := filepath.Join(dstDir, "feed.xml")
190		os.WriteFile(feedFile, xml, 0755)
191	}
192	return nil
193}
194
195// Core builder function. Converts markdown to html,
196// copies over non .md files, etc.
197func Build() error {
198	pages := Pages{}
199	if err := pages.initPages(); err != nil {
200		return err
201	}
202
203	// Clean the build directory.
204	if err := util.Clean(BUILD); err != nil {
205		return err
206	}
207
208	// Deal with files.
209	// ex: pages/{_index,about,etc}.md
210	if err := pages.processFiles(); err != nil {
211		return err
212	}
213
214	// Deal with dirs -- i.e. dirs of markdown files.
215	// ex: pages/{blog,travel}/*.md
216	if err := pages.processDirs(); err != nil {
217		return err
218	}
219
220	// Copy the static directory into build
221	// ex: build/static/
222	buildStatic := filepath.Join(BUILD, STATIC)
223	os.Mkdir(buildStatic, 0755)
224	if err := util.CopyDir(STATIC, buildStatic); err != nil {
225		return err
226	}
227
228	return nil
229}