chore: Migrate to a goldmark extension

This commit is contained in:
Manuel Rüger 2023-09-01 22:59:04 +02:00
parent 10d0778adf
commit f57b4245f9
13 changed files with 851 additions and 654 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/kovetskiy/lorg" "github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark" "github.com/kovetskiy/mark/pkg/mark"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/includes" "github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/kovetskiy/mark/pkg/mark/macro" "github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/kovetskiy/mark/pkg/mark/stdlib" "github.com/kovetskiy/mark/pkg/mark/stdlib"
@ -436,12 +437,12 @@ func processFile(
} }
// Resolve attachments created from <!-- Attachment: --> directive // Resolve attachments created from <!-- Attachment: --> directive
localAttachments, err := mark.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments) localAttachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments)
if err != nil { if err != nil {
log.Fatalf(err, "unable to locate attachments") log.Fatalf(err, "unable to locate attachments")
} }
attaches, err := mark.ResolveAttachments( attaches, err := attachment.ResolveAttachments(
api, api,
target, target,
localAttachments, localAttachments,
@ -450,7 +451,7 @@ func processFile(
log.Fatalf(err, "unable to create/update attachments") log.Fatalf(err, "unable to create/update attachments")
} }
markdown = mark.CompileAttachmentLinks(markdown, attaches) markdown = attachment.CompileAttachmentLinks(markdown, attaches)
if cCtx.Bool("drop-h1") { if cCtx.Bool("drop-h1") {
log.Info( log.Info(
@ -461,7 +462,7 @@ func processFile(
html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1")) html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"))
// Resolve attachements detected from markdown // Resolve attachements detected from markdown
_, err = mark.ResolveAttachments( _, err = attachment.ResolveAttachments(
api, api,
target, target,
inlineAttachments, inlineAttachments,

View File

@ -1,4 +1,4 @@
package mark package attachment
import ( import (
"bytes" "bytes"

View File

@ -1,4 +1,4 @@
package mark package attachment
import ( import (
"bytes" "bytes"

View File

@ -2,18 +2,15 @@ package mark
import ( import (
"bytes" "bytes"
"fmt"
"path/filepath"
"regexp" "regexp"
"strings"
"github.com/kovetskiy/mark/pkg/mark/attachment"
cparser "github.com/kovetskiy/mark/pkg/mark/parser" cparser "github.com/kovetskiy/mark/pkg/mark/parser"
crenderer "github.com/kovetskiy/mark/pkg/mark/renderer"
"github.com/kovetskiy/mark/pkg/mark/stdlib" "github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/reconquest/pkg/log" "github.com/reconquest/pkg/log"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer"
@ -21,642 +18,60 @@ import (
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
) )
var reBlockDetails = regexp.MustCompile(
// (<Lang>|-) (collapse|<theme>|\d)* (title <title>)?
`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`,
)
// 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]
}
type BlockQuoteLevelMap map[ast.Node]int
func (m BlockQuoteLevelMap) Level(node ast.Node) int {
return m[node]
}
// Renderer renders anchor [Node]s. // Renderer renders anchor [Node]s.
type ConfluenceRenderer struct { type ConfluenceExtension struct {
html.Config html.Config
Stdlib *stdlib.Lib Stdlib *stdlib.Lib
Path string Path string
MermaidProvider string MermaidProvider string
MermaidScale float64 MermaidScale float64
DropFirstH1 bool DropFirstH1 bool
LevelMap BlockQuoteLevelMap Attachments []attachment.Attachment
Attachments []Attachment
} }
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer // NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceRenderer(stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool, opts ...html.Option) renderer.NodeRenderer { func NewConfluenceExtension(stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool) *ConfluenceExtension {
return &ConfluenceRenderer{ return &ConfluenceExtension{
Config: html.NewConfig(), Config: html.NewConfig(),
Stdlib: stdlib, Stdlib: stdlib,
Path: path, Path: path,
MermaidProvider: mermaidProvider, MermaidProvider: mermaidProvider,
MermaidScale: mermaidScale, MermaidScale: mermaidScale,
DropFirstH1: dropFirstH1, DropFirstH1: dropFirstH1,
LevelMap: nil, Attachments: []attachment.Attachment{},
Attachments: []Attachment{},
} }
} }
// RegisterFuncs implements NodeRenderer.RegisterFuncs . func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// blocks
// reg.Register(ast.KindDocument, r.renderNode)
reg.Register(ast.KindHeading, r.renderHeading)
reg.Register(ast.KindBlockquote, r.renderBlockQuote)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
// 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 m.Renderer().AddOptions(renderer.WithNodeRenderers(
// reg.Register(ast.KindAutoLink, r.renderNode) util.Prioritized(crenderer.NewConfluenceBlockQuoteRenderer(), 100),
// reg.Register(ast.KindCodeSpan, r.renderNode) util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
// reg.Register(ast.KindEmphasis, r.renderNode) util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, &c.Attachments, c.MermaidProvider, c.MermaidScale), 100),
reg.Register(ast.KindImage, r.renderImage) util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
reg.Register(ast.KindLink, r.renderLink) util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.DropFirstH1), 100),
// reg.Register(ast.KindRawHTML, r.renderNode) util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, &c.Attachments, c.Path), 100),
// reg.Register(ast.KindText, r.renderNode) util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
// reg.Register(ast.KindString, r.renderNode) ))
m.Parser().AddOptions(parser.WithInlineParsers(
// Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse
// the <ac:*/> tags.
util.Prioritized(cparser.NewConfluenceTagParser(), 199),
))
} }
func (r *ConfluenceRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool) (string, []attachment.Attachment) {
n := node.(*ast.Heading)
// If this is the first h1 heading of the document and we want to drop it, let's not render it at all.
if n.Level == 1 && r.DropFirstH1 {
if !entering {
r.DropFirstH1 = false
}
return ast.WalkSkipChildren, nil
}
return r.goldmarkRenderHeading(w, source, node, entering)
}
func (r *ConfluenceRenderer) goldmarkRenderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
if entering {
_, _ = w.WriteString("<h")
_ = w.WriteByte("0123456"[n.Level])
if n.Attributes() != nil {
html.RenderAttributes(w, node, html.HeadingAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</h")
_ = w.WriteByte("0123456"[n.Level])
_, _ = w.WriteString(">\n")
}
return ast.WalkContinue, nil
}
func ParseLanguage(lang string) string {
// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
// let's split it by spaces
paramlist := strings.Fields(lang)
// get the word in question, aka the first one
first := lang
if len(paramlist) > 0 {
first = paramlist[0]
}
if first == "collapse" || first == "title" {
// collapsing or including a title without a language
return ""
}
// the default case with language being the first one
return first
}
func ParseTitle(lang string) string {
index := strings.Index(lang, "title")
if index >= 0 {
// it's found, check if title is given and return it
start := index + 6
if len(lang) > start {
return lang[start:]
}
}
return ""
}
// 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 = None
switch {
case infoPattern.MatchString(literal):
t = Info
case notePattern.MatchString(literal):
t = Note
case warnPattern.MatchString(literal):
t = Warn
}
return t
}
// 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
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if node.Kind() == ast.KindParagraph && entering {
countParagraphs += 1
}
// Type of block quote should be defined on the first blockquote line
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 ast.WalkContinue, nil
})
return t
}
// 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[ast.Node]int)
rootNode := someNode
for rootNode.Parent() != nil {
rootNode = rootNode.Parent()
}
_ = 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.Kind() == ast.KindBlockquote && !entering {
blockQuoteLevel -= 1
}
return ast.WalkContinue, nil
})
return blockQuoteLevelMap
}
// 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 r.LevelMap == nil {
r.LevelMap = GenerateBlockQuoteLevel(node)
}
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
}
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)
}
// 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 {
return ast.WalkStop, err
}
if len(node.(*ast.Link).Destination) < 4 {
_, err := writer.Write(node.FirstChild().Text(source))
if err != nil {
return ast.WalkStop, err
}
} else {
_, err := writer.Write(node.(*ast.Link).Destination[3:])
if err != nil {
return ast.WalkStop, err
}
}
_, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA["))
if err != nil {
return ast.WalkStop, err
}
_, err = writer.Write(node.FirstChild().Text(source))
if err != nil {
return ast.WalkStop, err
}
_, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>"))
if err != nil {
return ast.WalkStop, err
}
}
return ast.WalkSkipChildren, nil
}
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
}
// renderFencedCodeBlock renders a FencedCodeBlock
func (r *ConfluenceRenderer) renderFencedCodeBlock(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)...)
}
if lang == "mermaid" && r.MermaidProvider == "mermaid-go" {
attachment, err := processMermaidLocally(title, lval, r.MermaidScale)
if err != nil {
return ast.WalkStop, err
}
r.Attachments = append(r.Attachments, attachment)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
attachment.Width,
attachment.Height,
attachment.Name,
"",
attachment.Filename,
"",
},
)
if err != nil {
return ast.WalkStop, err
}
} else {
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
}
// renderCodeBlock renders a CodeBlock
func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
linenumbers := false
firstline := 0
theme := ""
collapse := false
lang := ""
title := ""
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
}
// renderImage renders an inline image
func (r *ConfluenceRenderer) renderImage(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
attachments, err := ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(r.Path), []string{string(n.Destination)})
// We were unable to resolve it locally, treat as URL
if err != nil {
escapedURL := string(n.Destination)
escapedURL = strings.ReplaceAll(escapedURL, "&", "&amp;")
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
"",
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
"",
escapedURL,
},
)
} else {
r.Attachments = append(r.Attachments, attachments[0])
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
"",
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
attachments[0].Filename,
"",
},
)
}
if err != nil {
return ast.WalkStop, err
}
return ast.WalkSkipChildren, nil
}
func (r *ConfluenceRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return r.goldmarkRenderHTMLBlock(w, source, node, entering)
}
n := node.(*ast.HTMLBlock)
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
switch strings.Trim(string(line.Value(source)), "\n") {
case "<!-- ac:layout -->":
_, _ = w.WriteString("<ac:layout>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout end -->":
_, _ = w.WriteString("</ac:layout>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:single -->":
_, _ = w.WriteString("<ac:layout-section type=\"single\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_equal -->":
_, _ = w.WriteString("<ac:layout-section type=\"two_equal\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_left_sidebar -->":
_, _ = w.WriteString("<ac:layout-section type=\"two_left_sidebar\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_right_sidebar -->":
_, _ = w.WriteString("<ac:layout-section type=\"two_right_sidebar\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:three -->":
_, _ = w.WriteString("<ac:layout-section type=\"three\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:three_with_sidebars -->":
_, _ = w.WriteString("<ac:layout-section type=\"three_with_sidebars\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section end -->":
_, _ = w.WriteString("</ac:layout-section>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-cell -->":
_, _ = w.WriteString("<ac:layout-cell>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-cell end -->":
_, _ = w.WriteString("</ac:layout-cell>\n")
return ast.WalkContinue, nil
}
}
return r.goldmarkRenderHTMLBlock(w, source, node, entering)
}
func (r *ConfluenceRenderer) goldmarkRenderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.HTMLBlock)
if entering {
if r.Unsafe {
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.SecureWrite(w, line.Value(source))
}
} else {
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
}
} else {
if n.HasClosure() {
if r.Unsafe {
closure := n.ClosureLine
r.Writer.SecureWrite(w, closure.Value(source))
} else {
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
}
}
}
return ast.WalkContinue, nil
}
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool) (string, []Attachment) {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown)) log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
confluenceRenderer := NewConfluenceRenderer(stdlib, path, mermaidProvider, mermaidScale, dropFirstH1) confluenceExtension := NewConfluenceExtension(stdlib, path, mermaidProvider, mermaidScale, dropFirstH1)
converter := goldmark.New( converter := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
extension.GFM, extension.GFM,
extension.Footnote, extension.Footnote,
extension.DefinitionList, extension.DefinitionList,
confluenceExtension,
), ),
goldmark.WithParserOptions( goldmark.WithParserOptions(
parser.WithAutoHeadingID(), parser.WithAutoHeadingID(),
@ -666,16 +81,6 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidPr
html.WithUnsafe(), html.WithUnsafe(),
)) ))
converter.Parser().AddOptions(parser.WithInlineParsers(
// Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse
// the <ac:*/> tags.
util.Prioritized(cparser.NewConfluenceTagParser(), 199),
))
converter.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(confluenceRenderer, 100),
))
var buf bytes.Buffer var buf bytes.Buffer
err := converter.Convert(markdown, &buf) err := converter.Convert(markdown, &buf)
@ -687,7 +92,7 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidPr
log.Tracef(nil, "rendered markdown to html:\n%s", string(html)) log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
return string(html), confluenceRenderer.(*ConfluenceRenderer).Attachments return string(html), confluenceExtension.Attachments
} }
@ -701,18 +106,3 @@ func ExtractDocumentLeadingH1(markdown []byte) string {
return string(groups[1]) return string(groups[1])
} }
} }
// https://github.com/yuin/goldmark/blob/c446c414ef3a41fb562da0ae5badd18f1502c42f/renderer/html/html.go
func nodeToHTMLText(n ast.Node, source []byte) []byte {
var buf bytes.Buffer
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if s, ok := c.(*ast.String); ok && s.IsCode() {
buf.Write(s.Text(source))
} else if !c.HasChildren() {
buf.Write(util.EscapeHTML(c.Text(source)))
} else {
buf.Write(nodeToHTMLText(c, source))
}
}
return buf.Bytes()
}

View File

@ -1,4 +1,4 @@
package mark package mermaid
import ( import (
"bytes" "bytes"
@ -7,29 +7,30 @@ import (
"time" "time"
mermaid "github.com/dreampuf/mermaid.go" mermaid "github.com/dreampuf/mermaid.go"
"github.com/kovetskiy/mark/pkg/mark/attachment"
) )
var renderTimeout = 60 * time.Second var renderTimeout = 60 * time.Second
func processMermaidLocally(title string, mermaidDiagram []byte, scale float64) (attachement Attachment, err error) { func ProcessMermaidLocally(title string, mermaidDiagram []byte, scale float64) (attachement attachment.Attachment, err error) {
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout) ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)
defer cancel() defer cancel()
renderer, err := mermaid.NewRenderEngine(ctx) renderer, err := mermaid.NewRenderEngine(ctx)
if err != nil { if err != nil {
return Attachment{}, err return attachment.Attachment{}, err
} }
pngBytes, boxModel, err := renderer.RenderAsScaledPng(string(mermaidDiagram), scale) pngBytes, boxModel, err := renderer.RenderAsScaledPng(string(mermaidDiagram), scale)
if err != nil { if err != nil {
return Attachment{}, err return attachment.Attachment{}, err
} }
checkSum, err := GetChecksum(bytes.NewReader(mermaidDiagram)) checkSum, err := attachment.GetChecksum(bytes.NewReader(mermaidDiagram))
if err != nil { if err != nil {
return Attachment{}, err return attachment.Attachment{}, err
} }
if title == "" { if title == "" {
title = checkSum title = checkSum
@ -37,7 +38,7 @@ func processMermaidLocally(title string, mermaidDiagram []byte, scale float64) (
fileName := title + ".png" fileName := title + ".png"
return Attachment{ return attachment.Attachment{
ID: "", ID: "",
Name: title, Name: title,
Filename: fileName, Filename: fileName,

View File

@ -1,9 +1,10 @@
package mark package mermaid
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -12,10 +13,10 @@ func TestExtractMermaidImage(t *testing.T) {
name string name string
markdown []byte markdown []byte
scale float64 scale float64
want Attachment want attachment.Attachment
wantErr assert.ErrorAssertionFunc wantErr assert.ErrorAssertionFunc
}{ }{
{"example", []byte("graph TD;\n A-->B;"), 1.0, Attachment{ {"example", []byte("graph TD;\n A-->B;"), 1.0, attachment.Attachment{
// This is only the PNG Magic Header // This is only the PNG Magic Header
FileBytes: []byte{0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa}, FileBytes: []byte{0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa},
Filename: "example.png", Filename: "example.png",
@ -30,7 +31,7 @@ func TestExtractMermaidImage(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := processMermaidLocally(tt.name, tt.markdown, tt.scale) got, err := ProcessMermaidLocally(tt.name, tt.markdown, tt.scale)
if !tt.wantErr(t, err, fmt.Sprintf("processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))) { if !tt.wantErr(t, err, fmt.Sprintf("processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))) {
return return
} }

View File

@ -0,0 +1,172 @@
package renderer
import (
"fmt"
"regexp"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceBlockQuoteRenderer struct {
html.Config
LevelMap BlockQuoteLevelMap
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceBlockQuoteRenderer(opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceBlockQuoteRenderer{
Config: html.NewConfig(),
LevelMap: nil,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceBlockQuoteRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindBlockquote, r.renderBlockQuote)
}
// 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]
}
type BlockQuoteLevelMap map[ast.Node]int
func (m BlockQuoteLevelMap) Level(node ast.Node) int {
return m[node]
}
// 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 = None
switch {
case infoPattern.MatchString(literal):
t = Info
case notePattern.MatchString(literal):
t = Note
case warnPattern.MatchString(literal):
t = Warn
}
return t
}
// 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
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if node.Kind() == ast.KindParagraph && entering {
countParagraphs += 1
}
// Type of block quote should be defined on the first blockquote line
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 ast.WalkContinue, nil
})
return t
}
// 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[ast.Node]int)
rootNode := someNode
for rootNode.Parent() != nil {
rootNode = rootNode.Parent()
}
_ = 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.Kind() == ast.KindBlockquote && !entering {
blockQuoteLevel -= 1
}
return ast.WalkContinue, nil
})
return blockQuoteLevelMap
}
// renderBlockQuote will render a BlockQuote
func (r *ConfluenceBlockQuoteRenderer) renderBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// Initialize BlockQuote level map
if r.LevelMap == nil {
r.LevelMap = GenerateBlockQuoteLevel(node)
}
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
}
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)
}
// goldmarkRenderBlockquote is the default renderBlockquote implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L286
func (r *ConfluenceBlockQuoteRenderer) 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
}

View File

@ -0,0 +1,77 @@
package renderer
import (
"strings"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceCodeBlockRenderer struct {
html.Config
Stdlib *stdlib.Lib
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceCodeBlockRenderer(stdlib *stdlib.Lib, path string, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceCodeBlockRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceCodeBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
}
// renderCodeBlock renders a CodeBlock
func (r *ConfluenceCodeBlockRenderer) renderCodeBlock(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
linenumbers := false
firstline := 0
theme := ""
collapse := false
lang := ""
title := ""
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
}

View File

@ -0,0 +1,187 @@
package renderer
import (
"fmt"
"regexp"
"strings"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/mermaid"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceFencedCodeBlockRenderer struct {
html.Config
Stdlib *stdlib.Lib
MermaidProvider string
MermaidScale float64
Attachments []attachment.Attachment
}
var reBlockDetails = regexp.MustCompile(
// (<Lang>|-) (collapse|<theme>|\d)* (title <title>)?
`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`,
)
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceFencedCodeBlockRenderer(stdlib *stdlib.Lib, attachments *[]attachment.Attachment, mermaidProvider string, mermaidScale float64, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceFencedCodeBlockRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
MermaidProvider: mermaidProvider,
MermaidScale: mermaidScale,
Attachments: *attachments,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceFencedCodeBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
}
func ParseLanguage(lang string) string {
// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
// let's split it by spaces
paramlist := strings.Fields(lang)
// get the word in question, aka the first one
first := lang
if len(paramlist) > 0 {
first = paramlist[0]
}
if first == "collapse" || first == "title" {
// collapsing or including a title without a language
return ""
}
// the default case with language being the first one
return first
}
func ParseTitle(lang string) string {
index := strings.Index(lang, "title")
if index >= 0 {
// it's found, check if title is given and return it
start := index + 6
if len(lang) > start {
return lang[start:]
}
}
return ""
}
// renderFencedCodeBlock renders a FencedCodeBlock
func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(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)...)
}
if lang == "mermaid" && r.MermaidProvider == "mermaid-go" {
attachment, err := mermaid.ProcessMermaidLocally(title, lval, r.MermaidScale)
if err != nil {
return ast.WalkStop, err
}
r.Attachments = append(r.Attachments, attachment)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
attachment.Width,
attachment.Height,
attachment.Name,
"",
attachment.Filename,
"",
},
)
if err != nil {
return ast.WalkStop, err
}
} else {
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
}

