Rework markup link rendering (#26745)

Fixes #26548

This PR refactors the rendering of markup links. The old code uses
`strings.Replace` to change some urls while the new code uses more
context to decide which link should be generated.

The added tests should ensure the same output for the old and new
behaviour (besides the bug).

We may need to refactor the rendering a bit more to make it clear how
the different helper methods render the input string. There are lots of
options (resolve links / images / mentions / git hashes / emojis / ...)
but you don't really know what helper uses which options. For example,
we currently support images in the user description which should not be
allowed I think:

<details>
  <summary>Profile</summary> 

https://try.gitea.io/KN4CK3R

![grafik](https://github.com/go-gitea/gitea/assets/1666336/109ae422-496d-4200-b52e-b3a528f553e5)

</details>

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
pull/28796/head^2
KN4CK3R 10 months ago committed by GitHub
parent c7e4629c02
commit 637451a45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      models/issues/comment_code.go
  2. 3
      models/repo/repo.go
  3. 13
      modules/markup/external/external.go
  4. 57
      modules/markup/html.go
  5. 30
      modules/markup/html_internal_test.go
  6. 117
      modules/markup/html_test.go
  7. 30
      modules/markup/markdown/goldmark.go
  8. 6
      modules/markup/markdown/markdown.go
  9. 422
      modules/markup/markdown/markdown_test.go
  10. 58
      modules/markup/orgmode/orgmode.go
  11. 15
      modules/markup/orgmode/orgmode_test.go
  12. 42
      modules/markup/renderer.go
  13. 1
      modules/templates/helper.go
  14. 53
      modules/templates/util_render.go
  15. 141
      modules/templates/util_render_test.go
  16. 12
      routers/common/markup.go
  17. 19
      routers/web/feed/convert.go
  18. 6
      routers/web/feed/profile.go
  19. 23
      routers/web/org/home.go
  20. 18
      routers/web/repo/commit.go
  21. 51
      routers/web/repo/issue.go
  22. 20
      routers/web/repo/milestone.go
  23. 20
      routers/web/repo/projects.go
  24. 20
      routers/web/repo/release.go
  25. 15
      routers/web/repo/render.go
  26. 58
      routers/web/repo/view.go
  27. 10
      routers/web/repo/wiki.go
  28. 6
      routers/web/shared/user/header.go
  29. 8
      routers/web/user/home.go
  30. 24
      routers/web/user/profile.go
  31. 8
      services/mailer/mail.go
  32. 8
      services/mailer/mail_release.go
  33. 4
      templates/repo/branch/list.tmpl
  34. 8
      templates/repo/commit_page.tmpl
  35. 4
      templates/repo/commits_list.tmpl
  36. 4
      templates/repo/commits_list_small.tmpl
  37. 2
      templates/repo/diff/compare.tmpl
  38. 2
      templates/repo/graph/commits.tmpl
  39. 2
      templates/repo/issue/view_title.tmpl
  40. 6
      templates/repo/view_list.tmpl
  41. 2
      templates/user/dashboard/feeds.tmpl
  42. 6
      tests/fuzz/fuzz_test.go

@ -109,9 +109,11 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
var err error var err error
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: issue.Repo.Link(), Links: markup.Links{
Metas: issue.Repo.ComposeMetas(ctx), Base: issue.Repo.Link(),
},
Metas: issue.Repo.ComposeMetas(ctx),
}, comment.Content); err != nil { }, comment.Content); err != nil {
return nil, err return nil, err
} }

