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&nbsp;&nbsp;</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