mirror of
https://github.com/go-gitea/gitea.git
synced 2024-11-27 12:39:29 +08:00
Support quote selected comments to reply (#32431)
Many existing tests were quite hacky, these could be improved later. <details> ![image](https://github.com/user-attachments/assets/93aebb4f-9de5-4cb8-910b-50c64cbcd25a) </details>
This commit is contained in:
parent
276500c314
commit
145e266987
@ -442,7 +442,10 @@ func createLink(href, content, class string) *html.Node {
|
|||||||
a := &html.Node{
|
a := &html.Node{
|
||||||
Type: html.ElementNode,
|
Type: html.ElementNode,
|
||||||
Data: atom.A.String(),
|
Data: atom.A.String(),
|
||||||
Attr: []html.Attribute{{Key: "href", Val: href}},
|
Attr: []html.Attribute{
|
||||||
|
{Key: "href", Val: href},
|
||||||
|
{Key: "data-markdown-generated-content"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if class != "" {
|
if class != "" {
|
||||||
|
@ -30,5 +30,5 @@ func TestRenderCodePreview(t *testing.T) {
|
|||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||||
}
|
}
|
||||||
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
|
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
|
||||||
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
|
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" data-markdown-generated-content="" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
|
||||||
}
|
}
|
||||||
|
@ -33,11 +33,9 @@ func numericIssueLink(baseURL, class string, index int, marker string) string {
|
|||||||
|
|
||||||
// link an HTML link
|
// link an HTML link
|
||||||
func link(href, class, contents string) string {
|
func link(href, class, contents string) string {
|
||||||
if class != "" {
|
extra := ` data-markdown-generated-content=""`
|
||||||
class = " class=\"" + class + "\""
|
extra += util.Iif(class != "", ` class="`+class+`"`, "")
|
||||||
}
|
return fmt.Sprintf(`<a href="%s"%s>%s</a>`, href, extra, contents)
|
||||||
|
|
||||||
return fmt.Sprintf("<a href=\"%s\"%s>%s</a>", href, class, contents)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var numericMetas = map[string]string{
|
var numericMetas = map[string]string{
|
||||||
@ -353,7 +351,9 @@ func TestRender_FullIssueURLs(t *testing.T) {
|
|||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, result.String())
|
actual := result.String()
|
||||||
|
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6",
|
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")
|
"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6")
|
||||||
|
@ -116,7 +116,9 @@ func TestRender_CrossReferences(t *testing.T) {
|
|||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
actual := strings.TrimSpace(buffer)
|
||||||
|
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, strings.TrimSpace(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@ -156,7 +158,9 @@ func TestRender_links(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
actual := strings.TrimSpace(buffer)
|
||||||
|
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, strings.TrimSpace(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
||||||
@ -267,7 +271,9 @@ func TestRender_email(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
|
actual := strings.TrimSpace(res)
|
||||||
|
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, strings.TrimSpace(expected), actual)
|
||||||
}
|
}
|
||||||
// Text that should be turned into email link
|
// Text that should be turned into email link
|
||||||
|
|
||||||
@ -616,7 +622,9 @@ func TestPostProcess_RenderDocument(t *testing.T) {
|
|||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, strings.NewReader(input), &res)
|
}, strings.NewReader(input), &res)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
|
actual := strings.TrimSpace(res.String())
|
||||||
|
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, strings.TrimSpace(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue index shouldn't be post processing in a document.
|
// Issue index shouldn't be post processing in a document.
|
||||||
|
@ -311,7 +311,8 @@ func TestTotal_RenderWiki(t *testing.T) {
|
|||||||
IsWiki: true,
|
IsWiki: true,
|
||||||
}, sameCases[i])
|
}, sameCases[i])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, template.HTML(answers[i]), line)
|
actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, answers[i], actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
@ -336,7 +337,8 @@ func TestTotal_RenderWiki(t *testing.T) {
|
|||||||
IsWiki: true,
|
IsWiki: true,
|
||||||
}, testCases[i])
|
}, testCases[i])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, template.HTML(testCases[i+1]), line)
|
actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.EqualValues(t, testCases[i+1], actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +358,8 @@ func TestTotal_RenderString(t *testing.T) {
|
|||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, sameCases[i])
|
}, sameCases[i])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, template.HTML(answers[i]), line)
|
actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, answers[i], actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []string{}
|
testCases := []string{}
|
||||||
@ -996,7 +999,8 @@ space</p>
|
|||||||
for i, c := range cases {
|
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, IsWiki: c.IsWiki}, input)
|
||||||
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
||||||
assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
|
actual := strings.ReplaceAll(string(result), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, c.Expected, actual, "Unexpected result in testcase %v", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +107,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
|||||||
"start", "summary", "tabindex", "target",
|
"start", "summary", "tabindex", "target",
|
||||||
"title", "type", "usemap", "valign", "value",
|
"title", "type", "usemap", "valign", "value",
|
||||||
"vspace", "width", "itemprop",
|
"vspace", "width", "itemprop",
|
||||||
|
"data-markdown-generated-content",
|
||||||
}
|
}
|
||||||
|
|
||||||
generalSafeElements := []string{
|
generalSafeElements := []string{
|
||||||
|
@ -129,18 +129,18 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
|||||||
<a href="/mention-user" class="mention">@mention-user</a> test
|
<a href="/mention-user" class="mention">@mention-user</a> test
|
||||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
|
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
|
||||||
space`
|
space`
|
||||||
|
actual := strings.ReplaceAll(string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)), ` data-markdown-generated-content=""`, "")
|
||||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitBody(testInput(), testMetas))
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCommitMessage(t *testing.T) {
|
func TestRenderCommitMessage(t *testing.T) {
|
||||||
expected := `space <a href="/mention-user" class="mention">@mention-user</a> `
|
expected := `space <a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a> `
|
||||||
|
|
||||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
|
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCommitMessageLinkSubject(t *testing.T) {
|
func TestRenderCommitMessageLinkSubject(t *testing.T) {
|
||||||
expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" class="mention">@mention-user</a>`
|
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))
|
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
|
||||||
}
|
}
|
||||||
@ -168,7 +168,8 @@ mail@domain.com
|
|||||||
space<SPACE><SPACE>
|
space<SPACE><SPACE>
|
||||||
`
|
`
|
||||||
expected = strings.ReplaceAll(expected, "<SPACE>", " ")
|
expected = strings.ReplaceAll(expected, "<SPACE>", " ")
|
||||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderIssueTitle(testInput(), testMetas))
|
actual := strings.ReplaceAll(string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderMarkdownToHtml(t *testing.T) {
|
func TestRenderMarkdownToHtml(t *testing.T) {
|
||||||
@ -193,7 +194,8 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
|||||||
#123
|
#123
|
||||||
space</p>
|
space</p>
|
||||||
`
|
`
|
||||||
assert.Equal(t, expected, string(newTestRenderUtils().MarkdownToHtml(testInput())))
|
actual := strings.ReplaceAll(string(newTestRenderUtils().MarkdownToHtml(testInput())), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLabels(t *testing.T) {
|
func TestRenderLabels(t *testing.T) {
|
||||||
@ -211,5 +213,5 @@ func TestRenderLabels(t *testing.T) {
|
|||||||
|
|
||||||
func TestUserMention(t *testing.T) {
|
func TestUserMention(t *testing.T) {
|
||||||
rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user")
|
rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user")
|
||||||
assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" rel="nofollow">@mention-user</a> <a href="/mention-user" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered)))
|
assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" data-markdown-generated-content="" rel="nofollow">@mention-user</a> <a href="/mention-user" data-markdown-generated-content="" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered)))
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,8 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
|
|||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
||||||
web.SetForm(ctx, &options)
|
web.SetForm(ctx, &options)
|
||||||
Markup(ctx)
|
Markup(ctx)
|
||||||
assert.Equal(t, expectedBody, resp.Body.String())
|
actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, expectedBody, actual)
|
||||||
assert.Equal(t, expectedCode, resp.Code)
|
assert.Equal(t, expectedCode, resp.Code)
|
||||||
resp.Body.Reset()
|
resp.Body.Reset()
|
||||||
}
|
}
|
||||||
@ -58,7 +59,8 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody
|
|||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||||
web.SetForm(ctx, &options)
|
web.SetForm(ctx, &options)
|
||||||
Markdown(ctx)
|
Markdown(ctx)
|
||||||
assert.Equal(t, responseBody, resp.Body.String())
|
actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "")
|
||||||
|
assert.Equal(t, responseBody, actual)
|
||||||
assert.Equal(t, responseCode, resp.Code)
|
assert.Equal(t, responseCode, resp.Code)
|
||||||
resp.Body.Reset()
|
resp.Body.Reset()
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment comment-body">
|
<div class="ui attached segment comment-body">
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
|
{{template "repo/issue/view_content/context_menu" dict "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment comment-body" role="article">
|
<div class="ui attached segment comment-body" role="article">
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment comment-body" role="article">
|
<div class="ui attached segment comment-body" role="article">
|
||||||
@ -430,7 +430,7 @@
|
|||||||
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,29 +5,29 @@
|
|||||||
<div class="menu">
|
<div class="menu">
|
||||||
{{$referenceUrl := ""}}
|
{{$referenceUrl := ""}}
|
||||||
{{if .issue}}
|
{{if .issue}}
|
||||||
{{$referenceUrl = printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
|
{{$referenceUrl = printf "%s#%s" ctx.RootData.Issue.Link .item.HashTag}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
|
{{$referenceUrl = printf "%s/files#%s" ctx.RootData.Issue.Link .item.HashTag}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
|
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
|
||||||
{{if .ctxData.IsSigned}}
|
{{if ctx.RootData.IsSigned}}
|
||||||
{{$needDivider := false}}
|
{{$needDivider := false}}
|
||||||
{{if not .ctxData.Repository.IsArchived}}
|
{{if not ctx.RootData.Repository.IsArchived}}
|
||||||
{{$needDivider = true}}
|
{{$needDivider = true}}
|
||||||
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
|
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
|
||||||
{{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
|
{{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
|
||||||
<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
|
<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
|
{{if or ctx.RootData.Permission.IsAdmin .IsCommentPoster ctx.RootData.HasIssuesOrPullsWritePermission}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
||||||
{{if .delete}}
|
{{if .delete}}
|
||||||
<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
|
<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{ctx.RootData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}}
|
{{$canUserBlock := call ctx.RootData.CanBlockUser ctx.RootData.SignedUser .item.Poster}}
|
||||||
{{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}}
|
{{$canOrgBlock := and ctx.RootData.Repository.Owner.IsOrganization (call ctx.RootData.CanBlockUser ctx.RootData.Repository.Owner .item.Poster)}}
|
||||||
{{if or $canOrgBlock $canUserBlock}}
|
{{if or $canOrgBlock $canUserBlock}}
|
||||||
{{if $needDivider}}
|
{{if $needDivider}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
|
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if $canOrgBlock}}
|
{{if $canOrgBlock}}
|
||||||
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
|
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{ctx.RootData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import $ from 'jquery';
|
|
||||||
import {handleReply} from './repo-issue.ts';
|
import {handleReply} from './repo-issue.ts';
|
||||||
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
@ -7,11 +6,14 @@ import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts';
|
|||||||
import {attachRefIssueContextPopup} from './contextpopup.ts';
|
import {attachRefIssueContextPopup} from './contextpopup.ts';
|
||||||
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
|
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
|
||||||
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
|
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
|
||||||
|
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
|
||||||
|
|
||||||
async function onEditContent(event) {
|
async function tryOnEditContent(e) {
|
||||||
event.preventDefault();
|
const clickTarget = e.target.closest('.edit-content');
|
||||||
|
if (!clickTarget) return;
|
||||||
|
|
||||||
const segment = this.closest('.header').nextElementSibling;
|
e.preventDefault();
|
||||||
|
const segment = clickTarget.closest('.header').nextElementSibling;
|
||||||
const editContentZone = segment.querySelector('.edit-content-zone');
|
const editContentZone = segment.querySelector('.edit-content-zone');
|
||||||
const renderContent = segment.querySelector('.render-content');
|
const renderContent = segment.querySelector('.render-content');
|
||||||
const rawContent = segment.querySelector('.raw-content');
|
const rawContent = segment.querySelector('.raw-content');
|
||||||
@ -102,33 +104,53 @@ async function onEditContent(event) {
|
|||||||
triggerUploadStateChanged(comboMarkdownEditor.container);
|
triggerUploadStateChanged(comboMarkdownEditor.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractSelectedMarkdown(container: HTMLElement) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection.rangeCount) return '';
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (!container.contains(range.commonAncestorContainer)) return '';
|
||||||
|
|
||||||
|
// todo: if commonAncestorContainer parent has "[data-markdown-original-content]" attribute, use the parent's markdown content
|
||||||
|
// otherwise, use the selected HTML content and respect all "[data-markdown-original-content]/[data-markdown-generated-content]" attributes
|
||||||
|
const contents = selection.getRangeAt(0).cloneContents();
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.append(contents);
|
||||||
|
return convertHtmlToMarkdown(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryOnQuoteReply(e) {
|
||||||
|
const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
|
||||||
|
if (!clickTarget) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const contentToQuoteId = clickTarget.getAttribute('data-target');
|
||||||
|
const targetRawToQuote = document.querySelector<HTMLElement>(`#${contentToQuoteId}.raw-content`);
|
||||||
|
const targetMarkupToQuote = targetRawToQuote.parentElement.querySelector<HTMLElement>('.render-content.markup');
|
||||||
|
let contentToQuote = extractSelectedMarkdown(targetMarkupToQuote);
|
||||||
|
if (!contentToQuote) contentToQuote = targetRawToQuote.textContent;
|
||||||
|
const quotedContent = `${contentToQuote.replace(/^/mg, '> ')}\n`;
|
||||||
|
|
||||||
|
let editor;
|
||||||
|
if (clickTarget.classList.contains('quote-reply-diff')) {
|
||||||
|
const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
|
||||||
|
editor = await handleReply(replyBtn);
|
||||||
|
} else {
|
||||||
|
// for normal issue/comment page
|
||||||
|
editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor.value()) {
|
||||||
|
editor.value(`${editor.value()}\n\n${quotedContent}`);
|
||||||
|
} else {
|
||||||
|
editor.value(quotedContent);
|
||||||
|
}
|
||||||
|
editor.focus();
|
||||||
|
editor.moveCursorToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
export function initRepoIssueCommentEdit() {
|
export function initRepoIssueCommentEdit() {
|
||||||
// Edit issue or comment content
|
document.addEventListener('click', (e) => {
|
||||||
$(document).on('click', '.edit-content', onEditContent);
|
tryOnEditContent(e); // Edit issue or comment content
|
||||||
|
tryOnQuoteReply(e); // Quote reply to the comment editor
|
||||||
// Quote reply
|
|
||||||
$(document).on('click', '.quote-reply', async function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const target = this.getAttribute('data-target');
|
|
||||||
const quote = document.querySelector(`#${target}`).textContent.replace(/\n/g, '\n> ');
|
|
||||||
const content = `> ${quote}\n\n`;
|
|
||||||
|
|
||||||
let editor;
|
|
||||||
if (this.classList.contains('quote-reply-diff')) {
|
|
||||||
const replyBtn = this.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
|
|
||||||
editor = await handleReply(replyBtn);
|
|
||||||
} else {
|
|
||||||
// for normal issue/comment page
|
|
||||||
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
|
|
||||||
}
|
|
||||||
if (editor) {
|
|
||||||
if (editor.value()) {
|
|
||||||
editor.value(`${editor.value()}\n\n${content}`);
|
|
||||||
} else {
|
|
||||||
editor.value(content);
|
|
||||||
}
|
|
||||||
editor.focus();
|
|
||||||
editor.moveCursorToEnd();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
24
web_src/js/markup/html2markdown.test.ts
Normal file
24
web_src/js/markup/html2markdown.test.ts
Normal file
@ -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');
|
||||||
|
});
|
119
web_src/js/markup/html2markdown.ts
Normal file
119
web_src/js/markup/html2markdown.ts
Normal file
@ -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
Block a user