1// Copyright 2009 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package godoc
6
7import (
8	"bytes"
9	"fmt"
10	"net/http"
11	"regexp"
12	"strings"
13)
14
15type SearchResult struct {
16	Query string
17	Alert string // error or warning message
18
19	// identifier matches
20	Pak HitList       // packages matching Query
21	Hit *LookupResult // identifier matches of Query
22	Alt *AltWords     // alternative identifiers to look for
23
24	// textual matches
25	Found    int         // number of textual occurrences found
26	Textual  []FileLines // textual matches of Query
27	Complete bool        // true if all textual occurrences of Query are reported
28	Idents   map[SpotKind][]Ident
29}
30
31func (c *Corpus) Lookup(query string) SearchResult {
32	result := &SearchResult{Query: query}
33
34	index, timestamp := c.CurrentIndex()
35	if index != nil {
36		// identifier search
37		if r, err := index.Lookup(query); err == nil {
38			result = r
39		} else if err != nil && !c.IndexFullText {
40			// ignore the error if full text search is enabled
41			// since the query may be a valid regular expression
42			result.Alert = "Error in query string: " + err.Error()
43			return *result
44		}
45
46		// full text search
47		if c.IndexFullText && query != "" {
48			rx, err := regexp.Compile(query)
49			if err != nil {
50				result.Alert = "Error in query regular expression: " + err.Error()
51				return *result
52			}
53			// If we get maxResults+1 results we know that there are more than
54			// maxResults results and thus the result may be incomplete (to be
55			// precise, we should remove one result from the result set, but
56			// nobody is going to count the results on the result page).
57			result.Found, result.Textual = index.LookupRegexp(rx, c.MaxResults+1)
58			result.Complete = result.Found <= c.MaxResults
59			if !result.Complete {
60				result.Found-- // since we looked for maxResults+1
61			}
62		}
63	}
64
65	// is the result accurate?
66	if c.IndexEnabled {
67		if ts := c.FSModifiedTime(); timestamp.Before(ts) {
68			// The index is older than the latest file system change under godoc's observation.
69			result.Alert = "Indexing in progress: result may be inaccurate"
70		}
71	} else {
72		result.Alert = "Search index disabled: no results available"
73	}
74
75	return *result
76}
77
78// SearchResultDoc optionally specifies a function returning an HTML body
79// displaying search results matching godoc documentation.
80func (p *Presentation) SearchResultDoc(result SearchResult) []byte {
81	return applyTemplate(p.SearchDocHTML, "searchDocHTML", result)
82}
83
84// SearchResultCode optionally specifies a function returning an HTML body
85// displaying search results matching source code.
86func (p *Presentation) SearchResultCode(result SearchResult) []byte {
87	return applyTemplate(p.SearchCodeHTML, "searchCodeHTML", result)
88}
89
90// SearchResultTxt optionally specifies a function returning an HTML body
91// displaying search results of textual matches.
92func (p *Presentation) SearchResultTxt(result SearchResult) []byte {
93	return applyTemplate(p.SearchTxtHTML, "searchTxtHTML", result)
94}
95
96// HandleSearch obtains results for the requested search and returns a page
97// to display them.
98func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) {
99	query := strings.TrimSpace(r.FormValue("q"))
100	result := p.Corpus.Lookup(query)
101
102	var contents bytes.Buffer
103	for _, f := range p.SearchResults {
104		contents.Write(f(p, result))
105	}
106
107	var title string
108	if haveResults := contents.Len() > 0; haveResults {
109		title = fmt.Sprintf(`Results for query: %v`, query)
110		if !p.Corpus.IndexEnabled {
111			result.Alert = ""
112		}
113	} else {
114		title = fmt.Sprintf(`No results found for query %q`, query)
115	}
116
117	body := bytes.NewBuffer(applyTemplate(p.SearchHTML, "searchHTML", result))
118	body.Write(contents.Bytes())
119
120	p.ServePage(w, Page{
121		Title:    title,
122		Tabtitle: query,
123		Query:    query,
124		Body:     body.Bytes(),
125		GoogleCN: googleCN(r),
126	})
127}
128
129func (p *Presentation) serveSearchDesc(w http.ResponseWriter, r *http.Request) {
130	w.Header().Set("Content-Type", "application/opensearchdescription+xml")
131	data := map[string]interface{}{
132		"BaseURL": fmt.Sprintf("http://%s", r.Host),
133	}
134	applyTemplateToResponseWriter(w, p.SearchDescXML, &data)
135}
136
137// tocColCount returns the no. of columns
138// to split the toc table to.
139func tocColCount(result SearchResult) int {
140	tocLen := tocLen(result)
141	colCount := 0
142	// Simple heuristic based on visual aesthetic in manual testing.
143	switch {
144	case tocLen <= 10:
145		colCount = 1
146	case tocLen <= 20:
147		colCount = 2
148	case tocLen <= 80:
149		colCount = 3
150	default:
151		colCount = 4
152	}
153	return colCount
154}
155
156// tocLen calculates the no. of items in the toc table
157// by going through various fields in the SearchResult
158// that is rendered in the UI.
159func tocLen(result SearchResult) int {
160	tocLen := 0
161	for _, val := range result.Idents {
162		if len(val) != 0 {
163			tocLen++
164		}
165	}
166	// If no identifiers, then just one item for the header text "Package <result.Query>".
167	// See searchcode.html for further details.
168	if len(result.Idents) == 0 {
169		tocLen++
170	}
171	if result.Hit != nil {
172		if len(result.Hit.Decls) > 0 {
173			tocLen += len(result.Hit.Decls)
174			// We need one extra item for the header text "Package-level declarations".
175			tocLen++
176		}
177		if len(result.Hit.Others) > 0 {
178			tocLen += len(result.Hit.Others)
179			// We need one extra item for the header text "Local declarations and uses".
180			tocLen++
181		}
182	}
183	// For "textual occurrences".
184	tocLen++
185	return tocLen
186}
187