github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/website/godoc.go (about) 1 /* 2 Copyright 2013 The Camlistore Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // This is a hacked-up version of godoc. 18 19 package main 20 21 import ( 22 "bytes" 23 "errors" 24 "fmt" 25 "go/ast" 26 "go/build" 27 "go/doc" 28 "go/parser" 29 "go/printer" 30 "go/token" 31 "io" 32 "io/ioutil" 33 "log" 34 "net/http" 35 "os" 36 pathpkg "path" 37 "path/filepath" 38 "regexp" 39 "strings" 40 "text/template" 41 "time" 42 ) 43 44 const ( 45 domainName = "camlistore.org" 46 pkgPattern = "/pkg/" 47 cmdPattern = "/cmd/" 48 fileembedPattern = "fileembed.go" 49 ) 50 51 var docRx = regexp.MustCompile(`^/((?:pkg|cmd)/([\w/]+?)(\.go)??)/?$`) 52 53 var tabwidth = 4 54 55 type PageInfo struct { 56 Dirname string // directory containing the package 57 Err error // error or nil 58 59 // package info 60 FSet *token.FileSet // nil if no package documentation 61 PDoc *doc.Package // nil if no package documentation 62 Examples []*doc.Example // nil if no example code 63 PAst *ast.File // nil if no AST with package exports 64 IsPkg bool // true for pkg, false for cmd 65 66 // directory info 67 Dirs *DirList // nil if no directory information 68 DirTime time.Time // directory time stamp 69 DirFlat bool // if set, show directory in a flat (non-indented) manner 70 PList []string // list of package names found 71 } 72 73 // godocFmap describes the template functions installed with all godoc templates. 74 // Convention: template function names ending in "_html" or "_url" produce 75 // HTML- or URL-escaped strings; all other function results may 76 // require explicit escaping in the template. 77 var godocFmap = template.FuncMap{ 78 // various helpers 79 "filename": filenameFunc, 80 "repeat": strings.Repeat, 81 82 // accss to FileInfos (directory listings) 83 "fileInfoName": fileInfoNameFunc, 84 "fileInfoTime": fileInfoTimeFunc, 85 86 // access to search result information 87 //"infoKind_html": infoKind_htmlFunc, 88 //"infoLine": infoLineFunc, 89 //"infoSnippet_html": infoSnippet_htmlFunc, 90 91 // formatting of AST nodes 92 "node": nodeFunc, 93 "node_html": node_htmlFunc, 94 "comment_html": comment_htmlFunc, 95 //"comment_text": comment_textFunc, 96 97 // support for URL attributes 98 "srcLink": srcLinkFunc, 99 "posLink_url": posLink_urlFunc, 100 101 // formatting of Examples 102 "example_html": example_htmlFunc, 103 "example_name": example_nameFunc, 104 "example_suffix": example_suffixFunc, 105 } 106 107 func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.FileSet) string { 108 return "" 109 } 110 111 func example_nameFunc(s string) string { 112 return "" 113 } 114 115 func example_suffixFunc(name string) string { 116 return "" 117 } 118 119 func filenameFunc(path string) string { 120 _, localname := pathpkg.Split(path) 121 return localname 122 } 123 124 func fileInfoNameFunc(fi os.FileInfo) string { 125 name := fi.Name() 126 if fi.IsDir() { 127 name += "/" 128 } 129 return name 130 } 131 132 func fileInfoTimeFunc(fi os.FileInfo) string { 133 if t := fi.ModTime(); t.Unix() != 0 { 134 return t.Local().String() 135 } 136 return "" // don't return epoch if time is obviously not set 137 } 138 139 // Write an AST node to w. 140 func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { 141 // convert trailing tabs into spaces using a tconv filter 142 // to ensure a good outcome in most browsers (there may still 143 // be tabs in comments and strings, but converting those into 144 // the right number of spaces is much harder) 145 // 146 // TODO(gri) rethink printer flags - perhaps tconv can be eliminated 147 // with an another printer mode (which is more efficiently 148 // implemented in the printer than here with another layer) 149 mode := printer.TabIndent | printer.UseSpaces 150 err := (&printer.Config{Mode: mode, Tabwidth: tabwidth}).Fprint(&tconv{output: w}, fset, x) 151 if err != nil { 152 log.Print(err) 153 } 154 } 155 156 func nodeFunc(node interface{}, fset *token.FileSet) string { 157 var buf bytes.Buffer 158 writeNode(&buf, fset, node) 159 return buf.String() 160 } 161 162 func node_htmlFunc(node interface{}, fset *token.FileSet) string { 163 var buf1 bytes.Buffer 164 writeNode(&buf1, fset, node) 165 var buf2 bytes.Buffer 166 FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) 167 return buf2.String() 168 } 169 170 func comment_htmlFunc(comment string) string { 171 var buf bytes.Buffer 172 // TODO(gri) Provide list of words (e.g. function parameters) 173 // to be emphasized by ToHTML. 174 doc.ToHTML(&buf, comment, nil) // does html-escaping 175 return buf.String() 176 } 177 178 func posLink_urlFunc(node ast.Node, fset *token.FileSet) string { 179 var relpath string 180 var line int 181 var low, high int // selection 182 183 if p := node.Pos(); p.IsValid() { 184 pos := fset.Position(p) 185 idx := strings.LastIndex(pos.Filename, domainName) 186 if idx == -1 { 187 log.Fatalf("No \"%s\" in path to file %s", domainName, pos.Filename) 188 } 189 relpath = pathpkg.Clean(pos.Filename[idx+len(domainName):]) 190 line = pos.Line 191 low = pos.Offset 192 } 193 if p := node.End(); p.IsValid() { 194 high = fset.Position(p).Offset 195 } 196 197 var buf bytes.Buffer 198 template.HTMLEscape(&buf, []byte(relpath)) 199 // selection ranges are of form "s=low:high" 200 if low < high { 201 fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping 202 // if we have a selection, position the page 203 // such that the selection is a bit below the top 204 line -= 10 205 if line < 1 { 206 line = 1 207 } 208 } 209 // line id's in html-printed source are of the 210 // form "L%d" where %d stands for the line number 211 if line > 0 { 212 fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping 213 } 214 215 return buf.String() 216 } 217 218 func srcLinkFunc(s string) string { 219 idx := strings.LastIndex(s, domainName) 220 if idx == -1 { 221 log.Fatalf("No \"%s\" in path to file %s", domainName, s) 222 } 223 return pathpkg.Clean(s[idx+len(domainName):]) 224 } 225 226 func (pi *PageInfo) populateDirs(diskPath string, depth int) { 227 var dir *Directory 228 dir = newDirectory(diskPath, depth) 229 pi.Dirs = dir.listing(true) 230 pi.DirTime = time.Now() 231 } 232 233 func getPageInfo(pkgName, diskPath string) (pi PageInfo, err error) { 234 if pkgName == pathpkg.Join(domainName, pkgPattern) || 235 pkgName == pathpkg.Join(domainName, cmdPattern) { 236 pi.Dirname = diskPath 237 pi.populateDirs(diskPath, -1) 238 return 239 } 240 bpkg, err := build.ImportDir(diskPath, 0) 241 if err != nil { 242 if _, ok := err.(*build.NoGoError); ok { 243 pi.populateDirs(diskPath, -1) 244 return pi, nil 245 } 246 return 247 } 248 inSet := make(map[string]bool) 249 for _, name := range bpkg.GoFiles { 250 if name == fileembedPattern { 251 continue 252 } 253 inSet[filepath.Base(name)] = true 254 } 255 256 pi.FSet = token.NewFileSet() 257 filter := func(fi os.FileInfo) bool { 258 return inSet[fi.Name()] 259 } 260 aPkgMap, err := parser.ParseDir(pi.FSet, diskPath, filter, parser.ParseComments) 261 if err != nil { 262 return 263 } 264 aPkg := aPkgMap[pathpkg.Base(pkgName)] 265 if aPkg == nil { 266 for _, v := range aPkgMap { 267 aPkg = v 268 break 269 } 270 if aPkg == nil { 271 err = errors.New("no apkg found?") 272 return 273 } 274 } 275 276 pi.Dirname = diskPath 277 pi.PDoc = doc.New(aPkg, pkgName, 0) 278 pi.IsPkg = strings.Contains(pkgName, domainName+pkgPattern) 279 280 // get directory information 281 pi.populateDirs(diskPath, -1) 282 return 283 } 284 285 const ( 286 indenting = iota 287 collecting 288 ) 289 290 // A tconv is an io.Writer filter for converting leading tabs into spaces. 291 type tconv struct { 292 output io.Writer 293 state int // indenting or collecting 294 indent int // valid if state == indenting 295 } 296 297 var spaces = []byte(" ") // 32 spaces seems like a good number 298 299 func (p *tconv) writeIndent() (err error) { 300 i := p.indent 301 for i >= len(spaces) { 302 i -= len(spaces) 303 if _, err = p.output.Write(spaces); err != nil { 304 return 305 } 306 } 307 // i < len(spaces) 308 if i > 0 { 309 _, err = p.output.Write(spaces[0:i]) 310 } 311 return 312 } 313 314 func (p *tconv) Write(data []byte) (n int, err error) { 315 if len(data) == 0 { 316 return 317 } 318 pos := 0 // valid if p.state == collecting 319 var b byte 320 for n, b = range data { 321 switch p.state { 322 case indenting: 323 switch b { 324 case '\t': 325 p.indent += tabwidth 326 case '\n': 327 p.indent = 0 328 if _, err = p.output.Write(data[n : n+1]); err != nil { 329 return 330 } 331 case ' ': 332 p.indent++ 333 default: 334 p.state = collecting 335 pos = n 336 if err = p.writeIndent(); err != nil { 337 return 338 } 339 } 340 case collecting: 341 if b == '\n' { 342 p.state = indenting 343 p.indent = 0 344 if _, err = p.output.Write(data[pos : n+1]); err != nil { 345 return 346 } 347 } 348 } 349 } 350 n = len(data) 351 if pos < n && p.state == collecting { 352 _, err = p.output.Write(data[pos:]) 353 } 354 return 355 } 356 357 func readTextTemplate(name string) *template.Template { 358 fileName := filepath.Join(*root, "tmpl", name) 359 data, err := ioutil.ReadFile(fileName) 360 if err != nil { 361 log.Fatalf("ReadFile %s: %v", fileName, err) 362 } 363 t, err := template.New(name).Funcs(godocFmap).Parse(string(data)) 364 if err != nil { 365 log.Fatalf("%s: %v", fileName, err) 366 } 367 return t 368 } 369 370 func applyTextTemplate(t *template.Template, name string, data interface{}) []byte { 371 var buf bytes.Buffer 372 if err := t.Execute(&buf, data); err != nil { 373 log.Printf("%s.Execute: %s", name, err) 374 } 375 return buf.Bytes() 376 } 377 378 func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) { 379 src, err := ioutil.ReadFile(abspath) 380 if err != nil { 381 log.Printf("ReadFile: %s", err) 382 serveError(w, r, relpath, err) 383 return 384 } 385 386 var buf bytes.Buffer 387 buf.WriteString("<pre>") 388 FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) 389 buf.WriteString("</pre>") 390 391 servePage(w, title, "", buf.Bytes()) 392 } 393 394 type godocHandler struct{} 395 396 func (godocHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 397 m := docRx.FindStringSubmatch(r.URL.Path) 398 suffix := "" 399 if m == nil { 400 if r.URL.Path != pkgPattern && r.URL.Path != cmdPattern { 401 http.NotFound(w, r) 402 return 403 } 404 suffix = r.URL.Path 405 } else { 406 suffix = m[1] 407 } 408 diskPath := filepath.Join(*root, "..", suffix) 409 410 switch pathpkg.Ext(suffix) { 411 case ".go": 412 serveTextFile(w, r, diskPath, suffix, "Source file") 413 return 414 } 415 416 pkgName := pathpkg.Join(domainName, suffix) 417 pi, err := getPageInfo(pkgName, diskPath) 418 if err != nil { 419 log.Print(err) 420 return 421 } 422 423 subtitle := pathpkg.Base(diskPath) 424 title := subtitle + " (" + pkgName + ")" 425 servePage(w, title, subtitle, applyTextTemplate(packageHTML, "packageHTML", pi)) 426 }