From c1c269d9ef50595475cf4c6728d9b20a6417c490 Mon Sep 17 00:00:00 2001 From: Unknwon Date: Fri, 31 Mar 2017 15:29:43 -0400 Subject: modules: rename markdown -> markup To further support more markup languages (e.g. Org-mode, AsciiDoc, reStructuredText), the name 'markdown' is inappropriate. This is the first step towards more markup language support. --- models/comment.go | 4 +- models/issue_mail.go | 4 +- models/repo.go | 10 +- models/repo_test.go | 2 +- modules/mailer/mail.go | 4 +- modules/markdown/markdown.go | 490 -------------------------------------- modules/markdown/markdown_test.go | 308 ------------------------ modules/markup/markdown.go | 490 ++++++++++++++++++++++++++++++++++++++ modules/markup/markdown_test.go | 308 ++++++++++++++++++++++++ modules/template/template.go | 6 +- routers/api/v1/misc/markdown.go | 8 +- routers/install.go | 4 +- routers/repo/issue.go | 12 +- routers/repo/release.go | 6 +- routers/repo/view.go | 14 +- routers/repo/wiki.go | 4 +- 16 files changed, 837 insertions(+), 837 deletions(-) delete mode 100644 modules/markdown/markdown.go delete mode 100644 modules/markdown/markdown_test.go create mode 100644 modules/markup/markdown.go create mode 100644 modules/markup/markdown_test.go diff --git a/models/comment.go b/models/comment.go index 2b5ac923..7a46f6f8 100644 --- a/models/comment.go +++ b/models/comment.go @@ -16,7 +16,7 @@ import ( api "github.com/gogits/go-gogs-client" "github.com/gogits/gogs/models/errors" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" ) // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. @@ -168,7 +168,7 @@ func (c *Comment) EventTag() string { // mailParticipants sends new comment emails to repository watchers // and mentioned people. func (cmt *Comment) mailParticipants(e Engine, opType ActionType, issue *Issue) (err error) { - mentions := markdown.FindAllMentions(cmt.Content) + mentions := markup.FindAllMentions(cmt.Content) if err = updateIssueMentions(e, cmt.IssueID, mentions); err != nil { return fmt.Errorf("UpdateIssueMentions [%d]: %v", cmt.IssueID, err) } diff --git a/models/issue_mail.go b/models/issue_mail.go index be653b89..d0447261 100644 --- a/models/issue_mail.go +++ b/models/issue_mail.go @@ -11,7 +11,7 @@ import ( log "gopkg.in/clog.v1" "github.com/gogits/gogs/modules/mailer" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" ) @@ -162,7 +162,7 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) // MailParticipants sends new issue thread created emails to repository watchers // and mentioned people. func (issue *Issue) MailParticipants() (err error) { - mentions := markdown.FindAllMentions(issue.Content) + mentions := markup.FindAllMentions(issue.Content) if err = updateIssueMentions(x, issue.ID, mentions); err != nil { return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) } diff --git a/models/repo.go b/models/repo.go index a597f82e..b12ed58e 100644 --- a/models/repo.go +++ b/models/repo.go @@ -28,7 +28,7 @@ import ( "github.com/gogits/gogs/models/errors" "github.com/gogits/gogs/modules/bindata" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/sync" @@ -219,7 +219,7 @@ func (repo *Repository) AfterSet(colName string, _ xorm.Cell) { repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones case "external_tracker_style": if len(repo.ExternalTrackerStyle) == 0 { - repo.ExternalTrackerStyle = markdown.ISSUE_NAME_STYLE_NUMERIC + repo.ExternalTrackerStyle = markup.ISSUE_NAME_STYLE_NUMERIC } case "created_unix": repo.Created = time.Unix(repo.CreatedUnix, 0).Local() @@ -356,10 +356,10 @@ func (repo *Repository) ComposeMetas() map[string]string { "repo": repo.Name, } switch repo.ExternalTrackerStyle { - case markdown.ISSUE_NAME_STYLE_ALPHANUMERIC: - repo.ExternalMetas["style"] = markdown.ISSUE_NAME_STYLE_ALPHANUMERIC + case markup.ISSUE_NAME_STYLE_ALPHANUMERIC: + repo.ExternalMetas["style"] = markup.ISSUE_NAME_STYLE_ALPHANUMERIC default: - repo.ExternalMetas["style"] = markdown.ISSUE_NAME_STYLE_NUMERIC + repo.ExternalMetas["style"] = markup.ISSUE_NAME_STYLE_NUMERIC } } diff --git a/models/repo_test.go b/models/repo_test.go index 69d90b93..653a0962 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -5,7 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" "testing" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" ) func TestRepo(t *testing.T) { diff --git a/modules/mailer/mail.go b/modules/mailer/mail.go index b65073a1..95543294 100644 --- a/modules/mailer/mail.go +++ b/modules/mailer/mail.go @@ -13,7 +13,7 @@ import ( "gopkg.in/macaron.v1" "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" ) @@ -174,7 +174,7 @@ func composeTplData(subject, body, link string) map[string]interface{} { func composeIssueMessage(issue Issue, repo Repository, doer User, tplName base.TplName, tos []string, info string) *Message { subject := issue.MailSubject() - body := string(markdown.RenderSpecialLink([]byte(issue.Content()), repo.HTMLURL(), repo.ComposeMetas())) + body := string(markup.RenderSpecialLink([]byte(issue.Content()), repo.HTMLURL(), repo.ComposeMetas())) data := composeTplData(subject, body, issue.HTMLURL()) data["Doer"] = doer content, err := mailRender.HTMLString(string(tplName), data) diff --git a/modules/markdown/markdown.go b/modules/markdown/markdown.go deleted file mode 100644 index 6101670c..00000000 --- a/modules/markdown/markdown.go +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright 2014 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 ( - "bytes" - "fmt" - "io" - "path" - "path/filepath" - "regexp" - "strings" - - "github.com/Unknwon/com" - "github.com/microcosm-cc/bluemonday" - "github.com/russross/blackfriday" - "golang.org/x/net/html" - - "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/setting" -) - -const ( - ISSUE_NAME_STYLE_NUMERIC = "numeric" - ISSUE_NAME_STYLE_ALPHANUMERIC = "alphanumeric" -) - -var Sanitizer = bluemonday.UGCPolicy() - -// BuildSanitizer initializes sanitizer with allowed attributes based on settings. -// This function should only be called once during entire application lifecycle. -func BuildSanitizer() { - // We only want to allow HighlightJS specific classes for code blocks - Sanitizer.AllowAttrs("class").Matching(regexp.MustCompile(`^language-\w+`)).OnElements("code") - - // Checkboxes - Sanitizer.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") - Sanitizer.AllowAttrs("checked", "disabled").OnElements("input") - - // Custom URL-Schemes - Sanitizer.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) -} - -var validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://|^mailto:`) - -// isLink reports whether link fits valid format. -func isLink(link []byte) bool { - return validLinksPattern.Match(link) -} - -// 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 -} - -// IsReadmeFile reports whether name looks like a README file -// based on its extension. -func IsReadmeFile(name string) bool { - name = strings.ToLower(name) - if len(name) < 6 { - return false - } else if len(name) == 6 { - return name == "readme" - } - return name[:7] == "readme." -} - -var ( - // MentionPattern matches string that mentions someone, e.g. @Unknwon - MentionPattern = regexp.MustCompile(`(\s|^|\W)@[0-9a-zA-Z-_\.]+`) - - // CommitPattern matches link to certain commit with or without trailing hash, - // e.g. https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2 - CommitPattern = regexp.MustCompile(`(\s|^)https?.*commit/[0-9a-zA-Z]+(#+[0-9a-zA-Z-]*)?`) - - // IssueFullPattern matches link to an issue with or without trailing hash, - // e.g. https://try.gogs.io/gogs/gogs/issues/4#issue-685 - IssueFullPattern = regexp.MustCompile(`(\s|^)https?.*issues/[0-9]+(#+[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 difference 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 - // FIXME: this pattern matches pure numbers as well, right now we do a hack to check in RenderSha1CurrentPattern - // by converting string to a number. - Sha1CurrentPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`) -) - -// 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 - urlPrefix string -} - -// 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 link[0] != '#' { - link = []byte(path.Join(r.urlPrefix, string(link))) - } - } - - r.Renderer.Link(out, link, title, content) -} - -// AutoLink defines how auto-detected links should be processed to produce corresponding HTML elements. -// Reference for kind: https://github.com/russross/blackfriday/blob/master/markdown.go#L69-L76 -func (r *Renderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { - if kind != blackfriday.LINK_TYPE_NORMAL { - r.Renderer.AutoLink(out, link, kind) - return - } - - // Since this method could only possibly serve one link at a time, - // we do not need to find all. - if bytes.HasPrefix(link, []byte(setting.AppUrl)) { - m := CommitPattern.Find(link) - if m != nil { - m = bytes.TrimSpace(m) - i := strings.Index(string(m), "commit/") - j := strings.Index(string(m), "#") - if j == -1 { - j = len(m) - } - out.WriteString(fmt.Sprintf(` %s`, m, base.ShortSha(string(m[i+7:j])))) - return - } - - m = IssueFullPattern.Find(link) - if m != nil { - m = bytes.TrimSpace(m) - i := strings.Index(string(m), "issues/") - j := strings.Index(string(m), "#") - if j == -1 { - j = len(m) - } - - index := string(m[i+7 : j]) - fullRepoURL := setting.AppUrl + strings.TrimPrefix(r.urlPrefix, "/") - var link string - if strings.HasPrefix(string(m), fullRepoURL) { - // Use a short issue reference if the URL refers to this repository - link = fmt.Sprintf(`#%s`, m, index) - } else { - // Use a cross-repository issue reference if the URL refers to a different repository - repo := string(m[len(setting.AppUrl) : i-1]) - link = fmt.Sprintf(`%s#%s`, m, repo, index) - } - out.WriteString(link) - return - } - } - - r.Renderer.AutoLink(out, link, kind) -} - -// ListItem defines how list items should be processed to produce corresponding HTML elements. -func (options *Renderer) ListItem(out *bytes.Buffer, text []byte, flags int) { - // Detect procedures to draw checkboxes. - switch { - case bytes.HasPrefix(text, []byte("[ ] ")): - text = append([]byte(``), text[3:]...) - case bytes.HasPrefix(text, []byte("[x] ")): - text = append([]byte(``), text[3:]...) - } - options.Renderer.ListItem(out, text, flags) -} - -// Note: this section is for purpose of increase performance and -// reduce memory allocation at runtime since they are constant literals. -var ( - pound = []byte("#") - space = " " - spaceEncoded = "%20" -) - -// 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 -} - -// 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"] == ISSUE_NAME_STYLE_ALPHANUMERIC { - 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(`%s`, urlPrefix, m[1:], m) - } else { - // Support for external issue tracker - if metas["style"] == ISSUE_NAME_STYLE_ALPHANUMERIC { - metas["index"] = string(m) - } else { - metas["index"] = string(m[1:]) - } - link = fmt.Sprintf(`%s`, com.Expand(metas["format"], metas), m) - } - rawBytes = bytes.Replace(rawBytes, m, []byte(link), 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 - } - - delimIdx := bytes.Index(m, pound) - repo := string(m[:delimIdx]) - index := string(m[delimIdx+1:]) - - link := fmt.Sprintf(`%s`, setting.AppUrl, repo, index, 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 { - return []byte(Sha1CurrentPattern.ReplaceAllStringFunc(string(rawBytes[:]), func(m string) string { - if com.StrTo(m).MustInt() > 0 { - return m - } - return fmt.Sprintf(`%s`, urlPrefix, m, base.ShortSha(string(m))) - })) -} - -// RenderSpecialLink renders mentions, indexes and SHA1 strings to corresponding links. -func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { - ms := MentionPattern.FindAll(rawBytes, -1) - for _, m := range ms { - m = m[bytes.Index(m, []byte("@")):] - rawBytes = bytes.Replace(rawBytes, m, - []byte(fmt.Sprintf(`%s`, setting.AppSubUrl, m[1:], m)), -1) - } - - rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas) - rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas) - rawBytes = RenderSha1CurrentPattern(rawBytes, urlPrefix) - return rawBytes -} - -// RenderRaw renders Markdown to HTML without handling special links. -func RenderRaw(body []byte, urlPrefix string) []byte { - htmlFlags := 0 - htmlFlags |= blackfriday.HTML_SKIP_STYLE - htmlFlags |= blackfriday.HTML_OMIT_CONTENTS - - if setting.Smartypants.Enabled { - htmlFlags |= blackfriday.HTML_USE_SMARTYPANTS - if setting.Smartypants.Fractions { - htmlFlags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS - } - if setting.Smartypants.Dashes { - htmlFlags |= blackfriday.HTML_SMARTYPANTS_DASHES - } - if setting.Smartypants.LatexDashes { - htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES - } - if setting.Smartypants.AngledQuotes { - htmlFlags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES - } - } - - renderer := &Renderer{ - Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), - urlPrefix: urlPrefix, - } - - // set up the parser - extensions := 0 - extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS - extensions |= blackfriday.EXTENSION_TABLES - extensions |= blackfriday.EXTENSION_FENCED_CODE - extensions |= blackfriday.EXTENSION_AUTOLINK - extensions |= blackfriday.EXTENSION_STRIKETHROUGH - extensions |= blackfriday.EXTENSION_SPACE_HEADERS - extensions |= blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK - - if setting.Markdown.EnableHardLineBreak { - extensions |= blackfriday.EXTENSION_HARD_LINE_BREAK - } - - body = blackfriday.Markdown(body, renderer, extensions) - return body -} - -var ( - leftAngleBracket = []byte("") -) - -var noEndTags = []string{"input", "br", "hr", "img"} - -// wrapImgWithLink warps link to standalone tags. -func wrapImgWithLink(urlPrefix string, buf *bytes.Buffer, token html.Token) { - var src, alt string - // Extract "src" and "alt" attributes - for i := range token.Attr { - switch token.Attr[i].Key { - case "src": - src = token.Attr[i].Val - case "alt": - alt = token.Attr[i].Val - } - } - - // Skip in case the "src" is empty - if len(src) == 0 { - buf.WriteString(token.String()) - return - } - - // Prepend repository base URL for internal links - needPrepend := !isLink([]byte(src)) - if needPrepend { - urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) - if src[0] != '/' { - urlPrefix += "/" - } - } - - buf.WriteString(``) - - if needPrepend { - src = strings.Replace(urlPrefix+string(src), " ", "%20", -1) - buf.WriteString(` 0 { - buf.WriteString(` alt="`) - buf.WriteString(alt) - buf.WriteString(`"`) - } - - buf.WriteString(`>`) - - } else { - buf.WriteString(token.String()) - } - - buf.WriteString(``) -} - -// 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) []byte { - startTags := make([]string, 0, 5) - buf := bytes.NewBuffer(nil) - 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)) - - case html.StartTagToken: - tagName := token.Data - - if tagName == "img" { - wrapImgWithLink(urlPrefix, buf, token) - continue OUTER_LOOP - } - - buf.WriteString(token.String()) - // 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.WriteString(token.String()) - - // Stack number doesn't increate for tags without end tags. - 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 special links. -func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { - urlPrefix = strings.Replace(urlPrefix, space, spaceEncoded, -1) - result := RenderRaw(rawBytes, urlPrefix) - result = PostProcess(result, urlPrefix, metas) - result = Sanitizer.SanitizeBytes(result) - return result -} - -// 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)) -} diff --git a/modules/markdown/markdown_test.go b/modules/markdown/markdown_test.go deleted file mode 100644 index d4071ba3..00000000 --- a/modules/markdown/markdown_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package markdown_test - -import ( - . "github.com/gogits/gogs/modules/markdown" - . "github.com/smartystreets/goconvey/convey" - "testing" - - "bytes" - "github.com/gogits/gogs/modules/setting" - "github.com/russross/blackfriday" -) - -func TestMarkdown(t *testing.T) { - Convey("Rendering an issue mention", t, func() { - var ( - urlPrefix = "/prefix" - metas map[string]string = nil - ) - setting.AppSubUrlDepth = 0 - - Convey("To the internal issue tracker", func() { - Convey("It should not render anything when there are no mentions", func() { - testCases := []string{ - "", - "this is a test", - "test 123 123 1234", - "#", - "# # #", - "# 123", - "#abcd", - "##1234", - "test#1234", - "#1234test", - " test #1234test", - } - - for i := 0; i < len(testCases); i++ { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i]) - } - }) - Convey("It should render freestanding mentions", func() { - testCases := []string{ - "#1234 test", "#1234 test", - "test #1234 issue", "test #1234 issue", - "test issue #1234", "test issue #1234", - "#5 test", "#5 test", - "test #5 issue", "test #5 issue", - "test issue #5", "test issue #5", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - Convey("It should not render issue mention without leading space", func() { - input := []byte("test#54321 issue") - expected := "test#54321 issue" - So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) - }) - Convey("It should not render issue mention without trailing space", func() { - input := []byte("test #54321issue") - expected := "test #54321issue" - So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) - }) - Convey("It should render issue mention in parentheses", func() { - testCases := []string{ - "(#54321 issue)", "(#54321 issue)", - "test (#54321) issue", "test (#54321) issue", - "test (#54321 extra) issue", "test (#54321 extra) issue", - "test (#54321 issue)", "test (#54321 issue)", - "test (#54321)", "test (#54321)", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - Convey("It should render multiple issue mentions in the same line", func() { - testCases := []string{ - "#54321 #1243", "#54321 #1243", - "test #54321 #1243", "test #54321 #1243", - "(#54321 #1243)", "(#54321 #1243)", - "(#54321)(#1243)", "(#54321)(#1243)", - "text #54321 test #1243 issue", "text #54321 test #1243 issue", - "#1 (#4321) test", "#1 (#4321) test", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - }) - Convey("To an external issue tracker with numeric style", func() { - metas = make(map[string]string) - metas["format"] = "https://someurl.com/{user}/{repo}/{index}" - metas["user"] = "someuser" - metas["repo"] = "somerepo" - metas["style"] = ISSUE_NAME_STYLE_NUMERIC - - Convey("should not render anything when there are no mentions", func() { - testCases := []string{ - "this is a test", - "test 123 123 1234", - "#", - "# # #", - "# 123", - "#abcd", - } - - for i := 0; i < len(testCases); i++ { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i]) - } - }) - Convey("It should render freestanding issue mentions", func() { - testCases := []string{ - "#1234 test", "#1234 test", - "test #1234 issue", "test #1234 issue", - "test issue #1234", "test issue #1234", - "#5 test", "#5 test", - "test #5 issue", "test #5 issue", - "test issue #5", "test issue #5", - } - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - Convey("It should not render issue mention without leading space", func() { - input := []byte("test#54321 issue") - expected := "test#54321 issue" - So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) - }) - Convey("It should not render issue mention without trailing space", func() { - input := []byte("test #54321issue") - expected := "test #54321issue" - So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) - }) - Convey("It should render issue mention in parentheses", func() { - testCases := []string{ - "(#54321 issue)", "(#54321 issue)", - "test (#54321) issue", "test (#54321) issue", - "test (#54321 extra) issue", "test (#54321 extra) issue", - "test (#54321 issue)", "test (#54321 issue)", - "test (#54321)", "test (#54321)", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - Convey("It should render multiple issue mentions in the same line", func() { - testCases := []string{ - "#54321 #1243", "#54321 #1243", - "test #54321 #1243", "test #54321 #1243", - "(#54321 #1243)", "(#54321 #1243)", - "(#54321)(#1243)", "(#54321)(#1243)", - "text #54321 test #1243 issue", "text #54321 test #1243 issue", - "#1 (#4321) test", "#1 (#4321) test", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - }) - Convey("To an external issue tracker with alphanumeric style", func() { - metas = make(map[string]string) - metas["format"] = "https://someurl.com/{user}/{repo}/?b={index}" - metas["user"] = "someuser" - metas["repo"] = "somerepo" - metas["style"] = ISSUE_NAME_STYLE_ALPHANUMERIC - Convey("It should not render anything when there are no mentions", func() { - testCases := []string{ - "", - "this is a test", - "test 123 123 1234", - "#", - "##1234", - "# 123", - "#abcd", - "test #123", - "abc-1234", // issue prefix must be capital - "ABc-1234", // issue prefix must be _all_ capital - "ABCDEFGHIJK-1234", // the limit is 10 characters in the prefix - "ABC1234", // dash is required - "test ABC- test", // number is required - "test -1234 test", // prefix is required - "testABC-123 test", // leading space is required - "test ABC-123test", // trailing space is required - "ABC-0123", // no leading zero - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i]) - } - }) - Convey("It should render freestanding issue mention", func() { - testCases := []string{ - "OTT-1234 test", "OTT-1234 test", - "test T-12 issue", "test T-12 issue", - "test issue ABCDEFGHIJ-1234567890", "test issue ABCDEFGHIJ-1234567890", - "A-1 test", "A-1 test", - "test ZED-1 issue", "test ZED-1 issue", - "test issue DEED-7154", "test issue DEED-7154", - } - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - Convey("It should render issue mention in parentheses", func() { - testCases := []string{ - "(ABG-124 issue)", "(ABG-124 issue)", - "test (ABG-124) issue", "test (ABG-124) issue", - "test (ABG-124 extra) issue", "test (ABG-124 extra) issue", - "test (ABG-124 issue)", "test (ABG-124 issue)", - "test (ABG-124)", "test (ABG-124)", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - Convey("It should render multiple issue mentions in the same line", func() { - testCases := []string{ - "ABG-124 OTT-4321", "ABG-124 OTT-4321", - "test ABG-124 OTT-4321", "test ABG-124 OTT-4321", - "(ABG-124 OTT-4321)", "(ABG-124 OTT-4321)", - "(ABG-124)(OTT-4321)", "(ABG-124)(OTT-4321)", - "text ABG-124 test OTT-4321 issue", "text ABG-124 test OTT-4321 issue", - "A-1 (RRE-345) test", "A-1 (RRE-345) test", - } - - for i := 0; i < len(testCases); i += 2 { - So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) - } - }) - }) - }) - - Convey("Rendering an issue URL", t, func() { - setting.AppUrl = "http://localhost:3000/" - htmlFlags := 0 - htmlFlags |= blackfriday.HTML_SKIP_STYLE - htmlFlags |= blackfriday.HTML_OMIT_CONTENTS - renderer := &Renderer{ - Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), - } - buffer := new(bytes.Buffer) - Convey("To the internal issue tracker", func() { - Convey("It should render valid issue URLs", func() { - testCases := []string{ - "http://localhost:3000/user/repo/issues/3333", "#3333", - } - - for i := 0; i < len(testCases); i += 2 { - renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL) - - line, _ := buffer.ReadString(0) - So(line, ShouldEqual, testCases[i+1]) - } - }) - Convey("It should render but not change non-issue URLs", func() { - testCases := []string{ - "http://1111/2222/ssss-issues/3333?param=blah&blahh=333", "http://1111/2222/ssss-issues/3333?param=blah&blahh=333", - "http://test.com/issues/33333", "http://test.com/issues/33333", - "http://test.com/issues/3", "http://test.com/issues/3", - "http://issues/333", "http://issues/333", - "https://issues/333", "https://issues/333", - "http://tissues/0", "http://tissues/0", - } - - for i := 0; i < len(testCases); i += 2 { - renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL) - - line, _ := buffer.ReadString(0) - So(line, ShouldEqual, testCases[i+1]) - } - }) - }) - }) - - Convey("Rendering a commit URL", t, func() { - setting.AppUrl = "http://localhost:3000/" - htmlFlags := 0 - htmlFlags |= blackfriday.HTML_SKIP_STYLE - htmlFlags |= blackfriday.HTML_OMIT_CONTENTS - renderer := &Renderer{ - Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), - } - buffer := new(bytes.Buffer) - Convey("To the internal issue tracker", func() { - Convey("It should correctly convert URLs", func() { - testCases := []string{ - "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae", " d8a994ef24", - "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", " d8a994ef24", - "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", - "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae", "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae", - } - - for i := 0; i < len(testCases); i += 2 { - renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL) - - line, _ := buffer.ReadString(0) - So(line, ShouldEqual, testCases[i+1]) - } - }) - }) - }) -} diff --git a/modules/markup/markdown.go b/modules/markup/markdown.go new file mode 100644 index 00000000..fa91553a --- /dev/null +++ b/modules/markup/markdown.go @@ -0,0 +1,490 @@ +// Copyright 2014 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 ( + "bytes" + "fmt" + "io" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" + "golang.org/x/net/html" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/setting" +) + +const ( + ISSUE_NAME_STYLE_NUMERIC = "numeric" + ISSUE_NAME_STYLE_ALPHANUMERIC = "alphanumeric" +) + +var Sanitizer = bluemonday.UGCPolicy() + +// BuildSanitizer initializes sanitizer with allowed attributes based on settings. +// This function should only be called once during entire application lifecycle. +func BuildSanitizer() { + // We only want to allow HighlightJS specific classes for code blocks + Sanitizer.AllowAttrs("class").Matching(regexp.MustCompile(`^language-\w+`)).OnElements("code") + + // Checkboxes + Sanitizer.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") + Sanitizer.AllowAttrs("checked", "disabled").OnElements("input") + + // Custom URL-Schemes + Sanitizer.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) +} + +var validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://|^mailto:`) + +// isLink reports whether link fits valid format. +func isLink(link []byte) bool { + return validLinksPattern.Match(link) +} + +// 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 +} + +// IsReadmeFile reports whether name looks like a README file +// based on its extension. +func IsReadmeFile(name string) bool { + name = strings.ToLower(name) + if len(name) < 6 { + return false + } else if len(name) == 6 { + return name == "readme" + } + return name[:7] == "readme." +} + +var ( + // MentionPattern matches string that mentions someone, e.g. @Unknwon + MentionPattern = regexp.MustCompile(`(\s|^|\W)@[0-9a-zA-Z-_\.]+`) + + // CommitPattern matches link to certain commit with or without trailing hash, + // e.g. https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2 + CommitPattern = regexp.MustCompile(`(\s|^)https?.*commit/[0-9a-zA-Z]+(#+[0-9a-zA-Z-]*)?`) + + // IssueFullPattern matches link to an issue with or without trailing hash, + // e.g. https://try.gogs.io/gogs/gogs/issues/4#issue-685 + IssueFullPattern = regexp.MustCompile(`(\s|^)https?.*issues/[0-9]+(#+[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 difference 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 + // FIXME: this pattern matches pure numbers as well, right now we do a hack to check in RenderSha1CurrentPattern + // by converting string to a number. + Sha1CurrentPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`) +) + +// 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 + urlPrefix string +} + +// 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 link[0] != '#' { + link = []byte(path.Join(r.urlPrefix, string(link))) + } + } + + r.Renderer.Link(out, link, title, content) +} + +// AutoLink defines how auto-detected links should be processed to produce corresponding HTML elements. +// Reference for kind: https://github.com/russross/blackfriday/blob/master/markdown.go#L69-L76 +func (r *Renderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { + if kind != blackfriday.LINK_TYPE_NORMAL { + r.Renderer.AutoLink(out, link, kind) + return + } + + // Since this method could only possibly serve one link at a time, + // we do not need to find all. + if bytes.HasPrefix(link, []byte(setting.AppUrl)) { + m := CommitPattern.Find(link) + if m != nil { + m = bytes.TrimSpace(m) + i := strings.Index(string(m), "commit/") + j := strings.Index(string(m), "#") + if j == -1 { + j = len(m) + } + out.WriteString(fmt.Sprintf(` %s`, m, base.ShortSha(string(m[i+7:j])))) + return + } + + m = IssueFullPattern.Find(link) + if m != nil { + m = bytes.TrimSpace(m) + i := strings.Index(string(m), "issues/") + j := strings.Index(string(m), "#") + if j == -1 { + j = len(m) + } + + index := string(m[i+7 : j]) + fullRepoURL := setting.AppUrl + strings.TrimPrefix(r.urlPrefix, "/") + var link string + if strings.HasPrefix(string(m), fullRepoURL) { + // Use a short issue reference if the URL refers to this repository + link = fmt.Sprintf(`#%s`, m, index) + } else { + // Use a cross-repository issue reference if the URL refers to a different repository + repo := string(m[len(setting.AppUrl) : i-1]) + link = fmt.Sprintf(`%s#%s`, m, repo, index) + } + out.WriteString(link) + return + } + } + + r.Renderer.AutoLink(out, link, kind) +} + +// ListItem defines how list items should be processed to produce corresponding HTML elements. +func (options *Renderer) ListItem(out *bytes.Buffer, text []byte, flags int) { + // Detect procedures to draw checkboxes. + switch { + case bytes.HasPrefix(text, []byte("[ ] ")): + text = append([]byte(``), text[3:]...) + case bytes.HasPrefix(text, []byte("[x] ")): + text = append([]byte(``), text[3:]...) + } + options.Renderer.ListItem(out, text, flags) +} + +// Note: this section is for purpose of increase performance and +// reduce memory allocation at runtime since they are constant literals. +var ( + pound = []byte("#") + space = " " + spaceEncoded = "%20" +) + +// 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 +} + +// 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"] == ISSUE_NAME_STYLE_ALPHANUMERIC { + 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(`%s`, urlPrefix, m[1:], m) + } else { + // Support for external issue tracker + if metas["style"] == ISSUE_NAME_STYLE_ALPHANUMERIC { + metas["index"] = string(m) + } else { + metas["index"] = string(m[1:]) + } + link = fmt.Sprintf(`%s`, com.Expand(metas["format"], metas), m) + } + rawBytes = bytes.Replace(rawBytes, m, []byte(link), 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 + } + + delimIdx := bytes.Index(m, pound) + repo := string(m[:delimIdx]) + index := string(m[delimIdx+1:]) + + link := fmt.Sprintf(`%s`, setting.AppUrl, repo, index, 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 { + return []byte(Sha1CurrentPattern.ReplaceAllStringFunc(string(rawBytes[:]), func(m string) string { + if com.StrTo(m).MustInt() > 0 { + return m + } + return fmt.Sprintf(`%s`, urlPrefix, m, base.ShortSha(string(m))) + })) +} + +// RenderSpecialLink renders mentions, indexes and SHA1 strings to corresponding links. +func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { + ms := MentionPattern.FindAll(rawBytes, -1) + for _, m := range ms { + m = m[bytes.Index(m, []byte("@")):] + rawBytes = bytes.Replace(rawBytes, m, + []byte(fmt.Sprintf(`%s`, setting.AppSubUrl, m[1:], m)), -1) + } + + rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas) + rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas) + rawBytes = RenderSha1CurrentPattern(rawBytes, urlPrefix) + return rawBytes +} + +// RenderRaw renders Markdown to HTML without handling special links. +func RenderRaw(body []byte, urlPrefix string) []byte { + htmlFlags := 0 + htmlFlags |= blackfriday.HTML_SKIP_STYLE + htmlFlags |= blackfriday.HTML_OMIT_CONTENTS + + if setting.Smartypants.Enabled { + htmlFlags |= blackfriday.HTML_USE_SMARTYPANTS + if setting.Smartypants.Fractions { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS + } + if setting.Smartypants.Dashes { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_DASHES + } + if setting.Smartypants.LatexDashes { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES + } + if setting.Smartypants.AngledQuotes { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES + } + } + + renderer := &Renderer{ + Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), + urlPrefix: urlPrefix, + } + + // set up the parser + extensions := 0 + extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS + extensions |= blackfriday.EXTENSION_TABLES + extensions |= blackfriday.EXTENSION_FENCED_CODE + extensions |= blackfriday.EXTENSION_AUTOLINK + extensions |= blackfriday.EXTENSION_STRIKETHROUGH + extensions |= blackfriday.EXTENSION_SPACE_HEADERS + extensions |= blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK + + if setting.Markdown.EnableHardLineBreak { + extensions |= blackfriday.EXTENSION_HARD_LINE_BREAK + } + + body = blackfriday.Markdown(body, renderer, extensions) + return body +} + +var ( + leftAngleBracket = []byte("") +) + +var noEndTags = []string{"input", "br", "hr", "img"} + +// wrapImgWithLink warps link to standalone tags. +func wrapImgWithLink(urlPrefix string, buf *bytes.Buffer, token html.Token) { + var src, alt string + // Extract "src" and "alt" attributes + for i := range token.Attr { + switch token.Attr[i].Key { + case "src": + src = token.Attr[i].Val + case "alt": + alt = token.Attr[i].Val + } + } + + // Skip in case the "src" is empty + if len(src) == 0 { + buf.WriteString(token.String()) + return + } + + // Prepend repository base URL for internal links + needPrepend := !isLink([]byte(src)) + if needPrepend { + urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) + if src[0] != '/' { + urlPrefix += "/" + } + } + + buf.WriteString(``) + + if needPrepend { + src = strings.Replace(urlPrefix+string(src), " ", "%20", -1) + buf.WriteString(` 0 { + buf.WriteString(` alt="`) + buf.WriteString(alt) + buf.WriteString(`"`) + } + + buf.WriteString(`>`) + + } else { + buf.WriteString(token.String()) + } + + buf.WriteString(``) +} + +// 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) []byte { + startTags := make([]string, 0, 5) + buf := bytes.NewBuffer(nil) + 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)) + + case html.StartTagToken: + tagName := token.Data + + if tagName == "img" { + wrapImgWithLink(urlPrefix, buf, token) + continue OUTER_LOOP + } + + buf.WriteString(token.String()) + // 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.WriteString(token.String()) + + // Stack number doesn't increate for tags without end tags. + 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 special links. +func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { + urlPrefix = strings.Replace(urlPrefix, space, spaceEncoded, -1) + result := RenderRaw(rawBytes, urlPrefix) + result = PostProcess(result, urlPrefix, metas) + result = Sanitizer.SanitizeBytes(result) + return result +} + +// 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)) +} diff --git a/modules/markup/markdown_test.go b/modules/markup/markdown_test.go new file mode 100644 index 00000000..2d06a149 --- /dev/null +++ b/modules/markup/markdown_test.go @@ -0,0 +1,308 @@ +package markup_test + +import ( + . "github.com/gogits/gogs/modules/markup" + . "github.com/smartystreets/goconvey/convey" + "testing" + + "bytes" + "github.com/gogits/gogs/modules/setting" + "github.com/russross/blackfriday" +) + +func TestMarkdown(t *testing.T) { + Convey("Rendering an issue mention", t, func() { + var ( + urlPrefix = "/prefix" + metas map[string]string = nil + ) + setting.AppSubUrlDepth = 0 + + Convey("To the internal issue tracker", func() { + Convey("It should not render anything when there are no mentions", func() { + testCases := []string{ + "", + "this is a test", + "test 123 123 1234", + "#", + "# # #", + "# 123", + "#abcd", + "##1234", + "test#1234", + "#1234test", + " test #1234test", + } + + for i := 0; i < len(testCases); i++ { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i]) + } + }) + Convey("It should render freestanding mentions", func() { + testCases := []string{ + "#1234 test", "#1234 test", + "test #1234 issue", "test #1234 issue", + "test issue #1234", "test issue #1234", + "#5 test", "#5 test", + "test #5 issue", "test #5 issue", + "test issue #5", "test issue #5", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + Convey("It should not render issue mention without leading space", func() { + input := []byte("test#54321 issue") + expected := "test#54321 issue" + So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) + }) + Convey("It should not render issue mention without trailing space", func() { + input := []byte("test #54321issue") + expected := "test #54321issue" + So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) + }) + Convey("It should render issue mention in parentheses", func() { + testCases := []string{ + "(#54321 issue)", "(#54321 issue)", + "test (#54321) issue", "test (#54321) issue", + "test (#54321 extra) issue", "test (#54321 extra) issue", + "test (#54321 issue)", "test (#54321 issue)", + "test (#54321)", "test (#54321)", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + Convey("It should render multiple issue mentions in the same line", func() { + testCases := []string{ + "#54321 #1243", "#54321 #1243", + "test #54321 #1243", "test #54321 #1243", + "(#54321 #1243)", "(#54321 #1243)", + "(#54321)(#1243)", "(#54321)(#1243)", + "text #54321 test #1243 issue", "text #54321 test #1243 issue", + "#1 (#4321) test", "#1 (#4321) test", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + }) + Convey("To an external issue tracker with numeric style", func() { + metas = make(map[string]string) + metas["format"] = "https://someurl.com/{user}/{repo}/{index}" + metas["user"] = "someuser" + metas["repo"] = "somerepo" + metas["style"] = ISSUE_NAME_STYLE_NUMERIC + + Convey("should not render anything when there are no mentions", func() { + testCases := []string{ + "this is a test", + "test 123 123 1234", + "#", + "# # #", + "# 123", + "#abcd", + } + + for i := 0; i < len(testCases); i++ { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i]) + } + }) + Convey("It should render freestanding issue mentions", func() { + testCases := []string{ + "#1234 test", "#1234 test", + "test #1234 issue", "test #1234 issue", + "test issue #1234", "test issue #1234", + "#5 test", "#5 test", + "test #5 issue", "test #5 issue", + "test issue #5", "test issue #5", + } + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + Convey("It should not render issue mention without leading space", func() { + input := []byte("test#54321 issue") + expected := "test#54321 issue" + So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) + }) + Convey("It should not render issue mention without trailing space", func() { + input := []byte("test #54321issue") + expected := "test #54321issue" + So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected) + }) + Convey("It should render issue mention in parentheses", func() { + testCases := []string{ + "(#54321 issue)", "(#54321 issue)", + "test (#54321) issue", "test (#54321) issue", + "test (#54321 extra) issue", "test (#54321 extra) issue", + "test (#54321 issue)", "test (#54321 issue)", + "test (#54321)", "test (#54321)", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + Convey("It should render multiple issue mentions in the same line", func() { + testCases := []string{ + "#54321 #1243", "#54321 #1243", + "test #54321 #1243", "test #54321 #1243", + "(#54321 #1243)", "(#54321 #1243)", + "(#54321)(#1243)", "(#54321)(#1243)", + "text #54321 test #1243 issue", "text #54321 test #1243 issue", + "#1 (#4321) test", "#1 (#4321) test", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + }) + Convey("To an external issue tracker with alphanumeric style", func() { + metas = make(map[string]string) + metas["format"] = "https://someurl.com/{user}/{repo}/?b={index}" + metas["user"] = "someuser" + metas["repo"] = "somerepo" + metas["style"] = ISSUE_NAME_STYLE_ALPHANUMERIC + Convey("It should not render anything when there are no mentions", func() { + testCases := []string{ + "", + "this is a test", + "test 123 123 1234", + "#", + "##1234", + "# 123", + "#abcd", + "test #123", + "abc-1234", // issue prefix must be capital + "ABc-1234", // issue prefix must be _all_ capital + "ABCDEFGHIJK-1234", // the limit is 10 characters in the prefix + "ABC1234", // dash is required + "test ABC- test", // number is required + "test -1234 test", // prefix is required + "testABC-123 test", // leading space is required + "test ABC-123test", // trailing space is required + "ABC-0123", // no leading zero + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i]) + } + }) + Convey("It should render freestanding issue mention", func() { + testCases := []string{ + "OTT-1234 test", "OTT-1234 test", + "test T-12 issue", "test T-12 issue", + "test issue ABCDEFGHIJ-1234567890", "test issue ABCDEFGHIJ-1234567890", + "A-1 test", "A-1 test", + "test ZED-1 issue", "test ZED-1 issue", + "test issue DEED-7154", "test issue DEED-7154", + } + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + Convey("It should render issue mention in parentheses", func() { + testCases := []string{ + "(ABG-124 issue)", "(ABG-124 issue)", + "test (ABG-124) issue", "test (ABG-124) issue", + "test (ABG-124 extra) issue", "test (ABG-124 extra) issue", + "test (ABG-124 issue)", "test (ABG-124 issue)", + "test (ABG-124)", "test (ABG-124)", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + Convey("It should render multiple issue mentions in the same line", func() { + testCases := []string{ + "ABG-124 OTT-4321", "ABG-124 OTT-4321", + "test ABG-124 OTT-4321", "test ABG-124 OTT-4321", + "(ABG-124 OTT-4321)", "(ABG-124 OTT-4321)", + "(ABG-124)(OTT-4321)", "(ABG-124)(OTT-4321)", + "text ABG-124 test OTT-4321 issue", "text ABG-124 test OTT-4321 issue", + "A-1 (RRE-345) test", "A-1 (RRE-345) test", + } + + for i := 0; i < len(testCases); i += 2 { + So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1]) + } + }) + }) + }) + + Convey("Rendering an issue URL", t, func() { + setting.AppUrl = "http://localhost:3000/" + htmlFlags := 0 + htmlFlags |= blackfriday.HTML_SKIP_STYLE + htmlFlags |= blackfriday.HTML_OMIT_CONTENTS + renderer := &Renderer{ + Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), + } + buffer := new(bytes.Buffer) + Convey("To the internal issue tracker", func() { + Convey("It should render valid issue URLs", func() { + testCases := []string{ + "http://localhost:3000/user/repo/issues/3333", "#3333", + } + + for i := 0; i < len(testCases); i += 2 { + renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL) + + line, _ := buffer.ReadString(0) + So(line, ShouldEqual, testCases[i+1]) + } + }) + Convey("It should render but not change non-issue URLs", func() { + testCases := []string{ + "http://1111/2222/ssss-issues/3333?param=blah&blahh=333", "http://1111/2222/ssss-issues/3333?param=blah&blahh=333", + "http://test.com/issues/33333", "http://test.com/issues/33333", + "http://test.com/issues/3", "http://test.com/issues/3", + "http://issues/333", "http://issues/333", + "https://issues/333", "https://issues/333", + "http://tissues/0", "http://tissues/0", + } + + for i := 0; i < len(testCases); i += 2 { + renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL) + + line, _ := buffer.ReadString(0) + So(line, ShouldEqual, testCases[i+1]) + } + }) + }) + }) + + Convey("Rendering a commit URL", t, func() { + setting.AppUrl = "http://localhost:3000/" + htmlFlags := 0 + htmlFlags |= blackfriday.HTML_SKIP_STYLE + htmlFlags |= blackfriday.HTML_OMIT_CONTENTS + renderer := &Renderer{ + Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), + } + buffer := new(bytes.Buffer) + Convey("To the internal issue tracker", func() { + Convey("It should correctly convert URLs", func() { + testCases := []string{ + "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae", " d8a994ef24", + "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", " d8a994ef24", + "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", + "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae", "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae", + } + + for i := 0; i < len(testCases); i += 2 { + renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL) + + line, _ := buffer.ReadString(0) + So(line, ShouldEqual, testCases[i+1]) + } + }) + }) + }) +} diff --git a/modules/template/template.go b/modules/template/template.go index acdfab8d..0bd5fa3f 100644 --- a/modules/template/template.go +++ b/modules/template/template.go @@ -23,7 +23,7 @@ import ( "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" ) @@ -125,7 +125,7 @@ func Safe(raw string) template.HTML { } func Str2html(raw string) template.HTML { - return template.HTML(markdown.Sanitizer.Sanitize(raw)) + return template.HTML(markup.Sanitizer.Sanitize(raw)) } func List(l *list.List) chan interface{} { @@ -201,7 +201,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 { diff --git a/routers/api/v1/misc/markdown.go b/routers/api/v1/misc/markdown.go index 64895db0..58ff93f5 100644 --- a/routers/api/v1/misc/markdown.go +++ b/routers/api/v1/misc/markdown.go @@ -8,7 +8,7 @@ import ( api "github.com/gogits/go-gogs-client" "github.com/gogits/gogs/modules/context" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" ) // https://github.com/gogits/go-gogs-client/wiki/Miscellaneous#render-an-arbitrary-markdown-document @@ -25,9 +25,9 @@ func Markdown(ctx *context.APIContext, form api.MarkdownOption) { switch form.Mode { case "gfm": - ctx.Write(markdown.Render([]byte(form.Text), form.Context, nil)) + ctx.Write(markup.Render([]byte(form.Text), form.Context, nil)) default: - ctx.Write(markdown.RenderRaw([]byte(form.Text), "")) + ctx.Write(markup.RenderRaw([]byte(form.Text), "")) } } @@ -38,5 +38,5 @@ func MarkdownRaw(ctx *context.APIContext) { ctx.Error(422, "", err) return } - ctx.Write(markdown.RenderRaw(body, "")) + ctx.Write(markup.RenderRaw(body, "")) } diff --git a/routers/install.go b/routers/install.go index 0a856934..2e6bee10 100644 --- a/routers/install.go +++ b/routers/install.go @@ -26,7 +26,7 @@ import ( "github.com/gogits/gogs/modules/cron" "github.com/gogits/gogs/modules/form" "github.com/gogits/gogs/modules/mailer" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/ssh" "github.com/gogits/gogs/modules/template/highlight" @@ -62,7 +62,7 @@ func GlobalInit() { if setting.InstallLock { highlight.NewContext() - markdown.BuildSanitizer() + markup.BuildSanitizer() if err := models.NewEngine(); err != nil { log.Fatal(2, "Fail to initialize ORM engine: %v", err) } diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 4ab8bc9b..917bcad5 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -22,7 +22,7 @@ import ( "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/context" "github.com/gogits/gogs/modules/form" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" ) @@ -541,7 +541,7 @@ func viewIssue(ctx *context.Context, isPullList bool) { ctx.Data["PageIsIssueList"] = true } - issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink, + issue.RenderedContent = string(markup.Render([]byte(issue.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) repo := ctx.Repo.Repository @@ -608,7 +608,7 @@ func viewIssue(ctx *context.Context, isPullList bool) { participants[0] = issue.Poster for _, comment = range issue.Comments { if comment.Type == models.COMMENT_TYPE_COMMENT { - comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink, + comment.RenderedContent = string(markup.Render([]byte(comment.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) // Check tag. @@ -728,7 +728,7 @@ func UpdateIssueContent(ctx *context.Context) { } ctx.JSON(200, map[string]interface{}{ - "content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())), + "content": string(markup.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())), }) } @@ -939,7 +939,7 @@ func UpdateCommentContent(ctx *context.Context) { } ctx.JSON(200, map[string]interface{}{ - "content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())), + "content": string(markup.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())), }) } @@ -1092,7 +1092,7 @@ func Milestones(ctx *context.Context) { if m.NumOpenIssues+m.NumClosedIssues > 0 { m.Completeness = m.NumClosedIssues * 100 / (m.NumOpenIssues + m.NumClosedIssues) } - m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) + m.RenderedContent = string(markup.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) } ctx.Data["Milestones"] = miles diff --git a/routers/repo/release.go b/routers/repo/release.go index 5c6ba4ea..b25add77 100644 --- a/routers/repo/release.go +++ b/routers/repo/release.go @@ -14,7 +14,7 @@ import ( "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/context" "github.com/gogits/gogs/modules/form" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" ) @@ -83,7 +83,7 @@ func Releases(ctx *context.Context) { return } - r.Note = markdown.RenderString(r.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()) + r.Note = markup.RenderString(r.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()) results[i] = r break } @@ -132,7 +132,7 @@ func Releases(ctx *context.Context) { return } - r.Note = markdown.RenderString(r.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()) + r.Note = markup.RenderString(r.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()) } if len(drafts) > 0 { diff --git a/routers/repo/view.go b/routers/repo/view.go index 8b68d188..a97c6705 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -20,7 +20,7 @@ import ( "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/context" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" "github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/template" "github.com/gogits/gogs/modules/template/highlight" @@ -55,7 +55,7 @@ func renderDirectory(ctx *context.Context, treeLink string) { var readmeFile *git.Blob for _, entry := range entries { - if entry.IsDir() || !markdown.IsReadmeFile(entry.Name()) { + if entry.IsDir() || !markup.IsReadmeFile(entry.Name()) { continue } @@ -86,9 +86,9 @@ func renderDirectory(ctx *context.Context, treeLink string) { d, _ := ioutil.ReadAll(dataRc) buf = append(buf, d...) switch { - case markdown.IsMarkdownFile(readmeFile.Name()): + case markup.IsMarkdownFile(readmeFile.Name()): ctx.Data["IsMarkdown"] = true - buf = markdown.Render(buf, treeLink, ctx.Repo.Repository.ComposeMetas()) + buf = markup.Render(buf, treeLink, ctx.Repo.Repository.ComposeMetas()) default: buf = bytes.Replace(buf, []byte("\n"), []byte(`
`), -1) } @@ -153,14 +153,14 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st d, _ := ioutil.ReadAll(dataRc) buf = append(buf, d...) - isMarkdown := markdown.IsMarkdownFile(blob.Name()) + isMarkdown := markup.IsMarkdownFile(blob.Name()) ctx.Data["IsMarkdown"] = isMarkdown - ctx.Data["ReadmeExist"] = isMarkdown && markdown.IsReadmeFile(blob.Name()) + ctx.Data["ReadmeExist"] = isMarkdown && markup.IsReadmeFile(blob.Name()) ctx.Data["IsIPythonNotebook"] = strings.HasSuffix(blob.Name(), ".ipynb") if isMarkdown { - ctx.Data["FileContent"] = string(markdown.Render(buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas())) + ctx.Data["FileContent"] = string(markup.Render(buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas())) } else { // Building code view blocks with line number on server side. var fileContent string diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go index df8d78a3..e0c5fdc5 100644 --- a/routers/repo/wiki.go +++ b/routers/repo/wiki.go @@ -15,7 +15,7 @@ import ( "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/context" "github.com/gogits/gogs/modules/form" - "github.com/gogits/gogs/modules/markdown" + "github.com/gogits/gogs/modules/markup" ) const ( @@ -107,7 +107,7 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, str return nil, "" } if isViewPage { - ctx.Data["content"] = string(markdown.Render(data, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) + ctx.Data["content"] = string(markup.Render(data, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) } else { ctx.Data["content"] = string(data) } -- cgit v1.2.3