Вставка Markdown в редактор: ссылки [текст](/p/<id>), форматирование, выделение + URL → ссылка

This commit is contained in:
crm-wiki agent
2026-10-13 14:12:22 +00:00
parent c8453ac6bb
commit 4ebbbe4533

View File

@@ -40,6 +40,27 @@ const AuthImage = Image.extend({
const isInternal = (href: string) => /^\/p\/[A-Za-z0-9_-]+$/.test(href);
/** Адрес, который можно повесить на выделение: http(s), mailto или внутренняя ссылка /p/<id>. */
const isLinkTarget = (s: string) => /^(https?:\/\/[^\s]+|mailto:[^\s]+|\/p\/[A-Za-z0-9_-]+)$/i.test(s);
/** Похоже ли на Markdown: ссылки, картинки, заголовки, списки, цитаты, код, таблицы, жирный/курсив. */
const looksLikeMarkdown = (s: string) =>
/!?\[[^\]\n]+\]\([^)\s]+\)|\*\*[^*\n]+\*\*|__[^_\n]+__|~~[^~\n]+~~|`[^`\n]+`/.test(s) ||
/^(#{1,6}\s|\s*[-*+]\s|\s*\d+[.)]\s|>\s?|```|\|.*\|\s*$|\s*[-*_]{3,}\s*$)/m.test(s);
/**
* Вставка Markdown тем же парсером, которым загружается страница.
* Один абзац (например, одна ссылка [Назва](/p/<id>) или фраза с ней) вставляется в строку, без нового абзаца.
*/
function pasteMarkdown(ed: Editor, md: string): boolean {
const doc = ed.markdown?.parse(md);
const blocks = doc?.content ?? [];
if (!blocks.length) return false;
const content = blocks.length === 1 && blocks[0].type === "paragraph" ? blocks[0].content ?? [] : blocks;
if (!content.length) return false;
return ed.chain().focus().insertContent(content).run();
}
export function WikiEditor(props: {
markdown: string; pageId: string; nodes: Node[];
onChange: (getMarkdown: () => string) => void;
@@ -88,11 +109,24 @@ export function WikiEditor(props: {
else if (/^(https?:|mailto:)/i.test(href)) window.open(href, "_blank", "noopener,noreferrer");
return true;
},
handlePaste: (_view, event) => {
const files = [...(event.clipboardData?.files ?? [])].filter((f) => f.type.startsWith("image/"));
if (!files.length || !editor) return false;
void upload(editor, files);
handlePaste: (view, event): boolean => {
const cd = event.clipboardData;
if (!cd || !editor) return false;
const files = [...cd.files].filter((f) => f.type.startsWith("image/"));
if (files.length) { void upload(editor, files); return true; }
const plain = cd.getData("text/plain");
const target = plain.trim();
// Выделили текст и вставили адрес — выделение становится ссылкой.
if (!view.state.selection.empty && isLinkTarget(target) && !editor.isActive("codeBlock")) {
// Курсор — в конец ссылки: следующий ввод не заменит только что оформленный текст.
const end = view.state.selection.to;
editor.chain().focus().setLink({ href: target }).setTextSelection(end).run();
return true;
}
// HTML (из браузера, из другой страницы вики) — как по умолчанию в TipTap.
if (cd.types.includes("text/html")) return false;
if (!plain || editor.isActive("codeBlock") || editor.isActive("code") || !looksLikeMarkdown(plain)) return false;
return pasteMarkdown(editor, plain);
},
handleDrop: (view, event) => {
const files = [...(event.dataTransfer?.files ?? [])].filter((f) => f.type.startsWith("image/"));