* restructure markup & markdown to prepare for multiple markup languages support * adjust some functions between markdown and markup * fix tests * improve the commentsmaster
@@ -16,7 +16,7 @@ import ( | |||||
api "code.gitea.io/sdk/gitea" | api "code.gitea.io/sdk/gitea" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
) | ) | ||||
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. | // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. | ||||
@@ -272,7 +272,7 @@ func (c *Comment) LoadAssignees() error { | |||||
// MailParticipants sends new comment emails to repository watchers | // MailParticipants sends new comment emails to repository watchers | ||||
// and mentioned people. | // and mentioned people. | ||||
func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) { | func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) { | ||||
mentions := markdown.FindAllMentions(c.Content) | |||||
mentions := markup.FindAllMentions(c.Content) | |||||
if err = UpdateIssueMentions(e, c.IssueID, mentions); err != nil { | if err = UpdateIssueMentions(e, c.IssueID, mentions); err != nil { | ||||
return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err) | return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err) | ||||
} | } | ||||
@@ -10,7 +10,7 @@ import ( | |||||
"github.com/Unknwon/com" | "github.com/Unknwon/com" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
) | ) | ||||
@@ -104,7 +104,7 @@ func (issue *Issue) MailParticipants() (err error) { | |||||
} | } | ||||
func (issue *Issue) mailParticipants(e Engine) (err error) { | func (issue *Issue) mailParticipants(e Engine) (err error) { | ||||
mentions := markdown.FindAllMentions(issue.Content) | |||||
mentions := markup.FindAllMentions(issue.Content) | |||||
if err = UpdateIssueMentions(e, issue.ID, mentions); err != nil { | if err = UpdateIssueMentions(e, issue.ID, mentions); err != nil { | ||||
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) | return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) | ||||
} | } | ||||
@@ -14,6 +14,7 @@ import ( | |||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/mailer" | "code.gitea.io/gitea/modules/mailer" | ||||
"code.gitea.io/gitea/modules/markdown" | "code.gitea.io/gitea/modules/markdown" | ||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"gopkg.in/gomail.v2" | "gopkg.in/gomail.v2" | ||||
"gopkg.in/macaron.v1" | "gopkg.in/macaron.v1" | ||||
@@ -150,7 +151,7 @@ func composeTplData(subject, body, link string) map[string]interface{} { | |||||
func composeIssueCommentMessage(issue *Issue, doer *User, comment *Comment, tplName base.TplName, tos []string, info string) *mailer.Message { | func composeIssueCommentMessage(issue *Issue, doer *User, comment *Comment, tplName base.TplName, tos []string, info string) *mailer.Message { | ||||
subject := issue.mailSubject() | subject := issue.mailSubject() | ||||
body := string(markdown.RenderString(issue.Content, issue.Repo.HTMLURL(), issue.Repo.ComposeMetas())) | |||||
body := string(markup.RenderByType(markdown.MarkupName, []byte(issue.Content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas())) | |||||
data := make(map[string]interface{}, 10) | data := make(map[string]interface{}, 10) | ||||
if comment != nil { | if comment != nil { | ||||
@@ -8,7 +8,7 @@ import ( | |||||
"fmt" | "fmt" | ||||
"time" | "time" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"github.com/go-xorm/xorm" | "github.com/go-xorm/xorm" | ||||
) | ) | ||||
@@ -101,7 +101,7 @@ func addUnitsToTables(x *xorm.Engine) error { | |||||
config["ExternalTrackerURL"] = repo.ExternalTrackerURL | config["ExternalTrackerURL"] = repo.ExternalTrackerURL | ||||
config["ExternalTrackerFormat"] = repo.ExternalTrackerFormat | config["ExternalTrackerFormat"] = repo.ExternalTrackerFormat | ||||
if len(repo.ExternalTrackerStyle) == 0 { | if len(repo.ExternalTrackerStyle) == 0 { | ||||
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||||
repo.ExternalTrackerStyle = markup.IssueNameStyleNumeric | |||||
} | } | ||||
config["ExternalTrackerStyle"] = repo.ExternalTrackerStyle | config["ExternalTrackerStyle"] = repo.ExternalTrackerStyle | ||||
case V16UnitTypeExternalWiki: | case V16UnitTypeExternalWiki: | ||||
@@ -22,7 +22,7 @@ import ( | |||||
"code.gitea.io/git" | "code.gitea.io/git" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/options" | "code.gitea.io/gitea/modules/options" | ||||
"code.gitea.io/gitea/modules/process" | "code.gitea.io/gitea/modules/process" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
@@ -480,10 +480,10 @@ func (repo *Repository) ComposeMetas() map[string]string { | |||||
"repo": repo.Name, | "repo": repo.Name, | ||||
} | } | ||||
switch unit.ExternalTrackerConfig().ExternalTrackerStyle { | switch unit.ExternalTrackerConfig().ExternalTrackerStyle { | ||||
case markdown.IssueNameStyleAlphanumeric: | |||||
repo.ExternalMetas["style"] = markdown.IssueNameStyleAlphanumeric | |||||
case markup.IssueNameStyleAlphanumeric: | |||||
repo.ExternalMetas["style"] = markup.IssueNameStyleAlphanumeric | |||||
default: | default: | ||||
repo.ExternalMetas["style"] = markdown.IssueNameStyleNumeric | |||||
repo.ExternalMetas["style"] = markup.IssueNameStyleNumeric | |||||
} | } | ||||
} | } | ||||
@@ -708,7 +708,7 @@ func (repo *Repository) DescriptionHTML() template.HTML { | |||||
sanitize := func(s string) string { | sanitize := func(s string) string { | ||||
return fmt.Sprintf(`<a href="%[1]s" target="_blank" rel="noopener">%[1]s</a>`, s) | return fmt.Sprintf(`<a href="%[1]s" target="_blank" rel="noopener">%[1]s</a>`, s) | ||||
} | } | ||||
return template.HTML(descPattern.ReplaceAllStringFunc(markdown.Sanitize(repo.Description), sanitize)) | |||||
return template.HTML(descPattern.ReplaceAllStringFunc(markup.Sanitize(repo.Description), sanitize)) | |||||
} | } | ||||
// LocalCopyPath returns the local repository copy path | // LocalCopyPath returns the local repository copy path | ||||
@@ -8,7 +8,7 @@ import ( | |||||
"path" | "path" | ||||
"testing" | "testing" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"github.com/Unknwon/com" | "github.com/Unknwon/com" | ||||
@@ -39,13 +39,13 @@ func TestRepo(t *testing.T) { | |||||
assert.Equal(t, "https://someurl.com/{user}/{repo}/{issue}", metas["format"]) | assert.Equal(t, "https://someurl.com/{user}/{repo}/{issue}", metas["format"]) | ||||
} | } | ||||
testSuccess(markdown.IssueNameStyleNumeric) | |||||
testSuccess(markup.IssueNameStyleNumeric) | |||||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markdown.IssueNameStyleAlphanumeric | |||||
testSuccess(markdown.IssueNameStyleAlphanumeric) | |||||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleAlphanumeric | |||||
testSuccess(markup.IssueNameStyleAlphanumeric) | |||||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||||
testSuccess(markdown.IssueNameStyleNumeric) | |||||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric | |||||
testSuccess(markup.IssueNameStyleNumeric) | |||||
} | } | ||||
func TestGetRepositoryCount(t *testing.T) { | func TestGetRepositoryCount(t *testing.T) { | ||||
@@ -6,107 +6,14 @@ package markdown | |||||
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"fmt" | |||||
"io" | |||||
"net/url" | |||||
"path" | |||||
"path/filepath" | |||||
"regexp" | |||||
"strings" | "strings" | ||||
"code.gitea.io/gitea/modules/base" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/markup" | "code.gitea.io/gitea/modules/markup" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"github.com/Unknwon/com" | |||||
"github.com/russross/blackfriday" | "github.com/russross/blackfriday" | ||||
"golang.org/x/net/html" | |||||
) | ) | ||||
// Issue name styles | |||||
const ( | |||||
IssueNameStyleNumeric = "numeric" | |||||
IssueNameStyleAlphanumeric = "alphanumeric" | |||||
) | |||||
// IsMarkdownFile reports whether name looks like a Markdown file | |||||
// based on its extension. | |||||
func IsMarkdownFile(name string) bool { | |||||
extension := strings.ToLower(filepath.Ext(name)) | |||||
for _, ext := range setting.Markdown.FileExtensions { | |||||
if strings.ToLower(ext) == extension { | |||||
return true | |||||
} | |||||
} | |||||
return false | |||||
} | |||||
var ( | |||||
// NOTE: All below regex matching do not perform any extra validation. | |||||
// Thus a link is produced even if the user does not exist, the issue does not exist, the commit does not exist, etc. | |||||
// While fast, this is also incorrect and lead to false positives. | |||||
// MentionPattern matches string that mentions someone, e.g. @Unknwon | |||||
MentionPattern = regexp.MustCompile(`(\s|^|\W)@[0-9a-zA-Z-_\.]+`) | |||||
// IssueNumericPattern matches string that references to a numeric issue, e.g. #1287 | |||||
IssueNumericPattern = regexp.MustCompile(`( |^|\()#[0-9]+\b`) | |||||
// IssueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 | |||||
IssueAlphanumericPattern = regexp.MustCompile(`( |^|\()[A-Z]{1,10}-[1-9][0-9]*\b`) | |||||
// CrossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository | |||||
// e.g. gogits/gogs#12345 | |||||
CrossReferenceIssueNumericPattern = regexp.MustCompile(`( |^)[0-9a-zA-Z]+/[0-9a-zA-Z]+#[0-9]+\b`) | |||||
// 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. | |||||
Sha1CurrentPattern = regexp.MustCompile(`(?:^|\s|\()([0-9a-f]{7,40})\b`) | |||||
// ShortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax | |||||
ShortLinkPattern = regexp.MustCompile(`(\[\[.*?\]\]\w*)`) | |||||
// AnySHA1Pattern allows to split url containing SHA into parts | |||||
AnySHA1Pattern = regexp.MustCompile(`(http\S*)://(\S+)/(\S+)/(\S+)/(\S+)/([0-9a-f]{40})(?:/?([^#\s]+)?(?:#(\S+))?)?`) | |||||
validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) | |||||
) | |||||
// regexp for full links to issues/pulls | |||||
var issueFullPattern *regexp.Regexp | |||||
// InitMarkdown initialize regexps for markdown parsing | |||||
func InitMarkdown() { | |||||
getIssueFullPattern() | |||||
} | |||||
func getIssueFullPattern() *regexp.Regexp { | |||||
if issueFullPattern == nil { | |||||
appURL := setting.AppURL | |||||
if len(appURL) > 0 && appURL[len(appURL)-1] != '/' { | |||||
appURL += "/" | |||||
} | |||||
issueFullPattern = regexp.MustCompile(appURL + | |||||
`\w+/\w+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#]\S+.(\S+)?)?\b`) | |||||
} | |||||
return issueFullPattern | |||||
} | |||||
// isLink reports whether link fits valid format. | |||||
func isLink(link []byte) bool { | |||||
return validLinksPattern.Match(link) | |||||
} | |||||
// FindAllMentions matches mention patterns in given content | |||||
// and returns a list of found user names without @ prefix. | |||||
func FindAllMentions(content string) []string { | |||||
mentions := MentionPattern.FindAllString(content, -1) | |||||
for i := range mentions { | |||||
mentions[i] = mentions[i][strings.Index(mentions[i], "@")+1:] // Strip @ character | |||||
} | |||||
return mentions | |||||
} | |||||
// Renderer is a extended version of underlying render object. | // Renderer is a extended version of underlying render object. | ||||
type Renderer struct { | type Renderer struct { | ||||
blackfriday.Renderer | blackfriday.Renderer | ||||
@@ -116,13 +23,13 @@ type Renderer struct { | |||||
// Link defines how formal links should be processed to produce corresponding HTML elements. | // Link defines how formal links should be processed to produce corresponding HTML elements. | ||||
func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { | func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { | ||||
if len(link) > 0 && !isLink(link) { | |||||
if len(link) > 0 && !markup.IsLink(link) { | |||||
if link[0] != '#' { | if link[0] != '#' { | ||||
lnk := string(link) | lnk := string(link) | ||||
if r.isWikiMarkdown { | if r.isWikiMarkdown { | ||||
lnk = URLJoin("wiki", lnk) | |||||
lnk = markup.URLJoin("wiki", lnk) | |||||
} | } | ||||
mLink := URLJoin(r.urlPrefix, lnk) | |||||
mLink := markup.URLJoin(r.urlPrefix, lnk) | |||||
link = []byte(mLink) | link = []byte(mLink) | ||||
} | } | ||||
} | } | ||||
@@ -190,11 +97,11 @@ var ( | |||||
func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { | func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { | ||||
prefix := r.urlPrefix | prefix := r.urlPrefix | ||||
if r.isWikiMarkdown { | if r.isWikiMarkdown { | ||||
prefix = URLJoin(prefix, "wiki", "src") | |||||
prefix = markup.URLJoin(prefix, "wiki", "src") | |||||
} | } | ||||
prefix = strings.Replace(prefix, "/src/", "/raw/", 1) | prefix = strings.Replace(prefix, "/src/", "/raw/", 1) | ||||
if len(link) > 0 { | if len(link) > 0 { | ||||
if isLink(link) { | |||||
if markup.IsLink(link) { | |||||
// External link with .svg suffix usually means CI status. | // External link with .svg suffix usually means CI status. | ||||
// TODO: define a keyword to allow non-svg images render as external link. | // TODO: define a keyword to allow non-svg images render as external link. | ||||
if bytes.HasSuffix(link, svgSuffix) || bytes.Contains(link, svgSuffixWithMark) { | if bytes.HasSuffix(link, svgSuffix) || bytes.Contains(link, svgSuffixWithMark) { | ||||
@@ -203,7 +110,7 @@ func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byt | |||||
} | } | ||||
} else { | } else { | ||||
lnk := string(link) | lnk := string(link) | ||||
lnk = URLJoin(prefix, lnk) | |||||
lnk = markup.URLJoin(prefix, lnk) | |||||
lnk = strings.Replace(lnk, " ", "+", -1) | lnk = strings.Replace(lnk, " ", "+", -1) | ||||
link = []byte(lnk) | link = []byte(lnk) | ||||
} | } | ||||
@@ -216,351 +123,6 @@ func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byt | |||||
out.WriteString("</a>") | out.WriteString("</a>") | ||||
} | } | ||||
// cutoutVerbosePrefix cutouts URL prefix including sub-path to | |||||
// return a clean unified string of request URL path. | |||||
func cutoutVerbosePrefix(prefix string) string { | |||||
if len(prefix) == 0 || prefix[0] != '/' { | |||||
return prefix | |||||
} | |||||
count := 0 | |||||
for i := 0; i < len(prefix); i++ { | |||||
if prefix[i] == '/' { | |||||
count++ | |||||
} | |||||
if count >= 3+setting.AppSubURLDepth { | |||||
return prefix[:i] | |||||
} | |||||
} | |||||
return prefix | |||||
} | |||||
// URLJoin joins url components, like path.Join, but preserving contents | |||||
func URLJoin(base string, elems ...string) string { | |||||
u, err := url.Parse(base) | |||||
if err != nil { | |||||
log.Error(4, "URLJoin: Invalid base URL %s", base) | |||||
return "" | |||||
} | |||||
joinArgs := make([]string, 0, len(elems)+1) | |||||
joinArgs = append(joinArgs, u.Path) | |||||
joinArgs = append(joinArgs, elems...) | |||||
u.Path = path.Join(joinArgs...) | |||||
return u.String() | |||||
} | |||||
// RenderIssueIndexPattern renders issue indexes to corresponding links. | |||||
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
urlPrefix = cutoutVerbosePrefix(urlPrefix) | |||||
pattern := IssueNumericPattern | |||||
if 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 link string | |||||
if metas == nil { | |||||
link = fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(urlPrefix, "issues", string(m[1:])), m) | |||||
} else { | |||||
// Support for external issue tracker | |||||
if metas["style"] == IssueNameStyleAlphanumeric { | |||||
metas["index"] = string(m) | |||||
} else { | |||||
metas["index"] = string(m[1:]) | |||||
} | |||||
link = fmt.Sprintf(`<a href="%s">%s</a>`, com.Expand(metas["format"], metas), m) | |||||
} | |||||
rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// IsSameDomain checks if given url string has the same hostname as current Gitea instance | |||||
func IsSameDomain(s string) bool { | |||||
if strings.HasPrefix(s, "/") { | |||||
return true | |||||
} | |||||
if uapp, err := url.Parse(setting.AppURL); err == nil { | |||||
if u, err := url.Parse(s); err == nil { | |||||
return u.Host == uapp.Host | |||||
} | |||||
return false | |||||
} | |||||
return false | |||||
} | |||||
// renderFullSha1Pattern renders SHA containing URLs | |||||
func renderFullSha1Pattern(rawBytes []byte, urlPrefix string) []byte { | |||||
ms := AnySHA1Pattern.FindAllSubmatch(rawBytes, -1) | |||||
for _, m := range ms { | |||||
all := m[0] | |||||
protocol := string(m[1]) | |||||
paths := string(m[2]) | |||||
path := protocol + "://" + paths | |||||
author := string(m[3]) | |||||
repoName := string(m[4]) | |||||
path = URLJoin(path, author, repoName) | |||||
ltype := "src" | |||||
itemType := m[5] | |||||
if IsSameDomain(paths) { | |||||
ltype = string(itemType) | |||||
} else if string(itemType) == "commit" { | |||||
ltype = "commit" | |||||
} | |||||
sha := m[6] | |||||
var subtree string | |||||
if len(m) > 7 && len(m[7]) > 0 { | |||||
subtree = string(m[7]) | |||||
} | |||||
var line []byte | |||||
if len(m) > 8 && len(m[8]) > 0 { | |||||
line = m[8] | |||||
} | |||||
urlSuffix := "" | |||||
text := base.ShortSha(string(sha)) | |||||
if subtree != "" { | |||||
urlSuffix = "/" + subtree | |||||
text += urlSuffix | |||||
} | |||||
if line != nil { | |||||
value := string(line) | |||||
urlSuffix += "#" | |||||
urlSuffix += value | |||||
text += " (" | |||||
text += value | |||||
text += ")" | |||||
} | |||||
rawBytes = bytes.Replace(rawBytes, all, []byte(fmt.Sprintf( | |||||
`<a href="%s">%s</a>`, URLJoin(path, ltype, string(sha))+urlSuffix, text)), -1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// RenderFullIssuePattern renders issues-like URLs | |||||
func RenderFullIssuePattern(rawBytes []byte) []byte { | |||||
ms := getIssueFullPattern().FindAllSubmatch(rawBytes, -1) | |||||
for _, m := range ms { | |||||
all := m[0] | |||||
id := string(m[1]) | |||||
text := "#" + id | |||||
// TODO if m[2] is not nil, then link is to a comment, | |||||
// and we should indicate that in the text somehow | |||||
rawBytes = bytes.Replace(rawBytes, all, []byte(fmt.Sprintf( | |||||
`<a href="%s">%s</a>`, string(all), text)), -1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
func firstIndexOfByte(sl []byte, target byte) int { | |||||
for i := 0; i < len(sl); i++ { | |||||
if sl[i] == target { | |||||
return i | |||||
} | |||||
} | |||||
return -1 | |||||
} | |||||
func lastIndexOfByte(sl []byte, target byte) int { | |||||
for i := len(sl) - 1; i >= 0; i-- { | |||||
if sl[i] == target { | |||||
return i | |||||
} | |||||
} | |||||
return -1 | |||||
} | |||||
// RenderShortLinks processes [[syntax]] | |||||
// | |||||
// noLink flag disables making link tags when set to true | |||||
// so this function just replaces the whole [[...]] with the content text | |||||
// | |||||
// isWikiMarkdown is a flag to choose linking url prefix | |||||
func RenderShortLinks(rawBytes []byte, urlPrefix string, noLink bool, isWikiMarkdown bool) []byte { | |||||
ms := ShortLinkPattern.FindAll(rawBytes, -1) | |||||
for _, m := range ms { | |||||
orig := bytes.TrimSpace(m) | |||||
m = orig[2:] | |||||
tailPos := lastIndexOfByte(m, ']') + 1 | |||||
tail := []byte{} | |||||
if tailPos < len(m) { | |||||
tail = m[tailPos:] | |||||
m = m[:tailPos-1] | |||||
} | |||||
m = m[:len(m)-2] | |||||
props := map[string]string{} | |||||
// MediaWiki uses [[link|text]], while GitHub uses [[text|link]] | |||||
// It makes page handling terrible, but we prefer GitHub syntax | |||||
// And fall back to MediaWiki only when it is obvious from the look | |||||
// Of text and link contents | |||||
sl := bytes.Split(m, []byte("|")) | |||||
for _, v := range sl { | |||||
switch bytes.Count(v, []byte("=")) { | |||||
// Piped args without = sign, these are mandatory arguments | |||||
case 0: | |||||
{ | |||||
sv := string(v) | |||||
if props["name"] == "" { | |||||
if isLink(v) { | |||||
// If we clearly see it is a link, we save it so | |||||
// But first we need to ensure, that if both mandatory args provided | |||||
// look like links, we stick to GitHub syntax | |||||
if props["link"] != "" { | |||||
props["name"] = props["link"] | |||||
} | |||||
props["link"] = strings.TrimSpace(sv) | |||||
} else { | |||||
props["name"] = sv | |||||
} | |||||
} else { | |||||
props["link"] = strings.TrimSpace(sv) | |||||
} | |||||
} | |||||
// Piped args with = sign, these are optional arguments | |||||
case 1: | |||||
{ | |||||
sep := firstIndexOfByte(v, '=') | |||||
key, val := string(v[:sep]), html.UnescapeString(string(v[sep+1:])) | |||||
lastCharIndex := len(val) - 1 | |||||
if (val[0] == '"' || val[0] == '\'') && (val[lastCharIndex] == '"' || val[lastCharIndex] == '\'') { | |||||
val = val[1:lastCharIndex] | |||||
} | |||||
props[key] = val | |||||
} | |||||
} | |||||
} | |||||
var name string | |||||
var link string | |||||
if props["link"] != "" { | |||||
link = props["link"] | |||||
} else if props["name"] != "" { | |||||
link = props["name"] | |||||
} | |||||
if props["title"] != "" { | |||||
name = props["title"] | |||||
} else if props["name"] != "" { | |||||
name = props["name"] | |||||
} else { | |||||
name = link | |||||
} | |||||
name += string(tail) | |||||
image := false | |||||
ext := filepath.Ext(string(link)) | |||||
if ext != "" { | |||||
switch ext { | |||||
case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg": | |||||
{ | |||||
image = true | |||||
} | |||||
} | |||||
} | |||||
absoluteLink := isLink([]byte(link)) | |||||
if !absoluteLink { | |||||
link = strings.Replace(link, " ", "+", -1) | |||||
} | |||||
if image { | |||||
if !absoluteLink { | |||||
if IsSameDomain(urlPrefix) { | |||||
urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) | |||||
} | |||||
if isWikiMarkdown { | |||||
link = URLJoin("wiki", "raw", link) | |||||
} | |||||
link = URLJoin(urlPrefix, link) | |||||
} | |||||
title := props["title"] | |||||
if title == "" { | |||||
title = props["alt"] | |||||
} | |||||
if title == "" { | |||||
title = path.Base(string(name)) | |||||
} | |||||
alt := props["alt"] | |||||
if alt == "" { | |||||
alt = name | |||||
} | |||||
if alt != "" { | |||||
alt = `alt="` + alt + `"` | |||||
} | |||||
name = fmt.Sprintf(`<img src="%s" %s title="%s" />`, link, alt, title) | |||||
} else if !absoluteLink { | |||||
if isWikiMarkdown { | |||||
link = URLJoin("wiki", link) | |||||
} | |||||
link = URLJoin(urlPrefix, link) | |||||
} | |||||
if noLink { | |||||
rawBytes = bytes.Replace(rawBytes, orig, []byte(name), -1) | |||||
} else { | |||||
rawBytes = bytes.Replace(rawBytes, orig, | |||||
[]byte(fmt.Sprintf(`<a href="%s">%s</a>`, link, name)), -1) | |||||
} | |||||
} | |||||
return rawBytes | |||||
} | |||||
// RenderCrossReferenceIssueIndexPattern renders issue indexes from other repositories to corresponding links. | |||||
func RenderCrossReferenceIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
ms := CrossReferenceIssueNumericPattern.FindAll(rawBytes, -1) | |||||
for _, m := range ms { | |||||
if m[0] == ' ' || m[0] == '(' { | |||||
m = m[1:] // ignore leading space or opening parentheses | |||||
} | |||||
repo := string(bytes.Split(m, []byte("#"))[0]) | |||||
issue := string(bytes.Split(m, []byte("#"))[1]) | |||||
link := fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(setting.AppURL, repo, "issues", issue), m) | |||||
rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// renderSha1CurrentPattern renders SHA1 strings to corresponding links that assumes in the same repository. | |||||
func renderSha1CurrentPattern(rawBytes []byte, urlPrefix string) []byte { | |||||
ms := Sha1CurrentPattern.FindAllSubmatch(rawBytes, -1) | |||||
for _, m := range ms { | |||||
hash := m[1] | |||||
// The regex does not lie, it matches the hash pattern. | |||||
// However, a regex cannot know if a hash actually exists or not. | |||||
// We could assume that a SHA1 hash should probably contain alphas AND numerics | |||||
// but that is not always the case. | |||||
// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash | |||||
// as used by git and github for linking and thus we have to do similar. | |||||
rawBytes = bytes.Replace(rawBytes, hash, []byte(fmt.Sprintf( | |||||
`<a href="%s">%s</a>`, URLJoin(urlPrefix, "commit", string(hash)), base.ShortSha(string(hash)))), -1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// RenderSpecialLink renders mentions, indexes and SHA1 strings to corresponding links. | |||||
func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]string, isWikiMarkdown bool) []byte { | |||||
ms := MentionPattern.FindAll(rawBytes, -1) | |||||
for _, m := range ms { | |||||
m = m[bytes.Index(m, []byte("@")):] | |||||
rawBytes = bytes.Replace(rawBytes, m, | |||||
[]byte(fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(setting.AppURL, string(m[1:])), m)), -1) | |||||
} | |||||
rawBytes = RenderFullIssuePattern(rawBytes) | |||||
rawBytes = RenderShortLinks(rawBytes, urlPrefix, false, isWikiMarkdown) | |||||
rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas) | |||||
rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas) | |||||
rawBytes = renderFullSha1Pattern(rawBytes, urlPrefix) | |||||
rawBytes = renderSha1CurrentPattern(rawBytes, urlPrefix) | |||||
return rawBytes | |||||
} | |||||
// RenderRaw renders Markdown to HTML without handling special links. | // RenderRaw renders Markdown to HTML without handling special links. | ||||
func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | ||||
htmlFlags := 0 | htmlFlags := 0 | ||||
@@ -589,107 +151,6 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | |||||
} | } | ||||
var ( | var ( | ||||
leftAngleBracket = []byte("</") | |||||
rightAngleBracket = []byte(">") | |||||
) | |||||
var noEndTags = []string{"img", "input", "br", "hr"} | |||||
// PostProcess treats different types of HTML differently, | |||||
// and only renders special links for plain text blocks. | |||||
func PostProcess(rawHTML []byte, urlPrefix string, metas map[string]string, isWikiMarkdown bool) []byte { | |||||
startTags := make([]string, 0, 5) | |||||
var buf bytes.Buffer | |||||
tokenizer := html.NewTokenizer(bytes.NewReader(rawHTML)) | |||||
OUTER_LOOP: | |||||
for html.ErrorToken != tokenizer.Next() { | |||||
token := tokenizer.Token() | |||||
switch token.Type { | |||||
case html.TextToken: | |||||
buf.Write(RenderSpecialLink([]byte(token.String()), urlPrefix, metas, isWikiMarkdown)) | |||||
case html.StartTagToken: | |||||
buf.WriteString(token.String()) | |||||
tagName := token.Data | |||||
// If this is an excluded tag, we skip processing all output until a close tag is encountered. | |||||
if strings.EqualFold("a", tagName) || strings.EqualFold("code", tagName) || strings.EqualFold("pre", tagName) { | |||||
stackNum := 1 | |||||
for html.ErrorToken != tokenizer.Next() { | |||||
token = tokenizer.Token() | |||||
// Copy the token to the output verbatim | |||||
buf.Write(RenderShortLinks([]byte(token.String()), urlPrefix, true, isWikiMarkdown)) | |||||
if token.Type == html.StartTagToken && !com.IsSliceContainsStr(noEndTags, token.Data) { | |||||
stackNum++ | |||||
} | |||||
// If this is the close tag to the outer-most, we are done | |||||
if token.Type == html.EndTagToken { | |||||
stackNum-- | |||||
if stackNum <= 0 && strings.EqualFold(tagName, token.Data) { | |||||
break | |||||
} | |||||
} | |||||
} | |||||
continue OUTER_LOOP | |||||
} | |||||
if !com.IsSliceContainsStr(noEndTags, tagName) { | |||||
startTags = append(startTags, tagName) | |||||
} | |||||
case html.EndTagToken: | |||||
if len(startTags) == 0 { | |||||
buf.WriteString(token.String()) | |||||
break | |||||
} | |||||
buf.Write(leftAngleBracket) | |||||
buf.WriteString(startTags[len(startTags)-1]) | |||||
buf.Write(rightAngleBracket) | |||||
startTags = startTags[:len(startTags)-1] | |||||
default: | |||||
buf.WriteString(token.String()) | |||||
} | |||||
} | |||||
if io.EOF == tokenizer.Err() { | |||||
return buf.Bytes() | |||||
} | |||||
// If we are not at the end of the input, then some other parsing error has occurred, | |||||
// so return the input verbatim. | |||||
return rawHTML | |||||
} | |||||
// Render renders Markdown to HTML with all specific handling stuff. | |||||
func render(rawBytes []byte, urlPrefix string, metas map[string]string, isWikiMarkdown bool) []byte { | |||||
urlPrefix = strings.Replace(urlPrefix, " ", "+", -1) | |||||
result := RenderRaw(rawBytes, urlPrefix, isWikiMarkdown) | |||||
result = PostProcess(result, urlPrefix, metas, isWikiMarkdown) | |||||
result = SanitizeBytes(result) | |||||
return result | |||||
} | |||||
// Render renders Markdown to HTML with all specific handling stuff. | |||||
func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
return render(rawBytes, urlPrefix, metas, false) | |||||
} | |||||
// RenderString renders Markdown to HTML with special links and returns string type. | |||||
func RenderString(raw, urlPrefix string, metas map[string]string) string { | |||||
return string(render([]byte(raw), urlPrefix, metas, false)) | |||||
} | |||||
// RenderWiki renders markdown wiki page to HTML and return HTML string | |||||
func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string { | |||||
return string(render(rawBytes, urlPrefix, metas, true)) | |||||
} | |||||
var ( | |||||
// MarkupName describes markup's name | // MarkupName describes markup's name | ||||
MarkupName = "markdown" | MarkupName = "markdown" | ||||
) | ) | ||||
@@ -714,5 +175,26 @@ func (Parser) Extensions() []string { | |||||
// Render implements markup.Parser | // Render implements markup.Parser | ||||
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | ||||
return render(rawBytes, urlPrefix, metas, isWiki) | |||||
return RenderRaw(rawBytes, urlPrefix, isWiki) | |||||
} | |||||
// Render renders Markdown to HTML with all specific handling stuff. | |||||
func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
return markup.Render("a.md", rawBytes, urlPrefix, metas) | |||||
} | |||||
// RenderString renders Markdown to HTML with special links and returns string type. | |||||
func RenderString(raw, urlPrefix string, metas map[string]string) string { | |||||
return markup.RenderString("a.md", raw, urlPrefix, metas) | |||||
} | |||||
// RenderWiki renders markdown wiki page to HTML and return HTML string | |||||
func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string { | |||||
return markup.RenderWiki("a.md", rawBytes, urlPrefix, metas) | |||||
} | |||||
// IsMarkdownFile reports whether name looks like a Markdown file | |||||
// based on its extension. | |||||
func IsMarkdownFile(name string) bool { | |||||
return markup.IsMarkupFile(name, MarkupName) | |||||
} | } |
@@ -7,12 +7,13 @@ package markdown_test | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"strconv" | "strconv" | ||||
"testing" | |||||
"strings" | "strings" | ||||
"testing" | |||||
. "code.gitea.io/gitea/modules/markdown" | . "code.gitea.io/gitea/modules/markdown" | ||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
) | ) | ||||
@@ -24,24 +25,24 @@ var numericMetas = map[string]string{ | |||||
"format": "https://someurl.com/{user}/{repo}/{index}", | "format": "https://someurl.com/{user}/{repo}/{index}", | ||||
"user": "someUser", | "user": "someUser", | ||||
"repo": "someRepo", | "repo": "someRepo", | ||||
"style": IssueNameStyleNumeric, | |||||
"style": markup.IssueNameStyleNumeric, | |||||
} | } | ||||
var alphanumericMetas = map[string]string{ | var alphanumericMetas = map[string]string{ | ||||
"format": "https://someurl.com/{user}/{repo}/{index}", | "format": "https://someurl.com/{user}/{repo}/{index}", | ||||
"user": "someUser", | "user": "someUser", | ||||
"repo": "someRepo", | "repo": "someRepo", | ||||
"style": IssueNameStyleAlphanumeric, | |||||
"style": markup.IssueNameStyleAlphanumeric, | |||||
} | } | ||||
// numericLink an HTML to a numeric-style issue | // numericLink an HTML to a numeric-style issue | ||||
func numericIssueLink(baseURL string, index int) string { | func numericIssueLink(baseURL string, index int) string { | ||||
return link(URLJoin(baseURL, strconv.Itoa(index)), fmt.Sprintf("#%d", index)) | |||||
return link(markup.URLJoin(baseURL, strconv.Itoa(index)), fmt.Sprintf("#%d", index)) | |||||
} | } | ||||
// alphanumLink an HTML link to an alphanumeric-style issue | // alphanumLink an HTML link to an alphanumeric-style issue | ||||
func alphanumIssueLink(baseURL string, name string) string { | func alphanumIssueLink(baseURL string, name string) string { | ||||
return link(URLJoin(baseURL, name), name) | |||||
return link(markup.URLJoin(baseURL, name), name) | |||||
} | } | ||||
// urlContentsLink an HTML link whose contents is the target URL | // urlContentsLink an HTML link whose contents is the target URL | ||||
@@ -56,175 +57,7 @@ func link(href, contents string) string { | |||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) { | func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) { | ||||
assert.Equal(t, expected, | assert.Equal(t, expected, | ||||
string(RenderIssueIndexPattern([]byte(input), AppSubURL, metas))) | |||||
} | |||||
func TestURLJoin(t *testing.T) { | |||||
type test struct { | |||||
Expected string | |||||
Base string | |||||
Elements []string | |||||
} | |||||
newTest := func(expected, base string, elements ...string) test { | |||||
return test{Expected: expected, Base: base, Elements: elements} | |||||
} | |||||
for _, test := range []test{ | |||||
newTest("https://try.gitea.io/a/b/c", | |||||
"https://try.gitea.io", "a/b", "c"), | |||||
newTest("https://try.gitea.io/a/b/c", | |||||
"https://try.gitea.io/", "/a/b/", "/c/"), | |||||
newTest("https://try.gitea.io/a/c", | |||||
"https://try.gitea.io/", "/a/./b/", "../c/"), | |||||
newTest("a/b/c", | |||||
"a", "b/c/"), | |||||
newTest("a/b/d", | |||||
"a/", "b/c/", "/../d/"), | |||||
} { | |||||
assert.Equal(t, test.Expected, URLJoin(test.Base, test.Elements...)) | |||||
} | |||||
} | |||||
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) | |||||
} | |||||
// should not render anything when there are no mentions | |||||
test("") | |||||
test("this is a test") | |||||
test("test 123 123 1234") | |||||
test("#") | |||||
test("# # #") | |||||
test("# 123") | |||||
test("#abcd") | |||||
test("##1234") | |||||
test("test#1234") | |||||
test("#1234test") | |||||
test(" test #1234test") | |||||
// should not render issue mention without leading space | |||||
test("test#54321 issue") | |||||
// should not render issue mention without trailing space | |||||
test("test #54321issue") | |||||
} | |||||
func TestRender_IssueIndexPattern2(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
// numeric: render inputs with valid mentions | |||||
test := func(s, expectedFmt string, indices ...int) { | |||||
links := make([]interface{}, len(indices)) | |||||
for i, index := range indices { | |||||
links[i] = numericIssueLink(URLJoin(setting.AppSubURL, "issues"), index) | |||||
} | |||||
expectedNil := fmt.Sprintf(expectedFmt, links...) | |||||
testRenderIssueIndexPattern(t, s, expectedNil, nil) | |||||
for i, index := range indices { | |||||
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index) | |||||
} | |||||
expectedNum := fmt.Sprintf(expectedFmt, links...) | |||||
testRenderIssueIndexPattern(t, s, expectedNum, numericMetas) | |||||
} | |||||
// should render freestanding mentions | |||||
test("#1234 test", "%s test", 1234) | |||||
test("test #8 issue", "test %s issue", 8) | |||||
test("test issue #1234", "test issue %s", 1234) | |||||
// should render mentions in parentheses | |||||
test("(#54321 issue)", "(%s issue)", 54321) | |||||
test("test (#9801 extra) issue", "test (%s extra) issue", 9801) | |||||
test("test (#1)", "test (%s)", 1) | |||||
// should render multiple issue mentions in the same line | |||||
test("#54321 #1243", "%s %s", 54321, 1243) | |||||
test("wow (#54321 #1243)", "wow (%s %s)", 54321, 1243) | |||||
test("(#4)(#5)", "(%s)(%s)", 4, 5) | |||||
test("#1 (#4321) test", "%s (%s) test", 1, 4321) | |||||
} | |||||
func TestRender_IssueIndexPattern3(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
// alphanumeric: render inputs without valid mentions | |||||
test := func(s string) { | |||||
testRenderIssueIndexPattern(t, s, s, alphanumericMetas) | |||||
} | |||||
test("") | |||||
test("this is a test") | |||||
test("test 123 123 1234") | |||||
test("#") | |||||
test("##1234") | |||||
test("# 123") | |||||
test("#abcd") | |||||
test("test #123") | |||||
test("abc-1234") // issue prefix must be capital | |||||
test("ABc-1234") // issue prefix must be _all_ capital | |||||
test("ABCDEFGHIJK-1234") // the limit is 10 characters in the prefix | |||||
test("ABC1234") // dash is required | |||||
test("test ABC- test") // number is required | |||||
test("test -1234 test") // prefix is required | |||||
test("testABC-123 test") // leading space is required | |||||
test("test ABC-123test") // trailing space is required | |||||
test("ABC-0123") // no leading zero | |||||
} | |||||
func TestRender_IssueIndexPattern4(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
// alphanumeric: render inputs with valid mentions | |||||
test := func(s, expectedFmt string, names ...string) { | |||||
links := make([]interface{}, len(names)) | |||||
for i, name := range names { | |||||
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", name) | |||||
} | |||||
expected := fmt.Sprintf(expectedFmt, links...) | |||||
testRenderIssueIndexPattern(t, s, expected, 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 TestRender_AutoLink(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
buffer := RenderSpecialLink([]byte(input), setting.AppSubURL, nil, false) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
buffer = RenderSpecialLink([]byte(input), setting.AppSubURL, nil, true) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
} | |||||
// render valid issue URLs | |||||
test(URLJoin(setting.AppSubURL, "issues", "3333"), | |||||
numericIssueLink(URLJoin(setting.AppSubURL, "issues"), 3333)) | |||||
// render external issue URLs | |||||
for _, externalURL := range []string{ | |||||
"http://1111/2222/ssss-issues/3333?param=blah&blahh=333", | |||||
"http://test.com/issues/33333", | |||||
"https://issues/333"} { | |||||
test(externalURL, externalURL) | |||||
} | |||||
// render valid commit URLs | |||||
tmp := URLJoin(AppSubURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae") | |||||
test(tmp, "<a href=\""+tmp+"\">d8a994ef24</a>") | |||||
tmp += "#diff-2" | |||||
test(tmp, "<a href=\""+tmp+"\">d8a994ef24 (diff-2)</a>") | |||||
// render other commit URLs | |||||
tmp = "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" | |||||
test(tmp, "<a href=\""+tmp+"\">d8a994ef24 (diff-2)</a>") | |||||
string(markup.RenderIssueIndexPattern([]byte(input), AppSubURL, metas))) | |||||
} | } | ||||
func TestRender_StandardLinks(t *testing.T) { | func TestRender_StandardLinks(t *testing.T) { | ||||
@@ -241,8 +74,8 @@ func TestRender_StandardLinks(t *testing.T) { | |||||
googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` | googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` | ||||
test("<https://google.com/>", googleRendered, googleRendered) | test("<https://google.com/>", googleRendered, googleRendered) | ||||
lnk := URLJoin(AppSubURL, "WikiPage") | |||||
lnkWiki := URLJoin(AppSubURL, "wiki", "WikiPage") | |||||
lnk := markup.URLJoin(AppSubURL, "WikiPage") | |||||
lnkWiki := markup.URLJoin(AppSubURL, "wiki", "WikiPage") | |||||
test("[WikiPage](WikiPage)", | test("[WikiPage](WikiPage)", | ||||
`<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`, | `<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`, | ||||
`<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`) | `<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`) | ||||
@@ -251,7 +84,7 @@ func TestRender_StandardLinks(t *testing.T) { | |||||
func TestRender_ShortLinks(t *testing.T) { | func TestRender_ShortLinks(t *testing.T) { | ||||
setting.AppURL = AppURL | setting.AppURL = AppURL | ||||
setting.AppSubURL = AppSubURL | setting.AppSubURL = AppSubURL | ||||
tree := URLJoin(AppSubURL, "src", "master") | |||||
tree := markup.URLJoin(AppSubURL, "src", "master") | |||||
test := func(input, expected, expectedWiki string) { | test := func(input, expected, expectedWiki string) { | ||||
buffer := RenderString(input, tree, nil) | buffer := RenderString(input, tree, nil) | ||||
@@ -260,13 +93,13 @@ func TestRender_ShortLinks(t *testing.T) { | |||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | ||||
} | } | ||||
rawtree := URLJoin(AppSubURL, "raw", "master") | |||||
url := URLJoin(tree, "Link") | |||||
otherUrl := URLJoin(tree, "OtherLink") | |||||
imgurl := URLJoin(rawtree, "Link.jpg") | |||||
urlWiki := URLJoin(AppSubURL, "wiki", "Link") | |||||
otherUrlWiki := URLJoin(AppSubURL, "wiki", "OtherLink") | |||||
imgurlWiki := URLJoin(AppSubURL, "wiki", "raw", "Link.jpg") | |||||
rawtree := markup.URLJoin(AppSubURL, "raw", "master") | |||||
url := markup.URLJoin(tree, "Link") | |||||
otherUrl := markup.URLJoin(tree, "OtherLink") | |||||
imgurl := markup.URLJoin(rawtree, "Link.jpg") | |||||
urlWiki := markup.URLJoin(AppSubURL, "wiki", "Link") | |||||
otherUrlWiki := markup.URLJoin(AppSubURL, "wiki", "OtherLink") | |||||
imgurlWiki := markup.URLJoin(AppSubURL, "wiki", "raw", "Link.jpg") | |||||
favicon := "http://google.com/favicon.ico" | favicon := "http://google.com/favicon.ico" | ||||
test( | test( | ||||
@@ -311,27 +144,26 @@ func TestRender_ShortLinks(t *testing.T) { | |||||
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherUrlWiki+`" rel="nofollow">OtherLink</a></p>`) | `<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherUrlWiki+`" rel="nofollow">OtherLink</a></p>`) | ||||
} | } | ||||
func TestRender_Commits(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
buffer := RenderString(input, setting.AppSubURL, nil) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
func TestMisc_IsMarkdownFile(t *testing.T) { | |||||
setting.Markdown.FileExtensions = []string{".md", ".markdown", ".mdown", ".mkd"} | |||||
trueTestCases := []string{ | |||||
"test.md", | |||||
"wow.MARKDOWN", | |||||
"LOL.mDoWn", | |||||
} | |||||
falseTestCases := []string{ | |||||
"test", | |||||
"abcdefg", | |||||
"abcdefghijklmnopqrstuvwxyz", | |||||
"test.md.test", | |||||
} | } | ||||
var sha = "b6dd6210eaebc915fd5be5579c58cce4da2e2579" | |||||
var commit = URLJoin(AppSubURL, "commit", sha) | |||||
var subtree = URLJoin(commit, "src") | |||||
var tree = strings.Replace(subtree, "/commit/", "/tree/", -1) | |||||
var src = strings.Replace(subtree, "/commit/", "/src/", -1) | |||||
test(sha, `<p><a href="`+commit+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
test(sha[:7], `<p><a href="`+commit[:len(commit)-(40-7)]+`" rel="nofollow">b6dd621</a></p>`) | |||||
test(sha[:39], `<p><a href="`+commit[:len(commit)-(40-39)]+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
test(commit, `<p><a href="`+commit+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
test(tree, `<p><a href="`+src+`" rel="nofollow">b6dd6210ea/src</a></p>`) | |||||
test("commit "+sha, `<p>commit <a href="`+commit+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, IsMarkdownFile(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, IsMarkdownFile(testCase)) | |||||
} | |||||
} | } | ||||
func TestRender_Images(t *testing.T) { | func TestRender_Images(t *testing.T) { | ||||
@@ -345,7 +177,7 @@ func TestRender_Images(t *testing.T) { | |||||
url := "../../.images/src/02/train.jpg" | url := "../../.images/src/02/train.jpg" | ||||
title := "Train" | title := "Train" | ||||
result := URLJoin(AppSubURL, url) | |||||
result := markup.URLJoin(AppSubURL, url) | |||||
test( | test( | ||||
"", | "", | ||||
@@ -356,143 +188,6 @@ func TestRender_Images(t *testing.T) { | |||||
`<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" alt="`+title+`" title="`+title+`"/></a></p>`) | `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" alt="`+title+`" title="`+title+`"/></a></p>`) | ||||
} | } | ||||
func TestRender_CrossReferences(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
buffer := RenderString(input, setting.AppSubURL, nil) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
} | |||||
test( | |||||
"gogits/gogs#12345", | |||||
`<p><a href="`+URLJoin(AppURL, "gogits", "gogs", "issues", "12345")+`" rel="nofollow">gogits/gogs#12345</a></p>`) | |||||
} | |||||
func TestRender_FullIssueURLs(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
result := RenderFullIssuePattern([]byte(input)) | |||||
assert.Equal(t, expected, string(result)) | |||||
} | |||||
test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", | |||||
"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") | |||||
test("Look here http://localhost:3000/person/repo/issues/4", | |||||
`Look here <a href="http://localhost:3000/person/repo/issues/4">#4</a>`) | |||||
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234", | |||||
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234">#4</a>`) | |||||
} | |||||
func TestRegExp_MentionPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"@Unknwon", | |||||
"@ANT_123", | |||||
"@xxx-DiN0-z-A..uru..s-xxx", | |||||
" @lol ", | |||||
" @Te/st", | |||||
} | |||||
falseTestCases := []string{ | |||||
"@ 0", | |||||
"@ ", | |||||
"@", | |||||
"", | |||||
"ABC", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
res := MentionPattern.MatchString(testCase) | |||||
if !res { | |||||
println() | |||||
println(testCase) | |||||
} | |||||
assert.True(t, res) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
res := MentionPattern.MatchString(testCase) | |||||
if res { | |||||
println() | |||||
println(testCase) | |||||
} | |||||
assert.False(t, res) | |||||
} | |||||
} | |||||
func TestRegExp_IssueNumericPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"#1234", | |||||
"#0", | |||||
"#1234567890987654321", | |||||
} | |||||
falseTestCases := []string{ | |||||
"# 1234", | |||||
"# 0", | |||||
"# ", | |||||
"#", | |||||
"#ABC", | |||||
"#1A2B", | |||||
"", | |||||
"ABC", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, IssueNumericPattern.MatchString(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, IssueNumericPattern.MatchString(testCase)) | |||||
} | |||||
} | |||||
func TestRegExp_IssueAlphanumericPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"ABC-1234", | |||||
"A-1", | |||||
"RC-80", | |||||
"ABCDEFGHIJ-1234567890987654321234567890", | |||||
} | |||||
falseTestCases := []string{ | |||||
"RC-08", | |||||
"PR-0", | |||||
"ABCDEFGHIJK-1", | |||||
"PR_1", | |||||
"", | |||||
"#ABC", | |||||
"", | |||||
"ABC", | |||||
"GG-", | |||||
"rm-1", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, IssueAlphanumericPattern.MatchString(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, IssueAlphanumericPattern.MatchString(testCase)) | |||||
} | |||||
} | |||||
func TestRegExp_Sha1CurrentPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"d8a994ef243349f321568f9e36d5c3f444b99cae", | |||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcd", | |||||
} | |||||
falseTestCases := []string{ | |||||
"test", | |||||
"abcdefg", | |||||
"abcdefghijklmnopqrstuvwxyzabcdefghijklmn", | |||||
"abcdefghijklmnopqrstuvwxyzabcdefghijklmO", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, Sha1CurrentPattern.MatchString(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, Sha1CurrentPattern.MatchString(testCase)) | |||||
} | |||||
} | |||||
func TestRegExp_ShortLinkPattern(t *testing.T) { | func TestRegExp_ShortLinkPattern(t *testing.T) { | ||||
trueTestCases := []string{ | trueTestCases := []string{ | ||||
"[[stuff]]", | "[[stuff]]", | ||||
@@ -510,139 +205,13 @@ func TestRegExp_ShortLinkPattern(t *testing.T) { | |||||
} | } | ||||
for _, testCase := range trueTestCases { | for _, testCase := range trueTestCases { | ||||
assert.True(t, ShortLinkPattern.MatchString(testCase)) | |||||
assert.True(t, markup.ShortLinkPattern.MatchString(testCase)) | |||||
} | } | ||||
for _, testCase := range falseTestCases { | for _, testCase := range falseTestCases { | ||||
assert.False(t, ShortLinkPattern.MatchString(testCase)) | |||||
assert.False(t, markup.ShortLinkPattern.MatchString(testCase)) | |||||
} | } | ||||
} | } | ||||
func TestRegExp_AnySHA1Pattern(t *testing.T) { | |||||
testCases := map[string][]string{ | |||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"blob", | |||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||||
"test/unit/event.js", | |||||
"L2703", | |||||
}, | |||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"blob", | |||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||||
"test/unit/event.js", | |||||
"", | |||||
}, | |||||
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"commit", | |||||
"0705be475092aede1eddae01319ec931fb9c65fc", | |||||
"", | |||||
"", | |||||
}, | |||||
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"tree", | |||||
"0705be475092aede1eddae01319ec931fb9c65fc", | |||||
"src", | |||||
"", | |||||
}, | |||||
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": { | |||||
"https", | |||||
"try.gogs.io", | |||||
"gogs", | |||||
"gogs", | |||||
"commit", | |||||
"d8a994ef243349f321568f9e36d5c3f444b99cae", | |||||
"", | |||||
"diff-2", | |||||
}, | |||||
} | |||||
for k, v := range testCases { | |||||
assert.Equal(t, AnySHA1Pattern.FindStringSubmatch(k)[1:], v) | |||||
} | |||||
} | |||||
func TestMisc_IsMarkdownFile(t *testing.T) { | |||||
setting.Markdown.FileExtensions = []string{".md", ".markdown", ".mdown", ".mkd"} | |||||
trueTestCases := []string{ | |||||
"test.md", | |||||
"wow.MARKDOWN", | |||||
"LOL.mDoWn", | |||||
} | |||||
falseTestCases := []string{ | |||||
"test", | |||||
"abcdefg", | |||||
"abcdefghijklmnopqrstuvwxyz", | |||||
"test.md.test", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, IsMarkdownFile(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, IsMarkdownFile(testCase)) | |||||
} | |||||
} | |||||
func TestMisc_IsSameDomain(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
var sha = "b6dd6210eaebc915fd5be5579c58cce4da2e2579" | |||||
var commit = URLJoin(AppSubURL, "commit", sha) | |||||
assert.True(t, IsSameDomain(commit)) | |||||
assert.False(t, IsSameDomain("http://google.com/ncr")) | |||||
assert.False(t, IsSameDomain("favicon.ico")) | |||||
} | |||||
// Test cases without ambiguous links | |||||
var sameCases = []string{ | |||||
// dear imgui wiki markdown extract: special wiki syntax | |||||
`Wiki! Enjoy :) | |||||
- [[Links, Language bindings, Engine bindings|Links]] | |||||
- [[Tips]] | |||||
Ideas and codes | |||||
- Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786 | |||||
- Node graph editors https://github.com/ocornut/imgui/issues/306 | |||||
- [[Memory Editor|memory_editor_example]] | |||||
- [[Plot var helper|plot_var_example]]`, | |||||
// wine-staging wiki home extract: tables, special wiki syntax, images | |||||
`## What is Wine Staging? | |||||
**Wine Staging** on website [wine-staging.com](http://wine-staging.com). | |||||
## Quick Links | |||||
Here are some links to the most important topics. You can find the full list of pages at the sidebar. | |||||
| [[images/icon-install.png]] | [[Installation]] | | |||||
|--------------------------------|----------------------------------------------------------| | |||||
| [[images/icon-usage.png]] | [[Usage]] | | |||||
`, | |||||
// libgdx wiki page: inline images with special syntax | |||||
`[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X. | |||||
1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop) | |||||
[[images/1.png]] | |||||
2. Perform a test run by hitting the Run! button. | |||||
[[images/2.png]]`, | |||||
} | |||||
func testAnswers(baseURLContent, baseURLImages string) []string { | func testAnswers(baseURLContent, baseURLImages string) []string { | ||||
return []string{ | return []string{ | ||||
`<p>Wiki! Enjoy :)</p> | `<p>Wiki! Enjoy :)</p> | ||||
@@ -697,24 +266,41 @@ func testAnswers(baseURLContent, baseURLImages string) []string { | |||||
} | } | ||||
} | } | ||||
func TestTotal_RenderString(t *testing.T) { | |||||
answers := testAnswers(URLJoin(AppSubURL, "src", "master/"), URLJoin(AppSubURL, "raw", "master/")) | |||||
// Test cases without ambiguous links | |||||
var sameCases = []string{ | |||||
// dear imgui wiki markdown extract: special wiki syntax | |||||
`Wiki! Enjoy :) | |||||
- [[Links, Language bindings, Engine bindings|Links]] | |||||
- [[Tips]] | |||||
for i := 0; i < len(sameCases); i++ { | |||||
line := RenderString(sameCases[i], URLJoin(AppSubURL, "src", "master/"), nil) | |||||
assert.Equal(t, answers[i], line) | |||||
} | |||||
Ideas and codes | |||||
testCases := []string{} | |||||
- Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786 | |||||
- Node graph editors https://github.com/ocornut/imgui/issues/306 | |||||
- [[Memory Editor|memory_editor_example]] | |||||
- [[Plot var helper|plot_var_example]]`, | |||||
// wine-staging wiki home extract: tables, special wiki syntax, images | |||||
`## What is Wine Staging? | |||||
**Wine Staging** on website [wine-staging.com](http://wine-staging.com). | |||||
for i := 0; i < len(testCases); i += 2 { | |||||
line := RenderString(testCases[i], AppSubURL, nil) | |||||
assert.Equal(t, testCases[i+1], line) | |||||
} | |||||
## Quick Links | |||||
Here are some links to the most important topics. You can find the full list of pages at the sidebar. | |||||
| [[images/icon-install.png]] | [[Installation]] | | |||||
|--------------------------------|----------------------------------------------------------| | |||||
| [[images/icon-usage.png]] | [[Usage]] | | |||||
`, | |||||
// libgdx wiki page: inline images with special syntax | |||||
`[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X. | |||||
1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop) | |||||
[[images/1.png]] | |||||
2. Perform a test run by hitting the Run! button. | |||||
[[images/2.png]]`, | |||||
} | } | ||||
func TestTotal_RenderWiki(t *testing.T) { | func TestTotal_RenderWiki(t *testing.T) { | ||||
answers := testAnswers(URLJoin(AppSubURL, "wiki/"), URLJoin(AppSubURL, "wiki", "raw/")) | |||||
answers := testAnswers(markup.URLJoin(AppSubURL, "wiki/"), markup.URLJoin(AppSubURL, "wiki", "raw/")) | |||||
for i := 0; i < len(sameCases); i++ { | for i := 0; i < len(sameCases); i++ { | ||||
line := RenderWiki([]byte(sameCases[i]), AppSubURL, nil) | line := RenderWiki([]byte(sameCases[i]), AppSubURL, nil) | ||||
@@ -739,3 +325,19 @@ func TestTotal_RenderWiki(t *testing.T) { | |||||
assert.Equal(t, testCases[i+1], line) | assert.Equal(t, testCases[i+1], line) | ||||
} | } | ||||
} | } | ||||
func TestTotal_RenderString(t *testing.T) { | |||||
answers := testAnswers(markup.URLJoin(AppSubURL, "src", "master/"), markup.URLJoin(AppSubURL, "raw", "master/")) | |||||
for i := 0; i < len(sameCases); i++ { | |||||
line := RenderString(sameCases[i], markup.URLJoin(AppSubURL, "src", "master/"), nil) | |||||
assert.Equal(t, answers[i], line) | |||||
} | |||||
testCases := []string{} | |||||
for i := 0; i < len(testCases); i += 2 { | |||||
line := RenderString(testCases[i], AppSubURL, nil) | |||||
assert.Equal(t, testCases[i+1], line) | |||||
} | |||||
} |
@@ -0,0 +1,517 @@ | |||||
// Copyright 2017 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package markup | |||||
import ( | |||||
"bytes" | |||||
"fmt" | |||||
"io" | |||||
"net/url" | |||||
"path" | |||||
"path/filepath" | |||||
"regexp" | |||||
"strings" | |||||
"code.gitea.io/gitea/modules/base" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
"github.com/Unknwon/com" | |||||
"golang.org/x/net/html" | |||||
) | |||||
// Issue name styles | |||||
const ( | |||||
IssueNameStyleNumeric = "numeric" | |||||
IssueNameStyleAlphanumeric = "alphanumeric" | |||||
) | |||||
var ( | |||||
// NOTE: All below regex matching do not perform any extra validation. | |||||
// Thus a link is produced even if the linked entity does not exist. | |||||
// While fast, this is also incorrect and lead to false positives. | |||||
// TODO: fix invalid linking issue | |||||
// MentionPattern matches string that mentions someone, e.g. @Unknwon | |||||
MentionPattern = regexp.MustCompile(`(\s|^|\W)@[0-9a-zA-Z-_\.]+`) | |||||
// IssueNumericPattern matches string that references to a numeric issue, e.g. #1287 | |||||
IssueNumericPattern = regexp.MustCompile(`( |^|\()#[0-9]+\b`) | |||||
// IssueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 | |||||
IssueAlphanumericPattern = regexp.MustCompile(`( |^|\()[A-Z]{1,10}-[1-9][0-9]*\b`) | |||||
// CrossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository | |||||
// e.g. gogits/gogs#12345 | |||||
CrossReferenceIssueNumericPattern = regexp.MustCompile(`( |^)[0-9a-zA-Z]+/[0-9a-zA-Z]+#[0-9]+\b`) | |||||
// 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. | |||||
Sha1CurrentPattern = regexp.MustCompile(`(?:^|\s|\()([0-9a-f]{7,40})\b`) | |||||
// ShortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax | |||||
ShortLinkPattern = regexp.MustCompile(`(\[\[.*?\]\]\w*)`) | |||||
// AnySHA1Pattern allows to split url containing SHA into parts | |||||
AnySHA1Pattern = regexp.MustCompile(`(http\S*)://(\S+)/(\S+)/(\S+)/(\S+)/([0-9a-f]{40})(?:/?([^#\s]+)?(?:#(\S+))?)?`) | |||||
validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) | |||||
) | |||||
// regexp for full links to issues/pulls | |||||
var issueFullPattern *regexp.Regexp | |||||
// IsLink reports whether link fits valid format. | |||||
func IsLink(link []byte) bool { | |||||
return isLink(link) | |||||
} | |||||
// isLink reports whether link fits valid format. | |||||
func isLink(link []byte) bool { | |||||
return validLinksPattern.Match(link) | |||||
} | |||||
func getIssueFullPattern() *regexp.Regexp { | |||||
if issueFullPattern == nil { | |||||
appURL := setting.AppURL | |||||
if len(appURL) > 0 && appURL[len(appURL)-1] != '/' { | |||||
appURL += "/" | |||||
} | |||||
issueFullPattern = regexp.MustCompile(appURL + | |||||
`\w+/\w+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#]\S+.(\S+)?)?\b`) | |||||
} | |||||
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.FindAllString(content, -1) | |||||
for i := range mentions { | |||||
mentions[i] = mentions[i][strings.Index(mentions[i], "@")+1:] // Strip @ character | |||||
} | |||||
return mentions | |||||
} | |||||
// cutoutVerbosePrefix cutouts URL prefix including sub-path to | |||||
// return a clean unified string of request URL path. | |||||
func cutoutVerbosePrefix(prefix string) string { | |||||
if len(prefix) == 0 || prefix[0] != '/' { | |||||
return prefix | |||||
} | |||||
count := 0 | |||||
for i := 0; i < len(prefix); i++ { | |||||
if prefix[i] == '/' { | |||||
count++ | |||||
} | |||||
if count >= 3+setting.AppSubURLDepth { | |||||
return prefix[:i] | |||||
} | |||||
} | |||||
return prefix | |||||
} | |||||
// URLJoin joins url components, like path.Join, but preserving contents | |||||
func URLJoin(base string, elems ...string) string { | |||||
u, err := url.Parse(base) | |||||
if err != nil { | |||||
log.Error(4, "URLJoin: Invalid base URL %s", base) | |||||
return "" | |||||
} | |||||
joinArgs := make([]string, 0, len(elems)+1) | |||||
joinArgs = append(joinArgs, u.Path) | |||||
joinArgs = append(joinArgs, elems...) | |||||
u.Path = path.Join(joinArgs...) | |||||
return u.String() | |||||
} | |||||
// RenderIssueIndexPattern renders issue indexes to corresponding links. | |||||
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
urlPrefix = cutoutVerbosePrefix(urlPrefix) | |||||
pattern := IssueNumericPattern | |||||
if 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 link string | |||||
if metas == nil { | |||||
link = fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(urlPrefix, "issues", string(m[1:])), m) | |||||
} else { | |||||
// Support for external issue tracker | |||||
if metas["style"] == IssueNameStyleAlphanumeric { | |||||
metas["index"] = string(m) | |||||
} else { | |||||
metas["index"] = string(m[1:]) | |||||
} | |||||
link = fmt.Sprintf(`<a href="%s">%s</a>`, com.Expand(metas["format"], metas), m) | |||||
} | |||||
rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// IsSameDomain checks if given url string has the same hostname as current Gitea instance | |||||
func IsSameDomain(s string) bool { | |||||
if strings.HasPrefix(s, "/") { | |||||
return true | |||||
} | |||||
if uapp, err := url.Parse(setting.AppURL); err == nil { | |||||
if u, err := url.Parse(s); err == nil { | |||||
return u.Host == uapp.Host | |||||
} | |||||
return false | |||||
} | |||||
return false | |||||
} | |||||
// renderFullSha1Pattern renders SHA containing URLs | |||||
func renderFullSha1Pattern(rawBytes []byte, urlPrefix string) []byte { | |||||
ms := AnySHA1Pattern.FindAllSubmatch(rawBytes, -1) | |||||
for _, m := range ms { | |||||
all := m[0] | |||||
protocol := string(m[1]) | |||||
paths := string(m[2]) | |||||
path := protocol + "://" + paths | |||||
author := string(m[3]) | |||||
repoName := string(m[4]) | |||||
path = URLJoin(path, author, repoName) | |||||
ltype := "src" | |||||
itemType := m[5] | |||||
if IsSameDomain(paths) { | |||||
ltype = string(itemType) | |||||
} else if string(itemType) == "commit" { | |||||
ltype = "commit" | |||||
} | |||||
sha := m[6] | |||||
var subtree string | |||||
if len(m) > 7 && len(m[7]) > 0 { | |||||
subtree = string(m[7]) | |||||
} | |||||
var line []byte | |||||
if len(m) > 8 && len(m[8]) > 0 { | |||||
line = m[8] | |||||
} | |||||
urlSuffix := "" | |||||
text := base.ShortSha(string(sha)) | |||||
if subtree != "" { | |||||
urlSuffix = "/" + subtree | |||||
text += urlSuffix | |||||
} | |||||
if line != nil { | |||||
value := string(line) | |||||
urlSuffix += "#" | |||||
urlSuffix += value | |||||
text += " (" | |||||
text += value | |||||
text += ")" | |||||
} | |||||
rawBytes = bytes.Replace(rawBytes, all, []byte(fmt.Sprintf( | |||||
`<a href="%s">%s</a>`, URLJoin(path, ltype, string(sha))+urlSuffix, text)), -1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// RenderFullIssuePattern renders issues-like URLs | |||||
func RenderFullIssuePattern(rawBytes []byte) []byte { | |||||
ms := getIssueFullPattern().FindAllSubmatch(rawBytes, -1) | |||||
for _, m := range ms { | |||||
all := m[0] | |||||
id := string(m[1]) | |||||
text := "#" + id | |||||
// TODO if m[2] is not nil, then link is to a comment, | |||||
// and we should indicate that in the text somehow | |||||
rawBytes = bytes.Replace(rawBytes, all, []byte(fmt.Sprintf( | |||||
`<a href="%s">%s</a>`, string(all), text)), -1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
func firstIndexOfByte(sl []byte, target byte) int { | |||||
for i := 0; i < len(sl); i++ { | |||||
if sl[i] == target { | |||||
return i | |||||
} | |||||
} | |||||
return -1 | |||||
} | |||||
func lastIndexOfByte(sl []byte, target byte) int { | |||||
for i := len(sl) - 1; i >= 0; i-- { | |||||
if sl[i] == target { | |||||
return i | |||||
} | |||||
} | |||||
return -1 | |||||
} | |||||
// RenderShortLinks processes [[syntax]] | |||||
// | |||||
// noLink flag disables making link tags when set to true | |||||
// so this function just replaces the whole [[...]] with the content text | |||||
// | |||||
// isWikiMarkdown is a flag to choose linking url prefix | |||||
func RenderShortLinks(rawBytes []byte, urlPrefix string, noLink bool, isWikiMarkdown bool) []byte { | |||||
ms := ShortLinkPattern.FindAll(rawBytes, -1) | |||||
for _, m := range ms { | |||||
orig := bytes.TrimSpace(m) | |||||
m = orig[2:] | |||||
tailPos := lastIndexOfByte(m, ']') + 1 | |||||
tail := []byte{} | |||||
if tailPos < len(m) { | |||||
tail = m[tailPos:] | |||||
m = m[:tailPos-1] | |||||
} | |||||
m = m[:len(m)-2] | |||||
props := map[string]string{} | |||||
// MediaWiki uses [[link|text]], while GitHub uses [[text|link]] | |||||
// It makes page handling terrible, but we prefer GitHub syntax | |||||
// And fall back to MediaWiki only when it is obvious from the look | |||||
// Of text and link contents | |||||
sl := bytes.Split(m, []byte("|")) | |||||
for _, v := range sl { | |||||
switch bytes.Count(v, []byte("=")) { | |||||
// Piped args without = sign, these are mandatory arguments | |||||
case 0: | |||||
{ | |||||
sv := string(v) | |||||
if props["name"] == "" { | |||||
if isLink(v) { | |||||
// If we clearly see it is a link, we save it so | |||||
// But first we need to ensure, that if both mandatory args provided | |||||
// look like links, we stick to GitHub syntax | |||||
if props["link"] != "" { | |||||
props["name"] = props["link"] | |||||
} | |||||
props["link"] = strings.TrimSpace(sv) | |||||
} else { | |||||
props["name"] = sv | |||||
} | |||||
} else { | |||||
props["link"] = strings.TrimSpace(sv) | |||||
} | |||||
} | |||||
// Piped args with = sign, these are optional arguments | |||||
case 1: | |||||
{ | |||||
sep := firstIndexOfByte(v, '=') | |||||
key, val := string(v[:sep]), html.UnescapeString(string(v[sep+1:])) | |||||
lastCharIndex := len(val) - 1 | |||||
if (val[0] == '"' || val[0] == '\'') && (val[lastCharIndex] == '"' || val[lastCharIndex] == '\'') { | |||||
val = val[1:lastCharIndex] | |||||
} | |||||
props[key] = val | |||||
} | |||||
} | |||||
} | |||||
var name string | |||||
var link string | |||||
if props["link"] != "" { | |||||
link = props["link"] | |||||
} else if props["name"] != "" { | |||||
link = props["name"] | |||||
} | |||||
if props["title"] != "" { | |||||
name = props["title"] | |||||
} else if props["name"] != "" { | |||||
name = props["name"] | |||||
} else { | |||||
name = link | |||||
} | |||||
name += string(tail) | |||||
image := false | |||||
ext := filepath.Ext(string(link)) | |||||
if ext != "" { | |||||
switch ext { | |||||
case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg": | |||||
{ | |||||
image = true | |||||
} | |||||
} | |||||
} | |||||
absoluteLink := isLink([]byte(link)) | |||||
if !absoluteLink { | |||||
link = strings.Replace(link, " ", "+", -1) | |||||
} | |||||
if image { | |||||
if !absoluteLink { | |||||
if IsSameDomain(urlPrefix) { | |||||
urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) | |||||
} | |||||
if isWikiMarkdown { | |||||
link = URLJoin("wiki", "raw", link) | |||||
} | |||||
link = URLJoin(urlPrefix, link) | |||||
} | |||||
title := props["title"] | |||||
if title == "" { | |||||
title = props["alt"] | |||||
} | |||||
if title == "" { | |||||
title = path.Base(string(name)) | |||||
} | |||||
alt := props["alt"] | |||||
if alt == "" { | |||||
alt = name | |||||
} | |||||
if alt != "" { | |||||
alt = `alt="` + alt + `"` | |||||
} | |||||
name = fmt.Sprintf(`<img src="%s" %s title="%s" />`, link, alt, title) | |||||
} else if !absoluteLink { | |||||
if isWikiMarkdown { | |||||
link = URLJoin("wiki", link) | |||||
} | |||||
link = URLJoin(urlPrefix, link) | |||||
} | |||||
if noLink { | |||||
rawBytes = bytes.Replace(rawBytes, orig, []byte(name), -1) | |||||
} else { | |||||
rawBytes = bytes.Replace(rawBytes, orig, | |||||
[]byte(fmt.Sprintf(`<a href="%s">%s</a>`, link, name)), -1) | |||||
} | |||||
} | |||||
return rawBytes | |||||
} | |||||
// RenderCrossReferenceIssueIndexPattern renders issue indexes from other repositories to corresponding links. | |||||
func RenderCrossReferenceIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
ms := CrossReferenceIssueNumericPattern.FindAll(rawBytes, -1) | |||||
for _, m := range ms { | |||||
if m[0] == ' ' || m[0] == '(' { | |||||
m = m[1:] // ignore leading space or opening parentheses | |||||
} | |||||
repo := string(bytes.Split(m, []byte("#"))[0]) | |||||
issue := string(bytes.Split(m, []byte("#"))[1]) | |||||
link := fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(setting.AppURL, repo, "issues", issue), m) | |||||
rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// renderSha1CurrentPattern renders SHA1 strings to corresponding links that assumes in the same repository. | |||||
func renderSha1CurrentPattern(rawBytes []byte, urlPrefix string) []byte { | |||||
ms := Sha1CurrentPattern.FindAllSubmatch(rawBytes, -1) | |||||
for _, m := range ms { | |||||
hash := m[1] | |||||
// The regex does not lie, it matches the hash pattern. | |||||
// However, a regex cannot know if a hash actually exists or not. | |||||
// We could assume that a SHA1 hash should probably contain alphas AND numerics | |||||
// but that is not always the case. | |||||
// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash | |||||
// as used by git and github for linking and thus we have to do similar. | |||||
rawBytes = bytes.Replace(rawBytes, hash, []byte(fmt.Sprintf( | |||||
`<a href="%s">%s</a>`, URLJoin(urlPrefix, "commit", string(hash)), base.ShortSha(string(hash)))), -1) | |||||
} | |||||
return rawBytes | |||||
} | |||||
// RenderSpecialLink renders mentions, indexes and SHA1 strings to corresponding links. | |||||
func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]string, isWikiMarkdown bool) []byte { | |||||
ms := MentionPattern.FindAll(rawBytes, -1) | |||||
for _, m := range ms { | |||||
m = m[bytes.Index(m, []byte("@")):] | |||||
rawBytes = bytes.Replace(rawBytes, m, | |||||
[]byte(fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(setting.AppURL, string(m[1:])), m)), -1) | |||||
} | |||||
rawBytes = RenderFullIssuePattern(rawBytes) | |||||
rawBytes = RenderShortLinks(rawBytes, urlPrefix, false, isWikiMarkdown) | |||||
rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas) | |||||
rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas) | |||||
rawBytes = renderFullSha1Pattern(rawBytes, urlPrefix) | |||||
rawBytes = renderSha1CurrentPattern(rawBytes, urlPrefix) | |||||
return rawBytes | |||||
} | |||||
var ( | |||||
leftAngleBracket = []byte("</") | |||||
rightAngleBracket = []byte(">") | |||||
) | |||||
var noEndTags = []string{"img", "input", "br", "hr"} | |||||
// PostProcess treats different types of HTML differently, | |||||
// and only renders special links for plain text blocks. | |||||
func PostProcess(rawHTML []byte, urlPrefix string, metas map[string]string, isWikiMarkdown bool) []byte { | |||||
startTags := make([]string, 0, 5) | |||||
var buf bytes.Buffer | |||||
tokenizer := html.NewTokenizer(bytes.NewReader(rawHTML)) | |||||
OUTER_LOOP: | |||||
for html.ErrorToken != tokenizer.Next() { | |||||
token := tokenizer.Token() | |||||
switch token.Type { | |||||
case html.TextToken: | |||||
buf.Write(RenderSpecialLink([]byte(token.String()), urlPrefix, metas, isWikiMarkdown)) | |||||
case html.StartTagToken: | |||||
buf.WriteString(token.String()) | |||||
tagName := token.Data | |||||
// If this is an excluded tag, we skip processing all output until a close tag is encountered. | |||||
if strings.EqualFold("a", tagName) || strings.EqualFold("code", tagName) || strings.EqualFold("pre", tagName) { | |||||
stackNum := 1 | |||||
for html.ErrorToken != tokenizer.Next() { | |||||
token = tokenizer.Token() | |||||
// Copy the token to the output verbatim | |||||
buf.Write(RenderShortLinks([]byte(token.String()), urlPrefix, true, isWikiMarkdown)) | |||||
if token.Type == html.StartTagToken && !com.IsSliceContainsStr(noEndTags, token.Data) { | |||||
stackNum++ | |||||
} | |||||
// If this is the close tag to the outer-most, we are done | |||||
if token.Type == html.EndTagToken { | |||||
stackNum-- | |||||
if stackNum <= 0 && strings.EqualFold(tagName, token.Data) { | |||||
break | |||||
} | |||||
} | |||||
} | |||||
continue OUTER_LOOP | |||||
} | |||||
if !com.IsSliceContainsStr(noEndTags, tagName) { | |||||
startTags = append(startTags, tagName) | |||||
} | |||||
case html.EndTagToken: | |||||
if len(startTags) == 0 { | |||||
buf.WriteString(token.String()) | |||||
break | |||||
} | |||||
buf.Write(leftAngleBracket) | |||||
buf.WriteString(startTags[len(startTags)-1]) | |||||
buf.Write(rightAngleBracket) | |||||
startTags = startTags[:len(startTags)-1] | |||||
default: | |||||
buf.WriteString(token.String()) | |||||
} | |||||
} | |||||
if io.EOF == tokenizer.Err() { | |||||
return buf.Bytes() | |||||
} | |||||
// If we are not at the end of the input, then some other parsing error has occurred, | |||||
// so return the input verbatim. | |||||
return rawHTML | |||||
} |
@@ -0,0 +1,460 @@ | |||||
// Copyright 2017 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package markup_test | |||||
import ( | |||||
"fmt" | |||||
"strconv" | |||||
"strings" | |||||
"testing" | |||||
_ "code.gitea.io/gitea/modules/markdown" | |||||
. "code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
const AppURL = "http://localhost:3000/" | |||||
const Repo = "gogits/gogs" | |||||
const AppSubURL = AppURL + Repo + "/" | |||||
var numericMetas = map[string]string{ | |||||
"format": "https://someurl.com/{user}/{repo}/{index}", | |||||
"user": "someUser", | |||||
"repo": "someRepo", | |||||
"style": IssueNameStyleNumeric, | |||||
} | |||||
var alphanumericMetas = map[string]string{ | |||||
"format": "https://someurl.com/{user}/{repo}/{index}", | |||||
"user": "someUser", | |||||
"repo": "someRepo", | |||||
"style": IssueNameStyleAlphanumeric, | |||||
} | |||||
// numericLink an HTML to a numeric-style issue | |||||
func numericIssueLink(baseURL string, index int) string { | |||||
return link(URLJoin(baseURL, strconv.Itoa(index)), fmt.Sprintf("#%d", index)) | |||||
} | |||||
// alphanumLink an HTML link to an alphanumeric-style issue | |||||
func alphanumIssueLink(baseURL string, name string) string { | |||||
return link(URLJoin(baseURL, name), name) | |||||
} | |||||
// urlContentsLink an HTML link whose contents is the target URL | |||||
func urlContentsLink(href string) string { | |||||
return link(href, href) | |||||
} | |||||
// link an HTML link | |||||
func link(href, contents string) string { | |||||
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 TestURLJoin(t *testing.T) { | |||||
type test struct { | |||||
Expected string | |||||
Base string | |||||
Elements []string | |||||
} | |||||
newTest := func(expected, base string, elements ...string) test { | |||||
return test{Expected: expected, Base: base, Elements: elements} | |||||
} | |||||
for _, test := range []test{ | |||||
newTest("https://try.gitea.io/a/b/c", | |||||
"https://try.gitea.io", "a/b", "c"), | |||||
newTest("https://try.gitea.io/a/b/c", | |||||
"https://try.gitea.io/", "/a/b/", "/c/"), | |||||
newTest("https://try.gitea.io/a/c", | |||||
"https://try.gitea.io/", "/a/./b/", "../c/"), | |||||
newTest("a/b/c", | |||||
"a", "b/c/"), | |||||
newTest("a/b/d", | |||||
"a/", "b/c/", "/../d/"), | |||||
} { | |||||
assert.Equal(t, test.Expected, URLJoin(test.Base, test.Elements...)) | |||||
} | |||||
} | |||||
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) | |||||
} | |||||
// should not render anything when there are no mentions | |||||
test("") | |||||
test("this is a test") | |||||
test("test 123 123 1234") | |||||
test("#") | |||||
test("# # #") | |||||
test("# 123") | |||||
test("#abcd") | |||||
test("##1234") | |||||
test("test#1234") | |||||
test("#1234test") | |||||
test(" test #1234test") | |||||
// should not render issue mention without leading space | |||||
test("test#54321 issue") | |||||
// should not render issue mention without trailing space | |||||
test("test #54321issue") | |||||
} | |||||
func TestRender_IssueIndexPattern2(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
// numeric: render inputs with valid mentions | |||||
test := func(s, expectedFmt string, indices ...int) { | |||||
links := make([]interface{}, len(indices)) | |||||
for i, index := range indices { | |||||
links[i] = numericIssueLink(URLJoin(setting.AppSubURL, "issues"), index) | |||||
} | |||||
expectedNil := fmt.Sprintf(expectedFmt, links...) | |||||
testRenderIssueIndexPattern(t, s, expectedNil, nil) | |||||
for i, index := range indices { | |||||
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index) | |||||
} | |||||
expectedNum := fmt.Sprintf(expectedFmt, links...) | |||||
testRenderIssueIndexPattern(t, s, expectedNum, numericMetas) | |||||
} | |||||
// should render freestanding mentions | |||||
test("#1234 test", "%s test", 1234) | |||||
test("test #8 issue", "test %s issue", 8) | |||||
test("test issue #1234", "test issue %s", 1234) | |||||
// should render mentions in parentheses | |||||
test("(#54321 issue)", "(%s issue)", 54321) | |||||
test("test (#9801 extra) issue", "test (%s extra) issue", 9801) | |||||
test("test (#1)", "test (%s)", 1) | |||||
// should render multiple issue mentions in the same line | |||||
test("#54321 #1243", "%s %s", 54321, 1243) | |||||
test("wow (#54321 #1243)", "wow (%s %s)", 54321, 1243) | |||||
test("(#4)(#5)", "(%s)(%s)", 4, 5) | |||||
test("#1 (#4321) test", "%s (%s) test", 1, 4321) | |||||
} | |||||
func TestRender_IssueIndexPattern3(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
// alphanumeric: render inputs without valid mentions | |||||
test := func(s string) { | |||||
testRenderIssueIndexPattern(t, s, s, alphanumericMetas) | |||||
} | |||||
test("") | |||||
test("this is a test") | |||||
test("test 123 123 1234") | |||||
test("#") | |||||
test("##1234") | |||||
test("# 123") | |||||
test("#abcd") | |||||
test("test #123") | |||||
test("abc-1234") // issue prefix must be capital | |||||
test("ABc-1234") // issue prefix must be _all_ capital | |||||
test("ABCDEFGHIJK-1234") // the limit is 10 characters in the prefix | |||||
test("ABC1234") // dash is required | |||||
test("test ABC- test") // number is required | |||||
test("test -1234 test") // prefix is required | |||||
test("testABC-123 test") // leading space is required | |||||
test("test ABC-123test") // trailing space is required | |||||
test("ABC-0123") // no leading zero | |||||
} | |||||
func TestRender_IssueIndexPattern4(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
// alphanumeric: render inputs with valid mentions | |||||
test := func(s, expectedFmt string, names ...string) { | |||||
links := make([]interface{}, len(names)) | |||||
for i, name := range names { | |||||
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", name) | |||||
} | |||||
expected := fmt.Sprintf(expectedFmt, links...) | |||||
testRenderIssueIndexPattern(t, s, expected, 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 TestRender_AutoLink(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
buffer := RenderSpecialLink([]byte(input), setting.AppSubURL, nil, false) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
buffer = RenderSpecialLink([]byte(input), setting.AppSubURL, nil, true) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
} | |||||
// render valid issue URLs | |||||
test(URLJoin(setting.AppSubURL, "issues", "3333"), | |||||
numericIssueLink(URLJoin(setting.AppSubURL, "issues"), 3333)) | |||||
// render external issue URLs | |||||
for _, externalURL := range []string{ | |||||
"http://1111/2222/ssss-issues/3333?param=blah&blahh=333", | |||||
"http://test.com/issues/33333", | |||||
"https://issues/333"} { | |||||
test(externalURL, externalURL) | |||||
} | |||||
// render valid commit URLs | |||||
tmp := URLJoin(AppSubURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae") | |||||
test(tmp, "<a href=\""+tmp+"\">d8a994ef24</a>") | |||||
tmp += "#diff-2" | |||||
test(tmp, "<a href=\""+tmp+"\">d8a994ef24 (diff-2)</a>") | |||||
// render other commit URLs | |||||
tmp = "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" | |||||
test(tmp, "<a href=\""+tmp+"\">d8a994ef24 (diff-2)</a>") | |||||
} | |||||
func TestRender_Commits(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
buffer := RenderString(".md", input, setting.AppSubURL, nil) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
} | |||||
var sha = "b6dd6210eaebc915fd5be5579c58cce4da2e2579" | |||||
var commit = URLJoin(AppSubURL, "commit", sha) | |||||
var subtree = URLJoin(commit, "src") | |||||
var tree = strings.Replace(subtree, "/commit/", "/tree/", -1) | |||||
var src = strings.Replace(subtree, "/commit/", "/src/", -1) | |||||
test(sha, `<p><a href="`+commit+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
test(sha[:7], `<p><a href="`+commit[:len(commit)-(40-7)]+`" rel="nofollow">b6dd621</a></p>`) | |||||
test(sha[:39], `<p><a href="`+commit[:len(commit)-(40-39)]+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
test(commit, `<p><a href="`+commit+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
test(tree, `<p><a href="`+src+`" rel="nofollow">b6dd6210ea/src</a></p>`) | |||||
test("commit "+sha, `<p>commit <a href="`+commit+`" rel="nofollow">b6dd6210ea</a></p>`) | |||||
} | |||||
func TestRender_CrossReferences(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
buffer := RenderString("a.md", input, setting.AppSubURL, nil) | |||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||||
} | |||||
test( | |||||
"gogits/gogs#12345", | |||||
`<p><a href="`+URLJoin(AppURL, "gogits", "gogs", "issues", "12345")+`" rel="nofollow">gogits/gogs#12345</a></p>`) | |||||
} | |||||
func TestRender_FullIssueURLs(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
test := func(input, expected string) { | |||||
result := RenderFullIssuePattern([]byte(input)) | |||||
assert.Equal(t, expected, string(result)) | |||||
} | |||||
test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", | |||||
"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") | |||||
test("Look here http://localhost:3000/person/repo/issues/4", | |||||
`Look here <a href="http://localhost:3000/person/repo/issues/4">#4</a>`) | |||||
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234", | |||||
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234">#4</a>`) | |||||
} | |||||
func TestRegExp_MentionPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"@Unknwon", | |||||
"@ANT_123", | |||||
"@xxx-DiN0-z-A..uru..s-xxx", | |||||
" @lol ", | |||||
" @Te/st", | |||||
} | |||||
falseTestCases := []string{ | |||||
"@ 0", | |||||
"@ ", | |||||
"@", | |||||
"", | |||||
"ABC", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
res := MentionPattern.MatchString(testCase) | |||||
if !res { | |||||
println() | |||||
println(testCase) | |||||
} | |||||
assert.True(t, res) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
res := MentionPattern.MatchString(testCase) | |||||
if res { | |||||
println() | |||||
println(testCase) | |||||
} | |||||
assert.False(t, res) | |||||
} | |||||
} | |||||
func TestRegExp_IssueNumericPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"#1234", | |||||
"#0", | |||||
"#1234567890987654321", | |||||
} | |||||
falseTestCases := []string{ | |||||
"# 1234", | |||||
"# 0", | |||||
"# ", | |||||
"#", | |||||
"#ABC", | |||||
"#1A2B", | |||||
"", | |||||
"ABC", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, IssueNumericPattern.MatchString(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, IssueNumericPattern.MatchString(testCase)) | |||||
} | |||||
} | |||||
func TestRegExp_IssueAlphanumericPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"ABC-1234", | |||||
"A-1", | |||||
"RC-80", | |||||
"ABCDEFGHIJ-1234567890987654321234567890", | |||||
} | |||||
falseTestCases := []string{ | |||||
"RC-08", | |||||
"PR-0", | |||||
"ABCDEFGHIJK-1", | |||||
"PR_1", | |||||
"", | |||||
"#ABC", | |||||
"", | |||||
"ABC", | |||||
"GG-", | |||||
"rm-1", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, IssueAlphanumericPattern.MatchString(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, IssueAlphanumericPattern.MatchString(testCase)) | |||||
} | |||||
} | |||||
func TestRegExp_Sha1CurrentPattern(t *testing.T) { | |||||
trueTestCases := []string{ | |||||
"d8a994ef243349f321568f9e36d5c3f444b99cae", | |||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcd", | |||||
} | |||||
falseTestCases := []string{ | |||||
"test", | |||||
"abcdefg", | |||||
"abcdefghijklmnopqrstuvwxyzabcdefghijklmn", | |||||
"abcdefghijklmnopqrstuvwxyzabcdefghijklmO", | |||||
} | |||||
for _, testCase := range trueTestCases { | |||||
assert.True(t, Sha1CurrentPattern.MatchString(testCase)) | |||||
} | |||||
for _, testCase := range falseTestCases { | |||||
assert.False(t, Sha1CurrentPattern.MatchString(testCase)) | |||||
} | |||||
} | |||||
func TestRegExp_AnySHA1Pattern(t *testing.T) { | |||||
testCases := map[string][]string{ | |||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"blob", | |||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||||
"test/unit/event.js", | |||||
"L2703", | |||||
}, | |||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"blob", | |||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||||
"test/unit/event.js", | |||||
"", | |||||
}, | |||||
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"commit", | |||||
"0705be475092aede1eddae01319ec931fb9c65fc", | |||||
"", | |||||
"", | |||||
}, | |||||
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": { | |||||
"https", | |||||
"github.com", | |||||
"jquery", | |||||
"jquery", | |||||
"tree", | |||||
"0705be475092aede1eddae01319ec931fb9c65fc", | |||||
"src", | |||||
"", | |||||
}, | |||||
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": { | |||||
"https", | |||||
"try.gogs.io", | |||||
"gogs", | |||||
"gogs", | |||||
"commit", | |||||
"d8a994ef243349f321568f9e36d5c3f444b99cae", | |||||
"", | |||||
"diff-2", | |||||
}, | |||||
} | |||||
for k, v := range testCases { | |||||
assert.Equal(t, AnySHA1Pattern.FindStringSubmatch(k)[1:], v) | |||||
} | |||||
} | |||||
func TestMisc_IsSameDomain(t *testing.T) { | |||||
setting.AppURL = AppURL | |||||
setting.AppSubURL = AppSubURL | |||||
var sha = "b6dd6210eaebc915fd5be5579c58cce4da2e2579" | |||||
var commit = URLJoin(AppSubURL, "commit", sha) | |||||
assert.True(t, IsSameDomain(commit)) | |||||
assert.False(t, IsSameDomain("http://google.com/ncr")) | |||||
assert.False(t, IsSameDomain("favicon.ico")) | |||||
} |
@@ -9,6 +9,12 @@ import ( | |||||
"strings" | "strings" | ||||
) | ) | ||||
// Init initialize regexps for markdown parsing | |||||
func Init() { | |||||
getIssueFullPattern() | |||||
NewSanitizer() | |||||
} | |||||
// Parser defines an interface for parsering markup file to HTML | // Parser defines an interface for parsering markup file to HTML | ||||
type Parser interface { | type Parser interface { | ||||
Name() string // markup format name | Name() string // markup format name | ||||
@@ -17,66 +23,94 @@ type Parser interface { | |||||
} | } | ||||
var ( | var ( | ||||
parsers = make(map[string]Parser) | |||||
extParsers = make(map[string]Parser) | |||||
parsers = make(map[string]Parser) | |||||
) | ) | ||||
// RegisterParser registers a new markup file parser | // RegisterParser registers a new markup file parser | ||||
func RegisterParser(parser Parser) { | func RegisterParser(parser Parser) { | ||||
parsers[parser.Name()] = parser | |||||
for _, ext := range parser.Extensions() { | for _, ext := range parser.Extensions() { | ||||
parsers[strings.ToLower(ext)] = parser | |||||
extParsers[strings.ToLower(ext)] = parser | |||||
} | } | ||||
} | } | ||||
// GetParserByFileName get parser by filename | |||||
func GetParserByFileName(filename string) Parser { | |||||
extension := strings.ToLower(filepath.Ext(filename)) | |||||
return extParsers[extension] | |||||
} | |||||
// GetParserByType returns a parser according type | |||||
func GetParserByType(tp string) Parser { | |||||
return parsers[tp] | |||||
} | |||||
// Render renders markup file to HTML with all specific handling stuff. | // Render renders markup file to HTML with all specific handling stuff. | ||||
func Render(filename string, rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | func Render(filename string, rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | ||||
return render(filename, rawBytes, urlPrefix, metas, false) | |||||
return renderFile(filename, rawBytes, urlPrefix, metas, false) | |||||
} | } | ||||
func render(filename string, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | |||||
extension := strings.ToLower(filepath.Ext(filename)) | |||||
if parser, ok := parsers[extension]; ok { | |||||
return parser.Render(rawBytes, urlPrefix, metas, isWiki) | |||||
} | |||||
return nil | |||||
// RenderByType renders markup to HTML with special links and returns string type. | |||||
func RenderByType(tp string, rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | |||||
return renderByType(tp, rawBytes, urlPrefix, metas, false) | |||||
} | } | ||||
// RenderString renders Markdown to HTML with special links and returns string type. | // RenderString renders Markdown to HTML with special links and returns string type. | ||||
func RenderString(filename string, raw, urlPrefix string, metas map[string]string) string { | func RenderString(filename string, raw, urlPrefix string, metas map[string]string) string { | ||||
return string(render(filename, []byte(raw), urlPrefix, metas, false)) | |||||
return string(renderFile(filename, []byte(raw), urlPrefix, metas, false)) | |||||
} | } | ||||
// RenderWiki renders markdown wiki page to HTML and return HTML string | // RenderWiki renders markdown wiki page to HTML and return HTML string | ||||
func RenderWiki(filename string, rawBytes []byte, urlPrefix string, metas map[string]string) string { | func RenderWiki(filename string, rawBytes []byte, urlPrefix string, metas map[string]string) string { | ||||
return string(render(filename, rawBytes, urlPrefix, metas, true)) | |||||
return string(renderFile(filename, rawBytes, urlPrefix, metas, true)) | |||||
} | |||||
func render(parser Parser, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | |||||
urlPrefix = strings.Replace(urlPrefix, " ", "+", -1) | |||||
result := parser.Render(rawBytes, urlPrefix, metas, isWiki) | |||||
result = PostProcess(result, urlPrefix, metas, isWiki) | |||||
return SanitizeBytes(result) | |||||
} | |||||
func renderByType(tp string, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | |||||
if parser, ok := parsers[tp]; ok { | |||||
return render(parser, rawBytes, urlPrefix, metas, isWiki) | |||||
} | |||||
return nil | |||||
} | |||||
func renderFile(filename string, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | |||||
extension := strings.ToLower(filepath.Ext(filename)) | |||||
if parser, ok := extParsers[extension]; ok { | |||||
return render(parser, rawBytes, urlPrefix, metas, isWiki) | |||||
} | |||||
return nil | |||||
} | } | ||||
// Type returns if markup format via the filename | // Type returns if markup format via the filename | ||||
func Type(filename string) string { | func Type(filename string) string { | ||||
extension := strings.ToLower(filepath.Ext(filename)) | |||||
if parser, ok := parsers[extension]; ok { | |||||
if parser := GetParserByFileName(filename); parser != nil { | |||||
return parser.Name() | return parser.Name() | ||||
} | } | ||||
return "" | return "" | ||||
} | } | ||||
// ReadmeFileType reports whether name looks like a README file | |||||
// based on its name and find the parser via its ext name | |||||
func ReadmeFileType(name string) (string, bool) { | |||||
if IsReadmeFile(name) { | |||||
return Type(name), true | |||||
// IsMarkupFile reports whether file is a markup type file | |||||
func IsMarkupFile(name, markup string) bool { | |||||
if parser := GetParserByFileName(name); parser != nil { | |||||
return parser.Name() == markup | |||||
} | } | ||||
return "", false | |||||
return false | |||||
} | } | ||||
// IsReadmeFile reports whether name looks like a README file | // IsReadmeFile reports whether name looks like a README file | ||||
// based on its name. | // based on its name. | ||||
func IsReadmeFile(name string) bool { | func IsReadmeFile(name string) bool { | ||||
name = strings.ToLower(name) | |||||
if len(name) < 6 { | if len(name) < 6 { | ||||
return false | return false | ||||
} | |||||
name = strings.ToLower(name) | |||||
if len(name) == 6 { | |||||
} else if len(name) == 6 { | |||||
return name == "readme" | return name == "readme" | ||||
} | } | ||||
return name[:7] == "readme." | return name[:7] == "readme." | ||||
@@ -2,11 +2,14 @@ | |||||
// Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
package markup | |||||
package markup_test | |||||
import ( | import ( | ||||
"testing" | "testing" | ||||
_ "code.gitea.io/gitea/modules/markdown" | |||||
. "code.gitea.io/gitea/modules/markup" | |||||
"github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
) | ) | ||||
@@ -3,7 +3,7 @@ | |||||
// Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
package markdown | |||||
package markup | |||||
import ( | import ( | ||||
"regexp" | "regexp" |
@@ -3,7 +3,7 @@ | |||||
// Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
package markdown | |||||
package markup | |||||
import ( | import ( | ||||
"testing" | "testing" |
@@ -24,7 +24,7 @@ import ( | |||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
) | ) | ||||
@@ -173,7 +173,7 @@ func SafeJS(raw string) template.JS { | |||||
// Str2html render Markdown text to HTML | // Str2html render Markdown text to HTML | ||||
func Str2html(raw string) template.HTML { | func Str2html(raw string) template.HTML { | ||||
return template.HTML(markdown.Sanitize(raw)) | |||||
return template.HTML(markup.Sanitize(raw)) | |||||
} | } | ||||
// List traversings the list | // List traversings the list | ||||
@@ -253,7 +253,7 @@ func ReplaceLeft(s, old, new string) string { | |||||
// RenderCommitMessage renders commit message with XSS-safe and special links. | // 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(full bool, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
cleanMsg := template.HTMLEscapeString(msg) | cleanMsg := template.HTMLEscapeString(msg) | ||||
fullMessage := string(markdown.RenderIssueIndexPattern([]byte(cleanMsg), urlPrefix, metas)) | |||||
fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), urlPrefix, metas)) | |||||
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") | msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") | ||||
numLines := len(msgLines) | numLines := len(msgLines) | ||||
if numLines == 0 { | if numLines == 0 { | ||||
@@ -9,6 +9,7 @@ import ( | |||||
"code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
"code.gitea.io/gitea/modules/markdown" | "code.gitea.io/gitea/modules/markdown" | ||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
) | ) | ||||
@@ -39,7 +40,7 @@ func Markdown(ctx *context.APIContext, form api.MarkdownOption) { | |||||
switch form.Mode { | switch form.Mode { | ||||
case "gfm": | case "gfm": | ||||
md := []byte(form.Text) | md := []byte(form.Text) | ||||
context := markdown.URLJoin(setting.AppURL, form.Context) | |||||
context := markup.URLJoin(setting.AppURL, form.Context) | |||||
if form.Wiki { | if form.Wiki { | ||||
ctx.Write([]byte(markdown.RenderWiki(md, context, nil))) | ctx.Write([]byte(markdown.RenderWiki(md, context, nil))) | ||||
} else { | } else { | ||||
@@ -1,23 +1,21 @@ | |||||
package misc | package misc | ||||
import ( | import ( | ||||
"io/ioutil" | |||||
"net/http" | "net/http" | ||||
"net/http/httptest" | "net/http/httptest" | ||||
"testing" | |||||
macaron "gopkg.in/macaron.v1" | |||||
"net/url" | "net/url" | ||||
"io/ioutil" | |||||
"strings" | "strings" | ||||
"testing" | |||||
"code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
api "code.gitea.io/sdk/gitea" | api "code.gitea.io/sdk/gitea" | ||||
"github.com/go-macaron/inject" | "github.com/go-macaron/inject" | ||||
"github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
macaron "gopkg.in/macaron.v1" | |||||
) | ) | ||||
const AppURL = "http://localhost:3000/" | const AppURL = "http://localhost:3000/" | ||||
@@ -55,7 +53,7 @@ func TestAPI_RenderGFM(t *testing.T) { | |||||
Context: Repo, | Context: Repo, | ||||
Wiki: true, | Wiki: true, | ||||
} | } | ||||
requrl, _ := url.Parse(markdown.URLJoin(AppURL, "api", "v1", "markdown")) | |||||
requrl, _ := url.Parse(markup.URLJoin(AppURL, "api", "v1", "markdown")) | |||||
req := &http.Request{ | req := &http.Request{ | ||||
Method: "POST", | Method: "POST", | ||||
URL: requrl, | URL: requrl, | ||||
@@ -149,7 +147,7 @@ func TestAPI_RenderSimple(t *testing.T) { | |||||
Text: "", | Text: "", | ||||
Context: Repo, | Context: Repo, | ||||
} | } | ||||
requrl, _ := url.Parse(markdown.URLJoin(AppURL, "api", "v1", "markdown")) | |||||
requrl, _ := url.Parse(markup.URLJoin(AppURL, "api", "v1", "markdown")) | |||||
req := &http.Request{ | req := &http.Request{ | ||||
Method: "POST", | Method: "POST", | ||||
URL: requrl, | URL: requrl, | ||||
@@ -168,7 +166,7 @@ func TestAPI_RenderSimple(t *testing.T) { | |||||
func TestAPI_RenderRaw(t *testing.T) { | func TestAPI_RenderRaw(t *testing.T) { | ||||
setting.AppURL = AppURL | setting.AppURL = AppURL | ||||
requrl, _ := url.Parse(markdown.URLJoin(AppURL, "api", "v1", "markdown")) | |||||
requrl, _ := url.Parse(markup.URLJoin(AppURL, "api", "v1", "markdown")) | |||||
req := &http.Request{ | req := &http.Request{ | ||||
Method: "POST", | Method: "POST", | ||||
URL: requrl, | URL: requrl, | ||||
@@ -16,7 +16,7 @@ import ( | |||||
"code.gitea.io/gitea/modules/indexer" | "code.gitea.io/gitea/modules/indexer" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/mailer" | "code.gitea.io/gitea/modules/mailer" | ||||
"code.gitea.io/gitea/modules/markdown" | |||||
"code.gitea.io/gitea/modules/markup" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"code.gitea.io/gitea/modules/ssh" | "code.gitea.io/gitea/modules/ssh" | ||||
macaron "gopkg.in/macaron.v1" | macaron "gopkg.in/macaron.v1" | ||||
@@ -50,8 +50,8 @@ func GlobalInit() { | |||||
if setting.InstallLock { | if setting.InstallLock { | ||||
highlight.NewContext() | highlight.NewContext() | ||||
markdown.InitMarkdown() | |||||
markdown.NewSanitizer() | |||||
markup.Init() | |||||
if err := models.NewEngine(migrations.Migrate); err != nil { | if err := models.NewEngine(migrations.Migrate); err != nil { | ||||
log.Fatal(4, "Failed to initialize ORM engine: %v", err) | log.Fatal(4, "Failed to initialize ORM engine: %v", err) | ||||
} | } | ||||
@@ -61,13 +61,12 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||
continue | continue | ||||
} | } | ||||
tp, ok := markup.ReadmeFileType(entry.Name()) | |||||
if !ok { | |||||
if !markup.IsReadmeFile(entry.Name()) { | |||||
continue | continue | ||||
} | } | ||||
readmeFile = entry.Blob() | readmeFile = entry.Blob() | ||||
if tp != "" { | |||||
if markup.Type(entry.Name()) != "" { | |||||
break | break | ||||
} | } | ||||
} | } | ||||