--- /dev/null
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package godoc
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/doc"
+ "go/token"
+ htmlpkg "html"
+ htmltemplate "html/template"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ pathpkg "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "text/template"
+ "time"
+
+ "golang.org/x/tools/godoc/analysis"
+ "golang.org/x/tools/godoc/util"
+ "golang.org/x/tools/godoc/vfs"
+)
+
+// handlerServer is a migration from an old godoc http Handler type.
+// This should probably merge into something else.
+type handlerServer struct {
+ p *Presentation
+ c *Corpus // copy of p.Corpus
+ pattern string // url pattern; e.g. "/pkg/"
+ stripPrefix string // prefix to strip from import path; e.g. "pkg/"
+ fsRoot string // file system root to which the pattern is mapped; e.g. "/src"
+ exclude []string // file system paths to exclude; e.g. "/src/cmd"
+}
+
+func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
+ mux.Handle(s.pattern, s)
+}
+
+// GetPageInfo returns the PageInfo for a package directory abspath. If the
+// parameter genAST is set, an AST containing only the package exports is
+// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
+// is extracted from the AST. If there is no corresponding package in the
+// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
+// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
+// set to the respective error but the error is not logged.
+//
+func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
+ info := &PageInfo{Dirname: abspath, Mode: mode}
+
+ // Restrict to the package files that would be used when building
+ // the package on this system. This makes sure that if there are
+ // separate implementations for, say, Windows vs Unix, we don't
+ // jumble them all together.
+ // Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
+ // are used.
+ ctxt := build.Default
+ ctxt.IsAbsPath = pathpkg.IsAbs
+ ctxt.IsDir = func(path string) bool {
+ fi, err := h.c.fs.Stat(filepath.ToSlash(path))
+ return err == nil && fi.IsDir()
+ }
+ ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
+ f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
+ filtered := make([]os.FileInfo, 0, len(f))
+ for _, i := range f {
+ if mode&NoFiltering != 0 || i.Name() != "internal" {
+ filtered = append(filtered, i)
+ }
+ }
+ return filtered, err
+ }
+ ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
+ data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
+ if err != nil {
+ return nil, err
+ }
+ return ioutil.NopCloser(bytes.NewReader(data)), nil
+ }
+
+ // Make the syscall/js package always visible by default.
+ // It defaults to the host's GOOS/GOARCH, and golang.org's
+ // linux/amd64 means the wasm syscall/js package was blank.
+ // And you can't run godoc on js/wasm anyway, so host defaults
+ // don't make sense here.
+ if goos == "" && goarch == "" && relpath == "syscall/js" {
+ goos, goarch = "js", "wasm"
+ }
+ if goos != "" {
+ ctxt.GOOS = goos
+ }
+ if goarch != "" {
+ ctxt.GOARCH = goarch
+ }
+
+ pkginfo, err := ctxt.ImportDir(abspath, 0)
+ // continue if there are no Go source files; we still want the directory info
+ if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
+ info.Err = err
+ return info
+ }
+
+ // collect package files
+ pkgname := pkginfo.Name
+ pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
+ if len(pkgfiles) == 0 {
+ // Commands written in C have no .go files in the build.
+ // Instead, documentation may be found in an ignored file.
+ // The file may be ignored via an explicit +build ignore
+ // constraint (recommended), or by defining the package
+ // documentation (historic).
+ pkgname = "main" // assume package main since pkginfo.Name == ""
+ pkgfiles = pkginfo.IgnoredGoFiles
+ }
+
+ // get package information, if any
+ if len(pkgfiles) > 0 {
+ // build package AST
+ fset := token.NewFileSet()
+ files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
+ if err != nil {
+ info.Err = err
+ return info
+ }
+
+ // ignore any errors - they are due to unresolved identifiers
+ pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
+
+ // extract package documentation
+ info.FSet = fset
+ if mode&ShowSource == 0 {
+ // show extracted documentation
+ var m doc.Mode
+ if mode&NoFiltering != 0 {
+ m |= doc.AllDecls
+ }
+ if mode&AllMethods != 0 {
+ m |= doc.AllMethods
+ }
+ info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
+ if mode&NoTypeAssoc != 0 {
+ for _, t := range info.PDoc.Types {
+ info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
+ info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
+ info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
+ t.Consts = nil
+ t.Vars = nil
+ t.Funcs = nil
+ }
+ // for now we cannot easily sort consts and vars since
+ // go/doc.Value doesn't export the order information
+ sort.Sort(funcsByName(info.PDoc.Funcs))
+ }
+
+ // collect examples
+ testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
+ files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
+ if err != nil {
+ log.Println("parsing examples:", err)
+ }
+ info.Examples = collectExamples(h.c, pkg, files)
+
+ // collect any notes that we want to show
+ if info.PDoc.Notes != nil {
+ // could regexp.Compile only once per godoc, but probably not worth it
+ if rx := h.p.NotesRx; rx != nil {
+ for m, n := range info.PDoc.Notes {
+ if rx.MatchString(m) {
+ if info.Notes == nil {
+ info.Notes = make(map[string][]*doc.Note)
+ }
+ info.Notes[m] = n
+ }
+ }
+ }
+ }
+
+ } else {
+ // show source code
+ // TODO(gri) Consider eliminating export filtering in this mode,
+ // or perhaps eliminating the mode altogether.
+ if mode&NoFiltering == 0 {
+ packageExports(fset, pkg)
+ }
+ info.PAst = files
+ }
+ info.IsMain = pkgname == "main"
+ }
+
+ // get directory information, if any
+ var dir *Directory
+ var timestamp time.Time
+ if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
+ // directory tree is present; lookup respective directory
+ // (may still fail if the file system was updated and the
+ // new directory tree has not yet been computed)
+ dir = tree.(*Directory).lookup(abspath)
+ timestamp = ts
+ }
+ if dir == nil {
+ // TODO(agnivade): handle this case better, now since there is no CLI mode.
+ // no directory tree present (happens in command-line mode);
+ // compute 2 levels for this page. The second level is to
+ // get the synopses of sub-directories.
+ // note: cannot use path filter here because in general
+ // it doesn't contain the FSTree path
+ dir = h.c.newDirectory(abspath, 2)
+ timestamp = time.Now()
+ }
+ info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
+
+ info.DirTime = timestamp
+ info.DirFlat = mode&FlatDir != 0
+
+ return info
+}
+
+func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
+ // if the path is under one of the exclusion paths, don't list.
+ for _, e := range h.exclude {
+ if strings.HasPrefix(path, e) {
+ return false
+ }
+ }
+
+ // if the path includes 'internal', don't list unless we are in the NoFiltering mode.
+ if mode&NoFiltering != 0 {
+ return true
+ }
+ if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
+ for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
+ if c == "internal" || c == "vendor" {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+type funcsByName []*doc.Func
+
+func (s funcsByName) Len() int { return len(s) }
+func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
+
+func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if redirect(w, r) {
+ return
+ }
+
+ relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
+
+ if !h.corpusInitialized() {
+ h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
+ return
+ }
+
+ abspath := pathpkg.Join(h.fsRoot, relpath)
+ mode := h.p.GetPageInfoMode(r)
+ if relpath == builtinPkgPath {
+ // The fake built-in package contains unexported identifiers,
+ // but we want to show them. Also, disable type association,
+ // since it's not helpful for this fake package (see issue 6645).
+ mode |= NoFiltering | NoTypeAssoc
+ }
+ info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
+ if info.Err != nil {
+ log.Print(info.Err)
+ h.p.ServeError(w, r, relpath, info.Err)
+ return
+ }
+
+ var tabtitle, title, subtitle string
+ switch {
+ case info.PAst != nil:
+ for _, ast := range info.PAst {
+ tabtitle = ast.Name.Name
+ break
+ }
+ case info.PDoc != nil:
+ tabtitle = info.PDoc.Name
+ default:
+ tabtitle = info.Dirname
+ title = "Directory "
+ if h.p.ShowTimestamps {
+ subtitle = "Last update: " + info.DirTime.String()
+ }
+ }
+ if title == "" {
+ if info.IsMain {
+ // assume that the directory name is the command name
+ _, tabtitle = pathpkg.Split(relpath)
+ title = "Command "
+ } else {
+ title = "Package "
+ }
+ }
+ title += tabtitle
+
+ // special cases for top-level package/command directories
+ switch tabtitle {
+ case "/src":
+ title = "Packages"
+ tabtitle = "Packages"
+ case "/src/cmd":
+ title = "Commands"
+ tabtitle = "Commands"
+ }
+
+ // Emit JSON array for type information.
+ pi := h.c.Analysis.PackageInfo(relpath)
+ hasTreeView := len(pi.CallGraph) != 0
+ info.CallGraphIndex = pi.CallGraphIndex
+ info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
+ info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
+ info.TypeInfoIndex = make(map[string]int)
+ for i, ti := range pi.Types {
+ info.TypeInfoIndex[ti.Name] = i
+ }
+
+ info.GoogleCN = googleCN(r)
+ var body []byte
+ if info.Dirname == "/src" {
+ body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
+ } else {
+ body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
+ }
+ h.p.ServePage(w, Page{
+ Title: title,
+ Tabtitle: tabtitle,
+ Subtitle: subtitle,
+ Body: body,
+ GoogleCN: info.GoogleCN,
+ TreeView: hasTreeView,
+ })
+}
+
+func (h *handlerServer) corpusInitialized() bool {
+ h.c.initMu.RLock()
+ defer h.c.initMu.RUnlock()
+ return h.c.initDone
+}
+
+type PageInfoMode uint
+
+const (
+ PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
+
+ NoFiltering PageInfoMode = 1 << iota // do not filter exports
+ AllMethods // show all embedded methods
+ ShowSource // show source code, do not extract documentation
+ FlatDir // show directory in a flat (non-indented) manner
+ NoTypeAssoc // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
+)
+
+// modeNames defines names for each PageInfoMode flag.
+var modeNames = map[string]PageInfoMode{
+ "all": NoFiltering,
+ "methods": AllMethods,
+ "src": ShowSource,
+ "flat": FlatDir,
+}
+
+// generate a query string for persisting PageInfoMode between pages.
+func modeQueryString(mode PageInfoMode) string {
+ if modeNames := mode.names(); len(modeNames) > 0 {
+ return "?m=" + strings.Join(modeNames, ",")
+ }
+ return ""
+}
+
+// alphabetically sorted names of active flags for a PageInfoMode.
+func (m PageInfoMode) names() []string {
+ var names []string
+ for name, mode := range modeNames {
+ if m&mode != 0 {
+ names = append(names, name)
+ }
+ }
+ sort.Strings(names)
+ return names
+}
+
+// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
+// URL form value "m". It is value is a comma-separated list of mode names
+// as defined by modeNames (e.g.: m=src,text).
+func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
+ var mode PageInfoMode
+ for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
+ if m, found := modeNames[strings.TrimSpace(k)]; found {
+ mode |= m
+ }
+ }
+ if p.AdjustPageInfoMode != nil {
+ mode = p.AdjustPageInfoMode(r, mode)
+ }
+ return mode
+}
+
+// poorMansImporter returns a (dummy) package object named
+// by the last path component of the provided package path
+// (as is the convention for packages). This is sufficient
+// to resolve package identifiers without doing an actual
+// import. It never returns an error.
+//
+func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
+ pkg := imports[path]
+ if pkg == nil {
+ // note that strings.LastIndex returns -1 if there is no "/"
+ pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
+ pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
+ imports[path] = pkg
+ }
+ return pkg, nil
+}
+
+// globalNames returns a set of the names declared by all package-level
+// declarations. Method names are returned in the form Receiver_Method.
+func globalNames(pkg *ast.Package) map[string]bool {
+ names := make(map[string]bool)
+ for _, file := range pkg.Files {
+ for _, decl := range file.Decls {
+ addNames(names, decl)
+ }
+ }
+ return names
+}
+
+// collectExamples collects examples for pkg from testfiles.
+func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
+ var files []*ast.File
+ for _, f := range testfiles {
+ files = append(files, f)
+ }
+
+ var examples []*doc.Example
+ globals := globalNames(pkg)
+ for _, e := range doc.Examples(files...) {
+ name := stripExampleSuffix(e.Name)
+ if name == "" || globals[name] {
+ examples = append(examples, e)
+ } else if c.Verbose {
+ log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
+ }
+ }
+
+ return examples
+}
+
+// addNames adds the names declared by decl to the names set.
+// Method names are added in the form ReceiverTypeName_Method.
+func addNames(names map[string]bool, decl ast.Decl) {
+ switch d := decl.(type) {
+ case *ast.FuncDecl:
+ name := d.Name.Name
+ if d.Recv != nil {
+ var typeName string
+ switch r := d.Recv.List[0].Type.(type) {
+ case *ast.StarExpr:
+ typeName = r.X.(*ast.Ident).Name
+ case *ast.Ident:
+ typeName = r.Name
+ }
+ name = typeName + "_" + name
+ }
+ names[name] = true
+ case *ast.GenDecl:
+ for _, spec := range d.Specs {
+ switch s := spec.(type) {
+ case *ast.TypeSpec:
+ names[s.Name.Name] = true
+ case *ast.ValueSpec:
+ for _, id := range s.Names {
+ names[id.Name] = true
+ }
+ }
+ }
+ }
+}
+
+// packageExports is a local implementation of ast.PackageExports
+// which correctly updates each package file's comment list.
+// (The ast.PackageExports signature is frozen, hence the local
+// implementation).
+//
+func packageExports(fset *token.FileSet, pkg *ast.Package) {
+ for _, src := range pkg.Files {
+ cmap := ast.NewCommentMap(fset, src, src.Comments)
+ ast.FileExports(src)
+ src.Comments = cmap.Filter(src).Comments()
+ }
+}
+
+func applyTemplate(t *template.Template, name string, data interface{}) []byte {
+ var buf bytes.Buffer
+ if err := t.Execute(&buf, data); err != nil {
+ log.Printf("%s.Execute: %s", name, err)
+ }
+ return buf.Bytes()
+}
+
+type writerCapturesErr struct {
+ w io.Writer
+ err error
+}
+
+func (w *writerCapturesErr) Write(p []byte) (int, error) {
+ n, err := w.w.Write(p)
+ if err != nil {
+ w.err = err
+ }
+ return n, err
+}
+
+// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
+// for the call to template.Execute. It uses an io.Writer wrapper to capture
+// errors from the underlying http.ResponseWriter. Errors are logged only when
+// they come from the template processing and not the Writer; this avoid
+// polluting log files with error messages due to networking issues, such as
+// client disconnects and http HEAD protocol violations.
+func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
+ w := &writerCapturesErr{w: rw}
+ err := t.Execute(w, data)
+ // There are some cases where template.Execute does not return an error when
+ // rw returns an error, and some where it does. So check w.err first.
+ if w.err == nil && err != nil {
+ // Log template errors.
+ log.Printf("%s.Execute: %s", t.Name(), err)
+ }
+}
+
+func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
+ canonical := pathpkg.Clean(r.URL.Path)
+ if !strings.HasSuffix(canonical, "/") {
+ canonical += "/"
+ }
+ if r.URL.Path != canonical {
+ url := *r.URL
+ url.Path = canonical
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
+ redirected = true
+ }
+ return
+}
+
+func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
+ c := pathpkg.Clean(r.URL.Path)
+ c = strings.TrimRight(c, "/")
+ if r.URL.Path != c {
+ url := *r.URL
+ url.Path = c
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
+ redirected = true
+ }
+ return
+}
+
+func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
+ src, err := vfs.ReadFile(p.Corpus.fs, abspath)
+ if err != nil {
+ log.Printf("ReadFile: %s", err)
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+
+ if r.FormValue(PageInfoModeQueryString) == "text" {
+ p.ServeText(w, src)
+ return
+ }
+
+ h := r.FormValue("h")
+ s := RangeSelection(r.FormValue("s"))
+
+ var buf bytes.Buffer
+ if pathpkg.Ext(abspath) == ".go" {
+ // Find markup links for this file (e.g. "/src/fmt/print.go").
+ fi := p.Corpus.Analysis.FileInfo(abspath)
+ buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
+ buf.Write(marshalJSON(fi.Data))
+ buf.WriteString(";</script>\n")
+
+ if status := p.Corpus.Analysis.Status(); status != "" {
+ buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
+ // TODO(adonovan): show analysis status at per-file granularity.
+ fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
+ }
+
+ buf.WriteString("<pre>")
+ formatGoSource(&buf, src, fi.Links, h, s)
+ buf.WriteString("</pre>")
+ } else {
+ buf.WriteString("<pre>")
+ FormatText(&buf, src, 1, false, h, s)
+ buf.WriteString("</pre>")
+ }
+ fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
+
+ p.ServePage(w, Page{
+ Title: title,
+ SrcPath: relpath,
+ Tabtitle: relpath,
+ Body: buf.Bytes(),
+ GoogleCN: googleCN(r),
+ })
+}
+
+// formatGoSource HTML-escapes Go source text and writes it to w,
+// decorating it with the specified analysis links.
+//
+func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
+ // Emit to a temp buffer so that we can add line anchors at the end.
+ saved, buf := buf, new(bytes.Buffer)
+
+ var i int
+ var link analysis.Link // shared state of the two funcs below
+ segmentIter := func() (seg Segment) {
+ if i < len(links) {
+ link = links[i]
+ i++
+ seg = Segment{link.Start(), link.End()}
+ }
+ return
+ }
+ linkWriter := func(w io.Writer, offs int, start bool) {
+ link.Write(w, offs, start)
+ }
+
+ comments := tokenSelection(text, token.COMMENT)
+ var highlights Selection
+ if pattern != "" {
+ highlights = regexpSelection(text, pattern)
+ }
+
+ FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
+
+ // Now copy buf to saved, adding line anchors.
+
+ // The lineSelection mechanism can't be composed with our
+ // linkWriter, so we have to add line spans as another pass.
+ n := 1
+ for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
+ // The line numbers are inserted into the document via a CSS ::before
+ // pseudo-element. This prevents them from being copied when users
+ // highlight and copy text.
+ // ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
+ // This is also the trick Github uses to hide line numbers.
+ //
+ // The first tab for the code snippet needs to start in column 9, so
+ // it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
+ // character only indents a short amount.
+ //
+ // Due to rounding and font width Firefox might not treat 8 rendered
+ // characters as 8 characters wide, and subsequently may treat the tab
+ // character in the 9th position as moving the width from (7.5 or so) up
+ // to 8. See
+ // https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
+ // for a fuller explanation. The solution is to add a CSS class to
+ // explicitly declare the width to be 8 characters.
+ fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d </span>`, n, n)
+ n++
+ saved.Write(line)
+ saved.WriteByte('\n')
+ }
+}
+
+func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
+ if redirect(w, r) {
+ return
+ }
+
+ list, err := p.Corpus.fs.ReadDir(abspath)
+ if err != nil {
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+
+ p.ServePage(w, Page{
+ Title: "Directory",
+ SrcPath: relpath,
+ Tabtitle: relpath,
+ Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
+ GoogleCN: googleCN(r),
+ })
+}
+
+func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
+ // get HTML body contents
+ src, err := vfs.ReadFile(p.Corpus.fs, abspath)
+ if err != nil {
+ log.Printf("ReadFile: %s", err)
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+
+ // if it begins with "<!DOCTYPE " assume it is standalone
+ // html that doesn't need the template wrapping.
+ if bytes.HasPrefix(src, doctype) {
+ w.Write(src)
+ return
+ }
+
+ // if it begins with a JSON blob, read in the metadata.
+ meta, src, err := extractMetadata(src)
+ if err != nil {
+ log.Printf("decoding metadata %s: %v", relpath, err)
+ }
+
+ page := Page{
+ Title: meta.Title,
+ Subtitle: meta.Subtitle,
+ GoogleCN: googleCN(r),
+ }
+
+ // evaluate as template if indicated
+ if meta.Template {
+ tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
+ if err != nil {
+ log.Printf("parsing template %s: %v", relpath, err)
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, page); err != nil {
+ log.Printf("executing template %s: %v", relpath, err)
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+ src = buf.Bytes()
+ }
+
+ // if it's the language spec, add tags to EBNF productions
+ if strings.HasSuffix(abspath, "go_spec.html") {
+ var buf bytes.Buffer
+ Linkify(&buf, src)
+ src = buf.Bytes()
+ }
+
+ page.Body = src
+ p.ServePage(w, page)
+}
+
+func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
+ p.serveFile(w, r)
+}
+
+func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
+ if strings.HasSuffix(r.URL.Path, "/index.html") {
+ // We'll show index.html for the directory.
+ // Use the dir/ version as canonical instead of dir/index.html.
+ http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
+ return
+ }
+
+ // Check to see if we need to redirect or serve another file.
+ relpath := r.URL.Path
+ if m := p.Corpus.MetadataFor(relpath); m != nil {
+ if m.Path != relpath {
+ // Redirect to canonical path.
+ http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
+ return
+ }
+ // Serve from the actual filesystem path.
+ relpath = m.filePath
+ }
+
+ abspath := relpath
+ relpath = relpath[1:] // strip leading slash
+
+ switch pathpkg.Ext(relpath) {
+ case ".html":
+ p.ServeHTMLDoc(w, r, abspath, relpath)
+ return
+
+ case ".go":
+ p.serveTextFile(w, r, abspath, relpath, "Source file")
+ return
+ }
+
+ dir, err := p.Corpus.fs.Lstat(abspath)
+ if err != nil {
+ log.Print(err)
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+
+ if dir != nil && dir.IsDir() {
+ if redirect(w, r) {
+ return
+ }
+ if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
+ p.ServeHTMLDoc(w, r, index, index)
+ return
+ }
+ p.serveDirectory(w, r, abspath, relpath)
+ return
+ }
+
+ if util.IsTextFile(p.Corpus.fs, abspath) {
+ if redirectFile(w, r) {
+ return
+ }
+ p.serveTextFile(w, r, abspath, relpath, "Text file")
+ return
+ }
+
+ p.fileServer.ServeHTTP(w, r)
+}
+
+func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Write(text)
+}
+
+func marshalJSON(x interface{}) []byte {
+ var data []byte
+ var err error
+ const indentJSON = false // for easier debugging
+ if indentJSON {
+ data, err = json.MarshalIndent(x, "", " ")
+ } else {
+ data, err = json.Marshal(x)
+ }
+ if err != nil {
+ panic(fmt.Sprintf("json.Marshal failed: %s", err))
+ }
+ return data
+}