aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Cox <rsc@golang.org>2021-06-01 10:33:20 -0400
committerRuss Cox <rsc@golang.org>2021-06-14 17:41:29 +0000
commit117627b3c2b2bd5ae6ac92b9e8e3a82f51aa6a6d (patch)
tree7db522dcd6d64fcb392c1cb283dad1655bc89e0e
parent4c9e549253faa360e399b19be93adb3ad820ca4e (diff)
downloadgo-x-website-117627b3c2b2bd5ae6ac92b9e8e3a82f51aa6a6d.tar.xz
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>
-rw-r--r--cmd/golangorg/codewalk.go14
-rw-r--r--cmd/golangorg/handlers.go6
-rw-r--r--cmd/golangorg/main.go8
-rw-r--r--cmd/golangorg/release_test.go10
-rw-r--r--internal/godoc/page.go72
-rw-r--r--internal/godoc/pres.go77
-rw-r--r--internal/web/astfuncs.go (renamed from internal/godoc/astfuncs.go)16
-rw-r--r--internal/web/docfuncs.go (renamed from internal/godoc/template.go)127
-rw-r--r--internal/web/examplefuncs.go (renamed from internal/godoc/examplefuncs.go)26
-rw-r--r--internal/web/file.go (renamed from internal/godoc/meta.go)56
-rw-r--r--internal/web/istext.go (renamed from internal/godoc/util.go)8
-rw-r--r--internal/web/markdown.go (renamed from internal/godoc/markdown.go)2
-rw-r--r--internal/web/pkgdoc.go100
-rw-r--r--internal/web/site.go (renamed from internal/godoc/server.go)466
-rw-r--r--internal/web/site_test.go (renamed from internal/godoc/server_test.go)8
-rw-r--r--internal/web/sitefuncs.go (renamed from internal/godoc/godoc.go)38
-rw-r--r--internal/web/tab.go (renamed from internal/godoc/tab.go)6
-rw-r--r--internal/web/template_test.go (renamed from internal/godoc/godoc_test.go)8
18 files changed, 497 insertions, 551 deletions
diff --git a/cmd/golangorg/codewalk.go b/cmd/golangorg/codewalk.go
index d8d22841..52deb299 100644
--- a/cmd/golangorg/codewalk.go
+++ b/cmd/golangorg/codewalk.go
@@ -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"))
diff --git a/cmd/golangorg/handlers.go b/cmd/golangorg/handlers.go
index 1ee843da..1ea84789 100644
--- a/cmd/golangorg/handlers.go
+++ b/cmd/golangorg/handlers.go
@@ -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")
}
diff --git a/cmd/golangorg/main.go b/cmd/golangorg/main.go
index 9c20031a..7e832d53 100644
--- a/cmd/golangorg/main.go
+++ b/cmd/golangorg/main.go
@@ -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
diff --git a/cmd/golangorg/release_test.go b/cmd/golangorg/release_test.go
index 5c6a5746..9d44ad78 100644
--- a/cmd/golangorg/release_test.go
+++ b/cmd/golangorg/release_test.go
@@ -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()
diff --git a/internal/godoc/page.go b/internal/godoc/page.go
deleted file mode 100644
index 9426dd76..00000000
--- a/internal/godoc/page.go
+++ /dev/null
@@ -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,
- })
-}
diff --git a/internal/godoc/pres.go b/internal/godoc/pres.go
deleted file mode 100644
index 51928c4a..00000000
--- a/internal/godoc/pres.go
+++ /dev/null
@@ -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)
-}
diff --git a/internal/godoc/astfuncs.go b/internal/web/astfuncs.go
index e21b52a3..3e7a4997 100644
--- a/internal/godoc/astfuncs.go
+++ b/internal/web/astfuncs.go
@@ -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)
}
diff --git a/internal/godoc/template.go b/internal/web/docfuncs.go
index 214a1591..a11190f8 100644
--- a/internal/godoc/template.go
+++ b/internal/web/docfuncs.go
@@ -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,46 +16,25 @@ import (
"regexp"
"strings"
+ "golang.org/x/website/internal/history"
"golang.org/x/website/internal/texthtml"
)
-// 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))
- if err != nil {
- log.Panic(err)
+func (s *Site) initDocFuncs() {
+ s.docFuncs = template.FuncMap{
+ "code": s.code,
+ "releases": func() []*history.Major { return history.Majors },
}
- return string(file)
}
-// stringFor returns a textual representation of the arg, formatted according to its nature.
-func stringFor(arg interface{}) string {
- switch arg := arg.(type) {
- case int:
- return fmt.Sprintf("%d", arg)
- case string:
- if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
- return fmt.Sprintf("%#q", arg)
- }
- return fmt.Sprintf("%q", arg)
- default:
- log.Panicf("unrecognized argument: %v type %T", arg, arg)
- }
- return ""
-}
-
-func (p *Presentation) code(file string, arg ...interface{}) (_ template.HTML, err error) {
+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 := p.contents(file)
+ text := s.contents(file)
var command string
switch len(arg) {
case 0:
@@ -90,10 +42,10 @@ func (p *Presentation) code(file string, arg ...interface{}) (_ template.HTML, e
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])
+ text = s.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])
+ 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))
}
@@ -109,24 +61,38 @@ func (p *Presentation) code(file string, arg ...interface{}) (_ template.HTML, e
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) {
+// 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 (s *Site) contents(name string) string {
+ file, err := fs.ReadFile(s.fs, toFS(name))
+ if err != nil {
+ log.Panic(err)
+ }
+ return string(file)
+}
+
+// stringFor returns a textual representation of the arg, formatted according to its nature.
+func stringFor(arg interface{}) string {
+ switch arg := arg.(type) {
case int:
- if n <= 0 || n > max {
- log.Panicf("%q:%d is out of range", file, n)
- }
- return n, "", true
+ return fmt.Sprintf("%d", arg)
case string:
- return 0, n, false
+ if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
+ return fmt.Sprintf("%#q", arg)
+ }
+ return fmt.Sprintf("%q", arg)
+ default:
+ log.Panicf("unrecognized argument: %v type %T", arg, arg)
}
- log.Panicf("unrecognized argument %v type %T", arg, arg)
- return
+ 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.
diff --git a/internal/godoc/examplefuncs.go b/internal/web/examplefuncs.go
index 67099ec3..bfc88daa 100644
--- a/internal/godoc/examplefuncs.go
+++ b/internal/web/examplefuncs.go
@@ -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
-}
diff --git a/internal/godoc/meta.go b/internal/web/file.go
index 27f4def1..f2ff605f 100644
--- a/internal/godoc/meta.go
+++ b/internal/web/file.go
@@ -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
+}
diff --git a/internal/godoc/util.go b/internal/web/istext.go
index ba062a66..c5845406 100644
--- a/internal/godoc/util.go
+++ b/internal/web/istext.go
@@ -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])
}
diff --git a/internal/godoc/markdown.go b/internal/web/markdown.go
index 9149bca5..1ca78c6a 100644
--- a/internal/godoc/markdown.go
+++ b/internal/web/markdown.go
@@ -5,7 +5,7 @@
//go:build go1.16
// +build go1.16
-package godoc
+package web
import (
"bytes"
diff --git a/internal/web/pkgdoc.go b/internal/web/pkgdoc.go
new file mode 100644
index 00000000..c0b5e916
--- /dev/null
+++ b/internal/web/pkgdoc.go
@@ -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
+}
diff --git a/internal/godoc/server.go b/internal/web/site.go
index 90b119f8..57567144 100644
--- a/internal/godoc/server.go
+++ b/internal/web/site.go
@@ -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
-func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if redirect(w, r) {
- return
- }
+ mux *http.ServeMux
+ fileServer http.Handler
- // TODO(rsc): URL should be clean already.
- relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg"))
- relpath = strings.TrimPrefix(relpath, "/")
+ Templates *template.Template
- 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
- }
+ // GoogleCN reports whether this request should be marked GoogleCN.
+ // If the function is nil, no requests are marked GoogleCN.
+ GoogleCN func(*http.Request) bool
- var tabtitle, title, subtitle string
- switch {
- case info.PDoc != nil:
- tabtitle = info.PDoc.Name
- default:
- tabtitle = info.Dirname
- title = "Directory "
+ // GoogleAnalytics optionally adds Google Analytics via the provided
+ // tracking ID to each page.
+ GoogleAnalytics string
+
+ docFuncs template.FuncMap
+}
+
+// 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
}
- if title == "" {
- if info.IsMain {
- // assume that the directory name is the command name
- _, tabtitle = path.Split(relpath)
- title = "Command "
- } else {
- title = "Package "
- }
+ p := &Site{
+ fs: fsys,
+ api: apiDB,
+ mux: http.NewServeMux(),
+ fileServer: http.FileServer(http.FS(fsys)),
}
- 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"
+ 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()
- name := "package.html"
- if info.Dirname == "/src" {
- name = "packageroot.html"
+ t, err := template.New("").Funcs(siteFuncs).ParseFS(fsys, "lib/godoc/*.html")
+ if err != nil {
+ return nil, err
}
- h.p.ServePage(w, r, Page{
- Title: title,
- TabTitle: tabtitle,
- Subtitle: subtitle,
- Template: name,
- Data: info,
+ p.Templates = t
+
+ return p, nil
+}
+
+// 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,93 +278,9 @@ func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
return
}
-var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
-
-// rangeSelection computes the Selection for a text range described
-// by the argument str, of the form Start:End, where Start and End
-// are decimal byte offsets.
-func rangeSelection(str string) texthtml.Selection {
- m := selRx.FindStringSubmatch(str)
- if len(m) >= 2 {
- from, _ := strconv.Atoi(m[1])
- to, _ := strconv.Atoi(m[2])
- if from < to {
- return texthtml.Spans(texthtml.Span{Start: from, End: to})
- }
- }
- 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) {
+func (s *Site) serveHTML(w http.ResponseWriter, r *http.Request, f *file) {
src := f.Body
isMarkdown := strings.HasSuffix(f.FilePath, ".md")
@@ -291,17 +298,17 @@ func (p *Presentation) serveHTML(w http.ResponseWriter, r *http.Request, f *file
// evaluate as template if indicated
if f.Template {
- page = p.fullPage(r, page)
- tmpl, err := template.New("main").Funcs(p.docFuncs).Parse(string(src))
+ 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)
- p.ServeError(w, r, 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)
- p.ServeError(w, r, err)
+ s.ServeError(w, r, err)
return
}
src = buf.Bytes()
@@ -313,7 +320,7 @@ func (p *Presentation) serveHTML(w http.ResponseWriter, r *http.Request, f *file
html, err := renderMarkdown(src)
if err != nil {
log.Printf("executing markdown %s: %v", f.Path, err)
- p.ServeError(w, r, err)
+ s.ServeError(w, r, err)
return
}
src = html
@@ -327,83 +334,98 @@ func (p *Presentation) serveHTML(w http.ResponseWriter, r *http.Request, f *file
}
page.Data = template.HTML(src)
- p.ServePage(w, r, page)
+ s.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)
+func (s *Site) serveDir(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
+ if maybeRedirect(w, r) {
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)
+ list, err := fs.ReadDir(s.fs, toFS(abspath))
+ if err != nil {
+ s.ServeError(w, r, err)
return
}
- relpath := abspath[1:] // strip leading slash
+ var info []fs.FileInfo
+ for _, d := range list {
+ i, err := d.Info()
+ if err == nil {
+ info = append(info, i)
+ }
+ }
- dir, err := fs.Stat(p.fs, toFS(abspath))
+ 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 {
- // 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)
+ log.Printf("ReadFile: %s", err)
+ s.ServeError(w, r, err)
return
}
- fsPath := toFS(abspath)
- if dir != nil && dir.IsDir() {
- if redirect(w, r) {
- return
- }
- p.serveDirectory(w, r, abspath, relpath)
+ if r.FormValue("m") == "text" {
+ s.serveRawText(w, src)
return
}
- if isTextFile(p.fs, fsPath) {
- if redirectFile(w, r) {
- return
- }
- p.serveTextFile(w, r, abspath, relpath)
- return
+ cfg := texthtml.Config{
+ GoComments: path.Ext(abspath) == ".go",
+ Highlight: r.FormValue("h"),
+ Selection: rangeSelection(r.FormValue("s")),
+ Line: 1,
}
- p.fileServer.ServeHTTP(w, r)
+ 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
+// by the argument str, of the form Start:End, where Start and End
+// are decimal byte offsets.
+func rangeSelection(str string) texthtml.Selection {
+ m := selRx.FindStringSubmatch(str)
+ if len(m) >= 2 {
+ from, _ := strconv.Atoi(m[1])
+ to, _ := strconv.Atoi(m[2])
+ if from < to {
+ return texthtml.Spans(texthtml.Span{Start: from, End: to})
+ }
+ }
+ return nil
}
-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)
}
diff --git a/internal/godoc/server_test.go b/internal/web/site_test.go
index 18290360..798ac2f7 100644
--- a/internal/godoc/server_test.go
+++ b/internal/web/site_test.go
@@ -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}}`)},
diff --git a/internal/godoc/godoc.go b/internal/web/sitefuncs.go
index 7e6f9897..04f1ad74 100644
--- a/internal/godoc/godoc.go
+++ b/internal/web/sitefuncs.go
@@ -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
diff --git a/internal/godoc/tab.go b/internal/web/tab.go
index 17fe2936..e84aa8ea 100644
--- a/internal/godoc/tab.go
+++ b/internal/web/tab.go
@@ -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}
}
diff --git a/internal/godoc/godoc_test.go b/internal/web/template_test.go
index eba1bd03..3ca8bbdb 100644
--- a/internal/godoc/godoc_test.go
+++ b/internal/web/template_test.go
@@ -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 := ""