From 4ebbbe453335c4951e36bc8d36e741f2485e20dc Mon Sep 17 00:00:00 2001 From: crm-wiki agent Date: Tue, 13 Oct 2026 14:12:22 +0000 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=81=D1=82=D0=B0=D0=B2=D0=BA=D0=B0=20Ma?= =?UTF-8?q?rkdown=20=D0=B2=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80:=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20[=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82](/p/),=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20+=20UR?= =?UTF-8?q?L=20=E2=86=92=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/Editor.tsx | 44 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/frontend/src/Editor.tsx b/frontend/src/Editor.tsx index 08ed83d..afd6baf 100644 --- a/frontend/src/Editor.tsx +++ b/frontend/src/Editor.tsx @@ -40,6 +40,27 @@ const AuthImage = Image.extend({ const isInternal = (href: string) => /^\/p\/[A-Za-z0-9_-]+$/.test(href); +/** Адрес, который можно повесить на выделение: http(s), mailto или внутренняя ссылка /p/. */ +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/) или фраза с ней) вставляется в строку, без нового абзаца. + */ +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); - return true; + 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/"));