mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00:00 
			
		
		
		
	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  ### After  ## ⚠️ BREAKING ⚠️ 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 <git@yardenshoham.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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(`<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 { | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user