From 5eebe1dc5fb29a162c51d050396fce7b14e47f4e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 16 Nov 2024 16:41:44 +0800 Subject: [PATCH] Fix and refactor markdown rendering (#32522) --- models/repo/repo.go | 49 +++++--- models/repo/repo_test.go | 67 ++++++----- modules/markup/html.go | 126 +++++++++------------ modules/markup/html_commit.go | 6 +- modules/markup/html_email.go | 2 +- modules/markup/html_emoji.go | 2 +- modules/markup/html_internal_test.go | 68 +++++++---- modules/markup/html_issue.go | 14 ++- modules/markup/html_link.go | 27 +---- modules/markup/html_node.go | 4 +- modules/markup/html_test.go | 15 ++- modules/markup/markdown/goldmark.go | 5 +- modules/markup/markdown/markdown_test.go | 69 +++++------ modules/markup/markdown/transform_image.go | 2 +- modules/markup/orgmode/orgmode.go | 5 +- modules/markup/orgmode/orgmode_test.go | 2 +- modules/markup/render.go | 29 ++--- modules/markup/render_links.go | 2 +- modules/templates/util_render.go | 23 ++-- modules/templates/util_render_test.go | 20 ++-- routers/common/markup.go | 13 ++- routers/web/feed/convert.go | 2 +- routers/web/feed/profile.go | 4 +- routers/web/org/home.go | 2 +- routers/web/repo/wiki.go | 5 +- routers/web/shared/user/header.go | 2 +- templates/repo/view_file.tmpl | 2 +- 27 files changed, 289 insertions(+), 278 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 4776ff0b9c..7d78cee287 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "html/template" + "maps" "net" "net/url" "path/filepath" @@ -165,10 +166,10 @@ type Repository struct { Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` - RenderingMetas map[string]string `xorm:"-"` - DocumentRenderingMetas map[string]string `xorm:"-"` - Units []*RepoUnit `xorm:"-"` - PrimaryLanguage *LanguageStat `xorm:"-"` + commonRenderingMetas map[string]string `xorm:"-"` + + Units []*RepoUnit `xorm:"-"` + PrimaryLanguage *LanguageStat `xorm:"-"` IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"` ForkID int64 `xorm:"INDEX"` @@ -473,9 +474,8 @@ func (repo *Repository) MustOwner(ctx context.Context) *user_model.User { return repo.Owner } -// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. -func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { - if len(repo.RenderingMetas) == 0 { +func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]string { + if len(repo.commonRenderingMetas) == 0 { metas := map[string]string{ "user": repo.OwnerName, "repo": repo.Name, @@ -508,21 +508,34 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { metas["org"] = strings.ToLower(repo.OwnerName) } - repo.RenderingMetas = metas + repo.commonRenderingMetas = metas } - return repo.RenderingMetas + return repo.commonRenderingMetas } -// ComposeDocumentMetas composes a map of metas for properly rendering documents +// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message) +func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { + metas := maps.Clone(repo.composeCommonMetas(ctx)) + metas["markdownLineBreakStyle"] = "comment" + metas["markupAllowShortIssuePattern"] = "true" + return metas +} + +// ComposeWikiMetas composes a map of metas for properly rendering wikis +func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string { + // does wiki need the "teams" and "org" from common metas? + metas := maps.Clone(repo.composeCommonMetas(ctx)) + metas["markdownLineBreakStyle"] = "document" + metas["markupAllowShortIssuePattern"] = "true" + return metas +} + +// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files) func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { - if len(repo.DocumentRenderingMetas) == 0 { - metas := map[string]string{} - for k, v := range repo.ComposeMetas(ctx) { - metas[k] = v - } - repo.DocumentRenderingMetas = metas - } - return repo.DocumentRenderingMetas + // does document(file) need the "teams" and "org" from common metas? + metas := maps.Clone(repo.composeCommonMetas(ctx)) + metas["markdownLineBreakStyle"] = "document" + return metas } // GetBaseRepo populates repo.BaseRepo for a fork repository and diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index c13b698abf..6468e0f605 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -1,13 +1,12 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo_test +package repo import ( "testing" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -20,18 +19,18 @@ import ( ) var ( - countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} - countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} - countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} + countRepospts = CountRepositoryOptions{OwnerID: 10} + countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} + countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} ) func TestGetRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx := db.DefaultContext - count, err1 := repo_model.CountRepositories(ctx, countRepospts) - privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate) - publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic) + count, err1 := CountRepositories(ctx, countRepospts) + privateCount, err2 := CountRepositories(ctx, countReposptsPrivate) + publicCount, err3 := CountRepositories(ctx, countReposptsPublic) assert.NoError(t, err1) assert.NoError(t, err2) assert.NoError(t, err3) @@ -42,7 +41,7 @@ func TestGetRepositoryCount(t *testing.T) { func TestGetPublicRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic) + count, err := CountRepositories(db.DefaultContext, countReposptsPublic) assert.NoError(t, err) assert.Equal(t, int64(1), count) } @@ -50,14 +49,14 @@ func TestGetPublicRepositoryCount(t *testing.T) { func TestGetPrivateRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate) + count, err := CountRepositories(db.DefaultContext, countReposptsPrivate) assert.NoError(t, err) assert.Equal(t, int64(2), count) } func TestRepoAPIURL(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10}) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL()) } @@ -65,22 +64,22 @@ func TestRepoAPIURL(t *testing.T) { func TestWatchRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) + assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false)) - unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) + assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) } func TestMetas(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := &repo_model.Repository{Name: "testRepo"} + repo := &Repository{Name: "testRepo"} repo.Owner = &user_model.User{Name: "testOwner"} repo.OwnerName = repo.Owner.Name @@ -90,16 +89,16 @@ func TestMetas(t *testing.T) { assert.Equal(t, "testRepo", metas["repo"]) assert.Equal(t, "testOwner", metas["user"]) - externalTracker := repo_model.RepoUnit{ + externalTracker := RepoUnit{ Type: unit.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ + Config: &ExternalTrackerConfig{ ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", }, } testSuccess := func(expectedStyle string) { - repo.Units = []*repo_model.RepoUnit{&externalTracker} - repo.RenderingMetas = nil + repo.Units = []*RepoUnit{&externalTracker} + repo.commonRenderingMetas = nil metas := repo.ComposeMetas(db.DefaultContext) assert.Equal(t, expectedStyle, metas["style"]) assert.Equal(t, "testRepo", metas["repo"]) @@ -118,7 +117,7 @@ func TestMetas(t *testing.T) { externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp testSuccess(markup.IssueNameStyleRegexp) - repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 3) + repo, err := GetRepositoryByID(db.DefaultContext, 3) assert.NoError(t, err) metas = repo.ComposeMetas(db.DefaultContext) @@ -132,7 +131,7 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) t.Run("InvalidPath", func(t *testing.T) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something") + repo, err := GetRepositoryByURL(db.DefaultContext, "something") assert.Nil(t, repo) assert.Error(t, err) @@ -140,7 +139,7 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("ValidHttpURL", func(t *testing.T) { test := func(t *testing.T, url string) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + repo, err := GetRepositoryByURL(db.DefaultContext, url) assert.NotNil(t, repo) assert.NoError(t, err) @@ -155,7 +154,7 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("ValidGitSshURL", func(t *testing.T) { test := func(t *testing.T, url string) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + repo, err := GetRepositoryByURL(db.DefaultContext, url) assert.NotNil(t, repo) assert.NoError(t, err) @@ -173,7 +172,7 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("ValidImplicitSshURL", func(t *testing.T) { test := func(t *testing.T, url string) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + repo, err := GetRepositoryByURL(db.DefaultContext, url) assert.NotNil(t, repo) assert.NoError(t, err) @@ -200,21 +199,21 @@ func TestComposeSSHCloneURL(t *testing.T) { setting.SSH.Domain = "domain" setting.SSH.Port = 22 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "git@domain:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo")) // test SSH_DOMAIN while use non-standard SSH port setting.SSH.Port = 123 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) // test IPv6 SSH_DOMAIN setting.Repository.UseCompatSSHURI = false setting.SSH.Domain = "::1" setting.SSH.Port = 22 - assert.Equal(t, "git@[::1]:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo")) setting.SSH.Port = 123 - assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) } diff --git a/modules/markup/html.go b/modules/markup/html.go index 54c65c95d2..16ccd4b406 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -7,11 +7,11 @@ import ( "bytes" "io" "regexp" + "slices" "strings" "sync" "code.gitea.io/gitea/modules/markup/common" - "code.gitea.io/gitea/modules/setting" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -25,7 +25,27 @@ const ( IssueNameStyleRegexp = "regexp" ) -var ( +// CSS class for action keywords (e.g. "closes: #1") +const keywordClass = "issue-keyword" + +type globalVarsType struct { + hashCurrentPattern *regexp.Regexp + shortLinkPattern *regexp.Regexp + anyHashPattern *regexp.Regexp + comparePattern *regexp.Regexp + fullURLPattern *regexp.Regexp + emailRegex *regexp.Regexp + blackfridayExtRegex *regexp.Regexp + emojiShortCodeRegex *regexp.Regexp + issueFullPattern *regexp.Regexp + filesChangedFullPattern *regexp.Regexp + + tagCleaner *regexp.Regexp + nulCleaner *strings.Replacer +} + +var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType { + v := &globalVarsType{} // 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. @@ -36,79 +56,56 @@ var ( // hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae // Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length // so that abbreviated hash links can be used as well. This matches git and GitHub usability. - hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`) + v.hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`) // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax - shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) + v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) // anyHashPattern splits url containing SHA into parts - anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) + v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" - comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) + v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) // fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..." - fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`) + v.fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`) // emailRegex is definitely not perfect with edge cases, // it is still accepted by the CommonMark specification, as well as the HTML5 spec: // http://spec.commonmark.org/0.28/#email-address // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) - emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") + v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") // blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote - blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) + v.blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) // emojiShortCodeRegex find emoji by alias like :smile: - emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) -) + v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) -// CSS class for action keywords (e.g. "closes: #1") -const keywordClass = "issue-keyword" + // example: https://domain/org/repo/pulls/27#hash + v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) + + // example: https://domain/org/repo/pulls/27/files#hash + v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`) + + v.tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`) + v.nulCleaner = strings.NewReplacer("\000", "") + return v +}) // IsFullURLBytes reports whether link fits valid format. func IsFullURLBytes(link []byte) bool { - return fullURLPattern.Match(link) + return globalVars().fullURLPattern.Match(link) } func IsFullURLString(link string) bool { - return fullURLPattern.MatchString(link) + return globalVars().fullURLPattern.MatchString(link) } func IsNonEmptyRelativePath(link string) bool { return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#' } -// regexp for full links to issues/pulls -var issueFullPattern *regexp.Regexp - -// Once for to prevent races -var issueFullPatternOnce sync.Once - -// regexp for full links to hash comment in pull request files changed tab -var filesChangedFullPattern *regexp.Regexp - -// Once for to prevent races -var filesChangedFullPatternOnce sync.Once - -func getIssueFullPattern() *regexp.Regexp { - issueFullPatternOnce.Do(func() { - // example: https://domain/org/repo/pulls/27#hash - issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + - `[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) - }) - return issueFullPattern -} - -func getFilesChangedFullPattern() *regexp.Regexp { - filesChangedFullPatternOnce.Do(func() { - // example: https://domain/org/repo/pulls/27/files#hash - filesChangedFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + - `[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`) - }) - return filesChangedFullPattern -} - // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text func CustomLinkURLSchemes(schemes []string) { schemes = append(schemes, "http", "https") @@ -197,13 +194,6 @@ func RenderCommitMessage( content string, ) (string, error) { procs := commitMessageProcessors - if ctx.DefaultLink != "" { - // we don't have to fear data races, because being - // commitMessageProcessors of fixed len and cap, every time we append - // something to it the slice is realloc+copied, so append always - // generates the slice ex-novo. - procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) - } return renderProcessString(ctx, procs, content) } @@ -231,16 +221,17 @@ var emojiProcessors = []processor{ // which changes every text node into a link to the passed default link. func RenderCommitMessageSubject( ctx *RenderContext, - content string, + defaultLink, content string, ) (string, error) { - procs := commitMessageSubjectProcessors - if ctx.DefaultLink != "" { - // we don't have to fear data races, because being - // commitMessageSubjectProcessors of fixed len and cap, every time we - // append something to it the slice is realloc+copied, so append always - // generates the slice ex-novo. - procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) - } + procs := slices.Clone(commitMessageSubjectProcessors) + procs = append(procs, func(ctx *RenderContext, node *html.Node) { + ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data} + node.Type = html.ElementNode + node.Data = "a" + node.DataAtom = atom.A + node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} + node.FirstChild, node.LastChild = ch, ch + }) return renderProcessString(ctx, procs, content) } @@ -249,10 +240,8 @@ func RenderIssueTitle( ctx *RenderContext, title string, ) (string, error) { + // do not render other issue/commit links in an issue's title - which in most cases is already a link. return renderProcessString(ctx, []processor{ - issueIndexPatternProcessor, - commitCrossReferencePatternProcessor, - hashCurrentPatternProcessor, emojiShortCodeProcessor, emojiProcessor, }, title) @@ -288,11 +277,6 @@ func RenderEmoji( return renderProcessString(ctx, emojiProcessors, content) } -var ( - tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`) - nulCleaner = strings.NewReplacer("\000", "") -) - func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { defer ctx.Cancel() // FIXME: don't read all content to memory @@ -306,7 +290,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output // prepend "" strings.NewReader(""), // Strip out nuls - they're always invalid - bytes.NewReader(tagCleaner.ReplaceAll([]byte(nulCleaner.Replace(string(rawHTML))), []byte("<$1"))), + bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))), // close the tags strings.NewReader(""), )) @@ -353,7 +337,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod // Add user-content- to IDs and "#" links if they don't already have them for idx, attr := range node.Attr { val := strings.TrimPrefix(attr.Val, "#") - notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val)) + notHasPrefix := !(strings.HasPrefix(val, "user-content-") || globalVars().blackfridayExtRegex.MatchString(val)) if attr.Key == "id" && notHasPrefix { node.Attr[idx].Val = "user-content-" + attr.Val diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 86d70746d4..0e674c83e1 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -54,7 +54,7 @@ func createCodeLink(href, content, class string) *html.Node { } func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { - m := anyHashPattern.FindStringSubmatchIndex(s) + m := globalVars().anyHashPattern.FindStringSubmatchIndex(s) if m == nil { return ret, false } @@ -120,7 +120,7 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling continue } - m := comparePattern.FindStringSubmatchIndex(node.Data) + m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data) if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match node = node.NextSibling continue @@ -173,7 +173,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { ctx.ShaExistCache = make(map[string]bool) } for node != nil && node != next && start < len(node.Data) { - m := hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:]) + m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:]) if m == nil { return } diff --git a/modules/markup/html_email.go b/modules/markup/html_email.go index a062789b35..32d0285eb4 100644 --- a/modules/markup/html_email.go +++ b/modules/markup/html_email.go @@ -9,7 +9,7 @@ import "golang.org/x/net/html" func emailAddressProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := emailRegex.FindStringSubmatchIndex(node.Data) + m := globalVars().emailRegex.FindStringSubmatchIndex(node.Data) if m == nil { return } diff --git a/modules/markup/html_emoji.go b/modules/markup/html_emoji.go index c60d06b823..6eacb2067f 100644 --- a/modules/markup/html_emoji.go +++ b/modules/markup/html_emoji.go @@ -62,7 +62,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { start := 0 next := node.NextSibling for node != nil && node != next && start < len(node.Data) { - m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) + m := globalVars().emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) if m == nil { return } diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 2fb657f56b..cdcc94d563 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -40,17 +40,19 @@ func link(href, class, contents string) string { } var numericMetas = map[string]string{ - "format": "https://someurl.com/{user}/{repo}/{index}", - "user": "someUser", - "repo": "someRepo", - "style": IssueNameStyleNumeric, + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleNumeric, + "markupAllowShortIssuePattern": "true", } var alphanumericMetas = map[string]string{ - "format": "https://someurl.com/{user}/{repo}/{index}", - "user": "someUser", - "repo": "someRepo", - "style": IssueNameStyleAlphanumeric, + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleAlphanumeric, + "markupAllowShortIssuePattern": "true", } var regexpMetas = map[string]string{ @@ -62,8 +64,15 @@ var regexpMetas = map[string]string{ // these values should match the TestOrgRepo const above var localMetas = map[string]string{ - "user": "test-owner", - "repo": "test-repo", + "user": "test-owner", + "repo": "test-repo", + "markupAllowShortIssuePattern": "true", +} + +var localWikiMetas = map[string]string{ + "user": "test-owner", + "repo": "test-repo", + "markupContentMode": "wiki", } func TestRender_IssueIndexPattern(t *testing.T) { @@ -124,9 +133,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) { } expectedNil := fmt.Sprintf(expectedFmt, links...) testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ - Ctx: git.DefaultContext, - Metas: localMetas, - ContentMode: RenderContentAsComment, + Ctx: git.DefaultContext, + Metas: localMetas, }) class := "ref-issue" @@ -139,9 +147,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) { } expectedNum := fmt.Sprintf(expectedFmt, links...) testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ - Ctx: git.DefaultContext, - Metas: numericMetas, - ContentMode: RenderContentAsComment, + Ctx: git.DefaultContext, + Metas: numericMetas, }) } @@ -262,7 +269,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) { }) } -func TestRender_IssueIndexPattern_Document(t *testing.T) { +func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) { setting.AppURL = TestAppURL metas := map[string]string{ "format": "https://someurl.com/{user}/{repo}/{index}", @@ -285,6 +292,22 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) { }) } +func TestRender_RenderIssueTitle(t *testing.T) { + setting.AppURL = TestAppURL + metas := map[string]string{ + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleNumeric, + } + actual, err := RenderIssueTitle(&RenderContext{ + Ctx: git.DefaultContext, + Metas: metas, + }, "#1") + assert.NoError(t, err) + assert.Equal(t, "#1", actual) +} + func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { ctx.Links.AbsolutePrefix = true if ctx.Links.Base == "" { @@ -318,8 +341,7 @@ func TestRender_AutoLink(t *testing.T) { Links: Links{ Base: TestRepoURL, }, - Metas: localMetas, - ContentMode: RenderContentAsWiki, + Metas: localWikiMetas, }, strings.NewReader(input), &buffer) assert.Equal(t, err, nil) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) @@ -391,10 +413,10 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) { } for _, testCase := range trueTestCases { - assert.True(t, hashCurrentPattern.MatchString(testCase)) + assert.True(t, globalVars().hashCurrentPattern.MatchString(testCase)) } for _, testCase := range falseTestCases { - assert.False(t, hashCurrentPattern.MatchString(testCase)) + assert.False(t, globalVars().hashCurrentPattern.MatchString(testCase)) } } @@ -474,9 +496,9 @@ func TestRegExp_shortLinkPattern(t *testing.T) { } for _, testCase := range trueTestCases { - assert.True(t, shortLinkPattern.MatchString(testCase)) + assert.True(t, globalVars().shortLinkPattern.MatchString(testCase)) } for _, testCase := range falseTestCases { - assert.False(t, shortLinkPattern.MatchString(testCase)) + assert.False(t, globalVars().shortLinkPattern.MatchString(testCase)) } } diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index fa630656ce..2acf154ad2 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -7,6 +7,7 @@ import ( "strings" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/regexplru" @@ -23,18 +24,21 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { } next := node.NextSibling for node != nil && node != next { - m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) + m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data) if m == nil { return } - mDiffView := getFilesChangedFullPattern().FindStringSubmatchIndex(node.Data) + mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data) // leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files if mDiffView != nil { return } link := node.Data[m[0]:m[1]] + if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) { + return + } text := "#" + node.Data[m[2]:m[3]] // if m[4] and m[5] is not -1, then link is to a comment // indicate that in the text by appending (comment) @@ -67,8 +71,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { return } - // crossLinkOnly if not comment and not wiki - crossLinkOnly := ctx.ContentMode != RenderContentAsTitle && ctx.ContentMode != RenderContentAsComment && ctx.ContentMode != RenderContentAsWiki + // crossLinkOnly: do not parse "#123", only parse "owner/repo#123" + // if there is no repo in the context, then the "#123" format can't be parsed + // old logic: crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki + crossLinkOnly := ctx.Metas["markupAllowShortIssuePattern"] != "true" var ( found bool diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 30564da548..b7562d0aa6 100644 --- a/modules/markup/html_link.go +++ b/modules/markup/html_link.go @@ -20,9 +20,9 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu isAnchorFragment := link != "" && link[0] == '#' if !isAnchorFragment && !IsFullURLString(link) { linkBase := ctx.Links.Base - if ctx.ContentMode == RenderContentAsWiki { + if ctx.IsMarkupContentWiki() { // no need to check if the link should be resolved as a wiki link or a wiki raw link - // just use wiki link here and it will be redirected to a wiki raw link if necessary + // just use wiki link here, and it will be redirected to a wiki raw link if necessary linkBase = ctx.Links.WikiLink() } else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" { // if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}" @@ -40,7 +40,7 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu func shortLinkProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := shortLinkPattern.FindStringSubmatchIndex(node.Data) + m := globalVars().shortLinkPattern.FindStringSubmatchIndex(node.Data) if m == nil { return } @@ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { } if image { if !absoluteLink { - link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), link) + link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link) } title := props["title"] if title == "" { @@ -200,25 +200,6 @@ func linkProcessor(ctx *RenderContext, node *html.Node) { } } -func genDefaultLinkProcessor(defaultLink string) processor { - return func(ctx *RenderContext, node *html.Node) { - ch := &html.Node{ - Parent: node, - Type: html.TextNode, - Data: node.Data, - } - - node.Type = html.ElementNode - node.Data = "a" - node.DataAtom = atom.A - node.Attr = []html.Attribute{ - {Key: "href", Val: defaultLink}, - {Key: "class", Val: "default-link muted"}, - } - node.FirstChild, node.LastChild = ch, ch - } -} - // descriptionLinkProcessor creates links for DescriptionHTML func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go index c499854053..234adba2bf 100644 --- a/modules/markup/html_node.go +++ b/modules/markup/html_node.go @@ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { } if IsNonEmptyRelativePath(attr.Val) { - attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val) + attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val) // By default, the "" tag should also be clickable, // because frontend use `` to paste the re-scaled image into the markdown, @@ -53,7 +53,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) { continue } if IsNonEmptyRelativePath(attr.Val) { - attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val) + attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val) } attr.Val = camoHandleLink(attr.Val) node.Attr[i] = attr diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 262d0fc4dd..67ac2758a3 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -27,6 +27,11 @@ var ( "user": testRepoOwnerName, "repo": testRepoName, } + localWikiMetas = map[string]string{ + "user": testRepoOwnerName, + "repo": testRepoName, + "markupContentMode": "wiki", + } ) type mockRepo struct { @@ -413,8 +418,7 @@ func TestRender_ShortLinks(t *testing.T) { Links: markup.Links{ Base: markup.TestRepoURL, }, - Metas: localMetas, - ContentMode: markup.RenderContentAsWiki, + Metas: localWikiMetas, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) @@ -526,10 +530,9 @@ func TestRender_ShortLinks(t *testing.T) { func TestRender_RelativeMedias(t *testing.T) { render := func(input string, isWiki bool, links markup.Links) string { buffer, err := markdown.RenderString(&markup.RenderContext{ - Ctx: git.DefaultContext, - Links: links, - Metas: localMetas, - ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsComment), + Ctx: git.DefaultContext, + Links: links, + Metas: util.Iif(isWiki, localWikiMetas, localMetas), }, input) assert.NoError(t, err) return strings.TrimSpace(string(buffer)) diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index c8488cfb50..c837b21e78 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -75,11 +75,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa // TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }` // many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting // especially in many tests. + markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"] if markup.RenderBehaviorForTesting.ForceHardLineBreak { v.SetHardLineBreak(true) - } else if ctx.ContentMode == markup.RenderContentAsComment { + } else if markdownLineBreakStyle == "comment" { v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) - } else { + } else if markdownLineBreakStyle == "document" { v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) } } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 315eed2e62..780df8727f 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -37,6 +37,12 @@ var localMetas = map[string]string{ "repo": testRepoName, } +var localWikiMetas = map[string]string{ + "user": testRepoOwnerName, + "repo": testRepoName, + "markupContentMode": "wiki", +} + type mockRepo struct { OwnerName string RepoName string @@ -75,7 +81,7 @@ func TestRender_StandardLinks(t *testing.T) { Links: markup.Links{ Base: FullURL, }, - ContentMode: markup.RenderContentAsWiki, + Metas: localWikiMetas, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) @@ -307,9 +313,8 @@ func TestTotal_RenderWiki(t *testing.T) { Links: markup.Links{ Base: FullURL, }, - Repo: newMockRepo(testRepoOwnerName, testRepoName), - Metas: localMetas, - ContentMode: markup.RenderContentAsWiki, + Repo: newMockRepo(testRepoOwnerName, testRepoName), + Metas: localWikiMetas, }, sameCases[i]) assert.NoError(t, err) assert.Equal(t, answers[i], string(line)) @@ -334,7 +339,7 @@ func TestTotal_RenderWiki(t *testing.T) { Links: markup.Links{ Base: FullURL, }, - ContentMode: markup.RenderContentAsWiki, + Metas: localWikiMetas, }, testCases[i]) assert.NoError(t, err) assert.EqualValues(t, testCases[i+1], string(line)) @@ -657,9 +662,9 @@ mail@domain.com remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -684,9 +689,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -713,9 +718,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -742,9 +747,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -771,9 +776,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -800,9 +805,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -830,9 +835,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -860,9 +865,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -890,9 +895,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -920,9 +925,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -951,9 +956,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -982,9 +987,9 @@ space

remote image
local image
remote link
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@@ -999,9 +1004,9 @@ space

defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() for i, c := range cases { result, err := markdown.RenderString(&markup.RenderContext{ - Ctx: context.Background(), - Links: c.Links, - ContentMode: util.Iif(c.IsWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault), + Ctx: context.Background(), + Links: c.Links, + Metas: util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{}), }, input) assert.NoError(t, err, "Unexpected error in testcase: %v", i) assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i) diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go index 4ed4118854..b2262c1c78 100644 --- a/modules/markup/markdown/transform_image.go +++ b/modules/markup/markdown/transform_image.go @@ -21,7 +21,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image) // Check if the destination is a real link if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) { v.Destination = []byte(giteautil.URLJoin( - ctx.Links.ResolveMediaLink(ctx.ContentMode == markup.RenderContentAsWiki), + ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), strings.TrimLeft(string(v.Destination), "/"), )) } diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 6b9c963157..c587a6ada5 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -144,15 +144,14 @@ func (r *Writer) resolveLink(kind, link string) string { } base := r.Ctx.Links.Base - isWiki := r.Ctx.ContentMode == markup.RenderContentAsWiki - if isWiki { + if r.Ctx.IsMarkupContentWiki() { base = r.Ctx.Links.WikiLink() } else if r.Ctx.Links.HasBranchInfo() { base = r.Ctx.Links.SrcLink() } if kind == "image" || kind == "video" { - base = r.Ctx.Links.ResolveMediaLink(isWiki) + base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki()) } link = util.URLJoin(base, link) diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index b882678c7e..a3eefc3db3 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -27,7 +27,7 @@ func TestRender_StandardLinks(t *testing.T) { Base: "/relative-path", BranchPath: "branch/main", }, - ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault), + Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")}, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) diff --git a/modules/markup/render.go b/modules/markup/render.go index add50f4382..1977dc73f5 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -27,15 +27,6 @@ const ( RenderMetaAsTable RenderMetaMode = "table" ) -type RenderContentMode string - -const ( - RenderContentAsDefault RenderContentMode = "" // empty means "default", no special handling, maybe just a simple "document" - RenderContentAsComment RenderContentMode = "comment" - RenderContentAsTitle RenderContentMode = "title" - RenderContentAsWiki RenderContentMode = "wiki" -) - var RenderBehaviorForTesting struct { // Markdown line break rendering has 2 default behaviors: // * Use hard: replace "\n" with "
" for comments, setting.Markdown.EnableHardLineBreakInComments=true @@ -59,12 +50,14 @@ type RenderContext struct { // for file mode, it could be left as empty, and will be detected by file extension in RelativePath MarkupType string - // what the content will be used for: eg: for comment or for wiki? or just render a file? - ContentMode RenderContentMode + Links Links // special link references for rendering, especially when there is a branch/tree path + + // user&repo, format&style®exp (for external issue pattern), teams&org (for mention) + // BranchNameSubURL (for iframe&asciicast) + // markupAllowShortIssuePattern, markupContentMode (wiki) + // markdownLineBreakStyle (comment, document) + Metas map[string]string - Links Links // special link references for rendering, especially when there is a branch/tree path - Metas map[string]string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention), BranchNameSubURL(for iframe&asciicast) - DefaultLink string // TODO: need to figure out GitRepo *git.Repository Repo gitrepo.Repository ShaExistCache map[string]bool @@ -102,6 +95,10 @@ func (ctx *RenderContext) AddCancel(fn func()) { } } +func (ctx *RenderContext) IsMarkupContentWiki() bool { + return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki" +} + // Render renders markup file to HTML with all specific handling stuff. func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { if ctx.MarkupType == "" && ctx.RelativePath != "" { @@ -232,3 +229,7 @@ func Init(ph *ProcessorHelper) { } } } + +func ComposeSimpleDocumentMetas() map[string]string { + return map[string]string{"markdownLineBreakStyle": "document"} +} diff --git a/modules/markup/render_links.go b/modules/markup/render_links.go index 3e1aa7ce3a..c8339d8f8b 100644 --- a/modules/markup/render_links.go +++ b/modules/markup/render_links.go @@ -10,7 +10,7 @@ import ( type Links struct { AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias - Base string // base prefix for pre-provided links and medias (images, videos) + Base string // base prefix for pre-provided links and medias (images, videos), usually it is the path to the repo BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0" TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 1db1e4a111..8e443446bd 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -62,19 +62,18 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me } msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace) if len(msgLine) == 0 { - return template.HTML("") + return "" } // we can safely assume that it will not return any error, since there // shouldn't be any special HTML. renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ - Ctx: ut.ctx, - DefaultLink: urlDefault, - Metas: metas, - }, template.HTMLEscapeString(msgLine)) + Ctx: ut.ctx, + Metas: metas, + }, urlDefault, template.HTMLEscapeString(msgLine)) if err != nil { log.Error("RenderCommitMessageSubject: %v", err) - return template.HTML("") + return "" } return renderCodeBlock(template.HTML(renderedMessage)) } @@ -94,9 +93,8 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem } renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ - Ctx: ut.ctx, - Metas: metas, - ContentMode: markup.RenderContentAsComment, + Ctx: ut.ctx, + Metas: metas, }, template.HTMLEscapeString(msgLine)) if err != nil { log.Error("RenderCommitMessage: %v", err) @@ -117,9 +115,8 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { // RenderIssueTitle renders issue/pull title with defined post processors func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ - Ctx: ut.ctx, - ContentMode: markup.RenderContentAsTitle, - Metas: metas, + Ctx: ut.ctx, + Metas: metas, }, template.HTMLEscapeString(text)) if err != nil { log.Error("RenderIssueTitle: %v", err) @@ -212,7 +209,7 @@ func reactionToEmoji(reaction string) template.HTML { func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive output, err := markdown.RenderString(&markup.RenderContext{ Ctx: ut.ctx, - Metas: map[string]string{"mode": "document"}, + Metas: markup.ComposeSimpleDocumentMetas(), }, input) if err != nil { log.Error("RenderString: %v", err) diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 2d331b8317..529507e7ea 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -47,10 +47,11 @@ mail@domain.com } var testMetas = map[string]string{ - "user": "user13", - "repo": "repo11", - "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/", - "mode": "comment", + "user": "user13", + "repo": "repo11", + "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/", + "markdownLineBreakStyle": "comment", + "markupAllowShortIssuePattern": "true", } func TestMain(m *testing.M) { @@ -75,8 +76,7 @@ func newTestRenderUtils() *RenderUtils { func TestRenderCommitBody(t *testing.T) { defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() type args struct { - msg string - metas map[string]string + msg string } tests := []struct { name string @@ -108,7 +108,7 @@ func TestRenderCommitBody(t *testing.T) { ut := newTestRenderUtils() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, tt.args.metas), "RenderCommitBody(%v, %v)", tt.args.msg, tt.args.metas) + assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil) }) } @@ -140,7 +140,7 @@ func TestRenderCommitMessage(t *testing.T) { } func TestRenderCommitMessageLinkSubject(t *testing.T) { - expected := `space @mention-user` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) } @@ -164,11 +164,11 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit 👍 mail@domain.com @mention-user test -#123 +#123 space ` expected = strings.ReplaceAll(expected, "", " ") - assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas))) + assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), nil))) } func TestRenderMarkdownToHtml(t *testing.T) { diff --git a/routers/common/markup.go b/routers/common/markup.go index c8cc1a5ff1..dd6b286109 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -47,11 +47,12 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa switch mode { case "gfm": // legacy mode, do nothing case "comment": - renderCtx.ContentMode = markup.RenderContentAsComment + renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "comment"} case "wiki": - renderCtx.ContentMode = markup.RenderContentAsWiki + renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"} case "file": // render the repo file content by its extension + renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document"} renderCtx.MarkupType = "" renderCtx.RelativePath = filePath renderCtx.InStandalonePage = true @@ -74,10 +75,12 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa if repo != nil && repo.Repository != nil { renderCtx.Repo = repo.Repository - if renderCtx.ContentMode == markup.RenderContentAsComment { - renderCtx.Metas = repo.Repository.ComposeMetas(ctx) - } else { + if mode == "file" { renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx) + } else if mode == "wiki" { + renderCtx.Metas = repo.Repository.ComposeWikiMetas(ctx) + } else if mode == "comment" { + renderCtx.Metas = repo.Repository.ComposeMetas(ctx) } } if err := markup.Render(renderCtx, strings.NewReader(text), ctx.Resp); err != nil { diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index a273515c8a..afc2c343a6 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -56,7 +56,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content Links: markup.Links{ Base: act.GetRepoLink(ctx), }, - Metas: map[string]string{ + Metas: map[string]string{ // FIXME: not right here, it should use issue to compose the metas "user": act.GetRepoUserName(ctx), "repo": act.GetRepoName(ctx), }, diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 08cbcd9e12..6dd2d14cc6 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -46,9 +46,7 @@ func showUserFeed(ctx *context.Context, formatType string) { Links: markup.Links{ Base: ctx.ContextUser.HTMLURL(), }, - Metas: map[string]string{ - "user": ctx.ContextUser.GetDisplayName(), - }, + Metas: markup.ComposeSimpleDocumentMetas(), }, ctx.ContextUser.Description) if err != nil { ctx.ServerError("RenderString", err) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 544f5362b8..18648d33cd 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -189,7 +189,7 @@ func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool { Base: profileDbRepo.Link(), BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, - Metas: map[string]string{"mode": "document"}, + Metas: markup.ComposeSimpleDocumentMetas(), }, bytes); err != nil { log.Error("failed to RenderString: %v", err) } else { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 2b8312f10a..13f6b69493 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -289,9 +289,8 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } rctx := &markup.RenderContext{ - Ctx: ctx, - ContentMode: markup.RenderContentAsWiki, - Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx), + Ctx: ctx, + Metas: ctx.Repo.Repository.ComposeWikiMetas(ctx), Links: markup.Links{ Base: ctx.Repo.RepoLink, }, diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index ef111cff80..9467b0986b 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -50,7 +50,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { ctx.Data["OpenIDs"] = openIDs if len(ctx.ContextUser.Description) != 0 { content, err := markdown.RenderString(&markup.RenderContext{ - Metas: map[string]string{"mode": "document"}, + Metas: markup.ComposeSimpleDocumentMetas(), Ctx: ctx, }, ctx.ContextUser.Description) if err != nil { diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index fb5bc14fe2..89bb371e7c 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -29,7 +29,7 @@
{{if .ReadmeInList}} {{svg "octicon-book" 16 "tw-mr-2"}} - {{.FileName}} + {{.FileName}} {{else}} {{template "repo/file_info" .}} {{end}}