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