diff --git a/godoc/markdown.go b/godoc/markdown.go
new file mode 100644
index 0000000000..fd61aa5553
--- /dev/null
+++ b/godoc/markdown.go
@@ -0,0 +1,31 @@
+// 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 godoc
+
+import (
+ "bytes"
+
+ "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) {
+ // 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
+}
diff --git a/godoc/meta.go b/godoc/meta.go
index 260833dbd1..8d3b82534d 100644
--- a/godoc/meta.go
+++ b/godoc/meta.go
@@ -26,12 +26,15 @@ var (
// ----------------------------------------------------------------------------
// Documentation Metadata
-// TODO(adg): why are some exported and some aren't? -brad
type Metadata struct {
+ // These fields can be set in the JSON header at the top of a doc.
Title string
Subtitle string
- Template bool // execute as template
- Path string // canonical path for this page
+ Template bool // execute as template
+ Path string // canonical path for this page
+ AltPaths []string // redirect these other paths to this page
+
+ // These are internal to the implementation.
filePath string // filesystem path relative to goroot
}
@@ -58,7 +61,7 @@ func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
return
}
-// UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata,
+// UpdateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata,
// and updates the DocMetadata map.
func (c *Corpus) updateMetadata() {
metadata := make(map[string]*Metadata)
@@ -79,7 +82,7 @@ func (c *Corpus) updateMetadata() {
scan(name) // recurse
continue
}
- if !strings.HasSuffix(name, ".html") {
+ if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".md") {
continue
}
// Extract metadata from the file.
@@ -93,15 +96,23 @@ func (c *Corpus) updateMetadata() {
log.Printf("updateMetadata: %s: %v", name, err)
continue
}
+ // Present all .md as if they were .html,
+ // so that it doesn't matter which one a page is written in.
+ if strings.HasSuffix(name, ".md") {
+ name = strings.TrimSuffix(name, ".md") + ".html"
+ }
// Store relative filesystem path in Metadata.
meta.filePath = name
if meta.Path == "" {
- // If no Path, canonical path is actual path.
- meta.Path = meta.filePath
+ // If no Path, canonical path is actual path with .html removed.
+ meta.Path = strings.TrimSuffix(name, ".html")
}
// Store under both paths.
metadata[meta.Path] = &meta
metadata[meta.filePath] = &meta
+ for _, path := range meta.AltPaths {
+ metadata[path] = &meta
+ }
}
}
scan("/doc")
diff --git a/godoc/server.go b/godoc/server.go
index 8724291c6c..8c9b1b9fc3 100644
--- a/godoc/server.go
+++ b/godoc/server.go
@@ -695,7 +695,15 @@ func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, ab
func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
// get HTML body contents
+ isMarkdown := false
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
+ if err != nil && strings.HasSuffix(abspath, ".html") {
+ if md, errMD := vfs.ReadFile(p.Corpus.fs, strings.TrimSuffix(abspath, ".html")+".md"); errMD == nil {
+ src = md
+ isMarkdown = true
+ err = nil
+ }
+ }
if err != nil {
log.Printf("ReadFile: %s", err)
p.ServeError(w, r, relpath, err)
@@ -738,6 +746,18 @@ func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, absp
src = buf.Bytes()
}
+ // Apply markdown as indicated.
+ // (Note template applies before Markdown.)
+ if isMarkdown {
+ html, err := renderMarkdown(src)
+ if err != nil {
+ log.Printf("executing markdown %s: %v", relpath, err)
+ p.ServeError(w, r, relpath, err)
+ return
+ }
+ src = html
+ }
+
// if it's the language spec, add tags to EBNF productions
if strings.HasSuffix(abspath, "go_spec.html") {
var buf bytes.Buffer
@@ -797,7 +817,8 @@ func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
if redirect(w, r) {
return
}
- if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
+ index := pathpkg.Join(abspath, "index.html")
+ if util.IsTextFile(p.Corpus.fs, index) || util.IsTextFile(p.Corpus.fs, pathpkg.Join(abspath, "index.md")) {
p.ServeHTMLDoc(w, r, index, index)
return
}
diff --git a/godoc/server_test.go b/godoc/server_test.go
index f8621352f0..0d48e9f04b 100644
--- a/godoc/server_test.go
+++ b/godoc/server_test.go
@@ -73,6 +73,17 @@ func F()
}
}
+func testServeBody(t *testing.T, p *Presentation, path, body string) {
+ t.Helper()
+ r := &http.Request{URL: &url.URL{Path: path}}
+ rw := httptest.NewRecorder()
+ p.ServeFile(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)
+ }
+}
+
func TestRedirectAndMetadata(t *testing.T) {
c := NewCorpus(mapfs.New(map[string]string{
"doc/y/index.html": "Hello, y.",
@@ -87,13 +98,13 @@ Hello, x.
Corpus: c,
GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
}
- r := &http.Request{URL: &url.URL{}}
// Test that redirect is sent back correctly.
// Used to panic. See golang.org/issue/40665.
for _, elem := range []string{"x", "y"} {
dir := "/doc/" + elem + "/"
- r.URL.Path = dir + "index.html"
+
+ r := &http.Request{URL: &url.URL{Path: dir + "index.html"}}
rw := httptest.NewRecorder()
p.ServeFile(rw, r)
loc := rw.Result().Header.Get("Location")
@@ -101,12 +112,19 @@ Hello, x.
t.Errorf("GET %s: expected 301 -> %q, got %d -> %q", r.URL.Path, dir, rw.Code, loc)
}
- r.URL.Path = dir
- rw = httptest.NewRecorder()
- p.ServeFile(rw, r)
- if rw.Code != 200 || !strings.Contains(rw.Body.String(), "Hello, "+elem) {
- t.Fatalf("GET %s: expected 200 w/ Hello, %s: got %d w/ body:\n%s",
- r.URL.Path, elem, rw.Code, rw.Body)
- }
+ testServeBody(t, p, dir, "Hello, "+elem)
}
}
+
+func TestMarkdown(t *testing.T) {
+ p := &Presentation{
+ Corpus: NewCorpus(mapfs.New(map[string]string{
+ "doc/test.md": "**bold**",
+ "doc/test2.md": `{{"*template*"}}`,
+ })),
+ GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
+ }
+
+ testServeBody(t, p, "/doc/test.html", "bold")
+ testServeBody(t, p, "/doc/test2.html", "template")
+}