(text: string, symbolLookup: SymbolLookup)
| 100 | * @internal Exported for testing |
| 101 | */ |
| 102 | export async function renderMarkdown(text: string, symbolLookup: SymbolLookup): Promise<string> { |
| 103 | // Extract fenced code blocks FIRST (before any HTML escaping) |
| 104 | // Pattern handles: |
| 105 | // - Optional whitespace before/after language identifier |
| 106 | // - \r\n, \n, or \r line endings |
| 107 | const codeBlockData: Array<{ lang: string; code: string }> = [] |
| 108 | let result = text.replace( |
| 109 | /```[ \t]*(\w*)[ \t]*(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)?```/g, |
| 110 | (_, lang, code) => { |
| 111 | const index = codeBlockData.length |
| 112 | codeBlockData.push({ lang: lang || 'text', code: code.trim() }) |
| 113 | return `__CODE_BLOCK_${index}__` |
| 114 | }, |
| 115 | ) |
| 116 | |
| 117 | // Now process the rest (JSDoc links, HTML escaping, etc.) |
| 118 | result = parseJsDocLinks(result, symbolLookup) |
| 119 | |
| 120 | // Markdown links - i.e. [text](url) |
| 121 | result = result.replace( |
| 122 | /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, |
| 123 | '<a href="$2" target="_blank" rel="noreferrer" class="docs-link">$1</a>', |
| 124 | ) |
| 125 | |
| 126 | // Handle inline code (single backticks) - won't interfere with fenced blocks |
| 127 | result = result |
| 128 | .replace(/`([^`]+)`/g, '<code class="docs-inline-code">$1</code>') |
| 129 | .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') |
| 130 | .replace(/\n{2,}/g, '<br><br>') |
| 131 | .replace(/\n/g, '<br>') |
| 132 | |
| 133 | // Highlight and restore code blocks |
| 134 | const highlightedCodeBlocks = await Promise.all( |
| 135 | codeBlockData.map(({ lang, code }) => highlightCodeBlock(code, lang)), |
| 136 | ) |
| 137 | |
| 138 | highlightedCodeBlocks.forEach((highlighted, i) => { |
| 139 | result = result.replace(`__CODE_BLOCK_${i}__`, () => highlighted) |
| 140 | }) |
| 141 | |
| 142 | return result |
| 143 | } |
no test coverage detected