Skip to content
This repository has been archived by the owner on Jul 29, 2021. It is now read-only.

implements godoc.CommandLine as writeOutput #26

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 205 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"io"
"io/ioutil"
"log"
"os"
"path"
pathpkg "path"
"path/filepath"
"regexp"
"runtime"
"strings"
"text/template"
Expand Down Expand Up @@ -51,6 +54,14 @@ var (
srcLinkFormat = flag.String("srclink", "", "if set, format for entire source link")
)

const (
targetPath = "/target"
cmdPathPrefix = "cmd/"
srcPathPrefix = "src/"
toolsPath = "golang.org/x/tools/cmd/"
builtinPkgPath = "builtin"
)

func usage() {
fmt.Fprintf(os.Stderr,
"usage: godoc2md package [name ...]\n")
Expand All @@ -64,7 +75,7 @@ var (

funcs = map[string]interface{}{
"comment_md": commentMdFunc,
"base": path.Base,
"base": pathpkg.Base,
"md": mdFunc,
"pre": preFunc,
"kebab": kebabFunc,
Expand All @@ -91,7 +102,7 @@ func preFunc(text string) string {

// Original Source https://github.com/golang/tools/blob/master/godoc/godoc.go#L562
func srcLinkFunc(s string) string {
s = path.Clean("/" + s)
s = pathpkg.Clean("/" + s)
if !strings.HasPrefix(s, "/src/") {
s = "/src" + s
}
Expand Down Expand Up @@ -170,23 +181,210 @@ func main() {
pres.TabWidth = *tabWidth
pres.ShowTimestamps = *showTimestamps
pres.ShowPlayground = *showPlayground
pres.ShowExamples = *showExamples
pres.DeclLinks = *declLinks
pres.SrcMode = false
pres.HTMLMode = false
pres.URLForSrcPos = srcPosLinkFunc

var tmpl *template.Template

if *altPkgTemplate != "" {
buf, err := ioutil.ReadFile(*altPkgTemplate)
if err != nil {
log.Fatal(err)
}
pres.PackageText = readTemplate("package.txt", string(buf))
tmpl = readTemplate("package.txt", string(buf))
} else {
pres.PackageText = readTemplate("package.txt", pkgTemplate)
tmpl = readTemplate("package.txt", pkgTemplate)
}

if err := godoc.CommandLine(os.Stdout, fs, pres, flag.Args()); err != nil {
if err := writeOutput(os.Stdout, fs, pres, flag.Args(), tmpl); err != nil {
log.Print(err)
}
}

// writeOutpur returns godoc results to w.
// Note that it may add a /target path to fs.
func writeOutput(w io.Writer, fs vfs.NameSpace, pres *godoc.Presentation, args []string, packageText *template.Template) error {
path := args[0]
srcMode := pres.SrcMode
cmdMode := strings.HasPrefix(path, cmdPathPrefix)
if strings.HasPrefix(path, srcPathPrefix) {
path = strings.TrimPrefix(path, srcPathPrefix)
srcMode = true
}
var abspath, relpath string
if cmdMode {
path = strings.TrimPrefix(path, cmdPathPrefix)
} else {
abspath, relpath = paths(fs, pres, path)
}

var mode godoc.PageInfoMode
if relpath == builtinPkgPath {
// the fake built-in package contains unexported identifiers
mode = godoc.NoFiltering | godoc.NoTypeAssoc
}
if pres.AllMode {
mode |= godoc.NoFiltering
}
if srcMode {
// only filter exports if we don't have explicit command-line filter arguments
if len(args) > 1 {
mode |= godoc.NoFiltering
}
mode |= godoc.ShowSource
}

// First, try as package unless forced as command.
var info *godoc.PageInfo
if !cmdMode {
info = pres.GetPkgPageInfo(abspath, relpath, mode)
}

// Second, try as command (if the path is not absolute).
var cinfo *godoc.PageInfo
if !filepath.IsAbs(path) {
// First try go.tools/cmd.
abspath = pathpkg.Join(pres.PkgFSRoot(), toolsPath+path)
cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
if cinfo.IsEmpty() {
// Then try $GOROOT/src/cmd.
abspath = pathpkg.Join(pres.CmdFSRoot(), cmdPathPrefix, path)
cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
}
}

// determine what to use
if info == nil || info.IsEmpty() {
if cinfo != nil && !cinfo.IsEmpty() {
// only cinfo exists - switch to cinfo
info = cinfo
}
} else if cinfo != nil && !cinfo.IsEmpty() {
// both info and cinfo exist - use cinfo if info
// contains only subdirectory information
if info.PAst == nil && info.PDoc == nil {
info = cinfo
} else if relpath != targetPath {
// The above check handles the case where an operating system path
// is provided (see documentation for paths below). In that case,
// relpath is set to "/target" (in anticipation of accessing packages there),
// and is therefore not expected to match a command.
fmt.Fprintf(w, "use 'godoc %s%s' for documentation on the %s command \n\n", cmdPathPrefix, relpath, relpath)
}
}

if info == nil {
return fmt.Errorf("%s: no such directory or package", args[0])
}
if info.Err != nil {
return info.Err
}

if info.PDoc != nil && info.PDoc.ImportPath == targetPath {
// Replace virtual /target with actual argument from command line.
info.PDoc.ImportPath = args[0]
}

// If we have more than one argument, use the remaining arguments for filtering.
if len(args) > 1 {
info.IsFiltered = true
filterInfo(args[1:], info)
}

if err := packageText.Execute(w, info); err != nil {
return err
}
return nil
}

// paths determines the paths to use.
//
// If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc,
// we need to map that path somewhere in the fs name space so that routines
// like getPageInfo will see it. We use the arbitrarily-chosen virtual path "/target"
// for this. That is, if we get passed a directory like the above, we map that
// directory so that getPageInfo sees it as /target.
// Returns the absolute and relative paths.
func paths(fs vfs.NameSpace, pres *godoc.Presentation, path string) (abspath, relpath string) {
if filepath.IsAbs(path) {
fs.Bind(targetPath, vfs.OS(path), "/", vfs.BindReplace)
return targetPath, targetPath
}
if build.IsLocalImport(path) {
cwd, err := os.Getwd()
if err != nil {
log.Printf("error while getting working directory: %v", err)
}
path = filepath.Join(cwd, path)
fs.Bind(targetPath, vfs.OS(path), "/", vfs.BindReplace)
return targetPath, targetPath
}
bp, err := build.Import(path, "", build.FindOnly)
if err != nil {
log.Printf("error while importing build package: %v", err)
}
if bp.Dir != "" && bp.ImportPath != "" {
fs.Bind(targetPath, vfs.OS(bp.Dir), "/", vfs.BindReplace)
return targetPath, bp.ImportPath
}
return pathpkg.Join(pres.PkgFSRoot(), path), path
}

// filterInfo updates info to include only the nodes that match the given
// filter args.
func filterInfo(args []string, info *godoc.PageInfo) {
rx, err := makeRx(args)
if err != nil {
log.Fatalf("illegal regular expression from %v: %v", args, err)
}

filter := func(s string) bool { return rx.MatchString(s) }
switch {
case info.PAst != nil:
newPAst := map[string]*ast.File{}
for name, a := range info.PAst {
cmap := ast.NewCommentMap(info.FSet, a, a.Comments)
a.Comments = []*ast.CommentGroup{} // remove all comments.
ast.FilterFile(a, filter)
if len(a.Decls) > 0 {
newPAst[name] = a
}
for _, d := range a.Decls {
// add back the comments associated with d only
comments := cmap.Filter(d).Comments()
a.Comments = append(a.Comments, comments...)
}
}
info.PAst = newPAst // add only matching files.
case info.PDoc != nil:
info.PDoc.Filter(filter)
}
}

// Does s look like a regular expression?
func isRegexp(s string) bool {
return strings.ContainsAny(s, ".(|)*+?^$[]")
}

// Make a regular expression of the form
// names[0]|names[1]|...names[len(names)-1].
// Returns an error if the regular expression is illegal.
func makeRx(names []string) (*regexp.Regexp, error) {
if len(names) == 0 {
return nil, fmt.Errorf("no expression provided")
}
s := ""
for i, name := range names {
if i > 0 {
s += "|"
}
if isRegexp(name) {
s += name
} else {
s += "^" + name + "$" // must match exactly
}
}
return regexp.Compile(s)
}