blob: 7964b5245c04ea40c7afbb032149bb595567ed39 [file] [log] [blame]
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +10001// 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
11
12import (
13 "bytes"
14 "fmt"
15 "go/ast"
16 "go/doc"
17 "go/format"
18 "go/printer"
19 "go/token"
20 "io"
21 "log"
22 "os"
23 pathpkg "path"
24 "regexp"
Brad Garciaff7cfaf2013-12-04 10:37:01 -050025 "strconv"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100026 "strings"
27 "text/template"
28 "time"
29 "unicode"
30 "unicode/utf8"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100031)
32
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100033// Fake relative package path for built-ins. Documentation for all globals
34// (not just exported ones) will be shown for packages in this directory.
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +100035const builtinPkgPath = "builtin"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100036
37// FuncMap defines template functions used in godoc templates.
38//
39// Convention: template function names ending in "_html" or "_url" produce
40// HTML- or URL-escaped strings; all other function results may
41// require explicit escaping in the template.
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100042func (p *Presentation) FuncMap() template.FuncMap {
43 p.initFuncMapOnce.Do(p.initFuncMap)
44 return p.funcMap
45}
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100046
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100047func (p *Presentation) TemplateFuncs() template.FuncMap {
48 p.initFuncMapOnce.Do(p.initFuncMap)
49 return p.templateFuncs
50}
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100051
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100052func (p *Presentation) initFuncMap() {
53 if p.Corpus == nil {
54 panic("nil Presentation.Corpus")
55 }
56 p.templateFuncs = template.FuncMap{
57 "code": p.code,
58 }
59 p.funcMap = template.FuncMap{
60 // various helpers
61 "filename": filenameFunc,
62 "repeat": strings.Repeat,
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100063
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100064 // access to FileInfos (directory listings)
65 "fileInfoName": fileInfoNameFunc,
66 "fileInfoTime": fileInfoTimeFunc,
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100067
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100068 // access to search result information
69 "infoKind_html": infoKind_htmlFunc,
70 "infoLine": p.infoLineFunc,
71 "infoSnippet_html": p.infoSnippet_htmlFunc,
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100072
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100073 // formatting of AST nodes
74 "node": p.nodeFunc,
75 "node_html": p.node_htmlFunc,
76 "comment_html": comment_htmlFunc,
77 "comment_text": comment_textFunc,
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100078
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100079 // support for URL attributes
80 "pkgLink": pkgLinkFunc,
81 "srcLink": srcLinkFunc,
Brad Garciad9d5e512013-11-01 13:20:32 -040082 "posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
Brad Garciaf3faf8b2013-11-21 11:55:42 -050083 "docLink": docLinkFunc,
Brad Garciaff7cfaf2013-12-04 10:37:01 -050084 "queryLink": queryLinkFunc,
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100085
86 // formatting of Examples
87 "example_html": p.example_htmlFunc,
88 "example_text": p.example_textFunc,
89 "example_name": p.example_nameFunc,
90 "example_suffix": p.example_suffixFunc,
91
92 // formatting of Notes
93 "noteTitle": noteTitle,
94 }
Brad Garciad9d5e512013-11-01 13:20:32 -040095 if p.URLForSrc != nil {
96 p.funcMap["srcLink"] = p.URLForSrc
97 }
98 if p.URLForSrcPos != nil {
99 p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos)
100 }
Brad Garciaff7cfaf2013-12-04 10:37:01 -0500101 if p.URLForSrcQuery != nil {
102 p.funcMap["queryLink"] = p.URLForSrcQuery
103 }
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000104}
105
106func filenameFunc(path string) string {
107 _, localname := pathpkg.Split(path)
108 return localname
109}
110
111func fileInfoNameFunc(fi os.FileInfo) string {
112 name := fi.Name()
113 if fi.IsDir() {
114 name += "/"
115 }
116 return name
117}
118
119func fileInfoTimeFunc(fi os.FileInfo) string {
120 if t := fi.ModTime(); t.Unix() != 0 {
121 return t.Local().String()
122 }
123 return "" // don't return epoch if time is obviously not set
124}
125
126// The strings in infoKinds must be properly html-escaped.
127var infoKinds = [nKinds]string{
128 PackageClause: "package clause",
129 ImportDecl: "import decl",
130 ConstDecl: "const decl",
131 TypeDecl: "type decl",
132 VarDecl: "var decl",
133 FuncDecl: "func decl",
134 MethodDecl: "method decl",
135 Use: "use",
136}
137
138func infoKind_htmlFunc(info SpotInfo) string {
139 return infoKinds[info.Kind()] // infoKind entries are html-escaped
140}
141
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000142func (p *Presentation) infoLineFunc(info SpotInfo) int {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000143 line := info.Lori()
144 if info.IsIndex() {
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000145 index, _ := p.Corpus.searchIndex.Get()
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000146 if index != nil {
147 line = index.(*Index).Snippet(line).Line
148 } else {
149 // no line information available because
150 // we don't have an index - this should
151 // never happen; be conservative and don't
152 // crash
153 line = 0
154 }
155 }
156 return line
157}
158
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000159func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000160 if info.IsIndex() {
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000161 index, _ := p.Corpus.searchIndex.Get()
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000162 // Snippet.Text was HTML-escaped when it was generated
163 return index.(*Index).Snippet(info.Lori()).Text
164 }
165 return `<span class="alert">no snippet text available</span>`
166}
167
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000168func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000169 var buf bytes.Buffer
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000170 p.writeNode(&buf, info.FSet, node)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000171 return buf.String()
172}
173
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000174func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000175 var buf1 bytes.Buffer
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000176 p.writeNode(&buf1, info.FSet, node)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000177
178 var buf2 bytes.Buffer
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000179 if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000180 LinkifyText(&buf2, buf1.Bytes(), n)
181 } else {
182 FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
183 }
184
185 return buf2.String()
186}
187
188func comment_htmlFunc(comment string) string {
189 var buf bytes.Buffer
190 // TODO(gri) Provide list of words (e.g. function parameters)
191 // to be emphasized by ToHTML.
192 doc.ToHTML(&buf, comment, nil) // does html-escaping
193 return buf.String()
194}
195
196// punchCardWidth is the number of columns of fixed-width
197// characters to assume when wrapping text. Very few people
198// use terminals or cards smaller than 80 characters, so 80 it is.
199// We do not try to sniff the environment or the tty to adapt to
200// the situation; instead, by using a constant we make sure that
201// godoc always produces the same output regardless of context,
202// a consistency that is lost otherwise. For example, if we sniffed
203// the environment or tty, then http://golang.org/pkg/math/?m=text
204// would depend on the width of the terminal where godoc started,
205// which is clearly bogus. More generally, the Unix tools that behave
206// differently when writing to a tty than when writing to a file have
207// a history of causing confusion (compare `ls` and `ls | cat`), and we
208// want to avoid that mistake here.
209const punchCardWidth = 80
210
211func comment_textFunc(comment, indent, preIndent string) string {
212 var buf bytes.Buffer
213 doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent))
214 return buf.String()
215}
216
217type PageInfo struct {
218 Dirname string // directory containing the package
219 Err error // error or nil
220
221 // package info
Brad Garcia2ca2bfc2014-01-30 06:28:19 -0500222 FSet *token.FileSet // nil if no package documentation
223 PDoc *doc.Package // nil if no package documentation
224 Examples []*doc.Example // nil if no example code
225 Notes map[string][]*doc.Note // nil if no package Notes
226 PAst map[string]*ast.File // nil if no AST with package exports
227 IsMain bool // true for package main
228 IsFiltered bool // true if results were filtered
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000229
230 // directory info
231 Dirs *DirList // nil if no directory information
232 DirTime time.Time // directory time stamp
233 DirFlat bool // if set, show directory in a flat (non-indented) manner
234}
235
236func (info *PageInfo) IsEmpty() bool {
237 return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
238}
239
240func pkgLinkFunc(path string) string {
241 relpath := path[1:]
242 // because of the irregular mapping under goroot
243 // we need to correct certain relative paths
244 relpath = strings.TrimPrefix(relpath, "src/pkg/")
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +1000245 return "pkg/" + relpath // remove trailing '/' for relative URL
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000246}
247
Brad Garciad9d5e512013-11-01 13:20:32 -0400248func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
249 // n must be an ast.Node or a *doc.Note
250 return func(info *PageInfo, n interface{}) string {
251 var pos, end token.Pos
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000252
Brad Garciad9d5e512013-11-01 13:20:32 -0400253 switch n := n.(type) {
254 case ast.Node:
255 pos = n.Pos()
256 end = n.End()
257 case *doc.Note:
258 pos = n.Pos
259 end = n.End
260 default:
261 panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
262 }
263
264 var relpath string
265 var line int
266 var low, high int // selection offset range
267
268 if pos.IsValid() {
269 p := info.FSet.Position(pos)
270 relpath = p.Filename
271 line = p.Line
272 low = p.Offset
273 }
274 if end.IsValid() {
275 high = info.FSet.Position(end).Offset
276 }
277
278 return srcPosLinkFunc(relpath, line, low, high)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000279 }
Brad Garciad9d5e512013-11-01 13:20:32 -0400280}
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000281
Brad Garciad9d5e512013-11-01 13:20:32 -0400282func srcPosLinkFunc(s string, line, low, high int) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000283 var buf bytes.Buffer
Brad Garciad9d5e512013-11-01 13:20:32 -0400284 template.HTMLEscape(&buf, []byte(s))
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000285 // selection ranges are of form "s=low:high"
286 if low < high {
287 fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
288 // if we have a selection, position the page
289 // such that the selection is a bit below the top
290 line -= 10
291 if line < 1 {
292 line = 1
293 }
294 }
295 // line id's in html-printed source are of the
296 // form "L%d" where %d stands for the line number
297 if line > 0 {
298 fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
299 }
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000300 return buf.String()
301}
302
303func srcLinkFunc(s string) string {
304 return pathpkg.Clean("/" + s)
305}
306
Brad Garciaff7cfaf2013-12-04 10:37:01 -0500307// queryLinkFunc returns a URL for a line in a source file with a highlighted
308// query term.
309// s is expected to be a path to a source file.
310// query is expected to be a string that has already been appropriately escaped
311// for use in a URL query.
312func queryLinkFunc(s, query string, line int) string {
313 url := pathpkg.Clean("/"+s) + "?h=" + query
314 if line > 0 {
315 url += "#L" + strconv.Itoa(line)
316 }
317 return url
318}
319
Brad Garciaf3faf8b2013-11-21 11:55:42 -0500320func docLinkFunc(s string, ident string) string {
321 s = strings.TrimPrefix(s, "/src")
322 return pathpkg.Clean("/"+s) + "/#" + ident
323}
324
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000325func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string {
326 if !p.ShowExamples {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000327 return ""
328 }
329
330 var buf bytes.Buffer
331 first := true
332 for _, eg := range info.Examples {
333 name := stripExampleSuffix(eg.Name)
334 if name != funcName {
335 continue
336 }
337
338 if !first {
339 buf.WriteString("\n")
340 }
341 first = false
342
343 // print code
344 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
345 var buf1 bytes.Buffer
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000346 p.writeNode(&buf1, info.FSet, cnode)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000347 code := buf1.String()
348 // Additional formatting if this is a function body.
349 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
350 // remove surrounding braces
351 code = code[1 : n-1]
352 // unindent
353 code = strings.Replace(code, "\n ", "\n", -1)
354 }
355 code = strings.Trim(code, "\n")
356 code = strings.Replace(code, "\n", "\n\t", -1)
357
358 buf.WriteString(indent)
359 buf.WriteString("Example:\n\t")
360 buf.WriteString(code)
361 buf.WriteString("\n")
362 }
363 return buf.String()
364}
365
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000366func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000367 var buf bytes.Buffer
368 for _, eg := range info.Examples {
369 name := stripExampleSuffix(eg.Name)
370
371 if name != funcName {
372 continue
373 }
374
375 // print code
376 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000377 code := p.node_htmlFunc(info, cnode, true)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000378 out := eg.Output
379 wholeFile := true
380
381 // Additional formatting if this is a function body.
382 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
383 wholeFile = false
384 // remove surrounding braces
385 code = code[1 : n-1]
386 // unindent
387 code = strings.Replace(code, "\n ", "\n", -1)
388 // remove output comment
389 if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
390 code = strings.TrimSpace(code[:loc[0]])
391 }
392 }
393
394 // Write out the playground code in standard Go style
395 // (use tabs, no comment highlight, etc).
396 play := ""
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000397 if eg.Play != nil && p.ShowPlayground {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000398 var buf bytes.Buffer
399 if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
400 log.Print(err)
401 } else {
402 play = buf.String()
403 }
404 }
405
406 // Drop output, as the output comment will appear in the code.
407 if wholeFile && play == "" {
408 out = ""
409 }
410
Brad Fitzpatrick66f0d6e2013-07-18 13:51:17 +1000411 if p.ExampleHTML == nil {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000412 out = ""
413 return ""
414 }
415
Brad Fitzpatrick66f0d6e2013-07-18 13:51:17 +1000416 err := p.ExampleHTML.Execute(&buf, struct {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000417 Name, Doc, Code, Play, Output string
418 }{eg.Name, eg.Doc, code, play, out})
419 if err != nil {
420 log.Print(err)
421 }
422 }
423 return buf.String()
424}
425
426// example_nameFunc takes an example function name and returns its display
427// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000428func (p *Presentation) example_nameFunc(s string) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000429 name, suffix := splitExampleName(s)
430 // replace _ with . for method names
431 name = strings.Replace(name, "_", ".", 1)
432 // use "Package" if no name provided
433 if name == "" {
434 name = "Package"
435 }
436 return name + suffix
437}
438
439// example_suffixFunc takes an example function name and returns its suffix in
440// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000441func (p *Presentation) example_suffixFunc(name string) string {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000442 _, suffix := splitExampleName(name)
443 return suffix
444}
445
446func noteTitle(note string) string {
447 return strings.Title(strings.ToLower(note))
448}
449
450func startsWithUppercase(s string) bool {
451 r, _ := utf8.DecodeRuneInString(s)
452 return unicode.IsUpper(r)
453}
454
455var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`)
456
457// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
458// while keeping uppercase Braz in Foo_Braz.
459func stripExampleSuffix(name string) string {
460 if i := strings.LastIndex(name, "_"); i != -1 {
461 if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
462 name = name[:i]
463 }
464 }
465 return name
466}
467
468func splitExampleName(s string) (name, suffix string) {
469 i := strings.LastIndex(s, "_")
470 if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
471 name = s[:i]
472 suffix = " (" + strings.Title(s[i+1:]) + ")"
473 return
474 }
475 name = s
476 return
477}
478
479// Write an AST node to w.
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000480func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000481 // convert trailing tabs into spaces using a tconv filter
482 // to ensure a good outcome in most browsers (there may still
483 // be tabs in comments and strings, but converting those into
484 // the right number of spaces is much harder)
485 //
486 // TODO(gri) rethink printer flags - perhaps tconv can be eliminated
487 // with an another printer mode (which is more efficiently
488 // implemented in the printer than here with another layer)
489 mode := printer.TabIndent | printer.UseSpaces
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000490 err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000491 if err != nil {
492 log.Print(err)
493 }
494}
495
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000496// WriteNote writes x to w.
497func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
498 p.writeNode(w, fset, x)
499}