Repository: kovetskiy/mark
Pull request #260 (2023-03-15 20:17:48)

Replace blackfriday with goldmark
This change replaces the unmaintained blackfriday markdown renderer with the goldmark renderer.
The goal is to have wider support for more markdown extensions and ideally have a better solution for mermaid transformation via extensions as well.


Patch:
diff --git a/go.mod b/go.mod
index 5c98b99..fd8cbdc 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,6 @@ go 1.19
 
 require (
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
-	github.com/kovetskiy/blackfriday/v2 v2.3.0
 	github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69
 	github.com/kovetskiy/ko v1.6.1
 	github.com/kovetskiy/lorg v1.2.0
@@ -12,6 +11,7 @@ require (
 	github.com/reconquest/pkg v1.3.0
 	github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
 	github.com/stretchr/testify v1.8.1
+	github.com/yuin/goldmark v1.5.4
 	golang.org/x/tools v0.7.0
 	gopkg.in/yaml.v3 v3.0.1
 )
diff --git a/go.sum b/go.sum
index 56e730c..db6100c 100644
--- a/go.sum
+++ b/go.sum
@@ -8,8 +8,6 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
-github.com/kovetskiy/blackfriday/v2 v2.3.0 h1:KKABLPopQ2+DWKtM/ifx0RijGz09mNlCuEcZy5KvZVA=
-github.com/kovetskiy/blackfriday/v2 v2.3.0/go.mod h1:ES7tjNJdnHp1h8dib5cmoa//rgvQeYrtzGzGM/Kozk4=
 github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69 h1:vn82v0gKhTTm67znr7nxYBNW4mJ8zfY7dywZivUy3tY=
 github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69/go.mod h1:t7LFI5v8Q5+nl9sqId9PS0C9H9F4c5d4XlhkLve1MCM=
 github.com/kovetskiy/ko v1.6.1 h1:EO5v6CrW6x6vzxo7CKbN0r+foIRjz06U6wVSgxUVqMc=
@@ -41,6 +39,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
+github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3 h1:BhVaeQJc3xalHGONn215FylzuxdQBIT3d/aRjDg4nXQ=
 github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess=
 golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
diff --git a/pkg/confluence/api.go b/pkg/confluence/api.go
index d777ecf..d4d7be4 100644
--- a/pkg/confluence/api.go
+++ b/pkg/confluence/api.go
@@ -555,7 +555,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ne
 			"labels": labels,
 			// Fix to set full-width as has changed on Confluence APIs again.
 			// https://jira.atlassian.com/browse/CONFCLOUD-65447
-			// 
+			//
 			"properties": map[string]interface{}{
 				"content-appearance-published": map[string]interface{}{
 					"value": appearance,
diff --git a/pkg/mark/markdown.go b/pkg/mark/markdown.go
index 1122c92..52c8123 100644
--- a/pkg/mark/markdown.go
+++ b/pkg/mark/markdown.go
@@ -1,14 +1,21 @@
 package mark
 
 import (
+	"bytes"
 	"fmt"
-	"io"
 	"regexp"
 	"strings"
 
-	bf "github.com/kovetskiy/blackfriday/v2"
 	"github.com/kovetskiy/mark/pkg/mark/stdlib"
 	"github.com/reconquest/pkg/log"
+	"github.com/yuin/goldmark"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/extension"
+	"github.com/yuin/goldmark/parser"
+	"github.com/yuin/goldmark/renderer"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/util"
 )
 
 var reBlockDetails = regexp.MustCompile(
@@ -17,20 +24,69 @@ var reBlockDetails = regexp.MustCompile(
 	`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`,
 )
 
-type BlockQuoteLevelMap map[*bf.Node]int
+// Define BlockQuoteType enum
+type BlockQuoteType int
 
-func (m BlockQuoteLevelMap) Level(node *bf.Node) int {
+const (
+	Info BlockQuoteType = iota
+	Note
+	Warn
+	None
+)
+
+func (t BlockQuoteType) String() string {
+	return []string{"info", "note", "warn", "none"}[t]
+}
+
+type BlockQuoteLevelMap map[ast.Node]int
+
+func (m BlockQuoteLevelMap) Level(node ast.Node) int {
 	return m[node]
 }
 
+// Renderer renders anchor [Node]s.
 type ConfluenceRenderer struct {
-	bf.Renderer
-
+	html.Config
 	Stdlib *stdlib.Lib
 
 	LevelMap BlockQuoteLevelMap
 }
 
+// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
+func NewConfluenceRenderer(stdlib *stdlib.Lib, opts ...html.Option) renderer.NodeRenderer {
+	return &ConfluenceRenderer{
+		Config:   html.NewConfig(),
+		Stdlib:   stdlib,
+		LevelMap: nil,
+	}
+}
+
+// RegisterFuncs implements NodeRenderer.RegisterFuncs .
+func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+	// blocks
+	// reg.Register(ast.KindDocument, r.renderNode)
+	// reg.Register(ast.KindHeading, r.renderNode)
+	reg.Register(ast.KindBlockquote, r.renderBlockQuote)
+	reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
+	reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
+	// reg.Register(ast.KindHTMLBlock, r.renderNode)
+	// reg.Register(ast.KindList, r.renderNode)
+	// reg.Register(ast.KindListItem, r.renderNode)
+	// reg.Register(ast.KindParagraph, r.renderNode)
+	// reg.Register(ast.KindTextBlock, r.renderNode)
+	// reg.Register(ast.KindThematicBreak, r.renderNode)
+
+	// inlines
+	// reg.Register(ast.KindAutoLink, r.renderNode)
+	// reg.Register(ast.KindCodeSpan, r.renderNode)
+	// reg.Register(ast.KindEmphasis, r.renderNode)
+	// reg.Register(ast.KindImage, r.renderNode)
+	reg.Register(ast.KindLink, r.renderLink)
+	// reg.Register(ast.KindRawHTML, r.renderNode)
+	// reg.Register(ast.KindText, r.renderNode)
+	// reg.Register(ast.KindString, r.renderNode)
+}
+
 func ParseLanguage(lang string) string {
 	// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
 	// let's split it by spaces
@@ -62,26 +118,13 @@ func ParseTitle(lang string) string {
 	return ""
 }
 
-// Define BlockQuoteType enum
-type BlockQuoteType int
-
-const (
-	Info BlockQuoteType = iota
-	Note
-	Warn
-	None
-)
-
-func (t BlockQuoteType) String() string {
-	return []string{"info", "note", "warn", "none"}[t]
-}
-
-func ClasifyingBlockQuote(literal string) BlockQuoteType {
+// ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
+func ClassifyingBlockQuote(literal string) BlockQuoteType {
 	infoPattern := regexp.MustCompile(`info|Info|INFO`)
 	notePattern := regexp.MustCompile(`note|Note|NOTE`)
 	warnPattern := regexp.MustCompile(`warn|Warn|WARN`)
 
-	var t BlockQuoteType = None
+	var t = None
 	switch {
 	case infoPattern.MatchString(literal):
 		t = Info
@@ -93,191 +136,259 @@ func ClasifyingBlockQuote(literal string) BlockQuoteType {
 	return t
 }
 
-func ParseBlockQuoteType(node *bf.Node) BlockQuoteType {
-	var t BlockQuoteType = None
+// ParseBlockQuoteType parses the first line of a blockquote and returns its type
+func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
+	var t = None
 
 	countParagraphs := 0
-	node.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
+	_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
 
-		if node.Type == bf.Paragraph && entering {
+		if node.Kind() == ast.KindParagraph && entering {
 			countParagraphs += 1
 		}
 		// Type of block quote should be defined on the first blockquote line
-		if node.Type == bf.Text && countParagraphs < 2 {
-			t = ClasifyingBlockQuote(string(node.Literal))
-		} else if countParagraphs > 1 {
-			return bf.Terminate
+		if countParagraphs < 2 && entering {
+			if node.Kind() == ast.KindText {
+				n := node.(*ast.Text)
+				t = ClassifyingBlockQuote(string(n.Text(source)))
+				countParagraphs += 1
+			}
+			if node.Kind() == ast.KindHTMLBlock {
+
+				n := node.(*ast.HTMLBlock)
+				for i := 0; i < n.BaseBlock.Lines().Len(); i++ {
+					line := n.BaseBlock.Lines().At(i)
+					t = ClassifyingBlockQuote(string(line.Value(source)))
+					if t != None {
+						break
+					}
+				}
+				countParagraphs += 1
+			}
+		} else if countParagraphs > 1 && entering {
+			return ast.WalkStop, nil
 		}
-		return bf.GoToNext
+		return ast.WalkContinue, nil
 	})
+
 	return t
 }
 
-func GenerateBlockQuoteLevel(someNode *bf.Node) BlockQuoteLevelMap {
+// GenerateBlockQuoteLevel walks a given node and returns a map of blockquote levels
+func GenerateBlockQuoteLevel(someNode ast.Node) BlockQuoteLevelMap {
 
 	// We define state variable that track BlockQuote level while we walk the tree
 	blockQuoteLevel := 0
-	blockQuoteLevelMap := make(map[*bf.Node]int)
+	blockQuoteLevelMap := make(map[ast.Node]int)
 
 	rootNode := someNode
-	for rootNode.Parent != nil {
-		rootNode = rootNode.Parent
+	for rootNode.Parent() != nil {
+		rootNode = rootNode.Parent()
 	}
-	rootNode.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
-		if node.Type == bf.BlockQuote && entering {
+	_ = ast.Walk(rootNode, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
+		if node.Kind() == ast.KindBlockquote && entering {
 			blockQuoteLevelMap[node] = blockQuoteLevel
 			blockQuoteLevel += 1
 		}
-		if node.Type == bf.BlockQuote && !entering {
+		if node.Kind() == ast.KindBlockquote && !entering {
 			blockQuoteLevel -= 1
 		}
-		return bf.GoToNext
+		return ast.WalkContinue, nil
 	})
 	return blockQuoteLevelMap
 }
 
-func (renderer ConfluenceRenderer) RenderNode(
-	writer io.Writer,
-	node *bf.Node,
-	entering bool,
-) bf.WalkStatus {
+// renderBlockQuote will render a BlockQuote
+func (r *ConfluenceRenderer) renderBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	// Initialize BlockQuote level map
-	if renderer.LevelMap == nil {
-		renderer.LevelMap = GenerateBlockQuoteLevel(node)
+	if r.LevelMap == nil {
+		r.LevelMap = GenerateBlockQuoteLevel(node)
 	}
 
-	if node.Type == bf.CodeBlock {
-
-		groups := reBlockDetails.FindStringSubmatch(string(node.Info))
-		linenumbers := false
-		firstline := 0
-		theme := ""
-		collapse := false
-		lang := ""
-		var options []string
-		title := ""
-		if len(groups) > 0 {
-			lang, options, title = groups[1], strings.Fields(groups[2]), groups[3]
-			for _, option := range options {
-				if option == "collapse" {
-					collapse = true
-					continue
-				}
-				if option == "nocollapse" {
-					collapse = false
-					continue
-				}
-				var i int
-				if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
-					linenumbers = i > 0
-					firstline = i
-					continue
-				}
-				theme = option
-			}
+	quoteType := ParseBlockQuoteType(node, source)
+	quoteLevel := r.LevelMap.Level(node)
+
+	if quoteLevel == 0 && entering && quoteType != None {
+		prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", quoteType)
+		if _, err := writer.Write([]byte(prefix)); err != nil {
+			return ast.WalkStop, err
 		}
-		err := renderer.Stdlib.Templates.ExecuteTemplate(
-			writer,
-			"ac:code",
-			struct {
-				Language    string
-				Collapse    bool
-				Title       string
-				Theme       string
-				Linenumbers bool
-				Firstline   int
-				Text        string
-			}{
-				lang,
-				collapse,
-				title,
-				theme,
-				linenumbers,
-				firstline,
-				strings.TrimSuffix(string(node.Literal), "\n"),
-			},
-		)
-		if err != nil {
-			panic(err)
+		return ast.WalkContinue, nil
+	}
+	if quoteLevel == 0 && !entering && quoteType != None {
+		suffix := "</ac:rich-text-body></ac:structured-macro>\n"
+		if _, err := writer.Write([]byte(suffix)); err != nil {
+			return ast.WalkStop, err
 		}
+		return ast.WalkContinue, nil
+	}
+	return r.goldmarkRenderBlockquote(writer, source, node, entering)
+}
 
-		return bf.GoToNext
+// goldmarkRenderBlockquote is the default renderBlockquote implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L286
+func (r *ConfluenceRenderer) goldmarkRenderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+	if entering {
+		if n.Attributes() != nil {
+			_, _ = w.WriteString("<blockquote")
+			html.RenderAttributes(w, n, html.BlockquoteAttributeFilter)
+			_ = w.WriteByte('>')
+		} else {
+			_, _ = w.WriteString("<blockquote>\n")
+		}
+	} else {
+		_, _ = w.WriteString("</blockquote>\n")
 	}
-	if node.Type == bf.Link && string(node.Destination[0:3]) == "ac:" {
+	return ast.WalkContinue, nil
+}
+
+// renderLink renders links specifically for confluence
+func (r *ConfluenceRenderer) renderLink(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if string(node.(*ast.Link).Destination[0:3]) == "ac:" {
 		if entering {
 			_, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\""))
 			if err != nil {
-				panic(err)
+				return ast.WalkStop, err
 			}
 
-			if len(node.Destination) < 4 {
-				_, err := writer.Write(node.FirstChild.Literal)
+			if len(node.(*ast.Link).Destination) < 4 {
+				_, err := writer.Write(node.FirstChild().Text(source))
 				if err != nil {
-					panic(err)
+					return ast.WalkStop, err
 				}
 			} else {
-				_, err := writer.Write(node.Destination[3:])
+				_, err := writer.Write(node.(*ast.Link).Destination[3:])
 				if err != nil {
-					panic(err)
+					return ast.WalkStop, err
 				}
 
 			}
 			_, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA["))
 			if err != nil {
-				panic(err)
+				return ast.WalkStop, err
 			}
 
-			_, err = writer.Write(node.FirstChild.Literal)
+			_, err = writer.Write(node.FirstChild().Text(source))
 			if err != nil {
-				panic(err)
+				return ast.WalkStop, err
 			}
 
 			_, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>"))
 			if err != nil {
-				panic(err)
+				return ast.WalkStop, err
 			}
 
-			return bf.SkipChildren
+			return ast.WalkSkipChildren, nil
 		}
-		return bf.GoToNext
 	}
-	if node.Type == bf.BlockQuote {
-		quoteType := ParseBlockQuoteType(node)
-		quoteLevel := renderer.LevelMap.Level(node)
-
-		re := regexp.MustCompile(`[\n\t]`)
-
-		if quoteLevel == 0 && entering && quoteType != None {
-			if _, err := writer.Write([]byte(re.ReplaceAllString(fmt.Sprintf(`
-			<ac:structured-macro ac:name="%s">
-			<ac:parameter ac:name="icon">true</ac:parameter>
-			<ac:rich-text-body>
-			`, quoteType), ""))); err != nil {
-				panic(err)
-			}
-			return bf.GoToNext
+	return r.goldmarkRenderLink(writer, source, node, entering)
+}
+
+// goldmarkRenderLink is the default renderLink implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L552
+func (r *ConfluenceRenderer) goldmarkRenderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Link)
+	if entering {
+		_, _ = w.WriteString("<a href=\"")
+		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+		}
+		_ = w.WriteByte('"')
+		if n.Title != nil {
+			_, _ = w.WriteString(` title="`)
+			r.Writer.Write(w, n.Title)
+			_ = w.WriteByte('"')
 		}
-		if quoteLevel == 0 && !entering && quoteType != None {
-			if _, err := writer.Write([]byte(re.ReplaceAllString(`
-			</ac:rich-text-body>
-			</ac:structured-macro>
-			`, ""))); err != nil {
-				panic(err)
+		if n.Attributes() != nil {
+			html.RenderAttributes(w, n, html.LinkAttributeFilter)
+		}
+		_ = w.WriteByte('>')
+	} else {
+		_, _ = w.WriteString("</a>")
+	}
+	return ast.WalkContinue, nil
+}
+
+// renderCodeBlock renders a (Fenced)CodeBlock
+func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+	var info []byte
+	nodeFencedCodeBlock := node.(*ast.FencedCodeBlock)
+	if nodeFencedCodeBlock.Info != nil {
+		segment := nodeFencedCodeBlock.Info.Segment
+		info = segment.Value(source)
+	}
+	groups := reBlockDetails.FindStringSubmatch(string(info))
+	linenumbers := false
+	firstline := 0
+	theme := ""
+	collapse := false
+	lang := ""
+	var options []string
+	title := ""
+	if len(groups) > 0 {
+		lang, options, title = groups[1], strings.Fields(groups[2]), groups[3]
+		for _, option := range options {
+			if option == "collapse" {
+				collapse = true
+				continue
+			}
+			if option == "nocollapse" {
+				collapse = false
+				continue
+			}
+			var i int
+			if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
+				linenumbers = i > 0
+				firstline = i
+				continue
 			}
-			return bf.GoToNext
+			theme = option
 		}
+
 	}
-	return renderer.Renderer.RenderNode(writer, node, entering)
+
+	var lval []byte
+
+	lines := node.Lines().Len()
+	for i := 0; i < lines; i++ {
+		line := node.Lines().At(i)
+		lval = append(lval, line.Value(source)...)
+	}
+	err := r.Stdlib.Templates.ExecuteTemplate(
+		writer,
+		"ac:code",
+		struct {
+			Language    string
+			Collapse    bool
+			Title       string
+			Theme       string
+			Linenumbers bool
+			Firstline   int
+			Text        string
+		}{
+			lang,
+			collapse,
+			title,
+			theme,
+			linenumbers,
+			firstline,
+			strings.TrimSuffix(string(lval), "\n"),
+		},
+	)
+	if err != nil {
+		return ast.WalkStop, err
+	}
+
+	return ast.WalkContinue, nil
 }
 
 // compileMarkdown will replace tags like <ac:rich-tech-body> with escaped
-// equivalent, because bf markdown parser replaces that tags with
+// equivalent, because goldmark markdown parser replaces that tags with
 // <a href="ac:rich-text-body">ac:rich-text-body</a> because of the autolink
 // rule.
-func CompileMarkdown(
-	markdown []byte,
-	stdlib *stdlib.Lib,
-) string {
+func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string {
 	log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
 
 	colon := regexp.MustCompile(`---bf-COLON---`)
@@ -289,42 +400,33 @@ func CompileMarkdown(
 		[]byte(`<$1`+colon.String()+`$2>`),
 	)
 
-	renderer := ConfluenceRenderer{
-		Renderer: bf.NewHTMLRenderer(
-			bf.HTMLRendererParameters{
-				Flags: bf.UseXHTML |
-					bf.Smartypants |
-					bf.SmartypantsFractions |
-					bf.SmartypantsDashes |
-					bf.SmartypantsLatexDashes,
-			},
+	converter := goldmark.New(
+		goldmark.WithExtensions(
+			extension.GFM,
+			extension.Footnote,
+			extension.DefinitionList,
+			extension.Typographer,
 		),
-		Stdlib:   stdlib,
-		LevelMap: nil,
-	}
-
-	html := bf.Run(
-		markdown,
-		bf.WithRenderer(renderer),
-		bf.WithExtensions(
-			bf.NoIntraEmphasis|
-				bf.Tables|
-				bf.FencedCode|
-				bf.Autolink|
-				bf.LaxHTMLBlocks|
-				bf.Strikethrough|
-				bf.SpaceHeadings|
-				bf.HeadingIDs|
-				bf.AutoHeadingIDs|
-				bf.Titleblock|
-				bf.BackslashLineBreak|
-				bf.DefinitionLists|
-				bf.NoEmptyLineBeforeBlock|
-				bf.Footnotes,
+		goldmark.WithParserOptions(
+			parser.WithAutoHeadingID(),
 		),
-	)
+		goldmark.WithRendererOptions(
+			html.WithXHTML(),
+			html.WithUnsafe(),
+		))
+
+	converter.Renderer().AddOptions(renderer.WithNodeRenderers(
+		util.Prioritized(NewConfluenceRenderer(stdlib), 100),
+	))
+
+	var buf bytes.Buffer
+	err := converter.Convert(markdown, &buf)
+
+	if err != nil {
+		panic(err)
+	}
 
-	html = colon.ReplaceAll(html, []byte(`:`))
+	html := colon.ReplaceAll(buf.Bytes(), []byte(`:`))
 
 	log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
 


Test patch:
diff --git a/pkg/mark/markdown_test.go b/pkg/mark/markdown_test.go
index 9819386..d48ebbc 100644
--- a/pkg/mark/markdown_test.go
+++ b/pkg/mark/markdown_test.go
@@ -10,10 +10,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-const (
-	NL = "\n"
-)
-
 func TestCompileMarkdown(t *testing.T) {
 	test := assert.New(t)
 
diff --git a/pkg/mark/testdata/codes.html b/pkg/mark/testdata/codes.html
index ef0f80c..c3d724d 100644
--- a/pkg/mark/testdata/codes.html
+++ b/pkg/mark/testdata/codes.html
@@ -20,7 +20,6 @@
 <ac:parameter ac:name="collapse">false</ac:parameter>
 <ac:plain-text-body><![CDATA[unknown code]]></ac:plain-text-body>
 </ac:structured-macro>
-
 <p>text
 text 2</p>
 <ac:structured-macro ac:name="code">
diff --git a/pkg/mark/testdata/header.html b/pkg/mark/testdata/header.html
index 89d0a10..566c4ae 100644
--- a/pkg/mark/testdata/header.html
+++ b/pkg/mark/testdata/header.html
@@ -1,13 +1,7 @@
 <h1 id="a">a</h1>
-
 <h2 id="b">b</h2>
-
 <h3 id="c">c</h3>
-
 <h4 id="d">d</h4>
-
 <h5 id="e">e</h5>
-
 <h1 id="f">f</h1>
-
 <h2 id="g">g</h2>
diff --git a/pkg/mark/testdata/links.html b/pkg/mark/testdata/links.html
index 4a3293e..3b1f468 100644
--- a/pkg/mark/testdata/links.html
+++ b/pkg/mark/testdata/links.html
@@ -1,15 +1,11 @@
 <p>Use <a href="https://example.com">https://example.com</a></p>
-
 <p>Use <ac:rich-text-body>aaa</ac:rich-text-body></p>
-
-<p>Use footnotes link <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></p>
-
-<div class="footnotes">
-
+<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
+<div class="footnotes" role="doc-endnotes">
 <hr />
-
 <ol>
-<li id="fn:1">a footnote link</li>
+<li id="fn:1">
+<p>a footnote link&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
+</li>
 </ol>
-
 </div>
diff --git a/pkg/mark/testdata/lists.html b/pkg/mark/testdata/lists.html
index 25fbcd1..af27c54 100644
--- a/pkg/mark/testdata/lists.html
+++ b/pkg/mark/testdata/lists.html
@@ -2,20 +2,18 @@
 <li>dash 1-1</li>
 <li>dash 1-2</li>
 <li>dash 1-3
-
 <ul>
 <li>dash 1-3-1</li>
 <li>dash 1-3-2</li>
 <li>dash 1-3-3
-
 <ul>
 <li>dash 1-3-3-1</li>
-</ul></li>
-</ul></li>
 </ul>
-
+</li>
+</ul>
+</li>
+</ul>
 <p>text</p>
-
 <ul>
 <li>a</li>
 <li>b</li>
diff --git a/pkg/mark/testdata/newlines.html b/pkg/mark/testdata/newlines.html
index c425cc6..b97e510 100644
--- a/pkg/mark/testdata/newlines.html
+++ b/pkg/mark/testdata/newlines.html
@@ -1,16 +1,10 @@
 <p>one-1
 one-2</p>
-
 <p>two-1</p>
-
 <p>two-2</p>
-
 <p>three-1</p>
-
 <p>three-2</p>
-
 <p>space-1
 space-2</p>
-
 <p>2space-1<br />
 2space-2</p>
diff --git a/pkg/mark/testdata/quotes.html b/pkg/mark/testdata/quotes.html
index 1eb3596..acb61d6 100644
--- a/pkg/mark/testdata/quotes.html
+++ b/pkg/mark/testdata/quotes.html
@@ -1,32 +1,31 @@
 <h1 id="main-heading">Main Heading</h1>
-
 <h2 id="first-heading">First Heading</h2>
 <ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
 <p><strong>NOTES:</strong></p>
-
 <ol>
 <li>Note number one</li>
 <li>Note number two</li>
 </ol>
-
 <blockquote>
 <p>a
 b</p>
 </blockquote>
-
 <p><strong>Warn (Should not be picked as blockquote type)</strong></p>
 </ac:rich-text-body></ac:structured-macro>
 <h2 id="second-heading">Second Heading</h2>
 <ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
 <p><strong>Warn</strong></p>
-
 <ul>
 <li>Warn bullet 1</li>
 <li>Warn bullet 2</li>
 </ul>
 </ac:rich-text-body></ac:structured-macro>
+<h2 id="third-heading">Third Heading</h2>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<!-- Info -->
+<p>Test</p>
+</ac:rich-text-body></ac:structured-macro>
 <h2 id="simple-blockquote">Simple Blockquote</h2>
-
 <blockquote>
 <p>This paragraph is a simple blockquote</p>
 </blockquote>
diff --git a/pkg/mark/testdata/quotes.md b/pkg/mark/testdata/quotes.md
index b9837ae..f6b9958 100644
--- a/pkg/mark/testdata/quotes.md
+++ b/pkg/mark/testdata/quotes.md
@@ -19,6 +19,11 @@
 > * Warn bullet 1
 > * Warn bullet 2
 
+
+## Third Heading
+> <!-- Info -->
+> Test
+
 ## Simple Blockquote
 
 > This paragraph is a simple blockquote
diff --git a/pkg/mark/testdata/table.html b/pkg/mark/testdata/table.html
index 8129391..9b4f80a 100644
--- a/pkg/mark/testdata/table.html
+++ b/pkg/mark/testdata/table.html
@@ -5,7 +5,6 @@
 <th>HEADER2</th>
 </tr>
 </thead>
-
 <tbody>
 <tr>
 <td>row1</td>
diff --git a/pkg/mark/testdata/tags.html b/pkg/mark/testdata/tags.html
index c9ab4f8..a5161db 100644
--- a/pkg/mark/testdata/tags.html
+++ b/pkg/mark/testdata/tags.html
@@ -1,5 +1,6 @@
 <p><b>bold</b>
 <strong>bold</strong></p>
-
 <p><i>vitalik</i>
 <em>vitalik</em></p>
+<p>
strikethrough

+<del>strikethrough</del></p>
diff --git a/pkg/mark/testdata/tags.md b/pkg/mark/testdata/tags.md
index 3dc28c9..502c6d6 100644
--- a/pkg/mark/testdata/tags.md
+++ b/pkg/mark/testdata/tags.md
@@ -3,3 +3,6 @@
 
 <i>vitalik</i>
 *vitalik*
+
+
strikethrough

+~~strikethrough~~