github.com/v2fly/tools@v0.100.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 "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 "github.com/v2fly/tools/godoc/analysis" 31 "github.com/v2fly/tools/godoc/util" 32 "github.com/v2fly/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 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 // 414 func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { 415 pkg := imports[path] 416 if pkg == nil { 417 // note that strings.LastIndex returns -1 if there is no "/" 418 pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:]) 419 pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import 420 imports[path] = pkg 421 } 422 return pkg, nil 423 } 424 425 // globalNames returns a set of the names declared by all package-level 426 // declarations. Method names are returned in the form Receiver_Method. 427 func globalNames(pkg *ast.Package) map[string]bool { 428 names := make(map[string]bool) 429 for _, file := range pkg.Files { 430 for _, decl := range file.Decls { 431 addNames(names, decl) 432 } 433 } 434 return names 435 } 436 437 // collectExamples collects examples for pkg from testfiles. 438 func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example { 439 var files []*ast.File 440 for _, f := range testfiles { 441 files = append(files, f) 442 } 443 444 var examples []*doc.Example 445 globals := globalNames(pkg) 446 for _, e := range doc.Examples(files...) { 447 name := stripExampleSuffix(e.Name) 448 if name == "" || globals[name] { 449 examples = append(examples, e) 450 } else if c.Verbose { 451 log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name) 452 } 453 } 454 455 return examples 456 } 457 458 // addNames adds the names declared by decl to the names set. 459 // Method names are added in the form ReceiverTypeName_Method. 460 func addNames(names map[string]bool, decl ast.Decl) { 461 switch d := decl.(type) { 462 case *ast.FuncDecl: 463 name := d.Name.Name 464 if d.Recv != nil { 465 var typeName string 466 switch r := d.Recv.List[0].Type.(type) { 467 case *ast.StarExpr: 468 typeName = r.X.(*ast.Ident).Name 469 case *ast.Ident: 470 typeName = r.Name 471 } 472 name = typeName + "_" + name 473 } 474 names[name] = true 475 case *ast.GenDecl: 476 for _, spec := range d.Specs { 477 switch s := spec.(type) { 478 case *ast.TypeSpec: 479 names[s.Name.Name] = true 480 case *ast.ValueSpec: 481 for _, id := range s.Names { 482 names[id.Name] = true 483 } 484 } 485 } 486 } 487 } 488 489 // packageExports is a local implementation of ast.PackageExports 490 // which correctly updates each package file's comment list. 491 // (The ast.PackageExports signature is frozen, hence the local 492 // implementation). 493 // 494 func packageExports(fset *token.FileSet, pkg *ast.Package) { 495 for _, src := range pkg.Files { 496 cmap := ast.NewCommentMap(fset, src, src.Comments) 497 ast.FileExports(src) 498 src.Comments = cmap.Filter(src).Comments() 499 } 500 } 501 502 func applyTemplate(t *template.Template, name string, data interface{}) []byte { 503 var buf bytes.Buffer 504 if err := t.Execute(&buf, data); err != nil { 505 log.Printf("%s.Execute: %s", name, err) 506 } 507 return buf.Bytes() 508 } 509 510 type writerCapturesErr struct { 511 w io.Writer 512 err error 513 } 514 515 func (w *writerCapturesErr) Write(p []byte) (int, error) { 516 n, err := w.w.Write(p) 517 if err != nil { 518 w.err = err 519 } 520 return n, err 521 } 522 523 // applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer 524 // for the call to template.Execute. It uses an io.Writer wrapper to capture 525 // errors from the underlying http.ResponseWriter. Errors are logged only when 526 // they come from the template processing and not the Writer; this avoid 527 // polluting log files with error messages due to networking issues, such as 528 // client disconnects and http HEAD protocol violations. 529 func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) { 530 w := &writerCapturesErr{w: rw} 531 err := t.Execute(w, data) 532 // There are some cases where template.Execute does not return an error when 533 // rw returns an error, and some where it does. So check w.err first. 534 if w.err == nil && err != nil { 535 // Log template errors. 536 log.Printf("%s.Execute: %s", t.Name(), err) 537 } 538 } 539 540 func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) { 541 canonical := pathpkg.Clean(r.URL.Path) 542 if !strings.HasSuffix(canonical, "/") { 543 canonical += "/" 544 } 545 if r.URL.Path != canonical { 546 url := *r.URL 547 url.Path = canonical 548 http.Redirect(w, r, url.String(), http.StatusMovedPermanently) 549 redirected = true 550 } 551 return 552 } 553 554 func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) { 555 c := pathpkg.Clean(r.URL.Path) 556 c = strings.TrimRight(c, "/") 557 if r.URL.Path != c { 558 url := *r.URL 559 url.Path = c 560 http.Redirect(w, r, url.String(), http.StatusMovedPermanently) 561 redirected = true 562 } 563 return 564 } 565 566 func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) { 567 src, err := vfs.ReadFile(p.Corpus.fs, abspath) 568 if err != nil { 569 log.Printf("ReadFile: %s", err) 570 p.ServeError(w, r, relpath, err) 571 return 572 } 573 574 if r.FormValue(PageInfoModeQueryString) == "text" { 575 p.ServeText(w, src) 576 return 577 } 578 579 h := r.FormValue("h") 580 s := RangeSelection(r.FormValue("s")) 581 582 var buf bytes.Buffer 583 if pathpkg.Ext(abspath) == ".go" { 584 // Find markup links for this file (e.g. "/src/fmt/print.go"). 585 fi := p.Corpus.Analysis.FileInfo(abspath) 586 buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ") 587 buf.Write(marshalJSON(fi.Data)) 588 buf.WriteString(";</script>\n") 589 590 if status := p.Corpus.Analysis.Status(); status != "" { 591 buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ") 592 // TODO(adonovan): show analysis status at per-file granularity. 593 fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status)) 594 } 595 596 buf.WriteString("<pre>") 597 formatGoSource(&buf, src, fi.Links, h, s) 598 buf.WriteString("</pre>") 599 } else { 600 buf.WriteString("<pre>") 601 FormatText(&buf, src, 1, false, h, s) 602 buf.WriteString("</pre>") 603 } 604 fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath)) 605 606 p.ServePage(w, Page{ 607 Title: title, 608 SrcPath: relpath, 609 Tabtitle: relpath, 610 Body: buf.Bytes(), 611 }) 612 } 613 614 // formatGoSource HTML-escapes Go source text and writes it to w, 615 // decorating it with the specified analysis links. 616 // 617 func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) { 618 // Emit to a temp buffer so that we can add line anchors at the end. 619 saved, buf := buf, new(bytes.Buffer) 620 621 var i int 622 var link analysis.Link // shared state of the two funcs below 623 segmentIter := func() (seg Segment) { 624 if i < len(links) { 625 link = links[i] 626 i++ 627 seg = Segment{link.Start(), link.End()} 628 } 629 return 630 } 631 linkWriter := func(w io.Writer, offs int, start bool) { 632 link.Write(w, offs, start) 633 } 634 635 comments := tokenSelection(text, token.COMMENT) 636 var highlights Selection 637 if pattern != "" { 638 highlights = regexpSelection(text, pattern) 639 } 640 641 FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection) 642 643 // Now copy buf to saved, adding line anchors. 644 645 // The lineSelection mechanism can't be composed with our 646 // linkWriter, so we have to add line spans as another pass. 647 n := 1 648 for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) { 649 // The line numbers are inserted into the document via a CSS ::before 650 // pseudo-element. This prevents them from being copied when users 651 // highlight and copy text. 652 // ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent 653 // This is also the trick Github uses to hide line numbers. 654 // 655 // The first tab for the code snippet needs to start in column 9, so 656 // it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab 657 // character only indents a short amount. 658 // 659 // Due to rounding and font width Firefox might not treat 8 rendered 660 // characters as 8 characters wide, and subsequently may treat the tab 661 // character in the 9th position as moving the width from (7.5 or so) up 662 // to 8. See 663 // https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091 664 // for a fuller explanation. The solution is to add a CSS class to 665 // explicitly declare the width to be 8 characters. 666 fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d </span>`, n, n) 667 n++ 668 saved.Write(line) 669 saved.WriteByte('\n') 670 } 671 } 672 673 func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { 674 if redirect(w, r) { 675 return 676 } 677 678 list, err := p.Corpus.fs.ReadDir(abspath) 679 if err != nil { 680 p.ServeError(w, r, relpath, err) 681 return 682 } 683 684 p.ServePage(w, Page{ 685 Title: "Directory", 686 SrcPath: relpath, 687 Tabtitle: relpath, 688 Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list), 689 }) 690 } 691 692 func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { 693 // get HTML body contents 694 isMarkdown := false 695 src, err := vfs.ReadFile(p.Corpus.fs, abspath) 696 if err != nil && strings.HasSuffix(abspath, ".html") { 697 if md, errMD := vfs.ReadFile(p.Corpus.fs, strings.TrimSuffix(abspath, ".html")+".md"); errMD == nil { 698 src = md 699 isMarkdown = true 700 err = nil 701 } 702 } 703 if err != nil { 704 log.Printf("ReadFile: %s", err) 705 p.ServeError(w, r, relpath, err) 706 return 707 } 708 709 // if it begins with "<!DOCTYPE " assume it is standalone 710 // html that doesn't need the template wrapping. 711 if bytes.HasPrefix(src, doctype) { 712 w.Write(src) 713 return 714 } 715 716 // if it begins with a JSON blob, read in the metadata. 717 meta, src, err := extractMetadata(src) 718 if err != nil { 719 log.Printf("decoding metadata %s: %v", relpath, err) 720 } 721 722 page := Page{ 723 Title: meta.Title, 724 Subtitle: meta.Subtitle, 725 } 726 727 // evaluate as template if indicated 728 if meta.Template { 729 tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src)) 730 if err != nil { 731 log.Printf("parsing template %s: %v", relpath, err) 732 p.ServeError(w, r, relpath, err) 733 return 734 } 735 var buf bytes.Buffer 736 if err := tmpl.Execute(&buf, page); err != nil { 737 log.Printf("executing template %s: %v", relpath, err) 738 p.ServeError(w, r, relpath, err) 739 return 740 } 741 src = buf.Bytes() 742 } 743 744 // Apply markdown as indicated. 745 // (Note template applies before Markdown.) 746 if isMarkdown { 747 html, err := renderMarkdown(src) 748 if err != nil { 749 log.Printf("executing markdown %s: %v", relpath, err) 750 p.ServeError(w, r, relpath, err) 751 return 752 } 753 src = html 754 } 755 756 // if it's the language spec, add tags to EBNF productions 757 if strings.HasSuffix(abspath, "go_spec.html") { 758 var buf bytes.Buffer 759 Linkify(&buf, src) 760 src = buf.Bytes() 761 } 762 763 page.Body = src 764 p.ServePage(w, page) 765 } 766 767 func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) { 768 p.serveFile(w, r) 769 } 770 771 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) { 772 if strings.HasSuffix(r.URL.Path, "/index.html") { 773 // We'll show index.html for the directory. 774 // Use the dir/ version as canonical instead of dir/index.html. 775 http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) 776 return 777 } 778 779 // Check to see if we need to redirect or serve another file. 780 relpath := r.URL.Path 781 if m := p.Corpus.MetadataFor(relpath); m != nil { 782 if m.Path != relpath { 783 // Redirect to canonical path. 784 http.Redirect(w, r, m.Path, http.StatusMovedPermanently) 785 return 786 } 787 // Serve from the actual filesystem path. 788 relpath = m.filePath 789 } 790 791 abspath := relpath 792 relpath = relpath[1:] // strip leading slash 793 794 switch pathpkg.Ext(relpath) { 795 case ".html": 796 p.ServeHTMLDoc(w, r, abspath, relpath) 797 return 798 799 case ".go": 800 p.serveTextFile(w, r, abspath, relpath, "Source file") 801 return 802 } 803 804 dir, err := p.Corpus.fs.Lstat(abspath) 805 if err != nil { 806 log.Print(err) 807 p.ServeError(w, r, relpath, err) 808 return 809 } 810 811 if dir != nil && dir.IsDir() { 812 if redirect(w, r) { 813 return 814 } 815 index := pathpkg.Join(abspath, "index.html") 816 if util.IsTextFile(p.Corpus.fs, index) || util.IsTextFile(p.Corpus.fs, pathpkg.Join(abspath, "index.md")) { 817 p.ServeHTMLDoc(w, r, index, index) 818 return 819 } 820 p.serveDirectory(w, r, abspath, relpath) 821 return 822 } 823 824 if util.IsTextFile(p.Corpus.fs, abspath) { 825 if redirectFile(w, r) { 826 return 827 } 828 p.serveTextFile(w, r, abspath, relpath, "Text file") 829 return 830 } 831 832 p.fileServer.ServeHTTP(w, r) 833 } 834 835 func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) { 836 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 837 w.Write(text) 838 } 839 840 func marshalJSON(x interface{}) []byte { 841 var data []byte 842 var err error 843 const indentJSON = false // for easier debugging 844 if indentJSON { 845 data, err = json.MarshalIndent(x, "", " ") 846 } else { 847 data, err = json.Marshal(x) 848 } 849 if err != nil { 850 panic(fmt.Sprintf("json.Marshal failed: %s", err)) 851 } 852 return data 853 }