* restructure markup & markdown to prepare for multiple markup languages support * adjust some functions between markdown and markup * fix tests * improve the commentstags/v1.21.12.1
| @@ -16,7 +16,7 @@ import ( | |||
| api "code.gitea.io/sdk/gitea" | |||
| "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. | |||
| @@ -272,7 +272,7 @@ func (c *Comment) LoadAssignees() error { | |||
| // MailParticipants sends new comment emails to repository watchers | |||
| // and mentioned people. | |||
| 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 { | |||
| return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err) | |||
| } | |||
| @@ -10,7 +10,7 @@ import ( | |||
| "github.com/Unknwon/com" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| @@ -104,7 +104,7 @@ func (issue *Issue) MailParticipants() (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 { | |||
| 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/mailer" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "gopkg.in/gomail.v2" | |||
| "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 { | |||
| 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) | |||
| if comment != nil { | |||
| @@ -8,7 +8,7 @@ import ( | |||
| "fmt" | |||
| "time" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "github.com/go-xorm/xorm" | |||
| ) | |||
| @@ -101,7 +101,7 @@ func addUnitsToTables(x *xorm.Engine) error { | |||
| config["ExternalTrackerURL"] = repo.ExternalTrackerURL | |||
| config["ExternalTrackerFormat"] = repo.ExternalTrackerFormat | |||
| if len(repo.ExternalTrackerStyle) == 0 { | |||
| repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric | |||
| repo.ExternalTrackerStyle = markup.IssueNameStyleNumeric | |||
| } | |||
| config["ExternalTrackerStyle"] = repo.ExternalTrackerStyle | |||
| case V16UnitTypeExternalWiki: | |||
| @@ -22,7 +22,7 @@ import ( | |||
| "code.gitea.io/git" | |||
| "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/process" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| @@ -480,10 +480,10 @@ func (repo *Repository) ComposeMetas() map[string]string { | |||
| "repo": repo.Name, | |||
| } | |||
| switch unit.ExternalTrackerConfig().ExternalTrackerStyle { | |||
| case markdown.IssueNameStyleAlphanumeric: | |||
| repo.ExternalMetas["style"] = markdown.IssueNameStyleAlphanumeric | |||
| case markup.IssueNameStyleAlphanumeric: | |||
| repo.ExternalMetas["style"] = markup.IssueNameStyleAlphanumeric | |||
| 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 { | |||
| 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 | |||
| @@ -8,7 +8,7 @@ import ( | |||
| "path" | |||
| "testing" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "github.com/Unknwon/com" | |||
| @@ -39,13 +39,13 @@ func TestRepo(t *testing.T) { | |||
| 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) { | |||
| @@ -6,107 +6,14 @@ package markdown | |||
| 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/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "github.com/Unknwon/com" | |||
| "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. | |||
| type Renderer struct { | |||
| blackfriday.Renderer | |||
| @@ -116,13 +23,13 @@ type Renderer struct { | |||
| // 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) { | |||
| if len(link) > 0 && !isLink(link) { | |||
| if len(link) > 0 && !markup.IsLink(link) { | |||
| if link[0] != '#' { | |||
| lnk := string(link) | |||
| 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) | |||
| } | |||
| } | |||
| @@ -190,11 +97,11 @@ var ( | |||
| func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { | |||
| prefix := r.urlPrefix | |||
| if r.isWikiMarkdown { | |||
| prefix = URLJoin(prefix, "wiki", "src") | |||
| prefix = markup.URLJoin(prefix, "wiki", "src") | |||
| } | |||
| prefix = strings.Replace(prefix, "/src/", "/raw/", 1) | |||
| if len(link) > 0 { | |||
| if isLink(link) { | |||
| if markup.IsLink(link) { | |||
| // External link with .svg suffix usually means CI status. | |||
| // TODO: define a keyword to allow non-svg images render as external link. | |||
| 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 { | |||
| lnk := string(link) | |||
| lnk = URLJoin(prefix, lnk) | |||
| lnk = markup.URLJoin(prefix, lnk) | |||
| lnk = strings.Replace(lnk, " ", "+", -1) | |||
| link = []byte(lnk) | |||
| } | |||
| @@ -216,351 +123,6 @@ func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byt | |||
| 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. | |||
| func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | |||
| htmlFlags := 0 | |||
| @@ -588,107 +150,6 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | |||
| return body | |||
| } | |||
| 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 = "markdown" | |||
| @@ -714,5 +175,26 @@ func (Parser) Extensions() []string { | |||
| // Render implements markup.Parser | |||
| 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 ( | |||
| "fmt" | |||
| "strconv" | |||
| "testing" | |||
| "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" | |||
| ) | |||
| @@ -24,24 +25,24 @@ var numericMetas = map[string]string{ | |||
| "format": "https://someurl.com/{user}/{repo}/{index}", | |||
| "user": "someUser", | |||
| "repo": "someRepo", | |||
| "style": IssueNameStyleNumeric, | |||
| "style": markup.IssueNameStyleNumeric, | |||
| } | |||
| var alphanumericMetas = map[string]string{ | |||
| "format": "https://someurl.com/{user}/{repo}/{index}", | |||
| "user": "someUser", | |||
| "repo": "someRepo", | |||
| "style": IssueNameStyleAlphanumeric, | |||
| "style": markup.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)) | |||
| return link(markup.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) | |||
| return link(markup.URLJoin(baseURL, name), name) | |||
| } | |||
| // 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) { | |||
| 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) { | |||
| @@ -241,8 +74,8 @@ func TestRender_StandardLinks(t *testing.T) { | |||
| googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` | |||
| 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)", | |||
| `<p><a href="`+lnk+`" 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) { | |||
| setting.AppURL = AppURL | |||
| setting.AppSubURL = AppSubURL | |||
| tree := URLJoin(AppSubURL, "src", "master") | |||
| tree := markup.URLJoin(AppSubURL, "src", "master") | |||
| test := func(input, expected, expectedWiki string) { | |||
| 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))) | |||
| } | |||
| 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" | |||
| 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>`) | |||
| } | |||
| 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) { | |||
| @@ -345,7 +177,7 @@ func TestRender_Images(t *testing.T) { | |||
| url := "../../.images/src/02/train.jpg" | |||
| title := "Train" | |||
| result := URLJoin(AppSubURL, url) | |||
| result := markup.URLJoin(AppSubURL, url) | |||
| 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>`) | |||
| } | |||
| 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) { | |||
| trueTestCases := []string{ | |||
| "[[stuff]]", | |||
| @@ -510,139 +205,13 @@ func TestRegExp_ShortLinkPattern(t *testing.T) { | |||
| } | |||
| for _, testCase := range trueTestCases { | |||
| assert.True(t, ShortLinkPattern.MatchString(testCase)) | |||
| assert.True(t, markup.ShortLinkPattern.MatchString(testCase)) | |||
| } | |||
| 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 { | |||
| return []string{ | |||
| `<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) { | |||
| 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++ { | |||
| line := RenderWiki([]byte(sameCases[i]), AppSubURL, nil) | |||
| @@ -739,3 +325,19 @@ func TestTotal_RenderWiki(t *testing.T) { | |||
| 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" | |||
| ) | |||
| // Init initialize regexps for markdown parsing | |||
| func Init() { | |||
| getIssueFullPattern() | |||
| NewSanitizer() | |||
| } | |||
| // Parser defines an interface for parsering markup file to HTML | |||
| type Parser interface { | |||
| Name() string // markup format name | |||
| @@ -17,66 +23,94 @@ type Parser interface { | |||
| } | |||
| var ( | |||
| parsers = make(map[string]Parser) | |||
| extParsers = make(map[string]Parser) | |||
| parsers = make(map[string]Parser) | |||
| ) | |||
| // RegisterParser registers a new markup file parser | |||
| func RegisterParser(parser Parser) { | |||
| parsers[parser.Name()] = parser | |||
| 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. | |||
| 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. | |||
| 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 | |||
| 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 | |||
| 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 "" | |||
| } | |||
| // 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 | |||
| // based on its name. | |||
| func IsReadmeFile(name string) bool { | |||
| name = strings.ToLower(name) | |||
| if len(name) < 6 { | |||
| return false | |||
| } | |||
| name = strings.ToLower(name) | |||
| if len(name) == 6 { | |||
| } else if len(name) == 6 { | |||
| return name == "readme" | |||
| } | |||
| return name[:7] == "readme." | |||
| @@ -2,11 +2,14 @@ | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package markup | |||
| package markup_test | |||
| import ( | |||
| "testing" | |||
| _ "code.gitea.io/gitea/modules/markdown" | |||
| . "code.gitea.io/gitea/modules/markup" | |||
| "github.com/stretchr/testify/assert" | |||
| ) | |||
| @@ -3,7 +3,7 @@ | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package markdown | |||
| package markup | |||
| import ( | |||
| "regexp" | |||
| @@ -3,7 +3,7 @@ | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package markdown | |||
| package markup | |||
| import ( | |||
| "testing" | |||
| @@ -24,7 +24,7 @@ import ( | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| @@ -173,7 +173,7 @@ func SafeJS(raw string) template.JS { | |||
| // Str2html render Markdown text to HTML | |||
| func Str2html(raw string) template.HTML { | |||
| return template.HTML(markdown.Sanitize(raw)) | |||
| return template.HTML(markup.Sanitize(raw)) | |||
| } | |||
| // 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. | |||
| func RenderCommitMessage(full bool, msg, urlPrefix string, metas map[string]string) template.HTML { | |||
| 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") | |||
| numLines := len(msgLines) | |||
| if numLines == 0 { | |||
| @@ -9,6 +9,7 @@ import ( | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| @@ -39,7 +40,7 @@ func Markdown(ctx *context.APIContext, form api.MarkdownOption) { | |||
| switch form.Mode { | |||
| case "gfm": | |||
| md := []byte(form.Text) | |||
| context := markdown.URLJoin(setting.AppURL, form.Context) | |||
| context := markup.URLJoin(setting.AppURL, form.Context) | |||
| if form.Wiki { | |||
| ctx.Write([]byte(markdown.RenderWiki(md, context, nil))) | |||
| } else { | |||
| @@ -1,23 +1,21 @@ | |||
| package misc | |||
| import ( | |||
| "io/ioutil" | |||
| "net/http" | |||
| "net/http/httptest" | |||
| "testing" | |||
| macaron "gopkg.in/macaron.v1" | |||
| "net/url" | |||
| "io/ioutil" | |||
| "strings" | |||
| "testing" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/markdown" | |||
| "code.gitea.io/gitea/modules/markup" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| api "code.gitea.io/sdk/gitea" | |||
| "github.com/go-macaron/inject" | |||
| "github.com/stretchr/testify/assert" | |||
| macaron "gopkg.in/macaron.v1" | |||
| ) | |||
| const AppURL = "http://localhost:3000/" | |||
| @@ -55,7 +53,7 @@ func TestAPI_RenderGFM(t *testing.T) { | |||
| Context: Repo, | |||
| Wiki: true, | |||
| } | |||
| requrl, _ := url.Parse(markdown.URLJoin(AppURL, "api", "v1", "markdown")) | |||
| requrl, _ := url.Parse(markup.URLJoin(AppURL, "api", "v1", "markdown")) | |||
| req := &http.Request{ | |||
| Method: "POST", | |||
| URL: requrl, | |||
| @@ -149,7 +147,7 @@ func TestAPI_RenderSimple(t *testing.T) { | |||
| Text: "", | |||
| Context: Repo, | |||
| } | |||
| requrl, _ := url.Parse(markdown.URLJoin(AppURL, "api", "v1", "markdown")) | |||
| requrl, _ := url.Parse(markup.URLJoin(AppURL, "api", "v1", "markdown")) | |||
| req := &http.Request{ | |||
| Method: "POST", | |||
| URL: requrl, | |||
| @@ -168,7 +166,7 @@ func TestAPI_RenderSimple(t *testing.T) { | |||
| func TestAPI_RenderRaw(t *testing.T) { | |||
| setting.AppURL = AppURL | |||
| requrl, _ := url.Parse(markdown.URLJoin(AppURL, "api", "v1", "markdown")) | |||
| requrl, _ := url.Parse(markup.URLJoin(AppURL, "api", "v1", "markdown")) | |||
| req := &http.Request{ | |||
| Method: "POST", | |||
| URL: requrl, | |||
| @@ -16,7 +16,7 @@ import ( | |||
| "code.gitea.io/gitea/modules/indexer" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "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/ssh" | |||
| macaron "gopkg.in/macaron.v1" | |||
| @@ -50,8 +50,8 @@ func GlobalInit() { | |||
| if setting.InstallLock { | |||
| highlight.NewContext() | |||
| markdown.InitMarkdown() | |||
| markdown.NewSanitizer() | |||
| markup.Init() | |||
| if err := models.NewEngine(migrations.Migrate); err != nil { | |||
| log.Fatal(4, "Failed to initialize ORM engine: %v", err) | |||
| } | |||
| @@ -61,13 +61,12 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||
| continue | |||
| } | |||
| tp, ok := markup.ReadmeFileType(entry.Name()) | |||
| if !ok { | |||
| if !markup.IsReadmeFile(entry.Name()) { | |||
| continue | |||
| } | |||
| readmeFile = entry.Blob() | |||
| if tp != "" { | |||
| if markup.Type(entry.Name()) != "" { | |||
| break | |||
| } | |||
| } | |||