// 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"
"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"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
)
// RenderCommitMessage renders commit message with XSS-safe and special links.
func RenderCommitMessage ( ctx context . Context , msg string , metas map [ string ] string ) template . HTML {
cleanMsg := template . HTMLEscapeString ( msg )
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
fullMessage , err := markup . RenderCommitMessage ( & markup . RenderContext {
Ctx : ctx ,
Metas : metas ,
} , cleanMsg )
if err != nil {
log . Error ( "RenderCommitMessage: %v" , err )
return ""
}
msgLines := strings . Split ( strings . TrimSpace ( fullMessage ) , "\n" )
if len ( msgLines ) == 0 {
return template . HTML ( "" )
}
return template . HTML ( msgLines [ 0 ] )
}
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
// the provided default url, handling for special links without email to links.
func RenderCommitMessageLinkSubject ( ctx context . Context , msg , urlDefault string , metas map [ string ] string ) template . HTML {
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 {
return template . HTML ( "" )
}
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
renderedMessage , err := markup . RenderCommitMessageSubject ( & markup . RenderContext {
Ctx : ctx ,
DefaultLink : urlDefault ,
Metas : metas ,
} , template . HTMLEscapeString ( msgLine ) )
if err != nil {
log . Error ( "RenderCommitMessageSubject: %v" , err )
return template . HTML ( "" )
}
return template . HTML ( renderedMessage )
}
// RenderCommitBody extracts the body of a commit message without its title.
func RenderCommitBody ( ctx context . Context , msg string , metas map [ string ] string ) template . HTML {
msgLine := strings . TrimSpace ( msg )
lineEnd := strings . IndexByte ( msgLine , '\n' )
if lineEnd > 0 {
msgLine = msgLine [ lineEnd + 1 : ]
} else {
return ""
}
msgLine = strings . TrimLeftFunc ( msgLine , unicode . IsSpace )
if len ( msgLine ) == 0 {
return ""
}
renderedMessage , err := markup . RenderCommitMessage ( & markup . RenderContext {
Ctx : ctx ,
Metas : metas ,
} , template . HTMLEscapeString ( msgLine ) )
if err != nil {
log . Error ( "RenderCommitMessage: %v" , err )
return ""
}
return template . HTML ( renderedMessage )
}
// Match text that is between back ticks.
var codeMatcher = regexp . MustCompile ( "`([^`]+)`" )
// RenderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
func RenderCodeBlock ( htmlEscapedTextToRender template . HTML ) template . HTML {
htmlWithCodeTags := codeMatcher . ReplaceAllString ( string ( htmlEscapedTextToRender ) , ` <code class="inline-code-block">$1</code> ` ) // replace with HTML <code> tags
return template . HTML ( htmlWithCodeTags )
}
// RenderIssueTitle renders issue/pull title with defined post processors
func RenderIssueTitle ( ctx context . Context , text string , metas map [ string ] string ) template . HTML {
renderedText , err := markup . RenderIssueTitle ( & markup . RenderContext {
Ctx : ctx ,
Metas : metas ,
} , template . HTMLEscapeString ( text ) )
if err != nil {
log . Error ( "RenderIssueTitle: %v" , err )
return template . HTML ( "" )
}
return template . HTML ( renderedText )
}
// RenderLabel renders a label
// locale is needed due to an import cycle with our context providing the `Tr` function
func RenderLabel ( ctx context . Context , locale translation . Locale , label * issues_model . Label ) template . HTML {
var (
archivedCSSClass string
textColor = "#111"
labelScope = label . ExclusiveScope ( )
)
r , g , b := util . HexToRBGColor ( label . Color )
// Determine if label text should be light or dark to be readable on background color
if util . UseLightTextOnBackground ( r , g , b ) {
textColor = "#eee"
}
description := emoji . ReplaceAliases ( template . HTMLEscapeString ( label . Description ) )
if label . IsArchived ( ) {
archivedCSSClass = "archived-label"
description = fmt . Sprintf ( "(%s) %s" , locale . TrString ( "archived" ) , description )
}
if labelScope == "" {
// Regular label
s := fmt . Sprintf ( "<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>" ,
archivedCSSClass , textColor , label . Color , description , RenderEmoji ( ctx , label . Name ) )
return template . HTML ( s )
}
// Scoped label
scopeText := RenderEmoji ( ctx , labelScope )
itemText := RenderEmoji ( ctx , label . Name [ len ( labelScope ) + 1 : ] )
// Make scope and item background colors slightly darker and lighter respectively.
// More contrast needed with higher luminance, empirically tweaked.
luminance := util . GetLuminance ( r , g , b )
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 )
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 ) ) ,
}
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 )
s := fmt . Sprintf ( "<span class='ui label %s scope-parent' data-tooltip-content title='%s'>" +
"<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>" ,
archivedCSSClass , description ,
textColor , scopeColor , scopeText ,
textColor , itemColor , itemText )
return template . HTML ( s )
}
// RenderEmoji renders html text with emoji post processors
func RenderEmoji ( ctx context . Context , text string ) template . HTML {
renderedText , err := markup . RenderEmoji ( & markup . RenderContext { Ctx : ctx } ,
template . HTMLEscapeString ( text ) )
if err != nil {
log . Error ( "RenderEmoji: %v" , err )
return template . HTML ( "" )
}
return template . HTML ( renderedText )
}
// ReactionToEmoji renders emoji for use in reactions
func ReactionToEmoji ( reaction string ) template . HTML {
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 ) ) )
}
func RenderMarkdownToHtml ( ctx context . Context , input string ) template . HTML { //nolint:revive
output , err := markdown . RenderString ( & markup . RenderContext {
Ctx : ctx ,
Metas : map [ string ] string { "mode" : "document" } ,
} , input )
if err != nil {
log . Error ( "RenderString: %v" , err )
}
return output
}
func RenderLabels ( ctx context . Context , locale translation . Locale , labels [ ] * issues_model . Label , repoLink string ) template . HTML {
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
}
htmlCode += fmt . Sprintf ( "<a href='%s/issues?labels=%d'>%s</a> " ,
repoLink , label . ID , RenderLabel ( ctx , locale , label ) )
}
htmlCode += "</span>"
return template . HTML ( htmlCode )
}