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