github.com/sdqri/sequined@v0.0.0-20240421190656-fc6bf956f4d8/internal/hyperrenderer/webpage.go (about) 1 package hyperrenderer 2 3 import ( 4 "embed" 5 "fmt" 6 "html/template" 7 "io" 8 "math/rand" 9 "net/url" 10 "strings" 11 12 "github.com/brianvoe/gofakeit/v7" 13 "github.com/goccy/go-graphviz" 14 "github.com/goccy/go-graphviz/cgraph" 15 ) 16 17 var _ HyperRenderer = &Webpage{} 18 19 type WebpageType string 20 21 const ( 22 WebpageTypeAuthority = "authority" 23 WebpageTypeHub = "hub" 24 ) 25 26 type PathGeneratorfunc func(parent *Webpage) string 27 28 type Webpage struct { 29 ID uint64 30 Path string 31 PathPrefix string 32 Parent *Webpage 33 Links []*Webpage 34 Type WebpageType 35 36 PathGenerator PathGeneratorfunc 37 AuthorityTmpl *template.Template 38 HubTmpl *template.Template 39 CustomTmpl *template.Template 40 } 41 42 type WebpageOption func(*Webpage) 43 44 //go:embed templates/*.tmpl 45 var templateFS embed.FS 46 47 func NewWebpage( 48 webpageType WebpageType, 49 opts ...WebpageOption, 50 ) *Webpage { 51 id := rand.Uint64() 52 53 fnMaps := template.FuncMap{"Split": strings.Split} 54 55 defaultAuthorityTmpl, err := template.New("default_authority.html.tmpl"). 56 Funcs(fnMaps). 57 ParseFS(templateFS, "templates/default_authority.html.tmpl") 58 if err != nil { 59 panic(err) 60 } 61 defaultHubTmpl, err := template.New("default_hub.html.tmpl"). 62 Funcs(fnMaps). 63 ParseFS(templateFS, "templates/default_hub.html.tmpl") 64 if err != nil { 65 panic(err) 66 } 67 68 webpage := Webpage{ 69 ID: id, 70 Links: make([]*Webpage, 0), 71 Type: webpageType, 72 73 PathGenerator: defaultPathGenerator, 74 AuthorityTmpl: defaultAuthorityTmpl, 75 HubTmpl: defaultHubTmpl, 76 } 77 78 for _, opt := range opts { 79 opt(&webpage) 80 } 81 return &webpage 82 } 83 84 // Clone creates a new Webpage instance based on the current one with a specified type, a unique ID, and no links. 85 func (wp *Webpage) Clone(webpageType WebpageType) *Webpage { 86 webpage := *wp 87 // assign a unique id 88 id := rand.Uint64() 89 webpage.ID = id 90 91 // initializing links & type 92 webpage.Links = make([]*Webpage, 0) 93 webpage.Type = webpageType 94 return &webpage 95 } 96 97 func (wp *Webpage) AddChild(webpageType WebpageType, opts ...WebpageOption) *Webpage { 98 newPage := wp.Clone(webpageType) 99 wp.AddLink(newPage) 100 101 for _, opt := range opts { 102 opt(newPage) 103 } 104 return newPage 105 } 106 107 func (wp *Webpage) GetID() string { 108 return fmt.Sprintf("%d", wp.ID) 109 } 110 111 func (wp *Webpage) GetPath() string { 112 if wp.PathGenerator != nil { 113 return wp.PathGenerator(wp) 114 } 115 116 if wp.PathPrefix != "" { 117 return fmt.Sprintf("%s/%s", wp.PathPrefix, wp.Path) 118 } 119 120 return wp.Path 121 } 122 123 func (wp *Webpage) Render(writer io.Writer) error { 124 data := struct { 125 Node *Webpage 126 }{ 127 Node: wp, 128 } 129 if wp.CustomTmpl != nil { 130 return wp.CustomTmpl.Execute(writer, data) 131 } 132 if wp.Type == WebpageTypeAuthority { 133 return wp.AuthorityTmpl.Execute(writer, data) 134 } 135 return wp.HubTmpl.Execute(writer, data) 136 } 137 138 func (wp *Webpage) GetLinks() []HyperRenderer { 139 links := make([]HyperRenderer, len(wp.Links)) 140 for i, link := range wp.Links { 141 links[i] = link 142 } 143 return links 144 } 145 146 func (wp *Webpage) AddLink(page *Webpage) { 147 page.Parent = wp 148 wp.Links = append(wp.Links, page) 149 } 150 151 func (wp *Webpage) Faker() *gofakeit.Faker { 152 return gofakeit.New(wp.ID) 153 } 154 155 func (wp *Webpage) CountLinksByType(t WebpageType) int { 156 i := 0 157 for _, link := range wp.Links { 158 if link.Type == t { 159 i++ 160 } 161 } 162 return i 163 } 164 165 func (wp *Webpage) Draw(format graphviz.Format, w io.Writer) error { 166 g := graphviz.New() 167 168 graph, err := g.Graph() 169 if err != nil { 170 return err 171 } 172 173 nodes := make(map[string]*cgraph.Node) 174 visited := Traverse(wp, NoOpVisit) 175 176 for renderer := range visited { 177 webpage, ok := renderer.(*Webpage) 178 if !ok { 179 return fmt.Errorf("unable to type assert renderer to webpage") 180 } 181 // Create node for the webpage 182 node, err := graph.CreateNode(renderer.GetID()) 183 if err != nil { 184 return err 185 } 186 187 node.SetStyle(cgraph.FilledNodeStyle) 188 if webpage.Type == WebpageTypeHub { 189 node.SetFillColor("#99D19C") 190 } else if webpage.Type == WebpageTypeAuthority { 191 node.SetFillColor("#ADE1E5") 192 } 193 194 node.SetLabel( 195 fmt.Sprintf( 196 "Title: %s\nType: %s\nPath: %s", 197 webpage.Faker().City(), 198 string(webpage.Type), 199 webpage.GetPath(), 200 ), 201 ) 202 nodes[renderer.GetID()] = node 203 } 204 205 for node := range visited { 206 parentNode, ok := nodes[node.GetID()] 207 if !ok { 208 panic("unabe to get parentNode id") 209 } 210 211 for _, link := range node.GetLinks() { 212 child, ok := nodes[link.GetID()] 213 if !ok { 214 panic("unable to get child id") 215 } 216 _, err := graph.CreateEdge("", parentNode, child) 217 if err != nil { 218 return err 219 } 220 } 221 } 222 223 err = g.Render(graph, format, w) 224 if err != nil { 225 return err 226 } 227 228 return nil 229 } 230 231 func defaultPathGenerator(webpage *Webpage) string { 232 if webpage.Parent == nil { 233 if webpage.PathPrefix != "" { 234 return webpage.PathPrefix 235 } 236 return "/" 237 } 238 result, err := url.JoinPath(webpage.Parent.GetPath(), fmt.Sprintf("%s", webpage.GetID())) 239 if err != nil { 240 panic(err) 241 } 242 return result 243 } 244 245 func CityPathGenerator(webpage *Webpage) string { 246 if webpage.Parent == nil { 247 return "/" 248 } 249 250 result, err := url.JoinPath( 251 webpage.Parent.GetPath(), 252 strings.ReplaceAll(strings.ToLower(webpage.Faker().City()), " ", "-"), 253 ) 254 if err != nil { 255 panic(err) 256 } 257 return result 258 } 259 260 func WithAuthorityTemplate(tmpl *template.Template) WebpageOption { 261 return func(w *Webpage) { 262 w.AuthorityTmpl = tmpl 263 } 264 } 265 266 func WithHubTemplate(tmpl *template.Template) WebpageOption { 267 return func(w *Webpage) { 268 w.HubTmpl = tmpl 269 } 270 } 271 272 func WithCustomTemplate(tmpl *template.Template) WebpageOption { 273 return func(w *Webpage) { 274 w.CustomTmpl = tmpl 275 } 276 } 277 278 func WithPathGenerator(f func(*Webpage) string) WebpageOption { 279 return func(w *Webpage) { 280 w.PathGenerator = f 281 } 282 } 283 284 func WithPathPrefix(prefix string) WebpageOption { 285 return func(w *Webpage) { 286 w.PathPrefix = strings.TrimSuffix(prefix, "/") 287 } 288 }