mirror of
https://github.com/kovetskiy/mark.git
synced 2025-04-24 05:42:40 +08:00
Merge pull request #260 from mrueg/switch-to-goldmark
Replace blackfriday with goldmark
This commit is contained in:
commit
93218f1e69
2
go.mod
2
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
|
||||
)
|
||||
|
4
go.sum
4
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=
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
quoteType := ParseBlockQuoteType(node, source)
|
||||
quoteLevel := r.LevelMap.Level(node)
|
||||
|
||||
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
|
||||
}
|
||||
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 bf.GoToNext
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
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 {
|
||||
_, 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 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 ast.WalkSkipChildren, nil
|
||||
}
|
||||
}
|
||||
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
|
||||
// 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,
|
||||
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 := 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,
|
||||
),
|
||||
)
|
||||
|
||||
html = colon.ReplaceAll(html, []byte(`:`))
|
||||
html := colon.ReplaceAll(buf.Bytes(), []byte(`:`))
|
||||
|
||||
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
|
||||
|
||||
|
@ -10,10 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
NL = "\n"
|
||||
)
|
||||
|
||||
func TestCompileMarkdown(t *testing.T) {
|
||||
test := assert.New(t)
|
||||
|
||||
|
1
pkg/mark/testdata/codes.html
vendored
1
pkg/mark/testdata/codes.html
vendored
@ -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">
|
||||
|
6
pkg/mark/testdata/header.html
vendored
6
pkg/mark/testdata/header.html
vendored
@ -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>
|
||||
|
14
pkg/mark/testdata/links.html
vendored
14
pkg/mark/testdata/links.html
vendored
@ -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 <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
10
pkg/mark/testdata/lists.html
vendored
10
pkg/mark/testdata/lists.html
vendored
@ -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>
|
||||
|
6
pkg/mark/testdata/newlines.html
vendored
6
pkg/mark/testdata/newlines.html
vendored
@ -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>
|
||||
|
11
pkg/mark/testdata/quotes.html
vendored
11
pkg/mark/testdata/quotes.html
vendored
@ -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>
|
||||
|
5
pkg/mark/testdata/quotes.md
vendored
5
pkg/mark/testdata/quotes.md
vendored
@ -19,6 +19,11 @@
|
||||
> * Warn bullet 1
|
||||
> * Warn bullet 2
|
||||
|
||||
|
||||
## Third Heading
|
||||
> <!-- Info -->
|
||||
> Test
|
||||
|
||||
## Simple Blockquote
|
||||
|
||||
> This paragraph is a simple blockquote
|
||||
|
1
pkg/mark/testdata/table.html
vendored
1
pkg/mark/testdata/table.html
vendored
@ -5,7 +5,6 @@
|
||||
<th>HEADER2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>row1</td>
|
||||
|
3
pkg/mark/testdata/tags.html
vendored
3
pkg/mark/testdata/tags.html
vendored
@ -1,5 +1,6 @@
|
||||
<p><b>bold</b>
|
||||
<strong>bold</strong></p>
|
||||
|
||||
<p><i>vitalik</i>
|
||||
<em>vitalik</em></p>
|
||||
<p><s>strikethrough</s>
|
||||
<del>strikethrough</del></p>
|
||||
|
3
pkg/mark/testdata/tags.md
vendored
3
pkg/mark/testdata/tags.md
vendored
@ -3,3 +3,6 @@
|
||||
|
||||
<i>vitalik</i>
|
||||
*vitalik*
|
||||
|
||||
<s>strikethrough</s>
|
||||
~~strikethrough~~
|
||||
|
Loading…
x
Reference in New Issue
Block a user