diff --git a/modules/markup/html.go b/modules/markup/html.go
index 2e65827bf7..3355c021ce 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -472,7 +472,7 @@ func createInlineCode(content string) *html.Node {
return code
}
-func createEmoji(content, class, name string) *html.Node {
+func createEmoji(content, class, name, alias string) *html.Node {
span := &html.Node{
Type: html.ElementNode,
Data: atom.Span.String(),
@@ -484,6 +484,9 @@ func createEmoji(content, class, name string) *html.Node {
if name != "" {
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: name})
}
+ if alias != "" {
+ span.Attr = append(span.Attr, html.Attribute{Key: "data-alias", Val: alias})
+ }
text := &html.Node{
Type: html.TextNode,
@@ -502,6 +505,7 @@ func createCustomEmoji(alias string) *html.Node {
}
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"})
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
+ span.Attr = append(span.Attr, html.Attribute{Key: "data-alias", Val: alias})
img := &html.Node{
Type: html.ElementNode,
@@ -1147,7 +1151,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
continue
}
- replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
+ replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description, alias))
node = node.NextSibling.NextSibling
start = 0
}
@@ -1169,7 +1173,7 @@ func emojiProcessor(ctx *RenderContext, node *html.Node) {
start = m[1]
val := emoji.FromCode(codepoint)
if val != nil {
- replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
+ replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description, val.Aliases[0]))
node = node.NextSibling.NextSibling
start = 0
}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 68d1ada5b3..50ea70905c 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -329,42 +329,42 @@ func TestRender_emoji(t *testing.T) {
for i := range emoji.GemojiData {
test(
emoji.GemojiData[i].Emoji,
- `
`+emoji.GemojiData[i].Emoji+`
`)
+ ``+emoji.GemojiData[i].Emoji+`
`)
}
for i := range emoji.GemojiData {
test(
":"+emoji.GemojiData[i].Aliases[0]+":",
- ``+emoji.GemojiData[i].Emoji+`
`)
+ ``+emoji.GemojiData[i].Emoji+`
`)
}
// Text that should be turned into or recognized as emoji
test(
":gitea:",
- `
`)
+ `
`)
test(
":custom-emoji:",
`:custom-emoji:
`)
setting.UI.CustomEmojisMap["custom-emoji"] = ":custom-emoji:"
test(
":custom-emoji:",
- `
`)
+ `
`)
test(
"θΏζ―ε符:1::+1: someπ \U0001f44d:custom-emoji: :gitea:",
- `θΏζ―ε符:1:π someπ `+
- `π `+
- `
`)
+ `θΏζ―ε符:1:π someπ `+
+ `π `+
+ `
`)
test(
"Some text with π in the middle",
- `Some text with π in the middle
`)
+ `Some text with π in the middle
`)
test(
"Some text with :smile: in the middle",
- `Some text with π in the middle
`)
+ `Some text with π in the middle
`)
test(
"Some text with ππ 2 emoji next to each other",
- `Some text with ππ 2 emoji next to each other
`)
+ `Some text with ππ 2 emoji next to each other
`)
test(
"ππ€ͺππ€β",
- `ππ€ͺππ€β
`)
+ `ππ€ͺππ€β
`)
// should match nothing
test(
@@ -601,10 +601,10 @@ func TestPostProcess_RenderDocument(t *testing.T) {
// Test that other post processing still works.
test(
":gitea:",
- ``)
+ ``)
test(
"Some text with π in the middle",
- `Some text with π in the middle`)
+ `Some text with π in the middle`)
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
`person/repo#4 (comment)`)
}
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index e3dc6c9655..84a7c5f882 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -135,8 +135,8 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
See commit 65f1bf27bc
Ideas and codes
-- Bezier widget (by @r-lyeh) ocornut/imgui#786
-- Bezier widget (by @r-lyeh) #786
+- Bezier widget (by @r-lyeh) ocornut/imgui#786
+- Bezier widget (by @r-lyeh) #786
- Node graph editors https://github.com/ocornut/imgui/issues/306
- Memory Editor
- Plot var helper
@@ -422,7 +422,7 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
testcase := `[Link with emoji :moon: in text](https://gitea.io)`
- expected := `Link with emoji π in text
+ expected := `Link with emoji π in text
`
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
require.NoError(t, err)
@@ -855,7 +855,7 @@ mail@domain.com
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -882,7 +882,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -911,7 +911,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -940,7 +940,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -969,7 +969,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -998,7 +998,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -1028,7 +1028,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -1058,7 +1058,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -1088,7 +1088,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -1118,7 +1118,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -1149,7 +1149,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -1180,7 +1180,7 @@ space
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index ddc218c1b8..53ccdfab0d 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -94,7 +94,7 @@ func createDefaultPolicy() *bluemonday.Policy {
}
// Allow classes for anchors
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ref-issue( ref-external-issue)?|mention)$`)).OnElements("a")
// Allow classes for task lists
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
@@ -110,6 +110,7 @@ func createDefaultPolicy() *bluemonday.Policy {
// Allow icons, emojis, chroma syntax and keyword markup on span
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
+ policy.AllowAttrs("data-alias").Matching(regexp.MustCompile(`^[a-zA-Z0-9-_+]+$`)).OnElements("span")
// Allow 'color' and 'background-color' properties for the style attribute on text elements and table cells.
policy.AllowStyles("color", "background-color").OnElements("span", "p", "th", "td")
diff --git a/modules/markup/sanitizer_test.go b/modules/markup/sanitizer_test.go
index 4441a41544..9805a34910 100644
--- a/modules/markup/sanitizer_test.go
+++ b/modules/markup/sanitizer_test.go
@@ -68,6 +68,13 @@ func Test_Sanitizer(t *testing.T) {
`bad`, `bad`,
`bad`, `bad`,
`bad`, `bad`,
+
+ // Mention
+ `@forgejo/UI`, `@forgejo/UI`,
+
+ // Emoji
+ `THUMBS UP`, `THUMBS UP`,
+ `THUMBS UP`, `THUMBS UP`,
}
for i := 0; i < len(testCases); i += 2 {
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index da74298ef7..3479d94039 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -47,12 +47,12 @@ var testMetas = map[string]string{
func TestApostrophesInMentions(t *testing.T) {
rendered := RenderMarkdownToHtml(context.Background(), "@mention-user's comment")
- assert.EqualValues(t, template.HTML("@mention-user's comment
\n"), rendered)
+ assert.EqualValues(t, template.HTML("@mention-user's comment
\n"), rendered)
}
func TestNonExistantUserMention(t *testing.T) {
rendered := RenderMarkdownToHtml(context.Background(), "@ThisUserDoesNotExist @mention-user")
- assert.EqualValues(t, template.HTML("@ThisUserDoesNotExist @mention-user
\n"), rendered)
+ assert.EqualValues(t, template.HTML("@ThisUserDoesNotExist @mention-user
\n"), rendered)
}
func TestRenderCommitBody(t *testing.T) {
@@ -111,12 +111,12 @@ func TestRenderCommitBody(t *testing.T) {
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
space
-` + "`code π #123 code`"
+` + "`code π #123 code`"
assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
}
@@ -148,7 +148,7 @@ https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb..
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -174,7 +174,7 @@ https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb..
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
@mention-user test
#123
@@ -185,7 +185,7 @@ mail@domain.com
}
func TestRenderMarkdownToHtml(t *testing.T) {
- expected := `space @mention-user
+ expected := `
space @mention-user
/just/a/path.bin
https://example.com/file.bin
local link
@@ -200,9 +200,9 @@ func TestRenderMarkdownToHtml(t *testing.T) {
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
-π
+π
mail@domain.com
-@mention-user test
+@mention-user test
#123
space
code :+1: #123 code
diff --git a/package-lock.json b/package-lock.json
index 8a81d65b8b..95c67e00d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"@citation-js/plugin-bibtex": "0.7.16",
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
+ "@github/quote-selection": "2.1.0",
"@github/relative-time-element": "4.4.3",
"@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
@@ -3177,6 +3178,12 @@
"integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A==",
"license": "MIT"
},
+ "node_modules/@github/quote-selection": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@github/quote-selection/-/quote-selection-2.1.0.tgz",
+ "integrity": "sha512-zyTvG6GpfWuVrRnxa/JpWPlTyj8ItTCMHXNrdXrvNPrSFCsDAiqEaxTW+644lwxXNfzTPQeN11paR9SRRvE2zg==",
+ "license": "MIT"
+ },
"node_modules/@github/relative-time-element": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.3.tgz",
diff --git a/package.json b/package.json
index 067310037d..f55ec454f4 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"@citation-js/plugin-bibtex": "0.7.16",
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
+ "@github/quote-selection": "2.1.0",
"@github/relative-time-element": "4.4.3",
"@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
diff --git a/release-notes/5677.md b/release-notes/5677.md
new file mode 100644
index 0000000000..d089cbf01a
--- /dev/null
+++ b/release-notes/5677.md
@@ -0,0 +1 @@
+If you select a portion of a comment and use the 'Quote reply' feature in the context menu, only that portion will be quoted. The markdown syntax is preserved.
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index 5236fd06ae..df70ee49ef 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -76,7 +76,7 @@ func TestAPI_RenderGFM(t *testing.T) {
`,
// Guard wiki sidebar: special syntax
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 2e0c85d0a1..e2597af0e3 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -53,7 +53,7 @@
-
+
-
+
{{if .Issue.RenderedContent}}
{{.Issue.RenderedContent}}
{{else}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 08c83c07d7..99703f81ec 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -59,7 +59,7 @@
code
`, } diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 73aaa457f2..257e735e8e 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -27,6 +27,8 @@ import {hideElem, showElem} from '../utils/dom.js'; import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {attachRefIssueContextPopup} from './contextpopup.js'; import {POST, GET} from '../modules/fetch.js'; +import {MarkdownQuote} from '@github/quote-selection'; +import {toAbsoluteUrl} from '../utils.js'; const {csrfToken} = window.config; @@ -579,32 +581,105 @@ export function initRepository() { initUnicodeEscapeButton(); } +const filters = { + A(el) { + if (el.classList.contains('mention') || el.classList.contains('ref-issue')) { + return el.textContent; + } + return el; + }, + PRE(el) { + const firstChild = el.children[0]; + if (firstChild && el.classList.contains('code-block')) { + // Get the language of the codeblock. + const language = firstChild.className.match(/language-(\S+)/); + // Remove trailing newlines. + const text = el.textContent.replace(/\n+$/, ''); + el.textContent = `\`\`\`${language[1]}\n${text}\n\`\`\`\n\n`; + } + return el; + }, + SPAN(el) { + const emojiAlias = el.getAttribute('data-alias'); + if (emojiAlias && el.classList.contains('emoji')) { + return `:${emojiAlias}:`; + } + if (el.classList.contains('katex')) { + const texCode = el.querySelector('annotation[encoding="application/x-tex"]').textContent; + if (el.parentElement.classList.contains('katex-display')) { + el.textContent = `\\[${texCode}\\]\n\n`; + } else { + el.textContent = `\\(${texCode}\\)\n\n`; + } + } + return el; + }, +}; + +function hasContent(node) { + return node.nodeName === 'IMG' || node.firstChild !== null; +} + +// This code matches that of what is done by @github/quote-selection +function preprocessFragment(fragment) { + const nodeIterator = document.createNodeIterator(fragment, NodeFilter.SHOW_ELEMENT, { + acceptNode(node) { + if (node.nodeName in filters && hasContent(node)) { + return NodeFilter.FILTER_ACCEPT; + } + + return NodeFilter.FILTER_SKIP; + }, + }); + const results = []; + let node = nodeIterator.nextNode(); + + while (node) { + if (node instanceof HTMLElement) { + results.push(node); + } + node = nodeIterator.nextNode(); + } + + // process deepest matches first + results.reverse(); + + for (const el of results) { + el.replaceWith(filters[el.nodeName](el)); + } +} + function initRepoIssueCommentEdit() { // Edit issue or comment content $(document).on('click', '.edit-content', onEditContent); // Quote reply - $(document).on('click', '.quote-reply', async function (event) { + $(document).on('click', '.quote-reply', async (event) => { event.preventDefault(); - const target = $(this).data('target'); - const quote = $(`#${target}`).text().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').find('button.comment-form-reply'); - editor = await handleReply($replyBtn); + const quote = new MarkdownQuote('', preprocessFragment); + + let editorTextArea; + if (event.target.classList.contains('quote-reply-diff')) { + // Temporarily store the range so it doesn't get lost (likely caused by async code). + const currentRange = quote.range; + + const replyButton = event.target.closest('.comment-code-cloud').querySelector('button.comment-form-reply'); + editorTextArea = (await handleReply($(replyButton))).textarea; + + quote.range = currentRange; } else { - // for normal issue/comment page - editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor')); + editorTextArea = document.querySelector('#comment-form .combo-markdown-editor textarea'); } - if (editor) { - if (editor.value()) { - editor.value(`${editor.value()}\n\n${content}`); - } else { - editor.value(content); - } - editor.focus(); - editor.moveCursorToEnd(); + + // Select the whole comment body if there's no selection. + if (quote.range.collapsed) { + quote.select(document.querySelector(`#${event.target.getAttribute('data-target')}`)); + } + + // If the selection is in the comment body, then insert the quote. + if (quote.closest(`#${event.target.getAttribute('data-target')}`)) { + editorTextArea.value += `@${event.target.getAttribute('data-author')} wrote in ${toAbsoluteUrl(event.target.getAttribute('data-reference-url'))}:`; + quote.insert(editorTextArea); } }); }