github.com/jd-ly/tools@v0.5.7/godoc/godoc.go (about) 1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package godoc is a work-in-progress (2013-07-17) package to 6 // begin splitting up the godoc binary into multiple pieces. 7 // 8 // This package comment will evolve over time as this package splits 9 // into smaller pieces. 10 package godoc // import "github.com/jd-ly/tools/godoc" 11 12 import ( 13 "bufio" 14 "bytes" 15 "fmt" 16 "go/ast" 17 "go/doc" 18 "go/format" 19 "go/printer" 20 "go/token" 21 htmltemplate "html/template" 22 "io" 23 "log" 24 "os" 25 pathpkg "path" 26 "regexp" 27 "strconv" 28 "strings" 29 "text/template" 30 "time" 31 "unicode" 32 "unicode/utf8" 33 ) 34 35 // Fake relative package path for built-ins. Documentation for all globals 36 // (not just exported ones) will be shown for packages in this directory, 37 // and there will be no association of consts, vars, and factory functions 38 // with types (see issue 6645). 39 const builtinPkgPath = "builtin" 40 41 // FuncMap defines template functions used in godoc templates. 42 // 43 // Convention: template function names ending in "_html" or "_url" produce 44 // HTML- or URL-escaped strings; all other function results may 45 // require explicit escaping in the template. 46 func (p *Presentation) FuncMap() template.FuncMap { 47 p.initFuncMapOnce.Do(p.initFuncMap) 48 return p.funcMap 49 } 50 51 func (p *Presentation) TemplateFuncs() template.FuncMap { 52 p.initFuncMapOnce.Do(p.initFuncMap) 53 return p.templateFuncs 54 } 55 56 func (p *Presentation) initFuncMap() { 57 if p.Corpus == nil { 58 panic("nil Presentation.Corpus") 59 } 60 p.templateFuncs = template.FuncMap{ 61 "code": p.code, 62 } 63 p.funcMap = template.FuncMap{ 64 // various helpers 65 "filename": filenameFunc, 66 "repeat": strings.Repeat, 67 "since": p.Corpus.pkgAPIInfo.sinceVersionFunc, 68 69 // access to FileInfos (directory listings) 70 "fileInfoName": fileInfoNameFunc, 71 "fileInfoTime": fileInfoTimeFunc, 72 73 // access to search result information 74 "infoKind_html": infoKind_htmlFunc, 75 "infoLine": p.infoLineFunc, 76 "infoSnippet_html": p.infoSnippet_htmlFunc, 77 78 // formatting of AST nodes 79 "node": p.nodeFunc, 80 "node_html": p.node_htmlFunc, 81 "comment_html": comment_htmlFunc, 82 "sanitize": sanitizeFunc, 83 84 // support for URL attributes 85 "pkgLink": pkgLinkFunc, 86 "srcLink": srcLinkFunc, 87 "posLink_url": newPosLink_urlFunc(srcPosLinkFunc), 88 "docLink": docLinkFunc, 89 "queryLink": queryLinkFunc, 90 "srcBreadcrumb": srcBreadcrumbFunc, 91 "srcToPkgLink": srcToPkgLinkFunc, 92 93 // formatting of Examples 94 "example_html": p.example_htmlFunc, 95 "example_name": p.example_nameFunc, 96 "example_suffix": p.example_suffixFunc, 97 98 // formatting of analysis information 99 "callgraph_html": p.callgraph_htmlFunc, 100 "implements_html": p.implements_htmlFunc, 101 "methodset_html": p.methodset_htmlFunc, 102 103 // formatting of Notes 104 "noteTitle": noteTitle, 105 106 // Number operation 107 "multiply": multiply, 108 109 // formatting of PageInfoMode query string 110 "modeQueryString": modeQueryString, 111 112 // check whether to display third party section or not 113 "hasThirdParty": hasThirdParty, 114 115 // get the no. of columns to split the toc in search page 116 "tocColCount": tocColCount, 117 } 118 if p.URLForSrc != nil { 119 p.funcMap["srcLink"] = p.URLForSrc 120 } 121 if p.URLForSrcPos != nil { 122 p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos) 123 } 124 if p.URLForSrcQuery != nil { 125 p.funcMap["queryLink"] = p.URLForSrcQuery 126 } 127 } 128 129 func multiply(a, b int) int { return a * b } 130 131 func filenameFunc(path string) string { 132 _, localname := pathpkg.Split(path) 133 return localname 134 } 135 136 func fileInfoNameFunc(fi os.FileInfo) string { 137 name := fi.Name() 138 if fi.IsDir() { 139 name += "/" 140 } 141 return name 142 } 143 144 func fileInfoTimeFunc(fi os.FileInfo) string { 145 if t := fi.ModTime(); t.Unix() != 0 { 146 return t.Local().String() 147 } 148 return "" // don't return epoch if time is obviously not set 149 } 150 151 // The strings in infoKinds must be properly html-escaped. 152 var infoKinds = [nKinds]string{ 153 PackageClause: "package clause", 154 ImportDecl: "import decl", 155 ConstDecl: "const decl", 156 TypeDecl: "type decl", 157 VarDecl: "var decl", 158 FuncDecl: "func decl", 159 MethodDecl: "method decl", 160 Use: "use", 161 } 162 163 func infoKind_htmlFunc(info SpotInfo) string { 164 return infoKinds[info.Kind()] // infoKind entries are html-escaped 165 } 166 167 func (p *Presentation) infoLineFunc(info SpotInfo) int { 168 line := info.Lori() 169 if info.IsIndex() { 170 index, _ := p.Corpus.searchIndex.Get() 171 if index != nil { 172 line = index.(*Index).Snippet(line).Line 173 } else { 174 // no line information available because 175 // we don't have an index - this should 176 // never happen; be conservative and don't 177 // crash 178 line = 0 179 } 180 } 181 return line 182 } 183 184 func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string { 185 if info.IsIndex() { 186 index, _ := p.Corpus.searchIndex.Get() 187 // Snippet.Text was HTML-escaped when it was generated 188 return index.(*Index).Snippet(info.Lori()).Text 189 } 190 return `<span class="alert">no snippet text available</span>` 191 } 192 193 func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string { 194 var buf bytes.Buffer 195 p.writeNode(&buf, info, info.FSet, node) 196 return buf.String() 197 } 198 199 func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { 200 var buf1 bytes.Buffer 201 p.writeNode(&buf1, info, info.FSet, node) 202 203 var buf2 bytes.Buffer 204 if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks { 205 LinkifyText(&buf2, buf1.Bytes(), n) 206 if st, name := isStructTypeDecl(n); st != nil { 207 addStructFieldIDAttributes(&buf2, name, st) 208 } 209 } else { 210 FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) 211 } 212 213 return buf2.String() 214 } 215 216 // isStructTypeDecl checks whether n is a struct declaration. 217 // It either returns a non-nil StructType and its name, or zero values. 218 func isStructTypeDecl(n ast.Node) (st *ast.StructType, name string) { 219 gd, ok := n.(*ast.GenDecl) 220 if !ok || gd.Tok != token.TYPE { 221 return nil, "" 222 } 223 if gd.Lparen > 0 { 224 // Parenthesized type. Who does that, anyway? 225 // TODO: Reportedly gri does. Fix this to handle that too. 226 return nil, "" 227 } 228 if len(gd.Specs) != 1 { 229 return nil, "" 230 } 231 ts, ok := gd.Specs[0].(*ast.TypeSpec) 232 if !ok { 233 return nil, "" 234 } 235 st, ok = ts.Type.(*ast.StructType) 236 if !ok { 237 return nil, "" 238 } 239 return st, ts.Name.Name 240 } 241 242 // addStructFieldIDAttributes modifies the contents of buf such that 243 // all struct fields of the named struct have <span id='name.Field'> 244 // in them, so people can link to /#Struct.Field. 245 func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) { 246 if st.Fields == nil { 247 return 248 } 249 // needsLink is a set of identifiers that still need to be 250 // linked, where value == key, to avoid an allocation in func 251 // linkedField. 252 needsLink := make(map[string]string) 253 254 for _, f := range st.Fields.List { 255 if len(f.Names) == 0 { 256 continue 257 } 258 fieldName := f.Names[0].Name 259 needsLink[fieldName] = fieldName 260 } 261 var newBuf bytes.Buffer 262 foreachLine(buf.Bytes(), func(line []byte) { 263 if fieldName := linkedField(line, needsLink); fieldName != "" { 264 fmt.Fprintf(&newBuf, `<span id="%s.%s"></span>`, name, fieldName) 265 delete(needsLink, fieldName) 266 } 267 newBuf.Write(line) 268 }) 269 buf.Reset() 270 buf.Write(newBuf.Bytes()) 271 } 272 273 // foreachLine calls fn for each line of in, where a line includes 274 // the trailing "\n", except on the last line, if it doesn't exist. 275 func foreachLine(in []byte, fn func(line []byte)) { 276 for len(in) > 0 { 277 nl := bytes.IndexByte(in, '\n') 278 if nl == -1 { 279 fn(in) 280 return 281 } 282 fn(in[:nl+1]) 283 in = in[nl+1:] 284 } 285 } 286 287 // commentPrefix is the line prefix for comments after they've been HTMLified. 288 var commentPrefix = []byte(`<span class="comment">// `) 289 290 // linkedField determines whether the given line starts with an 291 // identifier in the provided ids map (mapping from identifier to the 292 // same identifier). The line can start with either an identifier or 293 // an identifier in a comment. If one matches, it returns the 294 // identifier that matched. Otherwise it returns the empty string. 295 func linkedField(line []byte, ids map[string]string) string { 296 line = bytes.TrimSpace(line) 297 298 // For fields with a doc string of the 299 // conventional form, we put the new span into 300 // the comment instead of the field. 301 // The "conventional" form is a complete sentence 302 // per https://golang.org/s/style#comment-sentences like: 303 // 304 // // Foo is an optional Fooer to foo the foos. 305 // Foo Fooer 306 // 307 // In this case, we want the #StructName.Foo 308 // link to make the browser go to the comment 309 // line "Foo is an optional Fooer" instead of 310 // the "Foo Fooer" line, which could otherwise 311 // obscure the docs above the browser's "fold". 312 // 313 // TODO: do this better, so it works for all 314 // comments, including unconventional ones. 315 line = bytes.TrimPrefix(line, commentPrefix) 316 id := scanIdentifier(line) 317 if len(id) == 0 { 318 // No leading identifier. Avoid map lookup for 319 // somewhat common case. 320 return "" 321 } 322 return ids[string(id)] 323 } 324 325 // scanIdentifier scans a valid Go identifier off the front of v and 326 // either returns a subslice of v if there's a valid identifier, or 327 // returns a zero-length slice. 328 func scanIdentifier(v []byte) []byte { 329 var n int // number of leading bytes of v belonging to an identifier 330 for { 331 r, width := utf8.DecodeRune(v[n:]) 332 if !(isLetter(r) || n > 0 && isDigit(r)) { 333 break 334 } 335 n += width 336 } 337 return v[:n] 338 } 339 340 func isLetter(ch rune) bool { 341 return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch) 342 } 343 344 func isDigit(ch rune) bool { 345 return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) 346 } 347 348 func comment_htmlFunc(comment string) string { 349 var buf bytes.Buffer 350 // TODO(gri) Provide list of words (e.g. function parameters) 351 // to be emphasized by ToHTML. 352 doc.ToHTML(&buf, comment, nil) // does html-escaping 353 return buf.String() 354 } 355 356 // sanitizeFunc sanitizes the argument src by replacing newlines with 357 // blanks, removing extra blanks, and by removing trailing whitespace 358 // and commas before closing parentheses. 359 func sanitizeFunc(src string) string { 360 buf := make([]byte, len(src)) 361 j := 0 // buf index 362 comma := -1 // comma index if >= 0 363 for i := 0; i < len(src); i++ { 364 ch := src[i] 365 switch ch { 366 case '\t', '\n', ' ': 367 // ignore whitespace at the beginning, after a blank, or after opening parentheses 368 if j == 0 { 369 continue 370 } 371 if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' { 372 continue 373 } 374 // replace all whitespace with blanks 375 ch = ' ' 376 case ',': 377 comma = j 378 case ')', '}', ']': 379 // remove any trailing comma 380 if comma >= 0 { 381 j = comma 382 } 383 // remove any trailing whitespace 384 if j > 0 && buf[j-1] == ' ' { 385 j-- 386 } 387 default: 388 comma = -1 389 } 390 buf[j] = ch 391 j++ 392 } 393 // remove trailing blank, if any 394 if j > 0 && buf[j-1] == ' ' { 395 j-- 396 } 397 return string(buf[:j]) 398 } 399 400 type PageInfo struct { 401 Dirname string // directory containing the package 402 Err error // error or nil 403 GoogleCN bool // page is being served from golang.google.cn 404 405 Mode PageInfoMode // display metadata from query string 406 407 // package info 408 FSet *token.FileSet // nil if no package documentation 409 PDoc *doc.Package // nil if no package documentation 410 Examples []*doc.Example // nil if no example code 411 Notes map[string][]*doc.Note // nil if no package Notes 412 PAst map[string]*ast.File // nil if no AST with package exports 413 IsMain bool // true for package main 414 IsFiltered bool // true if results were filtered 415 416 // analysis info 417 TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type) 418 AnalysisData htmltemplate.JS // array of TypeInfoJSON values 419 CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer) 420 CallGraphIndex map[string]int // maps func name to index in CallGraph 421 422 // directory info 423 Dirs *DirList // nil if no directory information 424 DirTime time.Time // directory time stamp 425 DirFlat bool // if set, show directory in a flat (non-indented) manner 426 } 427 428 func (info *PageInfo) IsEmpty() bool { 429 return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil 430 } 431 432 func pkgLinkFunc(path string) string { 433 // because of the irregular mapping under goroot 434 // we need to correct certain relative paths 435 path = strings.TrimPrefix(path, "/") 436 path = strings.TrimPrefix(path, "src/") 437 path = strings.TrimPrefix(path, "pkg/") 438 return "pkg/" + path 439 } 440 441 // srcToPkgLinkFunc builds an <a> tag linking to the package 442 // documentation of relpath. 443 func srcToPkgLinkFunc(relpath string) string { 444 relpath = pkgLinkFunc(relpath) 445 relpath = pathpkg.Dir(relpath) 446 if relpath == "pkg" { 447 return `<a href="/pkg">Index</a>` 448 } 449 return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):]) 450 } 451 452 // srcBreadcrumbFun converts each segment of relpath to a HTML <a>. 453 // Each segment links to its corresponding src directories. 454 func srcBreadcrumbFunc(relpath string) string { 455 segments := strings.Split(relpath, "/") 456 var buf bytes.Buffer 457 var selectedSegment string 458 var selectedIndex int 459 460 if strings.HasSuffix(relpath, "/") { 461 // relpath is a directory ending with a "/". 462 // Selected segment is the segment before the last slash. 463 selectedIndex = len(segments) - 2 464 selectedSegment = segments[selectedIndex] + "/" 465 } else { 466 selectedIndex = len(segments) - 1 467 selectedSegment = segments[selectedIndex] 468 } 469 470 for i := range segments[:selectedIndex] { 471 buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`, 472 strings.Join(segments[:i+1], "/"), 473 segments[i], 474 )) 475 } 476 477 buf.WriteString(`<span class="text-muted">`) 478 buf.WriteString(selectedSegment) 479 buf.WriteString(`</span>`) 480 return buf.String() 481 } 482 483 func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string { 484 // n must be an ast.Node or a *doc.Note 485 return func(info *PageInfo, n interface{}) string { 486 var pos, end token.Pos 487 488 switch n := n.(type) { 489 case ast.Node: 490 pos = n.Pos() 491 end = n.End() 492 case *doc.Note: 493 pos = n.Pos 494 end = n.End 495 default: 496 panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) 497 } 498 499 var relpath string 500 var line int 501 var low, high int // selection offset range 502 503 if pos.IsValid() { 504 p := info.FSet.Position(pos) 505 relpath = p.Filename 506 line = p.Line 507 low = p.Offset 508 } 509 if end.IsValid() { 510 high = info.FSet.Position(end).Offset 511 } 512 513 return srcPosLinkFunc(relpath, line, low, high) 514 } 515 } 516 517 func srcPosLinkFunc(s string, line, low, high int) string { 518 s = srcLinkFunc(s) 519 var buf bytes.Buffer 520 template.HTMLEscape(&buf, []byte(s)) 521 // selection ranges are of form "s=low:high" 522 if low < high { 523 fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping 524 // if we have a selection, position the page 525 // such that the selection is a bit below the top 526 line -= 10 527 if line < 1 { 528 line = 1 529 } 530 } 531 // line id's in html-printed source are of the 532 // form "L%d" where %d stands for the line number 533 if line > 0 { 534 fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping 535 } 536 return buf.String() 537 } 538 539 func srcLinkFunc(s string) string { 540 s = pathpkg.Clean("/" + s) 541 if !strings.HasPrefix(s, "/src/") { 542 s = "/src" + s 543 } 544 return s 545 } 546 547 // queryLinkFunc returns a URL for a line in a source file with a highlighted 548 // query term. 549 // s is expected to be a path to a source file. 550 // query is expected to be a string that has already been appropriately escaped 551 // for use in a URL query. 552 func queryLinkFunc(s, query string, line int) string { 553 url := pathpkg.Clean("/"+s) + "?h=" + query 554 if line > 0 { 555 url += "#L" + strconv.Itoa(line) 556 } 557 return url 558 } 559 560 func docLinkFunc(s string, ident string) string { 561 return pathpkg.Clean("/pkg/"+s) + "/#" + ident 562 } 563 564 func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string { 565 var buf bytes.Buffer 566 for _, eg := range info.Examples { 567 name := stripExampleSuffix(eg.Name) 568 569 if name != funcName { 570 continue 571 } 572 573 // print code 574 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} 575 code := p.node_htmlFunc(info, cnode, true) 576 out := eg.Output 577 wholeFile := true 578 579 // Additional formatting if this is a function body. 580 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { 581 wholeFile = false 582 // remove surrounding braces 583 code = code[1 : n-1] 584 // unindent 585 code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "") 586 // remove output comment 587 if loc := exampleOutputRx.FindStringIndex(code); loc != nil { 588 code = strings.TrimSpace(code[:loc[0]]) 589 } 590 } 591 592 // Write out the playground code in standard Go style 593 // (use tabs, no comment highlight, etc). 594 play := "" 595 if eg.Play != nil && p.ShowPlayground { 596 var buf bytes.Buffer 597 eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments) 598 if err := format.Node(&buf, info.FSet, eg.Play); err != nil { 599 log.Print(err) 600 } else { 601 play = buf.String() 602 } 603 } 604 605 // Drop output, as the output comment will appear in the code. 606 if wholeFile && play == "" { 607 out = "" 608 } 609 610 if p.ExampleHTML == nil { 611 out = "" 612 return "" 613 } 614 615 err := p.ExampleHTML.Execute(&buf, struct { 616 Name, Doc, Code, Play, Output string 617 GoogleCN bool 618 }{eg.Name, eg.Doc, code, play, out, info.GoogleCN}) 619 if err != nil { 620 log.Print(err) 621 } 622 } 623 return buf.String() 624 } 625 626 func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup { 627 if len(cg) == 0 { 628 return cg 629 } 630 631 for i := range cg { 632 if !strings.HasPrefix(cg[i].Text(), "+build ") { 633 // Found the first non-build tag, return from here until the end 634 // of the slice. 635 return cg[i:] 636 } 637 } 638 639 // There weren't any non-build tags, return an empty slice. 640 return []*ast.CommentGroup{} 641 } 642 643 // example_nameFunc takes an example function name and returns its display 644 // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". 645 func (p *Presentation) example_nameFunc(s string) string { 646 name, suffix := splitExampleName(s) 647 // replace _ with . for method names 648 name = strings.Replace(name, "_", ".", 1) 649 // use "Package" if no name provided 650 if name == "" { 651 name = "Package" 652 } 653 return name + suffix 654 } 655 656 // example_suffixFunc takes an example function name and returns its suffix in 657 // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". 658 func (p *Presentation) example_suffixFunc(name string) string { 659 _, suffix := splitExampleName(name) 660 return suffix 661 } 662 663 // implements_html returns the "> Implements" toggle for a package-level named type. 664 // Its contents are populated from JSON data by client-side JS at load time. 665 func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string { 666 if p.ImplementsHTML == nil { 667 return "" 668 } 669 index, ok := info.TypeInfoIndex[typeName] 670 if !ok { 671 return "" 672 } 673 var buf bytes.Buffer 674 err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index}) 675 if err != nil { 676 log.Print(err) 677 } 678 return buf.String() 679 } 680 681 // methodset_html returns the "> Method set" toggle for a package-level named type. 682 // Its contents are populated from JSON data by client-side JS at load time. 683 func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string { 684 if p.MethodSetHTML == nil { 685 return "" 686 } 687 index, ok := info.TypeInfoIndex[typeName] 688 if !ok { 689 return "" 690 } 691 var buf bytes.Buffer 692 err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index}) 693 if err != nil { 694 log.Print(err) 695 } 696 return buf.String() 697 } 698 699 // callgraph_html returns the "> Call graph" toggle for a package-level func. 700 // Its contents are populated from JSON data by client-side JS at load time. 701 func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string { 702 if p.CallGraphHTML == nil { 703 return "" 704 } 705 if recv != "" { 706 // Format must match (*ssa.Function).RelString(). 707 name = fmt.Sprintf("(%s).%s", recv, name) 708 } 709 index, ok := info.CallGraphIndex[name] 710 if !ok { 711 return "" 712 } 713 var buf bytes.Buffer 714 err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index}) 715 if err != nil { 716 log.Print(err) 717 } 718 return buf.String() 719 } 720 721 func noteTitle(note string) string { 722 return strings.Title(strings.ToLower(note)) 723 } 724 725 func startsWithUppercase(s string) bool { 726 r, _ := utf8.DecodeRuneInString(s) 727 return unicode.IsUpper(r) 728 } 729 730 var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`) 731 732 // stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name 733 // while keeping uppercase Braz in Foo_Braz. 734 func stripExampleSuffix(name string) string { 735 if i := strings.LastIndex(name, "_"); i != -1 { 736 if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { 737 name = name[:i] 738 } 739 } 740 return name 741 } 742 743 func splitExampleName(s string) (name, suffix string) { 744 i := strings.LastIndex(s, "_") 745 if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { 746 name = s[:i] 747 suffix = " (" + strings.Title(s[i+1:]) + ")" 748 return 749 } 750 name = s 751 return 752 } 753 754 // replaceLeadingIndentation replaces oldIndent at the beginning of each line 755 // with newIndent. This is used for formatting examples. Raw strings that 756 // span multiple lines are handled specially: oldIndent is not removed (since 757 // go/printer will not add any indentation there), but newIndent is added 758 // (since we may still want leading indentation). 759 func replaceLeadingIndentation(body, oldIndent, newIndent string) string { 760 // Handle indent at the beginning of the first line. After this, we handle 761 // indentation only after a newline. 762 var buf bytes.Buffer 763 if strings.HasPrefix(body, oldIndent) { 764 buf.WriteString(newIndent) 765 body = body[len(oldIndent):] 766 } 767 768 // Use a state machine to keep track of whether we're in a string or 769 // rune literal while we process the rest of the code. 770 const ( 771 codeState = iota 772 runeState 773 interpretedStringState 774 rawStringState 775 ) 776 searchChars := []string{ 777 "'\"`\n", // codeState 778 `\'`, // runeState 779 `\"`, // interpretedStringState 780 "`\n", // rawStringState 781 // newlineState does not need to search 782 } 783 state := codeState 784 for { 785 i := strings.IndexAny(body, searchChars[state]) 786 if i < 0 { 787 buf.WriteString(body) 788 break 789 } 790 c := body[i] 791 buf.WriteString(body[:i+1]) 792 body = body[i+1:] 793 switch state { 794 case codeState: 795 switch c { 796 case '\'': 797 state = runeState 798 case '"': 799 state = interpretedStringState 800 case '`': 801 state = rawStringState 802 case '\n': 803 if strings.HasPrefix(body, oldIndent) { 804 buf.WriteString(newIndent) 805 body = body[len(oldIndent):] 806 } 807 } 808 809 case runeState: 810 switch c { 811 case '\\': 812 r, size := utf8.DecodeRuneInString(body) 813 buf.WriteRune(r) 814 body = body[size:] 815 case '\'': 816 state = codeState 817 } 818 819 case interpretedStringState: 820 switch c { 821 case '\\': 822 r, size := utf8.DecodeRuneInString(body) 823 buf.WriteRune(r) 824 body = body[size:] 825 case '"': 826 state = codeState 827 } 828 829 case rawStringState: 830 switch c { 831 case '`': 832 state = codeState 833 case '\n': 834 buf.WriteString(newIndent) 835 } 836 } 837 } 838 return buf.String() 839 } 840 841 // writeNode writes the AST node x to w. 842 // 843 // The provided fset must be non-nil. The pageInfo is optional. If 844 // present, the pageInfo is used to add comments to struct fields to 845 // say which version of Go introduced them. 846 func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) { 847 // convert trailing tabs into spaces using a tconv filter 848 // to ensure a good outcome in most browsers (there may still 849 // be tabs in comments and strings, but converting those into 850 // the right number of spaces is much harder) 851 // 852 // TODO(gri) rethink printer flags - perhaps tconv can be eliminated 853 // with an another printer mode (which is more efficiently 854 // implemented in the printer than here with another layer) 855 856 var pkgName, structName string 857 var apiInfo pkgAPIVersions 858 if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil && 859 p.Corpus != nil && 860 gd.Tok == token.TYPE && len(gd.Specs) != 0 { 861 pkgName = pageInfo.PDoc.ImportPath 862 if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok { 863 if _, ok := ts.Type.(*ast.StructType); ok { 864 structName = ts.Name.Name 865 } 866 } 867 apiInfo = p.Corpus.pkgAPIInfo[pkgName] 868 } 869 870 var out = w 871 var buf bytes.Buffer 872 if structName != "" { 873 out = &buf 874 } 875 876 mode := printer.TabIndent | printer.UseSpaces 877 err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x) 878 if err != nil { 879 log.Print(err) 880 } 881 882 // Add comments to struct fields saying which Go version introduced them. 883 if structName != "" { 884 fieldSince := apiInfo.fieldSince[structName] 885 typeSince := apiInfo.typeSince[structName] 886 // Add/rewrite comments on struct fields to note which Go version added them. 887 var buf2 bytes.Buffer 888 buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10) 889 bs := bufio.NewScanner(&buf) 890 for bs.Scan() { 891 line := bs.Bytes() 892 field := firstIdent(line) 893 var since string 894 if field != "" { 895 since = fieldSince[field] 896 if since != "" && since == typeSince { 897 // Don't highlight field versions if they were the 898 // same as the struct itself. 899 since = "" 900 } 901 } 902 if since == "" { 903 buf2.Write(line) 904 } else { 905 if bytes.Contains(line, slashSlash) { 906 line = bytes.TrimRight(line, " \t.") 907 buf2.Write(line) 908 buf2.WriteString("; added in Go ") 909 } else { 910 buf2.Write(line) 911 buf2.WriteString(" // Go ") 912 } 913 buf2.WriteString(since) 914 } 915 buf2.WriteByte('\n') 916 } 917 w.Write(buf2.Bytes()) 918 } 919 } 920 921 var slashSlash = []byte("//") 922 923 // WriteNode writes x to w. 924 // TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode. 925 func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) { 926 p.writeNode(w, nil, fset, x) 927 } 928 929 // firstIdent returns the first identifier in x. 930 // This actually parses "identifiers" that begin with numbers too, but we 931 // never feed it such input, so it's fine. 932 func firstIdent(x []byte) string { 933 x = bytes.TrimSpace(x) 934 i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) }) 935 if i == -1 { 936 return string(x) 937 } 938 return string(x[:i]) 939 }