mirror of
https://github.com/golang/go
synced 2024-10-05 10:31:22 -06:00
6bf84214c1
Example: PACKAGE package utf8 import "unicode/utf8" Package utf8 implements functions and constants to support text encoded in UTF-8. This package calls a Unicode character a rune for brevity. CONSTANTS const ( RuneError = unicode.ReplacementChar // the "error" Rune or "replacement character". RuneSelf = 0x80 // characters below Runeself are represented as themselves in a single byte. UTFMax = 4 // maximum number of bytes of a UTF-8 encoded Unicode character. ) Numbers fundamental to the encoding. FUNCTIONS func DecodeLastRune(p []byte) (r rune, size int) DecodeLastRune unpacks the last UTF-8 encoding in p and returns the rune and its width in bytes. func DecodeLastRuneInString(s string) (r rune, size int) DecodeLastRuneInString is like DecodeLastRune but its input is a string. func DecodeRune(p []byte) (r rune, size int) DecodeRune unpacks the first UTF-8 encoding in p and returns the rune and its width in bytes. func DecodeRuneInString(s string) (r rune, size int) DecodeRuneInString is like DecodeRune but its input is a string. func EncodeRune(p []byte, r rune) int EncodeRune writes into p (which must be large enough) the UTF-8 encoding of the rune. It returns the number of bytes written. func FullRune(p []byte) bool FullRune reports whether the bytes in p begin with a full UTF-8 encoding of a rune. An invalid encoding is considered a full Rune since it will convert as a width-1 error rune. func FullRuneInString(s string) bool FullRuneInString is like FullRune but its input is a string. func RuneCount(p []byte) int RuneCount returns the number of runes in p. Erroneous and short encodings are treated as single runes of width 1 byte. func RuneCountInString(s string) (n int) RuneCountInString is like RuneCount but its input is a string. func RuneLen(r rune) int RuneLen returns the number of bytes required to encode the rune. func RuneStart(b byte) bool RuneStart reports whether the byte could be the first byte of an encoded rune. Second and subsequent bytes always have the top two bits set to 10. func Valid(p []byte) bool Valid reports whether p consists entirely of valid UTF-8-encoded runes. func ValidString(s string) bool ValidString reports whether s consists entirely of valid UTF-8-encoded runes. TYPES type String struct { // contains filtered or unexported fields } String wraps a regular string with a small structure that provides more efficient indexing by code point index, as opposed to byte index. Scanning incrementally forwards or backwards is O(1) per index operation (although not as fast a range clause going forwards). Random access is O(N) in the length of the string, but the overhead is less than always scanning from the beginning. If the string is ASCII, random access is O(1). Unlike the built-in string type, String has internal mutable state and is not thread-safe. func NewString(contents string) *String NewString returns a new UTF-8 string with the provided contents. func (s *String) At(i int) rune At returns the rune with index i in the String. The sequence of runes is the same as iterating over the contents with a "for range" clause. func (s *String) Init(contents string) *String Init initializes an existing String to hold the provided contents. It returns a pointer to the initialized String. func (s *String) IsASCII() bool IsASCII returns a boolean indicating whether the String contains only ASCII bytes. func (s *String) RuneCount() int RuneCount returns the number of runes (Unicode code points) in the String. func (s *String) Slice(i, j int) string Slice returns the string sliced at rune positions [i:j]. func (s *String) String() string String returns the contents of the String. This method also means the String is directly printable by fmt.Print. Fixes #2479. R=golang-dev, dsymonds, mattn.jp, r, gri, r CC=golang-dev https://golang.org/cl/5472051
1405 lines
39 KiB
Go
1405 lines
39 KiB
Go
// Copyright 2009 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 main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/doc"
|
|
"go/printer"
|
|
"go/token"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Globals
|
|
|
|
type delayTime struct {
|
|
RWValue
|
|
}
|
|
|
|
func (dt *delayTime) backoff(max time.Duration) {
|
|
dt.mutex.Lock()
|
|
v := dt.value.(time.Duration) * 2
|
|
if v > max {
|
|
v = max
|
|
}
|
|
dt.value = v
|
|
// don't change dt.timestamp - calling backoff indicates an error condition
|
|
dt.mutex.Unlock()
|
|
}
|
|
|
|
var (
|
|
verbose = flag.Bool("v", false, "verbose mode")
|
|
|
|
// file system roots
|
|
// TODO(gri) consider the invariant that goroot always end in '/'
|
|
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
|
|
testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)")
|
|
pkgPath = flag.String("path", "", "additional package directories (colon-separated)")
|
|
filter = flag.String("filter", "", "filter file containing permitted package directory paths")
|
|
filterMin = flag.Int("filter_minutes", 0, "filter file update interval in minutes; disabled if <= 0")
|
|
filterDelay delayTime // actual filter update interval in minutes; usually filterDelay == filterMin, but filterDelay may back off exponentially
|
|
|
|
// layout control
|
|
tabwidth = flag.Int("tabwidth", 4, "tab width")
|
|
showTimestamps = flag.Bool("timestamps", true, "show timestamps with directory listings")
|
|
templateDir = flag.String("templates", "", "directory containing alternate template files")
|
|
|
|
// search index
|
|
indexEnabled = flag.Bool("index", false, "enable search index")
|
|
indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+
|
|
"if not empty, the index is read from these files in sorted order")
|
|
maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown")
|
|
indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle")
|
|
|
|
// file system mapping
|
|
fs FileSystem // the underlying file system for godoc
|
|
fsHttp http.FileSystem // the underlying file system for http
|
|
fsMap Mapping // user-defined mapping
|
|
fsTree RWValue // *Directory tree of packages, updated with each sync
|
|
pathFilter RWValue // filter used when building fsMap directory trees
|
|
fsModified RWValue // timestamp of last call to invalidateIndex
|
|
|
|
// http handlers
|
|
fileServer http.Handler // default file server
|
|
cmdHandler httpHandler
|
|
pkgHandler httpHandler
|
|
)
|
|
|
|
func initHandlers() {
|
|
paths := filepath.SplitList(*pkgPath)
|
|
for _, t := range build.Path {
|
|
if t.Goroot {
|
|
continue
|
|
}
|
|
paths = append(paths, t.SrcDir())
|
|
}
|
|
fsMap.Init(paths)
|
|
|
|
fileServer = http.FileServer(fsHttp)
|
|
cmdHandler = httpHandler{"/cmd/", filepath.Join(*goroot, "src", "cmd"), false}
|
|
pkgHandler = httpHandler{"/pkg/", filepath.Join(*goroot, "src", "pkg"), true}
|
|
}
|
|
|
|
func registerPublicHandlers(mux *http.ServeMux) {
|
|
mux.Handle(cmdHandler.pattern, &cmdHandler)
|
|
mux.Handle(pkgHandler.pattern, &pkgHandler)
|
|
mux.HandleFunc("/doc/codewalk/", codewalk)
|
|
mux.HandleFunc("/search", search)
|
|
mux.Handle("/robots.txt", fileServer)
|
|
mux.HandleFunc("/opensearch.xml", serveSearchDesc)
|
|
mux.HandleFunc("/", serveFile)
|
|
}
|
|
|
|
func initFSTree() {
|
|
fsTree.set(newDirectory(filepath.Join(*goroot, *testDir), nil, -1))
|
|
invalidateIndex()
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Directory filters
|
|
|
|
// isParentOf returns true if p is a parent of (or the same as) q
|
|
// where p and q are directory paths.
|
|
func isParentOf(p, q string) bool {
|
|
n := len(p)
|
|
return strings.HasPrefix(q, p) && (len(q) <= n || q[n] == '/')
|
|
}
|
|
|
|
func setPathFilter(list []string) {
|
|
if len(list) == 0 {
|
|
pathFilter.set(nil)
|
|
return
|
|
}
|
|
|
|
// len(list) > 0
|
|
pathFilter.set(func(path string) bool {
|
|
// list is sorted in increasing order and for each path all its children are removed
|
|
i := sort.Search(len(list), func(i int) bool { return list[i] > path })
|
|
// Now we have list[i-1] <= path < list[i].
|
|
// Path may be a child of list[i-1] or a parent of list[i].
|
|
return i > 0 && isParentOf(list[i-1], path) || i < len(list) && isParentOf(path, list[i])
|
|
})
|
|
}
|
|
|
|
func getPathFilter() func(string) bool {
|
|
f, _ := pathFilter.get()
|
|
if f != nil {
|
|
return f.(func(string) bool)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readDirList reads a file containing a newline-separated list
|
|
// of directory paths and returns the list of paths.
|
|
func readDirList(filename string) ([]string, error) {
|
|
contents, err := ReadFile(fs, filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// create a sorted list of valid directory names
|
|
filter := func(path string) bool {
|
|
d, e := fs.Lstat(path)
|
|
if e != nil && err == nil {
|
|
// remember first error and return it from readDirList
|
|
// so we have at least some information if things go bad
|
|
err = e
|
|
}
|
|
return e == nil && isPkgDir(d)
|
|
}
|
|
list := canonicalizePaths(strings.Split(string(contents), "\n"), filter)
|
|
// for each parent path, remove all its children q
|
|
// (requirement for binary search to work when filtering)
|
|
i := 0
|
|
for _, q := range list {
|
|
if i == 0 || !isParentOf(list[i-1], q) {
|
|
list[i] = q
|
|
i++
|
|
}
|
|
}
|
|
return list[0:i], err
|
|
}
|
|
|
|
// updateMappedDirs computes the directory tree for
|
|
// each user-defined file system mapping. If a filter
|
|
// is provided, it is used to filter directories.
|
|
//
|
|
func updateMappedDirs(filter func(string) bool) {
|
|
if !fsMap.IsEmpty() {
|
|
fsMap.Iterate(func(path string, value *RWValue) bool {
|
|
value.set(newDirectory(path, filter, -1))
|
|
return true
|
|
})
|
|
invalidateIndex()
|
|
}
|
|
}
|
|
|
|
func updateFilterFile() {
|
|
updateMappedDirs(nil) // no filter for accuracy
|
|
|
|
// collect directory tree leaf node paths
|
|
var buf bytes.Buffer
|
|
fsMap.Iterate(func(_ string, value *RWValue) bool {
|
|
v, _ := value.get()
|
|
if v != nil && v.(*Directory) != nil {
|
|
v.(*Directory).writeLeafs(&buf)
|
|
}
|
|
return true
|
|
})
|
|
|
|
// update filter file
|
|
if err := writeFileAtomically(*filter, buf.Bytes()); err != nil {
|
|
log.Printf("writeFileAtomically(%s): %s", *filter, err)
|
|
filterDelay.backoff(24 * time.Hour) // back off exponentially, but try at least once a day
|
|
} else {
|
|
filterDelay.set(*filterMin) // revert to regular filter update schedule
|
|
}
|
|
}
|
|
|
|
func initDirTrees() {
|
|
// setup initial path filter
|
|
if *filter != "" {
|
|
list, err := readDirList(*filter)
|
|
if err != nil {
|
|
log.Printf("readDirList(%s): %s", *filter, err)
|
|
}
|
|
if *verbose || len(list) == 0 {
|
|
log.Printf("found %d directory paths in file %s", len(list), *filter)
|
|
}
|
|
setPathFilter(list)
|
|
}
|
|
|
|
go updateMappedDirs(getPathFilter()) // use filter for speed
|
|
|
|
// start filter update goroutine, if enabled.
|
|
if *filter != "" && *filterMin > 0 {
|
|
filterDelay.set(time.Duration(*filterMin) * time.Minute) // initial filter update delay
|
|
go func() {
|
|
for {
|
|
if *verbose {
|
|
log.Printf("start update of %s", *filter)
|
|
}
|
|
updateFilterFile()
|
|
delay, _ := filterDelay.get()
|
|
dt := delay.(time.Duration)
|
|
if *verbose {
|
|
log.Printf("next filter update in %s", dt)
|
|
}
|
|
time.Sleep(dt)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Path mapping
|
|
|
|
// Absolute paths are file system paths (backslash-separated on Windows),
|
|
// but relative paths are always slash-separated.
|
|
|
|
func absolutePath(relpath, defaultRoot string) string {
|
|
abspath := fsMap.ToAbsolute(relpath)
|
|
if abspath == "" {
|
|
// no user-defined mapping found; use default mapping
|
|
abspath = filepath.Join(defaultRoot, filepath.FromSlash(relpath))
|
|
}
|
|
return abspath
|
|
}
|
|
|
|
func relativeURL(abspath string) string {
|
|
relpath := fsMap.ToRelative(abspath)
|
|
if relpath == "" {
|
|
// prefix must end in a path separator
|
|
prefix := *goroot
|
|
if len(prefix) > 0 && prefix[len(prefix)-1] != filepath.Separator {
|
|
prefix += string(filepath.Separator)
|
|
}
|
|
if strings.HasPrefix(abspath, prefix) {
|
|
// no user-defined mapping found; use default mapping
|
|
relpath = filepath.ToSlash(abspath[len(prefix):])
|
|
}
|
|
}
|
|
// Only if path is an invalid absolute path is relpath == ""
|
|
// at this point. This should never happen since absolute paths
|
|
// are only created via godoc for files that do exist. However,
|
|
// it is ok to return ""; it will simply provide a link to the
|
|
// top of the pkg or src directories.
|
|
return relpath
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Tab conversion
|
|
|
|
var spaces = []byte(" ") // 32 spaces seems like a good number
|
|
|
|
const (
|
|
indenting = iota
|
|
collecting
|
|
)
|
|
|
|
// A tconv is an io.Writer filter for converting leading tabs into spaces.
|
|
type tconv struct {
|
|
output io.Writer
|
|
state int // indenting or collecting
|
|
indent int // valid if state == indenting
|
|
}
|
|
|
|
func (p *tconv) writeIndent() (err error) {
|
|
i := p.indent
|
|
for i >= len(spaces) {
|
|
i -= len(spaces)
|
|
if _, err = p.output.Write(spaces); err != nil {
|
|
return
|
|
}
|
|
}
|
|
// i < len(spaces)
|
|
if i > 0 {
|
|
_, err = p.output.Write(spaces[0:i])
|
|
}
|
|
return
|
|
}
|
|
|
|
func (p *tconv) Write(data []byte) (n int, err error) {
|
|
if len(data) == 0 {
|
|
return
|
|
}
|
|
pos := 0 // valid if p.state == collecting
|
|
var b byte
|
|
for n, b = range data {
|
|
switch p.state {
|
|
case indenting:
|
|
switch b {
|
|
case '\t':
|
|
p.indent += *tabwidth
|
|
case '\n':
|
|
p.indent = 0
|
|
if _, err = p.output.Write(data[n : n+1]); err != nil {
|
|
return
|
|
}
|
|
case ' ':
|
|
p.indent++
|
|
default:
|
|
p.state = collecting
|
|
pos = n
|
|
if err = p.writeIndent(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
case collecting:
|
|
if b == '\n' {
|
|
p.state = indenting
|
|
p.indent = 0
|
|
if _, err = p.output.Write(data[pos : n+1]); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
n = len(data)
|
|
if pos < n && p.state == collecting {
|
|
_, err = p.output.Write(data[pos:])
|
|
}
|
|
return
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Templates
|
|
|
|
// Write an AST node to w.
|
|
func writeNode(w io.Writer, fset *token.FileSet, x interface{}) {
|
|
// convert trailing tabs into spaces using a tconv filter
|
|
// to ensure a good outcome in most browsers (there may still
|
|
// be tabs in comments and strings, but converting those into
|
|
// the right number of spaces is much harder)
|
|
//
|
|
// TODO(gri) rethink printer flags - perhaps tconv can be eliminated
|
|
// with an another printer mode (which is more efficiently
|
|
// implemented in the printer than here with another layer)
|
|
mode := printer.TabIndent | printer.UseSpaces
|
|
(&printer.Config{mode, *tabwidth}).Fprint(&tconv{output: w}, fset, x)
|
|
}
|
|
|
|
func filenameFunc(path string) string {
|
|
_, localname := filepath.Split(path)
|
|
return localname
|
|
}
|
|
|
|
func fileInfoNameFunc(fi os.FileInfo) string {
|
|
name := fi.Name()
|
|
if fi.IsDir() {
|
|
name += "/"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func fileInfoTimeFunc(fi os.FileInfo) string {
|
|
if t := fi.ModTime(); t.Unix() != 0 {
|
|
return t.Local().String()
|
|
}
|
|
return "" // don't return epoch if time is obviously not set
|
|
}
|
|
|
|
// The strings in infoKinds must be properly html-escaped.
|
|
var infoKinds = [nKinds]string{
|
|
PackageClause: "package clause",
|
|
ImportDecl: "import decl",
|
|
ConstDecl: "const decl",
|
|
TypeDecl: "type decl",
|
|
VarDecl: "var decl",
|
|
FuncDecl: "func decl",
|
|
MethodDecl: "method decl",
|
|
Use: "use",
|
|
}
|
|
|
|
func infoKind_htmlFunc(info SpotInfo) string {
|
|
return infoKinds[info.Kind()] // infoKind entries are html-escaped
|
|
}
|
|
|
|
func infoLineFunc(info SpotInfo) int {
|
|
line := info.Lori()
|
|
if info.IsIndex() {
|
|
index, _ := searchIndex.get()
|
|
if index != nil {
|
|
line = index.(*Index).Snippet(line).Line
|
|
} else {
|
|
// no line information available because
|
|
// we don't have an index - this should
|
|
// never happen; be conservative and don't
|
|
// crash
|
|
line = 0
|
|
}
|
|
}
|
|
return line
|
|
}
|
|
|
|
func infoSnippet_htmlFunc(info SpotInfo) string {
|
|
if info.IsIndex() {
|
|
index, _ := searchIndex.get()
|
|
// Snippet.Text was HTML-escaped when it was generated
|
|
return index.(*Index).Snippet(info.Lori()).Text
|
|
}
|
|
return `<span class="alert">no snippet text available</span>`
|
|
}
|
|
|
|
func nodeFunc(node interface{}, fset *token.FileSet) string {
|
|
var buf bytes.Buffer
|
|
writeNode(&buf, fset, node)
|
|
return buf.String()
|
|
}
|
|
|
|
func node_htmlFunc(node interface{}, fset *token.FileSet) string {
|
|
var buf1 bytes.Buffer
|
|
writeNode(&buf1, fset, node)
|
|
var buf2 bytes.Buffer
|
|
FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
|
|
return buf2.String()
|
|
}
|
|
|
|
func comment_htmlFunc(comment string) string {
|
|
var buf bytes.Buffer
|
|
// TODO(gri) Provide list of words (e.g. function parameters)
|
|
// to be emphasized by ToHTML.
|
|
doc.ToHTML(&buf, comment, nil) // does html-escaping
|
|
return buf.String()
|
|
}
|
|
|
|
// punchCardWidth is the number of columns of fixed-width
|
|
// characters to assume when wrapping text. Very few people
|
|
// use terminals or cards smaller than 80 characters, so 80 it is.
|
|
// We do not try to sniff the environment or the tty to adapt to
|
|
// the situation; instead, by using a constant we make sure that
|
|
// godoc always produces the same output regardless of context,
|
|
// a consistency that is lost otherwise. For example, if we sniffed
|
|
// the environment or tty, then http://golang.org/pkg/math/?m=text
|
|
// would depend on the width of the terminal where godoc started,
|
|
// which is clearly bogus. More generally, the Unix tools that behave
|
|
// differently when writing to a tty than when writing to a file have
|
|
// a history of causing confusion (compare `ls` and `ls | cat`), and we
|
|
// want to avoid that mistake here.
|
|
const punchCardWidth = 80
|
|
|
|
func comment_textFunc(comment, indent, preIndent string) string {
|
|
var buf bytes.Buffer
|
|
doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent))
|
|
return buf.String()
|
|
}
|
|
|
|
func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.FileSet) string {
|
|
var buf bytes.Buffer
|
|
for _, eg := range examples {
|
|
// accept Foo or Foo_.* for funcName == Foo
|
|
name := eg.Name
|
|
if i := strings.Index(name, "_"); i >= 0 {
|
|
name = name[:i]
|
|
}
|
|
if name != funcName {
|
|
continue
|
|
}
|
|
|
|
// print code, unindent and remove surrounding braces
|
|
code := node_htmlFunc(eg.Body, fset)
|
|
code = strings.Replace(code, "\n ", "\n", -1)
|
|
code = code[2 : len(code)-2]
|
|
|
|
err := exampleHTML.Execute(&buf, struct {
|
|
Code, Output string
|
|
}{code, eg.Output})
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func pkgLinkFunc(path string) string {
|
|
relpath := relativeURL(path)
|
|
// because of the irregular mapping under goroot
|
|
// we need to correct certain relative paths
|
|
if strings.HasPrefix(relpath, "src/pkg/") {
|
|
relpath = relpath[len("src/pkg/"):]
|
|
}
|
|
return pkgHandler.pattern[1:] + relpath // remove trailing '/' for relative URL
|
|
}
|
|
|
|
func posLink_urlFunc(node ast.Node, fset *token.FileSet) string {
|
|
var relpath string
|
|
var line int
|
|
var low, high int // selection
|
|
|
|
if p := node.Pos(); p.IsValid() {
|
|
pos := fset.Position(p)
|
|
relpath = relativeURL(pos.Filename)
|
|
line = pos.Line
|
|
low = pos.Offset
|
|
}
|
|
if p := node.End(); p.IsValid() {
|
|
high = fset.Position(p).Offset
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
template.HTMLEscape(&buf, []byte(relpath))
|
|
// selection ranges are of form "s=low:high"
|
|
if low < high {
|
|
fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
|
|
// if we have a selection, position the page
|
|
// such that the selection is a bit below the top
|
|
line -= 10
|
|
if line < 1 {
|
|
line = 1
|
|
}
|
|
}
|
|
// line id's in html-printed source are of the
|
|
// form "L%d" where %d stands for the line number
|
|
if line > 0 {
|
|
fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// fmap describes the template functions installed with all godoc templates.
|
|
// Convention: template function names ending in "_html" or "_url" produce
|
|
// HTML- or URL-escaped strings; all other function results may
|
|
// require explicit escaping in the template.
|
|
var fmap = template.FuncMap{
|
|
// various helpers
|
|
"filename": filenameFunc,
|
|
"repeat": strings.Repeat,
|
|
|
|
// accss to FileInfos (directory listings)
|
|
"fileInfoName": fileInfoNameFunc,
|
|
"fileInfoTime": fileInfoTimeFunc,
|
|
|
|
// access to search result information
|
|
"infoKind_html": infoKind_htmlFunc,
|
|
"infoLine": infoLineFunc,
|
|
"infoSnippet_html": infoSnippet_htmlFunc,
|
|
|
|
// formatting of AST nodes
|
|
"node": nodeFunc,
|
|
"node_html": node_htmlFunc,
|
|
"comment_html": comment_htmlFunc,
|
|
"comment_text": comment_textFunc,
|
|
|
|
// support for URL attributes
|
|
"pkgLink": pkgLinkFunc,
|
|
"srcLink": relativeURL,
|
|
"posLink_url": posLink_urlFunc,
|
|
|
|
// formatting of Examples
|
|
"example_html": example_htmlFunc,
|
|
}
|
|
|
|
func readTemplate(name string) *template.Template {
|
|
path := filepath.Join(*goroot, "lib", "godoc", name)
|
|
if *templateDir != "" {
|
|
defaultpath := path
|
|
path = filepath.Join(*templateDir, name)
|
|
if _, err := fs.Stat(path); err != nil {
|
|
log.Print("readTemplate:", err)
|
|
path = defaultpath
|
|
}
|
|
}
|
|
|
|
// use underlying file system fs to read the template file
|
|
// (cannot use template ParseFile functions directly)
|
|
data, err := ReadFile(fs, path)
|
|
if err != nil {
|
|
log.Fatal("readTemplate: ", err)
|
|
}
|
|
// be explicit with errors (for app engine use)
|
|
t, err := template.New(name).Funcs(fmap).Parse(string(data))
|
|
if err != nil {
|
|
log.Fatal("readTemplate: ", err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
var (
|
|
codewalkHTML,
|
|
codewalkdirHTML,
|
|
dirlistHTML,
|
|
errorHTML,
|
|
exampleHTML,
|
|
godocHTML,
|
|
packageHTML,
|
|
packageText,
|
|
searchHTML,
|
|
searchText,
|
|
searchDescXML *template.Template
|
|
)
|
|
|
|
func readTemplates() {
|
|
// have to delay until after flags processing since paths depend on goroot
|
|
codewalkHTML = readTemplate("codewalk.html")
|
|
codewalkdirHTML = readTemplate("codewalkdir.html")
|
|
dirlistHTML = readTemplate("dirlist.html")
|
|
errorHTML = readTemplate("error.html")
|
|
exampleHTML = readTemplate("example.html")
|
|
godocHTML = readTemplate("godoc.html")
|
|
packageHTML = readTemplate("package.html")
|
|
packageText = readTemplate("package.txt")
|
|
searchHTML = readTemplate("search.html")
|
|
searchText = readTemplate("search.txt")
|
|
searchDescXML = readTemplate("opensearch.xml")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Generic HTML wrapper
|
|
|
|
func servePage(w http.ResponseWriter, title, subtitle, query string, content []byte) {
|
|
d := struct {
|
|
Title string
|
|
Subtitle string
|
|
PkgRoots []string
|
|
SearchBox bool
|
|
Query string
|
|
Version string
|
|
Menu []byte
|
|
Content []byte
|
|
}{
|
|
title,
|
|
subtitle,
|
|
fsMap.PrefixList(),
|
|
*indexEnabled,
|
|
query,
|
|
runtime.Version(),
|
|
nil,
|
|
content,
|
|
}
|
|
|
|
if err := godocHTML.Execute(w, &d); err != nil {
|
|
log.Printf("godocHTML.Execute: %s", err)
|
|
}
|
|
}
|
|
|
|
func serveText(w http.ResponseWriter, text []byte) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Write(text)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Files
|
|
|
|
var (
|
|
titleRx = regexp.MustCompile(`<!-- title ([^\-]*)-->`)
|
|
subtitleRx = regexp.MustCompile(`<!-- subtitle ([^\-]*)-->`)
|
|
firstCommentRx = regexp.MustCompile(`<!--([^\-]*)-->`)
|
|
)
|
|
|
|
func extractString(src []byte, rx *regexp.Regexp) (s string) {
|
|
m := rx.FindSubmatch(src)
|
|
if m != nil {
|
|
s = strings.TrimSpace(string(m[1]))
|
|
}
|
|
return
|
|
}
|
|
|
|
func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
|
// get HTML body contents
|
|
src, err := ReadFile(fs, abspath)
|
|
if err != nil {
|
|
log.Printf("ReadFile: %s", err)
|
|
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, []byte("<!DOCTYPE ")) {
|
|
w.Write(src)
|
|
return
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// get title and subtitle, if any
|
|
title := extractString(src, titleRx)
|
|
if title == "" {
|
|
// no title found; try first comment for backward-compatibility
|
|
title = extractString(src, firstCommentRx)
|
|
}
|
|
subtitle := extractString(src, subtitleRx)
|
|
|
|
servePage(w, title, subtitle, "", src)
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
|
if canonical := path.Clean(r.URL.Path) + "/"; r.URL.Path != canonical {
|
|
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
|
|
redirected = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
|
|
src, err := ReadFile(fs, abspath)
|
|
if err != nil {
|
|
log.Printf("ReadFile: %s", err)
|
|
serveError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString("<pre>")
|
|
FormatText(&buf, src, 1, filepath.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s")))
|
|
buf.WriteString("</pre>")
|
|
|
|
servePage(w, title+" "+relpath, "", "", buf.Bytes())
|
|
}
|
|
|
|
func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
|
if redirect(w, r) {
|
|
return
|
|
}
|
|
|
|
list, err := fs.ReadDir(abspath)
|
|
if err != nil {
|
|
log.Printf("ReadDir: %s", err)
|
|
serveError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
contents := applyTemplate(dirlistHTML, "dirlistHTML", list)
|
|
servePage(w, "Directory "+relpath, "", "", contents)
|
|
}
|
|
|
|
func serveFile(w http.ResponseWriter, r *http.Request) {
|
|
relpath := r.URL.Path[1:] // serveFile URL paths start with '/'
|
|
abspath := absolutePath(relpath, *goroot)
|
|
|
|
// pick off special cases and hand the rest to the standard file server
|
|
switch r.URL.Path {
|
|
case "/":
|
|
serveHTMLDoc(w, r, filepath.Join(*goroot, "doc", "root.html"), "doc/root.html")
|
|
return
|
|
|
|
case "/doc/root.html":
|
|
// hide landing page from its real name
|
|
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
|
|
switch path.Ext(relpath) {
|
|
case ".html":
|
|
if strings.HasSuffix(relpath, "/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
|
|
}
|
|
serveHTMLDoc(w, r, abspath, relpath)
|
|
return
|
|
|
|
case ".go":
|
|
serveTextFile(w, r, abspath, relpath, "Source file")
|
|
return
|
|
}
|
|
|
|
dir, err := fs.Lstat(abspath)
|
|
if err != nil {
|
|
log.Print(err)
|
|
serveError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
if dir != nil && dir.IsDir() {
|
|
if redirect(w, r) {
|
|
return
|
|
}
|
|
if index := filepath.Join(abspath, "index.html"); isTextFile(index) {
|
|
serveHTMLDoc(w, r, index, relativeURL(index))
|
|
return
|
|
}
|
|
serveDirectory(w, r, abspath, relpath)
|
|
return
|
|
}
|
|
|
|
if isTextFile(abspath) {
|
|
serveTextFile(w, r, abspath, relpath, "Text file")
|
|
return
|
|
}
|
|
|
|
fileServer.ServeHTTP(w, r)
|
|
}
|
|
|
|
func serveSearchDesc(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
|
|
data := map[string]interface{}{
|
|
"BaseURL": fmt.Sprintf("http://%s", r.Host),
|
|
}
|
|
if err := searchDescXML.Execute(w, &data); err != nil {
|
|
log.Printf("searchDescXML.Execute: %s", err)
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Packages
|
|
|
|
// Fake package file and name for commands. Contains the command documentation.
|
|
const fakePkgFile = "doc.go"
|
|
const fakePkgName = "documentation"
|
|
|
|
// Fake relative package path for built-ins. Documentation for all globals
|
|
// (not just exported ones) will be shown for packages in this directory.
|
|
const builtinPkgPath = "builtin"
|
|
|
|
type PageInfoMode uint
|
|
|
|
const (
|
|
noFiltering PageInfoMode = 1 << iota // do not filter exports
|
|
showSource // show source code, do not extract documentation
|
|
noHtml // show result in textual form, do not generate HTML
|
|
flatDir // show directory in a flat (non-indented) manner
|
|
)
|
|
|
|
// modeNames defines names for each PageInfoMode flag.
|
|
var modeNames = map[string]PageInfoMode{
|
|
"all": noFiltering,
|
|
"src": showSource,
|
|
"text": noHtml,
|
|
"flat": flatDir,
|
|
}
|
|
|
|
// 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 getPageInfoMode(r *http.Request) PageInfoMode {
|
|
var mode PageInfoMode
|
|
for _, k := range strings.Split(r.FormValue("m"), ",") {
|
|
if m, found := modeNames[strings.TrimSpace(k)]; found {
|
|
mode |= m
|
|
}
|
|
}
|
|
return adjustPageInfoMode(r, mode)
|
|
}
|
|
|
|
// Specialized versions of godoc may adjust the PageInfoMode by overriding
|
|
// this variable.
|
|
var adjustPageInfoMode = func(_ *http.Request, mode PageInfoMode) PageInfoMode {
|
|
return mode
|
|
}
|
|
|
|
// remoteSearchURL returns the search URL for a given query as needed by
|
|
// remoteSearch. If html is set, an html result is requested; otherwise
|
|
// the result is in textual form.
|
|
// Adjust this function as necessary if modeNames or FormValue parameters
|
|
// change.
|
|
func remoteSearchURL(query string, html bool) string {
|
|
s := "/search?m=text&q="
|
|
if html {
|
|
s = "/search?q="
|
|
}
|
|
return s + url.QueryEscape(query)
|
|
}
|
|
|
|
type PageInfo struct {
|
|
Dirname string // directory containing the package
|
|
PList []string // list of package names found
|
|
FSet *token.FileSet // corresponding file set
|
|
PAst *ast.File // nil if no single AST with package exports
|
|
PDoc *doc.PackageDoc // nil if no single package documentation
|
|
Examples []*doc.Example // nil if no example code
|
|
Dirs *DirList // nil if no directory information
|
|
DirTime time.Time // directory time stamp
|
|
DirFlat bool // if set, show directory in a flat (non-indented) manner
|
|
IsPkg bool // false if this is not documenting a real package
|
|
Err error // I/O error or nil
|
|
}
|
|
|
|
func (info *PageInfo) IsEmpty() bool {
|
|
return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
|
|
}
|
|
|
|
type httpHandler struct {
|
|
pattern string // url pattern; e.g. "/pkg/"
|
|
fsRoot string // file system root to which the pattern is mapped
|
|
isPkg bool // true if this handler serves real package documentation (as opposed to command documentation)
|
|
}
|
|
|
|
// fsReadDir implements ReadDir for the go/build package.
|
|
func fsReadDir(dir string) ([]os.FileInfo, error) {
|
|
return fs.ReadDir(dir)
|
|
}
|
|
|
|
// fsReadFile implements ReadFile for the go/build package.
|
|
func fsReadFile(dir, name string) (path string, data []byte, err error) {
|
|
path = filepath.Join(dir, name)
|
|
data, err = ReadFile(fs, path)
|
|
return
|
|
}
|
|
|
|
func inList(name string, list []string) bool {
|
|
for _, l := range list {
|
|
if name == l {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func stripFunctionBodies(pkg *ast.Package) {
|
|
for _, f := range pkg.Files {
|
|
for _, d := range f.Decls {
|
|
if f, ok := d.(*ast.FuncDecl); ok {
|
|
f.Body = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 a directory read error occurred,
|
|
// PageInfo.Err is set to the respective error but the error is not logged.
|
|
//
|
|
func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo {
|
|
var pkgFiles []string
|
|
|
|
// If we're showing the default package, restrict to the ones
|
|
// 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.
|
|
if pkgname == "" {
|
|
// Note: Uses current binary's GOOS/GOARCH.
|
|
// To use different pair, such as if we allowed the user
|
|
// to choose, set ctxt.GOOS and ctxt.GOARCH before
|
|
// calling ctxt.ScanDir.
|
|
ctxt := build.DefaultContext
|
|
ctxt.ReadDir = fsReadDir
|
|
ctxt.ReadFile = fsReadFile
|
|
dir, err := ctxt.ScanDir(abspath)
|
|
if err == nil {
|
|
pkgFiles = append(dir.GoFiles, dir.CgoFiles...)
|
|
}
|
|
}
|
|
|
|
// filter function to select the desired .go files
|
|
filter := func(d os.FileInfo) bool {
|
|
// Only Go files.
|
|
if !isPkgFile(d) {
|
|
return false
|
|
}
|
|
// If we are looking at cmd documentation, only accept
|
|
// the special fakePkgFile containing the documentation.
|
|
if !h.isPkg {
|
|
return d.Name() == fakePkgFile
|
|
}
|
|
// Also restrict file list to pkgFiles.
|
|
return pkgFiles == nil || inList(d.Name(), pkgFiles)
|
|
}
|
|
|
|
// get package ASTs
|
|
fset := token.NewFileSet()
|
|
pkgs, err := parseDir(fset, abspath, filter)
|
|
if err != nil && pkgs == nil {
|
|
// only report directory read errors, ignore parse errors
|
|
// (may be able to extract partial package information)
|
|
return PageInfo{Dirname: abspath, Err: err}
|
|
}
|
|
|
|
// select package
|
|
var pkg *ast.Package // selected package
|
|
var plist []string // list of other package (names), if any
|
|
if len(pkgs) == 1 {
|
|
// Exactly one package - select it.
|
|
for _, p := range pkgs {
|
|
pkg = p
|
|
}
|
|
|
|
} else if len(pkgs) > 1 {
|
|
// Multiple packages - select the best matching package: The
|
|
// 1st choice is the package with pkgname, the 2nd choice is
|
|
// the package with dirname, and the 3rd choice is a package
|
|
// that is not called "main" if there is exactly one such
|
|
// package. Otherwise, don't select a package.
|
|
dirpath, dirname := filepath.Split(abspath)
|
|
|
|
// If the dirname is "go" we might be in a sub-directory for
|
|
// .go files - use the outer directory name instead for better
|
|
// results.
|
|
if dirname == "go" {
|
|
_, dirname = filepath.Split(filepath.Clean(dirpath))
|
|
}
|
|
|
|
var choice3 *ast.Package
|
|
loop:
|
|
for _, p := range pkgs {
|
|
switch {
|
|
case p.Name == pkgname:
|
|
pkg = p
|
|
break loop // 1st choice; we are done
|
|
case p.Name == dirname:
|
|
pkg = p // 2nd choice
|
|
case p.Name != "main":
|
|
choice3 = p
|
|
}
|
|
}
|
|
if pkg == nil && len(pkgs) == 2 {
|
|
pkg = choice3
|
|
}
|
|
|
|
// Compute the list of other packages
|
|
// (excluding the selected package, if any).
|
|
plist = make([]string, len(pkgs))
|
|
i := 0
|
|
for name := range pkgs {
|
|
if pkg == nil || name != pkg.Name {
|
|
plist[i] = name
|
|
i++
|
|
}
|
|
}
|
|
plist = plist[0:i]
|
|
}
|
|
|
|
// get examples from *_test.go files
|
|
var examples []*doc.Example
|
|
filter = func(d os.FileInfo) bool {
|
|
return isGoFile(d) && strings.HasSuffix(d.Name(), "_test.go")
|
|
}
|
|
if testpkgs, err := parseDir(fset, abspath, filter); err != nil {
|
|
log.Println("parsing test files:", err)
|
|
} else {
|
|
for _, testpkg := range testpkgs {
|
|
examples = append(examples, doc.Examples(testpkg)...)
|
|
}
|
|
}
|
|
|
|
// compute package documentation
|
|
var past *ast.File
|
|
var pdoc *doc.PackageDoc
|
|
if pkg != nil {
|
|
if mode&noFiltering == 0 {
|
|
ast.PackageExports(pkg)
|
|
}
|
|
if mode&showSource == 0 {
|
|
stripFunctionBodies(pkg)
|
|
pdoc = doc.NewPackageDoc(pkg, path.Clean(relpath)) // no trailing '/' in importpath
|
|
} else {
|
|
past = ast.MergePackageFiles(pkg, ast.FilterUnassociatedComments)
|
|
}
|
|
}
|
|
|
|
// get directory information
|
|
var dir *Directory
|
|
var timestamp time.Time
|
|
if tree, ts := 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 {
|
|
// the path may refer to a user-specified file system mapped
|
|
// via fsMap; lookup that mapping and corresponding RWValue
|
|
// if any
|
|
var v *RWValue
|
|
fsMap.Iterate(func(path string, value *RWValue) bool {
|
|
if isParentOf(path, abspath) {
|
|
// mapping found
|
|
v = value
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if v != nil {
|
|
// found a RWValue associated with a user-specified file
|
|
// system; a non-nil RWValue stores a (possibly out-of-date)
|
|
// directory tree for that file system
|
|
if tree, ts := v.get(); tree != nil && tree.(*Directory) != nil {
|
|
dir = tree.(*Directory).lookup(abspath)
|
|
timestamp = ts
|
|
}
|
|
}
|
|
}
|
|
if dir == nil {
|
|
// no directory tree present (too early after startup or
|
|
// command-line mode); compute one level for this page
|
|
// note: cannot use path filter here because in general
|
|
// it doesn't contain the fsTree path
|
|
dir = newDirectory(abspath, nil, 1)
|
|
timestamp = time.Now()
|
|
}
|
|
|
|
return PageInfo{
|
|
Dirname: abspath,
|
|
PList: plist,
|
|
FSet: fset,
|
|
PAst: past,
|
|
PDoc: pdoc,
|
|
Examples: examples,
|
|
Dirs: dir.listing(true),
|
|
DirTime: timestamp,
|
|
DirFlat: mode&flatDir != 0,
|
|
IsPkg: h.isPkg,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if redirect(w, r) {
|
|
return
|
|
}
|
|
|
|
relpath := path.Clean(r.URL.Path[len(h.pattern):])
|
|
abspath := absolutePath(relpath, h.fsRoot)
|
|
mode := getPageInfoMode(r)
|
|
if relpath == builtinPkgPath {
|
|
mode = noFiltering
|
|
}
|
|
info := h.getPageInfo(abspath, relpath, r.FormValue("p"), mode)
|
|
if info.Err != nil {
|
|
log.Print(info.Err)
|
|
serveError(w, r, relpath, info.Err)
|
|
return
|
|
}
|
|
|
|
if mode&noHtml != 0 {
|
|
contents := applyTemplate(packageText, "packageText", info)
|
|
serveText(w, contents)
|
|
return
|
|
}
|
|
|
|
var title, subtitle string
|
|
switch {
|
|
case info.PAst != nil:
|
|
title = "Package " + info.PAst.Name.Name
|
|
case info.PDoc != nil:
|
|
switch {
|
|
case info.IsPkg:
|
|
title = "Package " + info.PDoc.PackageName
|
|
case info.PDoc.PackageName == fakePkgName:
|
|
// assume that the directory name is the command name
|
|
_, pkgname := path.Split(relpath)
|
|
title = "Command " + pkgname
|
|
default:
|
|
title = "Command " + info.PDoc.PackageName
|
|
}
|
|
default:
|
|
title = "Directory " + relativeURL(info.Dirname)
|
|
if *showTimestamps {
|
|
subtitle = "Last update: " + info.DirTime.String()
|
|
}
|
|
}
|
|
|
|
contents := applyTemplate(packageHTML, "packageHTML", info)
|
|
servePage(w, title, subtitle, "", contents)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Search
|
|
|
|
var searchIndex RWValue
|
|
|
|
type SearchResult struct {
|
|
Query string
|
|
Alert string // error or warning message
|
|
|
|
// identifier matches
|
|
Pak HitList // packages matching Query
|
|
Hit *LookupResult // identifier matches of Query
|
|
Alt *AltWords // alternative identifiers to look for
|
|
|
|
// textual matches
|
|
Found int // number of textual occurrences found
|
|
Textual []FileLines // textual matches of Query
|
|
Complete bool // true if all textual occurrences of Query are reported
|
|
}
|
|
|
|
func lookup(query string) (result SearchResult) {
|
|
result.Query = query
|
|
|
|
index, timestamp := searchIndex.get()
|
|
if index != nil {
|
|
index := index.(*Index)
|
|
|
|
// identifier search
|
|
var err error
|
|
result.Pak, result.Hit, result.Alt, err = index.Lookup(query)
|
|
if err != nil && *maxResults <= 0 {
|
|
// ignore the error if full text search is enabled
|
|
// since the query may be a valid regular expression
|
|
result.Alert = "Error in query string: " + err.Error()
|
|
return
|
|
}
|
|
|
|
// full text search
|
|
if *maxResults > 0 && query != "" {
|
|
rx, err := regexp.Compile(query)
|
|
if err != nil {
|
|
result.Alert = "Error in query regular expression: " + err.Error()
|
|
return
|
|
}
|
|
// If we get maxResults+1 results we know that there are more than
|
|
// maxResults results and thus the result may be incomplete (to be
|
|
// precise, we should remove one result from the result set, but
|
|
// nobody is going to count the results on the result page).
|
|
result.Found, result.Textual = index.LookupRegexp(rx, *maxResults+1)
|
|
result.Complete = result.Found <= *maxResults
|
|
if !result.Complete {
|
|
result.Found-- // since we looked for maxResults+1
|
|
}
|
|
}
|
|
}
|
|
|
|
// is the result accurate?
|
|
if *indexEnabled {
|
|
if _, ts := fsModified.get(); timestamp.Before(ts) {
|
|
// The index is older than the latest file system change under godoc's observation.
|
|
result.Alert = "Indexing in progress: result may be inaccurate"
|
|
}
|
|
} else {
|
|
result.Alert = "Search index disabled: no results available"
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func search(w http.ResponseWriter, r *http.Request) {
|
|
query := strings.TrimSpace(r.FormValue("q"))
|
|
result := lookup(query)
|
|
|
|
if getPageInfoMode(r)&noHtml != 0 {
|
|
contents := applyTemplate(searchText, "searchText", result)
|
|
serveText(w, contents)
|
|
return
|
|
}
|
|
|
|
var title string
|
|
if result.Hit != nil || len(result.Textual) > 0 {
|
|
title = fmt.Sprintf(`Results for query %q`, query)
|
|
} else {
|
|
title = fmt.Sprintf(`No results found for query %q`, query)
|
|
}
|
|
|
|
contents := applyTemplate(searchHTML, "searchHTML", result)
|
|
servePage(w, title, "", query, contents)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Indexer
|
|
|
|
// invalidateIndex should be called whenever any of the file systems
|
|
// under godoc's observation change so that the indexer is kicked on.
|
|
//
|
|
func invalidateIndex() {
|
|
fsModified.set(nil)
|
|
}
|
|
|
|
// indexUpToDate() returns true if the search index is not older
|
|
// than any of the file systems under godoc's observation.
|
|
//
|
|
func indexUpToDate() bool {
|
|
_, fsTime := fsModified.get()
|
|
_, siTime := searchIndex.get()
|
|
return !fsTime.After(siTime)
|
|
}
|
|
|
|
// feedDirnames feeds the directory names of all directories
|
|
// under the file system given by root to channel c.
|
|
//
|
|
func feedDirnames(root *RWValue, c chan<- string) {
|
|
if dir, _ := root.get(); dir != nil {
|
|
for d := range dir.(*Directory).iter(false) {
|
|
c <- d.Path
|
|
}
|
|
}
|
|
}
|
|
|
|
// fsDirnames() returns a channel sending all directory names
|
|
// of all the file systems under godoc's observation.
|
|
//
|
|
func fsDirnames() <-chan string {
|
|
c := make(chan string, 256) // asynchronous for fewer context switches
|
|
go func() {
|
|
feedDirnames(&fsTree, c)
|
|
fsMap.Iterate(func(_ string, root *RWValue) bool {
|
|
feedDirnames(root, c)
|
|
return true
|
|
})
|
|
close(c)
|
|
}()
|
|
return c
|
|
}
|
|
|
|
func readIndex(filenames string) error {
|
|
matches, err := filepath.Glob(filenames)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Strings(matches) // make sure files are in the right order
|
|
files := make([]io.Reader, 0, len(matches))
|
|
for _, filename := range matches {
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
files = append(files, f)
|
|
}
|
|
x := new(Index)
|
|
if err := x.Read(io.MultiReader(files...)); err != nil {
|
|
return err
|
|
}
|
|
searchIndex.set(x)
|
|
return nil
|
|
}
|
|
|
|
func updateIndex() {
|
|
if *verbose {
|
|
log.Printf("updating index...")
|
|
}
|
|
start := time.Now()
|
|
index := NewIndex(fsDirnames(), *maxResults > 0, *indexThrottle)
|
|
stop := time.Now()
|
|
searchIndex.set(index)
|
|
if *verbose {
|
|
secs := stop.Sub(start).Seconds()
|
|
stats := index.Stats()
|
|
log.Printf("index updated (%gs, %d bytes of source, %d files, %d lines, %d unique words, %d spots)",
|
|
secs, stats.Bytes, stats.Files, stats.Lines, stats.Words, stats.Spots)
|
|
}
|
|
log.Printf("before GC: bytes = %d footprint = %d", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
|
|
runtime.GC()
|
|
log.Printf("after GC: bytes = %d footprint = %d", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
|
|
}
|
|
|
|
func indexer() {
|
|
// initialize the index from disk if possible
|
|
if *indexFiles != "" {
|
|
if err := readIndex(*indexFiles); err != nil {
|
|
log.Printf("error reading index: %s", err)
|
|
}
|
|
}
|
|
|
|
// repeatedly update the index when it goes out of date
|
|
for {
|
|
if !indexUpToDate() {
|
|
// index possibly out of date - make a new one
|
|
updateIndex()
|
|
}
|
|
delay := 60 * time.Second // by default, try every 60s
|
|
if *testDir != "" {
|
|
// in test mode, try once a second for fast startup
|
|
delay = 1 * time.Second
|
|
}
|
|
time.Sleep(delay)
|
|
}
|
|
}
|