Compare commits

...

6 Commits

Author SHA1 Message Date
Wiktor Kwapisiewicz
37191dcfbd
Fix missing link on outgoing new release notifications (#29079)
Outgoing new release e-mail notifications were missing links to the
actual release. An example from Codeberg.org e-mail:

    <a href=3D"">View it on Codeberg.org</a>.<br/>

This PR adds `"Link"` context property pointing to the release on the
web interface.

The change was tested using `[mailer] PROTOCOL=dummy`.

Signed-off-by: Wiktor Kwapisiewicz <wiktor@metacode.biz>
2024-02-08 03:09:15 +00:00
wxiaoguang
f290c24d28
Avoid showing unnecessary JS errors when there are elements with different origin on the page (#29081)
Try to fix #29080
2024-02-08 02:42:18 +00:00
silverwind
b6bf8041d8
Fix gitea-origin-url with default ports (#29085)
When setting `url.host` on a URL object with no port specified (like is
the case of default port), the resulting URL's port will not change.
Workaround this quirk in the URL standard by explicitely setting port
for the http and https protocols.

Extracted the logic to a function for the purpose of testing. Initially
I wanted to have the function in utils.js, but it turns out esbuild can
not treeshake the unused functions which would result in the
webcomponents chunk having all 2kB utils.js inlined, so it seemed not
worth.

Fixes: https://github.com/go-gitea/gitea/issues/29084
2024-02-08 02:37:09 +00:00
wxiaoguang
a4859dcfea
Improve user experience for outdated comments (#29050)
Try to improve #28949

1. Make `ctx.Data["ShowOutdatedComments"] = true` by default: it brings
consistent user experience, and sometimes the "outdated (source
changed)" comments are still valuable.
2. Show a friendly message if the comment won't show, then the end users
won't fell that "the comment disappears" (it is the special case when
`ShowOutdatedComments = false`)
2024-02-08 01:50:48 +00:00
dark-angel
5c0fc90872
fix: Elasticsearch: Request Entity Too Large #28117 (#29062)
Fix for gitea putting everything into one request without batching and
sending it to Elasticsearch for indexing as issued in #28117

This issue occured in large repositories while Gitea tries to 
index the code using ElasticSearch.

I've applied necessary changes that takes batch length from below config
(app.ini)
```
[queue.code_indexer]
BATCH_LENGTH=<length_int>
```
and batches all requests to Elasticsearch in chunks as configured in the
above config
2024-02-07 08:57:16 +00:00
wxiaoguang
2bac85dc33
Fix orgmode link resolving (#29024)
Fix #28974

Add some new tests and fix some legacy unclear tests.
2024-02-07 08:32:31 +00:00
15 changed files with 206 additions and 83 deletions

View File

@ -811,7 +811,7 @@ rules:
wc/no-constructor-params: [2] wc/no-constructor-params: [2]
wc/no-constructor: [2] wc/no-constructor: [2]
wc/no-customized-built-in-elements: [2] wc/no-customized-built-in-elements: [2]
wc/no-exports-with-element: [2] wc/no-exports-with-element: [0]
wc/no-invalid-element-name: [2] wc/no-invalid-element-name: [2]
wc/no-invalid-extends: [2] wc/no-invalid-extends: [2]
wc/no-method-prefixed-with-on: [2] wc/no-method-prefixed-with-on: [2]

View File

@ -40,8 +40,19 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
return req return req
} }
type MockContextOption struct {
Render context.Render
}
// MockContext mock context for unit tests // MockContext mock context for unit tests
func MockContext(t *testing.T, reqPath string) (*context.Context, *httptest.ResponseRecorder) { func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*context.Context, *httptest.ResponseRecorder) {
var opt MockContextOption
if len(opts) > 0 {
opt = opts[0]
}
if opt.Render == nil {
opt.Render = &MockRender{}
}
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req := mockRequest(t, reqPath) req := mockRequest(t, reqPath)
base, baseCleanUp := context.NewBaseContext(resp, req) base, baseCleanUp := context.NewBaseContext(resp, req)
@ -49,7 +60,7 @@ func MockContext(t *testing.T, reqPath string) (*context.Context, *httptest.Resp
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
ctx := context.NewWebContext(base, &MockRender{}, nil) ctx := context.NewWebContext(base, opt.Render, nil)
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)

View File

@ -180,12 +180,18 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st
} }
if len(reqs) > 0 { if len(reqs) > 0 {
esBatchSize := 50
for i := 0; i < len(reqs); i += esBatchSize {
_, err := b.inner.Client.Bulk(). _, err := b.inner.Client.Bulk().
Index(b.inner.VersionedIndexName()). Index(b.inner.VersionedIndexName()).
Add(reqs...). Add(reqs[i:min(i+esBatchSize, len(reqs))]...).
Do(ctx) Do(ctx)
if err != nil {
return err return err
} }
}
}
return nil return nil
} }

