$v) {
$url .= '&' . urlencode($k) . '=' . urlencode((string)$v);
}
return $url;
}
function publiserPublicUrl(string $section, string $folder): string {
return '/' . rawurlencode($section) . '/' . rawurlencode($folder) . '/';
}
/**
* Human-friendly display of a stored publish_at value (e.g. "2026-08-30T14:30",
* the raw format). Mirrors the day/month/year
* order app/plugins/global/languages.php's formatDate() uses for regular
* post dates, extended with a time-of-day. Only for display - never use
* this for the datetime-local input's own value attribute, which must stay
* in machine format.
*/
function publiserNorwegianDate(int $ts): string {
static $months = ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'];
$day = (int)date('j', $ts);
$month = $months[(int)date('n', $ts) - 1];
return "{$day}. {$month} " . date('Y', $ts);
}
/** Human-friendly display of a date-only value (e.g. "2026-08-25"). Never
* use this for the value attribute, which must stay ISO. */
function publiserFormatDate(?string $raw): string {
if (!$raw) return '';
$ts = strtotime($raw);
return $ts === false ? $raw : publiserNorwegianDate($ts);
}
function publiserFormatScheduleTime(?string $raw): string {
if (!$raw) return '';
$ts = strtotime($raw);
if ($ts === false) return $raw;
return publiserNorwegianDate($ts) . ' kl. ' . date('H:i', $ts);
}
function publiserImageUrl(string $location, string $section, string $folder, string $filename): string {
if ($location === 'live') {
return '/' . rawurlencode($section) . '/' . rawurlencode($folder) . '/' . rawurlencode($filename);
}
return publiserBaseUrl() . '?action=image&location=' . urlencode($location)
. '§ion=' . urlencode($section) . '&folder=' . urlencode($folder) . '&file=' . urlencode($filename);
}
function publiserCheckCsrf(): void {
global $csrf;
$token = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!hash_equals($csrf, $token)) {
http_response_code(403);
die('Ugyldig forespørsel (utløpt økt). Last siden på nytt og prøv igjen.');
}
}
function publiserRedirect(string $url): never {
header("Location: $url");
exit;
}
function publiserItemDir(string $location, string $section, string $folder): string {
return match ($location) {
'live' => publiserLiveDir($section, $folder),
'draft' => publiserDraftDir($section, $folder),
'trash' => publiserTrashDir($section, $folder),
default => throw new RuntimeException('Ugyldig plassering'),
};
}
// --- minimal Markdown -> HTML for populating the WYSIWYG editor on load ----
// (mirrors the bounded tag set publiser.js can serialize back to Markdown)
function publiserEscapeInline(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
function publiserInlineMdToHtml(string $text, callable $imageUrlResolver): string {
$text = publiserEscapeInline($text);
$text = preg_replace_callback('/!\[(.*?)\]\((.*?)\)/', function ($m) use ($imageUrlResolver) {
$alt = $m[1];
$file = $m[2];
$realFile = html_entity_decode($file, ENT_QUOTES, 'UTF-8');
$url = htmlspecialchars($imageUrlResolver($realFile), ENT_QUOTES, 'UTF-8');
return '';
}, $text) ?? $text;
$text = preg_replace_callback('/\[(.*?)\]\((.*?)\)/', function ($m) {
return '' . $m[1] . '';
}, $text) ?? $text;
$text = preg_replace('/\*\*(.+?)\*\*/', '$1', $text) ?? $text;
$text = preg_replace('/(?$1', $text) ?? $text;
return $text;
}
function publiserBlockMdToHtml(string $md, callable $imageUrlResolver): string {
$md = str_replace("\r\n", "\n", trim($md));
if ($md === '') return '