diff --git a/cmd/godoc/codewalk.go b/cmd/godoc/codewalk.go index 8b151b46..9be894f4 100644 --- a/cmd/godoc/codewalk.go +++ b/cmd/godoc/codewalk.go @@ -43,7 +43,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) { } // If directory exists, serve list of code walks. - dir, err := godoc.FS.Lstat(abspath) + dir, err := fs.Lstat(abspath) if err == nil && dir.IsDir() { codewalkDir(w, r, relpath, abspath) return @@ -51,7 +51,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) { // If file exists, serve using standard file server. if err == nil { - serveFile(w, r) + pres.ServeFile(w, r) return } @@ -117,7 +117,7 @@ func (st *Codestep) String() string { // loadCodewalk reads a codewalk from the named XML file. func loadCodewalk(filename string) (*Codewalk, error) { - f, err := godoc.FS.Open(filename) + f, err := fs.Open(filename) if err != nil { return nil, err } @@ -138,7 +138,7 @@ func loadCodewalk(filename string) (*Codewalk, error) { i = len(st.Src) } filename := st.Src[0:i] - data, err := vfs.ReadFile(godoc.FS, filename) + data, err := vfs.ReadFile(fs, filename) if err != nil { st.Err = err continue @@ -185,7 +185,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string Title string } - dir, err := godoc.FS.ReadDir(abspath) + dir, err := fs.ReadDir(abspath) if err != nil { log.Print(err) pres.ServeError(w, r, relpath, err) @@ -219,7 +219,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string // the usual godoc HTML wrapper. func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) { abspath := f - data, err := vfs.ReadFile(godoc.FS, abspath) + data, err := vfs.ReadFile(fs, abspath) if err != nil { log.Print(err) pres.ServeError(w, r, f, err) diff --git a/cmd/godoc/godoc.go b/cmd/godoc/godoc.go index fa550553..9fc2c8c1 100644 --- a/cmd/godoc/godoc.go +++ b/cmd/godoc/godoc.go @@ -8,7 +8,6 @@ import ( "bytes" "flag" "fmt" - htmlpkg "html" "log" "net/http" "net/url" @@ -19,33 +18,9 @@ import ( "text/template" "code.google.com/p/go.tools/godoc" - "code.google.com/p/go.tools/godoc/util" "code.google.com/p/go.tools/godoc/vfs" ) -// ---------------------------------------------------------------------------- -// Globals - -func flagBool(b *bool, name string, value bool, usage string) interface{} { - flag.BoolVar(b, name, value, usage) - return nil -} - -func flagInt(v *int, name string, value int, usage string) interface{} { - flag.IntVar(v, name, value, usage) - return nil -} - -func flagString(v *string, name string, value string, usage string) interface{} { - flag.StringVar(v, name, value, usage) - return nil -} - -func flagFloat64(v *float64, name string, value float64, usage string) interface{} { - flag.Float64Var(v, name, value, usage) - return nil -} - var ( verbose = flag.Bool("v", false, "verbose mode") @@ -54,27 +29,33 @@ var ( goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") // layout control - _ = flagInt(&godoc.TabWidth, "tabwidth", 4, "tab width") - _ = flagBool(&godoc.ShowTimestamps, "timestamps", false, "show timestamps with directory listings") - templateDir = flag.String("templates", "", "directory containing alternate template files") - _ = flagBool(&godoc.ShowPlayground, "play", false, "enable playground in web interface") - _ = flagBool(&godoc.ShowExamples, "ex", false, "show examples in command line mode") - _ = flagBool(&godoc.DeclLinks, "links", true, "link identifiers to their declarations") + tabWidth = flag.Int("tabwidth", 4, "tab width") + showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings") + templateDir = flag.String("templates", "", "directory containing alternate template files") + showPlayground = flag.Bool("play", false, "enable playground in web interface") + showExamples = flag.Bool("ex", false, "show examples in command line mode") + declLinks = flag.Bool("links", true, "link identifiers to their declarations") // search index indexEnabled = flag.Bool("index", false, "enable search index") indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+ "if not empty, the index is read from these files in sorted order") - _ = flagInt(&godoc.MaxResults, "maxresults", 10000, "maximum number of full text search results shown") - _ = flagFloat64(&godoc.IndexThrottle, "index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") + maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown") + indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") // source code notes - _ = flagString(&godoc.NotesRx, "notes", "BUG", "regular expression matching note markers to show") + notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show") ) -var pres *godoc.Presentation +var ( + pres *godoc.Presentation + fs = vfs.NameSpace{} +) func registerPublicHandlers(mux *http.ServeMux) { + if pres == nil { + panic("nil Presentation") + } godoc.CmdHandler.RegisterWithMux(mux) godoc.PkgHandler.RegisterWithMux(mux) mux.HandleFunc("/doc/codewalk/", codewalk) @@ -82,23 +63,26 @@ func registerPublicHandlers(mux *http.ServeMux) { mux.HandleFunc("/search", search) mux.Handle("/robots.txt", godoc.FileServer) mux.HandleFunc("/opensearch.xml", serveSearchDesc) - mux.HandleFunc("/", serveFile) + mux.HandleFunc("/", pres.ServeFile) } // ---------------------------------------------------------------------------- // Templates func readTemplate(name string) *template.Template { + if pres == nil { + panic("no global Presentation set yet") + } path := "lib/godoc/" + name // use underlying file system fs to read the template file // (cannot use template ParseFile functions directly) - data, err := vfs.ReadFile(godoc.FS, path) + data, err := vfs.ReadFile(fs, path) if err != nil { log.Fatal("readTemplate: ", err) } // be explicit with errors (for app engine use) - t, err := template.New(name).Funcs(godoc.FuncMap).Parse(string(data)) + t, err := template.New(name).Funcs(pres.FuncMap()).Parse(string(data)) if err != nil { log.Fatal("readTemplate: ", err) } @@ -147,125 +131,6 @@ func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) { 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 serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) { - src, err := vfs.ReadFile(godoc.FS, abspath) - if err != nil { - log.Printf("ReadFile: %s", err) - pres.ServeError(w, r, relpath, err) - return - } - - if r.FormValue("m") == "text" { - pres.ServeText(w, src) - return - } - - var buf bytes.Buffer - buf.WriteString("
")
-	godoc.FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), godoc.RangeSelection(r.FormValue("s")))
-	buf.WriteString("
") - fmt.Fprintf(&buf, `

View as plain text

`, htmlpkg.EscapeString(relpath)) - - pres.ServePage(w, godoc.Page{ - Title: title + " " + relpath, - Tabtitle: relpath, - Body: buf.Bytes(), - }) -} - -func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { - if redirect(w, r) { - return - } - - list, err := godoc.FS.ReadDir(abspath) - if err != nil { - pres.ServeError(w, r, relpath, err) - return - } - - pres.ServePage(w, godoc.Page{ - Title: "Directory " + relpath, - Tabtitle: relpath, - Body: applyTemplate(godoc.DirlistHTML, "dirlistHTML", list), - }) -} - -func serveFile(w http.ResponseWriter, r *http.Request) { - relpath := r.URL.Path - - // Check to see if we need to redirect or serve another file. - if m := godoc.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": - if strings.HasSuffix(relpath, "/index.html") { - // We'll show index.html for the directory. - // Use the dir/ version as canonical instead of dir/index.html. - http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) - return - } - pres.ServeHTMLDoc(w, r, abspath, relpath) - return - - case ".go": - serveTextFile(w, r, abspath, relpath, "Source file") - return - } - - dir, err := godoc.FS.Lstat(abspath) - if err != nil { - log.Print(err) - pres.ServeError(w, r, relpath, err) - return - } - - if dir != nil && dir.IsDir() { - if redirect(w, r) { - return - } - if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(godoc.FS, index) { - pres.ServeHTMLDoc(w, r, index, index) - return - } - serveDirectory(w, r, abspath, relpath) - return - } - - if util.IsTextFile(godoc.FS, abspath) { - if redirectFile(w, r) { - return - } - serveTextFile(w, r, abspath, relpath, "Text file") - return - } - - godoc.FileServer.ServeHTTP(w, r) -} - func serveSearchDesc(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/opensearchdescription+xml") data := map[string]interface{}{ @@ -315,14 +180,13 @@ type SearchResult struct { func lookup(query string) (result SearchResult) { result.Query = query - index, timestamp := godoc.SearchIndex.Get() + corp := pres.Corpus + index, timestamp := corp.CurrentIndex() if index != nil { - index := index.(*godoc.Index) - // identifier search var err error result.Pak, result.Hit, result.Alt, err = index.Lookup(query) - if err != nil && godoc.MaxResults <= 0 { + if err != nil && corp.MaxResults <= 0 { // ignore the error if full text search is enabled // since the query may be a valid regular expression result.Alert = "Error in query string: " + err.Error() @@ -330,7 +194,7 @@ func lookup(query string) (result SearchResult) { } // full text search - if godoc.MaxResults > 0 && query != "" { + if corp.MaxResults > 0 && query != "" { rx, err := regexp.Compile(query) if err != nil { result.Alert = "Error in query regular expression: " + err.Error() @@ -340,8 +204,8 @@ func lookup(query string) (result SearchResult) { // 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, godoc.MaxResults+1) - result.Complete = result.Found <= godoc.MaxResults + result.Found, result.Textual = index.LookupRegexp(rx, corp.MaxResults+1) + result.Complete = result.Found <= corp.MaxResults if !result.Complete { result.Found-- // since we looked for maxResults+1 } @@ -350,7 +214,7 @@ func lookup(query string) (result SearchResult) { // is the result accurate? if pres.Corpus.IndexEnabled { - if _, ts := godoc.FSModified.Get(); timestamp.Before(ts) { + if ts := pres.Corpus.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" } diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go index 22be2df4..4b48f273 100644 --- a/cmd/godoc/main.go +++ b/cmd/godoc/main.go @@ -158,9 +158,6 @@ func main() { usage() } - fs := vfs.NameSpace{} - godoc.FS = fs // temp hack - // Determine file system to use. // TODO(gri) - fs and fsHttp should really be the same. Try to unify. // - fsHttp doesn't need to be set up in command-line mode, @@ -186,35 +183,40 @@ func main() { fs.Bind("/src/pkg", vfs.OS(p), "/src", vfs.BindAfter) } - readTemplates() - corpus := godoc.NewCorpus(fs) corpus.IndexEnabled = *indexEnabled corpus.IndexFiles = *indexFiles pres = godoc.NewPresentation(corpus) - // ... + pres.TabWidth = *tabWidth + pres.ShowTimestamps = *showTimestamps + pres.ShowPlayground = *showPlayground + pres.ShowExamples = *showExamples + pres.DeclLinks = *declLinks + + readTemplates() + godoc.InitHandlers(pres) if *writeIndex { // Write search index and exit. - if godoc.IndexFiles == "" { + if *indexFiles == "" { log.Fatal("no index file specified") } log.Println("initialize file systems") *verbose = true // want to see what happens - godoc.IndexThrottle = 1.0 - godoc.UpdateIndex() + corpus.IndexThrottle = 1.0 + corpus.UpdateIndex() - log.Println("writing index file", godoc.IndexFiles) - f, err := os.Create(godoc.IndexFiles) + log.Println("writing index file", *indexFiles) + f, err := os.Create(*indexFiles) if err != nil { log.Fatal(err) } - index, _ := godoc.SearchIndex.Get() - err = index.(*godoc.Index).Write(f) + index, _ := corpus.CurrentIndex() + err = index.Write(f) if err != nil { log.Fatal(err) } @@ -270,12 +272,12 @@ func main() { log.Printf("version = %s", runtime.Version()) log.Printf("address = %s", *httpAddr) log.Printf("goroot = %s", *goroot) - log.Printf("tabwidth = %d", godoc.TabWidth) + log.Printf("tabwidth = %d", *tabWidth) switch { case !*indexEnabled: log.Print("search index disabled") - case godoc.MaxResults > 0: - log.Printf("full text index enabled (maxresults = %d)", godoc.MaxResults) + case *maxResults > 0: + log.Printf("full text index enabled (maxresults = %d)", *maxResults) default: log.Print("identifier search index enabled") } @@ -287,7 +289,7 @@ func main() { // Initialize search index. if *indexEnabled { - go godoc.RunIndexer() + go corpus.RunIndexer() } // Start http server. @@ -431,10 +433,10 @@ func main() { } if *html { var buf bytes.Buffer - godoc.WriteNode(&buf, info.FSet, cn) + pres.WriteNode(&buf, info.FSet, cn) godoc.FormatText(os.Stdout, buf.Bytes(), -1, true, "", nil) } else { - godoc.WriteNode(os.Stdout, info.FSet, cn) + pres.WriteNode(os.Stdout, info.FSet, cn) } fmt.Println() } diff --git a/godoc/corpus.go b/godoc/corpus.go index 87d33bd9..fb889445 100644 --- a/godoc/corpus.go +++ b/godoc/corpus.go @@ -7,6 +7,7 @@ package godoc import ( "errors" pathpkg "path" + "time" "code.google.com/p/go.tools/godoc/util" "code.google.com/p/go.tools/godoc/vfs" @@ -46,6 +47,8 @@ type Corpus struct { 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 } // NewCorpus returns a new Corpus from a filesystem. @@ -61,6 +64,17 @@ func NewCorpus(fs vfs.FileSystem) *Corpus { 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 { diff --git a/godoc/dirtrees.go b/godoc/dirtrees.go index edf36b82..9316c19b 100644 --- a/godoc/dirtrees.go +++ b/godoc/dirtrees.go @@ -86,7 +86,7 @@ func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth i // though the directory doesn't contain any real package files - was bug) if synopses[0] == "" { // no "optimal" package synopsis yet; continue to collect synopses - file, err := parseFile(fset, pathpkg.Join(path, d.Name()), + file, err := b.c.parseFile(fset, pathpkg.Join(path, d.Name()), parser.ParseComments|parser.PackageClauseOnly) if err == nil { hasPkgFiles = true diff --git a/godoc/godoc.go b/godoc/godoc.go index e400f6de..093f007f 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -27,46 +27,10 @@ import ( "time" "unicode" "unicode/utf8" - - "code.google.com/p/go.tools/godoc/util" - "code.google.com/p/go.tools/godoc/vfs" ) -// FS is the file system that godoc reads from and serves. -// It is a virtual file system that operates on slash-separated paths, -// and its root corresponds to the Go distribution root: /src/pkg -// holds the source tree, and so on. This means that the URLs served by -// the godoc server are the same as the paths in the virtual file -// system, which helps keep things simple. -// TODO(bradfitz): delete this global -var FS = vfs.NameSpace{} - -// Old flags -var ( - // DeclLinks controls whether identifers are linked to their declaration. - DeclLinks = true - - // ShowExamples controls whether to show examples in command-line mode. - // TODO(bradfitz,adg): delete this flag - ShowExamples = false - - // ShowPlayground controls whether to enable the playground in - // the web interface. - // TODO(bradfitz,adg): delete this flag - ShowPlayground = false - - ShowTimestamps = false - - Verbose = false - - TabWidth = 4 - - // regular expression matching note markers to show - NotesRx = "BUG" -) - -// SearchIndex is the search index in use. -var SearchIndex util.RWValue +// Verbose controls logging verbosity. +var Verbose = false // Fake relative package path for built-ins. Documentation for all globals // (not just exported ones) will be shown for packages in this directory. @@ -77,39 +41,57 @@ const BuiltinPkgPath = "builtin" // Convention: template function names ending in "_html" or "_url" produce // HTML- or URL-escaped strings; all other function results may // require explicit escaping in the template. -var FuncMap = template.FuncMap{ - // various helpers - "filename": filenameFunc, - "repeat": strings.Repeat, +func (p *Presentation) FuncMap() template.FuncMap { + p.initFuncMapOnce.Do(p.initFuncMap) + return p.funcMap +} - // access to FileInfos (directory listings) - "fileInfoName": fileInfoNameFunc, - "fileInfoTime": fileInfoTimeFunc, +func (p *Presentation) TemplateFuncs() template.FuncMap { + p.initFuncMapOnce.Do(p.initFuncMap) + return p.templateFuncs +} - // access to search result information - "infoKind_html": infoKind_htmlFunc, - "infoLine": infoLineFunc, - "infoSnippet_html": infoSnippet_htmlFunc, +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, - // formatting of AST nodes - "node": nodeFunc, - "node_html": node_htmlFunc, - "comment_html": comment_htmlFunc, - "comment_text": comment_textFunc, + // access to FileInfos (directory listings) + "fileInfoName": fileInfoNameFunc, + "fileInfoTime": fileInfoTimeFunc, - // support for URL attributes - "pkgLink": pkgLinkFunc, - "srcLink": srcLinkFunc, - "posLink_url": posLink_urlFunc, + // access to search result information + "infoKind_html": infoKind_htmlFunc, + "infoLine": p.infoLineFunc, + "infoSnippet_html": p.infoSnippet_htmlFunc, - // formatting of Examples - "example_html": example_htmlFunc, - "example_text": example_textFunc, - "example_name": example_nameFunc, - "example_suffix": example_suffixFunc, + // formatting of AST nodes + "node": p.nodeFunc, + "node_html": p.node_htmlFunc, + "comment_html": comment_htmlFunc, + "comment_text": comment_textFunc, - // formatting of Notes - "noteTitle": noteTitle, + // support for URL attributes + "pkgLink": pkgLinkFunc, + "srcLink": srcLinkFunc, + "posLink_url": posLink_urlFunc, + + // formatting of Examples + "example_html": p.example_htmlFunc, + "example_text": p.example_textFunc, + "example_name": p.example_nameFunc, + "example_suffix": p.example_suffixFunc, + + // formatting of Notes + "noteTitle": noteTitle, + } } func filenameFunc(path string) string { @@ -148,10 +130,10 @@ func infoKind_htmlFunc(info SpotInfo) string { return infoKinds[info.Kind()] // infoKind entries are html-escaped } -func infoLineFunc(info SpotInfo) int { +func (p *Presentation) infoLineFunc(info SpotInfo) int { line := info.Lori() if info.IsIndex() { - index, _ := SearchIndex.Get() + index, _ := p.Corpus.searchIndex.Get() if index != nil { line = index.(*Index).Snippet(line).Line } else { @@ -165,27 +147,27 @@ func infoLineFunc(info SpotInfo) int { return line } -func infoSnippet_htmlFunc(info SpotInfo) string { +func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string { if info.IsIndex() { - index, _ := SearchIndex.Get() + index, _ := p.Corpus.searchIndex.Get() // Snippet.Text was HTML-escaped when it was generated return index.(*Index).Snippet(info.Lori()).Text } return `no snippet text available` } -func nodeFunc(info *PageInfo, node interface{}) string { +func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string { var buf bytes.Buffer - writeNode(&buf, info.FSet, node) + p.writeNode(&buf, info.FSet, node) return buf.String() } -func node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { +func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { var buf1 bytes.Buffer - writeNode(&buf1, info.FSet, node) + p.writeNode(&buf1, info.FSet, node) var buf2 bytes.Buffer - if n, _ := node.(ast.Node); n != nil && linkify && DeclLinks { + if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks { LinkifyText(&buf2, buf1.Bytes(), n) } else { FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) @@ -307,8 +289,8 @@ func srcLinkFunc(s string) string { return pathpkg.Clean("/" + s) } -func example_textFunc(info *PageInfo, funcName, indent string) string { - if !ShowExamples { +func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string { + if !p.ShowExamples { return "" } @@ -328,7 +310,7 @@ func example_textFunc(info *PageInfo, funcName, indent string) string { // print code cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} var buf1 bytes.Buffer - writeNode(&buf1, info.FSet, cnode) + p.writeNode(&buf1, info.FSet, cnode) code := buf1.String() // Additional formatting if this is a function body. if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { @@ -348,7 +330,7 @@ func example_textFunc(info *PageInfo, funcName, indent string) string { return buf.String() } -func example_htmlFunc(info *PageInfo, funcName string) string { +func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string { var buf bytes.Buffer for _, eg := range info.Examples { name := stripExampleSuffix(eg.Name) @@ -359,7 +341,7 @@ func example_htmlFunc(info *PageInfo, funcName string) string { // print code cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} - code := node_htmlFunc(info, cnode, true) + code := p.node_htmlFunc(info, cnode, true) out := eg.Output wholeFile := true @@ -379,7 +361,7 @@ func example_htmlFunc(info *PageInfo, funcName string) string { // Write out the playground code in standard Go style // (use tabs, no comment highlight, etc). play := "" - if eg.Play != nil && ShowPlayground { + if eg.Play != nil && p.ShowPlayground { var buf bytes.Buffer if err := format.Node(&buf, info.FSet, eg.Play); err != nil { log.Print(err) @@ -410,7 +392,7 @@ func example_htmlFunc(info *PageInfo, funcName string) string { // example_nameFunc takes an example function name and returns its display // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". -func example_nameFunc(s string) string { +func (p *Presentation) example_nameFunc(s string) string { name, suffix := splitExampleName(s) // replace _ with . for method names name = strings.Replace(name, "_", ".", 1) @@ -423,7 +405,7 @@ func example_nameFunc(s string) string { // example_suffixFunc takes an example function name and returns its suffix in // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". -func example_suffixFunc(name string) string { +func (p *Presentation) example_suffixFunc(name string) string { _, suffix := splitExampleName(name) return suffix } @@ -462,7 +444,7 @@ func splitExampleName(s string) (name, suffix string) { } // Write an AST node to w. -func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { +func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}) { // convert trailing tabs into spaces using a tconv filter // to ensure a good outcome in most browsers (there may still // be tabs in comments and strings, but converting those into @@ -472,10 +454,13 @@ func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { // with an another printer mode (which is more efficiently // implemented in the printer than here with another layer) mode := printer.TabIndent | printer.UseSpaces - err := (&printer.Config{Mode: mode, Tabwidth: TabWidth}).Fprint(&tconv{output: w}, fset, x) + err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x) if err != nil { log.Print(err) } } -var WriteNode = writeNode +// WriteNote writes x to w. +func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) { + p.writeNode(w, fset, x) +} diff --git a/godoc/index.go b/godoc/index.go index d7c79203..3040a358 100644 --- a/godoc/index.go +++ b/godoc/index.go @@ -62,19 +62,6 @@ import ( "code.google.com/p/go.tools/godoc/util" ) -// TODO(bradfitz,adg): legacy flag vars. clean up. -var ( - MaxResults = 1000 - - // index throttle value; 0.0 = no time allocated, 1.0 = full throttle - IndexThrottle float64 = 0.75 - - // IndexFiles is a glob pattern specifying index files; if - // not empty, the index is read from these files in sorted - // order") - IndexFiles string -) - // ---------------------------------------------------------------------------- // InterfaceSlice is a helper type for sorting interface // slices according to some slice-specific sort criteria. @@ -384,6 +371,7 @@ type Statistics struct { // interface for walking file trees, and the ast.Visitor interface for // walking Go ASTs. type Indexer struct { + c *Corpus fset *token.FileSet // file set for all indexed files sources bytes.Buffer // concatenated sources packages map[string]*Pak // map of canonicalized *Paks @@ -549,7 +537,7 @@ func pkgName(filename string) string { // failed (that is, if the file was not added), it returns file == nil. func (x *Indexer) addFile(filename string, goFile bool) (file *token.File, ast *ast.File) { // open file - f, err := FS.Open(filename) + f, err := x.c.fs.Open(filename) if err != nil { return } @@ -716,19 +704,20 @@ func canonical(w string) string { return strings.ToLower(w) } // NewIndex creates a new index for the .go files // in the directories given by dirnames. // -func NewIndex(dirnames <-chan string, fulltextIndex bool, throttle float64) *Index { +func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle float64) *Index { var x Indexer th := util.NewThrottle(throttle, 100*time.Millisecond) // run at least 0.1s at a time // initialize Indexer // (use some reasonably sized maps to start) + x.c = c x.fset = token.NewFileSet() x.packages = make(map[string]*Pak, 256) x.words = make(map[string]*IndexResult, 8192) // index all files in the directories given by dirnames for dirname := range dirnames { - list, err := FS.ReadDir(dirname) + list, err := c.fs.ReadDir(dirname) if err != nil { continue // ignore this directory } @@ -1046,19 +1035,19 @@ func (c *Corpus) invalidateIndex() { // indexUpToDate() returns true if the search index is not older // than any of the file systems under godoc's observation. // -func indexUpToDate() bool { - _, fsTime := FSModified.Get() - _, siTime := SearchIndex.Get() +func (c *Corpus) indexUpToDate() bool { + _, fsTime := c.fsModified.Get() + _, siTime := c.searchIndex.Get() return !fsTime.After(siTime) } // feedDirnames feeds the directory names of all directories // under the file system given by root to channel c. // -func feedDirnames(root *util.RWValue, c chan<- string) { - if dir, _ := root.Get(); dir != nil { +func (c *Corpus) feedDirnames(ch chan<- string) { + if dir, _ := c.fsTree.Get(); dir != nil { for d := range dir.(*Directory).iter(false) { - c <- d.Path + ch <- d.Path } } } @@ -1066,16 +1055,16 @@ func feedDirnames(root *util.RWValue, c chan<- string) { // fsDirnames() returns a channel sending all directory names // of all the file systems under godoc's observation. // -func fsDirnames() <-chan string { - c := make(chan string, 256) // buffered for fewer context switches +func (c *Corpus) fsDirnames() <-chan string { + ch := make(chan string, 256) // buffered for fewer context switches go func() { - feedDirnames(&FSTree, c) - close(c) + c.feedDirnames(ch) + close(ch) }() - return c + return ch } -func readIndex(filenames string) error { +func (c *Corpus) readIndex(filenames string) error { matches, err := filepath.Glob(filenames) if err != nil { return err @@ -1096,18 +1085,18 @@ func readIndex(filenames string) error { if err := x.Read(io.MultiReader(files...)); err != nil { return err } - SearchIndex.Set(x) + c.searchIndex.Set(x) return nil } -func UpdateIndex() { +func (c *Corpus) UpdateIndex() { if Verbose { log.Printf("updating index...") } start := time.Now() - index := NewIndex(fsDirnames(), MaxResults > 0, IndexThrottle) + index := NewIndex(c, c.fsDirnames(), c.MaxResults > 0, c.IndexThrottle) stop := time.Now() - SearchIndex.Set(index) + c.searchIndex.Set(index) if Verbose { secs := stop.Sub(start).Seconds() stats := index.Stats() @@ -1123,19 +1112,19 @@ func UpdateIndex() { } // RunIndexer runs forever, indexing. -func RunIndexer() { +func (c *Corpus) RunIndexer() { // initialize the index from disk if possible - if IndexFiles != "" { - if err := readIndex(IndexFiles); err != nil { + if c.IndexFiles != "" { + if err := c.readIndex(c.IndexFiles); err != nil { log.Printf("error reading index: %s", err) } } // repeatedly update the index when it goes out of date for { - if !indexUpToDate() { + if !c.indexUpToDate() { // index possibly out of date - make a new one - UpdateIndex() + c.UpdateIndex() } delay := 60 * time.Second // by default, try every 60s if false { // TODO(bradfitz): was: *testDir != "" { diff --git a/godoc/meta.go b/godoc/meta.go index 86f25e62..ae8ee5a9 100644 --- a/godoc/meta.go +++ b/godoc/meta.go @@ -99,14 +99,14 @@ func (c *Corpus) updateMetadata() { } } scan("/doc") - DocMetadata.Set(metadata) + c.docMetadata.Set(metadata) } // MetadataFor returns the *Metadata for a given relative path or nil if none // exists. // -func MetadataFor(relpath string) *Metadata { - if m, _ := DocMetadata.Get(); m != nil { +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 { diff --git a/godoc/page.go b/godoc/page.go index f8ee60ef..18934113 100644 --- a/godoc/page.go +++ b/godoc/page.go @@ -42,7 +42,7 @@ func (p *Presentation) ServePage(w http.ResponseWriter, page Page) { page.Tabtitle = page.Title } page.SearchBox = p.Corpus.IndexEnabled - page.Playground = ShowPlayground + page.Playground = p.ShowPlayground page.Version = runtime.Version() if err := GodocHTML.Execute(w, page); err != nil && err != http.ErrBodyNotAllowed { // Only log if there's an error that's not about writing on HEAD requests. diff --git a/godoc/parser.go b/godoc/parser.go index d8b5779a..a27d4fdc 100644 --- a/godoc/parser.go +++ b/godoc/parser.go @@ -16,19 +16,19 @@ import ( "code.google.com/p/go.tools/godoc/vfs" ) -func parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) { - src, err := vfs.ReadFile(FS, filename) +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 } return parser.ParseFile(fset, filename, src, mode) } -func parseFiles(fset *token.FileSet, abspath string, localnames []string) (map[string]*ast.File, error) { +func (c *Corpus) parseFiles(fset *token.FileSet, 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 := parseFile(fset, absname, parser.ParseComments) + file, err := c.parseFile(fset, absname, parser.ParseComments) if err != nil { return nil, err } diff --git a/godoc/pres.go b/godoc/pres.go index a002b3a0..3191ab80 100644 --- a/godoc/pres.go +++ b/godoc/pres.go @@ -5,6 +5,8 @@ package godoc import ( + "regexp" + "sync" "text/template" ) @@ -19,6 +21,12 @@ type Presentation struct { ShowPlayground bool ShowExamples bool DeclLinks bool + + NotesRx *regexp.Regexp + + initFuncMapOnce sync.Once + funcMap template.FuncMap + templateFuncs template.FuncMap } // NewPresentation returns a new Presentation from a corpus. @@ -33,7 +41,3 @@ func NewPresentation(c *Corpus) *Presentation { DeclLinks: true, } } - -func (p *Presentation) FuncMap() template.FuncMap { - panic("") -} diff --git a/godoc/server.go b/godoc/server.go index c4793ed7..9b2149b0 100644 --- a/godoc/server.go +++ b/godoc/server.go @@ -19,7 +19,6 @@ import ( "os" pathpkg "path" "path/filepath" - "regexp" "strings" "text/template" "time" @@ -35,11 +34,6 @@ var ( FileServer http.Handler // default file server CmdHandler Server PkgHandler Server - - // 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 ) func InitHandlers(p *Presentation) { @@ -83,8 +77,17 @@ func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageIn // set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir. ctxt := build.Default ctxt.IsAbsPath = pathpkg.IsAbs - ctxt.ReadDir = fsReadDir - ctxt.OpenFile = fsOpenFile + ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) { + return h.c.fs.ReadDir(filepath.ToSlash(dir)) + } + 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 ioutil.NopCloser(bytes.NewReader(data)), nil + } + 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 { @@ -109,7 +112,7 @@ func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageIn if len(pkgfiles) > 0 { // build package AST fset := token.NewFileSet() - files, err := parseFiles(fset, abspath, pkgfiles) + files, err := h.c.parseFiles(fset, abspath, pkgfiles) if err != nil { info.Err = err return info @@ -133,7 +136,7 @@ func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageIn // collect examples testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...) - files, err = parseFiles(fset, abspath, testfiles) + files, err = h.c.parseFiles(fset, abspath, testfiles) if err != nil { log.Println("parsing examples:", err) } @@ -142,7 +145,7 @@ func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageIn // 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, err := regexp.Compile(NotesRx); err == nil { + if rx := h.p.NotesRx; rx != nil { for m, n := range info.PDoc.Notes { if rx.MatchString(m) { if info.Notes == nil { @@ -169,7 +172,7 @@ func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageIn // get directory information, if any var dir *Directory var timestamp time.Time - if tree, ts := FSTree.Get(); tree != nil && tree.(*Directory) != nil { + 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) @@ -223,7 +226,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: tabtitle = info.Dirname title = "Directory " - if ShowTimestamps { + if h.p.ShowTimestamps { subtitle = "Last update: " + info.DirTime.String() } } @@ -292,20 +295,6 @@ var AdjustPageInfoMode = func(_ *http.Request, mode PageInfoMode) PageInfoMode { return mode } -// fsReadDir implements ReadDir for the go/build package. -func fsReadDir(dir string) ([]os.FileInfo, error) { - return FS.ReadDir(filepath.ToSlash(dir)) -} - -// fsOpenFile implements OpenFile for the go/build package. -func fsOpenFile(name string) (r io.ReadCloser, err error) { - data, err := vfs.ReadFile(FS, filepath.ToSlash(name)) - if err != nil { - return nil, err - } - return ioutil.NopCloser(bytes.NewReader(data)), nil -} - // 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 @@ -465,7 +454,7 @@ func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, ab return } - list, err := FS.ReadDir(abspath) + list, err := p.Corpus.fs.ReadDir(abspath) if err != nil { p.ServeError(w, r, relpath, err) return @@ -480,7 +469,7 @@ func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, ab func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { // get HTML body contents - src, err := vfs.ReadFile(FS, abspath) + src, err := vfs.ReadFile(p.Corpus.fs, abspath) if err != nil { log.Printf("ReadFile: %s", err) p.ServeError(w, r, relpath, err) @@ -502,7 +491,7 @@ func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, absp // evaluate as template if indicated if meta.Template { - tmpl, err := template.New("main").Funcs(TemplateFuncs).Parse(string(src)) + 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) @@ -531,11 +520,15 @@ func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, absp }) } +func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) { + p.serveFile(w, r) +} + func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) { relpath := r.URL.Path // Check to see if we need to redirect or serve another file. - if m := MetadataFor(relpath); m != nil { + if m := p.Corpus.MetadataFor(relpath); m != nil { if m.Path != relpath { // Redirect to canonical path. http.Redirect(w, r, m.Path, http.StatusMovedPermanently) @@ -564,7 +557,7 @@ func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) { return } - dir, err := FS.Lstat(abspath) + dir, err := p.Corpus.fs.Lstat(abspath) if err != nil { log.Print(err) p.ServeError(w, r, relpath, err) @@ -575,7 +568,7 @@ func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) { if redirect(w, r) { return } - if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(FS, index) { + if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) { p.ServeHTMLDoc(w, r, index, index) return } @@ -583,7 +576,7 @@ func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) { return } - if util.IsTextFile(FS, abspath) { + if util.IsTextFile(p.Corpus.fs, abspath) { if redirectFile(w, r) { return } diff --git a/godoc/snippet.go b/godoc/snippet.go index 1466d3a4..dd9c8225 100644 --- a/godoc/snippet.go +++ b/godoc/snippet.go @@ -21,10 +21,10 @@ type Snippet struct { Text string // HTML-escaped } -func newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet { +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 - writeNode(&buf1, fset, decl) + p.writeNode(&buf1, fset, decl) // wrap text with
 tag
 	var buf2 bytes.Buffer
 	buf2.WriteString("
")
@@ -55,7 +55,7 @@ func findSpec(list []ast.Spec, id *ast.Ident) ast.Spec {
 	return nil
 }
 
-func genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
+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
@@ -71,10 +71,10 @@ func genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
 		Rparen: d.Rparen,
 	}
 
-	return newSnippet(fset, dd, id)
+	return p.newSnippet(fset, dd, id)
 }
 
-func funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
+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
 	}
@@ -87,19 +87,30 @@ func funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
 		Type: d.Type,
 	}
 
-	return newSnippet(fset, dd, id)
+	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) (s *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 = genSnippet(fset, d, id)
+		s = p.genSnippet(fset, d, id)
 	case *ast.FuncDecl:
-		s = funcSnippet(fset, d, id)
+		s = p.funcSnippet(fset, d, id)
 	}
 
 	// handle failure gracefully
@@ -108,5 +119,5 @@ func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) (s *Snippet)
 		fmt.Fprintf(&buf, `could not generate a snippet for %s`, id.Name)
 		s = &Snippet{fset.Position(id.Pos()).Line, buf.String()}
 	}
-	return
+	return s
 }
diff --git a/godoc/tab.go b/godoc/tab.go
index 012fab61..7973b740 100644
--- a/godoc/tab.go
+++ b/godoc/tab.go
@@ -16,6 +16,7 @@ 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) {
@@ -44,7 +45,7 @@ func (p *tconv) Write(data []byte) (n int, err error) {
 		case indenting:
 			switch b {
 			case '\t':
-				p.indent += TabWidth
+				p.indent += p.p.TabWidth
 			case '\n':
 				p.indent = 0
 				if _, err = p.output.Write(data[n : n+1]); err != nil {
diff --git a/godoc/template.go b/godoc/template.go
index e8c4ba4a..325bc8ca 100644
--- a/godoc/template.go
+++ b/godoc/template.go
@@ -37,7 +37,6 @@ import (
 	"log"
 	"regexp"
 	"strings"
-	"text/template"
 
 	"code.google.com/p/go.tools/godoc/vfs"
 )
@@ -45,14 +44,10 @@ import (
 // Functions in this file panic on error, but the panic is recovered
 // to an error by 'code'.
 
-var TemplateFuncs = template.FuncMap{
-	"code": 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 contents(name string) string {
-	file, err := vfs.ReadFile(FS, name)
+func (c *Corpus) contents(name string) string {
+	file, err := vfs.ReadFile(c.fs, name)
 	if err != nil {
 		log.Panic(err)
 	}
@@ -75,14 +70,14 @@ func stringFor(arg interface{}) string {
 	return ""
 }
 
-func code(file string, arg ...interface{}) (s string, err error) {
+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 := contents(file)
+	text := p.Corpus.contents(file)
 	var command string
 	switch len(arg) {
 	case 0:
@@ -90,10 +85,10 @@ func code(file string, arg ...interface{}) (s string, err error) {
 		command = fmt.Sprintf("code %q", file)
 	case 1:
 		command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
-		text = oneLine(file, text, 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 = multipleLines(file, text, arg[0], arg[1])
+		text = p.Corpus.multipleLines(file, text, arg[0], arg[1])
 	default:
 		return "", fmt.Errorf("incorrect code invocation: code %q %q", file, arg)
 	}
@@ -125,8 +120,8 @@ func parseArg(arg interface{}, file string, max int) (ival int, sval string, isI
 }
 
 // oneLine returns the single line generated by a two-argument code invocation.
-func oneLine(file, text string, arg interface{}) string {
-	lines := strings.SplitAfter(contents(file), "\n")
+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]
@@ -135,8 +130,8 @@ func oneLine(file, text string, arg interface{}) string {
 }
 
 // multipleLines returns the text generated by a three-argument code invocation.
-func multipleLines(file, text string, arg1, arg2 interface{}) string {
-	lines := strings.SplitAfter(contents(file), "\n")
+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 {