Merge pull request #260 from mrueg/switch-to-goldmark

Replace blackfriday with goldmark
This commit is contained in:
Manuel Rüger 2023-03-28 16:54:04 +02:00 committed by GitHub
commit 93218f1e69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 308 additions and 222 deletions

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.19
require ( require (
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 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/gopencils v0.0.0-20230119081704-a73db75b2f69
github.com/kovetskiy/ko v1.6.1 github.com/kovetskiy/ko v1.6.1
github.com/kovetskiy/lorg v1.2.0 github.com/kovetskiy/lorg v1.2.0
@ -12,6 +11,7 @@ require (
github.com/reconquest/pkg v1.3.0 github.com/reconquest/pkg v1.3.0
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
github.com/yuin/goldmark v1.5.4
golang.org/x/tools v0.7.0 golang.org/x/tools v0.7.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )

4
go.sum
View File

@ -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/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 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 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 h1:vn82v0gKhTTm67znr7nxYBNW4mJ8zfY7dywZivUy3tY=
github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69/go.mod h1:t7LFI5v8Q5+nl9sqId9PS0C9H9F4c5d4XlhkLve1MCM= github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69/go.mod h1:t7LFI5v8Q5+nl9sqId9PS0C9H9F4c5d4XlhkLve1MCM=
github.com/kovetskiy/ko v1.6.1 h1:EO5v6CrW6x6vzxo7CKbN0r+foIRjz06U6wVSgxUVqMc= 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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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 h1:BhVaeQJc3xalHGONn215FylzuxdQBIT3d/aRjDg4nXQ=
github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess= github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=

View File

@ -555,7 +555,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ne
"labels": labels, "labels": labels,
// Fix to set full-width as has changed on Confluence APIs again. // Fix to set full-width as has changed on Confluence APIs again.
// https://jira.atlassian.com/browse/CONFCLOUD-65447 // https://jira.atlassian.com/browse/CONFCLOUD-65447
// //
"properties": map[string]interface{}{ "properties": map[string]interface{}{
"content-appearance-published": map[string]interface{}{ "content-appearance-published": map[string]interface{}{
"value": appearance, "value": appearance,

View File

@ -1,14 +1,21 @@
package mark package mark
import ( import (
"bytes"
"fmt" "fmt"
"io"
"regexp" "regexp"
"strings" "strings"
bf "github.com/kovetskiy/blackfriday/v2"
"github.com/kovetskiy/mark/pkg/mark/stdlib" "github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/pkg/log" "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( var reBlockDetails = regexp.MustCompile(
@ -17,20 +24,69 @@ var reBlockDetails = regexp.MustCompile(
`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`, `^(?:(\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] return m[node]
} }
// Renderer renders anchor [Node]s.
type ConfluenceRenderer struct { type ConfluenceRenderer struct {
bf.Renderer html.Config
Stdlib *stdlib.Lib Stdlib *stdlib.Lib
LevelMap BlockQuoteLevelMap 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 { func ParseLanguage(lang string) string {
// lang takes the following form: language? "collapse"? ("title"? <any string>*)? // lang takes the following form: language? "collapse"? ("title"? <any string>*)?
// let's split it by spaces // let's split it by spaces
@ -62,26 +118,13 @@ func ParseTitle(lang string) string {
return "" return ""
} }
// Define BlockQuoteType enum // ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
type BlockQuoteType int func ClassifyingBlockQuote(literal string) BlockQuoteType {
const (
Info BlockQuoteType = iota
Note
Warn
None
)
func (t BlockQuoteType) String() string {
return []string{"info", "note", "warn", "none"}[t]
}
func ClasifyingBlockQuote(literal string) BlockQuoteType {
infoPattern := regexp.MustCompile(`info|Info|INFO`) infoPattern := regexp.MustCompile(`info|Info|INFO`)
notePattern := regexp.MustCompile(`note|Note|NOTE`) notePattern := regexp.MustCompile(`note|Note|NOTE`)
warnPattern := regexp.MustCompile(`warn|Warn|WARN`) warnPattern := regexp.MustCompile(`warn|Warn|WARN`)
var t BlockQuoteType = None var t = None
switch { switch {
case infoPattern.MatchString(literal): case infoPattern.MatchString(literal):
t = Info t = Info
@ -93,191 +136,259 @@ func ClasifyingBlockQuote(literal string) BlockQuoteType {
return t return t
} }
func ParseBlockQuoteType(node *bf.Node) BlockQuoteType { // ParseBlockQuoteType parses the first line of a blockquote and returns its type
var t BlockQuoteType = None func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
var t = None
countParagraphs := 0 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 countParagraphs += 1
} }
// Type of block quote should be defined on the first blockquote line // Type of block quote should be defined on the first blockquote line
if node.Type == bf.Text && countParagraphs < 2 { if countParagraphs < 2 && entering {
t = ClasifyingBlockQuote(string(node.Literal)) if node.Kind() == ast.KindText {
} else if countParagraphs > 1 { n := node.(*ast.Text)
return bf.Terminate 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 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 // We define state variable that track BlockQuote level while we walk the tree
blockQuoteLevel := 0 blockQuoteLevel := 0
blockQuoteLevelMap := make(map[*bf.Node]int) blockQuoteLevelMap := make(map[ast.Node]int)
rootNode := someNode rootNode := someNode
for rootNode.Parent != nil { for rootNode.Parent() != nil {
rootNode = rootNode.Parent rootNode = rootNode.Parent()
} }
rootNode.Walk(func(node *bf.Node, entering bool) bf.WalkStatus { _ = ast.Walk(rootNode, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if node.Type == bf.BlockQuote && entering { if node.Kind() == ast.KindBlockquote && entering {
blockQuoteLevelMap[node] = blockQuoteLevel blockQuoteLevelMap[node] = blockQuoteLevel
blockQuoteLevel += 1 blockQuoteLevel += 1
} }
if node.Type == bf.BlockQuote && !entering { if node.Kind() == ast.KindBlockquote && !entering {
blockQuoteLevel -= 1 blockQuoteLevel -= 1
} }
return bf.GoToNext return ast.WalkContinue, nil
}) })
return blockQuoteLevelMap return blockQuoteLevelMap
} }
func (renderer ConfluenceRenderer) RenderNode( // renderBlockQuote will render a BlockQuote
writer io.Writer, func (r *ConfluenceRenderer) renderBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
node *bf.Node,
entering bool,
) bf.WalkStatus {
// Initialize BlockQuote level map // Initialize BlockQuote level map
if renderer.LevelMap == nil { if r.LevelMap == nil {
renderer.LevelMap = GenerateBlockQuoteLevel(node) r.LevelMap = GenerateBlockQuoteLevel(node)
} }
if node.Type == bf.CodeBlock { quoteType := ParseBlockQuoteType(node, source)
quoteLevel := r.LevelMap.Level(node)
groups := reBlockDetails.FindStringSubmatch(string(node.Info)) if quoteLevel == 0 && entering && quoteType != None {
linenumbers := false prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", quoteType)
firstline := 0 if _, err := writer.Write([]byte(prefix)); err != nil {
theme := "" return ast.WalkStop, err
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
}
} }
err := renderer.Stdlib.Templates.ExecuteTemplate( return ast.WalkContinue, nil
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 bf.GoToNext
} }
if node.Type == bf.Link && string(node.Destination[0:3]) == "ac:" { 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)
}
// 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")
}
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 { if entering {
_, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\"")) _, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\""))
if err != nil { if err != nil {
panic(err) return ast.WalkStop, err
} }
if len(node.Destination) < 4 { if len(node.(*ast.Link).Destination) < 4 {
_, err := writer.Write(node.FirstChild.Literal) _, err := writer.Write(node.FirstChild().Text(source))
if err != nil { if err != nil {
panic(err) return ast.WalkStop, err
} }
} else { } else {
_, err := writer.Write(node.Destination[3:]) _, err := writer.Write(node.(*ast.Link).Destination[3:])
if err != nil { if err != nil {
panic(err) return ast.WalkStop, err
} }
} }
_, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA[")) _, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA["))
if err != nil { 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 { if err != nil {
panic(err) return ast.WalkStop, err
} }
_, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>")) _, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>"))
if err != nil { 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
}
if quoteLevel == 0 && !entering && quoteType != None {
if _, err := writer.Write([]byte(re.ReplaceAllString(`
</ac:rich-text-body>
</ac:structured-macro>
`, ""))); err != nil {
panic(err)
}
return bf.GoToNext
} }
} }
return renderer.Renderer.RenderNode(writer, node, entering) 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 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
}
theme = option
}
}
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 // 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 // <a href="ac:rich-text-body">ac:rich-text-body</a> because of the autolink
// rule. // rule.
func CompileMarkdown( func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string {
markdown []byte,
stdlib *stdlib.Lib,
) string {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown)) log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
colon := regexp.MustCompile(`---bf-COLON---`) colon := regexp.MustCompile(`---bf-COLON---`)
@ -289,42 +400,33 @@ func CompileMarkdown(
[]byte(`<$1`+colon.String()+`$2>`), []byte(`<$1`+colon.String()+`$2>`),
) )
renderer := ConfluenceRenderer{ converter := goldmark.New(
Renderer: bf.NewHTMLRenderer( goldmark.WithExtensions(
bf.HTMLRendererParameters{ extension.GFM,
Flags: bf.UseXHTML | extension.Footnote,
bf.Smartypants | extension.DefinitionList,
bf.SmartypantsFractions | extension.Typographer,
bf.SmartypantsDashes |
bf.SmartypantsLatexDashes,
},
), ),
Stdlib: stdlib, goldmark.WithParserOptions(
LevelMap: nil, 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 := bf.Run( html := colon.ReplaceAll(buf.Bytes(), []byte(`:`))
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,
),
)
html = colon.ReplaceAll(html, []byte(`:`))
log.Tracef(nil, "rendered markdown to html:\n%s", string(html)) log.Tracef(nil, "rendered markdown to html:\n%s", string(html))

View File

@ -10,10 +10,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const (
NL = "\n"
)
func TestCompileMarkdown(t *testing.T) { func TestCompileMarkdown(t *testing.T) {
test := assert.New(t) test := assert.New(t)

View File

@ -20,7 +20,6 @@
<ac:parameter ac:name="collapse">false</ac:parameter> <ac:parameter ac:name="collapse">false</ac:parameter>
<ac:plain-text-body><![CDATA[unknown code]]></ac:plain-text-body> <ac:plain-text-body><![CDATA[unknown code]]></ac:plain-text-body>
</ac:structured-macro> </ac:structured-macro>
<p>text <p>text
text 2</p> text 2</p>
<ac:structured-macro ac:name="code"> <ac:structured-macro ac:name="code">

View File

@ -1,13 +1,7 @@
<h1 id="a">a</h1> <h1 id="a">a</h1>
<h2 id="b">b</h2> <h2 id="b">b</h2>
<h3 id="c">c</h3> <h3 id="c">c</h3>
<h4 id="d">d</h4> <h4 id="d">d</h4>
<h5 id="e">e</h5> <h5 id="e">e</h5>
<h1 id="f">f</h1> <h1 id="f">f</h1>
<h2 id="g">g</h2> <h2 id="g">g</h2>

View File

@ -1,15 +1,11 @@
<p>Use <a href="https://example.com">https://example.com</a></p> <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 <ac:rich-text-body>aaa</ac:rich-text-body></p>
<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Use footnotes link <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></p> <div class="footnotes" role="doc-endnotes">
<div class="footnotes">
<hr /> <hr />
<ol> <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> </ol>
</div> </div>

View File

@ -2,20 +2,18 @@
<li>dash 1-1</li> <li>dash 1-1</li>
<li>dash 1-2</li> <li>dash 1-2</li>
<li>dash 1-3 <li>dash 1-3
<ul> <ul>
<li>dash 1-3-1</li> <li>dash 1-3-1</li>
<li>dash 1-3-2</li> <li>dash 1-3-2</li>
<li>dash 1-3-3 <li>dash 1-3-3
<ul> <ul>
<li>dash 1-3-3-1</li> <li>dash 1-3-3-1</li>
</ul></li>
</ul></li>
</ul> </ul>
</li>
</ul>
</li>
</ul>
<p>text</p> <p>text</p>
<ul> <ul>
<li>a</li> <li>a</li>
<li>b</li> <li>b</li>

View File

@ -1,16 +1,10 @@
<p>one-1 <p>one-1
one-2</p> one-2</p>
<p>two-1</p> <p>two-1</p>
<p>two-2</p> <p>two-2</p>
<p>three-1</p> <p>three-1</p>
<p>three-2</p> <p>three-2</p>
<p>space-1 <p>space-1
space-2</p> space-2</p>
<p>2space-1<br /> <p>2space-1<br />
2space-2</p> 2space-2</p>

View File

@ -1,32 +1,31 @@
<h1 id="main-heading">Main Heading</h1> <h1 id="main-heading">Main Heading</h1>
<h2 id="first-heading">First Heading</h2> <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> <ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p> <p><strong>NOTES:</strong></p>
<ol> <ol>
<li>Note number one</li> <li>Note number one</li>
<li>Note number two</li> <li>Note number two</li>
</ol> </ol>
<blockquote> <blockquote>
<p>a <p>a
b</p> b</p>
</blockquote> </blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p> <p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro> </ac:rich-text-body></ac:structured-macro>
<h2 id="second-heading">Second Heading</h2> <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> <ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p> <p><strong>Warn</strong></p>
<ul> <ul>
<li>Warn bullet 1</li> <li>Warn bullet 1</li>
<li>Warn bullet 2</li> <li>Warn bullet 2</li>
</ul> </ul>
</ac:rich-text-body></ac:structured-macro> </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> <h2 id="simple-blockquote">Simple Blockquote</h2>
<blockquote> <blockquote>
<p>This paragraph is a simple blockquote</p> <p>This paragraph is a simple blockquote</p>
</blockquote> </blockquote>

View File

@ -19,6 +19,11 @@
> * Warn bullet 1 > * Warn bullet 1
> * Warn bullet 2 > * Warn bullet 2
## Third Heading
> <!-- Info -->
> Test
## Simple Blockquote ## Simple Blockquote
> This paragraph is a simple blockquote > This paragraph is a simple blockquote

View File

@ -5,7 +5,6 @@
<th>HEADER2</th> <th>HEADER2</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>row1</td> <td>row1</td>

View File

@ -1,5 +1,6 @@
<p><b>bold</b> <p><b>bold</b>
<strong>bold</strong></p> <strong>bold</strong></p>
<p><i>vitalik</i> <p><i>vitalik</i>
<em>vitalik</em></p> <em>vitalik</em></p>
<p><s>strikethrough</s>
<del>strikethrough</del></p>

View File

@ -3,3 +3,6 @@
<i>vitalik</i> <i>vitalik</i>
*vitalik* *vitalik*
<s>strikethrough</s>
~~strikethrough~~