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

     1  package vugu
     2  
     3  import (
     4  	"bytes"
     5  )
     6  
     7  // BuildIn is the input to a Build call.
     8  type BuildIn struct {
     9  
    10  	// the overall build environment
    11  	BuildEnv *BuildEnv
    12  
    13  	// a stack of position hashes, the last one can be used by a component to get a unique hash for overall position
    14  	PositionHashList []uint64
    15  }
    16  
    17  // CurrentPositionHash returns the hash value that can be used by a component to
    18  // mix into it's own hash values to achieve uniqueness based on overall position
    19  // in the output tree.  Basically you should XOR this with whatever unique reference
    20  // ID within the component itself used during Build.  The purpose is to ensure
    21  // that we use the same ID for the same component in the same position within
    22  // the overall tree but a different one for the same component in a different position.
    23  func (bi *BuildIn) CurrentPositionHash() uint64 {
    24  	if len(bi.PositionHashList) == 0 {
    25  		return 0
    26  	}
    27  	return bi.PositionHashList[len(bi.PositionHashList)-1]
    28  }
    29  
    30  // BuildOut is the output from a Build call.  It includes Out as the DOM elements
    31  // produced, plus slices for CSS and JS elements.
    32  type BuildOut struct {
    33  
    34  	// output element(s) - usually just one that is parent to the rest but slots can have multiple
    35  	Out []*VGNode
    36  
    37  	// components that need to be built next - corresponding to each VGNode in Out with a Component value set,
    38  	// we make the Builder populate this so BuildEnv doesn't have to traverse Out to gather up each node with a component
    39  	// FIXME: should this be called ChildComponents, NextComponents? something to make it clear that these are pointers to more work to
    40  	// do rather than something already done
    41  	Components []Builder
    42  
    43  	// optional CSS style or link tag(s)
    44  	CSS []*VGNode
    45  
    46  	// optional JS script tag(s)
    47  	JS []*VGNode
    48  }
    49  
    50  // AppendCSS will append a unique node to CSS (nodes match exactly will not be added again).
    51  func (b *BuildOut) AppendCSS(nlist ...*VGNode) {
    52  	// FIXME: we really should consider doing this dedeplication with a map or something, but for now this works
    53  nlistloop:
    54  	for _, n := range nlist {
    55  		for _, css := range b.CSS {
    56  			if !ssNodeEq(n, css) {
    57  				continue
    58  			}
    59  			if ssText(n) != ssText(css) {
    60  				continue
    61  			}
    62  			// these are the same, skip duplicate add
    63  			continue nlistloop
    64  		}
    65  		// not a duplicate, add it
    66  		b.CSS = append(b.CSS, n)
    67  	}
    68  }
    69  
    70  // AppendJS will append a unique node to JS (nodes match exactly will not be added again).
    71  func (b *BuildOut) AppendJS(nlist ...*VGNode) {
    72  nlistloop:
    73  	for _, n := range nlist {
    74  		for _, js := range b.JS {
    75  			if !ssNodeEq(n, js) {
    76  				continue
    77  			}
    78  			if ssText(n) != ssText(js) {
    79  				continue
    80  			}
    81  			// these are the same, skip duplicate add
    82  			continue nlistloop
    83  		}
    84  		// not a duplicate, add it
    85  		b.JS = append(b.JS, n)
    86  	}
    87  }
    88  
    89  // returns text content of child of script/style tag (assumes only one level of children and all text)
    90  func ssText(n *VGNode) string {
    91  	// special case 0 children
    92  	if n.FirstChild == nil {
    93  		return ""
    94  	}
    95  	// special case 1 child
    96  	if n.FirstChild.NextSibling == nil {
    97  		return n.FirstChild.Data
    98  	}
    99  	// more than one child
   100  	var buf bytes.Buffer
   101  	for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   102  		buf.WriteString(childN.Data)
   103  	}
   104  	return buf.String()
   105  }
   106  
   107  // script/style node compare - only care about Type, Data and Attributes, does not examine children
   108  func ssNodeEq(n1, n2 *VGNode) bool {
   109  	if n1.Type != n2.Type {
   110  		return false
   111  	}
   112  	if n1.Data != n2.Data {
   113  		return false
   114  	}
   115  	if len(n1.Attr) != len(n2.Attr) {
   116  		return false
   117  	}
   118  	for i := 0; i < len(n1.Attr); i++ {
   119  		if n1.Attr[i] != n2.Attr[i] {
   120  			return false
   121  		}
   122  	}
   123  	return true
   124  }
   125  
   126  // FIXME: CSS and JS are now full element nodes not just blocks of text (style, link, or script tags),
   127  // and we should have some sort of AppendCSS and AppendJS methods that understand that and also perform
   128  // deduplication.  There is no reason to wait and deduplicate later, we should be doing it here during
   129  // the Build.
   130  
   131  // func (b *BuildOut) AppendCSS(css string) {
   132  // 	// FIXME: should we be deduplicating here??
   133  // 	vgn := &VGNode{Type: ElementNode, Data: "style"}
   134  // 	vgn.AppendChild(&VGNode{Type: TextNode, Data: css})
   135  // 	b.CSS = append(b.CSS, vgn)
   136  // }
   137  
   138  // func (b *BuildOut) AppendJS(js string) {
   139  // 	// FIXME: should we be deduplicating here??
   140  // 	vgn := &VGNode{Type: ElementNode, Data: "script"}
   141  // 	vgn.AppendChild(&VGNode{Type: TextNode, Data: js})
   142  // 	b.JS = append(b.JS, vgn)
   143  // }
   144  
   145  // Builder is the interface that components implement.
   146  type Builder interface {
   147  	// Build is called to construct a tree of VGNodes corresponding to the DOM of this component.
   148  	// Note that Build does not return an error because there is nothing the caller can do about
   149  	// it except for stop rendering.  This way we force errors during Build to either be handled
   150  	// internally or to explicitly result in a panic.
   151  	Build(in *BuildIn) (out *BuildOut)
   152  }
   153  
   154  // builderFunc is a Build-like function that implements Builder.
   155  type builderFunc func(in *BuildIn) (out *BuildOut)
   156  
   157  // Build implements Builder.
   158  func (f builderFunc) Build(in *BuildIn) (out *BuildOut) { return f(in) }
   159  
   160  // NewBuilderFunc returns a Builder from a function that is called for Build.
   161  func NewBuilderFunc(f func(in *BuildIn) (out *BuildOut)) Builder {
   162  	// NOTE: for now, all components have to be struct pointers, so we wrap
   163  	// this function in a struct pointer.  Would be nice to fix this at some point.
   164  	return &struct {
   165  		Builder
   166  	}{
   167  		Builder: builderFunc(f),
   168  	}
   169  }
   170  
   171  // BeforeBuilder is deprecated.  It is replaced by the Compute lifecycle callback.
   172  type BeforeBuilder interface {
   173  	BeforeBuild()
   174  }