Skip to content

Refactor markup render to fix various path problems #34114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions models/renderhelper/repo_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions models/renderhelper/repo_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 5 additions & 5 deletions models/renderhelper/repo_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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)
})
Expand All @@ -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)
})

Expand Down
18 changes: 8 additions & 10 deletions models/renderhelper/repo_wiki.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions models/renderhelper/repo_wiki_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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)
})
Expand Down
10 changes: 8 additions & 2 deletions models/renderhelper/simple_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion models/renderhelper/simple_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
22 changes: 11 additions & 11 deletions modules/markup/external/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
73 changes: 29 additions & 44 deletions modules/markup/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]+:`)

Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 4 additions & 5 deletions modules/markup/html_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions modules/markup/html_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand All @@ -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))
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions modules/markup/html_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading