mirror of
https://github.com/kovetskiy/mark.git
synced 2025-04-24 05:42:40 +08:00
chore: Migrate to a goldmark extension
This commit is contained in:
parent
10d0778adf
commit
f57b4245f9
9
main.go
9
main.go
@ -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,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package mark
|
package attachment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,4 +1,4 @@
|
|||||||
package mark
|
package attachment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -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, "&", "&")
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
@ -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,
|
@ -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
|
||||||
}
|
}
|
172
pkg/mark/renderer/blockquote.go
Normal file
172
pkg/mark/renderer/blockquote.go
Normal 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
|
||||||
|
}
|
77
pkg/mark/renderer/codeblock.go
Normal file
77
pkg/mark/renderer/codeblock.go
Normal 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
|
||||||
|
}
|
187
pkg/mark/renderer/fencedcodeblock.go
Normal file
187
pkg/mark/renderer/fencedcodeblock.go
Normal 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
|
||||||
|
}
|
57
pkg/mark/renderer/heading.go
Normal file
57
pkg/mark/renderer/heading.go
Normal 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
|
||||||
|
}
|
104
pkg/mark/renderer/htmlblock.go
Normal file
104
pkg/mark/renderer/htmlblock.go
Normal 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
118
pkg/mark/renderer/image.go
Normal 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, "&", "&")
|
||||||
|
|
||||||
|
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
89
pkg/mark/renderer/link.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user