2023-09-01 22:59:04 +02:00
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
2024-08-27 15:39:39 +02:00
Tip
2023-09-01 22:59:04 +02:00
None
)
func ( t BlockQuoteType ) String ( ) string {
2024-08-27 15:39:39 +02:00
return [ ] string { "info" , "note" , "warning" , "tip" , "none" } [ t ]
2023-09-01 22:59:04 +02:00
}
type BlockQuoteLevelMap map [ ast . Node ] int
func ( m BlockQuoteLevelMap ) Level ( node ast . Node ) int {
return m [ node ]
}
2024-08-27 15:39:39 +02:00
type BlockQuoteClassifier struct {
patternMap map [ string ] * regexp . Regexp
}
func LegacyBlockQuoteClassifier ( ) BlockQuoteClassifier {
return BlockQuoteClassifier {
patternMap : map [ string ] * regexp . Regexp {
"info" : regexp . MustCompile ( ` (?i)info ` ) ,
"note" : regexp . MustCompile ( ` (?i)note ` ) ,
"warn" : regexp . MustCompile ( ` (?i)warn ` ) ,
"tip" : regexp . MustCompile ( ` (?i)tip ` ) ,
} ,
}
}
func GHAlertsBlockQuoteClassifier ( ) BlockQuoteClassifier {
return BlockQuoteClassifier {
patternMap : map [ string ] * regexp . Regexp {
"info" : regexp . MustCompile ( ` (?i)^\!(note|important) ` ) ,
"note" : regexp . MustCompile ( ` (?i)^\!warning ` ) ,
"warn" : regexp . MustCompile ( ` (?i)^\!caution ` ) ,
"tip" : regexp . MustCompile ( ` (?i)^\!tip ` ) ,
} ,
}
}
2023-09-01 22:59:04 +02:00
// ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
2024-08-27 15:39:39 +02:00
func ( classifier BlockQuoteClassifier ) ClassifyingBlockQuote ( literal string ) BlockQuoteType {
2023-09-01 22:59:04 +02:00
var t = None
switch {
2024-08-27 15:39:39 +02:00
case classifier . patternMap [ "info" ] . MatchString ( literal ) :
2023-09-01 22:59:04 +02:00
t = Info
2024-08-27 15:39:39 +02:00
case classifier . patternMap [ "note" ] . MatchString ( literal ) :
2023-09-01 22:59:04 +02:00
t = Note
2024-08-27 15:39:39 +02:00
case classifier . patternMap [ "warn" ] . MatchString ( literal ) :
2023-09-01 22:59:04 +02:00
t = Warn
2024-08-27 15:39:39 +02:00
case classifier . patternMap [ "tip" ] . MatchString ( literal ) :
t = Tip
2023-09-01 22:59:04 +02:00
}
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
2024-08-27 15:39:39 +02:00
var legacyClassifier = LegacyBlockQuoteClassifier ( )
var ghAlertsClassifier = GHAlertsBlockQuoteClassifier ( )
2023-09-01 22:59:04 +02:00
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 )
2024-10-21 13:24:49 +02:00
t = legacyClassifier . ClassifyingBlockQuote ( string ( n . Value ( source ) ) )
2024-08-27 15:39:39 +02:00
// If the node is a text node but classification returned none do not give up!
// Find the next two sibling nodes midNode and rightNode,
// 1. If both are also a text node
// 2. and the original node (node) text value is '['
// 3. and the rightNode text value is ']'
// It means with high degree of confidence that the original md doc contains a Github alert type of blockquote
// Classifying the next text type node (midNode) will confirm that.
if t == None {
midNode := node . NextSibling ( )
2024-10-05 23:56:07 +02:00
if midNode != nil && midNode . Kind ( ) == ast . KindText {
rightNode := midNode . NextSibling ( )
2024-08-27 15:39:39 +02:00
midTextNode := midNode . ( * ast . Text )
if rightNode != nil && rightNode . Kind ( ) == ast . KindText {
rightTextNode := rightNode . ( * ast . Text )
2024-10-21 13:24:49 +02:00
if string ( n . Value ( source ) ) == "[" && string ( rightTextNode . Value ( source ) ) == "]" {
t = ghAlertsClassifier . ClassifyingBlockQuote ( string ( midTextNode . Value ( source ) ) )
2024-08-27 15:39:39 +02:00
}
}
}
}
2023-09-01 22:59:04 +02:00
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 )
2024-08-27 15:39:39 +02:00
t = legacyClassifier . ClassifyingBlockQuote ( string ( line . Value ( source ) ) )
2023-09-01 22:59:04 +02:00
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
}