2023-04-29 20:02:29 +08:00
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"encoding/hex"
"fmt"
"html/template"
"math"
"net/url"
"regexp"
"strings"
"unicode"
issues_model "code.gitea.io/gitea/models/issues"
2025-05-09 20:42:35 +08:00
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/models/repo"
2026-04-06 13:07:33 +02:00
"code.gitea.io/gitea/modules/charset"
2023-04-29 20:02:29 +08:00
"code.gitea.io/gitea/modules/emoji"
2024-11-18 13:25:42 +08:00
"code.gitea.io/gitea/modules/htmlutil"
2023-04-29 20:02:29 +08:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
2025-03-10 15:57:17 +08:00
"code.gitea.io/gitea/modules/reqctx"
2023-04-29 20:02:29 +08:00
"code.gitea.io/gitea/modules/setting"
2025-10-28 18:25:00 +08:00
"code.gitea.io/gitea/modules/svg"
2024-03-12 18:32:05 +01:00
"code.gitea.io/gitea/modules/translation"
2023-05-10 19:19:03 +08:00
"code.gitea.io/gitea/modules/util"
2025-10-28 18:25:00 +08:00
"code.gitea.io/gitea/services/webtheme"
2023-04-29 20:02:29 +08:00
)
2024-11-05 14:04:26 +08:00
type RenderUtils struct {
2025-03-10 15:57:17 +08:00
ctx reqctx . RequestContext
2024-11-05 14:04:26 +08:00
}
2025-03-10 15:57:17 +08:00
func NewRenderUtils ( ctx reqctx . RequestContext ) * RenderUtils {
2024-11-05 14:04:26 +08:00
return & RenderUtils { ctx : ctx }
}
2023-04-29 20:02:29 +08:00
// RenderCommitMessage renders commit message with XSS-safe and special links.
2025-05-09 20:42:35 +08:00
func ( ut * RenderUtils ) RenderCommitMessage ( msg string , repo * repo . Repository ) template . HTML {
2023-04-29 20:02:29 +08:00
cleanMsg := template . HTMLEscapeString ( msg )
2025-06-10 23:20:32 +08:00
// we can safely assume that it will not return any error, since there shouldn't be any special HTML.
// "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed.
2025-05-09 20:42:35 +08:00
fullMessage , err := markup . PostProcessCommitMessage ( renderhelper . NewRenderContextRepoComment ( ut . ctx , repo ) , cleanMsg )
2023-04-29 20:02:29 +08:00
if err != nil {
2024-12-04 09:39:33 +08:00
log . Error ( "PostProcessCommitMessage: %v" , err )
2023-04-29 20:02:29 +08:00
return ""
}
msgLines := strings . Split ( strings . TrimSpace ( fullMessage ) , "\n" )
if len ( msgLines ) == 0 {
2025-06-10 23:20:32 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
2024-06-19 06:32:45 +08:00
return renderCodeBlock ( template . HTML ( msgLines [ 0 ] ) )
2023-04-29 20:02:29 +08:00
}
2024-11-05 14:04:26 +08:00
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
2023-04-29 20:02:29 +08:00
// the provided default url, handling for special links without email to links.
2025-05-09 20:42:35 +08:00
func ( ut * RenderUtils ) RenderCommitMessageLinkSubject ( msg , urlDefault string , repo * repo . Repository ) template . HTML {
2023-04-29 20:02:29 +08: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 16:41:44 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
2025-05-09 20:42:35 +08:00
// we can safely assume that it will not return any error, since there shouldn't be any special HTML.
renderedMessage , err := markup . PostProcessCommitMessageSubject ( renderhelper . NewRenderContextRepoComment ( ut . ctx , repo ) , urlDefault , template . HTMLEscapeString ( msgLine ) )
2023-04-29 20:02:29 +08:00
if err != nil {
2024-12-04 09:39:33 +08:00
log . Error ( "PostProcessCommitMessageSubject: %v" , err )
2024-11-16 16:41:44 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
2024-06-19 06:32:45 +08:00
return renderCodeBlock ( template . HTML ( renderedMessage ) )
2023-04-29 20:02:29 +08:00
}
2024-11-05 14:04:26 +08:00
// RenderCommitBody extracts the body of a commit message without its title.
2025-05-09 20:42:35 +08:00
func ( ut * RenderUtils ) RenderCommitBody ( msg string , repo * repo . Repository ) template . HTML {
2023-06-21 17:14:34 +08:00
msgLine := strings . TrimSpace ( msg )
2023-04-29 20:02:29 +08:00
lineEnd := strings . IndexByte ( msgLine , '\n' )
if lineEnd > 0 {
msgLine = msgLine [ lineEnd + 1 : ]
} else {
2023-06-21 17:14:34 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
msgLine = strings . TrimLeftFunc ( msgLine , unicode . IsSpace )
if len ( msgLine ) == 0 {
2023-06-21 17:14:34 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
2025-05-09 20:42:35 +08:00
renderedMessage , err := markup . PostProcessCommitMessage ( renderhelper . NewRenderContextRepoComment ( ut . ctx , repo ) , template . HTMLEscapeString ( msgLine ) )
2023-04-29 20:02:29 +08:00
if err != nil {
2024-12-04 09:39:33 +08:00
log . Error ( "PostProcessCommitMessage: %v" , err )
2023-04-29 20:02:29 +08:00
return ""
}
return template . HTML ( renderedMessage )
}
// Match text that is between back ticks.
var codeMatcher = regexp . MustCompile ( "`([^`]+)`" )
2024-06-19 06:32:45 +08:00
// renderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
func renderCodeBlock ( htmlEscapedTextToRender template . HTML ) template . HTML {
2023-08-31 07:01:01 +02:00
htmlWithCodeTags := codeMatcher . ReplaceAllString ( string ( htmlEscapedTextToRender ) , ` <code class="inline-code-block">$1</code> ` ) // replace with HTML <code> tags
2023-04-29 20:02:29 +08:00
return template . HTML ( htmlWithCodeTags )
}
2024-11-05 14:04:26 +08:00
// RenderIssueTitle renders issue/pull title with defined post processors
2025-05-09 20:42:35 +08:00
func ( ut * RenderUtils ) RenderIssueTitle ( text string , repo * repo . Repository ) template . HTML {
renderedText , err := markup . PostProcessIssueTitle ( renderhelper . NewRenderContextRepoComment ( ut . ctx , repo ) , template . HTMLEscapeString ( text ) )
2023-04-29 20:02:29 +08:00
if err != nil {
2024-12-04 09:39:33 +08:00
log . Error ( "PostProcessIssueTitle: %v" , err )
2024-11-05 14:04:26 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
2024-12-04 09:39:33 +08: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 20:02:29 +08:00
}
2025-06-27 17:12:25 +02:00
func ( ut * RenderUtils ) RenderLabelWithLink ( label * issues_model . Label , link any ) template . HTML {
var attrHref template . HTML
switch link . ( type ) {
case template . URL , string :
attrHref = htmlutil . HTMLFormat ( ` href="%s" ` , link )
default :
panic ( fmt . Sprintf ( "unexpected type %T for link" , link ) )
}
return ut . renderLabelWithTag ( label , "a" , attrHref )
}
2024-11-05 14:04:26 +08:00
func ( ut * RenderUtils ) RenderLabel ( label * issues_model . Label ) template . HTML {
2025-06-27 17:12:25 +02:00
return ut . renderLabelWithTag ( label , "span" , "" )
}
// RenderLabel renders a label
func ( ut * RenderUtils ) renderLabelWithTag ( label * issues_model . Label , tagName , tagAttrs template . HTML ) template . HTML {
2024-11-05 14:04:26 +08:00
locale := ut . ctx . Value ( translation . ContextKey ) . ( translation . Locale )
2024-04-30 10:36:32 +08:00
var extraCSSClasses string
textColor := util . ContrastColor ( label . Color )
labelScope := label . ExclusiveScope ( )
descriptionText := emoji . ReplaceAliases ( label . Description )
2023-04-29 20:02:29 +08:00
2024-03-13 07:04:07 +01:00
if label . IsArchived ( ) {
2024-04-30 10:36:32 +08:00
extraCSSClasses = "archived-label"
descriptionText = fmt . Sprintf ( "(%s) %s" , locale . TrString ( "archived" ) , descriptionText )
2024-03-12 18:32:05 +01:00
}
2023-04-29 20:02:29 +08:00
if labelScope == "" {
// Regular label
2025-06-27 17:12:25 +02:00
return htmlutil . HTMLFormat ( ` <%s %s class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></%s> ` ,
tagName , tagAttrs , extraCSSClasses , textColor , label . Color , descriptionText , ut . RenderEmoji ( label . Name ) , tagName )
2023-04-29 20:02:29 +08:00
}
// Scoped label
2024-11-05 14:04:26 +08:00
scopeHTML := ut . RenderEmoji ( labelScope )
itemHTML := ut . RenderEmoji ( label . Name [ len ( labelScope ) + 1 : ] )
2023-04-29 20:02:29 +08:00
2023-05-10 19:19:03 +08:00
// Make scope and item background colors slightly darker and lighter respectively.
// More contrast needed with higher luminance, empirically tweaked.
2024-04-07 18:19:25 +02:00
luminance := util . GetRelativeLuminance ( label . Color )
2023-05-10 19:19:03 +08: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 )
2025-06-27 17:12:25 +02:00
// Compute the factor to keep RGB values proportional.
2023-05-10 19:19:03 +08:00
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 18:19:25 +02:00
r , g , b := util . HexToRBGColor ( label . Color )
2023-05-10 19:19:03 +08: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 20:02:29 +08:00
}
2023-05-10 19:19:03 +08: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 20:02:29 +08:00
2025-04-10 12:18:07 -05:00
if label . ExclusiveOrder > 0 {
// <scope> | <label> | <order>
2025-06-27 17:12:25 +02:00
return htmlutil . HTMLFormat ( ` <%s %s class="ui label %s scope-parent" data-tooltip-content title="%s"> ` +
2025-04-10 12:18:07 -05:00
` <div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div> ` +
` <div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div> ` +
` <div class="ui label scope-right">%d</div> ` +
2025-06-27 17:12:25 +02:00
` </%s> ` ,
tagName , tagAttrs ,
2025-04-10 12:18:07 -05:00
extraCSSClasses , descriptionText ,
textColor , scopeColor , scopeHTML ,
textColor , itemColor , itemHTML ,
2025-06-27 17:12:25 +02:00
label . ExclusiveOrder ,
tagName )
2025-04-10 12:18:07 -05:00
}
// <scope> | <label>
2025-06-27 17:12:25 +02:00
return htmlutil . HTMLFormat ( ` <%s %s class="ui label %s scope-parent" data-tooltip-content title="%s"> ` +
2024-04-30 10:36:32 +08: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> ` +
2025-06-27 17:12:25 +02:00
` </%s> ` ,
tagName , tagAttrs ,
2024-04-30 10:36:32 +08:00
extraCSSClasses , descriptionText ,
textColor , scopeColor , scopeHTML ,
2025-04-10 12:18:07 -05:00
textColor , itemColor , itemHTML ,
2025-06-27 17:12:25 +02:00
tagName )
2023-04-29 20:02:29 +08:00
}
2024-11-05 14:04:26 +08:00
// RenderEmoji renders html text with emoji post processors
func ( ut * RenderUtils ) RenderEmoji ( text string ) template . HTML {
2024-12-04 09:39:33 +08:00
renderedText , err := markup . PostProcessEmoji ( markup . NewRenderContext ( ut . ctx ) , template . HTMLEscapeString ( text ) )
2023-04-29 20:02:29 +08:00
if err != nil {
log . Error ( "RenderEmoji: %v" , err )
2024-11-05 14:04:26 +08:00
return ""
2023-04-29 20:02:29 +08:00
}
return template . HTML ( renderedText )
}
2024-06-19 06:32:45 +08:00
// reactionToEmoji renders emoji for use in reactions
func reactionToEmoji ( reaction string ) template . HTML {
2023-04-29 20:02:29 +08: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 ) ) )
}
2025-06-27 07:59:55 +02:00
func ( ut * RenderUtils ) MarkdownToHtml ( input string ) template . HTML { //nolint:revive // variable naming triggers on Html, wants HTML
2024-11-22 13:48:09 +08:00
output , err := markdown . RenderString ( markup . NewRenderContext ( ut . ctx ) . WithMetas ( markup . ComposeSimpleDocumentMetas ( ) ) , input )
2023-04-29 20:02:29 +08:00
if err != nil {
log . Error ( "RenderString: %v" , err )
}
2024-03-01 15:11:51 +08:00
return output
2023-04-29 20:02:29 +08:00
}
2024-11-05 14:04:26 +08:00
func ( ut * RenderUtils ) RenderLabels ( labels [ ] * issues_model . Label , repoLink string , issue * issues_model . Issue ) template . HTML {
2024-04-13 18:05:33 +08:00
isPullRequest := issue != nil && issue . IsPull
baseLink := fmt . Sprintf ( "%s/%s" , repoLink , util . Iif ( isPullRequest , "pulls" , "issues" ) )
2025-12-17 21:50:53 +01:00
var htmlCode strings . Builder
htmlCode . WriteString ( ` <span class="labels-list"> ` )
2023-04-29 20:02:29 +08:00
for _ , label := range labels {
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
if label == nil {
continue
}
2025-06-27 17:12:25 +02:00
link := fmt . Sprintf ( "%s?labels=%d" , baseLink , label . ID )
2025-12-17 21:50:53 +01:00
htmlCode . WriteString ( string ( ut . RenderLabelWithLink ( label , template . URL ( link ) ) ) )
2023-04-29 20:02:29 +08:00
}
2025-12-17 21:50:53 +01:00
htmlCode . WriteString ( "</span>" )
return template . HTML ( htmlCode . String ( ) )
2023-04-29 20:02:29 +08:00
}
2025-10-28 18:25:00 +08:00
func ( ut * RenderUtils ) RenderThemeItem ( info * webtheme . ThemeMetaInfo , iconSize int ) template . HTML {
svgName := "octicon-paintbrush"
switch info . ColorScheme {
case "dark" :
svgName = "octicon-moon"
case "light" :
svgName = "octicon-sun"
case "auto" :
svgName = "gitea-eclipse"
}
icon := svg . RenderHTML ( svgName , iconSize )
extraIcon := svg . RenderHTML ( info . GetExtraIconName ( ) , iconSize )
return htmlutil . HTMLFormat ( ` <div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div> ` , info . GetDescription ( ) , icon , info . DisplayName , extraIcon )
}
2026-04-06 13:07:33 +02:00
func ( ut * RenderUtils ) RenderUnicodeEscapeToggleButton ( escapeStatus * charset . EscapeStatus ) template . HTML {
if escapeStatus == nil || ! escapeStatus . Escaped {
return ""
}
locale := ut . ctx . Value ( translation . ContextKey ) . ( translation . Locale )
var title template . HTML
if escapeStatus . HasAmbiguous {
title += locale . Tr ( "repo.ambiguous_runes_line" )
} else if escapeStatus . HasInvisible {
title += locale . Tr ( "repo.invisible_runes_line" )
}
return htmlutil . HTMLFormat ( ` <button type="button" class="toggle-escape-button btn interact-bg" title="%s"></button> ` , title )
}
func ( ut * RenderUtils ) RenderUnicodeEscapeToggleTd ( combined , escapeStatus * charset . EscapeStatus ) template . HTML {
if combined == nil || ! combined . Escaped {
return ""
}
return ` <td class="lines-escape"> ` + ut . RenderUnicodeEscapeToggleButton ( escapeStatus ) + ` </td> `
}