diff --git a/src/cmd/godoc/Makefile b/src/cmd/godoc/Makefile index 106f46effa..f93324f281 100644 --- a/src/cmd/godoc/Makefile +++ b/src/cmd/godoc/Makefile @@ -7,6 +7,7 @@ include ../../Make.inc TARG=godoc GOFILES=\ codewalk.go\ + dirtrees.go\ godoc.go\ index.go\ main.go\ diff --git a/src/cmd/godoc/dirtrees.go b/src/cmd/godoc/dirtrees.go new file mode 100644 index 0000000000..937d1047fc --- /dev/null +++ b/src/cmd/godoc/dirtrees.go @@ -0,0 +1,301 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains the code dealing with package directory trees. + +package main + +import ( + "go/doc" + "go/parser" + "io/ioutil" + "os" + pathutil "path" + "strings" + "unicode" +) + + +type Directory struct { + Depth int + Path string // includes Name + Name string + Text string // package documentation, if any + Dirs []*Directory // subdirectories +} + + +func isGoFile(f *os.FileInfo) bool { + return f.IsRegular() && + !strings.HasPrefix(f.Name, ".") && // ignore .files + pathutil.Ext(f.Name) == ".go" +} + + +func isPkgFile(f *os.FileInfo) bool { + return isGoFile(f) && + !strings.HasSuffix(f.Name, "_test.go") // ignore test files +} + + +func isPkgDir(f *os.FileInfo) bool { + return f.IsDirectory() && len(f.Name) > 0 && f.Name[0] != '_' +} + + +func firstSentence(s string) string { + i := -1 // index+1 of first terminator (punctuation ending a sentence) + j := -1 // index+1 of first terminator followed by white space + prev := 'A' + for k, ch := range s { + k1 := k + 1 + if ch == '.' || ch == '!' || ch == '?' { + if i < 0 { + i = k1 // first terminator + } + if k1 < len(s) && s[k1] <= ' ' { + if j < 0 { + j = k1 // first terminator followed by white space + } + if !unicode.IsUpper(prev) { + j = k1 + break + } + } + } + prev = ch + } + + if j < 0 { + // use the next best terminator + j = i + if j < 0 { + // no terminator at all, use the entire string + j = len(s) + } + } + + return s[0:j] +} + + +func newDirTree(path, name string, depth, maxDepth int) *Directory { + if depth >= maxDepth { + // return a dummy directory so that the parent directory + // doesn't get discarded just because we reached the max + // directory depth + return &Directory{depth, path, name, "", nil} + } + + list, _ := ioutil.ReadDir(path) // ignore errors + + // determine number of subdirectories and package files + ndirs := 0 + nfiles := 0 + var synopses [4]string // prioritized package documentation (0 == highest priority) + for _, d := range list { + switch { + case isPkgDir(d): + ndirs++ + case isPkgFile(d): + nfiles++ + if synopses[0] == "" { + // no "optimal" package synopsis yet; continue to collect synopses + file, err := parser.ParseFile(pathutil.Join(path, d.Name), nil, + parser.ParseComments|parser.PackageClauseOnly) + if err == nil && file.Doc != nil { + // prioritize documentation + i := -1 + switch file.Name.Name { + case name: + i = 0 // normal case: directory name matches package name + case fakePkgName: + i = 1 // synopses for commands + case "main": + i = 2 // directory contains a main package + default: + i = 3 // none of the above + } + if 0 <= i && i < len(synopses) && synopses[i] == "" { + synopses[i] = firstSentence(doc.CommentText(file.Doc)) + } + } + } + } + } + + // create subdirectory tree + var dirs []*Directory + if ndirs > 0 { + dirs = make([]*Directory, ndirs) + i := 0 + for _, d := range list { + if isPkgDir(d) { + dd := newDirTree(pathutil.Join(path, d.Name), d.Name, depth+1, maxDepth) + if dd != nil { + dirs[i] = dd + i++ + } + } + } + dirs = dirs[0:i] + } + + // if there are no package files and no subdirectories + // (with package files), ignore the directory + if nfiles == 0 && len(dirs) == 0 { + return nil + } + + // select the highest-priority synopsis for the directory entry, if any + synopsis := "" + for _, synopsis = range synopses { + if synopsis != "" { + break + } + } + + return &Directory{depth, path, name, synopsis, dirs} +} + + +// newDirectory creates a new package directory tree with at most maxDepth +// levels, anchored at root. The result tree is pruned such that it only +// contains directories that contain package files or that contain +// subdirectories containing package files (transitively). If maxDepth is +// too shallow, the leaf nodes are assumed to contain package files even if +// their contents are not known (i.e., in this case the tree may contain +// directories w/o any package files). +// +func newDirectory(root string, maxDepth int) *Directory { + d, err := os.Lstat(root) + if err != nil || !isPkgDir(d) { + return nil + } + return newDirTree(root, d.Name, 0, maxDepth) +} + + +func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) { + if dir != nil { + if !skipRoot { + c <- dir + } + for _, d := range dir.Dirs { + d.walk(c, false) + } + } +} + + +func (dir *Directory) iter(skipRoot bool) <-chan *Directory { + c := make(chan *Directory) + go func() { + dir.walk(c, skipRoot) + close(c) + }() + return c +} + + +func (dir *Directory) lookupLocal(name string) *Directory { + for _, d := range dir.Dirs { + if d.Name == name { + return d + } + } + return nil +} + + +// lookup looks for the *Directory for a given path, relative to dir. +func (dir *Directory) lookup(path string) *Directory { + d := strings.Split(dir.Path, "/", -1) + p := strings.Split(path, "/", -1) + i := 0 + for i < len(d) { + if i >= len(p) || d[i] != p[i] { + return nil + } + i++ + } + for dir != nil && i < len(p) { + dir = dir.lookupLocal(p[i]) + i++ + } + return dir +} + + +// DirEntry describes a directory entry. The Depth and Height values +// are useful for presenting an entry in an indented fashion. +// +type DirEntry struct { + Depth int // >= 0 + Height int // = DirList.MaxHeight - Depth, > 0 + Path string // includes Name, relative to DirList root + Name string + Synopsis string +} + + +type DirList struct { + MaxHeight int // directory tree height, > 0 + List []DirEntry +} + + +// listing creates a (linear) directory listing from a directory tree. +// If skipRoot is set, the root directory itself is excluded from the list. +// +func (root *Directory) listing(skipRoot bool) *DirList { + if root == nil { + return nil + } + + // determine number of entries n and maximum height + n := 0 + minDepth := 1 << 30 // infinity + maxDepth := 0 + for d := range root.iter(skipRoot) { + n++ + if minDepth > d.Depth { + minDepth = d.Depth + } + if maxDepth < d.Depth { + maxDepth = d.Depth + } + } + maxHeight := maxDepth - minDepth + 1 + + if n == 0 { + return nil + } + + // create list + list := make([]DirEntry, n) + i := 0 + for d := range root.iter(skipRoot) { + p := &list[i] + p.Depth = d.Depth - minDepth + p.Height = maxHeight - p.Depth + // the path is relative to root.Path - remove the root.Path + // prefix (the prefix should always be present but avoid + // crashes and check) + path := d.Path + if strings.HasPrefix(d.Path, root.Path) { + path = d.Path[len(root.Path):] + } + // remove trailing '/' if any - path must be relative + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + p.Path = path + p.Name = d.Name + p.Synopsis = d.Text + i++ + } + + return &DirList{maxHeight, list} +} diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go index a6b9acc707..b1456bb812 100644 --- a/src/cmd/godoc/godoc.go +++ b/src/cmd/godoc/godoc.go @@ -25,7 +25,6 @@ import ( "sync" "template" "time" - "unicode" "utf8" ) @@ -115,70 +114,7 @@ func registerPublicHandlers(mux *http.ServeMux) { // ---------------------------------------------------------------------------- -// Predicates and small utility functions - -func isGoFile(f *os.FileInfo) bool { - return f.IsRegular() && - !strings.HasPrefix(f.Name, ".") && // ignore .files - pathutil.Ext(f.Name) == ".go" -} - - -func isPkgFile(f *os.FileInfo) bool { - return isGoFile(f) && - !strings.HasSuffix(f.Name, "_test.go") // ignore test files -} - - -func isPkgDir(f *os.FileInfo) bool { - return f.IsDirectory() && len(f.Name) > 0 && f.Name[0] != '_' -} - - -func pkgName(filename string) string { - file, err := parser.ParseFile(filename, nil, parser.PackageClauseOnly) - if err != nil || file == nil { - return "" - } - return file.Name.Name -} - - -func firstSentence(s string) string { - i := -1 // index+1 of first terminator (punctuation ending a sentence) - j := -1 // index+1 of first terminator followed by white space - prev := 'A' - for k, ch := range s { - k1 := k + 1 - if ch == '.' || ch == '!' || ch == '?' { - if i < 0 { - i = k1 // first terminator - } - if k1 < len(s) && s[k1] <= ' ' { - if j < 0 { - j = k1 // first terminator followed by white space - } - if !unicode.IsUpper(prev) { - j = k1 - break - } - } - } - prev = ch - } - - if j < 0 { - // use the next best terminator - j = i - if j < 0 { - // no terminator at all, use the entire string - j = len(s) - } - } - - return s[0:j] -} - +// Path mapping func absolutePath(path, defaultRoot string) string { abspath := fsMap.ToAbsolute(path) @@ -205,239 +141,6 @@ func relativePath(path string) string { } -// ---------------------------------------------------------------------------- -// Package directories - -type Directory struct { - Depth int - Path string // includes Name - Name string - Text string // package documentation, if any - Dirs []*Directory // subdirectories -} - - -func newDirTree(path, name string, depth, maxDepth int) *Directory { - if depth >= maxDepth { - // return a dummy directory so that the parent directory - // doesn't get discarded just because we reached the max - // directory depth - return &Directory{depth, path, name, "", nil} - } - - list, _ := ioutil.ReadDir(path) // ignore errors - - // determine number of subdirectories and package files - ndirs := 0 - nfiles := 0 - var synopses [4]string // prioritized package documentation (0 == highest priority) - for _, d := range list { - switch { - case isPkgDir(d): - ndirs++ - case isPkgFile(d): - nfiles++ - if synopses[0] == "" { - // no "optimal" package synopsis yet; continue to collect synopses - file, err := parser.ParseFile(pathutil.Join(path, d.Name), nil, - parser.ParseComments|parser.PackageClauseOnly) - if err == nil && file.Doc != nil { - // prioritize documentation - i := -1 - switch file.Name.Name { - case name: - i = 0 // normal case: directory name matches package name - case fakePkgName: - i = 1 // synopses for commands - case "main": - i = 2 // directory contains a main package - default: - i = 3 // none of the above - } - if 0 <= i && i < len(synopses) && synopses[i] == "" { - synopses[i] = firstSentence(doc.CommentText(file.Doc)) - } - } - } - } - } - - // create subdirectory tree - var dirs []*Directory - if ndirs > 0 { - dirs = make([]*Directory, ndirs) - i := 0 - for _, d := range list { - if isPkgDir(d) { - dd := newDirTree(pathutil.Join(path, d.Name), d.Name, depth+1, maxDepth) - if dd != nil { - dirs[i] = dd - i++ - } - } - } - dirs = dirs[0:i] - } - - // if there are no package files and no subdirectories - // (with package files), ignore the directory - if nfiles == 0 && len(dirs) == 0 { - return nil - } - - // select the highest-priority synopsis for the directory entry, if any - synopsis := "" - for _, synopsis = range synopses { - if synopsis != "" { - break - } - } - - return &Directory{depth, path, name, synopsis, dirs} -} - - -// newDirectory creates a new package directory tree with at most maxDepth -// levels, anchored at root. The result tree is pruned such that it only -// contains directories that contain package files or that contain -// subdirectories containing package files (transitively). If maxDepth is -// too shallow, the leaf nodes are assumed to contain package files even if -// their contents are not known (i.e., in this case the tree may contain -// directories w/o any package files). -// -func newDirectory(root string, maxDepth int) *Directory { - d, err := os.Lstat(root) - if err != nil || !isPkgDir(d) { - return nil - } - return newDirTree(root, d.Name, 0, maxDepth) -} - - -func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) { - if dir != nil { - if !skipRoot { - c <- dir - } - for _, d := range dir.Dirs { - d.walk(c, false) - } - } -} - - -func (dir *Directory) iter(skipRoot bool) <-chan *Directory { - c := make(chan *Directory) - go func() { - dir.walk(c, skipRoot) - close(c) - }() - return c -} - - -func (dir *Directory) lookupLocal(name string) *Directory { - for _, d := range dir.Dirs { - if d.Name == name { - return d - } - } - return nil -} - - -// lookup looks for the *Directory for a given path, relative to dir. -func (dir *Directory) lookup(path string) *Directory { - d := strings.Split(dir.Path, "/", -1) - p := strings.Split(path, "/", -1) - i := 0 - for i < len(d) { - if i >= len(p) || d[i] != p[i] { - return nil - } - i++ - } - for dir != nil && i < len(p) { - dir = dir.lookupLocal(p[i]) - i++ - } - return dir -} - - -// DirEntry describes a directory entry. The Depth and Height values -// are useful for presenting an entry in an indented fashion. -// -type DirEntry struct { - Depth int // >= 0 - Height int // = DirList.MaxHeight - Depth, > 0 - Path string // includes Name, relative to DirList root - Name string - Synopsis string -} - - -type DirList struct { - MaxHeight int // directory tree height, > 0 - List []DirEntry -} - - -// listing creates a (linear) directory listing from a directory tree. -// If skipRoot is set, the root directory itself is excluded from the list. -// -func (root *Directory) listing(skipRoot bool) *DirList { - if root == nil { - return nil - } - - // determine number of entries n and maximum height - n := 0 - minDepth := 1 << 30 // infinity - maxDepth := 0 - for d := range root.iter(skipRoot) { - n++ - if minDepth > d.Depth { - minDepth = d.Depth - } - if maxDepth < d.Depth { - maxDepth = d.Depth - } - } - maxHeight := maxDepth - minDepth + 1 - - if n == 0 { - return nil - } - - // create list - list := make([]DirEntry, n) - i := 0 - for d := range root.iter(skipRoot) { - p := &list[i] - p.Depth = d.Depth - minDepth - p.Height = maxHeight - p.Depth - // the path is relative to root.Path - remove the root.Path - // prefix (the prefix should always be present but avoid - // crashes and check) - path := d.Path - if strings.HasPrefix(d.Path, root.Path) { - path = d.Path[len(root.Path):] - } - // remove trailing '/' if any - path must be relative - if len(path) > 0 && path[0] == '/' { - path = path[1:] - } - p.Path = path - p.Name = d.Name - p.Synopsis = d.Text - i++ - } - - return &DirList{maxHeight, list} -} - - // ---------------------------------------------------------------------------- // HTML formatting support diff --git a/src/cmd/godoc/index.go b/src/cmd/godoc/index.go index d5d8e3e360..c21c8bda01 100644 --- a/src/cmd/godoc/index.go +++ b/src/cmd/godoc/index.go @@ -583,6 +583,15 @@ func (x *Indexer) VisitDir(path string, f *os.FileInfo) bool { } +func pkgName(filename string) string { + file, err := parser.ParseFile(filename, nil, parser.PackageClauseOnly) + if err != nil || file == nil { + return "" + } + return file.Name.Name +} + + func (x *Indexer) VisitFile(path string, f *os.FileInfo) { if !isGoFile(f) { return