Riksdagsmonitor Intelligence Platform โ€” API Documentation - v1.0.30
    Preparing search index...
    CLIENT_RUNTIME_BODY: "\n // HTML-escape helper to prevent XSS when interpolating article fields into innerHTML\n function esc(str) {\n return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#x27;');\n }\n\n function safeHref(slug) {\n var s = String(slug);\n // Allow relative HTML paths including subdirectory articles (e.g. \"2026-05-04-election-cycle/current-en.html\").\n // Block control chars, backslashes, and protocol-relative URLs.\n if (!s || /[\\\\\\x00-\\x1F\\x7F]/.test(s) || s.indexOf('//') === 0) {\n return '#';\n }\n if (!/^[A-Za-z0-9._/-]+\\.html$/.test(s)) {\n return '#';\n }\n return esc(s);\n }\n\n function i18nShowing(shown, total) {\n var template;\n if (i18nShowingConfig && typeof i18nShowingConfig === 'object') {\n if (shown === 1 && Object.prototype.hasOwnProperty.call(i18nShowingConfig, 'one')) {\n template = i18nShowingConfig.one;\n } else if (Object.prototype.hasOwnProperty.call(i18nShowingConfig, 'other')) {\n template = i18nShowingConfig.other;\n } else {\n template = String(i18nShowingConfig);\n }\n } else {\n template = i18nShowingConfig || '';\n }\n if (typeof template !== 'string') {\n template = String(template);\n }\n return template\n .replace('{shown}', String(shown))\n .replace('{total}', String(total));\n }\n\n // Pagination state\n const PAGE_SIZE = 20;\n let visibleCount = PAGE_SIZE;\n let restoringFromURL = false;\n\n let filteredArticles = [...articles];\n\n function buildArticleCard(article) {\n const flag = LANGUAGE_FLAGS[article.lang] || '๐ŸŒ';\n const dirAttr = IS_RTL ? ' dir=\"ltr\"' : '';\n const langBadge = `<span class=\"language-badge\"${dirAttr} aria-label=\"${esc(article.lang)} language\"><span aria-hidden=\"true\">${flag}</span> ${esc(article.lang.toUpperCase())}</span>`;\n\n const availableLangs = article.availableLanguages || [article.lang];\n let availableDisplay = '';\n if (availableLangs.length > 1) {\n const availableBadges = availableLangs.map(l => {\n const lf = LANGUAGE_FLAGS[l] || '๐ŸŒ';\n return `<span class=\"lang-badge-sm\"${dirAttr}><span aria-hidden=\"true\">${lf}</span> ${esc(l.toUpperCase())}</span>`;\n }).join(' ');\n availableDisplay = `<p class=\"available-languages\"><strong>${AVAILABLE_IN_TEXT}:</strong> ${availableBadges}</p>`;\n }\n\n const primaryTopic = (article.topics && article.topics.length > 0) ? article.topics[0] : '';\n const recency = computeRecency(article.date);\n const recencyAttr = recency ? ` data-date-recent=\"${recency}\"` : '';\n const recencyBadge = recency ? `<span class=\"recency-badge\" data-recency=\"${recency}\">${esc(localizeRecency(recency))}</span>` : '';\n\n return `\n <article class=\"article-card\" data-type=\"${esc(article.type)}\" data-topic=\"${esc(primaryTopic)}\"${recencyAttr}>\n <div class=\"article-meta\">\n <time class=\"article-date\" datetime=\"${esc(article.date)}\">${formatDate(article.date)}</time>\n <span class=\"article-type\" data-type=\"${esc(article.type)}\">${typeIcon(article.type)} ${localizeType(article.type)}</span>\n ${recencyBadge}\n ${langBadge}\n </div>\n <h2 class=\"article-title\">\n <a href=\"${safeHref(article.slug)}\">${esc(article.title)}</a>\n </h2>\n <p class=\"article-excerpt\">${esc(article.excerpt)}</p>\n ${availableDisplay}\n <div class=\"article-tags\">\n ${article.tags.filter(Boolean).map(tag => `<span class=\"tag\">${esc(tag)}</span>`).join('')}\n </div>\n </article>\n `;\n }\n\n // Compute coarse recency bucket so CSS can surface a \"today\" / \"this-week\"\n // / \"this-month\" badge without re-running JS per scroll. Uses the\n // article date in the user's timezone, not UTC, so \"today\" feels right.\n function computeRecency(dateStr) {\n try {\n const d = new Date(dateStr);\n if (isNaN(d.getTime())) return null;\n const now = new Date();\n const dayMs = 24 * 60 * 60 * 1000;\n const diff = (now.getTime() - d.getTime()) / dayMs;\n if (diff < 0) return null; // future-dated, leave plain\n if (diff < 1.0) return 'today';\n if (diff < 7.0) return 'this-week';\n if (diff < 31.0) return 'this-month';\n return null;\n } catch (e) { return null; }\n }\n\n function localizeRecency(bucket) {\n return RECENCY_LABELS[bucket] || bucket;\n }\n\n // Emoji icon per article type โ€” keeps the visual language consistent with\n // the rest of the site (root index.html, political-intelligence.html).\n function typeIcon(type) {\n switch (type) {\n case 'prospective': return '<span aria-hidden=\"true\">๐Ÿ”ฎ</span>';\n case 'retrospective': return '<span aria-hidden=\"true\">๐Ÿ“Š</span>';\n case 'analysis': return '<span aria-hidden=\"true\">๐Ÿง </span>';\n case 'breaking': return '<span aria-hidden=\"true\">โšก</span>';\n default: return '<span aria-hidden=\"true\">๐Ÿ“ฐ</span>';\n }\n }\n\n function renderPage() {\n const grid = document.getElementById('articles-grid');\n const noArticles = document.getElementById('no-articles');\n const noResults = document.getElementById('no-results');\n const counter = document.getElementById('article-counter');\n const btn = document.getElementById('load-more-btn');\n\n if (articles.length === 0) {\n grid.innerHTML = '';\n grid.removeAttribute('aria-busy');\n if (noArticles) noArticles.hidden = false;\n noResults.hidden = true;\n if (counter) counter.textContent = '';\n if (btn) btn.hidden = true;\n updateFilterChrome();\n return;\n }\n\n if (filteredArticles.length === 0) {\n grid.innerHTML = '';\n grid.removeAttribute('aria-busy');\n noResults.hidden = false;\n if (noArticles) noArticles.hidden = true;\n if (counter) counter.textContent = '';\n if (btn) btn.hidden = true;\n updateFilterChrome();\n return;\n }\n\n if (noArticles) noArticles.hidden = true;\n noResults.hidden = true;\n\n const visible = filteredArticles.slice(0, visibleCount);\n grid.innerHTML = visible.map(buildArticleCard).join('');\n grid.removeAttribute('aria-busy');\n\n const shown = visible.length;\n const total = filteredArticles.length;\n if (counter) counter.textContent = i18nShowing(shown, total);\n\n if (btn) {\n if (total > visibleCount) {\n btn.hidden = false;\n btn.setAttribute('aria-label', i18nLoadMore);\n } else {\n btn.hidden = true;\n }\n }\n\n updateFilterChrome();\n }\n\n // Show/hide the \"Clear filters\" button + active-count badge based on\n // whether any filter is set away from its default. Keeps the filter\n // bar honest: empty state shows zero affordances, active state shows\n // exactly how many filters are biting.\n function updateFilterChrome() {\n const typeFilter = document.getElementById('filter-type').value;\n const topicFilter = document.getElementById('filter-topic').value;\n const sortFilter = document.getElementById('filter-sort').value;\n const searchInput = document.getElementById('search-input').value.trim();\n\n let activeCount = 0;\n if (typeFilter !== 'all') activeCount++;\n if (topicFilter !== 'all') activeCount++;\n if (sortFilter !== 'date-desc') activeCount++;\n if (searchInput) activeCount++;\n\n const clearBtn = document.getElementById('clear-filters-btn');\n if (clearBtn) {\n clearBtn.hidden = activeCount === 0;\n }\n const countBadge = document.getElementById('filter-active-count');\n if (countBadge) {\n countBadge.textContent = activeCount > 0 ? '(' + activeCount + ')' : '';\n countBadge.hidden = activeCount === 0;\n }\n }\n\n function clearAllFilters() {\n const typeEl = document.getElementById('filter-type');\n const topicEl = document.getElementById('filter-topic');\n const sortEl = document.getElementById('filter-sort');\n const searchEl = document.getElementById('search-input');\n if (typeEl) typeEl.value = 'all';\n if (topicEl) topicEl.value = 'all';\n if (sortEl) sortEl.value = 'date-desc';\n if (searchEl) searchEl.value = '';\n filterArticles();\n if (searchEl) searchEl.focus();\n }\n\n function loadMore() {\n const prevCount = visibleCount;\n visibleCount += PAGE_SIZE;\n updateURL();\n renderPage();\n const cards = document.querySelectorAll('.article-card');\n if (cards[prevCount]) {\n const link = cards[prevCount].querySelector('a');\n if (link) link.focus();\n }\n }\n\n function updateURL() {\n const typeFilter = document.getElementById('filter-type').value;\n const topicFilter = document.getElementById('filter-topic').value;\n const sortFilter = document.getElementById('filter-sort').value;\n const searchInput = document.getElementById('search-input').value.trim();\n const params = new URLSearchParams();\n if (typeFilter !== 'all') params.set('type', typeFilter);\n if (topicFilter !== 'all') params.set('topic', topicFilter);\n if (sortFilter !== 'date-desc') params.set('sort', sortFilter);\n if (searchInput) params.set('q', searchInput);\n const effectiveVisible = Math.min(visibleCount, filteredArticles.length);\n const page = Math.ceil(effectiveVisible / PAGE_SIZE);\n if (page > 1 && filteredArticles.length > PAGE_SIZE) {\n params.set('page', String(page));\n }\n const newURL = params.toString() ? '?' + params.toString() : window.location.pathname;\n if (window.history && window.history.replaceState) {\n window.history.replaceState(null, '', newURL);\n }\n }\n\n function localizeType(type) {\n return typeLabels[type] || type;\n }\n\n function formatDate(dateStr) {\n const date = new Date(dateStr);\n return date.toLocaleDateString(LOCALE_CODE, { year: 'numeric', month: 'long', day: 'numeric' });\n }\n\n function filterArticles() {\n const typeFilter = document.getElementById('filter-type').value;\n const topicFilter = document.getElementById('filter-topic').value;\n const sortFilter = document.getElementById('filter-sort').value;\n const searchQuery = document.getElementById('search-input').value.toLowerCase().trim();\n\n let filtered = [...articles];\n\n if (typeFilter !== 'all') {\n filtered = filtered.filter(article => article.type === typeFilter);\n }\n\n if (topicFilter !== 'all') {\n filtered = filtered.filter(article => article.topics.includes(topicFilter));\n }\n\n if (searchQuery) {\n filtered = filtered.filter(article => article.title.toLowerCase().includes(searchQuery));\n }\n\n switch(sortFilter) {\n case 'date-desc':\n filtered.sort((a, b) => new Date(b.date) - new Date(a.date));\n break;\n case 'date-asc':\n filtered.sort((a, b) => new Date(a.date) - new Date(b.date));\n break;\n case 'title':\n filtered.sort((a, b) => a.title.localeCompare(b.title));\n break;\n }\n\n filteredArticles = filtered;\n if (restoringFromURL) {\n restoringFromURL = false;\n } else {\n visibleCount = PAGE_SIZE;\n }\n updateURL();\n renderPage();\n }\n\n function readURLParams() {\n const params = new URLSearchParams(window.location.search);\n\n function safeSetSelect(id, value) {\n const el = document.getElementById(id);\n if (!el) return;\n const opts = Array.from(el.options);\n if (opts.some(o => o.value === value)) {\n el.value = value;\n }\n }\n\n if (params.has('type')) safeSetSelect('filter-type', params.get('type'));\n if (params.has('topic')) safeSetSelect('filter-topic', params.get('topic'));\n if (params.has('sort')) safeSetSelect('filter-sort', params.get('sort'));\n const searchInput = document.getElementById('search-input');\n if (searchInput && params.has('q')) searchInput.value = params.get('q');\n if (params.has('page')) {\n const page = parseInt(params.get('page'), 10);\n if (!isNaN(page) && page > 1) {\n visibleCount = page * PAGE_SIZE;\n restoringFromURL = true;\n }\n }\n }\n\n document.getElementById('filter-type').addEventListener('change', filterArticles);\n document.getElementById('filter-topic').addEventListener('change', filterArticles);\n document.getElementById('filter-sort').addEventListener('change', filterArticles);\n document.getElementById('load-more-btn').addEventListener('click', loadMore);\n var __clearBtn = document.getElementById('clear-filters-btn');\n if (__clearBtn) __clearBtn.addEventListener('click', clearAllFilters);\n\n let searchTimer;\n document.getElementById('search-input').addEventListener('input', function() {\n clearTimeout(searchTimer);\n searchTimer = setTimeout(filterArticles, 300);\n });\n\n readURLParams();\n filterArticles();\n" = ...

    Pure-literal client runtime body (see module doc).