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  }