internal/web: merge with go.dev/cmd/internal/site

internal/web was the framework left serving golang.org.
go.dev/cmd/internal/site was the framework serving go.dev.
This CL merges the two into a coherent, simple site serving
framework that works for both sites, a step toward merging
the sites themselves.

The CL is difficult to break up, so it's a bit larger than would be ideal.
The best place to start is the doc comment in internal/web/site.go
and then the other changes in that directory.
The rest of the CL is just minor adjustments to the repo to match.

Change-Id: I927dea29396104a817bd81b6bf25fa43f996968f
Reviewed-on: https://go-review.googlesource.com/c/website/+/339403
Trust: Russ Cox <rsc@golang.org>
Website-Publish: Russ Cox <rsc@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
This commit is contained in:
Russ Cox 2021-08-02 11:23:53 -04:00
Родитель f8f1822414
Коммит ef7fed48ec
52 изменённых файлов: 1509 добавлений и 1360 удалений

Просмотреть файл

@ -4,7 +4,8 @@
license that can be found in the LICENSE file.
-->
{{with .Data}}
{{define "layout"}}
{{with .codewalk}}
<style type='text/css'>@import "/doc/codewalk/codewalk.css";</style>
<script type="text/javascript" src="/doc/codewalk/codewalk.js"></script>
@ -56,3 +57,4 @@
</div>
</div>
{{end}}
{{end}}

Просмотреть файл

@ -4,8 +4,9 @@
license that can be found in the LICENSE file.
-->
{{define "layout"}}
<table class="layout">
{{range .Data}}
{{range .dirs}}
<tr>
<td><a href="{{.Name}}">{{.Name}}</a></td>
<td width="25">&nbsp;</td>
@ -13,3 +14,4 @@
</tr>
{{end}}
</table>
{{end}}

Просмотреть файл

@ -4,7 +4,7 @@
license that can be found in the LICENSE file.
-->
{{with .Data}}
{{define "layout"}}
<p>
<table class="layout">
<tr>
@ -15,10 +15,11 @@
<tr>
<td><a href="../">../</a></td>
</tr>
{{range .}}{{if .IsDir}}
{{range .dir}}{{if .IsDir}}
<tr><td align="left"><a href="{{.Name}}/">{{.Name}}/</a><td></tr>
{{end}}{{end}}
{{range .}}{{if not .IsDir}}
{{range .dir}}{{if not .IsDir}}
<tr><td align="left"><a href="{{.Name}}">{{.Name}}</a><td align="right">{{.Size}}</tr>
{{end}}{{end}}

Просмотреть файл

@ -1,4 +1,5 @@
{{with .Data}}
{{define "layout"}}
{{with .dl}}
<p>
After downloading a binary release suitable for your system,
please follow the <a href="/doc/install">installation instructions</a>.
@ -157,3 +158,4 @@ go get golang.org/dl/{{.Version}}
</div>
</a>
{{end}}
{{end}}

Просмотреть файл

@ -438,7 +438,7 @@ clear, idiomatic Go code.
</p>
<p>
Take {{if $.GoogleCN}}
Take {{if googleCN}}
A Tour of Go
{{else}}
<a href="//tour.golang.org/">A Tour of Go</a>

Просмотреть файл

@ -58,7 +58,7 @@ to build and test packages.
<img class="gopher" src="/doc/gopher/doc.png" alt=""/>
<h3 id="go_tour">
{{if $.GoogleCN}}
{{if googleCN}}
A Tour of Go
{{else}}
<a href="//tour.golang.org/">A Tour of Go</a>
@ -69,7 +69,7 @@ An interactive introduction to Go in three sections.
The first section covers basic syntax and data structures; the second discusses
methods and interfaces; and the third introduces Go's concurrency primitives.
Each section concludes with a few exercises so you can practice what you've
learned. You can {{if not $.GoogleCN}}<a href="//tour.golang.org/">take the tour
learned. You can {{if not googleCN}}<a href="//tour.golang.org/">take the tour
online</a> or{{end}} install it locally with:
</p>
<pre>
@ -254,7 +254,7 @@ Guided tours of Go programs.
<li><a href="/doc/codewalk/sharemem">Share Memory by Communicating</a></li>
</ul>
{{if not $.GoogleCN}}
{{if not googleCN}}
<h2 id="blog">From the Go Blog</h2>
<p>The <a href="//blog.golang.org/">official blog of the Go project</a>, featuring news and in-depth articles by
the Go team and guests.</p>
@ -296,7 +296,7 @@ the Go team and guests.</p>
<li><a href="/doc/gdb">Debugging Go Code with GDB</a></li>
<li><a href="/doc/articles/race_detector.html">Data Race Detector</a> - a manual for the data race detector.</li>
<li><a href="/doc/asm">A Quick Guide to Go's Assembler</a> - an introduction to the assembler used by Go.</li>
{{if not $.GoogleCN}}
{{if not googleCN}}
<li><a href="/blog/c-go-cgo">C? Go? Cgo!</a> - linking against C code with <a href="/cmd/cgo/">cgo</a>.</li>
<li><a href="/blog/godoc-documenting-go-code">Godoc: documenting Go code</a> - writing good documentation for <a href="/cmd/godoc/">godoc</a>.</li>
<li><a href="/blog/profiling-go-programs">Profiling Go Programs</a></li>
@ -314,7 +314,7 @@ See the <a href="/wiki/Learn">Learn</a> page at the <a href="/wiki">Wiki</a>
for more Go learning resources.
</p>
{{if not $.GoogleCN}}
{{if not googleCN}}
<h2 id="talks">Talks</h2>
<img class="gopher" src="/doc/gopher/talks.png" alt=""/>

Просмотреть файл

@ -4,8 +4,8 @@
license that can be found in the LICENSE file.
-->
{{with .Data}}
{{define "layout"}}
<p>
<span class="alert" style="font-size:120%">{{.}}</span>
<span class="alert" style="font-size:120%">{{.error}}</span>
</p>
{{end}}

Просмотреть файл

@ -9,7 +9,7 @@
<img class="gopher" src="/doc/gopher/help.png" alt=""/>
{{if not $.GoogleCN}}
{{if not googleCN}}
<h3 id="mailinglist"><a href="https://groups.google.com/group/golang-nuts">Go Nuts Mailing List</a></h3>
<p>
Get help from Go users, and share your work on the official mailing list.
@ -42,7 +42,7 @@ the Go IRC channel.</p>
<h3 id="faq"><a href="/doc/faq">Frequently Asked Questions (FAQ)</a></h3>
<p>Answers to common questions about Go.</p>
{{if not $.GoogleCN}}
{{if not googleCN}}
<h2 id="inform">Stay informed</h2>
<h3 id="announce"><a href="https://groups.google.com/group/golang-announce">Go Announcements Mailing List</a></h3>
@ -79,7 +79,7 @@ Each month in places around the world, groups of Go programmers ("gophers")
meet to talk about Go. Find a chapter near you.
</p>
{{if not $.GoogleCN}}
{{if not googleCN}}
<h3 id="playground"><a href="/play">Go Playground</a></h3>
<p>A place to write, run, and share Go code.</p>

Просмотреть файл

@ -22,7 +22,7 @@
<section class="HomeSection Playground">
<div class="Playground-headerContainer">
<h2 class="HomeSection-header">Try Go</h2>
{{if not $.GoogleCN}}
{{if not googleCN}}
<a class="Playground-popout js-playgroundShareEl">Open in Playground</a>
{{end}}
</div>
@ -55,7 +55,7 @@ func main() {
<div class="Playground-buttons">
<button class="Button Button--primary js-playgroundRunEl" title="Run this code [shift-enter]">Run</button>
<div class="Playground-secondaryButtons">
{{if not $.GoogleCN}}
{{if not googleCN}}
<button class="Button js-playgroundShareEl" title="Share this code">Share</button>
<a class="Button tour" href="https://tour.golang.org/" title="Playground Go from your browser">Tour</a>
{{end}}
@ -64,7 +64,7 @@ func main() {
</div>
</section>
{{if not $.GoogleCN}}
{{if not googleCN}}
<section class="HomeSection Blog js-blogContainerEl">
<h2 class="HomeSection-header">Featured articles</h2>
<div class="Blog-footer js-blogFooterEl"><a class="Button Button--primary" href="https://blog.golang.org/">Read more &gt;</a></div>
@ -101,7 +101,7 @@ func main() {
}
});
{{if not $.GoogleCN}}
{{if not googleCN}}
function readableTime(t) {
var m = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
@ -175,6 +175,6 @@ func main() {
var v = videos[Math.floor(Math.random()*videos.length)];
$(".js-videoContainer iframe").attr("src", v.s).attr("title", v.title);
});
{{end}} {{/* if not .GoogleCN */}}
{{end}} {{/* if not googleCN */}}
})();
</script>

Просмотреть файл

@ -9,7 +9,9 @@
them to conflict with generated attributes (some of which
correspond to Go identifiers).
-->
{{$pkg := .Data}}
{{define "layout"}}
{{$canShare := not googleCN}}
{{$pkg := .pkg}}
{{with $pkg.PDoc}}
{{if $pkg.IsMain}}
{{/* command documentation */}}
@ -39,7 +41,7 @@
<div class="expanded">
<h2 class="toggleButton" title="Click to hide Overview section">Overview ▾</h2>
{{$pkg.Comment .Doc}}
{{range $pkg.FmtExamples ""}}{{template "example" .}}{{end}}
{{range $pkg.FmtExamples ""}}{{example . $canShare}}{{end}}
</div>
</div>
@ -95,7 +97,7 @@
<p>
<span style="font-size:90%">
{{range .}}
<a href="/{{.}}">{{basename .}}</a>
<a href="/{{.}}">{{path.Base .}}</a>
{{end}}
</span>
</p>
@ -126,7 +128,7 @@
</h2>
<pre>{{$pkg.Node .Decl}}</pre>
{{$pkg.Comment .Doc}}
{{range $pkg.FmtExamples .Name}}{{template "example" .}}{{end}}
{{range $pkg.FmtExamples .Name}}{{example . $canShare}}{{end}}
{{end}}
{{range .Types}}
{{$typeName := .Name}}
@ -148,7 +150,7 @@
<pre>{{$pkg.Node .Decl}}</pre>
{{end}}
{{range $pkg.FmtExamples .Name}}{{template "example" .}}{{end}}
{{range $pkg.FmtExamples .Name}}{{example . $canShare}}{{end}}
{{range .Funcs}}
<h3 id="{{.Name}}">func <a href="{{$pkg.SrcPosLink .Decl}}">{{.Name}}</a>
@ -158,7 +160,7 @@
</h3>
<pre>{{$pkg.Node .Decl}}</pre>
{{$pkg.Comment .Doc}}
{{range $pkg.FmtExamples .Name}}{{template "example" .}}{{end}}
{{range $pkg.FmtExamples .Name}}{{example . $canShare}}{{end}}
{{end}}
{{range .Methods}}
@ -169,7 +171,7 @@
</h3>
<pre>{{$pkg.Node .Decl}}</pre>
{{$pkg.Comment .Doc}}
{{range $pkg.FmtExamples (printf "%s_%s" $typeName .Name)}}{{template "example" .}}{{end}}
{{range $pkg.FmtExamples (printf "%s_%s" $typeName .Name)}}{{example . $canShare}}{{end}}
{{end}}
{{end}}
{{end}}
@ -223,8 +225,11 @@
</table>
</div>
{{end}}
{{end}}
{{define "example"}}
{{define "example ex canShare"}}
{{$canShare := .canShare}}
{{with .ex}}
<div id="example_{{.Name}}" class="toggle">
<div class="collapsed">
<p class="exampleHeading toggleButton">▹ <span class="text">Example{{.Page.ExampleSuffix .Name}}</span></p>
@ -240,7 +245,7 @@
<div class="buttons">
<button class="Button Button--primary run" title="Run this code [shift-enter]">Run</button>
<button class="Button fmt" title="Format this code">Format</button>
{{if not $.Page.Web.GoogleCN}}
{{if $canShare}}
<button class="Button share" title="Share this code">Share</button>
{{end}}
</div>
@ -256,3 +261,4 @@
</div>
</div>
{{end}}
{{end}}

Просмотреть файл

@ -9,7 +9,8 @@
them to conflict with generated attributes (some of which
correspond to Go identifiers).
-->
{{$pkg := .Data}}
{{define "layout"}}
{{$pkg := .pkg}}
{{with $pkg.Dirs}}
{{/* DirList entries are numbers and strings - no need for FSet */}}
@ -99,3 +100,4 @@
<li><a href="/wiki/Projects">Projects at the Go Wiki</a> - a curated list of Go projects.</li>
</ul>
{{end}}
{{end}}

Просмотреть файл