@ -584,8 +584,7 @@ func (repo *Repository) CanEnableEditor() bool {
// DescriptionHTML does special handles to description and return HTML string. // DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{ desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: repo.HTMLURL(),
// Don't use Metas to speedup requests // Don't use Metas to speedup requests
}, repo.Description) }, repo.Description)
if err != nil { if err != nil {

@ -79,9 +79,10 @@ func envMark(envName string) string {
// Render renders the data of the document to HTML via the external tool. // Render renders the data of the document to HTML via the external tool.
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
var ( var (
urlRawPrefix = strings.Replace(ctx.URLPrefix, "/src/", "/raw/", 1) command = strings.NewReplacer(
command = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), ctx.URLPrefix, envMark("GITEA_PREFIX_SRC"), ctx.Links.SrcLink(),
envMark("GITEA_PREFIX_RAW"), urlRawPrefix).Replace(p.Command) envMark("GITEA_PREFIX_RAW"), ctx.Links.RawLink(),
).Replace(p.Command)
commands = strings.Fields(command) commands = strings.Fields(command)
args = commands[1:] args = commands[1:]
) )
@ -121,14 +122,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
ctx.Ctx = graceful.GetManager().ShutdownContext() ctx.Ctx = graceful.GetManager().ShutdownContext()
} }
processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.URLPrefix)) processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.Links.SrcLink()))
defer finished() defer finished()
cmd := exec.CommandContext(processCtx, commands[0], args...) cmd := exec.CommandContext(processCtx, commands[0], args...)
cmd.Env = append( cmd.Env = append(
os.Environ(), os.Environ(),
"GITEA_PREFIX_SRC="+ctx.URLPrefix, "GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
"GITEA_PREFIX_RAW="+urlRawPrefix, "GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
) )
if !p.IsInputFile { if !p.IsInputFile {
cmd.Stdin = input cmd.Stdin = input

@ -80,15 +80,10 @@ const keywordClass = "issue-keyword"
// IsLink reports whether link fits valid format. // IsLink reports whether link fits valid format.
func IsLink(link []byte) bool { func IsLink(link []byte) bool {
return isLink(link)
}
// isLink reports whether link fits valid format.
func isLink(link []byte) bool {
return validLinksPattern.Match(link) return validLinksPattern.Match(link)
} }
func isLinkStr(link string) bool { func IsLinkStr(link string) bool {
return validLinksPattern.MatchString(link) return validLinksPattern.MatchString(link)
} }
@ -344,7 +339,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
node = node.FirstChild node = node.FirstChild
} }
visitNode(ctx, procs, procs, node) visitNode(ctx, procs, node)
newNodes := make([]*html.Node, 0, 5) newNodes := make([]*html.Node, 0, 5)
@ -375,7 +370,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
return nil return nil
} }
func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node) { func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
// Add user-content- to IDs and "#" links if they don't already have them // Add user-content- to IDs and "#" links if they don't already have them
for idx, attr := range node.Attr { for idx, attr := range node.Attr {
val := strings.TrimPrefix(attr.Val, "#") val := strings.TrimPrefix(attr.Val, "#")
@ -390,35 +385,29 @@ func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node
} }
if attr.Key == "class" && attr.Val == "emoji" { if attr.Key == "class" && attr.Val == "emoji" {
textProcs = nil procs = nil
} }
} }
// We ignore code and pre. // We ignore code and pre.
switch node.Type { switch node.Type {
case html.TextNode: case html.TextNode:
textNode(ctx, textProcs, node) textNode(ctx, procs, node)
case html.ElementNode: case html.ElementNode:
if node.Data == "img" { if node.Data == "img" {
for i, attr := range node.Attr { for i, attr := range node.Attr {
if attr.Key != "src" { if attr.Key != "src" {
continue continue
} }
if len(attr.Val) > 0 && !isLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") { if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
prefix := ctx.URLPrefix attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
if ctx.IsWiki {
prefix = util.URLJoin(prefix, "wiki", "raw")
}
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
attr.Val = util.URLJoin(prefix, attr.Val)
} }
attr.Val = camoHandleLink(attr.Val) attr.Val = camoHandleLink(attr.Val)
node.Attr[i] = attr node.Attr[i] = attr
} }
} else if node.Data == "a" { } else if node.Data == "a" {
// Restrict text in links to emojis // Restrict text in links to emojis
textProcs = emojiProcessors procs = emojiProcessors
} else if node.Data == "code" || node.Data == "pre" { } else if node.Data == "code" || node.Data == "pre" {
return return
} else if node.Data == "i" { } else if node.Data == "i" {
@ -444,7 +433,7 @@ func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node
} }
} }
for n := node.FirstChild; n != nil; n = n.NextSibling { for n := node.FirstChild; n != nil; n = n.NextSibling {
visitNode(ctx, procs, textProcs, n) visitNode(ctx, procs, n)
} }
} }
// ignore everything else // ignore everything else
@ -641,10 +630,6 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
} }
func shortLinkProcessor(ctx *RenderContext, node *html.Node) { func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
shortLinkProcessorFull(ctx, node, false)
}
func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
next := node.NextSibling next := node.NextSibling
for node != nil && node != next { for node != nil && node != next {
m := shortLinkPattern.FindStringSubmatchIndex(node.Data) m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
@ -665,7 +650,7 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
if equalPos := strings.IndexByte(v, '='); equalPos == -1 { if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
// There is no equal in this argument; this is a mandatory arg // There is no equal in this argument; this is a mandatory arg
if props["name"] == "" { if props["name"] == "" {
if isLinkStr(v) { if IsLinkStr(v) {
// If we clearly see it is a link, we save it so // If we clearly see it is a link, we save it so
// But first we need to ensure, that if both mandatory args provided // But first we need to ensure, that if both mandatory args provided
@ -740,7 +725,7 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
DataAtom: atom.A, DataAtom: atom.A,
} }
childNode.Parent = linkNode childNode.Parent = linkNode
absoluteLink := isLinkStr(link) absoluteLink := IsLinkStr(link)
if !absoluteLink { if !absoluteLink {
if image { if image {
link = strings.ReplaceAll(link, " ", "+") link = strings.ReplaceAll(link, " ", "+")
@ -751,16 +736,9 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
link = url.PathEscape(link) link = url.PathEscape(link)
} }
} }
urlPrefix := ctx.URLPrefix
if image { if image {
if !absoluteLink { if !absoluteLink {
if IsSameDomain(urlPrefix) { link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link)
urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
}
if ctx.IsWiki {
link = util.URLJoin("wiki", "raw", link)
}
link = util.URLJoin(urlPrefix, link)
} }
title := props["title"] title := props["title"]
if title == "" { if title == "" {
@ -789,18 +767,15 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
} else { } else {
if !absoluteLink { if !absoluteLink {
if ctx.IsWiki { if ctx.IsWiki {
link = util.URLJoin("wiki", link) link = util.URLJoin(ctx.Links.WikiLink(), link)
} else {
link = util.URLJoin(ctx.Links.SrcLink(), link)
} }
link = util.URLJoin(urlPrefix, link)
} }
childNode.Type = html.TextNode childNode.Type = html.TextNode
childNode.Data = name childNode.Data = name
} }
if noLink { linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
linkNode = childNode
} else {
linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
}
replaceContent(node, m[0], m[1], linkNode) replaceContent(node, m[0], m[1], linkNode)
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
} }

@ -287,8 +287,8 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
} }
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
if ctx.URLPrefix == "" { if ctx.Links.Base == "" {
ctx.URLPrefix = TestAppURL ctx.Links.Base = TestRepoURL
} }
var buf strings.Builder var buf strings.Builder
@ -303,19 +303,23 @@ func TestRender_AutoLink(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var buffer strings.Builder var buffer strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
},
Metas: localMetas,
}, strings.NewReader(input), &buffer) }, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
buffer.Reset() buffer.Reset()
err = PostProcess(&RenderContext{ err = PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, strings.NewReader(input), &buffer) }, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
@ -342,9 +346,11 @@ func TestRender_FullIssueURLs(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var result strings.Builder var result strings.Builder
err := postProcess(&RenderContext{ err := postProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
},
Metas: localMetas,
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result) }, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result.String()) assert.Equal(t, expected, result.String())

