Skip to content

Commit 5bd3da9

Browse files
committed
godoc: convert Markdown files to HTML during serving
For golang.org today, Markdown is converted to HTML during the static file embedding, but that precludes using Markdown with "live serving". Moving the code here lets godoc itself do the conversion and therefore works with live serving. It is also more consistent with re-executing templates during serving for Template:true files. When a file is .md but also has Template: true, templates apply first, so that templates can generate Markdown. This is reversed from what x/website was doing (Markdown before templates) but that decision was mostly forced by doing it during static embedding and not necessarily the right one. There's no reason to force switching to raw HTML just because you want to use a template. (A template can of course still generate HTML.) Change-Id: I7db6d54b43e45803e965df7a1ab2f26293285cfd Reviewed-on: https://go-review.googlesource.com/c/tools/+/251343 Trust: Russ Cox <[email protected]> Run-TryBot: Russ Cox <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 9eba6e1 commit 5bd3da9

File tree

4 files changed

+98
-17
lines changed

4 files changed

+98
-17
lines changed

godoc/markdown.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package godoc
6+
7+
import (
8+
"bytes"
9+
10+
"github.com/yuin/goldmark"
11+
"github.com/yuin/goldmark/parser"
12+
"github.com/yuin/goldmark/renderer/html"
13+
)
14+
15+
// renderMarkdown converts a limited and opinionated flavor of Markdown (compliant with
16+
// CommonMark 0.29) to HTML for the purposes of Go websites.
17+
//
18+
// The Markdown source may contain raw HTML,
19+
// but Go templates have already been processed.
20+
func renderMarkdown(src []byte) ([]byte, error) {
21+
// parser.WithHeadingAttribute allows custom ids on headings.
22+
// html.WithUnsafe allows use of raw HTML, which we need for tables.
23+
md := goldmark.New(
24+
goldmark.WithParserOptions(parser.WithHeadingAttribute()),
25+
goldmark.WithRendererOptions(html.WithUnsafe()))
26+
var buf bytes.Buffer
27+
if err := md.Convert(src, &buf); err != nil {
28+
return nil, err
29+
}
30+
return buf.Bytes(), nil
31+
}

godoc/meta.go

+18-7
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ var (
2626
// ----------------------------------------------------------------------------
2727
// Documentation Metadata
2828

29-
// TODO(adg): why are some exported and some aren't? -brad
3029
type Metadata struct {
30+
// These fields can be set in the JSON header at the top of a doc.
3131
Title string
3232
Subtitle string
33-
Template bool // execute as template
34-
Path string // canonical path for this page
33+
Template bool // execute as template
34+
Path string // canonical path for this page
35+
AltPaths []string // redirect these other paths to this page
36+
37+
// These are internal to the implementation.
3538
filePath string // filesystem path relative to goroot
3639
}
3740

@@ -58,7 +61,7 @@ func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
5861
return
5962
}
6063

61-
// UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata,
64+
// UpdateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata,
6265
// and updates the DocMetadata map.
6366
func (c *Corpus) updateMetadata() {
6467
metadata := make(map[string]*Metadata)
@@ -79,7 +82,7 @@ func (c *Corpus) updateMetadata() {
7982
scan(name) // recurse
8083
continue
8184
}
82-
if !strings.HasSuffix(name, ".html") {
85+
if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".md") {
8386
continue
8487
}
8588
// Extract metadata from the file.
@@ -93,15 +96,23 @@ func (c *Corpus) updateMetadata() {
9396
log.Printf("updateMetadata: %s: %v", name, err)
9497
continue
9598
}
99+
// Present all .md as if they were .html,
100+
// so that it doesn't matter which one a page is written in.
101+
if strings.HasSuffix(name, ".md") {
102+
name = strings.TrimSuffix(name, ".md") + ".html"
103+
}
96104
// Store relative filesystem path in Metadata.
97105
meta.filePath = name
98106
if meta.Path == "" {
99-
// If no Path, canonical path is actual path.
100-
meta.Path = meta.filePath
107+
// If no Path, canonical path is actual path with .html removed.
108+
meta.Path = strings.TrimSuffix(name, ".html")
101109
}
102110
// Store under both paths.
103111
metadata[meta.Path] = &meta
104112
metadata[meta.filePath] = &meta
113+
for _, path := range meta.AltPaths {
114+
metadata[path] = &meta
115+
}
105116
}
106117
}
107118
scan("/doc")

