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

     1  package vugu
     2  
     3  import (
     4  	"fmt"
     5  	"html"
     6  	"reflect"
     7  	"strconv"
     8  
     9  	"github.com/vugu/vugu/js"
    10  )
    11  
    12  // VGNodeType is one of the valid node types (error, text, document, element, comment, doctype).
    13  // Note that only text, element and comment are currently used.
    14  type VGNodeType uint32
    15  
    16  // Available VGNodeTypes.
    17  const (
    18  	ErrorNode    = VGNodeType(0 /*html.ErrorNode*/)
    19  	TextNode     = VGNodeType(1 /*html.TextNode*/)
    20  	DocumentNode = VGNodeType(2 /*html.DocumentNode*/)
    21  	ElementNode  = VGNodeType(3 /*html.ElementNode*/)
    22  	CommentNode  = VGNodeType(4 /*html.CommentNode*/)
    23  	DoctypeNode  = VGNodeType(5 /*html.DoctypeNode*/)
    24  )
    25  
    26  // VGAtom is an integer corresponding to golang.org/x/net/html/atom.Atom.
    27  // Note that this may be removed for simplicity and to remove the dependency
    28  // on the package above.  Suggest you don't use it.
    29  // type VGAtom uint32
    30  
    31  // VGAttribute is the attribute on an HTML tag.
    32  type VGAttribute struct {
    33  	Namespace, Key, Val string
    34  }
    35  
    36  // VGProperty is a JS property to be set on a DOM element.
    37  type VGProperty struct {
    38  	Key     string
    39  	JSONVal []byte // value as JSON expression
    40  }
    41  
    42  // VGNode represents a node from our virtual DOM with the dynamic parts wired up into functions.
    43  //
    44  // For the static parts, an instance of VGNode corresponds directly to the DOM representation of
    45  // an HTML node.  The pointers to other VGNode instances (Parent, FirstChild, etc.) are used to manage the tree.
    46  // Type, Data, Namespace and Attr have the usual meanings for nodes.
    47  //
    48  // The Component field, if not-nil indicates that rendering should be delegated to the specified component,
    49  // all other fields are ignored.
    50  //
    51  // Another special case is when Type is ElementNode and Data is an empty string (and Component is nil) then this node is a "template"
    52  // (i.e. <vg-template>) and its children will be "flattened" into the DOM in position of this element
    53  // and attributes, events, etc. ignored.
    54  //
    55  // Prop contains JavaScript property values to be assigned during render. InnerHTML provides alternate
    56  // HTML content instead of children.  DOMEventHandlerSpecList specifies DOM handlers to register.
    57  // And the JS...Handler fields are used to register callbacks to obtain information at JS render-time.
    58  //
    59  // TODO: This and its related parts should probably move into a sub-package (vgnode?) and
    60  // the "VG" prefixes removed.
    61  type VGNode struct {
    62  	Parent, FirstChild, LastChild, PrevSibling, NextSibling *VGNode
    63  
    64  	Type VGNodeType
    65  	// DataAtom  VGAtom // this needed to come out, we're not using it and without well-defined behavior it just becomes confusing and problematic
    66  	Data      string
    67  	Namespace string
    68  	Attr      []VGAttribute
    69  
    70  	// JS properties to e set during render
    71  	Prop []VGProperty
    72  
    73  	// Props Props // dynamic attributes, used as input for components or converted to attributes for regular HTML elements
    74  
    75  	InnerHTML *string // indicates that children should be ignored and this raw HTML is the children of this tag; nil means not set, empty string means explicitly set to empty string
    76  
    77  	DOMEventHandlerSpecList []DOMEventHandlerSpec // describes invocations when DOM events happen
    78  
    79  	// indicates this node's output should be delegated to the specified component
    80  	Component interface{}
    81  
    82  	// if not-nil, called when element is created (but before examining child nodes)
    83  	JSCreateHandler JSValueHandler
    84  	// if not-nil, called after children have been visited
    85  	JSPopulateHandler JSValueHandler
    86  }
    87  
    88  // IsComponent returns true if this is a component (Component != nil).
    89  // Components have rendering delegated to them instead of processing this node.
    90  func (n *VGNode) IsComponent() bool {
    91  	return n.Component != nil
    92  }
    93  
    94  // IsTemplate returns true if this is a template (Type is ElementNode and Data is an empty string and not a Component).
    95  // Templates have their children flattened into the output DOM instead of being processed directly.
    96  func (n *VGNode) IsTemplate() bool {
    97  	if n.Type == ElementNode && n.Data == "" && n.Component == nil {
    98  		return true
    99  	}
   100  	return false
   101  }
   102  
   103  // InsertBefore inserts newChild as a child of n, immediately before oldChild
   104  // in the sequence of n's children. oldChild may be nil, in which case newChild
   105  // is appended to the end of n's children.
   106  //
   107  // It will panic if newChild already has a parent or siblings.
   108  func (n *VGNode) InsertBefore(newChild, oldChild *VGNode) {
   109  	if newChild.Parent != nil || newChild.PrevSibling != nil || newChild.NextSibling != nil {
   110  		panic("html: InsertBefore called for an attached child Node")
   111  	}
   112  	var prev, next *VGNode
   113  	if oldChild != nil {
   114  		prev, next = oldChild.PrevSibling, oldChild
   115  	} else {
   116  		prev = n.LastChild
   117  	}
   118  	if prev != nil {
   119  		prev.NextSibling = newChild
   120  	} else {
   121  		n.FirstChild = newChild
   122  	}
   123  	if next != nil {
   124  		next.PrevSibling = newChild
   125  	} else {
   126  		n.LastChild = newChild
   127  	}
   128  	newChild.Parent = n
   129  	newChild.PrevSibling = prev
   130  	newChild.NextSibling = next
   131  }
   132  
   133  // AppendChild adds a node c as a child of n.
   134  //
   135  // It will panic if c already has a parent or siblings.
   136  func (n *VGNode) AppendChild(c *VGNode) {
   137  	if c.Parent != nil || c.PrevSibling != nil || c.NextSibling != nil {
   138  		panic("html: AppendChild called for an attached child Node")
   139  	}
   140  	last := n.LastChild
   141  	if last != nil {
   142  		last.NextSibling = c
   143  	} else {
   144  		n.FirstChild = c
   145  	}
   146  	n.LastChild = c
   147  	c.Parent = n
   148  	c.PrevSibling = last
   149  }
   150  
   151  // RemoveChild removes a node c that is a child of n. Afterwards, c will have
   152  // no parent and no siblings.
   153  //
   154  // It will panic if c's parent is not n.
   155  func (n *VGNode) RemoveChild(c *VGNode) {
   156  	if c.Parent != n {
   157  		panic("html: RemoveChild called for a non-child Node")
   158  	}
   159  	if n.FirstChild == c {
   160  		n.FirstChild = c.NextSibling
   161  	}
   162  	if c.NextSibling != nil {
   163  		c.NextSibling.PrevSibling = c.PrevSibling
   164  	}
   165  	if n.LastChild == c {
   166  		n.LastChild = c.PrevSibling
   167  	}
   168  	if c.PrevSibling != nil {
   169  		c.PrevSibling.NextSibling = c.NextSibling
   170  	}
   171  	c.Parent = nil
   172  	c.PrevSibling = nil
   173  	c.NextSibling = nil
   174  }
   175  
   176  // type IfFunc func(data interface{}) (bool, error)
   177  
   178  // Walk will walk the tree under a VGNode using the specified callback function.
   179  // If the function provided returns a non-nil error then walking will be stopped
   180  // and this error will be returned.  Only FirstChild and NextSibling are used
   181  // while walking and so with well-formed documents should not loop. (But loops
   182  // created manually by modifying FirstChild or NextSibling pointers could cause
   183  // this function to recurse indefinitely.)  Note that f may modify nodes as it
   184  // visits them with predictable results as long as it does not modify elements
   185  // higher on the tree (up, toward the parent); it is safe to modify self and children.
   186  func (n *VGNode) Walk(f func(*VGNode) error) error {
   187  	if n == nil {
   188  		return nil
   189  	}
   190  	err := f(n)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	err = n.FirstChild.Walk(f)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	err = n.NextSibling.Walk(f)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  // AddAttrInterface sets an attribute based on the given interface. The followings types are supported
   206  // - string - value is used as attr value as it is
   207  // - int,float,... - the value is converted to string with strconv and used as attr value
   208  // - bool - treat the attribute as a flag. If false, the attribute will be ignored, if true outputs the attribute without a value
   209  // - fmt.Stringer - if the value implements fmt.Stringer, the returned string of StringVar() is used
   210  // - ptr - If the ptr is nil, the attribute will be ignored. Else, the rules above apply
   211  // any other type is handled via fmt.Sprintf()
   212  func (n *VGNode) AddAttrInterface(key string, val interface{}) {
   213  	// ignore nil attributes
   214  	if val == nil {
   215  		return
   216  	}
   217  
   218  	nattr := VGAttribute{
   219  		Key: key,
   220  	}
   221  
   222  	switch v := val.(type) {
   223  	case string:
   224  		nattr.Val = v
   225  	case int:
   226  		nattr.Val = strconv.Itoa(v)
   227  	case int8:
   228  		nattr.Val = strconv.Itoa(int(v))
   229  	case int16:
   230  		nattr.Val = strconv.Itoa(int(v))
   231  	case int32:
   232  		nattr.Val = strconv.Itoa(int(v))
   233  	case int64:
   234  		nattr.Val = strconv.FormatInt(v, 10)
   235  	case uint:
   236  		nattr.Val = strconv.FormatUint(uint64(v), 10)
   237  	case uint8:
   238  		nattr.Val = strconv.FormatUint(uint64(v), 10)
   239  	case uint16:
   240  		nattr.Val = strconv.FormatUint(uint64(v), 10)
   241  	case uint32:
   242  		nattr.Val = strconv.FormatUint(uint64(v), 10)
   243  	case uint64:
   244  		nattr.Val = strconv.FormatUint(v, 10)
   245  	case float32:
   246  		nattr.Val = strconv.FormatFloat(float64(v), 'f', 6, 32)
   247  	case float64:
   248  		nattr.Val = strconv.FormatFloat(v, 'f', 6, 64)
   249  	case bool:
   250  		if !v {
   251  			return
   252  		}
   253  		// FIXME: according to the HTML spec this is valid for flags, but not pretty
   254  		// To change this, we have to adopt the changes inside the domrender and staticrender too.
   255  		nattr.Val = key
   256  	case fmt.Stringer:
   257  		// we have to check that the given interface does not hide a nil ptr
   258  		if p := reflect.ValueOf(val); p.Kind() == reflect.Ptr && p.IsNil() {
   259  			return
   260  		}
   261  		nattr.Val = v.String()
   262  	default:
   263  		// check if this is a ptr
   264  		rv := reflect.ValueOf(val)
   265  		if rv.Kind() == reflect.Ptr {
   266  			if rv.IsNil() {
   267  				return
   268  			}
   269  			// indirect the ptr and recurse
   270  			if !rvIsZero(rv) {
   271  				n.AddAttrInterface(key, reflect.Indirect(rv).Interface())
   272  			}
   273  			return
   274  		}
   275  		// fall back to fmt
   276  		nattr.Val = fmt.Sprintf("%v", val)
   277  	}
   278  
   279  	n.Attr = append(n.Attr, nattr)
   280  }
   281  
   282  // AddAttrList takes a VGAttributeLister and sets the returned attributes to the node
   283  func (n *VGNode) AddAttrList(lister VGAttributeLister) {
   284  	n.Attr = append(n.Attr, lister.AttributeList()...)
   285  }
   286  
   287  // SetInnerHTML assigns the InnerHTML field with useful logic based on the type of input.
   288  // Values of string type are escaped using html.EscapeString().  Values in the
   289  // int or float type families or bool are converted to a string using the strconv package
   290  // (no escaping is required).  Nil values set InnerHTML to nil.  Non-nil pointers
   291  // are followed and the same rules applied.  Values implementing HTMLer will have their
   292  // HTML() method called and the result put into InnerHTML without escaping.
   293  // Values implementing fmt.Stringer have thier String() method called and the escaped result
   294  // used as in string above.
   295  //
   296  // All other values have undefined behavior but are currently handled by setting InnerHTML
   297  // to the result of: `html.EscapeString(fmt.Sprintf("%v", val))`
   298  func (n *VGNode) SetInnerHTML(val interface{}) {
   299  
   300  	var s string
   301  
   302  	if val == nil {
   303  		n.InnerHTML = nil
   304  		return
   305  	}
   306  
   307  	switch v := val.(type) {
   308  	case string:
   309  		s = html.EscapeString(v)
   310  	case int:
   311  		s = strconv.Itoa(v)
   312  	case int8:
   313  		s = strconv.Itoa(int(v))
   314  	case int16:
   315  		s = strconv.Itoa(int(v))
   316  	case int32:
   317  		s = strconv.Itoa(int(v))
   318  	case int64:
   319  		s = strconv.FormatInt(v, 10)
   320  	case uint:
   321  		s = strconv.FormatUint(uint64(v), 10)
   322  	case uint8:
   323  		s = strconv.FormatUint(uint64(v), 10)
   324  	case uint16:
   325  		s = strconv.FormatUint(uint64(v), 10)
   326  	case uint32:
   327  		s = strconv.FormatUint(uint64(v), 10)
   328  	case uint64:
   329  		s = strconv.FormatUint(v, 10)
   330  	case float32:
   331  		s = strconv.FormatFloat(float64(v), 'f', 6, 32)
   332  	case float64:
   333  		s = strconv.FormatFloat(v, 'f', 6, 64)
   334  	case bool:
   335  		// I don't see any use in making false result in no output in this case.
   336  		// I'm guessing if someone puts a bool in vg-content/vg-html
   337  		// they are expecting it to print out the text "true" or "false".
   338  		// It's different from AddAttrInterface but I think appropriate here.
   339  		s = strconv.FormatBool(v)
   340  
   341  	case HTMLer:
   342  		s = v.HTML()
   343  
   344  	case fmt.Stringer:
   345  		// we have to check that the given interface does not hide a nil ptr
   346  		if p := reflect.ValueOf(val); p.Kind() == reflect.Ptr && p.IsNil() {
   347  			n.InnerHTML = nil
   348  			return
   349  		}
   350  		s = html.EscapeString(v.String())
   351  	default:
   352  		// check if this is a ptr
   353  		rv := reflect.ValueOf(val)
   354  
   355  		if rv.Kind() == reflect.Ptr {
   356  			if rv.IsNil() {
   357  				return
   358  			}
   359  			if !rvIsZero(rv) {
   360  				indirected := reflect.Indirect(rv)
   361  				if !indirected.IsNil() {
   362  					n.SetInnerHTML(indirected.Interface())
   363  				}
   364  			}
   365  			return
   366  		}
   367  
   368  		// fall back to fmt
   369  		s = html.EscapeString(fmt.Sprintf("%v", val))
   370  	}
   371  
   372  	n.InnerHTML = &s
   373  }
   374  
   375  // JSValueHandler does something with a js.Value
   376  type JSValueHandler interface {
   377  	JSValueHandle(js.Value)
   378  }
   379  
   380  // JSValueFunc implements JSValueHandler as a function.
   381  type JSValueFunc func(js.Value)
   382  
   383  // JSValueHandle implements the JSValueHandler interface.
   384  func (f JSValueFunc) JSValueHandle(v js.Value) { f(v) }