mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 18:12:20 +01:00 
			
		
		
		
	Merge branch 'main' into feat-32257-add-comments-unchanged-lines-and-show
This commit is contained in:
		
						commit
						b387e41e03
					
				
							
								
								
									
										8
									
								
								.github/workflows/pull-compliance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/pull-compliance.yml
									
									
									
									
										vendored
									
									
								
							| @ -37,7 +37,7 @@ jobs: | ||||
|           python-version: "3.12" | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: pip install poetry | ||||
| @ -66,7 +66,7 @@ jobs: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend | ||||
| @ -137,7 +137,7 @@ jobs: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend | ||||
| @ -186,7 +186,7 @@ jobs: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/pull-e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pull-e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							| @ -23,7 +23,7 @@ jobs: | ||||
|           check-latest: true | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend frontend deps-backend | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/release-nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-nightly.yml
									
									
									
									
										vendored
									
									
								
							| @ -22,7 +22,7 @@ jobs: | ||||
|           check-latest: true | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend deps-backend | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag-rc.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag-rc.yml
									
									
									
									
										vendored
									
									
								
							| @ -23,7 +23,7 @@ jobs: | ||||
|           check-latest: true | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend deps-backend | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag-version.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag-version.yml
									
									
									
									
										vendored
									
									
								
							| @ -25,7 +25,7 @@ jobs: | ||||
|           check-latest: true | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           node-version: 22 | ||||
|           cache: npm | ||||
|           cache-dependency-path: package-lock.json | ||||
|       - run: make deps-frontend deps-backend | ||||
|  | ||||
							
								
								
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							| @ -5,11 +5,11 @@ | ||||
|         "systems": "systems" | ||||
|       }, | ||||
|       "locked": { | ||||
|         "lastModified": 1710146030, | ||||
|         "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", | ||||
|         "lastModified": 1726560853, | ||||
|         "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", | ||||
|         "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
| @ -20,11 +20,11 @@ | ||||
|     }, | ||||
|     "nixpkgs": { | ||||
|       "locked": { | ||||
|         "lastModified": 1720542800, | ||||
|         "narHash": "sha256-ZgnNHuKV6h2+fQ5LuqnUaqZey1Lqqt5dTUAiAnqH0QQ=", | ||||
|         "lastModified": 1731139594, | ||||
|         "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", | ||||
|         "owner": "nixos", | ||||
|         "repo": "nixpkgs", | ||||
|         "rev": "feb2849fdeb70028c70d73b848214b00d324a497", | ||||
|         "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|  | ||||
| @ -261,6 +261,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin | ||||
| } | ||||
| 
 | ||||
| // InsertRun inserts a run | ||||
| // The title will be cut off at 255 characters if it's longer than 255 characters. | ||||
| func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| @ -273,6 +274,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork | ||||
| 		return err | ||||
| 	} | ||||
| 	run.Index = index | ||||
| 	run.Title, _ = util.SplitStringAtByteN(run.Title, 255) | ||||
| 
 | ||||
| 	if err := db.Insert(ctx, run); err != nil { | ||||
| 		return err | ||||
| @ -399,6 +401,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { | ||||
| 	if len(cols) > 0 { | ||||
| 		sess.Cols(cols...) | ||||
| 	} | ||||
| 	run.Title, _ = util.SplitStringAtByteN(run.Title, 255) | ||||
| 	affected, err := sess.Update(run) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|  | ||||
| @ -252,6 +252,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { | ||||
| // UpdateRunner updates runner's information. | ||||
| func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { | ||||
| 	e := db.GetEngine(ctx) | ||||
| 	r.Name, _ = util.SplitStringAtByteN(r.Name, 255) | ||||
| 	var err error | ||||
| 	if len(cols) == 0 { | ||||
| 		_, err = e.ID(r.ID).AllCols().Update(r) | ||||
| @ -278,6 +279,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error { | ||||
| 		// Remove OwnerID to avoid confusion; it's not worth returning an error here. | ||||
| 		t.OwnerID = 0 | ||||
| 	} | ||||
| 	t.Name, _ = util.SplitStringAtByteN(t.Name, 255) | ||||
| 	return db.Insert(ctx, t) | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -12,6 +12,7 @@ import ( | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| ) | ||||
| 
 | ||||
| @ -67,6 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { | ||||
| 
 | ||||
| 	// Loop through each schedule row | ||||
| 	for _, row := range rows { | ||||
| 		row.Title, _ = util.SplitStringAtByteN(row.Title, 255) | ||||
| 		// Create new schedule row | ||||
| 		if err = db.Insert(ctx, row); err != nil { | ||||
| 			return err | ||||
|  | ||||
| @ -251,6 +251,9 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { | ||||
| // GetRepoUserName returns the name of the action repository owner. | ||||
| func (a *Action) GetRepoUserName(ctx context.Context) string { | ||||
| 	a.loadRepo(ctx) | ||||
| 	if a.Repo == nil { | ||||
| 		return "(non-existing-repo)" | ||||
| 	} | ||||
| 	return a.Repo.OwnerName | ||||
| } | ||||
| 
 | ||||
| @ -263,6 +266,9 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string { | ||||
| // GetRepoName returns the name of the action repository. | ||||
| func (a *Action) GetRepoName(ctx context.Context) string { | ||||
| 	a.loadRepo(ctx) | ||||
| 	if a.Repo == nil { | ||||
| 		return "(non-existing-repo)" | ||||
| 	} | ||||
| 	return a.Repo.Name | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -18,6 +18,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| 	"xorm.io/xorm/schemas" | ||||
| ) | ||||
| 
 | ||||
| type ( | ||||
| @ -50,25 +51,64 @@ const ( | ||||
| // Notification represents a notification | ||||
| type Notification struct { | ||||
| 	ID     int64 `xorm:"pk autoincr"` | ||||
| 	UserID int64 `xorm:"INDEX NOT NULL"` | ||||
| 	RepoID int64 `xorm:"INDEX NOT NULL"` | ||||
| 	UserID int64 `xorm:"NOT NULL"` | ||||
| 	RepoID int64 `xorm:"NOT NULL"` | ||||
| 
 | ||||
| 	Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"` | ||||
| 	Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"` | ||||
| 	Status NotificationStatus `xorm:"SMALLINT NOT NULL"` | ||||
| 	Source NotificationSource `xorm:"SMALLINT NOT NULL"` | ||||
| 
 | ||||
| 	IssueID   int64  `xorm:"INDEX NOT NULL"` | ||||
| 	CommitID  string `xorm:"INDEX"` | ||||
| 	IssueID   int64 `xorm:"NOT NULL"` | ||||
| 	CommitID  string | ||||
| 	CommentID int64 | ||||
| 
 | ||||
| 	UpdatedBy int64 `xorm:"INDEX NOT NULL"` | ||||
| 	UpdatedBy int64 `xorm:"NOT NULL"` | ||||
| 
 | ||||
| 	Issue      *issues_model.Issue    `xorm:"-"` | ||||
| 	Repository *repo_model.Repository `xorm:"-"` | ||||
| 	Comment    *issues_model.Comment  `xorm:"-"` | ||||
| 	User       *user_model.User       `xorm:"-"` | ||||
| 
 | ||||
| 	CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` | ||||
| 	UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"` | ||||
| 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` | ||||
| 	UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` | ||||
| } | ||||
| 
 | ||||
| // TableIndices implements xorm's TableIndices interface | ||||
| func (n *Notification) TableIndices() []*schemas.Index { | ||||
| 	indices := make([]*schemas.Index, 0, 8) | ||||
| 	usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType) | ||||
| 	usuuIndex.AddColumn("user_id", "status", "updated_unix") | ||||
| 	indices = append(indices, usuuIndex) | ||||
| 
 | ||||
| 	// Add the individual indices that were previously defined in struct tags | ||||
| 	userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType) | ||||
| 	userIDIndex.AddColumn("user_id") | ||||
| 	indices = append(indices, userIDIndex) | ||||
| 
 | ||||
| 	repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType) | ||||
| 	repoIDIndex.AddColumn("repo_id") | ||||
| 	indices = append(indices, repoIDIndex) | ||||
| 
 | ||||
| 	statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType) | ||||
| 	statusIndex.AddColumn("status") | ||||
| 	indices = append(indices, statusIndex) | ||||
| 
 | ||||
| 	sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType) | ||||
| 	sourceIndex.AddColumn("source") | ||||
| 	indices = append(indices, sourceIndex) | ||||
| 
 | ||||
| 	issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType) | ||||
| 	issueIDIndex.AddColumn("issue_id") | ||||
| 	indices = append(indices, issueIDIndex) | ||||
| 
 | ||||
| 	commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType) | ||||
| 	commitIDIndex.AddColumn("commit_id") | ||||
| 	indices = append(indices, commitIDIndex) | ||||
| 
 | ||||
| 	updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType) | ||||
| 	updatedByIndex.AddColumn("updated_by") | ||||
| 	indices = append(indices, updatedByIndex) | ||||
| 
 | ||||
| 	return indices | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
|  | ||||
| @ -21,6 +21,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/references" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| @ -138,6 +139,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 
 | ||||
| 	issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) | ||||
| 	if err = UpdateIssueCols(ctx, issue, "name"); err != nil { | ||||
| 		return fmt.Errorf("updateIssueCols: %w", err) | ||||
| 	} | ||||
| @ -386,6 +388,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue | ||||
| } | ||||
| 
 | ||||
| // NewIssue creates new issue with labels for repository. | ||||
| // The title will be cut off at 255 characters if it's longer than 255 characters. | ||||
| func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| @ -399,6 +402,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la | ||||
| 	} | ||||
| 
 | ||||
| 	issue.Index = idx | ||||
| 	issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) | ||||
| 
 | ||||
| 	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ | ||||
| 		Repo:        repo, | ||||
|  | ||||
| @ -572,6 +572,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss | ||||
| 	} | ||||
| 
 | ||||
