0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-09-24 22:16:27 -04:00

Enable more markdown paste features in textarea editor (#35494)

Enable the [same paste
features](https://github.com/github/paste-markdown#paste-markdown-objects)
that GitHub has, notably the ability to paste text containing HTML links
and have them automatically turn into Markdown links. As far as I can
tell, previous paste features all work as expected.

---------

Signed-off-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2025-09-17 01:55:57 +02:00
committed by GitHub
parent 9332ff291b
commit 6033c47f90
7 changed files with 18 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
import {pasteAsMarkdownLink, removeAttachmentLinksFromMarkdown} from './EditorUpload.ts';
import {removeAttachmentLinksFromMarkdown} from './EditorUpload.ts';
test('removeAttachmentLinksFromMarkdown', () => {
expect(removeAttachmentLinksFromMarkdown('a foo b', 'foo')).toBe('a foo b');
@@ -12,13 +12,3 @@ test('removeAttachmentLinksFromMarkdown', () => {
expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo"> b', 'foo')).toBe('a b');
expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo" width="100"/> b', 'foo')).toBe('a b');
});
test('preparePasteAsMarkdownLink', () => {
expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 0}, 'bar')).toBeNull();
expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 0}, 'https://gitea.com')).toBeNull();
expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 3}, 'bar')).toBeNull();
expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 3}, 'https://gitea.com')).toBe('[foo](https://gitea.com)');
expect(pasteAsMarkdownLink({value: '..(url)', selectionStart: 3, selectionEnd: 6}, 'https://gitea.com')).toBe('[url](https://gitea.com)');
expect(pasteAsMarkdownLink({value: '[](url)', selectionStart: 3, selectionEnd: 6}, 'https://gitea.com')).toBeNull();
expect(pasteAsMarkdownLink({value: 'https://example.com', selectionStart: 0, selectionEnd: 19}, 'https://gitea.com')).toBeNull();
});

View File

@@ -1,12 +1,11 @@
import {imageInfo} from '../../utils/image.ts';
import {replaceTextareaSelection} from '../../utils/dom.ts';
import {isUrl} from '../../utils/url.ts';
import {textareaInsertText, triggerEditorContentChanged} from './EditorMarkdown.ts';
import {
DropzoneCustomEventRemovedFile,
DropzoneCustomEventUploadDone,
generateMarkdownLinkForAttachment,
} from '../dropzone.ts';
import {subscribe} from '@github/paste-markdown';
import type CodeMirror from 'codemirror';
import type EasyMDE from 'easymde';
import type {DropzoneFile} from 'dropzone';
@@ -118,46 +117,20 @@ export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string
return text;
}
export function pasteAsMarkdownLink(textarea: {value: string, selectionStart: number, selectionEnd: number}, pastedText: string): string | null {
const {value, selectionStart, selectionEnd} = textarea;
const selectedText = value.substring(selectionStart, selectionEnd);
const trimmedText = pastedText.trim();
const beforeSelection = value.substring(0, selectionStart);
const afterSelection = value.substring(selectionEnd);
const isInMarkdownLink = beforeSelection.endsWith('](') && afterSelection.startsWith(')');
const asMarkdownLink = selectedText && isUrl(trimmedText) && !isUrl(selectedText) && !isInMarkdownLink;
return asMarkdownLink ? `[${selectedText}](${trimmedText})` : null;
}
function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, pastedText: string, isShiftDown: boolean) {
// pasting with "shift" means "paste as original content" in most applications
if (isShiftDown) return; // let the browser handle it
// when pasting links over selected text, turn it into [text](link)
const pastedAsMarkdown = pasteAsMarkdownLink(textarea, pastedText);
if (pastedAsMarkdown) {
e.preventDefault();
replaceTextareaSelection(textarea, pastedAsMarkdown);
}
// else, let the browser handle it
}
// extract text and images from "paste" event
function getPastedContent(e: ClipboardEvent) {
const images = [];
function getPastedImages(e: ClipboardEvent) {
const images: Array<File> = [];
for (const item of e.clipboardData?.items ?? []) {
if (item.type?.startsWith('image/')) {
images.push(item.getAsFile());
}
}
const text = e.clipboardData?.getData?.('text') ?? '';
return {text, images};
return images;
}
export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
const editor = new CodeMirrorEditor(easyMDE.codemirror as any);
easyMDE.codemirror.on('paste', (_, e) => {
const {images} = getPastedContent(e);
const images = getPastedImages(e);
if (!images.length) return;
handleUploadFiles(editor, dropzoneEl, images, e);
});
@@ -173,19 +146,11 @@ export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
}
export function initTextareaEvents(textarea: HTMLTextAreaElement, dropzoneEl: HTMLElement) {
let isShiftDown = false;
textarea.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.shiftKey) isShiftDown = true;
});
textarea.addEventListener('keyup', (e: KeyboardEvent) => {
if (!e.shiftKey) isShiftDown = false;
});
subscribe(textarea); // enable paste features
textarea.addEventListener('paste', (e: ClipboardEvent) => {
const {images, text} = getPastedContent(e);
const images = getPastedImages(e);
if (images.length && dropzoneEl) {
handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e);
} else if (text) {
handleClipboardText(textarea, e, text, isShiftDown);
}
});
textarea.addEventListener('drop', (e: DragEvent) => {