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