| 	issue.Index = idx | ||||
| 	issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) | ||||
| 
 | ||||
| 	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ | ||||
| 		Repo:        repo, | ||||
|  | ||||
| @ -366,6 +366,7 @@ func prepareMigrationTasks() []*migration { | ||||
| 		newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection), | ||||
| 		newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate), | ||||
| 		newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), | ||||
| 		newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), | ||||
| 	} | ||||
| 	return preparedMigrations | ||||
| } | ||||
|  | ||||
							
								
								
									
										77
									
								
								models/migrations/v1_23/v309.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								models/migrations/v1_23/v309.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package v1_23 //nolint | ||||
| 
 | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 
 | ||||
| 	"xorm.io/xorm" | ||||
| 	"xorm.io/xorm/schemas" | ||||
| ) | ||||
| 
 | ||||
| type improveNotificationTableIndicesAction struct { | ||||
| 	ID     int64 `xorm:"pk autoincr"` | ||||
| 	UserID int64 `xorm:"NOT NULL"` | ||||
| 	RepoID int64 `xorm:"NOT NULL"` | ||||
| 
 | ||||
| 	Status uint8 `xorm:"SMALLINT NOT NULL"` | ||||
| 	Source uint8 `xorm:"SMALLINT NOT NULL"` | ||||
| 
 | ||||
| 	IssueID   int64 `xorm:"NOT NULL"` | ||||
| 	CommitID  string | ||||
| 	CommentID int64 | ||||
| 
 | ||||
| 	UpdatedBy int64 `xorm:"NOT NULL"` | ||||
| 
 | ||||
| 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` | ||||
| 	UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` | ||||
| } | ||||
| 
 | ||||
| // TableName sets the name of this table | ||||
| func (*improveNotificationTableIndicesAction) TableName() string { | ||||
| 	return "notification" | ||||
| } | ||||
| 
 | ||||
| // TableIndices implements xorm's TableIndices interface | ||||
| func (*improveNotificationTableIndicesAction) TableIndices() []*schemas.Index { | ||||
| 	indices := make([]*schemas.Index, 0, 8) | ||||
| 	usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType) | ||||
| 	usuuIndex.AddColumn("user_id", "status", "updated_unix") | ||||
| 	indices = append(indices, usuuIndex) | ||||
| 
 | ||||
| 	// Add the individual indices that were previously defined in struct tags | ||||
| 	userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType) | ||||
| 	userIDIndex.AddColumn("user_id") | ||||
| 	indices = append(indices, userIDIndex) | ||||
| 
 | ||||
| 	repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType) | ||||
| 	repoIDIndex.AddColumn("repo_id") | ||||
| 	indices = append(indices, repoIDIndex) | ||||
| 
 | ||||
| 	statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType) | ||||
| 	statusIndex.AddColumn("status") | ||||
| 	indices = append(indices, statusIndex) | ||||
| 
 | ||||
| 	sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType) | ||||
| 	sourceIndex.AddColumn("source") | ||||
| 	indices = append(indices, sourceIndex) | ||||
| 
 | ||||
| 	issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType) | ||||
| 	issueIDIndex.AddColumn("issue_id") | ||||
| 	indices = append(indices, issueIDIndex) | ||||
| 
 | ||||
| 	commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType) | ||||
| 	commitIDIndex.AddColumn("commit_id") | ||||
| 	indices = append(indices, commitIDIndex) | ||||
| 
 | ||||
| 	updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType) | ||||
| 	updatedByIndex.AddColumn("updated_by") | ||||
| 	indices = append(indices, updatedByIndex) | ||||
| 
 | ||||
| 	return indices | ||||
| } | ||||
| 
 | ||||
| func ImproveNotificationTableIndices(x *xorm.Engine) error { | ||||
| 	return x.Sync(&improveNotificationTableIndicesAction{}) | ||||
| } | ||||
| @ -1,78 +0,0 @@ | ||||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package organization | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| // MinimalOrg represents a simple organization with only the needed columns | ||||
| type MinimalOrg = Organization | ||||
| 
 | ||||
| // GetUserOrgsList returns all organizations the given user has access to | ||||
| func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) { | ||||
| 	schema, err := db.TableInfo(new(user_model.User)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	outputCols := []string{ | ||||
| 		"id", | ||||
| 		"name", | ||||
| 		"full_name", | ||||
| 		"visibility", | ||||
| 		"avatar", | ||||
| 		"avatar_email", | ||||
| 		"use_custom_avatar", | ||||
| 	} | ||||
| 
 | ||||
| 	groupByCols := &strings.Builder{} | ||||
| 	for _, col := range outputCols { | ||||
| 		fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col) | ||||
| 	} | ||||
| 	groupByStr := groupByCols.String() | ||||
| 	groupByStr = groupByStr[0 : len(groupByStr)-1] | ||||
| 
 | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 	sess = sess.Select(groupByStr+", count(distinct repo_id) as org_count"). | ||||
| 		Table("user"). | ||||
| 		Join("INNER", "team", "`team`.org_id = `user`.id"). | ||||
| 		Join("INNER", "team_user", "`team`.id = `team_user`.team_id"). | ||||
| 		Join("LEFT", builder. | ||||
| 			Select("id as repo_id, owner_id as repo_owner_id"). | ||||
| 			From("repository"). | ||||
| 			Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)), "`repository`.repo_owner_id = `team`.org_id"). | ||||
| 		Where("`team_user`.uid = ?", user.ID). | ||||
| 		GroupBy(groupByStr) | ||||
| 
 | ||||
| 	type OrgCount struct { | ||||
| 		Organization `xorm:"extends"` | ||||
| 		OrgCount     int | ||||
| 	} | ||||
| 
 | ||||
| 	orgCounts := make([]*OrgCount, 0, 10) | ||||
| 
 | ||||
| 	if err := sess. | ||||
| 		Asc("`user`.name"). | ||||
| 		Find(&orgCounts); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	orgs := make([]*MinimalOrg, len(orgCounts)) | ||||
| 	for i, orgCount := range orgCounts { | ||||
| 		orgCount.Organization.NumRepos = orgCount.OrgCount | ||||
| 		orgs[i] = &orgCount.Organization | ||||
| 	} | ||||
| 
 | ||||
| 	return orgs, nil | ||||
| } | ||||
| @ -25,13 +25,6 @@ import ( | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
| 
 | ||||
| // ________                            .__                __  .__ | ||||
| // \_____  \_______  _________    ____ |__|____________ _/  |_|__| ____   ____ | ||||
| //  /   |   \_  __ \/ ___\__  \  /    \|  \___   /\__  \\   __\  |/  _ \ /    \ | ||||
| // /    |    \  | \/ /_/  > __ \|   |  \  |/    /  / __ \|  | |  (  <_> )   |  \ | ||||
| // \_______  /__|  \___  (____  /___|  /__/_____ \(____  /__| |__|\____/|___|  / | ||||
| //         \/     /_____/     \/     \/         \/     \/                    \/ | ||||
| 
 | ||||
| // ErrOrgNotExist represents a "OrgNotExist" kind of error. | ||||
| type ErrOrgNotExist struct { | ||||
| 	ID   int64 | ||||
| @ -465,42 +458,6 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u | ||||
| 		And("team_user.org_id = ?", orgID).Find(&users) | ||||
| } | ||||
| 
 | ||||
| // SearchOrganizationsOptions options to filter organizations | ||||
| type SearchOrganizationsOptions struct { | ||||
| 	db.ListOptions | ||||
| 	All bool | ||||
| } | ||||
| 
 | ||||
| // FindOrgOptions finds orgs options | ||||
| type FindOrgOptions struct { | ||||
| 	db.ListOptions | ||||
| 	UserID         int64 | ||||
| 	IncludePrivate bool | ||||
| } | ||||
| 
 | ||||
| func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { | ||||
| 	cond := builder.Eq{"uid": userID} | ||||
| 	if !includePrivate { | ||||
| 		cond["is_public"] = true | ||||
| 	} | ||||
| 	return builder.Select("org_id").From("org_user").Where(cond) | ||||
| } | ||||
| 
 | ||||
| func (opts FindOrgOptions) ToConds() builder.Cond { | ||||
| 	var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization} | ||||
| 	if opts.UserID > 0 { | ||||
| 		cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) | ||||
| 	} | ||||
| 	if !opts.IncludePrivate { | ||||
| 		cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) | ||||
| 	} | ||||
| 	return cond | ||||
| } | ||||
| 
 | ||||
| func (opts FindOrgOptions) ToOrders() string { | ||||
| 	return "`user`.name ASC" | ||||
| } | ||||
| 
 | ||||
