whatcanGOwrong
@@ -0,0 +1,31 @@
|
||||
# godoc
|
||||
|
||||
This directory contains most of the code for running a godoc server. The
|
||||
executable lives at golang.org/x/tools/cmd/godoc.
|
||||
|
||||
## Development mode
|
||||
|
||||
In production, CSS/JS/template assets need to be compiled into the godoc
|
||||
binary. It can be tedious to recompile assets every time, but you can pass a
|
||||
flag to load CSS/JS/templates from disk every time a page loads:
|
||||
|
||||
```
|
||||
godoc -templates=$GOPATH/src/golang.org/x/tools/godoc/static -http=:6060
|
||||
```
|
||||
|
||||
## Recompiling static assets
|
||||
|
||||
The files that live at `static/style.css`, `static/jquery.js` and so on are not
|
||||
present in the final binary. They are placed into `static/static.go` by running
|
||||
`go generate`. So to compile a change and test it in your browser:
|
||||
|
||||
1) Make changes to e.g. `static/style.css`.
|
||||
|
||||
2) Run `go generate golang.org/x/tools/godoc/static` so `static/static.go` picks
|
||||
up the change.
|
||||
|
||||
3) Run `go install golang.org/x/tools/cmd/godoc` so the compiled `godoc` binary
|
||||
picks up the change.
|
||||
|
||||
4) Run `godoc -http=:6060` and view your changes in the browser. You may need
|
||||
to disable your browser's cache to avoid reloading a stale file.
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2014 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 analysis performs type and pointer analysis
|
||||
// and generates mark-up for the Go source view.
|
||||
//
|
||||
// The Run method populates a Result object by running type and
|
||||
// (optionally) pointer analysis. The Result object is thread-safe
|
||||
// and at all times may be accessed by a serving thread, even as it is
|
||||
// progressively populated as analysis facts are derived.
|
||||
//
|
||||
// The Result is a mapping from each godoc file URL
|
||||
// (e.g. /src/fmt/print.go) to information about that file. The
|
||||
// information is a list of HTML markup links and a JSON array of
|
||||
// structured data values. Some of the links call client-side
|
||||
// JavaScript functions that index this array.
|
||||
//
|
||||
// The analysis computes mark-up for the following relations:
|
||||
//
|
||||
// IMPORTS: for each ast.ImportSpec, the package that it denotes.
|
||||
//
|
||||
// RESOLUTION: for each ast.Ident, its kind and type, and the location
|
||||
// of its definition.
|
||||
//
|
||||
// METHOD SETS, IMPLEMENTS: for each ast.Ident defining a named type,
|
||||
// its method-set, the set of interfaces it implements or is
|
||||
// implemented by, and its size/align values.
|
||||
//
|
||||
// CALLERS, CALLEES: for each function declaration ('func' token), its
|
||||
// callers, and for each call-site ('(' token), its callees.
|
||||
//
|
||||
// CALLGRAPH: the package docs include an interactive viewer for the
|
||||
// intra-package call graph of "fmt".
|
||||
//
|
||||
// CHANNEL PEERS: for each channel operation make/<-/close, the set of
|
||||
// other channel ops that alias the same channel(s).
|
||||
//
|
||||
// ERRORS: for each locus of a frontend (scanner/parser/type) error, the
|
||||
// location is highlighted in red and hover text provides the compiler
|
||||
// error message.
|
||||
package analysis // import "golang.org/x/tools/godoc/analysis"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// -- links ------------------------------------------------------------
|
||||
|
||||
// A Link is an HTML decoration of the bytes [Start, End) of a file.
|
||||
// Write is called before/after those bytes to emit the mark-up.
|
||||
type Link interface {
|
||||
Start() int
|
||||
End() int
|
||||
Write(w io.Writer, _ int, start bool) // the godoc.LinkWriter signature
|
||||
}
|
||||
|
||||
// -- fileInfo ---------------------------------------------------------
|
||||
|
||||
// FileInfo holds analysis information for the source file view.
|
||||
// Clients must not mutate it.
|
||||
type FileInfo struct {
|
||||
Data []interface{} // JSON serializable values
|
||||
Links []Link // HTML link markup
|
||||
}
|
||||
|
||||
// A fileInfo is the server's store of hyperlinks and JSON data for a
|
||||
// particular file.
|
||||
type fileInfo struct {
|
||||
mu sync.Mutex
|
||||
data []interface{} // JSON objects
|
||||
links []Link
|
||||
sorted bool
|
||||
hasErrors bool // TODO(adonovan): surface this in the UI
|
||||
}
|
||||
|
||||
// get returns the file info in external form.
|
||||
// Callers must not mutate its fields.
|
||||
func (fi *fileInfo) get() FileInfo {
|
||||
var r FileInfo
|
||||
// Copy slices, to avoid races.
|
||||
fi.mu.Lock()
|
||||
r.Data = append(r.Data, fi.data...)
|
||||
if !fi.sorted {
|
||||
sort.Sort(linksByStart(fi.links))
|
||||
fi.sorted = true
|
||||
}
|
||||
r.Links = append(r.Links, fi.links...)
|
||||
fi.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// PackageInfo holds analysis information for the package view.
|
||||
// Clients must not mutate it.
|
||||
type PackageInfo struct {
|
||||
CallGraph []*PCGNodeJSON
|
||||
CallGraphIndex map[string]int
|
||||
Types []*TypeInfoJSON
|
||||
}
|
||||
|
||||
type pkgInfo struct {
|
||||
mu sync.Mutex
|
||||
callGraph []*PCGNodeJSON
|
||||
callGraphIndex map[string]int // keys are (*ssa.Function).RelString()
|
||||
types []*TypeInfoJSON // type info for exported types
|
||||
}
|
||||
|
||||
// get returns the package info in external form.
|
||||
// Callers must not mutate its fields.
|
||||
func (pi *pkgInfo) get() PackageInfo {
|
||||
var r PackageInfo
|
||||
// Copy slices, to avoid races.
|
||||
pi.mu.Lock()
|
||||
r.CallGraph = append(r.CallGraph, pi.callGraph...)
|
||||
r.CallGraphIndex = pi.callGraphIndex
|
||||
r.Types = append(r.Types, pi.types...)
|
||||
pi.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// -- Result -----------------------------------------------------------
|
||||
|
||||
// Result contains the results of analysis.
|
||||
// The result contains a mapping from filenames to a set of HTML links
|
||||
// and JavaScript data referenced by the links.
|
||||
type Result struct {
|
||||
mu sync.Mutex // guards maps (but not their contents)
|
||||
status string // global analysis status
|
||||
fileInfos map[string]*fileInfo // keys are godoc file URLs
|
||||
pkgInfos map[string]*pkgInfo // keys are import paths
|
||||
}
|
||||
|
||||
// fileInfo returns the fileInfo for the specified godoc file URL,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) fileInfo(url string) *fileInfo {
|
||||
res.mu.Lock()
|
||||
fi, ok := res.fileInfos[url]
|
||||
if !ok {
|
||||
if res.fileInfos == nil {
|
||||
res.fileInfos = make(map[string]*fileInfo)
|
||||
}
|
||||
fi = new(fileInfo)
|
||||
res.fileInfos[url] = fi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return fi
|
||||
}
|
||||
|
||||
// Status returns a human-readable description of the current analysis status.
|
||||
func (res *Result) Status() string {
|
||||
res.mu.Lock()
|
||||
defer res.mu.Unlock()
|
||||
return res.status
|
||||
}
|
||||
|
||||
// FileInfo returns new slices containing opaque JSON values and the
|
||||
// HTML link markup for the specified godoc file URL. Thread-safe.
|
||||
// Callers must not mutate the elements.
|
||||
// It returns "zero" if no data is available.
|
||||
func (res *Result) FileInfo(url string) (fi FileInfo) {
|
||||
return res.fileInfo(url).get()
|
||||
}
|
||||
|
||||
// pkgInfo returns the pkgInfo for the specified import path,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) pkgInfo(importPath string) *pkgInfo {
|
||||
res.mu.Lock()
|
||||
pi, ok := res.pkgInfos[importPath]
|
||||
if !ok {
|
||||
if res.pkgInfos == nil {
|
||||
res.pkgInfos = make(map[string]*pkgInfo)
|
||||
}
|
||||
pi = new(pkgInfo)
|
||||
res.pkgInfos[importPath] = pi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return pi
|
||||
}
|
||||
|
||||
// PackageInfo returns new slices of JSON values for the callgraph and
|
||||
// type info for the specified package. Thread-safe.
|
||||
// Callers must not mutate its fields.
|
||||
// PackageInfo returns "zero" if no data is available.
|
||||
func (res *Result) PackageInfo(importPath string) PackageInfo {
|
||||
return res.pkgInfo(importPath).get()
|
||||
}
|
||||
|
||||
type linksByStart []Link
|
||||
|
||||
func (a linksByStart) Less(i, j int) bool { return a[i].Start() < a[j].Start() }
|
||||
func (a linksByStart) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a linksByStart) Len() int { return len(a) }
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2014 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 analysis
|
||||
|
||||
// This file defines types used by client-side JavaScript.
|
||||
|
||||
type anchorJSON struct {
|
||||
Text string // HTML
|
||||
Href string // URL
|
||||
}
|
||||
|
||||
// Indicates one of these forms of fact about a type T:
|
||||
// T "is implemented by <ByKind> type <Other>" (ByKind != "", e.g. "array")
|
||||
// T "implements <Other>" (ByKind == "")
|
||||
type implFactJSON struct {
|
||||
ByKind string `json:",omitempty"`
|
||||
Other anchorJSON
|
||||
}
|
||||
|
||||
// Implements facts are grouped by form, for ease of reading.
|
||||
type implGroupJSON struct {
|
||||
Descr string
|
||||
Facts []implFactJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickIdent() expects a TypeInfoJSON.
|
||||
type TypeInfoJSON struct {
|
||||
Name string // type name
|
||||
Size, Align int64
|
||||
Methods []anchorJSON
|
||||
ImplGroups []implGroupJSON
|
||||
}
|
||||
|
||||
// JavaScript's cgAddChild requires a global array of PCGNodeJSON
|
||||
// called CALLGRAPH, representing the intra-package call graph.
|
||||
// The first element is special and represents "all external callers".
|
||||
type PCGNodeJSON struct {
|
||||
Func anchorJSON
|
||||
Callees []int // indices within CALLGRAPH of nodes called by this one
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/godoc/analysis"
|
||||
"golang.org/x/tools/godoc/util"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// A Corpus holds all the state related to serving and indexing a
|
||||
// collection of Go code.
|
||||
//
|
||||
// Construct a new Corpus with NewCorpus, then modify options,
|
||||
// then call its Init method.
|
||||
type Corpus struct {
|
||||
fs vfs.FileSystem
|
||||
|
||||
// Verbose logging.
|
||||
Verbose bool
|
||||
|
||||
// IndexEnabled controls whether indexing is enabled.
|
||||
IndexEnabled bool
|
||||
|
||||
// IndexFiles specifies a glob pattern specifying index files.
|
||||
// If not empty, the index is read from these files in sorted
|
||||
// order.
|
||||
IndexFiles string
|
||||
|
||||
// IndexThrottle specifies the indexing throttle value
|
||||
// between 0.0 and 1.0. At 0.0, the indexer always sleeps.
|
||||
// At 1.0, the indexer never sleeps. Because 0.0 is useless
|
||||
// and redundant with setting IndexEnabled to false, the
|
||||
// zero value for IndexThrottle means 0.9.
|
||||
IndexThrottle float64
|
||||
|
||||
// IndexInterval specifies the time to sleep between reindexing
|
||||
// all the sources.
|
||||
// If zero, a default is used. If negative, the index is only
|
||||
// built once.
|
||||
IndexInterval time.Duration
|
||||
|
||||
// IndexDocs enables indexing of Go documentation.
|
||||
// This will produce search results for exported types, functions,
|
||||
// methods, variables, and constants, and will link to the godoc
|
||||
// documentation for those identifiers.
|
||||
IndexDocs bool
|
||||
|
||||
// IndexGoCode enables indexing of Go source code.
|
||||
// This will produce search results for internal and external identifiers
|
||||
// and will link to both declarations and uses of those identifiers in
|
||||
// source code.
|
||||
IndexGoCode bool
|
||||
|
||||
// IndexFullText enables full-text indexing.
|
||||
// This will provide search results for any matching text in any file that
|
||||
// is indexed, including non-Go files (see whitelisted in index.go).
|
||||
// Regexp searching is supported via full-text indexing.
|
||||
IndexFullText bool
|
||||
|
||||
// MaxResults optionally specifies the maximum results for indexing.
|
||||
MaxResults int
|
||||
|
||||
// SummarizePackage optionally specifies a function to
|
||||
// summarize a package. It exists as an optimization to
|
||||
// avoid reading files to parse package comments.
|
||||
//
|
||||
// If SummarizePackage returns false for ok, the caller
|
||||
// ignores all return values and parses the files in the package
|
||||
// as if SummarizePackage were nil.
|
||||
//
|
||||
// If showList is false, the package is hidden from the
|
||||
// package listing.
|
||||
SummarizePackage func(pkg string) (summary string, showList, ok bool)
|
||||
|
||||
// IndexDirectory optionally specifies a function to determine
|
||||
// whether the provided directory should be indexed. The dir
|
||||
// will be of the form "/src/cmd/6a", "/doc/play",
|
||||
// "/src/io", etc.
|
||||
// If nil, all directories are indexed if indexing is enabled.
|
||||
IndexDirectory func(dir string) bool
|
||||
|
||||
// Send a value on this channel to trigger a metadata refresh.
|
||||
// It is buffered so that if a signal is not lost if sent
|
||||
// during a refresh.
|
||||
refreshMetadataSignal chan bool
|
||||
|
||||
// file system information
|
||||
fsTree util.RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now)
|
||||
fsModified util.RWValue // timestamp of last call to invalidateIndex
|
||||
docMetadata util.RWValue // mapping from paths to *Metadata
|
||||
|
||||
// SearchIndex is the search index in use.
|
||||
searchIndex util.RWValue
|
||||
|
||||
// Analysis is the result of type and pointer analysis.
|
||||
Analysis analysis.Result
|
||||
|
||||
// flag to check whether a corpus is initialized or not
|
||||
initMu sync.RWMutex
|
||||
initDone bool
|
||||
|
||||
// pkgAPIInfo contains the information about which package API
|
||||
// features were added in which version of Go.
|
||||
pkgAPIInfo apiVersions
|
||||
}
|
||||
|
||||
// NewCorpus returns a new Corpus from a filesystem.
|
||||
// The returned corpus has all indexing enabled and MaxResults set to 1000.
|
||||
// Change or set any options on Corpus before calling the Corpus.Init method.
|
||||
func NewCorpus(fs vfs.FileSystem) *Corpus {
|
||||
c := &Corpus{
|
||||
fs: fs,
|
||||
refreshMetadataSignal: make(chan bool, 1),
|
||||
|
||||
MaxResults: 1000,
|
||||
IndexEnabled: true,
|
||||
IndexDocs: true,
|
||||
IndexGoCode: true,
|
||||
IndexFullText: true,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Corpus) CurrentIndex() (*Index, time.Time) {
|
||||
v, t := c.searchIndex.Get()
|
||||
idx, _ := v.(*Index)
|
||||
return idx, t
|
||||
}
|
||||
|
||||
func (c *Corpus) FSModifiedTime() time.Time {
|
||||
_, ts := c.fsModified.Get()
|
||||
return ts
|
||||
}
|
||||
|
||||
// Init initializes Corpus, once options on Corpus are set.
|
||||
// It must be called before any subsequent method calls.
|
||||
func (c *Corpus) Init() error {
|
||||
if err := c.initFSTree(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.updateMetadata()
|
||||
go c.refreshMetadataLoop()
|
||||
|
||||
c.initMu.Lock()
|
||||
c.initDone = true
|
||||
c.initMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Corpus) initFSTree() error {
|
||||
dir := c.newDirectory("/", -1)
|
||||
if dir == nil {
|
||||
return errors.New("godoc: corpus fstree is nil")
|
||||
}
|
||||
c.fsTree.Set(dir)
|
||||
c.invalidateIndex()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// Copyright 2010 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.
|
||||
|
||||
// This file contains the code dealing with package directory trees.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// Conventional name for directories containing test data.
|
||||
// Excluded from directory trees.
|
||||
const testdataDirName = "testdata"
|
||||
|
||||
type Directory struct {
|
||||
Depth int
|
||||
Path string // directory path; includes Name
|
||||
Name string // directory name
|
||||
HasPkg bool // true if the directory contains at least one package
|
||||
Synopsis string // package documentation, if any
|
||||
RootType vfs.RootType // root type of the filesystem containing the directory
|
||||
Dirs []*Directory // subdirectories
|
||||
}
|
||||
|
||||
func isGoFile(fi os.FileInfo) bool {
|
||||
name := fi.Name()
|
||||
return !fi.IsDir() &&
|
||||
len(name) > 0 && name[0] != '.' && // ignore .files
|
||||
pathpkg.Ext(name) == ".go"
|
||||
}
|
||||
|
||||
func isPkgFile(fi os.FileInfo) bool {
|
||||
return isGoFile(fi) &&
|
||||
!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
|
||||
}
|
||||
|
||||
func isPkgDir(fi os.FileInfo) bool {
|
||||
name := fi.Name()
|
||||
return fi.IsDir() && len(name) > 0 &&
|
||||
name[0] != '_' && name[0] != '.' // ignore _files and .files
|
||||
}
|
||||
|
||||
type treeBuilder struct {
|
||||
c *Corpus
|
||||
maxDepth int
|
||||
}
|
||||
|
||||
// ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc).
|
||||
// Send before an operation and receive after.
|
||||
var ioGate = make(chan struct{}, 20)
|
||||
|
||||
// workGate controls the number of concurrent workers. Too many concurrent
|
||||
// workers and performance degrades and the race detector gets overwhelmed. If
|
||||
// we cannot check out a concurrent worker, work is performed by the main thread
|
||||
// instead of spinning up another goroutine.
|
||||
var workGate = make(chan struct{}, runtime.NumCPU()*4)
|
||||
|
||||
func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
|
||||
if name == testdataDirName {
|
||||
return nil
|
||||
}
|
||||
|
||||
if depth >= b.maxDepth {
|
||||
// return a dummy directory so that the parent directory
|
||||
// doesn't get discarded just because we reached the max
|
||||
// directory depth
|
||||
return &Directory{
|
||||
Depth: depth,
|
||||
Path: path,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
var synopses [3]string // prioritized package documentation (0 == highest priority)
|
||||
|
||||
show := true // show in package listing
|
||||
hasPkgFiles := false
|
||||
haveSummary := false
|
||||
|
||||
if hook := b.c.SummarizePackage; hook != nil {
|
||||
if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok {
|
||||
hasPkgFiles = true
|
||||
show = show0
|
||||
synopses[0] = summary
|
||||
haveSummary = true
|
||||
}
|
||||
}
|
||||
|
||||
ioGate <- struct{}{}
|
||||
list, err := b.c.fs.ReadDir(path)
|
||||
<-ioGate
|
||||
if err != nil {
|
||||
// TODO: propagate more. See golang.org/issue/14252.
|
||||
// For now:
|
||||
if b.c.Verbose {
|
||||
log.Printf("newDirTree reading %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// determine number of subdirectories and if there are package files
|
||||
var dirchs []chan *Directory
|
||||
var dirs []*Directory
|
||||
|
||||
for _, d := range list {
|
||||
filename := pathpkg.Join(path, d.Name())
|
||||
switch {
|
||||
case isPkgDir(d):
|
||||
name := d.Name()
|
||||
select {
|
||||
case workGate <- struct{}{}:
|
||||
ch := make(chan *Directory, 1)
|
||||
dirchs = append(dirchs, ch)
|
||||
go func() {
|
||||
ch <- b.newDirTree(fset, filename, name, depth+1)
|
||||
<-workGate
|
||||
}()
|
||||
default:
|
||||
// no free workers, do work synchronously
|
||||
dir := b.newDirTree(fset, filename, name, depth+1)
|
||||
if dir != nil {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
case !haveSummary && isPkgFile(d):
|
||||
// looks like a package file, but may just be a file ending in ".go";
|
||||
// don't just count it yet (otherwise we may end up with hasPkgFiles even
|
||||
// though the directory doesn't contain any real package files - was bug)
|
||||
// no "optimal" package synopsis yet; continue to collect synopses
|
||||
ioGate <- struct{}{}
|
||||
const flags = parser.ParseComments | parser.PackageClauseOnly
|
||||
file, err := b.c.parseFile(fset, filename, flags)
|
||||
<-ioGate
|
||||
if err != nil {
|
||||
if b.c.Verbose {
|
||||
log.Printf("Error parsing %v: %v", filename, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
hasPkgFiles = true
|
||||
if file.Doc != nil {
|
||||
// prioritize documentation
|
||||
i := -1
|
||||
switch file.Name.Name {
|
||||
case name:
|
||||
i = 0 // normal case: directory name matches package name
|
||||
case "main":
|
||||
i = 1 // directory contains a main package
|
||||
default:
|
||||
i = 2 // none of the above
|
||||
}
|
||||
if 0 <= i && i < len(synopses) && synopses[i] == "" {
|
||||
synopses[i] = doc.Synopsis(file.Doc.Text())
|
||||
}
|
||||
}
|
||||
haveSummary = synopses[0] != ""
|
||||
}
|
||||
}
|
||||
|
||||
// create subdirectory tree
|
||||
for _, ch := range dirchs {
|
||||
if d := <-ch; d != nil {
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to sort the dirs slice because
|
||||
// it is appended again after reading from dirchs.
|
||||
sort.Slice(dirs, func(i, j int) bool {
|
||||
return dirs[i].Name < dirs[j].Name
|
||||
})
|
||||
|
||||
// if there are no package files and no subdirectories
|
||||
// containing package files, ignore the directory
|
||||
if !hasPkgFiles && len(dirs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// select the highest-priority synopsis for the directory entry, if any
|
||||
synopsis := ""
|
||||
for _, synopsis = range synopses {
|
||||
if synopsis != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &Directory{
|
||||
Depth: depth,
|
||||
Path: path,
|
||||
Name: name,
|
||||
HasPkg: hasPkgFiles && show, // TODO(bradfitz): add proper Hide field?
|
||||
Synopsis: synopsis,
|
||||
RootType: b.c.fs.RootType(path),
|
||||
Dirs: dirs,
|
||||
}
|
||||
}
|
||||
|
||||
// newDirectory creates a new package directory tree with at most maxDepth
|
||||
// levels, anchored at root. The result tree is pruned such that it only
|
||||
// contains directories that contain package files or that contain
|
||||
// subdirectories containing package files (transitively). If a non-nil
|
||||
// pathFilter is provided, directory paths additionally must be accepted
|
||||
// by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
|
||||
// provided for maxDepth, nodes at larger depths are pruned as well; they
|
||||
// are assumed to contain package files even if their contents are not known
|
||||
// (i.e., in this case the tree may contain directories w/o any package files).
|
||||
func (c *Corpus) newDirectory(root string, maxDepth int) *Directory {
|
||||
// The root could be a symbolic link so use Stat not Lstat.
|
||||
d, err := c.fs.Stat(root)
|
||||
// If we fail here, report detailed error messages; otherwise
|
||||
// is hard to see why a directory tree was not built.
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Printf("newDirectory(%s): %s", root, err)
|
||||
return nil
|
||||
case root != "/" && !isPkgDir(d):
|
||||
log.Printf("newDirectory(%s): not a package directory", root)
|
||||
return nil
|
||||
case root == "/" && !d.IsDir():
|
||||
log.Printf("newDirectory(%s): not a directory", root)
|
||||
return nil
|
||||
}
|
||||
if maxDepth < 0 {
|
||||
maxDepth = 1e6 // "infinity"
|
||||
}
|
||||
b := treeBuilder{c, maxDepth}
|
||||
// the file set provided is only for local parsing, no position
|
||||
// information escapes and thus we don't need to save the set
|
||||
return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
|
||||
}
|
||||
|
||||
func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
|
||||
if dir != nil {
|
||||
if !skipRoot {
|
||||
c <- dir
|
||||
}
|
||||
for _, d := range dir.Dirs {
|
||||
d.walk(c, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
|
||||
c := make(chan *Directory)
|
||||
go func() {
|
||||
dir.walk(c, skipRoot)
|
||||
close(c)
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
func (dir *Directory) lookupLocal(name string) *Directory {
|
||||
for _, d := range dir.Dirs {
|
||||
if d.Name == name {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitPath(p string) []string {
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
if p == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(p, "/")
|
||||
}
|
||||
|
||||
// lookup looks for the *Directory for a given path, relative to dir.
|
||||
func (dir *Directory) lookup(path string) *Directory {
|
||||
d := splitPath(dir.Path)
|
||||
p := splitPath(path)
|
||||
i := 0
|
||||
for i < len(d) {
|
||||
if i >= len(p) || d[i] != p[i] {
|
||||
return nil
|
||||
}
|
||||
i++
|
||||
}
|
||||
for dir != nil && i < len(p) {
|
||||
dir = dir.lookupLocal(p[i])
|
||||
i++
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// DirEntry describes a directory entry. The Depth and Height values
|
||||
// are useful for presenting an entry in an indented fashion.
|
||||
type DirEntry struct {
|
||||
Depth int // >= 0
|
||||
Height int // = DirList.MaxHeight - Depth, > 0
|
||||
Path string // directory path; includes Name, relative to DirList root
|
||||
Name string // directory name
|
||||
HasPkg bool // true if the directory contains at least one package
|
||||
Synopsis string // package documentation, if any
|
||||
RootType vfs.RootType // root type of the filesystem containing the direntry
|
||||
}
|
||||
|
||||
type DirList struct {
|
||||
MaxHeight int // directory tree height, > 0
|
||||
List []DirEntry
|
||||
}
|
||||
|
||||
// hasThirdParty checks whether a list of directory entries has packages outside
|
||||
// the standard library or not.
|
||||
func hasThirdParty(list []DirEntry) bool {
|
||||
for _, entry := range list {
|
||||
if entry.RootType == vfs.RootTypeGoPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listing creates a (linear) directory listing from a directory tree.
|
||||
// If skipRoot is set, the root directory itself is excluded from the list.
|
||||
// If filter is set, only the directory entries whose paths match the filter
|
||||
// are included.
|
||||
func (dir *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
|
||||
if dir == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine number of entries n and maximum height
|
||||
n := 0
|
||||
minDepth := 1 << 30 // infinity
|
||||
maxDepth := 0
|
||||
for d := range dir.iter(skipRoot) {
|
||||
n++
|
||||
if minDepth > d.Depth {
|
||||
minDepth = d.Depth
|
||||
}
|
||||
if maxDepth < d.Depth {
|
||||
maxDepth = d.Depth
|
||||
}
|
||||
}
|
||||
maxHeight := maxDepth - minDepth + 1
|
||||
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// create list
|
||||
list := make([]DirEntry, 0, n)
|
||||
for d := range dir.iter(skipRoot) {
|
||||
if filter != nil && !filter(d.Path) {
|
||||
continue
|
||||
}
|
||||
var p DirEntry
|
||||
p.Depth = d.Depth - minDepth
|
||||
p.Height = maxHeight - p.Depth
|
||||
// the path is relative to root.Path - remove the root.Path
|
||||
// prefix (the prefix should always be present but avoid
|
||||
// crashes and check)
|
||||
path := strings.TrimPrefix(d.Path, dir.Path)
|
||||
// remove leading separator if any - path must be relative
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
p.Path = path
|
||||
p.Name = d.Name
|
||||
p.HasPkg = d.HasPkg
|
||||
p.Synopsis = d.Synopsis
|
||||
p.RootType = d.RootType
|
||||
list = append(list, p)
|
||||
}
|
||||
|
||||
return &DirList{maxHeight, list}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
"golang.org/x/tools/godoc/vfs/gatefs"
|
||||
)
|
||||
|
||||
func TestNewDirTree(t *testing.T) {
|
||||
fsGate := make(chan bool, 20)
|
||||
rootfs := gatefs.New(vfs.OS(runtime.GOROOT()), fsGate)
|
||||
fs := vfs.NameSpace{}
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
|
||||
c := NewCorpus(fs)
|
||||
// 3 levels deep is enough for testing
|
||||
dir := c.newDirectory("/", 3)
|
||||
|
||||
processDir(t, dir)
|
||||
}
|
||||
|
||||
func processDir(t *testing.T, dir *Directory) {
|
||||
var list []string
|
||||
for _, d := range dir.Dirs {
|
||||
list = append(list, d.Name)
|
||||
// recursively process the lower level
|
||||
processDir(t, d)
|
||||
}
|
||||
|
||||
if sort.StringsAreSorted(list) == false {
|
||||
t.Errorf("list: %v is not sorted\n", list)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewDirectory(b *testing.B) {
|
||||
if testing.Short() {
|
||||
b.Skip("not running tests requiring large file scan in short mode")
|
||||
}
|
||||
|
||||
fsGate := make(chan bool, 20)
|
||||
|
||||
goroot := runtime.GOROOT()
|
||||
rootfs := gatefs.New(vfs.OS(goroot), fsGate)
|
||||
fs := vfs.NameSpace{}
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
||||
fs.Bind("/src/golang.org", gatefs.New(vfs.OS(p), fsGate), "/src/golang.org", vfs.BindAfter)
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for tries := 0; tries < b.N; tries++ {
|
||||
corpus := NewCorpus(fs)
|
||||
corpus.newDirectory("/", -1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
// This file implements FormatSelections and FormatText.
|
||||
// FormatText is used to HTML-format Go and non-Go source
|
||||
// text with line numbers and highlighted sections. It is
|
||||
// built on top of FormatSelections, a generic formatter
|
||||
// for "selected" text.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Implementation of FormatSelections
|
||||
|
||||
// A Segment describes a text segment [start, end).
|
||||
// The zero value of a Segment is a ready-to-use empty segment.
|
||||
type Segment struct {
|
||||
start, end int
|
||||
}
|
||||
|
||||
func (seg *Segment) isEmpty() bool { return seg.start >= seg.end }
|
||||
|
||||
// A Selection is an "iterator" function returning a text segment.
|
||||
// Repeated calls to a selection return consecutive, non-overlapping,
|
||||
// non-empty segments, followed by an infinite sequence of empty
|
||||
// segments. The first empty segment marks the end of the selection.
|
||||
type Selection func() Segment
|
||||
|
||||
// A LinkWriter writes some start or end "tag" to w for the text offset offs.
|
||||
// It is called by FormatSelections at the start or end of each link segment.
|
||||
type LinkWriter func(w io.Writer, offs int, start bool)
|
||||
|
||||
// A SegmentWriter formats a text according to selections and writes it to w.
|
||||
// The selections parameter is a bit set indicating which selections provided
|
||||
// to FormatSelections overlap with the text segment: If the n'th bit is set
|
||||
// in selections, the n'th selection provided to FormatSelections is overlapping
|
||||
// with the text.
|
||||
type SegmentWriter func(w io.Writer, text []byte, selections int)
|
||||
|
||||
// FormatSelections takes a text and writes it to w using link and segment
|
||||
// writers lw and sw as follows: lw is invoked for consecutive segment starts
|
||||
// and ends as specified through the links selection, and sw is invoked for
|
||||
// consecutive segments of text overlapped by the same selections as specified
|
||||
// by selections. The link writer lw may be nil, in which case the links
|
||||
// Selection is ignored.
|
||||
func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection, sw SegmentWriter, selections ...Selection) {
|
||||
// If we have a link writer, make the links
|
||||
// selection the last entry in selections
|
||||
if lw != nil {
|
||||
selections = append(selections, links)
|
||||
}
|
||||
|
||||
// compute the sequence of consecutive segment changes
|
||||
changes := newMerger(selections)
|
||||
|
||||
// The i'th bit in bitset indicates that the text
|
||||
// at the current offset is covered by selections[i].
|
||||
bitset := 0
|
||||
lastOffs := 0
|
||||
|
||||
// Text segments are written in a delayed fashion
|
||||
// such that consecutive segments belonging to the
|
||||
// same selection can be combined (peephole optimization).
|
||||
// last describes the last segment which has not yet been written.
|
||||
var last struct {
|
||||
begin, end int // valid if begin < end
|
||||
bitset int
|
||||
}
|
||||
|
||||
// flush writes the last delayed text segment
|
||||
flush := func() {
|
||||
if last.begin < last.end {
|
||||
sw(w, text[last.begin:last.end], last.bitset)
|
||||
}
|
||||
last.begin = last.end // invalidate last
|
||||
}
|
||||
|
||||
// segment runs the segment [lastOffs, end) with the selection
|
||||
// indicated by bitset through the segment peephole optimizer.
|
||||
segment := func(end int) {
|
||||
if lastOffs < end { // ignore empty segments
|
||||
if last.end != lastOffs || last.bitset != bitset {
|
||||
// the last segment is not adjacent to or
|
||||
// differs from the new one
|
||||
flush()
|
||||
// start a new segment
|
||||
last.begin = lastOffs
|
||||
}
|
||||
last.end = end
|
||||
last.bitset = bitset
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// get the next segment change
|
||||
index, offs, start := changes.next()
|
||||
if index < 0 || offs > len(text) {
|
||||
// no more segment changes or the next change
|
||||
// is past the end of the text - we're done
|
||||
break
|
||||
}
|
||||
// determine the kind of segment change
|
||||
if lw != nil && index == len(selections)-1 {
|
||||
// we have a link segment change (see start of this function):
|
||||
// format the previous selection segment, write the
|
||||
// link tag and start a new selection segment
|
||||
segment(offs)
|
||||
flush()
|
||||
lastOffs = offs
|
||||
lw(w, offs, start)
|
||||
} else {
|
||||
// we have a selection change:
|
||||
// format the previous selection segment, determine
|
||||
// the new selection bitset and start a new segment
|
||||
segment(offs)
|
||||
lastOffs = offs
|
||||
mask := 1 << uint(index)
|
||||
if start {
|
||||
bitset |= mask
|
||||
} else {
|
||||
bitset &^= mask
|
||||
}
|
||||
}
|
||||
}
|
||||
segment(len(text))
|
||||
flush()
|
||||
}
|
||||
|
||||
// A merger merges a slice of Selections and produces a sequence of
|
||||
// consecutive segment change events through repeated next() calls.
|
||||
type merger struct {
|
||||
selections []Selection
|
||||
segments []Segment // segments[i] is the next segment of selections[i]
|
||||
}
|
||||
|
||||
const infinity int = 2e9
|
||||
|
||||
func newMerger(selections []Selection) *merger {
|
||||
segments := make([]Segment, len(selections))
|
||||
for i, sel := range selections {
|
||||
segments[i] = Segment{infinity, infinity}
|
||||
if sel != nil {
|
||||
if seg := sel(); !seg.isEmpty() {
|
||||
segments[i] = seg
|
||||
}
|
||||
}
|
||||
}
|
||||
return &merger{selections, segments}
|
||||
}
|
||||
|
||||
// next returns the next segment change: index specifies the Selection
|
||||
// to which the segment belongs, offs is the segment start or end offset
|
||||
// as determined by the start value. If there are no more segment changes,
|
||||
// next returns an index value < 0.
|
||||
func (m *merger) next() (index, offs int, start bool) {
|
||||
// find the next smallest offset where a segment starts or ends
|
||||
offs = infinity
|
||||
index = -1
|
||||
for i, seg := range m.segments {
|
||||
switch {
|
||||
case seg.start < offs:
|
||||
offs = seg.start
|
||||
index = i
|
||||
start = true
|
||||
case seg.end < offs:
|
||||
offs = seg.end
|
||||
index = i
|
||||
start = false
|
||||
}
|
||||
}
|
||||
if index < 0 {
|
||||
// no offset found => all selections merged
|
||||
return
|
||||
}
|
||||
// offset found - it's either the start or end offset but
|
||||
// either way it is ok to consume the start offset: set it
|
||||
// to infinity so it won't be considered in the following
|
||||
// next call
|
||||
m.segments[index].start = infinity
|
||||
if start {
|
||||
return
|
||||
}
|
||||
// end offset found - consume it
|
||||
m.segments[index].end = infinity
|
||||
// advance to the next segment for that selection
|
||||
seg := m.selections[index]()
|
||||
if !seg.isEmpty() {
|
||||
m.segments[index] = seg
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Implementation of FormatText
|
||||
|
||||
// lineSelection returns the line segments for text as a Selection.
|
||||
func lineSelection(text []byte) Selection {
|
||||
i, j := 0, 0
|
||||
return func() (seg Segment) {
|
||||
// find next newline, if any
|
||||
for j < len(text) {
|
||||
j++
|
||||
if text[j-1] == '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < j {
|
||||
// text[i:j] constitutes a line
|
||||
seg = Segment{i, j}
|
||||
i = j
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// tokenSelection returns, as a selection, the sequence of
|
||||
// consecutive occurrences of token sel in the Go src text.
|
||||
func tokenSelection(src []byte, sel token.Token) Selection {
|
||||
var s scanner.Scanner
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
s.Init(file, src, nil, scanner.ScanComments)
|
||||
return func() (seg Segment) {
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
offs := file.Offset(pos)
|
||||
if tok == sel {
|
||||
seg = Segment{offs, offs + len(lit)}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// makeSelection is a helper function to make a Selection from a slice of pairs.
|
||||
// Pairs describing empty segments are ignored.
|
||||
func makeSelection(matches [][]int) Selection {
|
||||
i := 0
|
||||
return func() Segment {
|
||||
for i < len(matches) {
|
||||
m := matches[i]
|
||||
i++
|
||||
if m[0] < m[1] {
|
||||
// non-empty segment
|
||||
return Segment{m[0], m[1]}
|
||||
}
|
||||
}
|
||||
return Segment{}
|
||||
}
|
||||
}
|
||||
|
||||
// regexpSelection computes the Selection for the regular expression expr in text.
|
||||
func regexpSelection(text []byte, expr string) Selection {
|
||||
var matches [][]int
|
||||
if rx, err := regexp.Compile(expr); err == nil {
|
||||
matches = rx.FindAllIndex(text, -1)
|
||||
}
|
||||
return makeSelection(matches)
|
||||
}
|
||||
|
||||
var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
|
||||
|
||||
// RangeSelection computes the Selection for a text range described
|
||||
// by the argument str; the range description must match the selRx
|
||||
// regular expression.
|
||||
func RangeSelection(str string) Selection {
|
||||
m := selRx.FindStringSubmatch(str)
|
||||
if len(m) >= 2 {
|
||||
from, _ := strconv.Atoi(m[1])
|
||||
to, _ := strconv.Atoi(m[2])
|
||||
if from < to {
|
||||
return makeSelection([][]int{{from, to}})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Span tags for all the possible selection combinations that may
|
||||
// be generated by FormatText. Selections are indicated by a bitset,
|
||||
// and the value of the bitset specifies the tag to be used.
|
||||
//
|
||||
// bit 0: comments
|
||||
// bit 1: highlights
|
||||
// bit 2: selections
|
||||
var startTags = [][]byte{
|
||||
/* 000 */ []byte(``),
|
||||
/* 001 */ []byte(`<span class="comment">`),
|
||||
/* 010 */ []byte(`<span class="highlight">`),
|
||||
/* 011 */ []byte(`<span class="highlight-comment">`),
|
||||
/* 100 */ []byte(`<span class="selection">`),
|
||||
/* 101 */ []byte(`<span class="selection-comment">`),
|
||||
/* 110 */ []byte(`<span class="selection-highlight">`),
|
||||
/* 111 */ []byte(`<span class="selection-highlight-comment">`),
|
||||
}
|
||||
|
||||
var endTag = []byte(`</span>`)
|
||||
|
||||
func selectionTag(w io.Writer, text []byte, selections int) {
|
||||
if selections < len(startTags) {
|
||||
if tag := startTags[selections]; len(tag) > 0 {
|
||||
w.Write(tag)
|
||||
template.HTMLEscape(w, text)
|
||||
w.Write(endTag)
|
||||
return
|
||||
}
|
||||
}
|
||||
template.HTMLEscape(w, text)
|
||||
}
|
||||
|
||||
// FormatText HTML-escapes text and writes it to w.
|
||||
// Consecutive text segments are wrapped in HTML spans (with tags as
|
||||
// defined by startTags and endTag) as follows:
|
||||
//
|
||||
// - if line >= 0, line number (ln) spans are inserted before each line,
|
||||
// starting with the value of line
|
||||
// - if the text is Go source, comments get the "comment" span class
|
||||
// - each occurrence of the regular expression pattern gets the "highlight"
|
||||
// span class
|
||||
// - text segments covered by selection get the "selection" span class
|
||||
//
|
||||
// Comments, highlights, and selections may overlap arbitrarily; the respective
|
||||
// HTML span classes are specified in the startTags variable.
|
||||
func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) {
|
||||
var comments, highlights Selection
|
||||
if goSource {
|
||||
comments = tokenSelection(text, token.COMMENT)
|
||||
}
|
||||
if pattern != "" {
|
||||
highlights = regexpSelection(text, pattern)
|
||||
}
|
||||
if line >= 0 || comments != nil || highlights != nil || selection != nil {
|
||||
var lineTag LinkWriter
|
||||
if line >= 0 {
|
||||
lineTag = func(w io.Writer, _ int, start bool) {
|
||||
if start {
|
||||
fmt.Fprintf(w, "<span id=\"L%d\" class=\"ln\">%6d</span>", line, line)
|
||||
line++
|
||||
}
|
||||
}
|
||||
}
|
||||
FormatSelections(w, text, lineTag, lineSelection(text), selectionTag, comments, highlights, selection)
|
||||
} else {
|
||||
template.HTMLEscape(w, text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,935 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package godoc is a work-in-progress (2013-07-17) package to
|
||||
// begin splitting up the godoc binary into multiple pieces.
|
||||
//
|
||||
// This package comment will evolve over time as this package splits
|
||||
// into smaller pieces.
|
||||
package godoc // import "golang.org/x/tools/godoc"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/format"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
htmltemplate "html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Fake relative package path for built-ins. Documentation for all globals
|
||||
// (not just exported ones) will be shown for packages in this directory,
|
||||
// and there will be no association of consts, vars, and factory functions
|
||||
// with types (see issue 6645).
|
||||
const builtinPkgPath = "builtin"
|
||||
|
||||
// FuncMap defines template functions used in 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.
|
||||
func (p *Presentation) FuncMap() template.FuncMap {
|
||||
p.initFuncMapOnce.Do(p.initFuncMap)
|
||||
return p.funcMap
|
||||
}
|
||||
|
||||
func (p *Presentation) TemplateFuncs() template.FuncMap {
|
||||
p.initFuncMapOnce.Do(p.initFuncMap)
|
||||
return p.templateFuncs
|
||||
}
|
||||
|
||||
func (p *Presentation) initFuncMap() {
|
||||
if p.Corpus == nil {
|
||||
panic("nil Presentation.Corpus")
|
||||
}
|
||||
p.templateFuncs = template.FuncMap{
|
||||
"code": p.code,
|
||||
}
|
||||
p.funcMap = template.FuncMap{
|
||||
// various helpers
|
||||
"filename": filenameFunc,
|
||||
"repeat": strings.Repeat,
|
||||
"since": p.Corpus.pkgAPIInfo.sinceVersionFunc,
|
||||
|
||||
// access to FileInfos (directory listings)
|
||||
"fileInfoName": fileInfoNameFunc,
|
||||
"fileInfoTime": fileInfoTimeFunc,
|
||||
|
||||
// access to search result information
|
||||
"infoKind_html": infoKind_htmlFunc,
|
||||
"infoLine": p.infoLineFunc,
|
||||
"infoSnippet_html": p.infoSnippet_htmlFunc,
|
||||
|
||||
// formatting of AST nodes
|
||||
"node": p.nodeFunc,
|
||||
"node_html": p.node_htmlFunc,
|
||||
"comment_html": comment_htmlFunc,
|
||||
"sanitize": sanitizeFunc,
|
||||
|
||||
// support for URL attributes
|
||||
"pkgLink": pkgLinkFunc,
|
||||
"srcLink": srcLinkFunc,
|
||||
"posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
|
||||
"docLink": docLinkFunc,
|
||||
"queryLink": queryLinkFunc,
|
||||
"srcBreadcrumb": srcBreadcrumbFunc,
|
||||
"srcToPkgLink": srcToPkgLinkFunc,
|
||||
|
||||
// formatting of Examples
|
||||
"example_html": p.example_htmlFunc,
|
||||
"example_name": p.example_nameFunc,
|
||||
"example_suffix": p.example_suffixFunc,
|
||||
|
||||
// formatting of analysis information
|
||||
"callgraph_html": p.callgraph_htmlFunc,
|
||||
"implements_html": p.implements_htmlFunc,
|
||||
"methodset_html": p.methodset_htmlFunc,
|
||||
|
||||
// formatting of Notes
|
||||
"noteTitle": noteTitle,
|
||||
|
||||
// Number operation
|
||||
"multiply": multiply,
|
||||
|
||||
// formatting of PageInfoMode query string
|
||||
"modeQueryString": modeQueryString,
|
||||
|
||||
// check whether to display third party section or not
|
||||
"hasThirdParty": hasThirdParty,
|
||||
|
||||
// get the no. of columns to split the toc in search page
|
||||
"tocColCount": tocColCount,
|
||||
}
|
||||
if p.URLForSrc != nil {
|
||||
p.funcMap["srcLink"] = p.URLForSrc
|
||||
}
|
||||
if p.URLForSrcPos != nil {
|
||||
p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos)
|
||||
}
|
||||
if p.URLForSrcQuery != nil {
|
||||
p.funcMap["queryLink"] = p.URLForSrcQuery
|
||||
}
|
||||
}
|
||||
|
||||
func multiply(a, b int) int { return a * b }
|
||||
|
||||
func filenameFunc(path string) string {
|
||||
_, localname := pathpkg.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 (p *Presentation) infoLineFunc(info SpotInfo) int {
|
||||
line := info.Lori()
|
||||
if info.IsIndex() {
|
||||
index, _ := p.Corpus.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 (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
|
||||
if info.IsIndex() {
|
||||
index, _ := p.Corpus.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 (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
p.writeNode(&buf, info, info.FSet, node)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
|
||||
var buf1 bytes.Buffer
|
||||
p.writeNode(&buf1, info, info.FSet, node)
|
||||
|
||||
var buf2 bytes.Buffer
|
||||
if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
|
||||
LinkifyText(&buf2, buf1.Bytes(), n)
|
||||
if st, name := isStructTypeDecl(n); st != nil {
|
||||
addStructFieldIDAttributes(&buf2, name, st)
|
||||
}
|
||||
} else {
|
||||
FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
|
||||
}
|
||||
|
||||
return buf2.String()
|
||||
}
|
||||
|
||||
// isStructTypeDecl checks whether n is a struct declaration.
|
||||
// It either returns a non-nil StructType and its name, or zero values.
|
||||
func isStructTypeDecl(n ast.Node) (st *ast.StructType, name string) {
|
||||
gd, ok := n.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.TYPE {
|
||||
return nil, ""
|
||||
}
|
||||
if gd.Lparen > 0 {
|
||||
// Parenthesized type. Who does that, anyway?
|
||||
// TODO: Reportedly gri does. Fix this to handle that too.
|
||||
return nil, ""
|
||||
}
|
||||
if len(gd.Specs) != 1 {
|
||||
return nil, ""
|
||||
}
|
||||
ts, ok := gd.Specs[0].(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return nil, ""
|
||||
}
|
||||
st, ok = ts.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, ""
|
||||
}
|
||||
return st, ts.Name.Name
|
||||
}
|
||||
|
||||
// addStructFieldIDAttributes modifies the contents of buf such that
|
||||
// all struct fields of the named struct have <span id='name.Field'>
|
||||
// in them, so people can link to /#Struct.Field.
|
||||
func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) {
|
||||
if st.Fields == nil {
|
||||
return
|
||||
}
|
||||
// needsLink is a set of identifiers that still need to be
|
||||
// linked, where value == key, to avoid an allocation in func
|
||||
// linkedField.
|
||||
needsLink := make(map[string]string)
|
||||
|
||||
for _, f := range st.Fields.List {
|
||||
if len(f.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
fieldName := f.Names[0].Name
|
||||
needsLink[fieldName] = fieldName
|
||||
}
|
||||
var newBuf bytes.Buffer
|
||||
foreachLine(buf.Bytes(), func(line []byte) {
|
||||
if fieldName := linkedField(line, needsLink); fieldName != "" {
|
||||
fmt.Fprintf(&newBuf, `<span id="%s.%s"></span>`, name, fieldName)
|
||||
delete(needsLink, fieldName)
|
||||
}
|
||||
newBuf.Write(line)
|
||||
})
|
||||
buf.Reset()
|
||||
buf.Write(newBuf.Bytes())
|
||||
}
|
||||
|
||||
// foreachLine calls fn for each line of in, where a line includes
|
||||
// the trailing "\n", except on the last line, if it doesn't exist.
|
||||
func foreachLine(in []byte, fn func(line []byte)) {
|
||||
for len(in) > 0 {
|
||||
nl := bytes.IndexByte(in, '\n')
|
||||
if nl == -1 {
|
||||
fn(in)
|
||||
return
|
||||
}
|
||||
fn(in[:nl+1])
|
||||
in = in[nl+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// commentPrefix is the line prefix for comments after they've been HTMLified.
|
||||
var commentPrefix = []byte(`<span class="comment">// `)
|
||||
|
||||
// linkedField determines whether the given line starts with an
|
||||
// identifier in the provided ids map (mapping from identifier to the
|
||||
// same identifier). The line can start with either an identifier or
|
||||
// an identifier in a comment. If one matches, it returns the
|
||||
// identifier that matched. Otherwise it returns the empty string.
|
||||
func linkedField(line []byte, ids map[string]string) string {
|
||||
line = bytes.TrimSpace(line)
|
||||
|
||||
// For fields with a doc string of the
|
||||
// conventional form, we put the new span into
|
||||
// the comment instead of the field.
|
||||
// The "conventional" form is a complete sentence
|
||||
// per https://golang.org/s/style#comment-sentences like:
|
||||
//
|
||||
// // Foo is an optional Fooer to foo the foos.
|
||||
// Foo Fooer
|
||||
//
|
||||
// In this case, we want the #StructName.Foo
|
||||
// link to make the browser go to the comment
|
||||
// line "Foo is an optional Fooer" instead of
|
||||
// the "Foo Fooer" line, which could otherwise
|
||||
// obscure the docs above the browser's "fold".
|
||||
//
|
||||
// TODO: do this better, so it works for all
|
||||
// comments, including unconventional ones.
|
||||
line = bytes.TrimPrefix(line, commentPrefix)
|
||||
id := scanIdentifier(line)
|
||||
if len(id) == 0 {
|
||||
// No leading identifier. Avoid map lookup for
|
||||
// somewhat common case.
|
||||
return ""
|
||||
}
|
||||
return ids[string(id)]
|
||||
}
|
||||
|
||||
// scanIdentifier scans a valid Go identifier off the front of v and
|
||||
// either returns a subslice of v if there's a valid identifier, or
|
||||
// returns a zero-length slice.
|
||||
func scanIdentifier(v []byte) []byte {
|
||||
var n int // number of leading bytes of v belonging to an identifier
|
||||
for {
|
||||
r, width := utf8.DecodeRune(v[n:])
|
||||
if !(isLetter(r) || n > 0 && isDigit(r)) {
|
||||
break
|
||||
}
|
||||
n += width
|
||||
}
|
||||
return v[:n]
|
||||
}
|
||||
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
func comment_htmlFunc(info *PageInfo, comment string) string {
|
||||
// TODO(gri) Provide list of words (e.g. function parameters)
|
||||
// to be emphasized by ToHTML.
|
||||
return string(info.PDoc.HTML(comment))
|
||||
}
|
||||
|
||||
// sanitizeFunc sanitizes the argument src by replacing newlines with
|
||||
// blanks, removing extra blanks, and by removing trailing whitespace
|
||||
// and commas before closing parentheses.
|
||||
func sanitizeFunc(src string) string {
|
||||
buf := make([]byte, len(src))
|
||||
j := 0 // buf index
|
||||
comma := -1 // comma index if >= 0
|
||||
for i := 0; i < len(src); i++ {
|
||||
ch := src[i]
|
||||
switch ch {
|
||||
case '\t', '\n', ' ':
|
||||
// ignore whitespace at the beginning, after a blank, or after opening parentheses
|
||||
if j == 0 {
|
||||
continue
|
||||
}
|
||||
if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
|
||||
continue
|
||||
}
|
||||
// replace all whitespace with blanks
|
||||
ch = ' '
|
||||
case ',':
|
||||
comma = j
|
||||
case ')', '}', ']':
|
||||
// remove any trailing comma
|
||||
if comma >= 0 {
|
||||
j = comma
|
||||
}
|
||||
// remove any trailing whitespace
|
||||
if j > 0 && buf[j-1] == ' ' {
|
||||
j--
|
||||
}
|
||||
default:
|
||||
comma = -1
|
||||
}
|
||||
buf[j] = ch
|
||||
j++
|
||||
}
|
||||
// remove trailing blank, if any
|
||||
if j > 0 && buf[j-1] == ' ' {
|
||||
j--
|
||||
}
|
||||
return string(buf[:j])
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Dirname string // directory containing the package
|
||||
Err error // error or nil
|
||||
|
||||
Mode PageInfoMode // display metadata from query string
|
||||
|
||||
// package info
|
||||
FSet *token.FileSet // nil if no package documentation
|
||||
PDoc *doc.Package // nil if no package documentation
|
||||
Examples []*doc.Example // nil if no example code
|
||||
Notes map[string][]*doc.Note // nil if no package Notes
|
||||
PAst map[string]*ast.File // nil if no AST with package exports
|
||||
IsMain bool // true for package main
|
||||
IsFiltered bool // true if results were filtered
|
||||
|
||||
// analysis info
|
||||
TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type)
|
||||
AnalysisData htmltemplate.JS // array of TypeInfoJSON values
|
||||
CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer)
|
||||
CallGraphIndex map[string]int // maps func name to index in CallGraph
|
||||
|
||||
// directory info
|
||||
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
|
||||
}
|
||||
|
||||
func (info *PageInfo) IsEmpty() bool {
|
||||
return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
|
||||
}
|
||||
|
||||
func pkgLinkFunc(path string) string {
|
||||
// because of the irregular mapping under goroot
|
||||
// we need to correct certain relative paths
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimPrefix(path, "src/")
|
||||
path = strings.TrimPrefix(path, "pkg/")
|
||||
return "pkg/" + path
|
||||
}
|
||||
|
||||
// srcToPkgLinkFunc builds an <a> tag linking to the package
|
||||
// documentation of relpath.
|
||||
func srcToPkgLinkFunc(relpath string) string {
|
||||
relpath = pkgLinkFunc(relpath)
|
||||
relpath = pathpkg.Dir(relpath)
|
||||
if relpath == "pkg" {
|
||||
return `<a href="/pkg">Index</a>`
|
||||
}
|
||||
return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
|
||||
}
|
||||
|
||||
// srcBreadcrumbFunc converts each segment of relpath to a HTML <a>.
|
||||
// Each segment links to its corresponding src directories.
|
||||
func srcBreadcrumbFunc(relpath string) string {
|
||||
segments := strings.Split(relpath, "/")
|
||||
var buf bytes.Buffer
|
||||
var selectedSegment string
|
||||
var selectedIndex int
|
||||
|
||||
if strings.HasSuffix(relpath, "/") {
|
||||
// relpath is a directory ending with a "/".
|
||||
// Selected segment is the segment before the last slash.
|
||||
selectedIndex = len(segments) - 2
|
||||
selectedSegment = segments[selectedIndex] + "/"
|
||||
} else {
|
||||
selectedIndex = len(segments) - 1
|
||||
selectedSegment = segments[selectedIndex]
|
||||
}
|
||||
|
||||
for i := range segments[:selectedIndex] {
|
||||
buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
|
||||
strings.Join(segments[:i+1], "/"),
|
||||
segments[i],
|
||||
))
|
||||
}
|
||||
|
||||
buf.WriteString(`<span class="text-muted">`)
|
||||
buf.WriteString(selectedSegment)
|
||||
buf.WriteString(`</span>`)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
|
||||
// n must be an ast.Node or a *doc.Note
|
||||
return func(info *PageInfo, n interface{}) string {
|
||||
var pos, end token.Pos
|
||||
|
||||
switch n := n.(type) {
|
||||
case ast.Node:
|
||||
pos = n.Pos()
|
||||
end = n.End()
|
||||
case *doc.Note:
|
||||
pos = n.Pos
|
||||
end = n.End
|
||||
default:
|
||||
panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
|
||||
}
|
||||
|
||||
var relpath string
|
||||
var line int
|
||||
var low, high int // selection offset range
|
||||
|
||||
if pos.IsValid() {
|
||||
p := info.FSet.Position(pos)
|
||||
relpath = p.Filename
|
||||
line = p.Line
|
||||
low = p.Offset
|
||||
}
|
||||
if end.IsValid() {
|
||||
high = info.FSet.Position(end).Offset
|
||||
}
|
||||
|
||||
return srcPosLinkFunc(relpath, line, low, high)
|
||||
}
|
||||
}
|
||||
|
||||
func srcPosLinkFunc(s string, line, low, high int) string {
|
||||
s = srcLinkFunc(s)
|
||||
var buf bytes.Buffer
|
||||
template.HTMLEscape(&buf, []byte(s))
|
||||
// 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()
|
||||
}
|
||||
|
||||
func srcLinkFunc(s string) string {
|
||||
s = pathpkg.Clean("/" + s)
|
||||
if !strings.HasPrefix(s, "/src/") {
|
||||
s = "/src" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// queryLinkFunc returns a URL for a line in a source file with a highlighted
|
||||
// query term.
|
||||
// s is expected to be a path to a source file.
|
||||
// query is expected to be a string that has already been appropriately escaped
|
||||
// for use in a URL query.
|
||||
func queryLinkFunc(s, query string, line int) string {
|
||||
url := pathpkg.Clean("/"+s) + "?h=" + query
|
||||
if line > 0 {
|
||||
url += "#L" + strconv.Itoa(line)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func docLinkFunc(s string, ident string) string {
|
||||
return pathpkg.Clean("/pkg/"+s) + "/#" + ident
|
||||
}
|
||||
|
||||
func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
|
||||
var buf bytes.Buffer
|
||||
for _, eg := range info.Examples {
|
||||
name := stripExampleSuffix(eg.Name)
|
||||
|
||||
if name != funcName {
|
||||
continue
|
||||
}
|
||||
|
||||
// print code
|
||||
cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
|
||||
code := p.node_htmlFunc(info, cnode, true)
|
||||
out := eg.Output
|
||||
wholeFile := true
|
||||
|
||||
// Additional formatting if this is a function body.
|
||||
if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
|
||||
wholeFile = false
|
||||
// remove surrounding braces
|
||||
code = code[1 : n-1]
|
||||
// unindent
|
||||
code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "")
|
||||
// remove output comment
|
||||
if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
|
||||
code = strings.TrimSpace(code[:loc[0]])
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the playground code in standard Go style
|
||||
// (use tabs, no comment highlight, etc).
|
||||
play := ""
|
||||
if eg.Play != nil && p.ShowPlayground {
|
||||
var buf bytes.Buffer
|
||||
eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
|
||||
if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
play = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Drop output, as the output comment will appear in the code.
|
||||
if wholeFile && play == "" {
|
||||
out = ""
|
||||
}
|
||||
|
||||
if p.ExampleHTML == nil {
|
||||
out = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
err := p.ExampleHTML.Execute(&buf, struct {
|
||||
Name, Doc, Code, Play, Output string
|
||||
}{eg.Name, eg.Doc, code, play, out})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
|
||||
if len(cg) == 0 {
|
||||
return cg
|
||||
}
|
||||
|
||||
for i := range cg {
|
||||
if !strings.HasPrefix(cg[i].Text(), "+build ") {
|
||||
// Found the first non-build tag, return from here until the end
|
||||
// of the slice.
|
||||
return cg[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// There weren't any non-build tags, return an empty slice.
|
||||
return []*ast.CommentGroup{}
|
||||
}
|
||||
|
||||
// example_nameFunc takes an example function name and returns its display
|
||||
// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
|
||||
func (p *Presentation) example_nameFunc(s string) string {
|
||||
name, suffix := splitExampleName(s)
|
||||
// replace _ with . for method names
|
||||
name = strings.Replace(name, "_", ".", 1)
|
||||
// use "Package" if no name provided
|
||||
if name == "" {
|
||||
name = "Package"
|
||||
}
|
||||
return name + suffix
|
||||
}
|
||||
|
||||
// example_suffixFunc takes an example function name and returns its suffix in
|
||||
// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
|
||||
func (p *Presentation) example_suffixFunc(name string) string {
|
||||
_, suffix := splitExampleName(name)
|
||||
return suffix
|
||||
}
|
||||
|
||||
// implements_htmlFunc returns the "> Implements" toggle for a package-level named type.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string {
|
||||
if p.ImplementsHTML == nil {
|
||||
return ""
|
||||
}
|
||||
index, ok := info.TypeInfoIndex[typeName]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// methodset_htmlFunc returns the "> Method set" toggle for a package-level named type.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string {
|
||||
if p.MethodSetHTML == nil {
|
||||
return ""
|
||||
}
|
||||
index, ok := info.TypeInfoIndex[typeName]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// callgraph_htmlFunc returns the "> Call graph" toggle for a package-level func.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
|
||||
if p.CallGraphHTML == nil {
|
||||
return ""
|
||||
}
|
||||
if recv != "" {
|
||||
// Format must match (*ssa.Function).RelString().
|
||||
name = fmt.Sprintf("(%s).%s", recv, name)
|
||||
}
|
||||
index, ok := info.CallGraphIndex[name]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func noteTitle(note string) string {
|
||||
return strings.Title(strings.ToLower(note))
|
||||
}
|
||||
|
||||
func startsWithUppercase(s string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(s)
|
||||
return unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
|
||||
|
||||
// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
|
||||
// while keeping uppercase Braz in Foo_Braz.
|
||||
func stripExampleSuffix(name string) string {
|
||||
if i := strings.LastIndex(name, "_"); i != -1 {
|
||||
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
|
||||
name = name[:i]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func splitExampleName(s string) (name, suffix string) {
|
||||
i := strings.LastIndex(s, "_")
|
||||
if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
|
||||
name = s[:i]
|
||||
suffix = " (" + strings.Title(s[i+1:]) + ")"
|
||||
return
|
||||
}
|
||||
name = s
|
||||
return
|
||||
}
|
||||
|
||||
// replaceLeadingIndentation replaces oldIndent at the beginning of each line
|
||||
// with newIndent. This is used for formatting examples. Raw strings that
|
||||
// span multiple lines are handled specially: oldIndent is not removed (since
|
||||
// go/printer will not add any indentation there), but newIndent is added
|
||||
// (since we may still want leading indentation).
|
||||
func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
|
||||
// Handle indent at the beginning of the first line. After this, we handle
|
||||
// indentation only after a newline.
|
||||
var buf bytes.Buffer
|
||||
if strings.HasPrefix(body, oldIndent) {
|
||||
buf.WriteString(newIndent)
|
||||
body = body[len(oldIndent):]
|
||||
}
|
||||
|
||||
// Use a state machine to keep track of whether we're in a string or
|
||||
// rune literal while we process the rest of the code.
|
||||
const (
|
||||
codeState = iota
|
||||
runeState
|
||||
interpretedStringState
|
||||
rawStringState
|
||||
)
|
||||
searchChars := []string{
|
||||
"'\"`\n", // codeState
|
||||
`\'`, // runeState
|
||||
`\"`, // interpretedStringState
|
||||
"`\n", // rawStringState
|
||||
// newlineState does not need to search
|
||||
}
|
||||
state := codeState
|
||||
for {
|
||||
i := strings.IndexAny(body, searchChars[state])
|
||||
if i < 0 {
|
||||
buf.WriteString(body)
|
||||
break
|
||||
}
|
||||
c := body[i]
|
||||
buf.WriteString(body[:i+1])
|
||||
body = body[i+1:]
|
||||
switch state {
|
||||
case codeState:
|
||||
switch c {
|
||||
case '\'':
|
||||
state = runeState
|
||||
case '"':
|
||||
state = interpretedStringState
|
||||
case '`':
|
||||
state = rawStringState
|
||||
case '\n':
|
||||
if strings.HasPrefix(body, oldIndent) {
|
||||
buf.WriteString(newIndent)
|
||||
body = body[len(oldIndent):]
|
||||
}
|
||||
}
|
||||
|
||||
case runeState:
|
||||
switch c {
|
||||
case '\\':
|
||||
r, size := utf8.DecodeRuneInString(body)
|
||||
buf.WriteRune(r)
|
||||
body = body[size:]
|
||||
case '\'':
|
||||
state = codeState
|
||||
}
|
||||
|
||||
case interpretedStringState:
|
||||
switch c {
|
||||
case '\\':
|
||||
r, size := utf8.DecodeRuneInString(body)
|
||||
buf.WriteRune(r)
|
||||
body = body[size:]
|
||||
case '"':
|
||||
state = codeState
|
||||
}
|
||||
|
||||
case rawStringState:
|
||||
switch c {
|
||||
case '`':
|
||||
state = codeState
|
||||
case '\n':
|
||||
buf.WriteString(newIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// writeNode writes the AST node x to w.
|
||||
//
|
||||
// The provided fset must be non-nil. The pageInfo is optional. If
|
||||
// present, the pageInfo is used to add comments to struct fields to
|
||||
// say which version of Go introduced them.
|
||||
func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, 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)
|
||||
|
||||
var pkgName, structName string
|
||||
var apiInfo pkgAPIVersions
|
||||
if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
|
||||
p.Corpus != nil &&
|
||||
gd.Tok == token.TYPE && len(gd.Specs) != 0 {
|
||||
pkgName = pageInfo.PDoc.ImportPath
|
||||
if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
|
||||
if _, ok := ts.Type.(*ast.StructType); ok {
|
||||
structName = ts.Name.Name
|
||||
}
|
||||
}
|
||||
apiInfo = p.Corpus.pkgAPIInfo[pkgName]
|
||||
}
|
||||
|
||||
var out = w
|
||||
var buf bytes.Buffer
|
||||
if structName != "" {
|
||||
out = &buf
|
||||
}
|
||||
|
||||
mode := printer.TabIndent | printer.UseSpaces
|
||||
err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
// Add comments to struct fields saying which Go version introduced them.
|
||||
if structName != "" {
|
||||
fieldSince := apiInfo.fieldSince[structName]
|
||||
typeSince := apiInfo.typeSince[structName]
|
||||
// Add/rewrite comments on struct fields to note which Go version added them.
|
||||
var buf2 bytes.Buffer
|
||||
buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
|
||||
bs := bufio.NewScanner(&buf)
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
field := firstIdent(line)
|
||||
var since string
|
||||
if field != "" {
|
||||
since = fieldSince[field]
|
||||
if since != "" && since == typeSince {
|
||||
// Don't highlight field versions if they were the
|
||||
// same as the struct itself.
|
||||
since = ""
|
||||
}
|
||||
}
|
||||
if since == "" {
|
||||
buf2.Write(line)
|
||||
} else {
|
||||
if bytes.Contains(line, slashSlash) {
|
||||
line = bytes.TrimRight(line, " \t.")
|
||||
buf2.Write(line)
|
||||
buf2.WriteString("; added in Go ")
|
||||
} else {
|
||||
buf2.Write(line)
|
||||
buf2.WriteString(" // Go ")
|
||||
}
|
||||
buf2.WriteString(since)
|
||||
}
|
||||
buf2.WriteByte('\n')
|
||||
}
|
||||
w.Write(buf2.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
var slashSlash = []byte("//")
|
||||
|
||||
// WriteNode writes x to w.
|
||||
// TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode.
|
||||
func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
|
||||
p.writeNode(w, nil, fset, x)
|
||||
}
|
||||
|
||||
// firstIdent returns the first identifier in x.
|
||||
// This actually parses "identifiers" that begin with numbers too, but we
|
||||
// never feed it such input, so it's fine.
|
||||
func firstIdent(x []byte) string {
|
||||
x = bytes.TrimSpace(x)
|
||||
i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
|
||||
if i == -1 {
|
||||
return string(x)
|
||||
}
|
||||
return string(x[:i])
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
//go:build go1.7
|
||||
// +build go1.7
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Verify that scanIdentifier isn't quadratic.
|
||||
// This doesn't actually measure and fail on its own, but it was previously
|
||||
// very obvious when running by hand.
|
||||
//
|
||||
// TODO: if there's a reliable and non-flaky way to test this, do so.
|
||||
// Maybe count user CPU time instead of wall time? But that's not easy
|
||||
// to do portably in Go.
|
||||
func TestStructField(t *testing.T) {
|
||||
for _, n := range []int{10, 100, 1000, 10000} {
|
||||
n := n
|
||||
t.Run(fmt.Sprint(n), func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "package foo\n\ntype T struct {\n")
|
||||
for i := 0; i < n; i++ {
|
||||
fmt.Fprintf(&buf, "\t// Field%d is foo.\n\tField%d int\n\n", i, i)
|
||||
}
|
||||
fmt.Fprintf(&buf, "}\n")
|
||||
linkifySource(t, buf.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPkgLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt", "pkg/fmt"},
|
||||
{"src/fmt", "pkg/fmt"},
|
||||
{"/fmt", "pkg/fmt"},
|
||||
{"fmt", "pkg/fmt"},
|
||||
} {
|
||||
if got := pkgLinkFunc(tc.path); got != tc.want {
|
||||
t.Errorf("pkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcPosLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
line int
|
||||
low int
|
||||
high int
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt/print.go", 42, 30, 50, "/src/fmt/print.go?s=30:50#L32"},
|
||||
{"/src/fmt/print.go", 2, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
|
||||
{"/src/fmt/print.go", 2, 0, 0, "/src/fmt/print.go#L2"},
|
||||
{"/src/fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
|
||||
{"/src/fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
|
||||
{"fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
|
||||
{"fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
|
||||
} {
|
||||
if got := srcPosLinkFunc(tc.src, tc.line, tc.low, tc.high); got != tc.want {
|
||||
t.Errorf("srcLinkFunc(%v, %v, %v, %v) = %v; want %v", tc.src, tc.line, tc.low, tc.high, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt/print.go", "/src/fmt/print.go"},
|
||||
{"src/fmt/print.go", "/src/fmt/print.go"},
|
||||
{"/fmt/print.go", "/src/fmt/print.go"},
|
||||
{"fmt/print.go", "/src/fmt/print.go"},
|
||||
} {
|
||||
if got := srcLinkFunc(tc.src); got != tc.want {
|
||||
t.Errorf("srcLinkFunc(%v) = %v; want %v", tc.src, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
query string
|
||||
line int
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt/print.go", "Sprintf", 33, "/src/fmt/print.go?h=Sprintf#L33"},
|
||||
{"/src/fmt/print.go", "Sprintf", 0, "/src/fmt/print.go?h=Sprintf"},
|
||||
{"src/fmt/print.go", "EOF", 33, "/src/fmt/print.go?h=EOF#L33"},
|
||||
{"src/fmt/print.go", "a%3f+%26b", 1, "/src/fmt/print.go?h=a%3f+%26b#L1"},
|
||||
} {
|
||||
if got := queryLinkFunc(tc.src, tc.query, tc.line); got != tc.want {
|
||||
t.Errorf("queryLinkFunc(%v, %v, %v) = %v; want %v", tc.src, tc.query, tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
ident string
|
||||
want string
|
||||
}{
|
||||
{"fmt", "Sprintf", "/pkg/fmt/#Sprintf"},
|
||||
{"fmt", "EOF", "/pkg/fmt/#EOF"},
|
||||
} {
|
||||
if got := docLinkFunc(tc.src, tc.ident); got != tc.want {
|
||||
t.Errorf("docLinkFunc(%v, %v) = %v; want %v", tc.src, tc.ident, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{},
|
||||
{"foo", "foo"},
|
||||
{"func f()", "func f()"},
|
||||
{"func f(a int,)", "func f(a int)"},
|
||||
{"func f(a int,\n)", "func f(a int)"},
|
||||
{"func f(\n\ta int,\n\tb int,\n\tc int,\n)", "func f(a int, b int, c int)"},
|
||||
{" ( a, b, c ) ", "(a, b, c)"},
|
||||
{"( a, b, c int, foo bar , )", "(a, b, c int, foo bar)"},
|
||||
{"{ a, b}", "{a, b}"},
|
||||
{"[ a, b]", "[a, b]"},
|
||||
} {
|
||||
if got := sanitizeFunc(tc.src); got != tc.want {
|
||||
t.Errorf("sanitizeFunc(%v) = %v; want %v", tc.src, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we add <span id="StructName.FieldName"> elements
|
||||
// to the HTML of struct fields.
|
||||
func TestStructFieldsIDAttributes(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
type T struct {
|
||||
NoDoc string
|
||||
|
||||
// Doc has a comment.
|
||||
Doc string
|
||||
|
||||
// Opt, if non-nil, is an option.
|
||||
Opt *int
|
||||
|
||||
// Опция - другое поле.
|
||||
Опция bool
|
||||
}
|
||||
`))
|
||||
want := `type T struct {
|
||||
<span id="T.NoDoc"></span>NoDoc <a href="/pkg/builtin/#string">string</a>
|
||||
|
||||
<span id="T.Doc"></span><span class="comment">// Doc has a comment.</span>
|
||||
Doc <a href="/pkg/builtin/#string">string</a>
|
||||
|
||||
<span id="T.Opt"></span><span class="comment">// Opt, if non-nil, is an option.</span>
|
||||
Opt *<a href="/pkg/builtin/#int">int</a>
|
||||
|
||||
<span id="T.Опция"></span><span class="comment">// Опция - другое поле.</span>
|
||||
Опция <a href="/pkg/builtin/#bool">bool</a>
|
||||
}`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we add <span id="ConstName"> elements to the HTML
|
||||
// of definitions in const and var specs.
|
||||
func TestValueSpecIDAttributes(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
const (
|
||||
NoDoc string = "NoDoc"
|
||||
|
||||
// Doc has a comment
|
||||
Doc = "Doc"
|
||||
|
||||
NoVal
|
||||
)`))
|
||||
want := `const (
|
||||
<span id="NoDoc">NoDoc</span> <a href="/pkg/builtin/#string">string</a> = "NoDoc"
|
||||
|
||||
<span class="comment">// Doc has a comment</span>
|
||||
<span id="Doc">Doc</span> = "Doc"
|
||||
|
||||
<span id="NoVal">NoVal</span>
|
||||
)`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompositeLitLinkFields(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
type T struct {
|
||||
X int
|
||||
}
|
||||
|
||||
var S T = T{X: 12}`))
|
||||
want := `type T struct {
|
||||
<span id="T.X"></span>X <a href="/pkg/builtin/#int">int</a>
|
||||
}
|
||||
var <span id="S">S</span> <a href="#T">T</a> = <a href="#T">T</a>{<a href="#T.X">X</a>: 12}`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncDeclNotLink(t *testing.T) {
|
||||
// Function.
|
||||
got := linkifySource(t, []byte(`
|
||||
package http
|
||||
|
||||
func Get(url string) (resp *Response, err error)`))
|
||||
want := `func Get(url <a href="/pkg/builtin/#string">string</a>) (resp *<a href="#Response">Response</a>, err <a href="/pkg/builtin/#error">error</a>)`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
|
||||
// Method.
|
||||
got = linkifySource(t, []byte(`
|
||||
package http
|
||||
|
||||
func (h Header) Get(key string) string`))
|
||||
want = `func (h <a href="#Header">Header</a>) Get(key <a href="/pkg/builtin/#string">string</a>) <a href="/pkg/builtin/#string">string</a>`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func linkifySource(t *testing.T, src []byte) string {
|
||||
p := &Presentation{
|
||||
DeclLinks: true,
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
pi := &PageInfo{
|
||||
FSet: fset,
|
||||
}
|
||||
sep := ""
|
||||
for _, decl := range af.Decls {
|
||||
buf.WriteString(sep)
|
||||
sep = "\n"
|
||||
buf.WriteString(p.node_htmlFunc(pi, decl, true))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestScanIdentifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"foo bar", "foo"},
|
||||
{"foo/bar", "foo"},
|
||||
{" foo", ""},
|
||||
{"фоо", "фоо"},
|
||||
{"f123", "f123"},
|
||||
{"123f", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := scanIdentifier([]byte(tt.in))
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("scanIdentifier(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceLeadingIndentation(t *testing.T) {
|
||||
oldIndent := strings.Repeat(" ", 2)
|
||||
newIndent := strings.Repeat(" ", 4)
|
||||
tests := []struct {
|
||||
src, want string
|
||||
}{
|
||||
{" foo\n bar\n baz", " foo\n bar\n baz"},
|
||||
{" '`'\n '`'\n", " '`'\n '`'\n"},
|
||||
{" '\\''\n '`'\n", " '\\''\n '`'\n"},
|
||||
{" \"`\"\n \"`\"\n", " \"`\"\n \"`\"\n"},
|
||||
{" `foo\n bar`", " `foo\n bar`"},
|
||||
{" `foo\\`\n bar", " `foo\\`\n bar"},
|
||||
{" '\\`'`foo\n bar", " '\\`'`foo\n bar"},
|
||||
{
|
||||
" if true {\n foo := `One\n \tTwo\nThree`\n }\n",
|
||||
" if true {\n foo := `One\n \tTwo\n Three`\n }\n",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := replaceLeadingIndentation(tc.src, oldIndent, newIndent); got != tc.want {
|
||||
t.Errorf("replaceLeadingIndentation:\n%v\n---\nhave:\n%v\n---\nwant:\n%v\n",
|
||||
tc.src, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcBreadcrumbFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"src/", `<span class="text-muted">src/</span>`},
|
||||
{"src/fmt/", `<a href="/src">src</a>/<span class="text-muted">fmt/</span>`},
|
||||
{"src/fmt/print.go", `<a href="/src">src</a>/<a href="/src/fmt">fmt</a>/<span class="text-muted">print.go</span>`},
|
||||
} {
|
||||
if got := srcBreadcrumbFunc(tc.path); got != tc.want {
|
||||
t.Errorf("srcBreadcrumbFunc(%v) = %v; want %v", tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcToPkgLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"src/", `<a href="/pkg">Index</a>`},
|
||||
{"src/fmt/", `<a href="/pkg/fmt">fmt</a>`},
|
||||
{"pkg/", `<a href="/pkg">Index</a>`},
|
||||
{"pkg/LICENSE", `<a href="/pkg">Index</a>`},
|
||||
} {
|
||||
if got := srcToPkgLinkFunc(tc.path); got != tc.want {
|
||||
t.Errorf("srcToPkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterOutBuildAnnotations(t *testing.T) {
|
||||
// TODO: simplify this by using a multiline string once we stop
|
||||
// using go vet from 1.10 on the build dashboard.
|
||||
// https://golang.org/issue/26627
|
||||
src := []byte("// +build !foo\n" +
|
||||
"// +build !anothertag\n" +
|
||||
"\n" +
|
||||
"// non-tag comment\n" +
|
||||
"\n" +
|
||||
"package foo\n" +
|
||||
"\n" +
|
||||
"func bar() int {\n" +
|
||||
" return 42\n" +
|
||||
"}\n")
|
||||
|
||||
fset := token.NewFileSet()
|
||||
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, cg := range af.Comments {
|
||||
if strings.HasPrefix(cg.Text(), "+build ") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("TestFilterOutBuildAnnotations is broken: missing build tag in test input")
|
||||
}
|
||||
|
||||
found = false
|
||||
for _, cg := range filterOutBuildAnnotations(af.Comments) {
|
||||
if strings.HasPrefix(cg.Text(), "+build ") {
|
||||
t.Errorf("filterOutBuildAnnotations failed to filter build tag")
|
||||
}
|
||||
|
||||
if strings.Contains(cg.Text(), "non-tag comment") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("filterOutBuildAnnotations should not remove non-build tag comment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkifyGenerics(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
type T struct {
|
||||
field *T
|
||||
}
|
||||
|
||||
type ParametricStruct[T any] struct {
|
||||
field *T
|
||||
}
|
||||
|
||||
func F1[T any](arg T) { }
|
||||
|
||||
func F2(arg T) { }
|
||||
|
||||
func (*ParametricStruct[T]) M(arg T) { }
|
||||
|
||||
func (*T) M(arg T) { }
|
||||
|
||||
type ParametricStruct2[T1, T2 any] struct {
|
||||
a T1
|
||||
b T2
|
||||
}
|
||||
|
||||
func (*ParametricStruct2[T1, T2]) M(a T1, b T2) { }
|
||||
|
||||
|
||||
`))
|
||||
|
||||
want := `type T struct {
|
||||
<span id="T.field"></span>field *<a href="#T">T</a>
|
||||
}
|
||||
type ParametricStruct[T <a href="/pkg/builtin/#any">any</a>] struct {
|
||||
<span id="ParametricStruct.field"></span>field *T
|
||||
}
|
||||
func F1[T <a href="/pkg/builtin/#any">any</a>](arg T) {}
|
||||
func F2(arg <a href="#T">T</a>) {}
|
||||
func (*<a href="#ParametricStruct">ParametricStruct</a>[T]) M(arg T) {}
|
||||
func (*<a href="#T">T</a>) M(arg <a href="#T">T</a>) {}
|
||||
type ParametricStruct2[T1, T2 <a href="/pkg/builtin/#any">any</a>] struct {
|
||||
<span id="ParametricStruct2.a"></span>a T1
|
||||
<span id="ParametricStruct2.b"></span>b T2
|
||||
}
|
||||
func (*<a href="#ParametricStruct2">ParametricStruct2</a>[T1, T2]) M(a T1, b T2) {}`
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
func newCorpus(t *testing.T) *Corpus {
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"src/foo/foo.go": `// Package foo is an example.
|
||||
package foo
|
||||
|
||||
import "bar"
|
||||
|
||||
const Pi = 3.1415
|
||||
|
||||
var Foos []Foo
|
||||
|
||||
// Foo is stuff.
|
||||
type Foo struct{}
|
||||
|
||||
func New() *Foo {
|
||||
return new(Foo)
|
||||
}
|
||||
`,
|
||||
"src/bar/bar.go": `// Package bar is another example to test races.
|
||||
package bar
|
||||
`,
|
||||
"src/other/bar/bar.go": `// Package bar is another bar package.
|
||||
package bar
|
||||
func X() {}
|
||||
`,
|
||||
"src/skip/skip.go": `// Package skip should be skipped.
|
||||
package skip
|
||||
func Skip() {}
|
||||
`,
|
||||
"src/bar/readme.txt": `Whitelisted text file.
|
||||
`,
|
||||
"src/bar/baz.zzz": `Text file not whitelisted.
|
||||
`,
|
||||
}))
|
||||
c.IndexEnabled = true
|
||||
c.IndexDirectory = func(dir string) bool {
|
||||
return !strings.Contains(dir, "skip")
|
||||
}
|
||||
|
||||
if err := c.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
for _, docs := range []bool{true, false} {
|
||||
for _, goCode := range []bool{true, false} {
|
||||
for _, fullText := range []bool{true, false} {
|
||||
c := newCorpus(t)
|
||||
c.IndexDocs = docs
|
||||
c.IndexGoCode = goCode
|
||||
c.IndexFullText = fullText
|
||||
c.UpdateIndex()
|
||||
ix, _ := c.CurrentIndex()
|
||||
if ix == nil {
|
||||
t.Fatal("no index")
|
||||
}
|
||||
t.Logf("docs, goCode, fullText = %v,%v,%v", docs, goCode, fullText)
|
||||
testIndex(t, c, ix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexWriteRead(t *testing.T) {
|
||||
type key struct {
|
||||
docs, goCode, fullText bool
|
||||
}
|
||||
type val struct {
|
||||
buf *bytes.Buffer
|
||||
c *Corpus
|
||||
}
|
||||
m := map[key]val{}
|
||||
|
||||
for _, docs := range []bool{true, false} {
|
||||
for _, goCode := range []bool{true, false} {
|
||||
for _, fullText := range []bool{true, false} {
|
||||
k := key{docs, goCode, fullText}
|
||||
c := newCorpus(t)
|
||||
c.IndexDocs = docs
|
||||
c.IndexGoCode = goCode
|
||||
c.IndexFullText = fullText
|
||||
c.UpdateIndex()
|
||||
ix, _ := c.CurrentIndex()
|
||||
if ix == nil {
|
||||
t.Fatal("no index")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
nw, err := ix.WriteTo(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Index.WriteTo: %v", err)
|
||||
}
|
||||
m[k] = val{bytes.NewBuffer(buf.Bytes()), c}
|
||||
ix2 := new(Index)
|
||||
nr, err := ix2.ReadFrom(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Index.ReadFrom: %v", err)
|
||||
}
|
||||
if nr != nw {
|
||||
t.Errorf("Wrote %d bytes to index but read %d", nw, nr)
|
||||
}
|
||||
testIndex(t, c, ix)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Test CompatibleWith
|
||||
for k1, v1 := range m {
|
||||
ix := new(Index)
|
||||
if _, err := ix.ReadFrom(v1.buf); err != nil {
|
||||
t.Fatalf("Index.ReadFrom: %v", err)
|
||||
}
|
||||
for k2, v2 := range m {
|
||||
if got, want := ix.CompatibleWith(v2.c), k1 == k2; got != want {
|
||||
t.Errorf("CompatibleWith = %v; want %v for %v, %v", got, want, k1, k2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testIndex(t *testing.T, c *Corpus, ix *Index) {
|
||||
if _, ok := ix.words["Skip"]; ok {
|
||||
t.Errorf("the word Skip was found; expected it to be skipped")
|
||||
}
|
||||
checkStats(t, c, ix)
|
||||
checkImportCount(t, c, ix)
|
||||
checkPackagePath(t, c, ix)
|
||||
checkExports(t, c, ix)
|
||||
checkIdents(t, c, ix)
|
||||
}
|
||||
|
||||
// checkStats checks the Index's statistics.
|
||||
// Some statistics are only set when we're indexing Go code.
|
||||
func checkStats(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := Statistics{}
|
||||
if c.IndexFullText {
|
||||
want.Bytes = 314
|
||||
want.Files = 4
|
||||
want.Lines = 21
|
||||
} else if c.IndexDocs || c.IndexGoCode {
|
||||
want.Bytes = 291
|
||||
want.Files = 3
|
||||
want.Lines = 20
|
||||
}
|
||||
if c.IndexGoCode {
|
||||
want.Words = 8
|
||||
want.Spots = 12
|
||||
}
|
||||
if got := ix.Stats(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Stats = %#v; want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkImportCount checks the Index's import count map.
|
||||
// It is only set when we're indexing Go code.
|
||||
func checkImportCount(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[string]int{}
|
||||
if c.IndexGoCode {
|
||||
want = map[string]int{
|
||||
"bar": 1,
|
||||
}
|
||||
}
|
||||
if got := ix.ImportCount(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ImportCount = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPackagePath checks the Index's package path map.
|
||||
// It is set if at least one of the indexing options is enabled.
|
||||
func checkPackagePath(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[string]map[string]bool{}
|
||||
if c.IndexDocs || c.IndexGoCode || c.IndexFullText {
|
||||
want = map[string]map[string]bool{
|
||||
"foo": {
|
||||
"foo": true,
|
||||
},
|
||||
"bar": {
|
||||
"bar": true,
|
||||
"other/bar": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
if got := ix.PackagePath(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("PackagePath = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkExports checks the Index's exports map.
|
||||
// It is only set when we're indexing Go code.
|
||||
func checkExports(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[string]map[string]SpotKind{}
|
||||
if c.IndexGoCode {
|
||||
want = map[string]map[string]SpotKind{
|
||||
"foo": {
|
||||
"Pi": ConstDecl,
|
||||
"Foos": VarDecl,
|
||||
"Foo": TypeDecl,
|
||||
"New": FuncDecl,
|
||||
},
|
||||
"other/bar": {
|
||||
"X": FuncDecl,
|
||||
},
|
||||
}
|
||||
}
|
||||
if got := ix.Exports(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Exports = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkIdents checks the Index's indents map.
|
||||
// It is only set when we're indexing documentation.
|
||||
func checkIdents(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[SpotKind]map[string][]Ident{}
|
||||
if c.IndexDocs {
|
||||
want = map[SpotKind]map[string][]Ident{
|
||||
PackageClause: {
|
||||
"bar": {
|
||||
{"bar", "bar", "bar", "Package bar is another example to test races."},
|
||||
{"other/bar", "bar", "bar", "Package bar is another bar package."},
|
||||
},
|
||||
"foo": {{"foo", "foo", "foo", "Package foo is an example."}},
|
||||
"other": {{"other/bar", "bar", "bar", "Package bar is another bar package."}},
|
||||
},
|
||||
ConstDecl: {
|
||||
"Pi": {{"foo", "foo", "Pi", ""}},
|
||||
},
|
||||
VarDecl: {
|
||||
"Foos": {{"foo", "foo", "Foos", ""}},
|
||||
},
|
||||
TypeDecl: {
|
||||
"Foo": {{"foo", "foo", "Foo", "Foo is stuff."}},
|
||||
},
|
||||
FuncDecl: {
|
||||
"New": {{"foo", "foo", "New", ""}},
|
||||
"X": {{"other/bar", "bar", "X", ""}},
|
||||
},
|
||||
}
|
||||
}
|
||||
if got := ix.Idents(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Idents = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentResultSort(t *testing.T) {
|
||||
ic := map[string]int{
|
||||
"/a/b/pkg1": 10,
|
||||
"/a/b/pkg2": 2,
|
||||
"/b/d/pkg3": 20,
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
ir []Ident
|
||||
exp []Ident
|
||||
}{
|
||||
{
|
||||
ir: []Ident{
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
exp: []Ident{
|
||||
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
ir: []Ident{
|
||||
{"/a/a/pkg1", "pkg1", "MyFunc1", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
exp: []Ident{
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
{"/a/a/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
},
|
||||
} {
|
||||
if sort.Sort(byImportCount{tc.ir, ic}); !reflect.DeepEqual(tc.ir, tc.exp) {
|
||||
t.Errorf("got: %v, want %v", tc.ir, tc.exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentFilter(t *testing.T) {
|
||||
ic := map[string]int{}
|
||||
for _, tc := range []struct {
|
||||
ir []Ident
|
||||
pak string
|
||||
exp []Ident
|
||||
}{
|
||||
{
|
||||
ir: []Ident{
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
pak: "pkg2",
|
||||
exp: []Ident{
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
},
|
||||
},
|
||||
} {
|
||||
res := byImportCount{tc.ir, ic}.filter(tc.pak)
|
||||
if !reflect.DeepEqual(res, tc.exp) {
|
||||
t.Errorf("got: %v, want %v", res, tc.exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file implements LinkifyText which introduces
|
||||
// links for identifiers pointing to their declarations.
|
||||
// The approach does not cover all cases because godoc
|
||||
// doesn't have complete type information, but it's
|
||||
// reasonably good for browsing.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/token"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// LinkifyText HTML-escapes source text and writes it to w.
|
||||
// Identifiers that are in a "use" position (i.e., that are
|
||||
// not being declared), are wrapped with HTML links pointing
|
||||
// to the respective declaration, if possible. Comments are
|
||||
// formatted the same way as with FormatText.
|
||||
func LinkifyText(w io.Writer, text []byte, n ast.Node) {
|
||||
links := linksFor(n)
|
||||
|
||||
i := 0 // links index
|
||||
prev := "" // prev HTML tag
|
||||
linkWriter := func(w io.Writer, _ int, start bool) {
|
||||
// end tag
|
||||
if !start {
|
||||
if prev != "" {
|
||||
fmt.Fprintf(w, `</%s>`, prev)
|
||||
prev = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// start tag
|
||||
prev = ""
|
||||
if i < len(links) {
|
||||
switch info := links[i]; {
|
||||
case info.path != "" && info.name == "":
|
||||
// package path
|
||||
fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path)
|
||||
prev = "a"
|
||||
case info.path != "" && info.name != "":
|
||||
// qualified identifier
|
||||
fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.name)
|
||||
prev = "a"
|
||||
case info.path == "" && info.name != "":
|
||||
// local identifier
|
||||
if info.isVal {
|
||||
fmt.Fprintf(w, `<span id="%s">`, info.name)
|
||||
prev = "span"
|
||||
} else if ast.IsExported(info.name) {
|
||||
fmt.Fprintf(w, `<a href="#%s">`, info.name)
|
||||
prev = "a"
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
idents := tokenSelection(text, token.IDENT)
|
||||
comments := tokenSelection(text, token.COMMENT)
|
||||
FormatSelections(w, text, linkWriter, idents, selectionTag, comments)
|
||||
}
|
||||
|
||||
// A link describes the (HTML) link information for an identifier.
|
||||
// The zero value of a link represents "no link".
|
||||
type link struct {
|
||||
path, name string // package path, identifier name
|
||||
isVal bool // identifier is defined in a const or var declaration
|
||||
}
|
||||
|
||||
// linksFor returns the list of links for the identifiers used
|
||||
// by node in the same order as they appear in the source.
|
||||
func linksFor(node ast.Node) (links []link) {
|
||||
// linkMap tracks link information for each ast.Ident node. Entries may
|
||||
// be created out of source order (for example, when we visit a parent
|
||||
// definition node). These links are appended to the returned slice when
|
||||
// their ast.Ident nodes are visited.
|
||||
linkMap := make(map[*ast.Ident]link)
|
||||
|
||||
typeParams := make(map[string]bool)
|
||||
|
||||
ast.Inspect(node, func(node ast.Node) bool {
|
||||
switch n := node.(type) {
|
||||
case *ast.Field:
|
||||
for _, n := range n.Names {
|
||||
linkMap[n] = link{}
|
||||
}
|
||||
case *ast.ImportSpec:
|
||||
if name := n.Name; name != nil {
|
||||
linkMap[name] = link{}
|
||||
}
|
||||
case *ast.ValueSpec:
|
||||
for _, n := range n.Names {
|
||||
linkMap[n] = link{name: n.Name, isVal: true}
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
linkMap[n.Name] = link{}
|
||||
if n.Recv != nil {
|
||||
recv := n.Recv.List[0].Type
|
||||
if r, isstar := recv.(*ast.StarExpr); isstar {
|
||||
recv = r.X
|
||||
}
|
||||
switch x := recv.(type) {
|
||||
case *ast.IndexExpr:
|
||||
if ident, _ := x.Index.(*ast.Ident); ident != nil {
|
||||
typeParams[ident.Name] = true
|
||||
}
|
||||
case *ast.IndexListExpr:
|
||||
for _, index := range x.Indices {
|
||||
if ident, _ := index.(*ast.Ident); ident != nil {
|
||||
typeParams[ident.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.TypeSpec:
|
||||
linkMap[n.Name] = link{}
|
||||
case *ast.AssignStmt:
|
||||
// Short variable declarations only show up if we apply
|
||||
// this code to all source code (as opposed to exported
|
||||
// declarations only).
|
||||
if n.Tok == token.DEFINE {
|
||||
// Some of the lhs variables may be re-declared,
|
||||
// so technically they are not defs. We don't
|
||||
// care for now.
|
||||
for _, x := range n.Lhs {
|
||||
// Each lhs expression should be an
|
||||
// ident, but we are conservative and check.
|
||||
if n, _ := x.(*ast.Ident); n != nil {
|
||||
linkMap[n] = link{isVal: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.SelectorExpr:
|
||||
// Detect qualified identifiers of the form pkg.ident.
|
||||
// If anything fails we return true and collect individual
|
||||
// identifiers instead.
|
||||
if x, _ := n.X.(*ast.Ident); x != nil {
|
||||
// Create links only if x is a qualified identifier.
|
||||
if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg {
|
||||
if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil {
|
||||
// spec.Path.Value is the import path
|
||||
if path, err := strconv.Unquote(spec.Path.Value); err == nil {
|
||||
// Register two links, one for the package
|
||||
// and one for the qualified identifier.
|
||||
linkMap[x] = link{path: path}
|
||||
linkMap[n.Sel] = link{path: path, name: n.Sel.Name}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.CompositeLit:
|
||||
// Detect field names within composite literals. These links should
|
||||
// be prefixed by the type name.
|
||||
fieldPath := ""
|
||||
prefix := ""
|
||||
switch typ := n.Type.(type) {
|
||||
case *ast.Ident:
|
||||
prefix = typ.Name + "."
|
||||
case *ast.SelectorExpr:
|
||||
if x, _ := typ.X.(*ast.Ident); x != nil {
|
||||
// Create links only if x is a qualified identifier.
|
||||
if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg {
|
||||
if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil {
|
||||
// spec.Path.Value is the import path
|
||||
if path, err := strconv.Unquote(spec.Path.Value); err == nil {
|
||||
// Register two links, one for the package
|
||||
// and one for the qualified identifier.
|
||||
linkMap[x] = link{path: path}
|
||||
linkMap[typ.Sel] = link{path: path, name: typ.Sel.Name}
|
||||
fieldPath = path
|
||||
prefix = typ.Sel.Name + "."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range n.Elts {
|
||||
if kv, ok := e.(*ast.KeyValueExpr); ok {
|
||||
if k, ok := kv.Key.(*ast.Ident); ok {
|
||||
// Note: there is some syntactic ambiguity here. We cannot determine
|
||||
// if this is a struct literal or a map literal without type
|
||||
// information. We assume struct literal.
|
||||
name := prefix + k.Name
|
||||
linkMap[k] = link{path: fieldPath, name: name}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.Ident:
|
||||
if l, ok := linkMap[n]; ok {
|
||||
links = append(links, l)
|
||||
} else {
|
||||
l := link{name: n.Name}
|
||||
if n.Obj == nil {
|
||||
if doc.IsPredeclared(n.Name) {
|
||||
l.path = builtinPkgPath
|
||||
} else {
|
||||
if typeParams[n.Name] {
|
||||
// If a type parameter was declared then do not generate a link.
|
||||
// Doing this is necessary because type parameter identifiers do not
|
||||
// have their Decl recorded sometimes, see
|
||||
// https://golang.org/issue/50956.
|
||||
l = link{}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if n.Obj.Kind == ast.Typ {
|
||||
if _, isfield := n.Obj.Decl.(*ast.Field); isfield {
|
||||
// If an identifier is a type declared in a field assume it is a type
|
||||
// parameter and do not generate a link.
|
||||
l = link{}
|
||||
}
|
||||
}
|
||||
}
|
||||
links = append(links, l)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
// renderMarkdown converts a limited and opinionated flavor of Markdown (compliant with
|
||||
// CommonMark 0.29) to HTML for the purposes of Go websites.
|
||||
//
|
||||
// The Markdown source may contain raw HTML,
|
||||
// but Go templates have already been processed.
|
||||
func renderMarkdown(src []byte) ([]byte, error) {
|
||||
// parser.WithHeadingAttribute allows custom ids on headings.
|
||||
// html.WithUnsafe allows use of raw HTML, which we need for tables.
|
||||
md := goldmark.New(
|
||||
goldmark.WithParserOptions(parser.WithHeadingAttribute()),
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()))
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert(src, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// 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 godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
var (
|
||||
doctype = []byte("<!DOCTYPE ")
|
||||
jsonStart = []byte("<!--{")
|
||||
jsonEnd = []byte("}-->")
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Documentation Metadata
|
||||
|
||||
type Metadata struct {
|
||||
// These fields can be set in the JSON header at the top of a doc.
|
||||
Title string
|
||||
Subtitle string
|
||||
Template bool // execute as template
|
||||
Path string // canonical path for this page
|
||||
AltPaths []string // redirect these other paths to this page
|
||||
|
||||
// These are internal to the implementation.
|
||||
filePath string // filesystem path relative to goroot
|
||||
}
|
||||
|
||||
func (m *Metadata) FilePath() string { return m.filePath }
|
||||
|
||||
// extractMetadata extracts the Metadata from a byte slice.
|
||||
// It returns the Metadata value and the remaining data.
|
||||
// If no metadata is present the original byte slice is returned.
|
||||
func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
|
||||
tail = b
|
||||
if !bytes.HasPrefix(b, jsonStart) {
|
||||
return
|
||||
}
|
||||
end := bytes.Index(b, jsonEnd)
|
||||
if end < 0 {
|
||||
return
|
||||
}
|
||||
b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
|
||||
if err = json.Unmarshal(b, &meta); err != nil {
|
||||
return
|
||||
}
|
||||
tail = tail[end+len(jsonEnd):]
|
||||
return
|
||||
}
|
||||
|
||||
// updateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata,
|
||||
// and updates the DocMetadata map.
|
||||
func (c *Corpus) updateMetadata() {
|
||||
metadata := make(map[string]*Metadata)
|
||||
var scan func(string) // scan is recursive
|
||||
scan = func(dir string) {
|
||||
fis, err := c.fs.ReadDir(dir)
|
||||
if err != nil {
|
||||
if dir == "/doc" && errors.Is(err, os.ErrNotExist) {
|
||||
// Be quiet during tests that don't have a /doc tree.
|
||||
return
|
||||
}
|
||||
log.Printf("updateMetadata %s: %v", dir, err)
|
||||
return
|
||||
}
|
||||
for _, fi := range fis {
|
||||
name := pathpkg.Join(dir, fi.Name())
|
||||
if fi.IsDir() {
|
||||
scan(name) // recurse
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".md") {
|
||||
continue
|
||||
}
|
||||
// Extract metadata from the file.
|
||||
b, err := vfs.ReadFile(c.fs, name)
|
||||
if err != nil {
|
||||
log.Printf("updateMetadata %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
meta, _, err := extractMetadata(b)
|
||||
if err != nil {
|
||||
log.Printf("updateMetadata: %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
// Present all .md as if they were .html,
|
||||
// so that it doesn't matter which one a page is written in.
|
||||
if strings.HasSuffix(name, ".md") {
|
||||
name = strings.TrimSuffix(name, ".md") + ".html"
|
||||
}
|
||||
// Store relative filesystem path in Metadata.
|
||||
meta.filePath = name
|
||||
if meta.Path == "" {
|
||||
// If no Path, canonical path is actual path with .html removed.
|
||||
meta.Path = strings.TrimSuffix(name, ".html")
|
||||
}
|
||||
// Store under both paths.
|
||||
metadata[meta.Path] = &meta
|
||||
metadata[meta.filePath] = &meta
|
||||
for _, path := range meta.AltPaths {
|
||||
metadata[path] = &meta
|
||||
}
|
||||
}
|
||||
}
|
||||
scan("/doc")
|
||||
c.docMetadata.Set(metadata)
|
||||
}
|
||||
|
||||
// MetadataFor returns the *Metadata for a given relative path or nil if none
|
||||
// exists.
|
||||
func (c *Corpus) MetadataFor(relpath string) *Metadata {
|
||||
if m, _ := c.docMetadata.Get(); m != nil {
|
||||
meta := m.(map[string]*Metadata)
|
||||
// If metadata for this relpath exists, return it.
|
||||
if p := meta[relpath]; p != nil {
|
||||
return p
|
||||
}
|
||||
// Try with or without trailing slash.
|
||||
if strings.HasSuffix(relpath, "/") {
|
||||
relpath = relpath[:len(relpath)-1]
|
||||
} else {
|
||||
relpath = relpath + "/"
|
||||
}
|
||||
return meta[relpath]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshMetadata sends a signal to update DocMetadata. If a refresh is in
|
||||
// progress the metadata will be refreshed again afterward.
|
||||
func (c *Corpus) refreshMetadata() {
|
||||
select {
|
||||
case c.refreshMetadataSignal <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// refreshMetadataLoop runs forever, updating DocMetadata when the underlying
|
||||
// file system changes. It should be launched in a goroutine.
|
||||
func (c *Corpus) refreshMetadataLoop() {
|
||||
for {
|
||||
<-c.refreshMetadataSignal
|
||||
c.updateMetadata()
|
||||
time.Sleep(10 * time.Second) // at most once every 10 seconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 godoc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Page describes the contents of the top-level godoc webpage.
|
||||
type Page struct {
|
||||
Title string
|
||||
Tabtitle string
|
||||
Subtitle string
|
||||
SrcPath string
|
||||
Query string
|
||||
Body []byte
|
||||
TreeView bool // page needs to contain treeview related js and css
|
||||
|
||||
// filled in by ServePage
|
||||
SearchBox bool
|
||||
Playground bool
|
||||
Version string
|
||||
GoogleAnalytics string
|
||||
}
|
||||
|
||||
func (p *Presentation) ServePage(w http.ResponseWriter, page Page) {
|
||||
if page.Tabtitle == "" {
|
||||
page.Tabtitle = page.Title
|
||||
}
|
||||
page.SearchBox = p.Corpus.IndexEnabled
|
||||
page.Playground = p.ShowPlayground
|
||||
page.Version = runtime.Version()
|
||||
page.GoogleAnalytics = p.GoogleAnalytics
|
||||
applyTemplateToResponseWriter(w, p.GodocHTML, page)
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, relpath string, err error) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
if perr, ok := err.(*os.PathError); ok {
|
||||
rel, err := filepath.Rel(runtime.GOROOT(), perr.Path)
|
||||
if err != nil {
|
||||
perr.Path = "REDACTED"
|
||||
} else {
|
||||
perr.Path = filepath.Join("$GOROOT", rel)
|
||||
}
|
||||
}
|
||||
p.ServePage(w, Page{
|
||||
Title: "File " + relpath,
|
||||
Subtitle: relpath,
|
||||
Body: applyTemplate(p.ErrorHTML, "errorHTML", err),
|
||||
GoogleAnalytics: p.GoogleAnalytics,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
// This file contains support functions for parsing .go files
|
||||
// accessed via godoc's file system fs.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
pathpkg "path"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
var linePrefix = []byte("//line ")
|
||||
|
||||
// This function replaces source lines starting with "//line " with a blank line.
|
||||
// It does this irrespective of whether the line is truly a line comment or not;
|
||||
// e.g., the line may be inside a string, or a /*-style comment; however that is
|
||||
// rather unlikely (proper testing would require a full Go scan which we want to
|
||||
// avoid for performance).
|
||||
func replaceLinePrefixCommentsWithBlankLine(src []byte) {
|
||||
for {
|
||||
i := bytes.Index(src, linePrefix)
|
||||
if i < 0 {
|
||||
break // we're done
|
||||
}
|
||||
// 0 <= i && i+len(linePrefix) <= len(src)
|
||||
if i == 0 || src[i-1] == '\n' {
|
||||
// at beginning of line: blank out line
|
||||
for i < len(src) && src[i] != '\n' {
|
||||
src[i] = ' '
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
// not at beginning of line: skip over prefix
|
||||
i += len(linePrefix)
|
||||
}
|
||||
// i <= len(src)
|
||||
src = src[i:]
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Corpus) parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
|
||||
src, err := vfs.ReadFile(c.fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Temporary ad-hoc fix for issue 5247.
|
||||
// TODO(gri,dmitshur) Remove this in favor of a better fix, eventually (see issue 32092).
|
||||
replaceLinePrefixCommentsWithBlankLine(src)
|
||||
|
||||
return parser.ParseFile(fset, filename, src, mode)
|
||||
}
|
||||
|
||||
func (c *Corpus) parseFiles(fset *token.FileSet, relpath string, abspath string, localnames []string) (map[string]*ast.File, error) {
|
||||
files := make(map[string]*ast.File)
|
||||
for _, f := range localnames {
|
||||
absname := pathpkg.Join(abspath, f)
|
||||
file, err := c.parseFile(fset, absname, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files[pathpkg.Join(relpath, f)] = file
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs/httpfs"
|
||||
)
|
||||
|
||||
// SearchResultFunc functions return an HTML body for displaying search results.
|
||||
type SearchResultFunc func(p *Presentation, result SearchResult) []byte
|
||||
|
||||
// Presentation generates output from a corpus.
|
||||
type Presentation struct {
|
||||
Corpus *Corpus
|
||||
|
||||
mux *http.ServeMux
|
||||
fileServer http.Handler
|
||||
cmdHandler handlerServer
|
||||
pkgHandler handlerServer
|
||||
|
||||
CallGraphHTML,
|
||||
DirlistHTML,
|
||||
ErrorHTML,
|
||||
ExampleHTML,
|
||||
GodocHTML,
|
||||
ImplementsHTML,
|
||||
MethodSetHTML,
|
||||
PackageHTML,
|
||||
PackageRootHTML,
|
||||
SearchHTML,
|
||||
SearchDocHTML,
|
||||
SearchCodeHTML,
|
||||
SearchTxtHTML,
|
||||
SearchDescXML *template.Template // If not nil, register a /opensearch.xml handler with this template.
|
||||
|
||||
// TabWidth optionally specifies the tab width.
|
||||
TabWidth int
|
||||
|
||||
ShowTimestamps bool
|
||||
ShowPlayground bool
|
||||
DeclLinks bool
|
||||
|
||||
// NotesRx optionally specifies a regexp to match
|
||||
// notes to render in the output.
|
||||
NotesRx *regexp.Regexp
|
||||
|
||||
// AdjustPageInfoMode optionally specifies a function to
|
||||
// modify the PageInfoMode of a request. The default chosen
|
||||
// value is provided.
|
||||
AdjustPageInfoMode func(req *http.Request, mode PageInfoMode) PageInfoMode
|
||||
|
||||
// URLForSrc optionally specifies a function that takes a source file and
|
||||
// returns a URL for it.
|
||||
// The source file argument has the form /src/<path>/<filename>.
|
||||
URLForSrc func(src string) string
|
||||
|
||||
// URLForSrcPos optionally specifies a function to create a URL given a
|
||||
// source file, a line from the source file (1-based), and low & high offset
|
||||
// positions (0-based, bytes from beginning of file). Ideally, the returned
|
||||
// URL will be for the specified line of the file, while the high & low
|
||||
// positions will be used to highlight a section of the file.
|
||||
// The source file argument has the form /src/<path>/<filename>.
|
||||
URLForSrcPos func(src string, line, low, high int) string
|
||||
|
||||
// URLForSrcQuery optionally specifies a function to create a URL given a
|
||||
// source file, a query string, and a line from the source file (1-based).
|
||||
// The source file argument has the form /src/<path>/<filename>.
|
||||
// The query argument will be escaped for the purposes of embedding in a URL
|
||||
// query parameter.
|
||||
// Ideally, the returned URL will be for the specified line of the file with
|
||||
// the query string highlighted.
|
||||
URLForSrcQuery func(src, query string, line int) string
|
||||
|
||||
// SearchResults optionally specifies a list of functions returning an HTML
|
||||
// body for displaying search results.
|
||||
SearchResults []SearchResultFunc
|
||||
|
||||
// GoogleAnalytics optionally adds Google Analytics via the provided
|
||||
// tracking ID to each page.
|
||||
GoogleAnalytics string
|
||||
|
||||
initFuncMapOnce sync.Once
|
||||
funcMap template.FuncMap
|
||||
templateFuncs template.FuncMap
|
||||
}
|
||||
|
||||
// NewPresentation returns a new Presentation from a corpus.
|
||||
// It sets SearchResults to:
|
||||
// [SearchResultDoc SearchResultCode SearchResultTxt].
|
||||
func NewPresentation(c *Corpus) *Presentation {
|
||||
if c == nil {
|
||||
panic("nil Corpus")
|
||||
}
|
||||
p := &Presentation{
|
||||
Corpus: c,
|
||||
mux: http.NewServeMux(),
|
||||
fileServer: http.FileServer(httpfs.New(c.fs)),
|
||||
|
||||
TabWidth: 4,
|
||||
DeclLinks: true,
|
||||
SearchResults: []SearchResultFunc{
|
||||
(*Presentation).SearchResultDoc,
|
||||
(*Presentation).SearchResultCode,
|
||||
(*Presentation).SearchResultTxt,
|
||||
},
|
||||
}
|
||||
p.cmdHandler = handlerServer{
|
||||
p: p,
|
||||
c: c,
|
||||
pattern: "/cmd/",
|
||||
fsRoot: "/src",
|
||||
}
|
||||
p.pkgHandler = handlerServer{
|
||||
p: p,
|
||||
c: c,
|
||||
pattern: "/pkg/",
|
||||
stripPrefix: "pkg/",
|
||||
fsRoot: "/src",
|
||||
exclude: []string{"/src/cmd"},
|
||||
}
|
||||
p.cmdHandler.registerWithMux(p.mux)
|
||||
p.pkgHandler.registerWithMux(p.mux)
|
||||
p.mux.HandleFunc("/", p.ServeFile)
|
||||
p.mux.HandleFunc("/search", p.HandleSearch)
|
||||
if p.SearchDescXML != nil {
|
||||
p.mux.HandleFunc("/opensearch.xml", p.serveSearchDesc)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Presentation) FileServer() http.Handler {
|
||||
return p.fileServer
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) PkgFSRoot() string {
|
||||
return p.pkgHandler.fsRoot
|
||||
}
|
||||
|
||||
func (p *Presentation) CmdFSRoot() string {
|
||||
return p.cmdHandler.fsRoot
|
||||
}
|
||||
|
||||
// TODO(bradfitz): move this to be a method on Corpus. Just moving code around for now,
|
||||
// but this doesn't feel right.
|
||||
func (p *Presentation) GetPkgPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
|
||||
return p.pkgHandler.GetPageInfo(abspath, relpath, mode, "", "")
|
||||
}
|
||||
|
||||
// TODO(bradfitz): move this to be a method on Corpus. Just moving code around for now,
|
||||
// but this doesn't feel right.
|
||||
func (p *Presentation) GetCmdPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
|
||||
return p.cmdHandler.GetPageInfo(abspath, relpath, mode, "", "")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package redirect provides hooks to register HTTP handlers that redirect old
|
||||
// godoc paths to their new equivalents.
|
||||
package redirect // import "golang.org/x/tools/godoc/redirect"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Register registers HTTP handlers that redirect old godoc paths to their new equivalents.
|
||||
// If mux is nil it uses http.DefaultServeMux.
|
||||
func Register(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
// NB: /src/pkg (sans trailing slash) is the index of packages.
|
||||
mux.HandleFunc("/src/pkg/", srcPkgHandler)
|
||||
}
|
||||
|
||||
func Handler(target string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
url := target
|
||||
if qs := r.URL.RawQuery; qs != "" {
|
||||
url += "?" + qs
|
||||
}
|
||||
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
||||
})
|
||||
}
|
||||
|
||||
var validID = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
|
||||
|
||||
func PrefixHandler(prefix, baseURL string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.URL.Path; p == prefix {
|
||||
// redirect /prefix/ to /prefix
|
||||
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
|
||||
return
|
||||
}
|
||||
id := r.URL.Path[len(prefix):]
|
||||
if !validID.MatchString(id) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
target := baseURL + id
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
// Redirect requests from the old "/src/pkg/foo" to the new "/src/foo".
|
||||
// See http://golang.org/s/go14nopkg
|
||||
func srcPkgHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):]
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2015 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 redirect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type redirectResult struct {
|
||||
status int
|
||||
path string
|
||||
}
|
||||
|
||||
func errorResult(status int) redirectResult {
|
||||
return redirectResult{status, ""}
|
||||
}
|
||||
|
||||
func TestRedirects(t *testing.T) {
|
||||
var tests = map[string]redirectResult{
|
||||
"/foo": errorResult(404),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
Register(mux)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
for path, want := range tests {
|
||||
if want.path != "" && want.path[0] == '/' {
|
||||
// All redirects are absolute.
|
||||
want.path = ts.URL + want.path
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL+path, nil)
|
||||
if err != nil {
|
||||
t.Errorf("(path: %q) unexpected error: %v", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := http.DefaultTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("(path: %q) unexpected error: %v", path, err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close() // We only care about the headers, so close the body immediately.
|
||||
|
||||
if resp.StatusCode != want.status {
|
||||
t.Errorf("(path: %q) got status %d, want %d", path, resp.StatusCode, want.status)
|
||||
}
|
||||
|
||||
if want.status != 301 && want.status != 302 {
|
||||
// Not a redirect. Just check status.
|
||||
continue
|
||||
}
|
||||
|
||||
out, _ := resp.Location()
|
||||
if got := out.String(); got != want.path {
|
||||
t.Errorf("(path: %q) got %s, want %s", path, got, want.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// 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 godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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
|
||||
Idents map[SpotKind][]Ident
|
||||
}
|
||||
|
||||
func (c *Corpus) Lookup(query string) SearchResult {
|
||||
result := &SearchResult{Query: query}
|
||||
|
||||
index, timestamp := c.CurrentIndex()
|
||||
if index != nil {
|
||||
// identifier search
|
||||
if r, err := index.Lookup(query); err == nil {
|
||||
result = r
|
||||
} else if err != nil && !c.IndexFullText {
|
||||
// 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 *result
|
||||
}
|
||||
|
||||
// full text search
|
||||
if c.IndexFullText && query != "" {
|
||||
rx, err := regexp.Compile(query)
|
||||
if err != nil {
|
||||
result.Alert = "Error in query regular expression: " + err.Error()
|
||||
return *result
|
||||
}
|
||||
// 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, c.MaxResults+1)
|
||||
result.Complete = result.Found <= c.MaxResults
|
||||
if !result.Complete {
|
||||
result.Found-- // since we looked for maxResults+1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is the result accurate?
|
||||
if c.IndexEnabled {
|
||||
if ts := c.FSModifiedTime(); 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 *result
|
||||
}
|
||||
|
||||
// SearchResultDoc optionally specifies a function returning an HTML body
|
||||
// displaying search results matching godoc documentation.
|
||||
func (p *Presentation) SearchResultDoc(result SearchResult) []byte {
|
||||
return applyTemplate(p.SearchDocHTML, "searchDocHTML", result)
|
||||
}
|
||||
|
||||
// SearchResultCode optionally specifies a function returning an HTML body
|
||||
// displaying search results matching source code.
|
||||
func (p *Presentation) SearchResultCode(result SearchResult) []byte {
|
||||
return applyTemplate(p.SearchCodeHTML, "searchCodeHTML", result)
|
||||
}
|
||||
|
||||
// SearchResultTxt optionally specifies a function returning an HTML body
|
||||
// displaying search results of textual matches.
|
||||
func (p *Presentation) SearchResultTxt(result SearchResult) []byte {
|
||||
return applyTemplate(p.SearchTxtHTML, "searchTxtHTML", result)
|
||||
}
|
||||
|
||||
// HandleSearch obtains results for the requested search and returns a page
|
||||
// to display them.
|
||||
func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
query := strings.TrimSpace(r.FormValue("q"))
|
||||
result := p.Corpus.Lookup(query)
|
||||
|
||||
var contents bytes.Buffer
|
||||
for _, f := range p.SearchResults {
|
||||
contents.Write(f(p, result))
|
||||
}
|
||||
|
||||
var title string
|
||||
if haveResults := contents.Len() > 0; haveResults {
|
||||
title = fmt.Sprintf(`Results for query: %v`, query)
|
||||
if !p.Corpus.IndexEnabled {
|
||||
result.Alert = ""
|
||||
}
|
||||
} else {
|
||||
title = fmt.Sprintf(`No results found for query %q`, query)
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(applyTemplate(p.SearchHTML, "searchHTML", result))
|
||||
body.Write(contents.Bytes())
|
||||
|
||||
p.ServePage(w, Page{
|
||||
Title: title,
|
||||
Tabtitle: query,
|
||||
Query: query,
|
||||
Body: body.Bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Presentation) 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),
|
||||
}
|
||||
applyTemplateToResponseWriter(w, p.SearchDescXML, &data)
|
||||
}
|
||||
|
||||
// tocColCount returns the no. of columns
|
||||
// to split the toc table to.
|
||||
func tocColCount(result SearchResult) int {
|
||||
tocLen := tocLen(result)
|
||||
colCount := 0
|
||||
// Simple heuristic based on visual aesthetic in manual testing.
|
||||
switch {
|
||||
case tocLen <= 10:
|
||||
colCount = 1
|
||||
case tocLen <= 20:
|
||||
colCount = 2
|
||||
case tocLen <= 80:
|
||||
colCount = 3
|
||||
default:
|
||||
colCount = 4
|
||||
}
|
||||
return colCount
|
||||
}
|
||||
|
||||
// tocLen calculates the no. of items in the toc table
|
||||
// by going through various fields in the SearchResult
|
||||
// that is rendered in the UI.
|
||||
func tocLen(result SearchResult) int {
|
||||
tocLen := 0
|
||||
for _, val := range result.Idents {
|
||||
if len(val) != 0 {
|
||||
tocLen++
|
||||
}
|
||||
}
|
||||
// If no identifiers, then just one item for the header text "Package <result.Query>".
|
||||
// See searchcode.html for further details.
|
||||
if len(result.Idents) == 0 {
|
||||
tocLen++
|
||||
}
|
||||
if result.Hit != nil {
|
||||
if len(result.Hit.Decls) > 0 {
|
||||
tocLen += len(result.Hit.Decls)
|
||||
// We need one extra item for the header text "Package-level declarations".
|
||||
tocLen++
|
||||
}
|
||||
if len(result.Hit.Others) > 0 {
|
||||
tocLen += len(result.Hit.Others)
|
||||
// We need one extra item for the header text "Local declarations and uses".
|
||||
tocLen++
|
||||
}
|
||||
}
|
||||
// For "textual occurrences".
|
||||
tocLen++
|
||||
return tocLen
|
||||
}
|
||||
@@ -0,0 +1,855 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/doc"
|
||||
"go/token"
|
||||
htmlpkg "html"
|
||||
htmltemplate "html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/godoc/analysis"
|
||||
"golang.org/x/tools/godoc/util"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// handlerServer is a migration from an old godoc http Handler type.
|
||||
// This should probably merge into something else.
|
||||
type handlerServer struct {
|
||||
p *Presentation
|
||||
c *Corpus // copy of p.Corpus
|
||||
pattern string // url pattern; e.g. "/pkg/"
|
||||
stripPrefix string // prefix to strip from import path; e.g. "pkg/"
|
||||
fsRoot string // file system root to which the pattern is mapped; e.g. "/src"
|
||||
exclude []string // file system paths to exclude; e.g. "/src/cmd"
|
||||
}
|
||||
|
||||
func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
|
||||
mux.Handle(s.pattern, s)
|
||||
}
|
||||
|
||||
// GetPageInfo returns the PageInfo for a package directory abspath. If the
|
||||
// parameter genAST is set, an AST containing only the package exports is
|
||||
// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
|
||||
// is extracted from the AST. If there is no corresponding package in the
|
||||
// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
|
||||
// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
|
||||
// set to the respective error but the error is not logged.
|
||||
func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
|
||||
info := &PageInfo{Dirname: abspath, Mode: mode}
|
||||
|
||||
// Restrict to the package files that would be used when building
|
||||
// the package on this system. This makes sure that if there are
|
||||
// separate implementations for, say, Windows vs Unix, we don't
|
||||
// jumble them all together.
|
||||
// Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
|
||||
// are used.
|
||||
ctxt := build.Default
|
||||
ctxt.IsAbsPath = pathpkg.IsAbs
|
||||
ctxt.IsDir = func(path string) bool {
|
||||
fi, err := h.c.fs.Stat(filepath.ToSlash(path))
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
|
||||
f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
|
||||
filtered := make([]os.FileInfo, 0, len(f))
|
||||
for _, i := range f {
|
||||
if mode&NoFiltering != 0 || i.Name() != "internal" {
|
||||
filtered = append(filtered, i)
|
||||
}
|
||||
}
|
||||
return filtered, err
|
||||
}
|
||||
ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
|
||||
data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
|
||||
// Make the syscall/js package always visible by default.
|
||||
// It defaults to the host's GOOS/GOARCH, and golang.org's
|
||||
// linux/amd64 means the wasm syscall/js package was blank.
|
||||
// And you can't run godoc on js/wasm anyway, so host defaults
|
||||
// don't make sense here.
|
||||
if goos == "" && goarch == "" && relpath == "syscall/js" {
|
||||
goos, goarch = "js", "wasm"
|
||||
}
|
||||
if goos != "" {
|
||||
ctxt.GOOS = goos
|
||||
}
|
||||
if goarch != "" {
|
||||
ctxt.GOARCH = goarch
|
||||
}
|
||||
|
||||
pkginfo, err := ctxt.ImportDir(abspath, 0)
|
||||
// continue if there are no Go source files; we still want the directory info
|
||||
if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
|
||||
info.Err = err
|
||||
return info
|
||||
}
|
||||
|
||||
// collect package files
|
||||
pkgname := pkginfo.Name
|
||||
pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
|
||||
if len(pkgfiles) == 0 {
|
||||
// Commands written in C have no .go files in the build.
|
||||
// Instead, documentation may be found in an ignored file.
|
||||
// The file may be ignored via an explicit +build ignore
|
||||
// constraint (recommended), or by defining the package
|
||||
// documentation (historic).
|
||||
pkgname = "main" // assume package main since pkginfo.Name == ""
|
||||
pkgfiles = pkginfo.IgnoredGoFiles
|
||||
}
|
||||
|
||||
// get package information, if any
|
||||
if len(pkgfiles) > 0 {
|
||||
// build package AST
|
||||
fset := token.NewFileSet()
|
||||
files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
|
||||
if err != nil {
|
||||
info.Err = err
|
||||
return info
|
||||
}
|
||||
|
||||
// ignore any errors - they are due to unresolved identifiers
|
||||
pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
|
||||
|
||||
// extract package documentation
|
||||
info.FSet = fset
|
||||
if mode&ShowSource == 0 {
|
||||
// show extracted documentation
|
||||
var m doc.Mode
|
||||
if mode&NoFiltering != 0 {
|
||||
m |= doc.AllDecls
|
||||
}
|
||||
if mode&AllMethods != 0 {
|
||||
m |= doc.AllMethods
|
||||
}
|
||||
info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
|
||||
if mode&NoTypeAssoc != 0 {
|
||||
for _, t := range info.PDoc.Types {
|
||||
info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
|
||||
info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
|
||||
info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
|
||||
t.Consts = nil
|
||||
t.Vars = nil
|
||||
t.Funcs = nil
|
||||
}
|
||||
// for now we cannot easily sort consts and vars since
|
||||
// go/doc.Value doesn't export the order information
|
||||
sort.Sort(funcsByName(info.PDoc.Funcs))
|
||||
}
|
||||
|
||||
// collect examples
|
||||
testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
|
||||
files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
|
||||
if err != nil {
|
||||
log.Println("parsing examples:", err)
|
||||
}
|
||||
info.Examples = collectExamples(h.c, pkg, files)
|
||||
|
||||
// collect any notes that we want to show
|
||||
if info.PDoc.Notes != nil {
|
||||
// could regexp.Compile only once per godoc, but probably not worth it
|
||||
if rx := h.p.NotesRx; rx != nil {
|
||||
for m, n := range info.PDoc.Notes {
|
||||
if rx.MatchString(m) {
|
||||
if info.Notes == nil {
|
||||
info.Notes = make(map[string][]*doc.Note)
|
||||
}
|
||||
info.Notes[m] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// show source code
|
||||
// TODO(gri) Consider eliminating export filtering in this mode,
|
||||
// or perhaps eliminating the mode altogether.
|
||||
if mode&NoFiltering == 0 {
|
||||
packageExports(fset, pkg)
|
||||
}
|
||||
info.PAst = files
|
||||
}
|
||||
info.IsMain = pkgname == "main"
|
||||
}
|
||||
|
||||
// get directory information, if any
|
||||
var dir *Directory
|
||||
var timestamp time.Time
|
||||
if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
|
||||
// directory tree is present; lookup respective directory
|
||||
// (may still fail if the file system was updated and the
|
||||
// new directory tree has not yet been computed)
|
||||
dir = tree.(*Directory).lookup(abspath)
|
||||
timestamp = ts
|
||||
}
|
||||
if dir == nil {
|
||||
// TODO(agnivade): handle this case better, now since there is no CLI mode.
|
||||
// no directory tree present (happens in command-line mode);
|
||||
// compute 2 levels for this page. The second level is to
|
||||
// get the synopses of sub-directories.
|
||||
// note: cannot use path filter here because in general
|
||||
// it doesn't contain the FSTree path
|
||||
dir = h.c.newDirectory(abspath, 2)
|
||||
timestamp = time.Now()
|
||||
}
|
||||
info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
|
||||
|
||||
info.DirTime = timestamp
|
||||
info.DirFlat = mode&FlatDir != 0
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
|
||||
// if the path is under one of the exclusion paths, don't list.
|
||||
for _, e := range h.exclude {
|
||||
if strings.HasPrefix(path, e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
|
||||
if mode&NoFiltering != 0 {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
|
||||
for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
|
||||
if c == "internal" || c == "vendor" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type funcsByName []*doc.Func
|
||||
|
||||
func (s funcsByName) Len() int { return len(s) }
|
||||
func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||
|
||||
func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
|
||||
|
||||
if !h.corpusInitialized() {
|
||||
h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
|
||||
return
|
||||
}
|
||||
|
||||
abspath := pathpkg.Join(h.fsRoot, relpath)
|
||||
mode := h.p.GetPageInfoMode(r)
|
||||
if relpath == builtinPkgPath {
|
||||
// The fake built-in package contains unexported identifiers,
|
||||
// but we want to show them. Also, disable type association,
|
||||
// since it's not helpful for this fake package (see issue 6645).
|
||||
mode |= NoFiltering | NoTypeAssoc
|
||||
}
|
||||
info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
|
||||
if info.Err != nil {
|
||||
log.Print(info.Err)
|
||||
h.p.ServeError(w, r, relpath, info.Err)
|
||||
return
|
||||
}
|
||||
|
||||
var tabtitle, title, subtitle string
|
||||
switch {
|
||||
case info.PAst != nil:
|
||||
for _, ast := range info.PAst {
|
||||
tabtitle = ast.Name.Name
|
||||
break
|
||||
}
|
||||
case info.PDoc != nil:
|
||||
tabtitle = info.PDoc.Name
|
||||
default:
|
||||
tabtitle = info.Dirname
|
||||
title = "Directory "
|
||||
if h.p.ShowTimestamps {
|
||||
subtitle = "Last update: " + info.DirTime.String()
|
||||
}
|
||||
}
|
||||
if title == "" {
|
||||
if info.IsMain {
|
||||
// assume that the directory name is the command name
|
||||
_, tabtitle = pathpkg.Split(relpath)
|
||||
title = "Command "
|
||||
} else {
|
||||
title = "Package "
|
||||
}
|
||||
}
|
||||
title += tabtitle
|
||||
|
||||
// special cases for top-level package/command directories
|
||||
switch tabtitle {
|
||||
case "/src":
|
||||
title = "Packages"
|
||||
tabtitle = "Packages"
|
||||
case "/src/cmd":
|
||||
title = "Commands"
|
||||
tabtitle = "Commands"
|
||||
}
|
||||
|
||||
// Emit JSON array for type information.
|
||||
pi := h.c.Analysis.PackageInfo(relpath)
|
||||
hasTreeView := len(pi.CallGraph) != 0
|
||||
info.CallGraphIndex = pi.CallGraphIndex
|
||||
info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
|
||||
info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
|
||||
info.TypeInfoIndex = make(map[string]int)
|
||||
for i, ti := range pi.Types {
|
||||
info.TypeInfoIndex[ti.Name] = i
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if info.Dirname == "/src" {
|
||||
body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
|
||||
} else {
|
||||
body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
|
||||
}
|
||||
h.p.ServePage(w, Page{
|
||||
Title: title,
|
||||
Tabtitle: tabtitle,
|
||||
Subtitle: subtitle,
|
||||
Body: body,
|
||||
TreeView: hasTreeView,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handlerServer) corpusInitialized() bool {
|
||||
h.c.initMu.RLock()
|
||||
defer h.c.initMu.RUnlock()
|
||||
return h.c.initDone
|
||||
}
|
||||
|
||||
type PageInfoMode uint
|
||||
|
||||
const (
|
||||
PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
|
||||
|
||||
NoFiltering PageInfoMode = 1 << iota // do not filter exports
|
||||
AllMethods // show all embedded methods
|
||||
ShowSource // show source code, do not extract documentation
|
||||
FlatDir // show directory in a flat (non-indented) manner
|
||||
NoTypeAssoc // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
|
||||
)
|
||||
|
||||
// modeNames defines names for each PageInfoMode flag.
|
||||
var modeNames = map[string]PageInfoMode{
|
||||
"all": NoFiltering,
|
||||
"methods": AllMethods,
|
||||
"src": ShowSource,
|
||||
"flat": FlatDir,
|
||||
}
|
||||
|
||||
// generate a query string for persisting PageInfoMode between pages.
|
||||
func modeQueryString(mode PageInfoMode) string {
|
||||
if modeNames := mode.names(); len(modeNames) > 0 {
|
||||
return "?m=" + strings.Join(modeNames, ",")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// alphabetically sorted names of active flags for a PageInfoMode.
|
||||
func (m PageInfoMode) names() []string {
|
||||
var names []string
|
||||
for name, mode := range modeNames {
|
||||
if m&mode != 0 {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
|
||||
// URL form value "m". It is value is a comma-separated list of mode names
|
||||
// as defined by modeNames (e.g.: m=src,text).
|
||||
func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
|
||||
var mode PageInfoMode
|
||||
for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
|
||||
if m, found := modeNames[strings.TrimSpace(k)]; found {
|
||||
mode |= m
|
||||
}
|
||||
}
|
||||
if p.AdjustPageInfoMode != nil {
|
||||
mode = p.AdjustPageInfoMode(r, mode)
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// poorMansImporter returns a (dummy) package object named
|
||||
// by the last path component of the provided package path
|
||||
// (as is the convention for packages). This is sufficient
|
||||
// to resolve package identifiers without doing an actual
|
||||
// import. It never returns an error.
|
||||
func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
|
||||
pkg := imports[path]
|
||||
if pkg == nil {
|
||||
// note that strings.LastIndex returns -1 if there is no "/"
|
||||
pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
|
||||
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
|
||||
imports[path] = pkg
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// globalNames returns a set of the names declared by all package-level
|
||||
// declarations. Method names are returned in the form Receiver_Method.
|
||||
func globalNames(pkg *ast.Package) map[string]bool {
|
||||
names := make(map[string]bool)
|
||||
for _, file := range pkg.Files {
|
||||
for _, decl := range file.Decls {
|
||||
addNames(names, decl)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// collectExamples collects examples for pkg from testfiles.
|
||||
func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
|
||||
var files []*ast.File
|
||||
for _, f := range testfiles {
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
var examples []*doc.Example
|
||||
globals := globalNames(pkg)
|
||||
for _, e := range doc.Examples(files...) {
|
||||
name := stripExampleSuffix(e.Name)
|
||||
if name == "" || globals[name] {
|
||||
examples = append(examples, e)
|
||||
} else if c.Verbose {
|
||||
log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return examples
|
||||
}
|
||||
|
||||
// addNames adds the names declared by decl to the names set.
|
||||
// Method names are added in the form ReceiverTypeName_Method.
|
||||
func addNames(names map[string]bool, decl ast.Decl) {
|
||||
switch d := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
name := d.Name.Name
|
||||
if d.Recv != nil {
|
||||
r := d.Recv.List[0].Type
|
||||
if rr, isstar := r.(*ast.StarExpr); isstar {
|
||||
r = rr.X
|
||||
}
|
||||
|
||||
var typeName string
|
||||
switch x := r.(type) {
|
||||
case *ast.Ident:
|
||||
typeName = x.Name
|
||||
case *ast.IndexExpr:
|
||||
typeName = x.X.(*ast.Ident).Name
|
||||
case *ast.IndexListExpr:
|
||||
typeName = x.X.(*ast.Ident).Name
|
||||
}
|
||||
name = typeName + "_" + name
|
||||
}
|
||||
names[name] = true
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range d.Specs {
|
||||
switch s := spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
names[s.Name.Name] = true
|
||||
case *ast.ValueSpec:
|
||||
for _, id := range s.Names {
|
||||
names[id.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// packageExports is a local implementation of ast.PackageExports
|
||||
// which correctly updates each package file's comment list.
|
||||
// (The ast.PackageExports signature is frozen, hence the local
|
||||
// implementation).
|
||||
func packageExports(fset *token.FileSet, pkg *ast.Package) {
|
||||
for _, src := range pkg.Files {
|
||||
cmap := ast.NewCommentMap(fset, src, src.Comments)
|
||||
ast.FileExports(src)
|
||||
src.Comments = cmap.Filter(src).Comments()
|
||||
}
|
||||
}
|
||||
|
||||
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
log.Printf("%s.Execute: %s", name, err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
type writerCapturesErr struct {
|
||||
w io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *writerCapturesErr) Write(p []byte) (int, error) {
|
||||
n, err := w.w.Write(p)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
|
||||
// for the call to template.Execute. It uses an io.Writer wrapper to capture
|
||||
// errors from the underlying http.ResponseWriter. Errors are logged only when
|
||||
// they come from the template processing and not the Writer; this avoid
|
||||
// polluting log files with error messages due to networking issues, such as
|
||||
// client disconnects and http HEAD protocol violations.
|
||||
func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
|
||||
w := &writerCapturesErr{w: rw}
|
||||
err := t.Execute(w, data)
|
||||
// There are some cases where template.Execute does not return an error when
|
||||
// rw returns an error, and some where it does. So check w.err first.
|
||||
if w.err == nil && err != nil {
|
||||
// Log template errors.
|
||||
log.Printf("%s.Execute: %s", t.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
canonical := pathpkg.Clean(r.URL.Path)
|
||||
if !strings.HasSuffix(canonical, "/") {
|
||||
canonical += "/"
|
||||
}
|
||||
if r.URL.Path != canonical {
|
||||
url := *r.URL
|
||||
url.Path = canonical
|
||||
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
||||
redirected = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
c := pathpkg.Clean(r.URL.Path)
|
||||
c = strings.TrimRight(c, "/")
|
||||
if r.URL.Path != c {
|
||||
url := *r.URL
|
||||
url.Path = c
|
||||
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
||||
redirected = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
|
||||
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
|
||||
if err != nil {
|
||||
log.Printf("ReadFile: %s", err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue(PageInfoModeQueryString) == "text" {
|
||||
p.ServeText(w, src)
|
||||
return
|
||||
}
|
||||
|
||||
h := r.FormValue("h")
|
||||
s := RangeSelection(r.FormValue("s"))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if pathpkg.Ext(abspath) == ".go" {
|
||||
// Find markup links for this file (e.g. "/src/fmt/print.go").
|
||||
fi := p.Corpus.Analysis.FileInfo(abspath)
|
||||
buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
|
||||
buf.Write(marshalJSON(fi.Data))
|
||||
buf.WriteString(";</script>\n")
|
||||
|
||||
if status := p.Corpus.Analysis.Status(); status != "" {
|
||||
buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
|
||||
// TODO(adonovan): show analysis status at per-file granularity.
|
||||
fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
|
||||
}
|
||||
|
||||
buf.WriteString("<pre>")
|
||||
formatGoSource(&buf, src, fi.Links, h, s)
|
||||
buf.WriteString("</pre>")
|
||||
} else {
|
||||
buf.WriteString("<pre>")
|
||||
FormatText(&buf, src, 1, false, h, s)
|
||||
buf.WriteString("</pre>")
|
||||
}
|
||||
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
|
||||
|
||||
p.ServePage(w, Page{
|
||||
Title: title,
|
||||
SrcPath: relpath,
|
||||
Tabtitle: relpath,
|
||||
Body: buf.Bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
// formatGoSource HTML-escapes Go source text and writes it to w,
|
||||
// decorating it with the specified analysis links.
|
||||
func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
|
||||
// Emit to a temp buffer so that we can add line anchors at the end.
|
||||
saved, buf := buf, new(bytes.Buffer)
|
||||
|
||||
var i int
|
||||
var link analysis.Link // shared state of the two funcs below
|
||||
segmentIter := func() (seg Segment) {
|
||||
if i < len(links) {
|
||||
link = links[i]
|
||||
i++
|
||||
seg = Segment{link.Start(), link.End()}
|
||||
}
|
||||
return
|
||||
}
|
||||
linkWriter := func(w io.Writer, offs int, start bool) {
|
||||
link.Write(w, offs, start)
|
||||
}
|
||||
|
||||
comments := tokenSelection(text, token.COMMENT)
|
||||
var highlights Selection
|
||||
if pattern != "" {
|
||||
highlights = regexpSelection(text, pattern)
|
||||
}
|
||||
|
||||
FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
|
||||
|
||||
// Now copy buf to saved, adding line anchors.
|
||||
|
||||
// The lineSelection mechanism can't be composed with our
|
||||
// linkWriter, so we have to add line spans as another pass.
|
||||
n := 1
|
||||
for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
|
||||
// The line numbers are inserted into the document via a CSS ::before
|
||||
// pseudo-element. This prevents them from being copied when users
|
||||
// highlight and copy text.
|
||||
// ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
|
||||
// This is also the trick Github uses to hide line numbers.
|
||||
//
|
||||
// The first tab for the code snippet needs to start in column 9, so
|
||||
// it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
|
||||
// character only indents a short amount.
|
||||
//
|
||||
// Due to rounding and font width Firefox might not treat 8 rendered
|
||||
// characters as 8 characters wide, and subsequently may treat the tab
|
||||
// character in the 9th position as moving the width from (7.5 or so) up
|
||||
// to 8. See
|
||||
// https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
|
||||
// for a fuller explanation. The solution is to add a CSS class to
|
||||
// explicitly declare the width to be 8 characters.
|
||||
fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d </span>`, n, n)
|
||||
n++
|
||||
saved.Write(line)
|
||||
saved.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := p.Corpus.fs.ReadDir(abspath)
|
||||
if err != nil {
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.ServePage(w, Page{
|
||||
Title: "Directory",
|
||||
SrcPath: relpath,
|
||||
Tabtitle: relpath,
|
||||
Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
// get HTML body contents
|
||||
isMarkdown := false
|
||||
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
|
||||
if err != nil && strings.HasSuffix(abspath, ".html") {
|
||||
if md, errMD := vfs.ReadFile(p.Corpus.fs, strings.TrimSuffix(abspath, ".html")+".md"); errMD == nil {
|
||||
src = md
|
||||
isMarkdown = true
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("ReadFile: %s", err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// if it begins with "<!DOCTYPE " assume it is standalone
|
||||
// html that doesn't need the template wrapping.
|
||||
if bytes.HasPrefix(src, doctype) {
|
||||
w.Write(src)
|
||||
return
|
||||
}
|
||||
|
||||
// if it begins with a JSON blob, read in the metadata.
|
||||
meta, src, err := extractMetadata(src)
|
||||
if err != nil {
|
||||
log.Printf("decoding metadata %s: %v", relpath, err)
|
||||
}
|
||||
|
||||
page := Page{
|
||||
Title: meta.Title,
|
||||
Subtitle: meta.Subtitle,
|
||||
}
|
||||
|
||||
// evaluate as template if indicated
|
||||
if meta.Template {
|
||||
tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
|
||||
if err != nil {
|
||||
log.Printf("parsing template %s: %v", relpath, err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, page); err != nil {
|
||||
log.Printf("executing template %s: %v", relpath, err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
src = buf.Bytes()
|
||||
}
|
||||
|
||||
// Apply markdown as indicated.
|
||||
// (Note template applies before Markdown.)
|
||||
if isMarkdown {
|
||||
html, err := renderMarkdown(src)
|
||||
if err != nil {
|
||||
log.Printf("executing markdown %s: %v", relpath, err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
src = html
|
||||
}
|
||||
|
||||
// if it's the language spec, add tags to EBNF productions
|
||||
if strings.HasSuffix(abspath, "go_spec.html") {
|
||||
var buf bytes.Buffer
|
||||
Linkify(&buf, src)
|
||||
src = buf.Bytes()
|
||||
}
|
||||
|
||||
page.Body = src
|
||||
p.ServePage(w, page)
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
p.serveFile(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/index.html") {
|
||||
// We'll show index.html for the directory.
|
||||
// Use the dir/ version as canonical instead of dir/index.html.
|
||||
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
|
||||
// Check to see if we need to redirect or serve another file.
|
||||
relpath := r.URL.Path
|
||||
if m := p.Corpus.MetadataFor(relpath); m != nil {
|
||||
if m.Path != relpath {
|
||||
// Redirect to canonical path.
|
||||
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
// Serve from the actual filesystem path.
|
||||
relpath = m.filePath
|
||||
}
|
||||
|
||||
abspath := relpath
|
||||
relpath = relpath[1:] // strip leading slash
|
||||
|
||||
switch pathpkg.Ext(relpath) {
|
||||
case ".html":
|
||||
p.ServeHTMLDoc(w, r, abspath, relpath)
|
||||
return
|
||||
|
||||
case ".go":
|
||||
p.serveTextFile(w, r, abspath, relpath, "Source file")
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := p.Corpus.fs.Lstat(abspath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if dir != nil && dir.IsDir() {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
index := pathpkg.Join(abspath, "index.html")
|
||||
if util.IsTextFile(p.Corpus.fs, index) || util.IsTextFile(p.Corpus.fs, pathpkg.Join(abspath, "index.md")) {
|
||||
p.ServeHTMLDoc(w, r, index, index)
|
||||
return
|
||||
}
|
||||
p.serveDirectory(w, r, abspath, relpath)
|
||||
return
|
||||
}
|
||||
|
||||
if util.IsTextFile(p.Corpus.fs, abspath) {
|
||||
if redirectFile(w, r) {
|
||||
return
|
||||
}
|
||||
p.serveTextFile(w, r, abspath, relpath, "Text file")
|
||||
return
|
||||
}
|
||||
|
||||
p.fileServer.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write(text)
|
||||
}
|
||||
|
||||
func marshalJSON(x interface{}) []byte {
|
||||
var data []byte
|
||||
var err error
|
||||
const indentJSON = false // for easier debugging
|
||||
if indentJSON {
|
||||
data, err = json.MarshalIndent(x, "", " ")
|
||||
} else {
|
||||
data, err = json.Marshal(x)
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("json.Marshal failed: %s", err))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/doc"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
// TestIgnoredGoFiles tests the scenario where a folder has no .go or .c files,
|
||||
// but has an ignored go file.
|
||||
func TestIgnoredGoFiles(t *testing.T) {
|
||||
packagePath := "github.com/package"
|
||||
packageComment := "main is documented in an ignored .go file"
|
||||
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"src/" + packagePath + "/ignored.go": `// +build ignore
|
||||
|
||||
// ` + packageComment + `
|
||||
package main`}))
|
||||
srv := &handlerServer{
|
||||
p: &Presentation{
|
||||
Corpus: c,
|
||||
},
|
||||
c: c,
|
||||
}
|
||||
pInfo := srv.GetPageInfo("/src/"+packagePath, packagePath, NoFiltering, "linux", "amd64")
|
||||
|
||||
if pInfo.PDoc == nil {
|
||||
t.Error("pInfo.PDoc = nil; want non-nil.")
|
||||
} else {
|
||||
if got, want := pInfo.PDoc.Doc, packageComment+"\n"; got != want {
|
||||
t.Errorf("pInfo.PDoc.Doc = %q; want %q.", got, want)
|
||||
}
|
||||
if got, want := pInfo.PDoc.Name, "main"; got != want {
|
||||
t.Errorf("pInfo.PDoc.Name = %q; want %q.", got, want)
|
||||
}
|
||||
if got, want := pInfo.PDoc.ImportPath, packagePath; got != want {
|
||||
t.Errorf("pInfo.PDoc.ImportPath = %q; want %q.", got, want)
|
||||
}
|
||||
}
|
||||
if pInfo.FSet == nil {
|
||||
t.Error("pInfo.FSet = nil; want non-nil.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue5247(t *testing.T) {
|
||||
const packagePath = "example.com/p"
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"src/" + packagePath + "/p.go": `package p
|
||||
|
||||
//line notgen.go:3
|
||||
// F doc //line 1 should appear
|
||||
// line 2 should appear
|
||||
func F()
|
||||
//line foo.go:100`})) // No newline at end to check corner cases.
|
||||
|
||||
srv := &handlerServer{
|
||||
p: &Presentation{Corpus: c},
|
||||
c: c,
|
||||
}
|
||||
pInfo := srv.GetPageInfo("/src/"+packagePath, packagePath, 0, "linux", "amd64")
|
||||
if got, want := pInfo.PDoc.Funcs[0].Doc, "F doc //line 1 should appear\nline 2 should appear\n"; got != want {
|
||||
t.Errorf("pInfo.PDoc.Funcs[0].Doc = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func testServeBody(t *testing.T, p *Presentation, path, body string) {
|
||||
t.Helper()
|
||||
r := &http.Request{URL: &url.URL{Path: path}}
|
||||
rw := httptest.NewRecorder()
|
||||
p.ServeFile(rw, r)
|
||||
if rw.Code != 200 || !strings.Contains(rw.Body.String(), body) {
|
||||
t.Fatalf("GET %s: expected 200 w/ %q: got %d w/ body:\n%s",
|
||||
path, body, rw.Code, rw.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectAndMetadata(t *testing.T) {
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"doc/y/index.html": "Hello, y.",
|
||||
"doc/x/index.html": `<!--{
|
||||
"Path": "/doc/x/"
|
||||
}-->
|
||||
|
||||
Hello, x.
|
||||
`}))
|
||||
c.updateMetadata()
|
||||
p := &Presentation{
|
||||
Corpus: c,
|
||||
GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
|
||||
}
|
||||
|
||||
// Test that redirect is sent back correctly.
|
||||
// Used to panic. See golang.org/issue/40665.
|
||||
for _, elem := range []string{"x", "y"} {
|
||||
dir := "/doc/" + elem + "/"
|
||||
|
||||
r := &http.Request{URL: &url.URL{Path: dir + "index.html"}}
|
||||
rw := httptest.NewRecorder()
|
||||
p.ServeFile(rw, r)
|
||||
loc := rw.Result().Header.Get("Location")
|
||||
if rw.Code != 301 || loc != dir {
|
||||
t.Errorf("GET %s: expected 301 -> %q, got %d -> %q", r.URL.Path, dir, rw.Code, loc)
|
||||
}
|
||||
|
||||
testServeBody(t, p, dir, "Hello, "+elem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
p := &Presentation{
|
||||
Corpus: NewCorpus(mapfs.New(map[string]string{
|
||||
"doc/test.md": "**bold**",
|
||||
"doc/test2.md": `{{"*template*"}}`,
|
||||
})),
|
||||
GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
|
||||
}
|
||||
|
||||
testServeBody(t, p, "/doc/test.html", "<strong>bold</strong>")
|
||||
testServeBody(t, p, "/doc/test2.html", "<em>template</em>")
|
||||
}
|
||||
|
||||
func TestGenerics(t *testing.T) {
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"blah/blah.go": `package blah
|
||||
|
||||
var A AStruct[int]
|
||||
|
||||
type AStruct[T any] struct {
|
||||
A string
|
||||
X T
|
||||
}
|
||||
|
||||
func (a *AStruct[T]) Method() T {
|
||||
return a.X
|
||||
}
|
||||
|
||||
func (a AStruct[T]) NonPointerMethod() T {
|
||||
return a.X
|
||||
}
|
||||
|
||||
func NewAStruct[T any](arg T) *AStruct[T] {
|
||||
return &AStruct[T]{ X: arg }
|
||||
}
|
||||
|
||||
type NonGenericStruct struct {
|
||||
B int
|
||||
}
|
||||
|
||||
func (b *NonGenericStruct) NonGenericMethod() int {
|
||||
return b.B
|
||||
}
|
||||
|
||||
func NewNonGenericStruct(arg int) *NonGenericStruct {
|
||||
return &NonGenericStruct{arg}
|
||||
}
|
||||
|
||||
type Pair[K, V any] struct {
|
||||
K K
|
||||
V V
|
||||
}
|
||||
|
||||
func (p Pair[K, V]) Apply(kf func(K) K, vf func(V) V) Pair[K, V] {
|
||||
return &Pair{ K: kf(p.K), V: vf(p.V) }
|
||||
}
|
||||
|
||||
func (p *Pair[K, V]) Set(k K, v V) {
|
||||
p.K = k
|
||||
p.V = v
|
||||
}
|
||||
|
||||
func NewPair[K, V any](k K, v V) Pair[K, V] {
|
||||
return Pair[K, V]{ k, v }
|
||||
}
|
||||
`}))
|
||||
|
||||
srv := &handlerServer{
|
||||
p: &Presentation{
|
||||
Corpus: c,
|
||||
},
|
||||
c: c,
|
||||
}
|
||||
pInfo := srv.GetPageInfo("/blah/", "", NoFiltering, "linux", "amd64")
|
||||
t.Logf("%v\n", pInfo)
|
||||
|
||||
findType := func(name string) *doc.Type {
|
||||
for _, typ := range pInfo.PDoc.Types {
|
||||
if typ.Name == name {
|
||||
return typ
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
assertFuncs := func(typ *doc.Type, typFuncs []*doc.Func, funcs ...string) {
|
||||
typfuncs := make([]string, len(typFuncs))
|
||||
for i := range typFuncs {
|
||||
typfuncs[i] = typFuncs[i].Name
|
||||
}
|
||||
sort.Strings(typfuncs)
|
||||
sort.Strings(funcs)
|
||||
if len(typfuncs) != len(funcs) {
|
||||
t.Errorf("function mismatch for type %q, got: %q, want: %q", typ.Name, typfuncs, funcs)
|
||||
return
|
||||
}
|
||||
for i := range funcs {
|
||||
if funcs[i] != typfuncs[i] {
|
||||
t.Errorf("function mismatch for type %q: got: %q, want: %q", typ.Name, typfuncs, funcs)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aStructType := findType("AStruct")
|
||||
assertFuncs(aStructType, aStructType.Funcs, "NewAStruct")
|
||||
assertFuncs(aStructType, aStructType.Methods, "Method", "NonPointerMethod")
|
||||
|
||||
nonGenericStructType := findType("NonGenericStruct")
|
||||
assertFuncs(nonGenericStructType, nonGenericStructType.Funcs, "NewNonGenericStruct")
|
||||
assertFuncs(nonGenericStructType, nonGenericStructType.Methods, "NonGenericMethod")
|
||||
|
||||
pairType := findType("Pair")
|
||||
assertFuncs(pairType, pairType.Funcs, "NewPair")
|
||||
assertFuncs(pairType, pairType.Methods, "Apply", "Set")
|
||||
|
||||
if len(pInfo.PDoc.Funcs) > 0 {
|
||||
t.Errorf("unexpected functions in package documentation")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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.
|
||||
|
||||
// This file contains the infrastructure to create a code
|
||||
// snippet for search results.
|
||||
//
|
||||
// Note: At the moment, this only creates HTML snippets.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
type Snippet struct {
|
||||
Line int
|
||||
Text string // HTML-escaped
|
||||
}
|
||||
|
||||
func (p *Presentation) newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
||||
// TODO instead of pretty-printing the node, should use the original source instead
|
||||
var buf1 bytes.Buffer
|
||||
p.writeNode(&buf1, nil, fset, decl)
|
||||
// wrap text with <pre> tag
|
||||
var buf2 bytes.Buffer
|
||||
buf2.WriteString("<pre>")
|
||||
FormatText(&buf2, buf1.Bytes(), -1, true, id.Name, nil)
|
||||
buf2.WriteString("</pre>")
|
||||
return &Snippet{fset.Position(id.Pos()).Line, buf2.String()}
|
||||
}
|
||||
|
||||
func findSpec(list []ast.Spec, id *ast.Ident) ast.Spec {
|
||||
for _, spec := range list {
|
||||
switch s := spec.(type) {
|
||||
case *ast.ImportSpec:
|
||||
if s.Name == id {
|
||||
return s
|
||||
}
|
||||
case *ast.ValueSpec:
|
||||
for _, n := range s.Names {
|
||||
if n == id {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case *ast.TypeSpec:
|
||||
if s.Name == id {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Presentation) genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
|
||||
s := findSpec(d.Specs, id)
|
||||
if s == nil {
|
||||
return nil // declaration doesn't contain id - exit gracefully
|
||||
}
|
||||
|
||||
// only use the spec containing the id for the snippet
|
||||
dd := &ast.GenDecl{
|
||||
Doc: d.Doc,
|
||||
TokPos: d.Pos(),
|
||||
Tok: d.Tok,
|
||||
Lparen: d.Lparen,
|
||||
Specs: []ast.Spec{s},
|
||||
Rparen: d.Rparen,
|
||||
}
|
||||
|
||||
return p.newSnippet(fset, dd, id)
|
||||
}
|
||||
|
||||
func (p *Presentation) funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
|
||||
if d.Name != id {
|
||||
return nil // declaration doesn't contain id - exit gracefully
|
||||
}
|
||||
|
||||
// only use the function signature for the snippet
|
||||
dd := &ast.FuncDecl{
|
||||
Doc: d.Doc,
|
||||
Recv: d.Recv,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
}
|
||||
|
||||
return p.newSnippet(fset, dd, id)
|
||||
}
|
||||
|
||||
// NewSnippet creates a text snippet from a declaration decl containing an
|
||||
// identifier id. Parts of the declaration not containing the identifier
|
||||
// may be removed for a more compact snippet.
|
||||
func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
||||
// TODO(bradfitz, adg): remove this function. But it's used by indexer, which
|
||||
// doesn't have a *Presentation, and NewSnippet needs a TabWidth.
|
||||
var p Presentation
|
||||
p.TabWidth = 4
|
||||
return p.NewSnippet(fset, decl, id)
|
||||
}
|
||||
|
||||
// NewSnippet creates a text snippet from a declaration decl containing an
|
||||
// identifier id. Parts of the declaration not containing the identifier
|
||||
// may be removed for a more compact snippet.
|
||||
func (p *Presentation) NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
||||
var s *Snippet
|
||||
switch d := decl.(type) {
|
||||
case *ast.GenDecl:
|
||||
s = p.genSnippet(fset, d, id)
|
||||
case *ast.FuncDecl:
|
||||
s = p.funcSnippet(fset, d, id)
|
||||
}
|
||||
|
||||
// handle failure gracefully
|
||||
if s == nil {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, `<span class="alert">could not generate a snippet for <span class="highlight">%s</span></span>`, id.Name)
|
||||
s = &Snippet{fset.Position(id.Pos()).Line, buf.String()}
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// 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 godoc
|
||||
|
||||
// This file contains the mechanism to "linkify" html source
|
||||
// text containing EBNF sections (as found in go_spec.html).
|
||||
// The result is the input source text with the EBNF sections
|
||||
// modified such that identifiers are linked to the respective
|
||||
// definitions.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"text/scanner"
|
||||
)
|
||||
|
||||
type ebnfParser struct {
|
||||
out io.Writer // parser output
|
||||
src []byte // parser input
|
||||
scanner scanner.Scanner
|
||||
prev int // offset of previous token
|
||||
pos int // offset of current token
|
||||
tok rune // one token look-ahead
|
||||
lit string // token literal
|
||||
}
|
||||
|
||||
func (p *ebnfParser) flush() {
|
||||
p.out.Write(p.src[p.prev:p.pos])
|
||||
p.prev = p.pos
|
||||
}
|
||||
|
||||
func (p *ebnfParser) next() {
|
||||
p.tok = p.scanner.Scan()
|
||||
p.pos = p.scanner.Position.Offset
|
||||
p.lit = p.scanner.TokenText()
|
||||
}
|
||||
|
||||
func (p *ebnfParser) printf(format string, args ...interface{}) {
|
||||
p.flush()
|
||||
fmt.Fprintf(p.out, format, args...)
|
||||
}
|
||||
|
||||
func (p *ebnfParser) errorExpected(msg string) {
|
||||
p.printf(`<span class="highlight">error: expected %s, found %s</span>`, msg, scanner.TokenString(p.tok))
|
||||
}
|
||||
|
||||
func (p *ebnfParser) expect(tok rune) {
|
||||
if p.tok != tok {
|
||||
p.errorExpected(scanner.TokenString(tok))
|
||||
}
|
||||
p.next() // make progress in any case
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseIdentifier(def bool) {
|
||||
if p.tok == scanner.Ident {
|
||||
name := p.lit
|
||||
if def {
|
||||
p.printf(`<a id="%s">%s</a>`, name, name)
|
||||
} else {
|
||||
p.printf(`<a href="#%s" class="noline">%s</a>`, name, name)
|
||||
}
|
||||
p.prev += len(name) // skip identifier when printing next time
|
||||
p.next()
|
||||
} else {
|
||||
p.expect(scanner.Ident)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseTerm() bool {
|
||||
switch p.tok {
|
||||
case scanner.Ident:
|
||||
p.parseIdentifier(false)
|
||||
|
||||
case scanner.String, scanner.RawString:
|
||||
p.next()
|
||||
const ellipsis = '…' // U+2026, the horizontal ellipsis character
|
||||
if p.tok == ellipsis {
|
||||
p.next()
|
||||
p.expect(scanner.String)
|
||||
}
|
||||
|
||||
case '(':
|
||||
p.next()
|
||||
p.parseExpression()
|
||||
p.expect(')')
|
||||
|
||||
case '[':
|
||||
p.next()
|
||||
p.parseExpression()
|
||||
p.expect(']')
|
||||
|
||||
case '{':
|
||||
p.next()
|
||||
p.parseExpression()
|
||||
p.expect('}')
|
||||
|
||||
default:
|
||||
return false // no term found
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseSequence() {
|
||||
if !p.parseTerm() {
|
||||
p.errorExpected("term")
|
||||
}
|
||||
for p.parseTerm() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseExpression() {
|
||||
for {
|
||||
p.parseSequence()
|
||||
if p.tok != '|' {
|
||||
break
|
||||
}
|
||||
p.next()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseProduction() {
|
||||
p.parseIdentifier(true)
|
||||
p.expect('=')
|
||||
if p.tok != '.' {
|
||||
p.parseExpression()
|
||||
}
|
||||
p.expect('.')
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parse(out io.Writer, src []byte) {
|
||||
// initialize ebnfParser
|
||||
p.out = out
|
||||
p.src = src
|
||||
p.scanner.Init(bytes.NewBuffer(src))
|
||||
p.next() // initializes pos, tok, lit
|
||||
|
||||
// process source
|
||||
for p.tok != scanner.EOF {
|
||||
p.parseProduction()
|
||||
}
|
||||
p.flush()
|
||||
}
|
||||
|
||||
// Markers around EBNF sections
|
||||
var (
|
||||
openTag = []byte(`<pre class="ebnf">`)
|
||||
closeTag = []byte(`</pre>`)
|
||||
)
|
||||
|
||||
func Linkify(out io.Writer, src []byte) {
|
||||
for len(src) > 0 {
|
||||
// i: beginning of EBNF text (or end of source)
|
||||
i := bytes.Index(src, openTag)
|
||||
if i < 0 {
|
||||
i = len(src) - len(openTag)
|
||||
}
|
||||
i += len(openTag)
|
||||
|
||||
// j: end of EBNF text (or end of source)
|
||||
j := bytes.Index(src[i:], closeTag) // close marker
|
||||
if j < 0 {
|
||||
j = len(src) - i
|
||||
}
|
||||
j += i
|
||||
|
||||
// write text before EBNF
|
||||
out.Write(src[0:i])
|
||||
// process EBNF
|
||||
var p ebnfParser
|
||||
p.parse(out, src[i:j])
|
||||
|
||||
// advance
|
||||
src = src[j:]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEBNFString(t *testing.T) {
|
||||
var p ebnfParser
|
||||
var buf bytes.Buffer
|
||||
src := []byte("octal_byte_value = `\\` octal_digit octal_digit octal_digit .")
|
||||
p.parse(&buf, src)
|
||||
|
||||
if strings.Contains(buf.String(), "error") {
|
||||
t.Error(buf.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SpotInfo
|
||||
|
||||
// A SpotInfo value describes a particular identifier spot in a given file;
|
||||
// It encodes three values: the SpotKind (declaration or use), a line or
|
||||
// snippet index "lori", and whether it's a line or index.
|
||||
//
|
||||
// The following encoding is used:
|
||||
//
|
||||
// bits 32 4 1 0
|
||||
// value [lori|kind|isIndex]
|
||||
type SpotInfo uint32
|
||||
|
||||
// SpotKind describes whether an identifier is declared (and what kind of
|
||||
// declaration) or used.
|
||||
type SpotKind uint32
|
||||
|
||||
const (
|
||||
PackageClause SpotKind = iota
|
||||
ImportDecl
|
||||
ConstDecl
|
||||
TypeDecl
|
||||
VarDecl
|
||||
FuncDecl
|
||||
MethodDecl
|
||||
Use
|
||||
nKinds
|
||||
)
|
||||
|
||||
var (
|
||||
// These must match the SpotKind values above.
|
||||
name = []string{
|
||||
"Packages",
|
||||
"Imports",
|
||||
"Constants",
|
||||
"Types",
|
||||
"Variables",
|
||||
"Functions",
|
||||
"Methods",
|
||||
"Uses",
|
||||
"Unknown",
|
||||
}
|
||||
)
|
||||
|
||||
func (x SpotKind) Name() string { return name[x] }
|
||||
|
||||
func init() {
|
||||
// sanity check: if nKinds is too large, the SpotInfo
|
||||
// accessor functions may need to be updated
|
||||
if nKinds > 8 {
|
||||
panic("internal error: nKinds > 8")
|
||||
}
|
||||
}
|
||||
|
||||
// makeSpotInfo makes a SpotInfo.
|
||||
func makeSpotInfo(kind SpotKind, lori int, isIndex bool) SpotInfo {
|
||||
// encode lori: bits [4..32)
|
||||
x := SpotInfo(lori) << 4
|
||||
if int(x>>4) != lori {
|
||||
// lori value doesn't fit - since snippet indices are
|
||||
// most certainly always smaller then 1<<28, this can
|
||||
// only happen for line numbers; give it no line number (= 0)
|
||||
x = 0
|
||||
}
|
||||
// encode kind: bits [1..4)
|
||||
x |= SpotInfo(kind) << 1
|
||||
// encode isIndex: bit 0
|
||||
if isIndex {
|
||||
x |= 1
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (x SpotInfo) Kind() SpotKind { return SpotKind(x >> 1 & 7) }
|
||||
func (x SpotInfo) Lori() int { return int(x >> 4) }
|
||||
func (x SpotInfo) IsIndex() bool { return x&1 != 0 }
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,254 @@
|
||||
<!--{
|
||||
"Title": "Static analysis features of godoc"
|
||||
}-->
|
||||
|
||||
<style>
|
||||
span.err { 'font-size:120%; color:darkred; background-color: yellow; }
|
||||
img.ss { margin-left: 1in; } /* screenshot */
|
||||
img.dotted { border: thin dotted; }
|
||||
</style>
|
||||
|
||||
<!-- Images were grabbed from Chrome/Linux at 150% zoom, and are
|
||||
displayed at 66% of natural size. This allows users to zoom a
|
||||
little before seeing pixels. -->
|
||||
|
||||
<p>
|
||||
When invoked with the <code>-analysis</code> flag, godoc performs
|
||||
static analysis on the Go packages it indexes and displays the
|
||||
results in the source and package views. This document provides a
|
||||
brief tour of these features.
|
||||
</p>
|
||||
|
||||
<h2>Type analysis features</h2>
|
||||
<p>
|
||||
<code>godoc -analysis=type</code> performs static checking similar
|
||||
to that done by a compiler: it detects ill-formed programs, resolves
|
||||
each identifier to the entity it denotes, computes the type of each
|
||||
expression and the method set of each type, and determines which
|
||||
types are assignable to each interface type.
|
||||
|
||||
<b>Type analysis</b> is relatively quick, requiring about 10 seconds for
|
||||
the >200 packages of the standard library, for example.
|
||||
</p>
|
||||
|
||||
<h3>Compiler errors</h3>
|
||||
<p>
|
||||
If any source file contains a compilation error, the source view
|
||||
will highlight the errant location in red. Hovering over it
|
||||
displays the error message.
|
||||
</p>
|
||||
<img class="ss" width='811' src='error1.png'><br/>
|
||||
|
||||
<h3>Identifier resolution</h3>
|
||||
<p>
|
||||
In the source view, every referring identifier is annotated with
|
||||
information about the language entity it refers to: a package,
|
||||
constant, variable, type, function or statement label.
|
||||
|
||||
Hovering over the identifier reveals the entity's kind and type
|
||||
(e.g. <code>var x int</code> or <code>func f
|
||||
func(int) string</code>).
|
||||
</p>
|
||||
<img class="ss" width='652' src='ident-field.png'><br/>
|
||||
<br/>
|
||||
<img class="ss" width='652' src='ident-func.png'>
|
||||
<p>
|
||||
Clicking the link takes you to the entity's definition.
|
||||
</p>
|
||||
<img class="ss" width='652' src='ident-def.png'><br/>
|
||||
|
||||
<h3>Type information: size/alignment, method set, interfaces</h3>
|
||||
<p>
|
||||
Clicking on the identifier that defines a named type causes a panel
|
||||
to appear, displaying information about the named type, including
|
||||
its size and alignment in bytes, its
|
||||
<a href='https://golang.org/ref/spec#Method_sets'>method set</a>, and its
|
||||
<i>implements</i> relation: the set of types T that are assignable to
|
||||
or from this type U where at least one of T or U is an interface.
|
||||
|
||||
This example shows information about <code>net/rpc.methodType</code>.
|
||||
</p>
|
||||
<img class="ss" width='470' src='typeinfo-src.png'>
|
||||
<p>
|
||||
The method set includes not only the declared methods of the type,
|
||||
but also any methods "promoted" from anonymous fields of structs,
|
||||
such as <code>sync.Mutex</code> in this example.
|
||||
|
||||
In addition, the receiver type is displayed as <code>*T</code> or
|
||||
<code>T</code> depending on whether it requires the address or just
|
||||
a copy of the receiver value.
|
||||
</p>
|
||||
<p>
|
||||
The method set and <i>implements</i> relation are also available
|
||||
via the package view.
|
||||
</p>
|
||||
<img class="ss dotted" width='716' src='typeinfo-pkg.png'>
|
||||
|
||||
<h2>Pointer analysis features</h2>
|
||||
<p>
|
||||
<code>godoc -analysis=pointer</code> additionally performs a precise
|
||||
whole-program <b>pointer analysis</b>. In other words, it
|
||||
approximates the set of memory locations to which each
|
||||
reference—not just vars of kind <code>*T</code>, but also
|
||||
<code>[]T</code>, <code>func</code>, <code>map</code>,
|
||||
<code>chan</code>, and <code>interface</code>—may refer. This
|
||||
information reveals the possible destinations of each dynamic call
|
||||
(via a <code>func</code> variable or interface method), and the
|
||||
relationship between send and receive operations on the same
|
||||
channel.
|
||||
</p>
|
||||
<p>
|
||||
Compared to type analysis, pointer analysis requires more time and
|
||||
memory, and is impractical for code bases exceeding a million lines.
|
||||
</p>
|
||||
|
||||
<h3>Call graph navigation</h3>
|
||||
<p>
|
||||
When pointer analysis is complete, the source view annotates the
|
||||
code with <b>callers</b> and <b>callees</b> information: callers
|
||||
information is associated with the <code>func</code> keyword that
|
||||
declares a function, and callees information is associated with the
|
||||
open paren '<span style="color: dark-blue"><code>(</code></span>' of
|
||||
a function call.
|
||||
</p>
|
||||
<p>
|
||||
In this example, hovering over the declaration of the
|
||||
<code>rot13</code> function (defined in strings/strings_test.go)
|
||||
reveals that it is called in exactly one place.
|
||||
</p>
|
||||
<img class="ss" width='612' src='callers1.png'>
|
||||
<p>
|
||||
Clicking the link navigates to the sole caller. (If there were
|
||||
multiple callers, a list of choices would be displayed first.)
|
||||
</p>
|
||||
<img class="ss" width='680' src='callers2.png'>
|
||||
<p>
|
||||
Notice that hovering over this call reveals that there are 19
|
||||
possible callees at this site, of which our <code>rot13</code>
|
||||
function was just one: this is a dynamic call through a variable of
|
||||
type <code>func(rune) rune</code>.
|
||||
|
||||
Clicking on the call brings up the list of all 19 potential callees,
|
||||
shown truncated. Many of them are anonymous functions.
|
||||
</p>
|
||||
<img class="ss" width='564' src='call3.png'>
|
||||
<p>
|
||||
Pointer analysis gives a very precise approximation of the call
|
||||
graph compared to type-based techniques.
|
||||
|
||||
As a case in point, the next example shows the dynamic call inside
|
||||
the <code>testing</code> package responsible for calling all
|
||||
user-defined functions named <code>Example<i>XYZ</i></code>.
|
||||
</p>
|
||||
<img class="ss" width='361' src='call-eg.png'>
|
||||
<p>
|
||||
Recall that all such functions have type <code>func()</code>,
|
||||
i.e. no arguments and no results. A type-based approximation could
|
||||
only conclude that this call might dispatch to any function matching
|
||||
that type—and these are very numerous in most
|
||||
programs—but pointer analysis can track the flow of specific
|
||||
<code>func</code> values through the testing package.
|
||||
|
||||
As an indication of its precision, the result contains only
|
||||
functions whose name starts with <code>Example</code>.
|
||||
</p>
|
||||
|
||||
<h3>Intra-package call graph</h3>
|
||||
<p>
|
||||
The same call graph information is presented in a very different way
|
||||
in the package view. For each package, an interactive tree view
|
||||
allows exploration of the call graph as it relates to just that
|
||||
package; all functions from other packages are elided.
|
||||
|
||||
The roots of the tree are the external entry points of the package:
|
||||
not only its exported functions, but also any unexported or
|
||||
anonymous functions that are called (dynamically) from outside the
|
||||
package.
|
||||
</p>
|
||||
<p>
|
||||
This example shows the entry points of the
|
||||
<code>path/filepath</code> package, with the call graph for
|
||||
<code>Glob</code> expanded several levels
|
||||
</p>
|
||||
<img class="ss dotted" width='501' src='ipcg-pkg.png'>
|
||||
<p>
|
||||
Notice that the nodes for Glob and Join appear multiple times: the
|
||||
tree is a partial unrolling of a cyclic graph; the full unrolling
|
||||
is in general infinite.
|
||||
</p>
|
||||
<p>
|
||||
For each function documented in the package view, another
|
||||
interactive tree view allows exploration of the same graph starting
|
||||
at that function.
|
||||
|
||||
This is a portion of the internal graph of
|
||||
<code>net/http.ListenAndServe</code>.
|
||||
</p>
|
||||
<img class="ss dotted" width='455' src='ipcg-func.png'>
|
||||
|
||||
<h3>Channel peers (send ↔ receive)</h3>
|
||||
<p>
|
||||
Because concurrent Go programs use channels to pass not just values
|
||||
but also control between different goroutines, it is natural when
|
||||
reading Go code to want to navigate from a channel send to the
|
||||
corresponding receive so as to understand the sequence of events.
|
||||
</p>
|
||||
<p>
|
||||
Godoc annotates every channel operation—make, send, range,
|
||||
receive, close—with a link to a panel displaying information
|
||||
about other operations that might alias the same channel.
|
||||
</p>
|
||||
<p>
|
||||
This example, from the tests of <code>net/http</code>, shows a send
|
||||
operation on a <code>chan bool</code>.
|
||||
</p>
|
||||
<img class="ss" width='811' src='chan1.png'>
|
||||
<p>
|
||||
Clicking on the <code><-</code> send operator reveals that this
|
||||
channel is made at a unique location (line 332) and that there are
|
||||
three receive operations that might read this value.
|
||||
|
||||
It hardly needs pointing out that some channel element types are
|
||||
very widely used (e.g. struct{}, bool, int, interface{}) and that a
|
||||
typical Go program might contain dozens of receive operations on a
|
||||
value of type <code>chan bool</code>; yet the pointer analysis is
|
||||
able to distinguish operations on channels at a much finer precision
|
||||
than based on their type alone.
|
||||
</p>
|
||||
<p>
|
||||
Notice also that the send occurs in a different (anonymous) function
|
||||
from the outer one containing the <code>make</code> and the receive
|
||||
operations.
|
||||
</p>
|
||||
<p>
|
||||
Here's another example of send on a different <code>chan
|
||||
bool</code>, also in package <code>net/http</code>:
|
||||
</p>
|
||||
<img class="ss" width='774' src='chan2a.png'>
|
||||
<p>
|
||||
The analysis finds just one receive operation that might receive
|
||||
from this channel, in the test for this feature.
|
||||
</p>
|
||||
<img class="ss" width='737' src='chan2b.png'>
|
||||
|
||||
<h2>Known issues</h2>
|
||||
<p>
|
||||
All analysis results pertain to exactly
|
||||
one configuration (e.g. amd64 linux). Files that are conditionally
|
||||
compiled based on different platforms or build tags are not visible
|
||||
to the analysis.
|
||||
</p>
|
||||
<p>
|
||||
Files that <code>import "C"</code> require
|
||||
preprocessing by the cgo tool. The file offsets after preprocessing
|
||||
do not align with the unpreprocessed file, so markup is misaligned.
|
||||
</p>
|
||||
<p>
|
||||
Files are not periodically re-analyzed.
|
||||
If the files change underneath the running server, the displayed
|
||||
markup is misaligned.
|
||||
</p>
|
||||
<p>
|
||||
Additional issues are listed at
|
||||
<a href='https://go.googlesource.com/tools/+/master/godoc/analysis/README'>tools/godoc/analysis/README</a>.
|
||||
</p>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,15 @@
|
||||
<div class="toggle" style="display: none">
|
||||
<div class="collapsed">
|
||||
<p class="exampleHeading toggleButton">▹ <span class="text">Internal call graph</span></p>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<p class="exampleHeading toggleButton">▾ <span class="text">Internal call graph</span></p>
|
||||
<p>
|
||||
This viewer shows the portion of the internal call
|
||||
graph of this package that is reachable from this function.
|
||||
See the <a href='#pkg-callgraph'>package's call
|
||||
graph</a> for more information.
|
||||
</p>
|
||||
<ul style="margin-left: 0.5in" id="callgraph-{{.Index}}" class="treeview"></ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<p>
|
||||
<table class="layout">
|
||||
<tr>
|
||||
<th align="left">File</th>
|
||||
<td width="25"> </td>
|
||||
<th align="right">Bytes</th>
|
||||
<td width="25"> </td>
|
||||
<th align="left">Modified</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="..">..</a></td>
|
||||
</tr>
|
||||
{{range .}}
|
||||
<tr>
|
||||
{{$name_html := fileInfoName . | html}}
|
||||
<td align="left"><a href="{{$name_html}}">{{$name_html}}</a></td>
|
||||
<td></td>
|
||||
<td align="right">{{html .Size}}</td>
|
||||
<td></td>
|
||||
<td align="left">{{fileInfoTime . | html}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
</table>
|
||||
</p>
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package static exports a map of static file content that supports the godoc
|
||||
// user interface. The map should be used with the mapfs package, see
|
||||
// golang.org/x/tools/godoc/vfs/mapfs.
|
||||
package static // import "golang.org/x/tools/godoc/static"
|
||||
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<p>
|
||||
<span class="alert" style="font-size:120%">{{html .}}</span>
|
||||
</p>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div id="example_{{.Name}}" class="toggle">
|
||||
<div class="collapsed">
|
||||
<p class="exampleHeading toggleButton">▹ <span class="text">Example{{example_suffix .Name}}</span></p>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<p class="exampleHeading toggleButton">▾ <span class="text">Example{{example_suffix .Name}}</span></p>
|
||||
{{with .Doc}}<p>{{html .}}</p>{{end}}
|
||||
{{$output := .Output}}
|
||||
{{with .Play}}
|
||||
<div class="play">
|
||||
<div class="input"><textarea class="code" spellcheck="false">{{html .}}</textarea></div>
|
||||
<div class="output"><pre>{{html $output}}</pre></div>
|
||||
<div class="buttons">
|
||||
<a class="run" title="Run this code [shift-enter]">Run</a>
|
||||
<a class="fmt" title="Format this code">Format</a>
|
||||
<a class="share" title="Share this code">Share</a>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>Code:</p>
|
||||
<pre class="code">{{.Code}}</pre>
|
||||
{{with .Output}}
|
||||
<p>Output:</p>
|
||||
<pre class="output">{{html .}}</pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,107 @@
|
||||
// Copyright 2014 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 static
|
||||
|
||||
//go:generate go run makestatic.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var files = []string{
|
||||
"analysis/call3.png",
|
||||
"analysis/call-eg.png",
|
||||
"analysis/callers1.png",
|
||||
"analysis/callers2.png",
|
||||
"analysis/chan1.png",
|
||||
"analysis/chan2a.png",
|
||||
"analysis/chan2b.png",
|
||||
"analysis/error1.png",
|
||||
"analysis/help.html",
|
||||
"analysis/ident-def.png",
|
||||
"analysis/ident-field.png",
|
||||
"analysis/ident-func.png",
|
||||
"analysis/ipcg-func.png",
|
||||
"analysis/ipcg-pkg.png",
|
||||
"analysis/typeinfo-pkg.png",
|
||||
"analysis/typeinfo-src.png",
|
||||
"callgraph.html",
|
||||
"dirlist.html",
|
||||
"error.html",
|
||||
"example.html",
|
||||
"favicon.ico",
|
||||
"godoc.html",
|
||||
"godocs.js",
|
||||
"gopher/pkg.png",
|
||||
"images/minus.gif",
|
||||
"images/plus.gif",
|
||||
"images/treeview-black-line.gif",
|
||||
"images/treeview-black.gif",
|
||||
"images/treeview-default-line.gif",
|
||||
"images/treeview-default.gif",
|
||||
"images/treeview-gray-line.gif",
|
||||
"images/treeview-gray.gif",
|
||||
"implements.html",
|
||||
"jquery.js",
|
||||
"jquery.treeview.css",
|
||||
"jquery.treeview.edit.js",
|
||||
"jquery.treeview.js",
|
||||
"methodset.html",
|
||||
"package.html",
|
||||
"packageroot.html",
|
||||
"play.js",
|
||||
"playground.js",
|
||||
"search.html",
|
||||
"searchcode.html",
|
||||
"searchdoc.html",
|
||||
"searchtxt.html",
|
||||
"style.css",
|
||||
}
|
||||
|
||||
// Generate reads a set of files and returns a file buffer that declares
|
||||
// a map of string constants containing contents of the input files.
|
||||
func Generate() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, "%v\n\n%v\n\npackage static\n\n", license, warning)
|
||||
fmt.Fprintf(buf, "var Files = map[string]string{\n")
|
||||
for _, fn := range files {
|
||||
b, err := os.ReadFile(fn)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
fmt.Fprintf(buf, "\t%q: ", fn)
|
||||
appendQuote(buf, b)
|
||||
fmt.Fprintf(buf, ",\n\n")
|
||||
}
|
||||
fmt.Fprintln(buf, "}")
|
||||
return format.Source(buf.Bytes())
|
||||
}
|
||||
|
||||
// appendQuote is like strconv.AppendQuote, but we avoid the latter
|
||||
// because it changes when Unicode evolves, breaking gen_test.go.
|
||||
func appendQuote(out *bytes.Buffer, data []byte) {
|
||||
out.WriteByte('"')
|
||||
for _, b := range data {
|
||||
if b == '\\' || b == '"' {
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(b)
|
||||
} else if b <= unicode.MaxASCII && unicode.IsPrint(rune(b)) && !unicode.IsSpace(rune(b)) {
|
||||
out.WriteByte(b)
|
||||
} else {
|
||||
fmt.Fprintf(out, "\\x%02x", b)
|
||||
}
|
||||
}
|
||||
out.WriteByte('"')
|
||||
}
|
||||
|
||||
const warning = `// Code generated by "makestatic"; DO NOT EDIT.`
|
||||
|
||||
const license = `// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.`
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2018 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 static
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestStaticIsUpToDate(t *testing.T) {
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skip("files not available on android")
|
||||
}
|
||||
oldBuf, err := os.ReadFile("static.go")
|
||||
if err != nil {
|
||||
t.Errorf("error while reading static.go: %v\n", err)
|
||||
}
|
||||
|
||||
newBuf, err := Generate()
|
||||
if err != nil {
|
||||
t.Errorf("error while generating static.go: %v\n", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(oldBuf, newBuf) {
|
||||
t.Error(`static.go is stale. Run:
|
||||
$ go generate golang.org/x/tools/godoc/static
|
||||
$ git diff
|
||||
to see the differences.`)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TestAppendQuote ensures that AppendQuote produces a valid literal.
|
||||
func TestAppendQuote(t *testing.T) {
|
||||
var in, out bytes.Buffer
|
||||
for r := rune(0); r < unicode.MaxRune; r++ {
|
||||
in.WriteRune(r)
|
||||
}
|
||||
appendQuote(&out, in.Bytes())
|
||||
in2, err := strconv.Unquote(out.String())
|
||||
if err != nil {
|
||||
t.Fatalf("AppendQuote produced invalid string literal: %v", err)
|
||||
}
|
||||
if got, want := in2, in.String(); got != want {
|
||||
t.Fatal("AppendQuote modified string") // no point printing got/want: huge
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#375EAB">
|
||||
{{with .Tabtitle}}
|
||||
<title>{{html .}} - Go Documentation Server</title>
|
||||
{{else}}
|
||||
<title>Go Documentation Server</title>
|
||||
{{end}}
|
||||
<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
|
||||
{{if .TreeView}}
|
||||
<link rel="stylesheet" href="/lib/godoc/jquery.treeview.css">
|
||||
{{end}}
|
||||
<script>window.initFuncs = [];</script>
|
||||
<script src="/lib/godoc/jquery.js" defer></script>
|
||||
{{if .TreeView}}
|
||||
<script src="/lib/godoc/jquery.treeview.js" defer></script>
|
||||
<script src="/lib/godoc/jquery.treeview.edit.js" defer></script>
|
||||
{{end}}
|
||||
|
||||
{{if .Playground}}
|
||||
<script src="/lib/godoc/playground.js" defer></script>
|
||||
{{end}}
|
||||
{{with .Version}}<script>var goVersion = {{printf "%q" .}};</script>{{end}}
|
||||
<script src="/lib/godoc/godocs.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id='lowframe' style="position: fixed; bottom: 0; left: 0; height: 0; width: 100%; border-top: thin solid grey; background-color: white; overflow: auto;">
|
||||
...
|
||||
</div><!-- #lowframe -->
|
||||
|
||||
<div id="topbar"{{if .Title}} class="wide"{{end}}><div class="container">
|
||||
<div class="top-heading" id="heading-wide"><a href="/pkg/">Go Documentation Server</a></div>
|
||||
<div class="top-heading" id="heading-narrow"><a href="/pkg/">GoDoc</a></div>
|
||||
<a href="#" id="menu-button"><span id="menu-button-arrow">▽</span></a>
|
||||
<form method="GET" action="/search">
|
||||
<div id="menu">
|
||||
{{if (and .Playground .Title)}}
|
||||
<a id="playgroundButton" href="https://play.golang.org/" title="Show Go Playground">Play</a>
|
||||
{{end}}
|
||||
<span class="search-box"><input type="search" id="search" name="q" placeholder="Search" aria-label="Search" required><button type="submit"><span><!-- magnifying glass: --><svg width="24" height="24" viewBox="0 0 24 24"><title>submit search</title><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/><path d="M0 0h24v24H0z" fill="none"/></svg></span></button></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div></div>
|
||||
|
||||
{{if .Playground}}
|
||||
<div id="playground" class="play">
|
||||
<div class="input"><textarea class="code" spellcheck="false">package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, 世界")
|
||||
}</textarea></div>
|
||||
<div class="output"></div>
|
||||
<div class="buttons">
|
||||
<a class="run" title="Run this code [shift-enter]">Run</a>
|
||||
<a class="fmt" title="Format this code">Format</a>
|
||||
<a class="share" title="Share this code">Share</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div id="page"{{if .Title}} class="wide"{{end}}>
|
||||
<div class="container">
|
||||
|
||||
{{if or .Title .SrcPath}}
|
||||
<h1>
|
||||
{{html .Title}}
|
||||
{{html .SrcPath | srcBreadcrumb}}
|
||||
</h1>
|
||||
{{end}}
|
||||
|
||||
{{with .Subtitle}}
|
||||
<h2>{{html .}}</h2>
|
||||
{{end}}
|
||||
|
||||
{{with .SrcPath}}
|
||||
<h2>
|
||||
Documentation: {{html . | srcToPkgLink}}
|
||||
</h2>
|
||||
{{end}}
|
||||
|
||||
{{/* The Table of Contents is automatically inserted in this <div>.
|
||||
Do not delete this <div>. */}}
|
||||
<div id="nav"></div>
|
||||
|
||||
{{/* Body is HTML-escaped elsewhere */}}
|
||||
{{printf "%s" .Body}}
|
||||
|
||||
<div id="footer">
|
||||
Build version {{html .Version}}.<br>
|
||||
Except as <a href="https://developers.google.com/site-policies#restrictions">noted</a>,
|
||||
the content of this page is licensed under the
|
||||
Creative Commons Attribution 3.0 License,
|
||||
and code is licensed under a <a href="/LICENSE">BSD license</a>.<br>
|
||||
<a href="https://golang.org/doc/tos.html">Terms of Service</a> |
|
||||
<a href="https://www.google.com/intl/en/policies/privacy/">Privacy Policy</a>
|
||||
</div>
|
||||
|
||||
</div><!-- .container -->
|
||||
</div><!-- #page -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,688 @@
|
||||
// Copyright 2012 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.
|
||||
|
||||
/* A little code to ease navigation of these documents.
|
||||
*
|
||||
* On window load we:
|
||||
* + Generate a table of contents (generateTOC)
|
||||
* + Bind foldable sections (bindToggles)
|
||||
* + Bind links to foldable sections (bindToggleLinks)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Mobile-friendly topbar menu
|
||||
$(function() {
|
||||
var menu = $('#menu');
|
||||
var menuButton = $('#menu-button');
|
||||
var menuButtonArrow = $('#menu-button-arrow');
|
||||
menuButton.click(function(event) {
|
||||
menu.toggleClass('menu-visible');
|
||||
menuButtonArrow.toggleClass('vertical-flip');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
/* Generates a table of contents: looks for h2 and h3 elements and generates
|
||||
* links. "Decorates" the element with id=="nav" with this table of contents.
|
||||
*/
|
||||
function generateTOC() {
|
||||
if ($('#manual-nav').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For search, we send the toc precomputed from server-side.
|
||||
// TODO: Ideally, this should always be precomputed for all pages, but then
|
||||
// we need to do HTML parsing on the server-side.
|
||||
if (location.pathname === '/search') {
|
||||
return;
|
||||
}
|
||||
|
||||
var nav = $('#nav');
|
||||
if (nav.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var toc_items = [];
|
||||
$(nav)
|
||||
.nextAll('h2, h3')
|
||||
.each(function() {
|
||||
var node = this;
|
||||
if (node.id == '') node.id = 'tmp_' + toc_items.length;
|
||||
var link = $('<a/>')
|
||||
.attr('href', '#' + node.id)
|
||||
.text($(node).text());
|
||||
var item;
|
||||
if ($(node).is('h2')) {
|
||||
item = $('<dt/>');
|
||||
} else {
|
||||
// h3
|
||||
item = $('<dd class="indent"/>');
|
||||
}
|
||||
item.append(link);
|
||||
toc_items.push(item);
|
||||
});
|
||||
if (toc_items.length <= 1) {
|
||||
return;
|
||||
}
|
||||
var dl1 = $('<dl/>');
|
||||
var dl2 = $('<dl/>');
|
||||
|
||||
var split_index = toc_items.length / 2 + 1;
|
||||
if (split_index < 8) {
|
||||
split_index = toc_items.length;
|
||||
}
|
||||
for (var i = 0; i < split_index; i++) {
|
||||
dl1.append(toc_items[i]);
|
||||
}
|
||||
for (; /* keep using i */ i < toc_items.length; i++) {
|
||||
dl2.append(toc_items[i]);
|
||||
}
|
||||
|
||||
var tocTable = $('<table class="unruled"/>').appendTo(nav);
|
||||
var tocBody = $('<tbody/>').appendTo(tocTable);
|
||||
var tocRow = $('<tr/>').appendTo(tocBody);
|
||||
|
||||
// 1st column
|
||||
$('<td class="first"/>')
|
||||
.appendTo(tocRow)
|
||||
.append(dl1);
|
||||
// 2nd column
|
||||
$('<td/>')
|
||||
.appendTo(tocRow)
|
||||
.append(dl2);
|
||||
}
|
||||
|
||||
function bindToggle(el) {
|
||||
$('.toggleButton', el).click(function() {
|
||||
if ($(this).closest('.toggle, .toggleVisible')[0] != el) {
|
||||
// Only trigger the closest toggle header.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($(el).is('.toggle')) {
|
||||
$(el)
|
||||
.addClass('toggleVisible')
|
||||
.removeClass('toggle');
|
||||
} else {
|
||||
$(el)
|
||||
.addClass('toggle')
|
||||
.removeClass('toggleVisible');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindToggles(selector) {
|
||||
$(selector).each(function(i, el) {
|
||||
bindToggle(el);
|
||||
});
|
||||
}
|
||||
|
||||
function bindToggleLink(el, prefix) {
|
||||
$(el).click(function() {
|
||||
var href = $(el).attr('href');
|
||||
var i = href.indexOf('#' + prefix);
|
||||
if (i < 0) {
|
||||
return;
|
||||
}
|
||||
var id = '#' + prefix + href.slice(i + 1 + prefix.length);
|
||||
if ($(id).is('.toggle')) {
|
||||
$(id)
|
||||
.find('.toggleButton')
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
function bindToggleLinks(selector, prefix) {
|
||||
$(selector).each(function(i, el) {
|
||||
bindToggleLink(el, prefix);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDropdownPlayground() {
|
||||
if (!$('#page').is('.wide')) {
|
||||
return; // don't show on front page
|
||||
}
|
||||
var button = $('#playgroundButton');
|
||||
var div = $('#playground');
|
||||
var setup = false;
|
||||
button.toggle(
|
||||
function() {
|
||||
button.addClass('active');
|
||||
div.show();
|
||||
if (setup) {
|
||||
return;
|
||||
}
|
||||
setup = true;
|
||||
playground({
|
||||
codeEl: $('.code', div),
|
||||
outputEl: $('.output', div),
|
||||
runEl: $('.run', div),
|
||||
fmtEl: $('.fmt', div),
|
||||
shareEl: $('.share', div),
|
||||
shareRedirect: '//play.golang.org/p/',
|
||||
});
|
||||
},
|
||||
function() {
|
||||
button.removeClass('active');
|
||||
div.hide();
|
||||
}
|
||||
);
|
||||
$('#menu').css('min-width', '+=60');
|
||||
|
||||
// Hide inline playground if we click somewhere on the page.
|
||||
// This is needed in mobile devices, where the "Play" button
|
||||
// is not clickable once the playground opens up.
|
||||
$('#page').click(function() {
|
||||
if (button.hasClass('active')) {
|
||||
button.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupInlinePlayground() {
|
||||
'use strict';
|
||||
// Set up playground when each element is toggled.
|
||||
$('div.play').each(function(i, el) {
|
||||
// Set up playground for this example.
|
||||
var setup = function() {
|
||||
var code = $('.code', el);
|
||||
playground({
|
||||
codeEl: code,
|
||||
outputEl: $('.output', el),
|
||||
runEl: $('.run', el),
|
||||
fmtEl: $('.fmt', el),
|
||||
shareEl: $('.share', el),
|
||||
shareRedirect: '//play.golang.org/p/',
|
||||
});
|
||||
|
||||
// Make the code textarea resize to fit content.
|
||||
var resize = function() {
|
||||
code.height(0);
|
||||
var h = code[0].scrollHeight;
|
||||
code.height(h + 20); // minimize bouncing.
|
||||
code.closest('.input').height(h);
|
||||
};
|
||||
code.on('keydown', resize);
|
||||
code.on('keyup', resize);
|
||||
code.keyup(); // resize now.
|
||||
};
|
||||
|
||||
// If example already visible, set up playground now.
|
||||
if ($(el).is(':visible')) {
|
||||
setup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, set up playground when example is expanded.
|
||||
var built = false;
|
||||
$(el)
|
||||
.closest('.toggle')
|
||||
.click(function() {
|
||||
// Only set up once.
|
||||
if (!built) {
|
||||
setup();
|
||||
built = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// fixFocus tries to put focus to div#page so that keyboard navigation works.
|
||||
function fixFocus() {
|
||||
var page = $('div#page');
|
||||
var topbar = $('div#topbar');
|
||||
page.css('outline', 0); // disable outline when focused
|
||||
page.attr('tabindex', -1); // and set tabindex so that it is focusable
|
||||
$(window)
|
||||
.resize(function(evt) {
|
||||
// only focus page when the topbar is at fixed position (that is, it's in
|
||||
// front of page, and keyboard event will go to the former by default.)
|
||||
// by focusing page, keyboard event will go to page so that up/down arrow,
|
||||
// space, etc. will work as expected.
|
||||
if (topbar.css('position') == 'fixed') page.focus();
|
||||
})
|
||||
.resize();
|
||||
}
|
||||
|
||||
function toggleHash() {
|
||||
var id = window.location.hash.substring(1);
|
||||
// Open all of the toggles for a particular hash.
|
||||
var els = $(
|
||||
document.getElementById(id),
|
||||
$('a[name]').filter(function() {
|
||||
return $(this).attr('name') == id;
|
||||
})
|
||||
);
|
||||
|
||||
while (els.length) {
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = $(els[i]);
|
||||
if (el.is('.toggle')) {
|
||||
el.find('.toggleButton')
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
}
|
||||
els = el.parent();
|
||||
}
|
||||
}
|
||||
|
||||
function personalizeInstallInstructions() {
|
||||
var prefix = '?download=';
|
||||
var s = window.location.search;
|
||||
if (s.indexOf(prefix) != 0) {
|
||||
// No 'download' query string; detect "test" instructions from User Agent.
|
||||
if (navigator.platform.indexOf('Win') != -1) {
|
||||
$('.testUnix').hide();
|
||||
$('.testWindows').show();
|
||||
} else {
|
||||
$('.testUnix').show();
|
||||
$('.testWindows').hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var filename = s.substr(prefix.length);
|
||||
var filenameRE = /^go1\.\d+(\.\d+)?([a-z0-9]+)?\.([a-z0-9]+)(-[a-z0-9]+)?(-osx10\.[68])?\.([a-z.]+)$/;
|
||||
var m = filenameRE.exec(filename);
|
||||
if (!m) {
|
||||
// Can't interpret file name; bail.
|
||||
return;
|
||||
}
|
||||
$('.downloadFilename').text(filename);
|
||||
$('.hideFromDownload').hide();
|
||||
|
||||
var os = m[3];
|
||||
var ext = m[6];
|
||||
if (ext != 'tar.gz') {
|
||||
$('#tarballInstructions').hide();
|
||||
}
|
||||
if (os != 'darwin' || ext != 'pkg') {
|
||||
$('#darwinPackageInstructions').hide();
|
||||
}
|
||||
if (os != 'windows') {
|
||||
$('#windowsInstructions').hide();
|
||||
$('.testUnix').show();
|
||||
$('.testWindows').hide();
|
||||
} else {
|
||||
if (ext != 'msi') {
|
||||
$('#windowsInstallerInstructions').hide();
|
||||
}
|
||||
if (ext != 'zip') {
|
||||
$('#windowsZipInstructions').hide();
|
||||
}
|
||||
$('.testUnix').hide();
|
||||
$('.testWindows').show();
|
||||
}
|
||||
|
||||
var download = 'https://dl.google.com/go/' + filename;
|
||||
|
||||
var message = $(
|
||||
'<p class="downloading">' +
|
||||
'Your download should begin shortly. ' +
|
||||
'If it does not, click <a>this link</a>.</p>'
|
||||
);
|
||||
message.find('a').attr('href', download);
|
||||
message.insertAfter('#nav');
|
||||
|
||||
window.location = download;
|
||||
}
|
||||
|
||||
function updateVersionTags() {
|
||||
var v = window.goVersion;
|
||||
if (/^go[0-9.]+$/.test(v)) {
|
||||
$('.versionTag')
|
||||
.empty()
|
||||
.text(v);
|
||||
$('.whereTag').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function addPermalinks() {
|
||||
function addPermalink(source, parent) {
|
||||
var id = source.attr('id');
|
||||
if (id == '' || id.indexOf('tmp_') === 0) {
|
||||
// Auto-generated permalink.
|
||||
return;
|
||||
}
|
||||
if (parent.find('> .permalink').length) {
|
||||
// Already attached.
|
||||
return;
|
||||
}
|
||||
parent
|
||||
.append(' ')
|
||||
.append($("<a class='permalink'>¶</a>").attr('href', '#' + id));
|
||||
}
|
||||
|
||||
$('#page .container')
|
||||
.find('h2[id], h3[id]')
|
||||
.each(function() {
|
||||
var el = $(this);
|
||||
addPermalink(el, el);
|
||||
});
|
||||
|
||||
$('#page .container')
|
||||
.find('dl[id]')
|
||||
.each(function() {
|
||||
var el = $(this);
|
||||
// Add the anchor to the "dt" element.
|
||||
addPermalink(el, el.find('> dt').first());
|
||||
});
|
||||
}
|
||||
|
||||
$('.js-expandAll').click(function() {
|
||||
if ($(this).hasClass('collapsed')) {
|
||||
toggleExamples('toggle');
|
||||
$(this).text('(Collapse All)');
|
||||
} else {
|
||||
toggleExamples('toggleVisible');
|
||||
$(this).text('(Expand All)');
|
||||
}
|
||||
$(this).toggleClass('collapsed');
|
||||
});
|
||||
|
||||
function toggleExamples(className) {
|
||||
// We need to explicitly iterate through divs starting with "example_"
|
||||
// to avoid toggling Overview and Index collapsibles.
|
||||
$("[id^='example_']").each(function() {
|
||||
// Check for state and click it only if required.
|
||||
if ($(this).hasClass(className)) {
|
||||
$(this)
|
||||
.find('.toggleButton')
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
generateTOC();
|
||||
addPermalinks();
|
||||
bindToggles('.toggle');
|
||||
bindToggles('.toggleVisible');
|
||||
bindToggleLinks('.exampleLink', 'example_');
|
||||
bindToggleLinks('.overviewLink', '');
|
||||
bindToggleLinks('.examplesLink', '');
|
||||
bindToggleLinks('.indexLink', '');
|
||||
setupDropdownPlayground();
|
||||
setupInlinePlayground();
|
||||
fixFocus();
|
||||
setupTypeInfo();
|
||||
setupCallgraphs();
|
||||
toggleHash();
|
||||
personalizeInstallInstructions();
|
||||
updateVersionTags();
|
||||
|
||||
// godoc.html defines window.initFuncs in the <head> tag, and root.html and
|
||||
// codewalk.js push their on-page-ready functions to the list.
|
||||
// We execute those functions here, to avoid loading jQuery until the page
|
||||
// content is loaded.
|
||||
for (var i = 0; i < window.initFuncs.length; i++) window.initFuncs[i]();
|
||||
});
|
||||
|
||||
// -- analysis ---------------------------------------------------------
|
||||
|
||||
// escapeHTML returns HTML for s, with metacharacters quoted.
|
||||
// It is safe for use in both elements and attributes
|
||||
// (unlike the "set innerText, read innerHTML" trick).
|
||||
function escapeHTML(s) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/\'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// makeAnchor returns HTML for an <a> element, given an anchorJSON object.
|
||||
function makeAnchor(json) {
|
||||
var html = escapeHTML(json.Text);
|
||||
if (json.Href != '') {
|
||||
html = "<a href='" + escapeHTML(json.Href) + "'>" + html + '</a>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function showLowFrame(html) {
|
||||
var lowframe = document.getElementById('lowframe');
|
||||
lowframe.style.height = '200px';
|
||||
lowframe.innerHTML =
|
||||
"<p style='text-align: left;'>" +
|
||||
html +
|
||||
'</p>\n' +
|
||||
"<div onclick='hideLowFrame()' style='position: absolute; top: 0; right: 0; cursor: pointer;'>✘</div>";
|
||||
}
|
||||
|
||||
document.hideLowFrame = function() {
|
||||
var lowframe = document.getElementById('lowframe');
|
||||
lowframe.style.height = '0px';
|
||||
};
|
||||
|
||||
// onClickCallers is the onclick action for the 'func' tokens of a
|
||||
// function declaration.
|
||||
document.onClickCallers = function(index) {
|
||||
var data = document.ANALYSIS_DATA[index];
|
||||
if (data.Callers.length == 1 && data.Callers[0].Sites.length == 1) {
|
||||
document.location = data.Callers[0].Sites[0].Href; // jump to sole caller
|
||||
return;
|
||||
}
|
||||
|
||||
var html =
|
||||
'Callers of <code>' + escapeHTML(data.Callee) + '</code>:<br/>\n';
|
||||
for (var i = 0; i < data.Callers.length; i++) {
|
||||
var caller = data.Callers[i];
|
||||
html += '<code>' + escapeHTML(caller.Func) + '</code>';
|
||||
var sites = caller.Sites;
|
||||
if (sites != null && sites.length > 0) {
|
||||
html += ' at line ';
|
||||
for (var j = 0; j < sites.length; j++) {
|
||||
if (j > 0) {
|
||||
html += ', ';
|
||||
}
|
||||
html += '<code>' + makeAnchor(sites[j]) + '</code>';
|
||||
}
|
||||
}
|
||||
html += '<br/>\n';
|
||||
}
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
// onClickCallees is the onclick action for the '(' token of a function call.
|
||||
document.onClickCallees = function(index) {
|
||||
var data = document.ANALYSIS_DATA[index];
|
||||
if (data.Callees.length == 1) {
|
||||
document.location = data.Callees[0].Href; // jump to sole callee
|
||||
return;
|
||||
}
|
||||
|
||||
var html = 'Callees of this ' + escapeHTML(data.Descr) + ':<br/>\n';
|
||||
for (var i = 0; i < data.Callees.length; i++) {
|
||||
html += '<code>' + makeAnchor(data.Callees[i]) + '</code><br/>\n';
|
||||
}
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
// onClickTypeInfo is the onclick action for identifiers declaring a named type.
|
||||
document.onClickTypeInfo = function(index) {
|
||||
var data = document.ANALYSIS_DATA[index];
|
||||
var html =
|
||||
'Type <code>' +
|
||||
data.Name +
|
||||
'</code>: ' +
|
||||
' <small>(size=' +
|
||||
data.Size +
|
||||
', align=' +
|
||||
data.Align +
|
||||
')</small><br/>\n';
|
||||
html += implementsHTML(data);
|
||||
html += methodsetHTML(data);
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
// implementsHTML returns HTML for the implements relation of the
|
||||
// specified TypeInfoJSON value.
|
||||
function implementsHTML(info) {
|
||||
var html = '';
|
||||
if (info.ImplGroups != null) {
|
||||
for (var i = 0; i < info.ImplGroups.length; i++) {
|
||||
var group = info.ImplGroups[i];
|
||||
var x = '<code>' + escapeHTML(group.Descr) + '</code> ';
|
||||
for (var j = 0; j < group.Facts.length; j++) {
|
||||
var fact = group.Facts[j];
|
||||
var y = '<code>' + makeAnchor(fact.Other) + '</code>';
|
||||
if (fact.ByKind != null) {
|
||||
html += escapeHTML(fact.ByKind) + ' type ' + y + ' implements ' + x;
|
||||
} else {
|
||||
html += x + ' implements ' + y;
|
||||
}
|
||||
html += '<br/>\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// methodsetHTML returns HTML for the methodset of the specified
|
||||
// TypeInfoJSON value.
|
||||
function methodsetHTML(info) {
|
||||
var html = '';
|
||||
if (info.Methods != null) {
|
||||
for (var i = 0; i < info.Methods.length; i++) {
|
||||
html += '<code>' + makeAnchor(info.Methods[i]) + '</code><br/>\n';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// onClickComm is the onclick action for channel "make" and "<-"
|
||||
// send/receive tokens.
|
||||
document.onClickComm = function(index) {
|
||||
var ops = document.ANALYSIS_DATA[index].Ops;
|
||||
if (ops.length == 1) {
|
||||
document.location = ops[0].Op.Href; // jump to sole element
|
||||
return;
|
||||
}
|
||||
|
||||
var html = 'Operations on this channel:<br/>\n';
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
html +=
|
||||
makeAnchor(ops[i].Op) +
|
||||
' by <code>' +
|
||||
escapeHTML(ops[i].Fn) +
|
||||
'</code><br/>\n';
|
||||
}
|
||||
if (ops.length == 0) {
|
||||
html += '(none)<br/>\n';
|
||||
}
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
$(window).load(function() {
|
||||
// Scroll window so that first selection is visible.
|
||||
// (This means we don't need to emit id='L%d' spans for each line.)
|
||||
// TODO(adonovan): ideally, scroll it so that it's under the pointer,
|
||||
// but I don't know how to get the pointer y coordinate.
|
||||
var elts = document.getElementsByClassName('selection');
|
||||
if (elts.length > 0) {
|
||||
elts[0].scrollIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
// setupTypeInfo populates the "Implements" and "Method set" toggle for
|
||||
// each type in the package doc.
|
||||
function setupTypeInfo() {
|
||||
for (var i in document.ANALYSIS_DATA) {
|
||||
var data = document.ANALYSIS_DATA[i];
|
||||
|
||||
var el = document.getElementById('implements-' + i);
|
||||
if (el != null) {
|
||||
// el != null => data is TypeInfoJSON.
|
||||
if (data.ImplGroups != null) {
|
||||
el.innerHTML = implementsHTML(data);
|
||||
el.parentNode.parentNode.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
var el = document.getElementById('methodset-' + i);
|
||||
if (el != null) {
|
||||
// el != null => data is TypeInfoJSON.
|
||||
if (data.Methods != null) {
|
||||
el.innerHTML = methodsetHTML(data);
|
||||
el.parentNode.parentNode.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupCallgraphs() {
|
||||
if (document.CALLGRAPH == null) {
|
||||
return;
|
||||
}
|
||||
document.getElementById('pkg-callgraph').style.display = 'block';
|
||||
|
||||
var treeviews = document.getElementsByClassName('treeview');
|
||||
for (var i = 0; i < treeviews.length; i++) {
|
||||
var tree = treeviews[i];
|
||||
if (tree.id == null || tree.id.indexOf('callgraph-') != 0) {
|
||||
continue;
|
||||
}
|
||||
var id = tree.id.substring('callgraph-'.length);
|
||||
$(tree).treeview({ collapsed: true, animated: 'fast' });
|
||||
document.cgAddChildren(tree, tree, [id]);
|
||||
tree.parentNode.parentNode.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
document.cgAddChildren = function(tree, ul, indices) {
|
||||
if (indices != null) {
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
var li = cgAddChild(tree, ul, document.CALLGRAPH[indices[i]]);
|
||||
if (i == indices.length - 1) {
|
||||
$(li).addClass('last');
|
||||
}
|
||||
}
|
||||
}
|
||||
$(tree).treeview({ animated: 'fast', add: ul });
|
||||
};
|
||||
|
||||
// cgAddChild adds an <li> element for document.CALLGRAPH node cgn to
|
||||
// the parent <ul> element ul. tree is the tree's root <ul> element.
|
||||
function cgAddChild(tree, ul, cgn) {
|
||||
var li = document.createElement('li');
|
||||
ul.appendChild(li);
|
||||
li.className = 'closed';
|
||||
|
||||
var code = document.createElement('code');
|
||||
|
||||
if (cgn.Callees != null) {
|
||||
$(li).addClass('expandable');
|
||||
|
||||
// Event handlers and innerHTML updates don't play nicely together,
|
||||
// hence all this explicit DOM manipulation.
|
||||
var hitarea = document.createElement('div');
|
||||
hitarea.className = 'hitarea expandable-hitarea';
|
||||
li.appendChild(hitarea);
|
||||
|
||||
li.appendChild(code);
|
||||
|
||||
var childUL = document.createElement('ul');
|
||||
li.appendChild(childUL);
|
||||
childUL.setAttribute('style', 'display: none;');
|
||||
|
||||
var onClick = function() {
|
||||
document.cgAddChildren(tree, childUL, cgn.Callees);
|
||||
hitarea.removeEventListener('click', onClick);
|
||||
};
|
||||
hitarea.addEventListener('click', onClick);
|
||||
} else {
|
||||
li.appendChild(code);
|
||||
}
|
||||
code.innerHTML += ' ' + makeAnchor(cgn.Func);
|
||||
return li;
|
||||
}
|
||||
})();
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 58 B |
|
After Width: | Height: | Size: 61 B |
|
After Width: | Height: | Size: 1010 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 848 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 1010 B |
|
After Width: | Height: | Size: 394 B |
@@ -0,0 +1,9 @@
|
||||
<div class="toggle" style="display: none">
|
||||
<div class="collapsed">
|
||||
<p class="exampleHeading toggleButton">▹ <span class="text">Implements</span></p>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<p class="exampleHeading toggleButton">▾ <span class="text">Implements</span></p>
|
||||
<div style="margin-left: 1in" id='implements-{{.Index}}'>...</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
/* https://github.com/jzaefferer/jquery-treeview/blob/1.4.2/jquery.treeview.css */
|
||||
/* License: MIT. */
|
||||
.treeview, .treeview ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.treeview ul {
|
||||
background-color: white;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.treeview .hitarea {
|
||||
background: url(images/treeview-default.gif) -64px -25px no-repeat;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: -16px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* fix for IE6 */
|
||||
* html .hitarea {
|
||||
display: inline;
|
||||
float:none;
|
||||
}
|
||||
|
||||
.treeview li {
|
||||
margin: 0;
|
||||
padding: 3px 0pt 3px 16px;
|
||||
}
|
||||
|
||||
.treeview a.selected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#treecontrol { margin: 1em 0; display: none; }
|
||||
|
||||
.treeview .hover { color: red; cursor: pointer; }
|
||||
|
||||
.treeview li { background: url(images/treeview-default-line.gif) 0 0 no-repeat; }
|
||||
.treeview li.collapsable, .treeview li.expandable { background-position: 0 -176px; }
|
||||
|
||||
.treeview .expandable-hitarea { background-position: -80px -3px; }
|
||||
|
||||
.treeview li.last { background-position: 0 -1766px }
|
||||
.treeview li.lastCollapsable, .treeview li.lastExpandable { background-image: url(images/treeview-default.gif); }
|
||||
.treeview li.lastCollapsable { background-position: 0 -111px }
|
||||
.treeview li.lastExpandable { background-position: -32px -67px }
|
||||
|
||||
.treeview div.lastCollapsable-hitarea, .treeview div.lastExpandable-hitarea { background-position: 0; }
|
||||
|
||||
.treeview-red li { background-image: url(images/treeview-red-line.gif); }
|
||||
.treeview-red .hitarea, .treeview-red li.lastCollapsable, .treeview-red li.lastExpandable { background-image: url(images/treeview-red.gif); }
|
||||
|
||||
.treeview-black li { background-image: url(images/treeview-black-line.gif); }
|
||||
.treeview-black .hitarea, .treeview-black li.lastCollapsable, .treeview-black li.lastExpandable { background-image: url(images/treeview-black.gif); }
|
||||
|
||||
.treeview-gray li { background-image: url(images/treeview-gray-line.gif); }
|
||||
.treeview-gray .hitarea, .treeview-gray li.lastCollapsable, .treeview-gray li.lastExpandable { background-image: url(images/treeview-gray.gif); }
|
||||
|
||||
.treeview-famfamfam li { background-image: url(images/treeview-famfamfam-line.gif); }
|
||||
.treeview-famfamfam .hitarea, .treeview-famfamfam li.lastCollapsable, .treeview-famfamfam li.lastExpandable { background-image: url(images/treeview-famfamfam.gif); }
|
||||
|
||||
.treeview .placeholder {
|
||||
background: url(images/ajax-loader.gif) 0 0 no-repeat;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filetree li { padding: 3px 0 2px 16px; }
|
||||
.filetree span.folder, .filetree span.file { padding: 1px 0 1px 16px; display: block; }
|
||||
.filetree span.folder { background: url(images/folder.gif) 0 0 no-repeat; }
|
||||
.filetree li.expandable span.folder { background: url(images/folder-closed.gif) 0 0 no-repeat; }
|
||||
.filetree span.file { background: url(images/file.gif) 0 0 no-repeat; }
|
||||
@@ -0,0 +1,39 @@
|
||||
/* https://github.com/jzaefferer/jquery-treeview/blob/1.4.2/jquery.treeview.edit.js */
|
||||
/* License: MIT. */
|
||||
(function($) {
|
||||
var CLASSES = $.treeview.classes;
|
||||
var proxied = $.fn.treeview;
|
||||
$.fn.treeview = function(settings) {
|
||||
settings = $.extend({}, settings);
|
||||
if (settings.add) {
|
||||
return this.trigger("add", [settings.add]);
|
||||
}
|
||||
if (settings.remove) {
|
||||
return this.trigger("remove", [settings.remove]);
|
||||
}
|
||||
return proxied.apply(this, arguments).bind("add", function(event, branches) {
|
||||
$(branches).prev()
|
||||
.removeClass(CLASSES.last)
|
||||
.removeClass(CLASSES.lastCollapsable)
|
||||
.removeClass(CLASSES.lastExpandable)
|
||||
.find(">.hitarea")
|
||||
.removeClass(CLASSES.lastCollapsableHitarea)
|
||||
.removeClass(CLASSES.lastExpandableHitarea);
|
||||
$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings, $(this).data("toggler"));
|
||||
}).bind("remove", function(event, branches) {
|
||||
var prev = $(branches).prev();
|
||||
var parent = $(branches).parent();
|
||||
$(branches).remove();
|
||||
prev.filter(":last-child").addClass(CLASSES.last)
|
||||
.filter("." + CLASSES.expandable).replaceClass(CLASSES.last, CLASSES.lastExpandable).end()
|
||||
.find(">.hitarea").replaceClass(CLASSES.expandableHitarea, CLASSES.lastExpandableHitarea).end()
|
||||
.filter("." + CLASSES.collapsable).replaceClass(CLASSES.last, CLASSES.lastCollapsable).end()
|
||||
.find(">.hitarea").replaceClass(CLASSES.collapsableHitarea, CLASSES.lastCollapsableHitarea);
|
||||
if (parent.is(":not(:has(>))") && parent[0] != this) {
|
||||
parent.parent().removeClass(CLASSES.collapsable).removeClass(CLASSES.expandable)
|
||||
parent.siblings(".hitarea").andSelf().remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Treeview 1.4.2 - jQuery plugin to hide and show branches of a tree
|
||||
*
|
||||
* http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
|
||||
*
|
||||
* Copyright Jörn Zaefferer
|
||||
* Released under the MIT license:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
|
||||
;(function($) {
|
||||
|
||||
// TODO rewrite as a widget, removing all the extra plugins
|
||||
$.extend($.fn, {
|
||||
swapClass: function(c1, c2) {
|
||||
var c1Elements = this.filter('.' + c1);
|
||||
this.filter('.' + c2).removeClass(c2).addClass(c1);
|
||||
c1Elements.removeClass(c1).addClass(c2);
|
||||
return this;
|
||||
},
|
||||
replaceClass: function(c1, c2) {
|
||||
return this.filter('.' + c1).removeClass(c1).addClass(c2).end();
|
||||
},
|
||||
hoverClass: function(className) {
|
||||
className = className || "hover";
|
||||
return this.hover(function() {
|
||||
$(this).addClass(className);
|
||||
}, function() {
|
||||
$(this).removeClass(className);
|
||||
});
|
||||
},
|
||||
heightToggle: function(animated, callback) {
|
||||
animated ?
|
||||
this.animate({ height: "toggle" }, animated, callback) :
|
||||
this.each(function(){
|
||||
jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]();
|
||||
if(callback)
|
||||
callback.apply(this, arguments);
|
||||
});
|
||||
},
|
||||
heightHide: function(animated, callback) {
|
||||
if (animated) {
|
||||
this.animate({ height: "hide" }, animated, callback);
|
||||
} else {
|
||||
this.hide();
|
||||
if (callback)
|
||||
this.each(callback);
|
||||
}
|
||||
},
|
||||
prepareBranches: function(settings) {
|
||||
if (!settings.prerendered) {
|
||||
// mark last tree items
|
||||
this.filter(":last-child:not(ul)").addClass(CLASSES.last);
|
||||
// collapse whole tree, or only those marked as closed, anyway except those marked as open
|
||||
this.filter((settings.collapsed ? "" : "." + CLASSES.closed) + ":not(." + CLASSES.open + ")").find(">ul").hide();
|
||||
}
|
||||
// return all items with sublists
|
||||
return this.filter(":has(>ul)");
|
||||
},
|
||||
applyClasses: function(settings, toggler) {
|
||||
// TODO use event delegation
|
||||
this.filter(":has(>ul):not(:has(>a))").find(">span").unbind("click.treeview").bind("click.treeview", function(event) {
|
||||
// don't handle click events on children, eg. checkboxes
|
||||
if ( this == event.target )
|
||||
toggler.apply($(this).next());
|
||||
}).add( $("a", this) ).hoverClass();
|
||||
|
||||
if (!settings.prerendered) {
|
||||
// handle closed ones first
|
||||
this.filter(":has(>ul:hidden)")
|
||||
.addClass(CLASSES.expandable)
|
||||
.replaceClass(CLASSES.last, CLASSES.lastExpandable);
|
||||
|
||||
// handle open ones
|
||||
this.not(":has(>ul:hidden)")
|
||||
.addClass(CLASSES.collapsable)
|
||||
.replaceClass(CLASSES.last, CLASSES.lastCollapsable);
|
||||
|
||||
// create hitarea if not present
|
||||
var hitarea = this.find("div." + CLASSES.hitarea);
|
||||
if (!hitarea.length)
|
||||
hitarea = this.prepend("<div class=\"" + CLASSES.hitarea + "\"/>").find("div." + CLASSES.hitarea);
|
||||
hitarea.removeClass().addClass(CLASSES.hitarea).each(function() {
|
||||
var classes = "";
|
||||
$.each($(this).parent().attr("class").split(" "), function() {
|
||||
classes += this + "-hitarea ";
|
||||
});
|
||||
$(this).addClass( classes );
|
||||
})
|
||||
}
|
||||
|
||||
// apply event to hitarea
|
||||
this.find("div." + CLASSES.hitarea).click( toggler );
|
||||
},
|
||||
treeview: function(settings) {
|
||||
|
||||
settings = $.extend({
|
||||
cookieId: "treeview"
|
||||
}, settings);
|
||||
|
||||
if ( settings.toggle ) {
|
||||
var callback = settings.toggle;
|
||||
settings.toggle = function() {
|
||||
return callback.apply($(this).parent()[0], arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// factory for treecontroller
|
||||
function treeController(tree, control) {
|
||||
// factory for click handlers
|
||||
function handler(filter) {
|
||||
return function() {
|
||||
// reuse toggle event handler, applying the elements to toggle
|
||||
// start searching for all hitareas
|
||||
toggler.apply( $("div." + CLASSES.hitarea, tree).filter(function() {
|
||||
// for plain toggle, no filter is provided, otherwise we need to check the parent element
|
||||
return filter ? $(this).parent("." + filter).length : true;
|
||||
}) );
|
||||
return false;
|
||||
};
|
||||
}
|
||||
// click on first element to collapse tree
|
||||
$("a:eq(0)", control).click( handler(CLASSES.collapsable) );
|
||||
// click on second to expand tree
|
||||
$("a:eq(1)", control).click( handler(CLASSES.expandable) );
|
||||
// click on third to toggle tree
|
||||
$("a:eq(2)", control).click( handler() );
|
||||
}
|
||||
|
||||
// handle toggle event
|
||||
function toggler() {
|
||||
$(this)
|
||||
.parent()
|
||||
// swap classes for hitarea
|
||||
.find(">.hitarea")
|
||||
.swapClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
|
||||
.swapClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea )
|
||||
.end()
|
||||
// swap classes for parent li
|
||||
.swapClass( CLASSES.collapsable, CLASSES.expandable )
|
||||
.swapClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
|
||||
// find child lists
|
||||
.find( ">ul" )
|
||||
// toggle them
|
||||
.heightToggle( settings.animated, settings.toggle );
|
||||
if ( settings.unique ) {
|
||||
$(this).parent()
|
||||
.siblings()
|
||||
// swap classes for hitarea
|
||||
.find(">.hitarea")
|
||||
.replaceClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
|
||||
.replaceClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea )
|
||||
.end()
|
||||
.replaceClass( CLASSES.collapsable, CLASSES.expandable )
|
||||
.replaceClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
|
||||
.find( ">ul" )
|
||||
.heightHide( settings.animated, settings.toggle );
|
||||
}
|
||||
}
|
||||
this.data("toggler", toggler);
|
||||
|
||||
function serialize() {
|
||||
function binary(arg) {
|
||||
return arg ? 1 : 0;
|
||||
}
|
||||
var data = [];
|
||||
branches.each(function(i, e) {
|
||||
data[i] = $(e).is(":has(>ul:visible)") ? 1 : 0;
|
||||
});
|
||||
$.cookie(settings.cookieId, data.join(""), settings.cookieOptions );
|
||||
}
|
||||
|
||||
function deserialize() {
|
||||
var stored = $.cookie(settings.cookieId);
|
||||
if ( stored ) {
|
||||
var data = stored.split("");
|
||||
branches.each(function(i, e) {
|
||||
$(e).find(">ul")[ parseInt(data[i]) ? "show" : "hide" ]();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// add treeview class to activate styles
|
||||
this.addClass("treeview");
|
||||
|
||||
// prepare branches and find all tree items with child lists
|
||||
var branches = this.find("li").prepareBranches(settings);
|
||||
|
||||
switch(settings.persist) {
|
||||
case "cookie":
|
||||
var toggleCallback = settings.toggle;
|
||||
settings.toggle = function() {
|
||||
serialize();
|
||||
if (toggleCallback) {
|
||||
toggleCallback.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
deserialize();
|
||||
break;
|
||||
case "location":
|
||||
var current = this.find("a").filter(function() {
|
||||
return location.href.toLowerCase().indexOf(this.href.toLowerCase()) == 0;
|
||||
});
|
||||
if ( current.length ) {
|
||||
// TODO update the open/closed classes
|
||||
var items = current.addClass("selected").parents("ul, li").add( current.next() ).show();
|
||||
if (settings.prerendered) {
|
||||
// if prerendered is on, replicate the basic class swapping
|
||||
items.filter("li")
|
||||
.swapClass( CLASSES.collapsable, CLASSES.expandable )
|
||||
.swapClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
|
||||
.find(">.hitarea")
|
||||
.swapClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
|
||||
.swapClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
branches.applyClasses(settings, toggler);
|
||||
|
||||
// if control option is set, create the treecontroller and show it
|
||||
if ( settings.control ) {
|
||||
treeController(this, settings.control);
|
||||
$(settings.control).show();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// classes used by the plugin
|
||||
// need to be styled via external stylesheet, see first example
|
||||
$.treeview = {};
|
||||
var CLASSES = ($.treeview.classes = {
|
||||
open: "open",
|
||||
closed: "closed",
|
||||
expandable: "expandable",
|
||||
expandableHitarea: "expandable-hitarea",
|
||||
lastExpandableHitarea: "lastExpandable-hitarea",
|
||||
collapsable: "collapsable",
|
||||
collapsableHitarea: "collapsable-hitarea",
|
||||
lastCollapsableHitarea: "lastCollapsable-hitarea",
|
||||
lastCollapsable: "lastCollapsable",
|
||||
lastExpandable: "lastExpandable",
|
||||
last: "last",
|
||||
hitarea: "hitarea"
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// Command makestatic writes the generated file buffer to "static.go".
|
||||
// It is intended to be invoked via "go generate" (directive in "gen.go").
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/godoc/static"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := makestatic(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func makestatic() error {
|
||||
buf, err := static.Generate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while generating static.go: %v\n", err)
|
||||
}
|
||||
err = os.WriteFile("static.go", buf, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing static.go: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="toggle" style="display: none">
|
||||
<div class="collapsed">
|
||||
<p class="exampleHeading toggleButton">▹ <span class="text">Method set</span></p>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<p class="exampleHeading toggleButton">▾ <span class="text">Method set</span></p>
|
||||
<div style="margin-left: 1in" id='methodset-{{.Index}}'>...</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,292 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<!--
|
||||
Note: Static (i.e., not template-generated) href and id
|
||||
attributes start with "pkg-" to make it impossible for
|
||||
them to conflict with generated attributes (some of which
|
||||
correspond to Go identifiers).
|
||||
-->
|
||||
{{with .PDoc}}
|
||||
<script>
|
||||
document.ANALYSIS_DATA = {{$.AnalysisData}};
|
||||
document.CALLGRAPH = {{$.CallGraph}};
|
||||
</script>
|
||||
|
||||
{{if $.IsMain}}
|
||||
{{/* command documentation */}}
|
||||
{{comment_html $ .Doc}}
|
||||
{{else}}
|
||||
{{/* package documentation */}}
|
||||
<div id="short-nav">
|
||||
<dl>
|
||||
<dd><code>import "{{html .ImportPath}}"</code></dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dd><a href="#pkg-overview" class="overviewLink">Overview</a></dd>
|
||||
<dd><a href="#pkg-index" class="indexLink">Index</a></dd>
|
||||
{{if $.Examples}}
|
||||
<dd><a href="#pkg-examples" class="examplesLink">Examples</a></dd>
|
||||
{{end}}
|
||||
{{if $.Dirs}}
|
||||
<dd><a href="#pkg-subdirectories">Subdirectories</a></dd>
|
||||
{{end}}
|
||||
</dl>
|
||||
</div>
|
||||
<!-- The package's Name is printed as title by the top-level template -->
|
||||
<div id="pkg-overview" class="toggleVisible">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show Overview section">Overview ▹</h2>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide Overview section">Overview ▾</h2>
|
||||
{{comment_html $ .Doc}}
|
||||
{{example_html $ ""}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pkg-index" class="toggleVisible">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show Index section">Index ▹</h2>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide Index section">Index ▾</h2>
|
||||
|
||||
<!-- Table of contents for API; must be named manual-nav to turn off auto nav. -->
|
||||
<div id="manual-nav">
|
||||
<dl>
|
||||
{{if .Consts}}
|
||||
<dd><a href="#pkg-constants">Constants</a></dd>
|
||||
{{end}}
|
||||
{{if .Vars}}
|
||||
<dd><a href="#pkg-variables">Variables</a></dd>
|
||||
{{end}}
|
||||
{{range .Funcs}}
|
||||
{{$name_html := html .Name}}
|
||||
<dd><a href="#{{$name_html}}">{{node_html $ .Decl false | sanitize}}</a></dd>
|
||||
{{end}}
|
||||
{{range .Types}}
|
||||
{{$tname_html := html .Name}}
|
||||
<dd><a href="#{{$tname_html}}">type {{$tname_html}}</a></dd>
|
||||
{{range .Funcs}}
|
||||
{{$name_html := html .Name}}
|
||||
<dd> <a href="#{{$name_html}}">{{node_html $ .Decl false | sanitize}}</a></dd>
|
||||
{{end}}
|
||||
{{range .Methods}}
|
||||
{{$name_html := html .Name}}
|
||||
<dd> <a href="#{{$tname_html}}.{{$name_html}}">{{node_html $ .Decl false | sanitize}}</a></dd>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if $.Notes}}
|
||||
{{range $marker, $item := $.Notes}}
|
||||
<dd><a href="#pkg-note-{{$marker}}">{{noteTitle $marker | html}}s</a></dd>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</dl>
|
||||
</div><!-- #manual-nav -->
|
||||
|
||||
{{if $.Examples}}
|
||||
<div id="pkg-examples">
|
||||
<h3>Examples</h3>
|
||||
<div class="js-expandAll expandAll collapsed">(Expand All)</div>
|
||||
<dl>
|
||||
{{range $.Examples}}
|
||||
<dd><a class="exampleLink" href="#example_{{.Name}}">{{example_name .Name}}</a></dd>
|
||||
{{end}}
|
||||
</dl>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{with .Filenames}}
|
||||
<h3>Package files</h3>
|
||||
<p>
|
||||
<span style="font-size:90%">
|
||||
{{range .}}
|
||||
<a href="{{.|srcLink|html}}">{{.|filename|html}}</a>
|
||||
{{end}}
|
||||
</span>
|
||||
</p>
|
||||
{{end}}
|
||||
</div><!-- .expanded -->
|
||||
</div><!-- #pkg-index -->
|
||||
|
||||
{{if ne $.CallGraph "null"}}
|
||||
<div id="pkg-callgraph" class="toggle" style="display: none">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show Internal Call Graph section">Internal call graph ▹</h2>
|
||||
</div> <!-- .expanded -->
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide Internal Call Graph section">Internal call graph ▾</h2>
|
||||
<p>
|
||||
In the call graph viewer below, each node
|
||||
is a function belonging to this package
|
||||
and its children are the functions it
|
||||
calls—perhaps dynamically.
|
||||
</p>
|
||||
<p>
|
||||
The root nodes are the entry points of the
|
||||
package: functions that may be called from
|
||||
outside the package.
|
||||
There may be non-exported or anonymous
|
||||
functions among them if they are called
|
||||
dynamically from another package.
|
||||
</p>
|
||||
<p>
|
||||
Click a node to visit that function's source code.
|
||||
From there you can visit its callers by
|
||||
clicking its declaring <code>func</code>
|
||||
token.
|
||||
</p>
|
||||
<p>
|
||||
Functions may be omitted if they were
|
||||
determined to be unreachable in the
|
||||
particular programs or tests that were
|
||||
analyzed.
|
||||
</p>
|
||||
<!-- Zero means show all package entry points. -->
|
||||
<ul style="margin-left: 0.5in" id="callgraph-0" class="treeview"></ul>
|
||||
</div>
|
||||
</div> <!-- #pkg-callgraph -->
|
||||
{{end}}
|
||||
|
||||
{{with .Consts}}
|
||||
<h2 id="pkg-constants">Constants</h2>
|
||||
{{range .}}
|
||||
{{comment_html $ .Doc}}
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{with .Vars}}
|
||||
<h2 id="pkg-variables">Variables</h2>
|
||||
{{range .}}
|
||||
{{comment_html $ .Doc}}
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{range .Funcs}}
|
||||
{{/* Name is a string - no need for FSet */}}
|
||||
{{$name_html := html .Name}}
|
||||
<h2 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
|
||||
<a class="permalink" href="#{{$name_html}}">¶</a>
|
||||
{{$since := since "func" "" .Name $.PDoc.ImportPath}}
|
||||
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
|
||||
</h2>
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{comment_html $ .Doc}}
|
||||
{{example_html $ .Name}}
|
||||
{{callgraph_html $ "" .Name}}
|
||||
|
||||
{{end}}
|
||||
{{range .Types}}
|
||||
{{$tname := .Name}}
|
||||
{{$tname_html := html .Name}}
|
||||
<h2 id="{{$tname_html}}">type <a href="{{posLink_url $ .Decl}}">{{$tname_html}}</a>
|
||||
<a class="permalink" href="#{{$tname_html}}">¶</a>
|
||||
{{$since := since "type" "" .Name $.PDoc.ImportPath}}
|
||||
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
|
||||
</h2>
|
||||
{{comment_html $ .Doc}}
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
|
||||
{{range .Consts}}
|
||||
{{comment_html $ .Doc}}
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{end}}
|
||||
|
||||
{{range .Vars}}
|
||||
{{comment_html $ .Doc}}
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{end}}
|
||||
|
||||
{{example_html $ $tname}}
|
||||
{{implements_html $ $tname}}
|
||||
{{methodset_html $ $tname}}
|
||||
|
||||
{{range .Funcs}}
|
||||
{{$name_html := html .Name}}
|
||||
<h3 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
|
||||
<a class="permalink" href="#{{$name_html}}">¶</a>
|
||||
{{$since := since "func" "" .Name $.PDoc.ImportPath}}
|
||||
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
|
||||
</h3>
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{comment_html $ .Doc}}
|
||||
{{example_html $ .Name}}
|
||||
{{callgraph_html $ "" .Name}}
|
||||
{{end}}
|
||||
|
||||
{{range .Methods}}
|
||||
{{$name_html := html .Name}}
|
||||
<h3 id="{{$tname_html}}.{{$name_html}}">func ({{html .Recv}}) <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
|
||||
<a class="permalink" href="#{{$tname_html}}.{{$name_html}}">¶</a>
|
||||
{{$since := since "method" .Recv .Name $.PDoc.ImportPath}}
|
||||
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
|
||||
</h3>
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{comment_html $ .Doc}}
|
||||
{{$name := printf "%s_%s" $tname .Name}}
|
||||
{{example_html $ $name}}
|
||||
{{callgraph_html $ .Recv .Name}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{with $.Notes}}
|
||||
{{range $marker, $content := .}}
|
||||
<h2 id="pkg-note-{{$marker}}">{{noteTitle $marker | html}}s</h2>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
{{range .}}
|
||||
<li><a href="{{posLink_url $ .}}" style="float: left;">☞</a> {{comment_html $ .Body}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{with .PAst}}
|
||||
{{range $filename, $ast := .}}
|
||||
<a href="{{$filename|srcLink|html}}">{{$filename|filename|html}}</a>:<pre>{{node_html $ $ast false}}</pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{with .Dirs}}
|
||||
{{/* DirList entries are numbers and strings - no need for FSet */}}
|
||||
{{if $.PDoc}}
|
||||
<h2 id="pkg-subdirectories">Subdirectories</h2>
|
||||
{{end}}
|
||||
<div class="pkg-dir">
|
||||
<table>
|
||||
<tr>
|
||||
<th class="pkg-name">Name</th>
|
||||
<th class="pkg-synopsis">Synopsis</th>
|
||||
</tr>
|
||||
|
||||
{{if not (or (eq $.Dirname "/src/cmd") $.DirFlat)}}
|
||||
<tr>
|
||||
<td colspan="2"><a href="..">..</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
{{range .List}}
|
||||
<tr>
|
||||
{{if $.DirFlat}}
|
||||
{{if .HasPkg}}
|
||||
<td class="pkg-name">
|
||||
<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Path}}</a>
|
||||
</td>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
|
||||
<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Name}}</a>
|
||||
</td>
|
||||
{{end}}
|
||||
<td class="pkg-synopsis">
|
||||
{{html .Synopsis}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,150 @@
|
||||
<!--
|
||||
Copyright 2018 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.
|
||||
-->
|
||||
<!--
|
||||
Note: Static (i.e., not template-generated) href and id
|
||||
attributes start with "pkg-" to make it impossible for
|
||||
them to conflict with generated attributes (some of which
|
||||
correspond to Go identifiers).
|
||||
-->
|
||||
{{with .PAst}}
|
||||
{{range $filename, $ast := .}}
|
||||
<a href="{{$filename|srcLink|html}}">{{$filename|filename|html}}</a>:<pre>{{node_html $ $ast false}}</pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{with .Dirs}}
|
||||
{{/* DirList entries are numbers and strings - no need for FSet */}}
|
||||
{{if $.PDoc}}
|
||||
<h2 id="pkg-subdirectories">Subdirectories</h2>
|
||||
{{end}}
|
||||
<div id="manual-nav">
|
||||
<img alt="" class="gopher" src="/lib/godoc/gopher/pkg.png"/>
|
||||
<dl>
|
||||
<dt><a href="#stdlib">Standard library</a></dt>
|
||||
{{if hasThirdParty .List }}
|
||||
<dt><a href="#thirdparty">Third party</a></dt>
|
||||
{{end}}
|
||||
<dt><a href="#other">Other packages</a></dt>
|
||||
<dd><a href="#subrepo">Sub-repositories</a></dd>
|
||||
<dd><a href="#community">Community</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div id="stdlib" class="toggleVisible">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show Standard library section">Standard library ▹</h2>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide Standard library section">Standard library ▾</h2>
|
||||
<div class="pkg-dir">
|
||||
<table>
|
||||
<tr>
|
||||
<th class="pkg-name">Name</th>
|
||||
<th class="pkg-synopsis">Synopsis</th>
|
||||
</tr>
|
||||
|
||||
{{range .List}}
|
||||
<tr>
|
||||
{{if eq .RootType "GOROOT"}}
|
||||
{{if $.DirFlat}}
|
||||
{{if .HasPkg}}
|
||||
<td class="pkg-name">
|
||||
<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Path}}</a>
|
||||
</td>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
|
||||
<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Name}}</a>
|
||||
</td>
|
||||
{{end}}
|
||||
<td class="pkg-synopsis">
|
||||
{{html .Synopsis}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div> <!-- .pkg-dir -->
|
||||
</div> <!-- .expanded -->
|
||||
</div> <!-- #stdlib .toggleVisible -->
|
||||
|
||||
{{if hasThirdParty .List }}
|
||||
<div id="thirdparty" class="toggleVisible">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show Third party section">Third party ▹</h2>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide Third party section">Third party ▾</h2>
|
||||
<div class="pkg-dir">
|
||||
<table>
|
||||
<tr>
|
||||
<th class="pkg-name">Name</th>
|
||||
<th class="pkg-synopsis">Synopsis</th>
|
||||
</tr>
|
||||
|
||||
{{range .List}}
|
||||
<tr>
|
||||
{{if eq .RootType "GOPATH"}}
|
||||
{{if $.DirFlat}}
|
||||
{{if .HasPkg}}
|
||||
<td class="pkg-name">
|
||||
<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Path}}</a>
|
||||
</td>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
|
||||
<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Name}}</a>
|
||||
</td>
|
||||
{{end}}
|
||||
<td class="pkg-synopsis">
|
||||
{{html .Synopsis}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div> <!-- .pkg-dir -->
|
||||
</div> <!-- .expanded -->
|
||||
</div> <!-- #stdlib .toggleVisible -->
|
||||
{{end}}
|
||||
|
||||
<h2 id="other">Other packages</h2>
|
||||
<h3 id="subrepo">Sub-repositories</h3>
|
||||
<p>
|
||||
These packages are part of the Go Project but outside the main Go tree.
|
||||
They are developed under looser <a href="https://golang.org/doc/go1compat">compatibility requirements</a> than the Go core.
|
||||
Install them with "<a href="/cmd/go/#hdr-Download_and_install_packages_and_dependencies">go get</a>".
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/benchmarks">benchmarks</a> — benchmarks to measure Go as it is developed.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/blog">blog</a> — <a href="//blog.golang.org">blog.golang.org</a>'s implementation.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/build">build</a> — <a href="//build.golang.org">build.golang.org</a>'s implementation.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/crypto">crypto</a> — additional cryptography packages.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/debug">debug</a> — an experimental debugger for Go.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/image">image</a> — additional imaging packages.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/mobile">mobile</a> — experimental support for Go on mobile platforms.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/net">net</a> — additional networking packages.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/perf">perf</a> — packages and tools for performance measurement, storage, and analysis.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/pkgsite">pkgsite</a> — home of the pkg.go.dev website.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/review">review</a> — a tool for working with Gerrit code reviews.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/sync">sync</a> — additional concurrency primitives.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/sys">sys</a> — packages for making system calls.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/text">text</a> — packages for working with text.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/time">time</a> — additional time packages.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/tools">tools</a> — godoc, goimports, gorename, and other tools.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/tour">tour</a> — <a href="//tour.golang.org">tour.golang.org</a>'s implementation.</li>
|
||||
<li><a href="//pkg.go.dev/golang.org/x/exp">exp</a> — experimental and deprecated packages (handle with care; may change without warning).</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="community">Community</h3>
|
||||
<p>
|
||||
These services can help you find Open Source packages provided by the community.
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="//pkg.go.dev">Pkg.go.dev</a> - the Go package discovery site.</li>
|
||||
<li><a href="/wiki/Projects">Projects at the Go Wiki</a> - a curated list of Go projects.</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2012 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.
|
||||
|
||||
function initPlayground(transport) {
|
||||
'use strict';
|
||||
|
||||
function text(node) {
|
||||
var s = '';
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var n = node.childNodes[i];
|
||||
if (n.nodeType === 1) {
|
||||
if (n.tagName === 'BUTTON') continue;
|
||||
if (n.tagName === 'SPAN' && n.className === 'number') continue;
|
||||
if (n.tagName === 'DIV' || n.tagName === 'BR' || n.tagName === 'PRE') {
|
||||
s += '\n';
|
||||
}
|
||||
s += text(n);
|
||||
continue;
|
||||
}
|
||||
if (n.nodeType === 3) {
|
||||
s += n.nodeValue;
|
||||
}
|
||||
}
|
||||
return s.replace('\xA0', ' '); // replace non-breaking spaces
|
||||
}
|
||||
|
||||
// When presenter notes are enabled, the index passed
|
||||
// here will identify the playground to be synced
|
||||
function init(code, index) {
|
||||
var output = document.createElement('div');
|
||||
var outpre = document.createElement('pre');
|
||||
var running;
|
||||
|
||||
if ($ && $(output).resizable) {
|
||||
$(output).resizable({
|
||||
handles: 'n,w,nw',
|
||||
minHeight: 27,
|
||||
minWidth: 135,
|
||||
maxHeight: 608,
|
||||
maxWidth: 990,
|
||||
});
|
||||
}
|
||||
|
||||
function onKill() {
|
||||
if (running) running.Kill();
|
||||
if (window.notesEnabled) updatePlayStorage('onKill', index);
|
||||
}
|
||||
|
||||
function onRun(e) {
|
||||
var sk = e.shiftKey || localStorage.getItem('play-shiftKey') === 'true';
|
||||
if (running) running.Kill();
|
||||
output.style.display = 'block';
|
||||
outpre.textContent = '';
|
||||
run1.style.display = 'none';
|
||||
var options = { Race: sk };
|
||||
running = transport.Run(text(code), PlaygroundOutput(outpre), options);
|
||||
if (window.notesEnabled) updatePlayStorage('onRun', index, e);
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
if (running) running.Kill();
|
||||
output.style.display = 'none';
|
||||
run1.style.display = 'inline-block';
|
||||
if (window.notesEnabled) updatePlayStorage('onClose', index);
|
||||
}
|
||||
|
||||
if (window.notesEnabled) {
|
||||
playgroundHandlers.onRun.push(onRun);
|
||||
playgroundHandlers.onClose.push(onClose);
|
||||
playgroundHandlers.onKill.push(onKill);
|
||||
}
|
||||
|
||||
var run1 = document.createElement('button');
|
||||
run1.textContent = 'Run';
|
||||
run1.className = 'run';
|
||||
run1.addEventListener('click', onRun, false);
|
||||
var run2 = document.createElement('button');
|
||||
run2.className = 'run';
|
||||
run2.textContent = 'Run';
|
||||
run2.addEventListener('click', onRun, false);
|
||||
var kill = document.createElement('button');
|
||||
kill.className = 'kill';
|
||||
kill.textContent = 'Kill';
|
||||
kill.addEventListener('click', onKill, false);
|
||||
var close = document.createElement('button');
|
||||
close.className = 'close';
|
||||
close.textContent = 'Close';
|
||||
close.addEventListener('click', onClose, false);
|
||||
|
||||
var button = document.createElement('div');
|
||||
button.classList.add('buttons');
|
||||
button.appendChild(run1);
|
||||
// Hack to simulate insertAfter
|
||||
code.parentNode.insertBefore(button, code.nextSibling);
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.classList.add('buttons');
|
||||
buttons.appendChild(run2);
|
||||
buttons.appendChild(kill);
|
||||
buttons.appendChild(close);
|
||||
|
||||
output.classList.add('output');
|
||||
output.appendChild(buttons);
|
||||
output.appendChild(outpre);
|
||||
output.style.display = 'none';
|
||||
code.parentNode.insertBefore(output, button.nextSibling);
|
||||
}
|
||||
|
||||
var play = document.querySelectorAll('div.playground');
|
||||
for (var i = 0; i < play.length; i++) {
|
||||
init(play[i], i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
// Copyright 2012 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.
|
||||
|
||||
/*
|
||||
In the absence of any formal way to specify interfaces in JavaScript,
|
||||
here's a skeleton implementation of a playground transport.
|
||||
|
||||
function Transport() {
|
||||
// Set up any transport state (eg, make a websocket connection).
|
||||
return {
|
||||
Run: function(body, output, options) {
|
||||
// Compile and run the program 'body' with 'options'.
|
||||
// Call the 'output' callback to display program output.
|
||||
return {
|
||||
Kill: function() {
|
||||
// Kill the running program.
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// The output callback is called multiple times, and each time it is
|
||||
// passed an object of this form.
|
||||
var write = {
|
||||
Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
|
||||
Body: 'string' // content of write or end status message
|
||||
}
|
||||
|
||||
// The first call must be of Kind 'start' with no body.
|
||||
// Subsequent calls may be of Kind 'stdout' or 'stderr'
|
||||
// and must have a non-null Body string.
|
||||
// The final call should be of Kind 'end' with an optional
|
||||
// Body string, signifying a failure ("killed", for example).
|
||||
|
||||
// The output callback must be of this form.
|
||||
// See PlaygroundOutput (below) for an implementation.
|
||||
function outputCallback(write) {
|
||||
}
|
||||
*/
|
||||
|
||||
// HTTPTransport is the default transport.
|
||||
// enableVet enables running vet if a program was compiled and ran successfully.
|
||||
// If vet returned any errors, display them before the output of a program.
|
||||
function HTTPTransport(enableVet) {
|
||||
'use strict';
|
||||
|
||||
function playback(output, data) {
|
||||
// Backwards compatibility: default values do not affect the output.
|
||||
var events = data.Events || [];
|
||||
var errors = data.Errors || '';
|
||||
var status = data.Status || 0;
|
||||
var isTest = data.IsTest || false;
|
||||
var testsFailed = data.TestsFailed || 0;
|
||||
|
||||
var timeout;
|
||||
output({ Kind: 'start' });
|
||||
function next() {
|
||||
if (!events || events.length === 0) {
|
||||
if (isTest) {
|
||||
if (testsFailed > 0) {
|
||||
output({
|
||||
Kind: 'system',
|
||||
Body:
|
||||
'\n' +
|
||||
testsFailed +
|
||||
' test' +
|
||||
(testsFailed > 1 ? 's' : '') +
|
||||
' failed.',
|
||||
});
|
||||
} else {
|
||||
output({ Kind: 'system', Body: '\nAll tests passed.' });
|
||||
}
|
||||
} else {
|
||||
if (status > 0) {
|
||||
output({ Kind: 'end', Body: 'status ' + status + '.' });
|
||||
} else {
|
||||
if (errors !== '') {
|
||||
// errors are displayed only in the case of timeout.
|
||||
output({ Kind: 'end', Body: errors + '.' });
|
||||
} else {
|
||||
output({ Kind: 'end' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
var e = events.shift();
|
||||
if (e.Delay === 0) {
|
||||
output({ Kind: e.Kind, Body: e.Message });
|
||||
next();
|
||||
return;
|
||||
}
|
||||
timeout = setTimeout(function() {
|
||||
output({ Kind: e.Kind, Body: e.Message });
|
||||
next();
|
||||
}, e.Delay / 1000000);
|
||||
}
|
||||
next();
|
||||
return {
|
||||
Stop: function() {
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function error(output, msg) {
|
||||
output({ Kind: 'start' });
|
||||
output({ Kind: 'stderr', Body: msg });
|
||||
output({ Kind: 'end' });
|
||||
}
|
||||
|
||||
function buildFailed(output, msg) {
|
||||
output({ Kind: 'start' });
|
||||
output({ Kind: 'stderr', Body: msg });
|
||||
output({ Kind: 'system', Body: '\nGo build failed.' });
|
||||
}
|
||||
|
||||
var seq = 0;
|
||||
return {
|
||||
Run: function(body, output, options) {
|
||||
seq++;
|
||||
var cur = seq;
|
||||
var playing;
|
||||
$.ajax('/compile', {
|
||||
type: 'POST',
|
||||
data: { version: 2, body: body, withVet: enableVet },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (seq != cur) return;
|
||||
if (!data) return;
|
||||
if (playing != null) playing.Stop();
|
||||
if (data.Errors) {
|
||||
if (data.Errors === 'process took too long') {
|
||||
// Playback the output that was captured before the timeout.
|
||||
playing = playback(output, data);
|
||||
} else {
|
||||
buildFailed(output, data.Errors);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!data.Events) {
|
||||
data.Events = [];
|
||||
}
|
||||
if (data.VetErrors) {
|
||||
// Inject errors from the vet as the first events in the output.
|
||||
data.Events.unshift({
|
||||
Message: 'Go vet exited.\n\n',
|
||||
Kind: 'system',
|
||||
Delay: 0,
|
||||
});
|
||||
data.Events.unshift({
|
||||
Message: data.VetErrors,
|
||||
Kind: 'stderr',
|
||||
Delay: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (!enableVet || data.VetOK || data.VetErrors) {
|
||||
playing = playback(output, data);
|
||||
return;
|
||||
}
|
||||
|
||||
// In case the server support doesn't support
|
||||
// compile+vet in same request signaled by the
|
||||
// 'withVet' parameter above, also try the old way.
|
||||
// TODO: remove this when it falls out of use.
|
||||
// It is 2019-05-13 now.
|
||||
$.ajax('/vet', {
|
||||
data: { body: body },
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
success: function(dataVet) {
|
||||
if (dataVet.Errors) {
|
||||
// inject errors from the vet as the first events in the output
|
||||
data.Events.unshift({
|
||||
Message: 'Go vet exited.\n\n',
|
||||
Kind: 'system',
|
||||
Delay: 0,
|
||||
});
|
||||
data.Events.unshift({
|
||||
Message: dataVet.Errors,
|
||||
Kind: 'stderr',
|
||||
Delay: 0,
|
||||
});
|
||||
}
|
||||
playing = playback(output, data);
|
||||
},
|
||||
error: function() {
|
||||
playing = playback(output, data);
|
||||
},
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
error(output, 'Error communicating with remote server.');
|
||||
},
|
||||
});
|
||||
return {
|
||||
Kill: function() {
|
||||
if (playing != null) playing.Stop();
|
||||
output({ Kind: 'end', Body: 'killed' });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function SocketTransport() {
|
||||
'use strict';
|
||||
|
||||
var id = 0;
|
||||
var outputs = {};
|
||||
var started = {};
|
||||
var websocket;
|
||||
if (window.location.protocol == 'http:') {
|
||||
websocket = new WebSocket('ws://' + window.location.host + '/socket');
|
||||
} else if (window.location.protocol == 'https:') {
|
||||
websocket = new WebSocket('wss://' + window.location.host + '/socket');
|
||||
}
|
||||
|
||||
websocket.onclose = function() {
|
||||
console.log('websocket connection closed');
|
||||
};
|
||||
|
||||
websocket.onmessage = function(e) {
|
||||
var m = JSON.parse(e.data);
|
||||
var output = outputs[m.Id];
|
||||
if (output === null) return;
|
||||
if (!started[m.Id]) {
|
||||
output({ Kind: 'start' });
|
||||
started[m.Id] = true;
|
||||
}
|
||||
output({ Kind: m.Kind, Body: m.Body });
|
||||
};
|
||||
|
||||
function send(m) {
|
||||
websocket.send(JSON.stringify(m));
|
||||
}
|
||||
|
||||
return {
|
||||
Run: function(body, output, options) {
|
||||
var thisID = id + '';
|
||||
id++;
|
||||
outputs[thisID] = output;
|
||||
send({ Id: thisID, Kind: 'run', Body: body, Options: options });
|
||||
return {
|
||||
Kill: function() {
|
||||
send({ Id: thisID, Kind: 'kill' });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function PlaygroundOutput(el) {
|
||||
'use strict';
|
||||
|
||||
return function(write) {
|
||||
if (write.Kind == 'start') {
|
||||
el.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
var cl = 'system';
|
||||
if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind;
|
||||
|
||||
var m = write.Body;
|
||||
if (write.Kind == 'end') {
|
||||
m = '\nProgram exited' + (m ? ': ' + m : '.');
|
||||
}
|
||||
|
||||
if (m.indexOf('IMAGE:') === 0) {
|
||||
// TODO(adg): buffer all writes before creating image
|
||||
var url = 'data:image/png;base64,' + m.substr(6);
|
||||
var img = document.createElement('img');
|
||||
img.src = url;
|
||||
el.appendChild(img);
|
||||
return;
|
||||
}
|
||||
|
||||
// ^L clears the screen.
|
||||
var s = m.split('\x0c');
|
||||
if (s.length > 1) {
|
||||
el.innerHTML = '';
|
||||
m = s.pop();
|
||||
}
|
||||
|
||||
m = m.replace(/&/g, '&');
|
||||
m = m.replace(/</g, '<');
|
||||
m = m.replace(/>/g, '>');
|
||||
|
||||
var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight;
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.className = cl;
|
||||
span.innerHTML = m;
|
||||
el.appendChild(span);
|
||||
|
||||
if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight;
|
||||
};
|
||||
}
|
||||
|
||||
(function() {
|
||||
function lineHighlight(error) {
|
||||
var regex = /prog.go:([0-9]+)/g;
|
||||
var r = regex.exec(error);
|
||||
while (r) {
|
||||
$('.lines div')
|
||||
.eq(r[1] - 1)
|
||||
.addClass('lineerror');
|
||||
r = regex.exec(error);
|
||||
}
|
||||
}
|
||||
function highlightOutput(wrappedOutput) {
|
||||
return function(write) {
|
||||
if (write.Body) lineHighlight(write.Body);
|
||||
wrappedOutput(write);
|
||||
};
|
||||
}
|
||||
function lineClear() {
|
||||
$('.lineerror').removeClass('lineerror');
|
||||
}
|
||||
|
||||
// opts is an object with these keys
|
||||
// codeEl - code editor element
|
||||
// outputEl - program output element
|
||||
// runEl - run button element
|
||||
// fmtEl - fmt button element (optional)
|
||||
// fmtImportEl - fmt "imports" checkbox element (optional)
|
||||
// shareEl - share button element (optional)
|
||||
// shareURLEl - share URL text input element (optional)
|
||||
// shareRedirect - base URL to redirect to on share (optional)
|
||||
// toysEl - toys select element (optional)
|
||||
// enableHistory - enable using HTML5 history API (optional)
|
||||
// transport - playground transport to use (default is HTTPTransport)
|
||||
// enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
|
||||
// enableVet - enable running vet and displaying its errors
|
||||
function playground(opts) {
|
||||
var code = $(opts.codeEl);
|
||||
var transport = opts['transport'] || new HTTPTransport(opts['enableVet']);
|
||||
var running;
|
||||
|
||||
// autoindent helpers.
|
||||
function insertTabs(n) {
|
||||
// find the selection start and end
|
||||
var start = code[0].selectionStart;
|
||||
var end = code[0].selectionEnd;
|
||||
// split the textarea content into two, and insert n tabs
|
||||
var v = code[0].value;
|
||||
var u = v.substr(0, start);
|
||||
for (var i = 0; i < n; i++) {
|
||||
u += '\t';
|
||||
}
|
||||
u += v.substr(end);
|
||||
// set revised content
|
||||
code[0].value = u;
|
||||
// reset caret position after inserted tabs
|
||||
code[0].selectionStart = start + n;
|
||||
code[0].selectionEnd = start + n;
|
||||
}
|
||||
function autoindent(el) {
|
||||
var curpos = el.selectionStart;
|
||||
var tabs = 0;
|
||||
while (curpos > 0) {
|
||||
curpos--;
|
||||
if (el.value[curpos] == '\t') {
|
||||
tabs++;
|
||||
} else if (tabs > 0 || el.value[curpos] == '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
setTimeout(function() {
|
||||
insertTabs(tabs);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// NOTE(cbro): e is a jQuery event, not a DOM event.
|
||||
function handleSaveShortcut(e) {
|
||||
if (e.isDefaultPrevented()) return false;
|
||||
if (!e.metaKey && !e.ctrlKey) return false;
|
||||
if (e.key != 'S' && e.key != 's') return false;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Share and save
|
||||
share(function(url) {
|
||||
window.location.href = url + '.go?download=true';
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function keyHandler(e) {
|
||||
if (opts.enableShortcuts && handleSaveShortcut(e)) return;
|
||||
|
||||
if (e.keyCode == 9 && !e.ctrlKey) {
|
||||
// tab (but not ctrl-tab)
|
||||
insertTabs(1);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (e.keyCode == 13) {
|
||||
// enter
|
||||
if (e.shiftKey) {
|
||||
// +shift
|
||||
run();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
// +control
|
||||
fmt();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
autoindent(e.target);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
code.unbind('keydown').bind('keydown', keyHandler);
|
||||
var outdiv = $(opts.outputEl).empty();
|
||||
var output = $('<pre/>').appendTo(outdiv);
|
||||
|
||||
function body() {
|
||||
return $(opts.codeEl).val();
|
||||
}
|
||||
function setBody(text) {
|
||||
$(opts.codeEl).val(text);
|
||||
}
|
||||
function origin(href) {
|
||||
return ('' + href)
|
||||
.split('/')
|
||||
.slice(0, 3)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
var pushedEmpty = window.location.pathname == '/';
|
||||
function inputChanged() {
|
||||
if (pushedEmpty) {
|
||||
return;
|
||||
}
|
||||
pushedEmpty = true;
|
||||
$(opts.shareURLEl).hide();
|
||||
window.history.pushState(null, '', '/');
|
||||
}
|
||||
function popState(e) {
|
||||
if (e === null) {
|
||||
return;
|
||||
}
|
||||
if (e && e.state && e.state.code) {
|
||||
setBody(e.state.code);
|
||||
}
|
||||
}
|
||||
var rewriteHistory = false;
|
||||
if (
|
||||
window.history &&
|
||||
window.history.pushState &&
|
||||
window.addEventListener &&
|
||||
opts.enableHistory
|
||||
) {
|
||||
rewriteHistory = true;
|
||||
code[0].addEventListener('input', inputChanged);
|
||||
window.addEventListener('popstate', popState);
|
||||
}
|
||||
|
||||
function setError(error) {
|
||||
if (running) running.Kill();
|
||||
lineClear();
|
||||
lineHighlight(error);
|
||||
output
|
||||
.empty()
|
||||
.addClass('error')
|
||||
.text(error);
|
||||
}
|
||||
function loading() {
|
||||
lineClear();
|
||||
if (running) running.Kill();
|
||||
output.removeClass('error').text('Waiting for remote server...');
|
||||
}
|
||||
function run() {
|
||||
loading();
|
||||
running = transport.Run(
|
||||
body(),
|
||||
highlightOutput(PlaygroundOutput(output[0]))
|
||||
);
|
||||
}
|
||||
|
||||
function fmt() {
|
||||
loading();
|
||||
var data = { body: body() };
|
||||
if ($(opts.fmtImportEl).is(':checked')) {
|
||||
data['imports'] = 'true';
|
||||
}
|
||||
$.ajax('/fmt', {
|
||||
data: data,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data.Error) {
|
||||
setError(data.Error);
|
||||
} else {
|
||||
setBody(data.Body);
|
||||
setError('');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
var shareURL; // jQuery element to show the shared URL.
|
||||
var sharing = false; // true if there is a pending request.
|
||||
var shareCallbacks = [];
|
||||
function share(opt_callback) {
|
||||
if (opt_callback) shareCallbacks.push(opt_callback);
|
||||
|
||||
if (sharing) return;
|
||||
sharing = true;
|
||||
|
||||
var sharingData = body();
|
||||
$.ajax('https://play.golang.org/share', {
|
||||
processData: false,
|
||||
data: sharingData,
|
||||
type: 'POST',
|
||||
contentType: 'text/plain; charset=utf-8',
|
||||
complete: function(xhr) {
|
||||
sharing = false;
|
||||
if (xhr.status != 200) {
|
||||
alert('Server error; try again.');
|
||||
return;
|
||||
}
|
||||
if (opts.shareRedirect) {
|
||||
window.location = opts.shareRedirect + xhr.responseText;
|
||||
}
|
||||
var path = '/p/' + xhr.responseText;
|
||||
var url = origin(window.location) + path;
|
||||
|
||||
for (var i = 0; i < shareCallbacks.length; i++) {
|
||||
shareCallbacks[i](url);
|
||||
}
|
||||
shareCallbacks = [];
|
||||
|
||||
if (shareURL) {
|
||||
shareURL
|
||||
.show()
|
||||
.val(url)
|
||||
.focus()
|
||||
.select();
|
||||
|
||||
if (rewriteHistory) {
|
||||
var historyData = { code: sharingData };
|
||||
window.history.pushState(historyData, '', path);
|
||||
pushedEmpty = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(opts.runEl).click(run);
|
||||
$(opts.fmtEl).click(fmt);
|
||||
|
||||
if (
|
||||
opts.shareEl !== null &&
|
||||
(opts.shareURLEl !== null || opts.shareRedirect !== null)
|
||||
) {
|
||||
if (opts.shareURLEl) {
|
||||
shareURL = $(opts.shareURLEl).hide();
|
||||
}
|
||||
$(opts.shareEl).click(function() {
|
||||
share();
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.toysEl !== null) {
|
||||
$(opts.toysEl).bind('change', function() {
|
||||
var toy = $(this).val();
|
||||
$.ajax('/doc/play/' + toy, {
|
||||
processData: false,
|
||||
type: 'GET',
|
||||
complete: function(xhr) {
|
||||
if (xhr.status != 200) {
|
||||
alert('Server error; try again.');
|
||||
return;
|
||||
}
|
||||
setBody(xhr.responseText);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.playground = playground;
|
||||
})();
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
{{ $colCount := tocColCount .}}
|
||||
{{/* Generate the TOC */}}
|
||||
<nav class="search-nav" style="column-count: {{$colCount}}" role="navigation">
|
||||
{{range $key, $val := .Idents}}
|
||||
{{if $val}}
|
||||
<a href="#{{$key.Name}}">{{$key.Name}}</a>
|
||||
<br />
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if not .Idents}}
|
||||
{{with .Pak}}
|
||||
<a href="#Packages">Package {{html $.Query}}</a>
|
||||
<br />
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{with .Hit}}
|
||||
{{with .Decls}}
|
||||
<a href="#Global">Package-level declarations</a>
|
||||
<br />
|
||||
{{range .}}
|
||||
{{$pkg_html := pkgLink .Pak.Path | html}}
|
||||
<a href="#Global_{{$pkg_html}}" class="indent">package {{html .Pak.Name}}</a>
|
||||
<br />
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{with .Others}}
|
||||
<a href="#Local">Local declarations and uses</a>
|
||||
<br />
|
||||
{{range .}}
|
||||
{{$pkg_html := pkgLink .Pak.Path | html}}
|
||||
<a href="#Local_{{$pkg_html}}" class="indent">package {{html .Pak.Name}}</a>
|
||||
<br />
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{with .Textual}}
|
||||
{{if $.Complete}}
|
||||
<a href="#Textual">{{html $.Found}} textual occurrences</a>
|
||||
{{else}}
|
||||
<a href="#Textual">More than {{html $.Found}} textual occurrences</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
|
||||
{{with .Alert}}
|
||||
<p>
|
||||
<span class="alert" style="font-size:120%">{{html .}}</span>
|
||||
</p>
|
||||
{{end}}
|
||||
{{with .Alt}}
|
||||
<p>
|
||||
<span class="alert" style="font-size:120%">Did you mean: </span>
|
||||
{{range .Alts}}
|
||||
<a href="search?q={{urlquery .}}" style="font-size:120%">{{html .}}</a>
|
||||
{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
{{$query_url := urlquery .Query}}
|
||||
{{if not .Idents}}
|
||||
{{with .Pak}}
|
||||
<h2 id="Packages">Package {{html $.Query}}</h2>
|
||||
<p>
|
||||
<table class="layout">
|
||||
{{range .}}
|
||||
{{$pkg_html := pkgLink .Pak.Path | html}}
|
||||
<tr><td><a href="/{{$pkg_html}}">{{$pkg_html}}</a></td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{with .Hit}}
|
||||
{{with .Decls}}
|
||||
<h2 id="Global">Package-level declarations</h2>
|
||||
{{range .}}
|
||||
{{$pkg_html := pkgLink .Pak.Path | html}}
|
||||
<h3 id="Global_{{$pkg_html}}">package <a href="/{{$pkg_html}}">{{html .Pak.Name}}</a></h3>
|
||||
{{range .Files}}
|
||||
{{$file := .File.Path}}
|
||||
{{range .Groups}}
|
||||
{{range .}}
|
||||
{{$line := infoLine .}}
|
||||
<a href="{{queryLink $file $query_url $line | html}}">{{$file}}:{{$line}}</a>
|
||||
{{infoSnippet_html .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{with .Others}}
|
||||
<h2 id="Local">Local declarations and uses</h2>
|
||||
{{range .}}
|
||||
{{$pkg_html := pkgLink .Pak.Path | html}}
|
||||
<h3 id="Local_{{$pkg_html}}">package <a href="/{{$pkg_html}}">{{html .Pak.Name}}</a></h3>
|
||||
{{range .Files}}
|
||||
{{$file := .File.Path}}
|
||||
<a href="{{queryLink $file $query_url 0 | html}}">{{$file}}</a>
|
||||
<table class="layout">
|
||||
{{range .Groups}}
|
||||
<tr>
|
||||
<td width="25"></td>
|
||||
<th align="left" valign="top">{{index . 0 | infoKind_html}}</th>
|
||||
<td align="left" width="4"></td>
|
||||
<td>
|
||||
{{range .}}
|
||||
{{$line := infoLine .}}
|
||||
<a href="{{queryLink $file $query_url $line | html}}">{{$line}}</a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
{{range $key, $val := .Idents}}
|
||||
{{if $val}}
|
||||
<h2 id="{{$key.Name}}">{{$key.Name}}</h2>
|
||||
{{range $val}}
|
||||
{{$pkg_html := pkgLink .Path | html}}
|
||||
{{if eq "Packages" $key.Name}}
|
||||
<a href="/{{$pkg_html}}">{{html .Path}}</a>
|
||||
{{else}}
|
||||
{{$doc_html := docLink .Path .Name| html}}
|
||||
<a href="/{{$pkg_html}}">{{html .Package}}</a>.<a href="{{$doc_html}}">{{.Name}}</a>
|
||||
{{end}}
|
||||
{{if .Doc}}
|
||||
<p>{{comment_html $ .Doc}}</p>
|
||||
{{else}}
|
||||
<p><em>No documentation available</em></p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
{{$query_url := urlquery .Query}}
|
||||
{{with .Textual}}
|
||||
{{if $.Complete}}
|
||||
<h2 id="Textual">{{html $.Found}} textual occurrences</h2>
|
||||
{{else}}
|
||||
<h2 id="Textual">More than {{html $.Found}} textual occurrences</h2>
|
||||
<p>
|
||||
<span class="alert" style="font-size:120%">Not all files or lines containing "{{html $.Query}}" are shown.</span>
|
||||
</p>
|
||||
{{end}}
|
||||
<p>
|
||||
<table class="layout">
|
||||
{{range .}}
|
||||
{{$file := .Filename}}
|
||||
<tr>
|
||||
<td align="left" valign="top">
|
||||
<a href="{{queryLink $file $query_url 0}}">{{$file}}</a>:
|
||||
</td>
|
||||
<td align="left" width="4"></td>
|
||||
<th align="left" valign="top">{{len .Lines}}</th>
|
||||
<td align="left" width="4"></td>
|
||||
<td align="left">
|
||||
{{range .Lines}}
|
||||
<a href="{{queryLink $file $query_url .}}">{{html .}}</a>
|
||||
{{end}}
|
||||
{{if not $.Complete}}
|
||||
...
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if not $.Complete}}
|
||||
<tr><td align="left">...</td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</p>
|
||||
{{end}}
|
||||
@@ -0,0 +1,897 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #fff;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
color: #222;
|
||||
}
|
||||
textarea {
|
||||
/* Inherit text color from body avoiding illegible text in the case where the
|
||||
* user has inverted the browsers custom text and background colors. */
|
||||
color: inherit;
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
pre {
|
||||
line-height: 1.4;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre .comment {
|
||||
color: #006600;
|
||||
}
|
||||
pre .highlight,
|
||||
pre .highlight-comment,
|
||||
pre .selection-highlight,
|
||||
pre .selection-highlight-comment {
|
||||
background: #ffff00;
|
||||
}
|
||||
pre .selection,
|
||||
pre .selection-comment {
|
||||
background: #ff9632;
|
||||
}
|
||||
pre .ln {
|
||||
color: #999;
|
||||
background: #efefef;
|
||||
}
|
||||
.ln {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
/* Ensure 8 characters in the document - which due to floating
|
||||
* point rendering issues, might have a width of less than 1 each - are 8
|
||||
* characters wide, so a tab in the 9th position indents properly. See
|
||||
* https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
|
||||
* for more information. */
|
||||
display: inline-block;
|
||||
width: 8ch;
|
||||
}
|
||||
|
||||
.search-nav {
|
||||
margin-left: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
column-gap: 1.25rem;
|
||||
column-fill: auto;
|
||||
column-width: 14rem;
|
||||
}
|
||||
|
||||
.search-nav .indent {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
|
||||
a,
|
||||
.exampleHeading .text,
|
||||
.expandAll {
|
||||
color: #375eab;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover,
|
||||
.exampleHeading .text:hover,
|
||||
.expandAll:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.article a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.article .title a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.permalink {
|
||||
display: none;
|
||||
}
|
||||
:hover > .permalink {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
max-width: 50rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
p,
|
||||
pre,
|
||||
ul,
|
||||
ol {
|
||||
margin: 1.25rem;
|
||||
}
|
||||
pre {
|
||||
background: #efefef;
|
||||
padding: 0.625rem;
|
||||
border-radius: 0.3125rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
.rootHeading {
|
||||
margin: 1.25rem 0 1.25rem;
|
||||
padding: 0;
|
||||
color: #375eab;
|
||||
font-weight: bold;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
h1 .text-muted {
|
||||
color: #777;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
background: #e0ebf5;
|
||||
padding: 0.5rem;
|
||||
line-height: 1.25;
|
||||
font-weight: normal;
|
||||
overflow: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
h2 a {
|
||||
font-weight: bold;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.25;
|
||||
overflow: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
h3,
|
||||
h4 {
|
||||
margin: 1.25rem 0.3125rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.rootHeading {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 > span,
|
||||
h3 > span {
|
||||
float: right;
|
||||
margin: 0 25px 0 0;
|
||||
font-weight: normal;
|
||||
color: #5279c7;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 1.25rem;
|
||||
}
|
||||
dd {
|
||||
margin: 0 0 0 1.25rem;
|
||||
}
|
||||
dl,
|
||||
dd {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
div#nav table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#pkg-index h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.pkg-dir {
|
||||
padding: 0 0.625rem;
|
||||
}
|
||||
.pkg-dir table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.pkg-name {
|
||||
padding-right: 0.625rem;
|
||||
}
|
||||
.alert {
|
||||
color: #aa0000;
|
||||
}
|
||||
|
||||
.top-heading {
|
||||
float: left;
|
||||
padding: 1.313rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
.top-heading a {
|
||||
color: #222;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#pkg-examples h3 {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#pkg-examples dl {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.expandAll {
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
background: #e0ebf5;
|
||||
height: 4rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div#page {
|
||||
width: 100%;
|
||||
}
|
||||
div#page > .container,
|
||||
div#topbar > .container {
|
||||
text-align: left;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0 1.25rem;
|
||||
}
|
||||
div#topbar > .container,
|
||||
div#page > .container {
|
||||
max-width: 59.38rem;
|
||||
}
|
||||
div#page.wide > .container,
|
||||
div#topbar.wide > .container {
|
||||
max-width: none;
|
||||
}
|
||||
div#plusone {
|
||||
float: right;
|
||||
clear: right;
|
||||
margin-top: 0.3125rem;
|
||||
}
|
||||
|
||||
div#footer {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
div#menu > a,
|
||||
input#search,
|
||||
div#learn .buttons a,
|
||||
div.play .buttons a,
|
||||
div#blog .read a,
|
||||
#menu-button {
|
||||
padding: 0.625rem;
|
||||
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.3125rem;
|
||||
}
|
||||
div#playground .buttons a,
|
||||
div#menu > a,
|
||||
input#search,
|
||||
#menu-button {
|
||||
border: 0.0625rem solid #375eab;
|
||||
}
|
||||
div#playground .buttons a,
|
||||
div#menu > a,
|
||||
#menu-button {
|
||||
color: white;
|
||||
background: #375eab;
|
||||
}
|
||||
#playgroundButton.active {
|
||||
background: white;
|
||||
color: #375eab;
|
||||
}
|
||||
a#start,
|
||||
div#learn .buttons a,
|
||||
div.play .buttons a,
|
||||
div#blog .read a {
|
||||
color: #222;
|
||||
border: 0.0625rem solid #375eab;
|
||||
background: #e0ebf5;
|
||||
}
|
||||
.download {
|
||||
width: 9.375rem;
|
||||
}
|
||||
|
||||
div#menu {
|
||||
text-align: right;
|
||||
padding: 0.625rem;
|
||||
white-space: nowrap;
|
||||
max-height: 0;
|
||||
-moz-transition: max-height 0.25s linear;
|
||||
transition: max-height 0.25s linear;
|
||||
width: 100%;
|
||||
}
|
||||
div#menu.menu-visible {
|
||||
max-height: 31.25rem;
|
||||
}
|
||||
div#menu > a,
|
||||
#menu-button {
|
||||
margin: 0.625rem 0.125rem;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
::-webkit-input-placeholder {
|
||||
color: #7f7f7f;
|
||||
opacity: 1;
|
||||
}
|
||||
::placeholder {
|
||||
color: #7f7f7f;
|
||||
opacity: 1;
|
||||
}
|
||||
#menu .search-box {
|
||||
display: inline-flex;
|
||||
width: 8.75rem;
|
||||
}
|
||||
input#search {
|
||||
background: white;
|
||||
color: #222;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
min-width: 5.625rem;
|
||||
}
|
||||
input#search:-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
input#search:-moz-ui-invalid {
|
||||
box-shadow: unset;
|
||||
}
|
||||
input#search + button {
|
||||
display: inline;
|
||||
font-size: 1em;
|
||||
background-color: #375eab;
|
||||
color: white;
|
||||
border: 0.0625rem solid #375eab;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0.3125rem;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0.3125rem;
|
||||
margin-left: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
input#search + button span {
|
||||
display: flex;
|
||||
}
|
||||
input#search + button svg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
#menu-button {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0.3125rem;
|
||||
top: 0;
|
||||
margin-right: 0.3125rem;
|
||||
}
|
||||
#menu-button-arrow {
|
||||
display: inline-block;
|
||||
}
|
||||
.vertical-flip {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
div.left {
|
||||
float: left;
|
||||
clear: left;
|
||||
margin-right: 2.5%;
|
||||
}
|
||||
div.right {
|
||||
float: right;
|
||||
clear: right;
|
||||
margin-left: 2.5%;
|
||||
}
|
||||
div.left,
|
||||
div.right {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
div#learn,
|
||||
div#about {
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
div#learn h2,
|
||||
div#about {
|
||||
margin: 0;
|
||||
}
|
||||
div#about {
|
||||
font-size: 1.25rem;
|
||||
margin: 0 auto 1.875rem;
|
||||
}
|
||||
a#start {
|
||||
display: block;
|
||||
padding: 0.625rem;
|
||||
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 0.3125rem;
|
||||
}
|
||||
a#start .big {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
a#start .desc {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: normal;
|
||||
margin-top: 0.3125rem;
|
||||
}
|
||||
|
||||
div#learn .popout {
|
||||
float: right;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
background: url(/doc/share.png) no-repeat;
|
||||
background-position: right center;
|
||||
padding: 0.375rem 1.688rem;
|
||||
}
|
||||
div#learn pre,
|
||||
div#learn textarea {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
div#learn .input {
|
||||
padding: 0.625rem;
|
||||
margin-top: 0.625rem;
|
||||
height: 9.375rem;
|
||||
|
||||
border-top-left-radius: 0.3125rem;
|
||||
border-top-right-radius: 0.3125rem;
|
||||
}
|
||||
div#learn .input textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
div#learn .output {
|
||||
border-top: none !important;
|
||||
|
||||
padding: 0.625rem;
|
||||
height: 3.688rem;
|
||||
overflow: auto;
|
||||
|
||||
border-bottom-right-radius: 0.3125rem;
|
||||
border-bottom-left-radius: 0.3125rem;
|
||||
}
|
||||
div#learn .output pre {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
div#learn .input,
|
||||
div#learn .input textarea,
|
||||
div#learn .output,
|
||||
div#learn .output pre {
|
||||
background: #ffffd8;
|
||||
}
|
||||
div#learn .input,
|
||||
div#learn .output {
|
||||
border: 0.0625rem solid #375eab;
|
||||
}
|
||||
div#learn .buttons {
|
||||
float: right;
|
||||
padding: 1.25rem 0 0.625rem 0;
|
||||
text-align: right;
|
||||
}
|
||||
div#learn .buttons a {
|
||||
height: 1rem;
|
||||
margin-left: 0.3125rem;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
div#learn .toys {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
div#learn .toys select {
|
||||
font-size: 0.875rem;
|
||||
border: 0.0625rem solid #375eab;
|
||||
margin: 0;
|
||||
}
|
||||
div#learn .output .exit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div#video {
|
||||
max-width: 100%;
|
||||
}
|
||||
div#blog,
|
||||
div#video {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
div#blog > a,
|
||||
div#blog > div,
|
||||
div#blog > h2,
|
||||
div#video > a,
|
||||
div#video > div,
|
||||
div#video > h2 {
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
div#blog .title,
|
||||
div#video .title {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
div#blog .when {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
div#blog .read {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@supports (--c: 0) {
|
||||
[style*='--aspect-ratio-padding:'] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-top: var(--aspect-ratio-padding);
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio-padding:'] > * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle > .collapsed {
|
||||
display: block;
|
||||
}
|
||||
.toggle > .expanded {
|
||||
display: none;
|
||||
}
|
||||
.toggleVisible > .collapsed {
|
||||
display: none;
|
||||
}
|
||||
.toggleVisible > .expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table.codetable {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-style: none;
|
||||
}
|
||||
table.codetable td {
|
||||
padding-right: 0.625rem;
|
||||
}
|
||||
hr {
|
||||
border-style: none;
|
||||
border-top: 0.0625rem solid black;
|
||||
}
|
||||
|
||||
img.gopher {
|
||||
float: right;
|
||||
margin-left: 0.625rem;
|
||||
margin-top: -2.5rem;
|
||||
margin-bottom: 0.625rem;
|
||||
z-index: -1;
|
||||
}
|
||||
h2 {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
/* example and drop-down playground */
|
||||
div.play {
|
||||
padding: 0 1.25rem 2.5rem 1.25rem;
|
||||
}
|
||||
div.play pre,
|
||||
div.play textarea,
|
||||
div.play .lines {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
div.play .input {
|
||||
padding: 0.625rem;
|
||||
margin-top: 0.625rem;
|
||||
|
||||
border-top-left-radius: 0.3125rem;
|
||||
border-top-right-radius: 0.3125rem;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
div.play .input textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
div#playground .input textarea {
|
||||
overflow: auto;
|
||||
resize: auto;
|
||||
}
|
||||
div.play .output {
|
||||
border-top: none !important;
|
||||
|
||||
padding: 0.625rem;
|
||||
max-height: 12.5rem;
|
||||
overflow: auto;
|
||||
|
||||
border-bottom-right-radius: 0.3125rem;
|
||||
border-bottom-left-radius: 0.3125rem;
|
||||
}
|
||||
div.play .output pre {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
div.play .input,
|
||||
div.play .input textarea,
|
||||
div.play .output,
|
||||
div.play .output pre {
|
||||
background: #ffffd8;
|
||||
}
|
||||
div.play .input,
|
||||
div.play .output {
|
||||
border: 0.0625rem solid #375eab;
|
||||
}
|
||||
div.play .buttons {
|
||||
float: right;
|
||||
padding: 1.25rem 0 0.625rem 0;
|
||||
text-align: right;
|
||||
}
|
||||
div.play .buttons a {
|
||||
height: 1rem;
|
||||
margin-left: 0.3125rem;
|
||||
padding: 0.625rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.output .stderr {
|
||||
color: #933;
|
||||
}
|
||||
.output .system {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* drop-down playground */
|
||||
div#playground {
|
||||
/* start hidden; revealed by javascript */
|
||||
display: none;
|
||||
}
|
||||
div#playground {
|
||||
position: absolute;
|
||||
top: 3.938rem;
|
||||
right: 1.25rem;
|
||||
padding: 0 0.625rem 0.625rem 0.625rem;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
background: #e0ebf5;
|
||||
|
||||
border: 0.0625rem solid #b0bbc5;
|
||||
border-top: none;
|
||||
|
||||
border-bottom-left-radius: 0.3125rem;
|
||||
border-bottom-right-radius: 0.3125rem;
|
||||
}
|
||||
div#playground .code {
|
||||
width: 32.5rem;
|
||||
height: 12.5rem;
|
||||
}
|
||||
div#playground .output {
|
||||
height: 6.25rem;
|
||||
}
|
||||
|
||||
/* Inline runnable snippets (play.js/initPlayground) */
|
||||
#content .code pre,
|
||||
#content .playground pre,
|
||||
#content .output pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: 0 solid transparent;
|
||||
overflow: auto;
|
||||
}
|
||||
#content .playground .number,
|
||||
#content .code .number {
|
||||
color: #999;
|
||||
}
|
||||
#content .code,
|
||||
#content .playground,
|
||||
#content .output {
|
||||
width: auto;
|
||||
margin: 1.25rem;
|
||||
padding: 0.625rem;
|
||||
border-radius: 0.3125rem;
|
||||
}
|
||||
#content .code,
|
||||
#content .playground {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
#content .output {
|
||||
background: #202020;
|
||||
}
|
||||
#content .output .stdout,
|
||||
#content .output pre {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
#content .output .stderr,
|
||||
#content .output .error {
|
||||
color: rgb(244, 74, 63);
|
||||
}
|
||||
#content .output .system,
|
||||
#content .output .exit {
|
||||
color: rgb(255, 209, 77);
|
||||
}
|
||||
#content .buttons {
|
||||
position: relative;
|
||||
float: right;
|
||||
top: -3.125rem;
|
||||
right: 1.875rem;
|
||||
}
|
||||
#content .output .buttons {
|
||||
top: -3.75rem;
|
||||
right: 0;
|
||||
height: 0;
|
||||
}
|
||||
#content .buttons .kill {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
a.error {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: darkred;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
padding: 0.125rem 0.25rem 0.125rem 0.25rem; /* TRBL */
|
||||
}
|
||||
|
||||
#heading-narrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.downloading {
|
||||
background: #f9f9be;
|
||||
padding: 0.625rem;
|
||||
text-align: center;
|
||||
border-radius: 0.3125rem;
|
||||
}
|
||||
|
||||
@media (max-width: 58.125em) {
|
||||
#heading-wide {
|
||||
display: none;
|
||||
}
|
||||
#heading-narrow {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 47.5em) {
|
||||
.container .left,
|
||||
.container .right {
|
||||
width: auto;
|
||||
float: none;
|
||||
}
|
||||
|
||||
div#about {
|
||||
max-width: 31.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 43.75em) and (max-width: 62.5em) {
|
||||
div#menu > a {
|
||||
margin: 0.3125rem 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
input#search {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 43.75em) {
|
||||
body {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
div#playground {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-size: 0.866rem;
|
||||
}
|
||||
|
||||
div#page > .container {
|
||||
padding: 0 0.625rem;
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
height: auto;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
div#topbar > .container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#heading-wide {
|
||||
display: block;
|
||||
}
|
||||
#heading-narrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-heading {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
div#menu {
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div#menu > a {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#menu .search-box {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#menu-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
p,
|
||||
pre,
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.625rem;
|
||||
}
|
||||
|
||||
.pkg-synopsis {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img.gopher {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 30em) {
|
||||
#heading-wide {
|
||||
display: none;
|
||||
}
|
||||
#heading-narrow {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
pre {
|
||||
background: #fff;
|
||||
border: 0.0625rem solid #bbb;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO(bradfitz,adg): move to util
|
||||
|
||||
package godoc
|
||||
|
||||
import "io"
|
||||
|
||||
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
|
||||
p *Presentation
|
||||
}
|
||||
|
||||
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 += p.p.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
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
// Template support for writing HTML documents.
|
||||
// Documents that include Template: true in their
|
||||
// metadata are executed as input to text/template.
|
||||
//
|
||||
// This file defines functions for those templates to invoke.
|
||||
|
||||
// The template uses the function "code" to inject program
|
||||
// source into the output by extracting code from files and
|
||||
// injecting them as HTML-escaped <pre> blocks.
|
||||
//
|
||||
// The syntax is simple: 1, 2, or 3 space-separated arguments:
|
||||
//
|
||||
// Whole file:
|
||||
// {{code "foo.go"}}
|
||||
// One line (here the signature of main):
|
||||
// {{code "foo.go" `/^func.main/`}}
|
||||
// Block of text, determined by start and end (here the body of main):
|
||||
// {{code "foo.go" `/^func.main/` `/^}/`
|
||||
//
|
||||
// Patterns can be `/regular expression/`, a decimal number, or "$"
|
||||
// to signify the end of the file. In multi-line matches,
|
||||
// lines that end with the four characters
|
||||
// OMIT
|
||||
// are omitted from the output, making it easy to provide marker
|
||||
// lines in the input that will not appear in the output but are easy
|
||||
// to identify by pattern.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// Functions in this file panic on error, but the panic is recovered
|
||||
// to an error by 'code'.
|
||||
|
||||
// contents reads and returns the content of the named file
|
||||
// (from the virtual file system, so for example /doc refers to $GOROOT/doc).
|
||||
func (c *Corpus) contents(name string) string {
|
||||
file, err := vfs.ReadFile(c.fs, name)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return string(file)
|
||||
}
|
||||
|
||||
// stringFor returns a textual representation of the arg, formatted according to its nature.
|
||||
func stringFor(arg interface{}) string {
|
||||
switch arg := arg.(type) {
|
||||
case int:
|
||||
return fmt.Sprintf("%d", arg)
|
||||
case string:
|
||||
if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
|
||||
return fmt.Sprintf("%#q", arg)
|
||||
}
|
||||
return fmt.Sprintf("%q", arg)
|
||||
default:
|
||||
log.Panicf("unrecognized argument: %v type %T", arg, arg)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
text := p.Corpus.contents(file)
|
||||
var command string
|
||||
switch len(arg) {
|
||||
case 0:
|
||||
// text is already whole file.
|
||||
command = fmt.Sprintf("code %q", file)
|
||||
case 1:
|
||||
command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
|
||||
text = p.Corpus.oneLine(file, text, arg[0])
|
||||
case 2:
|
||||
command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
|
||||
text = p.Corpus.multipleLines(file, text, arg[0], arg[1])
|
||||
default:
|
||||
return "", fmt.Errorf("incorrect code invocation: code %q [%v, ...] (%d arguments)", file, arg[0], len(arg))
|
||||
}
|
||||
// Trim spaces from output.
|
||||
text = strings.Trim(text, "\n")
|
||||
// Replace tabs by spaces, which work better in HTML.
|
||||
text = strings.Replace(text, "\t", " ", -1)
|
||||
var buf bytes.Buffer
|
||||
// HTML-escape text and syntax-color comments like elsewhere.
|
||||
FormatText(&buf, []byte(text), -1, true, "", nil)
|
||||
// Include the command as a comment.
|
||||
text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// parseArg returns the integer or string value of the argument and tells which it is.
|
||||
func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) {
|
||||
switch n := arg.(type) {
|
||||
case int:
|
||||
if n <= 0 || n > max {
|
||||
log.Panicf("%q:%d is out of range", file, n)
|
||||
}
|
||||
return n, "", true
|
||||
case string:
|
||||
return 0, n, false
|
||||
}
|
||||
log.Panicf("unrecognized argument %v type %T", arg, arg)
|
||||
return
|
||||
}
|
||||
|
||||
// oneLine returns the single line generated by a two-argument code invocation.
|
||||
func (c *Corpus) oneLine(file, text string, arg interface{}) string {
|
||||
lines := strings.SplitAfter(c.contents(file), "\n")
|
||||
line, pattern, isInt := parseArg(arg, file, len(lines))
|
||||
if isInt {
|
||||
return lines[line-1]
|
||||
}
|
||||
return lines[match(file, 0, lines, pattern)-1]
|
||||
}
|
||||
|
||||
// multipleLines returns the text generated by a three-argument code invocation.
|
||||
func (c *Corpus) multipleLines(file, text string, arg1, arg2 interface{}) string {
|
||||
lines := strings.SplitAfter(c.contents(file), "\n")
|
||||
line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
|
||||
line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
|
||||
if !isInt1 {
|
||||
line1 = match(file, 0, lines, pattern1)
|
||||
}
|
||||
if !isInt2 {
|
||||
line2 = match(file, line1, lines, pattern2)
|
||||
} else if line2 < line1 {
|
||||
log.Panicf("lines out of order for %q: %d %d", text, line1, line2)
|
||||
}
|
||||
for k := line1 - 1; k < line2; k++ {
|
||||
if strings.HasSuffix(lines[k], "OMIT\n") {
|
||||
lines[k] = ""
|
||||
}
|
||||
}
|
||||
return strings.Join(lines[line1-1:line2], "")
|
||||
}
|
||||
|
||||
// match identifies the input line that matches the pattern in a code invocation.
|
||||
// If start>0, match lines starting there rather than at the beginning.
|
||||
// The return value is 1-indexed.
|
||||
func match(file string, start int, lines []string, pattern string) int {
|
||||
// $ matches the end of the file.
|
||||
if pattern == "$" {
|
||||
if len(lines) == 0 {
|
||||
log.Panicf("%q: empty file", file)
|
||||
}
|
||||
return len(lines)
|
||||
}
|
||||
// /regexp/ matches the line that matches the regexp.
|
||||
if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
|
||||
re, err := regexp.Compile(pattern[1 : len(pattern)-1])
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
for i := start; i < len(lines); i++ {
|
||||
if re.MatchString(lines[i]) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
log.Panicf("%s: no match for %#q", file, pattern)
|
||||
}
|
||||
log.Panicf("unrecognized pattern: %q", pattern)
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2011 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 util
|
||||
|
||||
import "time"
|
||||
|
||||
// A Throttle permits throttling of a goroutine by
|
||||
// calling the Throttle method repeatedly.
|
||||
type Throttle struct {
|
||||
f float64 // f = (1-r)/r for 0 < r < 1
|
||||
dt time.Duration // minimum run time slice; >= 0
|
||||
tr time.Duration // accumulated time running
|
||||
ts time.Duration // accumulated time stopped
|
||||
tt time.Time // earliest throttle time (= time Throttle returned + tm)
|
||||
}
|
||||
|
||||
// NewThrottle creates a new Throttle with a throttle value r and
|
||||
// a minimum allocated run time slice of dt:
|
||||
//
|
||||
// r == 0: "empty" throttle; the goroutine is always sleeping
|
||||
// r == 1: full throttle; the goroutine is never sleeping
|
||||
//
|
||||
// A value of r == 0.6 throttles a goroutine such that it runs
|
||||
// approx. 60% of the time, and sleeps approx. 40% of the time.
|
||||
// Values of r < 0 or r > 1 are clamped down to values between 0 and 1.
|
||||
// Values of dt < 0 are set to 0.
|
||||
func NewThrottle(r float64, dt time.Duration) *Throttle {
|
||||
var f float64
|
||||
switch {
|
||||
case r <= 0:
|
||||
f = -1 // indicates always sleep
|
||||
case r >= 1:
|
||||
f = 0 // assume r == 1 (never sleep)
|
||||
default:
|
||||
// 0 < r < 1
|
||||
f = (1 - r) / r
|
||||
}
|
||||
if dt < 0 {
|
||||
dt = 0
|
||||
}
|
||||
return &Throttle{f: f, dt: dt, tt: time.Now().Add(dt)}
|
||||
}
|
||||
|
||||
// Throttle calls time.Sleep such that over time the ratio tr/ts between
|
||||
// accumulated run (tr) and sleep times (ts) approximates the value 1/(1-r)
|
||||
// where r is the throttle value. Throttle returns immediately (w/o sleeping)
|
||||
// if less than tm ns have passed since the last call to Throttle.
|
||||
func (p *Throttle) Throttle() {
|
||||
if p.f < 0 {
|
||||
select {} // always sleep
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
if t0.Before(p.tt) {
|
||||
return // keep running (minimum time slice not exhausted yet)
|
||||
}
|
||||
|
||||
// accumulate running time
|
||||
p.tr += t0.Sub(p.tt) + p.dt
|
||||
|
||||
// compute sleep time
|
||||
// Over time we want:
|
||||
//
|
||||
// tr/ts = r/(1-r)
|
||||
//
|
||||
// Thus:
|
||||
//
|
||||
// ts = tr*f with f = (1-r)/r
|
||||
//
|
||||
// After some incremental run time δr added to the total run time
|
||||
// tr, the incremental sleep-time δs to get to the same ratio again
|
||||
// after waking up from time.Sleep is:
|
||||
if δs := time.Duration(float64(p.tr)*p.f) - p.ts; δs > 0 {
|
||||
time.Sleep(δs)
|
||||
}
|
||||
|
||||
// accumulate (actual) sleep time
|
||||
t1 := time.Now()
|
||||
p.ts += t1.Sub(t0)
|
||||
|
||||
// set earliest next throttle time
|
||||
p.tt = t1.Add(p.dt)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package util contains utility types and functions for godoc.
|
||||
package util // import "golang.org/x/tools/godoc/util"
|
||||
|
||||
import (
|
||||
pathpkg "path"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// An RWValue wraps a value and permits mutually exclusive
|
||||
// access to it and records the time the value was last set.
|
||||
type RWValue struct {
|
||||
mutex sync.RWMutex
|
||||
value interface{}
|
||||
timestamp time.Time // time of last set()
|
||||
}
|
||||
|
||||
func (v *RWValue) Set(value interface{}) {
|
||||
v.mutex.Lock()
|
||||
v.value = value
|
||||
v.timestamp = time.Now()
|
||||
v.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (v *RWValue) Get() (interface{}, time.Time) {
|
||||
v.mutex.RLock()
|
||||
defer v.mutex.RUnlock()
|
||||
return v.value, v.timestamp
|
||||
}
|
||||
|
||||
// IsText reports whether a significant prefix of s looks like correct UTF-8;
|
||||
// that is, if it is likely that s is human-readable text.
|
||||
func IsText(s []byte) bool {
|
||||
const max = 1024 // at least utf8.UTFMax
|
||||
if len(s) > max {
|
||||
s = s[0:max]
|
||||
}
|
||||
for i, c := range string(s) {
|
||||
if i+utf8.UTFMax > len(s) {
|
||||
// last char may be incomplete - ignore
|
||||
break
|
||||
}
|
||||
if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' {
|
||||
// decoding error or control character - not a text file
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// textExt[x] is true if the extension x indicates a text file, and false otherwise.
|
||||
var textExt = map[string]bool{
|
||||
".css": false, // must be served raw
|
||||
".js": false, // must be served raw
|
||||
".svg": false, // must be served raw
|
||||
}
|
||||
|
||||
// IsTextFile reports whether the file has a known extension indicating
|
||||
// a text file, or if a significant chunk of the specified file looks like
|
||||
// correct UTF-8; that is, if it is likely that the file contains human-
|
||||
// readable text.
|
||||
func IsTextFile(fs vfs.Opener, filename string) bool {
|
||||
// if the extension is known, use it for decision making
|
||||
if isText, found := textExt[pathpkg.Ext(filename)]; found {
|
||||
return isText
|
||||
}
|
||||
|
||||
// the extension is not known; read an initial chunk
|
||||
// of the file and check if it looks like text
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var buf [1024]byte
|
||||
n, err := f.Read(buf[0:])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsText(buf[0:n])
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// This file caches information about which standard library types, methods,
|
||||
// and functions appeared in what version of Go
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// apiVersions is a map of packages to information about those packages'
|
||||
// symbols and when they were added to Go.
|
||||
//
|
||||
// Only things added after Go1 are tracked. Version strings are of the
|
||||
// form "1.1", "1.2", etc.
|
||||
type apiVersions map[string]pkgAPIVersions // keyed by Go package ("net/http")
|
||||
|
||||
// pkgAPIVersions contains information about which version of Go added
|
||||
// certain package symbols.
|
||||
//
|
||||
// Only things added after Go1 are tracked. Version strings are of the
|
||||
// form "1.1", "1.2", etc.
|
||||
type pkgAPIVersions struct {
|
||||
typeSince map[string]string // "Server" -> "1.7"
|
||||
methodSince map[string]map[string]string // "*Server" ->"Shutdown"->1.8
|
||||
funcSince map[string]string // "NewServer" -> "1.7"
|
||||
fieldSince map[string]map[string]string // "ClientTrace" -> "Got1xxResponse" -> "1.11"
|
||||
}
|
||||
|
||||
// sinceVersionFunc returns a string (such as "1.7") specifying which Go
|
||||
// version introduced a symbol, unless it was introduced in Go1, in
|
||||
// which case it returns the empty string.
|
||||
//
|
||||
// The kind is one of "type", "method", or "func".
|
||||
//
|
||||
// The receiver is only used for "methods" and specifies the receiver type,
|
||||
// such as "*Server".
|
||||
//
|
||||
// The name is the symbol name ("Server") and the pkg is the package
|
||||
// ("net/http").
|
||||
func (v apiVersions) sinceVersionFunc(kind, receiver, name, pkg string) string {
|
||||
pv := v[pkg]
|
||||
switch kind {
|
||||
case "func":
|
||||
return pv.funcSince[name]
|
||||
case "type":
|
||||
return pv.typeSince[name]
|
||||
case "method":
|
||||
return pv.methodSince[receiver][name]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// versionedRow represents an API feature, a parsed line of a
|
||||
// $GOROOT/api/go.*txt file.
|
||||
type versionedRow struct {
|
||||
pkg string // "net/http"
|
||||
kind string // "type", "func", "method", "field" TODO: "const", "var"
|
||||
recv string // for methods, the receiver type ("Server", "*Server")
|
||||
name string // name of type, (struct) field, func, method
|
||||
structName string // for struct fields, the outer struct name
|
||||
}
|
||||
|
||||
// versionParser parses $GOROOT/api/go*.txt files and stores them in its rows field.
|
||||
type versionParser struct {
|
||||
res apiVersions // initialized lazily
|
||||
}
|
||||
|
||||
// parseFile parses the named $GOROOT/api/goVERSION.txt file.
|
||||
//
|
||||
// For each row, it updates the corresponding entry in
|
||||
// vp.res to VERSION, overwriting any previous value.
|
||||
// As a special case, if goVERSION is "go1", it deletes
|
||||
// from the map instead.
|
||||
func (vp *versionParser) parseFile(name string) error {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
base := filepath.Base(name)
|
||||
ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go")
|
||||
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
row, ok := parseRow(sc.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if vp.res == nil {
|
||||
vp.res = make(apiVersions)
|
||||
}
|
||||
pkgi, ok := vp.res[row.pkg]
|
||||
if !ok {
|
||||
pkgi = pkgAPIVersions{
|
||||
typeSince: make(map[string]string),
|
||||
methodSince: make(map[string]map[string]string),
|
||||
funcSince: make(map[string]string),
|
||||
fieldSince: make(map[string]map[string]string),
|
||||
}
|
||||
vp.res[row.pkg] = pkgi
|
||||
}
|
||||
switch row.kind {
|
||||
case "func":
|
||||
if ver == "1" {
|
||||
delete(pkgi.funcSince, row.name)
|
||||
break
|
||||
}
|
||||
pkgi.funcSince[row.name] = ver
|
||||
case "type":
|
||||
if ver == "1" {
|
||||
delete(pkgi.typeSince, row.name)
|
||||
break
|
||||
}
|
||||
pkgi.typeSince[row.name] = ver
|
||||
case "method":
|
||||
if ver == "1" {
|
||||
delete(pkgi.methodSince[row.recv], row.name)
|
||||
break
|
||||
}
|
||||
if _, ok := pkgi.methodSince[row.recv]; !ok {
|
||||
pkgi.methodSince[row.recv] = make(map[string]string)
|
||||
}
|
||||
pkgi.methodSince[row.recv][row.name] = ver
|
||||
case "field":
|
||||
if ver == "1" {
|
||||
delete(pkgi.fieldSince[row.structName], row.name)
|
||||
break
|
||||
}
|
||||
if _, ok := pkgi.fieldSince[row.structName]; !ok {
|
||||
pkgi.fieldSince[row.structName] = make(map[string]string)
|
||||
}
|
||||
pkgi.fieldSince[row.structName][row.name] = ver
|
||||
}
|
||||
}
|
||||
return sc.Err()
|
||||
}
|
||||
|
||||
func parseRow(s string) (vr versionedRow, ok bool) {
|
||||
if !strings.HasPrefix(s, "pkg ") {
|
||||
// Skip comments, blank lines, etc.
|
||||
return
|
||||
}
|
||||
rest := s[len("pkg "):]
|
||||
endPkg := strings.IndexFunc(rest, func(r rune) bool { return !(unicode.IsLetter(r) || r == '/' || unicode.IsDigit(r)) })
|
||||
if endPkg == -1 {
|
||||
return
|
||||
}
|
||||
vr.pkg, rest = rest[:endPkg], rest[endPkg:]
|
||||
if !strings.HasPrefix(rest, ", ") {
|
||||
// If the part after the pkg name isn't ", ", then it's a OS/ARCH-dependent line of the form:
|
||||
// pkg syscall (darwin-amd64), const ImplementsGetwd = false
|
||||
// We skip those for now.
|
||||
return
|
||||
}
|
||||
rest = rest[len(", "):]
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(rest, "type "):
|
||||
rest = rest[len("type "):]
|
||||
sp := strings.IndexByte(rest, ' ')
|
||||
if sp == -1 {
|
||||
return
|
||||
}
|
||||
vr.name, rest = rest[:sp], rest[sp+1:]
|
||||
if !strings.HasPrefix(rest, "struct, ") {
|
||||
vr.kind = "type"
|
||||
return vr, true
|
||||
}
|
||||
rest = rest[len("struct, "):]
|
||||
if i := strings.IndexByte(rest, ' '); i != -1 {
|
||||
vr.kind = "field"
|
||||
vr.structName = vr.name
|
||||
vr.name = rest[:i]
|
||||
return vr, true
|
||||
}
|
||||
case strings.HasPrefix(rest, "func "):
|
||||
vr.kind = "func"
|
||||
rest = rest[len("func "):]
|
||||
if i := strings.IndexByte(rest, '('); i != -1 {
|
||||
vr.name = rest[:i]
|
||||
return vr, true
|
||||
}
|
||||
case strings.HasPrefix(rest, "method "): // "method (*File) SetModTime(time.Time)"
|
||||
vr.kind = "method"
|
||||
rest = rest[len("method "):] // "(*File) SetModTime(time.Time)"
|
||||
sp := strings.IndexByte(rest, ' ')
|
||||
if sp == -1 {
|
||||
return
|
||||
}
|
||||
vr.recv = strings.Trim(rest[:sp], "()") // "*File"
|
||||
rest = rest[sp+1:] // SetMode(os.FileMode)
|
||||
paren := strings.IndexByte(rest, '(')
|
||||
if paren == -1 {
|
||||
return
|
||||
}
|
||||
vr.name = rest[:paren]
|
||||
return vr, true
|
||||
}
|
||||
return // TODO: handle more cases
|
||||
}
|
||||
|
||||
// InitVersionInfo parses the $GOROOT/api/go*.txt API definition files to discover
|
||||
// which API features were added in which Go releases.
|
||||
func (c *Corpus) InitVersionInfo() {
|
||||
var err error
|
||||
c.pkgAPIInfo, err = parsePackageAPIInfo()
|
||||
if err != nil {
|
||||
// TODO: consider making this fatal, after the Go 1.11 cycle.
|
||||
log.Printf("godoc: error parsing API version files: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parsePackageAPIInfo() (apiVersions, error) {
|
||||
var apiGlob string
|
||||
if os.Getenv("GOROOT") == "" {
|
||||
apiGlob = filepath.Join(build.Default.GOROOT, "api", "go*.txt")
|
||||
} else {
|
||||
apiGlob = filepath.Join(os.Getenv("GOROOT"), "api", "go*.txt")
|
||||
}
|
||||
|
||||
files, err := filepath.Glob(apiGlob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process files in go1.n, go1.n-1, ..., go1.2, go1.1, go1 order.
|
||||
//
|
||||
// It's rare, but the signature of an identifier may change
|
||||
// (for example, a function that accepts a type replaced with
|
||||
// an alias), and so an existing symbol may show up again in
|
||||
// a later api/go1.N.txt file. Parsing in reverse version
|
||||
// order means we end up with the earliest version of Go
|
||||
// when the symbol was added. See golang.org/issue/44081.
|
||||
//
|
||||
ver := func(name string) int {
|
||||
base := filepath.Base(name)
|
||||
ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go1.")
|
||||
if ver == "go1" {
|
||||
return 0
|
||||
}
|
||||
v, _ := strconv.Atoi(ver)
|
||||
return v
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool { return ver(files[i]) > ver(files[j]) })
|
||||
vp := new(versionParser)
|
||||
for _, f := range files {
|
||||
if err := vp.parseFile(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return vp.res, nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVersionRow(t *testing.T) {
|
||||
tests := []struct {
|
||||
row string
|
||||
want versionedRow
|
||||
}{
|
||||
{
|
||||
row: "# comment",
|
||||
},
|
||||
{
|
||||
row: "",
|
||||
},
|
||||
{
|
||||
row: "pkg archive/tar, type Writer struct",
|
||||
want: versionedRow{
|
||||
pkg: "archive/tar",
|
||||
kind: "type",
|
||||
name: "Writer",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg archive/tar, type Header struct, AccessTime time.Time",
|
||||
want: versionedRow{
|
||||
pkg: "archive/tar",
|
||||
kind: "field",
|
||||
structName: "Header",
|
||||
name: "AccessTime",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg archive/tar, method (*Reader) Read([]uint8) (int, error)",
|
||||
want: versionedRow{
|
||||
pkg: "archive/tar",
|
||||
kind: "method",
|
||||
name: "Read",
|
||||
recv: "*Reader",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg archive/zip, func FileInfoHeader(os.FileInfo) (*FileHeader, error)",
|
||||
want: versionedRow{
|
||||
pkg: "archive/zip",
|
||||
kind: "func",
|
||||
name: "FileInfoHeader",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg encoding/base32, method (Encoding) WithPadding(int32) *Encoding",
|
||||
want: versionedRow{
|
||||
pkg: "encoding/base32",
|
||||
kind: "method",
|
||||
name: "WithPadding",
|
||||
recv: "Encoding",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got, ok := parseRow(tt.row)
|
||||
if !ok {
|
||||
got = versionedRow{}
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("%d. parseRow(%q) = %+v; want %+v", i, tt.row, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasTag checks whether a given release tag is contained in the current version
|
||||
// of the go binary.
|
||||
func hasTag(t string) bool {
|
||||
for _, v := range build.Default.ReleaseTags {
|
||||
if t == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestAPIVersion(t *testing.T) {
|
||||
av, err := parsePackageAPIInfo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
kind string
|
||||
pkg string
|
||||
name string
|
||||
receiver string
|
||||
want string
|
||||
}{
|
||||
// Things that were added post-1.0 should appear
|
||||
{"func", "archive/tar", "FileInfoHeader", "", "1.1"},
|
||||
{"type", "bufio", "Scanner", "", "1.1"},
|
||||
{"method", "bufio", "WriteTo", "*Reader", "1.1"},
|
||||
|
||||
{"func", "bytes", "LastIndexByte", "", "1.5"},
|
||||
{"type", "crypto", "Decrypter", "", "1.5"},
|
||||
{"method", "crypto/rsa", "Decrypt", "*PrivateKey", "1.5"},
|
||||
{"method", "debug/dwarf", "GoString", "Class", "1.5"},
|
||||
|
||||
{"func", "os", "IsTimeout", "", "1.10"},
|
||||
{"type", "strings", "Builder", "", "1.10"},
|
||||
{"method", "strings", "WriteString", "*Builder", "1.10"},
|
||||
|
||||
// Should get the earliest Go version when an identifier
|
||||
// was initially added, rather than a later version when
|
||||
// it may have been updated. See issue 44081.
|
||||
{"func", "os", "Chmod", "", ""}, // Go 1 era function, updated in Go 1.16.
|
||||
{"method", "os", "Readdir", "*File", ""}, // Go 1 era method, updated in Go 1.16.
|
||||
{"method", "os", "ReadDir", "*File", "1.16"}, // New to Go 1.16.
|
||||
|
||||
// Things from package syscall should never appear
|
||||
{"func", "syscall", "FchFlags", "", ""},
|
||||
{"type", "syscall", "Inet4Pktinfo", "", ""},
|
||||
|
||||
// Things added in Go 1 should never appear
|
||||
{"func", "archive/tar", "NewReader", "", ""},
|
||||
{"type", "archive/tar", "Header", "", ""},
|
||||
{"method", "archive/tar", "Next", "*Reader", ""},
|
||||
} {
|
||||
if tc.want != "" && !hasTag("go"+tc.want) {
|
||||
continue
|
||||
}
|
||||
if got := av.sinceVersionFunc(tc.kind, tc.receiver, tc.name, tc.pkg); got != tc.want {
|
||||
t.Errorf(`sinceFunc("%s", "%s", "%s", "%s") = "%s"; want "%s"`, tc.kind, tc.receiver, tc.name, tc.pkg, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2016 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 vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewNameSpace returns a NameSpace pre-initialized with an empty
|
||||
// emulated directory mounted on the root mount point "/". This
|
||||
// allows directory traversal routines to work properly even if
|
||||
// a folder is not explicitly mounted at root by the user.
|
||||
func NewNameSpace() NameSpace {
|
||||
ns := NameSpace{}
|
||||
ns.Bind("/", &emptyVFS{}, "/", BindReplace)
|
||||
return ns
|
||||
}
|
||||
|
||||
// type emptyVFS emulates a FileSystem consisting of an empty directory
|
||||
type emptyVFS struct{}
|
||||
|
||||
// Open implements Opener. Since emptyVFS is an empty directory, all
|
||||
// attempts to open a file should returns errors.
|
||||
func (e *emptyVFS) Open(path string) (ReadSeekCloser, error) {
|
||||
if path == "/" {
|
||||
return nil, fmt.Errorf("open: / is a directory")
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// Stat returns os.FileInfo for an empty directory if the path
|
||||
// is root "/" or error. os.FileInfo is implemented by emptyVFS
|
||||
func (e *emptyVFS) Stat(path string) (os.FileInfo, error) {
|
||||
if path == "/" {
|
||||
return e, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Lstat(path string) (os.FileInfo, error) {
|
||||
return e.Stat(path)
|
||||
}
|
||||
|
||||
// ReadDir returns an empty os.FileInfo slice for "/", else error.
|
||||
func (e *emptyVFS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
if path == "/" {
|
||||
return []os.FileInfo{}, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (e *emptyVFS) String() string {
|
||||
return "emptyVFS(/)"
|
||||
}
|
||||
|
||||
func (e *emptyVFS) RootType(path string) RootType {
|
||||
return ""
|
||||
}
|
||||
|
||||
// These functions below implement os.FileInfo for the single
|
||||
// empty emulated directory.
|
||||
|
||||
func (e *emptyVFS) Name() string {
|
||||
return "/"
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Mode() os.FileMode {
|
||||
return os.ModeDir | os.ModePerm
|
||||
}
|
||||
|
||||
func (e *emptyVFS) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (e *emptyVFS) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FromFS converts an fs.FS to the FileSystem interface.
|
||||
func FromFS(fsys fs.FS) FileSystem {
|
||||
return &fsysToFileSystem{fsys}
|
||||
}
|
||||
|
||||
type fsysToFileSystem struct {
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
func (f *fsysToFileSystem) fsPath(name string) string {
|
||||
name = path.Clean(name)
|
||||
if name == "/" {
|
||||
return "."
|
||||
}
|
||||
return strings.TrimPrefix(name, "/")
|
||||
}
|
||||
|
||||
func (f *fsysToFileSystem) Open(name string) (ReadSeekCloser, error) {
|
||||
file, err := f.fsys.Open(f.fsPath(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rsc, ok := file.(ReadSeekCloser); ok {
|
||||
return rsc, nil
|
||||
}
|
||||
return &noSeekFile{f.fsPath(name), file}, nil
|
||||
}
|
||||
|
||||
func (f *fsysToFileSystem) Lstat(name string) (os.FileInfo, error) {
|
||||
return fs.Stat(f.fsys, f.fsPath(name))
|
||||
}
|
||||
|
||||
func (f *fsysToFileSystem) Stat(name string) (os.FileInfo, error) {
|
||||
return fs.Stat(f.fsys, f.fsPath(name))
|
||||
}
|
||||
|
||||
func (f *fsysToFileSystem) RootType(name string) RootType { return "" }
|
||||
|
||||
func (f *fsysToFileSystem) ReadDir(name string) ([]os.FileInfo, error) {
|
||||
dirs, err := fs.ReadDir(f.fsys, f.fsPath(name))
|
||||
var infos []os.FileInfo
|
||||
for _, d := range dirs {
|
||||
info, err1 := d.Info()
|
||||
if err1 != nil {
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
continue
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos, err
|
||||
}
|
||||
|
||||
func (f *fsysToFileSystem) String() string { return "io/fs" }
|
||||
|
||||
type noSeekFile struct {
|
||||
path string
|
||||
fs.File
|
||||
}
|
||||
|
||||
func (f *noSeekFile) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package gatefs provides an implementation of the FileSystem
|
||||
// interface that wraps another FileSystem and limits its concurrency.
|
||||
package gatefs // import "golang.org/x/tools/godoc/vfs/gatefs"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// New returns a new FileSystem that delegates to fs.
|
||||
// If gateCh is non-nil and buffered, it's used as a gate
|
||||
// to limit concurrency on calls to fs.
|
||||
func New(fs vfs.FileSystem, gateCh chan bool) vfs.FileSystem {
|
||||
if cap(gateCh) == 0 {
|
||||
return fs
|
||||
}
|
||||
return gatefs{fs, gate(gateCh)}
|
||||
}
|
||||
|
||||
type gate chan bool
|
||||
|
||||
func (g gate) enter() { g <- true }
|
||||
func (g gate) leave() { <-g }
|
||||
|
||||
type gatefs struct {
|
||||
fs vfs.FileSystem
|
||||
gate
|
||||
}
|
||||
|
||||
func (fs gatefs) String() string {
|
||||
return fmt.Sprintf("gated(%s, %d)", fs.fs.String(), cap(fs.gate))
|
||||
}
|
||||
|
||||
func (fs gatefs) RootType(path string) vfs.RootType {
|
||||
return fs.fs.RootType(path)
|
||||
}
|
||||
|
||||
func (fs gatefs) Open(p string) (vfs.ReadSeekCloser, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
rsc, err := fs.fs.Open(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gatef{rsc, fs.gate}, nil
|
||||
}
|
||||
|
||||
func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
return fs.fs.Lstat(p)
|
||||
}
|
||||
|
||||
func (fs gatefs) Stat(p string) (os.FileInfo, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
return fs.fs.Stat(p)
|
||||
}
|
||||
|
||||
func (fs gatefs) ReadDir(p string) ([]os.FileInfo, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
return fs.fs.ReadDir(p)
|
||||
}
|
||||
|
||||
type gatef struct {
|
||||
rsc vfs.ReadSeekCloser
|
||||
gate
|
||||
}
|
||||
|
||||
func (f gatef) Read(p []byte) (n int, err error) {
|
||||
f.enter()
|
||||
defer f.leave()
|
||||
return f.rsc.Read(p)
|
||||
}
|
||||
|
||||
func (f gatef) Seek(offset int64, whence int) (ret int64, err error) {
|
||||
f.enter()
|
||||
defer f.leave()
|
||||
return f.rsc.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f gatef) Close() error {
|
||||
f.enter()
|
||||
defer f.leave()
|
||||
return f.rsc.Close()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018 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 gatefs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
"golang.org/x/tools/godoc/vfs/gatefs"
|
||||
)
|
||||
|
||||
func TestRootType(t *testing.T) {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
var expectedType vfs.RootType
|
||||
if goPath == "" {
|
||||
expectedType = ""
|
||||
} else {
|
||||
expectedType = vfs.RootTypeGoPath
|
||||
}
|
||||
tests := []struct {
|
||||
path string
|
||||
fsType vfs.RootType
|
||||
}{
|
||||
{runtime.GOROOT(), vfs.RootTypeGoRoot},
|
||||
{goPath, expectedType},
|
||||
{"/tmp/", ""},
|
||||
}
|
||||
|
||||
for _, item := range tests {
|
||||
fs := gatefs.New(vfs.OS(item.path), make(chan bool, 1))
|
||||
if fs.RootType("path") != item.fsType {
|
||||
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType("path"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package httpfs implements http.FileSystem using a godoc vfs.FileSystem.
|
||||
package httpfs // import "golang.org/x/tools/godoc/vfs/httpfs"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
func New(fs vfs.FileSystem) http.FileSystem {
|
||||
return &httpFS{fs}
|
||||
}
|
||||
|
||||
type httpFS struct {
|
||||
fs vfs.FileSystem
|
||||
}
|
||||
|
||||
func (h *httpFS) Open(name string) (http.File, error) {
|
||||
fi, err := h.fs.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return &httpDir{h.fs, name, nil}, nil
|
||||
}
|
||||
f, err := h.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpFile{h.fs, f, name}, nil
|
||||
}
|
||||
|
||||
// httpDir implements http.File for a directory in a FileSystem.
|
||||
type httpDir struct {
|
||||
fs vfs.FileSystem
|
||||
name string
|
||||
pending []os.FileInfo
|
||||
}
|
||||
|
||||
func (h *httpDir) Close() error { return nil }
|
||||
func (h *httpDir) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
|
||||
func (h *httpDir) Read([]byte) (int, error) {
|
||||
return 0, fmt.Errorf("cannot Read from directory %s", h.name)
|
||||
}
|
||||
|
||||
func (h *httpDir) Seek(offset int64, whence int) (int64, error) {
|
||||
if offset == 0 && whence == 0 {
|
||||
h.pending = nil
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported Seek in directory %s", h.name)
|
||||
}
|
||||
|
||||
func (h *httpDir) Readdir(count int) ([]os.FileInfo, error) {
|
||||
if h.pending == nil {
|
||||
d, err := h.fs.ReadDir(h.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d == nil {
|
||||
d = []os.FileInfo{} // not nil
|
||||
}
|
||||
h.pending = d
|
||||
}
|
||||
|
||||
if len(h.pending) == 0 && count > 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
if count <= 0 || count > len(h.pending) {
|
||||
count = len(h.pending)
|
||||
}
|
||||
d := h.pending[:count]
|
||||
h.pending = h.pending[count:]
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// httpFile implements http.File for a file (not directory) in a FileSystem.
|
||||
type httpFile struct {
|
||||
fs vfs.FileSystem
|
||||
vfs.ReadSeekCloser
|
||||
name string
|
||||
}
|
||||
|
||||
func (h *httpFile) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
|
||||
func (h *httpFile) Readdir(int) ([]os.FileInfo, error) {
|
||||
return nil, fmt.Errorf("cannot Readdir from file %s", h.name)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package mapfs file provides an implementation of the FileSystem
|
||||
// interface based on the contents of a map[string]string.
|
||||
package mapfs // import "golang.org/x/tools/godoc/vfs/mapfs"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// New returns a new FileSystem from the provided map.
|
||||
// Map keys must be forward slash-separated paths with
|
||||
// no leading slash, such as "file1.txt" or "dir/file2.txt".
|
||||
// New panics if any of the paths contain a leading slash.
|
||||
func New(m map[string]string) vfs.FileSystem {
|
||||
// Verify all provided paths are relative before proceeding.
|
||||
var pathsWithLeadingSlash []string
|
||||
for p := range m {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
pathsWithLeadingSlash = append(pathsWithLeadingSlash, p)
|
||||
}
|
||||
}
|
||||
if len(pathsWithLeadingSlash) > 0 {
|
||||
panic(fmt.Errorf("mapfs.New: invalid paths with a leading slash: %q", pathsWithLeadingSlash))
|
||||
}
|
||||
|
||||
return mapFS(m)
|
||||
}
|
||||
|
||||
// mapFS is the map based implementation of FileSystem
|
||||
type mapFS map[string]string
|
||||
|
||||
func (fs mapFS) String() string { return "mapfs" }
|
||||
|
||||
func (fs mapFS) RootType(p string) vfs.RootType {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fs mapFS) Close() error { return nil }
|
||||
|
||||
func filename(p string) string {
|
||||
return strings.TrimPrefix(p, "/")
|
||||
}
|
||||
|
||||
func (fs mapFS) Open(p string) (vfs.ReadSeekCloser, error) {
|
||||
b, ok := fs[filename(p)]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return nopCloser{strings.NewReader(b)}, nil
|
||||
}
|
||||
|
||||
func fileInfo(name, contents string) os.FileInfo {
|
||||
return mapFI{name: pathpkg.Base(name), size: len(contents)}
|
||||
}
|
||||
|
||||
func dirInfo(name string) os.FileInfo {
|
||||
return mapFI{name: pathpkg.Base(name), dir: true}
|
||||
}
|
||||
|
||||
func (fs mapFS) Lstat(p string) (os.FileInfo, error) {
|
||||
b, ok := fs[filename(p)]
|
||||
if ok {
|
||||
return fileInfo(p, b), nil
|
||||
}
|
||||
ents, _ := fs.ReadDir(p)
|
||||
if len(ents) > 0 {
|
||||
return dirInfo(p), nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (fs mapFS) Stat(p string) (os.FileInfo, error) {
|
||||
return fs.Lstat(p)
|
||||
}
|
||||
|
||||
// slashdir returns path.Dir(p), but special-cases paths not beginning
|
||||
// with a slash to be in the root.
|
||||
func slashdir(p string) string {
|
||||
d := pathpkg.Dir(p)
|
||||
if d == "." {
|
||||
return "/"
|
||||
}
|
||||
if strings.HasPrefix(p, "/") {
|
||||
return d
|
||||
}
|
||||
return "/" + d
|
||||
}
|
||||
|
||||
func (fs mapFS) ReadDir(p string) ([]os.FileInfo, error) {
|
||||
p = pathpkg.Clean(p)
|
||||
var ents []string
|
||||
fim := make(map[string]os.FileInfo) // base -> fi
|
||||
for fn, b := range fs {
|
||||
dir := slashdir(fn)
|
||||
isFile := true
|
||||
var lastBase string
|
||||
for {
|
||||
if dir == p {
|
||||
base := lastBase
|
||||
if isFile {
|
||||
base = pathpkg.Base(fn)
|
||||
}
|
||||
if fim[base] == nil {
|
||||
var fi os.FileInfo
|
||||
if isFile {
|
||||
fi = fileInfo(fn, b)
|
||||
} else {
|
||||
fi = dirInfo(base)
|
||||
}
|
||||
ents = append(ents, base)
|
||||
fim[base] = fi
|
||||
}
|
||||
}
|
||||
if dir == "/" {
|
||||
break
|
||||
} else {
|
||||
isFile = false
|
||||
lastBase = pathpkg.Base(dir)
|
||||
dir = pathpkg.Dir(dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ents) == 0 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
sort.Strings(ents)
|
||||
var list []os.FileInfo
|
||||
for _, dir := range ents {
|
||||
list = append(list, fim[dir])
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// mapFI is the map-based implementation of FileInfo.
|
||||
type mapFI struct {
|
||||
name string
|
||||
size int
|
||||
dir bool
|
||||
}
|
||||
|
||||
func (fi mapFI) IsDir() bool { return fi.dir }
|
||||
func (fi mapFI) ModTime() time.Time { return time.Time{} }
|
||||
func (fi mapFI) Mode() os.FileMode {
|
||||
if fi.IsDir() {
|
||||
return 0755 | os.ModeDir
|
||||
}
|
||||
return 0444
|
||||
}
|
||||
func (fi mapFI) Name() string { return pathpkg.Base(fi.name) }
|
||||
func (fi mapFI) Size() int64 { return int64(fi.size) }
|
||||
func (fi mapFI) Sys() interface{} { return nil }
|
||||
|
||||
type nopCloser struct {
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
func (nc nopCloser) Close() error { return nil }
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mapfs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenRoot(t *testing.T) {
|
||||
fs := New(map[string]string{
|
||||
"foo/bar/three.txt": "a",
|
||||
"foo/bar.txt": "b",
|
||||
"top.txt": "c",
|
||||
"other-top.txt": "d",
|
||||
})
|
||||
tests := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"/foo/bar/three.txt", "a"},
|
||||
{"foo/bar/three.txt", "a"},
|
||||
{"foo/bar.txt", "b"},
|
||||
{"top.txt", "c"},
|
||||
{"/top.txt", "c"},
|
||||
{"other-top.txt", "d"},
|
||||
{"/other-top.txt", "d"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
rsc, err := fs.Open(tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("Open(%q) = %v", tt.path, err)
|
||||
continue
|
||||
}
|
||||
slurp, err := io.ReadAll(rsc)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(slurp) != tt.want {
|
||||
t.Errorf("Read(%q) = %q; want %q", tt.path, tt.want, slurp)
|
||||
}
|
||||
rsc.Close()
|
||||
}
|
||||
|
||||
_, err := fs.Open("/xxxx")
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("ReadDir /xxxx = %v; want os.IsNotExist error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaddir(t *testing.T) {
|
||||
fs := New(map[string]string{
|
||||
"foo/bar/three.txt": "333",
|
||||
"foo/bar.txt": "22",
|
||||
"top.txt": "top.txt file",
|
||||
"other-top.txt": "other-top.txt file",
|
||||
})
|
||||
tests := []struct {
|
||||
dir string
|
||||
want []os.FileInfo
|
||||
}{
|
||||
{
|
||||
dir: "/",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "foo", dir: true},
|
||||
mapFI{name: "other-top.txt", size: len("other-top.txt file")},
|
||||
mapFI{name: "top.txt", size: len("top.txt file")},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "/foo",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "bar", dir: true},
|
||||
mapFI{name: "bar.txt", size: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "/foo/",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "bar", dir: true},
|
||||
mapFI{name: "bar.txt", size: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "/foo/bar",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "three.txt", size: 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
fis, err := fs.ReadDir(tt.dir)
|
||||
if err != nil {
|
||||
t.Errorf("ReadDir(%q) = %v", tt.dir, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(fis, tt.want) {
|
||||
t.Errorf("ReadDir(%q) = %#v; want %#v", tt.dir, fis, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_, err := fs.ReadDir("/xxxx")
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("ReadDir /xxxx = %v; want os.IsNotExist error", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
// Copyright 2011 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 vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Setting debugNS = true will enable debugging prints about
|
||||
// name space translations.
|
||||
const debugNS = false
|
||||
|
||||
// A NameSpace is a file system made up of other file systems
|
||||
// mounted at specific locations in the name space.
|
||||
//
|
||||
// The representation is a map from mount point locations
|
||||
// to the list of file systems mounted at that location. A traditional
|
||||
// Unix mount table would use a single file system per mount point,
|
||||
// but we want to be able to mount multiple file systems on a single
|
||||
// mount point and have the system behave as if the union of those
|
||||
// file systems were present at the mount point.
|
||||
// For example, if the OS file system has a Go installation in
|
||||
// c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then
|
||||
// this name space creates the view we want for the godoc server:
|
||||
//
|
||||
// NameSpace{
|
||||
// "/": {
|
||||
// {old: "/", fs: OS(`c:\Go`), new: "/"},
|
||||
// },
|
||||
// "/src/pkg": {
|
||||
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// This is created by executing:
|
||||
//
|
||||
// ns := NameSpace{}
|
||||
// ns.Bind("/", OS(`c:\Go`), "/", BindReplace)
|
||||
// ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", BindAfter)
|
||||
// ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", BindAfter)
|
||||
//
|
||||
// A particular mount point entry is a triple (old, fs, new), meaning that to
|
||||
// operate on a path beginning with old, replace that prefix (old) with new
|
||||
// and then pass that path to the FileSystem implementation fs.
|
||||
//
|
||||
// If you do not explicitly mount a FileSystem at the root mountpoint "/" of the
|
||||
// NameSpace like above, Stat("/") will return a "not found" error which could
|
||||
// break typical directory traversal routines. In such cases, use NewNameSpace()
|
||||
// to get a NameSpace pre-initialized with an emulated empty directory at root.
|
||||
//
|
||||
// Given this name space, a ReadDir of /src/pkg/code will check each prefix
|
||||
// of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src,
|
||||
// then /), stopping when it finds one. For the above example, /src/pkg/code
|
||||
// will find the mount point at /src/pkg:
|
||||
//
|
||||
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
|
||||
//
|
||||
// ReadDir will when execute these three calls and merge the results:
|
||||
//
|
||||
// OS(`c:\Go`).ReadDir("/src/pkg/code")
|
||||
// OS(`d:\Work1').ReadDir("/src/code")
|
||||
// OS(`d:\Work2').ReadDir("/src/code")
|
||||
//
|
||||
// Note that the "/src/pkg" in "/src/pkg/code" has been replaced by
|
||||
// just "/src" in the final two calls.
|
||||
//
|
||||
// OS is itself an implementation of a file system: it implements
|
||||
// OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`).
|
||||
//
|
||||
// Because the new path is evaluated by fs (here OS(root)), another way
|
||||
// to read the mount table is to mentally combine fs+new, so that this table:
|
||||
//
|
||||
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
|
||||
//
|
||||
// reads as:
|
||||
//
|
||||
// "/src/pkg" -> c:\Go\src\pkg
|
||||
// "/src/pkg" -> d:\Work1\src
|
||||
// "/src/pkg" -> d:\Work2\src
|
||||
//
|
||||
// An invariant (a redundancy) of the name space representation is that
|
||||
// ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s
|
||||
// mount table entries always have old == "/src/pkg"). The 'old' field is
|
||||
// useful to callers, because they receive just a []mountedFS and not any
|
||||
// other indication of which mount point was found.
|
||||
type NameSpace map[string][]mountedFS
|
||||
|
||||
// A mountedFS handles requests for path by replacing
|
||||
// a prefix 'old' with 'new' and then calling the fs methods.
|
||||
type mountedFS struct {
|
||||
old string
|
||||
fs FileSystem
|
||||
new string
|
||||
}
|
||||
|
||||
// hasPathPrefix reports whether x == y or x == y + "/" + more.
|
||||
func hasPathPrefix(x, y string) bool {
|
||||
return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/"))
|
||||
}
|
||||
|
||||
// translate translates path for use in m, replacing old with new.
|
||||
//
|
||||
// mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code".
|
||||
func (m mountedFS) translate(path string) string {
|
||||
path = pathpkg.Clean("/" + path)
|
||||
if !hasPathPrefix(path, m.old) {
|
||||
panic("translate " + path + " but old=" + m.old)
|
||||
}
|
||||
return pathpkg.Join(m.new, path[len(m.old):])
|
||||
}
|
||||
|
||||
func (NameSpace) String() string {
|
||||
return "ns"
|
||||
}
|
||||
|
||||
// Fprint writes a text representation of the name space to w.
|
||||
func (ns NameSpace) Fprint(w io.Writer) {
|
||||
fmt.Fprint(w, "name space {\n")
|
||||
var all []string
|
||||
for mtpt := range ns {
|
||||
all = append(all, mtpt)
|
||||
}
|
||||
sort.Strings(all)
|
||||
for _, mtpt := range all {
|
||||
fmt.Fprintf(w, "\t%s:\n", mtpt)
|
||||
for _, m := range ns[mtpt] {
|
||||
fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, "}\n")
|
||||
}
|
||||
|
||||
// clean returns a cleaned, rooted path for evaluation.
|
||||
// It canonicalizes the path so that we can use string operations
|
||||
// to analyze it.
|
||||
func (NameSpace) clean(path string) string {
|
||||
return pathpkg.Clean("/" + path)
|
||||
}
|
||||
|
||||
type BindMode int
|
||||
|
||||
const (
|
||||
BindReplace BindMode = iota
|
||||
BindBefore
|
||||
BindAfter
|
||||
)
|
||||
|
||||
// Bind causes references to old to redirect to the path new in newfs.
|
||||
// If mode is BindReplace, old redirections are discarded.
|
||||
// If mode is BindBefore, this redirection takes priority over existing ones,
|
||||
// but earlier ones are still consulted for paths that do not exist in newfs.
|
||||
// If mode is BindAfter, this redirection happens only after existing ones
|
||||
// have been tried and failed.
|
||||
func (ns NameSpace) Bind(old string, newfs FileSystem, new string, mode BindMode) {
|
||||
old = ns.clean(old)
|
||||
new = ns.clean(new)
|
||||
m := mountedFS{old, newfs, new}
|
||||
var mtpt []mountedFS
|
||||
switch mode {
|
||||
case BindReplace:
|
||||
mtpt = append(mtpt, m)
|
||||
case BindAfter:
|
||||
mtpt = append(mtpt, ns.resolve(old)...)
|
||||
mtpt = append(mtpt, m)
|
||||
case BindBefore:
|
||||
mtpt = append(mtpt, m)
|
||||
mtpt = append(mtpt, ns.resolve(old)...)
|
||||
}
|
||||
|
||||
// Extend m.old, m.new in inherited mount point entries.
|
||||
for i := range mtpt {
|
||||
m := &mtpt[i]
|
||||
if m.old != old {
|
||||
if !hasPathPrefix(old, m.old) {
|
||||
// This should not happen. If it does, panic so
|
||||
// that we can see the call trace that led to it.
|
||||
panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new))
|
||||
}
|
||||
suffix := old[len(m.old):]
|
||||
m.old = pathpkg.Join(m.old, suffix)
|
||||
m.new = pathpkg.Join(m.new, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
ns[old] = mtpt
|
||||
}
|
||||
|
||||
// resolve resolves a path to the list of mountedFS to use for path.
|
||||
func (ns NameSpace) resolve(path string) []mountedFS {
|
||||
path = ns.clean(path)
|
||||
for {
|
||||
if m := ns[path]; m != nil {
|
||||
if debugNS {
|
||||
fmt.Printf("resolve %s: %v\n", path, m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
if path == "/" {
|
||||
break
|
||||
}
|
||||
path = pathpkg.Dir(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open implements the FileSystem Open method.
|
||||
func (ns NameSpace) Open(path string) (ReadSeekCloser, error) {
|
||||
var err error
|
||||
for _, m := range ns.resolve(path) {
|
||||
if debugNS {
|
||||
fmt.Printf("tx %s: %v\n", path, m.translate(path))
|
||||
}
|
||||
tp := m.translate(path)
|
||||
r, err1 := m.fs.Open(tp)
|
||||
if err1 == nil {
|
||||
return r, nil
|
||||
}
|
||||
// IsNotExist errors in overlay FSes can mask real errors in
|
||||
// the underlying FS, so ignore them if there is another error.
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
err = err1
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// stat implements the FileSystem Stat and Lstat methods.
|
||||
func (ns NameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) {
|
||||
var err error
|
||||
for _, m := range ns.resolve(path) {
|
||||
fi, err1 := f(m.fs, m.translate(path))
|
||||
if err1 == nil {
|
||||
return fi, nil
|
||||
}
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (ns NameSpace) Stat(path string) (os.FileInfo, error) {
|
||||
return ns.stat(path, FileSystem.Stat)
|
||||
}
|
||||
|
||||
func (ns NameSpace) Lstat(path string) (os.FileInfo, error) {
|
||||
return ns.stat(path, FileSystem.Lstat)
|
||||
}
|
||||
|
||||
// dirInfo is a trivial implementation of os.FileInfo for a directory.
|
||||
type dirInfo string
|
||||
|
||||
func (d dirInfo) Name() string { return string(d) }
|
||||
func (d dirInfo) Size() int64 { return 0 }
|
||||
func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 }
|
||||
func (d dirInfo) ModTime() time.Time { return startTime }
|
||||
func (d dirInfo) IsDir() bool { return true }
|
||||
func (d dirInfo) Sys() interface{} { return nil }
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
// ReadDir implements the FileSystem ReadDir method. It's where most of the magic is.
|
||||
// (The rest is in resolve.)
|
||||
//
|
||||
// Logically, ReadDir must return the union of all the directories that are named
|
||||
// by path. In order to avoid misinterpreting Go packages, of all the directories
|
||||
// that contain Go source code, we only include the files from the first,
|
||||
// but we include subdirectories from all.
|
||||
//
|
||||
// ReadDir must also return directory entries needed to reach mount points.
|
||||
// If the name space looks like the example in the type NameSpace comment,
|
||||
// but c:\Go does not have a src/pkg subdirectory, we still want to be able
|
||||
// to find that subdirectory, because we've mounted d:\Work1 and d:\Work2
|
||||
// there. So if we don't see "src" in the directory listing for c:\Go, we add an
|
||||
// entry for it before returning.
|
||||
func (ns NameSpace) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
path = ns.clean(path)
|
||||
|
||||
// List matching directories and determine whether any of them contain
|
||||
// Go files.
|
||||
var (
|
||||
dirs [][]os.FileInfo
|
||||
goDirIndex = -1
|
||||
readDirErr error
|
||||
)
|
||||
|
||||
for _, m := range ns.resolve(path) {
|
||||
dir, err := m.fs.ReadDir(m.translate(path))
|
||||
if err != nil {
|
||||
if readDirErr == nil {
|
||||
readDirErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
dirs = append(dirs, dir)
|
||||
|
||||
if goDirIndex < 0 {
|
||||
for _, f := range dir {
|
||||
if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") {
|
||||
goDirIndex = len(dirs) - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build a list of files and subdirectories. If a directory contains Go files,
|
||||
// only include files from that directory. Otherwise, include files from
|
||||
// all directories. Include subdirectories from all directories regardless
|
||||
// of whether Go files are present.
|
||||
haveName := make(map[string]bool)
|
||||
var all []os.FileInfo
|
||||
for i, dir := range dirs {
|
||||
for _, f := range dir {
|
||||
name := f.Name()
|
||||
if !haveName[name] && (f.IsDir() || goDirIndex < 0 || goDirIndex == i) {
|
||||
all = append(all, f)
|
||||
haveName[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any missing directories needed to reach mount points.
|
||||
for old := range ns {
|
||||
if hasPathPrefix(old, path) && old != path {
|
||||
// Find next element after path in old.
|
||||
elem := old[len(path):]
|
||||
elem = strings.TrimPrefix(elem, "/")
|
||||
if i := strings.Index(elem, "/"); i >= 0 {
|
||||
elem = elem[:i]
|
||||
}
|
||||
if !haveName[elem] {
|
||||
haveName[elem] = true
|
||||
all = append(all, dirInfo(elem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
return nil, readDirErr
|
||||
}
|
||||
|
||||
sort.Sort(byName(all))
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// RootType returns the RootType for the given path in the namespace.
|
||||
func (ns NameSpace) RootType(path string) RootType {
|
||||
// We resolve the given path to a list of mountedFS and then return
|
||||
// the root type for the filesystem which contains the path.
|
||||
for _, m := range ns.resolve(path) {
|
||||
_, err := m.fs.ReadDir(m.translate(path))
|
||||
// Found a match, return the filesystem's root type
|
||||
if err == nil {
|
||||
return m.fs.RootType(path)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// byName implements sort.Interface.
|
||||
type byName []os.FileInfo
|
||||
|
||||
func (f byName) Len() int { return len(f) }
|
||||
func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
|
||||
func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright 2016 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 vfs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
func TestNewNameSpace(t *testing.T) {
|
||||
|
||||
// We will mount this filesystem under /fs1
|
||||
mount := mapfs.New(map[string]string{"fs1file": "abcdefgh"})
|
||||
|
||||
// Existing process. This should give error on Stat("/")
|
||||
t1 := vfs.NameSpace{}
|
||||
t1.Bind("/fs1", mount, "/", vfs.BindReplace)
|
||||
|
||||
// using NewNameSpace. This should work fine.
|
||||
t2 := vfs.NewNameSpace()
|
||||
t2.Bind("/fs1", mount, "/", vfs.BindReplace)
|
||||
|
||||
testcases := map[string][]bool{
|
||||
"/": {false, true},
|
||||
"/fs1": {true, true},
|
||||
"/fs1/fs1file": {true, true},
|
||||
}
|
||||
|
||||
fss := []vfs.FileSystem{t1, t2}
|
||||
|
||||
for j, fs := range fss {
|
||||
for k, v := range testcases {
|
||||
_, err := fs.Stat(k)
|
||||
result := err == nil
|
||||
if result != v[j] {
|
||||
t.Errorf("fs: %d, testcase: %s, want: %v, got: %v, err: %s", j, k, v[j], result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := t2.Stat("/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if fi.Name() != "/" {
|
||||
t.Errorf("t2.Name() : want:%s got:%s", "/", fi.Name())
|
||||
}
|
||||
|
||||
if !fi.ModTime().IsZero() {
|
||||
t.Errorf("t2.ModTime() : want:%v got:%v", time.Time{}, fi.ModTime())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDirUnion(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
ns vfs.NameSpace
|
||||
path, want string
|
||||
}{
|
||||
{
|
||||
desc: "no_go_files",
|
||||
ns: func() vfs.NameSpace {
|
||||
rootFs := mapfs.New(map[string]string{
|
||||
"doc/a.txt": "1",
|
||||
"doc/b.txt": "1",
|
||||
"doc/dir1/d1.txt": "",
|
||||
})
|
||||
docFs := mapfs.New(map[string]string{
|
||||
"doc/a.txt": "22",
|
||||
"doc/dir2/d2.txt": "",
|
||||
})
|
||||
ns := vfs.NameSpace{}
|
||||
ns.Bind("/", rootFs, "/", vfs.BindReplace)
|
||||
ns.Bind("/doc", docFs, "/doc", vfs.BindBefore)
|
||||
return ns
|
||||
}(),
|
||||
path: "/doc",
|
||||
want: "a.txt:2,b.txt:1,dir1:0,dir2:0",
|
||||
}, {
|
||||
desc: "have_go_files",
|
||||
ns: func() vfs.NameSpace {
|
||||
a := mapfs.New(map[string]string{
|
||||
"src/x/a.txt": "",
|
||||
"src/x/suba/sub.txt": "",
|
||||
})
|
||||
b := mapfs.New(map[string]string{
|
||||
"src/x/b.go": "package b",
|
||||
"src/x/subb/sub.txt": "",
|
||||
})
|
||||
c := mapfs.New(map[string]string{
|
||||
"src/x/c.txt": "",
|
||||
"src/x/subc/sub.txt": "",
|
||||
})
|
||||
ns := vfs.NameSpace{}
|
||||
ns.Bind("/", a, "/", vfs.BindReplace)
|
||||
ns.Bind("/", b, "/", vfs.BindAfter)
|
||||
ns.Bind("/", c, "/", vfs.BindAfter)
|
||||
return ns
|
||||
}(),
|
||||
path: "/src/x",
|
||||
want: "b.go:9,suba:0,subb:0,subc:0",
|
||||
}, {
|
||||
desc: "empty_mount",
|
||||
ns: func() vfs.NameSpace {
|
||||
ns := vfs.NameSpace{}
|
||||
ns.Bind("/empty", mapfs.New(nil), "/empty", vfs.BindReplace)
|
||||
return ns
|
||||
}(),
|
||||
path: "/",
|
||||
want: "empty:0",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
fis, err := tc.ns.ReadDir(tc.path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := &strings.Builder{}
|
||||
sep := ""
|
||||
for _, fi := range fis {
|
||||
fmt.Fprintf(buf, "%s%s:%d", sep, fi.Name(), fi.Size())
|
||||
sep = ","
|
||||
}
|
||||
if got := buf.String(); got != tc.want {
|
||||
t.Errorf("got %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// We expose a new variable because otherwise we need to copy the findGOROOT logic again
|
||||
// from cmd/godoc which is already copied twice from the standard library.
|
||||
|
||||
// GOROOT is the GOROOT path under which the godoc binary is running.
|
||||
// It is needed to check whether a filesystem root is under GOROOT or not.
|
||||
// This is set from cmd/godoc/main.go.
|
||||
var GOROOT = runtime.GOROOT()
|
||||
|
||||
// OS returns an implementation of FileSystem reading from the
|
||||
// tree rooted at root. Recording a root is convenient everywhere
|
||||
// but necessary on Windows, because the slash-separated path
|
||||
// passed to Open has no way to specify a drive letter. Using a root
|
||||
// lets code refer to OS(`c:\`), OS(`d:\`) and so on.
|
||||
func OS(root string) FileSystem {
|
||||
var t RootType
|
||||
switch {
|
||||
case root == GOROOT:
|
||||
t = RootTypeGoRoot
|
||||
case isGoPath(root):
|
||||
t = RootTypeGoPath
|
||||
}
|
||||
return osFS{rootPath: root, rootType: t}
|
||||
}
|
||||
|
||||
type osFS struct {
|
||||
rootPath string
|
||||
rootType RootType
|
||||
}
|
||||
|
||||
func isGoPath(path string) bool {
|
||||
for _, bp := range filepath.SplitList(build.Default.GOPATH) {
|
||||
for _, gp := range filepath.SplitList(path) {
|
||||
if bp == gp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (root osFS) String() string { return "os(" + root.rootPath + ")" }
|
||||
|
||||
// RootType returns the root type for the filesystem.
|
||||
//
|
||||
// Note that we ignore the path argument because roottype is a property of
|
||||
// this filesystem. But for other filesystems, the roottype might need to be
|
||||
// dynamically deduced at call time.
|
||||
func (root osFS) RootType(path string) RootType {
|
||||
return root.rootType
|
||||
}
|
||||
|
||||
func (root osFS) resolve(path string) string {
|
||||
// Clean the path so that it cannot possibly begin with ../.
|
||||
// If it did, the result of filepath.Join would be outside the
|
||||
// tree rooted at root. We probably won't ever see a path
|
||||
// with .. in it, but be safe anyway.
|
||||
path = pathpkg.Clean("/" + path)
|
||||
|
||||
return filepath.Join(root.rootPath, path)
|
||||
}
|
||||
|
||||
func (root osFS) Open(path string) (ReadSeekCloser, error) {
|
||||
f, err := os.Open(root.resolve(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("Open: %s is a directory", path)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (root osFS) Lstat(path string) (os.FileInfo, error) {
|
||||
return os.Lstat(root.resolve(path))
|
||||
}
|
||||
|
||||
func (root osFS) Stat(path string) (os.FileInfo, error) {
|
||||
return os.Stat(root.resolve(path))
|
||||
}
|
||||
|
||||
func (root osFS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
return ioutil.ReadDir(root.resolve(path)) // is sorted
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2018 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 vfs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
func TestRootType(t *testing.T) {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
var expectedType vfs.RootType
|
||||
if goPath == "" {
|
||||
expectedType = ""
|
||||
} else {
|
||||
expectedType = vfs.RootTypeGoPath
|
||||
}
|
||||
tests := []struct {
|
||||
path string
|
||||
fsType vfs.RootType
|
||||
}{
|
||||
{runtime.GOROOT(), vfs.RootTypeGoRoot},
|
||||
{goPath, expectedType},
|
||||
{"/tmp/", ""},
|
||||
}
|
||||
|
||||
for _, item := range tests {
|
||||
fs := vfs.OS(item.path)
|
||||
if fs.RootType("path") != item.fsType {
|
||||
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType("path"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package vfs defines types for abstract file system access and provides an
|
||||
// implementation accessing the file system of the underlying OS.
|
||||
package vfs // import "golang.org/x/tools/godoc/vfs"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// RootType indicates the type of files contained within a directory.
|
||||
//
|
||||
// It is used to indicate whether a directory is the root
|
||||
// of a GOROOT, a GOPATH, or neither.
|
||||
// An empty string represents the case when a directory is neither.
|
||||
type RootType string
|
||||
|
||||
const (
|
||||
RootTypeGoRoot RootType = "GOROOT"
|
||||
RootTypeGoPath RootType = "GOPATH"
|
||||
)
|
||||
|
||||
// The FileSystem interface specifies the methods godoc is using
|
||||
// to access the file system for which it serves documentation.
|
||||
type FileSystem interface {
|
||||
Opener
|
||||
Lstat(path string) (os.FileInfo, error)
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
ReadDir(path string) ([]os.FileInfo, error)
|
||||
RootType(path string) RootType
|
||||
String() string
|
||||
}
|
||||
|
||||
// Opener is a minimal virtual filesystem that can only open regular files.
|
||||
type Opener interface {
|
||||
Open(name string) (ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
// A ReadSeekCloser can Read, Seek, and Close.
|
||||
type ReadSeekCloser interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// ReadFile reads the file named by path from fs and returns the contents.
|
||||
func ReadFile(fs Opener, path string) ([]byte, error) {
|
||||
rc, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
return io.ReadAll(rc)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright 2011 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 zipfs file provides an implementation of the FileSystem
|
||||
// interface based on the contents of a .zip file.
|
||||
//
|
||||
// Assumptions:
|
||||
//
|
||||
// - The file paths stored in the zip file must use a slash ('/') as path
|
||||
// separator; and they must be relative (i.e., they must not start with
|
||||
// a '/' - this is usually the case if the file was created w/o special
|
||||
// options).
|
||||
// - The zip file system treats the file paths found in the zip internally
|
||||
// like absolute paths w/o a leading '/'; i.e., the paths are considered
|
||||
// relative to the root of the file system.
|
||||
// - All path arguments to file system methods must be absolute paths.
|
||||
package zipfs // import "golang.org/x/tools/godoc/vfs/zipfs"
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
// zipFI is the zip-file based implementation of FileInfo
|
||||
type zipFI struct {
|
||||
name string // directory-local name
|
||||
file *zip.File // nil for a directory
|
||||
}
|
||||
|
||||
func (fi zipFI) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
|
||||
func (fi zipFI) Size() int64 {
|
||||
if f := fi.file; f != nil {
|
||||
return int64(f.UncompressedSize)
|
||||
}
|
||||
return 0 // directory
|
||||
}
|
||||
|
||||
func (fi zipFI) ModTime() time.Time {
|
||||
if f := fi.file; f != nil {
|
||||
return f.ModTime()
|
||||
}
|
||||
return time.Time{} // directory has no modified time entry
|
||||
}
|
||||
|
||||
func (fi zipFI) Mode() os.FileMode {
|
||||
if fi.file == nil {
|
||||
// Unix directories typically are executable, hence 555.
|
||||
return os.ModeDir | 0555
|
||||
}
|
||||
return 0444
|
||||
}
|
||||
|
||||
func (fi zipFI) IsDir() bool {
|
||||
return fi.file == nil
|
||||
}
|
||||
|
||||
func (fi zipFI) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// zipFS is the zip-file based implementation of FileSystem
|
||||
type zipFS struct {
|
||||
*zip.ReadCloser
|
||||
list zipList
|
||||
name string
|
||||
}
|
||||
|
||||
func (fs *zipFS) String() string {
|
||||
return "zip(" + fs.name + ")"
|
||||
}
|
||||
|
||||
func (fs *zipFS) RootType(abspath string) vfs.RootType {
|
||||
var t vfs.RootType
|
||||
switch {
|
||||
case exists(path.Join(vfs.GOROOT, abspath)):
|
||||
t = vfs.RootTypeGoRoot
|
||||
case isGoPath(abspath):
|
||||
t = vfs.RootTypeGoPath
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func isGoPath(abspath string) bool {
|
||||
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
||||
if exists(path.Join(p, abspath)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (fs *zipFS) Close() error {
|
||||
fs.list = nil
|
||||
return fs.ReadCloser.Close()
|
||||
}
|
||||
|
||||
func zipPath(name string) (string, error) {
|
||||
name = path.Clean(name)
|
||||
if !path.IsAbs(name) {
|
||||
return "", fmt.Errorf("stat: not an absolute path: %s", name)
|
||||
}
|
||||
return name[1:], nil // strip leading '/'
|
||||
}
|
||||
|
||||
func isRoot(abspath string) bool {
|
||||
return path.Clean(abspath) == "/"
|
||||
}
|
||||
|
||||
func (fs *zipFS) stat(abspath string) (int, zipFI, error) {
|
||||
if isRoot(abspath) {
|
||||
return 0, zipFI{
|
||||
name: "",
|
||||
file: nil,
|
||||
}, nil
|
||||
}
|
||||
zippath, err := zipPath(abspath)
|
||||
if err != nil {
|
||||
return 0, zipFI{}, err
|
||||
}
|
||||
i, exact := fs.list.lookup(zippath)
|
||||
if i < 0 {
|
||||
// zippath has leading '/' stripped - print it explicitly
|
||||
return -1, zipFI{}, &os.PathError{Path: "/" + zippath, Err: os.ErrNotExist}
|
||||
}
|
||||
_, name := path.Split(zippath)
|
||||
var file *zip.File
|
||||
if exact {
|
||||
file = fs.list[i] // exact match found - must be a file
|
||||
}
|
||||
return i, zipFI{name, file}, nil
|
||||
}
|
||||
|
||||
func (fs *zipFS) Open(abspath string) (vfs.ReadSeekCloser, error) {
|
||||
_, fi, err := fs.stat(abspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return nil, fmt.Errorf("Open: %s is a directory", abspath)
|
||||
}
|
||||
r, err := fi.file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &zipSeek{fi.file, r}, nil
|
||||
}
|
||||
|
||||
type zipSeek struct {
|
||||
file *zip.File
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (f *zipSeek) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == 0 && offset == 0 {
|
||||
r, err := f.file.Open()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.Close()
|
||||
f.ReadCloser = r
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name)
|
||||
}
|
||||
|
||||
func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) {
|
||||
_, fi, err := fs.stat(abspath)
|
||||
return fi, err
|
||||
}
|
||||
|
||||
func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) {
|
||||
_, fi, err := fs.stat(abspath)
|
||||
return fi, err
|
||||
}
|
||||
|
||||
func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) {
|
||||
i, fi, err := fs.stat(abspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath)
|
||||
}
|
||||
|
||||
var list []os.FileInfo
|
||||
|
||||
// make dirname the prefix that file names must start with to be considered
|
||||
// in this directory. we must special case the root directory because, per
|
||||
// the spec of this package, zip file entries MUST NOT start with /, so we
|
||||
// should not append /, as we would in every other case.
|
||||
var dirname string
|
||||
if isRoot(abspath) {
|
||||
dirname = ""
|
||||
} else {
|
||||
zippath, err := zipPath(abspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirname = zippath + "/"
|
||||
}
|
||||
prevname := ""
|
||||
for _, e := range fs.list[i:] {
|
||||
if !strings.HasPrefix(e.Name, dirname) {
|
||||
break // not in the same directory anymore
|
||||
}
|
||||
name := e.Name[len(dirname):] // local name
|
||||
file := e
|
||||
if i := strings.IndexRune(name, '/'); i >= 0 {
|
||||
// We infer directories from files in subdirectories.
|
||||
// If we have x/y, return a directory entry for x.
|
||||
name = name[0:i] // keep local directory name only
|
||||
file = nil
|
||||
}
|
||||
// If we have x/y and x/z, don't return two directory entries for x.
|
||||
// TODO(gri): It should be possible to do this more efficiently
|
||||
// by determining the (fs.list) range of local directory entries
|
||||
// (via two binary searches).
|
||||
if name != prevname {
|
||||
list = append(list, zipFI{name, file})
|
||||
prevname = name
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func New(rc *zip.ReadCloser, name string) vfs.FileSystem {
|
||||
list := make(zipList, len(rc.File))
|
||||
copy(list, rc.File) // sort a copy of rc.File
|
||||
sort.Sort(list)
|
||||
return &zipFS{rc, list, name}
|
||||
}
|
||||
|
||||
type zipList []*zip.File
|
||||
|
||||
// zipList implements sort.Interface
|
||||
func (z zipList) Len() int { return len(z) }
|
||||
func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name }
|
||||
func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] }
|
||||
|
||||
// lookup returns the smallest index of an entry with an exact match
|
||||
// for name, or an inexact match starting with name/. If there is no
|
||||
// such entry, the result is -1, false.
|
||||
func (z zipList) lookup(name string) (index int, exact bool) {
|
||||
// look for exact match first (name comes before name/ in z)
|
||||
i := sort.Search(len(z), func(i int) bool {
|
||||
return name <= z[i].Name
|
||||
})
|
||||
if i >= len(z) {
|
||||
return -1, false
|
||||
}
|
||||
// 0 <= i < len(z)
|
||||
if z[i].Name == name {
|
||||
return i, true
|
||||
}
|
||||
|
||||
// look for inexact match (must be in z[i:], if present)
|
||||
z = z[i:]
|
||||
name += "/"
|
||||
j := sort.Search(len(z), func(i int) bool {
|
||||
return name <= z[i].Name
|
||||
})
|
||||
if j >= len(z) {
|
||||
return -1, false
|
||||
}
|
||||
// 0 <= j < len(z)
|
||||
if strings.HasPrefix(z[j].Name, name) {
|
||||
return i + j, false
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright 2015 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 zipfs
|
||||
package zipfs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// files to use to build zip used by zipfs in testing; maps path : contents
|
||||
files = map[string]string{"foo": "foo", "bar/baz": "baz", "a/b/c": "c"}
|
||||
|
||||
// expected info for each entry in a file system described by files
|
||||
tests = []struct {
|
||||
Path string
|
||||
IsDir bool
|
||||
IsRegular bool
|
||||
Name string
|
||||
Contents string
|
||||
Files map[string]bool
|
||||
}{
|
||||
{"/", true, false, "", "", map[string]bool{"foo": true, "bar": true, "a": true}},
|
||||
{"//", true, false, "", "", map[string]bool{"foo": true, "bar": true, "a": true}},
|
||||
{"/foo", false, true, "foo", "foo", nil},
|
||||
{"/foo/", false, true, "foo", "foo", nil},
|
||||
{"/foo//", false, true, "foo", "foo", nil},
|
||||
{"/bar", true, false, "bar", "", map[string]bool{"baz": true}},
|
||||
{"/bar/", true, false, "bar", "", map[string]bool{"baz": true}},
|
||||
{"/bar/baz", false, true, "baz", "baz", nil},
|
||||
{"//bar//baz", false, true, "baz", "baz", nil},
|
||||
{"/a/b", true, false, "b", "", map[string]bool{"c": true}},
|
||||
}
|
||||
|
||||
// to be initialized in setup()
|
||||
fs vfs.FileSystem
|
||||
statFuncs []statFunc
|
||||
)
|
||||
|
||||
type statFunc struct {
|
||||
Name string
|
||||
Func func(string) (os.FileInfo, error)
|
||||
}
|
||||
|
||||
func TestMain(t *testing.M) {
|
||||
if err := setup(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error setting up zipfs testing state: %v.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(t.Run())
|
||||
}
|
||||
|
||||
// setups state each of the tests uses
|
||||
func setup() error {
|
||||
// create zipfs
|
||||
b := new(bytes.Buffer)
|
||||
zw := zip.NewWriter(b)
|
||||
for file, contents := range files {
|
||||
w, err := zw.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, contents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
zw.Close()
|
||||
zr, err := zip.NewReader(bytes.NewReader(b.Bytes()), int64(b.Len()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc := &zip.ReadCloser{
|
||||
Reader: *zr,
|
||||
}
|
||||
fs = New(rc, "foo")
|
||||
|
||||
// pull out different stat functions
|
||||
statFuncs = []statFunc{
|
||||
{"Stat", fs.Stat},
|
||||
{"Lstat", fs.Lstat},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestZipFSReadDir(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
if test.IsDir {
|
||||
infos, err := fs.ReadDir(test.Path)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read directory %v\n", test.Path)
|
||||
continue
|
||||
}
|
||||
got := make(map[string]bool)
|
||||
for _, info := range infos {
|
||||
got[info.Name()] = true
|
||||
}
|
||||
if want := test.Files; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ReadDir %v got %v\nwanted %v\n", test.Path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFSStatFuncs(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
for _, statFunc := range statFuncs {
|
||||
|
||||
// test can stat
|
||||
info, err := statFunc.Func(test.Path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error using %v for %v: %v\n", statFunc.Name, test.Path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// test info.Name()
|
||||
if got, want := info.Name(), test.Name; got != want {
|
||||
t.Errorf("Using %v for %v info.Name() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.IsDir()
|
||||
if got, want := info.IsDir(), test.IsDir; got != want {
|
||||
t.Errorf("Using %v for %v info.IsDir() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.Mode().IsDir()
|
||||
if got, want := info.Mode().IsDir(), test.IsDir; got != want {
|
||||
t.Errorf("Using %v for %v info.Mode().IsDir() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.Mode().IsRegular()
|
||||
if got, want := info.Mode().IsRegular(), test.IsRegular; got != want {
|
||||
t.Errorf("Using %v for %v info.Mode().IsRegular() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.Size()
|
||||
if test.IsRegular {
|
||||
if got, want := info.Size(), int64(len(test.Contents)); got != want {
|
||||
t.Errorf("Using %v for %v inf.Size() got %v wanted %v", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFSNotExist(t *testing.T) {
|
||||
_, err := fs.Open("/does-not-exist")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected an error.\n")
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("Expected an error satisfying os.IsNotExist: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFSOpenSeek(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
if test.IsRegular {
|
||||
|
||||
// test Open()
|
||||
f, err := fs.Open(test.Path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// test Seek() multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
all, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if got, want := string(all), test.Contents; got != want {
|
||||
t.Errorf("File contents for %v got %v wanted %v\n", test.Path, got, want)
|
||||
}
|
||||
f.Seek(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootType(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
fsType vfs.RootType
|
||||
}{
|
||||
{"/src/net/http", vfs.RootTypeGoRoot},
|
||||
{"/src/badpath", ""},
|
||||
{"/", vfs.RootTypeGoRoot},
|
||||
}
|
||||
|
||||
for _, item := range tests {
|
||||
if fs.RootType(item.path) != item.fsType {
|
||||
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType(item.path))
|
||||
}
|
||||
}
|
||||
}
|
||||