internal/pkgdoc: take on doc-specific parts of internal/web
Not all the package docs-specific parts of the server moved into
internal/pkgdoc before. Finish the job. Now the API for pkgdoc
is like the API for codewalk: just a NewServer that returns a handler.
Speaking of codewalk, unexport the Server type to match the
trimmed-down pkgdoc.
Change-Id: I19ba7351d55fb5d23d551a0296bb89d8abac6e9b
Reviewed-on: https://go-review.googlesource.com/c/website/+/328212
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Website-Publish: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/_content/lib/godoc/example.html b/_content/lib/godoc/example.html
index aef6edb..be4d1b1 100644
--- a/_content/lib/godoc/example.html
+++ b/_content/lib/godoc/example.html
@@ -1,10 +1,10 @@
{{with .Data}}
<div id="example_{{.Name}}" class="toggle">
<div class="collapsed">
- <p class="exampleHeading toggleButton">▹ <span class="text">Example{{example_suffix .Name}}</span></p>
+ <p class="exampleHeading toggleButton">▹ <span class="text">Example{{.Page.ExampleSuffix .Name}}</span></p>
</div>
<div class="expanded">
- <p class="exampleHeading toggleButton">▾ <span class="text">Example{{example_suffix .Name}}</span></p>
+ <p class="exampleHeading toggleButton">▾ <span class="text">Example{{.Page.ExampleSuffix .Name}}</span></p>
{{with .Doc}}<p>{{.}}</p>{{end}}
{{$output := .Output}}
{{with .Play}}
diff --git a/_content/lib/godoc/package.html b/_content/lib/godoc/package.html
index 7efdea1..9189dcc 100644
--- a/_content/lib/godoc/package.html
+++ b/_content/lib/godoc/package.html
@@ -13,7 +13,7 @@
{{with $pkg.PDoc}}
{{if $pkg.IsMain}}
{{/* command documentation */}}
- {{$.Comment .Doc}}
+ {{$pkg.Comment .Doc}}
{{else}}
{{/* package documentation */}}
<div id="short-nav">
@@ -38,8 +38,8 @@
</div>
<div class="expanded">
<h2 class="toggleButton" title="Click to hide Overview section">Overview ▾</h2>
- {{$.Comment .Doc}}
- {{$.Example ""}}
+ {{$pkg.Comment .Doc}}
+ {{range $pkg.FmtExamples ""}}{{$.Invoke "example.html" .}}{{end}}
</div>
</div>
@@ -60,16 +60,16 @@
<dd><a href="#pkg-variables">Variables</a></dd>
{{end}}
{{range .Funcs}}
- <dd><a href="#{{.Name}}">{{$.NodeTOC .Decl}}</a></dd>
+ <dd><a href="#{{.Name}}">{{$pkg.NodeTOC .Decl}}</a></dd>
{{end}}
{{range .Types}}
{{$typeName := .Name}}
<dd><a href="#{{.Name}}">type {{.Name}}</a></dd>
{{range .Funcs}}
- <dd> <a href="#{{.Name}}">{{$.NodeTOC .Decl}}</a></dd>
+ <dd> <a href="#{{.Name}}">{{$pkg.NodeTOC .Decl}}</a></dd>
{{end}}
{{range .Methods}}
- <dd> <a href="#{{$typeName}}.{{.Name}}">{{$.NodeTOC .Decl}}</a></dd>
+ <dd> <a href="#{{$typeName}}.{{.Name}}">{{$pkg.NodeTOC .Decl}}</a></dd>
{{end}}
{{end}}
{{if $pkg.Bugs}}
@@ -84,7 +84,7 @@
<div class="js-expandAll expandAll collapsed">(Expand All)</div>
<dl>
{{range $pkg.Examples}}
- <dd><a class="exampleLink" href="#example_{{.Name}}">{{example_name .Name}}</a></dd>
+ <dd><a class="exampleLink" href="#example_{{.Name}}">{{$pkg.ExampleName .Name}}</a></dd>
{{end}}
</dl>
</div>
@@ -106,70 +106,70 @@
{{with .Consts}}
<h2 id="pkg-constants">Constants</h2>
{{range .}}
- {{$.Comment .Doc}}
- <pre>{{$.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ <pre>{{$pkg.Node .Decl}}</pre>
{{end}}
{{end}}
{{with .Vars}}
<h2 id="pkg-variables">Variables</h2>
{{range .}}
- {{$.Comment .Doc}}
- <pre>{{$.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ <pre>{{$pkg.Node .Decl}}</pre>
{{end}}
{{end}}
{{range .Funcs}}
{{/* Name is a string - no need for FSet */}}
- <h2 id="{{.Name}}">func <a href="{{$.SrcPosLink .Decl}}">{{.Name}}</a>
+ <h2 id="{{.Name}}">func <a href="{{$pkg.SrcPosLink .Decl}}">{{.Name}}</a>
<a class="permalink" href="#{{.Name}}">¶</a>
- {{$since := $.Since "func" "" .Name}}
+ {{$since := $pkg.Since "func" "" .Name}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h2>
- <pre>{{$.Node .Decl}}</pre>
- {{$.Comment .Doc}}
- {{$.Example .Name}}
+ <pre>{{$pkg.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ {{range $pkg.FmtExamples .Name}}{{$.Invoke "example.html" .}}{{end}}
{{end}}
{{range .Types}}
{{$typeName := .Name}}
- <h2 id="{{.Name}}">type <a href="{{$.SrcPosLink .Decl}}">{{$typeName}}</a>
+ <h2 id="{{.Name}}">type <a href="{{$pkg.SrcPosLink .Decl}}">{{$typeName}}</a>
<a class="permalink" href="#{{.Name}}">¶</a>
- {{$since := $.Since "type" "" .Name}}
+ {{$since := $pkg.Since "type" "" .Name}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h2>
- {{$.Comment .Doc}}
- <pre>{{$.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ <pre>{{$pkg.Node .Decl}}</pre>
{{range .Consts}}
- {{$.Comment .Doc}}
- <pre>{{$.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ <pre>{{$pkg.Node .Decl}}</pre>
{{end}}
{{range .Vars}}
- {{$.Comment .Doc}}
- <pre>{{$.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ <pre>{{$pkg.Node .Decl}}</pre>
{{end}}
- {{$.Example .Name}}
+ {{range $pkg.FmtExamples .Name}}{{$.Invoke "example.html" .}}{{end}}
{{range .Funcs}}
- <h3 id="{{.Name}}">func <a href="{{$.SrcPosLink .Decl}}">{{.Name}}</a>
+ <h3 id="{{.Name}}">func <a href="{{$pkg.SrcPosLink .Decl}}">{{.Name}}</a>
<a class="permalink" href="#{{.Name}}">¶</a>
- {{$since := $.Since "func" "" .Name}}
+ {{$since := $pkg.Since "func" "" .Name}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h3>
- <pre>{{$.Node .Decl}}</pre>
- {{$.Comment .Doc}}
- {{$.Example .Name}}
+ <pre>{{$pkg.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ {{range $pkg.FmtExamples .Name}}{{$.Invoke "example.html" .}}{{end}}
{{end}}
{{range .Methods}}
- <h3 id="{{$typeName}}.{{.Name}}">func ({{html .Recv}}) <a href="{{$.SrcPosLink .Decl}}">{{.Name}}</a>
+ <h3 id="{{$typeName}}.{{.Name}}">func ({{html .Recv}}) <a href="{{$pkg.SrcPosLink .Decl}}">{{.Name}}</a>
<a class="permalink" href="#{{$typeName}}.{{.Name}}">¶</a>
- {{$since := $.Since "method" .Recv .Name}}
+ {{$since := $pkg.Since "method" .Recv .Name}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h3>
- <pre>{{$.Node .Decl}}</pre>
- {{$.Comment .Doc}}
- {{$.Example (printf "%s_%s" $typeName .Name)}}
+ <pre>{{$pkg.Node .Decl}}</pre>
+ {{$pkg.Comment .Doc}}
+ {{range $pkg.FmtExamples (printf "%s_%s" $typeName .Name)}}{{$.Invoke "example.html" .}}{{end}}
{{end}}
{{end}}
{{end}}
@@ -178,7 +178,7 @@
<h2 id="pkg-note-BUG">Bugs</h2>
<ul style="list-style: none; padding: 0;">
{{range .}}
- <li><a href="{{$.SrcPosLink .}}" style="float: left;">☞</a> {{$.Comment .Body}}</li>
+ <li><a href="{{$pkg.SrcPosLink .}}" style="float: left;">☞</a> {{$pkg.Comment .Body}}</li>
{{end}}
</ul>
{{end}}
@@ -202,17 +202,17 @@
</tr>
{{end}}
- {{range .List}}
+ {{range .}}
<tr>
{{if $pkg.DirFlat}}
{{if .HasPkg}}
<td class="pkg-name">
- <a href="{{.Path}}/{{$.ModeQuery}}">{{.Path}}</a>
+ <a href="{{.Path}}/{{$pkg.ModeQuery}}">{{.Path}}</a>
</td>
{{end}}
{{else}}
<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
- <a href="{{.Path}}/{{$.ModeQuery}}">{{.Name}}</a>
+ <a href="{{.Path}}/{{$pkg.ModeQuery}}">{{.Name}}</a>
</td>
{{end}}
<td class="pkg-synopsis">
diff --git a/_content/lib/godoc/packageroot.html b/_content/lib/godoc/packageroot.html
index 218c845..e2041b9 100644
--- a/_content/lib/godoc/packageroot.html
+++ b/_content/lib/godoc/packageroot.html
@@ -39,17 +39,17 @@
<th class="pkg-synopsis">Synopsis</th>
</tr>
- {{range .List}}
+ {{range .}}
<tr>
{{if $pkg.DirFlat}}
{{if .HasPkg}}
<td class="pkg-name">
- <a href="{{.Path}}/{{$.ModeQuery}}">{{.Path}}</a>
+ <a href="{{.Path}}/{{$pkg.ModeQuery}}">{{.Path}}</a>
</td>
{{end}}
{{else}}
<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
- <a href="{{.Path}}/{{$.ModeQuery}}">{{.Name}}</a>
+ <a href="{{.Path}}/{{$pkg.ModeQuery}}">{{.Name}}</a>
</td>
{{end}}
<td class="pkg-synopsis">
diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go
index dbb8b63..81b3df1 100644
--- a/cmd/golangorg/server.go
+++ b/cmd/golangorg/server.go
@@ -23,6 +23,7 @@
"cloud.google.com/go/datastore"
"golang.org/x/build/repos"
+ "golang.org/x/tools/playground"
"golang.org/x/website"
"golang.org/x/website/internal/backport/archive/zip"
"golang.org/x/website/internal/backport/html/template"
@@ -32,16 +33,12 @@
"golang.org/x/website/internal/dl"
"golang.org/x/website/internal/env"
"golang.org/x/website/internal/memcache"
+ "golang.org/x/website/internal/pkgdoc"
"golang.org/x/website/internal/proxy"
"golang.org/x/website/internal/redirect"
"golang.org/x/website/internal/short"
"golang.org/x/website/internal/web"
"golang.org/x/website/internal/webtest"
-
- // Registers "/compile" handler that redirects to play.golang.org/compile.
- // If we are in prod we will register "golang.org/compile" separately,
- // which will get used instead.
- _ "golang.org/x/tools/playground"
)
var (
@@ -144,6 +141,15 @@
mux := http.NewServeMux()
mux.Handle("/", site)
+
+ docs, err := pkgdoc.NewServer(fsys, site)
+ if err != nil {
+ log.Fatal(err)
+ }
+ mux.Handle("/cmd/", docs)
+ mux.Handle("/pkg/", docs)
+
+ mux.Handle("/compile", playground.Proxy())
mux.Handle("/doc/codewalk/", codewalk.NewServer(fsys, site))
mux.Handle("/fmt", http.HandlerFunc(fmtHandler))
mux.Handle("/x/", http.HandlerFunc(xHandler))
diff --git a/internal/codewalk/codewalk.go b/internal/codewalk/codewalk.go
index e4cb711..cefe691 100644
--- a/internal/codewalk/codewalk.go
+++ b/internal/codewalk/codewalk.go
@@ -2,14 +2,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// Package codewalk implements support for codewalk documents.
+//
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
// files named _content/doc/codewalk/*.xml.
// For an example and a description of the format, see
-// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
-// and see http://localhost:6060/doc/codewalk/codewalk .
+// https://golang.org/doc/codewalk/codewalk.
// That page is itself a codewalk; the source code for it is
// _content/doc/codewalk/codewalk.xml.
-
package codewalk
import (
@@ -32,17 +32,18 @@
"golang.org/x/website/internal/web"
)
-type Server struct {
+type server struct {
fsys fs.FS
site *web.Site
}
-func NewServer(fsys fs.FS, site *web.Site) *Server {
- return &Server{fsys, site}
+// NewServer returns a new server handling codewalk documents.
+func NewServer(fsys fs.FS, site *web.Site) http.Handler {
+ return &server{fsys, site}
}
// Handler for /doc/codewalk/ and below.
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
relpath := path.Clean(r.URL.Path[1:])
r.ParseForm()
@@ -143,7 +144,7 @@
}
// loadCodewalk reads a codewalk from the named XML file.
-func (s *Server) loadCodewalk(filename string) (*codewalk, error) {
+func (s *server) loadCodewalk(filename string) (*codewalk, error) {
f, err := s.fsys.Open(filename)
if err != nil {
return nil, err
@@ -206,7 +207,7 @@
// codewalkDir serves the codewalk directory listing.
// It scans the directory for subdirectories or files named *.xml
// and prepares a table.
-func (s *Server) codewalkDir(w http.ResponseWriter, r *http.Request, relpath string) {
+func (s *server) codewalkDir(w http.ResponseWriter, r *http.Request, relpath string) {
type elem struct {
Name string
Title string
@@ -245,7 +246,7 @@
// in the response. This format is used for the middle window pane
// of the codewalk pages. It is a separate iframe and does not get
// the usual godoc HTML wrapper.
-func (s *Server) codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
+func (s *server) codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
relpath := strings.Trim(path.Clean(f), "/")
data, err := fs.ReadFile(s.fsys, relpath)
if err != nil {
diff --git a/internal/pkgdoc/dir.go b/internal/pkgdoc/dir.go
index 5ff4814..d71a0cc 100644
--- a/internal/pkgdoc/dir.go
+++ b/internal/pkgdoc/dir.go
@@ -31,10 +31,6 @@
return path.Base(d.Path)
}
-type DirList struct {
- List []DirEntry
-}
-
// DirEntry describes a directory entry.
// The Depth gives the directory depth relative to the overall list,
// for use in presenting a hierarchical directory entry.
@@ -49,19 +45,18 @@
return path.Base(d.Path)
}
-// Lookup looks for the *Directory for a given named path, relative to dir.
-func (dir *Dir) Lookup(name string) *Dir {
- name = path.Join(dir.Path, name)
- if name == dir.Path {
- return dir
+// lookup looks for the *Dir for a given named path, relative to d.
+func (d *Dir) lookup(name string) *Dir {
+ name = path.Join(d.Path, name)
+ if name == d.Path {
+ return d
}
- if dir.Path != "." {
- if !strings.HasPrefix(name, dir.Path) || name[len(dir.Path)] != '/' {
+ if d.Path != "." {
+ if !strings.HasPrefix(name, d.Path) || name[len(d.Path)] != '/' {
return nil
}
- name = name[len(dir.Path)+1:]
+ name = name[len(d.Path)+1:]
}
- d := dir
Walk:
for i := 0; i <= len(name); i++ {
if i == len(name) || name[i] == '/' {
@@ -78,25 +73,24 @@
return d
}
-// List creates a (linear) directory List from a directory tree.
-// If skipRoot is set, the root directory itself is excluded from the list.
+// list creates a (linear) directory list from a directory tree.
// If filter is set, only the directory entries whose paths match the filter
// are included.
-//
-func (dir *Dir) List(filter func(string) bool) *DirList {
- if dir == nil {
+func (d *Dir) list(filter func(string) bool) []DirEntry {
+ if d == nil {
return nil
}
+ root := d
var list []DirEntry
- dir.walk(func(d *Dir, depth int) {
+ root.walk(func(d *Dir, depth int) {
if depth == 0 || filter != nil && !filter(d.Path) {
return
}
// the path is relative to root.Path - remove the root.Path
// prefix (the prefix should always be present but avoid
// crashes and check)
- path := strings.TrimPrefix(d.Path, dir.Path)
+ path := strings.TrimPrefix(d.Path, root.Path)
// remove leading separator if any - path must be relative
path = strings.TrimPrefix(path, "/")
list = append(list, DirEntry{
@@ -110,7 +104,7 @@
if len(list) == 0 {
return nil
}
- return &DirList{list}
+ return list
}
func newDir(fsys fs.FS, fset *token.FileSet, dirpath string) *Dir {
@@ -129,16 +123,16 @@
var dirchs []chan *Dir
var dirs []*Dir
- for _, d := range list {
- filename := path.Join(dirpath, d.Name())
+ for _, de := range list {
+ filename := path.Join(dirpath, de.Name())
switch {
- case isPkgDir(d):
- dir := newDir(fsys, fset, filename)
- if dir != nil {
- dirs = append(dirs, dir)
+ case isPkgDir(de):
+ d := newDir(fsys, fset, filename)
+ if d != nil {
+ dirs = append(dirs, d)
}
- case !haveSummary && isPkgFile(d):
+ case !haveSummary && isPkgFile(de):
// looks like a package file, but may just be a file ending in ".go";
// don't just count it yet (otherwise we may end up with hasPkgFiles even
// though the directory doesn't contain any real package files - was bug)
@@ -219,11 +213,11 @@
len(name) > 0 && name[0] != '_' && name[0] != '.' // ignore _files and .files
}
-// walk calls f(d, depth) for each directory d in the tree rooted at dir, including dir itself.
-// The depth argument specifies the depth of d in the tree.
-// The depth of dir itself is 0.
-func (dir *Dir) walk(f func(d *Dir, depth int)) {
- walkDirs(f, dir, 0)
+// walk calls f for each directory in the tree rooted at d, including d itself.
+// The depth argument specifies the depth of the directory in the tree.
+// The depth of d itself is 0.
+func (d *Dir) walk(f func(d *Dir, depth int)) {
+ walkDirs(f, d, 0)
}
func walkDirs(f func(d *Dir, depth int), d *Dir, depth int) {
diff --git a/internal/pkgdoc/dir_test.go b/internal/pkgdoc/dir_test.go
index 0faa961..bcbcb69 100644
--- a/internal/pkgdoc/dir_test.go
+++ b/internal/pkgdoc/dir_test.go
@@ -15,16 +15,16 @@
)
func TestNewDirTree(t *testing.T) {
- dir := newDir(osfs.DirFS(runtime.GOROOT()), token.NewFileSet(), "src")
- processDir(t, dir)
+ d := newDir(osfs.DirFS(runtime.GOROOT()), token.NewFileSet(), "src")
+ processDir(t, d)
}
-func processDir(t *testing.T, dir *Dir) {
+func processDir(t *testing.T, d *Dir) {
var list []string
- for _, d := range dir.Dirs {
- list = append(list, d.Name())
+ for _, child := range d.Dirs {
+ list = append(list, child.Name())
// recursively process the lower level
- processDir(t, d)
+ processDir(t, child)
}
if sort.StringsAreSorted(list) == false {
@@ -44,9 +44,9 @@
`)},
}
- dir := newDir(fs, token.NewFileSet(), "src/index/suffixarray")
- if got, want := dir.Synopsis, "P0: directory name matches package name"; got != want {
- t.Errorf("dir.Synopsis = %q; want %q", got, want)
+ d := newDir(fs, token.NewFileSet(), "src/index/suffixarray")
+ if got, want := d.Synopsis, "P0: directory name matches package name"; got != want {
+ t.Errorf("d.Synopsis = %q; want %q", got, want)
}
}
diff --git a/internal/pkgdoc/doc.go b/internal/pkgdoc/doc.go
index 6dd86e8..b414f29 100644
--- a/internal/pkgdoc/doc.go
+++ b/internal/pkgdoc/doc.go
@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// Package pkgdoc serves package documentation.
+//
+// The only API for Go programs is NewServer.
+// The exported data structures are consumed by the templates
+// in _content/lib/godoc/package*.html.
package pkgdoc
import (
@@ -13,6 +18,8 @@
"io"
"io/ioutil"
"log"
+ "net/http"
+ "net/url"
"os"
"path"
"path/filepath"
@@ -21,34 +28,54 @@
"unicode"
"unicode/utf8"
+ "golang.org/x/website/internal/api"
"golang.org/x/website/internal/backport/io/fs"
+ "golang.org/x/website/internal/web"
)
-type Docs struct {
+type docs struct {
fs fs.FS
+ api api.DB
+ site *web.Site
root *Dir
}
-func NewDocs(fsys fs.FS) *Docs {
+// NewServer returns an HTTP handler serving package docs
+// for packages loaded from fsys (a tree in GOROOT layout),
+// styled according to site.
+func NewServer(fsys fs.FS, site *web.Site) (http.Handler, error) {
+ apiDB, err := api.Load(fsys)
+ if err != nil {
+ return nil, err
+ }
src := newDir(fsys, token.NewFileSet(), "src")
root := &Dir{
Path: ".",
Dirs: []*Dir{src},
}
- return &Docs{
+ docs := &docs{
fs: fsys,
+ api: apiDB,
+ site: site,
root: root,
}
+ return docs, nil
}
type Page struct {
+ docs *docs // outer doc collection
+
+ Web *web.Page // filled in by caller
+
+ OldDocs bool // use ?m=old in doc links
+
Dirname string // directory containing the package
Err error // error or nil
- Mode Mode // display metadata from query string
+ mode mode // display metadata from query string
// package info
- FSet *token.FileSet // nil if no package documentation
+ fset *token.FileSet // nil if no package documentation
PDoc *doc.Package // nil if no package documentation
Examples []*doc.Example // nil if no example code
Bugs []*doc.Note // nil if no BUG comments
@@ -56,25 +83,25 @@
IsFiltered bool // true if results were filtered
// directory info
- Dirs *DirList // nil if no directory information
- DirFlat bool // if set, show directory in a flat (non-indented) manner
+ Dirs []DirEntry // nil if no directory information
+ DirFlat bool // if set, show directory in a flat (non-indented) manner
}
-func (info *Page) IsEmpty() bool {
- return info.Err != nil || info.PDoc == nil && info.Dirs == nil
+func (p *Page) SetWebPage(w *web.Page) {
+ p.Web = w
}
-type Mode uint
+type mode uint
const (
- ModeAll Mode = 1 << iota // do not filter exports
- ModeFlat // show directory in a flat (non-indented) manner
- ModeMethods // show all embedded methods
- ModeOld // do not redirect to pkg.go.dev
- ModeBuiltin // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
+ modeAll mode = 1 << iota // do not filter exports
+ modeFlat // show directory in a flat (non-indented) manner
+ modeMethods // show all embedded methods
+ modeOld // do not redirect to pkg.go.dev
+ modeBuiltin // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
)
-// modeNames defines names for each PageInfoMode flag.
+// modeNames defines names for each mode flag.
// The order here must match the order of the constants above.
var modeNames = []string{
"all",
@@ -83,8 +110,8 @@
"old",
}
-// generate a query string for persisting PageInfoMode between pages.
-func (m Mode) String() string {
+// generate a query string for persisting the mode m between pages.
+func (m mode) String() string {
s := ""
for i, name := range modeNames {
if m&(1<<i) != 0 && name != "" {
@@ -97,10 +124,10 @@
return s
}
-// ParseMode computes the PageInfoMode flags by analyzing the request
-// URL form value "m". It is value is a comma-separated list of mode names (for example, "all,flat").
-func ParseMode(text string) Mode {
- var mode Mode
+// parseMode computes the mode flags by analyzing the request URL form value "m".
+// Its value is a comma-separated list of mode names (for example, "all,flat").
+func parseMode(text string) mode {
+ var mode mode
for _, k := range strings.Split(text, ",") {
k = strings.TrimSpace(k)
for i, name := range modeNames {
@@ -112,15 +139,15 @@
return mode
}
-// Doc returns the Page for a package directory dir.
+// open returns the Page for a package directory dir.
// Package documentation (Page.PDoc) is extracted from the AST.
// If there is no corresponding package in the
// directory, Page.PDoc is nil. If there are no sub-
// directories, Page.Dirs is nil. If an error occurred, PageInfo.Err is
// set to the respective error but the error is not logged.
-func Doc(d *Docs, dir string, mode Mode, goos, goarch string) *Page {
+func (d *docs) open(dir string, mode mode, goos, goarch string) *Page {
dir = path.Clean(dir)
- info := &Page{Dirname: dir, Mode: mode}
+ info := &Page{docs: d, Dirname: dir, mode: mode}
// Restrict to the package files that would be used when building
// the package on this system. This makes sure that if there are
@@ -138,7 +165,7 @@
f, err := fs.ReadDir(d.fs, filepath.ToSlash(dir))
filtered := make([]os.FileInfo, 0, len(f))
for _, i := range f {
- if mode&ModeAll != 0 || i.Name() != "internal" {
+ if mode&modeAll != 0 || i.Name() != "internal" {
info, err := i.Info()
if err == nil {
filtered = append(filtered, info)
@@ -204,18 +231,18 @@
pkg, _ := ast.NewPackage(fset, files, simpleImporter, nil)
// extract package documentation
- info.FSet = fset
+ info.fset = fset
info.IsMain = pkgname == "main"
// show extracted documentation
var m doc.Mode
- if mode&ModeAll != 0 {
+ if mode&modeAll != 0 {
m |= doc.AllDecls
}
- if mode&ModeMethods != 0 {
+ if mode&modeMethods != 0 {
m |= doc.AllMethods
}
info.PDoc = doc.New(pkg, strings.TrimPrefix(dir, "src/"), m)
- if mode&ModeBuiltin != 0 {
+ if mode&modeBuiltin != 0 {
for _, t := range info.PDoc.Types {
info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
@@ -239,15 +266,15 @@
info.Bugs = info.PDoc.Notes["BUG"]
}
- info.Dirs = d.root.Lookup(dir).List(func(path string) bool { return d.includePath(path, mode) })
- info.DirFlat = mode&ModeFlat != 0
+ info.Dirs = d.root.lookup(dir).list(func(path string) bool { return d.includePath(path, mode) })
+ info.DirFlat = mode&modeFlat != 0
return info
}
-func (d *Docs) includePath(path string, mode Mode) (r bool) {
+func (d *docs) includePath(path string, mode mode) (r bool) {
// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
- if mode&ModeAll != 0 {
+ if mode&modeAll != 0 {
return true
}
if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
@@ -308,7 +335,7 @@
var examples []*doc.Example
globals := globalNames(pkg)
for _, e := range doc.Examples(files...) {
- name := TrimExampleSuffix(e.Name)
+ name := trimExampleSuffix(e.Name)
if name == "" || globals[name] {
examples = append(examples, e)
}
@@ -360,7 +387,7 @@
}
}
-func SplitExampleName(s string) (name, suffix string) {
+func splitExampleName(s string) (name, suffix string) {
i := strings.LastIndex(s, "_")
if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
name = s[:i]
@@ -371,9 +398,9 @@
return
}
-// TrimExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
+// trimExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
// while keeping uppercase Braz in Foo_Braz.
-func TrimExampleSuffix(name string) string {
+func trimExampleSuffix(name string) string {
if i := strings.LastIndex(name, "_"); i != -1 {
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
name = name[:i]
@@ -386,3 +413,123 @@
r, _ := utf8.DecodeRuneInString(s)
return unicode.IsUpper(r)
}
+
+func (d *docs) 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, "/")
+
+ mode := parseMode(r.FormValue("m"))
+
+ // Redirect to pkg.go.dev.
+ // We provide two overrides for the redirect.
+ // First, the request can set ?m=old to get the old pages.
+ // Second, the request can come from China:
+ // since pkg.go.dev is not available in China, we serve the docs directly.
+ if mode&modeOld == 0 && !web.GoogleCN(r) {
+ if relpath == "" {
+ relpath = "std"
+ }
+ suffix := ""
+ if r.Host == "tip.golang.org" {
+ suffix = "@master"
+ }
+ if goos, goarch := r.FormValue("GOOS"), r.FormValue("GOARCH"); goos != "" || goarch != "" {
+ suffix += "?"
+ if goos != "" {
+ suffix += "GOOS=" + url.QueryEscape(goos)
+ }
+ if goarch != "" {
+ if goos != "" {
+ suffix += "&"
+ }
+ suffix += "GOARCH=" + url.QueryEscape(goarch)
+ }
+ }
+ http.Redirect(w, r, "https://pkg.go.dev/"+relpath+suffix, http.StatusTemporaryRedirect)
+ return
+ }
+
+ 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 |= modeAll | modeBuiltin
+ }
+ info := d.open("src/"+relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
+ if info.Err != nil {
+ log.Print(info.Err)
+ d.site.ServeError(w, r, info.Err)
+ return
+ }
+ info.OldDocs = mode&modeOld != 0
+
+ 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"
+ }
+ d.site.ServePage(w, r, web.Page{
+ Title: title,
+ TabTitle: tabtitle,
+ Subtitle: subtitle,
+ Template: name,
+ Data: info,
+ })
+}
+
+// ModeQuery returns the "?m=..." query for the current page.
+func (p *Page) ModeQuery() string {
+ m := p.mode
+ s := m.String()
+ if s == "" {
+ return ""
+ }
+ return "?m=" + s
+}
+
+func maybeRedirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
+ canonical := path.Clean(r.URL.Path)
+ if !strings.HasSuffix(canonical, "/") {
+ canonical += "/"
+ }
+ if r.URL.Path != canonical {
+ url := *r.URL
+ url.Path = canonical
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
+ redirected = true
+ }
+ return
+}
diff --git a/internal/pkgdoc/doc_test.go b/internal/pkgdoc/doc_test.go
index ca5e23f..ad1eaa1 100644
--- a/internal/pkgdoc/doc_test.go
+++ b/internal/pkgdoc/doc_test.go
@@ -8,6 +8,7 @@
"testing"
"golang.org/x/website/internal/backport/testing/fstest"
+ "golang.org/x/website/internal/web"
)
// TestIgnoredGoFiles tests the scenario where a folder has no .go or .c files,
@@ -17,13 +18,22 @@
packageComment := "main is documented in an ignored .go file"
fs := fstest.MapFS{
+ "lib/godoc/x.html": {},
"src/" + packagePath + "/ignored.go": {Data: []byte(`// +build ignore
// ` + packageComment + `
package main`)},
}
- d := NewDocs(fs)
- pInfo := Doc(d, "src/"+packagePath, ModeAll, "linux", "amd64")
+ site, err := web.NewSite(fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ h, err := NewServer(fs, site)
+ if err != nil {
+ t.Fatal(err)
+ }
+ d := h.(*docs)
+ pInfo := d.open("src/"+packagePath, modeAll, "linux", "amd64")
if pInfo.PDoc == nil {
t.Error("pInfo.PDoc = nil; want non-nil.")
@@ -38,14 +48,15 @@
t.Errorf("pInfo.PDoc.ImportPath = %q; want %q.", got, want)
}
}
- if pInfo.FSet == nil {
- t.Error("pInfo.FSet = nil; want non-nil.")
+ if pInfo.fset == nil {
+ t.Error("pInfo.fset = nil; want non-nil.")
}
}
func TestIssue5247(t *testing.T) {
const packagePath = "example.com/p"
fs := fstest.MapFS{
+ "lib/godoc/x.html": {},
"src/" + packagePath + "/p.go": {Data: []byte(`package p
//line notgen.go:3
@@ -55,8 +66,17 @@
//line foo.go:100`)}, // No newline at end to check corner cases.
}
- d := NewDocs(fs)
- pInfo := Doc(d, "src/"+packagePath, 0, "linux", "amd64")
+ site, err := web.NewSite(fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ h, err := NewServer(fs, site)
+ if err != nil {
+ t.Fatal(err)
+ }
+ d := h.(*docs)
+
+ pInfo := d.open("src/"+packagePath, 0, "linux", "amd64")
if got, want := pInfo.PDoc.Funcs[0].Doc, "F doc //line 1 should appear\nline 2 should appear\n"; got != want {
t.Errorf("pInfo.PDoc.Funcs[0].Doc = %q; want %q", got, want)
}
diff --git a/internal/pkgdoc/funcs.go b/internal/pkgdoc/funcs.go
new file mode 100644
index 0000000..01305d0
--- /dev/null
+++ b/internal/pkgdoc/funcs.go
@@ -0,0 +1,467 @@
+// 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.
+
+package pkgdoc
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/doc"
+ "go/format"
+ "go/printer"
+ "go/token"
+ "io"
+ "log"
+ "path"
+ "regexp"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "golang.org/x/website/internal/api"
+ "golang.org/x/website/internal/backport/html/template"
+ "golang.org/x/website/internal/texthtml"
+)
+
+var slashSlash = []byte("//")
+
+// Node formats the given AST node as HTML.
+// Identifiers in the rendered node
+// are turned into links to their documentation.
+func (p *Page) Node(node interface{}) template.HTML {
+ var buf1 bytes.Buffer
+ p.docs.writeNode(&buf1, p, p.fset, node)
+
+ var buf2 bytes.Buffer
+ n, _ := node.(ast.Node)
+ buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
+ AST: n,
+ GoComments: true,
+ OldDocs: p.OldDocs,
+ }))
+ return template.HTML(buf2.String())
+}
+
+// NodeTOC formats the given AST node as HTML
+// for inclusion in the table of contents.
+func (p *Page) NodeTOC(node interface{}) template.HTML {
+ var buf1 bytes.Buffer
+ p.docs.writeNode(&buf1, p, p.fset, node)
+
+ var buf2 bytes.Buffer
+ buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
+ GoComments: true,
+ OldDocs: p.OldDocs,
+ }))
+
+ return sanitize(template.HTML(buf2.String()))
+}
+
+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 (d *docs) writeNode(w io.Writer, pageInfo *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
+ // the right number of spaces is much harder)
+ //
+ // TODO(gri) rethink printer flags - perhaps tconv can be eliminated
+ // with an another printer mode (which is more efficiently
+ // implemented in the printer than here with another layer)
+
+ var pkgName, structName string
+ var apiInfo api.PkgDB
+ if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
+ gd.Tok == token.TYPE && len(gd.Specs) != 0 {
+ pkgName = pageInfo.PDoc.ImportPath
+ if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
+ if _, ok := ts.Type.(*ast.StructType); ok {
+ structName = ts.Name.Name
+ }
+ }
+ apiInfo = d.api[pkgName]
+ }
+
+ var out = w
+ var buf bytes.Buffer
+ if structName != "" {
+ out = &buf
+ }
+
+ mode := printer.TabIndent | printer.UseSpaces
+ err := (&printer.Config{Mode: mode, Tabwidth: tabWidth}).Fprint(tabSpacer(out, tabWidth), fset, x)
+ if err != nil {
+ log.Print(err)
+ }
+
+ // Add comments to struct fields saying which Go version introduced them.
+ if structName != "" {
+ fieldSince := apiInfo.Field[structName]
+ typeSince := apiInfo.Type[structName]
+ // Add/rewrite comments on struct fields to note which Go version added them.
+ var buf2 bytes.Buffer
+ buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
+ bs := bufio.NewScanner(&buf)
+ for bs.Scan() {
+ line := bs.Bytes()
+ field := firstIdent(line)
+ var since string
+ if field != "" {
+ since = fieldSince[field]
+ if since != "" && since == typeSince {
+ // Don't highlight field versions if they were the
+ // same as the struct itself.
+ since = ""
+ }
+ }
+ if since == "" {
+ buf2.Write(line)
+ } else {
+ if bytes.Contains(line, slashSlash) {
+ line = bytes.TrimRight(line, " \t.")
+ buf2.Write(line)
+ buf2.WriteString("; added in Go ")
+ } else {
+ buf2.Write(line)
+ buf2.WriteString(" // Go ")
+ }
+ buf2.WriteString(since)
+ }
+ buf2.WriteByte('\n')
+ }
+ w.Write(buf2.Bytes())
+ }
+}
+
+// firstIdent returns the first identifier in x.
+// This actually parses "identifiers" that begin with numbers too, but we
+// never feed it such input, so it's fine.
+func firstIdent(x []byte) string {
+ x = bytes.TrimSpace(x)
+ i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
+ if i == -1 {
+ return string(x)
+ }
+ return string(x[:i])
+}
+
+// Comment formats the given documentation comment as HTML.
+func (p *Page) Comment(comment string) template.HTML {
+ var buf bytes.Buffer
+ // TODO(gri) Provide list of words (e.g. function parameters)
+ // to be emphasized by ToHTML.
+ doc.ToHTML(&buf, comment, nil) // does html-escaping
+ return template.HTML(buf.String())
+}
+
+// sanitize sanitizes the argument src by replacing newlines with
+// blanks, removing extra blanks, and by removing trailing whitespace
+// and commas before closing parentheses.
+func sanitize(src template.HTML) template.HTML {
+ buf := make([]byte, len(src))
+ j := 0 // buf index
+ comma := -1 // comma index if >= 0
+ for i := 0; i < len(src); i++ {
+ ch := src[i]
+ switch ch {
+ case '\t', '\n', ' ':
+ // ignore whitespace at the beginning, after a blank, or after opening parentheses
+ if j == 0 {
+ continue
+ }
+ if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
+ continue
+ }
+ // replace all whitespace with blanks
+ ch = ' '
+ case ',':
+ comma = j
+ case ')', '}', ']':
+ // remove any trailing comma
+ if comma >= 0 {
+ j = comma
+ }
+ // remove any trailing whitespace
+ if j > 0 && buf[j-1] == ' ' {
+ j--
+ }
+ default:
+ comma = -1
+ }
+ buf[j] = ch
+ j++
+ }
+ // remove trailing blank, if any
+ if j > 0 && buf[j-1] == ' ' {
+ j--
+ }
+ return template.HTML(buf[:j])
+}
+
+// Since reports the Go version that introduced the API feature
+// identified by kind, reeciver, name.
+func (p *Page) Since(kind, receiver, name string) string {
+ pkg := p.PDoc.ImportPath
+ return p.docs.api.Func(pkg, kind, receiver, name)
+}
+
+type Example struct {
+ Page *Page
+ Name string
+ Doc string
+ Code template.HTML
+ Play string
+ Output string
+}
+
+// Example renders the examples for the given function name as HTML.
+func (p *Page) FmtExamples(funcName string) []*Example {
+ var list []*Example
+ for _, eg := range p.Examples {
+ name := trimExampleSuffix(eg.Name)
+
+ if name != funcName {
+ continue
+ }
+
+ // print code
+ cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
+ code := p.Node(cnode)
+ out := eg.Output
+ wholeFile := true
+
+ // Additional formatting if this is a function body.
+ if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
+ wholeFile = false
+ // remove surrounding braces
+ code = code[1 : n-1]
+ // unindent
+ code = template.HTML(replaceLeadingIndentation(string(code), strings.Repeat(" ", tabWidth), ""))
+ // remove output comment
+ if loc := exampleOutputRx.FindStringIndex(string(code)); loc != nil {
+ code = template.HTML(strings.TrimSpace(string(code)[:loc[0]]))
+ }
+ }
+
+ // Write out the playground code in standard Go style
+ // (use tabs, no comment highlight, etc).
+ play := ""
+ if eg.Play != nil {
+ var buf bytes.Buffer
+ eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
+ if err := format.Node(&buf, p.fset, eg.Play); err != nil {
+ log.Print(err)
+ } else {
+ play = buf.String()
+ }
+ }
+
+ // Drop output, as the output comment will appear in the code.
+ if wholeFile && play == "" {
+ out = ""
+ }
+
+ list = append(list, &Example{
+ Page: p,
+ Name: eg.Name,
+ Doc: eg.Doc,
+ Code: code,
+ Play: play,
+ Output: out,
+ })
+ }
+ return list
+}
+
+// replaceLeadingIndentation replaces oldIndent at the beginning of each line
+// with newIndent. This is used for formatting examples. Raw strings that
+// span multiple lines are handled specially: oldIndent is not removed (since
+// go/printer will not add any indentation there), but newIndent is added
+// (since we may still want leading indentation).
+func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
+ // Handle indent at the beginning of the first line. After this, we handle
+ // indentation only after a newline.
+ var buf bytes.Buffer
+ if strings.HasPrefix(body, oldIndent) {
+ buf.WriteString(newIndent)
+ body = body[len(oldIndent):]
+ }
+
+ // Use a state machine to keep track of whether we're in a string or
+ // rune literal while we process the rest of the code.
+ const (
+ codeState = iota
+ runeState
+ interpretedStringState
+ rawStringState
+ )
+ searchChars := []string{
+ "'\"`\n", // codeState
+ `\'`, // runeState
+ `\"`, // interpretedStringState
+ "`\n", // rawStringState
+ // newlineState does not need to search
+ }
+ state := codeState
+ for {
+ i := strings.IndexAny(body, searchChars[state])
+ if i < 0 {
+ buf.WriteString(body)
+ break
+ }
+ c := body[i]
+ buf.WriteString(body[:i+1])
+ body = body[i+1:]
+ switch state {
+ case codeState:
+ switch c {
+ case '\'':
+ state = runeState
+ case '"':
+ state = interpretedStringState
+ case '`':
+ state = rawStringState
+ case '\n':
+ if strings.HasPrefix(body, oldIndent) {
+ buf.WriteString(newIndent)
+ body = body[len(oldIndent):]
+ }
+ }
+
+ case runeState:
+ switch c {
+ case '\\':
+ r, size := utf8.DecodeRuneInString(body)
+ buf.WriteRune(r)
+ body = body[size:]
+ case '\'':
+ state = codeState
+ }
+
+ case interpretedStringState:
+ switch c {
+ case '\\':
+ r, size := utf8.DecodeRuneInString(body)
+ buf.WriteRune(r)
+ body = body[size:]
+ case '"':
+ state = codeState
+ }
+
+ case rawStringState:
+ switch c {
+ case '`':
+ state = codeState
+ case '\n':
+ buf.WriteString(newIndent)
+ }
+ }
+ }
+ return buf.String()
+}
+
+var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
+
+func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
+ if len(cg) == 0 {
+ return cg
+ }
+
+ for i := range cg {
+ if !strings.HasPrefix(cg[i].Text(), "+build ") {
+ // Found the first non-build tag, return from here until the end
+ // of the slice.
+ return cg[i:]
+ }
+ }
+
+ // There weren't any non-build tags, return an empty slice.
+ return []*ast.CommentGroup{}
+}
+
+// ExampleName takes an example function name and returns its display
+// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
+func (*Page) ExampleName(s string) string {
+ name, suffix := 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
+}
+
+// ExampleSuffix takes an example function name and returns its suffix in
+// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
+func (*Page) ExampleSuffix(name string) string {
+ _, suffix := splitExampleName(name)
+ return suffix
+}
+
+// SrcPosLink returns a link to the specific source code position containing n,
+// which must be either an ast.Node or a *doc.Note.
+func (p *Page) SrcPosLink(n interface{}) template.HTML {
+ // n must be an ast.Node or a *doc.Note
+ var pos, end token.Pos
+
+ switch n := n.(type) {
+ case ast.Node:
+ pos = n.Pos()
+ end = n.End()
+ case *doc.Note:
+ pos = n.Pos
+ end = n.End
+ default:
+ panic(fmt.Sprintf("wrong type for SrcPosLink template formatter: %T", n))
+ }
+
+ var relpath string
+ var line int
+ var low, high int // selection offset range
+
+ if pos.IsValid() {
+ xp := p.fset.Position(pos)
+ relpath = xp.Filename
+ line = xp.Line
+ low = xp.Offset
+ }
+ if end.IsValid() {
+ high = p.fset.Position(end).Offset
+ }
+
+ return srcPosLink(relpath, line, low, high)
+}
+
+func srcPosLink(s string, line, low, high int) template.HTML {
+ s = path.Clean("/" + s)
+ if !strings.HasPrefix(s, "/src/") {
+ s = "/src" + s
+ }
+ var buf bytes.Buffer
+ template.HTMLEscape(&buf, []byte(s))
+ // selection ranges are of form "s=low:high"
+ if low < high {
+ fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
+ // if we have a selection, position the page
+ // such that the selection is a bit below the top
+ line -= 10
+ if line < 1 {
+ line = 1
+ }
+ }
+ // line id's in html-printed source are of the
+ // form "L%d" where %d stands for the line number
+ if line > 0 {
+ fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
+ }
+ return template.HTML(buf.String())
+}
diff --git a/internal/pkgdoc/html_test.go b/internal/pkgdoc/html_test.go
new file mode 100644
index 0000000..3b02145
--- /dev/null
+++ b/internal/pkgdoc/html_test.go
@@ -0,0 +1,284 @@
+// 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.
+
+package pkgdoc
+
+import (
+ "bytes"
+ "fmt"
+ "go/parser"
+ "go/token"
+ "strings"
+ "testing"
+
+ "golang.org/x/website/internal/backport/html/template"
+ "golang.org/x/website/internal/web"
+)
+
+func TestSrcPosLink(t *testing.T) {
+ for _, tc := range []struct {
+ src string
+ line int
+ low int
+ high int
+ want template.HTML
+ }{
+ {"/src/fmt/print.go", 42, 30, 50, "/src/fmt/print.go?s=30:50#L32"},
+ {"/src/fmt/print.go", 2, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
+ {"/src/fmt/print.go", 2, 0, 0, "/src/fmt/print.go#L2"},
+ {"/src/fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
+ {"/src/fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
+ {"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 := 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)
+ }
+ }
+}
+
+func TestSanitize(t *testing.T) {
+ for _, tc := range []struct {
+ src template.HTML
+ want template.HTML
+ }{
+ {},
+ {"foo", "foo"},
+ {"func f()", "func f()"},
+ {"func f(a int,)", "func f(a int)"},
+ {"func f(a int,\n)", "func f(a int)"},
+ {"func f(\n\ta int,\n\tb int,\n\tc int,\n)", "func f(a int, b int, c int)"},
+ {" ( a, b, c ) ", "(a, b, c)"},
+ {"( a, b, c int, foo bar , )", "(a, b, c int, foo bar)"},
+ {"{ a, b}", "{a, b}"},
+ {"[ a, b]", "[a, b]"},
+ } {
+ if got := sanitize(tc.src); got != tc.want {
+ t.Errorf("sanitize(%v) = %v; want %v", tc.src, got, tc.want)
+ }
+ }
+}
+
+// Test that we add <span id="StructName.FieldName"> elements
+// to the HTML of struct fields.
+func TestStructFieldsIDAttributes(t *testing.T) {
+ got := linkifySource(t, []byte(`
+package foo
+
+type T struct {
+ NoDoc string
+
+ // Doc has a comment.
+ Doc string
+
+ // Opt, if non-nil, is an option.
+ Opt *int
+
+ // Опция - другое поле.
+ Опция bool
+}
+`))
+ want := `type T struct {
+<span id="T.NoDoc"></span> NoDoc <a href="/pkg/builtin/#string">string</a>
+
+<span id="T.Doc"></span> <span class="comment">// Doc has a comment.</span>
+ Doc <a href="/pkg/builtin/#string">string</a>
+
+<span id="T.Opt"></span> <span class="comment">// Opt, if non-nil, is an option.</span>
+ Opt *<a href="/pkg/builtin/#int">int</a>
+
+<span id="T.Опция"></span> <span class="comment">// Опция - другое поле.</span>
+ Опция <a href="/pkg/builtin/#bool">bool</a>
+}`
+ if got != want {
+ t.Errorf("got: %s\n\nwant: %s\n", got, want)
+ }
+}
+
+// Test that we add <span id="ConstName"> elements to the HTML
+// of definitions in const and var specs.
+func TestValueSpecIDAttributes(t *testing.T) {
+ got := linkifySource(t, []byte(`
+package foo
+
+const (
+ NoDoc string = "NoDoc"
+
+ // Doc has a comment
+ Doc = "Doc"
+
+ NoVal
+)`))
+ want := `const (
+ <span id="NoDoc">NoDoc</span> <a href="/pkg/builtin/#string">string</a> = "NoDoc"
+
+ <span class="comment">// Doc has a comment</span>
+ <span id="Doc">Doc</span> = "Doc"
+
+ <span id="NoVal">NoVal</span>
+)`
+ if got != want {
+ t.Errorf("got: %s\n\nwant: %s\n", got, want)
+ }
+}
+
+func TestCompositeLitLinkFields(t *testing.T) {
+ got := linkifySource(t, []byte(`
+package foo
+
+type T struct {
+ X int
+}
+
+var S T = T{X: 12}`))
+ want := `type T struct {
+<span id="T.X"></span> X <a href="/pkg/builtin/#int">int</a>
+}
+var <span id="S">S</span> <a href="#T">T</a> = <a href="#T">T</a>{<a href="#T.X">X</a>: 12}`
+ if got != want {
+ t.Errorf("got: %s\n\nwant: %s\n", got, want)
+ }
+}
+
+func TestFuncDeclNotLink(t *testing.T) {
+ // Function.
+ got := linkifySource(t, []byte(`
+package http
+
+func Get(url string) (resp *Response, err error)`))
+ want := `func Get(url <a href="/pkg/builtin/#string">string</a>) (resp *<a href="#Response">Response</a>, err <a href="/pkg/builtin/#error">error</a>)`
+ if got != want {
+ t.Errorf("got: %s\n\nwant: %s\n", got, want)
+ }
+
+ // Method.
+ got = linkifySource(t, []byte(`
+package http
+
+func (h Header) Get(key string) string`))
+ want = `func (h <a href="#Header">Header</a>) Get(key <a href="/pkg/builtin/#string">string</a>) <a href="/pkg/builtin/#string">string</a>`
+ if got != want {
+ t.Errorf("got: %s\n\nwant: %s\n", got, want)
+ }
+}
+
+func linkifySource(t *testing.T, src []byte) string {
+ site := &web.Site{}
+ fset := token.NewFileSet()
+ af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var buf bytes.Buffer
+ pi := &Page{
+ fset: fset,
+ }
+ pg := &web.Page{
+ Data: pi,
+ Site: site,
+ }
+ pi.SetWebPage(pg)
+ sep := ""
+ for _, decl := range af.Decls {
+ buf.WriteString(sep)
+ sep = "\n"
+ buf.WriteString(string(pi.Node(decl)))
+ }
+ return buf.String()
+}
+
+func TestReplaceLeadingIndentation(t *testing.T) {
+ oldIndent := strings.Repeat(" ", 2)
+ newIndent := strings.Repeat(" ", 4)
+ tests := []struct {
+ src, want string
+ }{
+ {" foo\n bar\n baz", " foo\n bar\n baz"},
+ {" '`'\n '`'\n", " '`'\n '`'\n"},
+ {" '\\''\n '`'\n", " '\\''\n '`'\n"},
+ {" \"`\"\n \"`\"\n", " \"`\"\n \"`\"\n"},
+ {" `foo\n bar`", " `foo\n bar`"},
+ {" `foo\\`\n bar", " `foo\\`\n bar"},
+ {" '\\`'`foo\n bar", " '\\`'`foo\n bar"},
+ {
+ " if true {\n foo := `One\n \tTwo\nThree`\n }\n",
+ " if true {\n foo := `One\n \tTwo\n Three`\n }\n",
+ },
+ }
+ for _, tc := range tests {
+ if got := replaceLeadingIndentation(tc.src, oldIndent, newIndent); got != tc.want {
+ t.Errorf("replaceLeadingIndentation:\n%v\n---\nhave:\n%v\n---\nwant:\n%v\n",
+ tc.src, got, tc.want)
+ }
+ }
+}
+
+func TestFilterOutBuildAnnotations(t *testing.T) {
+ // TODO: simplify this by using a multiline string once we stop
+ // using go vet from 1.10 on the build dashboard.
+ // https://golang.org/issue/26627
+ src := []byte("// +build !foo\n" +
+ "// +build !anothertag\n" +
+ "\n" +
+ "// non-tag comment\n" +
+ "\n" +
+ "package foo\n" +
+ "\n" +
+ "func bar() int {\n" +
+ " return 42\n" +
+ "}\n")
+
+ fset := token.NewFileSet()
+ af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var found bool
+ for _, cg := range af.Comments {
+ if strings.HasPrefix(cg.Text(), "+build ") {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("TestFilterOutBuildAnnotations is broken: missing build tag in test input")
+ }
+
+ found = false
+ for _, cg := range filterOutBuildAnnotations(af.Comments) {
+ if strings.HasPrefix(cg.Text(), "+build ") {
+ t.Errorf("filterOutBuildAnnotations failed to filter build tag")
+ }
+
+ if strings.Contains(cg.Text(), "non-tag comment") {
+ found = true
+ }
+ }
+ if !found {
+ t.Errorf("filterOutBuildAnnotations should not remove non-build tag comment")
+ }
+}
+
+// Verify that scanIdentifier isn't quadratic.
+// This doesn't actually measure and fail on its own, but it was previously
+// very obvious when running by hand.
+//
+// TODO: if there's a reliable and non-flaky way to test this, do so.
+// Maybe count user CPU time instead of wall time? But that's not easy
+// to do portably in Go.
+func TestStructField(t *testing.T) {
+ for _, n := range []int{10, 100, 1000, 10000} {
+ n := n
+ t.Run(fmt.Sprint(n), func(t *testing.T) {
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, "package foo\n\ntype T struct {\n")
+ for i := 0; i < n; i++ {
+ fmt.Fprintf(&buf, "\t// Field%d is foo.\n\tField%d int\n\n", i, i)
+ }
+ fmt.Fprintf(&buf, "}\n")
+ linkifySource(t, buf.Bytes())
+ })
+ }
+}
diff --git a/internal/web/tab.go b/internal/pkgdoc/tab.go
similarity index 98%
rename from internal/web/tab.go
rename to internal/pkgdoc/tab.go
index 88cb4f7..3a8dfbc 100644
--- a/internal/web/tab.go
+++ b/internal/pkgdoc/tab.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package web
+package pkgdoc
import "io"
diff --git a/internal/web/astfuncs.go b/internal/web/astfuncs.go
deleted file mode 100644
index 4fbe5f7..0000000
--- a/internal/web/astfuncs.go
+++ /dev/null
@@ -1,212 +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.
-
-package web
-
-import (
- "bufio"
- "bytes"
- "go/ast"
- "go/doc"
- "go/printer"
- "go/token"
- "io"
- "log"
- "unicode"
-
- "golang.org/x/website/internal/api"
- "golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/pkgdoc"
- "golang.org/x/website/internal/texthtml"
-)
-
-var slashSlash = []byte("//")
-
-// Node formats the given AST node as HTML.
-// Identifiers in the rendered node
-// are turned into links to their documentation.
-func (p *Page) Node(node interface{}) template.HTML {
- info := p.Data.(*pkgdoc.Page)
- var buf1 bytes.Buffer
- p.site.writeNode(&buf1, info, info.FSet, node)
-
- var buf2 bytes.Buffer
- n, _ := node.(ast.Node)
- buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
- AST: n,
- GoComments: true,
- OldDocs: p.OldDocs,
- }))
- return template.HTML(buf2.String())
-}
-
-// NodeTOC formats the given AST node as HTML
-// for inclusion in the table of contents.
-func (p *Page) NodeTOC(node interface{}) template.HTML {
- info := p.Data.(*pkgdoc.Page)
- var buf1 bytes.Buffer
- p.site.writeNode(&buf1, info, info.FSet, node)
-
- var buf2 bytes.Buffer
- buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
- GoComments: true,
- OldDocs: p.OldDocs,
- }))
-
- return sanitize(template.HTML(buf2.String()))
-}
-
-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 (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
- // the right number of spaces is much harder)
- //
- // TODO(gri) rethink printer flags - perhaps tconv can be eliminated
- // with an another printer mode (which is more efficiently
- // implemented in the printer than here with another layer)
-
- var pkgName, structName string
- var apiInfo api.PkgDB
- if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
- gd.Tok == token.TYPE && len(gd.Specs) != 0 {
- pkgName = pageInfo.PDoc.ImportPath
- if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
- if _, ok := ts.Type.(*ast.StructType); ok {
- structName = ts.Name.Name
- }
- }
- apiInfo = s.api[pkgName]
- }
-
- var out = w
- var buf bytes.Buffer
- if structName != "" {
- out = &buf
- }
-
- mode := printer.TabIndent | printer.UseSpaces
- err := (&printer.Config{Mode: mode, Tabwidth: tabWidth}).Fprint(tabSpacer(out, tabWidth), fset, x)
- if err != nil {
- log.Print(err)
- }
-
- // Add comments to struct fields saying which Go version introduced them.
- if structName != "" {
- fieldSince := apiInfo.Field[structName]
- typeSince := apiInfo.Type[structName]
- // Add/rewrite comments on struct fields to note which Go version added them.
- var buf2 bytes.Buffer
- buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
- bs := bufio.NewScanner(&buf)
- for bs.Scan() {
- line := bs.Bytes()
- field := firstIdent(line)
- var since string
- if field != "" {
- since = fieldSince[field]
- if since != "" && since == typeSince {
- // Don't highlight field versions if they were the
- // same as the struct itself.
- since = ""
- }
- }
- if since == "" {
- buf2.Write(line)
- } else {
- if bytes.Contains(line, slashSlash) {
- line = bytes.TrimRight(line, " \t.")
- buf2.Write(line)
- buf2.WriteString("; added in Go ")
- } else {
- buf2.Write(line)
- buf2.WriteString(" // Go ")
- }
- buf2.WriteString(since)
- }
- buf2.WriteByte('\n')
- }
- w.Write(buf2.Bytes())
- }
-}
-
-// firstIdent returns the first identifier in x.
-// This actually parses "identifiers" that begin with numbers too, but we
-// never feed it such input, so it's fine.
-func firstIdent(x []byte) string {
- x = bytes.TrimSpace(x)
- i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
- if i == -1 {
- return string(x)
- }
- return string(x[:i])
-}
-
-// Comment formats the given documentation comment as HTML.
-func (p *Page) Comment(comment string) template.HTML {
- var buf bytes.Buffer
- // TODO(gri) Provide list of words (e.g. function parameters)
- // to be emphasized by ToHTML.
- doc.ToHTML(&buf, comment, nil) // does html-escaping
- return template.HTML(buf.String())
-}
-
-// sanitize sanitizes the argument src by replacing newlines with
-// blanks, removing extra blanks, and by removing trailing whitespace
-// and commas before closing parentheses.
-func sanitize(src template.HTML) template.HTML {
- buf := make([]byte, len(src))
- j := 0 // buf index
- comma := -1 // comma index if >= 0
- for i := 0; i < len(src); i++ {
- ch := src[i]
- switch ch {
- case '\t', '\n', ' ':
- // ignore whitespace at the beginning, after a blank, or after opening parentheses
- if j == 0 {
- continue
- }
- if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
- continue
- }
- // replace all whitespace with blanks
- ch = ' '
- case ',':
- comma = j
- case ')', '}', ']':
- // remove any trailing comma
- if comma >= 0 {
- j = comma
- }
- // remove any trailing whitespace
- if j > 0 && buf[j-1] == ' ' {
- j--
- }
- default:
- comma = -1
- }
- buf[j] = ch
- j++
- }
- // remove trailing blank, if any
- if j > 0 && buf[j-1] == ' ' {
- j--
- }
- return template.HTML(buf[:j])
-}
-
-// Since reports the Go version that introduced the API feature
-// identified by kind, reeciver, name.
-// 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.site.api.Func(pkg, kind, receiver, name)
-}
diff --git a/internal/web/examplefuncs.go b/internal/web/examplefuncs.go
deleted file mode 100644
index a47dc27..0000000
--- a/internal/web/examplefuncs.go
+++ /dev/null
@@ -1,194 +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.
-
-package web
-
-import (
- "bytes"
- "go/ast"
- "go/format"
- "go/printer"
- "log"
- "regexp"
- "strings"
- "unicode/utf8"
-
- "golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/pkgdoc"
-)
-
-// Example renders the examples for the given function name as HTML.
-// The current package is deduced from p.Data, which must be a *pkgdoc.Page.
-func (p *Page) Example(funcName string) template.HTML {
- info := p.Data.(*pkgdoc.Page)
- var buf bytes.Buffer
- for _, eg := range info.Examples {
- name := pkgdoc.TrimExampleSuffix(eg.Name)
-
- if name != funcName {
- continue
- }
-
- // print code
- cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
- code := string(p.Node(cnode))
- out := eg.Output
- wholeFile := true
-
- // Additional formatting if this is a function body.
- if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
- wholeFile = false
- // remove surrounding braces
- code = code[1 : n-1]
- // unindent
- code = replaceLeadingIndentation(code, strings.Repeat(" ", tabWidth), "")
- // remove output comment
- if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
- code = strings.TrimSpace(code[:loc[0]])
- }
- }
-
- // Write out the playground code in standard Go style
- // (use tabs, no comment highlight, etc).
- play := ""
- if eg.Play != nil {
- var buf bytes.Buffer
- eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
- if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
- log.Print(err)
- } else {
- play = buf.String()
- }
- }
-
- // Drop output, as the output comment will appear in the code.
- if wholeFile && play == "" {
- out = ""
- }
-
- t := p.site.Templates.Lookup("example.html")
- if t == nil {
- return ""
- }
-
- newPage := *p
- newPage.Data = struct {
- Name, Doc, Play, Output string
- Code template.HTML
- }{
- eg.Name, eg.Doc, play, out, template.HTML(code),
- }
- err := t.Execute(&buf, &newPage)
- if err != nil {
- log.Print(err)
- }
- }
- return template.HTML(buf.String())
-}
-
-// replaceLeadingIndentation replaces oldIndent at the beginning of each line
-// with newIndent. This is used for formatting examples. Raw strings that
-// span multiple lines are handled specially: oldIndent is not removed (since
-// go/printer will not add any indentation there), but newIndent is added
-// (since we may still want leading indentation).
-func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
- // Handle indent at the beginning of the first line. After this, we handle
- // indentation only after a newline.
- var buf bytes.Buffer
- if strings.HasPrefix(body, oldIndent) {
- buf.WriteString(newIndent)
- body = body[len(oldIndent):]
- }
-
- // Use a state machine to keep track of whether we're in a string or
- // rune literal while we process the rest of the code.
- const (
- codeState = iota
- runeState
- interpretedStringState
- rawStringState
- )
- searchChars := []string{
- "'\"`\n", // codeState
- `\'`, // runeState
- `\"`, // interpretedStringState
- "`\n", // rawStringState
- // newlineState does not need to search
- }
- state := codeState
- for {
- i := strings.IndexAny(body, searchChars[state])
- if i < 0 {
- buf.WriteString(body)
- break
- }
- c := body[i]
- buf.WriteString(body[:i+1])
- body = body[i+1:]
- switch state {
- case codeState:
- switch c {
- case '\'':
- state = runeState
- case '"':
- state = interpretedStringState
- case '`':
- state = rawStringState
- case '\n':
- if strings.HasPrefix(body, oldIndent) {
- buf.WriteString(newIndent)
- body = body[len(oldIndent):]
- }
- }
-
- case runeState:
- switch c {
- case '\\':
- r, size := utf8.DecodeRuneInString(body)
- buf.WriteRune(r)
- body = body[size:]
- case '\'':
- state = codeState
- }
-
- case interpretedStringState:
- switch c {
- case '\\':
- r, size := utf8.DecodeRuneInString(body)
- buf.WriteRune(r)
- body = body[size:]
- case '"':
- state = codeState
- }
-
- case rawStringState:
- switch c {
- case '`':
- state = codeState
- case '\n':
- buf.WriteString(newIndent)
- }
- }
- }
- return buf.String()
-}
-
-var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
-
-func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
- if len(cg) == 0 {
- return cg
- }
-
- for i := range cg {
- if !strings.HasPrefix(cg[i].Text(), "+build ") {
- // Found the first non-build tag, return from here until the end
- // of the slice.
- return cg[i:]
- }
- }
-
- // There weren't any non-build tags, return an empty slice.
- return []*ast.CommentGroup{}
-}
diff --git a/internal/web/pkgdoc.go b/internal/web/pkgdoc.go
deleted file mode 100644
index 21dc1cd..0000000
--- a/internal/web/pkgdoc.go
+++ /dev/null
@@ -1,128 +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.
-
-package web
-
-import (
- "log"
- "net/http"
- "net/url"
- "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, "/")
-
- mode := pkgdoc.ParseMode(r.FormValue("m"))
-
- // Redirect to pkg.go.dev.
- // We provide two overrides for the redirect.
- // First, the request can set ?m=old to get the old pages.
- // Second, the request can come from China:
- // since pkg.go.dev is not available in China, we serve the docs directly.
- if mode&pkgdoc.ModeOld == 0 && !GoogleCN(r) {
- if relpath == "" {
- relpath = "std"
- }
- suffix := ""
- if r.Host == "tip.golang.org" {
- suffix = "@master"
- }
- if goos, goarch := r.FormValue("GOOS"), r.FormValue("GOARCH"); goos != "" || goarch != "" {
- suffix += "?"
- if goos != "" {
- suffix += "GOOS=" + url.QueryEscape(goos)
- }
- if goarch != "" {
- if goos != "" {
- suffix += "&"
- }
- suffix += "GOARCH=" + url.QueryEscape(goarch)
- }
- }
- http.Redirect(w, r, "https://pkg.go.dev/"+relpath+suffix, http.StatusTemporaryRedirect)
- return
- }
-
- 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, "src/"+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,
- OldDocs: mode&pkgdoc.ModeOld != 0,
- })
-}
-
-// 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/web/site.go b/internal/web/site.go
index 96da21c..c4d9a30 100644
--- a/internal/web/site.go
+++ b/internal/web/site.go
@@ -17,19 +17,16 @@
"strconv"
"strings"
- "golang.org/x/website/internal/api"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/httpfs"
"golang.org/x/website/internal/backport/io/fs"
- "golang.org/x/website/internal/pkgdoc"
"golang.org/x/website/internal/spec"
"golang.org/x/website/internal/texthtml"
)
// Site is a website served from a file system.
type Site struct {
- fs fs.FS
- api api.DB
+ fs fs.FS
mux *http.ServeMux
fileServer http.Handler
@@ -45,22 +42,11 @@
// 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(httpfs.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()
@@ -91,6 +77,9 @@
// 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)
+ if d, ok := page.Data.(interface{ SetWebPage(*Page) }); ok {
+ d.SetWebPage(&page)
+ }
applyTemplateToResponseWriter(w, s.Templates.Lookup("site.html"), &page)
}
@@ -117,15 +106,11 @@
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 for document rendering
- OldDocs bool // use ?m=old in doc links
-
// Filled in automatically by ServePage
GoogleCN bool // served on golang.google.cn
GoogleAnalytics string // Google Analytics tag
- Version string // current Go version
-
- site *Site
+ Version string
+ Site *Site
}
// fullPage returns a copy of page with the “automatic” fields filled in.
@@ -136,14 +121,14 @@
page.Version = runtime.Version()
page.GoogleCN = GoogleCN(r)
page.GoogleAnalytics = s.GoogleAnalytics
- page.site = s
+ 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.site.Templates.Lookup(name)
+ t := p.Site.Templates.Lookup(name)
var buf bytes.Buffer
p1 := *p
p1.Data = data
diff --git a/internal/web/sitefuncs.go b/internal/web/sitefuncs.go
index 02d093c..747973b 100644
--- a/internal/web/sitefuncs.go
+++ b/internal/web/sitefuncs.go
@@ -7,49 +7,21 @@
import (
"bytes"
"fmt"
- "go/ast"
- "go/doc"
- "go/token"
"html"
"path"
"strings"
"golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/pkgdoc"
)
var siteFuncs = template.FuncMap{
// various helpers
"basename": path.Base,
- // formatting of Examples
- "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
@@ -100,64 +72,3 @@
buf.WriteString(`</span>`)
return template.HTML(buf.String())
}
-
-// SrcPosLink returns a link to the specific source code position containing n,
-// which must be either an ast.Node or a *doc.Note.
-// The current package is deduced from p.Data, which must be a *pkgdoc.Page.
-func (p *Page) SrcPosLink(n interface{}) template.HTML {
- info := p.Data.(*pkgdoc.Page)
- // n must be an ast.Node or a *doc.Note
- var pos, end token.Pos
-
- switch n := n.(type) {
- case ast.Node:
- pos = n.Pos()
- end = n.End()
- case *doc.Note:
- pos = n.Pos
- end = n.End
- default:
- panic(fmt.Sprintf("wrong type for SrcPosLink template formatter: %T", n))
- }
-
- var relpath string
- var line int
- var low, high int // selection offset range
-
- if pos.IsValid() {
- p := info.FSet.Position(pos)
- relpath = p.Filename
- line = p.Line
- low = p.Offset
- }
- if end.IsValid() {
- high = info.FSet.Position(end).Offset
- }
-
- return srcPosLink(relpath, line, low, high)
-}
-
-func srcPosLink(s string, line, low, high int) template.HTML {
- s = path.Clean("/" + s)
- if !strings.HasPrefix(s, "/src/") {
- s = "/src" + s
- }
- var buf bytes.Buffer
- template.HTMLEscape(&buf, []byte(s))
- // selection ranges are of form "s=low:high"
- if low < high {
- fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
- // if we have a selection, position the page
- // such that the selection is a bit below the top
- line -= 10
- if line < 1 {
- line = 1
- }
- }
- // line id's in html-printed source are of the
- // form "L%d" where %d stands for the line number
- if line > 0 {
- fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
- }
- return template.HTML(buf.String())
-}
diff --git a/internal/web/template_test.go b/internal/web/template_test.go
index c9fbb84..0c8371a 100644
--- a/internal/web/template_test.go
+++ b/internal/web/template_test.go
@@ -5,15 +5,9 @@
package web
import (
- "bytes"
- "fmt"
- "go/parser"
- "go/token"
- "strings"
"testing"
"golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/pkgdoc"
)
func TestSrcToPkg(t *testing.T) {
@@ -34,203 +28,6 @@
}
}
-func TestSrcPosLinkFunc(t *testing.T) {
- for _, tc := range []struct {
- src string
- line int
- low int
- high int
- want template.HTML
- }{
- {"/src/fmt/print.go", 42, 30, 50, "/src/fmt/print.go?s=30:50#L32"},
- {"/src/fmt/print.go", 2, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
- {"/src/fmt/print.go", 2, 0, 0, "/src/fmt/print.go#L2"},
- {"/src/fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
- {"/src/fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
- {"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 := 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)
- }
- }
-}
-
-func TestSanitize(t *testing.T) {
- for _, tc := range []struct {
- src template.HTML
- want template.HTML
- }{
- {},
- {"foo", "foo"},
- {"func f()", "func f()"},
- {"func f(a int,)", "func f(a int)"},
- {"func f(a int,\n)", "func f(a int)"},
- {"func f(\n\ta int,\n\tb int,\n\tc int,\n)", "func f(a int, b int, c int)"},
- {" ( a, b, c ) ", "(a, b, c)"},
- {"( a, b, c int, foo bar , )", "(a, b, c int, foo bar)"},
- {"{ a, b}", "{a, b}"},
- {"[ a, b]", "[a, b]"},
- } {
- if got := sanitize(tc.src); got != tc.want {
- t.Errorf("sanitize(%v) = %v; want %v", tc.src, got, tc.want)
- }
- }
-}
-
-// Test that we add <span id="StructName.FieldName"> elements
-// to the HTML of struct fields.
-func TestStructFieldsIDAttributes(t *testing.T) {
- got := linkifySource(t, []byte(`
-package foo
-
-type T struct {
- NoDoc string
-
- // Doc has a comment.
- Doc string
-
- // Opt, if non-nil, is an option.
- Opt *int
-
- // Опция - другое поле.
- Опция bool
-}
-`))
- want := `type T struct {
-<span id="T.NoDoc"></span> NoDoc <a href="/pkg/builtin/#string">string</a>
-
-<span id="T.Doc"></span> <span class="comment">// Doc has a comment.</span>
- Doc <a href="/pkg/builtin/#string">string</a>
-
-<span id="T.Opt"></span> <span class="comment">// Opt, if non-nil, is an option.</span>
- Opt *<a href="/pkg/builtin/#int">int</a>
-
-<span id="T.Опция"></span> <span class="comment">// Опция - другое поле.</span>
- Опция <a href="/pkg/builtin/#bool">bool</a>
-}`
- if got != want {
- t.Errorf("got: %s\n\nwant: %s\n", got, want)
- }
-}
-
-// Test that we add <span id="ConstName"> elements to the HTML
-// of definitions in const and var specs.
-func TestValueSpecIDAttributes(t *testing.T) {
- got := linkifySource(t, []byte(`
-package foo
-
-const (
- NoDoc string = "NoDoc"
-
- // Doc has a comment
- Doc = "Doc"
-
- NoVal
-)`))
- want := `const (
- <span id="NoDoc">NoDoc</span> <a href="/pkg/builtin/#string">string</a> = "NoDoc"
-
- <span class="comment">// Doc has a comment</span>
- <span id="Doc">Doc</span> = "Doc"
-
- <span id="NoVal">NoVal</span>
-)`
- if got != want {
- t.Errorf("got: %s\n\nwant: %s\n", got, want)
- }
-}
-
-func TestCompositeLitLinkFields(t *testing.T) {
- got := linkifySource(t, []byte(`
-package foo
-
-type T struct {
- X int
-}
-
-var S T = T{X: 12}`))
- want := `type T struct {
-<span id="T.X"></span> X <a href="/pkg/builtin/#int">int</a>
-}
-var <span id="S">S</span> <a href="#T">T</a> = <a href="#T">T</a>{<a href="#T.X">X</a>: 12}`
- if got != want {
- t.Errorf("got: %s\n\nwant: %s\n", got, want)
- }
-}
-
-func TestFuncDeclNotLink(t *testing.T) {
- // Function.
- got := linkifySource(t, []byte(`
-package http
-
-func Get(url string) (resp *Response, err error)`))
- want := `func Get(url <a href="/pkg/builtin/#string">string</a>) (resp *<a href="#Response">Response</a>, err <a href="/pkg/builtin/#error">error</a>)`
- if got != want {
- t.Errorf("got: %s\n\nwant: %s\n", got, want)
- }
-
- // Method.
- got = linkifySource(t, []byte(`
-package http
-
-func (h Header) Get(key string) string`))
- want = `func (h <a href="#Header">Header</a>) Get(key <a href="/pkg/builtin/#string">string</a>) <a href="/pkg/builtin/#string">string</a>`
- if got != want {
- t.Errorf("got: %s\n\nwant: %s\n", got, want)
- }
-}
-
-func linkifySource(t *testing.T, src []byte) string {
- p := &Site{}
- fset := token.NewFileSet()
- af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
- if err != nil {
- t.Fatal(err)
- }
- var buf bytes.Buffer
- pi := &pkgdoc.Page{
- FSet: fset,
- }
- pg := &Page{
- site: p,
- Data: pi,
- }
- sep := ""
- for _, decl := range af.Decls {
- buf.WriteString(sep)
- sep = "\n"
- buf.WriteString(string(pg.Node(decl)))
- }
- return buf.String()
-}
-
-func TestReplaceLeadingIndentation(t *testing.T) {
- oldIndent := strings.Repeat(" ", 2)
- newIndent := strings.Repeat(" ", 4)
- tests := []struct {
- src, want string
- }{
- {" foo\n bar\n baz", " foo\n bar\n baz"},
- {" '`'\n '`'\n", " '`'\n '`'\n"},
- {" '\\''\n '`'\n", " '\\''\n '`'\n"},
- {" \"`\"\n \"`\"\n", " \"`\"\n \"`\"\n"},
- {" `foo\n bar`", " `foo\n bar`"},
- {" `foo\\`\n bar", " `foo\\`\n bar"},
- {" '\\`'`foo\n bar", " '\\`'`foo\n bar"},
- {
- " if true {\n foo := `One\n \tTwo\nThree`\n }\n",
- " if true {\n foo := `One\n \tTwo\n Three`\n }\n",
- },
- }
- for _, tc := range tests {
- if got := replaceLeadingIndentation(tc.src, oldIndent, newIndent); got != tc.want {
- t.Errorf("replaceLeadingIndentation:\n%v\n---\nhave:\n%v\n---\nwant:\n%v\n",
- tc.src, got, tc.want)
- }
- }
-}
-
func TestSrcBreadcrumbFunc(t *testing.T) {
for _, tc := range []struct {
path string
@@ -261,72 +58,3 @@
}
}
}
-
-func TestFilterOutBuildAnnotations(t *testing.T) {
- // TODO: simplify this by using a multiline string once we stop
- // using go vet from 1.10 on the build dashboard.
- // https://golang.org/issue/26627
- src := []byte("// +build !foo\n" +
- "// +build !anothertag\n" +
- "\n" +
- "// non-tag comment\n" +
- "\n" +
- "package foo\n" +
- "\n" +
- "func bar() int {\n" +
- " return 42\n" +
- "}\n")
-
- fset := token.NewFileSet()
- af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
- if err != nil {
- t.Fatal(err)
- }
-
- var found bool
- for _, cg := range af.Comments {
- if strings.HasPrefix(cg.Text(), "+build ") {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("TestFilterOutBuildAnnotations is broken: missing build tag in test input")
- }
-
- found = false
- for _, cg := range filterOutBuildAnnotations(af.Comments) {
- if strings.HasPrefix(cg.Text(), "+build ") {
- t.Errorf("filterOutBuildAnnotations failed to filter build tag")
- }
-
- if strings.Contains(cg.Text(), "non-tag comment") {
- found = true
- }
- }
- if !found {
- t.Errorf("filterOutBuildAnnotations should not remove non-build tag comment")
- }
-}
-
-// Verify that scanIdentifier isn't quadratic.
-// This doesn't actually measure and fail on its own, but it was previously
-// very obvious when running by hand.
-//
-// TODO: if there's a reliable and non-flaky way to test this, do so.
-// Maybe count user CPU time instead of wall time? But that's not easy
-// to do portably in Go.
-func TestStructField(t *testing.T) {
- for _, n := range []int{10, 100, 1000, 10000} {
- n := n
- t.Run(fmt.Sprint(n), func(t *testing.T) {
- var buf bytes.Buffer
- fmt.Fprintf(&buf, "package foo\n\ntype T struct {\n")
- for i := 0; i < n; i++ {
- fmt.Fprintf(&buf, "\t// Field%d is foo.\n\tField%d int\n\n", i, i)
- }
- fmt.Fprintf(&buf, "}\n")
- linkifySource(t, buf.Bytes())
- })
- }
-}