blob: 883ef0fb05d7b5f376c475e2f41d262c6ce8d0c9 [file] [log] [blame]
// 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 (
"bytes"
"encoding/json"
"fmt"
htmlpkg "html"
"io"
"io/fs"
"log"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"text/template"
"golang.org/x/website/internal/pkgdoc"
"golang.org/x/website/internal/spec"
"golang.org/x/website/internal/texthtml"
)
// toFS returns the io/fs name for path (no leading slash).
func toFS(name string) string {
if name == "/" {
return "."
}
return path.Clean(strings.TrimPrefix(name, "/"))
}
// docServer serves a package doc tree (/cmd or /pkg).
type docServer struct {
p *Presentation
d *pkgdoc.Docs
}
func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if redirect(w, r) {
return
}
// TODO(rsc): URL should be clean already.
relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg/"))
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, relpath, info.Err)
return
}
var tabtitle, title, subtitle string
switch {
case info.PAst != nil:
for _, ast := range info.PAst {
tabtitle = ast.Name.Name
break
}
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"
}
info.GoogleCN = h.p.googleCN(r)
var body []byte
if info.Dirname == "/src" {
body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
} else {
body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
}
h.p.ServePage(w, Page{
Title: title,
Tabtitle: tabtitle,
Subtitle: subtitle,
Body: body,
GoogleCN: info.GoogleCN,
})
}
func modeQueryString(m pkgdoc.Mode) string {
s := m.String()
if s == "" {
return ""
}
return "?m=" + s
}
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
log.Printf("%s.Execute: %s", name, err)
}
return buf.Bytes()
}
type writerCapturesErr struct {
w io.Writer
err error
}
func (w *writerCapturesErr) Write(p []byte) (int, error) {
n, err := w.w.Write(p)
if err != nil {
w.err = err
}
return n, err
}
// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
// for the call to template.Execute. It uses an io.Writer wrapper to capture
// errors from the underlying http.ResponseWriter. Errors are logged only when
// they come from the template processing and not the Writer; this avoid
// 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}
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.
if w.err == nil && err != nil {
// Log template errors.
log.Printf("%s.Execute: %s", t.Name(), err)
}
}
func redirect(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
}
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
c := path.Clean(r.URL.Path)
c = strings.TrimRight(c, "/")
if r.URL.Path != c {
url := *r.URL
url.Path = c
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
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, title string) {
src, err := fs.ReadFile(p.Corpus.fs, toFS(abspath))
if err != nil {
log.Printf("ReadFile: %s", err)
p.ServeError(w, r, relpath, 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))
p.ServePage(w, Page{
Title: title,
SrcPath: relpath,
Tabtitle: relpath,
Body: buf.Bytes(),
GoogleCN: p.googleCN(r),
})
}
func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
if redirect(w, r) {
return
}
list, err := fs.ReadDir(p.Corpus.fs, toFS(abspath))
if err != nil {
p.ServeError(w, r, relpath, err)
return
}
var info []fs.FileInfo
for _, d := range list {
i, err := d.Info()
if err == nil {
info = append(info, i)
}
}
p.ServePage(w, Page{
Title: "Directory",
SrcPath: relpath,
Tabtitle: relpath,
Body: applyTemplate(p.DirlistHTML, "dirlistHTML", info),
GoogleCN: p.googleCN(r),
})
}
func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
// get HTML body contents
isMarkdown := false
src, err := fs.ReadFile(p.Corpus.fs, toFS(abspath))
if err != nil && strings.HasSuffix(abspath, ".html") {
if md, errMD := fs.ReadFile(p.Corpus.fs, toFS(strings.TrimSuffix(abspath, ".html")+".md")); errMD == nil {
src = md
isMarkdown = true
err = nil
}
}
if err != nil {
log.Printf("ReadFile: %s", err)
p.ServeError(w, r, relpath, err)
return
}
// if it begins with "<!DOCTYPE " assume it is standalone
// html that doesn't need the template wrapping.
if bytes.HasPrefix(src, doctype) {
w.Write(src)
return
}
// if it begins with a JSON blob, read in the metadata.
meta, src, err := extractMetadata(src)
if err != nil {
log.Printf("decoding metadata %s: %v", relpath, err)
}
page := Page{
Title: meta.Title,
Subtitle: meta.Subtitle,
GoogleCN: p.googleCN(r),
}
// evaluate as template if indicated
if meta.Template {
tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
if err != nil {
log.Printf("parsing template %s: %v", relpath, err)
p.ServeError(w, r, relpath, err)
return
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, page); err != nil {
log.Printf("executing template %s: %v", relpath, err)
p.ServeError(w, r, relpath, err)
return
}
src = buf.Bytes()
}
// Apply markdown as indicated.
// (Note template applies before Markdown.)
if isMarkdown {
html, err := renderMarkdown(src)
if err != nil {
log.Printf("executing markdown %s: %v", relpath, err)
p.ServeError(w, r, relpath, err)
return
}
src = html
}
// if it's the language spec, add tags to EBNF productions
if strings.HasSuffix(abspath, "go_spec.html") {
var buf bytes.Buffer
spec.Linkify(&buf, src)
src = buf.Bytes()
}
page.Body = src
p.ServePage(w, page)
}
func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
p.serveFile(w, r)
}
func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/index.html") {
// We'll show index.html for the directory.
// Use the dir/ version as canonical instead of dir/index.html.
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
return
}
// Check to see if we need to redirect or serve another file.
relpath := r.URL.Path
if m := p.Corpus.MetadataFor(relpath); m != nil {
if m.Path != relpath {
// Redirect to canonical path.
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
return
}
// Serve from the actual filesystem path.
relpath = m.filePath
}
abspath := relpath
relpath = relpath[1:] // strip leading slash
switch path.Ext(relpath) {
case ".html":
p.ServeHTMLDoc(w, r, abspath, relpath)
return
case ".go":
p.serveTextFile(w, r, abspath, relpath, "Source file")
return
}
dir, err := fs.Stat(p.Corpus.fs, toFS(abspath))
if err != nil {
log.Print(err)
p.ServeError(w, r, relpath, err)
return
}
fsPath := toFS(abspath)
if dir != nil && dir.IsDir() {
if redirect(w, r) {
return
}
index := path.Join(fsPath, "index.html")
if isTextFile(p.Corpus.fs, index) || isTextFile(p.Corpus.fs, path.Join(fsPath, "index.md")) {
p.ServeHTMLDoc(w, r, index, index)
return
}
p.serveDirectory(w, r, abspath, relpath)
return
}
if isTextFile(p.Corpus.fs, fsPath) {
if redirectFile(w, r) {
return
}
p.serveTextFile(w, r, abspath, relpath, "Text file")
return
}
p.fileServer.ServeHTTP(w, r)
}
func (p *Presentation) ServeText(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
}