Repository: gomarkdown/markdown

Issue:
Enhancement request: markdown renderer link methods
I'm planning to extend the gomarkdown markdown renderer to render links at the bottom of the page, rather than inline.  The best way that I've found to do that (I'm open to other suggestions!) is to override the renderer for links like this:

```
func (r *CustomMDRenderer) fmtLink(w io.Writer, node ast.Node, entering bool) ast.WalkStatus {
	if entering {
		w.Write([]byte("["))
	} else {
		w.Write([]byte("]"))
		linkNode, ok := node.(*ast.Link)
		if !ok {
			return ast.GoToNext
		}
		child := ast.GetFirstChild(node)
		if child == nil {
			return ast.GoToNext
		}
		if reflect.TypeOf(child) != reflect.TypeOf(&ast.Text{}) {
			return ast.GoToNext
		}
		textChild, ok := child.(*ast.Text)
		if !ok {
			return ast.GoToNext
		}
		linkStr := fmt.Sprintf("[%s]: %s", textChild.Leaf.Literal, linkNode.Destination)
		r.linkcache[linkStr] = true
	}
	return ast.GoToNext
}
```

...and then write out the linkcache in the footer.  However, this depends on directly referencing textChild.Leaf.Literal and linkNode.Destination, which makes my code fragile in case those field names change in the future.  Any chance of getting methods on ast.Link that return those values?  (e.g. Link.Text() and Link.Destination())

Discussion:
@kjk: This code is done. The names will not change.

I will accept a PR with a flag to markdown renderer to render links at bottom.
@laurelschmidt: Done in #343

Solution patch:
diff --git a/md/md_renderer.go b/md/md_renderer.go
index ebbea00..4d01a12 100644
--- a/md/md_renderer.go
+++ b/md/md_renderer.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"io"
+	"sort"
 	"strings"
 
 	"github.com/gomarkdown/markdown/ast"
@@ -21,14 +22,43 @@ type Renderer struct {
 	listDepth      int
 	indentSize     int
 	lastNormalText string
+
+	C *RendererConfig
+
+	linkcache map[string]bool // cache for link definitions to write in the footer, if renderLinksInFooter is set
+}
+
+type RendererConfig struct {
+	Flags Flags
 }
 
+type Flags int
+
+const renderLinksInFooter Flags = 1 << iota
+
+type RendererOpt func(c *RendererConfig)
+
 // NewRenderer returns a Markdown renderer.
-func NewRenderer() *Renderer {
+func NewRenderer(opts ...RendererOpt) *Renderer {
+	c := &RendererConfig{}
+	for _, opt := range opts {
+		opt(c)
+	}
 	return &Renderer{
 		orderedListCounter: map[int]int{},
 		paragraph:          map[int]bool{},
 		indentSize:         4,
+		C:                  c,
+	}
+}
+
+func WithRenderInFooter(renderInFooter bool) RendererOpt {
+	return func(c *RendererConfig) {
+		if renderInFooter {
+			c.Flags |= renderLinksInFooter
+		} else {
+			c.Flags &^= renderLinksInFooter
+		}
 	}
 }
 
@@ -44,7 +74,7 @@ func (r *Renderer) outs(w io.Writer, s string) {
 
 func (r *Renderer) doubleSpace(w io.Writer) {
 	// TODO: need to remember number of written bytes
-	//if out.Len() > 0 {
+	// if out.Len() > 0 {
 	r.outs(w, "\n")
 	//}
 }
@@ -83,7 +113,7 @@ func (r *Renderer) listItem(w io.Writer, node *ast.ListItem, entering bool) {
 
 func (r *Renderer) para(w io.Writer, node *ast.Paragraph, entering bool) {
 	if !entering && r.lastOutputLen > 0 {
-		var br = "\n\n"
+		br := "\n\n"
 
 		// List items don't need the extra line-break.
 		if _, ok := node.Parent.(*ast.ListItem); ok {
@@ -279,14 +309,29 @@ func (r *Renderer) link(w io.Writer, node *ast.Link, entering bool) {
 	} else {
 		link := string(escape(node.Destination))
 		title := string(node.Title)
-		r.outs(w, "](")
-		r.outs(w, link)
+		if r.C == nil || r.C.Flags&renderLinksInFooter == 0 {
+			r.outs(w, "](")
+			r.outs(w, link)
+			if len(title) != 0 {
+				r.outs(w, ` "`)
+				r.outs(w, title)
+				r.outs(w, `"`)
+			}
+			r.outs(w, ")")
+			return
+		}
+
+		r.outs(w, "]")
+		child, _ := ast.GetFirstChild(node).(*ast.Text)
+		linkdefn := fmt.Sprintf("[%s]: %s", string(escape(child.Leaf.Literal)), link)
 		if len(title) != 0 {
-			r.outs(w, ` "`)
-			r.outs(w, title)
-			r.outs(w, `"`)
+			linkdefn += fmt.Sprintf(" \"%s\"", title)
 		}
-		r.outs(w, ")")
+		if r.linkcache == nil {
+			r.linkcache = make(map[string]bool)
+		}
+		r.linkcache[linkdefn] = true
+
 	}
 }
 
@@ -382,5 +427,23 @@ func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) {
 
 // RenderFooter renders footer
 func (r *Renderer) RenderFooter(w io.Writer, ast ast.Node) {
-	// do nothing
+	if r.C != nil && r.C.Flags&renderLinksInFooter != 0 {
+		if r.linkcache == nil {
+			return
+		}
+
+		// Extract links so we can write links in a predictable order.
+		links := make([]string, 0, len(r.linkcache))
+		for k := range r.linkcache {
+			links = append(links, k)
+		}
+		// Sort the keys to ensure consistent order.
+		sort.Strings(links)
+
+		for _, linkdefn := range links {
+			r.outs(w, "\n")
+			r.outs(w, linkdefn)
+		}
+		r.outs(w, "\n")
+	}
 }



Test patch:
diff --git a/md/md_renderer_test.go b/md/md_renderer_test.go
index 8aa5614..8302f1b 100644
--- a/md/md_renderer_test.go
+++ b/md/md_renderer_test.go
@@ -9,9 +9,9 @@ import (
 )
 
 func TestRenderDocument(t *testing.T) {
-	var source = []byte("# title\n* aaa\n* bbb\n* ccc")
-	var input = markdown.Parse(source, nil)
-	var expected = "# title\n\n* aaa\n* bbb\n* ccc\n\n"
+	source := []byte("# title\n* aaa\n* bbb\n* ccc")
+	input := markdown.Parse(source, nil)
+	expected := "# title\n\n* aaa\n* bbb\n* ccc\n\n"
 	testRendering(t, input, expected)
 }
 
@@ -64,21 +64,21 @@ func TestRenderImage(t *testing.T) {
 }
 
 func TestRenderCode(t *testing.T) {
-	var input = &ast.Code{}
+	input := &ast.Code{}
 	input.Literal = []byte(string("val x : Int = 42"))
 	expected := "`val x : Int = 42`"
 	testRendering(t, input, expected)
 }
 
 func TestRenderCodeBlock(t *testing.T) {
-	var input = &ast.CodeBlock{Info: []byte(string("scala"))}
+	input := &ast.CodeBlock{Info: []byte(string("scala"))}
 	input.Literal = []byte(string("val x : Int = 42"))
 	expected := "\n```scala\nval x : Int = 42\n```\n"
 	testRendering(t, input, expected)
 }
 
 func TestRenderParagraph(t *testing.T) {
-	var input = &ast.Paragraph{}
+	input := &ast.Paragraph{}
 	ast.AppendChild(input, &ast.Text{Leaf: ast.Leaf{Literal: []byte(string("Hello World !"))}})
 	expected := "Hello World !\n\n"
 	testRendering(t, input, expected)
@@ -101,23 +101,23 @@ func TestRenderCodeWithParagraph(t *testing.T) {
 }
 
 func TestRenderHTMLSpan(t *testing.T) {
-	var input = &ast.HTMLSpan{}
+	input := &ast.HTMLSpan{}
 	input.Literal = []byte(string("hello"))
 	expected := "hello"
 	testRendering(t, input, expected)
 }
 
 func TestRenderHTMLBlock(t *testing.T) {
-	var input = &ast.HTMLBlock{}
+	input := &ast.HTMLBlock{}
 	input.Literal = []byte(string("hello"))
 	expected := "\nhello\n\n"
 	testRendering(t, input, expected)
 }
 
 func TestRenderList(t *testing.T) {
-	var source = []byte("* aaa\n* bbb\n* ccc\n* ddd\n")
-	var input = markdown.Parse(source, nil)
-	var expected = "* aaa\n* bbb\n* ccc\n* ddd\n\n"
+	source := []byte("* aaa\n* bbb\n* ccc\n* ddd\n")
+	input := markdown.Parse(source, nil)
+	expected := "* aaa\n* bbb\n* ccc\n* ddd\n\n"
 	testRendering(t, input, expected)
 
 	source = []byte("+ aaa\n+ bbb\n+ ccc\n+ ddd\n")
@@ -149,10 +149,16 @@ func TestRenderList(t *testing.T) {
 	input = markdown.Parse(source, nil)
 	expected = "* aaa\n    * aaa1\n    * aaa2\n\n* bbb\n* ccc\n* ddd\n\n"
 	testRendering(t, input, expected)
+
+	source = []byte("This is an [example](https://example.com) and another [website](https://github.com).")
+	input = markdown.Parse(source, nil)
+	rendererOpts := []RendererOpt{WithRenderInFooter(true)}
+	expected = "This is an [example] and another [website].\n\n\n[example]: https://example.com\n[website]: https://github.com\n"
+	testRendering(t, input, expected, rendererOpts...)
 }
 
-func testRendering(t *testing.T, input ast.Node, expected string) {
-	renderer := NewRenderer()
+func testRendering(t *testing.T, input ast.Node, expected string, opts ...RendererOpt) {
+	renderer := NewRenderer(opts...)
 	result := string(markdown.Render(input, renderer))
 	if strings.Compare(result, expected) != 0 {
 		t.Errorf("[%s] is not equal to [%s]", result, expected)