enhance: improve commit and compare link rendering (#40154)

Commit, compare and SHA links render the SHA in code with the rest of
the URL after it, and links to other repositories get an `owner/repo@`
prefix like references do.

- Abbreviated SHAs, branch compares and `<url>` autolinks now render as
commit links
- Pull request commit URLs no longer break into an issue reference plus
stray text
- Only routes that take a SHA are shortened, so file and API URLs stay
plain

Replaces https://github.com/go-gitea/gitea/pull/36725

<img width="620" alt="Commit links before and after"
src="https://github.com/user-attachments/assets/5919c8a0-94e3-4148-ad11-6944bf790572"
/>
This commit is contained in:
silverwind
2026-11-07 22:43:07 +00:00
committed by GitHub
parent 87a703819b
commit 51879941dc
7 changed files with 121 additions and 277 deletions

View File

@@ -32,12 +32,11 @@ const (
type globalVarsType struct { type globalVarsType struct {
hashCurrentPattern *regexp.Regexp hashCurrentPattern *regexp.Regexp
shortLinkPattern *regexp.Regexp shortLinkPattern *regexp.Regexp
anyHashPattern *regexp.Regexp compareURLPattern *regexp.Regexp
comparePattern *regexp.Regexp commitURLPattern *regexp.Regexp
emailRegex *regexp.Regexp emailRegex *regexp.Regexp
emojiShortCodeRegex *regexp.Regexp emojiShortCodeRegex *regexp.Regexp
issueFullPattern *regexp.Regexp issueFullPattern *regexp.Regexp
filesChangedFullPattern *regexp.Regexp
codePreviewPattern *regexp.Regexp codePreviewPattern *regexp.Regexp
tagCleaner *regexp.Regexp tagCleaner *regexp.Regexp
@@ -61,11 +60,9 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
// anyHashPattern splits url containing SHA into parts // repo sub-paths of the compare page and of the routes that take a commit SHA
v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})((\.\w+)*)(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) v.compareURLPattern = regexp.MustCompile(`^/compare/(.+?)(\.\.\.?)(.+?)((?:\.diff|\.patch)?)$`)
v.commitURLPattern = regexp.MustCompile(`^/(?:(?:[a-z-]+/)?commit|blob|archive|pulls/[0-9]+/commits)/([0-9a-f]{7,64})((?:[./].*)?)$`)
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
// emailRegex is definitely not perfect with edge cases, // emailRegex is definitely not perfect with edge cases,
// it is still accepted by the CommonMark specification, as well as the HTML5 spec: // it is still accepted by the CommonMark specification, as well as the HTML5 spec:
@@ -80,9 +77,6 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// example: https://domain/org/repo/pulls/27#hash // example: https://domain/org/repo/pulls/27#hash
v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
// example: https://domain/org/repo/pulls/27/files#hash
v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20" // codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
@@ -102,9 +96,7 @@ type processor func(ctx *RenderContext, node *html.Node)
func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error { func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error {
procs := []processor{ procs := []processor{
fullIssuePatternProcessor, fullIssuePatternProcessor,
comparePatternProcessor,
codePreviewPatternProcessor, codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor, shortLinkProcessor,
linkProcessor, linkProcessor,
mentionProcessor, mentionProcessor,
@@ -122,8 +114,6 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML { func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML {
procs := []processor{ procs := []processor{
fullIssuePatternProcessor, fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor, linkProcessor,
mentionProcessor, mentionProcessor,
issueIndexPatternProcessor, issueIndexPatternProcessor,
@@ -147,8 +137,6 @@ var emojiProcessors = []processor{
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) template.HTML { func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) template.HTML {
procs := []processor{ procs := []processor{
fullIssuePatternProcessor, fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
mentionProcessor, mentionProcessor,
issueIndexPatternProcessor, issueIndexPatternProcessor,
commitCrossReferencePatternProcessor, commitCrossReferencePatternProcessor,
@@ -361,6 +349,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
} }
if node.Data == "a" { if node.Data == "a" {
shortenCommitLink(ctx, node)
processNodeA(ctx, node) processNodeA(ctx, node)
// only use emoji processors for the content in the "A" tag, // 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. // because the content there is not processable, for example: the content is a commit id or a full URL.

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"gitea.dev/modules/base" "gitea.dev/modules/base"
"gitea.dev/modules/git"
"gitea.dev/modules/httplib" "gitea.dev/modules/httplib"
"gitea.dev/modules/references" "gitea.dev/modules/references"
@@ -16,176 +17,65 @@ import (
"golang.org/x/net/html/atom" "golang.org/x/net/html/atom"
) )
type anyHashPatternResult struct { func setCommitLinkContent(anchor *html.Node, prefix, refs, rest string) {
PosStart int anchor.Attr = append(anchor.Attr, html.Attribute{Key: "class", Val: "commit"}) // plain, so markdown strips it and commit messages keep it for CSS
PosEnd int anchor.FirstChild.Data = prefix
FullURL string code := &html.Node{Type: html.ElementNode, Data: atom.Code.String()}
CommitID string code.AppendChild(&html.Node{Type: html.TextNode, Data: refs})
CommitExt string anchor.AppendChild(code)
SubPath string if rest != "" {
QueryParams string anchor.AppendChild(&html.Node{Type: html.TextNode, Data: rest})
QueryHash string
}
func createCodeLink(href, content, class string) *html.Node {
a := &html.Node{
Type: html.ElementNode,
Data: atom.A.String(),
Attr: []html.Attribute{{Key: "href", Val: href}},
}
if class != "" {
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
}
text := &html.Node{
Type: html.TextNode,
Data: content,
}
code := &html.Node{
Type: html.ElementNode,
Data: atom.Code.String(),
}
code.AppendChild(text)
a.AppendChild(code)
return a
}
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
m := globalVars().anyHashPattern.FindStringSubmatchIndex(s)
if m == nil {
return ret, false
}
pos := 0
ret.PosStart, ret.PosEnd = m[pos], m[pos+1]
pos += 2
ret.FullURL = s[ret.PosStart:ret.PosEnd]
if strings.HasSuffix(ret.FullURL, ".") {
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
ret.PosEnd--
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
for i := range m {
m[i] = min(m[i], ret.PosEnd)
} }
} }
ret.CommitID = s[m[pos]:m[pos+1]] func createCommitLink(ctx *RenderContext, href, prefix, sha string) *html.Node {
pos += 2 anchor := createLink(ctx, href, "", "")
setCommitLinkContent(anchor, prefix, base.ShortSha(sha), "")
ret.CommitExt = s[m[pos]:m[pos+1]] return anchor
pos += 4
if m[pos] > 0 {
ret.SubPath = s[m[pos]:m[pos+1]]
}
pos += 2
if m[pos] > 0 {
ret.QueryParams = s[m[pos]:m[pos+1]]
}
pos += 2
if m[pos] > 0 {
ret.QueryHash = s[m[pos]:m[pos+1]][1:]
}
return ret, true
} }
// fullHashPatternProcessor renders SHA containing URLs func otherRepoPrefix(ctx *RenderContext, owner, repo string) string {
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { if strings.EqualFold(owner, ctx.RenderOptions.Metas["user"]) && strings.EqualFold(repo, ctx.RenderOptions.Metas["repo"]) {
if ctx.RenderOptions.Metas == nil { return ""
}
return owner + "/" + repo + "@"
}
// compare refs can also be branch or tag names, so only full SHAs are shortened
func shortCompareRef(ref string) string {
if git.IsStringValidObjectID(nil, ref) {
return base.ShortSha(ref)
}
return ref
}
// linkified URLs and "<url>" autolinks reach here as links whose text is their own URL
func shortenCommitLink(ctx *RenderContext, anchor *html.Node) {
text := anchor.FirstChild
if ctx.RenderOptions.Metas == nil || text == nil || text != anchor.LastChild || text.Type != html.TextNode || !strings.Contains(text.Data, "://") {
return return
} }
nodeStop := node.NextSibling if !slices.ContainsFunc(anchor.Attr, func(attr html.Attribute) bool { return attr.Key == "href" && attr.Val == text.Data }) {
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue
}
ret, ok := anyHashPatternExtract(node.Data)
if !ok {
node = node.NextSibling
continue
}
text := base.ShortSha(ret.CommitID)
if ret.CommitExt != "" {
text += ret.CommitExt
}
if ret.SubPath != "" {
text += ret.SubPath
}
if ret.QueryHash != "" {
text += " (" + ret.QueryHash + ")"
}
// only turn commit links to the current instance into hash link
if !httplib.IsCurrentGiteaSiteURL(ctx, ret.FullURL) {
node = node.NextSibling
continue
}
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
node = node.NextSibling.NextSibling
}
}
func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil {
return return
} }
nodeStop := node.NextSibling siteURL := httplib.ParseGiteaSiteURL(ctx, text.Data)
for node != nodeStop { if siteURL == nil {
if node.Type != html.TextNode { return
node = node.NextSibling
continue
} }
m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data) var refs, rest string
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match if match := globalVars().compareURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
node = node.NextSibling refs, rest = shortCompareRef(match[1])+match[2]+shortCompareRef(match[3]), match[4]
continue } else if match := globalVars().commitURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
refs, rest = base.ShortSha(match[1]), match[2]
} else {
return
} }
if idx := strings.IndexAny(text.Data, "?#"); idx != -1 {
urlFull := node.Data[m[0]:m[1]] rest += text.Data[idx:]
text1 := base.ShortSha(node.Data[m[2]:m[3]])
textDots := base.ShortSha(node.Data[m[4]:m[5]])
text2 := base.ShortSha(node.Data[m[6]:m[7]])
hash := ""
if m[9] > 0 {
hash = node.Data[m[8]:m[9]][1:]
}
start := m[0]
end := m[1]
// If url ends in '.', it's very likely that it is not part of the
// actual url but used to finish a sentence.
if strings.HasSuffix(urlFull, ".") {
end--
urlFull = urlFull[:len(urlFull)-1]
if hash != "" {
hash = hash[:len(hash)-1]
} else if text2 != "" {
text2 = text2[:len(text2)-1]
}
}
// only turn compare links to the current instance into hash link
if !httplib.IsCurrentGiteaSiteURL(ctx, urlFull) {
node = node.NextSibling
continue
}
text := text1 + textDots + text2
if hash != "" {
text += " (" + hash + ")"
}
replaceContent(node, start, end, createCodeLink(urlFull, text, "compare"))
node = node.NextSibling.NextSibling
} }
// quoting the link keeps its URL instead of the short text
anchor.Attr = slices.DeleteFunc(anchor.Attr, func(attr html.Attribute) bool { return attr.Key == "data-markdown-generated-content" })
setCommitLinkContent(anchor, otherRepoPrefix(ctx, siteURL.OwnerName, siteURL.RepoName), refs, rest)
} }
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that // hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
@@ -220,7 +110,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
} }
link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash) link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash)
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) replaceContent(node, m[2], m[3], createCommitLink(ctx, link, "", hash))
start = 0 start = 0
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
} }
@@ -235,9 +125,8 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
return return
} }
refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha) linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha)
link := createLink(ctx, linkHref, refText, "commit") link := createCommitLink(ctx, linkHref, otherRepoPrefix(ctx, ref.Owner, ref.Name), ref.CommitSha)
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling

