internal/godoc: refactor into internal/web [generated]
What's left of internal/godoc is mainly a simple web server for static content (plus the package docs provided by internal/pkgdoc) and bears little resemblance to the original godoc. Revamp the API and retire the name, moving to internal/web. CL generated by the script below. [git-generate] cd internal/godoc mv util.go istext.go mv godoc_test.go template_test.go mv server_test.go site_test.go rf ' mv TabWidth tabWidth mv IsText isText mv TabSpacer tabSpacer mv Presentation Site mv NewPresentation NewSite mv writerCapturesErr writeErrorSaver rm marshalJSON mv redirect maybeRedirect mv redirectFile maybeRedirectFile mv Site.serveText Site.serveRawText mv Site.serveTextFile Site.serveText mv Site.serveDirectory Site.serveDir mv Site.initFuncMap Site.initDocFuncs mv \ toFS \ Site \ NewSite \ Site.ServeError \ Site.ServeHTTP \ Site.ServePage \ Page \ Site.fullPage \ Page.Invoke \ writeErrorSaver \ writeErrorSaver.Write \ applyTemplateToResponseWriter \ Site.serveFile \ maybeRedirect \ maybeRedirectFile \ doctype \ Site.serveHTML \ Site.serveDir \ Site.serveText \ selRx \ rangeSelection \ Site.serveRawText \ Site.googleCN \ site.go mv example_nameFunc example_name mv example_suffixFunc example_suffix mv srcPosLinkFunc srcPosLink mv \ siteFuncs \ example_name \ example_suffix \ srcToPkg \ Page.SrcPkgLink \ Page.SrcBreadcrumb \ Page.SrcPosLink \ srcPosLink \ sitefuncs.go mv \ docServer \ docServer.ServeHTTP \ Page.ModeQuery \ pkgdoc.go mv metaJSON fileJSON mv extractMetadata parseFile mv \ file \ fileJSON \ join \ open \ jsonStart \ parseFile \ file.go mv \ Site.initDocFuncs \ Site.code \ Site.contents \ stringFor \ Site.oneLine \ Site.multipleLines \ parseArg \ match \ docfuncs.go mv Site.ServeError.p Site.ServeError.s mv Site.ServeHTTP.p Site.ServeHTTP.s mv Site.ServePage.p Site.ServePage.s mv Site.code.p Site.code.s mv Site.contents.p Site.contents.s mv Site.fullPage.p Site.fullPage.s mv Site.googleCN.p Site.googleCN.s mv Site.initDocFuncs.p Site.initDocFuncs.s mv Site.multipleLines.p Site.multipleLines.s mv Site.oneLine.p Site.oneLine.s mv Site.serveDir.p Site.serveDir.s mv Site.serveFile.p Site.serveFile.s mv Site.serveHTML.p Site.serveHTML.s mv Site.serveRawText.p Site.serveRawText.s mv Site.serveText.p Site.serveText.s mv Site.writeNode.p Site.writeNode.s mv Page.pres Page.site mv astfuncs.go docfuncs.go examplefuncs.go \ file.go istext.go markdown.go pkgdoc.go \ site.go site_test.go sitefuncs.go \ tab.go template_test.go \ golang.org/x/website/internal/web ' rm godoc.go meta.go page.go pres.go server.go template.go cd ../../cmd/golangorg rf ' mv pres site ' Change-Id: Ic03a2dbe14f74c60bd6a5a86ba4d3f36d8c5bea8 Reviewed-on: https://go-review.googlesource.com/c/website/+/317656 Trust: Russ Cox <rsc@golang.org> Run-TryBot: Russ Cox <rsc@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
This commit is contained in:
Родитель
4c9e549253
Коммит
117627b3c2
|
@ -32,7 +32,7 @@ import (
|
|||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/website/internal/godoc"
|
||||
"golang.org/x/website/internal/web"
|
||||
)
|
||||
|
||||
// Handler for /doc/codewalk/ and below.
|
||||
|
@ -55,7 +55,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// If file exists, serve using standard file server.
|
||||
if err == nil {
|
||||
pres.ServeHTTP(w, r)
|
||||
site.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) {
|
|||
cw, err := loadCodewalk(abspath + ".xml")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, err)
|
||||
site.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
pres.ServePage(w, r, godoc.Page{
|
||||
site.ServePage(w, r, web.Page{
|
||||
Title: "Codewalk: " + cw.Title,
|
||||
TabTitle: cw.Title,
|
||||
Template: "codewalk.html",
|
||||
|
@ -211,7 +211,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string
|
|||
dir, err := fs.ReadDir(fsys, toFS(abspath))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, err)
|
||||
site.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
var v []interface{}
|
||||
|
@ -228,7 +228,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string
|
|||
}
|
||||
}
|
||||
|
||||
pres.ServePage(w, r, godoc.Page{
|
||||
site.ServePage(w, r, web.Page{
|
||||
Title: "Codewalks",
|
||||
Template: "codewalkdir.html",
|
||||
Data: v,
|
||||
|
@ -246,7 +246,7 @@ func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
|
|||
data, err := fs.ReadFile(fsys, toFS(abspath))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, err)
|
||||
site.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
lo, _ := strconv.Atoi(r.FormValue("lo"))
|
||||
|
|
|
@ -16,12 +16,12 @@ import (
|
|||
"strings"
|
||||
|
||||
"golang.org/x/website/internal/env"
|
||||
"golang.org/x/website/internal/godoc"
|
||||
"golang.org/x/website/internal/redirect"
|
||||
"golang.org/x/website/internal/web"
|
||||
)
|
||||
|
||||
var (
|
||||
pres *godoc.Presentation
|
||||
site *web.Site
|
||||
fsys fs.FS
|
||||
)
|
||||
|
||||
|
@ -76,7 +76,7 @@ func (h hostEnforcerHandler) validHost(host string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func registerHandlers(pres *godoc.Presentation) *http.ServeMux {
|
||||
func registerHandlers(pres *web.Site) *http.ServeMux {
|
||||
if pres == nil {
|
||||
panic("nil Presentation")
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import (
|
|||
"runtime"
|
||||
|
||||
"golang.org/x/website"
|
||||
"golang.org/x/website/internal/godoc"
|
||||
"golang.org/x/website/internal/web"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -79,13 +79,13 @@ func main() {
|
|||
fsys = unionFS{content, os.DirFS(*goroot)}
|
||||
|
||||
var err error
|
||||
pres, err = godoc.NewPresentation(fsys)
|
||||
site, err = web.NewSite(fsys)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
pres.GoogleCN = googleCN
|
||||
site.GoogleCN = googleCN
|
||||
|
||||
mux := registerHandlers(pres)
|
||||
mux := registerHandlers(site)
|
||||
lateSetup(mux)
|
||||
|
||||
var handler http.Handler = http.DefaultServeMux
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"golang.org/x/website"
|
||||
"golang.org/x/website/internal/godoc"
|
||||
"golang.org/x/website/internal/web"
|
||||
)
|
||||
|
||||
// Test that the release history page includes expected entries.
|
||||
|
@ -24,15 +24,15 @@ import (
|
|||
// It can be relaxed whenever the presentation of the release history
|
||||
// page needs to be changed.
|
||||
func TestReleaseHistory(t *testing.T) {
|
||||
origFS, origPres := fsys, pres
|
||||
defer func() { fsys, pres = origFS, origPres }()
|
||||
origFS, origPres := fsys, site
|
||||
defer func() { fsys, site = origFS, origPres }()
|
||||
fsys = website.Content
|
||||
var err error
|
||||
pres, err = godoc.NewPresentation(fsys)
|
||||
site, err = web.NewSite(fsys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mux := registerHandlers(pres)
|
||||
mux := registerHandlers(site)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/doc/devel/release", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
// 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.
|
||||
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// A Page describes the contents of a webpage to be served.
|
||||
//
|
||||
// A Page's Methods are for use by the templates rendering the page.
|
||||
type Page struct {
|
||||
Title string // <h1>
|
||||
TabTitle string // prefix in <title>; defaults to Title
|
||||
Subtitle string // subtitle (date for spec, memory model)
|
||||
SrcPath string // path to file in /src for text view
|
||||
|
||||
// Template and Data describe the data to be
|
||||
// rendered into the overall site frame template.
|
||||
// If Template is empty, then Data should be a template.HTML
|
||||
// holding raw HTML to render into the site frame.
|
||||
// Otherwise, Template should be the name of a template file
|
||||
// in _content/lib/godoc (for example, "package.html"),
|
||||
// and that template will be executed
|
||||
// (with the *Page as its data argument) to produce HTML.
|
||||
//
|
||||
// The overall site template site.html is also invoked with
|
||||
// the *Page as its data argument. It is what arranges to call Template.
|
||||
Template string // template to apply to data (empty string when Data is raw template.HTML)
|
||||
Data interface{} // data to be rendered into page frame
|
||||
|
||||
// Filled in automatically by ServePage
|
||||
GoogleCN bool // page is being served from golang.google.cn
|
||||
GoogleAnalytics string // Google Analytics tag
|
||||
Version string // current Go version
|
||||
|
||||
pres *Presentation
|
||||
}
|
||||
|
||||
// fullPage returns a copy of page with the “automatic” fields filled in.
|
||||
func (p *Presentation) fullPage(r *http.Request, page Page) Page {
|
||||
if page.TabTitle == "" {
|
||||
page.TabTitle = page.Title
|
||||
}
|
||||
page.Version = runtime.Version()
|
||||
page.GoogleCN = p.googleCN(r)
|
||||
page.GoogleAnalytics = p.GoogleAnalytics
|
||||
page.pres = p
|
||||
return page
|
||||
}
|
||||
|
||||
// ServePage responds to the request with the content described by page.
|
||||
func (p *Presentation) ServePage(w http.ResponseWriter, r *http.Request, page Page) {
|
||||
page = p.fullPage(r, page)
|
||||
applyTemplateToResponseWriter(w, p.Templates.Lookup("site.html"), &page)
|
||||
}
|
||||
|
||||
// ServeError responds to the request with the given error.
|
||||
func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
p.ServePage(w, r, Page{
|
||||
Title: r.URL.Path,
|
||||
Template: "error.html",
|
||||
Data: err,
|
||||
})
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// 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 go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/website/internal/api"
|
||||
"golang.org/x/website/internal/pkgdoc"
|
||||
)
|
||||
|
||||
// Presentation is a website served from a file system.
|
||||
type Presentation struct {
|
||||
fs fs.FS
|
||||
api api.DB
|
||||
|
||||
mux *http.ServeMux
|
||||
fileServer http.Handler
|
||||
|
||||
Templates *template.Template
|
||||
|
||||
// GoogleCN reports whether this request should be marked GoogleCN.
|
||||
// If the function is nil, no requests are marked GoogleCN.
|
||||
GoogleCN func(*http.Request) bool
|
||||
|
||||
// GoogleAnalytics optionally adds Google Analytics via the provided
|
||||
// tracking ID to each page.
|
||||
GoogleAnalytics string
|
||||
|
||||
docFuncs template.FuncMap
|
||||
}
|
||||
|
||||
// NewPresentation returns a new Presentation from a file system.
|
||||
func NewPresentation(fsys fs.FS) (*Presentation, error) {
|
||||
apiDB, err := api.Load(fsys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := &Presentation{
|
||||
fs: fsys,
|
||||
api: apiDB,
|
||||
mux: http.NewServeMux(),
|
||||
fileServer: http.FileServer(http.FS(fsys)),
|
||||
}
|
||||
docs := &docServer{
|
||||
p: p,
|
||||
d: pkgdoc.NewDocs(fsys),
|
||||
}
|
||||
p.mux.Handle("/cmd/", docs)
|
||||
p.mux.Handle("/pkg/", docs)
|
||||
p.mux.HandleFunc("/", p.serveFile)
|
||||
p.initFuncMap()
|
||||
|
||||
t, err := template.New("").Funcs(siteFuncs).ParseFS(fsys, "lib/godoc/*.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Templates = t
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler, dispatching the request appropriately.
|
||||
func (p *Presentation) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) googleCN(r *http.Request) bool {
|
||||
return p.GoogleCN != nil && p.GoogleCN(r)
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -32,7 +32,7 @@ var slashSlash = []byte("//")
|
|||
func (p *Page) Node(node interface{}) template.HTML {
|
||||
info := p.Data.(*pkgdoc.Page)
|
||||
var buf1 bytes.Buffer
|
||||
p.pres.writeNode(&buf1, info, info.FSet, node)
|
||||
p.site.writeNode(&buf1, info, info.FSet, node)
|
||||
|
||||
var buf2 bytes.Buffer
|
||||
n, _ := node.(ast.Node)
|
||||
|
@ -48,7 +48,7 @@ func (p *Page) Node(node interface{}) template.HTML {
|
|||
func (p *Page) NodeTOC(node interface{}) template.HTML {
|
||||
info := p.Data.(*pkgdoc.Page)
|
||||
var buf1 bytes.Buffer
|
||||
p.pres.writeNode(&buf1, info, info.FSet, node)
|
||||
p.site.writeNode(&buf1, info, info.FSet, node)
|
||||
|
||||
var buf2 bytes.Buffer
|
||||
buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
|
||||
|
@ -58,14 +58,14 @@ func (p *Page) NodeTOC(node interface{}) template.HTML {
|
|||
return sanitize(template.HTML(buf2.String()))
|
||||
}
|
||||
|
||||
const TabWidth = 4
|
||||
const tabWidth = 4
|
||||
|
||||
// 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 *pkgdoc.Page, fset *token.FileSet, x interface{}) {
|
||||
func (s *Site) writeNode(w io.Writer, pageInfo *pkgdoc.Page, 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
|
||||
|
@ -85,7 +85,7 @@ func (p *Presentation) writeNode(w io.Writer, pageInfo *pkgdoc.Page, fset *token
|
|||
structName = ts.Name.Name
|
||||
}
|
||||
}
|
||||
apiInfo = p.api[pkgName]
|
||||
apiInfo = s.api[pkgName]
|
||||
}
|
||||
|
||||
var out = w
|
||||
|
@ -95,7 +95,7 @@ func (p *Presentation) writeNode(w io.Writer, pageInfo *pkgdoc.Page, fset *token
|
|||
}
|
||||
|
||||
mode := printer.TabIndent | printer.UseSpaces
|
||||
err := (&printer.Config{Mode: mode, Tabwidth: TabWidth}).Fprint(TabSpacer(out, TabWidth), fset, x)
|
||||
err := (&printer.Config{Mode: mode, Tabwidth: tabWidth}).Fprint(tabSpacer(out, tabWidth), fset, x)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
|
@ -209,5 +209,5 @@ func sanitize(src template.HTML) template.HTML {
|
|||
// The current package is deduced from p.Data, which must be a *pkgdoc.Page.
|
||||
func (p *Page) Since(kind, receiver, name string) string {
|
||||
pkg := p.Data.(*pkgdoc.Page).PDoc.ImportPath
|
||||
return p.pres.api.Func(pkg, kind, receiver, name)
|
||||
return p.site.api.Func(pkg, kind, receiver, name)
|
||||
}
|
|
@ -1,38 +1,11 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// 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 go1.16
|
||||
// +build go1.16
|
||||
|
||||
// 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
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -43,16 +16,58 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/internal/history"
|
||||
"golang.org/x/website/internal/texthtml"
|
||||
)
|
||||
|
||||
func (s *Site) initDocFuncs() {
|
||||
s.docFuncs = template.FuncMap{
|
||||
"code": s.code,
|
||||
"releases": func() []*history.Major { return history.Majors },
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) code(file string, arg ...interface{}) (_ template.HTML, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
text := s.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 = s.oneLine(file, text, arg[0])
|
||||
case 2:
|
||||
command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
|
||||
text = s.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.
|
||||
buf.Write(texthtml.Format([]byte(text), texthtml.Config{GoComments: true}))
|
||||
// Include the command as a comment.
|
||||
text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
|
||||
return template.HTML(text), nil
|
||||
}
|
||||
|
||||
// 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 (p *Presentation) contents(name string) string {
|
||||
file, err := fs.ReadFile(p.fs, toFS(name))
|
||||
func (s *Site) contents(name string) string {
|
||||
file, err := fs.ReadFile(s.fs, toFS(name))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
@ -75,58 +90,9 @@ func stringFor(arg interface{}) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (p *Presentation) code(file string, arg ...interface{}) (_ template.HTML, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
text := p.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.oneLine(file, text, arg[0])
|
||||
case 2:
|
||||
command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
|
||||
text = p.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.
|
||||
buf.Write(texthtml.Format([]byte(text), texthtml.Config{GoComments: true}))
|
||||
// Include the command as a comment.
|
||||
text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
|
||||
return template.HTML(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 (p *Presentation) oneLine(file, text string, arg interface{}) string {
|
||||
lines := strings.SplitAfter(p.contents(file), "\n")
|
||||
func (s *Site) oneLine(file, text string, arg interface{}) string {
|
||||
lines := strings.SplitAfter(s.contents(file), "\n")
|
||||
line, pattern, isInt := parseArg(arg, file, len(lines))
|
||||
if isInt {
|
||||
return lines[line-1]
|
||||
|
@ -135,8 +101,8 @@ func (p *Presentation) oneLine(file, text string, arg interface{}) string {
|
|||
}
|
||||
|
||||
// multipleLines returns the text generated by a three-argument code invocation.
|
||||
func (p *Presentation) multipleLines(file, text string, arg1, arg2 interface{}) string {
|
||||
lines := strings.SplitAfter(p.contents(file), "\n")
|
||||
func (s *Site) multipleLines(file, text string, arg1, arg2 interface{}) string {
|
||||
lines := strings.SplitAfter(s.contents(file), "\n")
|
||||
line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
|
||||
line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
|
||||
if !isInt1 {
|
||||
|
@ -155,6 +121,21 @@ func (p *Presentation) multipleLines(file, text string, arg1, arg2 interface{})
|
|||
return strings.Join(lines[line1-1:line2], "")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -45,7 +45,7 @@ func (p *Page) Example(funcName string) template.HTML {
|
|||
// remove surrounding braces
|
||||
code = code[1 : n-1]
|
||||
// unindent
|
||||
code = replaceLeadingIndentation(code, strings.Repeat(" ", TabWidth), "")
|
||||
code = replaceLeadingIndentation(code, strings.Repeat(" ", tabWidth), "")
|
||||
// remove output comment
|
||||
if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
|
||||
code = strings.TrimSpace(code[:loc[0]])
|
||||
|
@ -70,7 +70,7 @@ func (p *Page) Example(funcName string) template.HTML {
|
|||
out = ""
|
||||
}
|
||||
|
||||
t := p.pres.Templates.Lookup("example.html")
|
||||
t := p.site.Templates.Lookup("example.html")
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -194,23 +194,3 @@ func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
|
|||
// 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 example_nameFunc(s string) string {
|
||||
name, suffix := pkgdoc.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 example_suffixFunc(name string) string {
|
||||
_, suffix := pkgdoc.SplitExampleName(name)
|
||||
return suffix
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -16,11 +16,6 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
jsonStart = []byte("<!--{")
|
||||
jsonEnd = []byte("}-->")
|
||||
)
|
||||
|
||||
type file struct {
|
||||
// Copied from document metadata directives
|
||||
Title string
|
||||
|
@ -32,33 +27,13 @@ type file struct {
|
|||
Body []byte // content after metadata
|
||||
}
|
||||
|
||||
type metaJSON struct {
|
||||
type fileJSON struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
Template bool
|
||||
Redirect string // if set, redirect to other URL
|
||||
}
|
||||
|
||||
// extractMetadata extracts the metaJSON from a byte slice.
|
||||
// It returns the metadata and the remaining text.
|
||||
// If no metadata is present, it returns an empty metaJSON and the original text.
|
||||
func extractMetadata(b []byte) (meta metaJSON, 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
|
||||
}
|
||||
|
||||
var join = path.Join
|
||||
|
||||
// open returns the file for a given absolute path or nil if none exists.
|
||||
|
@ -112,7 +87,7 @@ func open(fsys fs.FS, path string) *file {
|
|||
path = filePath[:strings.LastIndex(filePath, "/")+1]
|
||||
}
|
||||
|
||||
js, body, err := extractMetadata(b)
|
||||
js, body, err := parseFile(b)
|
||||
if err != nil {
|
||||
log.Printf("extractMetadata %s: %v", path, err)
|
||||
return nil
|
||||
|
@ -133,3 +108,28 @@ func open(fsys fs.FS, path string) *file {
|
|||
|
||||
return f
|
||||
}
|
||||
|
||||
var (
|
||||
jsonStart = []byte("<!--{")
|
||||
jsonEnd = []byte("}-->")
|
||||
)
|
||||
|
||||
// parseFile extracts the metaJSON from a byte slice.
|
||||
// It returns the metadata and the remaining text.
|
||||
// If no metadata is present, it returns an empty metaJSON and the original text.
|
||||
func parseFile(b []byte) (meta fileJSON, 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
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
@ -14,9 +14,9 @@ import (
|
|||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// IsText reports whether a significant prefix of s looks like correct UTF-8;
|
||||
// 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 {
|
||||
func isText(s []byte) bool {
|
||||
const max = 1024 // at least utf8.UTFMax
|
||||
if len(s) > max {
|
||||
s = s[0:max]
|
||||
|
@ -62,5 +62,5 @@ func isTextFile(fsys fs.FS, filename string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return IsText(buf[0:n])
|
||||
return isText(buf[0:n])
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
|
@ -0,0 +1,100 @@
|
|||
// 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 go1.16
|
||||
// +build go1.16
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/internal/pkgdoc"
|
||||
)
|
||||
|
||||
// docServer serves a package doc tree (/cmd or /pkg).
|
||||
type docServer struct {
|
||||
p *Site
|
||||
d *pkgdoc.Docs
|
||||
}
|
||||
|
||||
func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if maybeRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(rsc): URL should be clean already.
|
||||
relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg"))
|
||||
relpath = strings.TrimPrefix(relpath, "/")
|
||||
|
||||
abspath := path.Join("/src", relpath)
|
||||
mode := pkgdoc.ParseMode(r.FormValue("m"))
|
||||
if relpath == "builtin" {
|
||||
// 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 |= pkgdoc.ModeAll | pkgdoc.ModeBuiltin
|
||||
}
|
||||
info := pkgdoc.Doc(h.d, abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
|
||||
if info.Err != nil {
|
||||
log.Print(info.Err)
|
||||
h.p.ServeError(w, r, info.Err)
|
||||
return
|
||||
}
|
||||
|
||||
var tabtitle, title, subtitle string
|
||||
switch {
|
||||
case info.PDoc != nil:
|
||||
tabtitle = info.PDoc.Name
|
||||
default:
|
||||
tabtitle = info.Dirname
|
||||
title = "Directory "
|
||||
}
|
||||
if title == "" {
|
||||
if info.IsMain {
|
||||
// assume that the directory name is the command name
|
||||
_, tabtitle = path.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"
|
||||
}
|
||||
|
||||
name := "package.html"
|
||||
if info.Dirname == "/src" {
|
||||
name = "packageroot.html"
|
||||
}
|
||||
h.p.ServePage(w, r, Page{
|
||||
Title: title,
|
||||
TabTitle: tabtitle,
|
||||
Subtitle: subtitle,
|
||||
Template: name,
|
||||
Data: info,
|
||||
})
|
||||
}
|
||||
|
||||
// ModeQuery returns the "?m=..." query for the current page.
|
||||
// The page's Data must be a *pkgdoc.Page (to find the mode).
|
||||
func (p *Page) ModeQuery() string {
|
||||
m := p.Data.(*pkgdoc.Page).Mode
|
||||
s := m.String()
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return "?m=" + s
|
||||
}
|
|
@ -5,13 +5,12 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
htmlpkg "html"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
@ -19,9 +18,11 @@ import (
|
|||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/internal/api"
|
||||
"golang.org/x/website/internal/pkgdoc"
|
||||
"golang.org/x/website/internal/spec"
|
||||
"golang.org/x/website/internal/texthtml"
|
||||
|
@ -35,93 +36,125 @@ func toFS(name string) string {
|
|||
return path.Clean(strings.TrimPrefix(name, "/"))
|
||||
}
|
||||
|
||||
// docServer serves a package doc tree (/cmd or /pkg).
|
||||
type docServer struct {
|
||||
p *Presentation
|
||||
d *pkgdoc.Docs
|
||||
// Site is a website served from a file system.
|
||||
type Site struct {
|
||||
fs fs.FS
|
||||
api api.DB
|
||||
|
||||
mux *http.ServeMux
|
||||
fileServer http.Handler
|
||||
|
||||
Templates *template.Template
|
||||
|
||||
// GoogleCN reports whether this request should be marked GoogleCN.
|
||||
// If the function is nil, no requests are marked GoogleCN.
|
||||
GoogleCN func(*http.Request) bool
|
||||
|
||||
// GoogleAnalytics optionally adds Google Analytics via the provided
|
||||
// tracking ID to each page.
|
||||
GoogleAnalytics string
|
||||
|
||||
docFuncs template.FuncMap
|
||||
}
|
||||
|
||||
func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
// NewSite returns a new Presentation from a file system.
|
||||
func NewSite(fsys fs.FS) (*Site, error) {
|
||||
apiDB, err := api.Load(fsys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := &Site{
|
||||
fs: fsys,
|
||||
api: apiDB,
|
||||
mux: http.NewServeMux(),
|
||||
fileServer: http.FileServer(http.FS(fsys)),
|
||||
}
|
||||
docs := &docServer{
|
||||
p: p,
|
||||
d: pkgdoc.NewDocs(fsys),
|
||||
}
|
||||
p.mux.Handle("/cmd/", docs)
|
||||
p.mux.Handle("/pkg/", docs)
|
||||
p.mux.HandleFunc("/", p.serveFile)
|
||||
p.initDocFuncs()
|
||||
|
||||
// TODO(rsc): URL should be clean already.
|
||||
relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg"))
|
||||
relpath = strings.TrimPrefix(relpath, "/")
|
||||
t, err := template.New("").Funcs(siteFuncs).ParseFS(fsys, "lib/godoc/*.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Templates = t
|
||||
|
||||
abspath := path.Join("/src", relpath)
|
||||
mode := pkgdoc.ParseMode(r.FormValue("m"))
|
||||
if relpath == "builtin" {
|
||||
// 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 |= pkgdoc.ModeAll | pkgdoc.ModeBuiltin
|
||||
}
|
||||
info := pkgdoc.Doc(h.d, abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
|
||||
if info.Err != nil {
|
||||
log.Print(info.Err)
|
||||
h.p.ServeError(w, r, info.Err)
|
||||
return
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
var tabtitle, title, subtitle string
|
||||
switch {
|
||||
case info.PDoc != nil:
|
||||
tabtitle = info.PDoc.Name
|
||||
default:
|
||||
tabtitle = info.Dirname
|
||||
title = "Directory "
|
||||
}
|
||||
if title == "" {
|
||||
if info.IsMain {
|
||||
// assume that the directory name is the command name
|
||||
_, tabtitle = path.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"
|
||||
}
|
||||
|
||||
name := "package.html"
|
||||
if info.Dirname == "/src" {
|
||||
name = "packageroot.html"
|
||||
}
|
||||
h.p.ServePage(w, r, Page{
|
||||
Title: title,
|
||||
TabTitle: tabtitle,
|
||||
Subtitle: subtitle,
|
||||
Template: name,
|
||||
Data: info,
|
||||
// ServeError responds to the request with the given error.
|
||||
func (s *Site) ServeError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
s.ServePage(w, r, Page{
|
||||
Title: r.URL.Path,
|
||||
Template: "error.html",
|
||||
Data: err,
|
||||
})
|
||||
}
|
||||
|
||||
// ModeQuery returns the "?m=..." query for the current page.
|
||||
// The page's Data must be a *pkgdoc.Page (to find the mode).
|
||||
func (p *Page) ModeQuery() string {
|
||||
m := p.Data.(*pkgdoc.Page).Mode
|
||||
s := m.String()
|
||||
if s == "" {
|
||||
return ""
|
||||
// ServeHTTP implements http.Handler, dispatching the request appropriately.
|
||||
func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ServePage responds to the request with the content described by page.
|
||||
func (s *Site) ServePage(w http.ResponseWriter, r *http.Request, page Page) {
|
||||
page = s.fullPage(r, page)
|
||||
applyTemplateToResponseWriter(w, s.Templates.Lookup("site.html"), &page)
|
||||
}
|
||||
|
||||
// A Page describes the contents of a webpage to be served.
|
||||
//
|
||||
// A Page's Methods are for use by the templates rendering the page.
|
||||
type Page struct {
|
||||
Title string // <h1>
|
||||
TabTitle string // prefix in <title>; defaults to Title
|
||||
Subtitle string // subtitle (date for spec, memory model)
|
||||
SrcPath string // path to file in /src for text view
|
||||
|
||||
// Template and Data describe the data to be
|
||||
// rendered into the overall site frame template.
|
||||
// If Template is empty, then Data should be a template.HTML
|
||||
// holding raw HTML to render into the site frame.
|
||||
// Otherwise, Template should be the name of a template file
|
||||
// in _content/lib/godoc (for example, "package.html"),
|
||||
// and that template will be executed
|
||||
// (with the *Page as its data argument) to produce HTML.
|
||||
//
|
||||
// The overall site template site.html is also invoked with
|
||||
// the *Page as its data argument. It is what arranges to call Template.
|
||||
Template string // template to apply to data (empty string when Data is raw template.HTML)
|
||||
Data interface{} // data to be rendered into page frame
|
||||
|
||||
// Filled in automatically by ServePage
|
||||
GoogleCN bool // page is being served from golang.google.cn
|
||||
GoogleAnalytics string // Google Analytics tag
|
||||
Version string // current Go version
|
||||
|
||||
site *Site
|
||||
}
|
||||
|
||||
// fullPage returns a copy of page with the “automatic” fields filled in.
|
||||
func (s *Site) fullPage(r *http.Request, page Page) Page {
|
||||
if page.TabTitle == "" {
|
||||
page.TabTitle = page.Title
|
||||
}
|
||||
return "?m=" + s
|
||||
page.Version = runtime.Version()
|
||||
page.GoogleCN = s.googleCN(r)
|
||||
page.GoogleAnalytics = s.GoogleAnalytics
|
||||
page.site = s
|
||||
return page
|
||||
}
|
||||
|
||||
// Invoke invokes the template with the given name on
|
||||
// a copy of p with .Data set to data, returning the resulting HTML.
|
||||
func (p *Page) Invoke(name string, data interface{}) template.HTML {
|
||||
t := p.pres.Templates.Lookup(name)
|
||||
t := p.site.Templates.Lookup(name)
|
||||
var buf bytes.Buffer
|
||||
p1 := *p
|
||||
p1.Data = data
|
||||
|
@ -131,12 +164,12 @@ func (p *Page) Invoke(name string, data interface{}) template.HTML {
|
|||
return template.HTML(buf.String())
|
||||
}
|
||||
|
||||
type writerCapturesErr struct {
|
||||
type writeErrorSaver struct {
|
||||
w io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *writerCapturesErr) Write(p []byte) (int, error) {
|
||||
func (w *writeErrorSaver) Write(p []byte) (int, error) {
|
||||
n, err := w.w.Write(p)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
|
@ -151,7 +184,7 @@ func (w *writerCapturesErr) Write(p []byte) (int, error) {
|
|||
// 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}
|
||||
w := &writeErrorSaver{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.
|
||||
|
@ -161,7 +194,65 @@ func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template,
|
|||
}
|
||||
}
|
||||
|
||||
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
func (s *Site) 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.
|
||||
abspath := r.URL.Path
|
||||
if f := open(s.fs, abspath); f != nil {
|
||||
if f.Path != abspath {
|
||||
// Redirect to canonical path.
|
||||
http.Redirect(w, r, f.Path, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
// Serve from the actual filesystem path.
|
||||
s.serveHTML(w, r, f)
|
||||
return
|
||||
}
|
||||
|
||||
relpath := abspath[1:] // strip leading slash
|
||||
|
||||
dir, err := fs.Stat(s.fs, toFS(abspath))
|
||||
if err != nil {
|
||||
// Check for spurious trailing slash.
|
||||
if strings.HasSuffix(abspath, "/") {
|
||||
trimmed := abspath[:len(abspath)-1]
|
||||
if _, err := fs.Stat(s.fs, toFS(trimmed)); err == nil ||
|
||||
open(s.fs, trimmed) != nil {
|
||||
http.Redirect(w, r, trimmed, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
fsPath := toFS(abspath)
|
||||
if dir != nil && dir.IsDir() {
|
||||
if maybeRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
s.serveDir(w, r, abspath, relpath)
|
||||
return
|
||||
}
|
||||
|
||||
if isTextFile(s.fs, fsPath) {
|
||||
if maybeRedirectFile(w, r) {
|
||||
return
|
||||
}
|
||||
s.serveText(w, r, abspath, relpath)
|
||||
return
|
||||
}
|
||||
|
||||
s.fileServer.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func maybeRedirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
canonical := path.Clean(r.URL.Path)
|
||||
if !strings.HasSuffix(canonical, "/") {
|
||||
canonical += "/"
|
||||
|
@ -175,7 +266,7 @@ func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
func maybeRedirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
c := path.Clean(r.URL.Path)
|
||||
c = strings.TrimRight(c, "/")
|
||||
if r.URL.Path != c {
|
||||
|
@ -187,6 +278,132 @@ func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
|||
return
|
||||
}
|
||||
|
||||
var doctype = []byte("<!DOCTYPE ")
|
||||
|
||||
func (s *Site) serveHTML(w http.ResponseWriter, r *http.Request, f *file) {
|
||||
src := f.Body
|
||||
isMarkdown := strings.HasSuffix(f.FilePath, ".md")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
page := Page{
|
||||
Title: f.Title,
|
||||
Subtitle: f.Subtitle,
|
||||
}
|
||||
|
||||
// evaluate as template if indicated
|
||||
if f.Template {
|
||||
page = s.fullPage(r, page)
|
||||
tmpl, err := template.New("main").Funcs(s.docFuncs).Parse(string(src))
|
||||
if err != nil {
|
||||
log.Printf("parsing template %s: %v", f.Path, err)
|
||||
s.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, page); err != nil {
|
||||
log.Printf("executing template %s: %v", f.Path, err)
|
||||
s.ServeError(w, r, 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", f.Path, err)
|
||||
s.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
src = html
|
||||
}
|
||||
|
||||
// if it's the language spec, add tags to EBNF productions
|
||||
if strings.HasSuffix(f.FilePath, "go_spec.html") {
|
||||
var buf bytes.Buffer
|
||||
spec.Linkify(&buf, src)
|
||||
src = buf.Bytes()
|
||||
}
|
||||
|
||||
page.Data = template.HTML(src)
|
||||
s.ServePage(w, r, page)
|
||||
}
|
||||
|
||||
func (s *Site) serveDir(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
if maybeRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := fs.ReadDir(s.fs, toFS(abspath))
|
||||
if err != nil {
|
||||
s.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var info []fs.FileInfo
|
||||
for _, d := range list {
|
||||
i, err := d.Info()
|
||||
if err == nil {
|
||||
info = append(info, i)
|
||||
}
|
||||
}
|
||||
|
||||
s.ServePage(w, r, Page{
|
||||
Title: "Directory",
|
||||
SrcPath: relpath,
|
||||
TabTitle: relpath,
|
||||
Template: "dirlist.html",
|
||||
Data: info,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Site) serveText(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
src, err := fs.ReadFile(s.fs, toFS(abspath))
|
||||
if err != nil {
|
||||
log.Printf("ReadFile: %s", err)
|
||||
s.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("m") == "text" {
|
||||
s.serveRawText(w, src)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := texthtml.Config{
|
||||
GoComments: path.Ext(abspath) == ".go",
|
||||
Highlight: r.FormValue("h"),
|
||||
Selection: rangeSelection(r.FormValue("s")),
|
||||
Line: 1,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("<pre>")
|
||||
buf.Write(texthtml.Format(src, cfg))
|
||||
buf.WriteString("</pre>")
|
||||
|
||||
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, html.EscapeString(relpath))
|
||||
|
||||
title := "Text file"
|
||||
if strings.HasSuffix(relpath, ".go") {
|
||||
title = "Source file"
|
||||
}
|
||||
s.ServePage(w, r, Page{
|
||||
Title: title,
|
||||
SrcPath: relpath,
|
||||
TabTitle: relpath,
|
||||
Data: template.HTML(buf.String()),
|
||||
})
|
||||
}
|
||||
|
||||
var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
|
||||
|
||||
// rangeSelection computes the Selection for a text range described
|
||||
|
@ -204,206 +421,11 @@ func rangeSelection(str string) texthtml.Selection {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
src, err := fs.ReadFile(p.fs, toFS(abspath))
|
||||
if err != nil {
|
||||
log.Printf("ReadFile: %s", err)
|
||||
p.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("m") == "text" {
|
||||
p.serveText(w, src)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := texthtml.Config{
|
||||
GoComments: path.Ext(abspath) == ".go",
|
||||
Highlight: r.FormValue("h"),
|
||||
Selection: rangeSelection(r.FormValue("s")),
|
||||
Line: 1,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("<pre>")
|
||||
buf.Write(texthtml.Format(src, cfg))
|
||||
buf.WriteString("</pre>")
|
||||
|
||||
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
|
||||
|
||||
title := "Text file"
|
||||
if strings.HasSuffix(relpath, ".go") {
|
||||
title = "Source file"
|
||||
}
|
||||
p.ServePage(w, r, Page{
|
||||
Title: title,
|
||||
SrcPath: relpath,
|
||||
TabTitle: relpath,
|
||||
Data: template.HTML(buf.String()),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := fs.ReadDir(p.fs, toFS(abspath))
|
||||
if err != nil {
|
||||
p.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var info []fs.FileInfo
|
||||
for _, d := range list {
|
||||
i, err := d.Info()
|
||||
if err == nil {
|
||||
info = append(info, i)
|
||||
}
|
||||
}
|
||||
|
||||
p.ServePage(w, r, Page{
|
||||
Title: "Directory",
|
||||
SrcPath: relpath,
|
||||
TabTitle: relpath,
|
||||
Template: "dirlist.html",
|
||||
Data: info,
|
||||
})
|
||||
}
|
||||
|
||||
var doctype = []byte("<!DOCTYPE ")
|
||||
|
||||
func (p *Presentation) serveHTML(w http.ResponseWriter, r *http.Request, f *file) {
|
||||
src := f.Body
|
||||
isMarkdown := strings.HasSuffix(f.FilePath, ".md")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
page := Page{
|
||||
Title: f.Title,
|
||||
Subtitle: f.Subtitle,
|
||||
}
|
||||
|
||||
// evaluate as template if indicated
|
||||
if f.Template {
|
||||
page = p.fullPage(r, page)
|
||||
tmpl, err := template.New("main").Funcs(p.docFuncs).Parse(string(src))
|
||||
if err != nil {
|
||||
log.Printf("parsing template %s: %v", f.Path, err)
|
||||
p.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, page); err != nil {
|
||||
log.Printf("executing template %s: %v", f.Path, err)
|
||||
p.ServeError(w, r, 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", f.Path, err)
|
||||
p.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
src = html
|
||||
}
|
||||
|
||||
// if it's the language spec, add tags to EBNF productions
|
||||
if strings.HasSuffix(f.FilePath, "go_spec.html") {
|
||||
var buf bytes.Buffer
|
||||
spec.Linkify(&buf, src)
|
||||
src = buf.Bytes()
|
||||
}
|
||||
|
||||
page.Data = template.HTML(src)
|
||||
p.ServePage(w, r, page)
|
||||
}
|
||||
|
||||
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.
|
||||
abspath := r.URL.Path
|
||||
if f := open(p.fs, abspath); f != nil {
|
||||
if f.Path != abspath {
|
||||
// Redirect to canonical path.
|
||||
http.Redirect(w, r, f.Path, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
// Serve from the actual filesystem path.
|
||||
p.serveHTML(w, r, f)
|
||||
return
|
||||
}
|
||||
|
||||
relpath := abspath[1:] // strip leading slash
|
||||
|
||||
dir, err := fs.Stat(p.fs, toFS(abspath))
|
||||
if err != nil {
|
||||
// Check for spurious trailing slash.
|
||||
if strings.HasSuffix(abspath, "/") {
|
||||
trimmed := abspath[:len(abspath)-1]
|
||||
if _, err := fs.Stat(p.fs, toFS(trimmed)); err == nil ||
|
||||
open(p.fs, trimmed) != nil {
|
||||
http.Redirect(w, r, trimmed, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
fsPath := toFS(abspath)
|
||||
if dir != nil && dir.IsDir() {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
p.serveDirectory(w, r, abspath, relpath)
|
||||
return
|
||||
}
|
||||
|
||||
if isTextFile(p.fs, fsPath) {
|
||||
if redirectFile(w, r) {
|
||||
return
|
||||
}
|
||||
p.serveTextFile(w, r, abspath, relpath)
|
||||
return
|
||||
}
|
||||
|
||||
p.fileServer.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) serveText(w http.ResponseWriter, text []byte) {
|
||||
func (s *Site) serveRawText(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
|
||||
func (s *Site) googleCN(r *http.Request) bool {
|
||||
return s.GoogleCN != nil && s.GoogleCN(r)
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -16,7 +16,7 @@ import (
|
|||
"testing/fstest"
|
||||
)
|
||||
|
||||
func testServeBody(t *testing.T, p *Presentation, path, body string) {
|
||||
func testServeBody(t *testing.T, p *Site, path, body string) {
|
||||
t.Helper()
|
||||
r := &http.Request{URL: &url.URL{Path: path}}
|
||||
rw := httptest.NewRecorder()
|
||||
|
@ -32,7 +32,7 @@ func TestRedirectAndMetadata(t *testing.T) {
|
|||
"doc/x/index.html": {Data: []byte("Hello, x.")},
|
||||
"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
|
||||
}
|
||||
p, err := NewPresentation(fsys)
|
||||
p, err := NewSite(fsys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func TestRedirectAndMetadata(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
p, err := NewPresentation(fstest.MapFS{
|
||||
p, err := NewSite(fstest.MapFS{
|
||||
"doc/test.md": {Data: []byte("**bold**")},
|
||||
"doc/test2.md": {Data: []byte(`{{"*template*"}}`)},
|
||||
"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -18,29 +18,41 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/internal/history"
|
||||
"golang.org/x/website/internal/pkgdoc"
|
||||
)
|
||||
|
||||
func (p *Presentation) initFuncMap() {
|
||||
p.docFuncs = template.FuncMap{
|
||||
"code": p.code,
|
||||
"releases": func() []*history.Major { return history.Majors },
|
||||
}
|
||||
}
|
||||
|
||||
var siteFuncs = template.FuncMap{
|
||||
// various helpers
|
||||
"basename": path.Base,
|
||||
|
||||
// formatting of Examples
|
||||
"example_name": example_nameFunc,
|
||||
"example_suffix": example_suffixFunc,
|
||||
"example_name": example_name,
|
||||
"example_suffix": example_suffix,
|
||||
|
||||
// Number operation
|
||||
"multiply": func(a, b int) int { return a * b },
|
||||
}
|
||||
|
||||
// example_name takes an example function name and returns its display
|
||||
// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
|
||||
func example_name(s string) string {
|
||||
name, suffix := pkgdoc.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_suffix takes an example function name and returns its suffix in
|
||||
// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
|
||||
func example_suffix(name string) string {
|
||||
_, suffix := pkgdoc.SplitExampleName(name)
|
||||
return suffix
|
||||
}
|
||||
|
||||
func srcToPkg(path string) string {
|
||||
// because of the irregular mapping under goroot
|
||||
// we need to correct certain relative paths
|
||||
|
@ -125,10 +137,10 @@ func (p *Page) SrcPosLink(n interface{}) template.HTML {
|
|||
high = info.FSet.Position(end).Offset
|
||||
}
|
||||
|
||||
return srcPosLinkFunc(relpath, line, low, high)
|
||||
return srcPosLink(relpath, line, low, high)
|
||||
}
|
||||
|
||||
func srcPosLinkFunc(s string, line, low, high int) template.HTML {
|
||||
func srcPosLink(s string, line, low, high int) template.HTML {
|
||||
s = path.Clean("/" + s)
|
||||
if !strings.HasPrefix(s, "/src/") {
|
||||
s = "/src" + s
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import "io"
|
||||
|
||||
|
@ -16,9 +16,9 @@ const (
|
|||
collecting
|
||||
)
|
||||
|
||||
// TabSpacer returns a writer that passes writes through to w,
|
||||
// tabSpacer returns a writer that passes writes through to w,
|
||||
// expanding tabs to one or more spaces ending at a width-spaces-aligned boundary.
|
||||
func TabSpacer(w io.Writer, width int) io.Writer {
|
||||
func tabSpacer(w io.Writer, width int) io.Writer {
|
||||
return &tconv{output: w, tabWidth: width}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package godoc
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -53,7 +53,7 @@ func TestSrcPosLinkFunc(t *testing.T) {
|
|||
{"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 {
|
||||
if got := srcPosLink(tc.src, tc.line, tc.low, tc.high); got != tc.want {
|
||||
t.Errorf("srcPosLink(%v, %v, %v, %v) = %v; want %v", tc.src, tc.line, tc.low, tc.high, got, tc.want)
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ func (h Header) Get(key string) string`))
|
|||
}
|
||||
|
||||
func linkifySource(t *testing.T, src []byte) string {
|
||||
p := &Presentation{}
|
||||
p := &Site{}
|
||||
fset := token.NewFileSet()
|
||||
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
|
@ -196,7 +196,7 @@ func linkifySource(t *testing.T, src []byte) string {
|
|||
FSet: fset,
|
||||
}
|
||||
pg := &Page{
|
||||
pres: p,
|
||||
site: p,
|
||||
Data: pi,
|
||||
}
|
||||
sep := ""
|
Загрузка…
Ссылка в новой задаче