Skip to content
142 changes: 102 additions & 40 deletions docs/src/assets/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ window.onload = function() {
})())

const documenterTarget = document.querySelector('#documenter');
documenterTarget.parentNode.insertBefore(header, documenterTarget);
if (documenterTarget && documenterTarget.parentNode) {
documenterTarget.parentNode.insertBefore(header, documenterTarget);
}

// === Site context banner for Docs ===
// Add banner directly to navbar after header is created
Expand All @@ -104,48 +106,108 @@ window.onload = function() {
}
}

// === Search results banner ===
// Add banner inside modal-card-head at the bottom when search results appear
function addSearchBanner() {
const searchModal = document.getElementById('search-modal');
if (!searchModal) {
return;
}

const modalCardHead = searchModal.querySelector('.modal-card-head');
if (!modalCardHead) {
return;
}

document.addEventListener('DOMContentLoaded', function() {
// === Cross-site search: also search docs.rxinfer.com ===
(function () {
const REMOTE_BASE = 'https://docs.rxinfer.com/stable';
const REMOTE_LABEL = 'Documentation';
let remoteIndex = null;
let remoteIndexPromise = null;

async function fetchRemoteIndex() {
if (remoteIndex !== null) return;
if (remoteIndexPromise) return remoteIndexPromise;

remoteIndexPromise = (async () => {
try {
const res = await fetch(REMOTE_BASE + '/search_index.js');
if (!res.ok) return;
const text = await res.text();
const start = text.indexOf('{');
const end = text.lastIndexOf('}');
if (start < 0 || end < start) throw new Error('Invalid format');
remoteIndex = JSON.parse(text.slice(start, end + 1)).docs || [];
} catch (e) {
remoteIndex = [];
}
})();

return remoteIndexPromise;
}

function searchRemote(query) {
if (!remoteIndex || !remoteIndex.length) return [];
const words = query.trim().toLowerCase().split(/\s+/).filter(w => w.length > 1);
if (!words.length) return [];
return remoteIndex.filter(doc => {
const hay = (doc.title + ' ' + doc.text).toLowerCase();
return words.every(w => hay.includes(w));
}).slice(0, 8);
}

function esc(s) {
return (s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

function renderResults(results) {
const items = results.map(doc => `
<a href="${REMOTE_BASE}/${doc.location || ''}" class="search-result-link w-100 is-flex is-flex-direction-column gap-2 px-4 py-2">
<div class="w-100 is-flex is-flex-wrap-wrap is-justify-content-space-between is-align-items-flex-start">
<div class="search-result-title has-text-weight-bold">${esc(doc.title)}</div>
<div class="property-search-result-badge">${esc(doc.category)}</div>
</div>
<div class="has-text-left" style="font-size:smaller;opacity:0.7">
<i class="fas fa-external-link-alt"></i> ${REMOTE_LABEL}: ${esc((doc.location||'').slice(0,60))}
</div>
</a>
<div class="search-divider w-100"></div>`).join('');

return `<div id="cross-site-results" class="w-100 is-flex is-flex-direction-column gap-2">
<div style="padding:0.5rem 1rem;border-top:1px solid var(--card-border-color,#e9ecef);margin-top:0.5rem">
<span class="is-size-7" style="opacity:0.7">Also from <strong>${REMOTE_LABEL}</strong> — ${results.length} result${results.length !== 1 ? 's' : ''}</span>
</div>${items}</div>`;
}

let injecting = false;

function inject() {
if (injecting) return;
const body = document.querySelector('.search-modal-card-body');
const input = document.querySelector('.documenter-search-input');
if (!body || !input || body.querySelector('#cross-site-results')) return;

// Check if banner already exists
if (modalCardHead.querySelector('#search-results-banner')) {
if (remoteIndex === null) {
fetchRemoteIndex().then(() => setTimeout(inject, 0));
return;
}

// Create a wrapper div for the banner
const bannerWrapper = document.createElement('div');
bannerWrapper.style.cssText = 'width: 100%; position: absolute; bottom: 0; display: flex; justify-content: center; align-items: center;';
bannerWrapper.id = 'search-results-banner';
bannerWrapper.className = 'is-size-7';
bannerWrapper.innerHTML = `
<strong>Note:</strong>&nbsp;Search results do not include the&nbsp;<a href="https://docs.rxinfer.com/">documentation website</a>
`;

// Insert after all existing children in modal-card-head
modalCardHead.appendChild(bannerWrapper);
const query = input.value || '';
if (query.trim().length < 2) return;
const results = searchRemote(query);
if (!results.length) return;
injecting = true;
body.insertAdjacentHTML('beforeend', renderResults(results));
injecting = false;
}

// Watch for search modal and results to appear (search results are dynamically loaded)
const observer = new MutationObserver(function(mutations) {
addSearchBanner();
});

// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});

// Also try immediately in case search modal already exists
setTimeout(addSearchBanner, 100);
}

let bodyObserver = null;
function connectBodyObserver() {
const body = document.querySelector('.search-modal-card-body');
if (!body || bodyObserver) return;
bodyObserver = new MutationObserver(() => { if (!injecting) setTimeout(inject, 30); });
bodyObserver.observe(body, { childList: true });
}

new MutationObserver(() => {
const modal = document.getElementById('search-modal');
if (modal) {
if (modal.classList.contains('is-active')) fetchRemoteIndex();
connectBodyObserver();
}
}).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
fetchRemoteIndex();
})();
});
Loading