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

     1  package gen
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"unicode"
     8  
     9  	// "github.com/vugu/vugu/internal/html"
    10  	// "golang.org/x/net/html"
    11  	"github.com/vugu/html"
    12  )
    13  
    14  // compactNodeTree operates on a Node tree in-place and find elements with static
    15  // contents and converts them to corresponding vg-html expressions with static output.
    16  // Since vg-html ends up with a call to set innerHTML on an element in the DOM,
    17  // it is much faster for large blocks of HTML than individual syncing DOM nodes.
    18  // Any modern browser's native HTML parser is always going to be a lot faster than
    19  // we can achieve calling back and forth from wasm for each element.
    20  func compactNodeTree(rootN *html.Node) error {
    21  
    22  	// do not collapse html, body or head, and nothing inside head
    23  
    24  	var visit func(n *html.Node) (canCompact bool, err error)
    25  	visit = func(n *html.Node) (canCompact bool, err error) {
    26  
    27  		// certain tags we just refuse to examine at all
    28  		if n.Type == html.ElementNode && (n.Data == "head" ||
    29  			n.Data == "script" ||
    30  			n.Data == "style" ||
    31  			strings.HasPrefix(n.Data, "vg-")) {
    32  			return false, nil
    33  		}
    34  
    35  		// text nodes are always compactable (at least in the current implementation)
    36  		if n.FirstChild == nil && n.Type == html.TextNode {
    37  			return true, nil
    38  		}
    39  
    40  		var compactableNodes []*html.Node
    41  		allCompactable := true
    42  		// iterate over the immediate children of n
    43  		for n2 := n.FirstChild; n2 != nil; n2 = n2.NextSibling {
    44  			cc, err := visit(n2)
    45  			if err != nil {
    46  				return false, err
    47  			}
    48  			allCompactable = allCompactable && cc // keep track of if they are all compactable
    49  			if cc {
    50  				compactableNodes = append(compactableNodes, n2) // keep track of individual nodes that are compactable
    51  			}
    52  		}
    53  
    54  		// if we're in the top level HTML tag, that's it, we visited already above and we're done
    55  		if n.Type == html.ElementNode && n.Data == "html" {
    56  			return false, nil
    57  		}
    58  
    59  		// if not everything is compactable or it's the body node, then go through and compact the ones that can be
    60  		if !allCompactable || (n.Type == html.ElementNode && n.Data == "body") {
    61  
    62  			for _, cn := range compactableNodes {
    63  
    64  				// NOTE: isStaticEl(cn) has already been run, since canCompact returned true above to put it in this list
    65  
    66  				if cn.Type != html.ElementNode { // only work on elements
    67  					continue
    68  				}
    69  
    70  				var htmlBuf bytes.Buffer
    71  				// walk each immediate child of cn
    72  				for cnChild := cn.FirstChild; cnChild != nil; cnChild = cnChild.NextSibling {
    73  					// render directly into htmlBuf
    74  					err := html.Render(&htmlBuf, cnChild)
    75  					if err != nil {
    76  						return false, err
    77  					}
    78  				}
    79  
    80  				// add a vg-html with the static Go string expression of the contents casted to a vugu.HTML
    81  				cn.Attr = append(cn.Attr, html.Attribute{Key: "vg-html", Val: "vugu.HTML(" + htmlGoQuoteString(htmlBuf.String()) + ")"})
    82  				// cn.Attr = append(cn.Attr, html.Attribute{Key: "vg-html", Val: htmlGoQuoteString(htmlBuf.String())})
    83  
    84  				// remove children, since vg-html supplants them
    85  				cn.FirstChild = nil
    86  				cn.LastChild = nil
    87  
    88  			}
    89  
    90  			return false, nil
    91  		}
    92  
    93  		// if all of the children are compactable, we need to check if this is an element that contains no dynamic attributes
    94  		if allCompactable {
    95  			return isStaticEl(n), nil
    96  		}
    97  
    98  		// default is not compactable
    99  		return false, nil
   100  	}
   101  	_, err := visit(rootN)
   102  
   103  	return err
   104  }
   105  
   106  func isStaticEl(n *html.Node) bool {
   107  
   108  	if n.Type != html.ElementNode { // must be element
   109  		return false
   110  	}
   111  
   112  	// component elements cannot be compacted
   113  	if strings.Contains(n.Data, ":") {
   114  		return false
   115  	}
   116  	if n.Data == "vg-comp" {
   117  		return false
   118  	}
   119  
   120  	for _, attr := range n.Attr {
   121  		if strings.HasPrefix(attr.Key, "vg-") { // vg- prefix means dynamic stuff
   122  			return false
   123  		}
   124  		if len(attr.Key) == 0 { // avoid panic in this strange case
   125  			continue
   126  		}
   127  		if !unicode.IsLetter(rune(attr.Key[0])) { // anything except a letter as an attr we assume to be dynamic
   128  			return false
   129  		}
   130  	}
   131  
   132  	// if it passes above, should be fine to compact
   133  	return true
   134  
   135  }
   136  
   137  // htmlGoQuoteString is similar to printf'ing with %q but converts common things that require html escaping to
   138  // backslashes instead for improved clarity
   139  func htmlGoQuoteString(s string) string {
   140  
   141  	var buf bytes.Buffer
   142  
   143  	for _, c := range fmt.Sprintf("%q", s) {
   144  		switch c {
   145  		case '<', '>', '&':
   146  			var qc string
   147  			qc = fmt.Sprintf("\\x%X", uint8(c))
   148  			buf.WriteString(qc)
   149  		default:
   150  			buf.WriteRune(c)
   151  		}
   152  
   153  	}
   154  
   155  	return buf.String()
   156  }