// 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(), GoogleCN: googleCN(r), }) } 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 ". // 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 }