]> source.dussan.org Git - gitea.git/commitdiff
Restructure markup & markdown to prepare for multiple markup languageā€¦ (#2411)
authorLunny Xiao <xiaolunwen@gmail.com>
Sat, 16 Sep 2017 17:17:57 +0000 (01:17 +0800)
committerLauris BH <lauris@nix.lv>
Sat, 16 Sep 2017 17:17:57 +0000 (20:17 +0300)
* restructure markup & markdown to prepare for multiple markup languages support

* adjust some functions between markdown and markup

* fix tests

* improve the comments

21 files changed:
models/issue_comment.go
models/issue_mail.go
models/mail.go
models/migrations/v16.go
models/repo.go
models/repo_test.go
modules/markdown/markdown.go
modules/markdown/markdown_test.go
modules/markdown/sanitizer.go [deleted file]
modules/markdown/sanitizer_test.go [deleted file]
modules/markup/html.go [new file with mode: 0644]
modules/markup/html_test.go [new file with mode: 0644]
modules/markup/markup.go
modules/markup/markup_test.go
modules/markup/sanitizer.go [new file with mode: 0644]
modules/markup/sanitizer_test.go [new file with mode: 0644]
modules/templates/helper.go
routers/api/v1/misc/markdown.go
routers/api/v1/misc/markdown_test.go
routers/init.go
routers/repo/view.go

index 6c9c75b1e42c96f65cd49b82088243fabe42b02c..675143437a9f6f1276ba15c542e1acd87a142653 100644 (file)
@@ -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)
        }
index 74ef6605548627c2475da7b76dd89386c565a24c..e4a1a40e6dae94db78601a6af5ad061ae46d0026 100644 (file)
@@ -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)
        }
index 41fddad2b49a9e6a91652fd4e75acee2c61a55b8..b7d24a06a173c9be1566ea1459959e3d89ea8839 100644 (file)
@@ -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 {
index 2a6d71de41cdcfdf124d3134c82ee0d43a3ccf33..ef342a5f882b264489e0d047fdfd9ee2052a2491 100644 (file)
@@ -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:
index 8d5b3b87c717d0e36efe8eddf75a252d05de1191..4b3b0322d6ac1b5b2b3468dcf176f5e603d9cbbb 100644 (file)
@@ -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
index c1eb4e2a79f7db8d3fb94603401851e28a863b4a..34eaa16c0b7a7bffd2ab22a0f035e781182b6cd1 100644 (file)
@@ -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) {
index ed673f2056c2cc8b1f02aa0762e067ae7220bf4c..6cf2d9eaa1675296caf4d5f328385769bee70219 100644 (file)
@@ -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)
 }
