From 12865ae9c6c164af6272b41e65b2cf2ea7a5e4b3 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sat, 10 Feb 2024 20:43:09 +0200 Subject: [PATCH] Add alert blocks in markdown (#29121) - Follows https://github.com/go-gitea/gitea/pull/21711 - Closes https://github.com/go-gitea/gitea/issues/28316 Implement GitHub's alert blocks markdown feature Docs: - https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts - https://github.com/orgs/community/discussions/16925 ### Before ![image](https://github.com/go-gitea/gitea/assets/20454870/14f7b02a-5de5-4fd0-8437-a055dadb31f2) ### After ![image](https://github.com/go-gitea/gitea/assets/20454870/ed06a869-e545-42f1-bf25-4ba20b1be196) ## :warning: BREAKING :warning: The old syntax no longer works How to migrate: If you used ```md > **Note** My note ``` Switch to ```md > [!NOTE] > My note ``` --------- Signed-off-by: Yarden Shoham Co-authored-by: silverwind Co-authored-by: Giteabot --- modules/markup/markdown/ast.go | 7 +-- modules/markup/markdown/goldmark.go | 77 +++++++++++++++++++++++------ modules/markup/sanitizer.go | 5 +- web_src/css/base.css | 39 ++++++++++++--- 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index 3e6e291ab25a..77ce5cb359b9 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 178e3d2fddf8..36ce6397f4d8 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,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 }) @@ -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(``) 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 +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 { diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 992e85b9898d..ffc33c3b8e5b 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -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") diff --git a/web_src/css/base.css b/web_src/css/base.css index 198e87c0e202..ea32aac6f777 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1268,20 +1268,45 @@ img.ui.avatar, border-radius: var(--border-radius); } -.attention-icon { - vertical-align: text-top; +.attention { + color: var(--color-text) !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; }