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:
@@ -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: {: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;
|
||||
}
|
||||
|
||||
@@ -123,20 +123,71 @@ class Logger {
|
||||
|
||||
/**
|
||||
* Get last N lines from log file
|
||||
*
|
||||
*
|
||||
* Reads the file backwards in chunks so large log files never have to be
|
||||
* loaded into memory in their entirety.
|
||||
*
|
||||
* @param int $lines Number of lines to retrieve
|
||||
* @return array Log lines
|
||||
* @return array Log lines (including trailing newlines, oldest first)
|
||||
*/
|
||||
public static function tail($lines = 100) {
|
||||
if (!self::$logFile || !file_exists(self::$logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$file = @file(self::$logFile);
|
||||
if ($file === false) {
|
||||
|
||||
$lines = max(1, (int)$lines);
|
||||
|
||||
$handle = @fopen(self::$logFile, 'rb');
|
||||
if ($handle === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_slice($file, -$lines);
|
||||
|
||||
if (fseek($handle, 0, SEEK_END) !== 0) {
|
||||
fclose($handle);
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileSize = ftell($handle);
|
||||
if ($fileSize === false || $fileSize === 0) {
|
||||
fclose($handle);
|
||||
return [];
|
||||
}
|
||||
|
||||
$chunkSize = 8192;
|
||||
$position = $fileSize;
|
||||
$buffer = '';
|
||||
$newlineCount = 0;
|
||||
|
||||
// Read backwards until we have enough newlines or reach the start
|
||||
while ($position > 0 && $newlineCount <= $lines) {
|
||||
$readSize = (int)min($chunkSize, $position);
|
||||
$position -= $readSize;
|
||||
|
||||
if (fseek($handle, $position, SEEK_SET) !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$chunk = fread($handle, $readSize);
|
||||
if ($chunk === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$buffer = $chunk . $buffer;
|
||||
$newlineCount = substr_count($buffer, "\n");
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
if ($buffer === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split while keeping the newline characters, matching file() behaviour
|
||||
$result = preg_split('/(?<=\n)/', $buffer, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if ($result === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_slice($result, -$lines);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user