github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/website/godoc.go (about)

     1  /*
     2  Copyright 2013 The Camlistore Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // This is a hacked-up version of godoc.
    18  
    19  package main
    20  
    21  import (
    22  	"bytes"
    23  	"errors"
    24  	"fmt"
    25  	"go/ast"
    26  	"go/build"
    27  	"go/doc"
    28  	"go/parser"
    29  	"go/printer"
    30  	"go/token"
    31  	"io"
    32  	"io/ioutil"
    33  	"log"
    34  	"net/http"
    35  	"os"
    36  	pathpkg "path"
    37  	"path/filepath"
    38  	"regexp"
    39  	"strings"
    40  	"text/template"
    41  	"time"
    42  )
    43  
    44  const (
    45  	domainName       = "camlistore.org"
    46  	pkgPattern       = "/pkg/"
    47  	cmdPattern       = "/cmd/"
    48  	fileembedPattern = "fileembed.go"
    49  )
    50  
    51  var docRx = regexp.MustCompile(`^/((?:pkg|cmd)/([\w/]+?)(\.go)??)/?$`)
    52  
    53  var tabwidth = 4
    54  
    55  type PageInfo struct {
    56  	Dirname string // directory containing the package
    57  	Err     error  // error or nil
    58  
    59  	// package info
    60  	FSet     *token.FileSet // nil if no package documentation
    61  	PDoc     *doc.Package   // nil if no package documentation
    62  	Examples []*doc.Example // nil if no example code
    63  	PAst     *ast.File      // nil if no AST with package exports
    64  	IsPkg    bool           // true for pkg, false for cmd
    65  
    66  	// directory info
    67  	Dirs    *DirList  // nil if no directory information
    68  	DirTime time.Time // directory time stamp
    69  	DirFlat bool      // if set, show directory in a flat (non-indented) manner
    70  	PList   []string  // list of package names found
    71  }
    72  
    73  // godocFmap describes the template functions installed with all godoc templates.
    74  // Convention: template function names ending in "_html" or "_url" produce
    75  //             HTML- or URL-escaped strings; all other function results may
    76  //             require explicit escaping in the template.
    77  var godocFmap = template.FuncMap{
    78  	// various helpers
    79  	"filename": filenameFunc,
    80  	"repeat":   strings.Repeat,
    81  
    82  	// accss to FileInfos (directory listings)
    83  	"fileInfoName": fileInfoNameFunc,
    84  	"fileInfoTime": fileInfoTimeFunc,
    85  
    86  	// access to search result information
    87  	//"infoKind_html":    infoKind_htmlFunc,
    88  	//"infoLine":         infoLineFunc,
    89  	//"infoSnippet_html": infoSnippet_htmlFunc,
    90  
    91  	// formatting of AST nodes
    92  	"node":         nodeFunc,
    93  	"node_html":    node_htmlFunc,
    94  	"comment_html": comment_htmlFunc,
    95  	//"comment_text": comment_textFunc,
    96  
    97  	// support for URL attributes
    98  	"srcLink":     srcLinkFunc,
    99  	"posLink_url": posLink_urlFunc,
   100  
   101  	// formatting of Examples
   102  	"example_html":   example_htmlFunc,
   103  	"example_name":   example_nameFunc,
   104  	"example_suffix": example_suffixFunc,
   105  }
   106  
   107  func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.FileSet) string {
   108  	return ""
   109  }
   110  
   111  func example_nameFunc(s string) string {
   112  	return ""
   113  }
   114  
   115  func example_suffixFunc(name string) string {
   116  	return ""
   117  }
   118  
   119  func filenameFunc(path string) string {
   120  	_, localname := pathpkg.Split(path)
   121  	return localname
   122  }
   123  
   124  func fileInfoNameFunc(fi os.FileInfo) string {
   125  	name := fi.Name()
   126  	if fi.IsDir() {
   127  		name += "/"
   128  	}
   129  	return name
   130  }
   131  
   132  func fileInfoTimeFunc(fi os.FileInfo) string {
   133  	if t := fi.ModTime(); t.Unix() != 0 {
   134  		return t.Local().String()
   135  	}
   136  	return "" // don't return epoch if time is obviously not set
   137  }
   138  
   139  // Write an AST node to w.
   140  func writeNode(w io.Writer, fset *token.FileSet, x interface{}) {
   141  	// convert trailing tabs into spaces using a tconv filter
   142  	// to ensure a good outcome in most browsers (there may still
   143  	// be tabs in comments and strings, but converting those into
   144  	// the right number of spaces is much harder)
   145  	//
   146  	// TODO(gri) rethink printer flags - perhaps tconv can be eliminated
   147  	//           with an another printer mode (which is more efficiently
   148  	//           implemented in the printer than here with another layer)
   149  	mode := printer.TabIndent | printer.UseSpaces
   150  	err := (&printer.Config{Mode: mode, Tabwidth: tabwidth}).Fprint(&tconv{output: w}, fset, x)
   151  	if err != nil {
   152  		log.Print(err)
   153  	}
   154  }
   155  
   156  func nodeFunc(node interface{}, fset *token.FileSet) string {
   157  	var buf bytes.Buffer
   158  	writeNode(&buf, fset, node)
   159  	return buf.String()
   160  }
   161  
   162  func node_htmlFunc(node interface{}, fset *token.FileSet) string {
   163  	var buf1 bytes.Buffer
   164  	writeNode(&buf1, fset, node)
   165  	var buf2 bytes.Buffer
   166  	FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
   167  	return buf2.String()
   168  }
   169  
   170  func comment_htmlFunc(comment string) string {
   171  	var buf bytes.Buffer
   172  	// TODO(gri) Provide list of words (e.g. function parameters)
   173  	//           to be emphasized by ToHTML.
   174  	doc.ToHTML(&buf, comment, nil) // does html-escaping
   175  	return buf.String()
   176  }
   177  
   178  func posLink_urlFunc(node ast.Node, fset *token.FileSet) string {
   179  	var relpath string
   180  	var line int
   181  	var low, high int // selection
   182  
   183  	if p := node.Pos(); p.IsValid() {
   184  		pos := fset.Position(p)
   185  		idx := strings.LastIndex(pos.Filename, domainName)
   186  		if idx == -1 {
   187  			log.Fatalf("No \"%s\" in path to file %s", domainName, pos.Filename)
   188  		}
   189  		relpath = pathpkg.Clean(pos.Filename[idx+len(domainName):])
   190  		line = pos.Line
   191  		low = pos.Offset
   192  	}
   193  	if p := node.End(); p.IsValid() {
   194  		high = fset.Position(p).Offset
   195  	}
   196  
   197  	var buf bytes.Buffer
   198  	template.HTMLEscape(&buf, []byte(relpath))
   199  	// selection ranges are of form "s=low:high"
   200  	if low < high {
   201  		fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
   202  		// if we have a selection, position the page
   203  		// such that the selection is a bit below the top
   204  		line -= 10
   205  		if line < 1 {
   206  			line = 1
   207  		}
   208  	}
   209  	// line id's in html-printed source are of the
   210  	// form "L%d" where %d stands for the line number
   211  	if line > 0 {
   212  		fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
   213  	}
   214  
   215  	return buf.String()
   216  }
   217  
   218  func srcLinkFunc(s string) string {
   219  	idx := strings.LastIndex(s, domainName)
   220  	if idx == -1 {
   221  		log.Fatalf("No \"%s\" in path to file %s", domainName, s)
   222  	}
   223  	return pathpkg.Clean(s[idx+len(domainName):])
   224  }
   225  
   226  func (pi *PageInfo) populateDirs(diskPath string, depth int) {
   227  	var dir *Directory
   228  	dir = newDirectory(diskPath, depth)
   229  	pi.Dirs = dir.listing(true)
   230  	pi.DirTime = time.Now()
   231  }
   232  
   233  func getPageInfo(pkgName, diskPath string) (pi PageInfo, err error) {
   234  	if pkgName == pathpkg.Join(domainName, pkgPattern) ||
   235  		pkgName == pathpkg.Join(domainName, cmdPattern) {
   236  		pi.Dirname = diskPath
   237  		pi.populateDirs(diskPath, -1)
   238  		return
   239  	}
   240  	bpkg, err := build.ImportDir(diskPath, 0)
   241  	if err != nil {
   242  		if _, ok := err.(*build.NoGoError); ok {
   243  			pi.populateDirs(diskPath, -1)
   244  			return pi, nil
   245  		}
   246  		return
   247  	}
   248  	inSet := make(map[string]bool)
   249  	for _, name := range bpkg.GoFiles {
   250  		if name == fileembedPattern {
   251  			continue
   252  		}
   253  		inSet[filepath.Base(name)] = true
   254  	}
   255  
   256  	pi.FSet = token.NewFileSet()
   257  	filter := func(fi os.FileInfo) bool {
   258  		return inSet[fi.Name()]
   259  	}
   260  	aPkgMap, err := parser.ParseDir(pi.FSet, diskPath, filter, parser.ParseComments)
   261  	if err != nil {
   262  		return
   263  	}
   264  	aPkg := aPkgMap[pathpkg.Base(pkgName)]
   265  	if aPkg == nil {
   266  		for _, v := range aPkgMap {
   267  			aPkg = v
   268  			break
   269  		}
   270  		if aPkg == nil {
   271  			err = errors.New("no apkg found?")
   272  			return
   273  		}
   274  	}
   275  
   276  	pi.Dirname = diskPath
   277  	pi.PDoc = doc.New(aPkg, pkgName, 0)
   278  	pi.IsPkg = strings.Contains(pkgName, domainName+pkgPattern)
   279  
   280  	// get directory information
   281  	pi.populateDirs(diskPath, -1)
   282  	return
   283  }
   284  
   285  const (
   286  	indenting = iota
   287  	collecting
   288  )
   289  
   290  // A tconv is an io.Writer filter for converting leading tabs into spaces.
   291  type tconv struct {
   292  	output io.Writer
   293  	state  int // indenting or collecting
   294  	indent int // valid if state == indenting
   295  }
   296  
   297  var spaces = []byte("                                ") // 32 spaces seems like a good number
   298  
   299  func (p *tconv) writeIndent() (err error) {
   300  	i := p.indent
   301  	for i >= len(spaces) {
   302  		i -= len(spaces)
   303  		if _, err = p.output.Write(spaces); err != nil {
   304  			return
   305  		}
   306  	}
   307  	// i < len(spaces)
   308  	if i > 0 {
   309  		_, err = p.output.Write(spaces[0:i])
   310  	}
   311  	return
   312  }
   313  
   314  func (p *tconv) Write(data []byte) (n int, err error) {
   315  	if len(data) == 0 {
   316  		return
   317  	}
   318  	pos := 0 // valid if p.state == collecting
   319  	var b byte
   320  	for n, b = range data {
   321  		switch p.state {
   322  		case indenting:
   323  			switch b {
   324  			case '\t':
   325  				p.indent += tabwidth
   326  			case '\n':
   327  				p.indent = 0
   328  				if _, err = p.output.Write(data[n : n+1]); err != nil {
   329  					return
   330  				}
   331  			case ' ':
   332  				p.indent++
   333  			default:
   334  				p.state = collecting
   335  				pos = n
   336  				if err = p.writeIndent(); err != nil {
   337  					return
   338  				}
   339  			}
   340  		case collecting:
   341  			if b == '\n' {
   342  				p.state = indenting
   343  				p.indent = 0
   344  				if _, err = p.output.Write(data[pos : n+1]); err != nil {
   345  					return
   346  				}
   347  			}
   348  		}
   349  	}
   350  	n = len(data)
   351  	if pos < n && p.state == collecting {
   352  		_, err = p.output.Write(data[pos:])
   353  	}
   354  	return
   355  }
   356  
   357  func readTextTemplate(name string) *template.Template {
   358  	fileName := filepath.Join(*root, "tmpl", name)
   359  	data, err := ioutil.ReadFile(fileName)
   360  	if err != nil {
   361  		log.Fatalf("ReadFile %s: %v", fileName, err)
   362  	}
   363  	t, err := template.New(name).Funcs(godocFmap).Parse(string(data))
   364  	if err != nil {
   365  		log.Fatalf("%s: %v", fileName, err)
   366  	}
   367  	return t
   368  }
   369  
   370  func applyTextTemplate(t *template.Template, name string, data interface{}) []byte {
   371  	var buf bytes.Buffer
   372  	if err := t.Execute(&buf, data); err != nil {
   373  		log.Printf("%s.Execute: %s", name, err)
   374  	}
   375  	return buf.Bytes()
   376  }
   377  
   378  func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
   379  	src, err := ioutil.ReadFile(abspath)
   380  	if err != nil {
   381  		log.Printf("ReadFile: %s", err)
   382  		serveError(w, r, relpath, err)
   383  		return
   384  	}
   385  
   386  	var buf bytes.Buffer
   387  	buf.WriteString("<pre>")
   388  	FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s")))
   389  	buf.WriteString("</pre>")
   390  
   391  	servePage(w, title, "", buf.Bytes())
   392  }
   393  
   394  type godocHandler struct{}
   395  
   396  func (godocHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   397  	m := docRx.FindStringSubmatch(r.URL.Path)
   398  	suffix := ""
   399  	if m == nil {
   400  		if r.URL.Path != pkgPattern && r.URL.Path != cmdPattern {
   401  			http.NotFound(w, r)
   402  			return
   403  		}
   404  		suffix = r.URL.Path
   405  	} else {
   406  		suffix = m[1]
   407  	}
   408  	diskPath := filepath.Join(*root, "..", suffix)
   409  
   410  	switch pathpkg.Ext(suffix) {
   411  	case ".go":
   412  		serveTextFile(w, r, diskPath, suffix, "Source file")
   413  		return
   414  	}
   415  
   416  	pkgName := pathpkg.Join(domainName, suffix)
   417  	pi, err := getPageInfo(pkgName, diskPath)
   418  	if err != nil {
   419  		log.Print(err)
   420  		return
   421  	}
   422  
   423  	subtitle := pathpkg.Base(diskPath)
   424  	title := subtitle + " (" + pkgName + ")"
   425  	servePage(w, title, subtitle, applyTextTemplate(packageHTML, "packageHTML", pi))
   426  }