1
2
3
4
5
6
7
8
9
10 package 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
36
37
38
39 const builtinPkgPath = "builtin"
40
41
42
43
44
45
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
65 "filename": filenameFunc,
66 "repeat": strings.Repeat,
67 "since": p.Corpus.pkgAPIInfo.sinceVersionFunc,
68
69
70 "fileInfoName": fileInfoNameFunc,
71 "fileInfoTime": fileInfoTimeFunc,
72
73
74 "infoKind_html": infoKind_htmlFunc,
75 "infoLine": p.infoLineFunc,
76 "infoSnippet_html": p.infoSnippet_htmlFunc,
77
78
79 "node": p.nodeFunc,
80 "node_html": p.node_htmlFunc,
81 "comment_html": comment_htmlFunc,
82 "sanitize": sanitizeFunc,
83
84
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
94 "example_html": p.example_htmlFunc,
95 "example_name": p.example_nameFunc,
96 "example_suffix": p.example_suffixFunc,
97
98
99 "callgraph_html": p.callgraph_htmlFunc,
100 "implements_html": p.implements_htmlFunc,
101 "methodset_html": p.methodset_htmlFunc,
102
103
104 "noteTitle": noteTitle,
105
106
107 "multiply": multiply,
108
109
110 "modeQueryString": modeQueryString,
111
112
113 "hasThirdParty": hasThirdParty,
114
115
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 ""
149 }
150
151
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()]
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
175
176
177
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
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
217
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
225
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
243
244
245 func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) {
246 if st.Fields == nil {
247 return
248 }
249
250
251
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
274
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
288 var commentPrefix = []byte(`<span class="comment">// `)
289
290
291
292
293
294
295 func linkedField(line []byte, ids map[string]string) string {
296 line = bytes.TrimSpace(line)
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315 line = bytes.TrimPrefix(line, commentPrefix)
316 id := scanIdentifier(line)
317 if len(id) == 0 {
318
319
320 return ""
321 }
322 return ids[string(id)]
323 }
324
325
326
327
328 func scanIdentifier(v []byte) []byte {
329 var n int
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
351
352
353
354
355
356 godocToHTML(&buf, info.PDoc, comment)
357
358 return buf.String()
359 }
360
361
362
363
364 func sanitizeFunc(src string) string {
365 buf := make([]byte, len(src))
366 j := 0
367 comma := -1
368 for i := 0; i < len(src); i++ {
369 ch := src[i]
370 switch ch {
371 case '\t', '\n', ' ':
372
373 if j == 0 {
374 continue
375 }
376 if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
377 continue
378 }
379
380 ch = ' '
381 case ',':
382 comma = j
383 case ')', '}', ']':
384
385 if comma >= 0 {
386 j = comma
387 }
388
389 if j > 0 && buf[j-1] == ' ' {
390 j--
391 }
392 default:
393 comma = -1
394 }
395 buf[j] = ch
396 j++
397 }
398
399 if j > 0 && buf[j-1] == ' ' {
400 j--
401 }
402 return string(buf[:j])
403 }
404
405 type PageInfo struct {
406 Dirname string
407 Err error
408
409 Mode PageInfoMode
410
411
412 FSet *token.FileSet
413 PDoc *doc.Package
414 Examples []*doc.Example
415 Notes map[string][]*doc.Note
416 PAst map[string]*ast.File
417 IsMain bool
418 IsFiltered bool
419
420
421 TypeInfoIndex map[string]int
422 AnalysisData htmltemplate.JS
423 CallGraph htmltemplate.JS
424 CallGraphIndex map[string]int
425
426
427 Dirs *DirList
428 DirTime time.Time
429 DirFlat bool
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
438
439 path = strings.TrimPrefix(path, "/")
440 path = strings.TrimPrefix(path, "src/")
441 path = strings.TrimPrefix(path, "pkg/")
442 return "pkg/" + path
443 }
444
445
446
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
457
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
466
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
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
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
526 if low < high {
527 fmt.Fprintf(&buf, "?s=%d:%d", low, high)
528
529
530 line -= 10
531 if line < 1 {
532 line = 1
533 }
534 }
535
536
537 if line > 0 {
538 fmt.Fprintf(&buf, "#L%d", line)
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
552
553
554
555
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
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
584 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
585 wholeFile = false
586
587 code = code[1 : n-1]
588
589 code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "")
590
591 if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
592 code = strings.TrimSpace(code[:loc[0]])
593 }
594 }
595
596
597
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
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
637
638 return cg[i:]
639 }
640 }
641
642
643 return []*ast.CommentGroup{}
644 }
645
646
647
648 func (p *Presentation) example_nameFunc(s string) string {
649 name, suffix := splitExampleName(s)
650
651 name = strings.Replace(name, "_", ".", 1)
652
653 if name == "" {
654 name = "Package"
655 }
656 return name + suffix
657 }
658
659
660
661 func (p *Presentation) example_suffixFunc(name string) string {
662 _, suffix := splitExampleName(name)
663 return suffix
664 }
665
666
667
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
685
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
703
704 func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
705 if p.CallGraphHTML == nil {
706 return ""
707 }
708 if recv != "" {
709
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
736
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
758
759
760
761
762 func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
763
764
765 var buf bytes.Buffer
766 if strings.HasPrefix(body, oldIndent) {
767 buf.WriteString(newIndent)
768 body = body[len(oldIndent):]
769 }
770
771
772
773 const (
774 codeState = iota
775 runeState
776 interpretedStringState
777 rawStringState
778 )
779 searchChars := []string{
780 "'\"`\n",
781 `\'`,
782 `\"`,
783 "`\n",
784
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
845
846
847
848
849 func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
850
851
852
853
854
855
856
857
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
886 if structName != "" {
887 fieldSince := apiInfo.fieldSince[structName]
888 typeSince := apiInfo.typeSince[structName]
889
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
901
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
927
928 func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
929 p.writeNode(w, nil, fset, x)
930 }
931
932
933
934
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
View as plain text