View File

@ -0,0 +1,57 @@
package renderer
import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceHeadingRenderer struct {
html.Config
DropFirstH1 bool
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceHeadingRenderer(dropFirstH1 bool, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceHeadingRenderer{
Config: html.NewConfig(),
DropFirstH1: dropFirstH1,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceHeadingRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHeading, r.renderHeading)
}
func (r *ConfluenceHeadingRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
// If this is the first h1 heading of the document and we want to drop it, let's not render it at all.
if n.Level == 1 && r.DropFirstH1 {
if !entering {
r.DropFirstH1 = false
}
return ast.WalkSkipChildren, nil
}
return r.goldmarkRenderHeading(w, source, node, entering)
}
func (r *ConfluenceHeadingRenderer) goldmarkRenderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
if entering {
_, _ = w.WriteString("<h")
_ = w.WriteByte("0123456"[n.Level])
if n.Attributes() != nil {
html.RenderAttributes(w, node, html.HeadingAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</h")
_ = w.WriteByte("0123456"[n.Level])
_, _ = w.WriteString(">\n")
}
return ast.WalkContinue, nil
}

View File

@ -0,0 +1,104 @@
package renderer
import (
"strings"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceHTMLBlockRenderer struct {
html.Config
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceHTMLBlockRenderer(stdlib *stdlib.Lib, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceHTMLBlockRenderer{
Config: html.NewConfig(),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceHTMLBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
}
func (r *ConfluenceHTMLBlockRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return r.goldmarkRenderHTMLBlock(w, source, node, entering)
}
n := node.(*ast.HTMLBlock)
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
switch strings.Trim(string(line.Value(source)), "\n") {
case "<!-- ac:layout -->":
_, _ = w.WriteString("<ac:layout>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout end -->":
_, _ = w.WriteString("</ac:layout>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:single -->":
_, _ = w.WriteString("<ac:layout-section type=\"single\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_equal -->":
_, _ = w.WriteString("<ac:layout-section type=\"two_equal\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_left_sidebar -->":
_, _ = w.WriteString("<ac:layout-section type=\"two_left_sidebar\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_right_sidebar -->":
_, _ = w.WriteString("<ac:layout-section type=\"two_right_sidebar\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:three -->":
_, _ = w.WriteString("<ac:layout-section type=\"three\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:three_with_sidebars -->":
_, _ = w.WriteString("<ac:layout-section type=\"three_with_sidebars\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section end -->":
_, _ = w.WriteString("</ac:layout-section>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-cell -->":
_, _ = w.WriteString("<ac:layout-cell>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-cell end -->":
_, _ = w.WriteString("</ac:layout-cell>\n")
return ast.WalkContinue, nil
}
}
return r.goldmarkRenderHTMLBlock(w, source, node, entering)
}
func (r *ConfluenceHTMLBlockRenderer) goldmarkRenderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.HTMLBlock)
if entering {
if r.Unsafe {
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.SecureWrite(w, line.Value(source))
}
} else {
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
}
} else {
if n.HasClosure() {
if r.Unsafe {
closure := n.ClosureLine
r.Writer.SecureWrite(w, closure.Value(source))
} else {
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
}
}
}
return ast.WalkContinue, nil
}

118
pkg/mark/renderer/image.go Normal file
View File

@ -0,0 +1,118 @@
package renderer
import (
"bytes"
"path/filepath"
"strings"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceImageRenderer struct {
html.Config
Stdlib *stdlib.Lib
Path string
Attachments []attachment.Attachment
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceImageRenderer(stdlib *stdlib.Lib, attachments *[]attachment.Attachment, path string, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceImageRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
Attachments: *attachments,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceImageRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindImage, r.renderImage)
}
// renderImage renders an inline image
func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
attachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(r.Path), []string{string(n.Destination)})
// We were unable to resolve it locally, treat as URL
if err != nil {
escapedURL := string(n.Destination)
escapedURL = strings.ReplaceAll(escapedURL, "&", "&amp;")
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
"",
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
"",
escapedURL,
},
)
} else {
r.Attachments = append(r.Attachments, attachments[0])
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
"",
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
attachments[0].Filename,
"",
},
)
}
if err != nil {
return ast.WalkStop, err
}
return ast.WalkSkipChildren, nil
}
// https://github.com/yuin/goldmark/blob/c446c414ef3a41fb562da0ae5badd18f1502c42f/renderer/html/html.go
func nodeToHTMLText(n ast.Node, source []byte) []byte {
var buf bytes.Buffer
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if s, ok := c.(*ast.String); ok && s.IsCode() {
buf.Write(s.Text(source))
} else if !c.HasChildren() {
buf.Write(util.EscapeHTML(c.Text(source)))
} else {
buf.Write(nodeToHTMLText(c, source))
}
}
return buf.Bytes()
}

89
pkg/mark/renderer/link.go Normal file
View File

@ -0,0 +1,89 @@
package renderer
import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceLinkRenderer struct {
html.Config
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceLinkRenderer(opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceLinkRenderer{
Config: html.NewConfig(),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindLink, r.renderLink)
}
// renderLink renders links specifically for confluence
func (r *ConfluenceLinkRenderer) 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 {
return ast.WalkStop, err
}
if len(node.(*ast.Link).Destination) < 4 {
_, err := writer.Write(node.FirstChild().Text(source))
if err != nil {
return ast.WalkStop, err
}
} else {
_, err := writer.Write(node.(*ast.Link).Destination[3:])
if err != nil {
return ast.WalkStop, err
}
}
_, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA["))
if err != nil {
return ast.WalkStop, err
}
_, err = writer.Write(node.FirstChild().Text(source))
if err != nil {
return ast.WalkStop, err
}
_, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>"))
if err != nil {
return ast.WalkStop, err
}
}
return ast.WalkSkipChildren, nil
}
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 *ConfluenceLinkRenderer) 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
}