1
0
Fork 0
forked from forgejo/forgejo

Rewrite reference processing code in preparation for opening/closing from comment references (#8261)

* Add a markdown stripper for mentions and xrefs

* Improve comments

* Small code simplification

* Move reference code to modules/references

* Fix typo

* Make MarkdownStripper return [][]byte

* Implement preliminary keywords parsing

* Add FIXME comment

* Fix comment

* make fmt

* Fix permissions check

* Fix text assumptions

* Fix imports

* Fix lint, fmt

* Fix unused import

* Add missing export comment

* Bypass revive on implemented interface

* Move mdstripper into its own package

* Support alphanumeric patterns

* Refactor FindAllMentions

* Move mentions test to references

* Parse mentions from reference package

* Refactor code to implement renderizable references

* Fix typo

* Move patterns and tests to the references package

* Fix nil reference

* Preliminary rendering attempt of closing keywords

* Normalize names, comments, general tidy-up

* Add CSS style for action keywords

* Fix permission for admin and owner

* Fix golangci-lint

* Fix golangci-lint
This commit is contained in:
guillep2k 2019-10-13 19:29:10 -03:00 committed by zeripath
parent 6e3f51098b
commit 15809d81f7
16 changed files with 1123 additions and 438 deletions

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -36,17 +37,6 @@ var (
// While fast, this is also incorrect and lead to false positives.
// TODO: fix invalid linking issue
// mentionPattern matches all mentions in the form of "@user"
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_\.]+)(?:\s|$|\)|\])`)
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`)
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. gogits/gogs#12345
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+#[0-9]+)(?:\s|$|\)|\]|\.(\s|$))`)
// sha1CurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
// Although SHA1 hashes are 40 chars long, the regex matches the hash from 7 to 40 chars in length
// so that abbreviated hash links can be used as well. This matches git and github useability.
@ -70,6 +60,9 @@ var (
linkRegex, _ = xurls.StrictMatchingScheme("https?://")
)
// CSS class for action keywords (e.g. "closes: #1")
const keywordClass = "issue-keyword"
// regexp for full links to issues/pulls
var issueFullPattern *regexp.Regexp
@ -99,17 +92,6 @@ func getIssueFullPattern() *regexp.Regexp {
return issueFullPattern
}
// FindAllMentions matches mention patterns in given content
// and returns a list of found user names without @ prefix.
func FindAllMentions(content string) []string {
mentions := mentionPattern.FindAllStringSubmatch(content, -1)
ret := make([]string, len(mentions))
for i, val := range mentions {
ret[i] = val[1][1:]
}
return ret
}
// IsSameDomain checks if given url string has the same hostname as current Gitea instance
func IsSameDomain(s string) bool {
if strings.HasPrefix(s, "/") {
@ -142,7 +124,6 @@ var defaultProcessors = []processor{
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
crossReferenceIssueIndexPatternProcessor,
sha1CurrentPatternProcessor,
emailAddressProcessor,
}
@ -183,7 +164,6 @@ var commitMessageProcessors = []processor{
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
crossReferenceIssueIndexPatternProcessor,
sha1CurrentPatternProcessor,
emailAddressProcessor,
}
@ -217,7 +197,6 @@ var commitMessageSubjectProcessors = []processor{
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
crossReferenceIssueIndexPatternProcessor,
sha1CurrentPatternProcessor,
}
@ -330,6 +309,24 @@ func (ctx *postProcessCtx) textNode(node *html.Node) {
}
}
// createKeyword() renders a highlighted version of an action keyword
func createKeyword(content string) *html.Node {
span := &html.Node{
Type: html.ElementNode,
Data: atom.Span.String(),
Attr: []html.Attribute{},
}
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass})
text := &html.Node{
Type: html.TextNode,
Data: content,
}
span.AppendChild(text)
return span
}
func createLink(href, content, class string) *html.Node {
a := &html.Node{
Type: html.ElementNode,
@ -377,10 +374,16 @@ func createCodeLink(href, content, class string) *html.Node {
return a
}
// replaceContent takes a text node, and in its content it replaces a section of
// it with the specified newNode. An example to visualize how this can work can
// be found here: https://play.golang.org/p/5zP8NnHZ03s
// replaceContent takes text node, and in its content it replaces a section of
// it with the specified newNode.
func replaceContent(node *html.Node, i, j int, newNode *html.Node) {
replaceContentList(node, i, j, []*html.Node{newNode})
}
// replaceContentList takes text node, and in its content it replaces a section of
// it with the specified newNodes. An example to visualize how this can work can
// be found here: https://play.golang.org/p/5zP8NnHZ03s
func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
// get the data before and after the match
before := node.Data[:i]
after := node.Data[j:]
@ -392,7 +395,9 @@ func replaceContent(node *html.Node, i, j int, newNode *html.Node) {
// Get the current next sibling, before which we place the replaced data,
// and after that we place the new text node.
nextSibling := node.NextSibling
node.Parent.InsertBefore(newNode, nextSibling)
for _, n := range newNodes {
node.Parent.InsertBefore(n, nextSibling)
}
if after != "" {
node.Parent.InsertBefore(&html.Node{
Type: html.TextNode,
@ -402,13 +407,13 @@ func replaceContent(node *html.Node, i, j int, newNode *html.Node) {
}
func mentionProcessor(_ *postProcessCtx, node *html.Node) {
m := mentionPattern.FindStringSubmatchIndex(node.Data)
if m == nil {
// We replace only the first mention; other mentions will be addressed later
found, loc := references.FindFirstMentionBytes([]byte(node.Data))
if !found {
return
}
// Replace the mention with a link to the specified user.
mention := node.Data[m[2]:m[3]]
replaceContent(node, m[2], m[3], createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
mention := node.Data[loc.Start:loc.End]
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
}
func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) {
@ -597,45 +602,44 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
if ctx.metas == nil {
return
}
// default to numeric pattern, unless alphanumeric is requested.
pattern := issueNumericPattern
var (
found bool
ref *references.RenderizableReference
)
if ctx.metas["style"] == IssueNameStyleAlphanumeric {
pattern = issueAlphanumericPattern
}
match := pattern.FindStringSubmatchIndex(node.Data)
if match == nil {
return
}
id := node.Data[match[2]:match[3]]
var link *html.Node
if _, ok := ctx.metas["format"]; ok {
// Support for external issue tracker
if ctx.metas["style"] == IssueNameStyleAlphanumeric {
ctx.metas["index"] = id
} else {
ctx.metas["index"] = id[1:]
}
link = createLink(com.Expand(ctx.metas["format"], ctx.metas), id, "issue")
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
} else {
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", id[1:]), id, "issue")
found, ref = references.FindRenderizableReferenceNumeric(node.Data)
}
replaceContent(node, match[2], match[3], link)
}
func crossReferenceIssueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
m := crossReferenceIssueNumericPattern.FindStringSubmatchIndex(node.Data)
if m == nil {
if !found {
return
}
ref := node.Data[m[2]:m[3]]
parts := strings.SplitN(ref, "#", 2)
repo, issue := parts[0], parts[1]
var link *html.Node
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
if _, ok := ctx.metas["format"]; ok {
ctx.metas["index"] = ref.Issue
link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "issue")
} else if ref.Owner == "" {
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", ref.Issue), reftext, "issue")
} else {
link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, "issues", ref.Issue), reftext, "issue")
}
replaceContent(node, m[2], m[3],
createLink(util.URLJoin(setting.AppURL, repo, "issues", issue), ref, issue))
if ref.Action == references.XRefActionNone {
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
return
}
// Decorate action keywords
keyword := createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
spaces := &html.Node{
Type: html.TextNode,
Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
}
replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
}
// fullSha1PatternProcessor renders SHA containing URLs