({ text, plain }: UseMarkdownOptions)
| 73 | |
| 74 | // Parse simple inline markdown to HTML |
| 75 | function parseMarkdown({ text, plain }: UseMarkdownOptions): string { |
| 76 | if (!text) return '' |
| 77 | |
| 78 | // First strip HTML tags and escape remaining HTML |
| 79 | let html = stripAndEscapeHtml(text) |
| 80 | |
| 81 | // Bold: **text** or __text__ |
| 82 | html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') |
| 83 | html = html.replace(/__(.+?)__/g, '<strong>$1</strong>') |
| 84 | |
| 85 | // Italic: *text* or _text_ |
| 86 | html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>') |
| 87 | html = html.replace(/\b_(.+?)_\b/g, '<em>$1</em>') |
| 88 | |
| 89 | // Inline code: `code` |
| 90 | html = html.replace(/`([^`]+)`/g, '<code>$1</code>') |
| 91 | |
| 92 | // Strikethrough: ~~text~~ |
| 93 | html = html.replace(/~~(.+?)~~/g, '<del>$1</del>') |
| 94 | |
| 95 | // Links: [text](url) - only allow https, mailto |
| 96 | html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, textGroup, url) => { |
| 97 | // In plain mode, just render the link text without the anchor |
| 98 | if (plain) { |
| 99 | return textGroup |
| 100 | } |
| 101 | const decodedUrl = url.replace(/&/g, '&') |
| 102 | try { |
| 103 | const { protocol, href } = new URL(decodedUrl) |
| 104 | if (['https:', 'mailto:'].includes(protocol)) { |
| 105 | const safeUrl = href.replace(/"/g, '"') |
| 106 | return `<a href="${safeUrl}" rel="nofollow noreferrer noopener" target="_blank">${textGroup}</a>` |
| 107 | } |
| 108 | } catch {} |
| 109 | return `${textGroup} (${url})` |
| 110 | }) |
| 111 | |
| 112 | return html |
| 113 | } |
no test coverage detected