View File

@@ -281,11 +281,6 @@ func TestRender_AutoLink(t *testing.T) {
err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer) err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
buffer.Reset()
err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
} }
// render valid issue URLs // render valid issue URLs
@@ -296,7 +291,7 @@ func TestRender_AutoLink(t *testing.T) {
tmp := TestRepoURL + "commit/d8a994ef243349f321568f9e36d5c3f444b99cae" tmp := TestRepoURL + "commit/d8a994ef243349f321568f9e36d5c3f444b99cae"
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>") test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>")
tmp += "#diff-2" tmp += "#diff-2"
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>") test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code>#diff-2</a>")
// render other commit URLs // render other commit URLs
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
@@ -355,65 +350,6 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
} }
} }
func TestRegExp_anySHA1Pattern(t *testing.T) {
testCases := map[string]anyHashPatternResult{
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
SubPath: "/test/unit/event.js",
QueryHash: "L2703",
},
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
SubPath: "/test/unit/event.js",
},
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
},
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
SubPath: "/src",
},
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae",
QueryHash: "diff-2",
},
"non-url": {},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "L1-L2",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
SubPath: "/sub",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "hash",
},
}
for k, v := range testCases {
ret, ok := anyHashPatternExtract(k)
if v.CommitID == "" {
assert.False(t, ok)
} else {
assert.Equal(t, strings.TrimSuffix(k, "."), ret.FullURL)
assert.Equal(t, v.CommitID, ret.CommitID)
assert.Equal(t, v.SubPath, ret.SubPath)
assert.Equal(t, v.QueryHash, ret.QueryHash)
}
}
}
func TestRegExp_shortLinkPattern(t *testing.T) { func TestRegExp_shortLinkPattern(t *testing.T) {
trueTestCases := []string{ trueTestCases := []string{
"[[stuff]]", "[[stuff]]",

View File

@@ -31,17 +31,23 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil { if ctx.RenderOptions.Metas == nil {
return return
} }
start := 0
next := node.NextSibling next := node.NextSibling
for node != nil && node != next { for node != nil && node != next && start < len(node.Data) {
m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data) m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data[start:])
if m == nil { if m == nil {
return return
} }
for idx := range m {
if m[idx] != -1 {
m[idx] += start
}
}
mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data) // leave links to sub-pages like https://domain/org/repo/pulls/27/files or .../pulls/27/commits/{sha} to the other processors
// leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files if m[1] < len(node.Data) && node.Data[m[1]] == '/' {
if mDiffView != nil { start = m[1]
return continue
} }
link := node.Data[m[0]:m[1]] link := node.Data[m[0]:m[1]]
@@ -71,6 +77,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
text = matchOrg + "/" + matchRepo + text text = matchOrg + "/" + matchRepo + text
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue")) replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
} }
start = 0
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
} }
} }

