Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.0.0-20201105173854-bc9fc8d8c4bc / godoc / server.go
diff --git a/.config/coc/extensions/coc-go-data/tools/pkg/mod/golang.org/x/tools@v0.0.0-20201105173854-bc9fc8d8c4bc/godoc/server.go b/.config/coc/extensions/coc-go-data/tools/pkg/mod/golang.org/x/tools@v0.0.0-20201105173854-bc9fc8d8c4bc/godoc/server.go
new file mode 100644 (file)
index 0000000..8724291
--- /dev/null
@@ -0,0 +1,837 @@
+// Copyright 2013 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.
+
+package godoc
+
+import (
+       "bytes"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "go/ast"
+       "go/build"
+       "go/doc"
+       "go/token"
+       htmlpkg "html"
+       htmltemplate "html/template"
+       "io"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       pathpkg "path"
+       "path/filepath"
+       "sort"
+       "strings"
+       "text/template"
+       "time"
+
+       "golang.org/x/tools/godoc/analysis"
+       "golang.org/x/tools/godoc/util"
+       "golang.org/x/tools/godoc/vfs"
+)
+
+// handlerServer is a migration from an old godoc http Handler type.
+// This should probably merge into something else.
+type handlerServer struct {
+       p           *Presentation
+       c           *Corpus  // copy of p.Corpus
+       pattern     string   // url pattern; e.g. "/pkg/"
+       stripPrefix string   // prefix to strip from import path; e.g. "pkg/"
+       fsRoot      string   // file system root to which the pattern is mapped; e.g. "/src"
+       exclude     []string // file system paths to exclude; e.g. "/src/cmd"
+}
+
+func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
+       mux.Handle(s.pattern, s)
+}
+
+// GetPageInfo returns the PageInfo for a package directory abspath. If the
+// parameter genAST is set, an AST containing only the package exports is
+// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
+// is extracted from the AST. If there is no corresponding package in the
+// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
+// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
+// set to the respective error but the error is not logged.
+//
+func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
+       info := &PageInfo{Dirname: abspath, Mode: mode}
+
+       // Restrict to the package files that would be used when building
+       // the package on this system.  This makes sure that if there are
+       // separate implementations for, say, Windows vs Unix, we don't
+       // jumble them all together.
+       // Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
+       // are used.
+       ctxt := build.Default
+       ctxt.IsAbsPath = pathpkg.IsAbs
+       ctxt.IsDir = func(path string) bool {
+               fi, err := h.c.fs.Stat(filepath.ToSlash(path))
+               return err == nil && fi.IsDir()
+       }
+       ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
+               f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
+               filtered := make([]os.FileInfo, 0, len(f))
+               for _, i := range f {
+                       if mode&NoFiltering != 0 || i.Name() != "internal" {
+                               filtered = append(filtered, i)
+                       }
+               }
+               return filtered, err
+       }
+       ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
+               data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
+               if err != nil {
+                       return nil, err
+               }
+               return ioutil.NopCloser(bytes.NewReader(data)), nil
+       }
+
+       // Make the syscall/js package always visible by default.
+       // It defaults to the host's GOOS/GOARCH, and golang.org's
+       // linux/amd64 means the wasm syscall/js package was blank.
+       // And you can't run godoc on js/wasm anyway, so host defaults
+       // don't make sense here.
+       if goos == "" && goarch == "" && relpath == "syscall/js" {
+               goos, goarch = "js", "wasm"
+       }
+       if goos != "" {
+               ctxt.GOOS = goos
+       }
+       if goarch != "" {
+               ctxt.GOARCH = goarch
+       }
+
+       pkginfo, err := ctxt.ImportDir(abspath, 0)
+       // continue if there are no Go source files; we still want the directory info
+       if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
+               info.Err = err
+               return info
+       }
+
+       // collect package files
+       pkgname := pkginfo.Name
+       pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
+       if len(pkgfiles) == 0 {
+               // Commands written in C have no .go files in the build.
+               // Instead, documentation may be found in an ignored file.
+               // The file may be ignored via an explicit +build ignore
+               // constraint (recommended), or by defining the package
+               // documentation (historic).
+               pkgname = "main" // assume package main since pkginfo.Name == ""
+               pkgfiles = pkginfo.IgnoredGoFiles
+       }
+
+       // get package information, if any
+       if len(pkgfiles) > 0 {
+               // build package AST
+               fset := token.NewFileSet()
+               files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
+               if err != nil {
+                       info.Err = err
+                       return info
+               }
+
+               // ignore any errors - they are due to unresolved identifiers
+               pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
+
+               // extract package documentation
+               info.FSet = fset
+               if mode&ShowSource == 0 {
+                       // show extracted documentation
+                       var m doc.Mode
+                       if mode&NoFiltering != 0 {
+                               m |= doc.AllDecls
+                       }
+                       if mode&AllMethods != 0 {
+                               m |= doc.AllMethods
+                       }
+                       info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
+                       if mode&NoTypeAssoc != 0 {
+                               for _, t := range info.PDoc.Types {
+                                       info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
+                                       info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
+                                       info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
+                                       t.Consts = nil
+                                       t.Vars = nil
+                                       t.Funcs = nil
+                               }
+                               // for now we cannot easily sort consts and vars since
+                               // go/doc.Value doesn't export the order information
+                               sort.Sort(funcsByName(info.PDoc.Funcs))
+                       }
+
+                       // collect examples
+                       testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
+                       files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
+                       if err != nil {
+                               log.Println("parsing examples:", err)
+                       }
+                       info.Examples = collectExamples(h.c, pkg, files)
+
+                       // collect any notes that we want to show
+                       if info.PDoc.Notes != nil {
+                               // could regexp.Compile only once per godoc, but probably not worth it
+                               if rx := h.p.NotesRx; rx != nil {
+                                       for m, n := range info.PDoc.Notes {
+                                               if rx.MatchString(m) {
+                                                       if info.Notes == nil {
+                                                               info.Notes = make(map[string][]*doc.Note)
+                                                       }
+                                                       info.Notes[m] = n
+                                               }
+                                       }
+                               }
+                       }
+
+               } else {
+                       // show source code
+                       // TODO(gri) Consider eliminating export filtering in this mode,
+                       //           or perhaps eliminating the mode altogether.
+                       if mode&NoFiltering == 0 {
+                               packageExports(fset, pkg)
+                       }
+                       info.PAst = files
+               }
+               info.IsMain = pkgname == "main"
+       }
+
+       // get directory information, if any
+       var dir *Directory
+       var timestamp time.Time
+       if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
+               // directory tree is present; lookup respective directory
+               // (may still fail if the file system was updated and the
+               // new directory tree has not yet been computed)
+               dir = tree.(*Directory).lookup(abspath)
+               timestamp = ts
+       }
+       if dir == nil {
+               // TODO(agnivade): handle this case better, now since there is no CLI mode.
+               // no directory tree present (happens in command-line mode);
+               // compute 2 levels for this page. The second level is to
+               // get the synopses of sub-directories.
+               // note: cannot use path filter here because in general
+               // it doesn't contain the FSTree path
+               dir = h.c.newDirectory(abspath, 2)
+               timestamp = time.Now()
+       }
+       info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
+
+       info.DirTime = timestamp
+       info.DirFlat = mode&FlatDir != 0
+
+       return info
+}
+
+func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
+       // if the path is under one of the exclusion paths, don't list.
+       for _, e := range h.exclude {
+               if strings.HasPrefix(path, e) {
+                       return false
+               }
+       }
+
+       // if the path includes 'internal', don't list unless we are in the NoFiltering mode.
+       if mode&NoFiltering != 0 {
+               return true
+       }
+       if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
+               for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
+                       if c == "internal" || c == "vendor" {
+                               return false
+                       }
+               }
+       }
+       return true
+}
+
+type funcsByName []*doc.Func
+
+func (s funcsByName) Len() int           { return len(s) }
+func (s funcsByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
+
+func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       if redirect(w, r) {
+               return
+       }
+
+       relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
+
+       if !h.corpusInitialized() {
+               h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
+               return
+       }
+
+       abspath := pathpkg.Join(h.fsRoot, relpath)
+       mode := h.p.GetPageInfoMode(r)
+       if relpath == builtinPkgPath {
+               // The fake built-in package contains unexported identifiers,
+               // but we want to show them. Also, disable type association,
+               // since it's not helpful for this fake package (see issue 6645).
+               mode |= NoFiltering | NoTypeAssoc
+       }
+       info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
+       if info.Err != nil {
+               log.Print(info.Err)
+               h.p.ServeError(w, r, relpath, info.Err)
+               return
+       }
+
+       var tabtitle, title, subtitle string
+       switch {
+       case info.PAst != nil:
+               for _, ast := range info.PAst {
+                       tabtitle = ast.Name.Name
+                       break
+               }
+       case info.PDoc != nil:
+               tabtitle = info.PDoc.Name
+       default:
+               tabtitle = info.Dirname
+               title = "Directory "
+               if h.p.ShowTimestamps {
+                       subtitle = "Last update: " + info.DirTime.String()
+               }
+       }
+       if title == "" {
+               if info.IsMain {
+                       // assume that the directory name is the command name
+                       _, tabtitle = pathpkg.Split(relpath)
+                       title = "Command "
+               } else {
+                       title = "Package "
+               }
+       }
+       title += tabtitle
+
+       // special cases for top-level package/command directories
+       switch tabtitle {
+       case "/src":
+               title = "Packages"
+               tabtitle = "Packages"
+       case "/src/cmd":
+               title = "Commands"
+               tabtitle = "Commands"
+       }
+
+       // Emit JSON array for type information.
+       pi := h.c.Analysis.PackageInfo(relpath)
+       hasTreeView := len(pi.CallGraph) != 0
+       info.CallGraphIndex = pi.CallGraphIndex
+       info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
+       info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
+       info.TypeInfoIndex = make(map[string]int)
+       for i, ti := range pi.Types {
+               info.TypeInfoIndex[ti.Name] = i
+       }
+
+       info.GoogleCN = googleCN(r)
+       var body []byte
+       if info.Dirname == "/src" {
+               body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
+       } else {
+               body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
+       }
+       h.p.ServePage(w, Page{
+               Title:    title,
+               Tabtitle: tabtitle,
+               Subtitle: subtitle,
+               Body:     body,
+               GoogleCN: info.GoogleCN,
+               TreeView: hasTreeView,
+       })
+}
+
+func (h *handlerServer) corpusInitialized() bool {
+       h.c.initMu.RLock()
+       defer h.c.initMu.RUnlock()
+       return h.c.initDone
+}
+
+type PageInfoMode uint
+
+const (
+       PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
+
+       NoFiltering PageInfoMode = 1 << iota // do not filter exports
+       AllMethods                           // show all embedded methods
+       ShowSource                           // show source code, do not extract documentation
+       FlatDir                              // show directory in a flat (non-indented) manner
+       NoTypeAssoc                          // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
+)
+
+// modeNames defines names for each PageInfoMode flag.
+var modeNames = map[string]PageInfoMode{
+       "all":     NoFiltering,
+       "methods": AllMethods,
+       "src":     ShowSource,
+       "flat":    FlatDir,
+}
+
+// generate a query string for persisting PageInfoMode between pages.
+func modeQueryString(mode PageInfoMode) string {
+       if modeNames := mode.names(); len(modeNames) > 0 {
+               return "?m=" + strings.Join(modeNames, ",")
+       }
+       return ""
+}
+
+// alphabetically sorted names of active flags for a PageInfoMode.
+func (m PageInfoMode) names() []string {
+       var names []string
+       for name, mode := range modeNames {
+               if m&mode != 0 {
+                       names = append(names, name)
+               }
+       }
+       sort.Strings(names)
+       return names
+}
+
+// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
+// URL form value "m". It is value is a comma-separated list of mode names
+// as defined by modeNames (e.g.: m=src,text).
+func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
+       var mode PageInfoMode
+       for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
+               if m, found := modeNames[strings.TrimSpace(k)]; found {
+                       mode |= m
+               }
+       }
+       if p.AdjustPageInfoMode != nil {
+               mode = p.AdjustPageInfoMode(r, mode)
+       }
+       return mode
+}
+
+// poorMansImporter returns a (dummy) package object named
+// by the last path component of the provided package path
+// (as is the convention for packages). This is sufficient
+// to resolve package identifiers without doing an actual
+// import. It never returns an error.
+//
+func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
+       pkg := imports[path]
+       if pkg == nil {
+               // note that strings.LastIndex returns -1 if there is no "/"
+               pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
+               pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
+               imports[path] = pkg
+       }
+       return pkg, nil
+}
+
+// globalNames returns a set of the names declared by all package-level
+// declarations. Method names are returned in the form Receiver_Method.
+func globalNames(pkg *ast.Package) map[string]bool {
+       names := make(map[string]bool)
+       for _, file := range pkg.Files {
+               for _, decl := range file.Decls {
+                       addNames(names, decl)
+               }
+       }
+       return names
+}
+
+// collectExamples collects examples for pkg from testfiles.
+func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
+       var files []*ast.File
+       for _, f := range testfiles {
+               files = append(files, f)
+       }
+
+       var examples []*doc.Example
+       globals := globalNames(pkg)
+       for _, e := range doc.Examples(files...) {
+               name := stripExampleSuffix(e.Name)
+               if name == "" || globals[name] {
+                       examples = append(examples, e)
+               } else if c.Verbose {
+                       log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
+               }
+       }
+
+       return examples
+}
+
+// addNames adds the names declared by decl to the names set.
+// Method names are added in the form ReceiverTypeName_Method.
+func addNames(names map[string]bool, decl ast.Decl) {
+       switch d := decl.(type) {
+       case *ast.FuncDecl:
+               name := d.Name.Name
+               if d.Recv != nil {
+                       var typeName string
+                       switch r := d.Recv.List[0].Type.(type) {
+                       case *ast.StarExpr:
+                               typeName = r.X.(*ast.Ident).Name
+                       case *ast.Ident:
+                               typeName = r.Name
+                       }
+                       name = typeName + "_" + name
+               }
+               names[name] = true
+       case *ast.GenDecl:
+               for _, spec := range d.Specs {
+                       switch s := spec.(type) {
+                       case *ast.TypeSpec:
+                               names[s.Name.Name] = true
+                       case *ast.ValueSpec:
+                               for _, id := range s.Names {
+                                       names[id.Name] = true
+                               }
+                       }
+               }
+       }
+}
+
+// packageExports is a local implementation of ast.PackageExports
+// which correctly updates each package file's comment list.
+// (The ast.PackageExports signature is frozen, hence the local
+// implementation).
+//
+func packageExports(fset *token.FileSet, pkg *ast.Package) {
+       for _, src := range pkg.Files {
+               cmap := ast.NewCommentMap(fset, src, src.Comments)
+               ast.FileExports(src)
+               src.Comments = cmap.Filter(src).Comments()
+       }
+}
+
+func applyTemplate(t *template.Template, name string, data interface{}) []byte {
+       var buf bytes.Buffer
+       if err := t.Execute(&buf, data); err != nil {
+               log.Printf("%s.Execute: %s", name, err)
+       }
+       return buf.Bytes()
+}
+
+type writerCapturesErr struct {
+       w   io.Writer
+       err error
+}
+
+func (w *writerCapturesErr) Write(p []byte) (int, error) {
+       n, err := w.w.Write(p)
+       if err != nil {
+               w.err = err
+       }
+       return n, err
+}
+
+// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
+// for the call to template.Execute.  It uses an io.Writer wrapper to capture
+// errors from the underlying http.ResponseWriter.  Errors are logged only when
+// they come from the template processing and not the Writer; this avoid
+// polluting log files with error messages due to networking issues, such as
+// client disconnects and http HEAD protocol violations.
+func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
+       w := &writerCapturesErr{w: rw}
+       err := t.Execute(w, data)
+       // There are some cases where template.Execute does not return an error when
+       // rw returns an error, and some where it does.  So check w.err first.
+       if w.err == nil && err != nil {
+               // Log template errors.
+               log.Printf("%s.Execute: %s", t.Name(), err)
+       }
+}
+
+func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
+       canonical := pathpkg.Clean(r.URL.Path)
+       if !strings.HasSuffix(canonical, "/") {
+               canonical += "/"
+       }
+       if r.URL.Path != canonical {
+               url := *r.URL
+               url.Path = canonical
+               http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
+               redirected = true
+       }
+       return
+}
+
+func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
+       c := pathpkg.Clean(r.URL.Path)
+       c = strings.TrimRight(c, "/")
+       if r.URL.Path != c {
+               url := *r.URL
+               url.Path = c
+               http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
+               redirected = true
+       }
+       return
+}
+
+func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
+       src, err := vfs.ReadFile(p.Corpus.fs, abspath)
+       if err != nil {
+               log.Printf("ReadFile: %s", err)
+               p.ServeError(w, r, relpath, err)
+               return
+       }
+
+       if r.FormValue(PageInfoModeQueryString) == "text" {
+               p.ServeText(w, src)
+               return
+       }
+
+       h := r.FormValue("h")
+       s := RangeSelection(r.FormValue("s"))
+
+       var buf bytes.Buffer
+       if pathpkg.Ext(abspath) == ".go" {
+               // Find markup links for this file (e.g. "/src/fmt/print.go").
+               fi := p.Corpus.Analysis.FileInfo(abspath)
+               buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
+               buf.Write(marshalJSON(fi.Data))
+               buf.WriteString(";</script>\n")
+
+               if status := p.Corpus.Analysis.Status(); status != "" {
+                       buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
+                       // TODO(adonovan): show analysis status at per-file granularity.
+                       fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
+               }
+
+               buf.WriteString("<pre>")
+               formatGoSource(&buf, src, fi.Links, h, s)
+               buf.WriteString("</pre>")
+       } else {
+               buf.WriteString("<pre>")
+               FormatText(&buf, src, 1, false, h, s)
+               buf.WriteString("</pre>")
+       }
+       fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
+
+       p.ServePage(w, Page{
+               Title:    title,
+               SrcPath:  relpath,
+               Tabtitle: relpath,
+               Body:     buf.Bytes(),
+               GoogleCN: googleCN(r),
+       })
+}
+
+// formatGoSource HTML-escapes Go source text and writes it to w,
+// decorating it with the specified analysis links.
+//
+func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
+       // Emit to a temp buffer so that we can add line anchors at the end.
+       saved, buf := buf, new(bytes.Buffer)
+
+       var i int
+       var link analysis.Link // shared state of the two funcs below
+       segmentIter := func() (seg Segment) {
+               if i < len(links) {
+                       link = links[i]
+                       i++
+                       seg = Segment{link.Start(), link.End()}
+               }
+               return
+       }
+       linkWriter := func(w io.Writer, offs int, start bool) {
+               link.Write(w, offs, start)
+       }
+
+       comments := tokenSelection(text, token.COMMENT)
+       var highlights Selection
+       if pattern != "" {
+               highlights = regexpSelection(text, pattern)
+       }
+
+       FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
+
+       // Now copy buf to saved, adding line anchors.
+
+       // The lineSelection mechanism can't be composed with our
+       // linkWriter, so we have to add line spans as another pass.
+       n := 1
+       for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
+               // The line numbers are inserted into the document via a CSS ::before
+               // pseudo-element. This prevents them from being copied when users
+               // highlight and copy text.
+               // ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
+               // This is also the trick Github uses to hide line numbers.
+               //
+               // The first tab for the code snippet needs to start in column 9, so
+               // it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
+               // character only indents a short amount.
+               //
+               // Due to rounding and font width Firefox might not treat 8 rendered
+               // characters as 8 characters wide, and subsequently may treat the tab
+               // character in the 9th position as moving the width from (7.5 or so) up
+               // to 8. See
+               // https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
+               // for a fuller explanation. The solution is to add a CSS class to
+               // explicitly declare the width to be 8 characters.
+               fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d&nbsp;&nbsp;</span>`, n, n)
+               n++
+               saved.Write(line)
+               saved.WriteByte('\n')
+       }
+}
+
+func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
+       if redirect(w, r) {
+               return
+       }
+
+       list, err := p.Corpus.fs.ReadDir(abspath)
+       if err != nil {
+               p.ServeError(w, r, relpath, err)
+               return
+       }
+
+       p.ServePage(w, Page{
+               Title:    "Directory",
+               SrcPath:  relpath,
+               Tabtitle: relpath,
+               Body:     applyTemplate(p.DirlistHTML, "dirlistHTML", list),
+               GoogleCN: googleCN(r),
+       })
+}
+
+func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
+       // get HTML body contents
+       src, err := vfs.ReadFile(p.Corpus.fs, abspath)
+       if err != nil {
+               log.Printf("ReadFile: %s", err)
+               p.ServeError(w, r, relpath, err)
+               return
+       }
+
+       // if it begins with "<!DOCTYPE " assume it is standalone
+       // html that doesn't need the template wrapping.
+       if bytes.HasPrefix(src, doctype) {
+               w.Write(src)
+               return
+       }
+
+       // if it begins with a JSON blob, read in the metadata.
+       meta, src, err := extractMetadata(src)
+       if err != nil {
+               log.Printf("decoding metadata %s: %v", relpath, err)
+       }
+
+       page := Page{
+               Title:    meta.Title,
+               Subtitle: meta.Subtitle,
+               GoogleCN: googleCN(r),
+       }
+
+       // evaluate as template if indicated
+       if meta.Template {
+               tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
+               if err != nil {
+                       log.Printf("parsing template %s: %v", relpath, err)
+                       p.ServeError(w, r, relpath, err)
+                       return
+               }
+               var buf bytes.Buffer
+               if err := tmpl.Execute(&buf, page); err != nil {
+                       log.Printf("executing template %s: %v", relpath, err)
+                       p.ServeError(w, r, relpath, err)
+                       return
+               }
+               src = buf.Bytes()
+       }
+
+       // if it's the language spec, add tags to EBNF productions
+       if strings.HasSuffix(abspath, "go_spec.html") {
+               var buf bytes.Buffer
+               Linkify(&buf, src)
+               src = buf.Bytes()
+       }
+
+       page.Body = src
+       p.ServePage(w, page)
+}
+
+func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
+       p.serveFile(w, r)
+}
+
+func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
+       if strings.HasSuffix(r.URL.Path, "/index.html") {
+               // We'll show index.html for the directory.
+               // Use the dir/ version as canonical instead of dir/index.html.
+               http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
+               return
+       }
+
+       // Check to see if we need to redirect or serve another file.
+       relpath := r.URL.Path
+       if m := p.Corpus.MetadataFor(relpath); m != nil {
+               if m.Path != relpath {
+                       // Redirect to canonical path.
+                       http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
+                       return
+               }
+               // Serve from the actual filesystem path.
+               relpath = m.filePath
+       }
+
+       abspath := relpath
+       relpath = relpath[1:] // strip leading slash
+
+       switch pathpkg.Ext(relpath) {
+       case ".html":
+               p.ServeHTMLDoc(w, r, abspath, relpath)
+               return
+
+       case ".go":
+               p.serveTextFile(w, r, abspath, relpath, "Source file")
+               return
+       }
+
+       dir, err := p.Corpus.fs.Lstat(abspath)
+       if err != nil {
+               log.Print(err)
+               p.ServeError(w, r, relpath, err)
+               return
+       }
+
+       if dir != nil && dir.IsDir() {
+               if redirect(w, r) {
+                       return
+               }
+               if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
+                       p.ServeHTMLDoc(w, r, index, index)
+                       return
+               }
+               p.serveDirectory(w, r, abspath, relpath)
+               return
+       }
+
+       if util.IsTextFile(p.Corpus.fs, abspath) {
+               if redirectFile(w, r) {
+                       return
+               }
+               p.serveTextFile(w, r, abspath, relpath, "Text file")
+               return
+       }
+
+       p.fileServer.ServeHTTP(w, r)
+}
+
+func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
+       w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+       w.Write(text)
+}
+
+func marshalJSON(x interface{}) []byte {
+       var data []byte
+       var err error
+       const indentJSON = false // for easier debugging
+       if indentJSON {
+               data, err = json.MarshalIndent(x, "", "    ")
+       } else {
+               data, err = json.Marshal(x)
+       }
+       if err != nil {
+               panic(fmt.Sprintf("json.Marshal failed: %s", err))
+       }
+       return data
+}