Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
250 lines
7.8 KiB
JavaScript
250 lines
7.8 KiB
JavaScript
(function () {
|
|
const navLinks = document.querySelectorAll('.wiki-nav a[href^="#"]');
|
|
const sections = Array.from(navLinks).map((link) => {
|
|
const id = link.getAttribute('href').slice(1);
|
|
return { link, el: document.getElementById(id) };
|
|
}).filter((s) => s.el);
|
|
|
|
function setActive(id) {
|
|
navLinks.forEach((a) => {
|
|
a.classList.toggle('active', a.getAttribute('href') === '#' + id);
|
|
});
|
|
}
|
|
|
|
function scrollToTarget(id, el) {
|
|
const target = el || document.getElementById(id);
|
|
if (!target) return;
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
history.replaceState(null, '', '#' + id);
|
|
const section =
|
|
target.closest('section') || (target.matches && target.matches('section') ? target : null);
|
|
if (section) setActive(section.id);
|
|
}
|
|
|
|
navLinks.forEach((link) => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const id = link.getAttribute('href').slice(1);
|
|
scrollToTarget(id);
|
|
});
|
|
});
|
|
|
|
if ('IntersectionObserver' in window && sections.length) {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const visible = entries
|
|
.filter((e) => e.isIntersecting)
|
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
|
if (visible) setActive(visible.target.id);
|
|
},
|
|
{ rootMargin: '-20% 0px -60% 0px', threshold: [0, 0.25, 0.5] }
|
|
);
|
|
sections.forEach((s) => observer.observe(s.el));
|
|
}
|
|
|
|
const hash = window.location.hash.slice(1);
|
|
if (hash && document.getElementById(hash)) {
|
|
setActive(hash);
|
|
const section = document.getElementById(hash).closest('section');
|
|
if (section) setActive(section.id);
|
|
} else if (sections.length) {
|
|
setActive(sections[0].el.id);
|
|
}
|
|
|
|
/* ── Search ── */
|
|
const searchInput = document.getElementById('wiki-search-input');
|
|
const searchResults = document.getElementById('wiki-search-results');
|
|
const HIGHLIGHT_CLASS = 'wiki-search-highlight';
|
|
let activeHighlights = [];
|
|
|
|
function stripText(el) {
|
|
return (el.textContent || '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function buildSearchIndex() {
|
|
const entries = [];
|
|
document.querySelectorAll('.wiki-content section').forEach((section) => {
|
|
const sectionId = section.id;
|
|
const sectionTitle = stripText(section.querySelector('h2') || section);
|
|
|
|
section.querySelectorAll('h3, h4').forEach((heading) => {
|
|
const headingId = heading.id || sectionId;
|
|
entries.push({
|
|
id: headingId,
|
|
sectionId,
|
|
title: stripText(heading),
|
|
sectionTitle,
|
|
text: stripText(heading),
|
|
el: heading,
|
|
});
|
|
});
|
|
|
|
section.querySelectorAll('p, li, td').forEach((block) => {
|
|
const text = stripText(block);
|
|
if (text.length < 12) return;
|
|
entries.push({
|
|
id: sectionId,
|
|
sectionId,
|
|
title: sectionTitle,
|
|
sectionTitle,
|
|
text,
|
|
el: block,
|
|
});
|
|
});
|
|
});
|
|
return entries;
|
|
}
|
|
|
|
const searchIndex = buildSearchIndex();
|
|
|
|
function clearHighlights() {
|
|
activeHighlights.forEach((mark) => {
|
|
const parent = mark.parentNode;
|
|
if (!parent) return;
|
|
parent.replaceChild(document.createTextNode(mark.textContent), mark);
|
|
parent.normalize();
|
|
});
|
|
activeHighlights = [];
|
|
}
|
|
|
|
function escapeRegExp(s) {
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function highlightMatches(el, query) {
|
|
clearHighlights();
|
|
if (!el || !query) return;
|
|
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
|
|
if (!terms.length) return;
|
|
|
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
const textNodes = [];
|
|
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
|
|
const pattern = new RegExp('(' + terms.map(escapeRegExp).join('|') + ')', 'gi');
|
|
|
|
textNodes.forEach((node) => {
|
|
const val = node.nodeValue;
|
|
if (!val || !pattern.test(val)) return;
|
|
pattern.lastIndex = 0;
|
|
const frag = document.createDocumentFragment();
|
|
let last = 0;
|
|
val.replace(pattern, (match, _g, offset) => {
|
|
if (offset > last) {
|
|
frag.appendChild(document.createTextNode(val.slice(last, offset)));
|
|
}
|
|
const mark = document.createElement('mark');
|
|
mark.className = HIGHLIGHT_CLASS;
|
|
mark.textContent = match;
|
|
frag.appendChild(mark);
|
|
activeHighlights.push(mark);
|
|
last = offset + match.length;
|
|
return match;
|
|
});
|
|
if (last < val.length) {
|
|
frag.appendChild(document.createTextNode(val.slice(last)));
|
|
}
|
|
node.parentNode.replaceChild(frag, node);
|
|
});
|
|
}
|
|
|
|
function scoreEntry(entry, terms) {
|
|
const title = entry.title.toLowerCase();
|
|
const text = entry.text.toLowerCase();
|
|
let score = 0;
|
|
terms.forEach((term) => {
|
|
if (title.includes(term)) score += 10;
|
|
if (text.includes(term)) score += 3;
|
|
if (title.startsWith(term)) score += 5;
|
|
});
|
|
return score;
|
|
}
|
|
|
|
function snippet(text, terms, maxLen) {
|
|
const lower = text.toLowerCase();
|
|
let idx = -1;
|
|
for (const term of terms) {
|
|
const i = lower.indexOf(term);
|
|
if (i !== -1 && (idx === -1 || i < idx)) idx = i;
|
|
}
|
|
if (idx === -1) return text.slice(0, maxLen) + (text.length > maxLen ? '…' : '');
|
|
const start = Math.max(0, idx - 30);
|
|
const slice = text.slice(start, start + maxLen);
|
|
return (start > 0 ? '…' : '') + slice + (start + maxLen < text.length ? '…' : '');
|
|
}
|
|
|
|
function renderSearchResults(query) {
|
|
if (!searchResults) return;
|
|
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
|
|
searchResults.innerHTML = '';
|
|
|
|
if (!terms.length) {
|
|
searchResults.hidden = true;
|
|
clearHighlights();
|
|
return;
|
|
}
|
|
|
|
const hits = searchIndex
|
|
.map((entry) => ({ entry, score: scoreEntry(entry, terms) }))
|
|
.filter((h) => h.score > 0)
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, 12);
|
|
|
|
if (!hits.length) {
|
|
const li = document.createElement('li');
|
|
li.className = 'wiki-search-empty';
|
|
li.textContent = 'No matches';
|
|
searchResults.appendChild(li);
|
|
searchResults.hidden = false;
|
|
return;
|
|
}
|
|
|
|
hits.forEach(({ entry }) => {
|
|
const li = document.createElement('li');
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'wiki-search-hit';
|
|
const title = document.createElement('span');
|
|
title.className = 'wiki-search-hit-title';
|
|
title.textContent = entry.title;
|
|
const preview = document.createElement('span');
|
|
preview.className = 'wiki-search-hit-preview';
|
|
preview.textContent = snippet(entry.text, terms, 80);
|
|
btn.appendChild(title);
|
|
btn.appendChild(preview);
|
|
btn.addEventListener('click', () => {
|
|
clearHighlights();
|
|
const scrollEl = entry.el.id ? entry.el : document.getElementById(entry.id);
|
|
scrollToTarget(entry.id, scrollEl);
|
|
const highlightRoot = entry.el.closest('section') || entry.el;
|
|
highlightMatches(highlightRoot, query);
|
|
searchResults.hidden = true;
|
|
searchInput.blur();
|
|
});
|
|
li.appendChild(btn);
|
|
searchResults.appendChild(li);
|
|
});
|
|
searchResults.hidden = false;
|
|
}
|
|
|
|
if (searchInput && searchResults) {
|
|
let debounceTimer;
|
|
searchInput.addEventListener('input', () => {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => renderSearchResults(searchInput.value.trim()), 120);
|
|
});
|
|
searchInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
searchInput.value = '';
|
|
searchResults.hidden = true;
|
|
clearHighlights();
|
|
}
|
|
});
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.wiki-search')) {
|
|
searchResults.hidden = true;
|
|
}
|
|
});
|
|
}
|
|
})();
|