const SECTION_HEADINGS = [ "Added", "Changed", "Fixed", "Removed", "Deprecated", "Security", ] as const; export type ChangelogSectionTitle = (typeof SECTION_HEADINGS)[number]; export type ChangelogSection = { title: ChangelogSectionTitle; items: string[]; }; export type ChangelogRelease = { version: string; date: string; major: number; minor: number; patch: number; /** Categorized release notes. */ sections: ChangelogSection[]; /** Optional paragraph summary for major or initial releases. */ summary?: string; }; export const CHANGELOG_SECTION_LABELS: Record = { Added: "Added", Changed: "Improved", Fixed: "Fixed", Removed: "Removed", Deprecated: "Deprecated", Security: "Security", }; export function parseChangelog(markdown: string): ChangelogRelease[] { const releases: ChangelogRelease[] = []; const lines = markdown.split("\n"); let i = 0; while (i < lines.length) { const versionMatch = lines[i].match(/^## \[([\d.]+)\] - (.+)$/); if (!versionMatch) { i++; continue; } const version = versionMatch[1]; const date = versionMatch[2]; const [major = 0, minor = 0, patch = 0] = version.split(".").map(Number); i++; const sections: ChangelogSection[] = []; while (i < lines.length && !lines[i].match(/^## \[/)) { const sectionMatch = lines[i].match(/^### (.+)$/); if ( sectionMatch && SECTION_HEADINGS.includes(sectionMatch[1] as ChangelogSectionTitle) ) { const title = sectionMatch[1] as ChangelogSectionTitle; i++; const items: string[] = []; while (i < lines.length && lines[i].startsWith("- ")) { items.push(lines[i].slice(2).trim()); i++; } if (items.length > 0) { sections.push({ title, items }); } continue; } i++; } releases.push({ version, date, major, minor, patch, sections }); } return releases; }