mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Fix and refactor markdown rendering (#32522)
This commit is contained in:
		| @@ -7,6 +7,7 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"maps" | ||||
| 	"net" | ||||
| 	"net/url" | ||||
| 	"path/filepath" | ||||
| @@ -165,8 +166,8 @@ type Repository struct { | ||||
|  | ||||
| 	Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` | ||||
|  | ||||
| 	RenderingMetas         map[string]string `xorm:"-"` | ||||
| 	DocumentRenderingMetas map[string]string `xorm:"-"` | ||||
| 	commonRenderingMetas map[string]string `xorm:"-"` | ||||
|  | ||||
| 	Units           []*RepoUnit   `xorm:"-"` | ||||
| 	PrimaryLanguage *LanguageStat `xorm:"-"` | ||||
|  | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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")) | ||||
| } | ||||
|   | ||||
| @@ -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 "<html><body>" | ||||
| 		strings.NewReader("<html><body>"), | ||||
| 		// 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("</body></html>"), | ||||
| 	)) | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -44,6 +44,7 @@ var numericMetas = map[string]string{ | ||||
| 	"user":                         "someUser", | ||||
| 	"repo":                         "someRepo", | ||||
| 	"style":                        IssueNameStyleNumeric, | ||||
| 	"markupAllowShortIssuePattern": "true", | ||||
| } | ||||
|  | ||||
| var alphanumericMetas = map[string]string{ | ||||
| @@ -51,6 +52,7 @@ var alphanumericMetas = map[string]string{ | ||||
| 	"user":                         "someUser", | ||||
| 	"repo":                         "someRepo", | ||||
| 	"style":                        IssueNameStyleAlphanumeric, | ||||
| 	"markupAllowShortIssuePattern": "true", | ||||
| } | ||||
|  | ||||
| var regexpMetas = map[string]string{ | ||||
| @@ -64,6 +66,13 @@ var regexpMetas = map[string]string{ | ||||
| var localMetas = map[string]string{ | ||||
| 	"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) { | ||||
| @@ -126,7 +135,6 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||
| 		testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ | ||||
| 			Ctx:   git.DefaultContext, | ||||
| 			Metas: localMetas, | ||||
| 			ContentMode: RenderContentAsComment, | ||||
| 		}) | ||||
|  | ||||
| 		class := "ref-issue" | ||||
| @@ -141,7 +149,6 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||
| 		testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ | ||||
| 			Ctx:   git.DefaultContext, | ||||
| 			Metas: numericMetas, | ||||
| 			ContentMode: RenderContentAsComment, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| @@ -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)) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 "<img>" tag should also be clickable, | ||||
| 			// because frontend use `<img>` 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 | ||||
|   | ||||
| @@ -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))) | ||||
| @@ -528,8 +532,7 @@ func TestRender_RelativeMedias(t *testing.T) { | ||||
| 		buffer, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:   git.DefaultContext, | ||||
| 			Links: links, | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsComment), | ||||
| 			Metas: util.Iif(isWiki, localWikiMetas, localMetas), | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		return strings.TrimSpace(string(buffer)) | ||||
|   | ||||
| @@ -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) | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
| @@ -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))) | ||||
| @@ -308,8 +314,7 @@ func TestTotal_RenderWiki(t *testing.T) { | ||||
| 				Base: FullURL, | ||||
| 			}, | ||||
| 			Repo:  newMockRepo(testRepoOwnerName, testRepoName), | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: markup.RenderContentAsWiki, | ||||
| 			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 | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -684,9 +689,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -713,9 +718,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -742,9 +747,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -771,9 +776,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -800,9 +805,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -830,9 +835,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -860,9 +865,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -890,9 +895,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -920,9 +925,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -951,9 +956,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -982,9 +987,9 @@ space</p> | ||||
| <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> | ||||
| <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/> | ||||
| <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/> | ||||
| @@ -1001,7 +1006,7 @@ space</p> | ||||
| 		result, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:   context.Background(), | ||||
| 			Links: c.Links, | ||||
| 			ContentMode: util.Iif(c.IsWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault), | ||||
| 			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) | ||||
|   | ||||
| @@ -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), "/"), | ||||
| 		)) | ||||
| 	} | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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 "<br>" 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 | ||||
| 	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 | ||||
|  | ||||
| 	// 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 | ||||
|  | ||||
| 	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"} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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)) | ||||
| 	}, urlDefault, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessageSubject: %v", err) | ||||
| 		return template.HTML("") | ||||
| 		return "" | ||||
| 	} | ||||
| 	return renderCodeBlock(template.HTML(renderedMessage)) | ||||
| } | ||||
| @@ -96,7 +95,6 @@ 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, | ||||
| 	}, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessage: %v", err) | ||||
| @@ -118,7 +116,6 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { | ||||
| 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, | ||||
| 	}, template.HTMLEscapeString(text)) | ||||
| 	if err != nil { | ||||
| @@ -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) | ||||
|   | ||||
| @@ -50,7 +50,8 @@ var testMetas = map[string]string{ | ||||
| 	"user":                         "user13", | ||||
| 	"repo":                         "repo11", | ||||
| 	"repoPath":                     "../../tests/gitea-repositories-meta/user13/repo11.git/", | ||||
| 	"mode":     "comment", | ||||
| 	"markdownLineBreakStyle":       "comment", | ||||
| 	"markupAllowShortIssuePattern": "true", | ||||
| } | ||||
|  | ||||
| func TestMain(m *testing.M) { | ||||
| @@ -76,7 +77,6 @@ func TestRenderCommitBody(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	type args struct { | ||||
| 		msg string | ||||
| 		metas map[string]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 := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a>` | ||||
| 	expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a>` | ||||
| 	assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) | ||||
| } | ||||
|  | ||||
| @@ -164,11 +164,11 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | ||||
| <span class="emoji" aria-label="thumbs up">👍</span> | ||||
| mail@domain.com | ||||
| @mention-user test | ||||
| <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> | ||||
| #123 | ||||
|   space<SPACE><SPACE> | ||||
| ` | ||||
| 	expected = strings.ReplaceAll(expected, "<SPACE>", " ") | ||||
| 	assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas))) | ||||
| 	assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), nil))) | ||||
| } | ||||
|  | ||||
| func TestRenderMarkdownToHtml(t *testing.T) { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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), | ||||
| 		}, | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -290,8 +290,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { | ||||
|  | ||||
| 	rctx := &markup.RenderContext{ | ||||
| 		Ctx:   ctx, | ||||
| 		ContentMode: markup.RenderContentAsWiki, | ||||
| 		Metas:       ctx.Repo.Repository.ComposeDocumentMetas(ctx), | ||||
| 		Metas: ctx.Repo.Repository.ComposeWikiMetas(ctx), | ||||
| 		Links: markup.Links{ | ||||
| 			Base: ctx.Repo.RepoLink, | ||||
| 		}, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -29,7 +29,7 @@ | ||||
| 		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4"> | ||||
| 			{{if .ReadmeInList}} | ||||
| 				{{svg "octicon-book" 16 "tw-mr-2"}} | ||||
| 				<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong> | ||||
| 				<strong><a class="muted" href="#readme">{{.FileName}}</a></strong> | ||||
| 			{{else}} | ||||
| 				{{template "repo/file_info" .}} | ||||
| 			{{end}} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 wxiaoguang
					wxiaoguang