Commit 889377c5 authored by Robert Griesemer's avatar Robert Griesemer

godoc: moved package directory support code into separate file

- in prep. for some restructuring to be able to better deal
  with very large file systems
- moved a utility function into index.go
- no functionality changes, only code reorg.

R=r, adg1
CC=golang-dev
https://golang.org/cl/2098044
parent 9e5bf27a
......@@ -7,6 +7,7 @@ include ../../Make.inc
TARG=godoc
GOFILES=\
codewalk.go\
dirtrees.go\
godoc.go\
index.go\
main.go\
......
// 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}
}
......@@ -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
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment