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 }