View File

@@ -60,14 +60,14 @@ func TestRender_Commits(t *testing.T) {
test(sha[:7], `<p><a href="`+commitPath[:len(commitPath)-(40-7)]+`" rel="nofollow"><code>65f1bf2</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(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(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>`) test(tree, `<p><a href="`+tree+`" rel="nofollow">`+tree+`</a></p>`)
test(file, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc/example.txt</code></a></p>`) test(file, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt</a></p>`)
test(fileWithExtra, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc/example.txt</code></a>:</p>`) test(fileWithExtra, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt</a>:</p>`)
test(fileWithHash, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc/example.txt (L2)</code></a></p>`) test(fileWithHash, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt#L2</a></p>`)
test(fileWithHasExtra, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc/example.txt (L2)</code></a>:</p>`) test(fileWithHasExtra, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt#L2</a>:</p>`)
test(commitCompare, `<p><a href="`+commitCompare+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code></a></p>`) 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(commitCompareWithHash, `<p><a href="`+commitCompareWithHash+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code>#L2</a></p>`)
test("commit "+sha, `<p>commit <a href="`+commitPath+`" 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("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>")
@@ -79,6 +79,32 @@ func TestRender_Commits(t *testing.T) {
test(sha[:14]+".", `<p>`+expected14+`.</p>`) test(sha[:14]+".", `<p>`+expected14+`.</p>`)
test(sha[:14]+",", `<p>`+expected14+`,</p>`) test(sha[:14]+",", `<p>`+expected14+`,</p>`)
test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`) test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`)
commitShort := repo + "commit/" + sha[:7]
pullCommit := repo + "pulls/1/commits/" + sha
otherCommit := markup.TestAppURL + "user2/repo1/commit/" + sha
branchCompare := repo + "compare/main...feature/x"
githubCommit := "https://github.com/a/b/commit/" + sha
test(pullCommit+" fixes "+repo+"issues/2", `<p><a href="`+pullCommit+`" rel="nofollow"><code>65f1bf27bc</code></a> fixes <a href="`+repo+`issues/2" class="ref-issue" rel="nofollow">#2</a></p>`)
test(commitShort+"#diff-2.", `<p><a href="`+commitShort+`#diff-2" rel="nofollow"><code>65f1bf2</code>#diff-2</a>.</p>`)
test(commitShort+"?w=1", `<p><a href="`+commitShort+`?w=1" rel="nofollow"><code>65f1bf2</code>?w=1</a></p>`)
test(otherCommit, `<p><a href="`+otherCommit+`" rel="nofollow">user2/repo1@<code>65f1bf27bc</code></a></p>`)
test(branchCompare, `<p><a href="`+branchCompare+`" rel="nofollow"><code>main...feature/x</code></a></p>`)
test(githubCommit+" "+commitShort+" "+commit, `<p><a href="`+githubCommit+`" rel="nofollow">`+githubCommit+`</a> <a href="`+commitShort+`" rel="nofollow"><code>65f1bf2</code></a> <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test("<"+commitShort+"> ["+commit+"]("+commit+")", `<p><a href="`+commitShort+`" rel="nofollow"><code>65f1bf2</code></a> <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
test("[[label|"+commitShort+"]]", `<p><a href="`+commitShort+`" rel="nofollow">label</a></p>`)
for input, attrs := range map[string]string{
commitShort + ".patch": ` rel="nofollow"><code>65f1bf2</code>.patch`,
repo + "compare/" + sha + ".." + sha: ` rel="nofollow"><code>65f1bf27bc..65f1bf27bc</code>`,
commitCompare + ".diff?w=1": ` rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code>.diff?w=1`,
repo + "compare/20240101...20240201": ` rel="nofollow"><code>20240101...20240201</code>`,
repo + "src/commit/" + sha + "/README.md": ` rel="nofollow"><code>65f1bf27bc</code>/README.md`,
repo + "commit/" + sha[:6]: ` rel="nofollow">` + repo + "commit/" + sha[:6],
repo + "src/branch/main/sub-dir/commit/" + sha[:7]: ` rel="nofollow">` + repo + "src/branch/main/sub-dir/commit/" + sha[:7],
repo + "src/branch/main/" + sha + ".txt": ` rel="nofollow">` + repo + "src/branch/main/" + sha + ".txt",
} {
test(input, `<p><a href="`+input+`"`+attrs+`</a></p>`)
}
} }
func TestRender_CrossReferences(t *testing.T) { func TestRender_CrossReferences(t *testing.T) {
@@ -113,17 +139,13 @@ func TestRender_CrossReferences(t *testing.T) {
inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
test( test(
inputURL, inputURL,
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`) `<p><a href="`+inputURL+`" rel="nofollow">a/b@<code>0123456789</code>/foo.txt?a=b#L2-L3</a></p>`)
inputURL = setting.AppURL + "repo/owner/archive/0123456789012345678901234567890123456789.tar.gz" inputURL = setting.AppURL + "owner/repo/archive/0123456789012345678901234567890123456789.tar.gz?key=val"
test( test(inputURL, `<p><a href="`+inputURL+`" rel="nofollow">owner/repo@<code>0123456789</code>.tar.gz?key=val</a></p>`)
inputURL,
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.tar.gz</code></a></p>`)
inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val" test("test-owner/test-repo@"+"0123456789012345678901234567890123456789", `<p><a href="/test-owner/test-repo/commit/0123456789012345678901234567890123456789" rel="nofollow">test-owner/test-repo@<code>0123456789</code></a></p>`)
test( test(testRepoOwnerName+"/"+testRepoName+"@"+"0123456789012345678901234567890123456789", `<p><a href="/user13/repo11/commit/0123456789012345678901234567890123456789" rel="nofollow"><code>0123456789</code></a></p>`)
inputURL,
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.patch</code></a></p>`)
} }
func TestRender_links(t *testing.T) { func TestRender_links(t *testing.T) {
@@ -178,6 +200,7 @@ func TestRender_links(t *testing.T) {
{"://", `<p>://</p>`}, {"://", `<p>://</p>`},
{"www", `<p>www</p>`}, {"www", `<p>www</p>`},
{"ftps://gitea.com", `<p>ftps://gitea.com</p>`}, {"ftps://gitea.com", `<p>ftps://gitea.com</p>`},
{markup.TestAppURL + "user13/repo11/commit/65f1bf2", `<p><a href="` + markup.TestAppURL + `user13/repo11/commit/65f1bf2" rel="nofollow">` + markup.TestAppURL + `user13/repo11/commit/65f1bf2</a></p>`},
} { } {
test(testCase.input, testCase.expected) test(testCase.input, testCase.expected)
} }
@@ -459,7 +482,7 @@ func TestIssue18471(t *testing.T) {
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, `<a href="`+markup.TestAppURL+`org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String()) assert.Equal(t, `<a href="`+markup.TestAppURL+`org/repo/compare/783b039...da951ce" class="commit">org/repo@<code>783b039...da951ce</code></a>`, res.String())
} }
func TestRender_TeamMention(t *testing.T) { func TestRender_TeamMention(t *testing.T) {

View File

@@ -555,8 +555,8 @@ space</p>
input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash` http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash`
expected := `<p><a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a> expected := `<p><a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">user/repo@<code>88fc37a3c0</code></a>
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a></p> <a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a></p>
` `
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input) result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
assert.NoError(t, err) assert.NoError(t, err)

View File

@@ -102,9 +102,9 @@ func TestRenderRepoComment(t *testing.T) {
![remote image](<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>) ![remote image](<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>)
[[local image|image.jpg]] [[local image|image.jpg]]
[[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]] [[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]]
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a> <a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="commit">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code>88fc37a3c0</code></a> <a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit">user/repo@<code>88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" data-alias="+1">👍</span> <span class="emoji" data-alias="+1">👍</span>
<a href="mailto:mail@domain.com">mail@domain.com</a> <a href="mailto:mail@domain.com">mail@domain.com</a>
@@ -226,9 +226,9 @@ func TestRenderMarkdownToHtml(t *testing.T) {
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a> <a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a> <a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a> <a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">user/repo@<code>88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" data-alias="+1">👍</span> <span class="emoji" data-alias="+1">👍</span>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a> <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>