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
350
351 return string(info.PDoc.HTML(comment))
352 }
353
354
355
356
357 func sanitizeFunc(src string) string {
358 buf := make([]byte, len(src))
359 j := 0
360 comma := -1
361 for i := 0; i < len(src); i++ {
362 ch := src[i]
363 switch ch {
364 case '\t', '\n', ' ':
365
366 if j == 0 {
367 continue
368 }
369 if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
370 continue
371 }
372
373 ch = ' '
374 case ',':
375 comma = j
376 case ')', '}', ']':
377
378 if comma >= 0 {
379 j = comma
380 }
381
382 if j > 0 && buf[j-1] == ' ' {
383 j--
384 }
385 default:
386 comma = -1
387 }
388 buf[j] = ch
389 j++
390 }
391
392 if j > 0 && buf[j-1] == ' ' {
393 j--
394 }
395 return string(buf[:j])
396 }
397
398 type PageInfo struct {
399 Dirname string
400 Err error
401
402 Mode PageInfoMode
403
404
405 FSet *token.FileSet
406 PDoc *doc.Package
407 Examples []*doc.Example
408 Notes map[string][]*doc.Note
409 PAst map[string]*ast.File
410 IsMain bool
411 IsFiltered bool
412
413
414 TypeInfoIndex map[string]int
415 AnalysisData htmltemplate.JS
416 CallGraph htmltemplate.JS
417 CallGraphIndex map[string]int
418
419
420 Dirs *DirList
421 DirTime time.Time
422 DirFlat bool
423 }
424
425 func (info *PageInfo) IsEmpty() bool {
426 return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
427 }
428
429 func pkgLinkFunc(path string) string {
430
431
432 path = strings.TrimPrefix(path, "/")
433 path = strings.TrimPrefix(path, "src/")
434 path = strings.TrimPrefix(path, "pkg/")
435 return "pkg/" + path
436 }
437
438
439
440 func srcToPkgLinkFunc(relpath string) string {
441 relpath = pkgLinkFunc(relpath)
442 relpath = pathpkg.Dir(relpath)
443 if relpath == "pkg" {
444 return `<a href="/pkg">Index</a>`
445 }
446 return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
447 }
448
449
450
451 func srcBreadcrumbFunc(relpath string) string {
452 segments := strings.Split(relpath, "/")
453 var buf bytes.Buffer
454 var selectedSegment string
455 var selectedIndex int
456
457 if strings.HasSuffix(relpath, "/") {
458
459
460 selectedIndex = len(segments) - 2
461 selectedSegment = segments[selectedIndex] + "/"
462 } else {
463 selectedIndex = len(segments) - 1
464 selectedSegment = segments[selectedIndex]
465 }
466
467 for i := range segments[:selectedIndex] {
468 buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
469 strings.Join(segments[:i+1], "/"),
470 segments[i],
471 ))
472 }
473
474 buf.WriteString(`<span class="text-muted">`)
475 buf.WriteString(selectedSegment)
476 buf.WriteString(`</span>`)
477 return buf.String()
478 }
479
480 func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
481
482 return func(info *PageInfo, n interface{}) string {
483 var pos, end token.Pos
484
485 switch n := n.(type) {
486 case ast.Node:
487 pos = n.Pos()
488 end = n.End()
489 case *doc.Note:
490 pos = n.Pos
491 end = n.End
492 default:
493 panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
494 }
495
496 var relpath string
497 var line int
498 var low, high int
499
500 if pos.IsValid() {
501 p := info.FSet.Position(pos)
502 relpath = p.Filename
503 line = p.Line
504 low = p.Offset
505 }
506 if end.IsValid() {
507 high = info.FSet.Position(end).Offset
508 }
509
510 return srcPosLinkFunc(relpath, line, low, high)
511 }
512 }
513
514 func srcPosLinkFunc(s string, line, low, high int) string {
515 s = srcLinkFunc(s)
516 var buf bytes.Buffer
517 template.HTMLEscape(&buf, []byte(s))
518
519 if low < high {
520 fmt.Fprintf(&buf, "?s=%d:%d", low, high)
521
522
523 line -= 10
524 if line < 1 {
525 line = 1
526 }
527 }
528
529
530 if line > 0 {
531 fmt.Fprintf(&buf, "#L%d", line)
532 }
533 return buf.String()
534 }
535
536 func srcLinkFunc(s string) string {
537 s = pathpkg.Clean("/" + s)
538 if !strings.HasPrefix(s, "/src/") {
539 s = "/src" + s
540 }
541 return s
542 }
543
544
545
546
547
548
549 func queryLinkFunc(s, query string, line int) string {
550 url := pathpkg.Clean("/"+s) + "?h=" + query
551 if line > 0 {
552 url += "#L" + strconv.Itoa(line)
553 }
554 return url
555 }
556
557 func docLinkFunc(s string, ident string) string {
558 return pathpkg.Clean("/pkg/"+s) + "/#" + ident
559 }
560
561 func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
562 var buf bytes.Buffer
563 for _, eg := range info.Examples {
564 name := stripExampleSuffix(eg.Name)
565
566 if name != funcName {
567 continue
568 }
569
570
571 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
572 code := p.node_htmlFunc(info, cnode, true)
573 out := eg.Output
574 wholeFile := true
575
576
577 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
578 wholeFile = false
579
580 code = code[1 : n-1]
581
582 code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "")
583
584 if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
585 code = strings.TrimSpace(code[:loc[0]])
586 }
587 }
588
589
590
591 play := ""
592 if eg.Play != nil && p.ShowPlayground {
593 var buf bytes.Buffer
594 eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
595 if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
596 log.Print(err)
597 } else {
598 play = buf.String()
599 }
600 }
601
602
603 if wholeFile && play == "" {
604 out = ""
605 }
606
607 if p.ExampleHTML == nil {
608 out = ""
609 return ""
610 }
611
612 err := p.ExampleHTML.Execute(&buf, struct {
613 Name, Doc, Code, Play, Output string
614 }{eg.Name, eg.Doc, code, play, out})
615 if err != nil {
616 log.Print(err)
617 }
618 }
619 return buf.String()
620 }
621
622 func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
623 if len(cg) == 0 {
624 return cg
625 }
626
627 for i := range cg {
628 if !strings.HasPrefix(cg[i].Text(), "+build ") {
629
630
631 return cg[i:]
632 }
633 }
634
635
636 return []*ast.CommentGroup{}
637 }
638
639
640
641 func (p *Presentation) example_nameFunc(s string) string {
642 name, suffix := splitExampleName(s)
643
644 name = strings.Replace(name, "_", ".", 1)
645
646 if name == "" {
647 name = "Package"
648 }
649 return name + suffix
650 }
651
652
653
654 func (p *Presentation) example_suffixFunc(name string) string {
655 _, suffix := splitExampleName(name)
656 return suffix
657 }
658
659
660
661 func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string {
662 if p.ImplementsHTML == nil {
663 return ""
664 }
665 index, ok := info.TypeInfoIndex[typeName]
666 if !ok {
667 return ""
668 }
669 var buf bytes.Buffer
670 err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index})
671 if err != nil {
672 log.Print(err)
673 }
674 return buf.String()
675 }
676
677
678
679 func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string {
680 if p.MethodSetHTML == nil {
681 return ""
682 }
683 index, ok := info.TypeInfoIndex[typeName]
684 if !ok {
685 return ""
686 }
687 var buf bytes.Buffer
688 err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index})
689 if err != nil {
690 log.Print(err)
691 }
692 return buf.String()
693 }
694
695
696
697 func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
698 if p.CallGraphHTML == nil {
699 return ""
700 }
701 if recv != "" {
702
703 name = fmt.Sprintf("(%s).%s", recv, name)
704 }
705 index, ok := info.CallGraphIndex[name]
706 if !ok {
707 return ""
708 }
709 var buf bytes.Buffer
710 err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index})
711 if err != nil {
712 log.Print(err)
713 }
714 return buf.String()
715 }
716
717 func noteTitle(note string) string {
718 return strings.Title(strings.ToLower(note))
719 }
720
721 func startsWithUppercase(s string) bool {
722 r, _ := utf8.DecodeRuneInString(s)
723 return unicode.IsUpper(r)
724 }
725
726 var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
727
728
729
730 func stripExampleSuffix(name string) string {
731 if i := strings.LastIndex(name, "_"); i != -1 {
732 if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
733 name = name[:i]
734 }
735 }
736 return name
737 }
738
739 func splitExampleName(s string) (name, suffix string) {
740 i := strings.LastIndex(s, "_")
741 if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
742 name = s[:i]
743 suffix = " (" + strings.Title(s[i+1:]) + ")"
744 return
745 }
746 name = s
747 return
748 }
749
750
751
752
753
754
755 func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
756
757
758 var buf bytes.Buffer
759 if strings.HasPrefix(body, oldIndent) {
760 buf.WriteString(newIndent)
761 body = body[len(oldIndent):]
762 }
763
764
765
766 const (
767 codeState = iota
768 runeState
769 interpretedStringState
770 rawStringState
771 )
772 searchChars := []string{
773 "'\"`\n",
774 `\'`,
775 `\"`,
776 "`\n",
777
778 }
779 state := codeState
780 for {
781 i := strings.IndexAny(body, searchChars[state])
782 if i < 0 {
783 buf.WriteString(body)
784 break
785 }
786 c := body[i]
787 buf.WriteString(body[:i+1])
788 body = body[i+1:]
789 switch state {
790 case codeState:
791 switch c {
792 case '\'':
793 state = runeState
794 case '"':
795 state = interpretedStringState
796 case '`':
797 state = rawStringState
798 case '\n':
799 if strings.HasPrefix(body, oldIndent) {
800 buf.WriteString(newIndent)
801 body = body[len(oldIndent):]
802 }
803 }
804
805 case runeState:
806 switch c {
807 case '\\':
808 r, size := utf8.DecodeRuneInString(body)
809 buf.WriteRune(r)
810 body = body[size:]
811 case '\'':
812 state = codeState
813 }
814
815 case interpretedStringState:
816 switch c {
817 case '\\':
818 r, size := utf8.DecodeRuneInString(body)
819 buf.WriteRune(r)
820 body = body[size:]
821 case '"':
822 state = codeState
823 }
824
825 case rawStringState:
826 switch c {
827 case '`':
828 state = codeState
829 case '\n':
830 buf.WriteString(newIndent)
831 }
832 }
833 }
834 return buf.String()
835 }
836
837
838
839
840
841
842 func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
843
844
845
846
847
848
849
850
851
852 var pkgName, structName string
853 var apiInfo pkgAPIVersions
854 if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
855 p.Corpus != nil &&
856 gd.Tok == token.TYPE && len(gd.Specs) != 0 {
857 pkgName = pageInfo.PDoc.ImportPath
858 if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
859 if _, ok := ts.Type.(*ast.StructType); ok {
860 structName = ts.Name.Name
861 }
862 }
863 apiInfo = p.Corpus.pkgAPIInfo[pkgName]
864 }
865
866 var out = w
867 var buf bytes.Buffer
868 if structName != "" {
869 out = &buf
870 }
871
872 mode := printer.TabIndent | printer.UseSpaces
873 err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
874 if err != nil {
875 log.Print(err)
876 }
877
878
879 if structName != "" {
880 fieldSince := apiInfo.fieldSince[structName]
881 typeSince := apiInfo.typeSince[structName]
882
883 var buf2 bytes.Buffer
884 buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
885 bs := bufio.NewScanner(&buf)
886 for bs.Scan() {
887 line := bs.Bytes()
888 field := firstIdent(line)
889 var since string
890 if field != "" {
891 since = fieldSince[field]
892 if since != "" && since == typeSince {
893
894
895 since = ""
896 }
897 }
898 if since == "" {
899 buf2.Write(line)
900 } else {
901 if bytes.Contains(line, slashSlash) {
902 line = bytes.TrimRight(line, " \t.")
903 buf2.Write(line)
904 buf2.WriteString("; added in Go ")
905 } else {
906 buf2.Write(line)
907 buf2.WriteString(" // Go ")
908 }
909 buf2.WriteString(since)
910 }
911 buf2.WriteByte('\n')
912 }
913 w.Write(buf2.Bytes())
914 }
915 }
916
917 var slashSlash = []byte("//")
918
919
920
921 func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
922 p.writeNode(w, nil, fset, x)
923 }
924
925
926
927
928 func firstIdent(x []byte) string {
929 x = bytes.TrimSpace(x)
930 i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
931 if i == -1 {
932 return string(x)
933 }
934 return string(x[:i])
935 }
936
View as plain text