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

     1  package gen
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"unicode"
    14  
    15  	// "github.com/vugu/vugu/internal/htmlx"
    16  	// "github.com/vugu/vugu/internal/htmlx/atom"
    17  	// "golang.org/x/net/html"
    18  	// "golang.org/x/net/html/atom"
    19  	"github.com/vugu/html"
    20  	"github.com/vugu/html/atom"
    21  	"github.com/vugu/vugu"
    22  )
    23  
    24  // ParserGo is a template parser that emits Go source code that will construct the appropriately wired VGNodes.
    25  type ParserGo struct {
    26  	PackageName string // name of package to use at top of files
    27  	StructType  string // just the struct name, no "*" (replaces ComponentType and DataType)
    28  	// ComponentType string // just the struct name, no "*"
    29  	// DataType      string // just the struct name, no "*"
    30  	OutDir  string // output dir
    31  	OutFile string // output file name with ".go" suffix
    32  
    33  	NoOptimizeStatic bool // set to true to disable optimization of static blocks of HTML into vg-html expressions
    34  	TinyGo           bool // set to true to enable TinyGo compatability changes to the generated code
    35  }
    36  
    37  func gofmt(pgm string) (string, error) {
    38  
    39  	// build up command to run
    40  	cmd := exec.Command("gofmt")
    41  
    42  	// I need to capture output
    43  	var fmtOutput bytes.Buffer
    44  	cmd.Stderr = &fmtOutput
    45  	cmd.Stdout = &fmtOutput
    46  
    47  	// also set up input pipe
    48  	read, write := io.Pipe()
    49  	defer write.Close() // make sure this always gets closed, it is safe to call more than once
    50  	cmd.Stdin = read
    51  
    52  	// copy down environment variables
    53  	cmd.Env = os.Environ()
    54  	// force wasm,js target
    55  	cmd.Env = append(cmd.Env, "GOOS=js")
    56  	cmd.Env = append(cmd.Env, "GOARCH=wasm")
    57  
    58  	// start gofmt
    59  	if err := cmd.Start(); err != nil {
    60  		return pgm, fmt.Errorf("can't run gofmt: %v", err)
    61  	}
    62  
    63  	// stream in the raw source
    64  	if _, err := write.Write([]byte(pgm)); err != nil && err != io.ErrClosedPipe {
    65  		return pgm, fmt.Errorf("gofmt failed: %v", err)
    66  	}
    67  
    68  	write.Close()
    69  
    70  	// wait until gofmt is done
    71  	if err := cmd.Wait(); err != nil {
    72  		return pgm, fmt.Errorf("go fmt error %v; full output: %s", err, fmtOutput.String())
    73  	}
    74  
    75  	return fmtOutput.String(), nil
    76  }
    77  
    78  // Parse is an experiment...
    79  // r is the actual input, fname is only used to emit line directives
    80  func (p *ParserGo) Parse(r io.Reader, fname string) error {
    81  
    82  	state := &parseGoState{}
    83  
    84  	inRaw, err := io.ReadAll(r)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	// use a tokenizer to peek at the first element and see if it's an HTML tag
    90  	state.isFullHTML = false
    91  	tmpZ := html.NewTokenizer(bytes.NewReader(inRaw))
    92  	for {
    93  		tt := tmpZ.Next()
    94  		if tt == html.ErrorToken {
    95  			return tmpZ.Err()
    96  		}
    97  		if tt != html.StartTagToken { // skip over non-tags
    98  			continue
    99  		}
   100  		t := tmpZ.Token()
   101  		if t.Data == "html" {
   102  			state.isFullHTML = true
   103  			break
   104  		}
   105  		break
   106  	}
   107  
   108  	// log.Printf("isFullHTML: %v", state.isFullHTML)
   109  
   110  	if state.isFullHTML {
   111  
   112  		n, err := html.Parse(bytes.NewReader(inRaw))
   113  		if err != nil {
   114  			return err
   115  		}
   116  		state.docNodeList = append(state.docNodeList, n) // docNodeList is just this one item
   117  
   118  	} else {
   119  
   120  		nlist, err := html.ParseFragment(bytes.NewReader(inRaw), &html.Node{
   121  			Type:     html.ElementNode,
   122  			DataAtom: atom.Div,
   123  			Data:     "div",
   124  		})
   125  		if err != nil {
   126  			return err
   127  		}
   128  
   129  		// only add elements
   130  		for _, n := range nlist {
   131  			if n.Type != html.ElementNode {
   132  				continue
   133  			}
   134  			// log.Printf("FRAGMENT: %#v", n)
   135  			state.docNodeList = append(state.docNodeList, n)
   136  		}
   137  
   138  	}
   139  
   140  	// run n through the optimizer and convert large chunks of static elements into
   141  	// vg-html attributes, this should provide a significiant performance boost for static HTML
   142  	if !p.NoOptimizeStatic {
   143  		for _, n := range state.docNodeList {
   144  			err = compactNodeTree(n)
   145  			if err != nil {
   146  				return err
   147  			}
   148  		}
   149  	}
   150  
   151  	// log.Printf("parsed document looks like so upon start of parsing:")
   152  	// for i, n := range state.docNodeList {
   153  	// 	var buf bytes.Buffer
   154  	// 	err := html.Render(&buf, n)
   155  	// 	if err != nil {
   156  	// 		return fmt.Errorf("error during debug render: %v", err)
   157  	// 	}
   158  	// 	log.Printf("state.docNodeList[%d]:\n%s", i, buf.Bytes())
   159  	// }
   160  
   161  	err = p.visitOverall(state)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	var buf bytes.Buffer
   167  	// log.Printf("goBuf.Len == %v", goBuf.Len())
   168  	buf.Write(state.goBuf.Bytes())
   169  	buf.Write(state.buildBuf.Bytes())
   170  	buf.Write(state.goBufBottom.Bytes())
   171  
   172  	outPath := filepath.Join(p.OutDir, p.OutFile)
   173  
   174  	fo, err := gofmt(buf.String())
   175  	if err != nil {
   176  
   177  		// if the gofmt errors, we still attempt to write out the non-fmt'ed output to the file, to assist in debugging
   178  		_ = os.WriteFile(outPath, buf.Bytes(), 0644)
   179  
   180  		return err
   181  	}
   182  
   183  	// run the import deduplicator
   184  	var dedupedBuf bytes.Buffer
   185  	err = dedupImports(bytes.NewReader([]byte(fo)), &dedupedBuf, p.OutFile)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	// write to final output file
   191  	err = os.WriteFile(outPath, dedupedBuf.Bytes(), 0644)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	err = removeRedundantDefinitions(outPath)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	return nil
   200  }
   201  
   202  func removeRedundantDefinitions(fileName string) error {
   203  	type definitions struct {
   204  		old, new string
   205  	}
   206  	r := []definitions{
   207  		{
   208  			old: "vugu.VGAttribute{vugu.VGAttribute",
   209  			new: "vugu.VGAttribute{",
   210  		},
   211  	}
   212  
   213  	content, err := os.ReadFile(fileName)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	var newContents string
   219  	for _, v := range r {
   220  		newContents = strings.Replace(string(content), v.old, v.new, -1)
   221  	}
   222  
   223  	err = os.WriteFile(fileName, []byte(newContents), 0644)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	return nil
   228  }
   229  
   230  //nolint:golint,unused
   231  type codeChunk struct {
   232  	Line   int
   233  	Column int
   234  	Code   string
   235  }
   236  
   237  type parseGoState struct {
   238  	isFullHTML  bool         // is the first node an <html> tag
   239  	docNodeList []*html.Node // top level nodes parsed out of source file
   240  	goBuf       bytes.Buffer // additional Go code (at top)
   241  	buildBuf    bytes.Buffer // Build() method Go code (below)
   242  	goBufBottom bytes.Buffer // additional Go code that is put as the very last thing
   243  	// cssChunkList []codeChunk
   244  	// jsChunkList  []codeChunk
   245  	outIsSet bool // set to true when vgout.Out has been set for to the level node
   246  }
   247  
   248  func (p *ParserGo) visitOverall(state *parseGoState) error {
   249  	// change the comment so that the first line matches the standard form found in a Go generate file.
   250  	// See: https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source
   251  	// The new second line tells the user to regenerate
   252  	fmt.Fprintf(&state.goBuf, "// Code generated by vugu via vugugen DO NOT EDIT.\n// Please regenerate instead of editing or add additional code in a separate file.\n\n")
   253  
   254  	fmt.Fprintf(&state.goBuf, "package %s\n\n", p.PackageName)
   255  	fmt.Fprintf(&state.goBuf, "import %q\n", "fmt")
   256  	fmt.Fprintf(&state.goBuf, "import %q\n", "reflect")
   257  	fmt.Fprintf(&state.goBuf, "import %q\n", "github.com/vugu/vjson")
   258  	fmt.Fprintf(&state.goBuf, "import %q\n", "github.com/vugu/vugu")
   259  	fmt.Fprintf(&state.goBuf, "import js %q\n", "github.com/vugu/vugu/js")
   260  	fmt.Fprintf(&state.goBuf, "import %q\n", "log")
   261  	fmt.Fprintf(&state.goBuf, "\n")
   262  
   263  	// TODO: we use a prefix like "vg" as our namespace; should document that user code should not use that prefix to avoid conflicts
   264  	fmt.Fprintf(&state.buildBuf, "func (c *%s) Build(vgin *vugu.BuildIn) (vgout *vugu.BuildOut) {\n", p.StructType)
   265  	fmt.Fprintf(&state.buildBuf, "    \n")
   266  	fmt.Fprintf(&state.buildBuf, "    vgout = &vugu.BuildOut{}\n")
   267  	fmt.Fprintf(&state.buildBuf, "    \n")
   268  	fmt.Fprintf(&state.buildBuf, "    var vgiterkey interface{}\n")
   269  	fmt.Fprintf(&state.buildBuf, "    _ = vgiterkey\n")
   270  	fmt.Fprintf(&state.buildBuf, "    var vgn *vugu.VGNode\n")
   271  	// fmt.Fprintf(&buildBuf, "    var vgparent *vugu.VGNode\n")
   272  
   273  	// NOTE: Use things that are lightweight here - e.g. don't do var _ = fmt.Sprintf because that brings in all of the
   274  	// (possibly quite large) formatting code, which might otherwise be avoided.
   275  	fmt.Fprintf(&state.goBufBottom, "// 'fix' unused imports\n")
   276  	fmt.Fprintf(&state.goBufBottom, "var _ fmt.Stringer\n")
   277  	fmt.Fprintf(&state.goBufBottom, "var _ reflect.Type\n")
   278  	fmt.Fprintf(&state.goBufBottom, "var _ vjson.RawMessage\n")
   279  	fmt.Fprintf(&state.goBufBottom, "var _ js.Value\n")
   280  	fmt.Fprintf(&state.goBufBottom, "var _ log.Logger\n")
   281  	fmt.Fprintf(&state.goBufBottom, "\n")
   282  
   283  	// remove document node if present
   284  	if len(state.docNodeList) == 1 && state.docNodeList[0].Type == html.DocumentNode {
   285  		state.docNodeList = []*html.Node{state.docNodeList[0].FirstChild}
   286  	}
   287  
   288  	if state.isFullHTML {
   289  
   290  		if len(state.docNodeList) != 1 {
   291  			return fmt.Errorf("full HTML mode but not exactly 1 node found (found %d)", len(state.docNodeList))
   292  		}
   293  		err := p.visitHTML(state, state.docNodeList[0])
   294  		if err != nil {
   295  			return err
   296  		}
   297  
   298  	} else {
   299  
   300  		gotTopNode := false
   301  
   302  		for _, n := range state.docNodeList {
   303  
   304  			// ignore comments
   305  			if n.Type == html.CommentNode {
   306  				continue
   307  			}
   308  
   309  			if n.Type == html.TextNode {
   310  
   311  				// ignore whitespace text
   312  				if strings.TrimSpace(n.Data) == "" {
   313  					continue
   314  				}
   315  
   316  				// error on non-whitespace text
   317  				return fmt.Errorf("unexpected text outside any element: %q", n.Data)
   318  
   319  			}
   320  
   321  			// must be an element at this point
   322  			if n.Type != html.ElementNode {
   323  				return fmt.Errorf("unexpected node type %v; node=%#v", n.Type, n)
   324  			}
   325  
   326  			if isScriptOrStyle(n) {
   327  
   328  				err := p.visitScriptOrStyle(state, n)
   329  				if err != nil {
   330  					return err
   331  				}
   332  				continue
   333  			}
   334  
   335  			if gotTopNode {
   336  				return fmt.Errorf("Found more than one top level element: %s", n.Data)
   337  			}
   338  			gotTopNode = true
   339  
   340  			// handle top node
   341  
   342  			// check for forbidden top level tags
   343  			nodeName := strings.ToLower(n.Data)
   344  			if nodeName == "head" ||
   345  				nodeName == "body" {
   346  				return fmt.Errorf("component cannot use %q as top level tag", nodeName)
   347  			}
   348  
   349  			err := p.visitTopNode(state, n)
   350  			if err != nil {
   351  				return err
   352  			}
   353  			continue
   354  
   355  		}
   356  
   357  	}
   358  
   359  	// for _, chunk := range state.cssChunkList {
   360  	// 	// fmt.Fprintf(&buildBuf, "    out.AppendCSS(/*line %s:%d*/%q)\n\n", fname, chunk.Line, chunk.Code)
   361  	// 	// fmt.Fprintf(&state.buildBuf, "    out.AppendCSS(%q)\n\n", chunk.Code)
   362  	// 	_ = chunk
   363  	// 	panic("need to append whole node, not AppendCSS")
   364  	// }
   365  
   366  	// for _, chunk := range state.jsChunkList {
   367  	// 	// fmt.Fprintf(&buildBuf, "    out.AppendJS(/*line %s:%d*/%q)\n\n", fname, chunk.Line, chunk.Code)
   368  	// 	// fmt.Fprintf(&state.buildBuf, "    out.AppendJS(%q)\n\n", chunk.Code)
   369  	// 	_ = chunk
   370  	// 	panic("need to append whole node, not AppendJS")
   371  	// }
   372  
   373  	fmt.Fprintf(&state.buildBuf, "    return vgout\n")
   374  	fmt.Fprintf(&state.buildBuf, "}\n\n")
   375  
   376  	return nil
   377  }
   378  
   379  func (p *ParserGo) visitHTML(state *parseGoState, n *html.Node) error {
   380  
   381  	pOutputTag(state, n)
   382  	// fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v}\n", n.Type, n.Data, staticVGAttr(n.Attr))
   383  	// fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn) // root for output\n") // for first element we need to assign as Doc on BuildOut
   384  	// state.outIsSet = true
   385  
   386  	// dynamic attrs
   387  	writeDynamicAttributes(state, n)
   388  
   389  	fmt.Fprintf(&state.buildBuf, "{\n")
   390  	fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n") // vgparent set for this block to vgn
   391  
   392  	for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   393  
   394  		if childN.Type != html.ElementNode {
   395  			continue
   396  		}
   397  
   398  		var err error
   399  		if strings.ToLower(childN.Data) == "head" {
   400  			err = p.visitHead(state, childN)
   401  		} else if strings.ToLower(childN.Data) == "body" {
   402  			err = p.visitBody(state, childN)
   403  		} else {
   404  			return fmt.Errorf("unknown tag inside html %q", childN.Data)
   405  
   406  		}
   407  
   408  		if err != nil {
   409  			return err
   410  		}
   411  
   412  	}
   413  
   414  	fmt.Fprintf(&state.buildBuf, "}\n")
   415  
   416  	return nil
   417  }
   418  
   419  func (p *ParserGo) visitHead(state *parseGoState, n *html.Node) error {
   420  
   421  	pOutputTag(state, n)
   422  	// fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v}\n", n.Type, n.Data, staticVGAttr(n.Attr))
   423  	// fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn) // root for output\n") // for first element we need to assign as Doc on BuildOut
   424  	// state.outIsSet = true
   425  
   426  	// dynamic attrs
   427  	writeDynamicAttributes(state, n)
   428  
   429  	fmt.Fprintf(&state.buildBuf, "{\n")
   430  	fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n") // vgparent set for this block to vgn
   431  
   432  	for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   433  
   434  		if isScriptOrStyle(childN) {
   435  			err := p.visitScriptOrStyle(state, childN)
   436  			if err != nil {
   437  				return err
   438  			}
   439  			continue
   440  		}
   441  
   442  		err := p.visitDefaultByType(state, childN)
   443  		if err != nil {
   444  			return err
   445  		}
   446  
   447  	}
   448  
   449  	fmt.Fprintf(&state.buildBuf, "}\n")
   450  
   451  	return nil
   452  
   453  }
   454  
   455  func (p *ParserGo) visitBody(state *parseGoState, n *html.Node) error {
   456  
   457  	pOutputTag(state, n)
   458  	// fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v}\n", n.Type, n.Data, staticVGAttr(n.Attr))
   459  	// fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn) // root for output\n") // for first element we need to assign as Doc on BuildOut
   460  	// state.outIsSet = true
   461  
   462  	// dynamic attrs
   463  	writeDynamicAttributes(state, n)
   464  
   465  	fmt.Fprintf(&state.buildBuf, "{\n")
   466  	fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n") // vgparent set for this block to vgn
   467  
   468  	foundMountEl := false
   469  
   470  	for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   471  
   472  		// ignore whitespace and comments directly in body
   473  		if childN.Type != html.ElementNode {
   474  			continue
   475  		}
   476  
   477  		if isScriptOrStyle(childN) {
   478  			err := p.visitScriptOrStyle(state, childN)
   479  			if err != nil {
   480  				return err
   481  			}
   482  			continue
   483  		}
   484  
   485  		if foundMountEl {
   486  			return fmt.Errorf("element %q found after we already have a mount element, you might have to wrap all your body content into a div", childN.Data)
   487  		}
   488  		foundMountEl = true
   489  
   490  		err := p.visitDefaultByType(state, childN)
   491  		if err != nil {
   492  			return err
   493  		}
   494  
   495  	}
   496  
   497  	fmt.Fprintf(&state.buildBuf, "}\n")
   498  
   499  	return nil
   500  
   501  }
   502  
   503  // visitScriptOrStyle calls visitJS, visitCSS or visitGo accordingly,
   504  // will error if the node does not correspond to one of those
   505  func (p *ParserGo) visitScriptOrStyle(state *parseGoState, n *html.Node) error {
   506  
   507  	nodeName := strings.ToLower(n.Data)
   508  
   509  	// script tag
   510  	if nodeName == "script" {
   511  
   512  		var mt string // mime type
   513  
   514  		ty := attrWithKey(n, "type")
   515  		if ty == nil {
   516  			// return fmt.Errorf("script tag without type attribute is not valid")
   517  			mt = ""
   518  		} else {
   519  			// tinygo support: just split on semi, don't need to import mime package
   520  			// mt, _, _ = mime.ParseMediaType(ty.Val)
   521  			mt = strings.Split(strings.TrimSpace(ty.Val), ";")[0]
   522  		}
   523  
   524  		// go code
   525  		if mt == "application/x-go" {
   526  			err := p.visitGo(state, n)
   527  			if err != nil {
   528  				return err
   529  			}
   530  			return nil
   531  		}
   532  
   533  		// component js (type attr omitted okay - means it is JS)
   534  		if mt == "text/javascript" || mt == "application/javascript" || mt == "" {
   535  			err := p.visitJS(state, n)
   536  			if err != nil {
   537  				return err
   538  			}
   539  			return nil
   540  		}
   541  
   542  		return fmt.Errorf("found script tag with invalid mime type %q", mt)
   543  
   544  	}
   545  
   546  	// component css
   547  	if nodeName == "style" || nodeName == "link" {
   548  		err := p.visitCSS(state, n)
   549  		if err != nil {
   550  			return err
   551  		}
   552  		return nil
   553  	}
   554  
   555  	return fmt.Errorf("element %q is not a valid script or style - %#v", n.Data, n)
   556  }
   557  
   558  func (p *ParserGo) visitJS(state *parseGoState, n *html.Node) error {
   559  
   560  	if n.Type != html.ElementNode {
   561  		return fmt.Errorf("visitJS, not an element node %#v", n)
   562  	}
   563  
   564  	nodeName := strings.ToLower(n.Data)
   565  
   566  	if nodeName != "script" {
   567  		return fmt.Errorf("visitJS, tag %q not a script", nodeName)
   568  	}
   569  
   570  	// see if there's a script inside, or if this is a script include
   571  	if n.FirstChild == nil {
   572  		// script include - we pretty much just let this through, don't care what the attrs are
   573  	} else {
   574  		// if there is a script inside, we do not allow attributes other than "type", to avoid
   575  		// people using features that might not be compatible with the funky stuff we have to do
   576  		// in vugu to make all this work
   577  
   578  		for _, a := range n.Attr {
   579  			if a.Key != "type" {
   580  				return fmt.Errorf("attribute %q not allowed on script tag that contains JS code", a.Key)
   581  			}
   582  			if a.Val != "text/javascript" && a.Val != "application/javascript" {
   583  				return fmt.Errorf("script type %q invalid (must be text/javascript)", a.Val)
   584  			}
   585  		}
   586  
   587  		// verify that all children are text nodes
   588  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   589  			if childN.Type != html.TextNode {
   590  				return fmt.Errorf("script tag contains non-text child: %#v", childN)
   591  			}
   592  		}
   593  
   594  	}
   595  
   596  	// allow control stuff, why not
   597  
   598  	// vg-for
   599  	if v, _ := vgForExpr(n); v.expr != "" {
   600  		if err := p.emitForExpr(state, n); err != nil {
   601  			return err
   602  		}
   603  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   604  	}
   605  
   606  	// vg-if
   607  	ife := vgIfExpr(n)
   608  	if ife != "" {
   609  		fmt.Fprintf(&state.buildBuf, "if %s {\n", ife)
   610  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   611  	}
   612  
   613  	// but then for the actual output, we append to vgout.JS, instead of parentNode
   614  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v}\n", n.Type, n.Data, staticVGAttr(n.Attr))
   615  
   616  	// output any text children
   617  	if n.FirstChild != nil {
   618  		fmt.Fprintf(&state.buildBuf, "{\n")
   619  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   620  			// NOTE: we already verified above that these are just text nodes
   621  			fmt.Fprintf(&state.buildBuf, "vgn.AppendChild(&vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v})\n", childN.Type, childN.Data, staticVGAttr(childN.Attr))
   622  		}
   623  		fmt.Fprintf(&state.buildBuf, "}\n")
   624  	}
   625  
   626  	fmt.Fprintf(&state.buildBuf, "vgout.AppendJS(vgn)\n")
   627  
   628  	// dynamic attrs
   629  	writeDynamicAttributes(state, n)
   630  
   631  	return nil
   632  }
   633  
   634  func (p *ParserGo) visitCSS(state *parseGoState, n *html.Node) error {
   635  
   636  	if n.Type != html.ElementNode {
   637  		return fmt.Errorf("visitCSS, not an element node %#v", n)
   638  	}
   639  
   640  	nodeName := strings.ToLower(n.Data)
   641  	switch nodeName {
   642  
   643  	case "link":
   644  
   645  		// okay as long as nothing is inside this node
   646  
   647  		if n.FirstChild != nil {
   648  			return fmt.Errorf("link tag should not have children")
   649  		}
   650  
   651  		// and it needs to have an href (url)
   652  		hrefAttr := attrWithKey(n, "href")
   653  		if hrefAttr == nil {
   654  			return fmt.Errorf("link tag must have href attribute but does not: %#v", n)
   655  		}
   656  
   657  	case "style":
   658  
   659  		// style must have child (will verify it is text below)
   660  		if n.FirstChild == nil {
   661  			return fmt.Errorf("style must have contents but does not: %#v", n)
   662  		}
   663  
   664  		// okay as long as only text nodes inside
   665  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   666  			if childN.Type != html.TextNode {
   667  				return fmt.Errorf("style tag contains non-text child: %#v", childN)
   668  			}
   669  		}
   670  
   671  	default:
   672  		return fmt.Errorf("visitCSS, unexpected tag name %q", nodeName)
   673  	}
   674  
   675  	// allow control stuff, why not
   676  
   677  	// vg-for
   678  	if v, _ := vgForExpr(n); v.expr != "" {
   679  		if err := p.emitForExpr(state, n); err != nil {
   680  			return err
   681  		}
   682  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   683  	}
   684  
   685  	// vg-if
   686  	ife := vgIfExpr(n)
   687  	if ife != "" {
   688  		fmt.Fprintf(&state.buildBuf, "if %s {\n", ife)
   689  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   690  	}
   691  
   692  	// but then for the actual output, we append to vgout.CSS, instead of parentNode
   693  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v}\n", n.Type, n.Data, staticVGAttr(n.Attr))
   694  
   695  	// output any text children
   696  	if n.FirstChild != nil {
   697  		fmt.Fprintf(&state.buildBuf, "{\n")
   698  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   699  			// NOTE: we already verified above that these are just text nodes
   700  			fmt.Fprintf(&state.buildBuf, "vgn.AppendChild(&vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v})\n", childN.Type, childN.Data, staticVGAttr(childN.Attr))
   701  		}
   702  		fmt.Fprintf(&state.buildBuf, "}\n")
   703  	}
   704  
   705  	fmt.Fprintf(&state.buildBuf, "vgout.AppendCSS(vgn)\n")
   706  
   707  	// dynamic attrs
   708  	writeDynamicAttributes(state, n)
   709  
   710  	return nil
   711  }
   712  
   713  func (p *ParserGo) visitGo(state *parseGoState, n *html.Node) error {
   714  
   715  	for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   716  		if childN.Type != html.TextNode {
   717  			return fmt.Errorf("unexpected node type %v inside of script tag", childN.Type)
   718  		}
   719  		// if childN.Line > 0 {
   720  		// 	fmt.Fprintf(&goBuf, "//line %s:%d\n", fname, childN.Line)
   721  		// }
   722  		state.goBuf.WriteString(childN.Data)
   723  	}
   724  
   725  	return nil
   726  }
   727  
   728  // visitTopNode handles the "mount point"
   729  func (p *ParserGo) visitTopNode(state *parseGoState, n *html.Node) error {
   730  
   731  	// handle the top element other than <html>
   732  
   733  	err := p.visitNodeJustElement(state, n)
   734  	if err != nil {
   735  		return err
   736  	}
   737  
   738  	return nil
   739  }
   740  
   741  // visitNodeElementAndCtrl handles an element that supports vg-if, vg-for etc
   742  func (p *ParserGo) visitNodeElementAndCtrl(state *parseGoState, n *html.Node) error {
   743  
   744  	// vg-for
   745  	if v, _ := vgForExpr(n); v.expr != "" {
   746  		if err := p.emitForExpr(state, n); err != nil {
   747  			return err
   748  		}
   749  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   750  	}
   751  
   752  	// vg-if
   753  	ife := vgIfExpr(n)
   754  	if ife != "" {
   755  		fmt.Fprintf(&state.buildBuf, "if %s {\n", ife)
   756  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   757  	}
   758  
   759  	err := p.visitNodeJustElement(state, n)
   760  	if err != nil {
   761  		return err
   762  	}
   763  
   764  	return nil
   765  }
   766  
   767  // visitNodeJustElement handles an element, ignoring any vg-if, vg-for (but it does handle vg-html - since that is not really "control" just a shorthand for it's contents)
   768  func (p *ParserGo) visitNodeJustElement(state *parseGoState, n *html.Node) error {
   769  
   770  	// regular element
   771  
   772  	// if n.Line > 0 {
   773  	// 	fmt.Fprintf(&buildBuf, "//line %s:%d\n", fname, n.Line)
   774  	// }
   775  
   776  	pOutputTag(state, n)
   777  	// fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q,Attr:%#v}\n", n.Type, n.Data, staticVGAttr(n.Attr))
   778  	// if state.outIsSet {
   779  	// 	fmt.Fprintf(&state.buildBuf, "vgparent.AppendChild(vgn)\n") // if not root, make AppendChild call
   780  	// } else {
   781  	// 	fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn) // root for output\n") // for first element we need to assign as Doc on BuildOut
   782  	// 	state.outIsSet = true
   783  	// }
   784  
   785  	// dynamic attrs
   786  	writeDynamicAttributes(state, n)
   787  
   788  	// vg-js-*
   789  	writeJSCallbackAttributes(state, n)
   790  
   791  	// js properties
   792  	propExprMap, propExprMapKeys := propVGAttrExpr(n)
   793  	for _, k := range propExprMapKeys {
   794  		valExpr := propExprMap[k]
   795  		fmt.Fprintf(&state.buildBuf, "{b, err := vjson.Marshal(%s); if err != nil { panic(err) }; vgn.Prop = append(vgn.Prop, vugu.VGProperty{Key:%q,JSONVal:vjson.RawMessage(b)})}\n", valExpr, k)
   796  	}
   797  
   798  	// vg-html
   799  	htmlExpr := vgHTMLExpr(n)
   800  	if htmlExpr != "" {
   801  		fmt.Fprintf(&state.buildBuf, "vgn.SetInnerHTML(%s)\n", htmlExpr)
   802  	}
   803  
   804  	// DOM events
   805  	eventMap, eventKeys := vgDOMEventExprs(n)
   806  	for _, k := range eventKeys {
   807  		expr := eventMap[k]
   808  		fmt.Fprintf(&state.buildBuf, "vgn.DOMEventHandlerSpecList = append(vgn.DOMEventHandlerSpecList, vugu.DOMEventHandlerSpec{\n")
   809  		fmt.Fprintf(&state.buildBuf, "EventType: %q,\n", k)
   810  		fmt.Fprintf(&state.buildBuf, "Func: func(event vugu.DOMEvent) { %s },\n", expr)
   811  		fmt.Fprintf(&state.buildBuf, "// TODO: implement capture, etc. mostly need to decide syntax\n")
   812  		fmt.Fprintf(&state.buildBuf, "})\n")
   813  	}
   814  
   815  	if n.FirstChild != nil {
   816  
   817  		fmt.Fprintf(&state.buildBuf, "{\n")
   818  		fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n") // vgparent set for this block to vgn
   819  
   820  		// iterate over children
   821  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   822  
   823  			err := p.visitDefaultByType(state, childN)
   824  			if err != nil {
   825  				return err
   826  			}
   827  		}
   828  
   829  		fmt.Fprintf(&state.buildBuf, "}\n")
   830  
   831  	}
   832  
   833  	return nil
   834  }
   835  
   836  func (p *ParserGo) visitDefaultByType(state *parseGoState, n *html.Node) error {
   837  
   838  	// handle child according to type
   839  	var err error
   840  	switch {
   841  	case n.Type == html.CommentNode:
   842  		err = p.visitNodeComment(state, n)
   843  	case n.Type == html.TextNode:
   844  		err = p.visitNodeText(state, n)
   845  	case n.Type == html.ElementNode:
   846  		if strings.Contains(n.Data, ":") {
   847  			// NOTE: this should check for a capital letter after the colon - this would distinguish
   848  			// svg:svg (valid regular HTML) from svg:Svg (a component reference)
   849  			err = p.visitNodeComponentElement(state, n)
   850  		} else if n.Data == "vg-comp" {
   851  			err = p.visitVGCompTag(state, n)
   852  		} else if n.Data == "vg-template" {
   853  			err = p.visitVGTemplateTag(state, n)
   854  		} else {
   855  			err = p.visitNodeElementAndCtrl(state, n)
   856  		}
   857  	default:
   858  		return fmt.Errorf("child node of unknown type %v: %#v", n.Type, n)
   859  	}
   860  
   861  	if err != nil {
   862  		return err
   863  	}
   864  
   865  	return nil
   866  }
   867  
   868  func (p *ParserGo) visitNodeText(state *parseGoState, n *html.Node) error {
   869  
   870  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q}\n", n.Type, n.Data)
   871  	fmt.Fprintf(&state.buildBuf, "vgparent.AppendChild(vgn)\n")
   872  
   873  	return nil
   874  }
   875  
   876  func (p *ParserGo) visitNodeComment(state *parseGoState, n *html.Node) error {
   877  
   878  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Data:%q}\n", n.Type, n.Data)
   879  	fmt.Fprintf(&state.buildBuf, "vgparent.AppendChild(vgn)\n")
   880  
   881  	return nil
   882  }
   883  
   884  // visitVGCompTag handles a vg-comp
   885  func (p *ParserGo) visitVGCompTag(state *parseGoState, n *html.Node) error {
   886  
   887  	// vg-for not allowed here
   888  
   889  	// vg-if is supported
   890  	ife := vgIfExpr(n)
   891  	if ife != "" {
   892  		fmt.Fprintf(&state.buildBuf, "if %s {\n", ife)
   893  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   894  	}
   895  
   896  	// for now, nothing else supported
   897  
   898  	// must have a "expr" which gives the Go expression which will result in a component
   899  	expr := vgCompExpr(n)
   900  	if expr == "" {
   901  		return fmt.Errorf("vg-comp must have an `expr` attribute with a Go expression in it")
   902  	}
   903  	fmt.Fprintf(&state.buildBuf, "{\n")
   904  	defer fmt.Fprintf(&state.buildBuf, "}\n")
   905  
   906  	fmt.Fprintf(&state.buildBuf, "var vgcomp vugu.Builder = %s\n", expr)
   907  	fmt.Fprintf(&state.buildBuf, "if vgcomp != nil {\n")
   908  	fmt.Fprintf(&state.buildBuf, "    vgin.BuildEnv.WireComponent(vgcomp)\n")
   909  	fmt.Fprintf(&state.buildBuf, "    vgout.Components = append(vgout.Components, vgcomp)\n")
   910  	fmt.Fprintf(&state.buildBuf, "    vgn = &vugu.VGNode{Component:vgcomp}\n")
   911  	fmt.Fprintf(&state.buildBuf, "    vgparent.AppendChild(vgn)\n")
   912  	fmt.Fprintf(&state.buildBuf, "}\n")
   913  
   914  	return nil
   915  }
   916  
   917  // visitVGTemplateTag handles vg-template
   918  func (p *ParserGo) visitVGTemplateTag(state *parseGoState, n *html.Node) error {
   919  
   920  	// vg-for
   921  	if v, _ := vgForExpr(n); v.expr != "" {
   922  		if err := p.emitForExpr(state, n); err != nil {
   923  			return err
   924  		}
   925  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   926  	}
   927  
   928  	// vg-if
   929  	ife := vgIfExpr(n)
   930  	if ife != "" {
   931  		fmt.Fprintf(&state.buildBuf, "if %s {\n", ife)
   932  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   933  	}
   934  
   935  	// output a node with type Element but empty data
   936  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d)} // <vg-template>\n", vugu.ElementNode)
   937  	fmt.Fprintf(&state.buildBuf, "vgparent.AppendChild(vgn)\n")
   938  
   939  	// and then only process children
   940  	if n.FirstChild != nil {
   941  
   942  		fmt.Fprintf(&state.buildBuf, "{\n")
   943  		fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n") // vgparent set for this block to vgn
   944  
   945  		// iterate over children
   946  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
   947  
   948  			err := p.visitDefaultByType(state, childN)
   949  			if err != nil {
   950  				return err
   951  			}
   952  		}
   953  
   954  		fmt.Fprintf(&state.buildBuf, "}\n")
   955  
   956  	}
   957  
   958  	return nil
   959  }
   960  
   961  // visitNodeComponentElement handles an element that is a call to a component
   962  func (p *ParserGo) visitNodeComponentElement(state *parseGoState, n *html.Node) error {
   963  
   964  	// components are just different so we handle all of our own vg-for vg-if and everything else
   965  
   966  	// vg-for
   967  	if v, _ := vgForExpr(n); v.expr != "" {
   968  		if err := p.emitForExpr(state, n); err != nil {
   969  			return err
   970  		}
   971  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   972  	}
   973  
   974  	// vg-if
   975  	ife := vgIfExpr(n)
   976  	if ife != "" {
   977  		fmt.Fprintf(&state.buildBuf, "if %s {\n", ife)
   978  		defer fmt.Fprintf(&state.buildBuf, "}\n")
   979  	}
   980  
   981  	nodeName := n.OrigData // use original case of element
   982  	nodeNameParts := strings.Split(nodeName, ":")
   983  	if len(nodeNameParts) != 2 {
   984  		return fmt.Errorf("invalid component tag name %q must contain exactly one colon", nodeName)
   985  	}
   986  
   987  	// x.Y or just Y depending on if in same package
   988  	typeExpr := strings.Join(nodeNameParts, ".")
   989  	pkgPrefix := nodeNameParts[0] + "." // needed so we can calc pkg name for pkg.WhateverEvent
   990  	if nodeNameParts[0] == p.PackageName {
   991  		typeExpr = nodeNameParts[1]
   992  		pkgPrefix = ""
   993  	}
   994  
   995  	compKeyID := compHashCounted(p.StructType + "." + n.OrigData)
   996  
   997  	fmt.Fprintf(&state.buildBuf, "{\n")
   998  	defer fmt.Fprintf(&state.buildBuf, "}\n")
   999  
  1000  	keyExpr := vgKeyExpr(n)
  1001  	if keyExpr != "" {
  1002  		fmt.Fprintf(&state.buildBuf, "vgcompKey := vugu.MakeCompKey(0x%X^vgin.CurrentPositionHash(), %s)\n", compKeyID, keyExpr)
  1003  	} else {
  1004  		fmt.Fprintf(&state.buildBuf, "vgcompKey := vugu.MakeCompKey(0x%X^vgin.CurrentPositionHash(), vgiterkey)\n", compKeyID)
  1005  	}
  1006  	fmt.Fprintf(&state.buildBuf, "// ask BuildEnv for prior instance of this specific component\n")
  1007  	fmt.Fprintf(&state.buildBuf, "vgcomp, _ := vgin.BuildEnv.CachedComponent(vgcompKey).(*%s)\n", typeExpr)
  1008  	fmt.Fprintf(&state.buildBuf, "if vgcomp == nil {\n")
  1009  	fmt.Fprintf(&state.buildBuf, "// create new one if needed\n")
  1010  	fmt.Fprintf(&state.buildBuf, "vgcomp = new(%s)\n", typeExpr)
  1011  	fmt.Fprintf(&state.buildBuf, "vgin.BuildEnv.WireComponent(vgcomp)\n")
  1012  	fmt.Fprintf(&state.buildBuf, "}\n")
  1013  	fmt.Fprintf(&state.buildBuf, "vgin.BuildEnv.UseComponent(vgcompKey, vgcomp) // ensure we can use this in the cache next time around\n")
  1014  
  1015  	// now that we have vgcomp with the right type and a correct value, we can declare the vg-var if specified
  1016  	if vgv := vgVarExpr(n); vgv != "" {
  1017  		fmt.Fprintf(&state.buildBuf, "var %s = vgcomp // vg-var\n", vgv)
  1018  
  1019  		// NOTE: It's a bit too much to have "unused variable" errors coming from a Vugu code-generated file,
  1020  		// too far off the beaten path of making "type-safe HTML templates with Go".  It makes sense with
  1021  		// hand-written Go code but I don't think so here.
  1022  		fmt.Fprintf(&state.buildBuf, "_ = %s\n", vgv) // avoid unused var error
  1023  	}
  1024  
  1025  	didAttrMap := false
  1026  
  1027  	// dynamic attrs
  1028  	dynExprMap, dynExprMapKeys := dynamicVGAttrExpr(n)
  1029  	for _, k := range dynExprMapKeys {
  1030  		// if k == "" {
  1031  		// 	return fmt.Errorf("invalid empty dynamic attribute name on component %#v", n)
  1032  		// }
  1033  
  1034  		valExpr := dynExprMap[k]
  1035  
  1036  		// if starts with upper case, it's a field name
  1037  		if hasUpperFirst(k) {
  1038  			fmt.Fprintf(&state.buildBuf, "vgcomp.%s = %s\n", k, valExpr)
  1039  		} else {
  1040  			// otherwise we use an "AttrMap"
  1041  			if !didAttrMap {
  1042  				didAttrMap = true
  1043  				fmt.Fprintf(&state.buildBuf, "vgcomp.AttrMap = make(map[string]interface{}, 8)\n")
  1044  			}
  1045  			fmt.Fprintf(&state.buildBuf, "vgcomp.AttrMap[%q] = %s\n", k, valExpr)
  1046  		}
  1047  
  1048  	}
  1049  
  1050  	// static attrs
  1051  	vgAttrs := staticVGAttr(n.Attr)
  1052  	for _, a := range vgAttrs {
  1053  		// if starts with upper case, it's a field name
  1054  		if hasUpperFirst(a.Key) {
  1055  			fmt.Fprintf(&state.buildBuf, "vgcomp.%s = %q\n", a.Key, a.Val)
  1056  		} else {
  1057  			// otherwise we use an "AttrMap"
  1058  			if !didAttrMap {
  1059  				didAttrMap = true
  1060  				fmt.Fprintf(&state.buildBuf, "vgcomp.AttrMap = make(map[string]interface{}, 8)\n")
  1061  			}
  1062  			fmt.Fprintf(&state.buildBuf, "vgcomp.AttrMap[%q] = %q\n", a.Key, a.Val)
  1063  		}
  1064  	}
  1065  
  1066  	// component events
  1067  	// NOTE: We keep component events really simple and the @ is just a thin wrapper around a field assignment:
  1068  	//     <pkg:Comp @Something="log.Println(event)"></pkg:Comp>
  1069  	// is shorthand for:
  1070  	//     <pkg:Comp :Something='func(event pkg.SomethingEvent) { log.Println(event) }'></pkg:Comp>
  1071  	//
  1072  	// I considered using the handler interface function approach for this, but it would mean
  1073  	// SomethingHandlerFunc would have to exist as a type, with a SomethingHandle method, which
  1074  	// implements a SomethingHandler interface, so the type of Comp.Something could be SomethingHandler,
  1075  	// and the emitted code could be vgcomp.Something = pkg.SomethingHandlerFunc(func...)
  1076  	// But that's two additional types and a method for every event.  I'm very concerned that it will
  1077  	// make component events feel crufty and arduous to implement (unless we could find a good way
  1078  	// to automatically generate those when missing - that's a possibility - actually I think
  1079  	// I'm going to try this, see https://github.com/vugu/vugu/issues/128).
  1080  	// But this this way with a func you can just do
  1081  	// type SomethingEvent struct { /* whatever relevant data */ } and then define your field on
  1082  	// your component as Something func(SomethingEvent) - still type-safe but very straightforward.
  1083  	// So far it seems like the best approach.
  1084  
  1085  	eventMap, eventKeys := vgEventExprs(n)
  1086  	for _, k := range eventKeys {
  1087  		expr := eventMap[k]
  1088  		// fmt.Fprintf(&state.buildBuf, "vgcomp.%s = func(event %s%sEvent){%s}\n", k, pkgPrefix, k, expr)
  1089  		// switched to using interfaces
  1090  		fmt.Fprintf(&state.buildBuf, "vgcomp.%s = %s%sFunc(func(event %s%sEvent){%s})\n", k, pkgPrefix, k, pkgPrefix, k, expr)
  1091  	}
  1092  
  1093  	// NOTE: vugugen:slot might come in really handy, have to work out the types involved - update: as it stands, this won't be needed.
  1094  
  1095  	// slots:
  1096  
  1097  	// scan children and see if it's default slot mode or vg-slot tags
  1098  	foundTagSlot, foundDefSlot := false, false
  1099  	var foundTagSlotNames []string
  1100  	for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
  1101  
  1102  		// non-ws text means default slot
  1103  		if childN.Type == html.TextNode {
  1104  			if strings.TrimSpace(childN.Data) != "" {
  1105  				foundDefSlot = true
  1106  			}
  1107  			continue
  1108  		}
  1109  
  1110  		// ignore comments
  1111  		if childN.Type == html.CommentNode {
  1112  			continue
  1113  		}
  1114  
  1115  		// should only be element at this point
  1116  		if childN.Type != html.ElementNode {
  1117  			return fmt.Errorf("in tag %q unexpected node found where slot expected: %#v", n.Data, childN)
  1118  		}
  1119  
  1120  		if childN.Data == "vg-slot" {
  1121  			foundTagSlot = true
  1122  			name := strings.TrimSpace(vgSlotName(childN))
  1123  			if name != "" {
  1124  				foundTagSlotNames = append(foundTagSlotNames, name)
  1125  			}
  1126  		} else {
  1127  			foundDefSlot = true
  1128  		}
  1129  	}
  1130  
  1131  	// now process slot(s) appropriately according to format
  1132  	switch {
  1133  
  1134  	case foundTagSlot && foundDefSlot:
  1135  		return fmt.Errorf("in tag %q found both vg-slot and other markup, only one or the other is allowed", n.Data)
  1136  
  1137  	case foundTagSlot:
  1138  
  1139  		// NOTE:
  1140  		// <vg-slot name="X"> will assign to vgcomp.X
  1141  		// <vg-slot name='X[Y]'> will assume X is of type map[string]Builder and create the map and then assign with X[Y] =
  1142  
  1143  		// find any names with map expressions and clear the maps
  1144  		sort.Strings(foundTagSlotNames)
  1145  		slotMapInited := make(map[string]bool)
  1146  		for _, slotName := range foundTagSlotNames {
  1147  			slotNameParts := strings.Split(slotName, "[") // check for map expr
  1148  			if len(slotNameParts) > 1 {                   // if map
  1149  				if slotMapInited[slotNameParts[0]] { // if not already initialized
  1150  					continue
  1151  				}
  1152  				slotMapInited[slotNameParts[0]] = true
  1153  
  1154  				// if nil create map, otherwise reuse
  1155  				fmt.Fprintf(&state.buildBuf, "if vgcomp.%s == nil {\n", slotNameParts[0])
  1156  				fmt.Fprintf(&state.buildBuf, "    vgcomp.%s = make(map[string]vugu.Builder)\n", slotNameParts[0])
  1157  				fmt.Fprintf(&state.buildBuf, "} else {\n")
  1158  				fmt.Fprintf(&state.buildBuf, "    for k := range vgcomp.%s { delete(vgcomp.%s, k) }\n", slotNameParts[0], slotNameParts[0])
  1159  				fmt.Fprintf(&state.buildBuf, "}\n")
  1160  			}
  1161  		}
  1162  
  1163  		// iterate over children
  1164  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
  1165  
  1166  			// ignore white space and coments
  1167  			if childN.Type == html.CommentNode ||
  1168  				(childN.Type == html.TextNode && strings.TrimSpace(childN.Data) == "") {
  1169  				continue
  1170  			}
  1171  
  1172  			if childN.Type != html.ElementNode { // should be impossible from foundTagSlot check above, just making sure
  1173  				panic(fmt.Errorf("unexpected non-element found where vg-slot should be: %#v", childN))
  1174  			}
  1175  
  1176  			if childN.Data != "vg-slot" { // should also be imposible
  1177  				panic(fmt.Errorf("unexpected element found where vg-slot should be: %#v", childN))
  1178  			}
  1179  
  1180  			slotName := strings.TrimSpace(vgSlotName(childN))
  1181  			if slotName == "" {
  1182  				return fmt.Errorf("found vg-slot tag without a 'name' attribute, the name is required")
  1183  			}
  1184  
  1185  			fmt.Fprintf(&state.buildBuf, "vgcomp.%s = vugu.NewBuilderFunc(func(vgin *vugu.BuildIn) (vgout *vugu.BuildOut) {\n", slotName)
  1186  			fmt.Fprintf(&state.buildBuf, "vgn := &vugu.VGNode{Type:vugu.VGNodeType(%d)}\n", vugu.ElementNode)
  1187  			fmt.Fprintf(&state.buildBuf, "vgout = &vugu.BuildOut{}\n")
  1188  			fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn)\n")
  1189  			fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n")
  1190  			fmt.Fprintf(&state.buildBuf, "\n")
  1191  
  1192  			// iterate over children and do the usual with each one
  1193  			for innerChildN := childN.FirstChild; innerChildN != nil; innerChildN = innerChildN.NextSibling {
  1194  				err := p.visitDefaultByType(state, innerChildN)
  1195  				if err != nil {
  1196  					return err
  1197  				}
  1198  			}
  1199  
  1200  			fmt.Fprintf(&state.buildBuf, "return\n")
  1201  			fmt.Fprintf(&state.buildBuf, "})\n")
  1202  
  1203  		}
  1204  
  1205  	case foundDefSlot:
  1206  		fmt.Fprintf(&state.buildBuf, "vgcomp.DefaultSlot = vugu.NewBuilderFunc(func(vgin *vugu.BuildIn) (vgout *vugu.BuildOut) {\n")
  1207  		// vgn is the equivalent of a vg-template tag and becomes the contents of vgout.Out and the vgparent
  1208  		fmt.Fprintf(&state.buildBuf, "vgn := &vugu.VGNode{Type:vugu.VGNodeType(%d)}\n", vugu.ElementNode)
  1209  		fmt.Fprintf(&state.buildBuf, "vgout = &vugu.BuildOut{}\n")
  1210  		fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn)\n")
  1211  		fmt.Fprintf(&state.buildBuf, "vgparent := vgn; _ = vgparent\n")
  1212  		fmt.Fprintf(&state.buildBuf, "\n")
  1213  
  1214  		// iterate over children and do the usual with each one
  1215  		for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
  1216  			err := p.visitDefaultByType(state, childN)
  1217  			if err != nil {
  1218  				return err
  1219  			}
  1220  		}
  1221  
  1222  		fmt.Fprintf(&state.buildBuf, "return\n")
  1223  		fmt.Fprintf(&state.buildBuf, "})\n")
  1224  
  1225  	default:
  1226  		// nothing meaningful inside this component tag
  1227  	}
  1228  
  1229  	// // keep track of contents for default slot
  1230  	// var defSlotNodes []*html.Node
  1231  	// defSlotMode := false // start off not in default slot mode and look for <vg-slot> tags
  1232  
  1233  	// // loop over all component children
  1234  	// for childN := n.FirstChild; childN != nil; childN = childN.NextSibling {
  1235  
  1236  	// 	if !defSlotMode {
  1237  
  1238  	// 		// anything not an element just add to the list for default
  1239  	// 		if childN.Type != html.ElementNode {
  1240  	// 			defSlotNodes = append(defSlotNodes, childN)
  1241  	// 			continue
  1242  	// 		}
  1243  
  1244  	// 		if childN.Data == "vg-slot" {
  1245  
  1246  	// 		}
  1247  
  1248  	// 	}
  1249  
  1250  	// }
  1251  
  1252  	// ignore whitespace
  1253  	// first non-slot, non-ws child, assume "DefaultSlot" (or whatever name) and consume rest of children
  1254  	// if vg-slot, then consume with specified name
  1255  	// <vg-slot name="SomeSlot"> <!-- field name syntax
  1256  	// <vg-slot name='SomeDynaSlot' index='"Row.FirstName"'> <!-- expression syntax, HM, NO
  1257  	// <vg-slot index='SomeDynaSlot["Row.FirstName"]'> <!-- maybe this - still annoying that we have to limit it to a map expression, but whatever
  1258  	// emit vgcomp.SlotName = vugu.NewBuilderFunc(func(vgin *vugu.BuildIn) (vgout *BuildOut, vgerr error) { ... })
  1259  	// and descend into children
  1260  
  1261  	fmt.Fprintf(&state.buildBuf, "vgout.Components = append(vgout.Components, vgcomp)\n")
  1262  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Component:vgcomp}\n")
  1263  	fmt.Fprintf(&state.buildBuf, "vgparent.AppendChild(vgn)\n")
  1264  
  1265  	return nil
  1266  	// return fmt.Errorf("component tag not yet supported (%q)", nodeName)
  1267  }
  1268  
  1269  // NOTE: caller is responsible for emitting the closing curly bracket
  1270  func (p *ParserGo) emitForExpr(state *parseGoState, n *html.Node) error {
  1271  
  1272  	forattr, err := vgForExpr(n)
  1273  	if err != nil {
  1274  		return err
  1275  	}
  1276  	forx := forattr.expr
  1277  	if forx == "" {
  1278  		return errors.New("no for expression, code should not be calling emitForExpr when no vg-for is present")
  1279  	}
  1280  
  1281  	// cases to select vgiterkey:
  1282  	// * check for vg-key attribute
  1283  	// * _, v := // replace _ with vgiterkey
  1284  	// * key, value := // unused vars, use 'key' as iter val
  1285  	// * k, v := // detect `k` and use as iterval
  1286  
  1287  	vgiterkeyx := vgKeyExpr(n)
  1288  
  1289  	// determine iteration variables
  1290  	var iterkey, iterval string
  1291  	if !strings.Contains(forx, ":=") {
  1292  		// make it so `w` is a shorthand for `key, value := range w`
  1293  		iterkey, iterval = "key", "value"
  1294  		forx = "key, value := range " + forx
  1295  	} else {
  1296  		// extract iteration variables
  1297  		var (
  1298  			itervars [2]string
  1299  			iteridx  int
  1300  		)
  1301  		for _, c := range forx {
  1302  			if c == ':' {
  1303  				break
  1304  			}
  1305  			if c == ',' {
  1306  				iteridx++
  1307  				continue
  1308  			}
  1309  			if unicode.IsSpace(c) {
  1310  				continue
  1311  			}
  1312  			itervars[iteridx] += string(c)
  1313  		}
  1314  
  1315  		iterkey = itervars[0]
  1316  		iterval = itervars[1]
  1317  	}
  1318  
  1319  	// detect "_, k := " form combined with no vg-key specified and replace
  1320  	if vgiterkeyx == "" && iterkey == "_" {
  1321  		iterkey = "vgiterkeyt"
  1322  		forx = "vgiterkeyt " + forx[1:]
  1323  	}
  1324  
  1325  	// if still no vgiterkeyx use the first identifier
  1326  	if vgiterkeyx == "" {
  1327  		vgiterkeyx = iterkey
  1328  	}
  1329  
  1330  	fmt.Fprintf(&state.buildBuf, "for %s {\n", forx)
  1331  	fmt.Fprintf(&state.buildBuf, "var vgiterkey interface{} = %s\n", vgiterkeyx)
  1332  	fmt.Fprintf(&state.buildBuf, "_ = vgiterkey\n")
  1333  	if !forattr.noshadow {
  1334  		if iterkey != "_" && iterkey != "vgiterkeyt" {
  1335  			fmt.Fprintf(&state.buildBuf, "%[1]s := %[1]s\n", iterkey)
  1336  			fmt.Fprintf(&state.buildBuf, "_ = %s\n", iterkey)
  1337  		}
  1338  		if iterval != "_" && iterval != "" {
  1339  			fmt.Fprintf(&state.buildBuf, "%[1]s := %[1]s\n", iterval)
  1340  			fmt.Fprintf(&state.buildBuf, "_ = %s\n", iterval)
  1341  		}
  1342  	}
  1343  
  1344  	return nil
  1345  }
  1346  
  1347  func hasUpperFirst(s string) bool {
  1348  	for _, c := range s {
  1349  		return unicode.IsUpper(c)
  1350  	}
  1351  	return false
  1352  }
  1353  
  1354  // isScriptOrStyle returns true if this is a "script", "style" or "link" tag
  1355  func isScriptOrStyle(n *html.Node) bool {
  1356  	if n.Type != html.ElementNode {
  1357  		return false
  1358  	}
  1359  	switch strings.ToLower(n.Data) {
  1360  	case "script", "style", "link":
  1361  		return true
  1362  	}
  1363  	return false
  1364  }
  1365  
  1366  func pOutputTag(state *parseGoState, n *html.Node) {
  1367  	fmt.Fprintf(&state.buildBuf, "vgn = &vugu.VGNode{Type:vugu.VGNodeType(%d),Namespace:%q,Data:%q,Attr:%#v}\n", n.Type, n.Namespace, n.Data, staticVGAttr(n.Attr))
  1368  	if state.outIsSet {
  1369  		fmt.Fprintf(&state.buildBuf, "vgparent.AppendChild(vgn)\n") // if not root, make AppendChild call
  1370  	} else {
  1371  		fmt.Fprintf(&state.buildBuf, "vgout.Out = append(vgout.Out, vgn) // root for output\n") // for first element we need to assign as Doc on BuildOut
  1372  		state.outIsSet = true
  1373  	}
  1374  
  1375  }
  1376  
  1377  func attrWithKey(n *html.Node, key string) *html.Attribute {
  1378  	for i := range n.Attr {
  1379  		if n.Attr[i].Key == key {
  1380  			return &n.Attr[i]
  1381  		}
  1382  	}
  1383  	return nil
  1384  }
  1385  
  1386  func writeDynamicAttributes(state *parseGoState, n *html.Node) {
  1387  	dynExprMap, dynExprMapKeys := dynamicVGAttrExpr(n)
  1388  	for _, k := range dynExprMapKeys {
  1389  		valExpr := dynExprMap[k]
  1390  		if k == "" || k == "vg-attr" {
  1391  			fmt.Fprintf(&state.buildBuf, "vgn.AddAttrList(%s)\n", valExpr)
  1392  		} else {
  1393  			fmt.Fprintf(&state.buildBuf, "vgn.AddAttrInterface(%q,%s)\n", k, valExpr)
  1394  		}
  1395  	}
  1396  }
  1397  
  1398  // writeJSCallbackAttributes handles vg-js-create and vg-js-populate
  1399  func writeJSCallbackAttributes(state *parseGoState, n *html.Node) {
  1400  	m := jsCallbackVGAttrExpr(n)
  1401  	createStmt := m["vg-js-create"]
  1402  	if createStmt != "" {
  1403  		fmt.Fprintf(&state.buildBuf, "vgn.JSCreateHandler = vugu.JSValueFunc(func(value js.Value) { %s })\n", createStmt)
  1404  	}
  1405  	populateStmt := m["vg-js-populate"]
  1406  	if populateStmt != "" {
  1407  		fmt.Fprintf(&state.buildBuf, "vgn.JSPopulateHandler = vugu.JSValueFunc(func(value js.Value) { %s })\n", populateStmt)
  1408  	}
  1409  }