@ -4,7 +4,7 @@
<meta name="description" content="Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#00ADD8">
{{with .TabTitle}}
{{with (or .tabTitle .title (strings.Trim .URL "/"))}}
<title>{{.}} - The Go Programming Language</title>
{{else}}
<title>The Go Programming Language</title>
@ -13,7 +13,7 @@
<link href="https://fonts.googleapis.com/css?family=Product+Sans&text=Supported%20by%20Google&display=swap" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
<script>window.initFuncs = [];</script>
{{with .GoogleAnalytics}}
{{with googleAnalytics}}
<script>
var _gaq = _gaq || [];
_gaq.push(["_setAccount", "{{.}}"]);
@ -29,7 +29,7 @@ window.trackEvent = function(category, action, opt_label, opt_value, opt_noninte
<script src="/lib/godoc/jquery.js" defer></script>
<script src="/lib/godoc/playground.js" defer></script>
{{with .Version}}<script>var goVersion = {{printf "%q" .}};</script>{{end}}
{{with version}}<script>var goVersion = {{printf "%q" .}};</script>{{end}}
<script src="/lib/godoc/godocs.js" defer></script>
<body class="Site">
@ -40,7 +40,7 @@ window.trackEvent = function(category, action, opt_label, opt_value, opt_noninte
target="_blank"
rel="noopener">Support the Equal Justice Initiative.</a>
</div>
<nav class="Header-nav {{if .Title}}Header-nav--wide{{end}}">
<nav class="Header-nav {{if .title}}Header-nav--wide{{end}}">
<a href="/"><img class="Header-logo" src="/lib/godoc/images/go-logo-blue.svg" alt="Go"></a>
<button class="Header-menuButton js-headerMenuButton" aria-label="Main menu" aria-expanded="false">
<div class="Header-menuButtonInner"></div>
@ -50,7 +50,7 @@ window.trackEvent = function(category, action, opt_label, opt_value, opt_noninte
<li class="Header-menuItem"><a href="/pkg/">Packages</a></li>
<li class="Header-menuItem"><a href="/project/">The Project</a></li>
<li class="Header-menuItem"><a href="/help/">Help</a></li>
{{if not .GoogleCN}}
{{if not googleCN}}
<li class="Header-menuItem"><a href="/blog/">Blog</a></li>
<li class="Header-menuItem"><a href="https://play.golang.org/">Play</a></li>
{{end}}
@ -58,38 +58,42 @@ window.trackEvent = function(category, action, opt_label, opt_value, opt_noninte
</nav>
</header>
<main id="page" class="Site-content{{if .Title}} wide{{end}}">
<main id="page" class="Site-content{{if .title}} wide{{end}}">
<div class="container">
{{define "srcBreadcrumb"}}
{{$elems := split . "/"}}
{{$prefix := slice $elems 0 (sub (len $elems) 1)}}
{{if hasSuffix . "/"}}
{{$prefix = slice $elems 0 (sub (len $elems) 2)}}
{{end}}
{{range $i, $elem := $prefix -}}
<a href="/{{join (slice $prefix 0 (add $i 1)) "/"}}">{{$elem}}</a>/
{{- end -}}
<span class="text-muted">{{join (slice $elems (len $prefix) (len $elems)) "/"}} {{len $prefix}} {{len $elems}}</span>
{{define "breadcrumb"}}
{{$elems := strings.Split (strings.Trim . "/") "/"}}
{{$prefix := slice $elems 0 (sub (len $elems) 1)}}
{{if strings.HasSuffix . "/"}}
{{$prefix = slice $elems 0 (sub (len $elems) 2)}}
{{end}}
{{range $i, $elem := $prefix -}}
<a href="/{{strings.Join (slice $prefix 0 (add $i 1)) "/"}}">{{$elem}}</a>/
{{- end -}}
<span class="text-muted">{{strings.Join (slice $elems (len $prefix) (len $elems)) "/"}}</span>
{{end}}
{{if or .Title .SrcPath}}
<h1>
{{.Title}}
{{template "srcBreadcrumb" .SrcPath}}
</h1>
{{if .title}}
<h1>{{.title}}</h1>
{{else if eq .layout "error"}}
<h1>Error</h1>
{{else if eq .layout "dir"}}
<h1>Directory {{breadcrumb .URL}}</h1>
{{else if and (eq .layout "texthtml") (strings.HasSuffix .URL ".go")}}
<h1>Source file {{breadcrumb .URL}}</h1>
{{else if eq .layout "texthtml"}}
<h1>Text file {{breadcrumb .URL}}</h1>
{{end}}
{{with .Subtitle}}
{{with .subtitle}}
<h2>{{.}}</h2>
{{end}}
{{if hasPrefix .SrcPath "src/"}}
{{if strings.HasPrefix .URL "/src/"}}
<h2>
Documentation:
{{$path := trimPrefix .SrcPath "src/"}}
{{if $path}}
<a href="/pkg/{{$path}}">{{$path}}</a>
{{with strings.TrimPrefix .URL "/src/"}}
<a href="/pkg/{{.}}">{{.}}</a>
{{else}}
<a href="/pkg">Index</a>
{{end}}
@ -100,12 +104,12 @@ window.trackEvent = function(category, action, opt_label, opt_value, opt_noninte
Do not delete this <div>. */}}
<div id="nav"></div>
{{.HTML}}
{{block "layout" .}}{{.Content}}{{end}}
</div><!-- .container -->
</main><!-- #page -->
<footer>
<div class="Footer {{if .Title}}Footer--wide{{end}}">
<div class="Footer {{if .title}}Footer--wide{{end}}">
<img class="Footer-gopher" src="/lib/godoc/images/footer-gopher.jpg" alt="The Go Gopher">
<ul class="Footer-links">
<li class="Footer-link"><a href="/doc/copyright.html">Copyright</a></li>
@ -116,7 +120,7 @@ window.trackEvent = function(category, action, opt_label, opt_value, opt_noninte
<a class="Footer-supportedBy" href="https://google.com">Supported by Google</a>
</div>
</footer>
{{if .GoogleAnalytics}}
{{if googleAnalytics}}
<script>
(function() {
var ga = document.createElement("script"); ga.type = "text/javascript"; ga.async = true;

Просмотреть файл

@ -1,3 +1,3 @@
{{define "layout"}}
{{.Content}}
{{.texthtml}}
{{end}}

Просмотреть файл

@ -2,18 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package web
package main
import (
"net/http"
"strings"
)
// GoogleCN reports whether request r is considered to be arriving from China.
// googleCN reports whether request r is considered to be arriving from China.
// Typically that means the request is for host golang.google.cn,
// but we also report true for requests that set googlecn=1 as a query parameter
// and requests that App Engine geolocates in China or in “unknown country.
func GoogleCN(r *http.Request) bool {
// and for requests that App Engine geolocates in China.
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
@ -21,7 +21,7 @@ func GoogleCN(r *http.Request) bool {
return true
}
switch r.Header.Get("X-Appengine-Country") {
case "ZZ", "CN":
case "CN":
return true
}
return false

Просмотреть файл

@ -17,6 +17,7 @@ import (
"log"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"runtime/debug"
@ -36,6 +37,7 @@ import (
"golang.org/x/website/internal/dl"
"golang.org/x/website/internal/env"
"golang.org/x/website/internal/gitfs"
"golang.org/x/website/internal/history"
"golang.org/x/website/internal/memcache"
"golang.org/x/website/internal/pkgdoc"
"golang.org/x/website/internal/proxy"
@ -52,6 +54,8 @@ var (
contentDir = flag.String("content", "", "path to _content directory")
runningOnAppEngine = os.Getenv("PORT") != ""
googleAnalytics string
)
func usage() {
@ -145,6 +149,9 @@ func NewHandler(contentDir, goroot string) http.Handler {
if err != nil {
log.Fatalf("newSite: %v", err)
}
if _, err := newSite(mux, "golang.google.cn", content, gorootFS); err != nil {
log.Fatalf("newSite: %v", err)
}
// tip.golang.org serves content from the very latest Git commit
// of the main Go repo, instead of the one the app is bundled with.
@ -198,12 +205,15 @@ func NewHandler(contentDir, goroot string) http.Handler {
// and registers it in mux to handle requests for host.
// If host is the empty string, the registrations are for the wildcard host.
func newSite(mux *http.ServeMux, host string, content, goroot fs.FS) (*web.Site, error) {
fsys := unionFS{content, goroot}
site, err := web.NewSite(fsys)
if err != nil {
return nil, err
}
docs, err := pkgdoc.NewServer(fsys, site)
fsys := unionFS{content, &fixSpecsFS{goroot}}
site := web.NewSite(fsys)
site.Funcs(template.FuncMap{
"googleAnalytics": func() string { return googleAnalytics },
"googleCN": func() bool { return host == "golang.google.cn" },
"releases": func() []*history.Major { return history.Majors },
"version": func() string { return runtime.Version() },
})
docs, err := pkgdoc.NewServer(fsys, site, googleCN)
if err != nil {
return nil, err
}
@ -282,7 +292,7 @@ func watchTip1(tipGoroot *atomicFS) {
}
func appEngineSetup(site *web.Site, mux *http.ServeMux) {
site.GoogleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
googleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
ctx := context.Background()
@ -302,7 +312,7 @@ func appEngineSetup(site *web.Site, mux *http.ServeMux) {
dl.RegisterHandlers(mux, site, datastoreClient, memcacheClient)
short.RegisterHandlers(mux, datastoreClient, memcacheClient)
proxy.RegisterHandlers(mux)
proxy.RegisterHandlers(mux, googleCN)
log.Println("AppEngine initialization complete")
}
@ -339,22 +349,30 @@ var validHosts = map[string]bool{
}
// hostEnforcerHandler redirects http://foo.golang.org/bar to https://golang.org/bar.
// It permits golang.google.cn as an alias for golang.org, for use in China.
// It also forces all requests coming from China to use golang.google.cn.
func hostEnforcerHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" || r.URL.Scheme == "https"
defaultHost := "golang.org"
isValidHost := validHosts[strings.ToLower(r.Host)]
if googleCN(r) {
// golang.google.cn is the only web site in China.
defaultHost = "golang.google.cn"
isValidHost = strings.ToLower(r.Host) == defaultHost
}
if !isHTTPS || !isValidHost {
r.URL.Scheme = "https"
if isValidHost {
r.URL.Host = r.Host
} else {
r.URL.Host = "golang.org"
r.URL.Host = defaultHost
}
http.Redirect(w, r, r.URL.String(), http.StatusFound)
return
}
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
h.ServeHTTP(w, r)
})
@ -543,6 +561,33 @@ func (fsys unionFS) ReadDir(name string) ([]fs.DirEntry, error) {
return nil, errOut
}
// A fixSpecsFS is an FS mapping /ref/mem.html and /ref/spec.html to
// /doc/go_mem.html and /doc/go_spec.html.
var _ fs.FS = &fixSpecsFS{}
type fixSpecsFS struct {
fs fs.FS
}
func (fsys fixSpecsFS) Open(name string) (fs.File, error) {
switch name {
case "ref/mem.html", "ref/spec.html":
if f, err := fsys.fs.Open(name); err == nil {
// Let Go distribution win if they move.
return f, nil
}
// Otherwise fall back to doc/go_*.html
name = "doc/go_" + strings.TrimPrefix(name, "ref/")
return fsys.fs.Open(name)
case "doc/go_mem.html", "doc/go_spec.html":
data := []byte("<!--{\n\t\"Redirect\": \"/ref/" + strings.TrimPrefix(strings.TrimSuffix(name, ".html"), "doc/go_") + "\"\n}-->\n")
return &memFile{path.Base(name), bytes.NewReader(data)}, nil
}
return fsys.fs.Open(name)
}
// A seekableFS is an FS wrapper that makes every file seekable
// by reading it entirely into memory when it is opened and then
// serving read operations (including seek) from the memory copy.
@ -587,6 +632,20 @@ func (f *seekableFile) Read(b []byte) (int, error) {
return f.Reader.Read(b)
}
// A memFile is an fs.File implementation backed by in-memory data.
type memFile struct {
name string
*bytes.Reader
}
func (f *memFile) Stat() (fs.FileInfo, error) { return f, nil }
func (f *memFile) Name() string { return f.name }
func (*memFile) Mode() fs.FileMode { return 0444 }
func (*memFile) ModTime() time.Time { return time.Time{} }
func (*memFile) IsDir() bool { return false }
func (*memFile) Sys() interface{} { return nil }
func (*memFile) Close() error { return nil }
// An atomicFS is an fs.FS value safe for reading from multiple goroutines
// as well as updating (assigning a different fs.FS to use in future read requests).
type atomicFS struct {

17
cmd/golangorg/testdata/web.txt поставляемый
Просмотреть файл

@ -88,9 +88,11 @@ redirect == https://pkg.go.dev/fmt
GET https://golang.org/pkg/fmt/?m=old
body contains Package fmt implements formatted I/O
body contains Share this code
GET https://golang.google.cn/pkg/fmt/
body contains Package fmt implements formatted I/O
body !contains Share this code
GET https://golang.org/pkg
redirect == /pkg/
@ -203,3 +205,18 @@ redirect == https://mail.google.com/a/golang.org/
GET https://m.golang.org/anything
redirect == https://mail.google.com/a/golang.org/
GET https://golang.org/doc/effective_go
body contains KB ByteSize
GET https://golang.org/doc/codewalk/codewalk/
body contains Codewalk: How to Write a Codewalk
body contains A codewalk is a guided tour
GET https://golang.org/doc/codewalk/
body contains Codewalks
body contains <td>How to Write a Codewalk</td>
GET https://golang.org/asdf
code == 404
body ~ <span class="alert" style="font-size:120%">open ../../_content/asdf: (.*)</span>

Просмотреть файл

@ -1,5 +1,5 @@
{{define "layout"}}
<article class="Article {{if ne .Section "/"}}Article--{{trim .Section "/"}}{{end -}}">
<article class="Article {{with section .}}Article--{{strings.Trim . "/"}}{{end}}">
<h1>{{.title}}</h1>
{{.Content}}
</article>

Просмотреть файл

@ -0,0 +1,11 @@
<!--
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.
-->
{{define "layout"}}
<p>
<span class="alert" style="font-size:120%">{{.error}}</span>
</p>
{{end}}

Просмотреть файл

@ -79,7 +79,7 @@ title: go.dev
</div>
<div class="WhoUsesCaseStudyList">
<ul class="WhoUsesCaseStudyList-gridContainer">
{{- range newest (pages "solutions/*")}}{{if eq .series "Case Studies"}}
{{- range newest (pages "/solutions/*")}}{{if eq .series "Case Studies"}}
{{- if .link }}
{{- if .inLandingPageGrid }}
<li class="WhoUsesCaseStudyList-caseStudy">
@ -97,7 +97,7 @@ title: go.dev
{{- end}}
{{- else}}
<li class="WhoUsesCaseStudyList-caseStudy">
<a href="{{.Path}}" class="WhoUsesCaseStudyList-caseStudyLink">
<a href="{{.URL}}" class="WhoUsesCaseStudyList-caseStudyLink">
<img
loading="lazy"
height="48"
@ -119,7 +119,7 @@ title: go.dev
<div class="GoCarousel-controlsContainer">
<div class="GoCarousel-wrapper">
<ul class="js-testimonialsGoQuotes TestimonialsGo-quotes">
{{- range $index, $element := data "testimonials"}}
{{- range $index, $element := data "/testimonials.yaml"}}
<li class="TestimonialsGo-quoteGroup GoCarousel-slide" id="quote_slide{{$index}}">
<div class="TestimonialsGo-quoteSingleItem">
<div class="TestimonialsGo-quoteSection">
@ -153,7 +153,7 @@ title: go.dev
</h4>
</div>
<ul class="WhyGo-reasons">
{{- range first 4 (data "resources")}}
{{- range first 4 (data "/resources.yaml")}}
<li class="WhyGo-reason">
<div class="WhyGo-reasonDetails">
<div class="WhyGo-reasonIcon" role="presentation">
@ -188,7 +188,7 @@ title: go.dev
</div>
</li>
{{- end}}
{{- if gt (len (data "resources")) 3}}
{{- if gt (len (data "resources.yaml")) 3}}
<li class="WhyGo-reason">
<div class="WhyGo-reasonShowMore">
<div class="WhyGo-reasonShowMoreImgWrapper">
@ -216,7 +216,7 @@ title: go.dev
<div class="GoCarousel-controlsContainer">
<div class="GoCarousel-eventsWrapper">
<ul class="js-goCarouselEventsSlides GoCarousel-eventsSlides">
{{- range $index, $element := (data "events").all}}
{{- range $index, $element := (data "/events.yaml").all}}
<li
class="GoCarousel-eventGroup"
id="event_slide{{$index}}">
@ -312,7 +312,7 @@ title: go.dev
<li class="GettingStartedGo-resourcesHeader">
In-Person Trainings
</li>
{{- range first 4 (data "learn/training")}}
{{- range first 4 (data "/learn/training.yaml")}}
<li class="GettingStartedGo-resourceItem">
<a href="{{.url}}" class="GettingStartedGo-resourceItemTitle">
{{.title}}

Просмотреть файл

@ -45,7 +45,7 @@ title: "Getting Started"
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-quickstarts Learn-cardList">
{{- range first 3 (data "learn/quickstart")}}
{{- range first 3 (data "quickstart.yaml")}}
<li class="Learn-quickstart Learn-card">
{{- template "learn-card" .}}
</li>
@ -66,7 +66,7 @@ title: "Getting Started"
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList">
{{- range first 4 (data "learn/guided")}}
{{- range first 4 (data "guided.yaml")}}
<li class="Learn-card">
{{- template "learn-card" .}}
</li>
@ -83,7 +83,7 @@ title: "Getting Started"
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList">
{{- range first 4 (data "learn/courses") }}
{{- range first 4 (data "courses.yaml") }}
<li class="Learn-card">
{{- template "learn-card" .}}
</li>
@ -100,7 +100,7 @@ title: "Getting Started"
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList">
{{- range first 4 (data "learn/cloud")}}
{{- range first 4 (data "cloud.yaml")}}
<li class="Learn-card">
{{- template "learn-self-paced-card" .}}
</li>
@ -118,7 +118,7 @@ title: "Getting Started"
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList Learn-bookList">
{{- range first 5 (data "learn/books")}}
{{- range first 5 (data "books.yaml")}}
<li class="Learn-card Learn-book">
{{template "learn-book" .}}
</li>
@ -135,7 +135,7 @@ title: "Getting Started"
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-inPersonList">
{{- range first 4 (data "learn/training")}}
{{- range first 4 (data "training.yaml")}}
<li class="Learn-inPerson">
<p class="Learn-inPersonTitle">
<a href="{{.url}}">{{.title}} </a>
@ -157,7 +157,7 @@ title: "Getting Started"
</p>
</div>
<ul class="Learn-events">
{{- range first 3 (data "events").all}}
{{- range first 3 (data "/events.yaml").all}}
<li class="Learn-eventItem">
<div
class="Learn-eventThumbnail {{if not .photourl}}Learn-eventThumbnail--noimage{{end}}"

Просмотреть файл

@ -23,7 +23,7 @@
})(window,document,'script','dataLayer','GTM-W8MVQXG');</script>
<!-- End Google Tag Manager -->
<script src="/js/site.js"></script>
<title>{{.title}}{{if .Parent}} - go.dev{{end}}</title>
<title>{{.title}}{{if ne .URL "/"}} - go.dev{{end}}</title>
{{if .link -}}
<meta http-equiv="refresh" content="0; url={{.link}}">
{{end -}}
@ -34,7 +34,7 @@
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{{$menus := data "menus"}}
{{$menus := data "/menus.yaml"}}
<header class="Site-header js-siteHeader">
<div class="Banner">
<div class="Banner-inner">
@ -93,7 +93,7 @@
</div>
<ul class="NavigationDrawer-list">
{{- range $menus.main}}
<li class="NavigationDrawer-listItem {{if eq .url $currentPage.Section}} NavigationDrawer-listItem--active{{end}}">
<li class="NavigationDrawer-listItem {{if eq .url (section $currentPage)}} NavigationDrawer-listItem--active{{end}}">
<a href="{{.url}}">{{.name}}</a>
</li>
{{- end}}
@ -102,7 +102,7 @@
</aside>
<div class="NavigationDrawer-scrim js-scrim" role="presentation"></div>
<main class="SiteContent SiteContent--default">
{{- block "layout" . -}}{{- end -}}
{{block "layout" .}}{{.Content}}{{end}}
</main>
<footer class="Site-footer">
<div class="Footer">
@ -173,13 +173,13 @@
</body>
</html>
{{define "breadcrumbnav"}}
{{- if .p1.Parent}}
{{- template "breadcrumbnav" (dict "p1" (page .p1.Parent) "p2" .p2 )}}
{{define "breadcrumbnav p1 p2"}}
{{- if ne .p1.URL "/"}}
{{- breadcrumbnav (page (path.Dir (strings.TrimRight .p1.URL "/"))) .p2}}
{{- end}}
{{- if not (eq .p1.title "go.dev")}}
<li class="BreadcrumbNav-li {{if eq .p1.Path .p2.Path}}active{{end}}">
<a class="BreadcrumbNav-link" href="{{.p1.Path}}">
<li class="BreadcrumbNav-li {{if eq .p1.URL .p2.URL}}active{{end}}">
<a class="BreadcrumbNav-link" href="{{.p1.URL}}">
{{or .p1.company .p1.title}}
</a>
</li>
@ -189,7 +189,7 @@
{{define "breadcrumbs"}}
<div class="BreadcrumbNav">
<ol class="BreadcrumbNav-inner">
{{template "breadcrumbnav" (dict "p1" . "p2" .)}}
{{breadcrumbnav . .}}
</ol>
</div>
{{- end}}

Просмотреть файл

@ -1,2 +0,0 @@
url: https://go.dev/
title: go.dev

Просмотреть файл

@ -10,7 +10,7 @@
<div class="Article-author">{{.}}</div>
{{end}}
{{if .date}}
<div class="Article-date">{{.Date.Format "2 January 2006"}}</div>
<div class="Article-date">{{.date.Format "2 January 2006"}}</div>
{{end}}
</div>
{{if .company}}
@ -22,7 +22,7 @@
</div>
</div>
<article class="Article {{if ne .Section "/"}}Article--{{trim .Section "/"}}{{end -}}">
<article class="Article {{if ne (section .) "/"}}Article--{{strings.Trim (section .) "/"}}{{end -}}">
{{if (eq .series "Case Studies") }}
<div class="CaseStudy-content">
<div class="CaseStudy-contentBody">
@ -73,7 +73,7 @@
</span>
</div>
{{ end }}
{{rawhtml (replace .Content ` .sectionHeading">` `" class="sectionHeading">`)}}
{{.Content}}
</div>
</div>
{{end}}

Просмотреть файл

@ -1,8 +1,9 @@
---
title: Why Go
layout: none
---
{{$solutions := pages "solutions/*"}}
{{$solutions := pages "/solutions/*"}}
<section class="Solutions-headline">
<div class="GoCarousel" id="SolutionsHeroCarousel-carousel">
<div class="GoCarousel-controlsContainer">
@ -17,17 +18,17 @@ title: Why Go
<div class="Solutions-headlineImg">
<img
src="/images/{{.carouselImgSrc}}"
alt="{{.linkTitle}}"
alt="{{(or .linkTitle .title)}}"
/>
</div>
<div class="Solutions-headlineText">
<p class="Solutions-headlineNotification">RECENTLY UPDATED</p>
<h2>
{{.linkTitle}}
{{(or .linkTitle .title)}}
</h2>
<p class="Solutions-headlineBody">
{{with .quote}}{{.}}{{end}}
<a href="{{.Path}}"
<a href="{{.URL}}"
>Learn more
<i class="material-icons Solutions-forwardArrowIcon"
>arrow_forward</i
@ -106,7 +107,7 @@ title: Why Go
/>
</div>
<div class="Solutions-useCaseBody">
<h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
<h3 class="Solutions-useCaseTitle">{{or .linkTitle .title}}</h3>
<p class="Solutions-useCaseDescription">
{{.description}}
</p>
@ -117,7 +118,7 @@ title: Why Go
</p>
</a>
{{- else}}
<a href="{{.Path}}" class="Solutions-useCaseLink">
<a href="{{.URL}}" class="Solutions-useCaseLink">
<div class="Solutions-useCaseLogo">
<img
loading="lazy"
@ -126,7 +127,7 @@ title: Why Go
/>
</div>
<div class="Solutions-useCaseBody">
<h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
<h3 class="Solutions-useCaseTitle">{{or .linkTitle .title}}</h3>
<p class="Solutions-useCaseDescription">
{{with .quote}}{{.}}{{end}}
</p>
@ -149,19 +150,19 @@ title: Why Go
>
{{- range newest $solutions}}{{if eq .series "Use Cases"}}
<li class="Solutions-card">
<a href="{{.Path}}" class="Solutions-useCaseLink">
<a href="{{.URL}}" class="Solutions-useCaseLink">
<div class="Solutions-useCaseLogo">
{{- $icon := .icon}}
{{- if $icon}}
<img
loading="lazy"
alt="{{$icon.alt}}"
src="{{.Dir}}/{{$icon.file}}"
src="{{path.Dir .URL}}/{{$icon.file}}"
/>
{{- end}}
</div>
<div class="Solutions-useCaseBody">
<h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
<h3 class="Solutions-useCaseTitle">{{or .linkTitle .title}}</h3>
<p class="Solutions-useCaseDescription">
{{.description}}
</p>

Просмотреть файл

@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package site
package main
import (
"bytes"
"io/ioutil"
"net/http/httptest"
"os"
"path"
"path/filepath"
@ -19,14 +20,14 @@ import (
func TestGolden(t *testing.T) {
start := time.Now()
site, err := Load("../../..")
h, err := NewHandler("../../_content")
if err != nil {
t.Fatal(err)
}
total := time.Since(start)
t.Logf("Load %v\n", total)
root := "../../../testdata/golden"
root := "../../testdata/golden"
err = filepath.Walk(root, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
@ -55,20 +56,29 @@ func TestGolden(t *testing.T) {
return nil
}
want, err := ioutil.ReadFile(site.file("testdata/golden/" + name))
want, err := ioutil.ReadFile(filepath.Join(root, name))
if err != nil {
t.Fatal(err)
}
start := time.Now()
f, err := site.Open(name)
if err != nil {
t.Fatal(err)
r := httptest.NewRequest("GET", "/"+name, nil)
resp := httptest.NewRecorder()
resp.Body = new(bytes.Buffer)
h.ServeHTTP(resp, r)
for nredir := 0; resp.Code/10 == 30; nredir++ {
if nredir > 10 {
t.Fatalf("%s <- redirect loop!", name)
}
r.URL.Path = resp.Result().Header.Get("Location")
resp = httptest.NewRecorder()
resp.Body = new(bytes.Buffer)
h.ServeHTTP(resp, r)
}
have, err := ioutil.ReadAll(f)
if err != nil {
t.Fatalf("%v: %v", name, err)
if resp.Code != 200 {
t.Fatalf("GET %s <- %d, want 200", r.URL, resp.Code)
}
have := resp.Body.Bytes()
total += time.Since(start)
if path.Ext(name) == ".html" {

Просмотреть файл

@ -11,9 +11,13 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
"golang.org/x/website/go.dev/cmd/internal/site"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/osfs"
"golang.org/x/website/internal/web"
"golang.org/x/website/internal/webtest"
)
@ -24,10 +28,10 @@ var discoveryHosts = map[string]string{
}
func main() {
dir := "../.."
dir := "../../_content"
if _, err := os.Stat("go.dev/_content/events.yaml"); err == nil {
// Running in repo root.
dir = "go.dev"
dir = "go.dev/_content"
}
h, err := NewHandler(dir)
@ -52,17 +56,63 @@ func main() {
}
func NewHandler(dir string) (http.Handler, error) {
godev, err := site.Load(dir)
if err != nil {
return nil, err
}
godev := web.NewSite(osfs.DirFS(dir))
godev.Funcs(template.FuncMap{
"newest": newest,
"section": section,
})
mux := http.NewServeMux()
mux.Handle("/", addCSP(http.FileServer(godev)))
mux.Handle("/", addCSP(godev))
mux.Handle("/explore/", http.StripPrefix("/explore/", redirectHosts(discoveryHosts)))
mux.Handle("learn.go.dev/", http.HandlerFunc(redirectLearn))
return mux, nil
}
// newest returns the pages sorted newest first,
// breaking ties by .linkTitle or else .title.
func newest(pages []web.Page) []web.Page {
out := make([]web.Page, len(pages))
copy(out, pages)
sort.Slice(out, func(i, j int) bool {
pi := out[i]
pj := out[j]
di, _ := pi["date"].(time.Time)
dj, _ := pj["date"].(time.Time)
if !di.Equal(dj) {
return di.After(dj)
}
ti, _ := pi["linkTitle"].(string)
if ti == "" {
ti, _ = pi["title"].(string)
}
tj, _ := pj["linkTitle"].(string)
if tj == "" {
tj, _ = pj["title"].(string)
}
if ti != tj {
return ti < tj
}
return false
})
return out
}
// section returns the site section for the given Page,
// defined as the first path element, or else an empty string.
// For example if p's URL is /x/y/z then section is "x".
func section(p web.Page) string {
u, _ := p["URL"].(string)
if !strings.HasPrefix(u, "/") {
return ""
}
i := strings.Index(u[1:], "/")
if i < 0 {
return ""
}
return u[:1+i+1]
}
func redirectLearn(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://go.dev/learn/"+strings.TrimPrefix(r.URL.Path, "/"), http.StatusMovedPermanently)
}

Просмотреть файл

@ -10,8 +10,6 @@ import (
"net/http/httptest"
"strings"
"testing"
"golang.org/x/website/go.dev/cmd/internal/site"
)
var testHosts = map[string]string{
@ -79,11 +77,11 @@ var siteTests = []struct {
}{
{"/", []string{"Go is an open source programming language supported by Google"}},
{"/solutions/", []string{"Using Go at Google"}},
{"/solutions/dropbox/", []string{"About Dropbox"}},
{"/solutions/dropbox", []string{"About Dropbox"}},
}
func TestSite(t *testing.T) {
godev, err := site.Load("../..")
h, err := NewHandler("../../_content")
if err != nil {
t.Fatal(err)
}
@ -93,7 +91,7 @@ func TestSite(t *testing.T) {
r := httptest.NewRequest("GET", tt.target, nil)
resp := httptest.NewRecorder()
resp.Body = new(bytes.Buffer)
http.FileServer(godev).ServeHTTP(resp, r)
h.ServeHTTP(resp, r)
if resp.Code != 200 {
t.Fatalf("Code = %d, want 200", resp.Code)
}

Просмотреть файл

@ -11,7 +11,7 @@ import (
)
func TestWeb(t *testing.T) {
h, err := NewHandler("../..")
h, err := NewHandler("../../_content")
if err != nil {
t.Fatal(err)
}

16
go.dev/cmd/frontend/testdata/godev.txt поставляемый
Просмотреть файл

@ -3,3 +3,19 @@ body contains <h2 class="WhoUses-headerH2">Companies using Go</h2>
GET https://go.dev/solutions/google/
body ~ it\s+has\s+powered\s+many\s+projects\s+at\s+Google.
GET /solutions/chrome
redirect == /solutions/google/chrome
GET /solutions/coredata
redirect == /solutions/google/coredata
GET /solutions/firebase
redirect == /solutions/google/firebase
GET /solutions/sitereliability
redirect == /solutions/google/sitereliability
GET /solutions/americanexpress
body contains <div class="Article-date">19 December 2019</div>

Просмотреть файл

@ -1,79 +0,0 @@
// Copyright 2021 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 site
import (
"bytes"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"golang.org/x/website/internal/backport/html/template"
)
// markdownToHTML converts markdown to HTML using the renderer and settings that Hugo uses.
func markdownToHTML(markdown string) (template.HTML, error) {
// parser.WithHeadingAttribute allows custom ids on headings.
// html.WithUnsafe allows use of raw HTML, which we need for tables.
md := goldmark.New(
goldmark.WithParserOptions(
parser.WithHeadingAttribute(),
parser.WithAutoHeadingID(),
parser.WithASTTransformers(util.Prioritized(mdTransformFunc(mdLink), 1)),
),
goldmark.WithRendererOptions(html.WithUnsafe()),
goldmark.WithExtensions(
extension.NewTypographer(),
extension.NewLinkify(
extension.WithLinkifyAllowedProtocols([][]byte{[]byte("http"), []byte("https")}),
extension.WithLinkifyEmailRegexp(regexp.MustCompile(`[^\x00-\x{10FFFF}]`)), // impossible
),
),
)
var buf bytes.Buffer
if err := md.Convert([]byte(markdown), &buf); err != nil {
return "", err
}
return template.HTML(buf.Bytes()), nil
}
// mdTransformFunc is a func implementing parser.ASTTransformer.
type mdTransformFunc func(*ast.Document, text.Reader, parser.Context)
func (f mdTransformFunc) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
f(node, reader, pc)
}
// mdLink walks doc, adding rel=noreferrer target=_blank to non-relative links.
func mdLink(doc *ast.Document, _ text.Reader, _ parser.Context) {
mdLinkWalk(doc)
}
func mdLinkWalk(n ast.Node) {
switch n := n.(type) {
case *ast.Link:
dest := string(n.Destination)
if strings.HasPrefix(dest, "https://") || strings.HasPrefix(dest, "http://") {
n.SetAttributeString("rel", []byte("noreferrer"))
n.SetAttributeString("target", []byte("_blank"))
}
return
case *ast.AutoLink:
// All autolinks are non-relative.
n.SetAttributeString("rel", []byte("noreferrer"))
n.SetAttributeString("target", []byte("_blank"))
return
}
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
mdLinkWalk(child)
}
}

Просмотреть файл

@ -1,221 +0,0 @@
// Copyright 2021 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 site
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"time"
"golang.org/x/website/internal/tmplfunc"
"gopkg.in/yaml.v3"
)
// A page is a single web page.
// It corresponds to some .md file in the content tree.
type page struct {
id string // page ID (url path excluding site.BaseURL and trailing slash)
file string // .md file for page
data []byte // page data (markdown)
html []byte // rendered page (HTML)
params tPage // parameters passed to templates
}
// A tPage is the template form of the page, the data passed to rendering templates.
type tPage map[string]interface{}
// loadPage loads the site's page from the given file.
// It returns the page but also adds the page to site.pages and site.pagesByID.
func (site *Site) loadPage(file string) (*page, error) {
file = filepath.ToSlash(file)
id := strings.TrimPrefix(file, "_content/")
if id == "index.md" {
id = ""
} else if strings.HasSuffix(id, "/index.md") {
id = strings.TrimSuffix(id, "/index.md")
} else {
id = strings.TrimSuffix(id, ".md")
}
p := site.newPage(id)
p.file = file
urlPath := "/" + p.id
if strings.HasSuffix(p.file, "/index.md") && p.id != "" {
urlPath += "/"
}
// Load content, including leading yaml.
data, err := ioutil.ReadFile(site.file(file))
if err != nil {
return nil, err
}
if bytes.HasPrefix(data, []byte("---\n")) {
i := bytes.Index(data, []byte("\n---\n"))
if i < 0 {
if bytes.HasSuffix(data, []byte("\n---")) {
i = len(data) - 4
}
}
if i >= 0 {
meta := data[4 : i+1]
err := yaml.Unmarshal(meta, p.params)
if err != nil {
return nil, fmt.Errorf("load %s: %v", file, err)
}
// Drop YAML but insert the right number of newlines to keep line numbers correct in template errors.
nl := 0
for _, c := range data[:i+4] {
if c == '\n' {
nl++
}
}
i += 4
for ; nl > 0; nl-- {
i--
data[i] = '\n'
}
data = data[i:]
}
}
p.data = data
// Default linkTitle to title
if _, ok := p.params["linkTitle"]; !ok {
p.params["linkTitle"] = p.params["title"]
}
// Parse date to Date.
// Note that YAML parser may have done it for us (!)
p.params["Date"] = time.Time{}
if d, ok := p.params["date"].(string); ok {
t, err := parseDate(d)
if err != nil {
return nil, err
}
p.params["Date"] = t
} else if d, ok := p.params["date"].(time.Time); ok {
p.params["Date"] = d
}
// Path, Dir, URL
p.params["Path"] = urlPath
p.params["Dir"] = path.Dir(urlPath)
p.params["URL"] = strings.TrimRight(site.URL, "/") + urlPath
// Parent
if p.id != "" {
parent := path.Dir("/" + p.id)
if parent != "/" {
parent += "/"
}
p.params["Parent"] = parent
}
// Section
section := "/"
if i := strings.Index(p.id, "/"); i >= 0 {
section = "/" + p.id[:i+1]
} else if strings.HasSuffix(p.file, "/index.md") {
section = "/" + p.id + "/"
}
p.params["Section"] = section
return p, nil
}
// renderHTML renders the HTML for the page, leaving it in p.html.
func (site *Site) renderHTML(p *page) error {
// Load base template.
base, err := ioutil.ReadFile(site.file("_content/site.tmpl"))
if err != nil {
return err
}
t := site.clone().New("_content/site.tmpl")
if err := tmplfunc.Parse(t, string(base)); err != nil {
return err
}
// Load page-specific layout template.
layout, _ := p.params["layout"].(string)
if layout == "" {
// Determine nearest default.tmpl in current or parent directory.
// In the case of index.md, the current directory's default.tmpl
// is ignored, under the assumption that it's for the other pages
// in the directory but not the index page.
dir := path.Dir(p.file)
rel := ""
if strings.HasSuffix(p.file, "/index.md") && p.id != "" {
dir = path.Dir(dir)
rel = "../"
}
for {
name := site.file(path.Join(dir, "default.tmpl"))
if _, err := os.Stat(name); err == nil {
layout = rel + "default"
break
}
if dir == "." {
break
}
dir = path.Dir(dir)
rel += "../"
}
if layout == "" {
return fmt.Errorf("%s: cannot find default template", p.id)
}
}
layout = path.Join(path.Dir(p.file), layout+".tmpl")
data, err := ioutil.ReadFile(site.file(layout))
if err != nil {
return err
}
if err := tmplfunc.Parse(t.New(layout), string(data)); err != nil {
return err
}
// Load actual Markdown content (also a template).
tf := t.New(p.file)
if err := tmplfunc.Parse(tf, string(p.data)); err != nil {
return err
}
var buf bytes.Buffer
if err := tf.Execute(&buf, p.params); err != nil {
return err
}
html, err := markdownToHTML(buf.String())
if err != nil {
return err
}
p.params["Content"] = html
buf.Reset()
if err := t.Execute(&buf, p.params); err != nil {
return err
}
p.html = buf.Bytes()
return nil
}
var dateFormats = []string{
"2006-01-02",
time.RFC3339,
}
func parseDate(d string) (time.Time, error) {
for _, f := range dateFormats {
if tt, err := time.Parse(f, d); err == nil {
return tt, nil
}
}
return time.Time{}, fmt.Errorf("invalid date: %s", d)
}

Просмотреть файл

@ -1,251 +0,0 @@
// Copyright 2021 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 site implements generation of content for serving from go.dev.
// It is meant to support a transition from being a Hugo-based web site
// to being a site compatible with x/website.
package site
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"golang.org/x/website/internal/backport/html/template"
"gopkg.in/yaml.v3"
)
// A Site holds metadata about the entire site.
type Site struct {
URL string
Title string
pagesByID map[string]*page
dir string
base *template.Template
}
// Load loads and returns the site in the directory rooted at dir.
func Load(dir string) (*Site, error) {
dir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
site := &Site{
dir: dir,
pagesByID: make(map[string]*page),
}
if err := site.initTemplate(); err != nil {
return nil, err
}
// Read site config.
data, err := ioutil.ReadFile(site.file("_content/site.yaml"))
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, &site); err != nil {
return nil, fmt.Errorf("parsing _content/site.yaml: %v", err)
}
// Load site pages from md files.
err = filepath.Walk(site.file("_content"), func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(name, ".md") {
_, err := site.loadPage(name[len(site.file("."))+1:])
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("loading pages: %v", err)
}
// Now that all pages are loaded and set up, can render all.
// (Pages can refer to other pages.)
for _, p := range site.pagesByID {
if err := site.renderHTML(p); err != nil {
return nil, err
}
}
return site, nil
}
// file returns the full path to the named file within the site.
func (site *Site) file(name string) string { return filepath.Join(site.dir, name) }
// newPage returns a new page belonging to site.
func (site *Site) newPage(short string) *page {
p := &page{
id: short,
params: make(tPage),
}
site.pagesByID[p.id] = p
return p
}
// data parses the named yaml file and returns its structured data.
func (site *Site) data(name string) (interface{}, error) {
data, err := ioutil.ReadFile(site.file("_content/" + name + ".yaml"))
if err != nil {
return nil, err
}
var d interface{}
if err := yaml.Unmarshal(data, &d); err != nil {
return nil, err
}
return d, nil
}
// pageByID returns the page with a given path.
func (site *Site) pageByPath(path string) (tPage, error) {
p := site.pagesByID[strings.Trim(path, "/")]
if p == nil {
return nil, fmt.Errorf("no such page with path %q", path)
}
return p.params, nil
}
// pagesGlob returns the pages with IDs matching glob.
func (site *Site) pagesGlob(glob string) ([]tPage, error) {
_, err := path.Match(glob, "")
if err != nil {
return nil, err
}
glob = strings.Trim(glob, "/")
var out []tPage
for _, p := range site.pagesByID {
if ok, _ := path.Match(glob, p.id); ok {
out = append(out, p.params)
}
}
sort.Slice(out, func(i, j int) bool {
return out[i]["Path"].(string) < out[j]["Path"].(string)
})
return out, nil
}
// newest returns the pages sorted newest first,
// breaking ties by .linkTitle or else .title.
func newest(pages []tPage) []tPage {
out := make([]tPage, len(pages))
copy(out, pages)
sort.Slice(out, func(i, j int) bool {
pi := out[i]
pj := out[j]
di, _ := pi["Date"].(time.Time)
dj, _ := pj["Date"].(time.Time)
if !di.Equal(dj) {
return di.After(dj)
}
ti, _ := pi["linkTitle"].(string)
tj, _ := pj["linkTitle"].(string)
if ti != tj {
return ti < tj
}
return false
})
return out
}
// Open returns the content to serve at the given path.
// This function makes Site an http.FileServer, for easy HTTP serving.
func (site *Site) Open(name string) (http.File, error) {
name = strings.TrimPrefix(name, "/")
switch ext := path.Ext(name); ext {
case ".css", ".jpeg", ".jpg", ".js", ".png", ".svg", ".txt":
if f, err := os.Open(site.file("_content/" + name)); err == nil {
return f, nil
}
case ".html":
id := strings.TrimSuffix(name, "/index.html")
if name == "index.html" {
id = ""
}
if p := site.pagesByID[id]; p != nil {
if redir, ok := p.params["redirect"].(string); ok {
s := fmt.Sprintf(redirectFmt, redir)
return &httpFile{strings.NewReader(s), int64(len(s))}, nil
}
return &httpFile{bytes.NewReader(p.html), int64(len(p.html))}, nil
}
}
if !strings.HasSuffix(name, ".html") {
if f, err := site.Open(name + "/index.html"); err == nil {
size, err := f.Seek(0, io.SeekEnd)
f.Close()
if err == nil {
return &httpDir{httpFileInfo{"index.html", size, false}, 0}, nil
}
}
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
type httpFile struct {
io.ReadSeeker
size int64
}
func (*httpFile) Close() error { return nil }
func (f *httpFile) Stat() (os.FileInfo, error) { return &httpFileInfo{".", f.size, false}, nil }
func (*httpFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, fmt.Errorf("readdir not available")
}
const redirectFmt = `<!DOCTYPE html><html><head><title>%s</title><link rel="canonical" href="%[1]s"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=%[1]s" /></head></html>`
type httpDir struct {
info httpFileInfo
off int // 0 or 1
}
func (*httpDir) Close() error { return nil }
func (*httpDir) Read([]byte) (int, error) { return 0, fmt.Errorf("read not available") }
func (*httpDir) Seek(int64, int) (int64, error) { return 0, fmt.Errorf("seek not available") }
func (*httpDir) Stat() (os.FileInfo, error) { return &httpFileInfo{".", 0, true}, nil }
func (d *httpDir) Readdir(count int) ([]os.FileInfo, error) {
if count == 0 {
return nil, nil
}
if d.off > 0 {
return nil, io.EOF
}
d.off = 1
return []os.FileInfo{&d.info}, nil
}
type httpFileInfo struct {
name string
size int64
dir bool
}
func (info *httpFileInfo) Name() string { return info.name }
func (info *httpFileInfo) Size() int64 { return info.size }
func (info *httpFileInfo) Mode() os.FileMode {
if info.dir {
return os.ModeDir | 0555
}
return 0444
}
func (info *httpFileInfo) ModTime() time.Time { return time.Time{} }
func (info *httpFileInfo) IsDir() bool { return info.dir }
func (info *httpFileInfo) Sys() interface{} { return nil }

Просмотреть файл

@ -1,118 +0,0 @@
// Copyright 2021 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 site
import (
"fmt"
"reflect"
"strings"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/tmplfunc"
"gopkg.in/yaml.v3"
)
func (site *Site) initTemplate() error {
funcs := template.FuncMap{
"add": func(i, j int) int { return i + j },
"data": site.data,
"dict": dict,
"first": first,
"markdown": markdown,
"newest": newest,
"page": site.pageByPath,
"pages": site.pagesGlob,
"replace": replace,
"rawhtml": rawhtml,
"trim": strings.Trim,
"yaml": yamlFn,
}
site.base = template.New("site").Funcs(funcs)
if err := tmplfunc.ParseGlob(site.base, site.file("_templates/*.tmpl")); err != nil && !strings.Contains(err.Error(), "pattern matches no files") {
return err
}
return nil
}
func (site *Site) clone() *template.Template {
t := template.Must(site.base.Clone())
if err := tmplfunc.Funcs(t); err != nil {
panic(err)
}
return t
}
func toString(x interface{}) string {
switch x := x.(type) {
case string:
return x
case template.HTML:
return string(x)
case nil:
return ""
default:
panic(fmt.Sprintf("cannot toString %T", x))
}
}
func first(n int, list reflect.Value) reflect.Value {
if !list.IsValid() {
return list
}
if list.Kind() == reflect.Interface {
if list.IsNil() {
return list
}
list = list.Elem()
}
if list.Len() < n {
return list
}
return list.Slice(0, n)
}
func dict(args ...interface{}) map[string]interface{} {
m := make(map[string]interface{})
for i := 0; i < len(args); i += 2 {
m[args[i].(string)] = args[i+1]
}
m["Identifier"] = "IDENT"
return m
}
func list(args ...interface{}) []interface{} {
return args
}
// markdown is the function provided to templates.
func markdown(data interface{}) (template.HTML, error) {
h, err := markdownToHTML(toString(data))
if err != nil {
return "", err
}
s := strings.TrimSpace(string(h))
if strings.HasPrefix(s, "<p>") && strings.HasSuffix(s, "</p>") && strings.Count(s, "<p>") == 1 {
h = template.HTML(strings.TrimSpace(s[len("<p>") : len(s)-len("</p>")]))
}
return h, nil
}
func replace(input, x, y interface{}) string {
return strings.ReplaceAll(toString(input), toString(x), toString(y))
}
func rawhtml(s interface{}) template.HTML {
return template.HTML(toString(s))
}
func yamlFn(s string) (interface{}, error) {
var d interface{}
if err := yaml.Unmarshal([]byte(s), &d); err != nil {
return nil, err
}
return d, nil
}

Просмотреть файл

@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>/solutions/google/chrome</title><link rel="canonical" href="/solutions/google/chrome"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/chrome" /></head></html>

Просмотреть файл

@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>/solutions/google/coredata</title><link rel="canonical" href="/solutions/google/coredata"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/coredata" /></head></html>

Просмотреть файл

@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>/solutions/google/firebase</title><link rel="canonical" href="/solutions/google/firebase"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/firebase" /></head></html>

Просмотреть файл

@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>/solutions/google/sitereliability</title><link rel="canonical" href="/solutions/google/sitereliability"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/sitereliability" /></head></html>

Просмотреть файл

@ -81,10 +81,10 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
s.site.ServePage(w, r, web.Page{
Title: "Codewalk: " + cw.Title,
TabTitle: cw.Title,
Template: "codewalk.html",
Data: cw,
"title": "Codewalk: " + cw.Title,
"tabTitle": cw.Title,
"layout": "codewalk",
"codewalk": cw,
})
}
@ -234,9 +234,9 @@ func (s *server) codewalkDir(w http.ResponseWriter, r *http.Request, relpath str
}
s.site.ServePage(w, r, web.Page{
Title: "Codewalks",
Template: "codewalkdir.html",
Data: v,
"title": "Codewalks",
"layout": "codewalkdir",
"dirs": v,
})
}

Просмотреть файл

@ -83,9 +83,9 @@ func (h server) listHandler(w http.ResponseWriter, r *http.Request) {
}
h.site.ServePage(w, r, web.Page{
Title: "Downloads",
Template: "dl.html",
Data: d,
"title": "Downloads",
"layout": "dl",
"dl": d,
})
}

Просмотреть файл

@ -34,16 +34,20 @@ import (
)
type docs struct {
fs fs.FS
api api.DB
site *web.Site
root *Dir
fs fs.FS
api api.DB
site *web.Site
root *Dir
forceOld func(*http.Request) bool
}
// 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) {
// If forceOld is not nil and returns true for a given request,
// NewServer will serve docs itself instead of redirecting to pkg.go.dev
// (forcing the ?m=old behavior).
func NewServer(fsys fs.FS, site *web.Site, forceOld func(*http.Request) bool) (http.Handler, error) {
apiDB, err := api.Load(fsys)
if err != nil {
return nil, err
@ -57,10 +61,11 @@ func NewServer(fsys fs.FS, site *web.Site) (http.Handler, error) {
Dirs: dirs,
}
docs := &docs{
fs: fsys,
api: apiDB,
site: site,
root: root,
fs: fsys,
api: apiDB,
site: site,
root: root,
forceOld: forceOld,
}
return docs, nil
}
@ -68,8 +73,6 @@ func NewServer(fsys fs.FS, site *web.Site) (http.Handler, error) {
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
@ -90,10 +93,6 @@ type Page struct {
DirFlat bool // if set, show directory in a flat (non-indented) manner
}
func (p *Page) SetWebPage(w *web.Page) {
p.Web = w
}
type mode uint
const (
@ -433,7 +432,7 @@ func (d *docs) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 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 mode&modeOld == 0 && (d.forceOld == nil || !d.forceOld(r)) {
if relpath == "" {
relpath = "std"
}
@ -500,16 +499,16 @@ func (d *docs) ServeHTTP(w http.ResponseWriter, r *http.Request) {
tabtitle = "Commands"
}
name := "package.html"
layout := "pkg"
if info.Dirname == "src" {
name = "packageroot.html"
layout = "pkgroot"
}
d.site.ServePage(w, r, web.Page{
Title: title,
TabTitle: tabtitle,
Subtitle: subtitle,
Template: name,
Data: info,
"title": title,
"tabTitle": tabtitle,
"subtitle": subtitle,
"layout": layout,
"pkg": info,
})
}

Просмотреть файл

@ -24,11 +24,8 @@ func TestIgnoredGoFiles(t *testing.T) {
// ` + packageComment + `
package main`)},
}
site, err := web.NewSite(fs)
if err != nil {
t.Fatal(err)
}
h, err := NewServer(fs, site)
site := web.NewSite(fs)
h, err := NewServer(fs, site, nil)
if err != nil {
t.Fatal(err)
}
@ -66,11 +63,8 @@ func F()
//line foo.go:100`)}, // No newline at end to check corner cases.
}
site, err := web.NewSite(fs)
if err != nil {
t.Fatal(err)
}
h, err := NewServer(fs, site)
site := web.NewSite(fs)
h, err := NewServer(fs, site, nil)
if err != nil {
t.Fatal(err)
}

Просмотреть файл

@ -13,7 +13,6 @@ import (
"testing"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/web"
)
func TestSrcPosLink(t *testing.T) {
@ -164,7 +163,6 @@ func (h Header) Get(key string) string`))
}
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 {
@ -174,11 +172,6 @@ func linkifySource(t *testing.T, src []byte) string {
pi := &Page{
fset: fset,
}
pg := &web.Page{
Data: pi,
Site: site,
}
pi.SetWebPage(pg)
sep := ""
for _, decl := range af.Decls {
buf.WriteString(sep)

Просмотреть файл

@ -16,8 +16,6 @@ import (
"log"
"net/http"
"time"
"golang.org/x/website/internal/web"
)
const playgroundURL = "https://play.golang.org"
@ -40,9 +38,13 @@ type Event struct {
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
func RegisterHandlers(mux *http.ServeMux) {
// RegisterHandlers registers handlers
// for golang.org/compile and golang.org/share on mux.
// If disallow is non-nil, then the share handler disallows requests
// for which disallowShare returns true.
func RegisterHandlers(mux *http.ServeMux, disallowShare func(*http.Request) bool) {
mux.HandleFunc("golang.org/compile", compile)
mux.HandleFunc("golang.org/share", share)
mux.HandleFunc("golang.org/share", share(disallowShare))
}
func compile(w http.ResponseWriter, r *http.Request) {
@ -122,31 +124,33 @@ func flatten(seq []Event) string {
return buf.String()
}
func share(w http.ResponseWriter, r *http.Request) {
if web.GoogleCN(r) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
// TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
req = req.WithContext(r.Context())
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("ERROR share error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
copyHeader := func(k string) {
if v := resp.Header.Get(k); v != "" {
w.Header().Set(k, v)
func share(disallow func(*http.Request) bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if disallow != nil && disallow(r) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
// TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
req = req.WithContext(r.Context())
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("ERROR share error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
copyHeader := func(k string) {
if v := resp.Header.Get(k); v != "" {
w.Header().Set(k, v)
}
}
copyHeader("Content-Type")
copyHeader("Content-Length")
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
copyHeader("Content-Type")
copyHeader("Content-Length")
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}

Просмотреть файл

@ -8,32 +8,21 @@ import (
"bytes"
"fmt"
"log"
"path"
"regexp"
"strings"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/io/fs"
"golang.org/x/website/internal/history"
"golang.org/x/website/internal/texthtml"
)
func (s *Site) initDocFuncs() {
s.docFuncs = template.FuncMap{
"code": s.code,
"releases": func() []*history.Major { return history.Majors },
}
}
func (s *Site) code(file string, arg ...interface{}) (_ template.HTML, err error) {
func (s *siteDir) code(file string, arg ...interface{}) (_ template.HTML, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
file = path.Clean(strings.TrimPrefix(file, "/"))
btext, err := fs.ReadFile(s.fs, file)
btext, err := s.readFile(s.dir, file)
if err != nil {
return "", err
}

Просмотреть файл

@ -1,133 +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.
package web
import (
"bytes"
"encoding/json"
"log"
"path"
"strings"
"golang.org/x/website/internal/backport/io/fs"
)
type file struct {
// Copied from document metadata directives
Title string
Subtitle string
Template bool
Path string // URL path
FilePath string // filesystem path relative to goroot
Body []byte // content after metadata
}
type fileJSON struct {
Title string
Subtitle string
Template bool
Redirect string // if set, redirect to other URL
}
// open returns the *file for a given relative path or nil if none exists.
func open(fsys fs.FS, relpath string) *file {
// Strip trailing .html or .md or /; it all names the same page.
if strings.HasSuffix(relpath, ".html") {
relpath = strings.TrimSuffix(relpath, ".html")
} else if strings.HasSuffix(relpath, ".md") {
relpath = strings.TrimSuffix(relpath, ".md")
} else if strings.HasSuffix(relpath, "/") {
relpath = strings.TrimSuffix(relpath, "/")
}
// Check md before html to work correctly when x/website is layered atop Go 1.15 goroot during Go 1.15 tests.
// Want to find x/website's debugging_with_gdb.md not Go 1.15's debuging_with_gdb.html.
files := []string{relpath + ".md", relpath + ".html", path.Join(relpath, "index.md"), path.Join(relpath, "index.html")}
var filePath string
var b []byte
var err error
for _, filePath = range files {
b, err = fs.ReadFile(fsys, filePath)
if err == nil {
break
}
}
// Special case for memory model and spec, which live
// in the main Go repo's doc directory and therefore have not
// been renamed to their serving relpaths.
// We wait until the ReadFiles above have failed so that the
// code works if these are ever moved to /ref/spec and /ref/mem.
if err != nil && relpath == "ref/spec" {
return open(fsys, "doc/go_spec")
}
if err != nil && relpath == "ref/mem" {
return open(fsys, "doc/go_mem")
}
if err != nil {
return nil
}
// Special case for memory model and spec, continued.
switch relpath {
case "doc/go_spec":
relpath = "ref/spec"
case "doc/go_mem":
relpath = "ref/mem"
}
// If we read an index.md or index.html, the canonical relpath is without the index.md/index.html suffix.
if name := path.Base(filePath); name == "index.html" || name == "index.md" {
relpath, _ = path.Split(filePath)
}
js, body, err := parseFile(b)
if err != nil {
log.Printf("extractMetadata %s: %v", relpath, err)
return nil
}
f := &file{
Title: js.Title,
Subtitle: js.Subtitle,
Template: js.Template,
Path: "/" + relpath,
FilePath: filePath,
Body: body,
}
if js.Redirect != "" {
// Allow (placeholder) documents to declare a redirect.
f.Path = js.Redirect
}
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
}

Просмотреть файл

@ -1,72 +0,0 @@
// Copyright 2020 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"
"unicode/utf8"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
// renderMarkdown converts a limited and opinionated flavor of Markdown (compliant with
// CommonMark 0.29) to HTML for the purposes of Go websites.
//
// The Markdown source may contain raw HTML,
// but Go templates have already been processed.
func renderMarkdown(src []byte) ([]byte, error) {
src = replaceTabs(src)
// parser.WithHeadingAttribute allows custom ids on headings.
// html.WithUnsafe allows use of raw HTML, which we need for tables.
md := goldmark.New(
goldmark.WithParserOptions(parser.WithHeadingAttribute()),
goldmark.WithRendererOptions(html.WithUnsafe()))
var buf bytes.Buffer
if err := md.Convert(src, &buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// replaceTabs replaces all tabs in text with spaces up to a 4-space tab stop.
//
// In Markdown, tabs used for indentation are required to be interpreted as
// 4-space tab stops. See https://spec.commonmark.org/0.30/#tabs.
// Go also renders nicely and more compactly on the screen with 4-space
// tab stops, while browsers often use 8-space.
// And Goldmark crashes in some inputs that mix spaces and tabs.
// Fix the crashes and make the Go code consistently compact across browsers,
// all while staying Markdown-compatible, by expanding to 4-space tab stops.
//
// This function does not handle multi-codepoint Unicode sequences correctly.
func replaceTabs(text []byte) []byte {
var buf bytes.Buffer
col := 0
for len(text) > 0 {
r, size := utf8.DecodeRune(text)
text = text[size:]
switch r {
case '\n':
buf.WriteByte('\n')
col = 0
case '\t':
buf.WriteByte(' ')
col++
for col%4 != 0 {
buf.WriteByte(' ')
col++
}
default:
buf.WriteRune(r)
col++
}
}
return buf.Bytes()
}

179
internal/web/page.go Normal file
Просмотреть файл

@ -0,0 +1,179 @@
// Copyright 2021 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"
"encoding/json"
"path"
"strings"
"sync/atomic"
"time"
"golang.org/x/website/internal/backport/io/fs"
"gopkg.in/yaml.v3"
)
// A pageFile is a Page loaded from a file.
// It corresponds to some .md or .html file in the content tree.
type pageFile struct {
file string // .md file for page
stat fs.FileInfo // stat for file when page was loaded
url string // url excluding site.BaseURL; always begins with slash
data []byte // page data (markdown)
page Page // parameters passed to templates
checked int64 // unix nano, atomically updated
}
// A Page is the data for a web page.
// See the package doc comment for details.
type Page map[string]interface{}
func (site *Site) openPage(file string) (*pageFile, error) {
// Strip trailing .html or .md or /; it all names the same page.
if strings.HasSuffix(file, "/index.md") {
file = strings.TrimSuffix(file, "/index.md")
} else if strings.HasSuffix(file, "/index.html") {
file = strings.TrimSuffix(file, "/index.html")
} else if file == "index.md" || file == "index.html" {
file = "."
} else if strings.HasSuffix(file, "/") {
file = strings.TrimSuffix(file, "/")
} else if strings.HasSuffix(file, ".html") {
file = strings.TrimSuffix(file, ".html")
} else {
file = strings.TrimSuffix(file, ".md")
}
now := time.Now().UnixNano()
if cp, ok := site.cache.Load(file); ok {
// Have cache entry; only use if the underlying file hasn't changed.
// To avoid continuous stats, only check it has been 3s since the last one.
// TODO(rsc): Move caching into a more general layer and cache templates.
p := cp.(*pageFile)
if now-atomic.LoadInt64(&p.checked) >= 3e9 {
info, err := fs.Stat(site.fs, p.file)
if err == nil && info.ModTime().Equal(p.stat.ModTime()) && info.Size() == p.stat.Size() {
atomic.StoreInt64(&p.checked, now)
return p, nil
}
}
}
// Check md before html to work correctly when x/website is layered atop Go 1.15 goroot during Go 1.15 tests.
// Want to find x/website's debugging_with_gdb.md not Go 1.15's debuging_with_gdb.html.
files := []string{file + ".md", file + ".html", path.Join(file, "index.md"), path.Join(file, "index.html")}
var filePath string
var b []byte
var err error
var stat fs.FileInfo
for _, filePath = range files {
stat, err = fs.Stat(site.fs, filePath)
if err == nil {
b, err = site.readFile(".", filePath)
if err == nil {
break
}
}
}
if err != nil {
return nil, err
}
// If we read an index.md or index.html, the canonical relpath is without the index.md/index.html suffix.
url := path.Join("/", file)
if name := path.Base(filePath); name == "index.html" || name == "index.md" {
url, _ = path.Split(path.Join("/", filePath))
}
params, body, err := parseMeta(b)
if err != nil {
return nil, err
}
p := &pageFile{
file: filePath,
stat: stat,
url: url,
data: body,
page: params,
checked: now,
}
// File, FileData, URL
p.page["File"] = filePath
p.page["FileData"] = string(body)
p.page["URL"] = p.url
// User-specified redirect: overrides url but not URL.
if redir, _ := p.page["redirect"].(string); redir != "" {
p.url = redir
}
site.cache.Store(file, p)
return p, nil
}
var (
jsonStart = []byte("<!--{")
jsonEnd = []byte("}-->")
yamlStart = []byte("---\n")
yamlEnd = []byte("\n---\n")
)
// parseMeta extracts top-of-file metadata from the file contents b.
// If there is no metadata, parseMeta returns Page{}, b, nil.
// Otherwise, the metdata is extracted, and parseMeta returns
// the metadata and the remainder of the file.
// The end of the metadata is overwritten in b to preserve
// the correct number of newlines so that the line numbers in tail
// match the line numbers in b.
//
// A JSON metadata object is bracketed by <!--{...}-->.
// A YAML metadata object is bracketed by "---\n" above and below the YAML.
//
// JSON is typically used in HTML; YAML is typically used in Markdown.
func parseMeta(b []byte) (meta Page, tail []byte, err error) {
tail = b
meta = make(Page)
var end int
if bytes.HasPrefix(b, jsonStart) {
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
}
end += len(jsonEnd)
for k, v := range meta {
delete(meta, k)
meta[strings.ToLower(k)] = v
}
} else if bytes.HasPrefix(b, yamlStart) {
end = bytes.Index(b, yamlEnd)
if end < 0 {
return
}
b = b[len(yamlStart) : end+1] // drop ---\n but include final \n
if err = yaml.Unmarshal(b, &meta); err != nil {
return
}
end += len(yamlEnd)
}
// Put the right number of \n at the start of tail to preserve line numbers.
nl := bytes.Count(tail[:end], []byte("\n"))
for i := 0; i < nl; i++ {
end--
tail[end] = '\n'
}
tail = tail[end:]
return
}

82
internal/web/pkg.go Normal file
Просмотреть файл

@ -0,0 +1,82 @@
// Copyright 2021 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 (
"path"
"strings"
"unicode"
)
type pkgPath struct{}
func (pkgPath) Base(a string) string { return path.Base(a) }
func (pkgPath) Clean(a string) string { return path.Clean(a) }
func (pkgPath) Dir(a string) string { return path.Dir(a) }
func (pkgPath) Ext(a string) string { return path.Ext(a) }
func (pkgPath) IsAbs(a string) bool { return path.IsAbs(a) }
func (pkgPath) Join(a ...string) string { return path.Join(a...) }
func (pkgPath) Match(a, b string) (bool, error) { return path.Match(a, b) }
func (pkgPath) Split(a string) (string, string) { return path.Split(a) }
type pkgStrings struct{}
func (pkgStrings) Compare(a, b string) int { return strings.Compare(a, b) }
func (pkgStrings) Contains(a, b string) bool { return strings.Contains(a, b) }
func (pkgStrings) ContainsAny(a, b string) bool { return strings.ContainsAny(a, b) }
func (pkgStrings) ContainsRune(a string, b rune) bool { return strings.ContainsRune(a, b) }
func (pkgStrings) Count(a, b string) int { return strings.Count(a, b) }
func (pkgStrings) EqualFold(a, b string) bool { return strings.EqualFold(a, b) }
func (pkgStrings) Fields(a string) []string { return strings.Fields(a) }
func (pkgStrings) FieldsFunc(a string, b func(rune) bool) []string { return strings.FieldsFunc(a, b) }
func (pkgStrings) HasPrefix(a, b string) bool { return strings.HasPrefix(a, b) }
func (pkgStrings) HasSuffix(a, b string) bool { return strings.HasSuffix(a, b) }
func (pkgStrings) Index(a, b string) int { return strings.Index(a, b) }
func (pkgStrings) IndexAny(a, b string) int { return strings.IndexAny(a, b) }
func (pkgStrings) IndexByte(a string, b byte) int { return strings.IndexByte(a, b) }
func (pkgStrings) IndexFunc(a string, b func(rune) bool) int { return strings.IndexFunc(a, b) }
func (pkgStrings) IndexRune(a string, b rune) int { return strings.IndexRune(a, b) }
func (pkgStrings) Join(a []string, b string) string { return strings.Join(a, b) }
func (pkgStrings) LastIndex(a, b string) int { return strings.LastIndex(a, b) }
func (pkgStrings) LastIndexAny(a, b string) int { return strings.LastIndexAny(a, b) }
func (pkgStrings) LastIndexByte(a string, b byte) int { return strings.LastIndexByte(a, b) }
func (pkgStrings) LastIndexFunc(a string, b func(rune) bool) int {
return strings.LastIndexFunc(a, b)
}
func (pkgStrings) Map(a func(rune) rune, b string) string { return strings.Map(a, b) }
func (pkgStrings) NewReader(a string) *strings.Reader { return strings.NewReader(a) }
func (pkgStrings) NewReplacer(a ...string) *strings.Replacer { return strings.NewReplacer(a...) }
func (pkgStrings) Repeat(a string, b int) string { return strings.Repeat(a, b) }
func (pkgStrings) Replace(a, b, c string, d int) string { return strings.Replace(a, b, c, d) }
func (pkgStrings) ReplaceAll(a, b, c string) string { return strings.ReplaceAll(a, b, c) }
func (pkgStrings) Split(a, b string) []string { return strings.Split(a, b) }
func (pkgStrings) SplitAfter(a, b string) []string { return strings.SplitAfter(a, b) }
func (pkgStrings) SplitAfterN(a, b string, c int) []string { return strings.SplitAfterN(a, b, c) }
func (pkgStrings) SplitN(a, b string, c int) []string { return strings.SplitN(a, b, c) }
func (pkgStrings) Title(a string) string { return strings.Title(a) }
func (pkgStrings) ToLower(a string) string { return strings.ToLower(a) }
func (pkgStrings) ToLowerSpecial(a unicode.SpecialCase, b string) string {
return strings.ToLowerSpecial(a, b)
}
func (pkgStrings) ToTitle(a string) string { return strings.ToTitle(a) }
func (pkgStrings) ToTitleSpecial(a unicode.SpecialCase, b string) string {
return strings.ToTitleSpecial(a, b)
}
func (pkgStrings) ToUpper(a string) string { return strings.ToUpper(a) }
func (pkgStrings) ToUpperSpecial(a unicode.SpecialCase, b string) string {
return strings.ToUpperSpecial(a, b)
}
func (pkgStrings) ToValidUTF8(a, b string) string { return strings.ToValidUTF8(a, b) }
func (pkgStrings) Trim(a, b string) string { return strings.Trim(a, b) }
func (pkgStrings) TrimFunc(a string, b func(rune) bool) string { return strings.TrimFunc(a, b) }
func (pkgStrings) TrimLeft(a, b string) string { return strings.TrimLeft(a, b) }
func (pkgStrings) TrimLeftFunc(a string, b func(rune) bool) string { return strings.TrimLeftFunc(a, b) }
func (pkgStrings) TrimPrefix(a, b string) string { return strings.TrimPrefix(a, b) }
func (pkgStrings) TrimRight(a, b string) string { return strings.TrimRight(a, b) }
func (pkgStrings) TrimRightFunc(a string, b func(rune) bool) string {
return strings.TrimRightFunc(a, b)
}
func (pkgStrings) TrimSpace(a string) string { return strings.TrimSpace(a) }
func (pkgStrings) TrimSuffix(a, b string) string { return strings.TrimSuffix(a, b) }

254
internal/web/render.go Normal file
Просмотреть файл

@ -0,0 +1,254 @@
// Copyright 2020 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"
"fmt"
"net/http"
"path"
"regexp"
"strings"
"unicode/utf8"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/io/fs"
"golang.org/x/website/internal/tmplfunc"
)
// renderHTML renders and returns the HTML for the page.
func (site *Site) renderHTML(p Page, r *http.Request) ([]byte, error) {
// Clone p, because we are going to set its Content key-value pair.
p2 := make(Page)
for k, v := range p {
p2[k] = v
}
p = p2
url, ok := p["URL"].(string)
if !ok {
// Set URL - caller did not.
p["URL"] = r.URL.Path
}
file, _ := p["File"].(string)
data, _ := p["FileData"].(string)
// Load base template.
base, err := site.readFile(".", "site.tmpl")
if err != nil {
return nil, err
}
dir := strings.Trim(path.Dir(url), "/")
if dir == "" {
dir = "."
}
sd := &siteDir{site, dir}
t := template.New("site.tmpl").Funcs(template.FuncMap{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
"div": func(a, b int) int { return a / b },
"code": sd.code,
"data": sd.data,
"page": sd.page,
"pages": sd.pages,
"request": func() *http.Request { return r },
"path": func() pkgPath { return pkgPath{} },
"strings": func() pkgStrings { return pkgStrings{} },
"first": first,
"markdown": markdown,
"rawhtml": rawhtml,
"readfile": sd.readfile,
"yaml": yamlFn,
})
t.Funcs(site.funcs)
if err := tmplfunc.Parse(t, string(base)); err != nil {
return nil, err
}
// Load page-specific layout template.
layout, _ := p["layout"].(string)
if layout == "" {
l, ok := site.findLayout(dir, "default")
if ok {
layout = l
} else {
layout = "none"
}
} else if path.IsAbs(layout) {
layout = strings.TrimLeft(path.Clean(layout+".tmpl"), "/")
} else if strings.Contains(layout, "/") {
layout = path.Join(dir, layout+".tmpl")
} else if layout != "none" {
l, ok := site.findLayout(dir, layout)
if !ok {
return nil, fmt.Errorf("cannot find layout %q", layout)
}
layout = l
}
if layout != "none" {
ldata, err := site.readFile(".", layout)
if err != nil {
return nil, err
}
if err := tmplfunc.Parse(t.New(layout), string(ldata)); err != nil {
return nil, err
}
}
var buf bytes.Buffer
if _, ok := p["Content"]; !ok && data != "" {
// Load actual Markdown content (also a template).
tf := t.New(file)
if err := tmplfunc.Parse(tf, data); err != nil {
return nil, err
}
if err := tf.Execute(&buf, p); err != nil {
return nil, err
}
if strings.HasSuffix(file, ".md") {
html, err := markdownToHTML(buf.String())
if err != nil {
return nil, err
}
p["Content"] = html
} else {
p["Content"] = template.HTML(buf.String())
}
buf.Reset()
}
if err := t.Execute(&buf, p); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// findLayout searches the start directory and parent directories for a template with the given base name.
func (site *Site) findLayout(dir, name string) (string, bool) {
name += ".tmpl"
for {
abs := path.Join(dir, name)
if _, err := fs.Stat(site.fs, abs); err == nil {
return abs, true
}
if dir == "." {
return "", false
}
dir = path.Dir(dir)
}
}
// markdownToHTML converts Markdown to HTML.
// The Markdown source may contain raw HTML,
// but Go templates have already been processed.
func markdownToHTML(markdown string) (template.HTML, error) {
// parser.WithHeadingAttribute allows custom ids on headings.
// html.WithUnsafe allows use of raw HTML, which we need for tables.
md := goldmark.New(
goldmark.WithParserOptions(
parser.WithHeadingAttribute(),
parser.WithAutoHeadingID(),
parser.WithASTTransformers(util.Prioritized(mdTransformFunc(mdLink), 1)),
),
goldmark.WithRendererOptions(html.WithUnsafe()),
goldmark.WithExtensions(
extension.NewTypographer(),
extension.NewLinkify(
extension.WithLinkifyAllowedProtocols([][]byte{[]byte("http"), []byte("https")}),
extension.WithLinkifyEmailRegexp(regexp.MustCompile(`[^\x00-\x{10FFFF}]`)), // impossible
),
extension.DefinitionList,
),
)
var buf bytes.Buffer
if err := md.Convert(replaceTabs([]byte(markdown)), &buf); err != nil {
return "", err
}
return template.HTML(buf.Bytes()), nil
}
// mdTransformFunc is a func implementing parser.ASTTransformer.
type mdTransformFunc func(*ast.Document, text.Reader, parser.Context)
func (f mdTransformFunc) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
f(node, reader, pc)
}
// mdLink walks doc, adding rel=noreferrer target=_blank to non-relative links.
func mdLink(doc *ast.Document, _ text.Reader, _ parser.Context) {
mdLinkWalk(doc)
}
func mdLinkWalk(n ast.Node) {
switch n := n.(type) {
case *ast.Link:
dest := string(n.Destination)
if strings.HasPrefix(dest, "https://") || strings.HasPrefix(dest, "http://") {
n.SetAttributeString("rel", []byte("noreferrer"))
n.SetAttributeString("target", []byte("_blank"))
}
return
case *ast.AutoLink:
// All autolinks are non-relative.
n.SetAttributeString("rel", []byte("noreferrer"))
n.SetAttributeString("target", []byte("_blank"))
return
}
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
mdLinkWalk(child)
}
}
// replaceTabs replaces all tabs in text with spaces up to a 4-space tab stop.
//
// In Markdown, tabs used for indentation are required to be interpreted as
// 4-space tab stops. See https://spec.commonmark.org/0.30/#tabs.
// Go also renders nicely and more compactly on the screen with 4-space
// tab stops, while browsers often use 8-space.
// And Goldmark crashes in some inputs that mix spaces and tabs.
// Fix the crashes and make the Go code consistently compact across browsers,
// all while staying Markdown-compatible, by expanding to 4-space tab stops.
//
// This function does not handle multi-codepoint Unicode sequences correctly.
func replaceTabs(text []byte) []byte {
var buf bytes.Buffer
col := 0
for len(text) > 0 {
r, size := utf8.DecodeRune(text)
text = text[size:]
switch r {
case '\n':
buf.WriteByte('\n')
col = 0
case '\t':
buf.WriteByte(' ')
col++
for col%4 != 0 {
buf.WriteByte(' ')
col++
}
default:
buf.WriteRune(r)
col++
}
}
return buf.Bytes()
}

Просмотреть файл

@ -2,20 +2,310 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package web implements a basic web site serving framework.
// The two fundamental types in this package are Site and Page.
//
// Sites
//
// A Site is an http.Handler that serves requests from a file system.
// Use NewSite(fsys) to create a new Site.
//
// The Site is defined primarily by the content of its file system fsys,
// which holds files to be served as well as templates for
// converting Markdown or HTML fragments into full HTML pages.
//
// Pages
//
// A Page, which is a map[string]interface{}, is the raw data that a Site renders into a web page.
// Typically a Page is loaded from a *.html or *.md file in the file system fsys, although
// dynamic pages can be computed and passed to ServePage as well,
// as described in “Serving Dynamic Pages” below.
//
// For a Page loaded from the file system, the key-value pairs in the map
// are initialized from the YAML or JSON metadata block at the top of a Markdown or HTML file,
// which looks like (YAML):
//
// ---
// key: value
// ...
// ---
//
// or (JSON):
//
// <!--{
// "Key": "value",
// ...
// }-->
//
// By convention, key-value pairs loaded from a metadata block use lower-case keys.
// For historical reasons, keys in JSON metadata are converted to lower-case when read,
// so that the two headers above both refer to a key with a lower-case k.
//
// A few keys have special meanings:
//
// The key-value pair “status: n” sets the HTTP response status to the integer code n.
//
// The key-value pair “redirect: url” causes requests for this page redirect to the given
// relative or absolute URL.
//
// The key-value pair “layout: name” selects the page layout template with the given name.
// See the next section, “Page Rendering”, for details about layout and rendering.
//
// In addition to these explicit key-value pairs, pages loaded from the file system
// have a few implicit key-value pairs added by the page loading process:
//
// - File: the path in fsys to the file containing the page
// - FileData: the file body, with the key-value metadata stripped
// - URL: this page's URL path (/x/y/z for x/y/z.md, /x/y/ for x/y/index.md)
//
// The key “Content” is added during during the rendering process.
// See “Page Rendering” for details.
//
// Page Rendering
//
// A Page's content is rendered in two steps: conversion to content, and framing of content.
//
// To convert a page to content, the page's file body (its FileData key, a []byte) is parsed
// and executed as an HTML template, with the page itself passed as the template input data.
// The template output is then interpreted as Markdown (perhaps with embedded HTML),
// and converted to HTML. The result is stored in the page under the key “Content”,
// with type template.HTML.
//
// A page's conversion to content can be skipped entirely in dynamically-generated pages
// by setting the “Content” key before passing the page to ServePage.
//
// The second step is framing the content in the overall site HTML, which is done by
// executing the site template, again using the Page itself as the template input data.
//
// The site template is constructed from two files in the file system.
// The first file is the fsys's “site.tmpl”, which provides the overall HTML frame for the site.
// The second file is a layout-specific template file, selected by the Page's
// “layout: name” key-value pair.
// The renderer searches for “name.tmpl” in the directory containing the page's file,
// then in the parent of that directory, and so on up to the root.
// If no such template is found, the rendering fails and reports that error.
// As a special case, “layout: none” skips the second file entirely.
//
// If there is no “layout: name” key-value pair, then the renderer tries using an
// implicit “layout: default”, but if no such “default.tmpl” template file can be found,
// the renderer uses an implicit “layout: none” instead.
//
// By convention, the site template and the layout-specific template are connected as follows.
// The site template, at the point where the content should be rendered, executes:
//
// {{block "layout" .}}{{.Content}}{{end}}
//
// The layout-specific template overrides this block by defining its own template named “layout”.
// For example:
//
// {{define "layout"}}
// Here's some <blink>great</blink> content: {{.Content}}
// {{end}}
//
// The use of the “block” template construct ensures that
// if there is no layout-specific template,
// the content will still be rendered.
//
// Page Template Functions
//
// In this web server, templates can themselves be invoked as functions.
// See https://pkg.go.dev/rsc.io/tmplfunc for more details about that feature.
//
// During page rendering, both when rendering a page to content and when framing the content,
// the following template functions are available (in addition to those provided by the
// template package itself and the per-template functions just mentioned).
//
// In all functions taking a file path f, if the path begins with a slash,
// it is interpreted relative to the fsys root.
// Otherwise, it is interpreted relative to the directory of the current page's URL.
//
// The “{{add x y}}”, “{{sub x y}}”, “{{mul x y}}”, and “{{div x y}}” functions
// provide basic math on arguments of type int.
//
// The “{{code f [start [end]]}}” function returns a template.HTML of a formatted display
// of code lines from the file f.
// If both start and end are omitted, then the display shows the entire file.
// If only the start line is specified, then the display shows that single line.
// If both start and end are specified, then the display shows a range of lines
// starting at start up to and including end.
// The arguments start and end can take two forms: a number indicates a specific line number,
// and a string is taken to be a regular expresion indicating the earliest matching line
// in the file (or, for end, the earliest matching line after the start line).
// Any lines ending in “OMIT” are elided from the display.
//
// For example:
//
// {{code "hello.go" `^func main` `^}`}}
//
// The “{{data f}}” function reads the file f,
// decodes it as YAML, and then returns the resulting data,
// typically a map[string]interface{}.
// It is effectively shorthand for “{{yaml (file f)}}”.
//
// The “{{file f}}” function reads the file f and returns its content as a string.
//
// The “{{first n slice}}” function returns a slice of the first n elements of slice,
// or else slice itself when slice has fewer than n elements.
//
// The “{{markdown text}}” function interprets text (a string) as Markdown
// and returns the equivalent HTML as a template.HTML.
//
// The “{{page f}}” function returns the page data (a Page)
// for the static page contained in the file f.
// The lookup ignores trailing slashes in f as well as the presence or absence
// of extensions like .md, .html, /index.md, and /index.html,
// making it possible for f to be a relative or absolute URL path instead of a file path.
//
// The “{{pages glob}}” function returns a slice of page data (a []Page)
// for all pages loaded from files or directories
// in fsys matching the given glob (a string),
// according to the usual file path rules (if the glob starts with slash,
// it is interpreted relative to the fsys root, and otherwise
// relative to the directory of the page's URL).
// If the glob pattern matches a directory,
// the page for the directory's index.md or index.html is used.
//
// For example:
//
// Here are all the articles:
// {{range (pages "/articles/*")}}
// - [{{.title}}]({{.URL}})
// {{end}}
//
// The “{{rawhtml s}}” function converts s (a string) to type template.HTML without any escaping,
// to allow using s as raw HTML in the final output.
//
// The “{{yaml s}}” function decodes s (a string) as YAML and returns the resulting data.
// It is most useful for defining templates that accept YAML-structured data as a literal argument.
// For example:
//
// {{define "quote info"}}
// {{with (yaml .info)}}
// .text
// — .name{{if .title}}, .title{{end}}
// {{end}}
//
// {{quote `
// text: If a program is too slow, it must have a loop.
// name: Ken Thompson
// `}}
//
// The “path” and “strings” functions return package objects with methods for every top-level
// function in these packages (except path.Split, which has more than one non-error result
// and would not be invokable). For example, “{{strings.ToUpper "abc"}}”.
//
// Serving Requests
//
// A Site is an http.Handler that serves requests by consulting the underlying
// file system and constructing and rendering pages, as well as serving binary
// and text files.
//
// To serve a request for URL path /p, if fsys has a file
// p/index.md, p/index.html, p.md, or p.html
// (in that order of preference), then the Site opens that file,
// parses it into a Page, renders the page as described
// in the “Page Rendering” section above,
// and responds to the request with the generated HTML.
// If the request URL does not match the parsed page's URL,
// then the Site responds with a redirect to the canonical URL.
//
// Otherwise, if fsys has a directory p and the Site
// can find a template “dir.tmpl” in that directory or a parent,
// then the Site responds with the rendering of
//
// Page{
// "URL": "/p/",
// "File": "p",
// "layout": "dir",
// "dir": []fs.FileInfo(dir),
// }
//
// where dir is the directory contents.
//
// Otherwise, if fsys has a file p containing valid UTF-8 text
// (at least up to the first kilobyte of the file) and the Site
// can find a template “text.tmpl” in that file's directory or a parent,
// and the file is not named robots.txt,
// and the file does not have a .css, .js, or .svg extension,
// then the Site responds with the rendering of
//
// Page{
// "URL": "/p",
// "File": "p",
// "layout": "texthtml",
// "texthtml": template.HTML(texthtml),
// }
//
// where texthtml is the text file as rendered by the
// golang.org/x/website/internal/texthtml package.
// In the texthtml.Config, GoComments is set to true for
// file names ending in .go;
// the h URL query parameter, if present, is passed as Highlight,
// and the s URL query parameter, if set to lo:hi, is passed as a
// single-range Selection.
//
// If the request has the URL query parameter m=text,
// then the text file content is not rendered or framed and is instead
// served directly as a plain text response.
//
// Otherwise, if none of those cases apply but the request path p
// does exist in the file system, then the Site passes the
// request to an http.FileServer serving from fsys.
// This last case handles binary static content as well as
// textual static content excluded from the text file case above.
//
// Otherwise, the Site responds with the rendering of
//
// Page{
// "URL": r.URL.Path,
// "status": 404,
// "layout": "error",
// "error": err,
// }
//
// where err is the “not exist” error returned by fs.Stat(fsys, p).
// (See also the “Serving Errors” section below.)
//
// Serving Dynamic Requests
//
// Of course, a web site may wish to serve more than static content.
// To allow dynamically generated web pages to make use of page
// rendering and site templates, the Site.ServePage method can be
// called with a dynamically generated Page value, which will then
// be rendered and served as the result of the request.
//
// Serving Errors
//
// If an error occurs while serving a request r,
// the Site responds with the rendering of
//
// Page{
// "URL": r.URL.Path,
// "status": 500,
// "layout": "error",
// "error": err,
// }
//
// If that rendering itself fails, the Site responds with status 500
// and the cryptic page text “error rendering error”.
//
// The Site.ServeError and Site.ServeErrorStatus methods provide a way
// for dynamic servers to generate similar responses.
//
package web
import (
"bytes"
"errors"
"fmt"
"html"
"io"
"log"
"net/http"
"path"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/httpfs"
@ -24,220 +314,164 @@ import (
"golang.org/x/website/internal/texthtml"
)
// Site is a website served from a file system.
// A Site is an http.Handler that serves requests from a file system.
// See the package doc comment for details.
type Site struct {
fs fs.FS
mux *http.ServeMux
fileServer http.Handler
Templates *template.Template
// GoogleAnalytics optionally adds Google Analytics via the provided
// tracking ID to each page.
GoogleAnalytics string
docFuncs template.FuncMap
fs fs.FS // from NewSite
fileServer http.Handler // http.FileServer(http.FS(fs))
funcs template.FuncMap // accumulated from s.Funcs
cache sync.Map // canonical file path -> *pageFile, for site.openPage
}
var siteFuncs = template.FuncMap{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
"div": func(a, b int) int { return a / b },
"basename": path.Base,
"split": strings.Split,
"join": strings.Join,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"trimPrefix": strings.TrimPrefix,
"trimSuffix": strings.TrimSuffix,
}
// NewSite returns a new Presentation from a file system.
func NewSite(fsys fs.FS) (*Site, error) {
p := &Site{
// NewSite returns a new Site for serving pages from the file system fsys.
func NewSite(fsys fs.FS) *Site {
return &Site{
fs: fsys,
mux: http.NewServeMux(),
fileServer: http.FileServer(httpfs.FS(fsys)),
}
p.mux.HandleFunc("/", p.serveFile)
p.initDocFuncs()
}
t, err := template.New("").Funcs(siteFuncs).ParseFS(fsys, "lib/godoc/*.html")
if err != nil {
return nil, err
// Funcs adds the functions in m to the set of functions available to templates.
// Funcs must not be called concurrently with any page rendering.
func (s *Site) Funcs(m template.FuncMap) {
if s.funcs == nil {
s.funcs = make(template.FuncMap)
}
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,
})
}
// 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)
if d, ok := page.Data.(interface{ SetWebPage(*Page) }); ok {
d.SetWebPage(&page)
for k, v := range m {
s.funcs[k] = v
}
}
if page.Template != "" {
t := s.Templates.Lookup(page.Template)
var buf bytes.Buffer
if err := t.Execute(&buf, &page); err != nil {
log.Printf("%s.Execute: %s", t.Name(), err)
}
page.HTML = template.HTML(buf.String())
// readFile returns the content of the named file in the site's file system.
// If file begins with a slash, it is interpreted relative to the root of the file system.
// Otherwise, it is interpreted relative to dir.
func (site *Site) readFile(dir, file string) ([]byte, error) {
if strings.HasPrefix(file, "/") {
file = path.Clean(file)
} else {
page.HTML = page.Data.(template.HTML)
file = path.Join(dir, file)
}
applyTemplateToResponseWriter(w, s.Templates.Lookup("site.html"), &page)
file = strings.Trim(file, "/")
if file == "" {
file = "."
}
return fs.ReadFile(site.fs, file)
}
// A Page describes the contents of a webpage to be served.
// ServeError is ServeErrorStatus with HTTP status code 500 (internal server error).
func (s *Site) ServeError(w http.ResponseWriter, r *http.Request, err error) {
s.ServeErrorStatus(w, r, err, http.StatusInternalServerError)
}
// ServeErrorStatus responds to the request
// with the given error and HTTP status.
// It is equivalent to calling ServePage(w, r, p) where p is:
//
// 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
HTML template.HTML
// Filled in automatically by ServePage
GoogleCN bool // served on golang.google.cn
GoogleAnalytics string // Google Analytics tag
Version string
Site *Site
// Page{
// "URL": r.URL.Path,
// "status": status,
// "layout": error,
// "error": err,
// }
//
func (s *Site) ServeErrorStatus(w http.ResponseWriter, r *http.Request, err error, status int) {
s.serveErrorStatus(w, r, err, status, false)
}
// 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
}
page.Version = runtime.Version()
page.GoogleCN = GoogleCN(r)
page.GoogleAnalytics = s.GoogleAnalytics
page.Site = s
return page
}
func (s *Site) serveErrorStatus(w http.ResponseWriter, r *http.Request, err error, status int, renderingError bool) {
type writeErrorSaver struct {
w io.Writer
err error
}
func (w *writeErrorSaver) 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 := &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.
if w.err == nil && err != nil {
// Log template errors.
log.Printf("%s.Execute: %s", t.Name(), err)
}
}
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)
if renderingError {
log.Printf("error rendering error: %v", err)
w.WriteHeader(status)
w.Write([]byte("error rendering error"))
return
}
// Check to see if we need to redirect or serve another file.
p := Page{
"URL": r.URL.Path,
"status": status,
"layout": "error",
"error": err,
}
s.servePage(w, r, p, true)
}
// ServePage renders the page p to HTML and writes that HTML to w.
// See the package doc comment for details about page rendering.
//
// So that all templates can assume the presence of p["URL"],
// if p["URL"] is unset or does not have type string, then ServePage
// sets p["URL"] to r.URL.Path in a clone of p before rendering the page.
func (s *Site) ServePage(w http.ResponseWriter, r *http.Request, p Page) {
s.servePage(w, r, p, false)
}
func (s *Site) servePage(w http.ResponseWriter, r *http.Request, p Page, renderingError bool) {
html, err := s.renderHTML(p, r)
if err != nil {
s.serveErrorStatus(w, r, fmt.Errorf("template execution: %v", err), http.StatusInternalServerError, renderingError)
return
}
if code, ok := p["status"].(int); ok {
w.WriteHeader(code)
}
w.Write(html)
}
// ServeHTTP implements http.Handler, serving from a file in the site.
// See the Site type documentation for details about how requests are handled.
func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
abspath := r.URL.Path
relpath := path.Clean(strings.TrimPrefix(abspath, "/"))
if f := open(s.fs, relpath); f != nil {
if f.Path != abspath {
// Is it a page we can generate?
if p, err := s.openPage(relpath); err == nil {
if p.url != abspath {
// Redirect to canonical path.
http.Redirect(w, r, f.Path, http.StatusMovedPermanently)
status := http.StatusMovedPermanently
if i, ok := p.page["status"].(int); ok {
status = i
}
http.Redirect(w, r, p.url, status)
return
}
// Serve from the actual filesystem path.
s.serveHTML(w, r, f)
s.serveHTML(w, r, p)
return
}
dir, err := fs.Stat(s.fs, relpath)
// Is it a directory or file we can serve?
info, err := fs.Stat(s.fs, relpath)
if err != nil {
// Check for spurious trailing slash.
if strings.HasSuffix(abspath, "/") {
trimmed := relpath[:len(relpath)-1]
if _, err := fs.Stat(s.fs, trimmed); err == nil ||
open(s.fs, trimmed) != nil {
http.Redirect(w, r, "/"+trimmed, http.StatusMovedPermanently)
return
status := http.StatusInternalServerError
if errors.Is(err, fs.ErrNotExist) {
status = http.StatusNotFound
}
s.ServeErrorStatus(w, r, err, status)
return
}
// Serve directory.
if info != nil && info.IsDir() {
if _, ok := s.findLayout(relpath, "dir"); ok {
if !maybeRedirect(w, r) {
s.serveDir(w, r, relpath)
}
}
s.ServeError(w, r, err)
return
}
if dir != nil && dir.IsDir() {
if maybeRedirect(w, r) {
return
}
s.serveDir(w, r, relpath)
return
}
// Serve text file.
if isTextFile(s.fs, relpath) {
if maybeRedirectFile(w, r) {
if _, ok := s.findLayout(path.Dir(relpath), "text"); ok {
if !maybeRedirectFile(w, r) {
s.serveText(w, r, relpath)
}
return
}
s.serveText(w, r, relpath)
return
}
// Serve raw bytes.
s.fileServer.ServeHTTP(w, r)
}
@ -267,63 +501,32 @@ func maybeRedirectFile(w http.ResponseWriter, r *http.Request) (redirected bool)
return
}
var doctype = []byte("<!DOCTYPE ")
func (s *Site) serveHTML(w http.ResponseWriter, r *http.Request, f *file) {
src := f.Body
isMarkdown := strings.HasSuffix(f.FilePath, ".md")
func (s *Site) serveHTML(w http.ResponseWriter, r *http.Request, p *pageFile) {
src, _ := p.page["FileData"].(string)
filePath, _ := p.page["File"].(string)
isMarkdown := strings.HasSuffix(filePath, ".md")
// if it begins with "<!DOCTYPE " assume it is standalone
// html that doesn't need the template wrapping.
if bytes.HasPrefix(src, doctype) {
w.Write(src)
if strings.HasPrefix(src, "<!DOCTYPE ") {
w.Write([]byte(src))
return
}
page := Page{
Title: f.Title,
Subtitle: f.Subtitle,
}
// evaluate as template if indicated
if f.Template {
page = s.fullPage(r, page)
tmpl, err := template.New("main").Funcs(s.docFuncs).Parse(string(src))
if err != nil {
log.Printf("parsing template %s: %v", f.Path, err)
s.ServeError(w, r, err)
return
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, page); err != nil {
log.Printf("executing template %s: %v", f.Path, err)
s.ServeError(w, r, err)
return
}
src = buf.Bytes()
}
// Apply markdown as indicated.
// (Note template applies before Markdown.)
if isMarkdown {
html, err := renderMarkdown(src)
if err != nil {
log.Printf("executing markdown %s: %v", f.Path, err)
s.ServeError(w, r, err)
return
}
src = html
}
// if it's the language spec, add tags to EBNF productions
if strings.HasSuffix(f.FilePath, "go_spec.html") {
if strings.HasSuffix(filePath, "go_spec.html") {
var buf bytes.Buffer
spec.Linkify(&buf, src)
src = buf.Bytes()
spec.Linkify(&buf, []byte(src))
src = buf.String()
}
page.Data = template.HTML(src)
s.ServePage(w, r, page)
// Template is enabled always in Markdown.
// It can only be disabled for HTML files.
isTemplate, _ := p.page["template"].(bool)
if !isTemplate && !isMarkdown {
p.page["Content"] = template.HTML(src)
}
s.ServePage(w, r, p.page)
}
func (s *Site) serveDir(w http.ResponseWriter, r *http.Request, relpath string) {
@ -345,13 +548,11 @@ func (s *Site) serveDir(w http.ResponseWriter, r *http.Request, relpath string)
}
}
dirpath := strings.TrimSuffix(relpath, "/") + "/"
s.ServePage(w, r, Page{
Title: "Directory",
SrcPath: dirpath,
TabTitle: dirpath,
Template: "dirlist.html",
Data: info,
"URL": r.URL.Path,
"File": relpath,
"layout": "dir",
"dir": info,
})
}
@ -382,15 +583,11 @@ func (s *Site) serveText(w http.ResponseWriter, r *http.Request, relpath string)
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()),
"URL": r.URL.Path,
"File": relpath,
"layout": "texthtml",
"texthtml": template.HTML(buf.String()),
})
}

Просмотреть файл

@ -18,7 +18,7 @@ func testServeBody(t *testing.T, p *Site, path, body string) {
t.Helper()
r := &http.Request{URL: &url.URL{Path: path}}
rw := httptest.NewRecorder()
p.serveFile(rw, r)
p.ServeHTTP(rw, r)
if rw.Code != 200 || !strings.Contains(rw.Body.String(), body) {
t.Fatalf("GET %s: expected 200 w/ %q: got %d w/ body:\n%s",
path, body, rw.Code, rw.Body)
@ -27,13 +27,11 @@ func testServeBody(t *testing.T, p *Site, path, body string) {
func TestRedirectAndMetadata(t *testing.T) {
fsys := fstest.MapFS{
"site.tmpl": {Data: []byte(`{{.Content}}`)},
"doc/x/index.html": {Data: []byte("Hello, x.")},
"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
}
p, err := NewSite(fsys)
if err != nil {
t.Fatal(err)
}
site := NewSite(fsys)
// Test that redirect is sent back correctly.
// Used to panic. See golang.org/issue/40665.
@ -41,25 +39,23 @@ func TestRedirectAndMetadata(t *testing.T) {
r := &http.Request{URL: &url.URL{Path: dir + "index.html"}}
rw := httptest.NewRecorder()
p.serveFile(rw, r)
site.ServeHTTP(rw, r)
loc := rw.Result().Header.Get("Location")
if rw.Code != 301 || loc != dir {
t.Errorf("GET %s: expected 301 -> %q, got %d -> %q", r.URL.Path, dir, rw.Code, loc)
}
testServeBody(t, p, dir, "Hello, x")
testServeBody(t, site, dir, "Hello, x")
}
func TestMarkdown(t *testing.T) {
p, err := NewSite(fstest.MapFS{
site := NewSite(fstest.MapFS{
"site.tmpl": {Data: []byte(`{{.Content}}`)},
"doc/test.md": {Data: []byte("**bold**")},
"doc/test2.md": {Data: []byte(`{{"*template*"}}`)},
"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
})
if err != nil {
t.Fatal(err)
}
testServeBody(t, p, "/doc/test", "<strong>bold</strong>")
testServeBody(t, p, "/doc/test2", "<em>template</em>")
testServeBody(t, site, "/doc/test", "<strong>bold</strong>")
testServeBody(t, site, "/doc/test2", "<em>template</em>")
}

161
internal/web/tmpl.go Normal file
Просмотреть файл

@ -0,0 +1,161 @@
// Copyright 2021 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 (
"fmt"
"path"
"reflect"
"sort"
"strings"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/io/fs"
"gopkg.in/yaml.v3"
)
// A siteDir is a site extended with a known directory for interpreting relative paths.
type siteDir struct {
*Site
dir string
}
func toString(x interface{}) string {
switch x := x.(type) {
case string:
return x
case template.HTML:
return string(x)
case nil:
return ""
default:
panic(fmt.Sprintf("cannot toString %T", x))
}
}
// data parses the named yaml file (relative to dir) and returns its structured data.
func (site *siteDir) data(name string) (interface{}, error) {
data, err := site.readFile(site.dir, name)
if err != nil {
return nil, err
}
var d interface{}
if err := yaml.Unmarshal(data, &d); err != nil {
return nil, err
}
return d, nil
}
func first(n int, list reflect.Value) reflect.Value {
if !list.IsValid() {
return list
}
if list.Kind() == reflect.Interface {
if list.IsNil() {
return list
}
list = list.Elem()
}
if list.Len() < n {
return list
}
return list.Slice(0, n)
}
// markdown is the function provided to templates.
func markdown(data interface{}) (template.HTML, error) {
h, err := markdownToHTML(toString(data))
if err != nil {
return "", err
}
s := strings.TrimSpace(string(h))
if strings.HasPrefix(s, "<p>") && strings.HasSuffix(s, "</p>") && strings.Count(s, "<p>") == 1 {
h = template.HTML(strings.TrimSpace(s[len("<p>") : len(s)-len("</p>")]))
}
return h, nil
}
func (site *siteDir) readfile(name string) (string, error) {
data, err := site.readFile(site.dir, name)
return string(data), err
}
// page returns the page params for the page with a given url u.
// The url may or may not have its leading slash.
func (site *siteDir) page(u string) (Page, error) {
if !path.IsAbs(u) {
u = path.Join(site.dir, u)
}
p, err := site.openPage(strings.Trim(u, "/"))
if err != nil {
return nil, err
}
return p.page, nil
}
// pages returns the page params for pages with urls matching glob.
func (site *siteDir) pages(glob string) ([]Page, error) {
if !path.IsAbs(glob) {
glob = path.Join(site.dir, glob)
}
// TODO(rsc): Add a cache?
_, err := path.Match(glob, "")
if err != nil {
return nil, err
}
glob = strings.Trim(glob, "/")
if glob == "" {
glob = "."
}
matches, err := fs.Glob(site.fs, glob)
if err != nil {
return nil, err
}
var out []Page
for _, file := range matches {
if !strings.HasSuffix(file, ".md") && !strings.HasSuffix(file, ".html") {
f := path.Join(file, "index.md")
if _, err := fs.Stat(site.fs, f); err != nil {
f = path.Join(file, "index.html")
if _, err = fs.Stat(site.fs, f); err != nil {
continue
}
}
file = f
}
p, err := site.openPage(file)
if err != nil {
return nil, err
}
out = append(out, p.page)
}
sort.Slice(out, func(i, j int) bool {
return out[i]["URL"].(string) < out[j]["URL"].(string)
})
return out, nil
}
// file parses the named file (relative to dir) and returns its content as a string.
func (site *siteDir) file(name string) (string, error) {
data, err := site.readFile(site.dir, name)
if err != nil {
return "", err
}
return string(data), nil
}
func rawhtml(s interface{}) template.HTML {
return template.HTML(toString(s))
}
func yamlFn(s string) (interface{}, error) {
var d interface{}
if err := yaml.Unmarshal([]byte(s), &d); err != nil {
return nil, err
}
return d, nil
}