...

Source file src/golang.org/x/tools/godoc/server.go

Documentation: golang.org/x/tools/godoc

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

View as plain text