github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/staticrender/renderer-static.go (about)

     1  package staticrender
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  	"sync"
     8  
     9  	// "golang.org/x/net/html"
    10  	// "golang.org/x/net/html/atom"
    11  	"github.com/vugu/html"
    12  	"github.com/vugu/html/atom"
    13  	"github.com/vugu/vugu"
    14  )
    15  
    16  // caller should be able to just specify a directory,
    17  // or provide custom logic for how to give an exact io.Writer to write to for a given output path,
    18  // should be callable a bunch of times for different URLs
    19  
    20  // New returns a new instance.  w may be nil as long as SetWriter is called with a valid value before rendering.
    21  func New(w io.Writer) *StaticRenderer {
    22  	return &StaticRenderer{
    23  		w: w,
    24  	}
    25  }
    26  
    27  // StaticRenderer provides rendering as static HTML to an io.Writer.
    28  type StaticRenderer struct {
    29  	w io.Writer
    30  }
    31  
    32  // SetWriter assigns the Writer to be used for subsequent calls to Render.
    33  // A Writer must be assigned before rendering (either with this method or in New).
    34  func (r *StaticRenderer) SetWriter(w io.Writer) {
    35  	r.w = w
    36  }
    37  
    38  // Render will perform a static render of the given BuildResults and write it to the writer assigned.
    39  func (r *StaticRenderer) Render(buildResults *vugu.BuildResults) error {
    40  
    41  	n, err := r.renderOne(buildResults, buildResults.Out)
    42  	if err != nil {
    43  		return err
    44  	}
    45  
    46  	err = html.Render(r.w, n)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	return nil
    52  
    53  }
    54  
    55  func (r *StaticRenderer) renderOne(br *vugu.BuildResults, bo *vugu.BuildOut) (*html.Node, error) {
    56  
    57  	if len(bo.Out) != 1 {
    58  		return nil, fmt.Errorf("BuildOut must contain exactly one element in Out")
    59  	}
    60  
    61  	vgn := bo.Out[0]
    62  
    63  	var visit func(vgn *vugu.VGNode) ([]*html.Node, error)
    64  	visit = func(vgn *vugu.VGNode) ([]*html.Node, error) {
    65  
    66  		// log.Printf("vgn: %#v", vgn)
    67  
    68  		// if component then look up BuildOut for it and call renderOne again and return
    69  		if vgn.Component != nil {
    70  			cbo := br.ResultFor(vgn.Component)
    71  			retn, err := r.renderOne(br, cbo)
    72  			if err != nil {
    73  				return nil, err
    74  			}
    75  			return []*html.Node{retn}, nil
    76  			// if len(retn) != 1 {
    77  			// 	return nil, fmt.Errorf("StaticRenderer.renderOne component renderOne returned unexpected %d nodes", len(retn))
    78  			// }
    79  			// return retn[0], nil
    80  		}
    81  
    82  		// if template then just traverse the children directly and return them in a series, omitting vgn
    83  		if vgn.IsTemplate() {
    84  			var retn []*html.Node
    85  			for vgchild := vgn.FirstChild; vgchild != nil; vgchild = vgchild.NextSibling {
    86  				nchildren, err := visit(vgchild)
    87  				if err != nil {
    88  					return nil, err
    89  				}
    90  				retn = append(retn, nchildren...)
    91  			}
    92  			return retn, nil
    93  		}
    94  
    95  		// no component set, continue with building this node
    96  		n := &html.Node{}
    97  		n.Type = html.NodeType(vgn.Type)           // type numbers are the same
    98  		n.Data = vgn.Data                          // copy Data over
    99  		n.DataAtom = atom.Lookup([]byte(vgn.Data)) // lookup atom
   100  		for _, vgattr := range vgn.Attr {
   101  			n.Attr = append(n.Attr, html.Attribute{Key: vgattr.Key, Val: vgattr.Val})
   102  		}
   103  
   104  		// handle InnerHTML
   105  
   106  		if vgn.InnerHTML != nil {
   107  
   108  			nparts, err := html.ParseFragment(strings.NewReader(*vgn.InnerHTML), n)
   109  			if err != nil {
   110  				return nil, err
   111  			}
   112  
   113  			// for _, nc := range nparts {
   114  			// 	n.AppendChild(nc)
   115  			// }
   116  			appendChildren(n, nparts)
   117  
   118  			// InnerHTML precludes other children
   119  			return []*html.Node{n}, nil
   120  		}
   121  
   122  		// handle children
   123  		for vgchild := vgn.FirstChild; vgchild != nil; vgchild = vgchild.NextSibling {
   124  			nchildren, err := visit(vgchild)
   125  			if err != nil {
   126  				return nil, err
   127  			}
   128  			// n.AppendChild(nchildren)
   129  			appendChildren(n, nchildren)
   130  		}
   131  
   132  		// special case for <head>, we need to emit the CSS here as that is separate
   133  		// (Vugu build output does not always have a head tag and multiple components
   134  		// can each emit it, so we have to keep things like CSS separate)
   135  		if n.Type == html.ElementNode && n.Data == "head" {
   136  			for _, css := range bo.CSS {
   137  
   138  				// convert each one
   139  				nchildren, err := visit(css)
   140  				if err != nil {
   141  					return nil, err
   142  				}
   143  				// n.AppendChild(n2)
   144  				appendChildren(n, nchildren)
   145  			}
   146  		}
   147  
   148  		// special case to append JS to end of <body>
   149  		if n.Type == html.ElementNode && n.Data == "body" {
   150  			for _, js := range bo.JS {
   151  
   152  				// convert each one
   153  				nchildren, err := visit(js)
   154  				if err != nil {
   155  					return nil, err
   156  				}
   157  				// n.AppendChild(n2)
   158  				appendChildren(n, nchildren)
   159  			}
   160  		}
   161  
   162  		return []*html.Node{n}, nil
   163  	}
   164  
   165  	nret, err := visit(vgn)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	if len(nret) != 1 {
   170  		return nil, fmt.Errorf("StaticRenderer.renderOne visit returned unexpected %d nodes", len(nret))
   171  	}
   172  	return nret[0], nil
   173  }
   174  
   175  func appendChildren(parent *html.Node, children []*html.Node) {
   176  	for _, c := range children {
   177  		parent.AppendChild(c)
   178  	}
   179  }
   180  
   181  // EventEnv returns a simple EventEnv implementation suitable for use with the static renderer.
   182  func (r *StaticRenderer) EventEnv() *RWMutexEventEnv {
   183  	return &RWMutexEventEnv{}
   184  }
   185  
   186  // RWMutexEventEnv implements EventEnv interface with a mutex but no render behavior.
   187  // (Because static rendering is driven by the caller, there is no way for dynamic logic
   188  // to re-render a static page, so UnlockRender is the same as UnlockOnly.)
   189  type RWMutexEventEnv struct {
   190  	sync.RWMutex
   191  }
   192  
   193  // UnlockOnly will release the write lock
   194  func (ee *RWMutexEventEnv) UnlockOnly() { ee.Unlock() }
   195  
   196  // UnlockRender is an alias for UnlockOnly.
   197  func (ee *RWMutexEventEnv) UnlockRender() { ee.Unlock() }