mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
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:
@@ -32,12 +32,11 @@ const (
|
||||
type globalVarsType struct {
|
||||
hashCurrentPattern *regexp.Regexp
|
||||
shortLinkPattern *regexp.Regexp
|
||||
anyHashPattern *regexp.Regexp
|
||||
comparePattern *regexp.Regexp
|
||||
compareURLPattern *regexp.Regexp
|
||||
commitURLPattern *regexp.Regexp
|
||||
emailRegex *regexp.Regexp
|
||||
emojiShortCodeRegex *regexp.Regexp
|
||||
issueFullPattern *regexp.Regexp
|
||||
filesChangedFullPattern *regexp.Regexp
|
||||
codePreviewPattern *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
|
||||
v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||
|
||||
// anyHashPattern splits url containing SHA into parts
|
||||
v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})((\.\w+)*)(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
|
||||
|
||||
// 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]+)?`)
|
||||
// repo sub-paths of the compare page and of the routes that take a commit SHA
|
||||
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})((?:[./].*)?)$`)
|
||||
|
||||
// emailRegex is definitely not perfect with edge cases,
|
||||
// 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
|
||||
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"
|
||||
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 {
|
||||
procs := []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
codePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
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 {
|
||||
procs := []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
@@ -147,8 +137,6 @@ var emojiProcessors = []processor{
|
||||
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) template.HTML {
|
||||
procs := []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
@@ -361,6 +349,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
||||
}
|
||||
|
||||
if node.Data == "a" {
|
||||
shortenCommitLink(ctx, node)
|
||||
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.
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/references"
|
||||
|
||||
@@ -16,176 +17,65 @@ import (
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
type anyHashPatternResult struct {
|
||||
PosStart int
|
||||
PosEnd int
|
||||
FullURL string
|
||||
CommitID string
|
||||
CommitExt string
|
||||
SubPath string
|
||||
QueryParams string
|
||||
QueryHash string
|
||||
func setCommitLinkContent(anchor *html.Node, prefix, refs, rest string) {
|
||||
anchor.Attr = append(anchor.Attr, html.Attribute{Key: "class", Val: "commit"}) // plain, so markdown strips it and commit messages keep it for CSS
|
||||
anchor.FirstChild.Data = prefix
|
||||
code := &html.Node{Type: html.ElementNode, Data: atom.Code.String()}
|
||||
code.AppendChild(&html.Node{Type: html.TextNode, Data: refs})
|
||||
anchor.AppendChild(code)
|
||||
if rest != "" {
|
||||
anchor.AppendChild(&html.Node{Type: html.TextNode, Data: rest})
|
||||
}
|
||||
}
|
||||
|
||||
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 createCommitLink(ctx *RenderContext, href, prefix, sha string) *html.Node {
|
||||
anchor := createLink(ctx, href, "", "")
|
||||
setCommitLinkContent(anchor, prefix, base.ShortSha(sha), "")
|
||||
return anchor
|
||||
}
|
||||
|
||||
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
||||
m := globalVars().anyHashPattern.FindStringSubmatchIndex(s)
|
||||
if m == nil {
|
||||
return ret, false
|
||||
func otherRepoPrefix(ctx *RenderContext, owner, repo string) string {
|
||||
if strings.EqualFold(owner, ctx.RenderOptions.Metas["user"]) && strings.EqualFold(repo, ctx.RenderOptions.Metas["repo"]) {
|
||||
return ""
|
||||
}
|
||||
|
||||
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]]
|
||||
pos += 2
|
||||
|
||||
ret.CommitExt = s[m[pos]:m[pos+1]]
|
||||
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
|
||||
return owner + "/" + repo + "@"
|
||||
}
|
||||
|
||||
// fullHashPatternProcessor renders SHA containing URLs
|
||||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
// 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
|
||||
}
|
||||
nodeStop := node.NextSibling
|
||||
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 {
|
||||
if !slices.ContainsFunc(anchor.Attr, func(attr html.Attribute) bool { return attr.Key == "href" && attr.Val == text.Data }) {
|
||||
return
|
||||
}
|
||||
nodeStop := node.NextSibling
|
||||
for node != nodeStop {
|
||||
if node.Type != html.TextNode {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
siteURL := httplib.ParseGiteaSiteURL(ctx, text.Data)
|
||||
if siteURL == nil {
|
||||
return
|
||||
}
|
||||
m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
|
||||
node = node.NextSibling
|
||||
continue
|
||||
var refs, rest string
|
||||
if match := globalVars().compareURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
|
||||
refs, rest = shortCompareRef(match[1])+match[2]+shortCompareRef(match[3]), match[4]
|
||||
} else if match := globalVars().commitURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
|
||||
refs, rest = base.ShortSha(match[1]), match[2]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
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
|
||||
if idx := strings.IndexAny(text.Data, "?#"); idx != -1 {
|
||||
rest += text.Data[idx:]
|
||||
}
|
||||
// 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
|
||||
@@ -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)
|
||||
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
|
||||
replaceContent(node, m[2], m[3], createCommitLink(ctx, link, "", hash))
|
||||
start = 0
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
@@ -235,9 +125,8 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
return
|
||||
}
|
||||
|
||||
refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(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)
|
||||
node = node.NextSibling.NextSibling
|
||||
|
||||
@@ -281,11 +281,6 @@ func TestRender_AutoLink(t *testing.T) {
|
||||
err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
|
||||
assert.NoError(t, err)
|
||||
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
|
||||
@@ -296,7 +291,7 @@ func TestRender_AutoLink(t *testing.T) {
|
||||
tmp := TestRepoURL + "commit/d8a994ef243349f321568f9e36d5c3f444b99cae"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>")
|
||||
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
|
||||
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) {
|
||||
trueTestCases := []string{
|
||||
"[[stuff]]",
|
||||
|
||||
@@ -31,17 +31,23 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
start := 0
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data)
|
||||
for node != nil && node != next && start < len(node.Data) {
|
||||
m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data[start:])
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
for idx := range m {
|
||||
if m[idx] != -1 {
|
||||
m[idx] += start
|
||||
}
|
||||
}
|
||||
|
||||
mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data)
|
||||
// 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 mDiffView != nil {
|
||||
return
|
||||
// leave links to sub-pages like https://domain/org/repo/pulls/27/files or .../pulls/27/commits/{sha} to the other processors
|
||||
if m[1] < len(node.Data) && node.Data[m[1]] == '/' {
|
||||
start = m[1]
|
||||
continue
|
||||
}
|
||||
|
||||
link := node.Data[m[0]:m[1]]
|
||||
@@ -71,6 +77,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
text = matchOrg + "/" + matchRepo + text
|
||||
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
|
||||
}
|
||||
start = 0
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[: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>`)
|
||||
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(fileWithExtra, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc/example.txt</code></a>:</p>`)
|
||||
test(fileWithHash, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc/example.txt (L2)</code></a></p>`)
|
||||
test(fileWithHasExtra, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc/example.txt (L2)</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</code>/example.txt</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</code>/example.txt#L2</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("/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>`)
|
||||
|
||||
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) {
|
||||
@@ -113,17 +139,13 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||
inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
|
||||
test(
|
||||
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"
|
||||
test(
|
||||
inputURL,
|
||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.tar.gz</code></a></p>`)
|
||||
inputURL = setting.AppURL + "owner/repo/archive/0123456789012345678901234567890123456789.tar.gz?key=val"
|
||||
test(inputURL, `<p><a href="`+inputURL+`" rel="nofollow">owner/repo@<code>0123456789</code>.tar.gz?key=val</a></p>`)
|
||||
|
||||
inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val"
|
||||
test(
|
||||
inputURL,
|
||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.patch</code></a></p>`)
|
||||
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(testRepoOwnerName+"/"+testRepoName+"@"+"0123456789012345678901234567890123456789", `<p><a href="/user13/repo11/commit/0123456789012345678901234567890123456789" rel="nofollow"><code>0123456789</code></a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_links(t *testing.T) {
|
||||
@@ -178,6 +200,7 @@ func TestRender_links(t *testing.T) {
|
||||
{"://", `<p>://</p>`},
|
||||
{"www", `<p>www</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)
|
||||
}
|
||||
@@ -459,7 +482,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="`+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) {
|
||||
|
||||
@@ -555,8 +555,8 @@ space</p>
|
||||
input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
|
||||
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>
|
||||
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a></p>
|
||||
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">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a></p>
|
||||
`
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -102,9 +102,9 @@ func TestRenderRepoComment(t *testing.T) {
|
||||

|
||||
[[local image|image.jpg]]
|
||||
[[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
|
||||
<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
|
||||
<span class="emoji" data-alias="+1">👍</span>
|
||||
<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="/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="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
|
||||
<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
|
||||
<span class="emoji" data-alias="+1">👍</span>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
|
||||
|
||||
Reference in New Issue
Block a user