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