internal/frontend: remove use of goldmark for readme rendering

delete the code that uses goldmark and clean up the variants of the
code that use the markdown parser

For golang/go#61399

Change-Id: I03e8c303086110278dd0f3994ba97729e0cbf7c1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/550039
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
TryBot-Bypass: Michael Matloob <matloob@golang.org>
diff --git a/go.mod b/go.mod
index 4c7736b..b486205 100644
--- a/go.mod
+++ b/go.mod
@@ -27,8 +27,6 @@
 	github.com/jba/templatecheck v0.6.0
 	github.com/lib/pq v1.10.9
 	github.com/russross/blackfriday/v2 v2.1.0
-	github.com/yuin/goldmark v1.6.0
-	github.com/yuin/goldmark-emoji v1.0.1
 	go.opencensus.io v0.24.0
 	golang.org/x/mod v0.14.0
 	golang.org/x/net v0.19.0
diff --git a/go.sum b/go.sum
index 533ec47..47b4431 100644
--- a/go.sum
+++ b/go.sum
@@ -1039,9 +1039,6 @@
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
-github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
-github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
 github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg=
 github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
diff --git a/internal/frontend/goldmark.go b/internal/frontend/goldmark.go
deleted file mode 100644
index 22f6e2c..0000000
--- a/internal/frontend/goldmark.go
+++ /dev/null
@@ -1,346 +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 frontend
-
-import (
-	"bytes"
-	"context"
-	"fmt"
-	"strings"
-
-	"github.com/yuin/goldmark/ast"
-	"github.com/yuin/goldmark/parser"
-	"github.com/yuin/goldmark/renderer"
-	"github.com/yuin/goldmark/renderer/html"
-	"github.com/yuin/goldmark/text"
-	"github.com/yuin/goldmark/util"
-	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/log"
-	"golang.org/x/pkgsite/internal/source"
-	"rsc.io/markdown"
-)
-
-// astTransformer is a default transformer of the goldmark tree. We pass in
-// readme information to use for the link transformations.
-type astTransformer struct {
-	info   *source.Info
-	readme *internal.Readme
-}
-
-// Transform transforms the given AST tree.
-func (g *astTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
-	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
-		if !entering {
-			return ast.WalkContinue, nil
-		}
-		switch v := n.(type) {
-		case *ast.Image:
-			if d := translateLink(string(v.Destination), g.info, true, g.readme); d != "" {
-				v.Destination = []byte(d)
-			}
-		case *ast.Link:
-			if d := translateLink(string(v.Destination), g.info, false, g.readme); d != "" {
-				v.Destination = []byte(d)
-			}
-		}
-		return ast.WalkContinue, nil
-	})
-}
-
-// htmlRenderer is a renderer.NodeRenderer implementation that renders
-// pkg.go.dev readme features.
-type htmlRenderer struct {
-	html.Config
-	info   *source.Info
-	readme *internal.Readme
-	// firstHeading and offset are used to calculate the first heading tag's level in a readme.
-	firstHeading bool
-	offset       int
-}
-
-// newHTMLRenderer creates a new HTMLRenderer for a readme.
-func newHTMLRenderer(info *source.Info, readme *internal.Readme, opts ...html.Option) renderer.NodeRenderer {
-	r := &htmlRenderer{
-		info:         info,
-		readme:       readme,
-		Config:       html.NewConfig(),
-		firstHeading: true,
-		offset:       0,
-	}
-	for _, opt := range opts {
-		opt.SetHTMLOption(&r.Config)
-	}
-	return r
-}
-
-// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
-func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
-	reg.Register(ast.KindHeading, r.renderHeading)
-	reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
-	reg.Register(ast.KindRawHTML, r.renderRawHTML)
-}
-
-func (r *htmlRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*ast.Heading)
-	if r.firstHeading {
-		// The offset ensures the first heading is always an <h3>.
-		r.offset = 3 - n.Level
-		r.firstHeading = false
-	}
-	newLevel := n.Level + r.offset
-	if entering {
-		// TODO(matloob): Do we want the div and h elements to have analogous classes?
-		// Currently we're using newLevel for the div's class but n.Level for the h element's
-		// class.
-		if newLevel > 6 {
-			_, _ = w.WriteString(fmt.Sprintf(`<div class="h%d" role="heading" aria-level="%d"`, newLevel, n.Level))
-		} else {
-			_, _ = w.WriteString(fmt.Sprintf(`<h%d class="h%d"`, newLevel, n.Level))
-		}
-		if n.Attributes() != nil {
-			html.RenderAttributes(w, node, html.HeadingAttributeFilter)
-		}
-		_ = w.WriteByte('>')
-	} else {
-		if newLevel > 6 {
-			_, _ = w.WriteString("</div>\n")
-		} else {
-			_, _ = w.WriteString(fmt.Sprintf("</h%d>\n", newLevel))
-		}
-	}
-	return ast.WalkContinue, nil
-}
-
-// renderHTMLBlock is copied directly from the goldmark source code and
-// modified to call translateHTML in every block
-func (r *htmlRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*ast.HTMLBlock)
-	if entering {
-		if r.Unsafe {
-			l := n.Lines().Len()
-			for i := 0; i < l; i++ {
-				line := n.Lines().At(i)
-				d, err := translateHTML(line.Value(source), r.info, r.readme)
-				if err != nil {
-					return ast.WalkStop, err
-				}
-				_, _ = w.Write(d)
-			}
-		} else {
-			_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
-		}
-	} else {
-		if n.HasClosure() {
-			if r.Unsafe {
-				closure := n.ClosureLine
-				_, _ = w.Write(closure.Value(source))
-			} else {
-				_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
-			}
-		}
-	}
-	return ast.WalkContinue, nil
-}
-
-func (r *htmlRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	if !entering {
-		return ast.WalkSkipChildren, nil
-	}
-	if r.Unsafe {
-		n := node.(*ast.RawHTML)
-		for i := 0; i < n.Segments.Len(); i++ {
-			segment := n.Segments.At(i)
-			d, err := translateHTML(segment.Value(source), r.info, r.readme)
-			if err != nil {
-				return ast.WalkStop, err
-			}
-			_, _ = w.Write(d)
-		}
-		return ast.WalkSkipChildren, nil
-	}
-	_, _ = w.WriteString("<!-- raw HTML omitted -->")
-	return ast.WalkSkipChildren, nil
-}
-
-// ids is a collection of element ids in document.
-type ids struct {
-	values map[string]bool
-}
-
-// newIDs creates a collection of element ids in a document.
-func newIDs() parser.IDs {
-	return &ids{
-		values: map[string]bool{},
-	}
-}
-
-// Generate turns heading content from a markdown document into a heading id.
-// First HTML markup and markdown images are stripped then ASCII letters
-// and numbers are used to generate the final result. Finally, all heading ids
-// are prefixed with "readme-" to avoid name collisions with other ids on the
-// unit page. Duplicated heading ids are given an incremental suffix. See
-// readme_test.go for examples.
-func (s *ids) Generate(value []byte, kind ast.NodeKind) []byte {
-	var defaultID string
-	if kind == ast.KindHeading {
-		defaultID = "heading"
-	} else {
-		defaultID = "id"
-	}
-
-	parser := &markdown.Parser{}
-	doc := parser.Parse("# " + string(value))
-	return []byte(s.generateID(doc, defaultID))
-}
-
-func (s *ids) generateID(block markdown.Block, defaultID string) string {
-	var buf bytes.Buffer
-	walkBlocks([]markdown.Block{block}, func(b markdown.Block) error {
-		if t, ok := b.(*markdown.Text); ok {
-			for _, inl := range t.Inline {
-				inl.PrintText(&buf)
-			}
-		}
-		return nil
-	})
-	f := func(c rune) bool {
-		return !('a' <= c && c <= 'z') && !('A' <= c && c <= 'Z') && !('0' <= c && c <= '9')
-	}
-	str := strings.Join(strings.FieldsFunc(buf.String(), f), "-")
-	str = strings.ToLower(str)
-	if len(str) == 0 {
-		str = defaultID
-	}
-	key := str
-	for i := 1; ; i++ {
-		if _, ok := s.values[key]; !ok {
-			s.values[key] = true
-			break
-		}
-		key = fmt.Sprintf("%s-%d", str, i)
-	}
-	return "readme-" + key
-}
-
-// Put implements Put from the goldmark parser IDs interface.
-func (s *ids) Put(value []byte) {
-	s.values[string(value)] = true
-}
-
-type extractLinks struct {
-	ctx            context.Context
-	inLinksHeading bool
-	links          []link
-}
-
-// The name of the heading from which we extract links.
-const linkHeadingText = "Links"
-
-var linkHeadingBytes = []byte(linkHeadingText) // for faster comparison to node contents
-
-// Transform extracts links from the "Links" section of a README.
-func (e *extractLinks) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
-	err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
-		if !entering {
-			return ast.WalkContinue, nil
-		}
-		switch n := n.(type) {
-
-		case *ast.Heading:
-			// We are in the links heading from the point we see a heading with
-			// linkHeadingText until the point we see the next heading.
-			if e.inLinksHeading {
-				return ast.WalkStop, nil
-			}
-			if bytes.Equal(n.Text(reader.Source()), linkHeadingBytes) {
-				e.inLinksHeading = true
-			}
-
-		case *ast.ListItem:
-			// When in the links heading, extract links from list items.
-			if !e.inLinksHeading {
-				return ast.WalkSkipChildren, nil
-			}
-			// We expect the pattern: ListItem -> TextBlock -> Link, with no
-			// other children.
-			if tb, ok := n.FirstChild().(*ast.TextBlock); ok {
-				if l, ok := tb.FirstChild().(*ast.Link); ok && l.NextSibling() == nil {
-					// Record the link.
-					e.links = append(e.links, link{
-						Href: string(l.Destination),
-						Body: string(l.Text(reader.Source())),
-					})
-				}
-			}
-			return ast.WalkSkipChildren, nil
-		}
-
-		return ast.WalkContinue, nil
-	})
-	if err != nil {
-		log.Errorf(e.ctx, "extractLinks.Transform: %v", err)
-	}
-}
-
-type extractTOC struct {
-	ctx         context.Context
-	Headings    []*Heading
-	removeTitle bool // omit title from TOC
-}
-
-// Transform collects the headings from a readme into an outline
-// of the document. It nests the headings based on the h-level hierarchy.
-// See tests for heading levels in TestReadme for behavior.
-func (e *extractTOC) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
-	var headings []*Heading
-	err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
-		if n.Kind() == ast.KindHeading && entering {
-			heading := n.(*ast.Heading)
-			section := &Heading{
-				Level: heading.Level,
-				Text:  string(n.Text(reader.Source())),
-			}
-			if id, ok := heading.AttributeString("id"); ok {
-				section.ID = string(id.([]byte))
-			}
-			headings = append(headings, section)
-			return ast.WalkSkipChildren, nil
-		}
-		return ast.WalkContinue, nil
-	})
-	if err != nil {
-		log.Errorf(e.ctx, "extractTOC.Transform: %v", err)
-	}
-
-	// We nest the headings by walking through the list we extracted and
-	// establishing parent child relationships based on heading levels.
-	var nested []*Heading
-	for i, h := range headings {
-		if i == 0 {
-			nested = append(nested, h)
-			continue
-		}
-		parent := headings[i-1]
-		for parent != nil && parent.Level >= h.Level {
-			parent = parent.parent
-		}
-		if parent == nil {
-			nested = append(nested, h)
-		} else {
-			h.parent = parent
-			parent.Children = append(parent.Children, h)
-		}
-	}
-	if e.removeTitle {
-		// If there is only one top tevel heading with 1 or more children we
-		// assume it is the title of the document and remove it from the TOC.
-		if len(nested) == 1 && len(nested[0].Children) > 0 {
-			nested = nested[0].Children
-		}
-	}
-	e.Headings = nested
-}
diff --git a/internal/frontend/main.go b/internal/frontend/main.go
index ae8f03d..32a4dc8 100644
--- a/internal/frontend/main.go
+++ b/internal/frontend/main.go
@@ -259,7 +259,7 @@
 	if !u.IsRedistributable {
 		return &Readme{}, nil
 	}