| // HasOrgOrUserVisible tells if the given user can see the given org or user | ||||
| func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool { | ||||
| 	// If user is nil, it's an anonymous user/request. | ||||
| @ -533,20 +490,6 @@ func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model. | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID | ||||
| // are allowed to create repos. | ||||
| func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) { | ||||
| 	orgs := make([]*Organization, 0, 10) | ||||
| 
 | ||||
| 	return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`"). | ||||
| 		Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id"). | ||||
| 		Join("INNER", "`team`", "`team`.id = `team_user`.team_id"). | ||||
| 		Where(builder.Eq{"`team_user`.uid": userID}). | ||||
| 		And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))). | ||||
| 		Asc("`user`.name"). | ||||
| 		Find(&orgs) | ||||
| } | ||||
| 
 | ||||
| // GetOrgUsersByOrgID returns all organization-user relations by organization ID. | ||||
| func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) { | ||||
| 	sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID) | ||||
|  | ||||
							
								
								
									
										138
									
								
								models/organization/org_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								models/organization/org_list.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,138 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package organization | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/perm" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| // SearchOrganizationsOptions options to filter organizations | ||||
| type SearchOrganizationsOptions struct { | ||||
| 	db.ListOptions | ||||
| 	All bool | ||||
| } | ||||
| 
 | ||||
| // FindOrgOptions finds orgs options | ||||
| type FindOrgOptions struct { | ||||
| 	db.ListOptions | ||||
| 	UserID         int64 | ||||
| 	IncludePrivate bool | ||||
| } | ||||
| 
 | ||||
| func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { | ||||
| 	cond := builder.Eq{"uid": userID} | ||||
| 	if !includePrivate { | ||||
| 		cond["is_public"] = true | ||||
| 	} | ||||
| 	return builder.Select("org_id").From("org_user").Where(cond) | ||||
| } | ||||
| 
 | ||||
| func (opts FindOrgOptions) ToConds() builder.Cond { | ||||
| 	var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization} | ||||
| 	if opts.UserID > 0 { | ||||
| 		cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) | ||||
| 	} | ||||
| 	if !opts.IncludePrivate { | ||||
| 		cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) | ||||
| 	} | ||||
| 	return cond | ||||
| } | ||||
| 
 | ||||
| func (opts FindOrgOptions) ToOrders() string { | ||||
| 	return "`user`.lower_name ASC" | ||||
| } | ||||
| 
 | ||||
| // GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID | ||||
| // are allowed to create repos. | ||||
| func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) { | ||||
| 	orgs := make([]*Organization, 0, 10) | ||||
| 
 | ||||
| 	return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`"). | ||||
| 		Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id"). | ||||
| 		Join("INNER", "`team`", "`team`.id = `team_user`.team_id"). | ||||
| 		Where(builder.Eq{"`team_user`.uid": userID}). | ||||
| 		And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))). | ||||
| 		Asc("`user`.name"). | ||||
| 		Find(&orgs) | ||||
| } | ||||
| 
 | ||||
| // MinimalOrg represents a simple organization with only the needed columns | ||||
| type MinimalOrg = Organization | ||||
| 
 | ||||
| // GetUserOrgsList returns all organizations the given user has access to | ||||
| func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) { | ||||
| 	schema, err := db.TableInfo(new(user_model.User)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	outputCols := []string{ | ||||
| 		"id", | ||||
| 		"name", | ||||
| 		"full_name", | ||||
| 		"visibility", | ||||
| 		"avatar", | ||||
| 		"avatar_email", | ||||
| 		"use_custom_avatar", | ||||
| 	} | ||||
| 
 | ||||
| 	selectColumns := &strings.Builder{} | ||||
| 	for i, col := range outputCols { | ||||
| 		fmt.Fprintf(selectColumns, "`%s`.%s", schema.Name, col) | ||||
| 		if i < len(outputCols)-1 { | ||||
| 			selectColumns.WriteString(", ") | ||||
| 		} | ||||
| 	} | ||||
| 	columnsStr := selectColumns.String() | ||||
| 
 | ||||
| 	var orgs []*MinimalOrg | ||||
| 	if err := db.GetEngine(ctx).Select(columnsStr). | ||||
| 		Table("user"). | ||||
| 		Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))). | ||||
| 		Find(&orgs); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	type orgCount struct { | ||||
| 		OrgID     int64 | ||||
| 		RepoCount int | ||||
| 	} | ||||
| 	var orgCounts []orgCount | ||||
| 	if err := db.GetEngine(ctx). | ||||
| 		Select("owner_id AS org_id, COUNT(DISTINCT(repository.id)) as repo_count"). | ||||
| 		Table("repository"). | ||||
| 		Join("INNER", "org_user", "owner_id = org_user.org_id"). | ||||
| 		Where("org_user.uid = ?", user.ID). | ||||
| 		And(builder.Or( | ||||
| 			builder.Eq{"repository.is_private": false}, | ||||
| 			builder.In("repository.id", builder.Select("repo_id").From("team_repo"). | ||||
| 				InnerJoin("team_user", "team_user.team_id = team_repo.team_id"). | ||||
| 				Where(builder.Eq{"team_user.uid": user.ID})), | ||||
| 			builder.In("repository.id", builder.Select("repo_id").From("collaboration"). | ||||
| 				Where(builder.Eq{"user_id": user.ID})), | ||||
| 		)). | ||||
| 		GroupBy("owner_id").Find(&orgCounts); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	orgCountMap := make(map[int64]int, len(orgCounts)) | ||||
| 	for _, orgCount := range orgCounts { | ||||
| 		orgCountMap[orgCount.OrgID] = orgCount.RepoCount | ||||
| 	} | ||||
| 
 | ||||
| 	for _, org := range orgs { | ||||
| 		org.NumRepos = orgCountMap[org.ID] | ||||
| 	} | ||||
| 
 | ||||
| 	return orgs, nil | ||||
| } | ||||
							
								
								
									
										62
									
								
								models/organization/org_list_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								models/organization/org_list_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package organization_test | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/organization" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestCountOrganizations(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) | ||||
| 	assert.NoError(t, err) | ||||
| 	cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expected, cnt) | ||||
| } | ||||
| 
 | ||||
| func TestFindOrgs(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ | ||||
| 		UserID:         4, | ||||
| 		IncludePrivate: true, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	if assert.Len(t, orgs, 1) { | ||||
| 		assert.EqualValues(t, 3, orgs[0].ID) | ||||
| 	} | ||||
| 
 | ||||
| 	orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ | ||||
| 		UserID:         4, | ||||
| 		IncludePrivate: false, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Len(t, orgs, 0) | ||||
| 
 | ||||
| 	total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ | ||||
| 		UserID:         4, | ||||
| 		IncludePrivate: true, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 1, total) | ||||
| } | ||||
| 
 | ||||
| func TestGetUserOrgsList(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4}) | ||||
| 	assert.NoError(t, err) | ||||
| 	if assert.Len(t, orgs, 1) { | ||||
| 		assert.EqualValues(t, 3, orgs[0].ID) | ||||
| 		// repo_id: 3 is in the team, 32 is public, 5 is private with no team | ||||
| 		assert.EqualValues(t, 2, orgs[0].NumRepos) | ||||
| 	} | ||||
| } | ||||
| @ -129,15 +129,6 @@ func TestGetOrgByName(t *testing.T) { | ||||
| 	assert.True(t, organization.IsErrOrgNotExist(err)) | ||||
| } | ||||
| 
 | ||||
| func TestCountOrganizations(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) | ||||
| 	assert.NoError(t, err) | ||||
| 	cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expected, cnt) | ||||
| } | ||||
| 
 | ||||
