fast and minimal static site generator
ssg

2024 rewrite

+514 -419
+3 -3
atom/feed.go
··· 7 7 "time" 8 8 9 9 "git.icyphox.sh/vite/config" 10 - "git.icyphox.sh/vite/markdown" 10 + "git.icyphox.sh/vite/types" 11 11 ) 12 12 13 13 type AtomLink struct { ··· 50 50 } 51 51 52 52 // Creates a new Atom feed. 53 - func NewAtomFeed(srcDir string, posts []markdown.Output) ([]byte, error) { 53 + func NewAtomFeed(srcDir string, posts []types.Post) ([]byte, error) { 54 54 entries := []AtomEntry{} 55 55 56 56 for _, p := range posts { ··· 76 76 Summary: &AtomSummary{ 77 77 Content: fmt.Sprintf("<h2>%s</h2>\n%s", 78 78 p.Meta["subtitle"], 79 - string(p.HTML)), 79 + string(p.Body)), 80 80 Type: "html", 81 81 }, 82 82 }
-318
commands/build.go
··· 1 - package commands 2 - 3 - import ( 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 - "gopkg.in/yaml.v3" 17 - ) 18 - 19 - const ( 20 - BuildDir = "build" 21 - PagesDir = "pages" 22 - TemplatesDir = "templates" 23 - StaticDir = "static" 24 - ) 25 - 26 - type Pages struct { 27 - Dirs []string 28 - Files []string 29 - } 30 - 31 - // Populates a Pages object with dirs and files 32 - // found in 'pages/'. 33 - func (pgs *Pages) initPages() error { 34 - files, err := os.ReadDir("./pages") 35 - if err != nil { 36 - return err 37 - } 38 - 39 - for _, f := range files { 40 - if f.IsDir() { 41 - pgs.Dirs = append(pgs.Dirs, f.Name()) 42 - } else { 43 - pgs.Files = append(pgs.Files, f.Name()) 44 - } 45 - } 46 - 47 - return nil 48 - } 49 - 50 - func (pgs *Pages) processFiles() error { 51 - for _, f := range pgs.Files { 52 - switch filepath.Ext(f) { 53 - case ".md": 54 - // ex: pages/about.md 55 - mdFile := filepath.Join(PagesDir, f) 56 - var htmlDir string 57 - // ex: build/index.html (root index) 58 - if f == "_index.md" { 59 - htmlDir = BuildDir 60 - } else { 61 - htmlDir = filepath.Join( 62 - BuildDir, 63 - strings.TrimSuffix(f, ".md"), 64 - ) 65 - } 66 - os.Mkdir(htmlDir, 0755) 67 - // ex: build/about/index.html 68 - htmlFile := filepath.Join(htmlDir, "index.html") 69 - 70 - fb, err := os.ReadFile(mdFile) 71 - if err != nil { 72 - return err 73 - } 74 - 75 - out := markdown.Output{} 76 - if err = out.RenderMarkdown(fb); err != nil { 77 - return err 78 - } 79 - if err = out.RenderHTML( 80 - htmlFile, 81 - TemplatesDir, 82 - struct { 83 - Cfg config.ConfigYaml 84 - Meta markdown.Matter 85 - Body string 86 - }{config.Config, out.Meta, string(out.HTML)}, 87 - ); err != nil { 88 - return err 89 - } 90 - case ".yaml": 91 - // ex: pages/reading.yaml 92 - yamlFile := filepath.Join(PagesDir, f) 93 - htmlDir := filepath.Join(BuildDir, strings.TrimSuffix(f, ".yaml")) 94 - os.Mkdir(htmlDir, 0755) 95 - htmlFile := filepath.Join(htmlDir, "index.html") 96 - 97 - yb, err := os.ReadFile(yamlFile) 98 - if err != nil { 99 - return err 100 - } 101 - 102 - data := map[string]interface{}{} 103 - err = yaml.Unmarshal(yb, &data) 104 - if err != nil { 105 - return fmt.Errorf("error: unmarshalling yaml file %s: %v", yamlFile, err) 106 - } 107 - 108 - meta := make(map[string]string) 109 - for k, v := range data["meta"].(map[string]interface{}) { 110 - meta[k] = v.(string) 111 - } 112 - 113 - out := markdown.Output{} 114 - out.Meta = meta 115 - if err = out.RenderHTML( 116 - htmlFile, 117 - TemplatesDir, 118 - struct { 119 - Cfg config.ConfigYaml 120 - Meta markdown.Matter 121 - Yaml map[string]interface{} 122 - Body string 123 - }{config.Config, meta, data, ""}, 124 - ); err != nil { 125 - return err 126 - } 127 - default: 128 - src := filepath.Join(PagesDir, f) 129 - dst := filepath.Join(BuildDir, f) 130 - if err := util.CopyFile(src, dst); err != nil { 131 - return err 132 - } 133 - } 134 - } 135 - return nil 136 - } 137 - 138 - func (pgs *Pages) processDirs() error { 139 - for _, d := range pgs.Dirs { 140 - // ex: build/blog 141 - dstDir := filepath.Join(BuildDir, d) 142 - // ex: pages/blog 143 - srcDir := filepath.Join(PagesDir, d) 144 - os.Mkdir(dstDir, 0755) 145 - 146 - entries, err := os.ReadDir(srcDir) 147 - if err != nil { 148 - return err 149 - } 150 - 151 - posts := []markdown.Output{} 152 - // Collect all posts 153 - for _, e := range entries { 154 - // foo-bar.md -> foo-bar 155 - slug := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name())) 156 - 157 - // ex: build/blog/foo-bar/ 158 - os.Mkdir(filepath.Join(dstDir, slug), 0755) 159 - // ex: build/blog/foo-bar/index.html 160 - htmlFile := filepath.Join(dstDir, slug, "index.html") 161 - 162 - if e.Name() != "_index.md" { 163 - ePath := filepath.Join(srcDir, e.Name()) 164 - fb, err := os.ReadFile(ePath) 165 - if err != nil { 166 - return err 167 - } 168 - 169 - out := markdown.Output{} 170 - if err := out.RenderMarkdown(fb); err != nil { 171 - return err 172 - } 173 - if err = out.RenderHTML( 174 - htmlFile, 175 - TemplatesDir, 176 - struct { 177 - Cfg config.ConfigYaml 178 - Meta markdown.Matter 179 - Body string 180 - }{config.Config, out.Meta, string(out.HTML)}, 181 - ); err != nil { 182 - return err 183 - } 184 - posts = append(posts, out) 185 - } 186 - 187 - // Sort posts slice by date 188 - sort.Slice(posts, func(i, j int) bool { 189 - dateStr1 := posts[j].Meta["date"] 190 - dateStr2 := posts[i].Meta["date"] 191 - date1, _ := time.Parse("2006-01-02", dateStr1) 192 - date2, _ := time.Parse("2006-01-02", dateStr2) 193 - return date1.Before(date2) 194 - }) 195 - } 196 - 197 - // Render index using posts slice. 198 - // ex: build/blog/index.html 199 - indexHTML := filepath.Join(dstDir, "index.html") 200 - // ex: pages/blog/_index.md 201 - indexMd, err := os.ReadFile(filepath.Join(srcDir, "_index.md")) 202 - if err != nil { 203 - return err 204 - } 205 - out := markdown.Output{} 206 - if err := out.RenderMarkdown(indexMd); err != nil { 207 - return err 208 - } 209 - 210 - out.RenderHTML(indexHTML, TemplatesDir, struct { 211 - Cfg config.ConfigYaml 212 - Meta markdown.Matter 213 - Body string 214 - Posts []markdown.Output 215 - }{config.Config, out.Meta, string(out.HTML), posts}) 216 - 217 - // Create feeds 218 - // ex: build/blog/feed.xml 219 - xml, err := atom.NewAtomFeed(d, posts) 220 - if err != nil { 221 - return err 222 - } 223 - feedFile := filepath.Join(dstDir, "feed.xml") 224 - os.WriteFile(feedFile, xml, 0755) 225 - } 226 - return nil 227 - } 228 - 229 - // Core builder function. Converts markdown to html, 230 - // copies over non .md files, etc. 231 - func Build() error { 232 - if err := preBuild(); err != nil { 233 - return err 234 - } 235 - fmt.Print("vite: building... ") 236 - pages := Pages{} 237 - if err := pages.initPages(); err != nil { 238 - return err 239 - } 240 - 241 - // Clean the build directory. 242 - if err := util.Clean(BuildDir); err != nil { 243 - return err 244 - } 245 - 246 - wg := sync.WaitGroup{} 247 - wg.Add(2) 248 - wgDone := make(chan bool) 249 - 250 - ec := make(chan error) 251 - 252 - // Deal with files. 253 - // ex: pages/{_index,about,etc}.md 254 - go func() { 255 - err := pages.processFiles() 256 - if err != nil { 257 - ec <- err 258 - } 259 - wg.Done() 260 - }() 261 - 262 - // Deal with dirs -- i.e. dirs of markdown files. 263 - // ex: pages/{blog,travel}/*.md 264 - go func() { 265 - err := pages.processDirs() 266 - if err != nil { 267 - ec <- err 268 - } 269 - wg.Done() 270 - }() 271 - 272 - go func() { 273 - wg.Wait() 274 - close(wgDone) 275 - }() 276 - 277 - select { 278 - case <-wgDone: 279 - break 280 - case err := <-ec: 281 - close(ec) 282 - return err 283 - } 284 - 285 - // Copy the static directory into build 286 - // ex: build/static/ 287 - buildStatic := filepath.Join(BuildDir, StaticDir) 288 - os.Mkdir(buildStatic, 0755) 289 - if err := util.CopyDir(StaticDir, buildStatic); err != nil { 290 - return err 291 - } 292 - fmt.Print("done\n") 293 - 294 - if err := postBuild(); err != nil { 295 - return err 296 - } 297 - return nil 298 - } 299 - 300 - func postBuild() error { 301 - for _, cmd := range config.Config.PostBuild { 302 - fmt.Println("vite: running post-build command:", cmd) 303 - if err := util.RunCmd(cmd); err != nil { 304 - return err 305 - } 306 - } 307 - return nil 308 - } 309 - 310 - func preBuild() error { 311 - for _, cmd := range config.Config.PreBuild { 312 - fmt.Println("vite: running pre-build command:", cmd) 313 - if err := util.RunCmd(cmd); err != nil { 314 - return err 315 - } 316 - } 317 - return nil 318 - }
+222
commands/build/build.go
··· 1 + package build 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + "path/filepath" 7 + "sort" 8 + "strings" 9 + "time" 10 + 11 + "git.icyphox.sh/vite/atom" 12 + "git.icyphox.sh/vite/config" 13 + "git.icyphox.sh/vite/formats" 14 + "git.icyphox.sh/vite/formats/markdown" 15 + "git.icyphox.sh/vite/formats/yaml" 16 + "git.icyphox.sh/vite/types" 17 + "git.icyphox.sh/vite/util" 18 + ) 19 + 20 + type Dir struct { 21 + Name string 22 + HasIndex bool 23 + Files []types.File 24 + } 25 + 26 + type Pages struct { 27 + Dirs []Dir 28 + Files []types.File 29 + } 30 + 31 + func NewPages() (*Pages, error) { 32 + pages := &Pages{} 33 + 34 + entries, err := os.ReadDir(types.PagesDir) 35 + if err != nil { 36 + return nil, err 37 + } 38 + 39 + for _, entry := range entries { 40 + if entry.IsDir() { 41 + thingsDir := filepath.Join(types.PagesDir, entry.Name()) 42 + dir := Dir{Name: entry.Name()} 43 + things, err := os.ReadDir(thingsDir) 44 + if err != nil { 45 + return nil, err 46 + } 47 + 48 + for _, thing := range things { 49 + if thing.Name() == "_index.md" { 50 + dir.HasIndex = true 51 + continue 52 + } 53 + switch filepath.Ext(thing.Name()) { 54 + case ".md": 55 + path := filepath.Join(thingsDir, thing.Name()) 56 + dir.Files = append(dir.Files, &markdown.Markdown{Path: path}) 57 + case ".yaml": 58 + path := filepath.Join(thingsDir, thing.Name()) 59 + dir.Files = append(dir.Files, &yaml.YAML{Path: path}) 60 + default: 61 + fmt.Printf("warn: unrecognized filetype for file: %s\n", thing.Name()) 62 + } 63 + } 64 + 65 + pages.Dirs = append(pages.Dirs, dir) 66 + } else { 67 + path := filepath.Join(types.PagesDir, entry.Name()) 68 + switch filepath.Ext(entry.Name()) { 69 + case ".md": 70 + pages.Files = append(pages.Files, &markdown.Markdown{Path: path}) 71 + case ".yaml": 72 + pages.Files = append(pages.Files, &yaml.YAML{Path: path}) 73 + default: 74 + pages.Files = append(pages.Files, formats.Anything{Path: path}) 75 + } 76 + } 77 + } 78 + 79 + return pages, nil 80 + } 81 + 82 + // Build is the core builder function. Converts markdown/yaml 83 + // to html, copies over non-.md/.yaml files, etc. 84 + func Build() error { 85 + if err := preBuild(); err != nil { 86 + return err 87 + } 88 + fmt.Println("vite: building") 89 + 90 + pages, err := NewPages() 91 + if err != nil { 92 + return fmt.Errorf("error: reading 'pages/' %w", err) 93 + } 94 + 95 + if err := util.Clean(types.BuildDir); err != nil { 96 + return err 97 + } 98 + 99 + if err := pages.ProcessFiles(); err != nil { 100 + return err 101 + } 102 + 103 + if err := pages.ProcessDirectories(); err != nil { 104 + return err 105 + } 106 + 107 + buildStatic := filepath.Join(types.BuildDir, types.StaticDir) 108 + if err := os.MkdirAll(buildStatic, 0755); err != nil { 109 + return err 110 + } 111 + if err := util.CopyDir(types.StaticDir, buildStatic); err != nil { 112 + return err 113 + } 114 + fmt.Println("done") 115 + 116 + return nil 117 + } 118 + 119 + // ProcessFiles handles root level files under 'pages', 120 + // for example: 'pages/_index.md' or 'pages/about.md'. 121 + func (p *Pages) ProcessFiles() error { 122 + for _, f := range p.Files { 123 + var htmlDir string 124 + if f.Basename() == "_index.md" { 125 + htmlDir = types.BuildDir 126 + } else { 127 + htmlDir = filepath.Join(types.BuildDir, strings.TrimSuffix(f.Basename(), f.Ext())) 128 + } 129 + 130 + destFile := filepath.Join(htmlDir, "index.html") 131 + if f.Ext() == "" { 132 + destFile = filepath.Join(types.BuildDir, f.Basename()) 133 + } else { 134 + if err := os.MkdirAll(htmlDir, 0755); err != nil { 135 + return err 136 + } 137 + } 138 + if err := f.Render(destFile, nil); err != nil { 139 + return fmt.Errorf("error: failed to render %s: %w", destFile, err) 140 + } 141 + } 142 + return nil 143 + } 144 + 145 + // ProcessDirectories handles directories of posts under 'pages', 146 + // for example: 'pages/photos/foo.md' or 'pages/blog/bar.md'. 147 + func (p *Pages) ProcessDirectories() error { 148 + for _, dir := range p.Dirs { 149 + dstDir := filepath.Join(types.BuildDir, dir.Name) 150 + if err := os.MkdirAll(dstDir, 0755); err != nil { 151 + return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err) 152 + } 153 + 154 + posts := []types.Post{} 155 + 156 + for _, file := range dir.Files { 157 + post := types.Post{} 158 + // foo-bar.md -> foo-bar 159 + slug := strings.TrimSuffix(file.Basename(), file.Ext()) 160 + dstFile := filepath.Join(dstDir, slug, "index.html") 161 + 162 + // ex: build/blog/foo-bar/ 163 + if err := os.MkdirAll(filepath.Join(dstDir, slug), 0755); err != nil { 164 + return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err) 165 + } 166 + 167 + if err := file.Render(dstFile, nil); err != nil { 168 + return fmt.Errorf("error: failed to render %s: %w", dstFile, err) 169 + } 170 + 171 + post.Meta = file.Frontmatter() 172 + post.Body = file.Body() 173 + posts = append(posts, post) 174 + } 175 + 176 + sort.Slice(posts, func(i, j int) bool { 177 + dateStr1 := posts[j].Meta["date"] 178 + dateStr2 := posts[i].Meta["date"] 179 + date1, _ := time.Parse("2006-01-02", dateStr1) 180 + date2, _ := time.Parse("2006-01-02", dateStr2) 181 + return date1.Before(date2) 182 + }) 183 + 184 + if dir.HasIndex { 185 + indexMd := filepath.Join(types.PagesDir, dir.Name, "_index.md") 186 + index := markdown.Markdown{Path: indexMd} 187 + dstFile := filepath.Join(dstDir, "index.html") 188 + if err := index.Render(dstFile, posts); err != nil { 189 + return fmt.Errorf("error: failed to render index %s: %w", dstFile, err) 190 + } 191 + } 192 + 193 + xml, err := atom.NewAtomFeed(filepath.Join(types.PagesDir, dir.Name), posts) 194 + if err != nil { 195 + return fmt.Errorf("error: failed to create atom feed for: %s: %w", dir.Name, err) 196 + } 197 + feedFile := filepath.Join(dstDir, "feed.xml") 198 + os.WriteFile(feedFile, xml, 0755) 199 + } 200 + 201 + return nil 202 + } 203 + 204 + func postBuild() error { 205 + for _, cmd := range config.Config.PostBuild { 206 + fmt.Println("vite: running post-build command:", cmd) 207 + if err := util.RunCmd(cmd); err != nil { 208 + return err 209 + } 210 + } 211 + return nil 212 + } 213 + 214 + func preBuild() error { 215 + for _, cmd := range config.Config.PreBuild { 216 + fmt.Println("vite: running pre-build command:", cmd) 217 + if err := util.RunCmd(cmd); err != nil { 218 + return err 219 + } 220 + } 221 + return nil 222 + }
+19
formats/anything.go
··· 1 + package formats 2 + 3 + import ( 4 + "path/filepath" 5 + 6 + "git.icyphox.sh/vite/util" 7 + ) 8 + 9 + // Anything is a stub format for unrecognized files 10 + type Anything struct{ Path string } 11 + 12 + func (Anything) Ext() string { return "" } 13 + func (Anything) Frontmatter() map[string]string { return nil } 14 + func (Anything) Body() string { return "" } 15 + func (a Anything) Basename() string { return filepath.Base(a.Path) } 16 + 17 + func (a Anything) Render(dest string, data interface{}) error { 18 + return util.CopyFile(a.Path, dest) 19 + }
+128
formats/markdown/markdown.go
··· 1 + package markdown 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + gotmpl "text/template" 9 + "time" 10 + 11 + "git.icyphox.sh/vite/config" 12 + "git.icyphox.sh/vite/template" 13 + "git.icyphox.sh/vite/types" 14 + "github.com/adrg/frontmatter" 15 + 16 + bf "git.icyphox.sh/grayfriday" 17 + ) 18 + 19 + var ( 20 + bfFlags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions | 21 + bf.SmartypantsDashes | bf.NofollowLinks | bf.FootnoteReturnLinks 22 + bfExts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink | 23 + bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak | 24 + bf.AutoHeadingIDs | bf.HeadingIDs | bf.Footnotes | bf.NoEmptyLineBeforeBlock 25 + ) 26 + 27 + type Markdown struct { 28 + body []byte 29 + frontmatter map[string]string 30 + Path string 31 + } 32 + 33 + func (*Markdown) Ext() string { return ".md" } 34 + 35 + func (md *Markdown) Basename() string { 36 + return filepath.Base(md.Path) 37 + } 38 + 39 + // mdToHtml renders source markdown to html 40 + func mdToHtml(source []byte) []byte { 41 + return bf.Run( 42 + source, 43 + bf.WithNoExtensions(), 44 + bf.WithRenderer(bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: bfFlags})), 45 + bf.WithExtensions(bfExts), 46 + ) 47 + } 48 + 49 + // template checks the frontmatter for a specified template or falls back 50 + // to the default template -- to which it, well, templates whatever is in 51 + // data and writes it to dest. 52 + func (md *Markdown) template(dest, tmplDir string, data interface{}) error { 53 + metaTemplate := md.frontmatter["template"] 54 + if metaTemplate == "" { 55 + metaTemplate = config.Config.DefaultTemplate 56 + } 57 + 58 + tmpl := template.NewTmpl() 59 + tmpl.SetFuncs(gotmpl.FuncMap{ 60 + "parsedate": func(s string) time.Time { 61 + date, _ := time.Parse("2006-01-02", s) 62 + return date 63 + }, 64 + }) 65 + if err := tmpl.Load(tmplDir); err != nil { 66 + return err 67 + } 68 + 69 + w, err := os.Create(dest) 70 + if err != nil { 71 + return err 72 + } 73 + 74 + if err = tmpl.ExecuteTemplate(w, metaTemplate, data); err != nil { 75 + return err 76 + } 77 + return nil 78 + } 79 + 80 + // extract takes the source markdown page, extracts the frontmatter 81 + // and body. The body is converted from markdown to html here. 82 + func (md *Markdown) extractFrontmatter(source []byte) error { 83 + r := bytes.NewReader(source) 84 + rest, err := frontmatter.Parse(r, &md.frontmatter) 85 + if err != nil { 86 + return err 87 + } 88 + md.body = mdToHtml(rest) 89 + return nil 90 + } 91 + 92 + func (md *Markdown) Frontmatter() map[string]string { 93 + return md.frontmatter 94 + } 95 + 96 + func (md *Markdown) Body() string { 97 + return string(md.body) 98 + } 99 + 100 + type templateData struct { 101 + Cfg config.ConfigYaml 102 + Meta map[string]string 103 + Body string 104 + Extra interface{} 105 + } 106 + 107 + func (md *Markdown) Render(dest string, data interface{}) error { 108 + source, err := os.ReadFile(md.Path) 109 + if err != nil { 110 + return fmt.Errorf("markdown: error reading file: %w", err) 111 + } 112 + 113 + err = md.extractFrontmatter(source) 114 + if err != nil { 115 + return fmt.Errorf("markdown: error extracting frontmatter: %w", err) 116 + } 117 + 118 + err = md.template(dest, types.TemplatesDir, templateData{ 119 + config.Config, 120 + md.frontmatter, 121 + string(md.body), 122 + data, 123 + }) 124 + if err != nil { 125 + return fmt.Errorf("markdown: failed to render to destination %s: %w", dest, err) 126 + } 127 + return nil 128 + }
+110
formats/yaml/yaml.go
··· 1 + package yaml 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + "path/filepath" 7 + gotmpl "text/template" 8 + "time" 9 + 10 + "git.icyphox.sh/vite/config" 11 + "git.icyphox.sh/vite/template" 12 + "git.icyphox.sh/vite/types" 13 + "gopkg.in/yaml.v3" 14 + ) 15 + 16 + type YAML struct { 17 + Path string 18 + 19 + meta map[string]string 20 + } 21 + 22 + func (*YAML) Ext() string { return ".yaml" } 23 + func (*YAML) Body() string { return "" } 24 + func (y *YAML) Basename() string { return filepath.Base(y.Path) } 25 + 26 + func (y *YAML) Frontmatter() map[string]string { 27 + return y.meta 28 + } 29 + 30 + type templateData struct { 31 + Cfg config.ConfigYaml 32 + Meta map[string]string 33 + Yaml map[string]interface{} 34 + Body string 35 + } 36 + 37 + func (y *YAML) template(dest, tmplDir string, data interface{}) error { 38 + metaTemplate := y.meta["template"] 39 + if metaTemplate == "" { 40 + metaTemplate = config.Config.DefaultTemplate 41 + } 42 + 43 + tmpl := template.NewTmpl() 44 + tmpl.SetFuncs(gotmpl.FuncMap{ 45 + "parsedate": func(s string) time.Time { 46 + date, _ := time.Parse("2006-01-02", s) 47 + return date 48 + }, 49 + }) 50 + if err := tmpl.Load(tmplDir); err != nil { 51 + return err 52 + } 53 + 54 + w, err := os.Create(dest) 55 + if err != nil { 56 + return err 57 + } 58 + 59 + if err = tmpl.ExecuteTemplate(w, metaTemplate, data); err != nil { 60 + return err 61 + } 62 + return nil 63 + } 64 + 65 + func (y *YAML) Render(dest string, data interface{}) error { 66 + yamlBytes, err := os.ReadFile(y.Path) 67 + if err != nil { 68 + return fmt.Errorf("yaml: failed to read file: %s: %w", y.Path, err) 69 + } 70 + 71 + yamlData := map[string]interface{}{} 72 + err = yaml.Unmarshal(yamlBytes, yamlData) 73 + if err != nil { 74 + return fmt.Errorf("yaml: failed to unmarshal yaml file: %s: %w", y.Path, err) 75 + } 76 + 77 + metaInterface := yamlData["meta"].(map[string]interface{}) 78 + 79 + meta := make(map[string]string) 80 + for k, v := range metaInterface { 81 + vStr := convertToString(v) 82 + meta[k] = vStr 83 + } 84 + 85 + y.meta = meta 86 + 87 + err = y.template(dest, types.TemplatesDir, templateData{ 88 + config.Config, 89 + y.meta, 90 + yamlData, 91 + "", 92 + }) 93 + if err != nil { 94 + return fmt.Errorf("yaml: failed to render to destination %s: %w", dest, err) 95 + } 96 + 97 + return nil 98 + } 99 + 100 + func convertToString(value interface{}) string { 101 + // Infer type and convert to string 102 + switch v := value.(type) { 103 + case string: 104 + return v 105 + case time.Time: 106 + return v.Format("2006-01-02") 107 + default: 108 + return fmt.Sprintf("%v", v) 109 + } 110 + }
+2 -1
main.go
··· 5 5 "os" 6 6 7 7 "git.icyphox.sh/vite/commands" 8 + "git.icyphox.sh/vite/commands/build" 8 9 ) 9 10 10 11 func main() { ··· 38 39 } 39 40 40 41 case "build": 41 - if err := commands.Build(); err != nil { 42 + if err := build.Build(); err != nil { 42 43 fmt.Fprintf(os.Stderr, "error: build: %+v\n", err) 43 44 } 44 45
-24
markdown/frontmatter.go
··· 1 - package markdown 2 - 3 - import ( 4 - "bytes" 5 - 6 - "github.com/adrg/frontmatter" 7 - ) 8 - 9 - type Matter map[string]string 10 - 11 - type MarkdownDoc struct { 12 - Frontmatter Matter 13 - Body []byte 14 - } 15 - 16 - func (md *MarkdownDoc) Extract(source []byte) error { 17 - r := bytes.NewReader(source) 18 - rest, err := frontmatter.Parse(r, &md.Frontmatter) 19 - if err != nil { 20 - return err 21 - } 22 - md.Body = rest 23 - return nil 24 - }
-73
markdown/markdown.go
··· 1 - package markdown 2 - 3 - import ( 4 - "fmt" 5 - "os" 6 - gotmpl "text/template" 7 - "time" 8 - 9 - "git.icyphox.sh/vite/config" 10 - "git.icyphox.sh/vite/markdown/template" 11 - 12 - bf "git.icyphox.sh/grayfriday" 13 - ) 14 - 15 - var ( 16 - bfFlags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions | 17 - bf.SmartypantsDashes | bf.NofollowLinks | bf.FootnoteReturnLinks 18 - bfExts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink | 19 - bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak | 20 - bf.AutoHeadingIDs | bf.HeadingIDs | bf.Footnotes | bf.NoEmptyLineBeforeBlock 21 - ) 22 - 23 - type Output struct { 24 - HTML []byte 25 - Meta Matter 26 - } 27 - 28 - // Renders markdown to html, and fetches metadata. 29 - func (out *Output) RenderMarkdown(source []byte) error { 30 - md := MarkdownDoc{} 31 - if err := md.Extract(source); err != nil { 32 - return fmt.Errorf("markdown: %w", err) 33 - } 34 - 35 - out.HTML = bf.Run( 36 - md.Body, 37 - bf.WithNoExtensions(), 38 - bf.WithRenderer(bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: bfFlags})), 39 - bf.WithExtensions(bfExts), 40 - ) 41 - out.Meta = md.Frontmatter 42 - return nil 43 - } 44 - 45 - // Renders out.HTML into dst html file, using the template specified 46 - // in the frontmatter. data is the template struct. 47 - func (out *Output) RenderHTML(dst, tmplDir string, data interface{}) error { 48 - metaTemplate := out.Meta["template"] 49 - if metaTemplate == "" { 50 - metaTemplate = config.Config.DefaultTemplate 51 - } 52 - 53 - tmpl := template.NewTmpl() 54 - tmpl.SetFuncs(gotmpl.FuncMap{ 55 - "parsedate": func(s string) time.Time { 56 - date, _ := time.Parse("2006-01-02", s) 57 - return date 58 - }, 59 - }) 60 - if err := tmpl.Load(tmplDir); err != nil { 61 - return err 62 - } 63 - 64 - w, err := os.Create(dst) 65 - if err != nil { 66 - return err 67 - } 68 - 69 - if err = tmpl.ExecuteTemplate(w, metaTemplate, data); err != nil { 70 - return err 71 - } 72 - return nil 73 - }
markdown/template/template.go template/template.go
+30
types/types.go
··· 1 + package types 2 + 3 + const ( 4 + BuildDir = "build" 5 + PagesDir = "pages" 6 + TemplatesDir = "templates" 7 + StaticDir = "static" 8 + ) 9 + 10 + type File interface { 11 + Ext() string 12 + // Render takes any arbitrary data and combines that with the global config, 13 + // page frontmatter and the body, as template params. Templates are read 14 + // from types.TemplateDir and the final html is written to dest, 15 + // with necessary directories being created. 16 + Render(dest string, data interface{}) error 17 + 18 + // Frontmatter will not be populated if Render hasn't been called. 19 + Frontmatter() map[string]string 20 + // Body will not be populated if Render hasn't been called. 21 + Body() string 22 + Basename() string 23 + } 24 + 25 + // Only used for building indexes and Atom feeds 26 + type Post struct { 27 + Meta map[string]string 28 + // HTML-formatted body of post 29 + Body string 30 + }