code.gitea.io/gitea@v1.22.3/modules/templates/util_render.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package templates
     5  
     6  import (
     7  	"context"
     8  	"encoding/hex"
     9  	"fmt"
    10  	"html/template"
    11  	"math"
    12  	"net/url"
    13  	"regexp"
    14  	"strings"
    15  	"unicode"
    16  
    17  	issues_model "code.gitea.io/gitea/models/issues"
    18  	"code.gitea.io/gitea/modules/emoji"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/markup"
    21  	"code.gitea.io/gitea/modules/markup/markdown"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	"code.gitea.io/gitea/modules/translation"
    24  	"code.gitea.io/gitea/modules/util"
    25  )
    26  
    27  // RenderCommitMessage renders commit message with XSS-safe and special links.
    28  func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
    29  	cleanMsg := template.HTMLEscapeString(msg)
    30  	// we can safely assume that it will not return any error, since there
    31  	// shouldn't be any special HTML.
    32  	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
    33  		Ctx:   ctx,
    34  		Metas: metas,
    35  	}, cleanMsg)
    36  	if err != nil {
    37  		log.Error("RenderCommitMessage: %v", err)
    38  		return ""
    39  	}
    40  	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
    41  	if len(msgLines) == 0 {
    42  		return template.HTML("")
    43  	}
    44  	return RenderCodeBlock(template.HTML(msgLines[0]))
    45  }
    46  
    47  // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
    48  // the provided default url, handling for special links without email to links.
    49  func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
    50  	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
    51  	lineEnd := strings.IndexByte(msgLine, '\n')
    52  	if lineEnd > 0 {
    53  		msgLine = msgLine[:lineEnd]
    54  	}
    55  	msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
    56  	if len(msgLine) == 0 {
    57  		return template.HTML("")
    58  	}
    59  
    60  	// we can safely assume that it will not return any error, since there
    61  	// shouldn't be any special HTML.
    62  	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
    63  		Ctx:         ctx,
    64  		DefaultLink: urlDefault,
    65  		Metas:       metas,
    66  	}, template.HTMLEscapeString(msgLine))
    67  	if err != nil {
    68  		log.Error("RenderCommitMessageSubject: %v", err)
    69  		return template.HTML("")
    70  	}
    71  	return RenderCodeBlock(template.HTML(renderedMessage))
    72  }
    73  
    74  // RenderCommitBody extracts the body of a commit message without its title.
    75  func RenderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML {
    76  	msgLine := strings.TrimSpace(msg)
    77  	lineEnd := strings.IndexByte(msgLine, '\n')
    78  	if lineEnd > 0 {
    79  		msgLine = msgLine[lineEnd+1:]
    80  	} else {
    81  		return ""
    82  	}
    83  	msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
    84  	if len(msgLine) == 0 {
    85  		return ""
    86  	}
    87  
    88  	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
    89  		Ctx:   ctx,
    90  		Metas: metas,
    91  	}, template.HTMLEscapeString(msgLine))
    92  	if err != nil {
    93  		log.Error("RenderCommitMessage: %v", err)
    94  		return ""
    95  	}
    96  	return template.HTML(renderedMessage)
    97  }
    98  
    99  // Match text that is between back ticks.
   100  var codeMatcher = regexp.MustCompile("`([^`]+)`")
   101  
   102  // RenderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
   103  func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
   104  	htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `<code class="inline-code-block">$1</code>`) // replace with HTML <code> tags
   105  	return template.HTML(htmlWithCodeTags)
   106  }
   107  
   108  // RenderIssueTitle renders issue/pull title with defined post processors
   109  func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML {
   110  	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
   111  		Ctx:   ctx,
   112  		Metas: metas,
   113  	}, template.HTMLEscapeString(text))
   114  	if err != nil {
   115  		log.Error("RenderIssueTitle: %v", err)
   116  		return template.HTML("")
   117  	}
   118  	return template.HTML(renderedText)
   119  }
   120  
   121  // RenderLabel renders a label
   122  // locale is needed due to an import cycle with our context providing the `Tr` function
   123  func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
   124  	var extraCSSClasses string
   125  	textColor := util.ContrastColor(label.Color)
   126  	labelScope := label.ExclusiveScope()
   127  	descriptionText := emoji.ReplaceAliases(label.Description)
   128  
   129  	if label.IsArchived() {
   130  		extraCSSClasses = "archived-label"
   131  		descriptionText = fmt.Sprintf("(%s) %s", locale.TrString("archived"), descriptionText)
   132  	}
   133  
   134  	if labelScope == "" {
   135  		// Regular label
   136  		return HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`,
   137  			extraCSSClasses, textColor, label.Color, descriptionText, RenderEmoji(ctx, label.Name))
   138  	}
   139  
   140  	// Scoped label
   141  	scopeHTML := RenderEmoji(ctx, labelScope)
   142  	itemHTML := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
   143  
   144  	// Make scope and item background colors slightly darker and lighter respectively.
   145  	// More contrast needed with higher luminance, empirically tweaked.
   146  	luminance := util.GetRelativeLuminance(label.Color)
   147  	contrast := 0.01 + luminance*0.03
   148  	// Ensure we add the same amount of contrast also near 0 and 1.
   149  	darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
   150  	lighten := contrast + math.Max(contrast-luminance, 0.0)
   151  	// Compute factor to keep RGB values proportional.
   152  	darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
   153  	lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
   154  
   155  	r, g, b := util.HexToRBGColor(label.Color)
   156  	scopeBytes := []byte{
   157  		uint8(math.Min(math.Round(r*darkenFactor), 255)),
   158  		uint8(math.Min(math.Round(g*darkenFactor), 255)),
   159  		uint8(math.Min(math.Round(b*darkenFactor), 255)),
   160  	}
   161  	itemBytes := []byte{
   162  		uint8(math.Min(math.Round(r*lightenFactor), 255)),
   163  		uint8(math.Min(math.Round(g*lightenFactor), 255)),
   164  		uint8(math.Min(math.Round(b*lightenFactor), 255)),
   165  	}
   166  
   167  	itemColor := "#" + hex.EncodeToString(itemBytes)
   168  	scopeColor := "#" + hex.EncodeToString(scopeBytes)
   169  
   170  	return HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
   171  		`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
   172  		`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
   173  		`</span>`,
   174  		extraCSSClasses, descriptionText,
   175  		textColor, scopeColor, scopeHTML,
   176  		textColor, itemColor, itemHTML)
   177  }
   178  
   179  // RenderEmoji renders html text with emoji post processors
   180  func RenderEmoji(ctx context.Context, text string) template.HTML {
   181  	renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
   182  		template.HTMLEscapeString(text))
   183  	if err != nil {
   184  		log.Error("RenderEmoji: %v", err)
   185  		return template.HTML("")
   186  	}
   187  	return template.HTML(renderedText)
   188  }
   189  
   190  // ReactionToEmoji renders emoji for use in reactions
   191  func ReactionToEmoji(reaction string) template.HTML {
   192  	val := emoji.FromCode(reaction)
   193  	if val != nil {
   194  		return template.HTML(val.Emoji)
   195  	}
   196  	val = emoji.FromAlias(reaction)
   197  	if val != nil {
   198  		return template.HTML(val.Emoji)
   199  	}
   200  	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
   201  }
   202  
   203  func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
   204  	output, err := markdown.RenderString(&markup.RenderContext{
   205  		Ctx:   ctx,
   206  		Metas: map[string]string{"mode": "document"},
   207  	}, input)
   208  	if err != nil {
   209  		log.Error("RenderString: %v", err)
   210  	}
   211  	return output
   212  }
   213  
   214  func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
   215  	isPullRequest := issue != nil && issue.IsPull
   216  	baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues"))
   217  	htmlCode := `<span class="labels-list">`
   218  	for _, label := range labels {
   219  		// Protect against nil value in labels - shouldn't happen but would cause a panic if so
   220  		if label == nil {
   221  			continue
   222  		}
   223  		htmlCode += fmt.Sprintf(`<a href="%s?labels=%d">%s</a>`, baseLink, label.ID, RenderLabel(ctx, locale, label))
   224  	}
   225  	htmlCode += "</span>"
   226  	return template.HTML(htmlCode)
   227  }