| func TestIsOrganizationOwner(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	test := func(orgID, userID int64, expected bool) { | ||||
| @ -251,33 +242,6 @@ func TestRestrictedUserOrgMembers(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFindOrgs(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ | ||||
| 		UserID:         4, | ||||
| 		IncludePrivate: true, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	if assert.Len(t, orgs, 1) { | ||||
| 		assert.EqualValues(t, 3, orgs[0].ID) | ||||
| 	} | ||||
| 
 | ||||
| 	orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ | ||||
| 		UserID:         4, | ||||
| 		IncludePrivate: false, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Len(t, orgs, 0) | ||||
| 
 | ||||
| 	total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ | ||||
| 		UserID:         4, | ||||
| 		IncludePrivate: true, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 1, total) | ||||
| } | ||||
| 
 | ||||
| func TestGetOrgUsersByOrgID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
|  | ||||
| @ -242,6 +242,7 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { | ||||
| } | ||||
| 
 | ||||
| // NewProject creates a new Project | ||||
| // The title will be cut off at 255 characters if it's longer than 255 characters. | ||||
| func NewProject(ctx context.Context, p *Project) error { | ||||
| 	if !IsTemplateTypeValid(p.TemplateType) { | ||||
| 		p.TemplateType = TemplateTypeNone | ||||
| @ -255,6 +256,8 @@ func NewProject(ctx context.Context, p *Project) error { | ||||
| 		return util.NewInvalidArgumentErrorf("project type is not valid") | ||||
| 	} | ||||
| 
 | ||||
| 	p.Title, _ = util.SplitStringAtByteN(p.Title, 255) | ||||
| 
 | ||||
| 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||
| 		if err := db.Insert(ctx, p); err != nil { | ||||
| 			return err | ||||
| @ -308,6 +311,7 @@ func UpdateProject(ctx context.Context, p *Project) error { | ||||
| 		p.CardType = CardTypeTextOnly | ||||
| 	} | ||||
| 
 | ||||
| 	p.Title, _ = util.SplitStringAtByteN(p.Title, 255) | ||||
| 	_, err := db.GetEngine(ctx).ID(p.ID).Cols( | ||||
| 		"title", | ||||
| 		"description", | ||||
|  | ||||
| @ -156,6 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er | ||||
| 
 | ||||
| // UpdateRelease updates all columns of a release | ||||
| func UpdateRelease(ctx context.Context, rel *Release) error { | ||||
| 	rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) | ||||
| 	_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| @ -479,7 +479,6 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { | ||||
| 		metas := map[string]string{ | ||||
| 			"user": repo.OwnerName, | ||||
| 			"repo": repo.Name, | ||||
| 			"mode": "comment", | ||||
| 		} | ||||
| 
 | ||||
| 		unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker) | ||||
| @ -521,7 +520,6 @@ func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]str | ||||
| 		for k, v := range repo.ComposeMetas(ctx) { | ||||
| 			metas[k] = v | ||||
| 		} | ||||
| 		metas["mode"] = "document" | ||||
| 		repo.DocumentRenderingMetas = metas | ||||
| 	} | ||||
| 	return repo.DocumentRenderingMetas | ||||
|  | ||||
| @ -8,7 +8,6 @@ import ( | ||||
| 	"io" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| @ -17,9 +16,6 @@ import ( | ||||
| 	"github.com/go-enry/go-enry/v2" | ||||
| ) | ||||
| 
 | ||||
| // MarkupName describes markup's name | ||||
| var MarkupName = "console" | ||||
| 
 | ||||
| func init() { | ||||
| 	markup.RegisterRenderer(Renderer{}) | ||||
| } | ||||
| @ -29,7 +25,7 @@ type Renderer struct{} | ||||
| 
 | ||||
| // Name implements markup.Renderer | ||||
| func (Renderer) Name() string { | ||||
| 	return MarkupName | ||||
| 	return "console" | ||||
| } | ||||
| 
 | ||||
| // Extensions implements markup.Renderer | ||||
| @ -67,20 +63,3 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri | ||||
| 	_, err = output.Write(buf) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // Render renders terminal colors to HTML with all specific handling stuff. | ||||
| func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	if ctx.Type == "" { | ||||
| 		ctx.Type = MarkupName | ||||
| 	} | ||||
| 	return markup.Render(ctx, input, output) | ||||
| } | ||||
| 
 | ||||
| // RenderString renders terminal colors in string to HTML with all specific handling stuff and return string | ||||
| func RenderString(ctx *markup.RenderContext, content string) (string, error) { | ||||
| 	var buf strings.Builder | ||||
| 	if err := Render(ctx, strings.NewReader(content), &buf); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return buf.String(), nil | ||||
| } | ||||
|  | ||||
| @ -442,12 +442,11 @@ func createLink(href, content, class string) *html.Node { | ||||
| 	a := &html.Node{ | ||||
| 		Type: html.ElementNode, | ||||
| 		Data: atom.A.String(), | ||||
| 		Attr: []html.Attribute{ | ||||
| 			{Key: "href", Val: href}, | ||||
| 			{Key: "data-markdown-generated-content"}, | ||||
| 		}, | ||||
| 		Attr: []html.Attribute{{Key: "href", Val: href}}, | ||||
| 	} | ||||
| 	if !RenderBehaviorForTesting.DisableInternalAttributes { | ||||
| 		a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"}) | ||||
| 	} | ||||
| 
 | ||||
| 	if class != "" { | ||||
| 		a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class}) | ||||
| 	} | ||||
|  | ||||
| @ -11,6 +11,7 @@ import ( | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @ -23,8 +24,8 @@ func TestRenderCodePreview(t *testing.T) { | ||||
| 	}) | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := markup.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:  git.DefaultContext, | ||||
| 			Type: "markdown", | ||||
| 			Ctx:        git.DefaultContext, | ||||
| 			MarkupType: markdown.MarkupName, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
|  | ||||
| @ -11,6 +11,7 @@ import ( | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	testModule "code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @ -123,8 +124,9 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||
| 		} | ||||
| 		expectedNil := fmt.Sprintf(expectedFmt, links...) | ||||
| 		testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ | ||||
| 			Ctx:   git.DefaultContext, | ||||
| 			Metas: localMetas, | ||||
| 			Ctx:         git.DefaultContext, | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: RenderContentAsComment, | ||||
| 		}) | ||||
| 
 | ||||
| 		class := "ref-issue" | ||||
| @ -137,8 +139,9 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||
| 		} | ||||
| 		expectedNum := fmt.Sprintf(expectedFmt, links...) | ||||
| 		testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ | ||||
| 			Ctx:   git.DefaultContext, | ||||
| 			Metas: numericMetas, | ||||
| 			Ctx:         git.DefaultContext, | ||||
| 			Metas:       numericMetas, | ||||
| 			ContentMode: RenderContentAsComment, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| @ -266,7 +269,6 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) { | ||||
| 		"user":   "someUser", | ||||
| 		"repo":   "someRepo", | ||||
| 		"style":  IssueNameStyleNumeric, | ||||
| 		"mode":   "document", | ||||
| 	} | ||||
| 
 | ||||
| 	testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{ | ||||
| @ -316,8 +318,8 @@ func TestRender_AutoLink(t *testing.T) { | ||||
| 			Links: Links{ | ||||
| 				Base: TestRepoURL, | ||||
| 			}, | ||||
| 			Metas:  localMetas, | ||||
| 			IsWiki: true, | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: RenderContentAsWiki, | ||||
| 		}, strings.NewReader(input), &buffer) | ||||
| 		assert.Equal(t, err, nil) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) | ||||
| @ -340,7 +342,7 @@ func TestRender_AutoLink(t *testing.T) { | ||||
| 
 | ||||
| func TestRender_FullIssueURLs(t *testing.T) { | ||||
| 	setting.AppURL = TestAppURL | ||||
| 
 | ||||
| 	defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	test := func(input, expected string) { | ||||
| 		var result strings.Builder | ||||
| 		err := postProcess(&RenderContext{ | ||||
| @ -351,9 +353,7 @@ func TestRender_FullIssueURLs(t *testing.T) { | ||||
| 			Metas: localMetas, | ||||
| 		}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := result.String() | ||||
| 		actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, expected, actual) | ||||
| 		assert.Equal(t, expected, result.String()) | ||||
| 	} | ||||
| 	test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", | ||||
| 		"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") | ||||
|  | ||||
| @ -67,9 +67,8 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered? | ||||
| 	// The "mode" approach should be refactored to some other more clear&reliable way. | ||||
| 	crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki | ||||
| 	// crossLinkOnly if not comment and not wiki | ||||
| 	crossLinkOnly := ctx.ContentMode != RenderContentAsTitle && ctx.ContentMode != RenderContentAsComment && ctx.ContentMode != RenderContentAsWiki | ||||
| 
 | ||||
| 	var ( | ||||
| 		found bool | ||||
|  | ||||
| @ -20,7 +20,7 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu | ||||
| 	isAnchorFragment := link != "" && link[0] == '#' | ||||
| 	if !isAnchorFragment && !IsFullURLString(link) { | ||||
| 		linkBase := ctx.Links.Base | ||||
| 		if ctx.IsWiki { | ||||
| 		if ctx.ContentMode == RenderContentAsWiki { | ||||
| 			// 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 | ||||
| 			linkBase = ctx.Links.WikiLink() | ||||
| @ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 		} | ||||
| 		if image { | ||||
| 			if !absoluteLink { | ||||
| 				link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link) | ||||
| 				link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), link) | ||||
| 			} | ||||
| 			title := props["title"] | ||||
| 			if title == "" { | ||||
|  | ||||
| @ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { | ||||
| 		} | ||||
| 
 | ||||
| 		if IsNonEmptyRelativePath(attr.Val) { | ||||
| 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val) | ||||
| 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val) | ||||
| 
 | ||||
| 			// By default, the "<img>" tag should also be clickable, | ||||
| 			// because frontend use `<img>` to paste the re-scaled image into the markdown, | ||||
| @ -53,7 +53,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) { | ||||
| 			continue | ||||
| 		} | ||||
| 		if IsNonEmptyRelativePath(attr.Val) { | ||||
| 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val) | ||||
| 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val) | ||||
| 		} | ||||
| 		attr.Val = camoHandleLink(attr.Val) | ||||
| 		node.Attr[i] = attr | ||||
|  | ||||
| @ -14,6 +14,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	testModule "code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @ -104,7 +105,7 @@ func TestRender_Commits(t *testing.T) { | ||||
| 
 | ||||
| func TestRender_CrossReferences(t *testing.T) { | ||||
| 	setting.AppURL = markup.TestAppURL | ||||
| 
 | ||||
| 	defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := markup.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:          git.DefaultContext, | ||||
| @ -116,9 +117,7 @@ func TestRender_CrossReferences(t *testing.T) { | ||||
| 			Metas: localMetas, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.TrimSpace(buffer) | ||||
| 		actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), actual) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| 	} | ||||
| 
 | ||||
| 	test( | ||||
| @ -148,7 +147,7 @@ func TestRender_CrossReferences(t *testing.T) { | ||||
| 
 | ||||
| func TestRender_links(t *testing.T) { | ||||
| 	setting.AppURL = markup.TestAppURL | ||||
| 
 | ||||
| 	defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := markup.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:          git.DefaultContext, | ||||
| @ -158,9 +157,7 @@ func TestRender_links(t *testing.T) { | ||||
| 			}, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.TrimSpace(buffer) | ||||
| 		actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), actual) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| 	} | ||||
| 
 | ||||
| 	oldCustomURLSchemes := setting.Markdown.CustomURLSchemes | ||||
| @ -261,7 +258,7 @@ func TestRender_links(t *testing.T) { | ||||
| 
 | ||||
| func TestRender_email(t *testing.T) { | ||||
| 	setting.AppURL = markup.TestAppURL | ||||
| 
 | ||||
| 	defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	test := func(input, expected string) { | ||||
| 		res, err := markup.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:          git.DefaultContext, | ||||
| @ -271,9 +268,7 @@ func TestRender_email(t *testing.T) { | ||||
| 			}, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.TrimSpace(res) | ||||
| 		actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), actual) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) | ||||
| 	} | ||||
| 	// Text that should be turned into email link | ||||
| 
 | ||||
