: ..." line; following lines that do // not start with "*" are wrapped continuations of that same entry, so // fold them back in instead of dropping them (which truncated the text). $current = null; $flush = function () use (&$current, &$all_entries) { if ($current === null) return; $current['text'] = trim(preg_replace('/\s+/', ' ', $current['text'])); $all_entries[] = $current; $current = null; }; foreach ($lines as $line) { $line = rtrim($line); if (str_starts_with(ltrim($line), '*')) { $flush(); // Format: * 9 Sept 2024: Description if (preg_match('/^\*\s*(\d+\s+\w+\s+(\d{4})):\s*(.*)$/', trim($line), $matches)) { $date_str = $matches[1]; $entry_year = (int)$matches[2]; $timestamp = strtotime($date_str); $iso_date = $timestamp ? date('Y-m-d', $timestamp) : ''; $display_date = $timestamp ? date('Y-m-d', $timestamp) : $date_str; $current = [ 'year' => $entry_year, 'date' => $iso_date, 'display_date' => $display_date, 'text' => $matches[3], 'raw_date' => $date_str ]; } } elseif ($current !== null && trim($line) !== '') { $current['text'] .= ' ' . trim($line); } } $flush(); } // Sort by date DESC usort($all_entries, function($a, $b) { return strcmp($b['date'], $a['date']); }); if ($limit !== null) { return array_slice($all_entries, 0, $limit); } return $all_entries; } function get_changelog_by_year() { $entries = get_changelog_entries(); $by_year = []; foreach ($entries as $e) { $by_year[$e['year']][] = $e; } krsort($by_year); return $by_year; } function render_changelog_list($entries) { if (empty($entries)) return '

No entries yet.

'; $html = ''; return $html; }