@ -42,8 +42,10 @@ func TestRender_Commits(t *testing.T) {
buffer, err := markup.RenderString(&markup.RenderContext{ buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: ".md", RelativePath: ".md",
URLPrefix: markup.TestRepoURL, Links: markup.Links{
Metas: localMetas, Base: markup.TestRepoURL,
},
Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -93,8 +95,10 @@ func TestRender_CrossReferences(t *testing.T) {
buffer, err := markup.RenderString(&markup.RenderContext{ buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: setting.AppSubURL, Links: markup.Links{
Metas: localMetas, Base: setting.AppSubURL,
},
Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -138,7 +142,9 @@ func TestRender_links(t *testing.T) {
buffer, err := markup.RenderString(&markup.RenderContext{ buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: markup.TestRepoURL, Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -238,7 +244,9 @@ func TestRender_email(t *testing.T) {
res, err := markup.RenderString(&markup.RenderContext{ res, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: markup.TestRepoURL, Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
@ -309,7 +317,9 @@ func TestRender_emoji(t *testing.T) {
buffer, err := markup.RenderString(&markup.RenderContext{ buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: markup.TestRepoURL, Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -371,29 +381,34 @@ func TestRender_ShortLinks(t *testing.T) {
test := func(input, expected, expectedWiki string) { test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{ buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: tree, Links: markup.Links{
Base: markup.TestRepoURL,
BranchPath: "master",
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
buffer, err = markdown.RenderString(&markup.RenderContext{ buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: markup.TestRepoURL, Links: markup.Links{
Metas: localMetas, Base: markup.TestRepoURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
} }
rawtree := util.URLJoin(markup.TestRepoURL, "raw", "master") mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
url := util.URLJoin(tree, "Link") url := util.URLJoin(tree, "Link")
otherURL := util.URLJoin(tree, "Other-Link") otherURL := util.URLJoin(tree, "Other-Link")
encodedURL := util.URLJoin(tree, "Link%3F") encodedURL := util.URLJoin(tree, "Link%3F")
imgurl := util.URLJoin(rawtree, "Link.jpg") imgurl := util.URLJoin(mediatree, "Link.jpg")
otherImgurl := util.URLJoin(rawtree, "Link+Other.jpg") otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg")
encodedImgurl := util.URLJoin(rawtree, "Link+%23.jpg") encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg")
notencodedImgurl := util.URLJoin(rawtree, "some", "path", "Link+#.jpg") notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg")
urlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link") urlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link")
otherURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Other-Link") otherURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Other-Link")
encodedURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link%3F") encodedURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link%3F")
@ -475,21 +490,25 @@ func TestRender_ShortLinks(t *testing.T) {
func TestRender_RelativeImages(t *testing.T) { func TestRender_RelativeImages(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
tree := util.URLJoin(markup.TestRepoURL, "src", "master")
test := func(input, expected, expectedWiki string) { test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{ buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: tree, Links: markup.Links{
Metas: localMetas, Base: markup.TestRepoURL,
BranchPath: "master",
},
Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
buffer, err = markdown.RenderString(&markup.RenderContext{ buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: markup.TestRepoURL, Links: markup.Links{
Metas: localMetas, Base: markup.TestRepoURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
@ -521,9 +540,11 @@ func Test_ParseClusterFuzz(t *testing.T) {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{ err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Links: markup.Links{
Metas: localMetas, Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html") assert.NotContains(t, res.String(), "<html")
@ -532,9 +553,11 @@ func Test_ParseClusterFuzz(t *testing.T) {
res.Reset() res.Reset()
err = markup.PostProcess(&markup.RenderContext{ err = markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Links: markup.Links{
Metas: localMetas, Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
@ -543,6 +566,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
func TestPostProcess_RenderDocument(t *testing.T) { func TestPostProcess_RenderDocument(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
localMetas := map[string]string{ localMetas := map[string]string{
"user": "go-gitea", "user": "go-gitea",
@ -553,9 +577,11 @@ func TestPostProcess_RenderDocument(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{ err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Links: markup.Links{
Metas: localMetas, Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(input), &res) }, strings.NewReader(input), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
@ -594,9 +620,8 @@ func TestIssue16020(t *testing.T) {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{ err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Metas: localMetas,
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, data, res.String()) assert.Equal(t, data, res.String())
@ -611,9 +636,8 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{ err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Metas: localMetas,
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(b, err) assert.NoError(b, err)
} }
@ -622,8 +646,10 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
func TestFuzz(t *testing.T) { func TestFuzz(t *testing.T) {
s := "t/l/issues/8#/../../a" s := "t/l/issues/8#/../../a"
renderContext := markup.RenderContext{ renderContext := markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com/go-gitea/gitea", Links: markup.Links{
Base: "https://example.com/go-gitea/gitea",
},
Metas: map[string]string{ Metas: map[string]string{
"user": "go-gitea", "user": "go-gitea",
"repo": "gitea", "repo": "gitea",
@ -640,9 +666,8 @@ func TestIssue18471(t *testing.T) {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{ err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Metas: localMetas,
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)

@ -87,18 +87,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// Check if the destination is a real link // Check if the destination is a real link
link := v.Destination link := v.Destination
if len(link) > 0 && !markup.IsLink(link) { if len(link) > 0 && !markup.IsLink(link) {
prefix := pc.Get(urlPrefixKey).(string) v.Destination = []byte(giteautil.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), string(link)))
if pc.Get(isWikiKey).(bool) {
prefix = giteautil.URLJoin(prefix, "wiki", "raw")
}
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
lnk := strings.TrimLeft(string(link), "/")
lnk = giteautil.URLJoin(prefix, lnk)
link = []byte(lnk)
} }
v.Destination = link
parent := n.Parent() parent := n.Parent()
// Create a link around image only if parent is not already a link // Create a link around image only if parent is not already a link
@ -107,7 +97,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// Create a link wrapper // Create a link wrapper
wrap := ast.NewLink() wrap := ast.NewLink()
wrap.Destination = link wrap.Destination = v.Destination
wrap.Title = v.Title wrap.Title = v.Title
wrap.SetAttributeString("target", []byte("_blank")) wrap.SetAttributeString("target", []byte("_blank"))
@ -143,11 +133,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
// special case: this is not a link, a hash link or a mailto:, so it's a // special case: this is not a link, a hash link or a mailto:, so it's a
// relative URL // relative URL
lnk := string(link)
if pc.Get(isWikiKey).(bool) { var base string
lnk = giteautil.URLJoin("wiki", lnk) if ctx.IsWiki {
base = ctx.Links.WikiLink()
} else {
base = ctx.Links.Base
} }
link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk))
link = []byte(giteautil.URLJoin(base, string(link)))
} }
if len(link) > 0 && link[0] == '#' { if len(link) > 0 && link[0] == '#' {
link = []byte("#user-content-" + string(link)[1:]) link = []byte("#user-content-" + string(link)[1:])
@ -188,9 +182,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
applyElementDir(v) applyElementDir(v)
case *ast.Text: case *ast.Text:
if v.SoftLineBreak() && !v.HardLineBreak() { if v.SoftLineBreak() && !v.HardLineBreak() {
renderMetas := pc.Get(renderMetasKey).(map[string]string) if ctx.Metas["mode"] != "document" {
mode := renderMetas["mode"]
if mode != "document" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
} else { } else {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)

@ -34,9 +34,6 @@ var (
) )
var ( var (
urlPrefixKey = parser.NewContextKey()
isWikiKey = parser.NewContextKey()
renderMetasKey = parser.NewContextKey()
renderContextKey = parser.NewContextKey() renderContextKey = parser.NewContextKey()
renderConfigKey = parser.NewContextKey() renderConfigKey = parser.NewContextKey()
) )
@ -66,9 +63,6 @@ func (l *limitWriter) Write(data []byte) (int, error) {
// newParserContext creates a parser.Context with the render context set // newParserContext creates a parser.Context with the render context set
func newParserContext(ctx *markup.RenderContext) parser.Context { func newParserContext(ctx *markup.RenderContext) parser.Context {
pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
pc.Set(urlPrefixKey, ctx.URLPrefix)
pc.Set(isWikiKey, ctx.IsWiki)
pc.Set(renderMetasKey, ctx.Metas)
pc.Set(renderContextKey, ctx) pc.Set(renderContextKey, ctx)
return pc return pc
} }

@ -52,16 +52,20 @@ func TestRender_StandardLinks(t *testing.T) {
test := func(input, expected, expectedWiki string) { test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{ buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
buffer, err = markdown.RenderString(&markup.RenderContext{ buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
IsWiki: true, Base: setting.AppSubURL,
},
IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
@ -83,8 +87,10 @@ func TestRender_Images(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := markdown.RenderString(&markup.RenderContext{ buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -107,7 +113,6 @@ func TestRender_Images(t *testing.T) {
"[!["+title+"]("+url+")]("+href+")", "[!["+title+"]("+url+")]("+href+")",
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
url = "/../../.images/src/02/train.jpg"
test( test(
"!["+title+"]("+url+")", "!["+title+"]("+url+")",
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
@ -286,14 +291,16 @@ func TestTotal_RenderWiki(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL setting.AppSubURL = AppSubURL
answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/")) answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw"))
for i := 0; i < len(sameCases); i++ { for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(&markup.RenderContext{ line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: AppSubURL, Links: markup.Links{
Metas: localMetas, Base: setting.AppSubURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, sameCases[i]) }, sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], line) assert.Equal(t, answers[i], line)
@ -314,9 +321,11 @@ func TestTotal_RenderWiki(t *testing.T) {
for i := 0; i < len(testCases); i += 2 { for i := 0; i < len(testCases); i += 2 {
line, err := markdown.RenderString(&markup.RenderContext{ line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: AppSubURL, Links: markup.Links{
IsWiki: true, Base: setting.AppSubURL,
},
IsWiki: true,
}, testCases[i]) }, testCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testCases[i+1], line) assert.Equal(t, testCases[i+1], line)
@ -327,13 +336,16 @@ func TestTotal_RenderString(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL setting.AppSubURL = AppSubURL
answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/")) answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master"))
for i := 0; i < len(sameCases); i++ { for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(&markup.RenderContext{ line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: util.URLJoin(AppSubURL, "src", "master/"), Links: markup.Links{
Metas: localMetas, Base: AppSubURL,
BranchPath: "master",
},
Metas: localMetas,
}, sameCases[i]) }, sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], line) assert.Equal(t, answers[i], line)
@ -343,8 +355,10 @@ func TestTotal_RenderString(t *testing.T) {
for i := 0; i < len(testCases); i += 2 { for i := 0; i < len(testCases); i += 2 {
line, err := markdown.RenderString(&markup.RenderContext{ line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: AppSubURL, Links: markup.Links{
Base: AppSubURL,
},
}, testCases[i]) }, testCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testCases[i+1], line) assert.Equal(t, testCases[i+1], line)
@ -556,3 +570,367 @@ foo: bar
assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
} }
} }
func TestRenderLinks(t *testing.T) {
input := ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
[remote link](https://example.com)
[[local link|file.bin]]
[[remote link|https://example.com]]
![local image](image.jpg)
![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
[[remote link|https://example.com/image.jpg]]
https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
:+1:
mail@domain.com
@mention-user test
#123
space
`
cases := []struct {
Links markup.Links
IsWiki bool
Expected string
}{
{ // 0
Links: markup.Links{},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 1
Links: markup.Links{},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 2
Links: markup.Links{
Base: "https://gitea.io/",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 3
Links: markup.Links{
Base: "https://gitea.io/",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 4
Links: markup.Links{
Base: "/relative/path",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 5
Links: markup.Links{
Base: "/relative/path",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 6
Links: markup.Links{
Base: "/user/repo",
BranchPath: "branch/main",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 7
Links: markup.Links{
Base: "/relative/path",
BranchPath: "branch/main",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 8
Links: markup.Links{
Base: "/user/repo",
TreePath: "sub/folder",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 9
Links: markup.Links{
Base: "/relative/path",
TreePath: "sub/folder",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 10
Links: markup.Links{
Base: "/user/repo",
BranchPath: "branch/main",
TreePath: "sub/folder",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 11
Links: markup.Links{
Base: "/relative/path",
BranchPath: "branch/main",
TreePath: "sub/folder",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
}
for i, c := range cases {
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
}
}

@ -4,7 +4,6 @@
package markup package markup
import ( import (
"bytes"
"fmt" "fmt"
"html" "html"
"io" "io"
@ -101,8 +100,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
w := &Writer{ w := &Writer{
HTMLWriter: htmlWriter, HTMLWriter: htmlWriter,
URLPrefix: ctx.URLPrefix, Ctx: ctx,
IsWiki: ctx.IsWiki,
} }
htmlWriter.ExtendingWriter = w htmlWriter.ExtendingWriter = w
@ -132,63 +130,53 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
// Writer implements org.Writer // Writer implements org.Writer
type Writer struct { type Writer struct {
*org.HTMLWriter *org.HTMLWriter
URLPrefix string Ctx *markup.RenderContext
IsWiki bool
} }
var byteMailto = []byte("mailto:") const mailto = "mailto:"
// WriteRegularLink renders images, links or videos func (r *Writer) resolveLink(l org.RegularLink) string {
func (r *Writer) WriteRegularLink(l org.RegularLink) { link := html.EscapeString(l.URL)
link := []byte(html.EscapeString(l.URL))
if l.Protocol == "file" { if l.Protocol == "file" {
link = link[len("file:"):] link = link[len("file:"):]
} }
if len(link) > 0 && !markup.IsLink(link) && if len(link) > 0 && !markup.IsLinkStr(link) &&
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { link[0] != '#' && !strings.HasPrefix(link, mailto) {
lnk := string(link) base := r.Ctx.Links.Base
if r.IsWiki { switch l.Kind() {
lnk = util.URLJoin("wiki", lnk) case "image", "video":
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
} }
link = []byte(util.URLJoin(r.URLPrefix, lnk)) link = util.URLJoin(base, link)
} }
return link
}
// WriteRegularLink renders images, links or videos
func (r *Writer) WriteRegularLink(l org.RegularLink) {
link := r.resolveLink(l)
// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
switch l.Kind() { switch l.Kind() {
case "image": case "image":
if l.Description == nil { if l.Description == nil {
imageSrc := getMediaURL(link) fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
fmt.Fprintf(r, `<img src="%s" alt="%s" />`, imageSrc, link)
} else { } else {
description := strings.TrimPrefix(org.String(l.Description...), "file:") imageSrc := r.resolveLink(l.Description[0].(org.RegularLink))
imageSrc := getMediaURL([]byte(description))
fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc) fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
} }
case "video": case "video":
if l.Description == nil { if l.Description == nil {
imageSrc := getMediaURL(link) fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
fmt.Fprintf(r, `<video src="%s">%s</video>`, imageSrc, link)
} else { } else {
description := strings.TrimPrefix(org.String(l.Description...), "file:") videoSrc := r.resolveLink(l.Description[0].(org.RegularLink))
videoSrc := getMediaURL([]byte(description))
fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc) fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
} }
default: default:
description := string(link) description := link
if l.Description != nil { if l.Description != nil {
description = r.WriteNodesAsString(l.Description...) description = r.WriteNodesAsString(l.Description...)
} }
fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description) fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description)
} }
} }
func getMediaURL(l []byte) string {
srcURL := string(l)
// Check if link is valid
if len(srcURL) > 0 && !markup.IsLink(l) {
srcURL = strings.Replace(srcURL, "/src/", "/media/", 1)
}
return srcURL
}

@ -27,8 +27,10 @@ func TestRender_StandardLinks(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -48,8 +50,10 @@ func TestRender_Media(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -84,8 +88,7 @@ func TestRender_Source(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
) )
@ -69,7 +70,7 @@ type RenderContext struct {
RelativePath string // relative path from tree root of the branch RelativePath string // relative path from tree root of the branch
Type string Type string
IsWiki bool IsWiki bool
URLPrefix string Links Links
Metas map[string]string Metas map[string]string
DefaultLink string DefaultLink string
GitRepo *git.Repository GitRepo *git.Repository
@ -80,6 +81,45 @@ type RenderContext struct {
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
} }
type Links struct {
Base string
BranchPath string
TreePath string
}
func (l *Links) HasBranchInfo() bool {
return l.BranchPath != ""
}
func (l *Links) SrcLink() string {
return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath)
}
func (l *Links) MediaLink() string {
return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath)
}
func (l *Links) RawLink() string {
return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath)
}
func (l *Links) WikiLink() string {
return util.URLJoin(l.Base, "wiki")
}
func (l *Links) WikiRawLink() string {
return util.URLJoin(l.Base, "wiki/raw")
}
func (l *Links) ResolveMediaLink(isWiki bool) string {
if isWiki {
return l.WikiRawLink()
} else if l.HasBranchInfo() {
return l.MediaLink()
}
return l.Base
}
// Cancel runs any cleanup functions that have been registered for this Ctx // Cancel runs any cleanup functions that have been registered for this Ctx
func (ctx *RenderContext) Cancel() { func (ctx *RenderContext) Cancel() {
if ctx == nil { if ctx == nil {

@ -161,7 +161,6 @@ func NewFuncMap() template.FuncMap {
"RenderEmoji": RenderEmoji, "RenderEmoji": RenderEmoji,
"RenderEmojiPlain": emoji.ReplaceAliases, "RenderEmojiPlain": emoji.ReplaceAliases,
"ReactionToEmoji": ReactionToEmoji, "ReactionToEmoji": ReactionToEmoji,
"RenderNote": RenderNote,
"RenderMarkdownToHtml": RenderMarkdownToHtml, "RenderMarkdownToHtml": RenderMarkdownToHtml,
"RenderLabel": RenderLabel, "RenderLabel": RenderLabel,

@ -24,21 +24,13 @@ import (
) )
// RenderCommitMessage renders commit message with XSS-safe and special links. // RenderCommitMessage renders commit message with XSS-safe and special links.
func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
}
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
// default url, handling for special links.
func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
cleanMsg := template.HTMLEscapeString(msg) cleanMsg := template.HTMLEscapeString(msg)
// we can safely assume that it will not return any error, since there // we can safely assume that it will not return any error, since there
// shouldn't be any special HTML. // shouldn't be any special HTML.
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Metas: metas,
DefaultLink: urlDefault,
Metas: metas,
}, cleanMsg) }, cleanMsg)
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("RenderCommitMessage: %v", err)
@ -51,9 +43,9 @@ func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault str
return template.HTML(msgLines[0]) return template.HTML(msgLines[0])
} }
// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
// the provided default url, handling for special links without email to links. // the provided default url, handling for special links without email to links.
func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML { func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
lineEnd := strings.IndexByte(msgLine, '\n') lineEnd := strings.IndexByte(msgLine, '\n')
if lineEnd > 0 { if lineEnd > 0 {
@ -68,7 +60,6 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa
// shouldn't be any special HTML. // shouldn't be any special HTML.
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix,
DefaultLink: urlDefault, DefaultLink: urlDefault,
Metas: metas, Metas: metas,
}, template.HTMLEscapeString(msgLine)) }, template.HTMLEscapeString(msgLine))
@ -80,7 +71,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa
} }
// RenderCommitBody extracts the body of a commit message without its title. // RenderCommitBody extracts the body of a commit message without its title.
func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { func RenderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML {
msgLine := strings.TrimSpace(msg) msgLine := strings.TrimSpace(msg)
lineEnd := strings.IndexByte(msgLine, '\n') lineEnd := strings.IndexByte(msgLine, '\n')
if lineEnd > 0 { if lineEnd > 0 {
@ -94,9 +85,8 @@ func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[stri
} }
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Metas: metas,
Metas: metas,
}, template.HTMLEscapeString(msgLine)) }, template.HTMLEscapeString(msgLine))
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("RenderCommitMessage: %v", err)
@ -115,11 +105,10 @@ func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
} }
// RenderIssueTitle renders issue/pull title with defined post processors // RenderIssueTitle renders issue/pull title with defined post processors
func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML { func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML {
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Metas: metas,
Metas: metas,
}, template.HTMLEscapeString(text)) }, template.HTMLEscapeString(text))
if err != nil { if err != nil {
log.Error("RenderIssueTitle: %v", err) log.Error("RenderIssueTitle: %v", err)
@ -211,26 +200,10 @@ func ReactionToEmoji(reaction string) template.HTML {
return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
} }
// RenderNote renders the contents of a git-notes file as a commit message.
func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
cleanMsg := template.HTMLEscapeString(msg)
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx,
URLPrefix: urlPrefix,
Metas: metas,
}, cleanMsg)
if err != nil {
log.Error("RenderNote: %v", err)
return ""
}
return template.HTML(fullMessage)
}
func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
output, err := markdown.RenderString(&markup.RenderContext{ output, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: setting.AppSubURL, Metas: map[string]string{"mode": "document"},
Metas: map[string]string{"mode": "document"},
}, input) }, input)
if err != nil { if err != nil {
log.Error("RenderString: %v", err) log.Error("RenderString: %v", err)

@ -6,17 +6,64 @@ package templates
import ( import (
"context" "context"
"html/template" "html/template"
"os"
"testing" "testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const testInput = ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
[remote link](https://example.com)
[[local link|file.bin]]
[[remote link|https://example.com]]
![local image](image.jpg)
![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
[[remote link|https://example.com/image.jpg]]
https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
:+1:
mail@domain.com
@mention-user test
#123
space
`
var testMetas = map[string]string{
"user": "user13",
"repo": "repo11",
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
"mode": "comment",
}
func TestMain(m *testing.M) {
unittest.InitSettings()
if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err)
}
markup.Init(&markup.ProcessorHelper{
IsUsernameMentionable: func(ctx context.Context, username string) bool {
return username == "mention-user"
},
})
os.Exit(m.Run())
}
func TestRenderCommitBody(t *testing.T) { func TestRenderCommitBody(t *testing.T) {
type args struct { type args struct {
ctx context.Context ctx context.Context
msg string msg string
urlPrefix string metas map[string]string
metas map[string]string
} }
tests := []struct { tests := []struct {
name string name string
@ -50,7 +97,91 @@ func TestRenderCommitBody(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas), "RenderCommitBody(%v, %v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas) assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.metas), "RenderCommitBody(%v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.metas)
}) })
} }
expected := `/just/a/path.bin
<a href="https://example.com/file.bin" class="link">https://example.com/file.bin</a>
[local link](file.bin)
[remote link](<a href="https://example.com" class="link">https://example.com</a>)
[[local link|file.bin]]
[[remote link|<a href="https://example.com" class="link">https://example.com</a>]]
![local image](image.jpg)
![remote image](<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>)
[[local image|image.jpg]]
[[remote link|<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>]]
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code class="nohighlight">88fc37a3c0...12fc37a3c0 (hash)</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code class="nohighlight">88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test
<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
space`
assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
}
func TestRenderCommitMessage(t *testing.T) {
expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> `
assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
}
func TestRenderCommitMessageLinkSubject(t *testing.T) {
expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>`
assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
}
func TestRenderIssueTitle(t *testing.T) {
expected := ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
[remote link](https://example.com)
[[local link|file.bin]]
[[remote link|https://example.com]]
![local image](image.jpg)
![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
[[remote link|https://example.com/image.jpg]]
https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
mail@domain.com
@mention-user test
<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
space
`
assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
}
func TestRenderMarkdownToHtml(t *testing.T) {
expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/>
/just/a/path.bin
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
<a href="/file.bin" rel="nofollow">local link</a>
<a href="https://example.com" rel="nofollow">remote link</a>
<a href="/src/file.bin" rel="nofollow">local link</a>
<a href="https://example.com" rel="nofollow">remote link</a>
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test
#123
space</p>
`
assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput))
} }

@ -32,8 +32,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
case "markdown": case "markdown":
// Raw markdown // Raw markdown
if err := markdown.RenderRaw(&markup.RenderContext{ if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Links: markup.Links{
Base: urlPrefix,
},
}, strings.NewReader(text), ctx.Resp); err != nil { }, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
} }
@ -75,8 +77,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
} }
if err := markup.Render(&markup.RenderContext{ if err := markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Links: markup.Links{
Base: urlPrefix,
},
Metas: meta, Metas: meta,
IsWiki: wiki, IsWiki: wiki,
Type: markupType, Type: markupType,

@ -51,9 +51,11 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
// If rendering fails, the original markdown text is returned // If rendering fails, the original markdown text is returned
func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string { func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
markdownCtx := &markup.RenderContext{ markdownCtx := &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: act.GetRepoLink(ctx), Links: markup.Links{
Type: markdown.MarkupName, Base: act.GetRepoLink(ctx),
},
Type: markdown.MarkupName,
Metas: map[string]string{ Metas: map[string]string{
"user": act.GetRepoUserName(ctx), "user": act.GetRepoUserName(ctx),
"repo": act.GetRepoName(ctx), "repo": act.GetRepoName(ctx),
@ -199,7 +201,6 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
switch act.OpType { switch act.OpType {
case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush: case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
push := templates.ActionContent2Commits(act) push := templates.ActionContent2Commits(act)
repoLink := act.GetRepoAbsoluteLink(ctx)
for _, commit := range push.Commits { for _, commit := range push.Commits {
if len(desc) != 0 { if len(desc) != 0 {
@ -208,7 +209,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s", desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s",
html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)), html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)),
commit.Sha1, commit.Sha1,
templates.RenderCommitMessage(ctx, commit.Message, repoLink, nil), templates.RenderCommitMessage(ctx, commit.Message, nil),
) )
} }
@ -288,9 +289,11 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
link := &feeds.Link{Href: rel.HTMLURL()} link := &feeds.Link{Href: rel.HTMLURL()}
content, err = markdown.RenderString(&markup.RenderContext{ content, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: rel.Repo.Link(), Links: markup.Links{
Metas: rel.Repo.ComposeMetas(ctx), Base: rel.Repo.Link(),
},
Metas: rel.Repo.ComposeMetas(ctx),
}, rel.Note) }, rel.Note)
if err != nil { if err != nil {

@ -42,8 +42,10 @@ func showUserFeed(ctx *context.Context, formatType string) {
} }
ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{ ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.ContextUser.HTMLURL(), Links: markup.Links{
Base: ctx.ContextUser.HTMLURL(),
},
Metas: map[string]string{ Metas: map[string]string{
"user": ctx.ContextUser.GetDisplayName(), "user": ctx.ContextUser.GetDisplayName(),
}, },

@ -5,6 +5,7 @@ package org
import ( import (
"net/http" "net/http"
"path"
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -47,10 +48,8 @@ func Home(ctx *context.Context) {
ctx.Data["Title"] = org.DisplayName() ctx.Data["Title"] = org.DisplayName()
if len(org.Description) != 0 { if len(org.Description) != 0 {
desc, err := markdown.RenderString(&markup.RenderContext{ desc, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.Repo.RepoLink, Metas: map[string]string{"mode": "document"},
Metas: map[string]string{"mode": "document"},
GitRepo: ctx.Repo.GitRepo,
}, org.Description) }, org.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -173,14 +172,16 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err) log.Error("failed to GetBlobContent: %v", err)
} else { } else {
// Pass URLPrefix to markdown render for the full link of media elements.
// The profile of default branch would be shown.
prefix := profileDbRepo.Link() + "/src/branch/" + util.PathEscapeSegments(profileDbRepo.DefaultBranch)
if profileContent, err := markdown.RenderString(&markup.RenderContext{ if profileContent, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
GitRepo: profileGitRepo, GitRepo: profileGitRepo,
URLPrefix: prefix, Links: markup.Links{
Metas: map[string]string{"mode": "document"}, // Pass repo link to markdown render for the full link of media elements.
// The profile of default branch would be shown.
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
},
Metas: map[string]string{"mode": "document"},
}, bytes); err != nil { }, bytes); err != nil {
log.Error("failed to RenderString: %v", err) log.Error("failed to RenderString: %v", err)
} else { } else {

@ -7,7 +7,9 @@ package repo
import ( import (
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"path"
"strings" "strings"
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
@ -21,7 +23,9 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitgraph" "code.gitea.io/gitea/modules/gitgraph"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository" git_service "code.gitea.io/gitea/services/repository"
) )
@ -370,9 +374,21 @@ func Diff(ctx *context.Context) {
note := &git.Note{} note := &git.Note{}
err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note) err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
if err == nil { if err == nil {
ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message))))
if err != nil {
ctx.ServerError("RenderCommitMessage", err)
return
}
} }
ctx.Data["BranchName"], err = commit.GetBranchName() ctx.Data["BranchName"], err = commit.GetBranchName()

@ -1436,12 +1436,13 @@ func ViewIssue(ctx *context.Context) {
} }
} }
ctx.Data["IssueWatch"] = iw ctx.Data["IssueWatch"] = iw
issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, issue.Content) }, issue.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -1601,10 +1602,12 @@ func ViewIssue(ctx *context.Context) {
} }
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content) }, comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -1678,10 +1681,12 @@ func ViewIssue(ctx *context.Context) {
} }
} else if comment.Type.HasContentSupport() { } else if comment.Type.HasContentSupport() {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content) }, comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -2234,10 +2239,12 @@ func UpdateIssueContent(ctx *context.Context) {
} }
content, err := markdown.RenderString(&markup.RenderContext{ content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, issue.Content) }, issue.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -3143,10 +3150,12 @@ func UpdateCommentContent(ctx *context.Context) {
} }
content, err := markdown.RenderString(&markup.RenderContext{ content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content) }, comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

@ -81,10 +81,12 @@ func Milestones(ctx *context.Context) {
} }
for _, m := range miles { for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, m.Content) }, m.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -275,10 +277,12 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
} }
milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, milestone.Content) }, milestone.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

@ -90,10 +90,12 @@ func Projects(ctx *context.Context) {
for i := range projects { for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, projects[i].Description) }, projects[i].Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -357,10 +359,12 @@ func ViewProject(ctx *context.Context) {
ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["LinkedPRs"] = linkedPrsMap
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, project.Description) }, project.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

@ -136,10 +136,12 @@ func Releases(ctx *context.Context) {
} }
r.Note, err = markdown.RenderString(&markup.RenderContext{ r.Note, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, r.Note) }, r.Note)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -287,10 +289,12 @@ func SingleRelease(ctx *context.Context) {
} }
} }
release.Note, err = markdown.RenderString(&markup.RenderContext{ release.Note, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, release.Note) }, release.Note)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

@ -57,16 +57,15 @@ func RenderFile(ctx *context.Context) {
return return
} }
treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts") ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
err = markup.Render(&markup.RenderContext{ err = markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
RelativePath: ctx.Repo.TreePath, RelativePath: ctx.Repo.TreePath,
URLPrefix: path.Dir(treeLink), Links: markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx), Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo, GitRepo: ctx.Repo.GitRepo,
InStandalonePage: true, InStandalonePage: true,

@ -158,7 +158,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
return "", readmeFile, nil return "", readmeFile, nil
} }
func renderDirectory(ctx *context.Context, treeLink string) { func renderDirectory(ctx *context.Context) {
entries := renderDirectoryFiles(ctx, 1*time.Second) entries := renderDirectoryFiles(ctx, 1*time.Second)
if ctx.Written() { if ctx.Written() {
return return
@ -175,7 +175,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
return return
} }
renderReadmeFile(ctx, subfolder, readmeFile, treeLink) renderReadmeFile(ctx, subfolder, readmeFile)
} }
// localizedExtensions prepends the provided language code with and without a // localizedExtensions prepends the provided language code with and without a
@ -259,7 +259,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
} }
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry, readmeTreelink string) { func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
target := readmeFile target := readmeFile
if readmeFile != nil && readmeFile.IsLink() { if readmeFile != nil && readmeFile.IsLink() {
target, _ = readmeFile.FollowLinks() target, _ = readmeFile.FollowLinks()
@ -312,9 +312,13 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
URLPrefix: path.Join(readmeTreelink, subfolder), Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Join(ctx.Repo.TreePath, subfolder),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
}, rd) }, rd)
if err != nil { if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
@ -337,7 +341,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
} }
} }
func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { func renderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["IsViewFile"] = true ctx.Data["IsViewFile"] = true
ctx.Data["HideRepoInfo"] = true ctx.Data["HideRepoInfo"] = true
blob := entry.Blob() blob := entry.Blob()
@ -351,7 +355,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileIsSymlink"] = entry.IsLink()
ctx.Data["FileName"] = blob.Name() ctx.Data["FileName"] = blob.Name()
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
if ctx.Repo.TreePath == ".editorconfig" { if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
@ -479,9 +483,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
Ctx: ctx, Ctx: ctx,
Type: markupType, Type: markupType,
RelativePath: ctx.Repo.TreePath, RelativePath: ctx.Repo.TreePath,
URLPrefix: path.Dir(treeLink), Links: markup.Links{
Metas: metas, Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: metas,
GitRepo: ctx.Repo.GitRepo,
}, rd) }, rd)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
@ -585,9 +593,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
RelativePath: ctx.Repo.TreePath, RelativePath: ctx.Repo.TreePath,
URLPrefix: path.Dir(treeLink), Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
}, rd) }, rd)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
@ -945,14 +957,6 @@ func renderCode(ctx *context.Context) {
} }
ctx.Data["Title"] = title ctx.Data["Title"] = title
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
// Get Topics of this repo // Get Topics of this repo
renderRepoTopics(ctx) renderRepoTopics(ctx)
if ctx.Written() { if ctx.Written() {
@ -977,9 +981,9 @@ func renderCode(ctx *context.Context) {
} }
if entry.IsDir() { if entry.IsDir() {
renderDirectory(ctx, treeLink) renderDirectory(ctx)
} else { } else {
renderFile(ctx, entry, treeLink, rawLink) renderFile(ctx, entry)
} }
if ctx.Written() { if ctx.Written() {
return return
@ -1020,6 +1024,12 @@ func renderCode(ctx *context.Context) {
} }
ctx.Data["Paths"] = paths ctx.Data["Paths"] = paths
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Data["TreeLink"] = treeLink ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink ctx.Data["BranchLink"] = branchLink

@ -238,10 +238,12 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
} }
rctx := &markup.RenderContext{ rctx := &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.Repo.RepoLink, Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx), Links: markup.Links{
IsWiki: true, Base: ctx.Repo.RepoLink,
},
IsWiki: true,
} }
buf := &strings.Builder{} buf := &strings.Builder{}

@ -47,10 +47,8 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
if len(ctx.ContextUser.Description) != 0 { if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{ content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Metas: map[string]string{"mode": "document"},
Metas: map[string]string{"mode": "document"}, Ctx: ctx,
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, ctx.ContextUser.Description) }, ctx.ContextUser.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

@ -258,9 +258,11 @@ func Milestones(ctx *context.Context) {
} }
milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: milestones[i].Repo.Link(), Links: markup.Links{
Metas: milestones[i].Repo.ComposeMetas(ctx), Base: milestones[i].Repo.Link(),
Ctx: ctx, },
Metas: milestones[i].Repo.ComposeMetas(ctx),
Ctx: ctx,
}, milestones[i].Content) }, milestones[i].Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

@ -7,6 +7,7 @@ package user
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"strings" "strings"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
@ -233,18 +234,19 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err) log.Error("failed to GetBlobContent: %v", err)
} else { } else {
// Give the URLPrefix to the markdown render for the full link of media element.
// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
// Eg. /Tom/.profile/media/branch/main
// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
// https://docs.gitea.com/usage/profile-readme
prefix := profileDbRepo.Link() + "/src/branch/" + util.PathEscapeSegments(profileDbRepo.DefaultBranch)
if profileContent, err := markdown.RenderString(&markup.RenderContext{ if profileContent, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
GitRepo: profileGitRepo, GitRepo: profileGitRepo,
URLPrefix: prefix, Links: markup.Links{
Metas: map[string]string{"mode": "document"}, // Give the repo link to the markdown render for the full link of media element.
// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
// Eg. /Tom/.profile/media/branch/main
// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
// https://docs.gitea.com/usage/profile-readme
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
},
Metas: map[string]string{"mode": "document"},
}, bytes); err != nil { }, bytes); err != nil {
log.Error("failed to RenderString: %v", err) log.Error("failed to RenderString: %v", err)
} else { } else {

@ -220,9 +220,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
// This is the body of the new issue or comment, not the mail body // This is the body of the new issue or comment, not the mail body
body, err := markdown.RenderString(&markup.RenderContext{ body, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.Issue.Repo.HTMLURL(), Links: markup.Links{
Metas: ctx.Issue.Repo.ComposeMetas(ctx), Base: ctx.Issue.Repo.HTMLURL(),
},
Metas: ctx.Issue.Repo.ComposeMetas(ctx),
}, ctx.Content) }, ctx.Content)
if err != nil { if err != nil {
return nil, err return nil, err

@ -57,9 +57,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
var err error var err error
rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{ rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: rel.Repo.Link(), Links: markup.Links{
Metas: rel.Repo.ComposeMetas(ctx), Base: rel.Repo.HTMLURL(),
},
Metas: rel.Repo.ComposeMetas(ctx),
}, rel.Note) }, rel.Note)
if err != nil { if err != nil {
log.Error("markdown.RenderString(%d): %v", rel.RepoID, err) log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)

@ -25,7 +25,7 @@
<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
</div> </div>
<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage .RepoLink (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> <p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
</td> </td>
<td class="right aligned middle aligned overflow-visible"> <td class="right aligned middle aligned overflow-visible">
{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
@ -101,7 +101,7 @@
<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
</div> </div>
<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage $.RepoLink ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> <p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
{{end}} {{end}}
</td> </td>
<td class="two wide ui"> <td class="two wide ui">

@ -19,7 +19,7 @@
{{end}} {{end}}
<div class="ui top attached header clearing segment gt-relative commit-header {{$class}}"> <div class="ui top attached header clearing segment gt-relative commit-header {{$class}}">
<div class="gt-df gt-mb-4 gt-fw"> <div class="gt-df gt-mb-4 gt-fw">
<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.RepoLink ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> <h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
{{if not $.PageIsWiki}} {{if not $.PageIsWiki}}
<div> <div>
<a class="ui primary tiny button" href="{{.SourcePath}}"> <a class="ui primary tiny button" href="{{.SourcePath}}">
@ -135,7 +135,7 @@
{{end}} {{end}}
</div> </div>
{{if IsMultilineCommitMessage .Commit.Message}} {{if IsMultilineCommitMessage .Commit.Message}}
<pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message $.RepoLink ($.Repository.ComposeMetas ctx)}}</pre> <pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</pre>
{{end}} {{end}}
{{template "repo/commit_load_branches_and_tags" .}} {{template "repo/commit_load_branches_and_tags" .}}
</div> </div>
@ -258,7 +258,7 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if .Note}} {{if .NoteRendered}}
<div class="ui top attached header segment git-notes"> <div class="ui top attached header segment git-notes">
{{svg "octicon-note" 16 "gt-mr-3"}} {{svg "octicon-note" 16 "gt-mr-3"}}
{{ctx.Locale.Tr "repo.diff.git-notes"}}: {{ctx.Locale.Tr "repo.diff.git-notes"}}:
@ -276,7 +276,7 @@
<span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span> <span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span>
</div> </div>
<div class="ui bottom attached info segment git-notes"> <div class="ui bottom attached info segment git-notes">
<pre class="commit-body">{{RenderNote $.Context .Note $.RepoLink ($.Repository.ComposeMetas ctx)}}</pre> <pre class="commit-body">{{.NoteRendered | Str2html}}</pre>
</div> </div>
{{end}} {{end}}
{{template "repo/diff/box" .}} {{template "repo/diff/box" .}}

