renderer.go (3597B)
1 package web 2 3 import ( 4 "embed" 5 "fmt" 6 "html/template" 7 "io/fs" 8 "path" 9 "strings" 10 11 "git.prime8.dev/moncheeta/prime8dev/internal/config" 12 "git.prime8.dev/moncheeta/prime8dev/internal/content" 13 "git.prime8.dev/moncheeta/prime8dev/internal/external" 14 "git.prime8.dev/moncheeta/prime8dev/internal/utils" 15 ) 16 17 //go:embed templates/*.gohtml templates/partials/*.gohtml styles/*.css 18 var templatesFS embed.FS 19 20 type Data struct { 21 Title string 22 Description string 23 ActiveTab string 24 ContentTemplate string 25 About config.About 26 AboutNameASCII string 27 Experience []config.Experience 28 SelectedExperienceID string 29 CurrentJob *config.Experience 30 Projects []config.Project 31 SelectedProjectID string 32 Project *config.Project 33 ProjectLinkLabel string 34 ProjectReadme template.HTML 35 Blogs []content.Blog 36 SelectedBlogID string 37 Blog *content.Blog 38 WakaStats *external.WakaTimeStats 39 Tabs []Tab 40 SiteURL string 41 } 42 43 type Tab struct { 44 ID string 45 Label string 46 Num int 47 } 48 49 type Renderer struct { 50 tpl *template.Template 51 } 52 53 func New() (*Renderer, error) { 54 inlineStyles, err := loadInlineStyles() 55 if err != nil { 56 return nil, err 57 } 58 59 funcs := template.FuncMap{ 60 "tabPath": utils.TabPath, 61 "tabURL": utils.TabURL, 62 "tagClass": utils.TagClass, 63 "techMeta": utils.TechMeta, 64 "formatMonthYear": utils.FormatMonthYear, 65 "formatEndDate": utils.FormatEndDate, 66 "formatDuration": utils.FormatDuration, 67 "durationHasYear": utils.DurationHasYear, 68 "timelineYear": utils.TimelineYear, 69 "toHTML": toHTML, 70 "inlineCSS": func(name string) template.CSS { 71 if css, ok := inlineStyles[name]; ok { 72 return css 73 } 74 return "" 75 }, 76 } 77 78 tpl, err := template.New("site").Funcs(funcs).ParseFS(templatesFS, "templates/*.gohtml", "templates/partials/*.gohtml") 79 if err != nil { 80 return nil, fmt.Errorf("parse templates: %w", err) 81 } 82 83 return &Renderer{tpl: tpl}, nil 84 } 85 86 func (r *Renderer) Render(name string, data Data) (string, error) { 87 var b strings.Builder 88 if err := r.tpl.ExecuteTemplate(&b, name, data); err != nil { 89 return "", err 90 } 91 return b.String(), nil 92 } 93 94 func DefaultTabs() []Tab { 95 return []Tab{ 96 {ID: "about", Label: "About", Num: 1}, 97 {ID: "projects", Label: "Projects", Num: 2}, 98 {ID: "experience", Label: "Experience", Num: 3}, 99 {ID: "blog", Label: "Blog", Num: 4}, 100 } 101 } 102 103 func BaseData(title, activeTab string, about config.About) Data { 104 return Data{ 105 Title: title, 106 Description: fmt.Sprintf("Portfolio and blog of %s", about.Name), 107 ActiveTab: activeTab, 108 ContentTemplate: activeTab, 109 About: about, 110 AboutNameASCII: utils.AboutNameASCII(about.Name), 111 Tabs: DefaultTabs(), 112 SiteURL: config.SiteURL, 113 } 114 } 115 116 func toHTML(s string) template.HTML { 117 return template.HTML(s) 118 } 119 120 func loadInlineStyles() (map[string]template.CSS, error) { 121 entries, err := fs.ReadDir(templatesFS, "styles") 122 if err != nil { 123 return nil, fmt.Errorf("read styles: %w", err) 124 } 125 126 styles := make(map[string]template.CSS, len(entries)) 127 for _, entry := range entries { 128 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".css") { 129 continue 130 } 131 132 stylePath := path.Join("styles", entry.Name()) 133 content, err := templatesFS.ReadFile(stylePath) 134 if err != nil { 135 return nil, fmt.Errorf("read style %s: %w", entry.Name(), err) 136 } 137 138 styles[entry.Name()] = template.CSS(string(content)) 139 } 140 141 return styles, nil 142 }