| 1 | const { escape } = require('./render'); |
| 2 | |
| 3 | function safeUrl(url) { |
| 4 | if (/^https?:\/\//i.test(url)) return url; |
| 5 | if (/^\//.test(url)) return url; |
| 6 | return ''; |
| 7 | } |
| 8 | |
| 9 | function inline(s) { |
| 10 | s = escape(s); |
| 11 | s = s.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`); |
| 12 | s = s.replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`); |
| 13 | s = s.replace(/\*([^*\s][^*]*)\*/g, (_, c) => `<em>${c}</em>`); |
| 14 | s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => { |
| 15 | const safe = safeUrl(url); |
| 16 | return safe ? `<a href="${escape(safe)}" rel="noopener noreferrer">${text}</a>` : text; |
| 17 | }); |
| 18 | return s; |
| 19 | } |
| 20 | |
| 21 | function render(md) { |
| 22 | const lines = (md || '').split('\n'); |
| 23 | const out = []; |
| 24 | let inCode = false; |
| 25 | let codeBuf = []; |
| 26 | let inList = false; |
| 27 | let listType = ''; |
| 28 | |
| 29 | function flushList() { |
| 30 | if (!inList) return; |
| 31 | out.push(`</${listType}>`); |
| 32 | inList = false; listType = ''; |
| 33 | } |
| 34 | |
| 35 | for (const line of lines) { |
| 36 | if (line.startsWith('```')) { |
| 37 | if (inCode) { |
| 38 | out.push(`<pre><code>${escape(codeBuf.join('\n'))}</code></pre>`); |
| 39 | codeBuf = []; inCode = false; |
| 40 | } else { |
| 41 | flushList(); |
| 42 | inCode = true; |
| 43 | } |
| 44 | continue; |
| 45 | } |
| 46 | if (inCode) { codeBuf.push(line); continue; } |
| 47 | |
| 48 | const hm = line.match(/^(#{1,6})\s+(.+)$/); |
| 49 | if (hm) { |
| 50 | flushList(); |
| 51 | const n = hm[1].length; |
| 52 | out.push(`<h${n}>${inline(hm[2])}</h${n}>`); |
| 53 | continue; |
| 54 | } |
| 55 | if (/^[-*_]{3,}\s*$/.test(line)) { |
| 56 | flushList(); out.push('<hr>'); continue; |
| 57 | } |
| 58 | const ulm = line.match(/^[-*+]\s+(.+)$/); |
| 59 | if (ulm) { |
| 60 | if (!inList || listType !== 'ul') { flushList(); out.push('<ul>'); inList = true; listType = 'ul'; } |
| 61 | out.push(`<li>${inline(ulm[1])}</li>`); |
| 62 | continue; |
| 63 | } |
| 64 | const olm = line.match(/^\d+\.\s+(.+)$/); |
| 65 | if (olm) { |
| 66 | if (!inList || listType !== 'ol') { flushList(); out.push('<ol>'); inList = true; listType = 'ol'; } |
| 67 | out.push(`<li>${inline(olm[1])}</li>`); |
| 68 | continue; |
| 69 | } |
| 70 | if (line.trim() === '') { |
| 71 | flushList(); out.push(''); continue; |
| 72 | } |
| 73 | flushList(); |
| 74 | out.push(`<p>${inline(line)}</p>`); |
| 75 | } |
| 76 | |
| 77 | if (inCode) out.push(`<pre><code>${escape(codeBuf.join('\n'))}</code></pre>`); |
| 78 | flushList(); |
| 79 | return out.join('\n'); |
| 80 | } |
| 81 | |
| 82 | module.exports = { render }; |
| 83 | |