move code out of linkify_comment.go

Move all code into linkify.go.

Code motion, except for shortened package prefix.

Change-Id: Id2683386bd1c959888ed9e7089919ad61f0b8768
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/413315
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/internal/godoc/dochtml/internal/render/linkify.go b/internal/godoc/dochtml/internal/render/linkify.go
index 542fd50..93fb3d2 100644
--- a/internal/godoc/dochtml/internal/render/linkify.go
+++ b/internal/godoc/dochtml/internal/render/linkify.go
@@ -9,6 +9,7 @@
 	"errors"
 	"fmt"
 	"go/ast"
+	"go/doc/comment"
 	"go/format"
 	"go/printer"
 	"go/scanner"
@@ -18,7 +19,7 @@
 	"strings"
 	"unicode"
 
-	"github.com/google/safehtml"
+	safe "github.com/google/safehtml"
 	"github.com/google/safehtml/legacyconversions"
 	"github.com/google/safehtml/template"
 	"golang.org/x/pkgsite/internal/godoc/internal/doc"
@@ -56,15 +57,135 @@
 	badAnchorRx = regexp.MustCompile(`[^a-zA-Z0-9]`)
 )
 
-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)
+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)
 	}
-	if decl != nil {
-		idr := &identifierResolver{r.pids, newDeclIDs(decl), r.packageURL}
-		out.Decl = r.formatDeclHTML(decl, idr)
+	var headings []heading
+	for _, b := range doc.Content {
+		if h, ok := b.(*comment.Heading); ok {
+			headings = append(headings, r.newHeading(h))
+		}
 	}
-	return out
+	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
 }
 
 // parseLinks extracts links from lines.
@@ -95,6 +216,176 @@
 	}
 }
 
