github.com/vugu/vugu@v0.3.5/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 }