Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | /** * Simple markdown-to-HTML conversion for the changelog. * Only handles the subset of markdown used in docs/CHANGELOG.md: * headings, bold, inline code, links, unordered lists, and horizontal rules. * * Strips HTML comments left over from markdownlint directives. */ /** * Strip HTML comments completely, including malformed or nested fragments. * Uses a loop to guarantee no `<!--` or `-->` sequences survive * (avoids incomplete multi-character sanitization). */ function stripHtmlComments(text: string): string { let result = text; // First pass: remove well-formed comments while ( result.includes('<!--') && (result.includes('-->') || result.includes('--!>')) ) { result = result.replace(/<!--[\s\S]*?--(?:>|!>)/g, ''); } // Second pass: remove any residual opener/closer fragments while ( result.includes('<!--') || result.includes('-->') || result.includes('--!>') ) { result = result.replace(/<!--|--(?:>|!>)/g, ''); } return result; } /** Clean the raw changelog markdown for website rendering. */ function cleanChangelog(md: string): string { return stripHtmlComments(md) .replace(/\n{3,}/g, '\n\n') .trim(); } /** Extract version entries as `{ tag, id, date }` for sidebar navigation. */ export function extractVersions( md: string, ): { tag: string; id: string; date: string }[] { const cleaned = cleanChangelog(md); const versions: { tag: string; id: string; date: string }[] = []; const re = /^## \[?([\d.]+)\]?.*?(?:[-–—]\s*)?(\d{4}-\d{2}-\d{2})?/gm; let m = re.exec(cleaned); while (m !== null) { const tag = m[1]; versions.push({ tag: `v${tag}`, id: `v${tag.replace(/\./g, '')}`, date: m[2] ?? '', }); m = re.exec(cleaned); } return versions; } /** Join continuation lines back into their parent list item. */ function joinMultiLineListItems(md: string): string { return md.replace( /^([*-] .+)\n(?![\s]*[*-] |#{1,6} |```|---|\s*$)(.+)/gm, '$1 $2', ); } /** Convert the cleaned markdown to HTML, adding `id` anchors on version headings. */ export function changelogToHtml(md: string): string { const cleaned = cleanChangelog(md); const joined = joinMultiLineListItems(cleaned); return ( joined // Version headings → h2 with id anchor .replace( /^## \[?([\d.]+)\]?.*?([-–—]\s*)?(\d{4}-\d{2}-\d{2})?\s*$/gm, (_match, ver, _sep, date) => { const id = `v${ver.replace(/\./g, '')}`; const dateHtml = date ? `<span class="release-date">${date}</span>` : ''; return `<h2 id="${id}"><span class="version-tag">v${ver}</span>${dateHtml}</h2>`; }, ) // Fenced code blocks (```...```) → <pre><code> .replace( /^```(\w*)\n([\s\S]*?)^```$/gm, (_m, _lang, code) => `<pre><code>${code.replace(/</g, '<').replace(/>/g, '>')}</code></pre>`, ) .replace(/^#### (.+)$/gm, '<h4>$1</h4>') .replace(/^### (.+)$/gm, '<h3>$1</h3>') // Generic ## headings (non-version, e.g. "## Features") .replace(/^## (.+)$/gm, '<h3>$1</h3>') .replace(/^# (.+)$/gm, '<h1>$1</h1>') // Horizontal rules .replace(/^---$/gm, '<hr />') .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace( /`([^`]+)`/g, (_m, code) => `<code>${code.replace(/</g, '<').replace(/>/g, '>')}</code>`, ) .replace( /\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>', ) // Both * and - list items (including indented sub-items) .replace(/^\s*[*-] (.+)$/gm, '<li>$1</li>') .replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>') .replace(/^(?!<[/huplo]|<li|<strong|<hr|<pre|<code)(.+)$/gm, '<p>$1</p>') .replace(/\n{2,}/g, '\n') ); } /** * @deprecated Use `changelogToHtml` + `extractVersions` instead. */ export function simpleMarkdownToHtml(md: string): string { return changelogToHtml(md); } |