internal/godoc/dochtml/internal/render: use go/doc/comment

Parse doc comments with the new go/doc/comment package.

Change-Id: If79c1035086b2b70f1207cb84153f8c0bcbd6599
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/412676
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/godoc/dochtml/internal/render/linkify.go b/internal/godoc/dochtml/internal/render/linkify.go
index fc1e6c8..542fd50 100644
--- a/internal/godoc/dochtml/internal/render/linkify.go
+++ b/internal/godoc/dochtml/internal/render/linkify.go
@@ -56,22 +56,6 @@
 	badAnchorRx = regexp.MustCompile(`[^a-zA-Z0-9]`)
 )
 
-type docData struct {
-	Elements         []docElement
-	Headings         []docElement
-	EnableCommandTOC bool
-}
-
-type docElement struct {
-	IsHeading   bool
-	IsPreformat bool
-	// for paragraph and preformat
-	Body safehtml.HTML
-	// for heading
-	Title string
-	ID    safehtml.Identifier
-}
-
 func (r *Renderer) declHTML(doc string, decl ast.Decl, extractLinks bool) (out struct{ Doc, Decl safehtml.HTML }) {
 	if doc != "" {
 		out.Doc = r.formatDocHTML(doc, extractLinks)
@@ -83,54 +67,6 @@
 	return out
 }
 
-func (r *Renderer) formatDocHTML(doc string, extractLinks bool) safehtml.HTML {
-	var els []docElement
-	inLinks := false
-	for _, blk := range docToBlocks(doc) {
-		var el docElement
-		switch blk := blk.(type) {
-		case *paragraph:
-			if inLinks {
-				r.links = append(r.links, parseLinks(blk.lines)...)
-			} else {
-				el.Body = r.linesToHTML(blk.lines, false)
-				els = append(els, el)
-			}
-		case *preformat:
-			if inLinks {
-				r.links = append(r.links, parseLinks(blk.lines)...)
-			} else {
-				el.IsPreformat = true
-				el.Body = r.linesToHTML(blk.lines, true)
-				els = append(els, el)
-			}
-		case *heading:
-			if extractLinks && blk.title == "Links" {
-				inLinks = true
-			} else {
-				inLinks = false
-				el.IsHeading = true
-				el.Title = blk.title
-				id := badAnchorRx.ReplaceAllString(blk.title, "_")
-				el.ID = safehtml.IdentifierFromConstantPrefix("hdr", id)
-				els = append(els, el)
-			}
-		}
-	}
-
-	var headings []docElement
-	for _, e := range els {
-		if e.IsHeading {
-			headings = append(headings, e)
-		}
-	}
-	return ExecuteToHTML(r.docTmpl, docData{
-		Elements:         els,
-		Headings:         headings,
-		EnableCommandTOC: r.enableCommandTOC,
-	})
-}
-
 // parseLinks extracts links from lines.
 func parseLinks(lines []string) []Link {
 	var links []Link
@@ -159,16 +95,6 @@
 	}
 }
 