View File

@ -133,18 +133,18 @@ type Writer struct {
Ctx *markup.RenderContext Ctx *markup.RenderContext
} }
const mailto = "mailto:" func (r *Writer) resolveLink(kind, link string) string {
link = strings.TrimPrefix(link, "file:")
func (r *Writer) resolveLink(l org.RegularLink) string { if !strings.HasPrefix(link, "#") && // not a URL fragment
link := html.EscapeString(l.URL) !markup.IsLinkStr(link) && // not an absolute URL
if l.Protocol == "file" { !strings.HasPrefix(link, "mailto:") {
link = link[len("file:"):] if kind == "regular" {
// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]"
// so we need to try to guess the link kind again here
kind = org.RegularLink{URL: link}.Kind()
} }
if len(link) > 0 && !markup.IsLinkStr(link) &&
link[0] != '#' && !strings.HasPrefix(link, mailto) {
base := r.Ctx.Links.Base base := r.Ctx.Links.Base
switch l.Kind() { if kind == "image" || kind == "video" {
case "image", "video":
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki) base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
} }
link = util.URLJoin(base, link) link = util.URLJoin(base, link)
@ -154,29 +154,29 @@ func (r *Writer) resolveLink(l org.RegularLink) string {
// WriteRegularLink renders images, links or videos // WriteRegularLink renders images, links or videos
func (r *Writer) WriteRegularLink(l org.RegularLink) { func (r *Writer) WriteRegularLink(l org.RegularLink) {
link := r.resolveLink(l) link := r.resolveLink(l.Kind(), l.URL)
// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
switch l.Kind() { switch l.Kind() {
case "image": case "image":
if l.Description == nil { if l.Description == nil {
fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link) _, _ = fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
} else { } else {
imageSrc := r.resolveLink(l.Description[0].(org.RegularLink)) imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc) _, _ = fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
} }
case "video": case "video":
if l.Description == nil { if l.Description == nil {
fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link) _, _ = fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
} else { } else {
videoSrc := r.resolveLink(l.Description[0].(org.RegularLink)) videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc) _, _ = fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
} }
default: default:
description := link description := link
if l.Description != nil { if l.Description != nil {
description = r.WriteNodesAsString(l.Description...) description = r.WriteNodesAsString(l.Description...)
} }
fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description) _, _ = fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description)
} }
} }

View File

