2023-04-29 12:02:29 +00:00
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"context"
"encoding/hex"
"fmt"
"html/template"
"math"
"net/url"
"regexp"
"strings"
"unicode"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/emoji"
2024-11-18 05:25:42 +00:00
"code.gitea.io/gitea/modules/htmlutil"
2023-04-29 12:02:29 +00:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
2024-03-12 17:32:05 +00:00
"code.gitea.io/gitea/modules/translation"
2023-05-10 11:19:03 +00:00
"code.gitea.io/gitea/modules/util"
2023-04-29 12:02:29 +00:00
)
2024-11-05 06:04:26 +00:00
type RenderUtils struct {
ctx context . Context
}
func NewRenderUtils ( ctx context . Context ) * RenderUtils {
return & RenderUtils { ctx : ctx }
}
2023-04-29 12:02:29 +00:00
// RenderCommitMessage renders commit message with XSS-safe and special links.
2024-11-05 06:04:26 +00:00
func ( ut * RenderUtils ) RenderCommitMessage ( msg string , metas map [ string ] string ) template . HTML {
2023-04-29 12:02:29 +00:00
cleanMsg := template . HTMLEscapeString ( msg )
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
2024-12-04 01:39:33 +00:00
fullMessage , err := markup . PostProcessCommitMessage ( markup . NewRenderContext ( ut . ctx ) . WithMetas ( metas ) , cleanMsg )
2023-04-29 12:02:29 +00:00
if err != nil {
2024-12-04 01:39:33 +00:00
log . Error ( "PostProcessCommitMessage: %v" , err )
2023-04-29 12:02:29 +00:00
return ""
}
msgLines := strings . Split ( strings . TrimSpace ( fullMessage ) , "\n" )
if len ( msgLines ) == 0 {
return template . HTML ( "" )
}
2024-06-18 22:32:45 +00:00
return renderCodeBlock ( template . HTML ( msgLines [ 0 ] ) )
2023-04-29 12:02:29 +00:00
}
2024-11-05 06:04:26 +00:00
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
2023-04-29 12:02:29 +00:00
// the provided default url, handling for special links without email to links.
2024-11-05 06:04:26 +00:00
func ( ut * RenderUtils ) RenderCommitMessageLinkSubject ( msg , urlDefault string , metas map [ string ] string ) template . HTML {
2023-04-29 12:02:29 +00:00
msgLine := strings . TrimLeftFunc ( msg , unicode . IsSpace )
lineEnd := strings . IndexByte ( msgLine , '\n' )
if lineEnd > 0 {
msgLine = msgLine [ : lineEnd ]
}
msgLine = strings . TrimRightFunc ( msgLine , unicode . IsSpace )
if len ( msgLine ) == 0 {
2024-11-16 08:41:44 +00:00
return ""
2023-04-29 12:02:29 +00:00
}
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
2024-12-04 01:39:33 +00:00
renderedMessage , err := markup . PostProcessCommitMessageSubject ( markup . NewRenderContext ( ut . ctx ) . WithMetas ( metas ) , urlDefault , template . HTMLEscapeString ( msgLine ) )
2023-04-29 12:02:29 +00:00
if err != nil {
2024-12-04 01:39:33 +00:00
log . Error ( "PostProcessCommitMessageSubject: %v" , err )
2024-11-16 08:41:44 +00:00
return ""
2023-04-29 12:02:29 +00:00
}
2024-06-18 22:32:45 +00:00
return renderCodeBlock ( template . HTML ( renderedMessage ) )
2023-04-29 12:02:29 +00:00
}
2024-11-05 06:04:26 +00:00
// RenderCommitBody extracts the body of a commit message without its title.
func ( ut * RenderUtils ) RenderCommitBody ( msg string , metas map [ string ] string ) template . HTML {
2023-06-21 09:14:34 +00:00
msgLine := strings . TrimSpace ( msg )
2023-04-29 12:02:29 +00:00
lineEnd := strings . IndexByte ( msgLine , '\n' )
if lineEnd > 0 {
msgLine = msgLine [ lineEnd + 1 : ]
} else {
2023-06-21 09:14:34 +00:00
return ""
2023-04-29 12:02:29 +00:00
}
msgLine = strings . TrimLeftFunc ( msgLine , unicode . IsSpace )
if len ( msgLine ) == 0 {
2023-06-21 09:14:34 +00:00
return ""
2023-04-29 12:02:29 +00:00
}
2024-12-04 01:39:33 +00:00
renderedMessage , err := markup . PostProcessCommitMessage ( markup . NewRenderContext ( ut . ctx ) . WithMetas ( metas ) , template . HTMLEscapeString ( msgLine ) )
2023-04-29 12:02:29 +00:00
if err != nil {
2024-12-04 01:39:33 +00:00
log . Error ( "PostProcessCommitMessage: %v" , err )
2023-04-29 12:02:29 +00:00
return ""
}
return template . HTML ( renderedMessage )
}
// Match text that is between back ticks.
var codeMatcher = regexp . MustCompile ( "`([^`]+)`" )
2024-06-18 22:32:45 +00:00
// renderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
func renderCodeBlock ( htmlEscapedTextToRender template . HTML ) template . HTML {
2023-08-31 05:01:01 +00:00
htmlWithCodeTags := codeMatcher . ReplaceAllString ( string ( htmlEscapedTextToRender ) , ` <code class="inline-code-block">$1</code> ` ) // replace with HTML <code> tags
2023-04-29 12:02:29 +00:00
return template . HTML ( htmlWithCodeTags )
}
2024-11-05 06:04:26 +00:00
// RenderIssueTitle renders issue/pull title with defined post processors
func ( ut * RenderUtils ) RenderIssueTitle ( text string , metas map [ string ] string ) template . HTML {
2024-12-04 01:39:33 +00:00
renderedText , err := markup . PostProcessIssueTitle ( markup . NewRenderContext ( ut . ctx ) . WithMetas ( metas ) , template . HTMLEscapeString ( text ) )
2023-04-29 12:02:29 +00:00
if err != nil {
2024-12-04 01:39:33 +00:00
log . Error ( "PostProcessIssueTitle: %v" , err )
2024-11-05 06:04:26 +00:00
return ""
2023-04-29 12:02:29 +00:00
}
2024-12-04 01:39:33 +00:00
return renderCodeBlock ( template . HTML ( renderedText ) )
}
// RenderIssueSimpleTitle only renders with emoji and inline code block
func ( ut * RenderUtils ) RenderIssueSimpleTitle ( text string ) template . HTML {
ret := ut . RenderEmoji ( text )
ret = renderCodeBlock ( ret )
return ret
2023-04-29 12:02:29 +00:00
}
2024-11-05 06:04:26 +00:00
// RenderLabel renders a label
func ( ut * RenderUtils ) RenderLabel ( label * issues_model . Label ) template . HTML {
locale := ut . ctx . Value ( translation . ContextKey ) . ( translation . Locale )
2024-04-30 02:36:32 +00:00
var extraCSSClasses string
textColor := util . ContrastColor ( label . Color )
labelScope := label . ExclusiveScope ( )
descriptionText := emoji . ReplaceAliases ( label . Description )
2023-04-29 12:02:29 +00:00
2024-03-13 06:04:07 +00:00
if label . IsArchived ( ) {
2024-04-30 02:36:32 +00:00
extraCSSClasses = "archived-label"
descriptionText = fmt . Sprintf ( "(%s) %s" , locale . TrString ( "archived" ) , descriptionText )
2024-03-12 17:32:05 +00:00
}
2023-04-29 12:02:29 +00:00
if labelScope == "" {
// Regular label
2024-11-18 05:25:42 +00:00
return htmlutil . HTMLFormat ( ` <div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div> ` ,
2024-11-05 06:04:26 +00:00
extraCSSClasses , textColor , label . Color , descriptionText , ut . RenderEmoji ( label . Name ) )
2023-04-29 12:02:29 +00:00
}
// Scoped label
2024-11-05 06:04:26 +00:00
scopeHTML := ut . RenderEmoji ( labelScope )
itemHTML := ut . RenderEmoji ( label . Name [ len ( labelScope ) + 1 : ] )
2023-04-29 12:02:29 +00:00
2023-05-10 11:19:03 +00:00
// Make scope and item background colors slightly darker and lighter respectively.
// More contrast needed with higher luminance, empirically tweaked.
2024-04-07 16:19:25 +00:00
luminance := util . GetRelativeLuminance ( label . Color )
2023-05-10 11:19:03 +00:00
contrast := 0.01 + luminance * 0.03
// Ensure we add the same amount of contrast also near 0 and 1.
darken := contrast + math . Max ( luminance + contrast - 1.0 , 0.0 )
lighten := contrast + math . Max ( contrast - luminance , 0.0 )
// Compute factor to keep RGB values proportional.
darkenFactor := math . Max ( luminance - darken , 0.0 ) / math . Max ( luminance , 1.0 / 255.0 )
lightenFactor := math . Min ( luminance + lighten , 1.0 ) / math . Max ( luminance , 1.0 / 255.0 )
2024-04-07 16:19:25 +00:00
r , g , b := util . HexToRBGColor ( label . Color )
2023-05-10 11:19:03 +00:00
scopeBytes := [ ] byte {
uint8 ( math . Min ( math . Round ( r * darkenFactor ) , 255 ) ) ,
uint8 ( math . Min ( math . Round ( g * darkenFactor ) , 255 ) ) ,
uint8 ( math . Min ( math . Round ( b * darkenFactor ) , 255 ) ) ,
2023-04-29 12:02:29 +00:00
}
2023-05-10 11:19:03 +00:00
itemBytes := [ ] byte {
uint8 ( math . Min ( math . Round ( r * lightenFactor ) , 255 ) ) ,
uint8 ( math . Min ( math . Round ( g * lightenFactor ) , 255 ) ) ,
uint8 ( math . Min ( math . Round ( b * lightenFactor ) , 255 ) ) ,
}
itemColor := "#" + hex . EncodeToString ( itemBytes )
scopeColor := "#" + hex . EncodeToString ( scopeBytes )
2023-04-29 12:02:29 +00:00
2024-11-18 05:25:42 +00:00
return htmlutil . HTMLFormat ( ` <span class="ui label %s scope-parent" data-tooltip-content title="%s"> ` +
2024-04-30 02:36:32 +00:00
` <div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div> ` +
` <div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div> ` +
` </span> ` ,
extraCSSClasses , descriptionText ,
textColor , scopeColor , scopeHTML ,
textColor , itemColor , itemHTML )
2023-04-29 12:02:29 +00:00
}
2024-11-05 06:04:26 +00:00
// RenderEmoji renders html text with emoji post processors
func ( ut * RenderUtils ) RenderEmoji ( text string ) template . HTML {
2024-12-04 01:39:33 +00:00
renderedText , err := markup . PostProcessEmoji ( markup . NewRenderContext ( ut . ctx ) , template . HTMLEscapeString ( text ) )
2023-04-29 12:02:29 +00:00
if err != nil {
log . Error ( "RenderEmoji: %v" , err )
2024-11-05 06:04:26 +00:00
return ""
2023-04-29 12:02:29 +00:00
}
return template . HTML ( renderedText )
}
2024-06-18 22:32:45 +00:00
// reactionToEmoji renders emoji for use in reactions
func reactionToEmoji ( reaction string ) template . HTML {
2023-04-29 12:02:29 +00:00
val := emoji . FromCode ( reaction )
if val != nil {
return template . HTML ( val . Emoji )
}
val = emoji . FromAlias ( reaction )
if val != nil {
return template . HTML ( val . Emoji )
}
return template . HTML ( fmt . Sprintf ( ` <img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img> ` , reaction , setting . StaticURLPrefix , url . PathEscape ( reaction ) ) )
}
2024-11-05 06:04:26 +00:00
func ( ut * RenderUtils ) MarkdownToHtml ( input string ) template . HTML { //nolint:revive
2024-11-22 05:48:09 +00:00
output , err := markdown . RenderString ( markup . NewRenderContext ( ut . ctx ) . WithMetas ( markup . ComposeSimpleDocumentMetas ( ) ) , input )
2023-04-29 12:02:29 +00:00
if err != nil {
log . Error ( "RenderString: %v" , err )
}
2024-03-01 07:11:51 +00:00
return output
2023-04-29 12:02:29 +00:00
}
2024-11-05 06:04:26 +00:00
func ( ut * RenderUtils ) RenderLabels ( labels [ ] * issues_model . Label , repoLink string , issue * issues_model . Issue ) template . HTML {
2024-04-13 10:05:33 +00:00
isPullRequest := issue != nil && issue . IsPull
baseLink := fmt . Sprintf ( "%s/%s" , repoLink , util . Iif ( isPullRequest , "pulls" , "issues" ) )
2023-04-29 12:02:29 +00:00
htmlCode := ` <span class="labels-list"> `
for _ , label := range labels {
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
if label == nil {
continue
}
2024-11-05 06:04:26 +00:00
htmlCode += fmt . Sprintf ( ` <a href="%s?labels=%d">%s</a> ` , baseLink , label . ID , ut . RenderLabel ( label ) )
2023-04-29 12:02:29 +00:00
}
htmlCode += "</span>"
return template . HTML ( htmlCode )
}