| @ -302,10 +297,10 @@ func TestRender_email(t *testing.T) { | ||||
| 	j.doe@example.com; | ||||
| 	j.doe@example.com? | ||||
| 	j.doe@example.com!`, | ||||
| 		`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>,<br/> | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>.<br/> | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>;<br/> | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?<br/> | ||||
| 		`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>, | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>. | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>; | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>? | ||||
| <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`) | ||||
| 
 | ||||
| 	// Test that should *not* be turned into email links | ||||
| @ -418,8 +413,8 @@ func TestRender_ShortLinks(t *testing.T) { | ||||
| 			Links: markup.Links{ | ||||
| 				Base: markup.TestRepoURL, | ||||
| 			}, | ||||
| 			Metas:  localMetas, | ||||
| 			IsWiki: true, | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: markup.RenderContentAsWiki, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | ||||
| @ -531,10 +526,10 @@ func TestRender_ShortLinks(t *testing.T) { | ||||
| func TestRender_RelativeMedias(t *testing.T) { | ||||
| 	render := func(input string, isWiki bool, links markup.Links) string { | ||||
| 		buffer, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:    git.DefaultContext, | ||||
| 			Links:  links, | ||||
| 			Metas:  localMetas, | ||||
| 			IsWiki: isWiki, | ||||
| 			Ctx:         git.DefaultContext, | ||||
| 			Links:       links, | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsComment), | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		return strings.TrimSpace(string(buffer)) | ||||
| @ -604,12 +599,7 @@ func Test_ParseClusterFuzz(t *testing.T) { | ||||
| func TestPostProcess_RenderDocument(t *testing.T) { | ||||
| 	setting.AppURL = markup.TestAppURL | ||||
| 	setting.StaticURLPrefix = markup.TestAppURL // can't run standalone | ||||
| 
 | ||||
| 	localMetas := map[string]string{ | ||||
| 		"user": "go-gitea", | ||||
| 		"repo": "gitea", | ||||
| 		"mode": "document", | ||||
| 	} | ||||
| 	defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 
 | ||||
| 	test := func(input, expected string) { | ||||
| 		var res strings.Builder | ||||
| @ -619,12 +609,10 @@ func TestPostProcess_RenderDocument(t *testing.T) { | ||||
| 				AbsolutePrefix: true, | ||||
| 				Base:           "https://example.com", | ||||
| 			}, | ||||
| 			Metas: localMetas, | ||||
| 			Metas: map[string]string{"user": "go-gitea", "repo": "gitea"}, | ||||
| 		}, strings.NewReader(input), &res) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.TrimSpace(res.String()) | ||||
| 		actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), actual) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String())) | ||||
| 	} | ||||
| 
 | ||||
| 	// Issue index shouldn't be post processing in a document. | ||||
|  | ||||
| @ -72,7 +72,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa | ||||
| 			g.transformList(ctx, v, rc) | ||||
| 		case *ast.Text: | ||||
| 			if v.SoftLineBreak() && !v.HardLineBreak() { | ||||
| 				if ctx.Metas["mode"] != "document" { | ||||
| 				// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }` | ||||
| 				// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting | ||||
| 				// especially in many tests. | ||||
| 				if markup.RenderBehaviorForTesting.ForceHardLineBreak { | ||||
| 					v.SetHardLineBreak(true) | ||||
| 				} else if ctx.ContentMode == markup.RenderContentAsComment { | ||||
| 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) | ||||
| 				} else { | ||||
| 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) | ||||
|  | ||||
| @ -257,9 +257,7 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri | ||||
| 
 | ||||
| // Render renders Markdown to HTML with all specific handling stuff. | ||||
| func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	if ctx.Type == "" { | ||||
| 		ctx.Type = MarkupName | ||||
| 	} | ||||
| 	ctx.MarkupType = MarkupName | ||||
| 	return markup.Render(ctx, input, output) | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/svg" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @ -74,7 +75,7 @@ func TestRender_StandardLinks(t *testing.T) { | ||||
| 			Links: markup.Links{ | ||||
| 				Base: FullURL, | ||||
| 			}, | ||||
| 			IsWiki: true, | ||||
| 			ContentMode: markup.RenderContentAsWiki, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) | ||||
| @ -296,23 +297,22 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno | ||||
| } | ||||
| 
 | ||||
| func TestTotal_RenderWiki(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	setting.AppURL = AppURL | ||||
| 
 | ||||
| 	answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw")) | ||||
| 
 | ||||
| 	for i := 0; i < len(sameCases); i++ { | ||||
| 		line, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Ctx: git.DefaultContext, | ||||
| 			Links: markup.Links{ | ||||
| 				Base: FullURL, | ||||
| 			}, | ||||
| 			Repo:   newMockRepo(testRepoOwnerName, testRepoName), | ||||
| 			Metas:  localMetas, | ||||
| 			IsWiki: true, | ||||
| 			Repo:        newMockRepo(testRepoOwnerName, testRepoName), | ||||
| 			Metas:       localMetas, | ||||
| 			ContentMode: markup.RenderContentAsWiki, | ||||
| 		}, sameCases[i]) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, answers[i], actual) | ||||
| 		assert.Equal(t, answers[i], string(line)) | ||||
| 	} | ||||
| 
 | ||||
| 	testCases := []string{ | ||||
| @ -334,19 +334,18 @@ func TestTotal_RenderWiki(t *testing.T) { | ||||
| 			Links: markup.Links{ | ||||
| 				Base: FullURL, | ||||
| 			}, | ||||
| 			IsWiki: true, | ||||
| 			ContentMode: markup.RenderContentAsWiki, | ||||
| 		}, testCases[i]) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") | ||||
| 		assert.EqualValues(t, testCases[i+1], actual) | ||||
| 		assert.EqualValues(t, testCases[i+1], string(line)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestTotal_RenderString(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	setting.AppURL = AppURL | ||||
| 
 | ||||
| 	answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master")) | ||||
| 
 | ||||
| 	for i := 0; i < len(sameCases); i++ { | ||||
| 		line, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Ctx: git.DefaultContext, | ||||
| @ -358,8 +357,7 @@ func TestTotal_RenderString(t *testing.T) { | ||||
| 			Metas: localMetas, | ||||
| 		}, sameCases[i]) | ||||
| 		assert.NoError(t, err) | ||||
| 		actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, answers[i], actual) | ||||
| 		assert.Equal(t, answers[i], string(line)) | ||||
| 	} | ||||
| 
 | ||||
| 	testCases := []string{} | ||||
| @ -428,6 +426,7 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) { | ||||
| 	expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br> | ||||
| <a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p> | ||||
| ` | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() | ||||
| 	res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expected, res) | ||||
| @ -996,11 +995,16 @@ space</p> | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	for i, c := range cases { | ||||
| 		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input) | ||||
| 		result, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Ctx:         context.Background(), | ||||
| 			Links:       c.Links, | ||||
| 			ContentMode: util.Iif(c.IsWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault), | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err, "Unexpected error in testcase: %v", i) | ||||
| 		actual := strings.ReplaceAll(string(result), ` data-markdown-generated-content=""`, "") | ||||
| 		assert.Equal(t, c.Expected, actual, "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 | ||||
| 	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) { | ||||
| 		v.Destination = []byte(giteautil.URLJoin( | ||||
| 			ctx.Links.ResolveMediaLink(ctx.IsWiki), | ||||
| 			ctx.Links.ResolveMediaLink(ctx.ContentMode == markup.RenderContentAsWiki), | ||||
| 			strings.TrimLeft(string(v.Destination), "/"), | ||||
| 		)) | ||||
| 	} | ||||
|  | ||||
| @ -144,14 +144,15 @@ func (r *Writer) resolveLink(kind, link string) string { | ||||
| 		} | ||||
| 
 | ||||
| 		base := r.Ctx.Links.Base | ||||
| 		if r.Ctx.IsWiki { | ||||
| 		isWiki := r.Ctx.ContentMode == markup.RenderContentAsWiki | ||||
| 		if isWiki { | ||||
| 			base = r.Ctx.Links.WikiLink() | ||||
| 		} else if r.Ctx.Links.HasBranchInfo() { | ||||
| 			base = r.Ctx.Links.SrcLink() | ||||
| 		} | ||||
| 
 | ||||
| 		if kind == "image" || kind == "video" { | ||||
| 			base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki) | ||||
| 			base = r.Ctx.Links.ResolveMediaLink(isWiki) | ||||
| 		} | ||||
| 
 | ||||
| 		link = util.URLJoin(base, link) | ||||
|  | ||||
| @ -10,6 +10,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @ -26,7 +27,7 @@ func TestRender_StandardLinks(t *testing.T) { | ||||
| 				Base:       "/relative-path", | ||||
| 				BranchPath: "branch/main", | ||||
| 			}, | ||||
| 			IsWiki: isWiki, | ||||
| 			ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault), | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
|  | ||||
| @ -5,11 +5,9 @@ package markup | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/url" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| @ -29,15 +27,44 @@ const ( | ||||
| 	RenderMetaAsTable   RenderMetaMode = "table" | ||||
| ) | ||||
| 
 | ||||
| type RenderContentMode string | ||||
| 
 | ||||
| const ( | ||||
| 	RenderContentAsDefault RenderContentMode = "" // empty means "default", no special handling, maybe just a simple "document" | ||||
| 	RenderContentAsComment RenderContentMode = "comment" | ||||
| 	RenderContentAsTitle   RenderContentMode = "title" | ||||
| 	RenderContentAsWiki    RenderContentMode = "wiki" | ||||
| ) | ||||
| 
 | ||||
| var RenderBehaviorForTesting struct { | ||||
| 	// Markdown line break rendering has 2 default behaviors: | ||||
| 	// * Use hard: replace "\n" with "<br>" for comments, setting.Markdown.EnableHardLineBreakInComments=true | ||||
| 	// * Keep soft: "\n" for non-comments (a.k.a. documents), setting.Markdown.EnableHardLineBreakInDocuments=false | ||||
| 	// In history, there was a mess: | ||||
| 	// * The behavior was controlled by `Metas["mode"] != "document", | ||||
| 	// * However, many places render the content without setting "mode" in Metas, all these places used comment line break setting incorrectly | ||||
| 	ForceHardLineBreak bool | ||||
| 
 | ||||
| 	// Gitea will emit some internal attributes for various purposes, these attributes don't affect rendering. | ||||
| 	// But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes. | ||||
| 	DisableInternalAttributes bool | ||||
| } | ||||
| 
 | ||||
