v1.9.1: fix world map rendering and resolve eight small TODO items

World map:
- Fix zero-padded ISO numeric ids leaving 31 countries unrendered
  (Brazil, Australia, Belgium, Austria, Algeria and more)
- Fix Russia and Fiji smearing across the full map width at the antimeridian
  by unwrapping ring longitudes and drawing them at both edges
- Crop to 84N-60S, add evenodd fill rule, 174 countries rendered

Improvements:
- Logger::tail() reads backwards in chunks instead of loading the whole file
- External links get rel=noopener noreferrer in footer and Markdown content
- formatDisplayName() cleaned up and guarded against empty input
- Export statistics as CSV (Excel BOM) or JSON
- GeoIP database auto-updates when older than 35 days
- Editor shortcuts Ctrl/Cmd+S to save and Ctrl/Cmd+N for a new page
- Live search filter in the admin content browser
- Content versioning with timestamped .bak copies in content/-backups/

Also removes eight stale TODO entries that were already implemented
This commit is contained in:
2026-07-29 15:53:35 +02:00
parent 5357bc8915
commit 0fe5c75eae
12 changed files with 772 additions and 281 deletions
+51 -10
View File
@@ -573,6 +573,35 @@ class CodePressCMS {
];
}
/**
* Get hosts that count as internal for external-link detection
*
* @return array List of host names
*/
private function getInternalHosts(): array
{
$hosts = [];
if (!empty($_SERVER['HTTP_HOST'])) {
$host = strtolower(preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']));
if ($host !== '') {
$hosts[] = $host;
// Treat www and apex variants as the same site
$hosts[] = str_starts_with($host, 'www.') ? substr($host, 4) : 'www.' . $host;
}
}
$authorWebsite = $this->config['author']['website'] ?? '';
if ($authorWebsite !== '') {
$authorHost = parse_url($authorWebsite, PHP_URL_HOST);
if ($authorHost) {
$hosts[] = strtolower($authorHost);
}
}
return array_values(array_unique(array_filter($hosts)));
}
/**
* Parse Markdown content to HTML using League CommonMark
*
@@ -607,6 +636,14 @@ class CodePressCMS {
'symbol' => '',
'aria_hidden' => true,
],
'external_link' => [
'internal_hosts' => $this->getInternalHosts(),
'open_in_new_window' => true,
'html_class' => 'external-link',
'nofollow' => '',
'noopener' => 'external',
'noreferrer' => 'external',
],
];
// Create environment with extensions
@@ -617,6 +654,7 @@ class CodePressCMS {
$environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension());
$environment->addExtension(new \League\CommonMark\Extension\TaskList\TaskListExtension());
$environment->addExtension(new \League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension());
$environment->addExtension(new \League\CommonMark\Extension\ExternalLink\ExternalLinkExtension());
// Handle custom image size syntax: ![alt](url){:width="300" height="200"}
$imagePlaceholders = [];
@@ -755,30 +793,33 @@ class CodePressCMS {
* @return string Formatted display name
*/
private function formatDisplayName($filename) {
$filename = (string)$filename;
if ($filename === '') {
return '';
}
$hasLeadingDash = $filename[0] === '-';
if ($hasLeadingDash) {
$filename = substr($filename, 1);
}
$availableLangs = array_keys($this->getAvailableLanguages());
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
if (preg_match($langPattern, $filename, $matches)) {
$filename = $matches[2];
if (!empty($availableLangs)) {
$langPattern = '/^(' . implode('|', array_map('preg_quote', $availableLangs)) . ')\.(.+)$/';
if (preg_match($langPattern, $filename, $matches)) {
$filename = $matches[2];
}
}
$filename = preg_replace('/\.(md|php|html)$/', '', $filename);
$name = str_replace(['-', '_'], ' ', $filename);
$name = trim($name);
$name = trim(str_replace(['-', '_'], ' ', $filename));
$name = ucwords(strtolower($name));
$specialCases = ['phpinfo' => 'phpinfo', 'ict' => 'ICT'];
$lower = strtolower($name);
if (isset($specialCases[$lower])) {
$name = $specialCases[$lower];
} else {
$name = str_ireplace(array_keys($specialCases), array_values($specialCases), $name);
}
$name = $specialCases[$lower]
?? str_ireplace(array_keys($specialCases), array_values($specialCases), $name);
return ($hasLeadingDash ? '- ' : '') . $name;
}