index 4506a29b1eb361faac0f9303aad44e4f0efaef29..1b57e4f203fba07df402c55713d4786b48d6dc2b 100644 (file)
@@ -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(
                "!["+title+"]("+url+")",
@@ -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)
+       }
+}
diff --git a/modules/markdown/sanitizer.go b/modules/markdown/sanitizer.go
deleted file mode 100644 (file)
index cc00c9a..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// Copyright 2017 The Gogs 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 markdown
-
-import (
-       "regexp"
-       "sync"
-
-       "code.gitea.io/gitea/modules/setting"
-
-       "github.com/microcosm-cc/bluemonday"
-)
-
-// Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow
-// any modification to the underlying policies once it's been created.
-type Sanitizer struct {
-       policy *bluemonday.Policy
-       init   sync.Once
-}
-
-var sanitizer = &Sanitizer{}
-
-// NewSanitizer initializes sanitizer with allowed attributes based on settings.
-// Multiple calls to this function will only create one instance of Sanitizer during
-// entire application lifecycle.
-func NewSanitizer() {
-       sanitizer.init.Do(func() {
-               sanitizer.policy = bluemonday.UGCPolicy()
-               // We only want to allow HighlightJS specific classes for code blocks
-               sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^language-\w+$`)).OnElements("code")
-
-               // Checkboxes
-               sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
-               sanitizer.policy.AllowAttrs("checked", "disabled").OnElements("input")
-
-               // Custom URL-Schemes
-               sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
-       })
-}
-
-// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
-func Sanitize(s string) string {
-       NewSanitizer()
-       return sanitizer.policy.Sanitize(s)
-}
-
-// SanitizeBytes takes a []byte slice that contains a HTML fragment or document and applies policy whitelist.
-func SanitizeBytes(b []byte) []byte {
-       if len(b) == 0 {
-               // nothing to sanitize
-               return b
-       }
-       NewSanitizer()
-       return sanitizer.policy.SanitizeBytes(b)
-}
diff --git a/modules/markdown/sanitizer_test.go b/modules/markdown/sanitizer_test.go
deleted file mode 100644 (file)
index 77a4b33..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// Copyright 2017 The Gogs 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 markdown
-
-import (
-       "testing"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func Test_Sanitizer(t *testing.T) {
-       NewSanitizer()
-       testCases := []string{
-               // Regular
-               `<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
-
-               // Code highlighting class
-               `<code class="random string"></code>`, `<code></code>`,
-               `<code class="language-random ui tab active menu attached animating sidebar following bar center"></code>`, `<code></code>`,
-               `<code class="language-go"></code>`, `<code class="language-go"></code>`,
-
-               // Input checkbox
-               `<input type="hidden">`, ``,
-               `<input type="checkbox">`, `<input type="checkbox">`,
-               `<input checked disabled autofocus>`, `<input checked="" disabled="">`,
-
-               // Code highlight injection
-               `<code class="language-random&#32;ui&#32;tab&#32;active&#32;menu&#32;attached&#32;animating&#32;sidebar&#32;following&#32;bar&#32;center"></code>`, `<code></code>`,
-               `<code class="language-lol&#32;ui&#32;tab&#32;active&#32;menu&#32;attached&#32;animating&#32;sidebar&#32;following&#32;bar&#32;center">
-<code class="language-lol&#32;ui&#32;container&#32;input&#32;huge&#32;basic&#32;segment&#32;center">&nbsp;</code>
-<img src="https://try.gogs.io/img/favicon.png" width="200" height="200">
-<code class="language-lol&#32;ui&#32;container&#32;input&#32;massive&#32;basic&#32;segment">Hello there! Something has gone wrong, we are working on it.</code>
-<code class="language-lol&#32;ui&#32;container&#32;input&#32;huge&#32;basic&#32;segment">In the meantime, play a game with us at&nbsp;<a href="http://example.com/">example.com</a>.</code>
-</code>`, "<code>\n<code>\u00a0</code>\n<img src=\"https://try.gogs.io/img/favicon.png\" width=\"200\" height=\"200\">\n<code>Hello there! Something has gone wrong, we are working on it.</code>\n<code>In the meantime, play a game with us at\u00a0<a href=\"http://example.com/\" rel=\"nofollow\">example.com</a>.</code>\n</code>",
-       }
-
-       for i := 0; i < len(testCases); i += 2 {
-               assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
-               assert.Equal(t, testCases[i+1], string(SanitizeBytes([]byte(testCases[i]))))
-       }
-}
diff --git a/modules/markup/html.go b/modules/markup/html.go
new file mode 100644 (file)
index 0000000..9daf0b0
--- /dev/null
@@ -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
+}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
new file mode 100644 (file)
index 0000000..4071155
--- /dev/null
@@ -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"))
+}
index 185cdc727418780033c053aef9265cc1d2a45186..ba28ec53c08ff52cfa4e1d2b32a85b0db0a2b136 100644 (file)
@@ -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."
index 0dd2be3564c157e0d88ae264913378c123fba1e0..8d061ae39e9a4d997746c35c7b5c39589a649cd3 100644 (file)
@@ -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"
 )
 
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
new file mode 100644 (file)
index 0000000..2ec43cf
--- /dev/null
@@ -0,0 +1,58 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2017 The Gogs 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 (
+       "regexp"
+       "sync"
+
+       "code.gitea.io/gitea/modules/setting"
+
+       "github.com/microcosm-cc/bluemonday"
+)
+
+// Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow
+// any modification to the underlying policies once it's been created.
+type Sanitizer struct {
+       policy *bluemonday.Policy
+       init   sync.Once
+}
+
+var sanitizer = &Sanitizer{}
+
+// NewSanitizer initializes sanitizer with allowed attributes based on settings.
+// Multiple calls to this function will only create one instance of Sanitizer during
+// entire application lifecycle.
+func NewSanitizer() {
+       sanitizer.init.Do(func() {
+               sanitizer.policy = bluemonday.UGCPolicy()
+               // We only want to allow HighlightJS specific classes for code blocks
+               sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^language-\w+$`)).OnElements("code")
+
+               // Checkboxes
+               sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
+               sanitizer.policy.AllowAttrs("checked", "disabled").OnElements("input")
+
+               // Custom URL-Schemes
+               sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
+       })
+}
+
+// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
+func Sanitize(s string) string {
+       NewSanitizer()
+       return sanitizer.policy.Sanitize(s)
+}
+
+// SanitizeBytes takes a []byte slice that contains a HTML fragment or document and applies policy whitelist.
+func SanitizeBytes(b []byte) []byte {
+       if len(b) == 0 {
+               // nothing to sanitize
+               return b
+       }
+       NewSanitizer()
+       return sanitizer.policy.SanitizeBytes(b)
+}
diff --git a/modules/markup/sanitizer_test.go b/modules/markup/sanitizer_test.go
new file mode 100644 (file)
index 0000000..211201d
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2017 The Gogs 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 (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func Test_Sanitizer(t *testing.T) {
+       NewSanitizer()
+       testCases := []string{
+               // Regular
+               `<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
+
+               // Code highlighting class
+               `<code class="random string"></code>`, `<code></code>`,
+               `<code class="language-random ui tab active menu attached animating sidebar following bar center"></code>`, `<code></code>`,
+               `<code class="language-go"></code>`, `<code class="language-go"></code>`,
+
+               // Input checkbox
+               `<input type="hidden">`, ``,
+               `<input type="checkbox">`, `<input type="checkbox">`,
+               `<input checked disabled autofocus>`, `<input checked="" disabled="">`,
+
+               // Code highlight injection
+               `<code class="language-random&#32;ui&#32;tab&#32;active&#32;menu&#32;attached&#32;animating&#32;sidebar&#32;following&#32;bar&#32;center"></code>`, `<code></code>`,
+               `<code class="language-lol&#32;ui&#32;tab&#32;active&#32;menu&#32;attached&#32;animating&#32;sidebar&#32;following&#32;bar&#32;center">
+<code class="language-lol&#32;ui&#32;container&#32;input&#32;huge&#32;basic&#32;segment&#32;center">&nbsp;</code>
+<img src="https://try.gogs.io/img/favicon.png" width="200" height="200">
+<code class="language-lol&#32;ui&#32;container&#32;input&#32;massive&#32;basic&#32;segment">Hello there! Something has gone wrong, we are working on it.</code>
+<code class="language-lol&#32;ui&#32;container&#32;input&#32;huge&#32;basic&#32;segment">In the meantime, play a game with us at&nbsp;<a href="http://example.com/">example.com</a>.</code>
+</code>`, "<code>\n<code>\u00a0</code>\n<img src=\"https://try.gogs.io/img/favicon.png\" width=\"200\" height=\"200\">\n<code>Hello there! Something has gone wrong, we are working on it.</code>\n<code>In the meantime, play a game with us at\u00a0<a href=\"http://example.com/\" rel=\"nofollow\">example.com</a>.</code>\n</code>",
+       }
+
+       for i := 0; i < len(testCases); i += 2 {
+               assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
+               assert.Equal(t, testCases[i+1], string(SanitizeBytes([]byte(testCases[i]))))
+       }
+}
index 5ac0f6ee54e46397c3de905149aa4234fe8c1450..181d3ff159ffc14a82eb98cd48335b3e28a63d41 100644 (file)
@@ -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 {
index 933488301b6ab97d35910e953b1c4ec8e26cb103..a2e65ecb0ad99d6ee05f92a8cf00402ac8b56675 100644 (file)
@@ -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 {
index d6e619347802416beef5e4932506c1e9cf68fbf0..8c3051ea8ee402801ad4af79e18a478eff6ac904 100644 (file)
@@ -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,
index bd46abdf94e8321893363485e878d35d17b23934..5113ec4cef8f127524f9eedf70559ef21a9e036e 100644 (file)
@@ -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)
                }
index 3a9e0e1d3b0f1a563b5847ccd8d297ade996975d..991d2c0400608c844f1403fcbdc2338c22f4548c 100644 (file)
@@ -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
                }
        }