| // RenderContext represents a render context | ||||
| type RenderContext struct { | ||||
| 	Ctx              context.Context | ||||
| 	RelativePath     string // relative path from tree root of the branch | ||||
| 	Type             string | ||||
| 	IsWiki           bool | ||||
| 	Links            Links | ||||
| 	Metas            map[string]string // user, repo, mode(comment/document) | ||||
| 	DefaultLink      string | ||||
| 	Ctx          context.Context | ||||
| 	RelativePath string // relative path from tree root of the branch | ||||
| 
 | ||||
| 	// eg: "orgmode", "asciicast", "console" | ||||
| 	// for file mode, it could be left as empty, and will be detected by file extension in RelativePath | ||||
| 	MarkupType string | ||||
| 
 | ||||
| 	// what the content will be used for: eg: for comment or for wiki? or just render a file? | ||||
| 	ContentMode RenderContentMode | ||||
| 
 | ||||
| 	Links            Links             // special link references for rendering, especially when there is a branch/tree path | ||||
| 	Metas            map[string]string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention), BranchNameSubURL(for iframe&asciicast) | ||||
| 	DefaultLink      string            // TODO: need to figure out | ||||
| 	GitRepo          *git.Repository | ||||
| 	Repo             gitrepo.Repository | ||||
| 	ShaExistCache    map[string]bool | ||||
| @ -77,12 +104,29 @@ func (ctx *RenderContext) AddCancel(fn func()) { | ||||
| 
 | ||||
| // Render renders markup file to HTML with all specific handling stuff. | ||||
| func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	if ctx.Type != "" { | ||||
| 		return renderByType(ctx, input, output) | ||||
| 	} else if ctx.RelativePath != "" { | ||||
| 		return renderFile(ctx, input, output) | ||||
| 	if ctx.MarkupType == "" && ctx.RelativePath != "" { | ||||
| 		ctx.MarkupType = DetectMarkupTypeByFileName(ctx.RelativePath) | ||||
| 		if ctx.MarkupType == "" { | ||||
| 			return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RelativePath) | ||||
| 		} | ||||
| 	} | ||||
| 	return errors.New("render options both filename and type missing") | ||||
| 
 | ||||
| 	renderer := renderers[ctx.MarkupType] | ||||
| 	if renderer == nil { | ||||
| 		return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.MarkupType) | ||||
| 	} | ||||
| 
 | ||||
| 	if ctx.RelativePath != "" { | ||||
| 		if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() { | ||||
| 			if !ctx.InStandalonePage { | ||||
| 				// for an external "DisplayInIFrame" render, it could only output its content in a standalone page | ||||
| 				// otherwise, a <iframe> should be outputted to embed the external rendered page | ||||
| 				return renderIFrame(ctx, output) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return render(ctx, renderer, input, output) | ||||
| } | ||||
| 
 | ||||
| // RenderString renders Markup string to HTML with all specific handling stuff and return string | ||||
| @ -170,42 +214,6 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	if renderer, ok := renderers[ctx.Type]; ok { | ||||
| 		return render(ctx, renderer, input, output) | ||||
| 	} | ||||
| 	return fmt.Errorf("unsupported render type: %s", ctx.Type) | ||||
| } | ||||
| 
 | ||||
| // ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render | ||||
| type ErrUnsupportedRenderExtension struct { | ||||
| 	Extension string | ||||
| } | ||||
| 
 | ||||
| func IsErrUnsupportedRenderExtension(err error) bool { | ||||
| 	_, ok := err.(ErrUnsupportedRenderExtension) | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| func (err ErrUnsupportedRenderExtension) Error() string { | ||||
| 	return fmt.Sprintf("Unsupported render extension: %s", err.Extension) | ||||
| } | ||||
| 
 | ||||
| func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	extension := strings.ToLower(filepath.Ext(ctx.RelativePath)) | ||||
| 	if renderer, ok := extRenderers[extension]; ok { | ||||
| 		if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() { | ||||
| 			if !ctx.InStandalonePage { | ||||
| 				// for an external render, it could only output its content in a standalone page | ||||
| 				// otherwise, a <iframe> should be outputted to embed the external rendered page | ||||
| 				return renderIFrame(ctx, output) | ||||
| 			} | ||||
| 		} | ||||
| 		return render(ctx, renderer, input, output) | ||||
| 	} | ||||
| 	return ErrUnsupportedRenderExtension{extension} | ||||
| } | ||||
| 
 | ||||
| // Init initializes the render global variables | ||||
| func Init(ph *ProcessorHelper) { | ||||
| 	if ph != nil { | ||||
|  | ||||
| @ -21,7 +21,7 @@ type MarkupOption struct { | ||||
| 	// | ||||
| 	// in: body | ||||
| 	Text string | ||||
| 	// Mode to render (comment, gfm, markdown, file) | ||||
| 	// Mode to render (markdown, comment, wiki, file) | ||||
| 	// | ||||
| 	// in: body | ||||
| 	Mode string | ||||
| @ -30,8 +30,9 @@ type MarkupOption struct { | ||||
| 	// | ||||
| 	// in: body | ||||
| 	Context string | ||||
| 	// Is it a wiki page ? | ||||
| 	// Is it a wiki page? (use mode=wiki instead) | ||||
| 	// | ||||
| 	// Deprecated: true | ||||
| 	// in: body | ||||
| 	Wiki bool | ||||
| 	// File path for detecting extension in file mode | ||||
| @ -50,7 +51,7 @@ type MarkdownOption struct { | ||||
| 	// | ||||
| 	// in: body | ||||
| 	Text string | ||||
| 	// Mode to render (comment, gfm, markdown) | ||||
| 	// Mode to render (markdown, comment, wiki, file) | ||||
| 	// | ||||
| 	// in: body | ||||
| 	Mode string | ||||
| @ -59,8 +60,9 @@ type MarkdownOption struct { | ||||
| 	// | ||||
| 	// in: body | ||||
| 	Context string | ||||
| 	// Is it a wiki page ? | ||||
| 	// Is it a wiki page? (use mode=wiki instead) | ||||
| 	// | ||||
| 	// Deprecated: true | ||||
| 	// in: body | ||||
| 	Wiki bool | ||||
| } | ||||
|  | ||||
| @ -94,8 +94,9 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem | ||||
| 	} | ||||
| 
 | ||||
| 	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:   ut.ctx, | ||||
| 		Metas: metas, | ||||
| 		Ctx:         ut.ctx, | ||||
| 		Metas:       metas, | ||||
| 		ContentMode: markup.RenderContentAsComment, | ||||
| 	}, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessage: %v", err) | ||||
| @ -116,8 +117,9 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { | ||||
| // RenderIssueTitle renders issue/pull title with defined post processors | ||||
| func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { | ||||
| 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ | ||||
| 		Ctx:   ut.ctx, | ||||
| 		Metas: metas, | ||||
| 		Ctx:         ut.ctx, | ||||
| 		ContentMode: markup.RenderContentAsTitle, | ||||
| 		Metas:       metas, | ||||
| 	}, template.HTMLEscapeString(text)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderIssueTitle: %v", err) | ||||
|  | ||||
| @ -15,6 +15,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @ -72,6 +73,7 @@ func newTestRenderUtils() *RenderUtils { | ||||
| } | ||||
| 
 | ||||
| func TestRenderCommitBody(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	type args struct { | ||||
| 		msg   string | ||||
| 		metas map[string]string | ||||
| @ -129,23 +131,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | ||||
| <a href="/mention-user" class="mention">@mention-user</a> test | ||||
| <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> | ||||
|   space` | ||||
| 	actual := strings.ReplaceAll(string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)), ` data-markdown-generated-content=""`, "") | ||||
| 	assert.EqualValues(t, expected, actual) | ||||
| 	assert.EqualValues(t, expected, string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas))) | ||||
| } | ||||
| 
 | ||||
| func TestRenderCommitMessage(t *testing.T) { | ||||
| 	expected := `space <a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a>  ` | ||||
| 
 | ||||
| 	assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas)) | ||||
| } | ||||
| 
 | ||||
| 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>` | ||||
| 
 | ||||
| 	assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) | ||||
| } | ||||
| 
 | ||||
| func TestRenderIssueTitle(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	expected := `  space @mention-user<SPACE><SPACE> | ||||
| /just/a/path.bin | ||||
| https://example.com/file.bin | ||||
| @ -168,11 +168,11 @@ mail@domain.com | ||||
|   space<SPACE><SPACE> | ||||
| ` | ||||
| 	expected = strings.ReplaceAll(expected, "<SPACE>", " ") | ||||
| 	actual := strings.ReplaceAll(string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)), ` data-markdown-generated-content=""`, "") | ||||
| 	assert.EqualValues(t, expected, actual) | ||||
| 	assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas))) | ||||
| } | ||||
| 
 | ||||
