return u.String()
}
+// RenderIssueIndexPatternOptions options for RenderIssueIndexPattern function
+type RenderIssueIndexPatternOptions struct {
+ // url to which non-special formatting should be linked. If empty,
+ // no such links will be added
+ DefaultURL string
+ URLPrefix string
+ Metas map[string]string
+}
+
+// addText add text to the given buffer, adding a link to the default url
+// if appropriate
+func (opts RenderIssueIndexPatternOptions) addText(text []byte, buf *bytes.Buffer) {
+ if len(text) == 0 {
+ return
+ } else if len(opts.DefaultURL) == 0 {
+ buf.Write(text)
+ return
+ }
+ buf.WriteString(`<a rel="nofollow" href="`)
+ buf.WriteString(opts.DefaultURL)
+ buf.WriteString(`">`)
+ buf.Write(text)
+ buf.WriteString(`</a>`)
+}
+
// RenderIssueIndexPattern renders issue indexes to corresponding links.
-func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
- urlPrefix = cutoutVerbosePrefix(urlPrefix)
+func RenderIssueIndexPattern(rawBytes []byte, opts RenderIssueIndexPatternOptions) []byte {
+ opts.URLPrefix = cutoutVerbosePrefix(opts.URLPrefix)
pattern := IssueNumericPattern
- if metas["style"] == IssueNameStyleAlphanumeric {
+ if opts.Metas["style"] == IssueNameStyleAlphanumeric {
pattern = IssueAlphanumericPattern
}
- ms := pattern.FindAll(rawBytes, -1)
- for _, m := range ms {
- if m[0] == ' ' || m[0] == '(' {
- m = m[1:] // ignore leading space or opening parentheses
+ var buf bytes.Buffer
+ remainder := rawBytes
+ for {
+ indices := pattern.FindIndex(remainder)
+ if indices == nil || len(indices) < 2 {
+ opts.addText(remainder, &buf)
+ return buf.Bytes()
}
- var link string
- if metas == nil {
- link = fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(urlPrefix, "issues", string(m[1:])), m)
+ startIndex := indices[0]
+ endIndex := indices[1]
+ opts.addText(remainder[:startIndex], &buf)
+ if remainder[startIndex] == '(' || remainder[startIndex] == ' ' {
+ buf.WriteByte(remainder[startIndex])
+ startIndex++
+ }
+ if opts.Metas == nil {
+ buf.WriteString(`<a href="`)
+ buf.WriteString(URLJoin(
+ opts.URLPrefix, "issues", string(remainder[startIndex+1:endIndex])))
+ buf.WriteString(`">`)
+ buf.Write(remainder[startIndex:endIndex])
+ buf.WriteString(`</a>`)
} else {
// Support for external issue tracker
- if metas["style"] == IssueNameStyleAlphanumeric {
- metas["index"] = string(m)
+ buf.WriteString(`<a href="`)
+ if opts.Metas["style"] == IssueNameStyleAlphanumeric {
+ opts.Metas["index"] = string(remainder[startIndex:endIndex])
} else {
- metas["index"] = string(m[1:])
+ opts.Metas["index"] = string(remainder[startIndex+1 : endIndex])
}
- link = fmt.Sprintf(`<a href="%s">%s</a>`, com.Expand(metas["format"], metas), m)
+ buf.WriteString(com.Expand(opts.Metas["format"], opts.Metas))
+ buf.WriteString(`">`)
+ buf.Write(remainder[startIndex:endIndex])
+ buf.WriteString(`</a>`)
}
- rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1)
+ if endIndex < len(remainder) &&
+ (remainder[endIndex] == ')' || remainder[endIndex] == ' ') {
+ buf.WriteByte(remainder[endIndex])
+ endIndex++
+ }
+ remainder = remainder[endIndex:]
}
- return rawBytes
}
// IsSameDomain checks if given url string has the same hostname as current Gitea instance
rawBytes = RenderFullIssuePattern(rawBytes)
rawBytes = RenderShortLinks(rawBytes, urlPrefix, false, isWikiMarkdown)
- rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas)
+ rawBytes = RenderIssueIndexPattern(rawBytes, RenderIssueIndexPatternOptions{
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ })
rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas)
rawBytes = renderFullSha1Pattern(rawBytes, urlPrefix)
rawBytes = renderSha1CurrentPattern(rawBytes, urlPrefix)
return fmt.Sprintf("<a href=\"%s\">%s</a>", href, contents)
}
-func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) {
- assert.Equal(t, expected,
- string(RenderIssueIndexPattern([]byte(input), AppSubURL, metas)))
+func testRenderIssueIndexPattern(t *testing.T, input, expected string, opts RenderIssueIndexPatternOptions) {
+ if len(opts.URLPrefix) == 0 {
+ opts.URLPrefix = AppSubURL
+ }
+ actual := string(RenderIssueIndexPattern([]byte(input), opts))
+ assert.Equal(t, expected, actual)
}
func TestURLJoin(t *testing.T) {
func TestRender_IssueIndexPattern(t *testing.T) {
// numeric: render inputs without valid mentions
test := func(s string) {
- testRenderIssueIndexPattern(t, s, s, nil)
- testRenderIssueIndexPattern(t, s, s, numericMetas)
+ testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{})
+ testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{Metas: numericMetas})
}
// should not render anything when there are no mentions
links[i] = numericIssueLink(URLJoin(setting.AppSubURL, "issues"), index)
}
expectedNil := fmt.Sprintf(expectedFmt, links...)
- testRenderIssueIndexPattern(t, s, expectedNil, nil)
+ testRenderIssueIndexPattern(t, s, expectedNil, RenderIssueIndexPatternOptions{})
for i, index := range indices {
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index)
}
expectedNum := fmt.Sprintf(expectedFmt, links...)
- testRenderIssueIndexPattern(t, s, expectedNum, numericMetas)
+ testRenderIssueIndexPattern(t, s, expectedNum, RenderIssueIndexPatternOptions{Metas: numericMetas})
}
// should render freestanding mentions
// alphanumeric: render inputs without valid mentions
test := func(s string) {
- testRenderIssueIndexPattern(t, s, s, alphanumericMetas)
+ testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{Metas: alphanumericMetas})
}
test("")
test("this is a test")
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", name)
}
expected := fmt.Sprintf(expectedFmt, links...)
- testRenderIssueIndexPattern(t, s, expected, alphanumericMetas)
+ testRenderIssueIndexPattern(t, s, expected, RenderIssueIndexPatternOptions{Metas: alphanumericMetas})
}
test("OTT-1234 test", "%s test", "OTT-1234")
test("test T-12 issue", "test %s issue", "T-12")
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
}
+func TestRenderIssueIndexPatternWithDefaultURL(t *testing.T) {
+ setting.AppURL = AppURL
+ setting.AppSubURL = AppSubURL
+
+ test := func(input string, expected string) {
+ testRenderIssueIndexPattern(t, input, expected, RenderIssueIndexPatternOptions{
+ DefaultURL: AppURL,
+ })
+ }
+ test("hello #123 world",
+ fmt.Sprintf(`<a rel="nofollow" href="%s">hello</a> `, AppURL)+
+ fmt.Sprintf(`<a href="%sissues/123">#123</a> `, AppSubURL)+
+ fmt.Sprintf(`<a rel="nofollow" href="%s">world</a>`, AppURL))
+ test("hello (#123) world",
+ fmt.Sprintf(`<a rel="nofollow" href="%s">hello </a>`, AppURL)+
+ fmt.Sprintf(`(<a href="%sissues/123">#123</a>)`, AppSubURL)+
+ fmt.Sprintf(`<a rel="nofollow" href="%s"> world</a>`, AppURL))
+}
+
func TestRender_AutoLink(t *testing.T) {
setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
"EscapePound": func(str string) string {
return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str)
},
- "RenderCommitMessage": RenderCommitMessage,
+ "RenderCommitMessage": RenderCommitMessage,
+ "RenderCommitMessageLink": RenderCommitMessageLink,
"ThemeColorMetaTag": func() string {
return setting.UI.ThemeColorMetaTag
},
}
// RenderCommitMessage renders commit message with XSS-safe and special links.
-func RenderCommitMessage(full bool, msg, urlPrefix string, metas map[string]string) template.HTML {
+func RenderCommitMessage(msg, urlPrefix string, metas map[string]string) template.HTML {
+ return renderCommitMessage(msg, markup.RenderIssueIndexPatternOptions{
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ })
+}
+
+// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
+// default url, handling for special links.
+func RenderCommitMessageLink(msg, urlPrefix string, urlDefault string, metas map[string]string) template.HTML {
+ return renderCommitMessage(msg, markup.RenderIssueIndexPatternOptions{
+ DefaultURL: urlDefault,
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ })
+}
+
+func renderCommitMessage(msg string, opts markup.RenderIssueIndexPatternOptions) template.HTML {
cleanMsg := template.HTMLEscapeString(msg)
- fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), urlPrefix, metas))
+ fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), opts))
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
- numLines := len(msgLines)
- if numLines == 0 {
+ if len(msgLines) == 0 {
return template.HTML("")
- } else if !full {
- return template.HTML(msgLines[0])
- } else if numLines == 1 || (numLines >= 2 && len(msgLines[1]) == 0) {
- // First line is a header, standalone or followed by empty line
- header := fmt.Sprintf("<h3>%s</h3>", msgLines[0])
- if numLines >= 2 {
- fullMessage = header + fmt.Sprintf("\n<pre>%s</pre>", strings.Join(msgLines[2:], "\n"))
- } else {
- fullMessage = header
- }
- } else {
- // Non-standard git message, there is no header line
- fullMessage = fmt.Sprintf("<h4>%s</h4>", strings.Join(msgLines, "<br>"))
}
- return template.HTML(fullMessage)
+ return template.HTML(msgLines[0])
}
// Actioner describes an action
</a>
</td>
<td class="message collapsing">
- <span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
+ <span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
{{template "repo/commit_status" .Status}}
</td>
<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
{{.i18n.Tr "repo.diff.browse_source"}}
</a>
- <h3>{{RenderCommitMessage false .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
+ <h3>{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
</div>
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
{{if .Author}}
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Rev}}">{{ .ShortRev}}</a>
</code>
<strong> {{.Branch}}</strong>
- <em>{{RenderCommitMessage false .Subject $.RepoLink $.Repository.ComposeMetas}}</em> by
+ <em>{{RenderCommitMessage .Subject $.RepoLink $.Repository.ComposeMetas}}</em> by
<span class="author">
{{.Author}}
</span>
</div>
{{end}}
</a>
- <span class="grey has-emoji">{{RenderCommitMessage false .LatestCommit.Summary .RepoLink $.Repository.ComposeMetas}}
+ <span class="grey has-emoji">{{RenderCommitMessage .LatestCommit.Summary .RepoLink $.Repository.ComposeMetas}}
{{template "repo/commit_status" .LatestCommitStatus}}</span>
</th>
<th class="nine wide">
</td>
{{end}}
<td class="message collapsing has-emoji">
- <a rel="nofollow" href="{{$.RepoLink}}/commit/{{$commit.ID}}">
- {{RenderCommitMessage false $commit.Summary $.RepoLink $.Repository.ComposeMetas}}
- </a>
+ {{RenderCommitMessageLink $commit.Summary $.RepoLink (print $.RepoLink "/commit/" $commit.ID) $.Repository.ComposeMetas}}
</td>
<td class="text grey right age">{{TimeSince $commit.Committer.When $.Lang}}</td>
</tr>