mirror of https://github.com/go-gitea/gitea
commit
e07ddcb5d0
@ -0,0 +1,52 @@ |
|||||||
|
// 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 improveActionTableIndicesAction struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
UserID int64 `xorm:"INDEX"` // Receiver user id.
|
||||||
|
OpType int |
||||||
|
ActUserID int64 // Action user id.
|
||||||
|
RepoID int64 |
||||||
|
CommentID int64 `xorm:"INDEX"` |
||||||
|
IsDeleted bool `xorm:"NOT NULL DEFAULT false"` |
||||||
|
RefName string |
||||||
|
IsPrivate bool `xorm:"NOT NULL DEFAULT false"` |
||||||
|
Content string `xorm:"TEXT"` |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"created"` |
||||||
|
} |
||||||
|
|
||||||
|
// TableName sets the name of this table
|
||||||
|
func (*improveActionTableIndicesAction) TableName() string { |
||||||
|
return "action" |
||||||
|
} |
||||||
|
|
||||||
|
func (a *improveActionTableIndicesAction) TableIndices() []*schemas.Index { |
||||||
|
repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) |
||||||
|
repoIndex.AddColumn("repo_id", "user_id", "is_deleted") |
||||||
|
|
||||||
|
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) |
||||||
|
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") |
||||||
|
|
||||||
|
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) |
||||||
|
cudIndex.AddColumn("created_unix", "user_id", "is_deleted") |
||||||
|
|
||||||
|
cuIndex := schemas.NewIndex("c_u", schemas.IndexType) |
||||||
|
cuIndex.AddColumn("user_id", "is_deleted") |
||||||
|
|
||||||
|
indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex} |
||||||
|
|
||||||
|
return indices |
||||||
|
} |
||||||
|
|
||||||
|
func AddNewIndexForUserDashboard(x *xorm.Engine) error { |
||||||
|
return x.Sync(new(improveActionTableIndicesAction)) |
||||||
|
} |
@ -1,48 +0,0 @@ |
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package repository |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db" |
|
||||||
"code.gitea.io/gitea/models/perm" |
|
||||||
access_model "code.gitea.io/gitea/models/perm/access" |
|
||||||
repo_model "code.gitea.io/gitea/models/repo" |
|
||||||
user_model "code.gitea.io/gitea/models/user" |
|
||||||
|
|
||||||
"xorm.io/builder" |
|
||||||
) |
|
||||||
|
|
||||||
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error { |
|
||||||
if err := repo.LoadOwner(ctx); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) { |
|
||||||
return user_model.ErrBlockedUser |
|
||||||
} |
|
||||||
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error { |
|
||||||
has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{ |
|
||||||
"repo_id": repo.ID, |
|
||||||
"user_id": u.ID, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} else if has { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
if err = db.Insert(ctx, &repo_model.Collaboration{ |
|
||||||
RepoID: repo.ID, |
|
||||||
UserID: u.ID, |
|
||||||
Mode: perm.AccessModeWrite, |
|
||||||
}); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return access_model.RecalculateUserAccess(ctx, repo, u.ID) |
|
||||||
}) |
|
||||||
} |
|
@ -1,280 +0,0 @@ |
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package repository |
|
||||||
|
|
||||||
import ( |
|
||||||
"testing" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db" |
|
||||||
"code.gitea.io/gitea/models/organization" |
|
||||||
perm_model "code.gitea.io/gitea/models/perm" |
|
||||||
access_model "code.gitea.io/gitea/models/perm/access" |
|
||||||
repo_model "code.gitea.io/gitea/models/repo" |
|
||||||
"code.gitea.io/gitea/models/unit" |
|
||||||
"code.gitea.io/gitea/models/unittest" |
|
||||||
user_model "code.gitea.io/gitea/models/user" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert" |
|
||||||
) |
|
||||||
|
|
||||||
func TestRepository_AddCollaborator(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
testSuccess := func(repoID, userID int64) { |
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) |
|
||||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext)) |
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) |
|
||||||
assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) |
|
||||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID}) |
|
||||||
} |
|
||||||
testSuccess(1, 4) |
|
||||||
testSuccess(1, 4) |
|
||||||
testSuccess(3, 4) |
|
||||||
} |
|
||||||
|
|
||||||
func TestRepoPermissionPublicNonOrgRepo(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
// public non-organization repo
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) |
|
||||||
assert.NoError(t, repo.LoadUnits(db.DefaultContext)) |
|
||||||
|
|
||||||
// plain user
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
|
||||||
perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// change to collaborator
|
|
||||||
assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// collaborator
|
|
||||||
collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, collaborator) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// owner
|
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// admin
|
|
||||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestRepoPermissionPrivateNonOrgRepo(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
// private non-organization repo
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) |
|
||||||
assert.NoError(t, repo.LoadUnits(db.DefaultContext)) |
|
||||||
|
|
||||||
// plain user
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) |
|
||||||
perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.False(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// change to collaborator to default write access
|
|
||||||
assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// owner
|
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// admin
|
|
||||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestRepoPermissionPublicOrgRepo(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
// public organization repo
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) |
|
||||||
assert.NoError(t, repo.LoadUnits(db.DefaultContext)) |
|
||||||
|
|
||||||
// plain user
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) |
|
||||||
perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// change to collaborator to default write access
|
|
||||||
assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// org member team owner
|
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// org member team tester
|
|
||||||
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, member) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
} |
|
||||||
assert.True(t, perm.CanWrite(unit.TypeIssues)) |
|
||||||
assert.False(t, perm.CanWrite(unit.TypeCode)) |
|
||||||
|
|
||||||
// admin
|
|
||||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestRepoPermissionPrivateOrgRepo(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
// private organization repo
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24}) |
|
||||||
assert.NoError(t, repo.LoadUnits(db.DefaultContext)) |
|
||||||
|
|
||||||
// plain user
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) |
|
||||||
perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.False(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// change to collaborator to default write access
|
|
||||||
assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.False(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// org member team owner
|
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// update team information and then check permission
|
|
||||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5}) |
|
||||||
err = organization.UpdateTeamUnits(db.DefaultContext, team, nil) |
|
||||||
assert.NoError(t, err) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
|
|
||||||
// org member team tester
|
|
||||||
tester := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, tester) |
|
||||||
assert.NoError(t, err) |
|
||||||
assert.True(t, perm.CanWrite(unit.TypeIssues)) |
|
||||||
assert.False(t, perm.CanWrite(unit.TypeCode)) |
|
||||||
assert.False(t, perm.CanRead(unit.TypeCode)) |
|
||||||
|
|
||||||
// org member team reviewer
|
|
||||||
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, reviewer) |
|
||||||
assert.NoError(t, err) |
|
||||||
assert.False(t, perm.CanRead(unit.TypeIssues)) |
|
||||||
assert.False(t, perm.CanWrite(unit.TypeCode)) |
|
||||||
assert.True(t, perm.CanRead(unit.TypeCode)) |
|
||||||
|
|
||||||
// admin
|
|
||||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) |
|
||||||
perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) |
|
||||||
assert.NoError(t, err) |
|
||||||
for _, unit := range repo.Units { |
|
||||||
assert.True(t, perm.CanRead(unit.Type)) |
|
||||||
assert.True(t, perm.CanWrite(unit.Type)) |
|
||||||
} |
|
||||||
} |
|
Binary file not shown.
@ -1,2 +1,2 @@ |
|||||||
P pack-393dc29256bc27cb2ec73898507df710be7a3cf5.pack |
P pack-a7bef76cf6e2b46bc816936ab69306fb10aea571.pack |
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,40 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth" |
||||||
|
repo_model "code.gitea.io/gitea/models/repo" |
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
"code.gitea.io/gitea/tests" |
||||||
|
) |
||||||
|
|
||||||
|
func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { |
||||||
|
// Limit the allowed release types (since by default there is no restriction)
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")() |
||||||
|
defer tests.PrepareTestEnv(t)() |
||||||
|
|
||||||
|
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9}) |
||||||
|
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID}) |
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) |
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||||
|
|
||||||
|
session := loginUser(t, repoOwner.Name) |
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) |
||||||
|
|
||||||
|
filename := "file.bad" |
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", repoOwner.Name, repo.Name, release.ID, attachment.ID) |
||||||
|
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ |
||||||
|
"name": filename, |
||||||
|
}).AddTokenAuth(token) |
||||||
|
|
||||||
|
session.MakeRequest(t, req, http.StatusUnprocessableEntity) |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
import {convertHtmlToMarkdown} from './html2markdown.ts'; |
||||||
|
import {createElementFromHTML} from '../utils/dom.ts'; |
||||||
|
|
||||||
|
const h = createElementFromHTML; |
||||||
|
|
||||||
|
test('convertHtmlToMarkdown', () => { |
||||||
|
expect(convertHtmlToMarkdown(h(`<h1>h</h1>`))).toBe('# h'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<strong>txt</strong>`))).toBe('**txt**'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<em>txt</em>`))).toBe('_txt_'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<del>txt</del>`))).toBe('~~txt~~'); |
||||||
|
|
||||||
|
expect(convertHtmlToMarkdown(h(`<a href="link">txt</a>`))).toBe('[txt](link)'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<a href="https://link">https://link</a>`))).toBe('https://link'); |
||||||
|
|
||||||
|
expect(convertHtmlToMarkdown(h(`<img src="link">`))).toBe('![image](link)'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<img src="link" alt="name">`))).toBe('![name](link)'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<img src="link" width="1" height="1">`))).toBe('<img alt="image" width="1" height="1" src="link">'); |
||||||
|
|
||||||
|
expect(convertHtmlToMarkdown(h(`<p>txt</p>`))).toBe('txt\n'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<blockquote>a\nb</blockquote>`))).toBe('> a\n> b\n'); |
||||||
|
|
||||||
|
expect(convertHtmlToMarkdown(h(`<ol><li>a<ul><li>b</li></ul></li></ol>`))).toBe('1. a\n * b\n\n'); |
||||||
|
expect(convertHtmlToMarkdown(h(`<ol><li><input checked>a</li></ol>`))).toBe('1. [x] a\n'); |
||||||
|
}); |
@ -0,0 +1,119 @@ |
|||||||
|
import {htmlEscape} from 'escape-goat'; |
||||||
|
|
||||||
|
type Processors = { |
||||||
|
[tagName: string]: (el: HTMLElement) => string | HTMLElement | void; |
||||||
|
} |
||||||
|
|
||||||
|
type ProcessorContext = { |
||||||
|
elementIsFirst: boolean; |
||||||
|
elementIsLast: boolean; |
||||||
|
listNestingLevel: number; |
||||||
|
} |
||||||
|
|
||||||
|
function prepareProcessors(ctx:ProcessorContext): Processors { |
||||||
|
const processors = { |
||||||
|
H1(el) { |
||||||
|
const level = parseInt(el.tagName.slice(1)); |
||||||
|
el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; |
||||||
|
}, |
||||||
|
STRONG(el) { |
||||||
|
return `**${el.textContent}**`; |
||||||
|
}, |
||||||
|
EM(el) { |
||||||
|
return `_${el.textContent}_`; |
||||||
|
}, |
||||||
|
DEL(el) { |
||||||
|
return `~~${el.textContent}~~`; |
||||||
|
}, |
||||||
|
|
||||||
|
A(el) { |
||||||
|
const text = el.textContent || 'link'; |
||||||
|
const href = el.getAttribute('href'); |
||||||
|
if (/^https?:/.test(text) && text === href) { |
||||||
|
return text; |
||||||
|
} |
||||||
|
return href ? `[${text}](${href})` : text; |
||||||
|
}, |
||||||
|
IMG(el) { |
||||||
|
const alt = el.getAttribute('alt') || 'image'; |
||||||
|
const src = el.getAttribute('src'); |
||||||
|
const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : ''; |
||||||
|
const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : ''; |
||||||
|
if (widthAttr || heightAttr) { |
||||||
|
return `<img alt="${htmlEscape(alt)}"${widthAttr}${heightAttr} src="${htmlEscape(src)}">`; |
||||||
|
} |
||||||
|
return `![${alt}](${src})`; |
||||||
|
}, |
||||||
|
|
||||||
|
P(el) { |
||||||
|
el.textContent = `${el.textContent}\n`; |
||||||
|
}, |
||||||
|
BLOCKQUOTE(el) { |
||||||
|
el.textContent = `${el.textContent.replace(/^/mg, '> ')}\n`; |
||||||
|
}, |
||||||
|
|
||||||
|
OL(el) { |
||||||
|
const preNewLine = ctx.listNestingLevel ? '\n' : ''; |
||||||
|
el.textContent = `${preNewLine}${el.textContent}\n`; |
||||||
|
}, |
||||||
|
LI(el) { |
||||||
|
const parent = el.parentNode; |
||||||
|
const bullet = parent.tagName === 'OL' ? `1. ` : '* '; |
||||||
|
const nestingIdentLevel = Math.max(0, ctx.listNestingLevel - 1); |
||||||
|
el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`; |
||||||
|
return el; |
||||||
|
}, |
||||||
|
INPUT(el) { |
||||||
|
return el.checked ? '[x] ' : '[ ] '; |
||||||
|
}, |
||||||
|
|
||||||
|
CODE(el) { |
||||||
|
const text = el.textContent; |
||||||
|
if (el.parentNode && el.parentNode.tagName === 'PRE') { |
||||||
|
el.textContent = `\`\`\`\n${text}\n\`\`\`\n`; |
||||||
|
return el; |
||||||
|
} |
||||||
|
if (text.includes('`')) { |
||||||
|
return `\`\` ${text} \`\``; |
||||||
|
} |
||||||
|
return `\`${text}\``; |
||||||
|
}, |
||||||
|
}; |
||||||
|
processors['UL'] = processors.OL; |
||||||
|
for (let level = 2; level <= 6; level++) { |
||||||
|
processors[`H${level}`] = processors.H1; |
||||||
|
} |
||||||
|
return processors; |
||||||
|
} |
||||||
|
|
||||||
|
function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement) { |
||||||
|
if (el.hasAttribute('data-markdown-generated-content')) return el.textContent; |
||||||
|
if (el.tagName === 'A' && el.children.length === 1 && el.children[0].tagName === 'IMG') { |
||||||
|
return processElement(ctx, processors, el.children[0] as HTMLElement); |
||||||
|
} |
||||||
|
|
||||||
|
const isListContainer = el.tagName === 'OL' || el.tagName === 'UL'; |
||||||
|
if (isListContainer) ctx.listNestingLevel++; |
||||||
|
for (let i = 0; i < el.children.length; i++) { |
||||||
|
ctx.elementIsFirst = i === 0; |
||||||
|
ctx.elementIsLast = i === el.children.length - 1; |
||||||
|
processElement(ctx, processors, el.children[i] as HTMLElement); |
||||||
|
} |
||||||
|
if (isListContainer) ctx.listNestingLevel--; |
||||||
|
|
||||||
|
if (processors[el.tagName]) { |
||||||
|
const ret = processors[el.tagName](el); |
||||||
|
if (ret && ret !== el) { |
||||||
|
el.replaceWith(typeof ret === 'string' ? document.createTextNode(ret) : ret); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function convertHtmlToMarkdown(el: HTMLElement): string { |
||||||
|
const div = document.createElement('div'); |
||||||
|
div.append(el); |
||||||
|
const ctx = {} as ProcessorContext; |
||||||
|
ctx.listNestingLevel = 0; |
||||||
|
processElement(ctx, prepareProcessors(ctx), el); |
||||||
|
return div.textContent; |
||||||
|
} |
Loading…
Reference in new issue