| func TestRenderMarkdownToHtml(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/> | ||||
| /just/a/path.bin | ||||
| <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a> | ||||
| @ -194,8 +194,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | ||||
| #123 | ||||
| space</p> | ||||
| ` | ||||
| 	actual := strings.ReplaceAll(string(newTestRenderUtils().MarkdownToHtml(testInput())), ` data-markdown-generated-content=""`, "") | ||||
| 	assert.Equal(t, expected, actual) | ||||
| 	assert.Equal(t, expected, string(newTestRenderUtils().MarkdownToHtml(testInput()))) | ||||
| } | ||||
| 
 | ||||
| func TestRenderLabels(t *testing.T) { | ||||
|  | ||||
| @ -9,6 +9,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/common" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| @ -41,7 +42,8 @@ func Markup(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki) | ||||
| 	mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck | ||||
| 	common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath) | ||||
| } | ||||
| 
 | ||||
| // Markdown render markdown document to HTML | ||||
| @ -71,12 +73,8 @@ func Markdown(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mode := "markdown" | ||||
| 	if form.Mode == "comment" || form.Mode == "gfm" { | ||||
| 		mode = form.Mode | ||||
| 	} | ||||
| 
 | ||||
| 	common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "", form.Wiki) | ||||
| 	mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck | ||||
| 	common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "") | ||||
| } | ||||
| 
 | ||||
| // MarkdownRaw render raw markdown HTML | ||||
|  | ||||
| @ -14,6 +14,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/services/contexttest" | ||||
| 
 | ||||
| @ -24,6 +25,7 @@ const AppURL = "http://localhost:3000/" | ||||
| 
 | ||||
| func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) { | ||||
| 	setting.AppURL = AppURL | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	context := "/gogits/gogs" | ||||
| 	if !wiki { | ||||
| 		context += path.Join("/src/branch/main", path.Dir(filePath)) | ||||
| @ -38,13 +40,13 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe | ||||
| 	ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") | ||||
| 	web.SetForm(ctx, &options) | ||||
| 	Markup(ctx) | ||||
| 	actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "") | ||||
| 	assert.Equal(t, expectedBody, actual) | ||||
| 	assert.Equal(t, expectedBody, resp.Body.String()) | ||||
| 	assert.Equal(t, expectedCode, resp.Code) | ||||
| 	resp.Body.Reset() | ||||
| } | ||||
| 
 | ||||
| func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() | ||||
| 	setting.AppURL = AppURL | ||||
| 	context := "/gogits/gogs" | ||||
| 	if !wiki { | ||||
| @ -59,8 +61,7 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody | ||||
| 	ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") | ||||
| 	web.SetForm(ctx, &options) | ||||
| 	Markdown(ctx) | ||||
| 	actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "") | ||||
| 	assert.Equal(t, responseBody, actual) | ||||
| 	assert.Equal(t, responseBody, resp.Body.String()) | ||||
| 	assert.Equal(t, responseCode, resp.Code) | ||||
| 	resp.Body.Reset() | ||||
| } | ||||
| @ -158,8 +159,8 @@ Here are some links to the most important topics. You can find the full list of | ||||
| <a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p> | ||||
| `, http.StatusOK) | ||||
| 
 | ||||
| 	testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) | ||||
| 	testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) | ||||
| 	testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity) | ||||
| 	testRenderMarkup(t, "unknown", false, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) | ||||
| } | ||||
| 
 | ||||
| var simpleCases = []string{ | ||||
|  | ||||
| @ -5,21 +5,22 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/httplib" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| ) | ||||
| 
 | ||||
| // RenderMarkup renders markup text for the /markup and /markdown endpoints | ||||
| func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) { | ||||
| func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string) { | ||||
| 	// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}" | ||||
| 	// filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file") | ||||
| 	// filePath will be used as RenderContext.RelativePath | ||||
| @ -27,32 +28,33 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa | ||||
| 	// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md" | ||||
| 	// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc" | ||||
| 
 | ||||
| 	var markupType, relativePath string | ||||
| 
 | ||||
| 	links := markup.Links{AbsolutePrefix: true} | ||||
| 	renderCtx := &markup.RenderContext{ | ||||
| 		Ctx:        ctx, | ||||
| 		Links:      markup.Links{AbsolutePrefix: true}, | ||||
| 		MarkupType: markdown.MarkupName, | ||||
| 	} | ||||
| 	if urlPathContext != "" { | ||||
| 		links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext) | ||||
| 		renderCtx.Links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext) | ||||
| 	} | ||||
| 
 | ||||
| 	switch mode { | ||||
| 	case "markdown": | ||||
| 		// Raw markdown | ||||
| 		if err := markdown.RenderRaw(&markup.RenderContext{ | ||||
| 			Ctx:   ctx, | ||||
| 			Links: links, | ||||
| 		}, strings.NewReader(text), ctx.Resp); err != nil { | ||||
| 	if mode == "" || mode == "markdown" { | ||||
| 		// raw markdown doesn't need any special handling | ||||
| 		if err := markdown.RenderRaw(renderCtx, strings.NewReader(text), ctx.Resp); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	switch mode { | ||||
| 	case "gfm": // legacy mode, do nothing | ||||
| 	case "comment": | ||||
| 		// Issue & comment content | ||||
| 		markupType = markdown.MarkupName | ||||
| 	case "gfm": | ||||
| 		// GitHub Flavored Markdown | ||||
| 		markupType = markdown.MarkupName | ||||
| 		renderCtx.ContentMode = markup.RenderContentAsComment | ||||
| 	case "wiki": | ||||
| 		renderCtx.ContentMode = markup.RenderContentAsWiki | ||||
| 	case "file": | ||||
| 		markupType = "" // render the repo file content by its extension | ||||
| 		relativePath = filePath | ||||
| 		// render the repo file content by its extension | ||||
| 		renderCtx.MarkupType = "" | ||||
| 		renderCtx.RelativePath = filePath | ||||
| 		renderCtx.InStandalonePage = true | ||||
| 	default: | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) | ||||
| 		return | ||||
| @ -67,33 +69,19 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa | ||||
| 		refPath := strings.Join(fields[3:], "/")           // it is "branch/features/feat-12/doc" | ||||
| 		refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12" | ||||
| 
 | ||||
| 		links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir} | ||||
| 		renderCtx.Links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir} | ||||
| 	} | ||||
| 
 | ||||
| 	meta := map[string]string{} | ||||
| 	var repoCtx *repo_model.Repository | ||||
| 	if repo != nil && repo.Repository != nil { | ||||
| 		repoCtx = repo.Repository | ||||
| 		if mode == "comment" { | ||||
| 			meta = repo.Repository.ComposeMetas(ctx) | ||||
| 		renderCtx.Repo = repo.Repository | ||||
| 		if renderCtx.ContentMode == markup.RenderContentAsComment { | ||||
| 			renderCtx.Metas = repo.Repository.ComposeMetas(ctx) | ||||
| 		} else { | ||||
| 			meta = repo.Repository.ComposeDocumentMetas(ctx) | ||||
| 			renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx) | ||||
| 		} | ||||
| 	} | ||||
| 	if mode != "comment" { | ||||
| 		meta["mode"] = "document" | ||||
| 	} | ||||
| 
 | ||||
