Refactor markup render to fix various path problems (#34114)

* Fix #33972
    * Use consistent path resolving for links and medias.
* No need to make the markup renders to resolve the paths, instead, the
paths are all correctly resolved in the "post process" step.
* Fix #33274
* Since 1.23, all paths starting with "/" are relative to current render
context (for example: the current repo branch)
* Introduce `/:root/path-relative-to-root`, then the path will be
rendered as relative to "ROOT_URL"
pull/34122/head^2
wxiaoguang 2025-04-04 23:45:23 +08:00 committed by GitHub
parent e8b54d9e44
commit 6cee3bfa96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 239 additions and 286 deletions

View File

@ -28,14 +28,14 @@ func (r *RepoComment) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) {
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
func (r *RepoComment) ResolveLink(link, preferLinkType string) string {
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
switch linkType {
case markup.LinkTypeRoot:
return r.ctx.ResolveLinkRoot(link)
default:
finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
}
return finalLink
}
var _ markup.RenderHelper = (*RepoComment)(nil)

View File

@ -29,17 +29,17 @@ func (r *RepoFile) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string {
finalLink := link
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
case markup.LinkTypeDefault:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) {
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
switch linkType {
case markup.LinkTypeRoot:
finalLink = r.ctx.ResolveLinkRoot(link)
case markup.LinkTypeRaw:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
case markup.LinkTypeMedia:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
default:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
}
return finalLink
}

View File

@ -48,8 +48,8 @@ func TestRepoFile(t *testing.T) {
assert.Equal(t,
`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a>
<a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a>
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
<a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
<a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
`, rendered)
})
@ -62,7 +62,7 @@ func TestRepoFile(t *testing.T) {
`)
assert.NoError(t, err)
assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a>
<a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
<a href="/user2/repo1/src/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
`, rendered)
})
@ -77,7 +77,7 @@ func TestRepoFile(t *testing.T) {
<video src="LINK">
`)
assert.NoError(t, err)
assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
assert.Equal(t, `<a href="/user2/repo1/src/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
<video src="/user2/repo1/media/commit/1234/my-dir/LINK">
</video>`, rendered)
})
@ -100,7 +100,7 @@ func TestRepoFileOrgMode(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, `<p>
<a href="https://google.com/" rel="nofollow">https://google.com/</a>
<a href="/user2/repo1/media/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
<a href="/user2/repo1/src/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
`, rendered)
})

View File

@ -30,18 +30,16 @@ func (r *RepoWiki) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string {
finalLink := link
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
case markup.LinkTypeDefault:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
case markup.LinkTypeMedia:
func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) {
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
switch linkType {
case markup.LinkTypeRoot:
finalLink = r.ctx.ResolveLinkRoot(link)
case markup.LinkTypeMedia, markup.LinkTypeRaw:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
case markup.LinkTypeRaw: // wiki doesn't use it
default:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
}
return finalLink
}

View File

@ -45,8 +45,8 @@ func TestRepoWiki(t *testing.T) {
assert.Equal(t,
`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a>
<a href="/user2/repo1/wiki/test" rel="nofollow">./test</a>
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
<a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
<a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
`, rendered)
})
@ -57,7 +57,7 @@ func TestRepoWiki(t *testing.T) {
<video src="LINK">
`)
assert.NoError(t, err)
assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
assert.Equal(t, `<a href="/user2/repo1/wiki/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
<video src="/user2/repo1/wiki/raw/LINK">
</video>`, rendered)
})

View File