@ -60,7 +60,7 @@
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span> <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
{{else}} {{else}}
{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitRepoLink $commitLink ($.Repository.ComposeMetas ctx)}}</span> <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
{{end}} {{end}}
</span> </span>
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
@ -68,7 +68,7 @@
{{end}} {{end}}
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message $commitRepoLink ($.Repository.ComposeMetas ctx)}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
{{end}} {{end}}
</td> </td>
{{if .Committer}} {{if .Committer}}

@ -38,12 +38,12 @@
</a> </a>
</span> </span>
<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span> <span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}} {{end}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}

@ -194,7 +194,7 @@
<div class="twelve wide column issue-title"> <div class="twelve wide column issue-title">
{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}} {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}}
<h1> <h1>
<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title $.RepoLink ($.Repository.ComposeMetas ctx)}}</span> <span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}</span>
<span class="index">#{{.PullRequest.Issue.Index}}</span> <span class="index">#{{.PullRequest.Issue.Index}}</span>
</h1> </h1>
</div> </div>

@ -29,7 +29,7 @@
</a> </a>
</span> </span>
<span class="message gt-dib gt-ellipsis gt-mr-3"> <span class="message gt-dib gt-ellipsis gt-mr-3">
<span>{{RenderCommitMessage $.Context $commit.Subject $.RepoLink ($.Repository.ComposeMetas ctx)}}</span> <span>{{RenderCommitMessage $.Context $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
</span> </span>
<span class="commit-refs gt-df gt-ac gt-mr-2"> <span class="commit-refs gt-df gt-ac gt-mr-2">
{{range $commit.Refs}} {{range $commit.Refs}}

@ -6,7 +6,7 @@
<div class="issue-title-header"> <div class="issue-title-header">
<div class="issue-title" id="issue-title-wrapper"> <div class="issue-title" id="issue-title-wrapper">
<h1 class="gt-word-break"> <h1 class="gt-word-break">
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title $.RepoLink ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span> <span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
</span> </span>
<div id="edit-title-input" class="ui input gt-f1 gt-hidden"> <div id="edit-title-input" class="ui input gt-f1 gt-hidden">
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off"> <input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">

@ -26,10 +26,10 @@
</a> </a>
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}}
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $.RepoLink $commitLink ($.Repository.ComposeMetas ctx)}}</span> <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}} {{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.RepoLink ($.Repository.ComposeMetas ctx)}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
{{end}} {{end}}
</span> </span>
{{end}} {{end}}
@ -83,7 +83,7 @@
<span class="truncate"> <span class="truncate">
{{if $commit}} {{if $commit}}
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
{{RenderCommitMessageLinkSubject $.Context $commit.Message $.RepoLink $commitLink ($.Repository.ComposeMetas ctx)}} {{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}}
{{else}} {{else}}
<div class="ui active tiny slow centered inline">…</div> <div class="ui active tiny slow centered inline">…</div>
{{end}} {{end}}

@ -91,7 +91,7 @@
<img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> <img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> <a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
<span class="text truncate"> <span class="text truncate">
{{RenderCommitMessage $.Context .Message $repoLink ($repo.ComposeMetas ctx)}} {{RenderCommitMessage $.Context .Message ($repo.ComposeMetas ctx)}}
</span> </span>
</div> </div>
{{end}} {{end}}

@ -15,8 +15,10 @@ import (
) )
var renderContext = markup.RenderContext{ var renderContext = markup.RenderContext{
Ctx: context.Background(), Ctx: context.Background(),
URLPrefix: "https://example.com/go-gitea/gitea", Links: markup.Links{
Base: "https://example.com/go-gitea/gitea",
},
Metas: map[string]string{ Metas: map[string]string{
"user": "go-gitea", "user": "go-gitea",
"repo": "gitea", "repo": "gitea",

Loading…
Cancel
Save