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. 10package godoc // import "golang.org/x/tools/godoc" 11 12import ( 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). 39const 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. 46func (p *Presentation) FuncMap() template.FuncMap { 47 p.initFuncMapOnce.Do(p.initFuncMap) 48 return p.funcMap 49} 50 51func (p *Presentation) TemplateFuncs() template.FuncMap { 52 p.initFuncMapOnce.Do(p.initFuncMap) 53 return p.templateFuncs 54} 55 56func (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 129func multiply(a, b int) int { return a * b } 130 131func filenameFunc(path string) string { 132 _, localname := pathpkg.Split(path) 133 return localname 134} 135 136func fileInfoNameFunc(fi os.FileInfo) string { 137 name := fi.Name() 138 if fi.IsDir() { 139 name += "/" 140 } 141 return name 142} 143 144func 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. 152var 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 163func infoKind_htmlFunc(info SpotInfo) string { 164 return infoKinds[info.Kind()] // infoKind entries are html-escaped 165} 166 167func (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 184func (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 193func (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 199func (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. 218func 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. 245func 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. 275func 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. 288var commentPrefix = []byte(`<span class="comment">// `) 289 290// linkedField determines whether the given line starts with an 291// identifer 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. 295func 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 if bytes.HasPrefix(line, commentPrefix) { 316 line = line[len(commentPrefix):] 317 } 318 id := scanIdentifier(line) 319 if len(id) == 0 { 320 // No leading identifier. Avoid map lookup for 321 // somewhat common case. 322 return "" 323 } 324 return ids[string(id)] 325} 326 327// scanIdentifier scans a valid Go identifier off the front of v and 328// either returns a subslice of v if there's a valid identifier, or 329// returns a zero-length slice. 330func scanIdentifier(v []byte) []byte { 331 var n int // number of leading bytes of v belonging to an identifier 332 for { 333 r, width := utf8.DecodeRune(v[n:]) 334 if !(isLetter(r) || n > 0 && isDigit(r)) { 335 break 336 } 337 n += width 338 } 339 return v[:n] 340} 341 342func isLetter(ch rune) bool { 343 return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch) 344} 345 346func isDigit(ch rune) bool { 347 return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) 348} 349 350func comment_htmlFunc(comment string) string { 351 var buf bytes.Buffer 352 // TODO(gri) Provide list of words (e.g. function parameters) 353 // to be emphasized by ToHTML. 354 doc.ToHTML(&buf, comment, nil) // does html-escaping 355 return buf.String() 356} 357 358// sanitizeFunc sanitizes the argument src by replacing newlines with 359// blanks, removing extra blanks, and by removing trailing whitespace 360// and commas before closing parentheses. 361func sanitizeFunc(src string) string { 362 buf := make([]byte, len(src)) 363 j := 0 // buf index 364 comma := -1 // comma index if >= 0 365 for i := 0; i < len(src); i++ { 366 ch := src[i] 367 switch ch { 368 case '\t', '\n', ' ': 369 // ignore whitespace at the beginning, after a blank, or after opening parentheses 370 if j == 0 { 371 continue 372 } 373 if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' { 374 continue 375 } 376 // replace all whitespace with blanks 377 ch = ' ' 378 case ',': 379 comma = j 380 case ')', '}', ']': 381 // remove any trailing comma 382 if comma >= 0 { 383 j = comma 384 } 385 // remove any trailing whitespace 386 if j > 0 && buf[j-1] == ' ' { 387 j-- 388 } 389 default: 390 comma = -1 391 } 392 buf[j] = ch 393 j++ 394 } 395 // remove trailing blank, if any 396 if j > 0 && buf[j-1] == ' ' { 397 j-- 398 } 399 return string(buf[:j]) 400} 401 402type PageInfo struct { 403 Dirname string // directory containing the package 404 Err error // error or nil 405 GoogleCN bool // page is being served from golang.google.cn 406 407 Mode PageInfoMode // display metadata from query string 408 409 // package info 410 FSet *token.FileSet // nil if no package documentation 411 PDoc *doc.Package // nil if no package documentation 412 Examples []*doc.Example // nil if no example code 413 Notes map[string][]*doc.Note // nil if no package Notes 414 PAst map[string]*ast.File // nil if no AST with package exports 415 IsMain bool // true for package main 416 IsFiltered bool // true if results were filtered 417 418 // analysis info 419 TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type) 420 AnalysisData htmltemplate.JS // array of TypeInfoJSON values 421 CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer) 422 CallGraphIndex map[string]int // maps func name to index in CallGraph 423 424 // directory info 425 Dirs *DirList // nil if no directory information 426 DirTime time.Time // directory time stamp 427 DirFlat bool // if set, show directory in a flat (non-indented) manner 428} 429 430func (info *PageInfo) IsEmpty() bool { 431 return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil 432} 433 434func pkgLinkFunc(path string) string { 435 // because of the irregular mapping under goroot 436 // we need to correct certain relative paths 437 path = strings.TrimPrefix(path, "/") 438 path = strings.TrimPrefix(path, "src/") 439 path = strings.TrimPrefix(path, "pkg/") 440 return "pkg/" + path 441} 442 443// srcToPkgLinkFunc builds an <a> tag linking to the package 444// documentation of relpath. 445func srcToPkgLinkFunc(relpath string) string { 446 relpath = pkgLinkFunc(relpath) 447 relpath = pathpkg.Dir(relpath) 448 if relpath == "pkg" { 449 return `<a href="/pkg">Index</a>` 450 } 451 return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):]) 452} 453 454// srcBreadcrumbFun converts each segment of relpath to a HTML <a>. 455// Each segment links to its corresponding src directories. 456func srcBreadcrumbFunc(relpath string) string { 457 segments := strings.Split(relpath, "/") 458 var buf bytes.Buffer 459 var selectedSegment string 460 var selectedIndex int 461 462 if strings.HasSuffix(relpath, "/") { 463 // relpath is a directory ending with a "/". 464 // Selected segment is the segment before the last slash. 465 selectedIndex = len(segments) - 2 466 selectedSegment = segments[selectedIndex] + "/" 467 } else { 468 selectedIndex = len(segments) - 1 469 selectedSegment = segments[selectedIndex] 470 } 471 472 for i := range segments[:selectedIndex] { 473 buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`, 474 strings.Join(segments[:i+1], "/"), 475 segments[i], 476 )) 477 } 478 479 buf.WriteString(`<span class="text-muted">`) 480 buf.WriteString(selectedSegment) 481 buf.WriteString(`</span>`) 482 return buf.String() 483} 484 485func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string { 486 // n must be an ast.Node or a *doc.Note 487 return func(info *PageInfo, n interface{}) string { 488 var pos, end token.Pos 489 490 switch n := n.(type) { 491 case ast.Node: 492 pos = n.Pos() 493 end = n.End() 494 case *doc.Note: 495 pos = n.Pos 496 end = n.End 497 default: 498 panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) 499 } 500 501 var relpath string 502 var line int 503 var low, high int // selection offset range 504 505 if pos.IsValid() { 506 p := info.FSet.Position(pos) 507 relpath = p.Filename 508 line = p.Line 509 low = p.Offset 510 } 511 if end.IsValid() { 512 high = info.FSet.Position(end).Offset 513 } 514 515 return srcPosLinkFunc(relpath, line, low, high) 516 } 517} 518 519func srcPosLinkFunc(s string, line, low, high int) string { 520 s = srcLinkFunc(s) 521 var buf bytes.Buffer 522 template.HTMLEscape(&buf, []byte(s)) 523 // selection ranges are of form "s=low:high" 524 if low < high { 525 fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping 526 // if we have a selection, position the page 527 // such that the selection is a bit below the top 528 line -= 10 529 if line < 1 { 530 line = 1 531 } 532 } 533 // line id's in html-printed source are of the 534 // form "L%d" where %d stands for the line number 535 if line > 0 { 536 fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping 537 } 538 return buf.String() 539} 540 541func srcLinkFunc(s string) string { 542 s = pathpkg.Clean("/" + s) 543 if !strings.HasPrefix(s, "/src/") { 544 s = "/src" + s 545 } 546 return s 547} 548 549// queryLinkFunc returns a URL for a line in a source file with a highlighted 550// query term. 551// s is expected to be a path to a source file. 552// query is expected to be a string that has already been appropriately escaped 553// for use in a URL query. 554func queryLinkFunc(s, query string, line int) string { 555 url := pathpkg.Clean("/"+s) + "?h=" + query 556 if line > 0 { 557 url += "#L" + strconv.Itoa(line) 558 } 559 return url 560} 561 562func docLinkFunc(s string, ident string) string { 563 return pathpkg.Clean("/pkg/"+s) + "/#" + ident 564} 565 566func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string { 567 var buf bytes.Buffer 568 for _, eg := range info.Examples { 569 name := stripExampleSuffix(eg.Name) 570 571 if name != funcName { 572 continue 573 } 574 575 // print code 576 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} 577 code := p.node_htmlFunc(info, cnode, true) 578 out := eg.Output 579 wholeFile := true 580 581 // Additional formatting if this is a function body. 582 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { 583 wholeFile = false 584 // remove surrounding braces 585 code = code[1 : n-1] 586 // unindent 587 code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "") 588 // remove output comment 589 if loc := exampleOutputRx.FindStringIndex(code); loc != nil { 590 code = strings.TrimSpace(code[:loc[0]]) 591 } 592 } 593 594 // Write out the playground code in standard Go style 595 // (use tabs, no comment highlight, etc). 596 play := "" 597 if eg.Play != nil && p.ShowPlayground { 598 var buf bytes.Buffer 599 eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments) 600 if err := format.Node(&buf, info.FSet, eg.Play); err != nil { 601 log.Print(err) 602 } else { 603 play = buf.String() 604 } 605 } 606 607 // Drop output, as the output comment will appear in the code. 608 if wholeFile && play == "" { 609 out = "" 610 } 611 612 if p.ExampleHTML == nil { 613 out = "" 614 return "" 615 } 616 617 err := p.ExampleHTML.Execute(&buf, struct { 618 Name, Doc, Code, Play, Output string 619 GoogleCN bool 620 }{eg.Name, eg.Doc, code, play, out, info.GoogleCN}) 621 if err != nil { 622 log.Print(err) 623 } 624 } 625 return buf.String() 626} 627 628func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup { 629 if len(cg) == 0 { 630 return cg 631 } 632 633 for i := range cg { 634 if !strings.HasPrefix(cg[i].Text(), "+build ") { 635 // Found the first non-build tag, return from here until the end 636 // of the slice. 637 return cg[i:] 638 } 639 } 640 641 // There weren't any non-build tags, return an empty slice. 642 return []*ast.CommentGroup{} 643} 644 645// example_nameFunc takes an example function name and returns its display 646// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". 647func (p *Presentation) example_nameFunc(s string) string { 648 name, suffix := splitExampleName(s) 649 // replace _ with . for method names 650 name = strings.Replace(name, "_", ".", 1) 651 // use "Package" if no name provided 652 if name == "" { 653 name = "Package" 654 } 655 return name + suffix 656} 657 658// example_suffixFunc takes an example function name and returns its suffix in 659// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". 660func (p *Presentation) example_suffixFunc(name string) string { 661 _, suffix := splitExampleName(name) 662 return suffix 663} 664 665// implements_html returns the "> Implements" toggle for a package-level named type. 666// Its contents are populated from JSON data by client-side JS at load time. 667func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string { 668 if p.ImplementsHTML == nil { 669 return "" 670 } 671 index, ok := info.TypeInfoIndex[typeName] 672 if !ok { 673 return "" 674 } 675 var buf bytes.Buffer 676 err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index}) 677 if err != nil { 678 log.Print(err) 679 } 680 return buf.String() 681} 682 683// methodset_html returns the "> Method set" toggle for a package-level named type. 684// Its contents are populated from JSON data by client-side JS at load time. 685func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string { 686 if p.MethodSetHTML == nil { 687 return "" 688 } 689 index, ok := info.TypeInfoIndex[typeName] 690 if !ok { 691 return "" 692 } 693 var buf bytes.Buffer 694 err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index}) 695 if err != nil { 696 log.Print(err) 697 } 698 return buf.String() 699} 700 701// callgraph_html returns the "> Call graph" toggle for a package-level func. 702// Its contents are populated from JSON data by client-side JS at load time. 703func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string { 704 if p.CallGraphHTML == nil { 705 return "" 706 } 707 if recv != "" { 708 // Format must match (*ssa.Function).RelString(). 709 name = fmt.Sprintf("(%s).%s", recv, name) 710 } 711 index, ok := info.CallGraphIndex[name] 712 if !ok { 713 return "" 714 } 715 var buf bytes.Buffer 716 err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index}) 717 if err != nil { 718 log.Print(err) 719 } 720 return buf.String() 721} 722 723func noteTitle(note string) string { 724 return strings.Title(strings.ToLower(note)) 725} 726 727func startsWithUppercase(s string) bool { 728 r, _ := utf8.DecodeRuneInString(s) 729 return unicode.IsUpper(r) 730} 731 732var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`) 733 734// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name 735// while keeping uppercase Braz in Foo_Braz. 736func stripExampleSuffix(name string) string { 737 if i := strings.LastIndex(name, "_"); i != -1 { 738 if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { 739 name = name[:i] 740 } 741 } 742 return name 743} 744 745func splitExampleName(s string) (name, suffix string) { 746 i := strings.LastIndex(s, "_") 747 if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { 748 name = s[:i] 749 suffix = " (" + strings.Title(s[i+1:]) + ")" 750 return 751 } 752 name = s 753 return 754} 755 756// replaceLeadingIndentation replaces oldIndent at the beginning of each line 757// with newIndent. This is used for formatting examples. Raw strings that 758// span multiple lines are handled specially: oldIndent is not removed (since 759// go/printer will not add any indentation there), but newIndent is added 760// (since we may still want leading indentation). 761func replaceLeadingIndentation(body, oldIndent, newIndent string) string { 762 // Handle indent at the beginning of the first line. After this, we handle 763 // indentation only after a newline. 764 var buf bytes.Buffer 765 if strings.HasPrefix(body, oldIndent) { 766 buf.WriteString(newIndent) 767 body = body[len(oldIndent):] 768 } 769 770 // Use a state machine to keep track of whether we're in a string or 771 // rune literal while we process the rest of the code. 772 const ( 773 codeState = iota 774 runeState 775 interpretedStringState 776 rawStringState 777 ) 778 searchChars := []string{ 779 "'\"`\n", // codeState 780 `\'`, // runeState 781 `\"`, // interpretedStringState 782 "`\n", // rawStringState 783 // newlineState does not need to search 784 } 785 state := codeState 786 for { 787 i := strings.IndexAny(body, searchChars[state]) 788 if i < 0 { 789 buf.WriteString(body) 790 break 791 } 792 c := body[i] 793 buf.WriteString(body[:i+1]) 794 body = body[i+1:] 795 switch state { 796 case codeState: 797 switch c { 798 case '\'': 799 state = runeState 800 case '"': 801 state = interpretedStringState 802 case '`': 803 state = rawStringState 804 case '\n': 805 if strings.HasPrefix(body, oldIndent) { 806 buf.WriteString(newIndent) 807 body = body[len(oldIndent):] 808 } 809 } 810 811 case runeState: 812 switch c { 813 case '\\': 814 r, size := utf8.DecodeRuneInString(body) 815 buf.WriteRune(r) 816 body = body[size:] 817 case '\'': 818 state = codeState 819 } 820 821 case interpretedStringState: 822 switch c { 823 case '\\': 824 r, size := utf8.DecodeRuneInString(body) 825 buf.WriteRune(r) 826 body = body[size:] 827 case '"': 828 state = codeState 829 } 830 831 case rawStringState: 832 switch c { 833 case '`': 834 state = codeState 835 case '\n': 836 buf.WriteString(newIndent) 837 } 838 } 839 } 840 return buf.String() 841} 842 843// writeNode writes the AST node x to w. 844// 845// The provided fset must be non-nil. The pageInfo is optional. If 846// present, the pageInfo is used to add comments to struct fields to 847// say which version of Go introduced them. 848func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) { 849 // convert trailing tabs into spaces using a tconv filter 850 // to ensure a good outcome in most browsers (there may still 851 // be tabs in comments and strings, but converting those into 852 // the right number of spaces is much harder) 853 // 854 // TODO(gri) rethink printer flags - perhaps tconv can be eliminated 855 // with an another printer mode (which is more efficiently 856 // implemented in the printer than here with another layer) 857 858 var pkgName, structName string 859 var apiInfo pkgAPIVersions 860 if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil && 861 p.Corpus != nil && 862 gd.Tok == token.TYPE && len(gd.Specs) != 0 { 863 pkgName = pageInfo.PDoc.ImportPath 864 if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok { 865 if _, ok := ts.Type.(*ast.StructType); ok { 866 structName = ts.Name.Name 867 } 868 } 869 apiInfo = p.Corpus.pkgAPIInfo[pkgName] 870 } 871 872 var out = w 873 var buf bytes.Buffer 874 if structName != "" { 875 out = &buf 876 } 877 878 mode := printer.TabIndent | printer.UseSpaces 879 err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x) 880 if err != nil { 881 log.Print(err) 882 } 883 884 // Add comments to struct fields saying which Go version introduced them. 885 if structName != "" { 886 fieldSince := apiInfo.fieldSince[structName] 887 typeSince := apiInfo.typeSince[structName] 888 // Add/rewrite comments on struct fields to note which Go version added them. 889 var buf2 bytes.Buffer 890 buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10) 891 bs := bufio.NewScanner(&buf) 892 for bs.Scan() { 893 line := bs.Bytes() 894 field := firstIdent(line) 895 var since string 896 if field != "" { 897 since = fieldSince[field] 898 if since != "" && since == typeSince { 899 // Don't highlight field versions if they were the 900 // same as the struct itself. 901 since = "" 902 } 903 } 904 if since == "" { 905 buf2.Write(line) 906 } else { 907 if bytes.Contains(line, slashSlash) { 908 line = bytes.TrimRight(line, " \t.") 909 buf2.Write(line) 910 buf2.WriteString("; added in Go ") 911 } else { 912 buf2.Write(line) 913 buf2.WriteString(" // Go ") 914 } 915 buf2.WriteString(since) 916 } 917 buf2.WriteByte('\n') 918 } 919 w.Write(buf2.Bytes()) 920 } 921} 922 923var slashSlash = []byte("//") 924 925// WriteNode writes x to w. 926// TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode. 927func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) { 928 p.writeNode(w, nil, fset, x) 929} 930 931// firstIdent returns the first identifier in x. 932// This actually parses "identifiers" that begin with numbers too, but we 933// never feed it such input, so it's fine. 934func firstIdent(x []byte) string { 935 x = bytes.TrimSpace(x) 936 i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) }) 937 if i == -1 { 938 return string(x) 939 } 940 return string(x[:i]) 941} 942