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" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
|  | 	"maps" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| @@ -165,8 +166,8 @@ type Repository struct { | |||||||
|  |  | ||||||
| 	Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` | 	Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` | ||||||
|  |  | ||||||
| 	RenderingMetas         map[string]string `xorm:"-"` | 	commonRenderingMetas map[string]string `xorm:"-"` | ||||||
| 	DocumentRenderingMetas map[string]string `xorm:"-"` |  | ||||||
| 	Units           []*RepoUnit   `xorm:"-"` | 	Units           []*RepoUnit   `xorm:"-"` | ||||||
| 	PrimaryLanguage *LanguageStat `xorm:"-"` | 	PrimaryLanguage *LanguageStat `xorm:"-"` | ||||||
|  |  | ||||||
| @@ -473,9 +474,8 @@ func (repo *Repository) MustOwner(ctx context.Context) *user_model.User { | |||||||
| 	return repo.Owner | 	return repo.Owner | ||||||
| } | } | ||||||
|  |  | ||||||
| // ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. | func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]string { | ||||||
| func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { | 	if len(repo.commonRenderingMetas) == 0 { | ||||||
| 	if len(repo.RenderingMetas) == 0 { |  | ||||||
| 		metas := map[string]string{ | 		metas := map[string]string{ | ||||||
| 			"user": repo.OwnerName, | 			"user": repo.OwnerName, | ||||||
| 			"repo": repo.Name, | 			"repo": repo.Name, | ||||||
| @@ -508,21 +508,34 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { | |||||||
| 			metas["org"] = strings.ToLower(repo.OwnerName) | 			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 { | func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { | ||||||
| 	if len(repo.DocumentRenderingMetas) == 0 { | 	// does document(file) need the "teams" and "org" from common metas? | ||||||
| 		metas := map[string]string{} | 	metas := maps.Clone(repo.composeCommonMetas(ctx)) | ||||||
| 		for k, v := range repo.ComposeMetas(ctx) { | 	metas["markdownLineBreakStyle"] = "document" | ||||||
| 			metas[k] = v | 	return metas | ||||||
| 		} |  | ||||||
| 		repo.DocumentRenderingMetas = metas |  | ||||||
| 	} |  | ||||||
| 	return repo.DocumentRenderingMetas |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetBaseRepo populates repo.BaseRepo for a fork repository and | // GetBaseRepo populates repo.BaseRepo for a fork repository and | ||||||
|   | |||||||
| @@ -1,13 +1,12 @@ | |||||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | // Copyright 2017 The Gitea Authors. All rights reserved. | ||||||
| // SPDX-License-Identifier: MIT | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
| package repo_test | package repo | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"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/unit" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -20,18 +19,18 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	countRepospts        = repo_model.CountRepositoryOptions{OwnerID: 10} | 	countRepospts        = CountRepositoryOptions{OwnerID: 10} | ||||||
| 	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} | 	countReposptsPublic  = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} | ||||||
| 	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} | 	countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestGetRepositoryCount(t *testing.T) { | func TestGetRepositoryCount(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	ctx := db.DefaultContext | 	ctx := db.DefaultContext | ||||||
| 	count, err1 := repo_model.CountRepositories(ctx, countRepospts) | 	count, err1 := CountRepositories(ctx, countRepospts) | ||||||
| 	privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate) | 	privateCount, err2 := CountRepositories(ctx, countReposptsPrivate) | ||||||
| 	publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic) | 	publicCount, err3 := CountRepositories(ctx, countReposptsPublic) | ||||||
| 	assert.NoError(t, err1) | 	assert.NoError(t, err1) | ||||||
| 	assert.NoError(t, err2) | 	assert.NoError(t, err2) | ||||||
| 	assert.NoError(t, err3) | 	assert.NoError(t, err3) | ||||||
| @@ -42,7 +41,7 @@ func TestGetRepositoryCount(t *testing.T) { | |||||||
| func TestGetPublicRepositoryCount(t *testing.T) { | func TestGetPublicRepositoryCount(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic) | 	count, err := CountRepositories(db.DefaultContext, countReposptsPublic) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, int64(1), count) | 	assert.Equal(t, int64(1), count) | ||||||
| } | } | ||||||
| @@ -50,14 +49,14 @@ func TestGetPublicRepositoryCount(t *testing.T) { | |||||||
| func TestGetPrivateRepositoryCount(t *testing.T) { | func TestGetPrivateRepositoryCount(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate) | 	count, err := CountRepositories(db.DefaultContext, countReposptsPrivate) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, int64(2), count) | 	assert.Equal(t, int64(2), count) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRepoAPIURL(t *testing.T) { | func TestRepoAPIURL(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	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()) | 	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) { | func TestWatchRepo(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	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}) | 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||||
|  |  | ||||||
| 	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true)) | 	assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, true)) | ||||||
| 	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) | 	unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) | ||||||
| 	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) | 	unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) | ||||||
|  |  | ||||||
| 	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false)) | 	assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, false)) | ||||||
| 	unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) | 	unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) | ||||||
| 	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) | 	unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestMetas(t *testing.T) { | func TestMetas(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	repo := &repo_model.Repository{Name: "testRepo"} | 	repo := &Repository{Name: "testRepo"} | ||||||
| 	repo.Owner = &user_model.User{Name: "testOwner"} | 	repo.Owner = &user_model.User{Name: "testOwner"} | ||||||
| 	repo.OwnerName = repo.Owner.Name | 	repo.OwnerName = repo.Owner.Name | ||||||
|  |  | ||||||
| @@ -90,16 +89,16 @@ func TestMetas(t *testing.T) { | |||||||
| 	assert.Equal(t, "testRepo", metas["repo"]) | 	assert.Equal(t, "testRepo", metas["repo"]) | ||||||
| 	assert.Equal(t, "testOwner", metas["user"]) | 	assert.Equal(t, "testOwner", metas["user"]) | ||||||
|  |  | ||||||
| 	externalTracker := repo_model.RepoUnit{ | 	externalTracker := RepoUnit{ | ||||||
| 		Type: unit.TypeExternalTracker, | 		Type: unit.TypeExternalTracker, | ||||||
| 		Config: &repo_model.ExternalTrackerConfig{ | 		Config: &ExternalTrackerConfig{ | ||||||
| 			ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", | 			ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	testSuccess := func(expectedStyle string) { | 	testSuccess := func(expectedStyle string) { | ||||||
| 		repo.Units = []*repo_model.RepoUnit{&externalTracker} | 		repo.Units = []*RepoUnit{&externalTracker} | ||||||
| 		repo.RenderingMetas = nil | 		repo.commonRenderingMetas = nil | ||||||
| 		metas := repo.ComposeMetas(db.DefaultContext) | 		metas := repo.ComposeMetas(db.DefaultContext) | ||||||
| 		assert.Equal(t, expectedStyle, metas["style"]) | 		assert.Equal(t, expectedStyle, metas["style"]) | ||||||
| 		assert.Equal(t, "testRepo", metas["repo"]) | 		assert.Equal(t, "testRepo", metas["repo"]) | ||||||
| @@ -118,7 +117,7 @@ func TestMetas(t *testing.T) { | |||||||
| 	externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp | 	externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp | ||||||
| 	testSuccess(markup.IssueNameStyleRegexp) | 	testSuccess(markup.IssueNameStyleRegexp) | ||||||
|  |  | ||||||
| 	repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 3) | 	repo, err := GetRepositoryByID(db.DefaultContext, 3) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|  |  | ||||||
| 	metas = repo.ComposeMetas(db.DefaultContext) | 	metas = repo.ComposeMetas(db.DefaultContext) | ||||||
| @@ -132,7 +131,7 @@ func TestGetRepositoryByURL(t *testing.T) { | |||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	t.Run("InvalidPath", func(t *testing.T) { | 	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.Nil(t, repo) | ||||||
| 		assert.Error(t, err) | 		assert.Error(t, err) | ||||||
| @@ -140,7 +139,7 @@ func TestGetRepositoryByURL(t *testing.T) { | |||||||
|  |  | ||||||
| 	t.Run("ValidHttpURL", func(t *testing.T) { | 	t.Run("ValidHttpURL", func(t *testing.T) { | ||||||
| 		test := func(t *testing.T, url string) { | 		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.NotNil(t, repo) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| @@ -155,7 +154,7 @@ func TestGetRepositoryByURL(t *testing.T) { | |||||||
|  |  | ||||||
| 	t.Run("ValidGitSshURL", func(t *testing.T) { | 	t.Run("ValidGitSshURL", func(t *testing.T) { | ||||||
| 		test := func(t *testing.T, url string) { | 		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.NotNil(t, repo) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| @@ -173,7 +172,7 @@ func TestGetRepositoryByURL(t *testing.T) { | |||||||
|  |  | ||||||
| 	t.Run("ValidImplicitSshURL", func(t *testing.T) { | 	t.Run("ValidImplicitSshURL", func(t *testing.T) { | ||||||
| 		test := func(t *testing.T, url string) { | 		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.NotNil(t, repo) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| @@ -200,21 +199,21 @@ func TestComposeSSHCloneURL(t *testing.T) { | |||||||
| 	setting.SSH.Domain = "domain" | 	setting.SSH.Domain = "domain" | ||||||
| 	setting.SSH.Port = 22 | 	setting.SSH.Port = 22 | ||||||
| 	setting.Repository.UseCompatSSHURI = false | 	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 | 	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 | 	// test SSH_DOMAIN while use non-standard SSH port | ||||||
| 	setting.SSH.Port = 123 | 	setting.SSH.Port = 123 | ||||||
| 	setting.Repository.UseCompatSSHURI = false | 	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 | 	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 | 	// test IPv6 SSH_DOMAIN | ||||||
| 	setting.Repository.UseCompatSSHURI = false | 	setting.Repository.UseCompatSSHURI = false | ||||||
| 	setting.SSH.Domain = "::1" | 	setting.SSH.Domain = "::1" | ||||||
| 	setting.SSH.Port = 22 | 	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 | 	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" | 	"bytes" | ||||||
| 	"io" | 	"io" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/markup/common" | 	"code.gitea.io/gitea/modules/markup/common" | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/net/html" | 	"golang.org/x/net/html" | ||||||
| 	"golang.org/x/net/html/atom" | 	"golang.org/x/net/html/atom" | ||||||
| @@ -25,7 +25,27 @@ const ( | |||||||
| 	IssueNameStyleRegexp       = "regexp" | 	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. | 	// NOTE: All below regex matching do not perform any extra validation. | ||||||
| 	// Thus a link is produced even if the linked entity does not exist. | 	// Thus a link is produced even if the linked entity does not exist. | ||||||
| 	// While fast, this is also incorrect and lead to false positives. | 	// 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 | 	// 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 | 	// 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. | 	// 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 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 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 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 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, | 	// emailRegex is definitely not perfect with edge cases, | ||||||
| 	// it is still accepted by the CommonMark specification, as well as the HTML5 spec: | 	// it is still accepted by the CommonMark specification, as well as the HTML5 spec: | ||||||
| 	//   http://spec.commonmark.org/0.28/#email-address | 	//   http://spec.commonmark.org/0.28/#email-address | ||||||
| 	//   https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) | 	//   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 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 find emoji by alias like :smile: | ||||||
| 	emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) | 	v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) | ||||||
| ) |  | ||||||
|  |  | ||||||
| // CSS class for action keywords (e.g. "closes: #1") | 	// example: https://domain/org/repo/pulls/27#hash | ||||||
| const keywordClass = "issue-keyword" | 	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. | // IsFullURLBytes reports whether link fits valid format. | ||||||
| func IsFullURLBytes(link []byte) bool { | func IsFullURLBytes(link []byte) bool { | ||||||
| 	return fullURLPattern.Match(link) | 	return globalVars().fullURLPattern.Match(link) | ||||||
| } | } | ||||||
|  |  | ||||||
| func IsFullURLString(link string) bool { | func IsFullURLString(link string) bool { | ||||||
| 	return fullURLPattern.MatchString(link) | 	return globalVars().fullURLPattern.MatchString(link) | ||||||
| } | } | ||||||
|  |  | ||||||
| func IsNonEmptyRelativePath(link string) bool { | func IsNonEmptyRelativePath(link string) bool { | ||||||
| 	return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#' | 	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 | // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text | ||||||
| func CustomLinkURLSchemes(schemes []string) { | func CustomLinkURLSchemes(schemes []string) { | ||||||
| 	schemes = append(schemes, "http", "https") | 	schemes = append(schemes, "http", "https") | ||||||
| @@ -197,13 +194,6 @@ func RenderCommitMessage( | |||||||
| 	content string, | 	content string, | ||||||
| ) (string, error) { | ) (string, error) { | ||||||
| 	procs := commitMessageProcessors | 	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) | 	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. | // which changes every text node into a link to the passed default link. | ||||||
| func RenderCommitMessageSubject( | func RenderCommitMessageSubject( | ||||||
| 	ctx *RenderContext, | 	ctx *RenderContext, | ||||||
| 	content string, | 	defaultLink, content string, | ||||||
| ) (string, error) { | ) (string, error) { | ||||||
| 	procs := commitMessageSubjectProcessors | 	procs := slices.Clone(commitMessageSubjectProcessors) | ||||||
| 	if ctx.DefaultLink != "" { | 	procs = append(procs, func(ctx *RenderContext, node *html.Node) { | ||||||
| 		// we don't have to fear data races, because being | 		ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data} | ||||||
| 		// commitMessageSubjectProcessors of fixed len and cap, every time we | 		node.Type = html.ElementNode | ||||||
| 		// append something to it the slice is realloc+copied, so append always | 		node.Data = "a" | ||||||
| 		// generates the slice ex-novo. | 		node.DataAtom = atom.A | ||||||
| 		procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) | 		node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} | ||||||
| 	} | 		node.FirstChild, node.LastChild = ch, ch | ||||||
|  | 	}) | ||||||
| 	return renderProcessString(ctx, procs, content) | 	return renderProcessString(ctx, procs, content) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -249,10 +240,8 @@ func RenderIssueTitle( | |||||||
| 	ctx *RenderContext, | 	ctx *RenderContext, | ||||||
| 	title string, | 	title string, | ||||||
| ) (string, error) { | ) (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{ | 	return renderProcessString(ctx, []processor{ | ||||||
| 		issueIndexPatternProcessor, |  | ||||||
| 		commitCrossReferencePatternProcessor, |  | ||||||
| 		hashCurrentPatternProcessor, |  | ||||||
| 		emojiShortCodeProcessor, | 		emojiShortCodeProcessor, | ||||||
| 		emojiProcessor, | 		emojiProcessor, | ||||||
| 	}, title) | 	}, title) | ||||||
| @@ -288,11 +277,6 @@ func RenderEmoji( | |||||||
| 	return renderProcessString(ctx, emojiProcessors, content) | 	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 { | func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { | ||||||
| 	defer ctx.Cancel() | 	defer ctx.Cancel() | ||||||
| 	// FIXME: don't read all content to memory | 	// 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>" | 		// prepend "<html><body>" | ||||||
| 		strings.NewReader("<html><body>"), | 		strings.NewReader("<html><body>"), | ||||||
| 		// Strip out nuls - they're always invalid | 		// 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 | 		// close the tags | ||||||
| 		strings.NewReader("</body></html>"), | 		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 | 	// Add user-content- to IDs and "#" links if they don't already have them | ||||||
| 	for idx, attr := range node.Attr { | 	for idx, attr := range node.Attr { | ||||||
| 		val := strings.TrimPrefix(attr.Val, "#") | 		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 { | 		if attr.Key == "id" && notHasPrefix { | ||||||
| 			node.Attr[idx].Val = "user-content-" + attr.Val | 			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) { | func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { | ||||||
| 	m := anyHashPattern.FindStringSubmatchIndex(s) | 	m := globalVars().anyHashPattern.FindStringSubmatchIndex(s) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return ret, false | 		return ret, false | ||||||
| 	} | 	} | ||||||
| @@ -120,7 +120,7 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 			node = node.NextSibling | 			node = node.NextSibling | ||||||
| 			continue | 			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 | 		if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match | ||||||
| 			node = node.NextSibling | 			node = node.NextSibling | ||||||
| 			continue | 			continue | ||||||
| @@ -173,7 +173,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 		ctx.ShaExistCache = make(map[string]bool) | 		ctx.ShaExistCache = make(map[string]bool) | ||||||
| 	} | 	} | ||||||
| 	for node != nil && node != next && start < len(node.Data) { | 	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 { | 		if m == nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import "golang.org/x/net/html" | |||||||
| func emailAddressProcessor(ctx *RenderContext, node *html.Node) { | func emailAddressProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	next := node.NextSibling | 	next := node.NextSibling | ||||||
| 	for node != nil && node != next { | 	for node != nil && node != next { | ||||||
| 		m := emailRegex.FindStringSubmatchIndex(node.Data) | 		m := globalVars().emailRegex.FindStringSubmatchIndex(node.Data) | ||||||
| 		if m == nil { | 		if m == nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 	start := 0 | 	start := 0 | ||||||
| 	next := node.NextSibling | 	next := node.NextSibling | ||||||
| 	for node != nil && node != next && start < len(node.Data) { | 	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 { | 		if m == nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ var numericMetas = map[string]string{ | |||||||
| 	"user":                         "someUser", | 	"user":                         "someUser", | ||||||
| 	"repo":                         "someRepo", | 	"repo":                         "someRepo", | ||||||
| 	"style":                        IssueNameStyleNumeric, | 	"style":                        IssueNameStyleNumeric, | ||||||
|  | 	"markupAllowShortIssuePattern": "true", | ||||||
| } | } | ||||||
|  |  | ||||||
| var alphanumericMetas = map[string]string{ | var alphanumericMetas = map[string]string{ | ||||||
| @@ -51,6 +52,7 @@ var alphanumericMetas = map[string]string{ | |||||||
| 	"user":                         "someUser", | 	"user":                         "someUser", | ||||||
| 	"repo":                         "someRepo", | 	"repo":                         "someRepo", | ||||||
| 	"style":                        IssueNameStyleAlphanumeric, | 	"style":                        IssueNameStyleAlphanumeric, | ||||||
|  | 	"markupAllowShortIssuePattern": "true", | ||||||
| } | } | ||||||
|  |  | ||||||
| var regexpMetas = map[string]string{ | var regexpMetas = map[string]string{ | ||||||
| @@ -64,6 +66,13 @@ var regexpMetas = map[string]string{ | |||||||
| var localMetas = map[string]string{ | var localMetas = map[string]string{ | ||||||
| 	"user":                         "test-owner", | 	"user":                         "test-owner", | ||||||
| 	"repo":                         "test-repo", | 	"repo":                         "test-repo", | ||||||
|  | 	"markupAllowShortIssuePattern": "true", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var localWikiMetas = map[string]string{ | ||||||
|  | 	"user":              "test-owner", | ||||||
|  | 	"repo":              "test-repo", | ||||||
|  | 	"markupContentMode": "wiki", | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRender_IssueIndexPattern(t *testing.T) { | func TestRender_IssueIndexPattern(t *testing.T) { | ||||||
| @@ -126,7 +135,6 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | |||||||
| 		testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ | 		testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ | ||||||
| 			Ctx:   git.DefaultContext, | 			Ctx:   git.DefaultContext, | ||||||
| 			Metas: localMetas, | 			Metas: localMetas, | ||||||
| 			ContentMode: RenderContentAsComment, |  | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		class := "ref-issue" | 		class := "ref-issue" | ||||||
| @@ -141,7 +149,6 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | |||||||
| 		testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ | 		testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ | ||||||
| 			Ctx:   git.DefaultContext, | 			Ctx:   git.DefaultContext, | ||||||
| 			Metas: numericMetas, | 			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 | 	setting.AppURL = TestAppURL | ||||||
| 	metas := map[string]string{ | 	metas := map[string]string{ | ||||||
| 		"format": "https://someurl.com/{user}/{repo}/{index}", | 		"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) { | func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { | ||||||
| 	ctx.Links.AbsolutePrefix = true | 	ctx.Links.AbsolutePrefix = true | ||||||
| 	if ctx.Links.Base == "" { | 	if ctx.Links.Base == "" { | ||||||
| @@ -318,8 +341,7 @@ func TestRender_AutoLink(t *testing.T) { | |||||||
| 			Links: Links{ | 			Links: Links{ | ||||||
| 				Base: TestRepoURL, | 				Base: TestRepoURL, | ||||||
| 			}, | 			}, | ||||||
| 			Metas:       localMetas, | 			Metas: localWikiMetas, | ||||||
| 			ContentMode: RenderContentAsWiki, |  | ||||||
| 		}, strings.NewReader(input), &buffer) | 		}, strings.NewReader(input), &buffer) | ||||||
| 		assert.Equal(t, err, nil) | 		assert.Equal(t, err, nil) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) | ||||||
| @@ -391,10 +413,10 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, testCase := range trueTestCases { | 	for _, testCase := range trueTestCases { | ||||||
| 		assert.True(t, hashCurrentPattern.MatchString(testCase)) | 		assert.True(t, globalVars().hashCurrentPattern.MatchString(testCase)) | ||||||
| 	} | 	} | ||||||
| 	for _, testCase := range falseTestCases { | 	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 { | 	for _, testCase := range trueTestCases { | ||||||
| 		assert.True(t, shortLinkPattern.MatchString(testCase)) | 		assert.True(t, globalVars().shortLinkPattern.MatchString(testCase)) | ||||||
| 	} | 	} | ||||||
| 	for _, testCase := range falseTestCases { | 	for _, testCase := range falseTestCases { | ||||||
| 		assert.False(t, shortLinkPattern.MatchString(testCase)) | 		assert.False(t, globalVars().shortLinkPattern.MatchString(testCase)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/httplib" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/references" | 	"code.gitea.io/gitea/modules/references" | ||||||
| 	"code.gitea.io/gitea/modules/regexplru" | 	"code.gitea.io/gitea/modules/regexplru" | ||||||
| @@ -23,18 +24,21 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 	} | 	} | ||||||
| 	next := node.NextSibling | 	next := node.NextSibling | ||||||
| 	for node != nil && node != next { | 	for node != nil && node != next { | ||||||
| 		m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) | 		m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data) | ||||||
| 		if m == nil { | 		if m == nil { | ||||||
| 			return | 			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 | 		// 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 { | 		if mDiffView != nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		link := node.Data[m[0]:m[1]] | 		link := node.Data[m[0]:m[1]] | ||||||
|  | 		if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 		text := "#" + node.Data[m[2]:m[3]] | 		text := "#" + node.Data[m[2]:m[3]] | ||||||
| 		// if m[4] and m[5] is not -1, then link is to a comment | 		// if m[4] and m[5] is not -1, then link is to a comment | ||||||
| 		// indicate that in the text by appending (comment) | 		// indicate that in the text by appending (comment) | ||||||
| @@ -67,8 +71,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// crossLinkOnly if not comment and not wiki | 	// crossLinkOnly: do not parse "#123", only parse "owner/repo#123" | ||||||
| 	crossLinkOnly := ctx.ContentMode != RenderContentAsTitle && ctx.ContentMode != RenderContentAsComment && ctx.ContentMode != RenderContentAsWiki | 	// 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 ( | 	var ( | ||||||
| 		found bool | 		found bool | ||||||
|   | |||||||
| @@ -20,9 +20,9 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu | |||||||
| 	isAnchorFragment := link != "" && link[0] == '#' | 	isAnchorFragment := link != "" && link[0] == '#' | ||||||
| 	if !isAnchorFragment && !IsFullURLString(link) { | 	if !isAnchorFragment && !IsFullURLString(link) { | ||||||
| 		linkBase := ctx.Links.Base | 		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 | 			// 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() | 			linkBase = ctx.Links.WikiLink() | ||||||
| 		} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" { | 		} 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}" | 			// 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) { | func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	next := node.NextSibling | 	next := node.NextSibling | ||||||
| 	for node != nil && node != next { | 	for node != nil && node != next { | ||||||
| 		m := shortLinkPattern.FindStringSubmatchIndex(node.Data) | 		m := globalVars().shortLinkPattern.FindStringSubmatchIndex(node.Data) | ||||||
| 		if m == nil { | 		if m == nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 		} | 		} | ||||||
| 		if image { | 		if image { | ||||||
| 			if !absoluteLink { | 			if !absoluteLink { | ||||||
| 				link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), link) | 				link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link) | ||||||
| 			} | 			} | ||||||
| 			title := props["title"] | 			title := props["title"] | ||||||
| 			if 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 | // descriptionLinkProcessor creates links for DescriptionHTML | ||||||
| func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { | func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	next := node.NextSibling | 	next := node.NextSibling | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if IsNonEmptyRelativePath(attr.Val) { | 		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, | 			// By default, the "<img>" tag should also be clickable, | ||||||
| 			// because frontend use `<img>` to paste the re-scaled image into the markdown, | 			// 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 | 			continue | ||||||
| 		} | 		} | ||||||
| 		if IsNonEmptyRelativePath(attr.Val) { | 		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) | 		attr.Val = camoHandleLink(attr.Val) | ||||||
| 		node.Attr[i] = attr | 		node.Attr[i] = attr | ||||||
|   | |||||||
| @@ -27,6 +27,11 @@ var ( | |||||||
| 		"user": testRepoOwnerName, | 		"user": testRepoOwnerName, | ||||||
| 		"repo": testRepoName, | 		"repo": testRepoName, | ||||||
| 	} | 	} | ||||||
|  | 	localWikiMetas = map[string]string{ | ||||||
|  | 		"user":              testRepoOwnerName, | ||||||
|  | 		"repo":              testRepoName, | ||||||
|  | 		"markupContentMode": "wiki", | ||||||
|  | 	} | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type mockRepo struct { | type mockRepo struct { | ||||||
| @@ -413,8 +418,7 @@ func TestRender_ShortLinks(t *testing.T) { | |||||||
| 			Links: markup.Links{ | 			Links: markup.Links{ | ||||||
| 				Base: markup.TestRepoURL, | 				Base: markup.TestRepoURL, | ||||||
| 			}, | 			}, | ||||||
| 			Metas:       localMetas, | 			Metas: localWikiMetas, | ||||||
| 			ContentMode: markup.RenderContentAsWiki, |  | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | 		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{ | 		buffer, err := markdown.RenderString(&markup.RenderContext{ | ||||||
| 			Ctx:   git.DefaultContext, | 			Ctx:   git.DefaultContext, | ||||||
| 			Links: links, | 			Links: links, | ||||||
| 			Metas:       localMetas, | 			Metas: util.Iif(isWiki, localWikiMetas, localMetas), | ||||||
| 			ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsComment), |  | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		return strings.TrimSpace(string(buffer)) | 		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 }` | 				// 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 | 				// 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. | 				// especially in many tests. | ||||||
|  | 				markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"] | ||||||
| 				if markup.RenderBehaviorForTesting.ForceHardLineBreak { | 				if markup.RenderBehaviorForTesting.ForceHardLineBreak { | ||||||
| 					v.SetHardLineBreak(true) | 					v.SetHardLineBreak(true) | ||||||
| 				} else if ctx.ContentMode == markup.RenderContentAsComment { | 				} else if markdownLineBreakStyle == "comment" { | ||||||
| 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) | 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) | ||||||
| 				} else { | 				} else if markdownLineBreakStyle == "document" { | ||||||
| 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) | 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -37,6 +37,12 @@ var localMetas = map[string]string{ | |||||||
| 	"repo": testRepoName, | 	"repo": testRepoName, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var localWikiMetas = map[string]string{ | ||||||
|  | 	"user":              testRepoOwnerName, | ||||||
|  | 	"repo":              testRepoName, | ||||||
|  | 	"markupContentMode": "wiki", | ||||||
|  | } | ||||||
|  |  | ||||||
| type mockRepo struct { | type mockRepo struct { | ||||||
| 	OwnerName string | 	OwnerName string | ||||||
| 	RepoName  string | 	RepoName  string | ||||||
| @@ -75,7 +81,7 @@ func TestRender_StandardLinks(t *testing.T) { | |||||||
| 			Links: markup.Links{ | 			Links: markup.Links{ | ||||||
| 				Base: FullURL, | 				Base: FullURL, | ||||||
| 			}, | 			}, | ||||||
| 			ContentMode: markup.RenderContentAsWiki, | 			Metas: localWikiMetas, | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | ||||||
| @@ -308,8 +314,7 @@ func TestTotal_RenderWiki(t *testing.T) { | |||||||
| 				Base: FullURL, | 				Base: FullURL, | ||||||
| 			}, | 			}, | ||||||
| 			Repo:  newMockRepo(testRepoOwnerName, testRepoName), | 			Repo:  newMockRepo(testRepoOwnerName, testRepoName), | ||||||
| 			Metas:       localMetas, | 			Metas: localWikiMetas, | ||||||
| 			ContentMode: markup.RenderContentAsWiki, |  | ||||||
| 		}, sameCases[i]) | 		}, sameCases[i]) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, answers[i], string(line)) | 		assert.Equal(t, answers[i], string(line)) | ||||||
| @@ -334,7 +339,7 @@ func TestTotal_RenderWiki(t *testing.T) { | |||||||
| 			Links: markup.Links{ | 			Links: markup.Links{ | ||||||
| 				Base: FullURL, | 				Base: FullURL, | ||||||
| 			}, | 			}, | ||||||
| 			ContentMode: markup.RenderContentAsWiki, | 			Metas: localWikiMetas, | ||||||
| 		}, testCases[i]) | 		}, testCases[i]) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.EqualValues(t, testCases[i+1], string(line)) | 		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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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://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://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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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://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://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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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="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="/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/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/> | 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/> | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/> | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span><br/> | <span class="emoji" aria-label="thumbs up">👍</span><br/> | ||||||
| <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><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{ | 		result, err := markdown.RenderString(&markup.RenderContext{ | ||||||
| 			Ctx:   context.Background(), | 			Ctx:   context.Background(), | ||||||
| 			Links: c.Links, | 			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) | 		}, input) | ||||||
| 		assert.NoError(t, err, "Unexpected error in testcase: %v", i) | 		assert.NoError(t, err, "Unexpected error in testcase: %v", i) | ||||||
| 		assert.Equal(t, c.Expected, string(result), "Unexpected result 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 | 	// Check if the destination is a real link | ||||||
| 	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) { | 	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) { | ||||||
| 		v.Destination = []byte(giteautil.URLJoin( | 		v.Destination = []byte(giteautil.URLJoin( | ||||||
| 			ctx.Links.ResolveMediaLink(ctx.ContentMode == markup.RenderContentAsWiki), | 			ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), | ||||||
| 			strings.TrimLeft(string(v.Destination), "/"), | 			strings.TrimLeft(string(v.Destination), "/"), | ||||||
| 		)) | 		)) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -144,15 +144,14 @@ func (r *Writer) resolveLink(kind, link string) string { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		base := r.Ctx.Links.Base | 		base := r.Ctx.Links.Base | ||||||
| 		isWiki := r.Ctx.ContentMode == markup.RenderContentAsWiki | 		if r.Ctx.IsMarkupContentWiki() { | ||||||
| 		if isWiki { |  | ||||||
| 			base = r.Ctx.Links.WikiLink() | 			base = r.Ctx.Links.WikiLink() | ||||||
| 		} else if r.Ctx.Links.HasBranchInfo() { | 		} else if r.Ctx.Links.HasBranchInfo() { | ||||||
| 			base = r.Ctx.Links.SrcLink() | 			base = r.Ctx.Links.SrcLink() | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if kind == "image" || kind == "video" { | 		if kind == "image" || kind == "video" { | ||||||
| 			base = r.Ctx.Links.ResolveMediaLink(isWiki) | 			base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki()) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		link = util.URLJoin(base, link) | 		link = util.URLJoin(base, link) | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ func TestRender_StandardLinks(t *testing.T) { | |||||||
| 				Base:       "/relative-path", | 				Base:       "/relative-path", | ||||||
| 				BranchPath: "branch/main", | 				BranchPath: "branch/main", | ||||||
| 			}, | 			}, | ||||||
| 			ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault), | 			Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")}, | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
|   | |||||||
| @@ -27,15 +27,6 @@ const ( | |||||||
| 	RenderMetaAsTable   RenderMetaMode = "table" | 	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 { | var RenderBehaviorForTesting struct { | ||||||
| 	// Markdown line break rendering has 2 default behaviors: | 	// Markdown line break rendering has 2 default behaviors: | ||||||
| 	// * Use hard: replace "\n" with "<br>" for comments, setting.Markdown.EnableHardLineBreakInComments=true | 	// * 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 | 	// for file mode, it could be left as empty, and will be detected by file extension in RelativePath | ||||||
| 	MarkupType string | 	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 | 	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 | 	GitRepo          *git.Repository | ||||||
| 	Repo             gitrepo.Repository | 	Repo             gitrepo.Repository | ||||||
| 	ShaExistCache    map[string]bool | 	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. | // Render renders markup file to HTML with all specific handling stuff. | ||||||
| func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	if ctx.MarkupType == "" && ctx.RelativePath != "" { | 	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 { | type Links struct { | ||||||
| 	AbsolutePrefix bool   // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias | 	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" | 	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 | 	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) | 	msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace) | ||||||
| 	if len(msgLine) == 0 { | 	if len(msgLine) == 0 { | ||||||
| 		return template.HTML("") | 		return "" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// we can safely assume that it will not return any error, since there | 	// we can safely assume that it will not return any error, since there | ||||||
| 	// shouldn't be any special HTML. | 	// shouldn't be any special HTML. | ||||||
| 	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ | 	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ | ||||||
| 		Ctx:   ut.ctx, | 		Ctx:   ut.ctx, | ||||||
| 		DefaultLink: urlDefault, |  | ||||||
| 		Metas: metas, | 		Metas: metas, | ||||||
| 	}, template.HTMLEscapeString(msgLine)) | 	}, urlDefault, template.HTMLEscapeString(msgLine)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderCommitMessageSubject: %v", err) | 		log.Error("RenderCommitMessageSubject: %v", err) | ||||||
| 		return template.HTML("") | 		return "" | ||||||
| 	} | 	} | ||||||
| 	return renderCodeBlock(template.HTML(renderedMessage)) | 	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{ | 	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||||
| 		Ctx:   ut.ctx, | 		Ctx:   ut.ctx, | ||||||
| 		Metas: metas, | 		Metas: metas, | ||||||
| 		ContentMode: markup.RenderContentAsComment, |  | ||||||
| 	}, template.HTMLEscapeString(msgLine)) | 	}, template.HTMLEscapeString(msgLine)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderCommitMessage: %v", err) | 		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 { | func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { | ||||||
| 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ | 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ | ||||||
| 		Ctx:   ut.ctx, | 		Ctx:   ut.ctx, | ||||||
| 		ContentMode: markup.RenderContentAsTitle, |  | ||||||
| 		Metas: metas, | 		Metas: metas, | ||||||
| 	}, template.HTMLEscapeString(text)) | 	}, template.HTMLEscapeString(text)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -212,7 +209,7 @@ func reactionToEmoji(reaction string) template.HTML { | |||||||
| func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive | func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive | ||||||
| 	output, err := markdown.RenderString(&markup.RenderContext{ | 	output, err := markdown.RenderString(&markup.RenderContext{ | ||||||
| 		Ctx:   ut.ctx, | 		Ctx:   ut.ctx, | ||||||
| 		Metas: map[string]string{"mode": "document"}, | 		Metas: markup.ComposeSimpleDocumentMetas(), | ||||||
| 	}, input) | 	}, input) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderString: %v", err) | 		log.Error("RenderString: %v", err) | ||||||
|   | |||||||
| @@ -50,7 +50,8 @@ var testMetas = map[string]string{ | |||||||
| 	"user":                         "user13", | 	"user":                         "user13", | ||||||
| 	"repo":                         "repo11", | 	"repo":                         "repo11", | ||||||
| 	"repoPath":                     "../../tests/gitea-repositories-meta/user13/repo11.git/", | 	"repoPath":                     "../../tests/gitea-repositories-meta/user13/repo11.git/", | ||||||
| 	"mode":     "comment", | 	"markdownLineBreakStyle":       "comment", | ||||||
|  | 	"markupAllowShortIssuePattern": "true", | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestMain(m *testing.M) { | func TestMain(m *testing.M) { | ||||||
| @@ -76,7 +77,6 @@ func TestRenderCommitBody(t *testing.T) { | |||||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||||
| 	type args struct { | 	type args struct { | ||||||
| 		msg string | 		msg string | ||||||
| 		metas map[string]string |  | ||||||
| 	} | 	} | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		name string | 		name string | ||||||
| @@ -108,7 +108,7 @@ func TestRenderCommitBody(t *testing.T) { | |||||||
| 	ut := newTestRenderUtils() | 	ut := newTestRenderUtils() | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		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) { | 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)) | 	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> | <span class="emoji" aria-label="thumbs up">👍</span> | ||||||
| mail@domain.com | mail@domain.com | ||||||
| @mention-user test | @mention-user test | ||||||
| <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> | #123 | ||||||
|   space<SPACE><SPACE> |   space<SPACE><SPACE> | ||||||
| ` | ` | ||||||
| 	expected = strings.ReplaceAll(expected, "<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) { | func TestRenderMarkdownToHtml(t *testing.T) { | ||||||
|   | |||||||
| @@ -47,11 +47,12 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa | |||||||
| 	switch mode { | 	switch mode { | ||||||
| 	case "gfm": // legacy mode, do nothing | 	case "gfm": // legacy mode, do nothing | ||||||
| 	case "comment": | 	case "comment": | ||||||
| 		renderCtx.ContentMode = markup.RenderContentAsComment | 		renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "comment"} | ||||||
| 	case "wiki": | 	case "wiki": | ||||||
| 		renderCtx.ContentMode = markup.RenderContentAsWiki | 		renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"} | ||||||
| 	case "file": | 	case "file": | ||||||
| 		// render the repo file content by its extension | 		// render the repo file content by its extension | ||||||
|  | 		renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document"} | ||||||
| 		renderCtx.MarkupType = "" | 		renderCtx.MarkupType = "" | ||||||
| 		renderCtx.RelativePath = filePath | 		renderCtx.RelativePath = filePath | ||||||
| 		renderCtx.InStandalonePage = true | 		renderCtx.InStandalonePage = true | ||||||
| @@ -74,10 +75,12 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa | |||||||
|  |  | ||||||
| 	if repo != nil && repo.Repository != nil { | 	if repo != nil && repo.Repository != nil { | ||||||
| 		renderCtx.Repo = repo.Repository | 		renderCtx.Repo = repo.Repository | ||||||
| 		if renderCtx.ContentMode == markup.RenderContentAsComment { | 		if mode == "file" { | ||||||
| 			renderCtx.Metas = repo.Repository.ComposeMetas(ctx) |  | ||||||
| 		} else { |  | ||||||
| 			renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx) | 			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 { | 	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{ | 		Links: markup.Links{ | ||||||
| 			Base: act.GetRepoLink(ctx), | 			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), | 			"user": act.GetRepoUserName(ctx), | ||||||
| 			"repo": act.GetRepoName(ctx), | 			"repo": act.GetRepoName(ctx), | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -46,9 +46,7 @@ func showUserFeed(ctx *context.Context, formatType string) { | |||||||
| 		Links: markup.Links{ | 		Links: markup.Links{ | ||||||
| 			Base: ctx.ContextUser.HTMLURL(), | 			Base: ctx.ContextUser.HTMLURL(), | ||||||
| 		}, | 		}, | ||||||
| 		Metas: map[string]string{ | 		Metas: markup.ComposeSimpleDocumentMetas(), | ||||||
| 			"user": ctx.ContextUser.GetDisplayName(), |  | ||||||
| 		}, |  | ||||||
| 	}, ctx.ContextUser.Description) | 	}, ctx.ContextUser.Description) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("RenderString", err) | 		ctx.ServerError("RenderString", err) | ||||||
|   | |||||||
| @@ -189,7 +189,7 @@ func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool { | |||||||
| 				Base:       profileDbRepo.Link(), | 				Base:       profileDbRepo.Link(), | ||||||
| 				BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), | 				BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), | ||||||
| 			}, | 			}, | ||||||
| 			Metas: map[string]string{"mode": "document"}, | 			Metas: markup.ComposeSimpleDocumentMetas(), | ||||||
| 		}, bytes); err != nil { | 		}, bytes); err != nil { | ||||||
| 			log.Error("failed to RenderString: %v", err) | 			log.Error("failed to RenderString: %v", err) | ||||||
| 		} else { | 		} else { | ||||||
|   | |||||||
| @@ -290,8 +290,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { | |||||||
|  |  | ||||||
| 	rctx := &markup.RenderContext{ | 	rctx := &markup.RenderContext{ | ||||||
| 		Ctx:   ctx, | 		Ctx:   ctx, | ||||||
| 		ContentMode: markup.RenderContentAsWiki, | 		Metas: ctx.Repo.Repository.ComposeWikiMetas(ctx), | ||||||
| 		Metas:       ctx.Repo.Repository.ComposeDocumentMetas(ctx), |  | ||||||
| 		Links: markup.Links{ | 		Links: markup.Links{ | ||||||
| 			Base: ctx.Repo.RepoLink, | 			Base: ctx.Repo.RepoLink, | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { | |||||||
| 	ctx.Data["OpenIDs"] = openIDs | 	ctx.Data["OpenIDs"] = openIDs | ||||||
| 	if len(ctx.ContextUser.Description) != 0 { | 	if len(ctx.ContextUser.Description) != 0 { | ||||||
| 		content, err := markdown.RenderString(&markup.RenderContext{ | 		content, err := markdown.RenderString(&markup.RenderContext{ | ||||||
| 			Metas: map[string]string{"mode": "document"}, | 			Metas: markup.ComposeSimpleDocumentMetas(), | ||||||
| 			Ctx:   ctx, | 			Ctx:   ctx, | ||||||
| 		}, ctx.ContextUser.Description) | 		}, ctx.ContextUser.Description) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ | |||||||
| 		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4"> | 		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4"> | ||||||
| 			{{if .ReadmeInList}} | 			{{if .ReadmeInList}} | ||||||
| 				{{svg "octicon-book" 16 "tw-mr-2"}} | 				{{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}} | 			{{else}} | ||||||
| 				{{template "repo/file_info" .}} | 				{{template "repo/file_info" .}} | ||||||
| 			{{end}} | 			{{end}} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 wxiaoguang
					wxiaoguang