Best-effort HTML→markdown. Captures headings, links, lists, code, paragraphs.
(html: string)
| 105 | |
| 106 | /** Best-effort HTML→markdown. Captures headings, links, lists, code, paragraphs. */ |
| 107 | function htmlToMarkdown(html: string): string { |
| 108 | html = html.replace(/<script[\s\S]*?<\/script>/gi, ''); |
| 109 | html = html.replace(/<style[\s\S]*?<\/style>/gi, ''); |
| 110 | html = html.replace(/<!--[\s\S]*?-->/g, ''); |
| 111 | // Headings |
| 112 | for (let n = 1; n <= 6; n++) { |
| 113 | const re = new RegExp(`<h${n}[^>]*>([\\s\\S]*?)</h${n}>`, 'gi'); |
| 114 | html = html.replace(re, (_m, txt) => `\n\n${'#'.repeat(n)} ${stripInner(txt).trim()}\n`); |
| 115 | } |
| 116 | // Code blocks |
| 117 | html = html.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_m, code) => `\n\n\`\`\`\n${decodeEntities(code)}\n\`\`\`\n`); |
| 118 | html = html.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_m, code) => `\`${decodeEntities(code)}\``); |
| 119 | // Links |
| 120 | html = html.replace(/<a [^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, txt) => `[${stripInner(txt).trim()}](${href})`); |
| 121 | // Lists |
| 122 | html = html.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, txt) => `- ${stripInner(txt).trim()}\n`); |
| 123 | // Bold/italic |
| 124 | html = html.replace(/<(?:b|strong)[^>]*>([\s\S]*?)<\/(?:b|strong)>/gi, '**$1**'); |
| 125 | html = html.replace(/<(?:i|em)[^>]*>([\s\S]*?)<\/(?:i|em)>/gi, '*$1*'); |
| 126 | // Paragraphs/divs/breaks |
| 127 | html = html.replace(/<\/(p|div)[^>]*>/gi, '\n\n'); |
| 128 | html = html.replace(/<br[^>]*>/gi, '\n'); |
| 129 | // Strip remaining tags |
| 130 | html = html.replace(/<[^>]+>/g, ''); |
| 131 | html = decodeEntities(html); |
| 132 | // Cleanup whitespace |
| 133 | html = html.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n'); |
| 134 | return html.trim(); |
| 135 | } |
| 136 | |
| 137 | function stripInner(s: string): string { |
| 138 | return s.replace(/<[^>]+>/g, ''); |
no test coverage detected