diff --git a/models/repo/repo.go b/models/repo/repo.go index 7f8507722f0..e56603fc812 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -23,6 +23,7 @@ import ( "gitea.dev/modules/base" "gitea.dev/modules/git" giturl "gitea.dev/modules/git/url" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/httplib" "gitea.dev/modules/log" "gitea.dev/modules/markup" @@ -641,12 +642,7 @@ func (repo *Repository) CanContentChange() bool { // DescriptionHTML does special handles to description and return HTML string. func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { - desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description) - if err != nil { - log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) - return template.HTML(markup.SanitizeDescription(repo.Description)) - } - return template.HTML(markup.SanitizeDescription(desc)) + return markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), htmlutil.EscapeString(repo.Description)) } // CloneLink represents different types of clone URLs of repository. diff --git a/modules/markup/html.go b/modules/markup/html.go index 4943bdf4a5f..a21c4707113 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -14,8 +14,10 @@ import ( "sync" "gitea.dev/modules/htmlutil" + "gitea.dev/modules/log" "gitea.dev/modules/markup/common" "gitea.dev/modules/translation" + "gitea.dev/modules/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -151,8 +153,7 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e } // PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor. -// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content. -func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) { +func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -166,8 +167,7 @@ func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (templa emojiProcessor, emojiShortCodeProcessor, } - s, err := postProcessString(ctx, procs, string(content)) - return template.HTML(s), err + return postProcessHTML(ctx, procs, content) } var emojiProcessors = []processor{ @@ -189,7 +189,7 @@ func isBareURLSubject(content string) bool { // PostProcessCommitMessageSubject will use the same logic as PostProcess and // PostProcessCommitMessage, but will disable the shortLinkProcessor and // emailAddressProcessor, and wraps the whole subject in defaultLink. -func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { +func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink string, content template.HTML) template.HTML { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -207,7 +207,7 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st // plain text inside defaultLink. Partial URLs inside larger text still become // their own links (nested anchors aren't legal HTML, so the outer defaultLink // naturally breaks on that span, same as on GitHub). - if !isBareURLSubject(content) { + if !isBareURLSubject(string(content)) { procs = append(procs, linkProcessor) } procs = append(procs, func(ctx *RenderContext, node *html.Node) { @@ -215,27 +215,28 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st node.Type = html.ElementNode node.Data = "a" node.DataAtom = atom.A - node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} + node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted title-full-link"}} node.FirstChild, node.LastChild = ch, ch }) - return postProcessString(ctx, procs, content) + rendered := postProcessHTML(ctx, procs, content) + return htmlutil.HTMLFormat(`%s`, rendered) } // PostProcessIssueTitle to process title on individual issue/pull page -func PostProcessIssueTitle(ctx *RenderContext, title string) (string, error) { - return postProcessString(ctx, []processor{ +func PostProcessIssueTitle(ctx *RenderContext, titleHTML template.HTML) template.HTML { + return postProcessHTML(ctx, []processor{ issueIndexPatternProcessor, commitCrossReferencePatternProcessor, hashCurrentPatternProcessor, emojiShortCodeProcessor, emojiProcessor, - }, title) + }, titleHTML) } // PostProcessDescriptionHTML will use similar logic as PostProcess, but will // use a single special linkProcessor. -func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, error) { - return postProcessString(ctx, []processor{ +func PostProcessDescriptionHTML(ctx *RenderContext, content template.HTML) template.HTML { + return postProcessHTML(ctx, []processor{ descriptionLinkProcessor, emojiShortCodeProcessor, emojiProcessor, @@ -243,17 +244,18 @@ func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, err } // PostProcessEmoji for when we want to just process emoji and shortcodes -// in various places it isn't already run through the normal markdown processor -func PostProcessEmoji(ctx *RenderContext, content string) (string, error) { - return postProcessString(ctx, emojiProcessors, content) +// in various places it isn't already run through the normal Markdown processor +func PostProcessEmoji(ctx *RenderContext, content template.HTML) template.HTML { + return postProcessHTML(ctx, emojiProcessors, content) } -func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) { +func postProcessHTML(ctx *RenderContext, procs []processor, content template.HTML) template.HTML { var buf strings.Builder - if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil { - return "", err + if err := postProcess(ctx, procs, strings.NewReader(string(content)), &buf); err != nil { + log.Warn("postProcessHTML err: %v, input: %s", err, util.TruncateRunes(string(content), 200)) + return content } - return buf.String(), nil + return template.HTML(buf.String()) } func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]string, out io.Writer) { diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 186489ae722..6036f368901 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -5,6 +5,7 @@ package markup import ( "fmt" + "html/template" "strconv" "strings" "testing" @@ -260,9 +261,8 @@ func TestRender_PostProcessIssueTitle(t *testing.T) { "repo": "someRepo", "style": IssueNameStyleNumeric, } - actual, err := PostProcessIssueTitle(NewTestRenderContext(metas), "#1") - assert.NoError(t, err) - assert.Equal(t, "#1", actual) + actual := PostProcessIssueTitle(NewTestRenderContext(metas), "#1") + assert.Equal(t, template.HTML("#1"), actual) } func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 3c7f9b0a7a1..81941902aee 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -11,7 +11,6 @@ import ( "net/url" "regexp" "strings" - "unicode" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" @@ -39,60 +38,35 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { return &RenderUtils{ctx: ctx} } -// RenderCommitMessage renders commit message with XSS-safe and special links. +// RenderCommitMessage renders commit message title (only title) func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTML(template.HTMLEscapeString(msg)) - // 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. - fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) - if err != nil { - log.Error("PostProcessCommitMessage: %v", err) - return "" - } - msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") - if len(msgLines) == 0 { - return "" - } - return renderCodeBlock(template.HTML(msgLines[0])) + msgLine := strings.TrimSpace(msg) + msgLine, _, _ = strings.Cut(msgLine, "\n") + msgLine = strings.TrimSpace(msgLine) + rendered := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlutil.EscapeString(msgLine)) + return renderCodeBlock(rendered) } // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to // the provided default url, handling for special links without email to links. func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) 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 "" - } - - // 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)) - if err != nil { - log.Error("PostProcessCommitMessageSubject: %v", err) - return "" - } - return renderCodeBlock(template.HTML(renderedMessage)) + msgLine := strings.TrimSpace(msg) + msgLine, _, _ = strings.Cut(msgLine, "\n") + msgLine = strings.TrimSpace(msgLine) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + rendered := markup.PostProcessCommitMessageSubject(rctx, urlDefault, htmlutil.EscapeString(msgLine)) + return renderCodeBlock(rendered) } // RenderCommitBody extracts the body of a commit message without its title. func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { _, body, _ := strings.Cut(strings.TrimSpace(msg), "\n") - body = strings.TrimFunc(body, unicode.IsSpace) + body = strings.TrimSpace(body) if body == "" { return "" } - rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) - htmlContent := template.HTML(template.HTMLEscapeString(body)) - renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) - if err != nil { - log.Error("PostProcessCommitMessage: %v", err) - return "" - } + renderedMessage := markup.PostProcessCommitMessage(rctx, htmlutil.EscapeString(body)) return renderedMessage } @@ -108,25 +82,15 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { // RenderIssueTitle renders issue/pull title with defined post processors func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { // wrap "`…`" in before post-processing so code-span content stays literal, like comment bodies - htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) - renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), string(htmlWithCode)) - if err != nil { - log.Error("PostProcessIssueTitle: %v", err) - return "" - } - return template.HTML(renderedText) + htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) + return markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlWithCode) } // RenderIssueSimpleTitle only renders with emoji and inline code block func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { // see RenderIssueTitle: wrap code spans before processing emoji - htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) - renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), string(htmlWithCode)) - if err != nil { - log.Error("RenderIssueSimpleTitle: %v", err) - return "" - } - return template.HTML(renderedText) + htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) + return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlWithCode) } func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { @@ -202,12 +166,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { // RenderEmoji renders html text with emoji post processors func (ut *RenderUtils) RenderEmoji(text string) template.HTML { - renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) - if err != nil { - log.Error("RenderEmoji: %v", err) - return "" - } - return template.HTML(renderedText) + return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlutil.EscapeString(text)) } // reactionToEmoji renders emoji for use in reactions diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 50a443c7468..5a28a1feba8 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -131,24 +131,24 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit }) t.Run("RenderCommitMessage", func(t *testing.T) { - expected := `space @mention-user ` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), mockRepo)) }) t.Run("RenderCommitMessageLinkSubject", func(t *testing.T) { - expected := `space @mention-user` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo)) }) t.Run("RenderCommitMessageLinkSubjectURLOnly", func(t *testing.T) { // a bare URL in the subject must not hijack the default link - expected := `https://example.com/file.bin` + expected := `https://example.com/file.bin` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("https://example.com/file.bin", "https://example.com/link", mockRepo)) }) t.Run("RenderCommitMessageLinkSubjectPartialURL", func(t *testing.T) { // a URL embedded in larger subject text still becomes its own link - expected := `see https://example.com/x here` + expected := `see https://example.com/x here` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo)) }) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 7cb2f82a903..ff3496629fa 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -414,11 +414,7 @@ func Diff(ctx *context.Context) { ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)}) htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage) - if err != nil { - ctx.ServerError("PostProcessCommitMessage", err) - return - } + ctx.Data["NoteRendered"] = markup.PostProcessCommitMessage(rctx, htmlMessage) } else if !git.IsErrNotExist(err) { log.Error("GetNote: %v", err) } diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index edc167e706b..58dc222cf5b 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -13,13 +13,10 @@
- - {{if $run.Title}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $run.Title $run.Link $.Repository}} - {{else}} - {{ctx.Locale.Tr "actions.runs.empty_commit_message"}} - {{end}} - +
+ {{$title := or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $title $run.Link $.Repository}} +
{{$workflowName := index $.WorkflowNames $run.WorkflowID}} {{if not $.CurWorkflow}}{{if $workflowName}}{{$workflowName}}{{else}}{{$run.WorkflowID}}{{end}} {{end}}#{{$run.Index}}: diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1a635f20199..cecf9920acd 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -92,3 +92,17 @@ margin-right: 8px; text-align: left; } + +/* for "title (#123)": + + title ( + #123 + ) + + * hover on "title": also highlight the right parentheses + * hover on "#123": don't highlight other parts + */ +.title-full-link-hover:not(:has(:not(.title-full-link):hover)):hover > a.title-full-link { + color: var(--color-primary); + text-decoration: underline; +}