github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/godoc/server.go (about)

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package godoc
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"go/ast"
    12  	"go/build"
    13  	"go/doc"
    14  	"go/token"
    15  	htmlpkg "html"
    16  	htmltemplate "html/template"
    17  	"io"
    18  	"io/ioutil"
    19  	"log"
    20  	"net/http"
    21  	"os"
    22  	pathpkg "path"
    23  	"path/filepath"
    24  	"sort"
    25  	"strings"
    26  	"text/template"
    27  	"time"
    28  
    29  	"golang.org/x/tools/godoc/analysis"
    30  	"golang.org/x/tools/godoc/util"
    31  	"golang.org/x/tools/godoc/vfs"
    32  )
    33  
    34  // handlerServer is a migration from an old godoc http Handler type.
    35  // This should probably merge into something else.
    36  type handlerServer struct {
    37  	p           *Presentation
    38  	c           *Corpus  // copy of p.Corpus
    39  	pattern     string   // url pattern; e.g. "/pkg/"
    40  	stripPrefix string   // prefix to strip from import path; e.g. "pkg/"
    41  	fsRoot      string   // file system root to which the pattern is mapped; e.g. "/src"
    42  	exclude     []string // file system paths to exclude; e.g. "/src/cmd"
    43  }
    44  
    45  func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
    46  	mux.Handle(s.pattern, s)
    47  }
    48  
    49  // getPageInfo returns the PageInfo for a package directory abspath. If the
    50  // parameter genAST is set, an AST containing only the package exports is
    51  // computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
    52  // is extracted from the AST. If there is no corresponding package in the
    53  // directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
    54  // directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
    55  // set to the respective error but the error is not logged.
    56  //
    57  func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
    58  	info := &PageInfo{Dirname: abspath}
    59  
    60  	// Restrict to the package files that would be used when building
    61  	// the package on this system.  This makes sure that if there are
    62  	// separate implementations for, say, Windows vs Unix, we don't
    63  	// jumble them all together.
    64  	// Note: Uses current binary's GOOS/GOARCH.
    65  	// To use different pair, such as if we allowed the user to choose,
    66  	// set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir.
    67  	ctxt := build.Default
    68  	ctxt.IsAbsPath = pathpkg.IsAbs
    69  	ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
    70  		f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
    71  		filtered := make([]os.FileInfo, 0, len(f))
    72  		for _, i := range f {
    73  			if mode&NoFiltering != 0 || i.Name() != "internal" {
    74  				filtered = append(filtered, i)
    75  			}
    76  		}
    77  		return filtered, err
    78  	}
    79  	ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
    80  		data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
    81  		if err != nil {
    82  			return nil, err
    83  		}
    84  		return ioutil.NopCloser(bytes.NewReader(data)), nil
    85  	}
    86  
    87  	pkginfo, err := ctxt.ImportDir(abspath, 0)
    88  	// continue if there are no Go source files; we still want the directory info
    89  	if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
    90  		info.Err = err
    91  		return info
    92  	}
    93  
    94  	// collect package files
    95  	pkgname := pkginfo.Name
    96  	pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
    97  	if len(pkgfiles) == 0 {
    98  		// Commands written in C have no .go files in the build.
    99  		// Instead, documentation may be found in an ignored file.
   100  		// The file may be ignored via an explicit +build ignore
   101  		// constraint (recommended), or by defining the package
   102  		// documentation (historic).
   103  		pkgname = "main" // assume package main since pkginfo.Name == ""
   104  		pkgfiles = pkginfo.IgnoredGoFiles
   105  	}
   106  
   107  	// get package information, if any
   108  	if len(pkgfiles) > 0 {
   109  		// build package AST
   110  		fset := token.NewFileSet()
   111  		files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
   112  		if err != nil {
   113  			info.Err = err
   114  			return info
   115  		}
   116  
   117  		// ignore any errors - they are due to unresolved identifiers
   118  		pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
   119  
   120  		// extract package documentation
   121  		info.FSet = fset
   122  		if mode&ShowSource == 0 {
   123  			// show extracted documentation
   124  			var m doc.Mode
   125  			if mode&NoFiltering != 0 {
   126  				m |= doc.AllDecls
   127  			}
   128  			if mode&AllMethods != 0 {
   129  				m |= doc.AllMethods
   130  			}
   131  			info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
   132  			if mode&NoTypeAssoc != 0 {
   133  				for _, t := range info.PDoc.Types {
   134  					info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
   135  					info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
   136  					info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
   137  					t.Consts = nil
   138  					t.Vars = nil
   139  					t.Funcs = nil
   140  				}
   141  				// for now we cannot easily sort consts and vars since
   142  				// go/doc.Value doesn't export the order information
   143  				sort.Sort(funcsByName(info.PDoc.Funcs))
   144  			}
   145  
   146  			// collect examples
   147  			testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
   148  			files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
   149  			if err != nil {
   150  				log.Println("parsing examples:", err)
   151  			}
   152  			info.Examples = collectExamples(h.c, pkg, files)
   153  
   154  			// collect any notes that we want to show
   155  			if info.PDoc.Notes != nil {
   156  				// could regexp.Compile only once per godoc, but probably not worth it
   157  				if rx := h.p.NotesRx; rx != nil {
   158  					for m, n := range info.PDoc.Notes {
   159  						if rx.MatchString(m) {
   160  							if info.Notes == nil {
   161  								info.Notes = make(map[string][]*doc.Note)
   162  							}
   163  							info.Notes[m] = n
   164  						}
   165  					}
   166  				}
   167  			}
   168  
   169  		} else {
   170  			// show source code
   171  			// TODO(gri) Consider eliminating export filtering in this mode,
   172  			//           or perhaps eliminating the mode altogether.
   173  			if mode&NoFiltering == 0 {
   174  				packageExports(fset, pkg)
   175  			}
   176  			info.PAst = files
   177  		}
   178  		info.IsMain = pkgname == "main"
   179  	}
   180  
   181  	// get directory information, if any
   182  	var dir *Directory
   183  	var timestamp time.Time
   184  	if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
   185  		// directory tree is present; lookup respective directory
   186  		// (may still fail if the file system was updated and the
   187  		// new directory tree has not yet been computed)
   188  		dir = tree.(*Directory).lookup(abspath)
   189  		timestamp = ts
   190  	}
   191  	if dir == nil {
   192  		// no directory tree present (too early after startup or
   193  		// command-line mode); compute one level for this page
   194  		// note: cannot use path filter here because in general
   195  		//       it doesn't contain the FSTree path
   196  		dir = h.c.newDirectory(abspath, 1)
   197  		timestamp = time.Now()
   198  	}
   199  	info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
   200  	info.DirTime = timestamp
   201  	info.DirFlat = mode&FlatDir != 0
   202  
   203  	return info
   204  }
   205  
   206  func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
   207  	// if the path is under one of the exclusion paths, don't list.
   208  	for _, e := range h.exclude {
   209  		if strings.HasPrefix(path, e) {
   210  			return false
   211  		}
   212  	}
   213  
   214  	// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
   215  	if mode&NoFiltering != 0 {
   216  		return true
   217  	}
   218  	if strings.Contains(path, "internal") {
   219  		for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
   220  			if c == "internal" {
   221  				return false
   222  			}
   223  		}
   224  	}
   225  	return true
   226  }
   227  
   228  type funcsByName []*doc.Func
   229  
   230  func (s funcsByName) Len() int           { return len(s) }
   231  func (s funcsByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   232  func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
   233  
   234  func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   235  	if redirect(w, r) {
   236  		return
   237  	}
   238  
   239  	relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
   240  	abspath := pathpkg.Join(h.fsRoot, relpath)
   241  	mode := h.p.GetPageInfoMode(r)
   242  	if relpath == builtinPkgPath {
   243  		mode = NoFiltering | NoTypeAssoc
   244  	}
   245  	info := h.GetPageInfo(abspath, relpath, mode)
   246  	if info.Err != nil {
   247  		log.Print(info.Err)
   248  		h.p.ServeError(w, r, relpath, info.Err)
   249  		return
   250  	}
   251  
   252  	if mode&NoHTML != 0 {
   253  		h.p.ServeText(w, applyTemplate(h.p.PackageText, "packageText", info))
   254  		return
   255  	}
   256  
   257  	var tabtitle, title, subtitle string
   258  	switch {
   259  	case info.PAst != nil:
   260  		for _, ast := range info.PAst {
   261  			tabtitle = ast.Name.Name
   262  			break
   263  		}
   264  	case info.PDoc != nil:
   265  		tabtitle = info.PDoc.Name
   266  	default:
   267  		tabtitle = info.Dirname
   268  		title = "Directory "
   269  		if h.p.ShowTimestamps {
   270  			subtitle = "Last update: " + info.DirTime.String()
   271  		}
   272  	}
   273  	if title == "" {
   274  		if info.IsMain {
   275  			// assume that the directory name is the command name
   276  			_, tabtitle = pathpkg.Split(relpath)
   277  			title = "Command "
   278  		} else {
   279  			title = "Package "
   280  		}
   281  	}
   282  	title += tabtitle
   283  
   284  	// special cases for top-level package/command directories
   285  	switch tabtitle {
   286  	case "/src":
   287  		title = "Packages"
   288  		tabtitle = "Packages"
   289  	case "/src/cmd":
   290  		title = "Commands"
   291  		tabtitle = "Commands"
   292  	}
   293  
   294  	// Emit JSON array for type information.
   295  	pi := h.c.Analysis.PackageInfo(relpath)
   296  	info.CallGraphIndex = pi.CallGraphIndex
   297  	info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
   298  	info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
   299  	info.TypeInfoIndex = make(map[string]int)
   300  	for i, ti := range pi.Types {
   301  		info.TypeInfoIndex[ti.Name] = i
   302  	}
   303  
   304  	info.Share = allowShare(r)
   305  	h.p.ServePage(w, Page{
   306  		Title:    title,
   307  		Tabtitle: tabtitle,
   308  		Subtitle: subtitle,
   309  		Body:     applyTemplate(h.p.PackageHTML, "packageHTML", info),
   310  		Share:    info.Share,
   311  	})
   312  }
   313  
   314  type PageInfoMode uint
   315  
   316  const (
   317  	NoFiltering PageInfoMode = 1 << iota // do not filter exports
   318  	AllMethods                           // show all embedded methods
   319  	ShowSource                           // show source code, do not extract documentation
   320  	NoHTML                               // show result in textual form, do not generate HTML
   321  	FlatDir                              // show directory in a flat (non-indented) manner
   322  	NoTypeAssoc                          // don't associate consts, vars, and factory functions with types
   323  )
   324  
   325  // modeNames defines names for each PageInfoMode flag.
   326  var modeNames = map[string]PageInfoMode{
   327  	"all":     NoFiltering,
   328  	"methods": AllMethods,
   329  	"src":     ShowSource,
   330  	"text":    NoHTML,
   331  	"flat":    FlatDir,
   332  }
   333  
   334  // GetPageInfoMode computes the PageInfoMode flags by analyzing the request
   335  // URL form value "m". It is value is a comma-separated list of mode names
   336  // as defined by modeNames (e.g.: m=src,text).
   337  func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
   338  	var mode PageInfoMode
   339  	for _, k := range strings.Split(r.FormValue("m"), ",") {
   340  		if m, found := modeNames[strings.TrimSpace(k)]; found {
   341  			mode |= m
   342  		}
   343  	}
   344  	if p.AdjustPageInfoMode != nil {
   345  		mode = p.AdjustPageInfoMode(r, mode)
   346  	}
   347  	return mode
   348  }
   349  
   350  // poorMansImporter returns a (dummy) package object named
   351  // by the last path component of the provided package path
   352  // (as is the convention for packages). This is sufficient
   353  // to resolve package identifiers without doing an actual
   354  // import. It never returns an error.
   355  //
   356  func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
   357  	pkg := imports[path]
   358  	if pkg == nil {
   359  		// note that strings.LastIndex returns -1 if there is no "/"
   360  		pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
   361  		pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
   362  		imports[path] = pkg
   363  	}
   364  	return pkg, nil
   365  }
   366  
   367  // globalNames returns a set of the names declared by all package-level
   368  // declarations. Method names are returned in the form Receiver_Method.
   369  func globalNames(pkg *ast.Package) map[string]bool {
   370  	names := make(map[string]bool)
   371  	for _, file := range pkg.Files {
   372  		for _, decl := range file.Decls {
   373  			addNames(names, decl)
   374  		}
   375  	}
   376  	return names
   377  }
   378  
   379  // collectExamples collects examples for pkg from testfiles.
   380  func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
   381  	var files []*ast.File
   382  	for _, f := range testfiles {
   383  		files = append(files, f)
   384  	}
   385  
   386  	var examples []*doc.Example
   387  	globals := globalNames(pkg)
   388  	for _, e := range doc.Examples(files...) {
   389  		name := stripExampleSuffix(e.Name)
   390  		if name == "" || globals[name] {
   391  			examples = append(examples, e)
   392  		} else if c.Verbose {
   393  			log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
   394  		}
   395  	}
   396  
   397  	return examples
   398  }
   399  
   400  // addNames adds the names declared by decl to the names set.
   401  // Method names are added in the form ReceiverTypeName_Method.
   402  func addNames(names map[string]bool, decl ast.Decl) {
   403  	switch d := decl.(type) {
   404  	case *ast.FuncDecl:
   405  		name := d.Name.Name
   406  		if d.Recv != nil {
   407  			var typeName string
   408  			switch r := d.Recv.List[0].Type.(type) {
   409  			case *ast.StarExpr:
   410  				typeName = r.X.(*ast.Ident).Name
   411  			case *ast.Ident:
   412  				typeName = r.Name
   413  			}
   414  			name = typeName + "_" + name
   415  		}
   416  		names[name] = true
   417  	case *ast.GenDecl:
   418  		for _, spec := range d.Specs {
   419  			switch s := spec.(type) {
   420  			case *ast.TypeSpec:
   421  				names[s.Name.Name] = true
   422  			case *ast.ValueSpec:
   423  				for _, id := range s.Names {
   424  					names[id.Name] = true
   425  				}
   426  			}
   427  		}
   428  	}
   429  }
   430  
   431  // packageExports is a local implementation of ast.PackageExports
   432  // which correctly updates each package file's comment list.
   433  // (The ast.PackageExports signature is frozen, hence the local
   434  // implementation).
   435  //
   436  func packageExports(fset *token.FileSet, pkg *ast.Package) {
   437  	for _, src := range pkg.Files {
   438  		cmap := ast.NewCommentMap(fset, src, src.Comments)
   439  		ast.FileExports(src)
   440  		src.Comments = cmap.Filter(src).Comments()
   441  	}
   442  }
   443  
   444  func applyTemplate(t *template.Template, name string, data interface{}) []byte {
   445  	var buf bytes.Buffer
   446  	if err := t.Execute(&buf, data); err != nil {
   447  		log.Printf("%s.Execute: %s", name, err)
   448  	}
   449  	return buf.Bytes()
   450  }
   451  
   452  type writerCapturesErr struct {
   453  	w   io.Writer
   454  	err error
   455  }
   456  
   457  func (w *writerCapturesErr) Write(p []byte) (int, error) {
   458  	n, err := w.w.Write(p)
   459  	if err != nil {
   460  		w.err = err
   461  	}
   462  	return n, err
   463  }
   464  
   465  // applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
   466  // for the call to template.Execute.  It uses an io.Writer wrapper to capture
   467  // errors from the underlying http.ResponseWriter.  Errors are logged only when
   468  // they come from the template processing and not the Writer; this avoid
   469  // polluting log files with error messages due to networking issues, such as
   470  // client disconnects and http HEAD protocol violations.
   471  func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
   472  	w := &writerCapturesErr{w: rw}
   473  	err := t.Execute(w, data)
   474  	// There are some cases where template.Execute does not return an error when
   475  	// rw returns an error, and some where it does.  So check w.err first.
   476  	if w.err == nil && err != nil {
   477  		// Log template errors.
   478  		log.Printf("%s.Execute: %s", t.Name(), err)
   479  	}
   480  }
   481  
   482  func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
   483  	canonical := pathpkg.Clean(r.URL.Path)
   484  	if !strings.HasSuffix(canonical, "/") {
   485  		canonical += "/"
   486  	}
   487  	if r.URL.Path != canonical {
   488  		url := *r.URL
   489  		url.Path = canonical
   490  		http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
   491  		redirected = true
   492  	}
   493  	return
   494  }
   495  
   496  func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
   497  	c := pathpkg.Clean(r.URL.Path)
   498  	c = strings.TrimRight(c, "/")
   499  	if r.URL.Path != c {
   500  		url := *r.URL
   501  		url.Path = c
   502  		http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
   503  		redirected = true
   504  	}
   505  	return
   506  }
   507  
   508  func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
   509  	src, err := vfs.ReadFile(p.Corpus.fs, abspath)
   510  	if err != nil {
   511  		log.Printf("ReadFile: %s", err)
   512  		p.ServeError(w, r, relpath, err)
   513  		return
   514  	}
   515  
   516  	if r.FormValue("m") == "text" {
   517  		p.ServeText(w, src)
   518  		return
   519  	}
   520  
   521  	h := r.FormValue("h")
   522  	s := RangeSelection(r.FormValue("s"))
   523  
   524  	var buf bytes.Buffer
   525  	if pathpkg.Ext(abspath) == ".go" {
   526  		// Find markup links for this file (e.g. "/src/fmt/print.go").
   527  		fi := p.Corpus.Analysis.FileInfo(abspath)
   528  		buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
   529  		buf.Write(marshalJSON(fi.Data))
   530  		buf.WriteString(";</script>\n")
   531  
   532  		if status := p.Corpus.Analysis.Status(); status != "" {
   533  			buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
   534  			// TODO(adonovan): show analysis status at per-file granularity.
   535  			fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
   536  		}
   537  
   538  		buf.WriteString("<pre>")
   539  		formatGoSource(&buf, src, fi.Links, h, s)
   540  		buf.WriteString("</pre>")
   541  	} else {
   542  		buf.WriteString("<pre>")
   543  		FormatText(&buf, src, 1, false, h, s)
   544  		buf.WriteString("</pre>")
   545  	}
   546  	fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
   547  
   548  	p.ServePage(w, Page{
   549  		Title:    title + " " + relpath,
   550  		Tabtitle: relpath,
   551  		Body:     buf.Bytes(),
   552  		Share:    allowShare(r),
   553  	})
   554  }
   555  
   556  // formatGoSource HTML-escapes Go source text and writes it to w,
   557  // decorating it with the specified analysis links.
   558  //
   559  func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
   560  	// Emit to a temp buffer so that we can add line anchors at the end.
   561  	saved, buf := buf, new(bytes.Buffer)
   562  
   563  	var i int
   564  	var link analysis.Link // shared state of the two funcs below
   565  	segmentIter := func() (seg Segment) {
   566  		if i < len(links) {
   567  			link = links[i]
   568  			i++
   569  			seg = Segment{link.Start(), link.End()}
   570  		}
   571  		return
   572  	}
   573  	linkWriter := func(w io.Writer, offs int, start bool) {
   574  		link.Write(w, offs, start)
   575  	}
   576  
   577  	comments := tokenSelection(text, token.COMMENT)
   578  	var highlights Selection
   579  	if pattern != "" {
   580  		highlights = regexpSelection(text, pattern)
   581  	}
   582  
   583  	FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
   584  
   585  	// Now copy buf to saved, adding line anchors.
   586  
   587  	// The lineSelection mechanism can't be composed with our
   588  	// linkWriter, so we have to add line spans as another pass.
   589  	n := 1
   590  	for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
   591  		fmt.Fprintf(saved, "<span id=\"L%d\" class=\"ln\">%6d</span>\t", n, n)
   592  		n++
   593  		saved.Write(line)
   594  		saved.WriteByte('\n')
   595  	}
   596  }
   597  
   598  func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
   599  	if redirect(w, r) {
   600  		return
   601  	}
   602  
   603  	list, err := p.Corpus.fs.ReadDir(abspath)
   604  	if err != nil {
   605  		p.ServeError(w, r, relpath, err)
   606  		return
   607  	}
   608  
   609  	p.ServePage(w, Page{
   610  		Title:    "Directory " + relpath,
   611  		Tabtitle: relpath,
   612  		Body:     applyTemplate(p.DirlistHTML, "dirlistHTML", list),
   613  		Share:    allowShare(r),
   614  	})
   615  }
   616  
   617  func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
   618  	// get HTML body contents
   619  	src, err := vfs.ReadFile(p.Corpus.fs, abspath)
   620  	if err != nil {
   621  		log.Printf("ReadFile: %s", err)
   622  		p.ServeError(w, r, relpath, err)
   623  		return
   624  	}
   625  
   626  	// if it begins with "<!DOCTYPE " assume it is standalone
   627  	// html that doesn't need the template wrapping.
   628  	if bytes.HasPrefix(src, doctype) {
   629  		w.Write(src)
   630  		return
   631  	}
   632  
   633  	// if it begins with a JSON blob, read in the metadata.
   634  	meta, src, err := extractMetadata(src)
   635  	if err != nil {
   636  		log.Printf("decoding metadata %s: %v", relpath, err)
   637  	}
   638  
   639  	page := Page{
   640  		Title:    meta.Title,
   641  		Subtitle: meta.Subtitle,
   642  		Share:    allowShare(r),
   643  	}
   644  
   645  	// evaluate as template if indicated
   646  	if meta.Template {
   647  		tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
   648  		if err != nil {
   649  			log.Printf("parsing template %s: %v", relpath, err)
   650  			p.ServeError(w, r, relpath, err)
   651  			return
   652  		}
   653  		var buf bytes.Buffer
   654  		if err := tmpl.Execute(&buf, page); err != nil {
   655  			log.Printf("executing template %s: %v", relpath, err)
   656  			p.ServeError(w, r, relpath, err)
   657  			return
   658  		}
   659  		src = buf.Bytes()
   660  	}
   661  
   662  	// if it's the language spec, add tags to EBNF productions
   663  	if strings.HasSuffix(abspath, "go_spec.html") {
   664  		var buf bytes.Buffer
   665  		Linkify(&buf, src)
   666  		src = buf.Bytes()
   667  	}
   668  
   669  	page.Body = src
   670  	p.ServePage(w, page)
   671  }
   672  
   673  func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
   674  	p.serveFile(w, r)
   675  }
   676  
   677  func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
   678  	relpath := r.URL.Path
   679  
   680  	// Check to see if we need to redirect or serve another file.
   681  	if m := p.Corpus.MetadataFor(relpath); m != nil {
   682  		if m.Path != relpath {
   683  			// Redirect to canonical path.
   684  			http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
   685  			return
   686  		}
   687  		// Serve from the actual filesystem path.
   688  		relpath = m.filePath
   689  	}
   690  
   691  	abspath := relpath
   692  	relpath = relpath[1:] // strip leading slash
   693  
   694  	switch pathpkg.Ext(relpath) {
   695  	case ".html":
   696  		if strings.HasSuffix(relpath, "/index.html") {
   697  			// We'll show index.html for the directory.
   698  			// Use the dir/ version as canonical instead of dir/index.html.
   699  			http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
   700  			return
   701  		}
   702  		p.ServeHTMLDoc(w, r, abspath, relpath)
   703  		return
   704  
   705  	case ".go":
   706  		p.serveTextFile(w, r, abspath, relpath, "Source file")
   707  		return
   708  	}
   709  
   710  	dir, err := p.Corpus.fs.Lstat(abspath)
   711  	if err != nil {
   712  		log.Print(err)
   713  		p.ServeError(w, r, relpath, err)
   714  		return
   715  	}
   716  
   717  	if dir != nil && dir.IsDir() {
   718  		if redirect(w, r) {
   719  			return
   720  		}
   721  		if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
   722  			p.ServeHTMLDoc(w, r, index, index)
   723  			return
   724  		}
   725  		p.serveDirectory(w, r, abspath, relpath)
   726  		return
   727  	}
   728  
   729  	if util.IsTextFile(p.Corpus.fs, abspath) {
   730  		if redirectFile(w, r) {
   731  			return
   732  		}
   733  		p.serveTextFile(w, r, abspath, relpath, "Text file")
   734  		return
   735  	}
   736  
   737  	p.fileServer.ServeHTTP(w, r)
   738  }
   739  
   740  func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
   741  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   742  	w.Write(text)
   743  }
   744  
   745  func marshalJSON(x interface{}) []byte {
   746  	var data []byte
   747  	var err error
   748  	const indentJSON = false // for easier debugging
   749  	if indentJSON {
   750  		data, err = json.MarshalIndent(x, "", "    ")
   751  	} else {
   752  		data, err = json.Marshal(x)
   753  	}
   754  	if err != nil {
   755  		panic(fmt.Sprintf("json.Marshal failed: %s", err))
   756  	}
   757  	return data
   758  }