/* ═══════════════════════════════════════════════════════
UNREDACTED — Declassified Document Archive
SPA Engine with hash routing & PDF.js viewer
═══════════════════════════════════════════════════════ */
(function () {
'use strict';
const ROOT = document.getElementById('unredactedRoot');
const PDF_BASE = '/unredacted/docs';
let DATA = null;
// ─── Data Loading ────────────────────────────────────
async function loadData() {
try {
const r = await fetch('/api/unredacted');
if (!r.ok) throw new Error(`HTTP ${r.status}`);
DATA = await r.json();
} catch (e) {
console.error('UNREDACTED: data load failed', e);
ROOT.innerHTML = `
⚠️
DATA FEED OFFLINE
Failed to load archive index. ${e.message}
`;
}
}
// ─── Helpers ──────────────────────────────────────────
function countDocuments(obj) {
if (!obj) return 0;
if (Array.isArray(obj.documents)) return obj.documents.length;
if (Array.isArray(obj.collections)) {
return obj.collections.reduce((s, c) => s + (c.documents ? c.documents.length : 0), 0);
}
if (Array.isArray(obj.countries)) {
return obj.countries.reduce((s, cn) => s + countDocuments(cn), 0);
}
return 0;
}
function countCollections(obj) {
if (!obj) return 0;
if (Array.isArray(obj.collections)) return obj.collections.length;
if (Array.isArray(obj.countries)) {
return obj.countries.reduce((s, cn) => s + (cn.collections ? cn.collections.length : 0), 0);
}
return 0;
}
function countCountries(cat) {
return cat.countries ? cat.countries.length : 0;
}
function totalStats() {
if (!DATA || !DATA.categories) return { cats: 0, countries: 0, collections: 0, docs: 0 };
let countries = new Set();
let collections = 0;
let docs = 0;
DATA.categories.forEach(cat => {
(cat.countries || []).forEach(cn => {
countries.add(cn.code);
(cn.collections || []).forEach(col => {
collections++;
docs += (col.documents || []).length;
});
});
});
return { cats: DATA.categories.length, countries: countries.size, collections, docs };
}
function findCategory(id) {
return DATA.categories.find(c => c.id === id);
}
function findCountry(cat, code) {
return (cat.countries || []).find(c => c.code === code);
}
function findCollection(country, colId) {
return (country.collections || []).find(c => c.id === colId);
}
function findDocument(collection, docId) {
return (collection.documents || []).find(d => d.id === docId);
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
// ─── Breadcrumb Builder ───────────────────────────────
function breadcrumb(parts) {
// parts: [{label, hash?}, ...]
let html = '';
parts.forEach((p, i) => {
if (i > 0) html += '
/';
if (p.hash) {
html += `
${esc(p.label)}`;
} else {
html += `
${esc(p.label)}`;
}
});
html += '
';
return html;
}
// ─── View: Categories (Home) ──────────────────────────
function renderCategories() {
const stats = totalStats();
let html = '';
html += `DECLASSIFIED // DOCUMENT ARCHIVE // ACCESS GRANTED
`;
html += `
CATEGORIES${stats.cats}
COUNTRIES${stats.countries}
COLLECTIONS${stats.collections}
DOCUMENTS${stats.docs}
`;
html += breadcrumb([{ label: 'UNREDACTED' }]);
// ─── Global Search Bar ────────────────────────────────
html += ``;
html += '';
html += '
SELECT CATEGORY
';
DATA.categories.forEach(cat => {
const nCountries = countCountries(cat);
const nDocs = countDocuments(cat);
const nCol = countCollections(cat);
html += `
${esc(cat.description)}
${nCountries} COUNTR${nCountries === 1 ? 'Y' : 'IES'} · ${nCol} COLLECTION${nCol === 1 ? '' : 'S'}
${nDocs} DOC${nDocs === 1 ? '' : 'S'}
`;
});
html += '
';
html += ''; // close #urCategorySection
ROOT.innerHTML = html;
// ─── Bind search events ────────────────────────────────
initSearchListeners();
}
// ─── View: Category (Country Selector + Collections) ──
function renderCategory(catId, activeCountry) {
const cat = findCategory(catId);
if (!cat) return renderNotFound('Category not found');
let html = '';
html += `DECLASSIFIED // ${esc(cat.name)} // ACCESS GRANTED
`;
html += breadcrumb([
{ label: 'UNREDACTED', hash: '' },
{ label: cat.name }
]);
// Country selector
if (cat.countries && cat.countries.length > 0) {
html += 'SELECT COUNTRY
';
html += '';
cat.countries.forEach(cn => {
const nCol = cn.collections ? cn.collections.length : 0;
const isActive = activeCountry === cn.code;
html += `
${cn.flag || ''}
${esc(cn.name)}
[${nCol}]
`;
});
html += '
';
// If a country is selected, show its collections
if (activeCountry) {
const country = findCountry(cat, activeCountry);
if (country) {
html += renderCountryCollections(cat, country);
}
} else {
// Show all collections from all countries
html += 'ALL COLLECTIONS
';
let hasCols = false;
cat.countries.forEach(cn => {
if (cn.collections && cn.collections.length > 0) {
hasCols = true;
}
});
if (hasCols) {
html += '';
cat.countries.forEach(cn => {
(cn.collections || []).forEach(col => {
html += renderCollectionCard(catId, cn.code, col, cn.flag);
});
});
html += '
';
} else {
html += renderEmpty('NO COLLECTIONS YET', 'Collections will appear here once documents are added to this category.');
}
}
} else {
html += renderEmpty('NO COUNTRIES REGISTERED', 'No country data available for this category yet.');
}
ROOT.innerHTML = html;
}
function renderCountryCollections(cat, country) {
let html = '';
html += `${country.flag || ''} ${esc(country.name)} — COLLECTIONS
`;
if (country.collections && country.collections.length > 0) {
html += '';
country.collections.forEach(col => {
html += renderCollectionCard(cat.id, country.code, col);
});
html += '
';
} else {
html += renderEmpty('NO COLLECTIONS', 'No document collections available for this country yet.');
}
return html;
}
function renderCollectionCard(catId, countryCode, col, flag) {
const nDocs = col.documents ? col.documents.length : 0;
return `
${flag ? flag + ' ' : ''}${esc(col.name)}
${esc(col.year)}
${esc(col.description)}
`;
}
// ─── View: Collection (Documents) ─────────────────────
function renderCollection(catId, countryCode, colId) {
const cat = findCategory(catId);
if (!cat) return renderNotFound('Category not found');
const country = findCountry(cat, countryCode);
if (!country) return renderNotFound('Country not found');
const col = findCollection(country, colId);
if (!col) return renderNotFound('Collection not found');
let html = '';
html += `DECLASSIFIED // ${esc(col.name).toUpperCase()} // ACCESS GRANTED
`;
html += breadcrumb([
{ label: 'UNREDACTED', hash: '' },
{ label: cat.name, hash: `category/${catId}` },
{ label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` },
{ label: col.name }
]);
// Detail header
html += ``;
// Documents
if (col.documents && col.documents.length > 0) {
html += `DOCUMENTS [${col.documents.length}]
`;
html += '';
col.documents.forEach(doc => {
html += `
📄
${esc(doc.title)}
${esc(doc.description)}
PAGES: ${doc.pages || '?'}
RELEASED: ${esc(doc.date_released || 'UNKNOWN')}
${esc(doc.filename)}
VIEW
`;
});
html += '
';
} else {
html += renderEmpty('NO DOCUMENTS UPLOADED YET', 'This collection has been catalogued but documents have not yet been uploaded to the archive.');
}
ROOT.innerHTML = html;
}
// ─── View: Document (PDF Viewer) ──────────────────────
function renderDocument(catId, countryCode, colId, docId) {
const cat = findCategory(catId);
if (!cat) return renderNotFound('Category not found');
const country = findCountry(cat, countryCode);
if (!country) return renderNotFound('Country not found');
const col = findCollection(country, colId);
if (!col) return renderNotFound('Collection not found');
const doc = findDocument(col, docId);
if (!doc) return renderNotFound('Document not found');
const pdfUrl = `${PDF_BASE}/${catId}/${colId}/${doc.filename}`;
let html = '';
html += `DECLASSIFIED // ${esc(doc.title).toUpperCase()}
`;
html += breadcrumb([
{ label: 'UNREDACTED', hash: '' },
{ label: cat.name, hash: `category/${catId}` },
{ label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` },
{ label: col.name, hash: `collection/${catId}/${countryCode}/${colId}` },
{ label: doc.title }
]);
// Viewer
html += ``;
// Doc info
html += ``;
ROOT.innerHTML = html;
// Init PDF viewer
initPdfViewer(pdfUrl);
}
// ─── PDF.js Viewer ───────────────────────────────────
let pdfDoc = null;
let pdfPage = 1;
let pdfScale = 1.5;
let pdfRendering = false;
let pdfPending = null;
let pdfAllText = [];
let pdfSearchActive = false;
let pdfSearchQuery = '';
let pdfSearchCaseSensitive = false;
let pdfSearchMatches = [];
let pdfSearchCurrentIdx = -1;
async function initPdfViewer(url) {
const canvasWrap = document.getElementById('pdfCanvasWrap');
const loadingEl = document.getElementById('pdfLoading');
const pageInfo = document.getElementById('pdfPageInfo');
// Check if pdf.js is loaded
if (typeof pdfjsLib === 'undefined') {
canvasWrap.innerHTML = ``;
return;
}
try {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const loadingTask = pdfjsLib.getDocument(url);
pdfDoc = await loadingTask.promise;
pdfPage = 1;
loadingEl.remove();
// Create page container with canvas
const pageContainer = document.createElement('div');
pageContainer.id = 'pdfPageContainer';
pageContainer.className = 'prop-page-container';
const canvas = document.createElement('canvas');
canvas.id = 'pdfCanvas';
pageContainer.appendChild(canvas);
canvasWrap.appendChild(pageContainer);
updatePageInfo();
renderPdfPage();
// Controls
document.getElementById('pdfPrev').addEventListener('click', () => {
if (pdfPage <= 1) return;
pdfPage--;
queueRender();
});
document.getElementById('pdfNext').addEventListener('click', () => {
if (pdfPage >= pdfDoc.numPages) return;
pdfPage++;
queueRender();
});
document.getElementById('pdfZoomIn').addEventListener('click', () => {
pdfScale = Math.min(pdfScale + 0.25, 4);
queueRender();
});
document.getElementById('pdfZoomOut').addEventListener('click', () => {
pdfScale = Math.max(pdfScale - 0.25, 0.5);
queueRender();
});
document.getElementById('pdfFit').addEventListener('click', () => {
pdfScale = 1.5;
queueRender();
});
// Extract all page text for search
extractAllText();
// Initialize search controls
initSearchControls();
} catch (e) {
console.error('PDF load failed:', e);
canvasWrap.innerHTML = ``;
}
}
function updatePageInfo() {
const el = document.getElementById('pdfPageInfo');
if (el && pdfDoc) {
el.textContent = `${pdfPage} / ${pdfDoc.numPages}`;
}
}
async function renderPdfPage() {
if (!pdfDoc) return;
pdfRendering = true;
const page = await pdfDoc.getPage(pdfPage);
const viewport = page.getViewport({ scale: pdfScale });
const canvas = document.getElementById('pdfCanvas');
if (!canvas) { pdfRendering = false; return; }
const ctx = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Update page container size to match viewport
const pageContainer = document.getElementById('pdfPageContainer');
if (pageContainer) {
pageContainer.style.width = viewport.width + 'px';
pageContainer.style.height = viewport.height + 'px';
}
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
// Render text layer for search and text selection
await renderPageTextLayer(page, viewport);
// Highlight search matches on this page
highlightCurrentPageMatches();
pdfRendering = false;
updatePageInfo();
if (pdfPending !== null) {
renderPdfPage();
pdfPending = null;
}
}
function queueRender() {
if (pdfRendering) {
pdfPending = pdfPage;
} else {
renderPdfPage();
}
}
// ─── Text Layer Rendering ────────────────────────────
async function renderPageTextLayer(page, viewport) {
const existing = document.getElementById('pdfTextLayer');
if (existing) existing.remove();
const pageContainer = document.getElementById('pdfPageContainer');
if (!pageContainer) return;
const textContent = await page.getTextContent();
const textLayerDiv = document.createElement('div');
textLayerDiv.id = 'pdfTextLayer';
textLayerDiv.className = 'prop-text-layer';
textLayerDiv.style.width = viewport.width + 'px';
textLayerDiv.style.height = viewport.height + 'px';
// Try pdf.js built-in renderTextLayer, fallback to manual
if (typeof pdfjsLib.renderTextLayer === 'function') {
try {
const task = pdfjsLib.renderTextLayer({
textContentSource: textContent,
container: textLayerDiv,
viewport: viewport,
});
await task.promise;
} catch (err) {
console.warn('renderTextLayer failed, using fallback:', err);
buildTextLayerFallback(textContent, viewport, textLayerDiv);
}
} else {
buildTextLayerFallback(textContent, viewport, textLayerDiv);
}
// Tag each span with its item index for search highlighting
let itemIdx = 0;
for (const child of textLayerDiv.childNodes) {
if (child.nodeName === 'SPAN') {
child.dataset.itemIdx = itemIdx;
itemIdx++;
}
}
pageContainer.appendChild(textLayerDiv);
}
function buildTextLayerFallback(textContent, viewport, container) {
textContent.items.forEach((item, idx) => {
if (!item.str) return;
const span = document.createElement('span');
span.textContent = item.str;
const tx = item.transform;
const fontHeight = Math.sqrt(tx[2] * tx[2] + tx[3] * tx[3]);
const s = viewport.scale;
span.style.fontSize = (fontHeight * s) + 'px';
span.style.fontFamily = 'sans-serif';
span.style.left = (tx[4] * s) + 'px';
span.style.top = (viewport.height - tx[5] * s - fontHeight * s) + 'px';
span.style.position = 'absolute';
span.style.color = 'transparent';
span.style.whiteSpace = 'pre';
container.appendChild(span);
});
}
// ─── Text Extraction for Search ──────────────────────
async function extractAllText() {
pdfAllText = [];
if (!pdfDoc) return;
for (let i = 1; i <= pdfDoc.numPages; i++) {
const page = await pdfDoc.getPage(i);
const tc = await page.getTextContent();
let fullText = '';
const itemOffsets = [];
tc.items.forEach((item, idx) => {
const start = fullText.length;
fullText += item.str;
itemOffsets.push({ start: start, end: fullText.length, itemIdx: idx });
if (item.hasEOL) fullText += '\n';
});
pdfAllText.push({ pageNum: i, text: fullText, items: tc.items, itemOffsets: itemOffsets });
}
console.log('UNREDACTED: Extracted text from ' + pdfAllText.length + ' pages');
}
// ─── Search Controls Init ────────────────────────────
function initSearchControls() {
const searchInput = document.getElementById('pdfSearchInput');
const searchPrev = document.getElementById('pdfSearchPrev');
const searchNext = document.getElementById('pdfSearchNext');
const searchClose = document.getElementById('pdfSearchClose');
const searchToggle = document.getElementById('pdfSearchToggle');
const searchCaseBtn = document.getElementById('pdfSearchCaseBtn');
if (!searchInput) return;
searchToggle.addEventListener('click', () => toggleSearchBar());
searchClose.addEventListener('click', () => closeSearchBar());
let searchTimeout = null;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
pdfSearchQuery = searchInput.value;
performSearch();
}, 200);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (e.shiftKey) prevMatch(); else nextMatch();
}
if (e.key === 'Escape') {
e.preventDefault();
closeSearchBar();
}
});
searchPrev.addEventListener('click', () => prevMatch());
searchNext.addEventListener('click', () => nextMatch());
searchCaseBtn.addEventListener('click', () => {
pdfSearchCaseSensitive = !pdfSearchCaseSensitive;
searchCaseBtn.classList.toggle('active', pdfSearchCaseSensitive);
performSearch();
});
// Ctrl+F shortcut — only when PDF viewer is present
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
if (document.getElementById('pdfViewerContainer')) {
e.preventDefault();
toggleSearchBar(true);
}
}
});
}
function toggleSearchBar(forceOpen) {
const searchBar = document.getElementById('pdfSearchBar');
const searchInput = document.getElementById('pdfSearchInput');
if (!searchBar) return;
if (forceOpen || !pdfSearchActive) {
searchBar.classList.add('active');
pdfSearchActive = true;
setTimeout(() => searchInput && searchInput.focus(), 50);
} else {
closeSearchBar();
}
}
function closeSearchBar() {
const searchBar = document.getElementById('pdfSearchBar');
if (!searchBar) return;
searchBar.classList.remove('active');
pdfSearchActive = false;
pdfSearchQuery = '';
pdfSearchMatches = [];
pdfSearchCurrentIdx = -1;
const searchInput = document.getElementById('pdfSearchInput');
if (searchInput) searchInput.value = '';
updateSearchInfo();
clearHighlights();
}
function performSearch() {
pdfSearchMatches = [];
pdfSearchCurrentIdx = -1;
const query = pdfSearchQuery.trim();
if (!query || query.length === 0) {
updateSearchInfo();
clearHighlights();
return;
}
const cs = pdfSearchCaseSensitive;
const q = cs ? query : query.toLowerCase();
pdfAllText.forEach(pageData => {
const text = cs ? pageData.text : pageData.text.toLowerCase();
let pos = 0;
while ((pos = text.indexOf(q, pos)) !== -1) {
const matchStart = pos;
const matchEnd = pos + q.length;
const affectedItems = pageData.itemOffsets.filter(
io => io.end > matchStart && io.start < matchEnd
);
pdfSearchMatches.push({
page: pageData.pageNum,
charStart: matchStart,
charEnd: matchEnd,
itemIndices: affectedItems.map(io => io.itemIdx)
});
pos += 1;
}
});
pdfSearchCurrentIdx = pdfSearchMatches.length > 0 ? 0 : -1;
updateSearchInfo();
if (pdfSearchMatches.length > 0) {
goToMatch(0);
} else {
clearHighlights();
}
}
function goToMatch(idx) {
if (idx < 0 || idx >= pdfSearchMatches.length) return;
pdfSearchCurrentIdx = idx;
const match = pdfSearchMatches[idx];
if (match.page !== pdfPage) {
pdfPage = match.page;
queueRender();
} else {
highlightCurrentPageMatches();
}
updateSearchInfo();
}
function nextMatch() {
if (pdfSearchMatches.length === 0) return;
goToMatch((pdfSearchCurrentIdx + 1) % pdfSearchMatches.length);
}
function prevMatch() {
if (pdfSearchMatches.length === 0) return;
goToMatch((pdfSearchCurrentIdx - 1 + pdfSearchMatches.length) % pdfSearchMatches.length);
}
function highlightCurrentPageMatches() {
clearHighlights();
const textLayer = document.getElementById('pdfTextLayer');
if (!textLayer) return;
const pageMatches = pdfSearchMatches.filter(m => m.page === pdfPage);
if (pageMatches.length === 0) return;
pageMatches.forEach(match => {
const globalIdx = pdfSearchMatches.indexOf(match);
const isActive = globalIdx === pdfSearchCurrentIdx;
match.itemIndices.forEach(itemIdx => {
const span = textLayer.querySelector('span[data-item-idx="' + itemIdx + '"]');
if (span) {
span.classList.add('prop-search-hl');
if (isActive) {
span.classList.add('prop-search-hl-active');
const canvasWrap = document.getElementById('pdfCanvasWrap');
if (canvasWrap) {
const sr = span.getBoundingClientRect();
const wr = canvasWrap.getBoundingClientRect();
const offset = sr.top - wr.top - wr.height / 3;
canvasWrap.scrollBy({ top: offset, behavior: 'smooth' });
}
}
}
});
});
}
function clearHighlights() {
const tl = document.getElementById('pdfTextLayer');
if (!tl) return;
tl.querySelectorAll('.prop-search-hl').forEach(el => {
el.classList.remove('prop-search-hl', 'prop-search-hl-active');
});
}
function updateSearchInfo() {
const info = document.getElementById('pdfSearchInfo');
if (!info) return;
if (pdfSearchMatches.length === 0) {
if (pdfSearchQuery && pdfSearchQuery.trim().length > 0) {
info.textContent = 'NO MATCHES';
info.className = 'prop-search-info no-match';
} else {
info.textContent = '';
info.className = 'prop-search-info';
}
} else {
info.textContent = (pdfSearchCurrentIdx + 1) + ' OF ' + pdfSearchMatches.length;
info.className = 'prop-search-info';
}
}
// ─── Global Archive Search ─────────────────────────────
let searchDebounceTimer = null;
function initSearchListeners() {
const input = document.getElementById('urSearchInput');
const clearBtn = document.getElementById('urSearchClear');
if (!input) return;
input.addEventListener('input', () => {
clearTimeout(searchDebounceTimer);
const val = input.value.trim();
clearBtn.style.display = val.length > 0 ? 'inline-block' : 'none';
searchDebounceTimer = setTimeout(() => {
globalArchiveSearch(val);
}, 300);
});
clearBtn.addEventListener('click', () => {
input.value = '';
clearBtn.style.display = 'none';
globalArchiveSearch('');
input.focus();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
input.value = '';
clearBtn.style.display = 'none';
globalArchiveSearch('');
}
});
}
function globalArchiveSearch(query) {
const resultsEl = document.getElementById('urSearchResults');
const statusEl = document.getElementById('urSearchStatus');
const catSection = document.getElementById('urCategorySection');
if (!resultsEl || !statusEl) return;
if (!query || query.length === 0) {
resultsEl.innerHTML = '';
statusEl.innerHTML = '';
if (catSection) catSection.style.display = '';
return;
}
const q = query.toLowerCase();
const results = [];
if (!DATA || !DATA.categories) return;
DATA.categories.forEach(cat => {
(cat.countries || []).forEach(cn => {
(cn.collections || []).forEach(col => {
(col.documents || []).forEach(doc => {
const fields = [
doc.title || '',
doc.description || '',
col.name || '',
cat.name || '',
cn.name || '',
doc.year ? String(doc.year) : '',
doc.classification || ''
];
const combined = fields.join(' ').toLowerCase();
if (combined.includes(q)) {
results.push({
doc,
catName: cat.name,
catId: cat.id,
catIcon: cat.icon || '📁',
countryName: cn.name,
countryCode: cn.code,
countryFlag: cn.flag || '',
colName: col.name,
colId: col.id
});
}
});
// Also match collection-level (even if no docs match)
const colFields = [col.name || '', col.description || '', cat.name || '', cn.name || ''].join(' ').toLowerCase();
if (colFields.includes(q) && !results.find(r => r.colId === col.id && r.catId === cat.id && r.countryCode === cn.code && !r.doc.id)) {
// Check if we already added docs from this collection
const hasDocResults = results.some(r => r.colId === col.id && r.catId === cat.id && r.countryCode === cn.code);
if (!hasDocResults) {
results.push({
doc: { title: col.name, description: col.description || 'Collection in ' + cat.name, id: '__col__' },
catName: cat.name,
catId: cat.id,
catIcon: cat.icon || '📁',
countryName: cn.name,
countryCode: cn.code,
countryFlag: cn.flag || '',
colName: col.name,
colId: col.id,
isCollection: true
});
}
}
});
});
});
// Hide categories when searching
if (catSection) catSection.style.display = 'none';
if (results.length === 0) {
statusEl.innerHTML = 'NO MATCHING DOCUMENTS // REFINE SEARCH PARAMETERS';
resultsEl.innerHTML = '';
return;
}
statusEl.innerHTML = `FOUND ${results.length} MATCHING DOCUMENT${results.length === 1 ? '' : 'S'}`;
let html = '';
results.forEach(r => {
const title = highlightMatch(esc(r.doc.title || 'Untitled'), q);
const desc = r.doc.description ? highlightMatch(esc(truncateText(r.doc.description, 160)), q) : '';
const hash = r.isCollection
? `country/${r.catId}/${r.countryCode}`
: `doc/${r.catId}/${r.countryCode}/${r.colId}/${r.doc.id}`;
html += `
${esc(r.catName)}
${r.countryFlag} ${esc(r.countryName)}
${esc(r.colName)}
${desc ? `
${desc}
` : ''}
`;
});
html += '
';
resultsEl.innerHTML = html;
}
function highlightMatch(text, query) {
if (!query) return text;
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return text.replace(regex, '$1');
}
function truncateText(text, maxLen) {
if (text.length <= maxLen) return text;
return text.substring(0, maxLen).replace(/\s+\S*$/, '') + '...';
}
// ─── Not Found / Empty ────────────────────────────────
function renderNotFound(msg) {
ROOT.innerHTML = `
⚠️
${esc(msg)}
The requested resource could not be located in the archive.
`;
}
function renderEmpty(title, text) {
return `
◎
${esc(title)}
${esc(text)}
`;
}
// ─── Router ───────────────────────────────────────────
function route() {
if (!DATA) return;
// Reset PDF state
pdfDoc = null;
pdfPage = 1;
pdfRendering = false;
pdfPending = null;
pdfAllText = [];
pdfSearchActive = false;
pdfSearchQuery = '';
pdfSearchMatches = [];
pdfSearchCurrentIdx = -1;
const hash = (location.hash || '').replace(/^#\/?/, '');
const parts = hash.split('/').filter(Boolean);
if (parts.length === 0 || hash === '') {
renderCategories();
} else if (parts[0] === 'category' && parts[1]) {
renderCategory(parts[1], null);
} else if (parts[0] === 'country' && parts[1] && parts[2]) {
renderCategory(parts[1], parts[2]);
} else if (parts[0] === 'collection' && parts.length >= 4) {
renderCollection(parts[1], parts[2], parts[3]);
} else if (parts[0] === 'doc' && parts.length >= 5) {
renderDocument(parts[1], parts[2], parts[3], parts[4]);
} else {
renderNotFound('INVALID ROUTE');
}
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// ─── Init ─────────────────────────────────────────────
async function init() {
ROOT.innerHTML = 'INITIALISING UNREDACTED ARCHIVE...
';
await loadData();
if (DATA) {
window.addEventListener('hashchange', route);
route();
}
}
// Wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();