+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...)
+}
+
+func (r *Renderer) declHTML(doc string, decl ast.Decl, extractLinks bool) (out struct{ Doc, Decl safe.HTML }) {
+	if doc != "" {
+		out.Doc = r.formatDocHTML(doc, extractLinks)
+	}
+	if decl != nil {
+		idr := &identifierResolver{r.pids, newDeclIDs(decl), r.packageURL}
+		out.Decl = r.formatDeclHTML(decl, idr)
+	}
+	return out
+}
+
 func (r *Renderer) codeString(ex *doc.Example) (string, error) {
 	if ex == nil || ex.Code == nil {
 		return "", errors.New("please include an example with code")
@@ -118,7 +409,7 @@
 	return buf.String(), nil
 }
 
-func (r *Renderer) codeHTML(ex *doc.Example) safehtml.HTML {
+func (r *Renderer) codeHTML(ex *doc.Example) safe.HTML {
 	codeStr, err := r.codeString(ex)
 	if err != nil {
 		log.Errorf(r.ctx, "Error converting *doc.Example into string: %v", err)
@@ -132,7 +423,7 @@
 	Comment bool
 }
 
-func codeHTML(src string, codeTmpl *template.Template) safehtml.HTML {
+func codeHTML(src string, codeTmpl *template.Template) safe.HTML {
 	var els []codeElement
 	// If code is an *ast.BlockStmt, then trim the braces.
 	var indent string
@@ -191,8 +482,8 @@
 // formatLineHTML formats the line as HTML-annotated text.
 // URLs and Go identifiers are linked to corresponding declarations.
 // If pre is true no conversion of doubled ` and ' to “ and ” is performed.
-func (r *Renderer) formatLineHTML(line string, pre bool) safehtml.HTML {
-	var htmls []safehtml.HTML
+func (r *Renderer) formatLineHTML(line string, pre bool) safe.HTML {
+	var htmls []safe.HTML
 	addLink := func(href, text string) {
 		htmls = append(htmls, ExecuteToHTML(LinkTemplate, Link{Href: href, Text: text}))
 	}
@@ -207,7 +498,7 @@
 		}
 		if m0 > 0 {
 			nonWord := line[:m0]
-			htmls = append(htmls, safehtml.HTMLEscaped(nonWord))
+			htmls = append(htmls, safe.HTMLEscaped(nonWord))
 		}
 		if m1 > m0 {
 			word := line[m0:m1]
@@ -251,25 +542,25 @@
 					addLink(fmt.Sprintf("https://rfc-editor.org/rfc/rfc%s.html", rfcFields[1]), word)
 				}
 			default:
-				htmls = append(htmls, safehtml.HTMLEscaped(word))
+				htmls = append(htmls, safe.HTMLEscaped(word))
 			}
 		}
 		line = line[m1:]
 	}
-	return safehtml.HTMLConcat(htmls...)
+	return safe.HTMLConcat(htmls...)
 }
 
-func ExecuteToHTML(tmpl *template.Template, data interface{}) safehtml.HTML {
+func ExecuteToHTML(tmpl *template.Template, data interface{}) safe.HTML {
 	h, err := tmpl.ExecuteToHTML(data)
 	if err != nil {
-		return safehtml.HTMLEscaped("[" + err.Error() + "]")
+		return safe.HTMLEscaped("[" + err.Error() + "]")
 	}
 	return h
 }
 
 // formatDeclHTML formats the decl as HTML-annotated source code for the
 // provided decl. Type identifiers are linked to corresponding declarations.
-func (r *Renderer) formatDeclHTML(decl ast.Decl, idr *identifierResolver) safehtml.HTML {
+func (r *Renderer) formatDeclHTML(decl ast.Decl, idr *identifierResolver) safe.HTML {
 	// Generate all anchor points and links for the given decl.
 	anchorPointsMap := generateAnchorPoints(decl)
 	anchorLinksMap := generateAnchorLinks(idr, decl)
@@ -309,7 +600,7 @@
 	numLines := bytes.Count(src, []byte("\n")) + 1
 	anchorLines := make([][]idKind, numLines)
 	lineTypes := make([]lineType, numLines)
-	htmlLines := make([][]safehtml.HTML, numLines)
+	htmlLines := make([][]safe.HTML, numLines)
 
 	// Scan through the source code, appropriately annotating it with HTML spans
 	// for comments, and HTML links and anchors for relevant identifiers.
@@ -331,7 +622,7 @@
 			if n < 0 { // possible at EOF
 				n = 0
 			}
-			htmlLines[n] = append(htmlLines[n], safehtml.HTMLEscaped(ln))
+			htmlLines[n] = append(htmlLines[n], safe.HTMLEscaped(ln))
 		}
 
 		lastOffset = offset
@@ -373,7 +664,7 @@
 	}
 
 	// Emit anchor IDs and data-kind attributes for each relevant line.
-	var htmls []safehtml.HTML
+	var htmls []safe.HTML
 	for line, iks := range anchorLines {
 		inAnchor := false
 		for _, ik := range iks {
@@ -395,7 +686,7 @@
 			htmls = append(htmls, template.MustParseAndExecuteToHTML("</span>"))
 		}
 	}
-	return safehtml.HTMLConcat(htmls...)
+	return safe.HTMLConcat(htmls...)
 }
 
 var anchorTemplate = template.Must(template.New("anchor").Parse(`<span id="{{.ID}}" data-kind="{{.Kind}}">`))
@@ -464,13 +755,13 @@
 // An idKind holds an anchor ID and the kind of the identifier being anchored.
 // The valid kinds are: "constant", "variable", "type", "function", "method" and "field".
 type idKind struct {
-	ID   safehtml.Identifier
+	ID   safe.Identifier
 	Kind string
 }
 
 // SafeGoID constructs a safe identifier from a Go symbol or dotted concatenation of symbols
 // (e.g. "Time.Equal").
-func SafeGoID(s string) safehtml.Identifier {
+func SafeGoID(s string) safe.Identifier {
 	ValidateGoDottedExpr(s)
 	return legacyconversions.RiskilyAssumeIdentifier(s)
 }
diff --git a/internal/godoc/dochtml/internal/render/linkify_comment.go b/internal/godoc/dochtml/internal/render/linkify_comment.go
deleted file mode 100644
index 057ccec..0000000
--- a/internal/godoc/dochtml/internal/render/linkify_comment.go
+++ /dev/null
@@ -1,306 +0,0 @@
-// 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...)
-}