| 	if err := markup.Render(&markup.RenderContext{ | ||||
| 		Ctx:          ctx, | ||||
| 		Repo:         repoCtx, | ||||
| 		Links:        links, | ||||
| 		Metas:        meta, | ||||
| 		IsWiki:       wiki, | ||||
| 		Type:         markupType, | ||||
| 		RelativePath: relativePath, | ||||
| 	}, strings.NewReader(text), ctx.Resp); err != nil { | ||||
| 		if markup.IsErrUnsupportedRenderExtension(err) { | ||||
| 	if err := markup.Render(renderCtx, strings.NewReader(text), ctx.Resp); err != nil { | ||||
| 		if errors.Is(err, util.ErrInvalidArgument) { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, err.Error()) | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
|  | ||||
| @ -56,7 +56,6 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content | ||||
| 		Links: markup.Links{ | ||||
| 			Base: act.GetRepoLink(ctx), | ||||
| 		}, | ||||
| 		Type: markdown.MarkupName, | ||||
| 		Metas: map[string]string{ | ||||
| 			"user": act.GetRepoUserName(ctx), | ||||
| 			"repo": act.GetRepoName(ctx), | ||||
|  | ||||
| @ -6,6 +6,7 @@ package misc | ||||
| 
 | ||||
| import ( | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/common" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| @ -14,5 +15,6 @@ import ( | ||||
| // Markup render markup document to HTML | ||||
| func Markup(ctx *context.Context) { | ||||
| 	form := web.GetForm(ctx).(*api.MarkupOption) | ||||
| 	common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki) | ||||
| 	mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck | ||||
| 	common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath) | ||||
| } | ||||
|  | ||||
| @ -312,6 +312,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr | ||||
| 
 | ||||
| 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ | ||||
| 			Ctx:          ctx, | ||||
| 			MarkupType:   markupType, | ||||
| 			RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). | ||||
| 			Links: markup.Links{ | ||||
| 				Base:       ctx.Repo.RepoLink, | ||||
| @ -502,28 +503,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { | ||||
| 		ctx.Data["ReadmeExist"] = readmeExist | ||||
| 
 | ||||
| 		markupType := markup.DetectMarkupTypeByFileName(blob.Name()) | ||||
| 		// If the markup is detected by custom markup renderer it should not be reset later on | ||||
| 		// to not pass it down to the render context. | ||||
| 		detected := false | ||||
| 		if markupType == "" { | ||||
| 			detected = true | ||||
| 			markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) | ||||
| 		} | ||||
| 		if markupType != "" { | ||||
| 			ctx.Data["HasSourceRenderedToggle"] = true | ||||
| 		} | ||||
| 
 | ||||
| 		if markupType != "" && !shouldRenderSource { | ||||
| 			ctx.Data["IsMarkup"] = true | ||||
| 			ctx.Data["MarkupType"] = markupType | ||||
| 			if !detected { | ||||
| 				markupType = "" | ||||
| 			} | ||||
| 			metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) | ||||
| 			metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() | ||||
| 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ | ||||
| 				Ctx:          ctx, | ||||
| 				Type:         markupType, | ||||
| 				MarkupType:   markupType, | ||||
| 				RelativePath: ctx.Repo.TreePath, | ||||
| 				Links: markup.Links{ | ||||
| 					Base:       ctx.Repo.RepoLink, | ||||
| @ -615,6 +608,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { | ||||
| 			ctx.Data["MarkupType"] = markupType | ||||
| 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ | ||||
| 				Ctx:          ctx, | ||||
| 				MarkupType:   markupType, | ||||
| 				RelativePath: ctx.Repo.TreePath, | ||||
| 				Links: markup.Links{ | ||||
| 					Base:       ctx.Repo.RepoLink, | ||||
|  | ||||
| @ -289,12 +289,12 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { | ||||
| 	} | ||||
| 
 | ||||
| 	rctx := &markup.RenderContext{ | ||||
| 		Ctx:   ctx, | ||||
| 		Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx), | ||||
| 		Ctx:         ctx, | ||||
| 		ContentMode: markup.RenderContentAsWiki, | ||||
| 		Metas:       ctx.Repo.Repository.ComposeDocumentMetas(ctx), | ||||
| 		Links: markup.Links{ | ||||
| 			Base: ctx.Repo.RepoLink, | ||||
| 		}, | ||||
| 		IsWiki: true, | ||||
| 	} | ||||
| 	buf := &strings.Builder{} | ||||
| 
 | ||||
|  | ||||
| @ -258,7 +258,6 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb | ||||
| 					Base:       profileDbRepo.Link(), | ||||
| 					BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), | ||||
| 				}, | ||||
| 				Metas: map[string]string{"mode": "document"}, | ||||
| 			}, bytes); err != nil { | ||||
| 				log.Error("failed to RenderString: %v", err) | ||||
| 			} else { | ||||
|  | ||||
| @ -260,8 +260,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { | ||||
| 	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) | ||||
| 	if len(ctx.ContextUser.Description) != 0 { | ||||
| 		content, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 			Metas: map[string]string{"mode": "document"}, | ||||
| 			Ctx:   ctx, | ||||
| 			Ctx: ctx, | ||||
| 		}, ctx.ContextUser.Description) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("RenderString", err) | ||||
|  | ||||
| @ -8,6 +8,7 @@ import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	auth "code.gitea.io/gitea/models/auth" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	org_model "code.gitea.io/gitea/models/organization" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| @ -192,7 +193,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server | ||||
| // returns a list of "org" and "org:team" strings, | ||||
| // that the given user is a part of. | ||||
| func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) { | ||||
| 	orgs, err := org_model.GetUserOrgsList(ctx, user) | ||||
| 	orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{ | ||||
| 		UserID:         user.ID, | ||||
| 		IncludePrivate: true, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("GetUserOrgList: %w", err) | ||||
| 	} | ||||
|  | ||||
| @ -142,6 +142,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) | ||||
| 	rel.LowerTagName = strings.ToLower(rel.TagName) | ||||
| 	if err = db.Insert(gitRepo.Ctx, rel); err != nil { | ||||
| 		return err | ||||
|  | ||||
| @ -44,7 +44,7 @@ parts: | ||||
|     source: . | ||||
|     stage-packages: [ git, sqlite3, openssh-client ] | ||||
|     build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential] | ||||
|     build-snaps: [ go/1.23/stable, node/20/stable ] | ||||
|     build-snaps: [ go/1.23/stable, node/22/stable ] | ||||
|     build-environment: | ||||
|       - LDFLAGS: "" | ||||
|     override-pull: | | ||||
|  | ||||
							
								
								
									
										8
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @ -22615,7 +22615,7 @@ | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "Mode": { | ||||
|           "description": "Mode to render (comment, gfm, markdown)\n\nin: body", | ||||
|           "description": "Mode to render (markdown, comment, wiki, file)\n\nin: body", | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "Text": { | ||||
| @ -22623,7 +22623,7 @@ | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "Wiki": { | ||||
|           "description": "Is it a wiki page ?\n\nin: body", | ||||
|           "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body", | ||||
|           "type": "boolean" | ||||
|         } | ||||
|       }, | ||||
| @ -22642,7 +22642,7 @@ | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "Mode": { | ||||
|           "description": "Mode to render (comment, gfm, markdown, file)\n\nin: body", | ||||
|           "description": "Mode to render (markdown, comment, wiki, file)\n\nin: body", | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "Text": { | ||||
| @ -22650,7 +22650,7 @@ | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "Wiki": { | ||||
|           "description": "Is it a wiki page ?\n\nin: body", | ||||
|           "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body", | ||||
|           "type": "boolean" | ||||
|         } | ||||
|       }, | ||||
|  | ||||
| @ -10,6 +10,8 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/external" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
| 
 | ||||
| @ -23,10 +25,9 @@ func TestExternalMarkupRenderer(t *testing.T) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	const repoURL = "user30/renderer" | ||||
| 	req := NewRequest(t, "GET", repoURL+"/src/branch/master/README.html") | ||||
| 	req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||
| 	resp := MakeRequest(t, req, http.StatusOK) | ||||
| 	assert.EqualValues(t, "text/html; charset=utf-8", resp.Header()["Content-Type"][0]) | ||||
| 	assert.EqualValues(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 
 | ||||
| 	bs, err := io.ReadAll(resp.Body) | ||||
| 	assert.NoError(t, err) | ||||
| @ -36,4 +37,24 @@ func TestExternalMarkupRenderer(t *testing.T) { | ||||
| 	data, err := div.Html() | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data)) | ||||
| 
 | ||||
| 	r := markup.GetRendererByFileName("a.html").(*external.Renderer) | ||||
| 	r.RenderContentMode = setting.RenderContentModeIframe | ||||
| 
 | ||||
| 	req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	assert.EqualValues(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 	bs, err = io.ReadAll(resp.Body) | ||||
| 	assert.NoError(t, err) | ||||
| 	doc = NewHTMLParser(t, bytes.NewBuffer(bs)) | ||||
| 	iframe := doc.Find("iframe") | ||||
| 	assert.EqualValues(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", "")) | ||||
| 
 | ||||
| 	req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	assert.EqualValues(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 	bs, err = io.ReadAll(resp.Body) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy")) | ||||
| 	assert.EqualValues(t, "<div>\n\ttest external renderer\n</div>\n", string(bs)) | ||||
| } | ||||
|  | ||||
| @ -74,7 +74,6 @@ export class ComboMarkdownEditor { | ||||
|   previewUrl: string; | ||||
|   previewContext: string; | ||||
|   previewMode: string; | ||||
|   previewWiki: boolean; | ||||
| 
 | ||||
|   constructor(container, options = {}) { | ||||
|     container._giteaComboMarkdownEditor = this; | ||||
| @ -213,13 +212,11 @@ export class ComboMarkdownEditor { | ||||
|     this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url'); | ||||
|     this.previewContext = this.tabPreviewer.getAttribute('data-preview-context'); | ||||
|     this.previewMode = this.options.previewMode ?? 'comment'; | ||||
|     this.previewWiki = this.options.previewWiki ?? false; | ||||
|     this.tabPreviewer.addEventListener('click', async () => { | ||||
|       const formData = new FormData(); | ||||
|       formData.append('mode', this.previewMode); | ||||
|       formData.append('context', this.previewContext); | ||||
|       formData.append('text', this.value()); | ||||
|       formData.append('wiki', String(this.previewWiki)); | ||||
|       const response = await POST(this.previewUrl, {data: formData}); | ||||
|       const data = await response.text(); | ||||
|       renderPreviewPanelContent($(panelPreviewer), data); | ||||
|  | ||||
| @ -26,7 +26,6 @@ async function initRepoWikiFormEditor() { | ||||
|       formData.append('mode', editor.previewMode); | ||||
|       formData.append('context', editor.previewContext); | ||||
|       formData.append('text', newContent); | ||||
|       formData.append('wiki', editor.previewWiki); | ||||
|       try { | ||||
|         const response = await POST(editor.previewUrl, {data: formData}); | ||||
|         const data = await response.text(); | ||||
| @ -51,8 +50,7 @@ async function initRepoWikiFormEditor() { | ||||
|     // And another benefit is that we only need to write the style once for both editors.
 | ||||
|     // TODO: Move height style to CSS after EasyMDE removal.
 | ||||
|     editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'}, | ||||
|     previewMode: 'gfm', | ||||
|     previewWiki: true, | ||||
|     previewMode: 'wiki', | ||||
|     easyMDEOptions: { | ||||
|       previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
 | ||||
|       toolbar: ['bold', 'italic', 'strikethrough', '|', | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user