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:
Родитель
f8f1822414
Коммит
ef7fed48ec
|
@ -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"> </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 ></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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) }
|
|
@ -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>")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Загрузка…
Ссылка в новой задаче