2024-03-22 20:16:23 +08:00
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markdown
import (
"strings"
2024-03-28 10:26:13 +08:00
"code.gitea.io/gitea/modules/svg"
2024-03-22 20:16:23 +08:00
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
2024-03-28 10:26:13 +08:00
"github.com/yuin/goldmark/util"
2024-03-22 20:16:23 +08:00
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
2024-06-04 23:35:29 +08:00
// renderAttention renders a quote marked with i.e. "> **Note**" or "> [!Warning]" with a corresponding svg
2024-03-28 10:26:13 +08:00
func ( r * HTMLRenderer ) renderAttention ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
if entering {
n := node . ( * Attention )
var octiconName string
switch n . AttentionType {
case "tip" :
octiconName = "light-bulb"
case "important" :
octiconName = "report"
case "warning" :
octiconName = "alert"
case "caution" :
octiconName = "stop"
default : // including "note"
octiconName = "info"
}
2024-11-18 13:25:42 +08:00
svgHTML := svg . RenderHTML ( "octicon-" + octiconName , 16 , "attention-icon attention-" + n . AttentionType )
_ , _ = w . WriteString ( string ( r . renderInternal . ProtectSafeAttrs ( svgHTML ) ) )
2024-03-28 10:26:13 +08:00
}
return ast . WalkContinue , nil
}
2024-06-04 23:35:29 +08:00
func ( g * ASTTransformer ) extractBlockquoteAttentionEmphasis ( firstParagraph ast . Node , reader text . Reader ) ( string , [ ] ast . Node ) {
if firstParagraph . ChildCount ( ) < 1 {
return "" , nil
}
node1 , ok := firstParagraph . FirstChild ( ) . ( * ast . Emphasis )
if ! ok {
return "" , nil
}
2024-10-31 20:05:54 +08:00
val1 := string ( node1 . Text ( reader . Source ( ) ) ) //nolint:staticcheck
2024-06-04 23:35:29 +08:00
attentionType := strings . ToLower ( val1 )
if g . attentionTypes . Contains ( attentionType ) {
return attentionType , [ ] ast . Node { node1 }
}
return "" , nil
}
2024-03-22 20:16:23 +08:00
2024-06-04 23:35:29 +08:00
func ( g * ASTTransformer ) extractBlockquoteAttention2 ( firstParagraph ast . Node , reader text . Reader ) ( string , [ ] ast . Node ) {
if firstParagraph . ChildCount ( ) < 2 {
return "" , nil
}
node1 , ok := firstParagraph . FirstChild ( ) . ( * ast . Text )
if ! ok {
return "" , nil
}
node2 , ok := node1 . NextSibling ( ) . ( * ast . Text )
if ! ok {
return "" , nil
}
val1 := string ( node1 . Segment . Value ( reader . Source ( ) ) )
val2 := string ( node2 . Segment . Value ( reader . Source ( ) ) )
if strings . HasPrefix ( val1 , ` \[! ` ) && val2 == ` \] ` {
attentionType := strings . ToLower ( val1 [ 3 : ] )
if g . attentionTypes . Contains ( attentionType ) {
return attentionType , [ ] ast . Node { node1 , node2 }
}
}
return "" , nil
}
func ( g * ASTTransformer ) extractBlockquoteAttention3 ( firstParagraph ast . Node , reader text . Reader ) ( string , [ ] ast . Node ) {
2024-03-22 20:16:23 +08:00
if firstParagraph . ChildCount ( ) < 3 {
2024-06-04 23:35:29 +08:00
return "" , nil
2024-03-22 20:16:23 +08:00
}
2024-03-27 17:09:25 +08:00
node1 , ok := firstParagraph . FirstChild ( ) . ( * ast . Text )
if ! ok {
2024-06-04 23:35:29 +08:00
return "" , nil
2024-03-27 17:09:25 +08:00
}
node2 , ok := node1 . NextSibling ( ) . ( * ast . Text )
if ! ok {
2024-06-04 23:35:29 +08:00
return "" , nil
2024-03-27 17:09:25 +08:00
}
node3 , ok := node2 . NextSibling ( ) . ( * ast . Text )
if ! ok {
2024-06-04 23:35:29 +08:00
return "" , nil
2024-03-22 20:16:23 +08:00
}
val1 := string ( node1 . Segment . Value ( reader . Source ( ) ) )
val2 := string ( node2 . Segment . Value ( reader . Source ( ) ) )
val3 := string ( node3 . Segment . Value ( reader . Source ( ) ) )
if val1 != "[" || val3 != "]" || ! strings . HasPrefix ( val2 , "!" ) {
2024-06-04 23:35:29 +08:00
return "" , nil
2024-03-22 20:16:23 +08:00
}
attentionType := strings . ToLower ( val2 [ 1 : ] )
2024-06-04 23:35:29 +08:00
if g . attentionTypes . Contains ( attentionType ) {
return attentionType , [ ] ast . Node { node1 , node2 , node3 }
}
return "" , nil
}
func ( g * ASTTransformer ) transformBlockquote ( v * ast . Blockquote , reader text . Reader ) ( ast . WalkStatus , error ) {
// We only want attention blockquotes when the AST looks like:
// > Text("[") Text("!TYPE") Text("]")
// > Text("\[!TYPE") TEXT("\]")
// > Text("**TYPE**")
// grab these nodes and make sure we adhere to the attention blockquote structure
firstParagraph := v . FirstChild ( )
g . applyElementDir ( firstParagraph )
attentionType , processedNodes := g . extractBlockquoteAttentionEmphasis ( firstParagraph , reader )
if attentionType == "" {
attentionType , processedNodes = g . extractBlockquoteAttention2 ( firstParagraph , reader )
}
if attentionType == "" {
attentionType , processedNodes = g . extractBlockquoteAttention3 ( firstParagraph , reader )
}
if attentionType == "" {
2024-03-22 20:16:23 +08:00
return ast . WalkContinue , nil
}
// color the blockquote
2024-11-18 13:25:42 +08:00
v . SetAttributeString ( g . renderInternal . SafeAttr ( "class" ) , [ ] byte ( g . renderInternal . SafeValue ( "attention-header attention-" + attentionType ) ) )
2024-03-22 20:16:23 +08:00
// create an emphasis to make it bold
attentionParagraph := ast . NewParagraph ( )
g . applyElementDir ( attentionParagraph )
emphasis := ast . NewEmphasis ( 2 )
2024-11-18 13:25:42 +08:00
emphasis . SetAttributeString ( g . renderInternal . SafeAttr ( "class" ) , [ ] byte ( g . renderInternal . SafeValue ( "attention-" + attentionType ) ) )
2024-03-22 20:16:23 +08:00
attentionAstString := ast . NewString ( [ ] byte ( cases . Title ( language . English ) . String ( attentionType ) ) )
// replace the ![TYPE] with a dedicated paragraph of icon+Type
emphasis . AppendChild ( emphasis , attentionAstString )
attentionParagraph . AppendChild ( attentionParagraph , NewAttention ( attentionType ) )
attentionParagraph . AppendChild ( attentionParagraph , emphasis )
firstParagraph . Parent ( ) . InsertBefore ( firstParagraph . Parent ( ) , firstParagraph , attentionParagraph )
2024-06-04 23:35:29 +08:00
for _ , processed := range processedNodes {
firstParagraph . RemoveChild ( firstParagraph , processed )
}
2024-03-22 20:16:23 +08:00
if firstParagraph . ChildCount ( ) == 0 {
firstParagraph . Parent ( ) . RemoveChild ( firstParagraph . Parent ( ) , firstParagraph )
}
return ast . WalkContinue , nil
}