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