$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 '' . $alt . ''; }, $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 '


'; $blocks = preg_split('/\n{2,}/', $md) ?: []; $html = []; foreach ($blocks as $block) { $block = trim($block); if ($block === '') continue; $lines = explode("\n", $block); if (preg_match('/^<[a-zA-Z][a-zA-Z0-9-]*(\s[^>]*)?>/', $lines[0])) { // Raw HTML embedded in markdown (e.g. an