-	return ProcessReadmeMarkdown(ctx, u)
+	return ProcessReadme(ctx, u)
 }
 
 const missingDocReplacement = `<p>Documentation is missing.</p>`
diff --git a/internal/frontend/markdown.go b/internal/frontend/markdown.go
index fcb5d98..9427447 100644
--- a/internal/frontend/markdown.go
+++ b/internal/frontend/markdown.go
@@ -19,7 +19,7 @@
 	"rsc.io/markdown"
 )
 
-// ProcessReadmeMarkdown processes the README of unit u, if it has one.
+// ProcessReadme processes the README of unit u, if it has one.
 // Processing includes rendering and sanitizing the HTML or Markdown,
 // and extracting headings and links.
 //
@@ -31,12 +31,12 @@
 // The extracted links are for display outside of the readme contents.
 //
 // This function is exported for use by external tools.
-func ProcessReadmeMarkdown(ctx context.Context, u *internal.Unit) (_ *Readme, err error) {
-	defer derrors.WrapAndReport(&err, "ProcessReadmeMarkdown(%q, %q, %q)", u.Path, u.ModulePath, u.Version)
-	return processReadmeMarkdown(ctx, u.Readme, u.SourceInfo)
+func ProcessReadme(ctx context.Context, u *internal.Unit) (_ *Readme, err error) {
+	defer derrors.WrapAndReport(&err, "ProcessReadme(%q, %q, %q)", u.Path, u.ModulePath, u.Version)
+	return processReadme(ctx, u.Readme, u.SourceInfo)
 }
 
-func processReadmeMarkdown(ctx context.Context, readme *internal.Readme, info *source.Info) (frontendReadme *Readme, err error) {
+func processReadme(ctx context.Context, readme *internal.Readme, info *source.Info) (frontendReadme *Readme, err error) {
 	if readme == nil || readme.Contents == "" {
 		return &Readme{}, nil
 	}
@@ -160,6 +160,15 @@
 	return nil
 }
 
+type extractTOC struct {
+	ctx         context.Context
+	Headings    []*Heading
+	removeTitle bool // omit title from TOC
+}
+
+// extract collects the headings from a readme into an outline
+// of the document. It nests the headings based on the h-level hierarchy.
+// See tests for heading levels in TestReadme for behavior.
 func (e *extractTOC) extract(doc *markdown.Document) {
 	var headings []*Heading
 	err := walkBlocks(doc.Blocks, func(b markdown.Block) error {
@@ -211,6 +220,18 @@
 	e.Headings = nested
 }
 
+type extractLinks struct {
+	ctx            context.Context
+	inLinksHeading bool
+	links          []link
+}
+
+// The name of the heading from which we extract links.
+const linkHeadingText = "Links"
+
+var linkHeadingBytes = []byte(linkHeadingText) // for faster comparison to node contents
+
+// extract extracts links from the "Links" section of a README.
 func (e *extractLinks) extract(doc *markdown.Document) {
 	var seenLinksHeading bool
 	err := walkBlocks(doc.Blocks, func(b markdown.Block) error {
@@ -371,16 +392,41 @@
 )
 
 // rewriteHeadingIDs generates ids based on the body of the heading.
-// The original code uses the raw markdown as the input to the ids.Generate
-// function, but we don't have the raw markdown anymore, so we use the
-// text instead.
+// The ASCII letters and numbers from the text are used to generate
+// each of the ids. Finally, all heading ids
+// are prefixed with "readme-" to avoid name collisions with other ids on the
+// unit page. Duplicated heading ids are given an incremental suffix. See
+// readme_test.go for examples.
 func rewriteHeadingIDs(doc *markdown.Document) {
-	ids := &ids{
-		values: map[string]bool{},
+	ids := map[string]bool{}
+
+	generateID := func(heading *markdown.Heading) string {
+		var buf bytes.Buffer
+		for _, inl := range heading.Text.Inline {
+			inl.PrintText(&buf)
+		}
+		f := func(c rune) bool {
+			return !('a' <= c && c <= 'z') && !('A' <= c && c <= 'Z') && !('0' <= c && c <= '9')
+		}
+		str := strings.Join(strings.FieldsFunc(buf.String(), f), "-")
+		str = strings.ToLower(str)
+		if len(str) == 0 {
+			str = "heading"
+		}
+		key := str
+		for i := 1; ; i++ {
+			if _, ok := ids[key]; !ok {
+				ids[key] = true
+				break
+			}
+			key = fmt.Sprintf("%s-%d", str, i)
+		}
+		return "readme-" + key
 	}
+
 	walkBlocks(doc.Blocks, func(b markdown.Block) error {
 		if heading, ok := b.(*markdown.Heading); ok {
-			id := ids.generateID(heading, "heading")
+			id := generateID(heading)
 			heading.ID = string(id)
 		}
 		return nil
diff --git a/internal/frontend/readme.go b/internal/frontend/readme.go
index 6709855..7cd159f 100644
--- a/internal/frontend/readme.go
+++ b/internal/frontend/readme.go
@@ -6,24 +6,10 @@
 
 import (
 	"bytes"
-	"context"
 
 	"github.com/google/safehtml"
-	"github.com/google/safehtml/template"
 	"github.com/google/safehtml/uncheckedconversions"
-	"github.com/yuin/goldmark"
-	emoji "github.com/yuin/goldmark-emoji"
-	"github.com/yuin/goldmark/extension"
-	"github.com/yuin/goldmark/parser"
-	"github.com/yuin/goldmark/renderer"
-	goldmarkHtml "github.com/yuin/goldmark/renderer/html"
-	gmtext "github.com/yuin/goldmark/text"
-	"github.com/yuin/goldmark/util"
-	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/sanitizer"
-	"golang.org/x/pkgsite/internal/source"
 )
 
 // Heading holds data about a heading and nested headings within a readme.
@@ -52,111 +38,6 @@
 	Links   []link        // links from the "Links" section
 }
 
-// ProcessReadme processes the README of unit u, if it has one.
-// Processing includes rendering and sanitizing the HTML or Markdown,
-// and extracting headings and links.
-//
-// Headings are prefixed with "readme-" and heading levels are adjusted to start
-// at h3 in order to nest them properly within the rest of the page. The
-// readme's original styling is preserved in the html by giving headings a css
-// class styled identical to their original heading level.
-//
-// The extracted links are for display outside of the readme contents.
-//
-// This function is exported for use by external tools.
-func ProcessReadme(ctx context.Context, u *internal.Unit) (_ *Readme, err error) {
-	defer derrors.WrapAndReport(&err, "ProcessReadme(%q, %q, %q)", u.Path, u.ModulePath, u.Version)
-	return processReadme(ctx, u.Readme, u.SourceInfo)
-}
-
-func processReadme(ctx context.Context, readme *internal.Readme, sourceInfo *source.Info) (frontendReadme *Readme, err error) {
-	if readme == nil || readme.Contents == "" {
-		return &Readme{}, nil
-	}
-	if !isMarkdown(readme.Filepath) {
-		t := template.Must(template.New("").Parse(`<pre class="readme">{{.}}</pre>`))
-		h, err := t.ExecuteToHTML(readme.Contents)
-		if err != nil {
-			return nil, err
-		}
-		return &Readme{HTML: h}, nil
-	}
-
-	// Sets priority value so that we always use our custom transformer
-	// instead of the default ones. The default values are in:
-	// https://github.com/yuin/goldmark/blob/7b90f04af43131db79ec320be0bd4744079b346f/parser/parser.go#L567
-	const astTransformerPriority = 10000
-	el := &extractLinks{ctx: ctx}
-	et := &extractTOC{ctx: ctx, removeTitle: true}
-	gdMarkdown := goldmark.New(
-		goldmark.WithParserOptions(
-			// WithHeadingAttribute allows us to include other attributes in
-			// heading tags. This is useful for our aria-level implementation of
-			// increasing heading rankings.
-			parser.WithHeadingAttribute(),
-			// Generates an id in every heading tag. This is used in github in
-			// order to generate a link with a hash that a user would scroll to
-			// <h1 id="goldmark">goldmark</h1> => github.com/yuin/goldmark#goldmark
-			parser.WithAutoHeadingID(),
-			// Include custom ASTTransformer using the readme and module info to
-			// use translateRelativeLink and translateHTML to modify the AST
-			// before it is rendered.
-			parser.WithASTTransformers(
-				util.Prioritized(&astTransformer{
-					info:   sourceInfo,
-					readme: readme,
-				}, astTransformerPriority),
-				// Extract links after we have transformed the URLs.
-				util.Prioritized(el, astTransformerPriority+1),
-				util.Prioritized(et, astTransformerPriority+1),
-			),
-		),
-		// These extensions lets users write HTML code in the README. This is
-		// fine since we process the contents using the sanitizer after.
-		goldmark.WithRendererOptions(goldmarkHtml.WithUnsafe(), goldmarkHtml.WithXHTML()),
-		goldmark.WithExtensions(
-			extension.GFM, // Support Github Flavored Markdown.
-			emoji.Emoji,   // Support Github markdown emoji markup.
-		),
-	)
-	gdMarkdown.Renderer().AddOptions(
-		renderer.WithNodeRenderers(
-			util.Prioritized(newHTMLRenderer(sourceInfo, readme), 100),
-		),
-	)
-	contents := []byte(readme.Contents)
-	gdParser := gdMarkdown.Parser()
-	reader := gmtext.NewReader(contents)
-	pctx := parser.NewContext(parser.WithIDs(newIDs()))
-	doc := gdParser.Parse(reader, parser.WithContext(pctx))
-	gdRenderer := gdMarkdown.Renderer()
-
-	var b bytes.Buffer
-	defer func() {
-		// It's possible for gdRenderer.Render to panic. For example,
-		// https://pkg.go.dev/github.com/jinghzhu/k8scrd/pkg/crd/jinghzhu/v1
-		// results in a panic because gdRenderer.Render tries to index a slice
-		// out of bounds.
-		//
-		// In case of a panic from gdRenderer.Render, treat this as a normal
-		// error from that function.
-		if p := recover(); p != nil {
-			log.Debugf(ctx, "gdRenderer.Render: %v", p)
-			frontendReadme = &Readme{}
-			err = nil
-		}
-	}()
-	if err := gdRenderer.Render(&b, contents, doc); err != nil {
-		log.Debugf(ctx, "gdRenderer.Render: %v", err)
-		return &Readme{}, nil
-	}
-	return &Readme{
-		HTML:    sanitizeHTML(&b),
-		Outline: et.Headings,
-		Links:   el.links,
-	}, nil
-}
-
 // sanitizeHTML sanitizes HTML from a bytes.Buffer so that it is safe.
 func sanitizeHTML(b *bytes.Buffer) safehtml.HTML {
 	s := string(sanitizer.SanitizeBytes(b.Bytes()))
diff --git a/internal/frontend/readme_test.go b/internal/frontend/readme_test.go
index 91b4b3e..60591ed 100644
--- a/internal/frontend/readme_test.go
+++ b/internal/frontend/readme_test.go
@@ -161,9 +161,9 @@
 				Filepath: sample.ReadmeFilePath,
 				Contents: "# :zap: Zap \n\n :joy:",
 			},
-			wantHTML: "<h3 class=\"h1\" id=\"readme-zap-zap\">⚡ Zap</h3>\n<p>😂</p>",
+			wantHTML: "<h3 class=\"h1\" id=\"readme-zap\">⚡ Zap</h3>\n<p>😂</p>",
 			wantOutline: []*Heading{
-				{Level: 1, Text: " Zap", ID: "readme-zap-zap"},
+				{Level: 1, Text: "⚡ Zap", ID: "readme-zap"},
 			},
 		},
 		{
@@ -379,7 +379,7 @@
 				Contents: `<p><img src="./foo.png"></p><p><img src="../bar.png"</p>` + "\n",
 			},
 			wantHTML: `<p><img src="https://github.com/valid/module_name/raw/v1.0.0/dir/sub/foo.png"/></p>` +
-				`<p><img src="https://github.com/valid/module_name/raw/v1.0.0/dir/bar.png"/>` + "\n</p>",
+				`<p><img src="https://github.com/valid/module_name/raw/v1.0.0/dir/bar.png"/></p>`,
 			wantOutline: nil,
 		},
 		{
@@ -481,41 +481,20 @@
 		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			processReadmes := map[string]func(ctx context.Context, u *internal.Unit) (frontendReadme *Readme, err error){
-				"goldmark": ProcessReadme,
-				"markdown": ProcessReadmeMarkdown,
+			test.unit.Readme = test.readme
+			readme, err := ProcessReadme(ctx, test.unit)
+			if err != nil {
+				t.Fatal(err)
 			}
-			for processFuncName, processFunc := range processReadmes {
-				t.Run(processFuncName, func(t *testing.T) {
-					wantHTML := test.wantHTML
-					if processFuncName == "markdown" {
-						if test.name == "Github markdown emoji markup is properly rendered" {
-							t.Skip("github markdown emoji is not yet supported with the markdown package")
-						}
-						if test.name == "body has more than one child" {
-							// The markdown package treats the newline differently when there's an incomplete tag.
-							wantHTML = `<p><img src="https://github.com/valid/module_name/raw/v1.0.0/dir/sub/foo.png"/></p>` +
-								`<p><img src="https://github.com/valid/module_name/raw/v1.0.0/dir/bar.png"/></p>`
-						}
-					}
-
-					test.unit.Readme = test.readme
-					readme, err := processFunc(ctx, test.unit)
-					if err != nil {
-						t.Fatal(err)
-					}
-					gotHTML := strings.TrimSpace(readme.HTML.String())
-					if diff := cmp.Diff(wantHTML, gotHTML); diff != "" {
-						t.Errorf("Readme(%v) html mismatch (-want +got):\n%s", test.unit.UnitMeta, diff)
-					}
-					if diff := cmp.Diff(test.wantOutline, readme.Outline, cmp.Options{
-						cmpopts.IgnoreUnexported(Heading{}),
-					}); diff != "" {
-						t.Errorf("Readme(%v) outline mismatch (-want +got):\n%s", test.unit.UnitMeta, diff)
-					}
-				})
+			gotHTML := strings.TrimSpace(readme.HTML.String())
+			if diff := cmp.Diff(test.wantHTML, gotHTML); diff != "" {
+				t.Errorf("Readme(%v) html mismatch (-want +got):\n%s", test.unit.UnitMeta, diff)
 			}
-
+			if diff := cmp.Diff(test.wantOutline, readme.Outline, cmp.Options{
+				cmpopts.IgnoreUnexported(Heading{}),
+			}); diff != "" {
+				t.Errorf("Readme(%v) outline mismatch (-want +got):\n%s", test.unit.UnitMeta, diff)
+			}
 		})
 	}
 }
@@ -617,24 +596,17 @@
 		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			processReadmes := map[string]func(ctx context.Context, u *internal.Unit) (frontendReadme *Readme, err error){
-				"goldmark": ProcessReadme,
-				"markdown": ProcessReadmeMarkdown,
+
+			unit.Readme = &internal.Readme{
+				Filepath: "README.md",
+				Contents: unindent(test.contents),
 			}
-			for name, processFunc := range processReadmes {
-				t.Run(name, func(t *testing.T) {
-					unit.Readme = &internal.Readme{
-						Filepath: "README.md",
-						Contents: unindent(test.contents),
-					}
-					got, err := processFunc(ctx, unit)
-					if err != nil {
-						t.Fatal(err)
-					}
-					if diff := cmp.Diff(test.want, got.Links); diff != "" {
-						t.Errorf("mismatch (-want +got):\n%s", diff)
-					}
-				})
+			got, err := ProcessReadme(ctx, unit)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if diff := cmp.Diff(test.want, got.Links); diff != "" {
+				t.Errorf("mismatch (-want +got):\n%s", diff)
 			}
 		})
 	}