mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	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"
This commit is contained in:
		@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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>`,
 | 
			
		||||
		)
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -308,12 +308,12 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
 | 
			
		||||
	testcase := `
 | 
			
		||||

 | 
			
		||||
`
 | 
			
		||||
	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))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -123,9 +123,9 @@ func TestRenderCommitBody(t *testing.T) {
 | 
			
		||||

 | 
			
		||||
[[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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
	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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user