Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alert blocks in markdown #29121

Merged
merged 16 commits into from
Feb 10, 2024
7 changes: 1 addition & 6 deletions modules/markup/markdown/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,7 @@ func IsColorPreview(node ast.Node) bool {
return ok
}

const (
AttentionNote string = "Note"
AttentionWarning string = "Warning"
)

// Attention is an inline for a color preview
// Attention is an inline for an attention
type Attention struct {
ast.BaseInline
AttentionType string
Expand Down
77 changes: 61 additions & 16 deletions modules/markup/markdown/goldmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
}
}

attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
Expand Down Expand Up @@ -197,18 +196,55 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
if css.ColorHandler(strings.ToLower(string(colorContent))) {
v.AppendChild(v, NewColorPreview(colorContent))
}
case *ast.Emphasis:
// check if inside blockquote for attention, expected hierarchy is
// Emphasis < Paragraph < Blockquote
blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote)
if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) {
fullText := string(n.Text(reader.Source()))
if fullText == AttentionNote || fullText == AttentionWarning {
v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText)))
v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText))
attentionMarkedBlockquotes.Add(blockquote)
}
case *ast.Blockquote:
// We only want attention blockquotes when the AST looks like:
// Text: "["
// Text: "!TYPE"
// Text(SoftLineBreak): "]"

// grab these nodes and make sure we adhere to the attention blockquote structure
firstParagraph := v.FirstChild()
if firstParagraph.ChildCount() < 3 {
return ast.WalkContinue, nil
}
firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text)
if !ok || string(firstTextNode.Segment.Value(reader.Source())) != "[" {
return ast.WalkContinue, nil
}
secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text)
if !ok || !attentionTypeRE.MatchString(string(secondTextNode.Segment.Value(reader.Source()))) {
return ast.WalkContinue, nil
}
thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text)
if !ok || string(thirdTextNode.Segment.Value(reader.Source())) != "]" {
return ast.WalkContinue, nil
}

// grab attention type from markdown source
attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!"))

// color the blockquote
v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType))

// create an emphasis to make it bold
emphasis := ast.NewEmphasis(2)
emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis)

// capitalize first letter
attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:]))

// replace the ![TYPE] with icon+Type
emphasis.AppendChild(emphasis, attentionText)
for i := 0; i < 2; i++ {
lineBreak := ast.NewText()
lineBreak.SetSoftLineBreak(true)
firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak)
}
firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType))
firstParagraph.RemoveChild(firstParagraph, firstTextNode)
firstParagraph.RemoveChild(firstParagraph, secondTextNode)
firstParagraph.RemoveChild(firstParagraph, thirdTextNode)
}
return ast.WalkContinue, nil
})
Expand Down Expand Up @@ -339,17 +375,23 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
_, _ = w.WriteString(`<span class="attention-icon attention-`)
_, _ = w.WriteString(`<span class="gt-mr-2 gt-vm attention-`)
n := node.(*Attention)
_, _ = w.WriteString(strings.ToLower(n.AttentionType))
_, _ = w.WriteString(`">`)

var octiconType string
switch n.AttentionType {
case AttentionNote:
case "note":
octiconType = "info"
case AttentionWarning:
case "tip":
octiconType = "light-bulb"
case "important":
octiconType = "report"
case "warning":
octiconType = "alert"
case "caution":
octiconType = "stop"
}
_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
} else {
Expand Down Expand Up @@ -417,7 +459,10 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, nil
}

var validNameRE = regexp.MustCompile("^[a-z ]+$")
var (
validNameRE = regexp.MustCompile("^[a-z ]+$")
attentionTypeRE = regexp.MustCompile("^!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)$")
)

func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
Expand Down
5 changes: 3 additions & 2 deletions modules/markup/sanitizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")

// For attention
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+$`)).OnElements("span", "strong")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-mr-2 gt-vm attention-\w+$`)).OnElements("span", "strong")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-(\w|-)+$`)).OnElements("svg")
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
policy.AllowAttrs("fill-rule", "d").OnElements("path")

Expand Down
39 changes: 32 additions & 7 deletions web_src/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1268,20 +1268,45 @@ img.ui.avatar,
border-radius: var(--border-radius);
}

.attention-icon {
vertical-align: text-top;
.attention {
color: unset !important;
}

.attention-note {
font-weight: unset;
color: var(--color-info-text);
blockquote.attention-note {
border-left-color: var(--color-blue-dark-1);
}
strong.attention-note, span.attention-note {
color: var(--color-blue-dark-1);
}

.attention-warning {
font-weight: unset;
blockquote.attention-tip {
border-left-color: var(--color-success-text);
}
strong.attention-tip, span.attention-tip {
color: var(--color-success-text);
}

blockquote.attention-important {
border-left-color: var(--color-violet-dark-1);
}
strong.attention-important, span.attention-important {
color: var(--color-violet-dark-1);
}

blockquote.attention-warning {
border-left-color: var(--color-warning-text);
}
strong.attention-warning, span.attention-warning {
color: var(--color-warning-text);
}

blockquote.attention-caution {
border-left-color: var(--color-red-dark-1);
}
strong.attention-caution, span.attention-caution {
color: var(--color-red-dark-1);
}

.center:not(.popup) {
text-align: center;
}
Expand Down
Loading