godoc/server.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,15 @@ func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, ab
695695

696696
func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
697697
// get HTML body contents
698+
isMarkdown := false
698699
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
700+
if err != nil && strings.HasSuffix(abspath, ".html") {
701+
if md, errMD := vfs.ReadFile(p.Corpus.fs, strings.TrimSuffix(abspath, ".html")+".md"); errMD == nil {
702+
src = md
703+
isMarkdown = true
704+
err = nil
705+
}
706+
}
699707
if err != nil {
700708
log.Printf("ReadFile: %s", err)
701709
p.ServeError(w, r, relpath, err)
@@ -738,6 +746,18 @@ func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, absp
738746
src = buf.Bytes()
739747
}
740748

749+
// Apply markdown as indicated.
750+
// (Note template applies before Markdown.)
751+
if isMarkdown {
752+
html, err := renderMarkdown(src)
753+
if err != nil {
754+
log.Printf("executing markdown %s: %v", relpath, err)
755+
p.ServeError(w, r, relpath, err)
756+
return
757+
}
758+
src = html
759+
}
760+
741761
// if it's the language spec, add tags to EBNF productions
742762
if strings.HasSuffix(abspath, "go_spec.html") {
743763
var buf bytes.Buffer
@@ -797,7 +817,8 @@ func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
797817
if redirect(w, r) {
798818
return
799819
}
800-
if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
820+
index := pathpkg.Join(abspath, "index.html")
821+
if util.IsTextFile(p.Corpus.fs, index) || util.IsTextFile(p.Corpus.fs, pathpkg.Join(abspath, "index.md")) {
801822
p.ServeHTMLDoc(w, r, index, index)
802823
return
803824
}

godoc/server_test.go

+27-9
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ func F()
7373
}
7474
}
7575

76+
func testServeBody(t *testing.T, p *Presentation, path, body string) {
77+
t.Helper()
78+
r := &http.Request{URL: &url.URL{Path: path}}
79+
rw := httptest.NewRecorder()
80+
p.ServeFile(rw, r)
81+
if rw.Code != 200 || !strings.Contains(rw.Body.String(), body) {
82+
t.Fatalf("GET %s: expected 200 w/ %q: got %d w/ body:\n%s",
83+
path, body, rw.Code, rw.Body)
84+
}
85+
}
86+
7687
func TestRedirectAndMetadata(t *testing.T) {
7788
c := NewCorpus(mapfs.New(map[string]string{
7889
"doc/y/index.html": "Hello, y.",
@@ -87,26 +98,33 @@ Hello, x.
8798
Corpus: c,
8899
GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
89100
}
90-
r := &http.Request{URL: &url.URL{}}
91101

92102
// Test that redirect is sent back correctly.
93103
// Used to panic. See golang.org/issue/40665.
94104
for _, elem := range []string{"x", "y"} {
95105
dir := "/doc/" + elem + "/"
96-
r.URL.Path = dir + "index.html"
106+
107+
r := &http.Request{URL: &url.URL{Path: dir + "index.html"}}
97108
rw := httptest.NewRecorder()
98109
p.ServeFile(rw, r)
99110
loc := rw.Result().Header.Get("Location")
100111
if rw.Code != 301 || loc != dir {
101112
t.Errorf("GET %s: expected 301 -> %q, got %d -> %q", r.URL.Path, dir, rw.Code, loc)
102113
}
103114

104-
r.URL.Path = dir
105-
rw = httptest.NewRecorder()
106-
p.ServeFile(rw, r)
107-
if rw.Code != 200 || !strings.Contains(rw.Body.String(), "Hello, "+elem) {
108-
t.Fatalf("GET %s: expected 200 w/ Hello, %s: got %d w/ body:\n%s",
109-
r.URL.Path, elem, rw.Code, rw.Body)
110-
}
115+
testServeBody(t, p, dir, "Hello, "+elem)
111116
}
112117
}
118+
119+
func TestMarkdown(t *testing.T) {
120+
p := &Presentation{
121+
Corpus: NewCorpus(mapfs.New(map[string]string{
122+
"doc/test.md": "**bold**",
123+
"doc/test2.md": `{{"*template*"}}`,
124+
})),
125+
GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
126+
}
127+
128+
testServeBody(t, p, "/doc/test.html", "<strong>bold</strong>")
129+
testServeBody(t, p, "/doc/test2.html", "<em>template</em>")
130+
}

0 commit comments

Comments
 (0)