-func (r *Renderer) linesToHTML(lines []string, pre bool) safehtml.HTML {
-	newline := safehtml.HTMLEscaped("\n")
-	htmls := make([]safehtml.HTML, 0, 2*len(lines))
-	for _, l := range lines {
-		htmls = append(htmls, r.formatLineHTML(l, pre))
-		htmls = append(htmls, newline)
-	}
-	return safehtml.HTMLConcat(htmls...)
-}
-
 func (r *Renderer) codeString(ex *doc.Example) (string, error) {
 	if ex == nil || ex.Code == nil {
 		return "", errors.New("please include an example with code")
diff --git a/internal/godoc/dochtml/internal/render/linkify_comment.go b/internal/godoc/dochtml/internal/render/linkify_comment.go
new file mode 100644
index 0000000..057ccec
--- /dev/null
+++ b/internal/godoc/dochtml/internal/render/linkify_comment.go
@@ -0,0 +1,306 @@
+// Copyright 2022 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 render
+
+import (
+	"fmt"
+	"go/doc/comment"
+	"regexp"
+	"strings"
+	"unicode"
+
+	safe "github.com/google/safehtml"
+	"github.com/google/safehtml/template"
+)
+
+type link struct {
+	Class string
+	Href  string
+	Text  any // string or safe.HTML
+}
+
+type heading struct {
+	ID    safe.Identifier
+	Title safe.HTML
+}
+
+var (
+	// tocTemplate expects a []heading.
+	tocTemplate = template.Must(template.New("toc").Parse(`<div role="navigation" aria-label="Table of Contents">
+  <ul class="Documentation-toc{{if gt (len .) 5}} Documentation-toc-columns{{end}}">
+    {{range . -}}
+      <li class="Documentation-tocItem">
+        <a href="#{{.ID}}">{{.Title}}</a>
+      </li>
+    {{end -}}
+  </ul>
+</div>
+`))
+
+	italicTemplate = template.Must(template.New("italics").Parse(`<i>{{.}}</i>`))
+
+	codeTemplate = template.Must(template.New("code").Parse(`<pre>{{.}}</pre>`))
+
+	paraTemplate = template.Must(template.New("para").Parse("<p>{{.}}\n</p>"))
+
+	headingTemplate = template.Must(template.New("heading").Parse(
+		`<h4 id="{{.ID}}">{{.Title}} <a class="Documentation-idLink" href="#{{.ID}}">¶</a></h4>`))
+
+	linkTemplate = template.Must(template.New("link").Parse(
+		`<a{{with .Class}}class="{{.}}" {{end}} href="{{.Href}}">{{.Text}}</a>`))
+
+	uListTemplate = template.Must(template.New("ulist").Parse(
+		`<ul>
+{{- range .}}
+  {{.}}
+{{- end}}
+</ul>`))
+
+	oListTemplate = template.Must(template.New("olist").Parse(
+		`<ol>
+		   {{range .}}
+		     {{.}}
+           {{end}}
+         </ol>`))
+
+	listItemTemplate = template.Must(template.New("li").Parse(
+		`<li{{with .Number}}value="{{.}}" {{end}}>{{.Content}}</li>`))
+)
+
+func (r *Renderer) formatDocHTML(text string, extractLinks bool) safe.HTML {
+	p := comment.Parser{}
+	doc := p.Parse(text)
+	if extractLinks {
+		r.removeLinks(doc)
+	}
+	var headings []heading
+	for _, b := range doc.Content {
+		if h, ok := b.(*comment.Heading); ok {
+			headings = append(headings, r.newHeading(h))
+		}
+	}
+	h := r.blocksToHTML(doc.Content, true, extractLinks)
+	if r.enableCommandTOC && len(headings) > 0 {
+		h = safe.HTMLConcat(ExecuteToHTML(tocTemplate, headings), h)
+	}
+	return h
+}
+
+func (r *Renderer) removeLinks(doc *comment.Doc) {
+	var bs []comment.Block
+	inLinks := false
+	for _, b := range doc.Content {
+		switch b := b.(type) {
+		case *comment.Heading:
+			if textsToString(b.Text) == "Links" {
+				inLinks = true
+			} else {
+				inLinks = false
+				bs = append(bs, b)
+			}
+		case *comment.List:
+			if inLinks {
+				for _, item := range b.Items {
+					fmt.Println("    ", item)
+					if link, ok := itemLink(item); ok {
+						r.links = append(r.links, link)
+					}
+				}
+			} else {
+				bs = append(bs, b)
+			}
+		case *comment.Paragraph:
+			if inLinks {
+				// Links section doesn't require leading whitespace, so
+				// the link may be in a paragraph.
+				s := textsToString(b.Text)
+				r.links = append(r.links, parseLinks(strings.Split(s, "\n"))...)
+			} else {
+				bs = append(bs, b)
+			}
+
+		default:
+			if !inLinks {
+				bs = append(bs, b)
+			}
+		}
+	}
+	doc.Content = bs
+}
+
+func itemLink(item *comment.ListItem) (l Link, ok bool) {
+	// Should be a single Paragraph.
+	if len(item.Content) != 1 {
+		return l, false
+	}
+	p, ok := item.Content[0].(*comment.Paragraph)
+	if !ok {
+		return l, false
+	}
+	// TODO: clean up.
+	if lp := parseLink("- " + textsToString(p.Text)); lp != nil {
+		return *lp, true
+	}
+	return l, false
+}
+
+func (r *Renderer) blocksToHTML(bs []comment.Block, useParagraph, extractLinks bool) safe.HTML {
+	return concatHTML(bs, func(b comment.Block) safe.HTML {
+		return r.blockToHTML(b, useParagraph, extractLinks)
+	})
+}
+
+func (r *Renderer) blockToHTML(b comment.Block, useParagraph, extractLinks bool) safe.HTML {
+	switch b := b.(type) {
+	case *comment.Paragraph:
+		th := r.textsToHTML(b.Text)
+		if useParagraph {
+			return ExecuteToHTML(paraTemplate, th)
+		}
+		return th
+
+	case *comment.Code:
+		return ExecuteToHTML(codeTemplate, b.Text)
+
+	case *comment.Heading:
+		return ExecuteToHTML(headingTemplate, r.newHeading(b))
+
+	case *comment.List:
+		var items []safe.HTML
+		useParagraph = b.BlankBetween()
+		for _, item := range b.Items {
+			items = append(items, ExecuteToHTML(listItemTemplate, struct {
+				Number  string
+				Content safe.HTML
+			}{item.Number, r.blocksToHTML(item.Content, useParagraph, false)}))
+		}
+		t := oListTemplate
+		if b.Items[0].Number == "" {
+			t = uListTemplate
+		}
+		return ExecuteToHTML(t, items)
+	default:
+		return badType(b)
+	}
+}
+
+func (r *Renderer) newHeading(h *comment.Heading) heading {
+	return heading{headingID(h), r.textsToHTML(h.Text)}
+}
+
+func (r *Renderer) textsToHTML(ts []comment.Text) safe.HTML {
+	return concatHTML(ts, r.textToHTML)
+}
+
+func (r *Renderer) textToHTML(t comment.Text) safe.HTML {
+	switch t := t.(type) {
+	case comment.Plain:
+		// Don't auto-link URLs. The doc/comment package already does that.
+		return linkRFCs(string(t))
+	case comment.Italic:
+		return ExecuteToHTML(italicTemplate, t)
+	case *comment.Link:
+		return ExecuteToHTML(linkTemplate, link{"", t.URL, r.textsToHTML(t.Text)})
+	case *comment.DocLink:
+		url := r.docLinkURL(t)
+		return ExecuteToHTML(linkTemplate, link{"", url, r.textsToHTML(t.Text)})
+	default:
+		return badType(t)
+	}
+}
+
+func (r *Renderer) docLinkURL(dl *comment.DocLink) string {
+	var url string
+	if dl.ImportPath != "" {
+		url = "/" + dl.ImportPath
+		if r.packageURL != nil {
+			url = r.packageURL(dl.ImportPath)
+		}
+	}
+	id := dl.Name
+	if dl.Recv != "" {
+		id = dl.Recv + "." + id
+	}
+	if id != "" {
+		url += "#" + id
+	}
+	return url
+}
+
+// TODO: any -> *comment.Text | *comment.Block
+func concatHTML[T any](xs []T, toHTML func(T) safe.HTML) safe.HTML {
+	var hs []safe.HTML
+	for _, x := range xs {
+		hs = append(hs, toHTML(x))
+	}
+	return safe.HTMLConcat(hs...)
+}
+
+func badType(x interface{}) safe.HTML {
+	return safe.HTMLEscaped(fmt.Sprintf("bad type %T", x))
+}
+
+func headingID(h *comment.Heading) safe.Identifier {
+	s := textsToString(h.Text)
+	id := badAnchorRx.ReplaceAllString(s, "_")
+	return safe.IdentifierFromConstantPrefix("hdr", id)
+}
+
+func textsToString(ts []comment.Text) string {
+	var b strings.Builder
+	for _, t := range ts {
+		switch t := t.(type) {
+		case comment.Plain:
+			b.WriteString(string(t))
+		case comment.Italic:
+			b.WriteString(string(t))
+		case *comment.Link:
+			b.WriteString(textsToString(t.Text))
+		case *comment.DocLink:
+			b.WriteString(textsToString(t.Text))
+		default:
+			fmt.Fprintf(&b, "bad text type %T", t)
+		}
+	}
+	return b.String()
+}
+
+var rfcRegexp = regexp.MustCompile(rfcRx)
+
+// TODO: merge/replace Renderer.formatLineHTML.
+// TODO: make more efficient.
+func linkRFCs(s string) safe.HTML {
+	var hs []safe.HTML
+	for len(s) > 0 {
+		m0, m1 := len(s), len(s)
+		if m := rfcRegexp.FindStringIndex(s); m != nil {
+			m0, m1 = m[0], m[1]
+		}
+		if m0 > 0 {
+			hs = append(hs, safe.HTMLEscaped(s[:m0]))
+		}
+		if m1 > m0 {
+			word := s[m0:m1]
+			// Strip all characters except for letters, numbers, and '.' to
+			// obtain RFC fields.
+			rfcFields := strings.FieldsFunc(word, func(c rune) bool {
+				return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '.'
+			})
+			var url string
+			if len(rfcFields) >= 4 {
+				// RFC x Section y
+				url = fmt.Sprintf("https://rfc-editor.org/rfc/rfc%s.html#section-%s",
+					rfcFields[1], rfcFields[3])
+			} else if len(rfcFields) >= 2 {
+				url = fmt.Sprintf("https://rfc-editor.org/rfc/rfc%s.html", rfcFields[1])
+			}
+			if url != "" {
+				hs = append(hs, ExecuteToHTML(linkTemplate, link{"", url, word}))
+			}
+		}
+		s = s[m1:]
+	}
+	return safe.HTMLConcat(hs...)
+}
diff --git a/internal/godoc/dochtml/internal/render/linkify_test.go b/internal/godoc/dochtml/internal/render/linkify_test.go
index 87ded26..b7b06b3 100644
--- a/internal/godoc/dochtml/internal/render/linkify_test.go
+++ b/internal/godoc/dochtml/internal/render/linkify_test.go
@@ -19,7 +19,7 @@
 	"golang.org/x/pkgsite/internal/godoc/internal/doc"
 )
 
-func TestDocHTML(t *testing.T) {
+func TestFormatDocHTML(t *testing.T) {
 	linksDoc := `Documentation.
 
 The Go Project
@@ -106,8 +106,8 @@
 `,
 			want: `<p>Package tls partially implements TLS 1.2, as specified in <a href="https://rfc-editor.org/rfc/rfc5246.html">RFC 5246</a>, and TLS 1.3, as specified in <a href="https://rfc-editor.org/rfc/rfc8446.html">RFC 8446</a>.
 </p><p>In TLS 1.3, this type is called NamedGroup, but at this time this library only supports Elliptic Curve based groups. See <a href="https://rfc-editor.org/rfc/rfc8446.html#section-4.2.7">RFC 8446, Section 4.2.7</a>.
-</p><p>TLSUnique contains the tls-unique channel binding value (see RFC
-5929, section 3). The newline-separated RFC should be linked, but the words RFC and RFCs should not be.
+</p><p>TLSUnique contains the tls-unique channel binding value (see <a href="https://rfc-editor.org/rfc/rfc5929.html#section-3">RFC
+5929, section 3</a>). The newline-separated RFC should be linked, but the words RFC and RFCs should not be.
 </p>`,
 		},
 		{
@@ -123,14 +123,27 @@
 </p>`,
 		},
 		{
+			name: "list",
+			doc: `
+			Here is a list:
+				- a
+				- b`,
+			want: `<p>Here is a list:
+</p><ul>
+  <li>a</li>
+  <li>b</li>
+</ul>`,
+		},
+		{
 			name:         "Links section is not extracted",
 			extractLinks: []bool{false},
 			doc:          linksDoc,
 			want: `<p>Documentation.
 </p><h4 id="hdr-The_Go_Project">The Go Project <a class="Documentation-idLink" href="#hdr-The_Go_Project">¶</a></h4><p>Go is an open source project.
 </p><h4 id="hdr-Links">Links <a class="Documentation-idLink" href="#hdr-Links">¶</a></h4><p>- title1, url1
-</p><pre>-		title2 , url2
-</pre><h4 id="hdr-Header">Header <a class="Documentation-idLink" href="#hdr-Header">¶</a></h4><p>More doc.
+</p><ul>
+  <li>title2 , url2</li>
+</ul><h4 id="hdr-Header">Header <a class="Documentation-idLink" href="#hdr-Header">¶</a></h4><p>More doc.
 </p>`,
 		},
 		{
@@ -151,12 +164,6 @@
 			doc:  "For more detail, run ``go help test'' and ``go help testflag''",
 			want: `<p>For more detail, run “go help test” and “go help testflag”` + "\n" + "</p>",
 		},
-		{
-			name: "single quotes in pre block",
-			doc: `Join
-			    [].join() // returns ''`,
-			want: `<p>Join` + "\n" + `</p><pre>[].join() // returns &#39;&#39;` + "\n" + `</pre>`,
-		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			extractLinks := test.extractLinks
@@ -166,10 +173,10 @@
 			for _, el := range extractLinks {
 				t.Run(fmt.Sprintf("extractLinks=%t", el), func(t *testing.T) {
 					r := New(context.Background(), nil, pkgTime, nil)
-					got := r.declHTML(test.doc, nil, el).Doc
+					got := r.formatDocHTML(test.doc, el)
 					want := testconversions.MakeHTMLForTest(test.want)
 					if diff := cmp.Diff(want, got, cmp.AllowUnexported(safehtml.HTML{})); diff != "" {
-						t.Errorf("r.declHTML() mismatch (-want +got)\n%s", diff)
+						t.Errorf("doc mismatch (-want +got)\n%s", diff)
 					}
 					if diff := cmp.Diff(test.wantLinks, r.Links()); diff != "" {
 						t.Errorf("r.Links() mismatch (-want +got)\n%s", diff)
@@ -554,15 +561,15 @@
 More text.`
 
 	want := testconversions.MakeHTMLForTest(`<div role="navigation" aria-label="Table of Contents">
-    <ul class="Documentation-toc">
-      <li class="Documentation-tocItem">
-          <a href="#hdr-The_Go_Project">The Go Project</a>
-        </li>
-      <li class="Documentation-tocItem">
-          <a href="#hdr-Heading_2">Heading 2</a>
-        </li>
-      </ul>
-  </div>
+  <ul class="Documentation-toc">
+    <li class="Documentation-tocItem">
+        <a href="#hdr-The_Go_Project">The Go Project</a>
+      </li>
+    <li class="Documentation-tocItem">
+        <a href="#hdr-Heading_2">Heading 2</a>
+      </li>
+    </ul>
+</div>
 <p>Documentation.
 </p><h4 id="hdr-The_Go_Project">The Go Project <a class="Documentation-idLink" href="#hdr-The_Go_Project">¶</a></h4><p>Go is an open source project.
 </p><h4 id="hdr-Heading_2">Heading 2 <a class="Documentation-idLink" href="#hdr-Heading_2">¶</a></h4><p>More text.
diff --git a/internal/godoc/dochtml/internal/render/render.go b/internal/godoc/dochtml/internal/render/render.go
index 9ba2f53..1287522 100644
--- a/internal/godoc/dochtml/internal/render/render.go
+++ b/internal/godoc/dochtml/internal/render/render.go
@@ -12,8 +12,6 @@
 	"go/token"
 	"regexp"
 	"strings"
-	"unicode"
-	"unicode/utf8"
 
 	"github.com/google/safehtml"
 	"github.com/google/safehtml/template"
@@ -202,76 +200,6 @@
 	return r.codeHTML(ex)
 }
 
-// block is (*heading | *paragraph | *preformat).
-type block interface{}
-
-type (
-	lines   []string
-	heading struct {
-		title string
-	}
-	paragraph struct {
-		lines lines
-	}
-	preformat struct {
-		lines lines
-	}
-)
-
-// docToBlocks converts doc string into list of blocks.
-//
-// Heading block is a non-blank line, surrounded by blank lines
-// and the next non-blank line is not indented.
-//
-// Preformat block contains single line or consecutive lines which have indent greater than 0.
-//
-// Paragraph block is a default block type if a block does not fall into heading and preformat.
-func docToBlocks(doc string) []block {
-	docLines := unindent(strings.Split(strings.Trim(doc, "\n"), "\n"))
-
-	// Group the lines based on indentation and blank lines.
-	var groups [][]string
-	var lastGroup []string
-	var wasInCode bool
-	for _, line := range docLines {
-		hasIndent := indentLength(line) > 0
-		nowInCode := hasIndent || (wasInCode && line == "")
-		newGroup := wasInCode != nowInCode || (!nowInCode && line == "")
-		wasInCode = nowInCode
-		if newGroup && len(lastGroup) > 0 {
-			groups = append(groups, lastGroup)
-			lastGroup = nil
-		}
-		if line != "" || nowInCode {
-			lastGroup = append(lastGroup, line)
-		}
-	}
-	if len(lastGroup) > 0 {
-		groups = append(groups, lastGroup)
-	}
-
-	// Classify groups of lines as individual blocks.
-	var blks []block
-	var lastBlk block
-	for i, group := range groups {
-		willParagraph := i+1 < len(groups) && indentLength(groups[i+1][0]) == 0
-		for len(group) > 0 && group[len(group)-1] == "" {
-			group = group[:len(group)-1] // remove trailing empty lines
-		}
-		_, wasHeading := lastBlk.(*heading)
-		switch {
-		case indentLength(group[0]) > 0:
-			blks = append(blks, &preformat{unindent(group)})
-		case i != 0 && !wasHeading && len(group) == 1 && isHeading(group[0]) && willParagraph:
-			blks = append(blks, &heading{group[0]})
-		default:
-			blks = append(blks, &paragraph{group})
-		}
-		lastBlk = blks[len(blks)-1]
-	}
-	return blks
-}
-
 func indentLength(s string) int {
 	return len(s) - len(trimIndent(s))
 }
@@ -279,80 +207,3 @@
 func trimIndent(s string) string {
 	return strings.TrimLeft(s, " \t")
 }
-
-func commonPrefixLength(a, b string) (n int) {
-	for n < len(a) && n < len(b) && a[n] == b[n] {
-		n++
-	}
-	return n
-}
-
-func unindent(lines []string) []string {
-	if len(lines) > 0 {
-		npre := indentLength(lines[0])
-		for _, line := range lines {
-			if line != "" {
-				npre = commonPrefixLength(lines[0][:npre], line)
-			}
-		}
-		for i, line := range lines {
-			if line != "" {
-				lines[i] = line[npre:]
-			}
-		}
-	}
-	return lines
-}
-
-// isHeading returns bool of if it passes as a section heading or not.
-// This is the copy of the heading function from the standard library.
-// https://go.googlesource.com/go/+/refs/tags/go1.16/src/go/doc/comment.go#212
-func isHeading(line string) bool {
-	line = strings.TrimSpace(line)
-	if len(line) == 0 {
-		return false
-	}
-
-	// a heading must start with an uppercase letter
-	r, _ := utf8.DecodeRuneInString(line)
-	if !unicode.IsLetter(r) || !unicode.IsUpper(r) {
-		return false
-	}
-
-	// it must end in a letter or digit:
-	r, _ = utf8.DecodeLastRuneInString(line)
-	if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
-		return false
-	}
-
-	// exclude lines with illegal characters. we allow "(),"
-	if strings.ContainsAny(line, ";:!?+*/=[]{}_^°&§~%#@<\">\\") {
-		return false
-	}
-
-	// allow "'" for possessive "'s" only
-	for b := line; ; {
-		i := strings.IndexRune(b, '\'')
-		if i < 0 {
-			break
-		}
-		if i+1 >= len(b) || b[i+1] != 's' || (i+2 < len(b) && b[i+2] != ' ') {
-			return false // not followed by "s "
-		}
-		b = b[i+2:]
-	}
-
-	// allow "." when followed by non-space
-	for b := line; ; {
-		i := strings.IndexRune(b, '.')
-		if i < 0 {
-			break
-		}
-		if i+1 >= len(b) || b[i+1] == ' ' {
-			return false // not followed by non-space
-		}
-		b = b[i+1:]
-	}
-
-	return true
-}
diff --git a/internal/godoc/dochtml/internal/render/render_test.go b/internal/godoc/dochtml/internal/render/render_test.go
index 26fcda9..9b6b2b7 100644
--- a/internal/godoc/dochtml/internal/render/render_test.go
+++ b/internal/godoc/dochtml/internal/render/render_test.go
@@ -10,19 +10,12 @@
 	"go/token"
 	"os"
 	"path/filepath"
-	"reflect"
 	"strings"
-	"testing"
 
 	"golang.org/x/pkgsite/internal/godoc/internal/doc"
 )
 
-var (
-	pkgIO, _          = mustLoadPackage("io")
-	pkgOS, _          = mustLoadPackage("os")
-	pkgTime, fsetTime = mustLoadPackage("time")
-	pkgTar, _         = mustLoadPackage("archive/tar")
-)
+var pkgTime, fsetTime = mustLoadPackage("time")
 
 func mustLoadPackage(path string) (*doc.Package, *token.FileSet) {
 	// simpleImporter is used by ast.NewPackage.
@@ -50,182 +43,3 @@
 	astPkg, _ := ast.NewPackage(fset, pkgFiles, simpleImporter, nil)
 	return doc.New(astPkg, path, 0), fset
 }
-
-func TestDocToBlocks(t *testing.T) {
-	tests := []struct {
-		in   string
-		want []block
-	}{{
-		in:   `This is a sentence.`,
-		want: []block{&paragraph{lines{"This is a sentence."}}},
-	}, {
-		in:   `    This is a sentence.`,
-		want: []block{&paragraph{lines{"This is a sentence."}}},
-	}, {
-		in: `
-			Some code:
-				func main() {}
-			`,
-		want: []block{
-			&paragraph{lines{"Some code:"}},
-			&preformat{lines{"func main() {}"}},
-		},
-	}, {
-		in: `
-			The quick brown fox jumped over the lazy dog.
-			This is another sentence. La de dah!
-
-			This is a paragraph`,
-		want: []block{
-			&paragraph{lines{
-				"The quick brown fox jumped over the lazy dog.",
-				"This is another sentence. La de dah!",
-			}},
-			&paragraph{lines{"This is a paragraph"}},
-		},
-	}, {
-		in: `
-			The quick brown fox jumped over the lazy dog.
-			This is another sentence. La de dah!
-
-			This is a heading
-
-			This is a paragraph.`,
-		want: []block{
-			&paragraph{lines{
-				"The quick brown fox jumped over the lazy dog.",
-				"This is another sentence. La de dah!",
-			}},
-			&heading{"This is a heading"},
-			&paragraph{lines{"This is a paragraph."}},
-		},
-	}, {
-		in: `
-			This is not a heading
-
-			The quick brown fox jumped over the lazy dog.
-			This is another sentence. La de dah!
-
-			This is not a heading
-
-				func main() {}
-
-			This is not a heading`,
-		want: []block{
-			&paragraph{lines{"This is not a heading"}},
-			&paragraph{lines{
-				"The quick brown fox jumped over the lazy dog.",
-				"This is another sentence. La de dah!",
-			}},
-			&paragraph{lines{"This is not a heading"}},
-			&preformat{lines{"func main() {}"}},
-			&paragraph{lines{"This is not a heading"}},
-		},
-	}, {
-		in: `
-			Xattrs stores extended attributes as PAX records under the
-			"SCHILY.xattr." namespace.
-
-			The following are semantically equivalent:
-			    h.Xattrs[key] = value
-			    h.PAXRecords["SCHILY.xattr."+key] = value
-
-			When Writer.WriteHeader is called, the contents of Xattrs will take
-			precedence over those in PAXRecords.
-
-			Deprecated: Use PAXRecords instead.`,
-		want: []block{
-			&paragraph{lines{
-				"Xattrs stores extended attributes as PAX records under the",
-				`"SCHILY.xattr." namespace.`,
-			}},
-			&paragraph{lines{"The following are semantically equivalent:"}},
-			&preformat{lines{
-				`h.Xattrs[key] = value`,
-				`h.PAXRecords["SCHILY.xattr."+key] = value`,
-			}},
-			&paragraph{lines{
-				"When Writer.WriteHeader is called, the contents of Xattrs will take",
-				"precedence over those in PAXRecords.",
-			}},
-			&paragraph{lines{"Deprecated: Use PAXRecords instead."}},
-		},
-	}, {
-		in: `
-			Package testing provides support for automated testing of Go packages.
-
-			Benchmarks
-
-			Functions of the form
-				    func BenchmarkXxx(*testing.B)
-			are considered benchmarks, and are executed by the "go test" command when
-			its -bench flag is provided. Benchmarks are run sequentially.
-
-			For a description of the testing flags, see
-			https://golang.org/cmd/go/#hdr-Description_of_testing_flags.
-
-			A sample benchmark function looks like this:
-				    func BenchmarkHello(b *testing.B) {
-				    	for i := 0; i < b.N; i++ {
-				    		fmt.Sprintf("hello")
-				    	}
-				    }
-
-			The benchmark function must run the target code b.N times.
-			During benchmark execution, b.N is adjusted until the benchmark function lasts
-			long enough to be timed reliably. The output
-				    BenchmarkHello    10000000    282 ns/op
-			means that the loop ran 10000000 times at a speed of 282 ns per loop.`,
-		want: []block{
-			&paragraph{lines{"Package testing provides support for automated testing of Go packages."}},
-			&heading{"Benchmarks"},
-			&paragraph{lines{"Functions of the form"}},
-			&preformat{lines{"func BenchmarkXxx(*testing.B)"}},
-			&paragraph{lines{
-				`are considered benchmarks, and are executed by the "go test" command when`,
-				"its -bench flag is provided. Benchmarks are run sequentially.",
-			}},
-			&paragraph{lines{
-				"For a description of the testing flags, see",
-				"https://golang.org/cmd/go/#hdr-Description_of_testing_flags.",
-			}},
-			&paragraph{lines{"A sample benchmark function looks like this:"}},
-			&preformat{lines{
-				`func BenchmarkHello(b *testing.B) {`,
-				`	for i := 0; i < b.N; i++ {`,
-				`		fmt.Sprintf("hello")`,
-				`	}`,
-				`}`,
-			}},
-			&paragraph{lines{
-				"The benchmark function must run the target code b.N times.",
-				"During benchmark execution, b.N is adjusted until the benchmark function lasts",
-				"long enough to be timed reliably. The output",
-			}},
-			&preformat{lines{"BenchmarkHello    10000000    282 ns/op"}},
-			&paragraph{lines{"means that the loop ran 10000000 times at a speed of 282 ns per loop."}},
-		},
-	}, {
-		in: `
-			See https://golang.org/s/go14customimport for details.
-
-			Modules, module versions, and more
-
-			Modules are how Go manages dependencies.
-
-			A module is a collection of packages that are released, versioned, and distributed together. Modules may be downloaded directly from version control repositories or from module proxy servers.`,
-		want: []block{
-			&paragraph{lines{"See https://golang.org/s/go14customimport for details."}},
-			&heading{"Modules, module versions, and more"},
-			&paragraph{lines{"Modules are how Go manages dependencies."}},
-			&paragraph{lines{"A module is a collection of packages that are released, versioned, and distributed together. Modules may be downloaded directly from version control repositories or from module proxy servers."}},
-		},
-	}}
-
-	for i, tt := range tests {
-		got := docToBlocks(tt.in)
-		if !reflect.DeepEqual(got, tt.want) {
-			t.Errorf("test %d, docToBlocks:\ngot  %v\nwant %v", i, got, tt.want)
-		}
-	}
-}