From cc8444deecbc63c263de57649d30ececc29c6e8c Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Fri, 9 Feb 2024 22:08:35 +0000 Subject: [PATCH] Add alert blocks in markdown Signed-off-by: Yarden Shoham --- modules/markup/markdown/ast.go | 7 +-- modules/markup/markdown/goldmark.go | 82 +++++++++++++++++++++++------ modules/markup/sanitizer.go | 3 +- web_src/css/base.css | 44 ++++++++++++++-- 4 files changed, 109 insertions(+), 27 deletions(-) diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index 3e6e291ab25a4..77ce5cb359b96 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -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 diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 178e3d2fddf87..a2483ad435941 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -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 @@ -197,18 +196,62 @@ 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 + firstParagraph := v.FirstChild() + if firstParagraph.ChildCount() < 3 { + return ast.WalkContinue, nil + } + firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text) + if !ok { + return ast.WalkContinue, nil + } + secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text) + if !ok { + return ast.WalkContinue, nil + } + thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text) + if !ok { + return ast.WalkContinue, nil } + + // make sure we adhere to the attention blockquote structure + if string(firstTextNode.Segment.Value(reader.Source())) != "[" || + !attentionTypeRE.MatchString(string(secondTextNode.Segment.Value(reader.Source()))) || + 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("gt-font-bold 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 }) @@ -346,10 +389,16 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast 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 { @@ -417,7 +466,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 { diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 992e85b9898d9..176ad383abfe8 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -64,7 +64,8 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span") // For attention - policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-font-bold 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("viewBox", "width", "height", "aria-hidden").OnElements("svg") diff --git a/web_src/css/base.css b/web_src/css/base.css index 198e87c0e202f..19f1e99f9cf3c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1268,20 +1268,54 @@ img.ui.avatar, border-radius: var(--border-radius); } +.attention { + color: unset !important; +} + .attention-icon { vertical-align: text-top; } -.attention-note { - font-weight: unset; - color: var(--color-info-text); +blockquote.attention-note { + border-left-color: var(--color-blue-dark-2); +} + +blockquote.attention-tip { + border-left-color: var(--color-success-border); } -.attention-warning { - font-weight: unset; +blockquote.attention-important { + border-left-color: var(--color-violet-dark-2); +} + +blockquote.attention-warning { + border-left-color: var(--color-warning-text); +} + +blockquote.attention-caution { + border-left-color: var(--color-red-dark-2); +} + +strong.attention-note, .attention-icon.attention-note { + color: var(--color-blue-dark-2); +} + +strong.attention-tip, .attention-icon.attention-tip { + color: var(--color-success-border); +} + +strong.attention-important, .attention-icon.attention-important { + color: var(--color-violet-dark-2); +} + +strong.attention-warning, .attention-icon.attention-warning { color: var(--color-warning-text); } +strong.attention-caution, .attention-icon.attention-caution { + color: var(--color-red-dark-2); +} + .center:not(.popup) { text-align: center; }