1
0
mirror of https://github.com/golang/go synced 2024-09-30 10:28:33 -06:00
go/godoc/search.go
Agniva De Sarker 3e7aa9e599 godoc: generate TOC from server-side for search page
Currently, we generate the TOC for a page dynamically after page load
through javascript. This is fine for pages with static content.

But for pages with dynamic output like /search, sometimes this causes a
noticeable page jump due to extensive DOM traversal. Also, the heuristics
to calculate the no. of columns is very rudimentary and fills the entire
above-the-fold area if the no. of results is very large.

Therefore, we generate the TOC from server side itself. And improve
the no. of columns heuristic further to accomodate up to 4 columns.

This improves page performance and utilizes real estate appropriately
according to the input.

Some screenshots at laptop (1366x768) resolution.
https://snag.gy/AXz2rP.jpg
https://snag.gy/th3Nn8.jpg

More can be found in the CL comments.

Fixes golang/go#21685
Updates golang/go#21686

Change-Id: Ia9b6dd1e67231d992709e4ba10ebdbedfe38b564
Reviewed-on: https://go-review.googlesource.com/c/129135
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
2018-10-19 20:12:13 +00:00

187 lines
5.3 KiB
Go

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package 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 <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
}