Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Markdown paste detection for the docs editor.
3 *
4 * Conservatively detects whether pasted plain text is markdown.
5 * Requires 2+ signals or 1 strong signal (heading, code block, table)
6 * to avoid false positives on normal text.
7 *
8 * Also detects when clipboard HTML contains unrendered markdown link
9 * syntax, indicating the source didn't convert markdown to rich text.
10 */
11
12const PATTERNS: readonly RegExp[] = [
13 /^#{1,6}\s+\S/m, // ATX headings (strong)
14 /^\s*[-*+]\s+\S/m, // Unordered lists
15 /^\s*\d+\.\s+\S/m, // Ordered lists
16 /^\s*```/m, // Fenced code blocks (strong)
17 /^\s*>\s+\S/m, // Blockquotes
18 /\[.+?\]\(.+?\)/, // Links
19 /!\[.*?\]\(.+?\)/, // Images
20 /^\s*[-*_]{3,}\s*$/m, // Horizontal rules
21 /^\s*\|.+\|.+\|/m, // Tables
22 /^\s*- \[([ xX])\]/m, // Task lists
23];
24
25// Strong solo signals — a single match is enough
26const HEADING_RE = /^#{1,6}\s+\S/m;
27const CODE_FENCE_RE = /^\s*```/m;
28const TABLE_WITH_SEPARATOR_RE = /^\s*\|.+\|.+\|\s*\n\s*\|[\s:|-]+\|/m;
29const MARKDOWN_LINK_RE = /\[.+?\]\(.+?\)/;
30const TASK_LIST_RE = /^\s*- \[([ xX])\]/m;
31
32export function looksLikeMarkdown(text: string): boolean {
33 if (!text || !text.trim()) return false;
34
35 let signals = 0;
36 for (const p of PATTERNS) {
37 if (p.test(text)) signals++;
38 }
39
40 if (signals >= 2) return true;
41
42 // Strong solo signals
43 if (HEADING_RE.test(text)) return true;
44 if (CODE_FENCE_RE.test(text)) return true;
45 if (TABLE_WITH_SEPARATOR_RE.test(text)) return true;
46 if (MARKDOWN_LINK_RE.test(text)) return true;
47 if (TASK_LIST_RE.test(text)) return true;
48
49 return false;
50}
51
52/**
53 * Check whether clipboard HTML contains unrendered markdown link syntax.
54 * When apps copy content, they may include both text/html and text/plain.
55 * If the HTML contains literal `[text](url)` strings (not rendered as <a> tags),
56 * the plain text markdown path should be preferred.
57 */
58// Looser task list pattern for HTML context (no start-of-line anchor)
59const RAW_TASK_LIST_IN_HTML = /- \[([ xX])\]\s/;
60
61export function htmlContainsRawMarkdown(html: string): boolean {
62 if (!html) return false;
63 // Strip actual <a> tags and their content to avoid false positives,
64 // then check if raw markdown link syntax remains in the text content.
65 const stripped = html.replace(/<a\b[^>]*>.*?<\/a>/gi, '');
66 return MARKDOWN_LINK_RE.test(stripped) || RAW_TASK_LIST_IN_HTML.test(stripped);
67}