github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/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 "golang.org/x/tools/godoc" 11 12 import ( 13 "bytes" 14 "fmt" 15 "go/ast" 16 "go/doc" 17 "go/format" 18 "go/printer" 19 "go/token" 20 htmltemplate "html/template" 21 "io" 22 "log" 23 "os" 24 pathpkg "path" 25 "regexp" 26 "strconv" 27 "strings" 28 "text/template" 29 "time" 30 "unicode" 31 "unicode/utf8" 32 ) 33 34 // Fake relative package path for built-ins. Documentation for all globals 35 // (not just exported ones) will be shown for packages in this directory. 36 const builtinPkgPath = "builtin" 37 38 // FuncMap defines template functions used in godoc templates. 39 // 40 // Convention: template function names ending in "_html" or "_url" produce 41 // HTML- or URL-escaped strings; all other function results may 42 // require explicit escaping in the template. 43 func (p *Presentation) FuncMap() template.FuncMap { 44 p.initFuncMapOnce.Do(p.initFuncMap) 45 return p.funcMap 46 } 47 48 func (p *Presentation) TemplateFuncs() template.FuncMap { 49 p.initFuncMapOnce.Do(p.initFuncMap) 50 return p.templateFuncs 51 } 52 53 func (p *Presentation) initFuncMap() { 54 if p.Corpus == nil { 55 panic("nil Presentation.Corpus") 56 } 57 p.templateFuncs = template.FuncMap{ 58 "code": p.code, 59 } 60 p.funcMap = template.FuncMap{ 61 // various helpers 62 "filename": filenameFunc, 63 "repeat": strings.Repeat, 64 65 // access to FileInfos (directory listings) 66 "fileInfoName": fileInfoNameFunc, 67 "fileInfoTime": fileInfoTimeFunc, 68 69 // access to search result information 70 "infoKind_html": infoKind_htmlFunc, 71 "infoLine": p.infoLineFunc, 72 "infoSnippet_html": p.infoSnippet_htmlFunc, 73 74 // formatting of AST nodes 75 "node": p.nodeFunc, 76 "node_html": p.node_htmlFunc, 77 "comment_html": comment_htmlFunc, 78 "comment_text": comment_textFunc, 79 "sanitize": sanitizeFunc, 80 81 // support for URL attributes 82 "pkgLink": pkgLinkFunc, 83 "srcLink": srcLinkFunc, 84 "posLink_url": newPosLink_urlFunc(srcPosLinkFunc), 85 "docLink": docLinkFunc, 86 "queryLink": queryLinkFunc, 87 88 // formatting of Examples 89 "example_html": p.example_htmlFunc, 90 "example_text": p.example_textFunc, 91 "example_name": p.example_nameFunc, 92 "example_suffix": p.example_suffixFunc, 93 94 // formatting of analysis information 95 "callgraph_html": p.callgraph_htmlFunc, 96 "implements_html": p.implements_htmlFunc, 97 "methodset_html": p.methodset_htmlFunc, 98 99 // formatting of Notes 100 "noteTitle": noteTitle, 101 102 // Number operation 103 "multiply": multiply, 104 } 105 if p.URLForSrc != nil { 106 p.funcMap["srcLink"] = p.URLForSrc 107 } 108 if p.URLForSrcPos != nil { 109 p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos) 110 } 111 if p.URLForSrcQuery != nil { 112 p.funcMap["queryLink"] = p.URLForSrcQuery 113 } 114 } 115 116 func multiply(a, b int) int { return a * b } 117 118 func filenameFunc(path string) string { 119 _, localname := pathpkg.Split(path) 120 return localname 121 } 122 123 func fileInfoNameFunc(fi os.FileInfo) string { 124 name := fi.Name() 125 if fi.IsDir() { 126 name += "/" 127 } 128 return name 129 } 130 131 func fileInfoTimeFunc(fi os.FileInfo) string { 132 if t := fi.ModTime(); t.Unix() != 0 { 133 return t.Local().String() 134 } 135 return "" // don't return epoch if time is obviously not set 136 } 137 138 // The strings in infoKinds must be properly html-escaped. 139 var infoKinds = [nKinds]string{ 140 PackageClause: "package clause", 141 ImportDecl: "import decl", 142 ConstDecl: "const decl", 143 TypeDecl: "type decl", 144 VarDecl: "var decl", 145 FuncDecl: "func decl", 146 MethodDecl: "method decl", 147 Use: "use", 148 } 149 150 func infoKind_htmlFunc(info SpotInfo) string { 151 return infoKinds[info.Kind()] // infoKind entries are html-escaped 152 } 153 154 func (p *Presentation) infoLineFunc(info SpotInfo) int { 155 line := info.Lori() 156 if info.IsIndex() { 157 index, _ := p.Corpus.searchIndex.Get() 158 if index != nil { 159 line = index.(*Index).Snippet(line).Line 160 } else { 161 // no line information available because 162 // we don't have an index - this should 163 // never happen; be conservative and don't 164 // crash 165 line = 0 166 } 167 } 168 return line 169 } 170 171 func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string { 172 if info.IsIndex() { 173 index, _ := p.Corpus.searchIndex.Get() 174 // Snippet.Text was HTML-escaped when it was generated 175 return index.(*Index).Snippet(info.Lori()).Text 176 } 177 return `<span class="alert">no snippet text available</span>` 178 } 179 180 func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string { 181 var buf bytes.Buffer 182 p.writeNode(&buf, info.FSet, node) 183 return buf.String() 184 } 185 186 func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { 187 var buf1 bytes.Buffer 188 p.writeNode(&buf1, info.FSet, node) 189 190 var buf2 bytes.Buffer 191 if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks { 192 LinkifyText(&buf2, buf1.Bytes(), n) 193 } else { 194 FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) 195 } 196 197 return buf2.String() 198 } 199 200 func comment_htmlFunc(comment string) string { 201 var buf bytes.Buffer 202 // TODO(gri) Provide list of words (e.g. function parameters) 203 // to be emphasized by ToHTML. 204 doc.ToHTML(&buf, comment, nil) // does html-escaping 205 return buf.String() 206 } 207 208 // punchCardWidth is the number of columns of fixed-width 209 // characters to assume when wrapping text. Very few people 210 // use terminals or cards smaller than 80 characters, so 80 it is. 211 // We do not try to sniff the environment or the tty to adapt to 212 // the situation; instead, by using a constant we make sure that 213 // godoc always produces the same output regardless of context, 214 // a consistency that is lost otherwise. For example, if we sniffed 215 // the environment or tty, then http://golang.org/pkg/math/?m=text 216 // would depend on the width of the terminal where godoc started, 217 // which is clearly bogus. More generally, the Unix tools that behave 218 // differently when writing to a tty than when writing to a file have 219 // a history of causing confusion (compare `ls` and `ls | cat`), and we 220 // want to avoid that mistake here. 221 const punchCardWidth = 80 222 223 func containsOnlySpace(buf []byte) bool { 224 isNotSpace := func(r rune) bool { return !unicode.IsSpace(r) } 225 return bytes.IndexFunc(buf, isNotSpace) == -1 226 } 227 228 func comment_textFunc(comment, indent, preIndent string) string { 229 var buf bytes.Buffer 230 doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent)) 231 if containsOnlySpace(buf.Bytes()) { 232 return "" 233 } 234 return buf.String() 235 } 236 237 // sanitizeFunc sanitizes the argument src by replacing newlines with 238 // blanks, removing extra blanks, and by removing trailing whitespace 239 // and commas before closing parentheses. 240 func sanitizeFunc(src string) string { 241 buf := make([]byte, len(src)) 242 j := 0 // buf index 243 comma := -1 // comma index if >= 0 244 for i := 0; i < len(src); i++ { 245 ch := src[i] 246 switch ch { 247 case '\t', '\n', ' ': 248 // ignore whitespace at the beginning, after a blank, or after opening parentheses 249 if j == 0 { 250 continue 251 } 252 if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' { 253 continue 254 } 255 // replace all whitespace with blanks 256 ch = ' ' 257 case ',': 258 comma = j 259 case ')', '}', ']': 260 // remove any trailing comma 261 if comma >= 0 { 262 j = comma 263 } 264 // remove any trailing whitespace 265 if j > 0 && buf[j-1] == ' ' { 266 j-- 267 } 268 default: 269 comma = -1 270 } 271 buf[j] = ch 272 j++ 273 } 274 // remove trailing blank, if any 275 if j > 0 && buf[j-1] == ' ' { 276 j-- 277 } 278 return string(buf[:j]) 279 } 280 281 type PageInfo struct { 282 Dirname string // directory containing the package 283 Err error // error or nil 284 Share bool // show share button on examples 285 286 // package info 287 FSet *token.FileSet // nil if no package documentation 288 PDoc *doc.Package // nil if no package documentation 289 Examples []*doc.Example // nil if no example code 290 Notes map[string][]*doc.Note // nil if no package Notes 291 PAst map[string]*ast.File // nil if no AST with package exports 292 IsMain bool // true for package main 293 IsFiltered bool // true if results were filtered 294 295 // analysis info 296 TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type) 297 AnalysisData htmltemplate.JS // array of TypeInfoJSON values 298 CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer) 299 CallGraphIndex map[string]int // maps func name to index in CallGraph 300 301 // directory info 302 Dirs *DirList // nil if no directory information 303 DirTime time.Time // directory time stamp 304 DirFlat bool // if set, show directory in a flat (non-indented) manner 305 } 306 307 func (info *PageInfo) IsEmpty() bool { 308 return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil 309 } 310 311 func pkgLinkFunc(path string) string { 312 // because of the irregular mapping under goroot 313 // we need to correct certain relative paths 314 path = strings.TrimPrefix(path, "/") 315 path = strings.TrimPrefix(path, "src/") 316 path = strings.TrimPrefix(path, "pkg/") 317 return "pkg/" + path 318 } 319 320 func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string { 321 // n must be an ast.Node or a *doc.Note 322 return func(info *PageInfo, n interface{}) string { 323 var pos, end token.Pos 324 325 switch n := n.(type) { 326 case ast.Node: 327 pos = n.Pos() 328 end = n.End() 329 case *doc.Note: 330 pos = n.Pos 331 end = n.End 332 default: 333 panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) 334 } 335 336 var relpath string 337 var line int 338 var low, high int // selection offset range 339 340 if pos.IsValid() { 341 p := info.FSet.Position(pos) 342 relpath = p.Filename 343 line = p.Line 344 low = p.Offset 345 } 346 if end.IsValid() { 347 high = info.FSet.Position(end).Offset 348 } 349 350 return srcPosLinkFunc(relpath, line, low, high) 351 } 352 } 353 354 func srcPosLinkFunc(s string, line, low, high int) string { 355 s = srcLinkFunc(s) 356 var buf bytes.Buffer 357 template.HTMLEscape(&buf, []byte(s)) 358 // selection ranges are of form "s=low:high" 359 if low < high { 360 fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping 361 // if we have a selection, position the page 362 // such that the selection is a bit below the top 363 line -= 10 364 if line < 1 { 365 line = 1 366 } 367 } 368 // line id's in html-printed source are of the 369 // form "L%d" where %d stands for the line number 370 if line > 0 { 371 fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping 372 } 373 return buf.String() 374 } 375 376 func srcLinkFunc(s string) string { 377 s = pathpkg.Clean("/" + s) 378 if !strings.HasPrefix(s, "/src/") { 379 s = "/src" + s 380 } 381 return s 382 } 383 384 // queryLinkFunc returns a URL for a line in a source file with a highlighted 385 // query term. 386 // s is expected to be a path to a source file. 387 // query is expected to be a string that has already been appropriately escaped 388 // for use in a URL query. 389 func queryLinkFunc(s, query string, line int) string { 390 url := pathpkg.Clean("/"+s) + "?h=" + query 391 if line > 0 { 392 url += "#L" + strconv.Itoa(line) 393 } 394 return url 395 } 396 397 func docLinkFunc(s string, ident string) string { 398 return pathpkg.Clean("/pkg/"+s) + "/#" + ident 399 } 400 401 func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string { 402 if !p.ShowExamples { 403 return "" 404 } 405 406 var buf bytes.Buffer 407 first := true 408 for _, eg := range info.Examples { 409 name := stripExampleSuffix(eg.Name) 410 if name != funcName { 411 continue 412 } 413 414 if !first { 415 buf.WriteString("\n") 416 } 417 first = false 418 419 // print code 420 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} 421 var buf1 bytes.Buffer 422 p.writeNode(&buf1, info.FSet, cnode) 423 code := buf1.String() 424 // Additional formatting if this is a function body. 425 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { 426 // remove surrounding braces 427 code = code[1 : n-1] 428 // unindent 429 code = strings.Replace(code, "\n ", "\n", -1) 430 } 431 code = strings.Trim(code, "\n") 432 code = strings.Replace(code, "\n", "\n\t", -1) 433 434 buf.WriteString(indent) 435 buf.WriteString("Example:\n\t") 436 buf.WriteString(code) 437 buf.WriteString("\n\n") 438 } 439 return buf.String() 440 } 441 442 func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string { 443 var buf bytes.Buffer 444 for _, eg := range info.Examples { 445 name := stripExampleSuffix(eg.Name) 446 447 if name != funcName { 448 continue 449 } 450 451 // print code 452 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} 453 code := p.node_htmlFunc(info, cnode, true) 454 out := eg.Output 455 wholeFile := true 456 457 // Additional formatting if this is a function body. 458 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { 459 wholeFile = false 460 // remove surrounding braces 461 code = code[1 : n-1] 462 // unindent 463 code = strings.Replace(code, "\n ", "\n", -1) 464 // remove output comment 465 if loc := exampleOutputRx.FindStringIndex(code); loc != nil { 466 code = strings.TrimSpace(code[:loc[0]]) 467 } 468 } 469 470 // Write out the playground code in standard Go style 471 // (use tabs, no comment highlight, etc). 472 play := "" 473 if eg.Play != nil && p.ShowPlayground { 474 var buf bytes.Buffer 475 if err := format.Node(&buf, info.FSet, eg.Play); err != nil { 476 log.Print(err) 477 } else { 478 play = buf.String() 479 } 480 } 481 482 // Drop output, as the output comment will appear in the code. 483 if wholeFile && play == "" { 484 out = "" 485 } 486 487 if p.ExampleHTML == nil { 488 out = "" 489 return "" 490 } 491 492 err := p.ExampleHTML.Execute(&buf, struct { 493 Name, Doc, Code, Play, Output string 494 Share bool 495 }{eg.Name, eg.Doc, code, play, out, info.Share}) 496 if err != nil { 497 log.Print(err) 498 } 499 } 500 return buf.String() 501 } 502 503 // example_nameFunc takes an example function name and returns its display 504 // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". 505 func (p *Presentation) example_nameFunc(s string) string { 506 name, suffix := splitExampleName(s) 507 // replace _ with . for method names 508 name = strings.Replace(name, "_", ".", 1) 509 // use "Package" if no name provided 510 if name == "" { 511 name = "Package" 512 } 513 return name + suffix 514 } 515 516 // example_suffixFunc takes an example function name and returns its suffix in 517 // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". 518 func (p *Presentation) example_suffixFunc(name string) string { 519 _, suffix := splitExampleName(name) 520 return suffix 521 } 522 523 // implements_html returns the "> Implements" toggle for a package-level named type. 524 // Its contents are populated from JSON data by client-side JS at load time. 525 func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string { 526 if p.ImplementsHTML == nil { 527 return "" 528 } 529 index, ok := info.TypeInfoIndex[typeName] 530 if !ok { 531 return "" 532 } 533 var buf bytes.Buffer 534 err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index}) 535 if err != nil { 536 log.Print(err) 537 } 538 return buf.String() 539 } 540 541 // methodset_html returns the "> Method set" toggle for a package-level named type. 542 // Its contents are populated from JSON data by client-side JS at load time. 543 func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string { 544 if p.MethodSetHTML == nil { 545 return "" 546 } 547 index, ok := info.TypeInfoIndex[typeName] 548 if !ok { 549 return "" 550 } 551 var buf bytes.Buffer 552 err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index}) 553 if err != nil { 554 log.Print(err) 555 } 556 return buf.String() 557 } 558 559 // callgraph_html returns the "> Call graph" toggle for a package-level func. 560 // Its contents are populated from JSON data by client-side JS at load time. 561 func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string { 562 if p.CallGraphHTML == nil { 563 return "" 564 } 565 if recv != "" { 566 // Format must match (*ssa.Function).RelString(). 567 name = fmt.Sprintf("(%s).%s", recv, name) 568 } 569 index, ok := info.CallGraphIndex[name] 570 if !ok { 571 return "" 572 } 573 var buf bytes.Buffer 574 err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index}) 575 if err != nil { 576 log.Print(err) 577 } 578 return buf.String() 579 } 580 581 func noteTitle(note string) string { 582 return strings.Title(strings.ToLower(note)) 583 } 584 585 func startsWithUppercase(s string) bool { 586 r, _ := utf8.DecodeRuneInString(s) 587 return unicode.IsUpper(r) 588 } 589 590 var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`) 591 592 // stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name 593 // while keeping uppercase Braz in Foo_Braz. 594 func stripExampleSuffix(name string) string { 595 if i := strings.LastIndex(name, "_"); i != -1 { 596 if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { 597 name = name[:i] 598 } 599 } 600 return name 601 } 602 603 func splitExampleName(s string) (name, suffix string) { 604 i := strings.LastIndex(s, "_") 605 if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { 606 name = s[:i] 607 suffix = " (" + strings.Title(s[i+1:]) + ")" 608 return 609 } 610 name = s 611 return 612 } 613 614 // Write an AST node to w. 615 func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}) { 616 // convert trailing tabs into spaces using a tconv filter 617 // to ensure a good outcome in most browsers (there may still 618 // be tabs in comments and strings, but converting those into 619 // the right number of spaces is much harder) 620 // 621 // TODO(gri) rethink printer flags - perhaps tconv can be eliminated 622 // with an another printer mode (which is more efficiently 623 // implemented in the printer than here with another layer) 624 mode := printer.TabIndent | printer.UseSpaces 625 err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x) 626 if err != nil { 627 log.Print(err) 628 } 629 } 630 631 // WriteNode writes x to w. 632 // TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode. 633 func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) { 634 p.writeNode(w, fset, x) 635 }