diff --git a/md/md_renderer.go b/md/md_renderer.go index ebbea004..4d01a124 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") + } } diff --git a/md/md_renderer_test.go b/md/md_renderer_test.go index 8aa5614f..8302f1bf 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)