@ -10,26 +10,21 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const ( const AppURL = "http://localhost:3000/"
AppURL = "http://localhost:3000/"
Repo = "gogits/gogs"
AppSubURL = AppURL + Repo + "/"
)
func TestRender_StandardLinks(t *testing.T) { func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
Base: setting.AppSubURL, Base: "/relative-path",
BranchPath: "branch/main",
}, },
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
@ -38,32 +33,30 @@ func TestRender_StandardLinks(t *testing.T) {
test("[[https://google.com/]]", test("[[https://google.com/]]",
`<p><a href="https://google.com/">https://google.com/</a></p>`) `<p><a href="https://google.com/">https://google.com/</a></p>`)
test("[[WikiPage][The WikiPage Desc]]",
lnk := util.URLJoin(AppSubURL, "WikiPage") `<p><a href="/relative-path/WikiPage">The WikiPage Desc</a></p>`)
test("[[WikiPage][WikiPage]]", test("[[ImageLink.svg][The Image Desc]]",
`<p><a href="`+lnk+`">WikiPage</a></p>`) `<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
} }
func TestRender_Media(t *testing.T) { func TestRender_Media(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
Base: setting.AppSubURL, Base: "./relative-path",
}, },
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
url := "../../.images/src/02/train.jpg" test("[[file:../../.images/src/02/train.jpg]]",
result := util.URLJoin(AppSubURL, url) `<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg" /></p>`)
test("[[file:train.jpg]]",
test("[[file:"+url+"]]", `<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg" /></p>`)
`<p><img src="`+result+`" alt="`+result+`" /></p>`)
// With description. // With description.
test("[[https://example.com][https://example.com/example.svg]]", test("[[https://example.com][https://example.com/example.svg]]",
@ -80,11 +73,20 @@ func TestRender_Media(t *testing.T) {
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`) `<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`)
test("[[https://example.com/example.mp4]]", test("[[https://example.com/example.mp4]]",
`<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`) `<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
// test [[LINK][DESCRIPTION]] syntax with "file:" prefix
test(`[[https://example.com/][file:https://example.com/foo%20bar.svg]]`,
`<p><a href="https://example.com/"><img src="https://example.com/foo%20bar.svg" alt="https://example.com/foo%20bar.svg" /></a></p>`)
test(`[[file:https://example.com/foo%20bar.svg][Goto Image]]`,
`<p><a href="https://example.com/foo%20bar.svg">Goto Image</a></p>`)
test(`[[file:https://example.com/link][https://example.com/image.jpg]]`,
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg" /></a></p>`)
test(`[[file:https://example.com/link][file:https://example.com/image.jpg]]`,
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg" /></a></p>`)
} }
func TestRender_Source(t *testing.T) { func TestRender_Source(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{

View File

@ -98,23 +98,15 @@ func SetWhitespaceBehavior(ctx *context.Context) {
// SetShowOutdatedComments set the show outdated comments option as context variable // SetShowOutdatedComments set the show outdated comments option as context variable
func SetShowOutdatedComments(ctx *context.Context) { func SetShowOutdatedComments(ctx *context.Context) {
showOutdatedCommentsValue := ctx.FormString("show-outdated") showOutdatedCommentsValue := ctx.FormString("show-outdated")
// var showOutdatedCommentsValue string
if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" { if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" {
// invalid or no value for this form string -> use default or stored user setting // invalid or no value for this form string -> use default or stored user setting
showOutdatedCommentsValue = "true"
if ctx.IsSigned { if ctx.IsSigned {
showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, "false") showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue)
} else {
// not logged in user -> use the default value
showOutdatedCommentsValue = "false"
} }
} else { } else if ctx.IsSigned {
// valid value -> update user setting if user is logged in // valid value -> update user setting if user is logged in
if ctx.IsSigned {
_ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue) _ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue)
} }
} ctx.Data["ShowOutdatedComments"], _ = strconv.ParseBool(showOutdatedCommentsValue)
showOutdatedComments, _ := strconv.ParseBool(showOutdatedCommentsValue)
ctx.Data["ShowOutdatedComments"] = showOutdatedComments
} }

View File

@ -22,6 +22,7 @@ import (
const ( const (
tplDiffConversation base.TplName = "repo/diff/conversation" tplDiffConversation base.TplName = "repo/diff/conversation"
tplConversationOutdated base.TplName = "repo/diff/conversation_outdated"
tplTimelineConversation base.TplName = "repo/issue/view_content/conversation" tplTimelineConversation base.TplName = "repo/issue/view_content/conversation"
tplNewComment base.TplName = "repo/diff/new_comment" tplNewComment base.TplName = "repo/diff/new_comment"
) )
@ -161,8 +162,8 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return return
} }
if len(comments) == 0 { if len(comments) == 0 {
// if the comments are empty (deleted, outdated, etc), it doesn't need to render anything, just return an empty body to replace "conversation-holder" on the page // if the comments are empty (deleted, outdated, etc), it's better to tell the users that it is outdated
ctx.Resp.WriteHeader(http.StatusOK) ctx.HTML(http.StatusOK, tplConversationOutdated)
return return
} }

View File

@ -0,0 +1,76 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http/httptest"
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/pull"
"github.com/stretchr/testify/assert"
)
func TestRenderConversation(t *testing.T) {
unittest.PrepareTestEnv(t)
pr, _ := issues_model.GetPullRequestByID(db.DefaultContext, 2)
_ = pr.LoadIssue(db.DefaultContext)
_ = pr.Issue.LoadPoster(db.DefaultContext)
_ = pr.Issue.LoadRepo(db.DefaultContext)
run := func(name string, cb func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder)) {
t.Run(name, func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
contexttest.LoadUser(t, ctx, pr.Issue.PosterID)
contexttest.LoadRepo(t, ctx, pr.BaseRepoID)
contexttest.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close()
cb(t, ctx, resp)
})
}
var preparedComment *issues_model.Comment
run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID)
if !assert.NoError(t, err) {
return
}
comment.Invalidated = true
err = issues_model.UpdateCommentInvalidate(ctx, comment)
if !assert.NoError(t, err) {
return
}
preparedComment = comment
})
if !assert.NotNil(t, preparedComment) {
return
}
run("diff with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = true
renderConversation(ctx, preparedComment, "diff")
assert.Contains(t, resp.Body.String(), `<div class="content comment-container"`)
})
run("diff without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = false
renderConversation(ctx, preparedComment, "diff")
assert.Contains(t, resp.Body.String(), `conversation-not-existing`)
})
run("timeline with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = true
renderConversation(ctx, preparedComment, "timeline")
assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
})
run("timeline without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = false
renderConversation(ctx, preparedComment, "timeline")
assert.Contains(t, resp.Body.String(), `conversation-not-existing`)
})
}

View File

