|
1 | 1 | export function renderInlineMarkdown(text: string): string { |
2 | 2 | if (!text) return ""; |
3 | 3 |
|
4 | | - // Helper function to escape HTML entities |
5 | | - const escapeHtml = (str: string): string => { |
6 | | - return str |
| 4 | + const escapeHtml = (str: string): string => |
| 5 | + str |
7 | 6 | .replace(/&/g, "&") |
8 | 7 | .replace(/</g, "<") |
9 | 8 | .replace(/>/g, ">") |
10 | 9 | .replace(/"/g, """) |
11 | 10 | .replace(/'/g, "'"); |
12 | | - }; |
13 | 11 |
|
14 | | - // Process inline markdown patterns |
15 | | - let html = text |
16 | | - // Code: `text` - escape content inside backticks, then wrap in <code> |
17 | | - .replace(/`([^`]+)`/g, (_, content) => `<code>${escapeHtml(content)}</code>`) |
| 12 | + const pattern = /(`([^`]+)`)|(\*\*([^*]+)\*\*)|(__([^_]+)__)|(~~([^~]+)~~)|(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)/g; |
18 | 13 |
|
19 | | - // Bold: **text** or __text__ - escape content, then wrap in <strong> |
20 | | - .replace(/\*\*([^*]+)\*\*/g, (_, content) => `<strong>${escapeHtml(content)}</strong>`) |
21 | | - .replace(/__([^_]+)__/g, (_, content) => `<strong>${escapeHtml(content)}</strong>`) |
| 14 | + let result = ""; |
| 15 | + let lastIndex = 0; |
22 | 16 |
|
23 | | - // Italic: *text* or _text_ (but not part of bold) - escape content, then wrap in <em> |
24 | | - .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, content) => `<em>${escapeHtml(content)}</em>`) |
25 | | - .replace(/(?<!_)_([^_]+)_(?!_)/g, (_, content) => `<em>${escapeHtml(content)}</em>`) |
| 17 | + for (const match of text.matchAll(pattern)) { |
| 18 | + const matchIndex = match.index ?? 0; |
| 19 | + if (matchIndex > lastIndex) { |
| 20 | + result += escapeHtml(text.slice(lastIndex, matchIndex)); |
| 21 | + } |
26 | 22 |
|
27 | | - // Strikethrough: ~~text~~ - escape content, then wrap in <del> |
28 | | - .replace(/~~([^~]+)~~/g, (_, content) => `<del>${escapeHtml(content)}</del>`); |
| 23 | + if (match[1]) { |
| 24 | + // `code` |
| 25 | + result += `<code>${escapeHtml(match[2] ?? "")}</code>`; |
| 26 | + } else if (match[3]) { |
| 27 | + // **bold** |
| 28 | + result += `<strong>${escapeHtml(match[4] ?? "")}</strong>`; |
| 29 | + } else if (match[5]) { |
| 30 | + // __bold__ |
| 31 | + result += `<strong>${escapeHtml(match[6] ?? "")}</strong>`; |
| 32 | + } else if (match[7]) { |
| 33 | + // ~~strikethrough~~ |
| 34 | + result += `<del>${escapeHtml(match[8] ?? "")}</del>`; |
| 35 | + } else if (match[9]) { |
| 36 | + // *italic* |
| 37 | + result += `<em>${escapeHtml(match[10] ?? "")}</em>`; |
| 38 | + } else if (match[11]) { |
| 39 | + // _italic_ |
| 40 | + result += `<em>${escapeHtml(match[12] ?? "")}</em>`; |
| 41 | + } |
29 | 42 |
|
30 | | - // Escape any remaining unprocessed text (text outside of markdown patterns) |
31 | | - // This is tricky because we need to avoid escaping the HTML we just created |
32 | | - // For now, we'll leave plain text unescaped since Astro should handle it |
| 43 | + lastIndex = matchIndex + match[0].length; |
| 44 | + } |
33 | 45 |
|
34 | | - return html; |
| 46 | + if (lastIndex < text.length) { |
| 47 | + result += escapeHtml(text.slice(lastIndex)); |
| 48 | + } |
| 49 | + |
| 50 | + return result; |
35 | 51 | } |
36 | 52 |
|
37 | 53 | /** |
|
0 commit comments