@ -15,8 +15,14 @@ type SimpleDocument struct {
baseLink string
}
func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string {
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
func (r *SimpleDocument) ResolveLink(link, preferLinkType string) string {
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
switch linkType {
case markup.LinkTypeRoot:
return r.ctx.ResolveLinkRoot(link)
default:
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
}
}
var _ markup.RenderHelper = (*SimpleDocument)(nil)

View File

@ -30,7 +30,7 @@ func TestSimpleDocument(t *testing.T) {
assert.Equal(t,
`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
<a href="/base/user2" rel="nofollow">@user2</a></p>
<a href="/user2" rel="nofollow">@user2</a></p>
<p><a href="/base/test" rel="nofollow">/test</a>
<a href="/base/test" rel="nofollow">./test</a>
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a>

View File

@ -77,14 +77,14 @@ func envMark(envName string) string {
// 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 {
var (
command = strings.NewReplacer(
envMark("GITEA_PREFIX_SRC"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
envMark("GITEA_PREFIX_RAW"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
).Replace(p.Command)
commands = strings.Fields(command)
args = commands[1:]
)
baseLinkSrc := ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)
baseLinkRaw := ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw)
command := strings.NewReplacer(
envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
).Replace(p.Command)
commands := strings.Fields(command)
args := commands[1:]
if p.IsInputFile {
// write to temp file
@ -112,14 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
args = append(args, f.Name())
}
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)))
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], baseLinkSrc))
defer finished()
cmd := exec.CommandContext(processCtx, commands[0], args...)
cmd.Env = append(
os.Environ(),
"GITEA_PREFIX_SRC="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
"GITEA_PREFIX_RAW="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
"GITEA_PREFIX_SRC="+baseLinkSrc,
"GITEA_PREFIX_RAW="+baseLinkRaw,
)
if !p.IsInputFile {
cmd.Stdin = input

View File

@ -32,7 +32,6 @@ type globalVarsType struct {
comparePattern *regexp.Regexp
fullURLPattern *regexp.Regexp
emailRegex *regexp.Regexp
blackfridayExtRegex *regexp.Regexp
emojiShortCodeRegex *regexp.Regexp
issueFullPattern *regexp.Regexp
filesChangedFullPattern *regexp.Regexp
@ -74,9 +73,6 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
v.blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
// emojiShortCodeRegex find emoji by alias like :smile:
v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
@ -94,17 +90,12 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
return v
})
// IsFullURLBytes reports whether link fits valid format.
func IsFullURLBytes(link []byte) bool {
return globalVars().fullURLPattern.Match(link)
}
func IsFullURLString(link string) bool {
return globalVars().fullURLPattern.MatchString(link)
}
func IsNonEmptyRelativePath(link string) bool {
return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
return link != "" && !IsFullURLString(link) && link[0] != '?' && link[0] != '#'
}
// CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text
@ -316,44 +307,38 @@ func isEmojiNode(node *html.Node) bool {
}
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
// Add user-content- to IDs and "#" links if they don't already have them
for idx, attr := range node.Attr {
val := strings.TrimPrefix(attr.Val, "#")
notHasPrefix := !(strings.HasPrefix(val, "user-content-") || globalVars().blackfridayExtRegex.MatchString(val))
if attr.Key == "id" && notHasPrefix {
node.Attr[idx].Val = "user-content-" + attr.Val
}
if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
node.Attr[idx].Val = "#user-content-" + val
}
}
switch node.Type {
case html.TextNode:
if node.Type == html.TextNode {
for _, proc := range procs {
proc(ctx, node) // it might add siblings
}
return node.NextSibling
}
if node.Type != html.ElementNode {
return node.NextSibling
}
case html.ElementNode:
if isEmojiNode(node) {
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
return node.NextSibling
} else if node.Data == "code" || node.Data == "pre" {
return node.NextSibling // ignore code and pre nodes
} else if node.Data == "img" {
return visitNodeImg(ctx, node)
} else if node.Data == "video" {
return visitNodeVideo(ctx, node)
} else if node.Data == "a" {
procs = emojiProcessors // Restrict text in links to emojis
}
for n := node.FirstChild; n != nil; {
n = visitNode(ctx, procs, n)
}
default:
processNodeAttrID(node)
if isEmojiNode(node) {
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
return node.NextSibling
} else if node.Data == "code" || node.Data == "pre" {
return node.NextSibling // ignore code and pre nodes
} else if node.Data == "img" {
return visitNodeImg(ctx, node)
} else if node.Data == "video" {
return visitNodeVideo(ctx, node)
}
if node.Data == "a" {
processNodeA(ctx, node)
// only use emoji processors for the content in the "A" tag,
// because the content there is not processable, for example: the content is a commit id or a full URL.
procs = emojiProcessors
}
for n := node.FirstChild; n != nil; {
n = visitNode(ctx, procs, n)
}
return node.NextSibling
}

View File

@ -43,7 +43,6 @@ func createCodeLink(href, content, class string) *html.Node {
code := &html.Node{
Type: html.ElementNode,
Data: atom.Code.String(),
Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}},
}
code.AppendChild(text)
@ -189,7 +188,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
continue
}
link := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash), LinkTypeApp)
link := "/:root/" + util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash)
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
start = 0
node = node.NextSibling.NextSibling
@ -205,9 +204,9 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
return
}
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
link := createLink(ctx, linkHref, reftext, "commit")
refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha)
link := createLink(ctx, linkHref, refText, "commit")
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
node = node.NextSibling.NextSibling

View File

@ -107,7 +107,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
isExternal := false
if marker == "!" {
path = "pulls"
prefix = "http://localhost:3000/someUser/someRepo/pulls/"
prefix = "/someUser/someRepo/pulls/"
} else {
path = "issues"
prefix = "https://someurl.com/someUser/someRepo/"
@ -116,7 +116,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
links := make([]any, len(indices))
for i, index := range indices {
links[i] = numericIssueLink(util.URLJoin(TestRepoURL, path), "ref-issue", index, marker)
links[i] = numericIssueLink(util.URLJoin("/test-owner/test-repo", path), "ref-issue", index, marker)
}
expectedNil := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas))
@ -293,13 +293,13 @@ func TestRender_AutoLink(t *testing.T) {
// render valid commit URLs
tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae")
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24</code></a>")
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>")
tmp += "#diff-2"
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>")
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>")
// render other commit URLs
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>")
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>")
}
func TestRender_FullIssueURLs(t *testing.T) {

View File

@ -82,7 +82,7 @@ func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref
h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{
OwnerName: ref.Owner,
RepoName: ref.Name,
LinkHref: linkHref,
LinkHref: ctx.RenderHelper.ResolveLink(linkHref, LinkTypeDefault),
IssueIndex: issueIndex,
})
if err != nil {
@ -162,7 +162,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner)
issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name)
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp)
linkHref := "/:root/" + util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue)
// at the moment, only render the issue index in a full line (or simple line) as icon+title
// otherwise it would be too noisy for "take #1 as an example" in a sentence

View File

@ -39,7 +39,7 @@ func TestRender_IssueList(t *testing.T) {
t.Run("NormalIssueRef", func(t *testing.T) {
test(
"#12345",
`<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
`<p><a href="/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
)
})
@ -56,7 +56,7 @@ func TestRender_IssueList(t *testing.T) {
test(
"* foo #12345 bar",
`<ul>
<li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
<li>foo <a href="/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
</ul>`,
)
})

View File

@ -125,7 +125,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
}
}
if image {
link = ctx.RenderHelper.ResolveLink(link, LinkTypeMedia)
title := props["title"]
if title == "" {
title = props["alt"]
@ -151,7 +150,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
childNode.Attr = childNode.Attr[:2]
}
} else {
link = ctx.RenderHelper.ResolveLink(link, LinkTypeDefault)
childNode.Type = html.TextNode
childNode.Data = name
}

View File

@ -33,7 +33,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
if ok && strings.Contains(mention, "/") {
mentionOrgAndTeam := strings.Split(mention, "/")
if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
link := ctx.RenderHelper.ResolveLink(util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), LinkTypeApp)
link := "/:root/" + util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1])
replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
node = node.NextSibling.NextSibling
start = 0
@ -45,7 +45,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
mentionedUsername := mention[1:]
if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) {
link := ctx.RenderHelper.ResolveLink(mentionedUsername, LinkTypeApp)
link := "/:root/" + mentionedUsername
replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
node = node.NextSibling.NextSibling
start = 0

View File

@ -4,42 +4,79 @@
package markup
import (
"strings"
"golang.org/x/net/html"
)
func isAnchorIDUserContent(s string) bool {
// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
// old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
}
func processNodeAttrID(node *html.Node) {
// Add user-content- to IDs and "#" links if they don't already have them,
// and convert the link href to a relative link to the host root
for idx, attr := range node.Attr {
if attr.Key == "id" {
if !isAnchorIDUserContent(attr.Val) {
node.Attr[idx].Val = "user-content-" + attr.Val
}
}
}
}
func processNodeA(ctx *RenderContext, node *html.Node) {
for idx, attr := range node.Attr {
if attr.Key == "href" {
if anchorID, ok := strings.CutPrefix(attr.Val, "#"); ok {
if !isAnchorIDUserContent(attr.Val) {
node.Attr[idx].Val = "#user-content-" + anchorID
}
} else {
node.Attr[idx].Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeDefault)
}
}
}
}
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
next = img.NextSibling
for i, attr := range img.Attr {
if attr.Key != "src" {
for i, imgAttr := range img.Attr {
if imgAttr.Key != "src" {
continue
}
if IsNonEmptyRelativePath(attr.Val) {
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
imgSrcOrigin := imgAttr.Val
isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:")
// By default, the "<img>" tag should also be clickable,
// because frontend use `<img>` to paste the re-scaled image into the markdown,
// so it must match the default markdown image behavior.
hasParentAnchor := false
for p := img.Parent; p != nil; p = p.Parent {
if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
break
}
}
if !hasParentAnchor {
imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
{Key: "href", Val: attr.Val},
{Key: "target", Val: "_blank"},
}}
parent := img.Parent
imgNext := img.NextSibling
parent.RemoveChild(img)
parent.InsertBefore(imgA, imgNext)
imgA.AppendChild(img)
// By default, the "<img>" tag should also be clickable,
// because frontend use `<img>` to paste the re-scaled image into the markdown,
// so it must match the default markdown image behavior.
cnt := 0
for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent {
if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
isLinkable = false
break
}
cnt++
}
attr.Val = camoHandleLink(attr.Val)
img.Attr[i] = attr
if isLinkable {
wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
{Key: "target", Val: "_blank"},
}}
parent := img.Parent
imgNext := img.NextSibling
parent.RemoveChild(img)
parent.InsertBefore(wrapper, imgNext)
wrapper.AppendChild(img)
}
imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
imgAttr.Val = camoHandleLink(imgAttr.Val)
img.Attr[i] = imgAttr
}
return next
}

View File

@ -35,6 +35,7 @@ func TestRender_Commits(t *testing.T) {
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/"
commit := util.URLJoin(repo, "commit", sha)
commitPath := "/user13/repo11/commit/" + sha
tree := util.URLJoin(repo, "tree", sha, "src")
file := util.URLJoin(repo, "commit", sha, "example.txt")
@ -44,9 +45,9 @@ func TestRender_Commits(t *testing.T) {
commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha)
commitCompareWithHash := commitCompare + "#L2"
test(sha, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test(sha[:7], `<p><a href="`+commit[:len(commit)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`)
test(sha[:39], `<p><a href="`+commit[:len(commit)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test(sha, `<p><a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test(sha[:7], `<p><a href="`+commitPath[:len(commitPath)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`)
test(sha[:39], `<p><a href="`+commitPath[:len(commitPath)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test(commit, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test(tree, `<p><a href="`+tree+`" rel="nofollow"><code>65f1bf27bc/src</code></a></p>`)
@ -57,13 +58,13 @@ func TestRender_Commits(t *testing.T) {
test(commitCompare, `<p><a href="`+commitCompare+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code></a></p>`)
test(commitCompareWithHash, `<p><a href="`+commitCompareWithHash+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc (L2)</code></a></p>`)
test("commit "+sha, `<p>commit <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test("commit "+sha, `<p>commit <a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>")
test("deadbeef", `<p>deadbeef</p>`)
test("d27ace93", `<p>d27ace93</p>`)
test(sha[:14]+".x", `<p>`+sha[:14]+`.x</p>`)
expected14 := `<a href="` + commit[:len(commit)-(40-14)] + `" rel="nofollow"><code>` + sha[:10] + `</code></a>`
expected14 := `<a href="` + commitPath[:len(commitPath)-(40-14)] + `" rel="nofollow"><code>` + sha[:10] + `</code></a>`
test(sha[:14]+".", `<p>`+expected14+`.</p>`)
test(sha[:14]+",", `<p>`+expected14+`,</p>`)
test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`)
@ -80,10 +81,10 @@ func TestRender_CrossReferences(t *testing.T) {
test(
"test-owner/test-repo#12345",
`<p><a href="`+util.URLJoin(markup.TestAppURL, "test-owner", "test-repo", "issues", "12345")+`" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
`<p><a href="/test-owner/test-repo/issues/12345" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
test(
"go-gitea/gitea#12345",
`<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
`<p><a href="/go-gitea/gitea/issues/12345" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
test(
"/home/gitea/go-gitea/gitea#12345",
`<p>/home/gitea/go-gitea/gitea#12345</p>`)
@ -487,7 +488,7 @@ func TestPostProcess_RenderDocument(t *testing.T) {
// But cross-referenced issue index should work.
test(
"go-gitea/gitea#12345",
`<a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue">go-gitea/gitea#12345</a>`)
`<a href="/go-gitea/gitea/issues/12345" class="ref-issue">go-gitea/gitea#12345</a>`)
// Test that other post processing still works.
test(
@ -543,7 +544,7 @@ func TestIssue18471(t *testing.T) {
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err)
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String())
}
func TestIsFullURL(t *testing.T) {

View File

@ -65,10 +65,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
g.transformHeading(ctx, v, reader, &tocList)
case *ast.Paragraph:
g.applyElementDir(v)
case *ast.Image:
g.transformImage(ctx, v)
case *ast.Link:
g.transformLink(ctx, v)
case *ast.List:
g.transformList(ctx, v, rc)
case *ast.Text:

View File

@ -308,12 +308,12 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
testcase := `![image1](/image1)
![image2](/image2)
`
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a>
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"/></a>
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"/></a></p>
`
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase)
res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase)
assert.NoError(t, err)
assert.Equal(t, expected, res)
assert.Equal(t, expected, string(res))
}
func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
@ -529,3 +529,16 @@ space</p>
assert.NoError(t, err)
assert.Equal(t, expected, string(result))
}
func TestMarkdownLink(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
input := `<a href=foo>link1</a>
<a href='/foo'>link2</a>
<a href="#foo">link3</a>`
result, err := markdown.RenderString(markup.NewTestRenderContext("/base", localMetas), input)
assert.NoError(t, err)
assert.Equal(t, `<p><a href="/base/foo" rel="nofollow">link1</a>
<a href="/base/foo" rel="nofollow">link2</a>
<a href="#user-content-foo" rel="nofollow">link3</a></p>
`, string(result))
}

View File

@ -1,59 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markdown
import (
"code.gitea.io/gitea/modules/markup"
"github.com/yuin/goldmark/ast"
)
func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image) {
// Images need two things:
//
// 1. Their src needs to munged to be a real value
// 2. If they're not wrapped with a link they need a link wrapper
// Check if the destination is a real link
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
v.Destination = []byte(ctx.RenderHelper.ResolveLink(string(v.Destination), markup.LinkTypeMedia))
}
parent := v.Parent()
// Create a link around image only if parent is not already a link
if _, ok := parent.(*ast.Link); !ok && parent != nil {
next := v.NextSibling()
// Create a link wrapper
wrap := ast.NewLink()
wrap.Destination = v.Destination
wrap.Title = v.Title
wrap.SetAttributeString("target", []byte("_blank"))
// Duplicate the current image node
image := ast.NewImage(ast.NewLink())
image.Destination = v.Destination
image.Title = v.Title
for _, attr := range v.Attributes() {
image.SetAttribute(attr.Name, attr.Value)
}
for child := v.FirstChild(); child != nil; {
next := child.NextSibling()
image.AppendChild(image, child)
child = next
}
// Append our duplicate image to the wrapper link
wrap.AppendChild(wrap, image)
// Wire in the next sibling
wrap.SetNextSibling(next)
// Replace the current node with the wrapper link
parent.ReplaceChild(parent, v, wrap)
// But most importantly ensure the next sibling is still on the old image too
v.SetNextSibling(next)
}
}

View File

@ -1,27 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markdown
import (
"code.gitea.io/gitea/modules/markup"
"github.com/yuin/goldmark/ast"
)
func resolveLink(ctx *markup.RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
isAnchorFragment := link != "" && link[0] == '#'
if !isAnchorFragment && !markup.IsFullURLString(link) {
link, resolved = ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault), true
}
if isAnchorFragment && userContentAnchorPrefix != "" {
link, resolved = userContentAnchorPrefix+link[1:], true
}
return link, resolved
}
func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) {
if link, resolved := resolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
v.Destination = []byte(link)
}
}

View File

@ -1,7 +1,7 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
package orgmode
import (
"fmt"
@ -125,27 +125,13 @@ type orgWriter struct {
var _ org.Writer = (*orgWriter)(nil)
func (r *orgWriter) resolveLink(kind, link string) string {
link = strings.TrimPrefix(link, "file:")
if !strings.HasPrefix(link, "#") && // not a URL fragment
!markup.IsFullURLString(link) {
if kind == "regular" {
// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]"
// so we need to try to guess the link kind again here
kind = org.RegularLink{URL: link}.Kind()
}
if kind == "image" || kind == "video" {
link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia)
} else {
link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault)
}
}
return link
func (r *orgWriter) resolveLink(link string) string {
return strings.TrimPrefix(link, "file:")
}
// WriteRegularLink renders images, links or videos
func (r *orgWriter) WriteRegularLink(l org.RegularLink) {
link := r.resolveLink(l.Kind(), l.URL)
link := r.resolveLink(l.URL)
printHTML := func(html template.HTML, a ...any) {
_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...))
@ -156,14 +142,14 @@ func (r *orgWriter) WriteRegularLink(l org.RegularLink) {
if l.Description == nil {
printHTML(`<img src="%s" alt="%s">`, link, link)
} else {
imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
imageSrc := r.resolveLink(org.String(l.Description...))
printHTML(`<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc)
}
case "video":
if l.Description == nil {
printHTML(`<video src="%s">%s</video>`, link, link)
} else {
videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
videoSrc := r.resolveLink(org.String(l.Description...))
printHTML(`<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
}
default:

View File

@ -1,7 +1,7 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
package orgmode_test
import (
"os"
@ -9,6 +9,7 @@ import (
"testing"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/orgmode"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@ -22,7 +23,7 @@ func TestMain(m *testing.M) {
func TestRender_StandardLinks(t *testing.T) {
test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/media/branch/main/"), input)
buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -30,37 +31,37 @@ func TestRender_StandardLinks(t *testing.T) {
test("[[https://google.com/]]",
`<p><a href="https://google.com/">https://google.com/</a></p>`)
test("[[ImageLink.svg][The Image Desc]]",
`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
`<p><a href="ImageLink.svg">The Image Desc</a></p>`)
}
func TestRender_InternalLinks(t *testing.T) {
test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/src/branch/main"), input)
buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
test("[[file:test.org][Test]]",
`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
`<p><a href="test.org">Test</a></p>`)
test("[[./test.org][Test]]",
`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
`<p><a href="./test.org">Test</a></p>`)
test("[[test.org][Test]]",
`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
`<p><a href="test.org">Test</a></p>`)
test("[[path/to/test.org][Test]]",
`<p><a href="/relative-path/src/branch/main/path/to/test.org">Test</a></p>`)
`<p><a href="path/to/test.org">Test</a></p>`)
}
func TestRender_Media(t *testing.T) {
test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext("./relative-path"), input)
buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
test("[[file:../../.images/src/02/train.jpg]]",
`<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg"></p>`)
`<p><img src="../../.images/src/02/train.jpg" alt="../../.images/src/02/train.jpg"></p>`)
test("[[file:train.jpg]]",
`<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg"></p>`)
`<p><img src="train.jpg" alt="train.jpg"></p>`)
// With description.
test("[[https://example.com][https://example.com/example.svg]]",
@ -91,7 +92,7 @@ func TestRender_Media(t *testing.T) {
func TestRender_Source(t *testing.T) {
test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext(), input)
buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}

View File

@ -261,8 +261,14 @@ func (r *TestRenderHelper) IsCommitIDExisting(commitID string) bool {
return strings.HasPrefix(commitID, "65f1bf2") //|| strings.HasPrefix(commitID, "88fc37a")
}
func (r *TestRenderHelper) ResolveLink(link string, likeType LinkType) string {
return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
func (r *TestRenderHelper) ResolveLink(link, preferLinkType string) string {
linkType, link := ParseRenderedLink(link, preferLinkType)
switch linkType {
case LinkTypeRoot:
return r.ctx.ResolveLinkRoot(link)
default:
return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
}
}
var _ RenderHelper = (*TestRenderHelper)(nil)

View File

@ -10,13 +10,11 @@ import (
"code.gitea.io/gitea/modules/setting"
)
type LinkType string
const (
LinkTypeApp LinkType = "app" // the link is relative to the AppSubURL
LinkTypeDefault LinkType = "default" // the link is relative to the default base (eg: repo link, or current ref tree path)
LinkTypeMedia LinkType = "media" // the link should be used to access media files (images, videos)
LinkTypeRaw LinkType = "raw" // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders
LinkTypeDefault = ""
LinkTypeRoot = "/:root" // the link is relative to the AppSubURL(ROOT_URL)
LinkTypeMedia = "/:media" // the link should be used to access media files (images, videos)
LinkTypeRaw = "/:raw" // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders
)
type RenderHelper interface {
@ -27,7 +25,7 @@ type RenderHelper interface {
// but not make processors to guess "is it rendering a comment or a wiki?" or "does it need to check commit ID?"
IsCommitIDExisting(commitID string) bool
ResolveLink(link string, likeType LinkType) string
ResolveLink(link, preferLinkType string) string
}
// RenderHelperFuncs is used to decouple cycle-import
@ -51,7 +49,8 @@ func (r *SimpleRenderHelper) IsCommitIDExisting(commitID string) bool {
return false
}
func (r *SimpleRenderHelper) ResolveLink(link string, likeType LinkType) string {
func (r *SimpleRenderHelper) ResolveLink(link, preferLinkType string) string {
_, link = ParseRenderedLink(link, preferLinkType)
return resolveLinkRelative(context.Background(), setting.AppSubURL+"/", "", link, false)
}

View File

@ -33,10 +33,24 @@ func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute b
return finalLink
}
func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) (finalLink string) {
func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) string {
if strings.HasPrefix(link, "/:") {
setting.PanicInDevOrTesting("invalid link %q, forgot to cut?", link)
}
return resolveLinkRelative(ctx, base, cur, link, ctx.RenderOptions.UseAbsoluteLink)
}
func (ctx *RenderContext) ResolveLinkApp(link string) string {
func (ctx *RenderContext) ResolveLinkRoot(link string) string {
return ctx.ResolveLinkRelative(setting.AppSubURL+"/", "", link)
}
func ParseRenderedLink(s, preferLinkType string) (linkType, link string) {
if strings.HasPrefix(s, "/:") {
p := strings.IndexByte(s[1:], '/')
if p == -1 {
return s, ""
}
return s[:p+1], s[p+2:]
}
return preferLinkType, s
}

View File

@ -123,9 +123,9 @@ func TestRenderCommitBody(t *testing.T) {
![remote image](<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>)
[[local image|image.jpg]]
[[remote link|<a href="https://example.com/image.jpg">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>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code>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>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code>88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com">mail@domain.com</a>

View File

@ -134,7 +134,7 @@ Here are some links to the most important topics. You can find the full list of
<h2 id="user-content-quick-links">Quick Links</h2>
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
<p><a href="http://localhost:3000/user2/repo1/wiki/Configuration" rel="nofollow">Configuration</a>
<a href="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
<a href="http://localhost:3000/user2/repo1/wiki/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
`,
}
@ -158,19 +158,19 @@ Here are some links to the most important topics. You can find the full list of
input := "[Link](test.md)\n![Image](image.png)"
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/path/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
<a href="http://localhost:3000/user2/repo1/src/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)