@ -74,6 +74,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
"Release": rel, "Release": rel,
"Subject": subject, "Subject": subject,
"Language": locale.Language(), "Language": locale.Language(),
"Link": rel.HTMLURL(),
} }
var mailBody bytes.Buffer var mailBody bytes.Buffer

View File

@ -0,0 +1,3 @@
<div class="ui segment conversation-holder conversation-not-existing">
{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}
</div>

View File

@ -1,5 +1,6 @@
// for performance considerations, it only uses performant syntax import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
// for performance considerations, it only uses performant syntax
function attachDirAuto(el) { function attachDirAuto(el) {
if (el.type !== 'hidden' && if (el.type !== 'hidden' &&
el.type !== 'checkbox' && el.type !== 'checkbox' &&
@ -18,7 +19,7 @@ export function initDirAuto() {
const len = mutation.addedNodes.length; const len = mutation.addedNodes.length;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const addedNode = mutation.addedNodes[i]; const addedNode = mutation.addedNodes[i];
if (addedNode.nodeType !== Node.ELEMENT_NODE && addedNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) continue; if (!isDocumentFragmentOrElementNode(addedNode)) continue;
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode); if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode);
const children = addedNode.querySelectorAll('input, textarea'); const children = addedNode.querySelectorAll('input, textarea');
const len = children.length; const len = children.length;

View File

@ -1,4 +1,5 @@
import tippy, {followCursor} from 'tippy.js'; import tippy, {followCursor} from 'tippy.js';
import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
const visibleInstances = new Set(); const visibleInstances = new Set();
@ -136,8 +137,6 @@ function attachChildrenLazyTooltip(target) {
} }
} }
const elementNodeTypes = new Set([Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE]);
export function initGlobalTooltips() { export function initGlobalTooltips() {
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed // use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
const observerConnect = (observer) => observer.observe(document, { const observerConnect = (observer) => observer.observe(document, {
@ -152,13 +151,12 @@ export function initGlobalTooltips() {
if (mutation.type === 'childList') { if (mutation.type === 'childList') {
// mainly for Vue components and AJAX rendered elements // mainly for Vue components and AJAX rendered elements
for (const el of mutation.addedNodes) { for (const el of mutation.addedNodes) {
if (elementNodeTypes.has(el.nodeType)) { if (!isDocumentFragmentOrElementNode(el)) continue;
attachChildrenLazyTooltip(el); attachChildrenLazyTooltip(el);
if (el.hasAttribute('data-tooltip-content')) { if (el.hasAttribute('data-tooltip-content')) {
attachLazyTooltip(el); attachLazyTooltip(el);
} }
} }
}
} else if (mutation.type === 'attributes') { } else if (mutation.type === 'attributes') {
attachTooltip(mutation.target); attachTooltip(mutation.target);
} }

View File

@ -59,6 +59,17 @@ export function onDomReady(cb) {
} }
} }
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
export function isDocumentFragmentOrElementNode(el) {
try {
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
} catch {
// in case the el is not in the same origin, then the access to nodeType would fail
return false;
}
}
// autosize a textarea to fit content. Based on // autosize a textarea to fit content. Based on
// https://github.com/github/textarea-autosize // https://github.com/github/textarea-autosize
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

View File

@ -1,17 +1,21 @@
// Convert an absolute or relative URL to an absolute URL with the current origin // Convert an absolute or relative URL to an absolute URL with the current origin
window.customElements.define('gitea-origin-url', class extends HTMLElement { export function toOriginUrl(urlStr) {
connectedCallback() {
const urlStr = this.getAttribute('data-url');
try { try {
// only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx') // only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx')
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) { if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
const url = new URL(urlStr, window.origin); const {origin, protocol, hostname, port} = window.location;
url.protocol = window.location.protocol; const url = new URL(urlStr, origin);
url.host = window.location.host; url.protocol = protocol;
this.textContent = url.toString(); url.hostname = hostname;
return; url.port = port || (protocol === 'https:' ? '443' : '80');
return url.toString();
} }
} catch {} } catch {}
this.textContent = urlStr; return urlStr;
}
window.customElements.define('gitea-origin-url', class extends HTMLElement {
connectedCallback() {
this.textContent = toOriginUrl(this.getAttribute('data-url'));
} }
}); });

View File

@ -0,0 +1,17 @@
import {toOriginUrl} from './GiteaOriginUrl.js';
test('toOriginUrl', () => {
const oldLocation = window.location;
for (const origin of ['https://example.com', 'https://example.com:3000']) {
window.location = new URL(`${origin}/`);
expect(toOriginUrl('/')).toEqual(`${origin}/`);
expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`);
expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`);
expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`);
}
window.location = oldLocation;
});