/** * Markdown paste detection for the docs editor. * * Conservatively detects whether pasted plain text is markdown. * Requires 2+ signals or 1 strong signal (heading, code block, table) * to avoid false positives on normal text. * * Also detects when clipboard HTML contains unrendered markdown link * syntax, indicating the source didn't convert markdown to rich text. */ const PATTERNS: readonly RegExp[] = [ /^#{1,6}\s+\S/m, // ATX headings (strong) /^\s*[-*+]\s+\S/m, // Unordered lists /^\s*\d+\.\s+\S/m, // Ordered lists /^\s*```/m, // Fenced code blocks (strong) /^\s*>\s+\S/m, // Blockquotes /\[.+?\]\(.+?\)/, // Links /!\[.*?\]\(.+?\)/, // Images /^\s*[-*_]{3,}\s*$/m, // Horizontal rules /^\s*\|.+\|.+\|/m, // Tables /^\s*- \[([ xX])\]/m, // Task lists ]; // Strong solo signals — a single match is enough const HEADING_RE = /^#{1,6}\s+\S/m; const CODE_FENCE_RE = /^\s*```/m; const TABLE_WITH_SEPARATOR_RE = /^\s*\|.+\|.+\|\s*\n\s*\|[\s:|-]+\|/m; const MARKDOWN_LINK_RE = /\[.+?\]\(.+?\)/; const TASK_LIST_RE = /^\s*- \[([ xX])\]/m; export function looksLikeMarkdown(text: string): boolean { if (!text || !text.trim()) return false; let signals = 0; for (const p of PATTERNS) { if (p.test(text)) signals++; } if (signals >= 2) return true; // Strong solo signals if (HEADING_RE.test(text)) return true; if (CODE_FENCE_RE.test(text)) return true; if (TABLE_WITH_SEPARATOR_RE.test(text)) return true; if (MARKDOWN_LINK_RE.test(text)) return true; if (TASK_LIST_RE.test(text)) return true; return false; } /** * Check whether clipboard HTML contains unrendered markdown link syntax. * When apps copy content, they may include both text/html and text/plain. * If the HTML contains literal `[text](url)` strings (not rendered as tags), * the plain text markdown path should be preferred. */ // Looser task list pattern for HTML context (no start-of-line anchor) const RAW_TASK_LIST_IN_HTML = /- \[([ xX])\]\s/; export function htmlContainsRawMarkdown(html: string): boolean { if (!html) return false; // Strip actual tags and their content to avoid false positives, // then check if raw markdown link syntax remains in the text content. const stripped = html.replace(/]*>.*?<\/a>/gi, ''); return MARKDOWN_LINK_RE.test(stripped) || RAW_TASK_LIST_IN_HTML.test(stripped); }