2023-04-29 15:02:29 +03: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"
"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-29 21:07:01 +03:00
"code.gitea.io/gitea/modules/translation"
2023-05-10 14:19:03 +03:00
"code.gitea.io/gitea/modules/util"
2023-04-29 15:02:29 +03:00
)
// RenderCommitMessage renders commit message with XSS-safe and special links.
2024-01-15 11:49:24 +03:00
func RenderCommitMessage ( ctx context . Context , msg string , metas map [ string ] string ) template . HTML {
2023-04-29 15:02:29 +03:00
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 {
2024-01-15 11:49:24 +03:00
Ctx : ctx ,
Metas : metas ,
2023-04-29 15:02:29 +03:00
} , cleanMsg )
if err != nil {
log . Error ( "RenderCommitMessage: %v" , err )
return ""
}
msgLines := strings . Split ( strings . TrimSpace ( fullMessage ) , "\n" )
if len ( msgLines ) == 0 {
return template . HTML ( "" )
}
2024-03-28 13:42:31 +03:00
return RenderCodeBlock ( template . HTML ( msgLines [ 0 ] ) )
2023-04-29 15:02:29 +03:00
}
2024-01-15 11:49:24 +03:00
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
2023-04-29 15:02:29 +03:00
// the provided default url, handling for special links without email to links.
2024-01-15 11:49:24 +03:00
func RenderCommitMessageLinkSubject ( ctx context . Context , msg , urlDefault string , metas map [ string ] string ) template . HTML {
2023-04-29 15:02:29 +03: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 {
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 ( "" )
}
2024-03-28 13:42:31 +03:00
return RenderCodeBlock ( template . HTML ( renderedMessage ) )
2023-04-29 15:02:29 +03:00
}
// RenderCommitBody extracts the body of a commit message without its title.
2024-01-15 11:49:24 +03:00
func RenderCommitBody ( ctx context . Context , msg string , metas map [ string ] string ) template . HTML {
2023-06-21 12:14:34 +03:00
msgLine := strings . TrimSpace ( msg )
2023-04-29 15:02:29 +03:00
lineEnd := strings . IndexByte ( msgLine , '\n' )
if lineEnd > 0 {
msgLine = msgLine [ lineEnd + 1 : ]
} else {
2023-06-21 12:14:34 +03:00
return ""
2023-04-29 15:02:29 +03:00
}
msgLine = strings . TrimLeftFunc ( msgLine , unicode . IsSpace )
if len ( msgLine ) == 0 {
2023-06-21 12:14:34 +03:00
return ""
2023-04-29 15:02:29 +03:00
}
renderedMessage , err := markup . RenderCommitMessage ( & markup . RenderContext {
2024-01-15 11:49:24 +03:00
Ctx : ctx ,
Metas : metas ,
2023-04-29 15:02:29 +03:00
} , 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 ( "`([^`]+)`" )
2023-08-31 08:01:01 +03:00
// RenderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
2023-04-29 15:02:29 +03:00
func RenderCodeBlock ( htmlEscapedTextToRender template . HTML ) template . HTML {
2023-08-31 08:01:01 +03:00
htmlWithCodeTags := codeMatcher . ReplaceAllString ( string ( htmlEscapedTextToRender ) , ` <code class="inline-code-block">$1</code> ` ) // replace with HTML <code> tags
2023-04-29 15:02:29 +03:00
return template . HTML ( htmlWithCodeTags )
}
2024-04-01 15:17:12 +03:00
const (
activeLabelOpacity = uint8 ( 255 )
archivedLabelOpacity = uint8 ( 127 )
)
func GetLabelOpacityByte ( isArchived bool ) uint8 {
if isArchived {
return archivedLabelOpacity
}
return activeLabelOpacity
}
2023-04-29 15:02:29 +03:00
// RenderIssueTitle renders issue/pull title with defined post processors
2024-01-15 11:49:24 +03:00
func RenderIssueTitle ( ctx context . Context , text string , metas map [ string ] string ) template . HTML {
2023-04-29 15:02:29 +03:00
renderedText , err := markup . RenderIssueTitle ( & markup . RenderContext {
2024-01-15 11:49:24 +03:00
Ctx : ctx ,
Metas : metas ,
2023-04-29 15:02:29 +03:00
} , template . HTMLEscapeString ( text ) )
if err != nil {
log . Error ( "RenderIssueTitle: %v" , err )
return template . HTML ( "" )
}
return template . HTML ( renderedText )
}
// RenderLabel renders a label
2024-03-29 21:07:01 +03:00
// 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 ( )
)
2023-05-10 14:19:03 +03:00
r , g , b := util . HexToRBGColor ( label . Color )
2024-04-01 15:17:12 +03:00
2023-05-10 14:19:03 +03:00
// Determine if label text should be light or dark to be readable on background color
2024-04-01 15:17:12 +03:00
// this doesn't account for saturation or transparency
2023-05-10 14:19:03 +03:00
if util . UseLightTextOnBackground ( r , g , b ) {
2023-04-29 15:02:29 +03:00
textColor = "#eee"
}
description := emoji . ReplaceAliases ( template . HTMLEscapeString ( label . Description ) )
2024-03-29 21:07:01 +03:00
if label . IsArchived ( ) {
archivedCSSClass = "archived-label"
2024-03-29 21:17:00 +03:00
description = locale . TrString ( "repo.issues.archived_label_description" , description )
2024-03-29 21:07:01 +03:00
}
2023-04-29 15:02:29 +03:00
if labelScope == "" {
// Regular label
2024-04-01 15:17:12 +03:00
labelColor := label . Color + hex . EncodeToString ( [ ] byte { GetLabelOpacityByte ( label . IsArchived ( ) ) } )
2024-03-29 21:07:01 +03:00
s := fmt . Sprintf ( "<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>" ,
2024-04-01 15:17:12 +03:00
archivedCSSClass , textColor , labelColor , description , RenderEmoji ( ctx , label . Name ) )
2023-04-29 15:02:29 +03:00
return template . HTML ( s )
}
// Scoped label
scopeText := RenderEmoji ( ctx , labelScope )
itemText := RenderEmoji ( ctx , label . Name [ len ( labelScope ) + 1 : ] )
2023-05-10 14:19:03 +03:00
// 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 )
2024-04-01 15:17:12 +03:00
opacity := GetLabelOpacityByte ( label . IsArchived ( ) )
2023-05-10 14:19:03 +03: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 ) ) ,
2024-04-01 15:17:12 +03:00
opacity ,
2023-04-29 15:02:29 +03:00
}
2023-05-10 14:19:03 +03: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 ) ) ,
2024-04-01 15:17:12 +03:00
opacity ,
2023-05-10 14:19:03 +03:00
}
scopeColor := "#" + hex . EncodeToString ( scopeBytes )
2024-04-01 15:17:12 +03:00
itemColor := "#" + hex . EncodeToString ( itemBytes )
2023-04-29 15:02:29 +03:00
2024-03-29 21:07:01 +03:00
s := fmt . Sprintf ( "<span class='ui label %s scope-parent' data-tooltip-content title='%s'>" +
2023-04-29 15:02:29 +03:00
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>" +
2023-10-24 02:02:00 +03:00
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>" +
2023-04-29 15:02:29 +03:00
"</span>" ,
2024-03-29 21:07:01 +03:00
archivedCSSClass , description ,
2023-04-29 15:02:29 +03:00
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 {
2024-01-15 11:49:24 +03:00
Ctx : ctx ,
Metas : map [ string ] string { "mode" : "document" } ,
2023-04-29 15:02:29 +03:00
} , input )
if err != nil {
log . Error ( "RenderString: %v" , err )
}
2024-03-01 10:11:51 +03:00
return output
2023-04-29 15:02:29 +03:00
}
2024-04-12 15:31:44 +03:00
func RenderLabels ( ctx context . Context , locale translation . Locale , labels [ ] * issues_model . Label , repoLink string , isPull bool ) template . HTML {
2023-04-29 15:02:29 +03: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-04-12 15:31:44 +03:00
issuesOrPull := "issues"
if isPull {
issuesOrPull = "pulls"
}
htmlCode += fmt . Sprintf ( "<a href='%s/%s?labels=%d'>%s</a> " ,
repoLink , issuesOrPull , label . ID , RenderLabel ( ctx , locale , label ) )
2023-04-29 15:02:29 +03:00
}
htmlCode += "</span>"
return template . HTML ( htmlCode )
}