fix: paste copied code as text, not as a markdown table (#40053) (#40280)

Backport #40053 by @silverwind

Copying lines from a diff, file view, blame, code search or code preview
and pasting them into a comment on Gitea or GitHub produced a markdown
table with escaped pipes and stripped indentation. Both use
`@github/paste-markdown`, which converts copied HTML tables unless they
carry `data-paste-markdown-skip`, as GitHub's own code tables do.
Gitea's code tables now set it too.

The test is a bit more complicated because of a upstream bug which
causes `paste-markdown` to insert pasted content twice, once
https://github.com/github/paste-markdown/pull/128 is merged and
released, the negative assertion could be removed.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Giteabot
2026-11-08 10:02:48 +00:00
committed by GitHub
parent f357a03049
commit 7d73ce020e
12 changed files with 39 additions and 11 deletions

View File

@@ -31,8 +31,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
// Chroma always uses 1-2 letters for style names, we could tolerate it at the moment
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^\w{0,2}$`)).OnElements("span")
// Line numbers on codepreview
// Line numbers and paste behavior on codepreview
policy.AllowAttrs("data-line-number").OnElements("span")
policy.AllowAttrs("data-paste-markdown-skip").OnElements("table")
// emoji aliases for dark theme inversion
policy.AllowAttrs("data-alias").OnElements("span")

View File

@@ -49,6 +49,8 @@ func TestSanitizer(t *testing.T) {
`<p style="bad-color: red">Hello World</p>`, `<p>Hello World</p>`,
`<code style="bad-color: red">Hello World</code>`, `<code>Hello World</code>`,
`<table data-paste-markdown-skip><tr><td>1</td></tr></table>`, `<table data-paste-markdown-skip=""><tr><td>1</td></tr></table>`,
// Org mode status of list items.
`<li class="checked"></li>`, `<li class="checked"></li>`,
`<li class="unchecked"></li>`, `<li class="unchecked"></li>`,

View File

@@ -290,7 +290,7 @@ func LFSFileGet(ctx *context.Context) {
escapeStatus, fileContentHTML := charset.EscapeControlHTML(fileContentHTML, ctx.Locale)
output := &htmlutil.HTMLBuilder{}
output.WriteHTML(`<table>`)
output.WriteHTML(`<table data-paste-markdown-skip>`)
writeLine := func(lineNum int, line template.HTML) {
output.WriteFormatf(`<tr><td class="lines-num">%d</td><td class="lines-code"><code class="code-inner">%s</code></td></tr>`, lineNum, line)
}

View File

@@ -34,7 +34,7 @@ func TestRenderHelperCodePreview(t *testing.T) {
<a href="http://full" class="tw-font-semibold" rel="nofollow">repo1/README.md</a>
repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" class="muted tw-font-mono tw-text-text" rel="nofollow">65f1bf27bc</a>
</div>
<table class="file-view">
<table class="file-view" data-paste-markdown-skip>
<tbody><tr>
<td class="lines-num"><span data-line-number="1"></span></td>
<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1
@@ -63,7 +63,7 @@ func TestRenderHelperCodePreview(t *testing.T) {
<a href="http://full" class="tw-font-semibold" rel="nofollow">repo1/README.md</a>
repo.code_preview_line_in:1,<a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" class="muted tw-font-mono tw-text-text" rel="nofollow">65f1bf27bc</a>
</div>
<table class="file-view">
<table class="file-view" data-paste-markdown-skip>
<tbody><tr>
<td class="lines-num"><span data-line-number="1"></span></td>
<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1

View File

@@ -8,7 +8,7 @@
{{ctx.Locale.Tr "repo.code_preview_line_from_to" .LineStart .LineStop $link}}
{{- end}}
</div>
<table class="file-view">
<table class="file-view" data-paste-markdown-skip>
<tbody>
{{- range $idx, $line := .HighlightLines -}}
<tr>

View File

@@ -35,7 +35,7 @@
{{else if not .FileSize}}
{{template "shared/fileisempty"}}
{{else}}
<table>
<table data-paste-markdown-skip>
<tbody>
{{range $row := .BlameRows}}
<tr class="{{if $row.CommitURL}}top-line-blame{{end}}">

View File

@@ -191,7 +191,7 @@
{{end}}
</div>
{{else if $file.Sections}}
<table class="chroma" data-new-comment-url="{{$.Issue.Link}}/files/reviews/new_comment" data-path="{{$file.Name}}">
<table class="chroma" data-paste-markdown-skip data-new-comment-url="{{$.Issue.Link}}/files/reviews/new_comment" data-path="{{$file.Name}}">
{{if $.IsSplitStyle}}
{{template "repo/diff/section_split" dict "file" . "root" $}}
{{else}}

View File

@@ -2,7 +2,7 @@
<div class="diff-file-box">
<div class="ui attached table segment">
<div class="file-body file-code code-diff code-diff-unified unicode-escaped">
<table>
<table data-paste-markdown-skip>
<tbody>
{{template "repo/diff/section_unified" dict "file" .File "root" $}}
</tbody>

View File

@@ -47,7 +47,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
<table>
<table data-paste-markdown-skip>
<tbody>
{{template "repo/diff/section_unified" dict "file" $file "root" $}}
</tbody>

View File

@@ -105,7 +105,7 @@
{{else if .IsPlainText}}
<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
{{else if .FileContent}}
<table>
<table data-paste-markdown-skip>
<tbody>
{{range $idx, $code := .FileContent}}
{{$line := Eval $idx "+" 1}}

View File

@@ -1,5 +1,5 @@
<div class="file-body file-code code-view">
<table>
<table data-paste-markdown-skip>
<tbody>
{{range .SearchResult.Lines}}
<tr>

View File

@@ -0,0 +1,25 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {apiCreateFiles, apiCreatePR, apiCreateRepo, login, randomString} from './utils.ts';
test('a copied pull request diff pastes as text', async ({page, request}) => {
const repoName = `e2e-pr-diff-copy-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
const [index] = await Promise.all([
(async () => {
await apiCreateRepo(request, {name: repoName});
await apiCreateFiles(request, owner, repoName, [{path: 'copy.txt', content: 'one | two\n'}], {branch: 'main', newBranch: 'feat'});
return apiCreatePR(request, owner, repoName, 'feat', 'main', 'title');
})(),
login(page),
]);
await page.goto(`/${owner}/${repoName}/pulls/${index}/files`);
await page.getByRole('main').selectText();
await page.keyboard.press('ControlOrMeta+KeyC');
await page.getByRole('button', {name: 'Review', exact: true}).click();
const comment = page.getByRole('textbox', {name: 'Review comment'});
await comment.press('ControlOrMeta+KeyV');
await expect(comment).toHaveValue(/one \| two/);
await expect(comment).not.toHaveValue(/one \\\| two/); // paste-markdown also inserts the table until https://github.com/github/paste-markdown/pull/128
});