|
1 | 1 | import { useState } from 'react' |
2 | 2 | import { Eye, Pencil } from 'lucide-react' |
| 3 | +import { escapeHtml } from './rich-editor/markdown-utils' |
3 | 4 |
|
4 | 5 | function renderMarkdown(text: string): string { |
5 | | - return ( |
6 | | - text |
7 | | - // Headers |
8 | | - .replace(/^### (.+)$/gm, '<h3 class="text-xs font-semibold text-gray-300 mt-3 mb-1">$1</h3>') |
9 | | - .replace(/^## (.+)$/gm, '<h2 class="text-sm font-semibold text-gray-200 mt-3 mb-1">$1</h2>') |
10 | | - // Bold |
11 | | - .replace(/\*\*(.+?)\*\*/g, '<strong class="text-gray-200">$1</strong>') |
12 | | - // Italic |
13 | | - .replace(/\*(.+?)\*/g, '<em>$1</em>') |
14 | | - // Inline code |
15 | | - .replace( |
16 | | - /`([^`]+)`/g, |
17 | | - '<code class="px-1 py-0.5 bg-white/[0.06] rounded text-[11px] font-mono text-gray-300">$1</code>' |
18 | | - ) |
19 | | - // Code blocks |
20 | | - .replace(/```[\s\S]*?```/g, (match) => { |
21 | | - const code = match.replace(/```\w*\n?/, '').replace(/\n?```$/, '') |
22 | | - return `<pre class="px-2 py-1.5 bg-white/[0.04] rounded-md text-[11px] font-mono text-gray-300 overflow-x-auto my-1">${code}</pre>` |
23 | | - }) |
24 | | - // Checkboxes |
25 | | - .replace( |
26 | | - /^- \[x\] (.+)$/gm, |
27 | | - '<div class="flex items-center gap-1.5 py-0.5"><span class="text-green-400">✓</span><span class="text-gray-300 line-through">$1</span></div>' |
28 | | - ) |
29 | | - .replace( |
30 | | - /^- \[ \] (.+)$/gm, |
31 | | - '<div class="flex items-center gap-1.5 py-0.5"><span class="text-gray-600">☐</span><span class="text-gray-300">$1</span></div>' |
32 | | - ) |
33 | | - // Unordered lists |
34 | | - .replace( |
35 | | - /^- (.+)$/gm, |
36 | | - '<div class="flex items-start gap-1.5 py-0.5"><span class="text-gray-600 mt-0.5">•</span><span class="text-gray-400">$1</span></div>' |
37 | | - ) |
38 | | - // Ordered lists |
39 | | - .replace( |
40 | | - /^(\d+)\. (.+)$/gm, |
41 | | - '<div class="flex items-start gap-1.5 py-0.5"><span class="text-gray-600">$1.</span><span class="text-gray-400">$2</span></div>' |
42 | | - ) |
43 | | - // Line breaks |
44 | | - .replace(/\n\n/g, '<div class="h-2"></div>') |
45 | | - .replace(/\n/g, '<br />') |
46 | | - ) |
| 6 | + // Tokenize code blocks and inline code first, replacing them with placeholders. |
| 7 | + // This prevents markdown regexes (bold, italic, line breaks, etc.) from mangling |
| 8 | + // code content. All user text is HTML-escaped to prevent XSS. |
| 9 | + const tokens: string[] = [] |
| 10 | + |
| 11 | + const tokenize = (html: string): string => { |
| 12 | + const id = tokens.length |
| 13 | + tokens.push(html) |
| 14 | + return `\uFFFDTOKEN${id}\uFFFD` |
| 15 | + } |
| 16 | + |
| 17 | + // 1. Extract fenced code blocks → placeholders |
| 18 | + let result = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code) => { |
| 19 | + return tokenize( |
| 20 | + `<pre class="px-2 py-1.5 bg-white/[0.04] rounded-md text-[11px] font-mono text-gray-300 overflow-x-auto my-1">${escapeHtml(code.replace(/\n$/, ''))}</pre>` |
| 21 | + ) |
| 22 | + }) |
| 23 | + |
| 24 | + // 2. Extract inline code → placeholders |
| 25 | + result = result.replace(/`([^`]+)`/g, (_match, code) => { |
| 26 | + return tokenize( |
| 27 | + `<code class="px-1 py-0.5 bg-white/[0.06] rounded text-[11px] font-mono text-gray-300">${escapeHtml(code)}</code>` |
| 28 | + ) |
| 29 | + }) |
| 30 | + |
| 31 | + // 3. Escape remaining text (everything outside code blocks) |
| 32 | + result = escapeHtml(result) |
| 33 | + |
| 34 | + // 4. Restore token placeholders (they were escaped, so fix them) |
| 35 | + result = result.replace(/\uFFFDTOKEN(\d+)\uFFFD/g, (_match, id) => tokens[parseInt(id)]) |
| 36 | + |
| 37 | + // 5. Apply markdown formatting on non-code text |
| 38 | + result = result |
| 39 | + // Headers |
| 40 | + .replace(/^### (.+)$/gm, '<h3 class="text-xs font-semibold text-gray-300 mt-3 mb-1">$1</h3>') |
| 41 | + .replace(/^## (.+)$/gm, '<h2 class="text-sm font-semibold text-gray-200 mt-3 mb-1">$1</h2>') |
| 42 | + // Bold |
| 43 | + .replace(/\*\*(.+?)\*\*/g, '<strong class="text-gray-200">$1</strong>') |
| 44 | + // Italic |
| 45 | + .replace(/\*(.+?)\*/g, '<em>$1</em>') |
| 46 | + // Checkboxes |
| 47 | + .replace( |
| 48 | + /^- \[x\] (.+)$/gm, |
| 49 | + '<div class="flex items-center gap-1.5 py-0.5"><span class="text-green-400">✓</span><span class="text-gray-300 line-through">$1</span></div>' |
| 50 | + ) |
| 51 | + .replace( |
| 52 | + /^- \[ \] (.+)$/gm, |
| 53 | + '<div class="flex items-center gap-1.5 py-0.5"><span class="text-gray-600">☐</span><span class="text-gray-300">$1</span></div>' |
| 54 | + ) |
| 55 | + // Unordered lists |
| 56 | + .replace( |
| 57 | + /^- (.+)$/gm, |
| 58 | + '<div class="flex items-start gap-1.5 py-0.5"><span class="text-gray-600 mt-0.5">•</span><span class="text-gray-400">$1</span></div>' |
| 59 | + ) |
| 60 | + // Ordered lists |
| 61 | + .replace( |
| 62 | + /^(\d+)\. (.+)$/gm, |
| 63 | + '<div class="flex items-start gap-1.5 py-0.5"><span class="text-gray-600">$1.</span><span class="text-gray-400">$2</span></div>' |
| 64 | + ) |
| 65 | + // Line breaks |
| 66 | + .replace(/\n\n/g, '<div class="h-2"></div>') |
| 67 | + .replace(/\n/g, '<br />') |
| 68 | + |
| 69 | + return result |
47 | 70 | } |
48 | 71 |
|
49 | 72 | export const TASK_TEMPLATE = `## Description |
|
0 commit comments