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