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
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 )
34
35 // handlerServer is a migration from an old godoc http Handler type.
36 // This should probably merge into something else.
37 type handlerServer struct {
38         p           *Presentation
39         c           *Corpus  // copy of p.Corpus
40         pattern     string   // url pattern; e.g. "/pkg/"
41         stripPrefix string   // prefix to strip from import path; e.g. "pkg/"
42         fsRoot      string   // file system root to which the pattern is mapped; e.g. "/src"
43         exclude     []string // file system paths to exclude; e.g. "/src/cmd"
44 }
45
46 func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
47         mux.Handle(s.pattern, s)
48 }
49
50 // GetPageInfo returns the PageInfo for a package directory abspath. If the
51 // parameter genAST is set, an AST containing only the package exports is
52 // computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
53 // is extracted from the AST. If there is no corresponding package in the
54 // directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
55 // directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
56 // set to the respective error but the error is not logged.
57 //
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         info.GoogleCN = googleCN(r)
332         var body []byte
333         if info.Dirname == "/src" {
334                 body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
335         } else {
336                 body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
337         }
338         h.p.ServePage(w, Page{
339                 Title:    title,
340                 Tabtitle: tabtitle,
341                 Subtitle: subtitle,
342                 Body:     body,
343                 GoogleCN: info.GoogleCN,
344                 TreeView: hasTreeView,
345         })
346 }
347
348 func (h *handlerServer) corpusInitialized() bool {
349         h.c.initMu.RLock()
350         defer h.c.initMu.RUnlock()
351         return h.c.initDone
352 }
353
354 type PageInfoMode uint
355
356 const (
357         PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
358
359         NoFiltering PageInfoMode = 1 << iota // do not filter exports
360         AllMethods                           // show all embedded methods
361         ShowSource                           // show source code, do not extract documentation
362         FlatDir                              // show directory in a flat (non-indented) manner
363         NoTypeAssoc                          // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
364 )
365
366 // modeNames defines names for each PageInfoMode flag.
367 var modeNames = map[string]PageInfoMode{
368         "all":     NoFiltering,
369         "methods": AllMethods,
370         "src":     ShowSource,
371         "flat":    FlatDir,
372 }
373
374 // generate a query string for persisting PageInfoMode between pages.
375 func modeQueryString(mode PageInfoMode) string {
376         if modeNames := mode.names(); len(modeNames) > 0 {
377                 return "?m=" + strings.Join(modeNames, ",")
378         }
379         return ""
380 }
381
382 // alphabetically sorted names of active flags for a PageInfoMode.
383 func (m PageInfoMode) names() []string {
384         var names []string
385         for name, mode := range modeNames {
386                 if m&mode != 0 {
387                         names = append(names, name)
388                 }
389         }
390         sort.Strings(names)
391         return names
392 }
393
394 // GetPageInfoMode computes the PageInfoMode flags by analyzing the request
395 // URL form value "m". It is value is a comma-separated list of mode names
396 // as defined by modeNames (e.g.: m=src,text).
397 func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
398         var mode PageInfoMode
399         for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
400                 if m, found := modeNames[strings.TrimSpace(k)]; found {
401                         mode |= m
402                 }
403         }
404         if p.AdjustPageInfoMode != nil {
405                 mode = p.AdjustPageInfoMode(r, mode)
406         }
407         return mode
408 }
409
410 // poorMansImporter returns a (dummy) package object named
411 // by the last path component of the provided package path
412 // (as is the convention for packages). This is sufficient
413 // to resolve package identifiers without doing an actual
414 // import. It never returns an error.
415 //
416 func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
417         pkg := imports[path]
418         if pkg == nil {
419                 // note that strings.LastIndex returns -1 if there is no "/"
420                 pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
421                 pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
422                 imports[path] = pkg
423         }
424         return pkg, nil
425 }
426
427 // globalNames returns a set of the names declared by all package-level
428 // declarations. Method names are returned in the form Receiver_Method.
429 func globalNames(pkg *ast.Package) map[string]bool {
430         names := make(map[string]bool)
431         for _, file := range pkg.Files {
432                 for _, decl := range file.Decls {
433                         addNames(names, decl)
434                 }
435         }
436         return names
437 }
438
439 // collectExamples collects examples for pkg from testfiles.
440 func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
441         var files []*ast.File
442         for _, f := range testfiles {
443                 files = append(files, f)
444         }
445
446         var examples []*doc.Example
447         globals := globalNames(pkg)
448         for _, e := range doc.Examples(files...) {
449                 name := stripExampleSuffix(e.Name)
450                 if name == "" || globals[name] {
451                         examples = append(examples, e)
452                 } else if c.Verbose {
453                         log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
454                 }
455         }
456
457         return examples
458 }
459
460 // addNames adds the names declared by decl to the names set.
461 // Method names are added in the form ReceiverTypeName_Method.
462 func addNames(names map[string]bool, decl ast.Decl) {
463         switch d := decl.(type) {
464         case *ast.FuncDecl:
465                 name := d.Name.Name
466                 if d.Recv != nil {
467                         var typeName string
468                         switch r := d.Recv.List[0].Type.(type) {
469                         case *ast.StarExpr:
470                                 typeName = r.X.(*ast.Ident).Name
471                         case *ast.Ident:
472                                 typeName = r.Name
473                         }
474                         name = typeName + "_" + name
475                 }
476                 names[name] = true
477         case *ast.GenDecl:
478                 for _, spec := range d.Specs {
479                         switch s := spec.(type) {
480                         case *ast.TypeSpec:
481                                 names[s.Name.Name] = true
482                         case *ast.ValueSpec:
483                                 for _, id := range s.Names {
484                                         names[id.Name] = true
485                                 }
486                         }
487                 }
488         }
489 }
490
491 // packageExports is a local implementation of ast.PackageExports
492 // which correctly updates each package file's comment list.
493 // (The ast.PackageExports signature is frozen, hence the local
494 // implementation).
495 //
496 func packageExports(fset *token.FileSet, pkg *ast.Package) {
497         for _, src := range pkg.Files {
498                 cmap := ast.NewCommentMap(fset, src, src.Comments)
499                 ast.FileExports(src)
500                 src.Comments = cmap.Filter(src).Comments()
501         }
502 }
503
504 func applyTemplate(t *template.Template, name string, data interface{}) []byte {
505         var buf bytes.Buffer
506         if err := t.Execute(&buf, data); err != nil {
507                 log.Printf("%s.Execute: %s", name, err)
508         }
509         return buf.Bytes()
510 }
511
512 type writerCapturesErr struct {
513         w   io.Writer
514         err error
515 }
516
517 func (w *writerCapturesErr) Write(p []byte) (int, error) {
518         n, err := w.w.Write(p)
519         if err != nil {
520                 w.err = err
521         }
522         return n, err
523 }
524
525 // applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
526 // for the call to template.Execute.  It uses an io.Writer wrapper to capture
527 // errors from the underlying http.ResponseWriter.  Errors are logged only when
528 // they come from the template processing and not the Writer; this avoid
529 // polluting log files with error messages due to networking issues, such as
530 // client disconnects and http HEAD protocol violations.
531 func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
532         w := &writerCapturesErr{w: rw}
533         err := t.Execute(w, data)
534         // There are some cases where template.Execute does not return an error when
535         // rw returns an error, and some where it does.  So check w.err first.
536         if w.err == nil && err != nil {
537                 // Log template errors.
538                 log.Printf("%s.Execute: %s", t.Name(), err)
539         }
540 }
541
542 func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
543         canonical := pathpkg.Clean(r.URL.Path)
544         if !strings.HasSuffix(canonical, "/") {
545                 canonical += "/"
546         }
547         if r.URL.Path != canonical {
548                 url := *r.URL
549                 url.Path = canonical
550                 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
551                 redirected = true
552         }
553         return
554 }
555
556 func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
557         c := pathpkg.Clean(r.URL.Path)
558         c = strings.TrimRight(c, "/")
559         if r.URL.Path != c {
560                 url := *r.URL
561                 url.Path = c
562                 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
563                 redirected = true
564         }
565         return
566 }
567
568 func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
569         src, err := vfs.ReadFile(p.Corpus.fs, abspath)
570         if err != nil {
571                 log.Printf("ReadFile: %s", err)
572                 p.ServeError(w, r, relpath, err)
573                 return
574         }
575
576         if r.FormValue(PageInfoModeQueryString) == "text" {
577                 p.ServeText(w, src)
578                 return
579         }
580
581         h := r.FormValue("h")
582         s := RangeSelection(r.FormValue("s"))
583
584         var buf bytes.Buffer
585         if pathpkg.Ext(abspath) == ".go" {
586                 // Find markup links for this file (e.g. "/src/fmt/print.go").
587                 fi := p.Corpus.Analysis.FileInfo(abspath)
588                 buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
589                 buf.Write(marshalJSON(fi.Data))
590                 buf.WriteString(";</script>\n")
591
592                 if status := p.Corpus.Analysis.Status(); status != "" {
593                         buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
594                         // TODO(adonovan): show analysis status at per-file granularity.
595                         fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
596                 }
597
598                 buf.WriteString("<pre>")
599                 formatGoSource(&buf, src, fi.Links, h, s)
600                 buf.WriteString("</pre>")
601         } else {
602                 buf.WriteString("<pre>")
603                 FormatText(&buf, src, 1, false, h, s)
604                 buf.WriteString("</pre>")
605         }
606         fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
607
608         p.ServePage(w, Page{
609                 Title:    title,
610                 SrcPath:  relpath,
611                 Tabtitle: relpath,
612                 Body:     buf.Bytes(),
613                 GoogleCN: googleCN(r),
614         })
615 }
616
617 // formatGoSource HTML-escapes Go source text and writes it to w,
618 // decorating it with the specified analysis links.
619 //
620 func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
621         // Emit to a temp buffer so that we can add line anchors at the end.
622         saved, buf := buf, new(bytes.Buffer)
623
624         var i int
625         var link analysis.Link // shared state of the two funcs below
626         segmentIter := func() (seg Segment) {
627                 if i < len(links) {
628                         link = links[i]
629                         i++
630                         seg = Segment{link.Start(), link.End()}
631                 }
632                 return
633         }
634         linkWriter := func(w io.Writer, offs int, start bool) {
635                 link.Write(w, offs, start)
636         }
637
638         comments := tokenSelection(text, token.COMMENT)
639         var highlights Selection
640         if pattern != "" {
641                 highlights = regexpSelection(text, pattern)
642         }
643
644         FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
645
646         // Now copy buf to saved, adding line anchors.
647
648         // The lineSelection mechanism can't be composed with our
649         // linkWriter, so we have to add line spans as another pass.
650         n := 1
651         for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
652                 // The line numbers are inserted into the document via a CSS ::before
653                 // pseudo-element. This prevents them from being copied when users
654                 // highlight and copy text.
655                 // ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
656                 // This is also the trick Github uses to hide line numbers.
657                 //
658                 // The first tab for the code snippet needs to start in column 9, so
659                 // it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
660                 // character only indents a short amount.
661                 //
662                 // Due to rounding and font width Firefox might not treat 8 rendered
663                 // characters as 8 characters wide, and subsequently may treat the tab
664                 // character in the 9th position as moving the width from (7.5 or so) up
665                 // to 8. See
666                 // https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
667                 // for a fuller explanation. The solution is to add a CSS class to
668                 // explicitly declare the width to be 8 characters.
669                 fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d&nbsp;&nbsp;</span>`, n, n)
670                 n++
671                 saved.Write(line)
672                 saved.WriteByte('\n')
673         }
674 }
675
676 func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
677         if redirect(w, r) {
678                 return
679         }
680
681         list, err := p.Corpus.fs.ReadDir(abspath)
682         if err != nil {
683                 p.ServeError(w, r, relpath, err)
684                 return
685         }
686
687         p.ServePage(w, Page{
688                 Title:    "Directory",
689                 SrcPath:  relpath,
690                 Tabtitle: relpath,
691                 Body:     applyTemplate(p.DirlistHTML, "dirlistHTML", list),
692                 GoogleCN: googleCN(r),
693         })
694 }
695
696 func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
697         // get HTML body contents
698         src, err := vfs.ReadFile(p.Corpus.fs, abspath)
699         if err != nil {
700                 log.Printf("ReadFile: %s", err)
701                 p.ServeError(w, r, relpath, err)
702                 return
703         }
704
705         // if it begins with "<!DOCTYPE " assume it is standalone
706         // html that doesn't need the template wrapping.
707         if bytes.HasPrefix(src, doctype) {
708                 w.Write(src)
709                 return
710         }
711
712         // if it begins with a JSON blob, read in the metadata.
713         meta, src, err := extractMetadata(src)
714         if err != nil {
715                 log.Printf("decoding metadata %s: %v", relpath, err)
716         }
717
718         page := Page{
719                 Title:    meta.Title,
720                 Subtitle: meta.Subtitle,
721                 GoogleCN: googleCN(r),
722         }
723
724         // evaluate as template if indicated
725         if meta.Template {
726                 tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
727                 if err != nil {
728                         log.Printf("parsing template %s: %v", relpath, err)
729                         p.ServeError(w, r, relpath, err)
730                         return
731                 }
732                 var buf bytes.Buffer
733                 if err := tmpl.Execute(&buf, page); err != nil {
734                         log.Printf("executing template %s: %v", relpath, err)
735                         p.ServeError(w, r, relpath, err)
736                         return
737                 }
738                 src = buf.Bytes()
739         }
740
741         // if it's the language spec, add tags to EBNF productions
742         if strings.HasSuffix(abspath, "go_spec.html") {
743                 var buf bytes.Buffer
744                 Linkify(&buf, src)
745                 src = buf.Bytes()
746         }
747
748         page.Body = src
749         p.ServePage(w, page)
750 }
751
752 func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
753         p.serveFile(w, r)
754 }
755
756 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
757         if strings.HasSuffix(r.URL.Path, "/index.html") {
758                 // We'll show index.html for the directory.
759                 // Use the dir/ version as canonical instead of dir/index.html.
760                 http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
761                 return
762         }
763
764         // Check to see if we need to redirect or serve another file.
765         relpath := r.URL.Path
766         if m := p.Corpus.MetadataFor(relpath); m != nil {
767                 if m.Path != relpath {
768                         // Redirect to canonical path.
769                         http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
770                         return
771                 }
772                 // Serve from the actual filesystem path.
773                 relpath = m.filePath
774         }
775
776         abspath := relpath
777         relpath = relpath[1:] // strip leading slash
778
779         switch pathpkg.Ext(relpath) {
780         case ".html":
781                 p.ServeHTMLDoc(w, r, abspath, relpath)
782                 return
783
784         case ".go":
785                 p.serveTextFile(w, r, abspath, relpath, "Source file")
786                 return
787         }
788
789         dir, err := p.Corpus.fs.Lstat(abspath)
790         if err != nil {
791                 log.Print(err)
792                 p.ServeError(w, r, relpath, err)
793                 return
794         }
795
796         if dir != nil && dir.IsDir() {
797                 if redirect(w, r) {
798                         return
799                 }
800                 if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
801                         p.ServeHTMLDoc(w, r, index, index)
802                         return
803                 }
804                 p.serveDirectory(w, r, abspath, relpath)
805                 return
806         }
807
808         if util.IsTextFile(p.Corpus.fs, abspath) {
809                 if redirectFile(w, r) {
810                         return
811                 }
812                 p.serveTextFile(w, r, abspath, relpath, "Text file")
813                 return
814         }
815
816         p.fileServer.ServeHTTP(w, r)
817 }
818
819 func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
820         w.Header().Set("Content-Type", "text/plain; charset=utf-8")
821         w.Write(text)
822 }
823
824 func marshalJSON(x interface{}) []byte {
825         var data []byte
826         var err error
827         const indentJSON = false // for easier debugging
828         if indentJSON {
829                 data, err = json.MarshalIndent(x, "", "    ")
830         } else {
831                 data, err = json.Marshal(x)
832         }
833         if err != nil {
834                 panic(fmt.Sprintf("json.Marshal failed: %s", err))
835         }
836         return data
837 }