blob: 76fc48b45d93abfe8daa935aaeebed3eb76ff91f [file] [log] [blame]
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +10001// 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"
Alan Donovan80c4f062014-03-14 18:58:22 -04009 "encoding/json"
Brad Garciaefd232e2014-01-29 10:53:45 -050010 "expvar"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100011 "fmt"
12 "go/ast"
13 "go/build"
14 "go/doc"
15 "go/token"
16 htmlpkg "html"
Alan Donovan80c4f062014-03-14 18:58:22 -040017 htmltemplate "html/template"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100018 "io"
19 "io/ioutil"
20 "log"
21 "net/http"
22 "os"
23 pathpkg "path"
24 "path/filepath"
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +100025 "sort"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100026 "strings"
27 "text/template"
28 "time"
29
Alan Donovan80c4f062014-03-14 18:58:22 -040030 "code.google.com/p/go.tools/godoc/analysis"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100031 "code.google.com/p/go.tools/godoc/util"
32 "code.google.com/p/go.tools/godoc/vfs"
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100033)
34
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +100035// handlerServer is a migration from an old godoc http Handler type.
36// This should probably merge into something else.
37type handlerServer struct {
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +100038 p *Presentation
39 c *Corpus // copy of p.Corpus
40 pattern string // url pattern; e.g. "/pkg/"
41 fsRoot string // file system root to which the pattern is mapped
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100042}
43
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +100044func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100045 mux.Handle(s.pattern, s)
46}
47
48// getPageInfo returns the PageInfo for a package directory abspath. If the
49// parameter genAST is set, an AST containing only the package exports is
50// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
51// is extracted from the AST. If there is no corresponding package in the
52// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
53// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
54// set to the respective error but the error is not logged.
55//
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +100056func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100057 info := &PageInfo{Dirname: abspath}
58
59 // Restrict to the package files that would be used when building
60 // the package on this system. This makes sure that if there are
61 // separate implementations for, say, Windows vs Unix, we don't
62 // jumble them all together.
63 // Note: Uses current binary's GOOS/GOARCH.
64 // To use different pair, such as if we allowed the user to choose,
65 // set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir.
66 ctxt := build.Default
67 ctxt.IsAbsPath = pathpkg.IsAbs
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +100068 ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
69 return h.c.fs.ReadDir(filepath.ToSlash(dir))
70 }
71 ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
72 data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
73 if err != nil {
74 return nil, err
75 }
76 return ioutil.NopCloser(bytes.NewReader(data)), nil
77 }
78
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +100079 pkginfo, err := ctxt.ImportDir(abspath, 0)
80 // continue if there are no Go source files; we still want the directory info
81 if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
82 info.Err = err
83 return info
84 }
85
86 // collect package files
87 pkgname := pkginfo.Name
88 pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
89 if len(pkgfiles) == 0 {
90 // Commands written in C have no .go files in the build.
91 // Instead, documentation may be found in an ignored file.
92 // The file may be ignored via an explicit +build ignore
93 // constraint (recommended), or by defining the package
94 // documentation (historic).
95 pkgname = "main" // assume package main since pkginfo.Name == ""
96 pkgfiles = pkginfo.IgnoredGoFiles
97 }
98
99 // get package information, if any
100 if len(pkgfiles) > 0 {
101 // build package AST
102 fset := token.NewFileSet()
Brad Garcia2ca2bfc2014-01-30 06:28:19 -0500103 files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000104 if err != nil {
105 info.Err = err
106 return info
107 }
108
109 // ignore any errors - they are due to unresolved identifiers
110 pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
111
112 // extract package documentation
113 info.FSet = fset
114 if mode&ShowSource == 0 {
115 // show extracted documentation
116 var m doc.Mode
117 if mode&NoFiltering != 0 {
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +1000118 m |= doc.AllDecls
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000119 }
120 if mode&AllMethods != 0 {
121 m |= doc.AllMethods
122 }
123 info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
Robert Griesemer5351a1c2014-02-04 10:26:38 -0800124 if mode&NoTypeAssoc != 0 {
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +1000125 for _, t := range info.PDoc.Types {
Robert Griesemer5351a1c2014-02-04 10:26:38 -0800126 info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
127 info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +1000128 info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
Robert Griesemer5351a1c2014-02-04 10:26:38 -0800129 t.Consts = nil
130 t.Vars = nil
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +1000131 t.Funcs = nil
132 }
Robert Griesemer5351a1c2014-02-04 10:26:38 -0800133 // for now we cannot easily sort consts and vars since
134 // go/doc.Value doesn't export the order information
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +1000135 sort.Sort(funcsByName(info.PDoc.Funcs))
136 }
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000137
138 // collect examples
139 testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
Brad Garcia2ca2bfc2014-01-30 06:28:19 -0500140 files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000141 if err != nil {
142 log.Println("parsing examples:", err)
143 }
Brad Fitzpatrick88f792c2013-11-14 09:01:08 -0800144 info.Examples = collectExamples(h.c, pkg, files)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000145
146 // collect any notes that we want to show
147 if info.PDoc.Notes != nil {
148 // could regexp.Compile only once per godoc, but probably not worth it
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000149 if rx := h.p.NotesRx; rx != nil {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000150 for m, n := range info.PDoc.Notes {
151 if rx.MatchString(m) {
152 if info.Notes == nil {
153 info.Notes = make(map[string][]*doc.Note)
154 }
155 info.Notes[m] = n
156 }
157 }
158 }
159 }
160
161 } else {
162 // show source code
163 // TODO(gri) Consider eliminating export filtering in this mode,
164 // or perhaps eliminating the mode altogether.
165 if mode&NoFiltering == 0 {
166 packageExports(fset, pkg)
167 }
Brad Garcia2ca2bfc2014-01-30 06:28:19 -0500168 info.PAst = files
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000169 }
170 info.IsMain = pkgname == "main"
171 }
172
173 // get directory information, if any
174 var dir *Directory
175 var timestamp time.Time
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000176 if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000177 // directory tree is present; lookup respective directory
178 // (may still fail if the file system was updated and the
179 // new directory tree has not yet been computed)
180 dir = tree.(*Directory).lookup(abspath)
181 timestamp = ts
182 }
183 if dir == nil {
184 // no directory tree present (too early after startup or
185 // command-line mode); compute one level for this page
186 // note: cannot use path filter here because in general
187 // it doesn't contain the FSTree path
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000188 dir = h.c.newDirectory(abspath, 1)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000189 timestamp = time.Now()
190 }
191 info.Dirs = dir.listing(true)
192 info.DirTime = timestamp
193 info.DirFlat = mode&FlatDir != 0
194
195 return info
196}
197
Andrew Gerrand2e6fbd82013-08-28 09:39:02 +1000198type funcsByName []*doc.Func
199
200func (s funcsByName) Len() int { return len(s) }
201func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
202func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
203
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +1000204func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000205 if redirect(w, r) {
206 return
207 }
208
209 relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):])
210 abspath := pathpkg.Join(h.fsRoot, relpath)
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +1000211 mode := h.p.GetPageInfoMode(r)
212 if relpath == builtinPkgPath {
Robert Griesemer5351a1c2014-02-04 10:26:38 -0800213 mode = NoFiltering | NoTypeAssoc
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000214 }
215 info := h.GetPageInfo(abspath, relpath, mode)
216 if info.Err != nil {
217 log.Print(info.Err)
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000218 h.p.ServeError(w, r, relpath, info.Err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000219 return
220 }
221
222 if mode&NoHTML != 0 {
Brad Fitzpatrick66f0d6e2013-07-18 13:51:17 +1000223 h.p.ServeText(w, applyTemplate(h.p.PackageText, "packageText", info))
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000224 return
225 }
226
227 var tabtitle, title, subtitle string
228 switch {
229 case info.PAst != nil:
Brad Garcia2ca2bfc2014-01-30 06:28:19 -0500230 for _, ast := range info.PAst {
231 tabtitle = ast.Name.Name
232 break
233 }
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000234 case info.PDoc != nil:
235 tabtitle = info.PDoc.Name
236 default:
237 tabtitle = info.Dirname
238 title = "Directory "
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000239 if h.p.ShowTimestamps {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000240 subtitle = "Last update: " + info.DirTime.String()
241 }
242 }
243 if title == "" {
244 if info.IsMain {
245 // assume that the directory name is the command name
246 _, tabtitle = pathpkg.Split(relpath)
247 title = "Command "
248 } else {
249 title = "Package "
250 }
251 }
252 title += tabtitle
253
254 // special cases for top-level package/command directories
255 switch tabtitle {
256 case "/src/pkg":
257 tabtitle = "Packages"
258 case "/src/cmd":
259 tabtitle = "Commands"
260 }
261
Alan Donovan80c4f062014-03-14 18:58:22 -0400262 // Emit JSON array for type information.
263 // TODO(adonovan): issue a "pending..." message if results not ready.
264 var callGraph []*analysis.PCGNodeJSON
265 var typeInfos []*analysis.TypeInfoJSON
266 callGraph, info.CallGraphIndex, typeInfos = h.c.Analysis.PackageInfo(relpath)
267 info.CallGraph = htmltemplate.JS(marshalJSON(callGraph))
268 info.AnalysisData = htmltemplate.JS(marshalJSON(typeInfos))
269 info.TypeInfoIndex = make(map[string]int)
270 for i, ti := range typeInfos {
271 info.TypeInfoIndex[ti.Name] = i
272 }
273
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000274 h.p.ServePage(w, Page{
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000275 Title: title,
276 Tabtitle: tabtitle,
277 Subtitle: subtitle,
Brad Fitzpatrick66f0d6e2013-07-18 13:51:17 +1000278 Body: applyTemplate(h.p.PackageHTML, "packageHTML", info),
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000279 })
280}
281
282type PageInfoMode uint
283
284const (
Robert Griesemer5351a1c2014-02-04 10:26:38 -0800285 NoFiltering PageInfoMode = 1 << iota // do not filter exports
286 AllMethods // show all embedded methods
287 ShowSource // show source code, do not extract documentation
288 NoHTML // show result in textual form, do not generate HTML
289 FlatDir // show directory in a flat (non-indented) manner
290 NoTypeAssoc // don't associate consts, vars, and factory functions with types
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000291)
292
293// modeNames defines names for each PageInfoMode flag.
294var modeNames = map[string]PageInfoMode{
295 "all": NoFiltering,
296 "methods": AllMethods,
297 "src": ShowSource,
298 "text": NoHTML,
299 "flat": FlatDir,
300}
301
302// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
303// URL form value "m". It is value is a comma-separated list of mode names
304// as defined by modeNames (e.g.: m=src,text).
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +1000305func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000306 var mode PageInfoMode
307 for _, k := range strings.Split(r.FormValue("m"), ",") {
308 if m, found := modeNames[strings.TrimSpace(k)]; found {
309 mode |= m
310 }
311 }
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +1000312 if p.AdjustPageInfoMode != nil {
313 mode = p.AdjustPageInfoMode(r, mode)
314 }
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000315 return mode
316}
317
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000318// poorMansImporter returns a (dummy) package object named
319// by the last path component of the provided package path
320// (as is the convention for packages). This is sufficient
321// to resolve package identifiers without doing an actual
322// import. It never returns an error.
323//
324func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
325 pkg := imports[path]
326 if pkg == nil {
327 // note that strings.LastIndex returns -1 if there is no "/"
328 pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
329 pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
330 imports[path] = pkg
331 }
332 return pkg, nil
333}
334
335// globalNames returns a set of the names declared by all package-level
336// declarations. Method names are returned in the form Receiver_Method.
337func globalNames(pkg *ast.Package) map[string]bool {
338 names := make(map[string]bool)
339 for _, file := range pkg.Files {
340 for _, decl := range file.Decls {
341 addNames(names, decl)
342 }
343 }
344 return names
345}
346
347// collectExamples collects examples for pkg from testfiles.
Brad Fitzpatrick88f792c2013-11-14 09:01:08 -0800348func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000349 var files []*ast.File
350 for _, f := range testfiles {
351 files = append(files, f)
352 }
353
354 var examples []*doc.Example
355 globals := globalNames(pkg)
356 for _, e := range doc.Examples(files...) {
357 name := stripExampleSuffix(e.Name)
358 if name == "" || globals[name] {
359 examples = append(examples, e)
Brad Fitzpatrick88f792c2013-11-14 09:01:08 -0800360 } else if c.Verbose {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000361 log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
362 }
363 }
364
365 return examples
366}
367
368// addNames adds the names declared by decl to the names set.
369// Method names are added in the form ReceiverTypeName_Method.
370func addNames(names map[string]bool, decl ast.Decl) {
371 switch d := decl.(type) {
372 case *ast.FuncDecl:
373 name := d.Name.Name
374 if d.Recv != nil {
375 var typeName string
376 switch r := d.Recv.List[0].Type.(type) {
377 case *ast.StarExpr:
378 typeName = r.X.(*ast.Ident).Name
379 case *ast.Ident:
380 typeName = r.Name
381 }
382 name = typeName + "_" + name
383 }
384 names[name] = true
385 case *ast.GenDecl:
386 for _, spec := range d.Specs {
387 switch s := spec.(type) {
388 case *ast.TypeSpec:
389 names[s.Name.Name] = true
390 case *ast.ValueSpec:
391 for _, id := range s.Names {
392 names[id.Name] = true
393 }
394 }
395 }
396 }
397}
398
399// packageExports is a local implementation of ast.PackageExports
400// which correctly updates each package file's comment list.
401// (The ast.PackageExports signature is frozen, hence the local
402// implementation).
403//
404func packageExports(fset *token.FileSet, pkg *ast.Package) {
405 for _, src := range pkg.Files {
406 cmap := ast.NewCommentMap(fset, src, src.Comments)
407 ast.FileExports(src)
408 src.Comments = cmap.Filter(src).Comments()
409 }
410}
411
412func applyTemplate(t *template.Template, name string, data interface{}) []byte {
413 var buf bytes.Buffer
414 if err := t.Execute(&buf, data); err != nil {
415 log.Printf("%s.Execute: %s", name, err)
416 }
417 return buf.Bytes()
418}
419
Brad Garciaefd232e2014-01-29 10:53:45 -0500420type writerCapturesErr struct {
421 w io.Writer
422 err error
423}
424
425func (w *writerCapturesErr) Write(p []byte) (int, error) {
426 n, err := w.w.Write(p)
427 if err != nil {
428 w.err = err
429 }
430 return n, err
431}
432
433var httpErrors *expvar.Map
434
435func init() {
436 httpErrors = expvar.NewMap("httpWriteErrors").Init()
437}
438
439// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
440// for the call to template.Execute. It uses an io.Writer wrapper to capture
441// errors from the underlying http.ResponseWriter. If an error is found, an
442// expvar will be incremented. Other template errors will be logged. This is
443// done to keep from polluting log files with error messages due to networking
444// issues, such as client disconnects and http HEAD protocol violations.
445func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
446 w := &writerCapturesErr{w: rw}
447 err := t.Execute(w, data)
448 // There are some cases where template.Execute does not return an error when
449 // rw returns an error, and some where it does. So check w.err first.
450 if w.err != nil {
451 // For http errors, increment an expvar.
452 httpErrors.Add(w.err.Error(), 1)
453 } else if err != nil {
454 // Log template errors.
455 log.Printf("%s.Execute: %s", t.Name(), err)
456 }
457}
458
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000459func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
460 canonical := pathpkg.Clean(r.URL.Path)
461 if !strings.HasSuffix(canonical, "/") {
462 canonical += "/"
463 }
464 if r.URL.Path != canonical {
465 url := *r.URL
466 url.Path = canonical
467 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
468 redirected = true
469 }
470 return
471}
472
473func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
474 c := pathpkg.Clean(r.URL.Path)
475 c = strings.TrimRight(c, "/")
476 if r.URL.Path != c {
477 url := *r.URL
478 url.Path = c
479 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
480 redirected = true
481 }
482 return
483}
484
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000485func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
486 src, err := vfs.ReadFile(p.Corpus.fs, abspath)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000487 if err != nil {
488 log.Printf("ReadFile: %s", err)
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000489 p.ServeError(w, r, relpath, err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000490 return
491 }
492
493 if r.FormValue("m") == "text" {
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000494 p.ServeText(w, src)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000495 return
496 }
497
Alan Donovan80c4f062014-03-14 18:58:22 -0400498 h := r.FormValue("h")
499 s := RangeSelection(r.FormValue("s"))
500
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000501 var buf bytes.Buffer
Alan Donovan80c4f062014-03-14 18:58:22 -0400502 if pathpkg.Ext(abspath) == ".go" {
503 // Find markup links for this file (e.g. "/src/pkg/fmt/print.go").
504 data, links := p.Corpus.Analysis.FileInfo(abspath)
505 buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
506 buf.Write(marshalJSON(data))
507 buf.WriteString(";</script>\n")
508
Alan Donovan503140c2014-04-16 16:35:08 -0400509 // TODO(adonovan): indicate whether analysis is
510 // disabled, pending, completed or failed.
511 // For now, display help link only if 'completed'.
512 if links != nil {
513 buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a><br/>")
514 }
515
Alan Donovan80c4f062014-03-14 18:58:22 -0400516 buf.WriteString("<pre>")
517 formatGoSource(&buf, src, links, h, s)
518 buf.WriteString("</pre>")
519 } else {
520 buf.WriteString("<pre>")
521 FormatText(&buf, src, 1, false, h, s)
522 buf.WriteString("</pre>")
523 }
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000524 fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
525
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000526 p.ServePage(w, Page{
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000527 Title: title + " " + relpath,
528 Tabtitle: relpath,
529 Body: buf.Bytes(),
530 })
531}
532
Alan Donovan80c4f062014-03-14 18:58:22 -0400533// formatGoSource HTML-escapes Go source text and writes it to w,
534// decorating it with the specified analysis links.
535//
536func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
Alan Donovan7f24a832014-04-09 15:41:18 -0400537 // Emit to a temp buffer so that we can add line anchors at the end.
538 saved, buf := buf, new(bytes.Buffer)
539
Alan Donovan80c4f062014-03-14 18:58:22 -0400540 var i int
541 var link analysis.Link // shared state of the two funcs below
542 segmentIter := func() (seg Segment) {
543 if i < len(links) {
544 link = links[i]
545 i++
546 seg = Segment{link.Start(), link.End()}
547 }
548 return
549 }
550 linkWriter := func(w io.Writer, offs int, start bool) {
551 link.Write(w, offs, start)
552 }
553
554 comments := tokenSelection(text, token.COMMENT)
555 var highlights Selection
556 if pattern != "" {
557 highlights = regexpSelection(text, pattern)
558 }
559
560 FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
Alan Donovan7f24a832014-04-09 15:41:18 -0400561
562 // Now copy buf to saved, adding line anchors.
563
564 // The lineSelection mechanism can't be composed with our
565 // linkWriter, so we have to add line spans as another pass.
566 n := 1
567 for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
568 fmt.Fprintf(saved, "<span id=\"L%d\" class=\"ln\">%6d</span>\t", n, n)
569 n++
570 saved.Write(line)
571 saved.WriteByte('\n')
572 }
Alan Donovan80c4f062014-03-14 18:58:22 -0400573}
574
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000575func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000576 if redirect(w, r) {
577 return
578 }
579
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000580 list, err := p.Corpus.fs.ReadDir(abspath)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000581 if err != nil {
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000582 p.ServeError(w, r, relpath, err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000583 return
584 }
585
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000586 p.ServePage(w, Page{
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000587 Title: "Directory " + relpath,
588 Tabtitle: relpath,
Brad Fitzpatrick66f0d6e2013-07-18 13:51:17 +1000589 Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000590 })
591}
592
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000593func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000594 // get HTML body contents
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000595 src, err := vfs.ReadFile(p.Corpus.fs, abspath)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000596 if err != nil {
597 log.Printf("ReadFile: %s", err)
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000598 p.ServeError(w, r, relpath, err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000599 return
600 }
601
602 // if it begins with "<!DOCTYPE " assume it is standalone
603 // html that doesn't need the template wrapping.
604 if bytes.HasPrefix(src, doctype) {
605 w.Write(src)
606 return
607 }
608
609 // if it begins with a JSON blob, read in the metadata.
610 meta, src, err := extractMetadata(src)
611 if err != nil {
612 log.Printf("decoding metadata %s: %v", relpath, err)
613 }
614
615 // evaluate as template if indicated
616 if meta.Template {
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000617 tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000618 if err != nil {
619 log.Printf("parsing template %s: %v", relpath, err)
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000620 p.ServeError(w, r, relpath, err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000621 return
622 }
623 var buf bytes.Buffer
624 if err := tmpl.Execute(&buf, nil); err != nil {
625 log.Printf("executing template %s: %v", relpath, err)
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000626 p.ServeError(w, r, relpath, err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000627 return
628 }
629 src = buf.Bytes()
630 }
631
632 // if it's the language spec, add tags to EBNF productions
633 if strings.HasSuffix(abspath, "go_spec.html") {
634 var buf bytes.Buffer
635 Linkify(&buf, src)
636 src = buf.Bytes()
637 }
638
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000639 p.ServePage(w, Page{
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000640 Title: meta.Title,
641 Subtitle: meta.Subtitle,
642 Body: src,
643 })
644}
645
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000646func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
647 p.serveFile(w, r)
648}
649
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000650func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000651 relpath := r.URL.Path
652
653 // Check to see if we need to redirect or serve another file.
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000654 if m := p.Corpus.MetadataFor(relpath); m != nil {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000655 if m.Path != relpath {
656 // Redirect to canonical path.
657 http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
658 return
659 }
660 // Serve from the actual filesystem path.
661 relpath = m.filePath
662 }
663
664 abspath := relpath
665 relpath = relpath[1:] // strip leading slash
666
667 switch pathpkg.Ext(relpath) {
668 case ".html":
669 if strings.HasSuffix(relpath, "/index.html") {
670 // We'll show index.html for the directory.
671 // Use the dir/ version as canonical instead of dir/index.html.
672 http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
673 return
674 }
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000675 p.ServeHTMLDoc(w, r, abspath, relpath)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000676 return
677
678 case ".go":
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000679 p.serveTextFile(w, r, abspath, relpath, "Source file")
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000680 return
681 }
682
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000683 dir, err := p.Corpus.fs.Lstat(abspath)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000684 if err != nil {
685 log.Print(err)
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000686 p.ServeError(w, r, relpath, err)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000687 return
688 }
689
690 if dir != nil && dir.IsDir() {
691 if redirect(w, r) {
692 return
693 }
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000694 if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000695 p.ServeHTMLDoc(w, r, index, index)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000696 return
697 }
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000698 p.serveDirectory(w, r, abspath, relpath)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000699 return
700 }
701
Brad Fitzpatrick5395cfe2013-07-18 13:14:09 +1000702 if util.IsTextFile(p.Corpus.fs, abspath) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000703 if redirectFile(w, r) {
704 return
705 }
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000706 p.serveTextFile(w, r, abspath, relpath, "Text file")
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000707 return
708 }
709
Brad Fitzpatrick705bb7f2013-07-19 10:27:53 +1000710 p.fileServer.ServeHTTP(w, r)
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000711}
712
Brad Fitzpatrick4fc63232013-07-18 09:52:45 +1000713func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
Brad Fitzpatrickca3319f2013-07-17 17:17:12 +1000714 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
715 w.Write(text)
716}
Alan Donovan80c4f062014-03-14 18:58:22 -0400717
718func marshalJSON(x interface{}) []byte {
719 var data []byte
720 var err error
721 const indentJSON = false // for easier debugging
722 if indentJSON {
723 data, err = json.MarshalIndent(x, "", " ")
724 } else {
725 data, err = json.Marshal(x)
726 }
727 if err != nil {
728 panic(fmt.Sprintf("json.Marshal failed: %s", err))
729 }
730 return data
731}